diff --git a/.github/workflows/lame_pins.yml b/.github/workflows/lame_pins.yml new file mode 100644 index 0000000..2e9d39d --- /dev/null +++ b/.github/workflows/lame_pins.yml @@ -0,0 +1,56 @@ +name: LAME Pin Update + +# Keeps assets/lame-pins.json in step with upstream LAME packages. The app reads +# that file from master, so merging the pull request this opens is what updates +# installed copies. It is never merged automatically: a changed hash is exactly +# what a tampered upstream would look like, so a person confirms it first. + +on: + schedule: + - cron: "0 5 * * 1" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-pins: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: master + + - name: Refresh pins + run: python3 tools/update_lame_pins.py | tee pin-changes.txt + + - name: Open pull request + env: + GH_TOKEN: ${{ github.token }} + run: | + if git diff --quiet -- assets/lame-pins.json; then + echo "Pins are current." + exit 0 + fi + branch=chore/lame-pins + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -C "$branch" + git add assets/lame-pins.json + git commit -m "chore(util): refresh LAME download pins" + git push --force origin "$branch" + { + echo "Upstream LAME packages changed. Merging this updates the pins every installed copy uses." + echo + echo "Check each change against its source before merging. Windows (zip) changes have no independent confirmation." + echo + echo '```' + cat pin-changes.txt + echo '```' + } > pr-body.md + gh pr view "$branch" --json state -q .state 2>/dev/null | grep -q OPEN \ + && gh pr edit "$branch" --body-file pr-body.md \ + || gh pr create --base master --head "$branch" --title "chore(util): refresh LAME download pins" --body-file pr-body.md diff --git a/.github/workflows/nightly_golden_eval.yml b/.github/workflows/nightly_golden_eval.yml index 158a5b8..5d2c543 100644 --- a/.github/workflows/nightly_golden_eval.yml +++ b/.github/workflows/nightly_golden_eval.yml @@ -1,6 +1,8 @@ name: Nightly Golden Eval on: + push: + branches: [master, "feat/**"] schedule: - cron: "0 2 * * *" workflow_dispatch: @@ -11,12 +13,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Install Dependencies run: | sudo apt-get update - sudo apt-get install -y libasound2-dev libjack-jackd2-dev libfreetype6-dev libfontconfig1-dev libx11-dev libxcomposite-dev libxcursor-dev libxext-dev libxinerama-dev libxrandr-dev libxrender-dev libwebkit2gtk-4.1-dev libgtk-3-dev libglu1-mesa-dev mesa-common-dev libcurl4-openssl-dev + sudo apt-get install -y ffmpeg libasound2-dev libjack-jackd2-dev libfreetype6-dev libfontconfig1-dev libx11-dev libxcomposite-dev libxcursor-dev libxext-dev libxinerama-dev libxrandr-dev libxrender-dev libwebkit2gtk-4.1-dev libgtk-3-dev libglu1-mesa-dev mesa-common-dev libcurl4-openssl-dev - name: Configure run: cmake -S . -B build-codex -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DBUILD_TOOLS=ON @@ -39,7 +41,7 @@ jobs: - name: Upload Eval Artifacts if: success() || failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: nightly-golden-eval path: | diff --git a/.github/workflows/onnx_native.yml b/.github/workflows/onnx_native.yml new file mode 100644 index 0000000..c94b01b --- /dev/null +++ b/.github/workflows/onnx_native.yml @@ -0,0 +1,95 @@ +name: Native ONNX Runtime CI + +on: + push: + branches: [main, "feat/**"] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: onnx-native-${{ github.ref }} + cancel-in-progress: true + +jobs: + native-ort: + name: Native ORT (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + cmake_args: "-G Ninja" + - os: ubuntu-24.04-arm + cmake_args: "-G Ninja" + - os: windows-latest + cmake_args: "-A x64" + - os: macos-15 + cmake_args: "-G Ninja" + + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential cmake ninja-build pkg-config curl ffmpeg \ + libasound2-dev libjack-jackd2-dev libfreetype6-dev libfontconfig1-dev \ + libx11-dev libxcomposite-dev libxcursor-dev libxext-dev libxinerama-dev \ + libxrandr-dev libxrender-dev libwebkit2gtk-4.1-dev libgtk-3-dev \ + libglu1-mesa-dev mesa-common-dev libcurl4-openssl-dev + + - name: Install macOS build tools + if: runner.os == 'macOS' + run: | + brew install ninja ffmpeg + + - name: Install Windows dependencies + if: runner.os == 'Windows' + run: | + choco install ffmpeg --no-progress + echo "C:\ProgramData\chocolatey\bin" >> $env:GITHUB_PATH + + - name: Cache fetched dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: build/_deps + key: deps-ort-1.30.0-${{ matrix.os }}-${{ runner.arch }}-${{ hashFiles('cmake/FetchOnnxRuntime.cmake', 'CMakeLists.txt') }} + restore-keys: | + deps-ort-1.30.0-${{ matrix.os }}-${{ runner.arch }}- + + - name: Configure CMake + run: > + cmake -S . -B build + ${{ matrix.cmake_args }} + -DCMAKE_BUILD_TYPE=Release + -DENABLE_ONNX=ON + -DAUTOMIX_FETCH_ORT=ON + -DAUTOMIX_ORT_FLAVOR=cpu + -DBUILD_TESTING=ON + -DBUILD_TOOLS=ON + + - name: Build + run: cmake --build build --config Release --parallel 3 + + - name: Run Tests (Full Suite) + run: ctest --test-dir build -C Release --output-on-failure + + - name: HTTPS download smoke (Linux, curl) + if: runner.os == 'Linux' + run: | + set -euo pipefail + tools_bin="$(find build -type f -name automix_dev_tools -perm -u+x | head -n1)" + test -n "$tools_bin" + ok=0 + for attempt in 1 2 3; do + if "$tools_bin" model browse --limit 1 --out browse.json; then + if python3 -c "import json,sys; d=json.load(open('browse.json')); sys.exit(0 if len(d)>=1 else 1)"; then ok=1; break; fi + fi + sleep 10 + done + test "$ok" = 1 diff --git a/.github/workflows/release_packages.yml b/.github/workflows/release_packages.yml index 308b4fe..6b57a8e 100644 --- a/.github/workflows/release_packages.yml +++ b/.github/workflows/release_packages.yml @@ -19,6 +19,11 @@ on: required: false default: false type: boolean + dry_run: + description: Build and verify all packages but do not publish a release + required: false + default: false + type: boolean permissions: contents: write @@ -30,35 +35,28 @@ concurrency: jobs: linux-packages: name: Linux (${{ matrix.arch }}) - runs-on: ubuntu-24.04 + runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - arch: x64 - emulated: false + runner: ubuntu-24.04 - arch: arm64 - emulated: true + runner: ubuntu-24.04-arm steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - # Persist compiler output so emulated arm64 rebuilds (failure retries) - # do not recompile the whole tree under QEMU. - name: Cache ccache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .ccache key: ccache-linux-${{ matrix.arch }}-${{ github.sha }} restore-keys: | ccache-linux-${{ matrix.arch }}- - - name: Set up QEMU for arm64 emulation - if: matrix.emulated - uses: docker/setup-qemu-action@v3 - - name: Install build dependencies - if: ${{ !matrix.emulated }} run: | sudo apt-get update sudo apt-get install -y \ @@ -69,39 +67,11 @@ jobs: libglu1-mesa-dev mesa-common-dev libcurl4-openssl-dev \ dpkg-dev fakeroot flatpak flatpak-builder - # Public ubuntu-24.04-arm64 runners are frequently starved for 10h+, - # so arm64 packages are built on the x64 runner inside an emulated - # arm64v8/ubuntu:24.04 container (docker/setup-qemu-action registers - # binfmt_misc, docker run --platform linux/arm64 executes native arm64 - # binaries under QEMU user-mode emulation). - name: Build .deb + AppImage - if: ${{ !matrix.emulated }} env: CCACHE_DIR: ${{ github.workspace }}/.ccache run: bash ./tools/package_linux.sh - - name: Build .deb + AppImage (arm64, QEMU-emulated) - if: matrix.emulated - run: | - docker run --rm --platform linux/arm64 \ - -v "${{ github.workspace }}:/workspace" \ - -w /workspace \ - -e CCACHE_DIR=/workspace/.ccache \ - -e AUTOMIX_QEMU_EMULATED=1 \ - arm64v8/ubuntu:24.04 \ - bash -c 'set -euo pipefail; \ - apt-get update && apt-get install -y --no-install-recommends \ - build-essential cmake ninja-build pkg-config curl git ca-certificates \ - ccache unzip file xz-utils \ - libasound2-dev libjack-jackd2-dev libfreetype6-dev libfontconfig1-dev \ - libx11-dev libxcomposite-dev libxcursor-dev libxext-dev libxinerama-dev \ - libxrandr-dev libxrender-dev libwebkit2gtk-4.1-dev libgtk-3-dev \ - libglu1-mesa-dev mesa-common-dev libcurl4-openssl-dev \ - dpkg-dev fakeroot; \ - bash ./tools/package_linux.sh' - sudo chown -R "$(id -u):$(id -g)" dist - if [[ -d .ccache ]]; then sudo chown -R "$(id -u):$(id -g)" .ccache; fi - - name: Verify Flatpak prefetch wiring run: | grep -q "FETCHCONTENT_FULLY_DISCONNECTED=ON" packaging/flatpak/io.automixmaster.AutoMixMaster.yml @@ -111,34 +81,29 @@ jobs: # Flatpak sandbox builds can fail if CMake FetchContent tries live GitHub clone. # Keep manifest prefetch sources + FETCHCONTENT_SOURCE_DIR_* overrides in sync. - name: Build Flatpak bundle - if: ${{ !matrix.emulated }} + env: + FLATPAK_ARCH: ${{ matrix.arch == 'arm64' && 'aarch64' || 'x86_64' }} + CCACHE_DIR: ${{ github.workspace }}/.ccache run: bash ./tools/build_flatpak.sh - - name: Build Flatpak bundle (arm64, QEMU-emulated) - if: matrix.emulated - run: | - docker run --rm --platform linux/arm64 \ - -v "${{ github.workspace }}:/workspace" \ - -w /workspace \ - -e AUTOMIX_FLATPAK_DISABLE_SANDBOX=1 \ - arm64v8/ubuntu:24.04 \ - bash -c 'set -euo pipefail; \ - apt-get update && apt-get install -y --no-install-recommends \ - flatpak flatpak-builder git ca-certificates; \ - bash ./tools/build_flatpak.sh' - sudo chown -R "$(id -u):$(id -g)" dist - - name: Collect Linux release assets run: | mkdir -p dist/release/linux cp dist/linux/*.deb dist/release/linux/ cp dist/linux/*.AppImage dist/release/linux/ - cp dist/linux/*.sha256 dist/release/linux/ || true + cp dist/linux/*.sha256 dist/release/linux/ cp dist/flatpak/AutoMixMaster.flatpak "dist/release/linux/AutoMixMaster-linux-${{ matrix.arch }}.flatpak" (cd dist/release/linux && sha256sum * > "SHA256SUMS-linux-${{ matrix.arch }}.txt") + - name: Assert Linux release packages exist + run: | + test -s "dist/release/linux/AutoMixMaster-linux-${{ matrix.arch }}.flatpak" + for f in dist/release/linux/*.deb dist/release/linux/*.AppImage; do + test -s "$f" + done + - name: Upload Linux assets - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: release-linux-${{ matrix.arch }} path: dist/release/linux/* @@ -157,40 +122,45 @@ jobs: vs_platform: ARM64 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - name: Configure + - name: Install dependencies (Windows) shell: pwsh run: | - # Auto-detect the installed Visual Studio generator - $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (Test-Path $vsWhere) { - $vsVersion = & $vsWhere -latest -property catalog_productLineVersion - Write-Host "Detected Visual Studio $vsVersion" + choco install ffmpeg --no-progress + $ffmpegDir = "C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin" + if (Test-Path $ffmpegDir) { + echo "$ffmpegDir" >> $env:GITHUB_PATH } - cmake -S . -B build_windows_${{ matrix.arch }} -A ${{ matrix.vs_platform }} -DBUILD_TESTING=OFF -DBUILD_TOOLS=OFF - - name: Build - run: cmake --build build_windows_${{ matrix.arch }} --config Release --target AutoMixMasterApp --parallel 3 + - name: Build & Package Windows x64 (CUDA + WebGPU) + if: matrix.arch == 'x64' + shell: pwsh + run: | + powershell -ExecutionPolicy Bypass -File packaging/windows/build-release.ps1 -OutputDir "dist/release/windows" - - name: Package Windows zip + - name: Build & Package Windows arm64 (WebGPU) + if: matrix.arch == 'arm64' shell: pwsh run: | + cmake -S . -B build_windows_arm64 -A ARM64 -DCMAKE_SYSTEM_PROCESSOR=ARM64 -DENABLE_ONNX=ON -DAUTOMIX_FETCH_ORT=ON -DAUTOMIX_ORT_FLAVOR=cpu -DBUILD_TESTING=OFF -DBUILD_TOOLS=OFF + cmake --build build_windows_arm64 --config Release --target AutoMixMasterApp --parallel 3 $releaseRoot = "dist/release/windows" - $packageDir = "$releaseRoot/AutoMixMaster-windows-${{ matrix.arch }}" + $packageDir = "$releaseRoot/AutoMixMaster-windows-arm64" New-Item -ItemType Directory -Force -Path $packageDir | Out-Null $exeCandidates = @( - "build_windows_${{ matrix.arch }}/AutoMixMasterApp_artefacts/Release/AutoMixMaster.exe", - "build_windows_${{ matrix.arch }}/AutoMixMasterApp_artefacts/Release/Standalone/AutoMixMaster.exe", - "build_windows_${{ matrix.arch }}/AutoMixMasterApp_artefacts/AutoMixMaster.exe" + "build_windows_arm64/AutoMixMasterApp_artefacts/Release/AutoMixMaster.exe", + "build_windows_arm64/AutoMixMasterApp_artefacts/Release/Standalone/AutoMixMaster.exe", + "build_windows_arm64/AutoMixMasterApp_artefacts/AutoMixMaster.exe" ) $exePath = $exeCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 if (-not $exePath) { throw "AutoMixMaster.exe not found in build outputs!" } - Copy-Item $exePath "$packageDir/AutoMixMaster.exe" -Force + $exeDir = Split-Path -Parent $exePath + Copy-Item "$exeDir/*" "$packageDir/" -Recurse -Force Copy-Item "assets" "$packageDir/assets" -Recurse -Force - $zipName = "AutoMixMaster-windows-${{ matrix.arch }}.zip" + $zipName = "AutoMixMaster-windows-arm64.zip" $zipPath = "$releaseRoot/$zipName" if (Test-Path $zipPath) { Remove-Item $zipPath -Force } Compress-Archive -Path "$packageDir/*" -DestinationPath $zipPath -CompressionLevel Optimal @@ -199,12 +169,12 @@ jobs: "$hash $zipName" | Set-Content "$zipPath.sha256" - name: Upload Windows assets - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: release-windows-${{ matrix.arch }} path: | - dist/release/windows/AutoMixMaster-windows-${{ matrix.arch }}.zip - dist/release/windows/AutoMixMaster-windows-${{ matrix.arch }}.zip.sha256 + dist/release/windows/*.zip + dist/release/windows/*.sha256 if-no-files-found: error macos-package: @@ -217,24 +187,35 @@ jobs: - arch: x64 runner: macos-15 cmake_arch: x86_64 + enable_onnx: "OFF" - arch: arm64 runner: macos-15 cmake_arch: arm64 + enable_onnx: "ON" steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Configure run: > cmake -S . -B build_macos_${{ matrix.arch }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} + -DENABLE_ONNX=${{ matrix.enable_onnx }} + -DAUTOMIX_FETCH_ORT=${{ matrix.enable_onnx }} + -DAUTOMIX_ORT_FLAVOR=cpu -DBUILD_TESTING=OFF -DBUILD_TOOLS=OFF - name: Build run: cmake --build build_macos_${{ matrix.arch }} --target AutoMixMasterApp --parallel 3 + - name: Ad-hoc codesign bundled dylibs + if: matrix.arch == 'arm64' + run: | + APP_BUNDLE="$(find build_macos_${{ matrix.arch }} -maxdepth 6 -type d -name 'AutoMixMaster.app' | head -n 1)" + bash ./packaging/macos/codesign-bundled-dylibs.sh "$APP_BUNDLE" + - name: Package macOS zip run: | mkdir -p dist/release/macos @@ -250,7 +231,7 @@ jobs: shasum -a 256 "$ZIP_PATH" > "${ZIP_PATH}.sha256" - name: Upload macOS assets - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: release-macos-${{ matrix.arch }} path: | @@ -260,7 +241,7 @@ jobs: publish-release-assets: name: Publish assets to GitHub Release - if: ${{ (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && always() }} + if: ${{ (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run) && always() }} runs-on: ubuntu-24.04 needs: - linux-packages @@ -268,7 +249,7 @@ jobs: - macos-package steps: - name: Download all packaged artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: pattern: release-* merge-multiple: true @@ -332,7 +313,7 @@ jobs: fi - name: Publish assets - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: tag_name: ${{ steps.release-meta.outputs.tag }} name: ${{ steps.release-meta.outputs.name }} @@ -340,3 +321,37 @@ jobs: files: release-assets/* fail_on_unmatched_files: true overwrite_files: true + + dry-run-summary: + name: Dry run summary (nothing published) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} + runs-on: ubuntu-24.04 + permissions: + contents: read + needs: + - linux-packages + - windows-package + - macos-package + steps: + - name: Download all packaged artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + pattern: release-* + merge-multiple: true + path: release-assets + + - name: Verify the assets the publish job would upload + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + assets=(release-assets/*) + if (( ${#assets[@]} == 0 )); then + echo "No release artifacts were found in release-assets/." >&2 + exit 1 + fi + ls -la release-assets + for f in "${assets[@]}"; do + test -s "$f" + done + echo "Dry run: ${#assets[@]} assets verified; no release or tag was created." diff --git a/.github/workflows/repo_hygiene.yml b/.github/workflows/repo_hygiene.yml new file mode 100644 index 0000000..22f2ce3 --- /dev/null +++ b/.github/workflows/repo_hygiene.yml @@ -0,0 +1,38 @@ +name: Repository Hygiene & Security + +on: + push: + branches: [master, main, "feat/**"] + pull_request: + branches: [master, main] + workflow_dispatch: + +concurrency: + group: hygiene-${{ github.ref }} + cancel-in-progress: true + +jobs: + hygiene: + name: Repository Hygiene & Secret Scan + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: '3.12' + + - name: Check repository hygiene & forbidden paths + run: | + python tools/check_repo_hygiene.py + python tools/check_repo_hygiene.py --check-workflows + python -m unittest tests/tools/test_check_repo_hygiene.py -v + + - name: Run Gitleaks secret detection + uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tsan_build.yml b/.github/workflows/tsan_build.yml index 66c1dc8..586a6d8 100644 --- a/.github/workflows/tsan_build.yml +++ b/.github/workflows/tsan_build.yml @@ -2,9 +2,9 @@ name: ThreadSanitizer Build on: push: - branches: [main, develop] + branches: [master, "feat/**"] pull_request: - branches: [main, develop] + branches: [master] workflow_dispatch: permissions: @@ -17,13 +17,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Install Dependencies run: | sudo apt-get update sudo apt-get install -y \ - clang-18 lld-18 \ + clang-18 lld-18 ffmpeg \ libasound2-dev libjack-jackd2-dev libfreetype6-dev libfontconfig1-dev \ libx11-dev libxcomposite-dev libxcursor-dev libxext-dev libxinerama-dev \ libxrandr-dev libxrender-dev libwebkit2gtk-4.1-dev libgtk-3-dev \ @@ -53,7 +53,7 @@ jobs: - name: Upload TSan Logs (on failure) if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: tsan-logs path: | diff --git a/.gitignore b/.gitignore index 70089da..ed4bbc9 100644 --- a/.gitignore +++ b/.gitignore @@ -108,6 +108,10 @@ personal/ config.ini .github/codex_agents.md CLAUDE.md +**/CLAUDE.md +AGENTS.md +**/AGENTS.md +diff_output.txt # === Documentation (all docs except README) === docs/* @@ -122,20 +126,26 @@ docs/* ccache/ .omo/ .omo/** -!/.omo/ -# === Model Hub Downloaded Weights === +# === Model Hub Downloaded Weights & Hub Assets === +assets/**/modelhub/ +assets/**/modelhub/** assets/**/modelhub/**/*.onnx assets/**/modelhub/**/*.pt assets/**/modelhub/**/*.bin assets/**/modelhub/**/*.safetensors +assets/phaselimiter/assets/ # === Tracked licensing audit === # Must stay last: git honours the LAST matching pattern, so the docs/* line # re-ignores the whole directory (undoing the case-insensitive !README*.md -# match above) and the two negations then re-include only these files. +# match above) and the negation then re-includes only this file. docs/* -!docs/model-licensing-audit.md !docs/model-licensing-audit.json + +# README screenshots that are captured but not embedded +assets/screenshots/model-hub.png +assets/screenshots/settings.png +assets/screenshots/shortcuts.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 3cac42c..305dcc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,14 @@ option(ENABLE_CLANG_TIDY "Enable clang-tidy analysis while compiling" OFF) option(BUILD_TESTING "Build tests" ON) option(BUILD_TOOLS "Build developer tools" ON) +# Linux has no native HTTPS client in JUCE: without libcurl, JUCE falls back to a raw-socket client that cannot +# speak TLS, so model/tool downloads fail. Windows (WinINet) and macOS (NSURLSession) must keep JUCE_USE_CURL=0. +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(AUTOMIX_JUCE_USE_CURL 1) +else() + set(AUTOMIX_JUCE_USE_CURL 0) +endif() + if(DISTRIBUTION_MODE STREQUAL "PROPRIETARY" AND ENABLE_GPL_BUNDLED_LIMITERS) message(WARNING "ENABLE_GPL_BUNDLED_LIMITERS is disabled in PROPRIETARY mode.") set(ENABLE_GPL_BUNDLED_LIMITERS OFF CACHE BOOL "Enable bundled GPL limiters (OSS mode only)" FORCE) @@ -108,6 +116,7 @@ add_library(automix_core src/analysis/ArtifactRiskEstimator.cpp src/analysis/StemHealthAssistant.cpp src/analysis/StemAnalyzer.cpp + src/analysis/SpectrogramFrontEnd.cpp src/automix/HeuristicAutoMixStrategy.cpp src/automaster/HeuristicAutoMasterStrategy.cpp src/automaster/ItoMasterStrategy.cpp @@ -144,33 +153,73 @@ add_library(automix_core src/ai/GpuCapabilityDetector.cpp src/ai/StemSeparator.cpp src/ai/StemRoleClassifierAI.cpp + src/ai/TensorTypes.cpp + src/ai/SeparationRunner.cpp + src/ai/OnnxTensorInference.cpp + src/ai/OrtRuntime.cpp + src/ai/OnnxExternalData.cpp + src/ai/GpuMemory.cpp + src/ai/GpuRuntimePack.cpp + src/ai/ModelStorage.cpp src/util/StringUtils.cpp src/util/LameDownloader.cpp src/util/MetadataPolicy.cpp src/util/WavWriter.cpp ) -target_include_directories(automix_core PUBLIC src) +target_include_directories(automix_core PUBLIC src "${CMAKE_BINARY_DIR}/generated") + +include(cmake/PhaseLimiterPins.cmake) +automix_generate_phaselimiter_pins_header() + +if(WIN32) + # GpuRuntimePack reads adapter memory through DXGI to decide whether to offer CUDA. + target_link_libraries(automix_core PRIVATE dxgi) +endif() set(AUTOMIX_HAS_NATIVE_ORT OFF) set(AUTOMIX_HAS_EP_PLUGIN OFF) set(AUTOMIX_HAS_EP_CONTEXT OFF) +option(AUTOMIX_FETCH_ORT "Automatically fetch pinned ONNX Runtime release archive" OFF) +set(AUTOMIX_ORT_VERSION "1.30.0" CACHE STRING "ONNX Runtime version to fetch") +set(AUTOMIX_ORT_FLAVOR "cpu" CACHE STRING "ONNX Runtime flavor (cpu or cuda)") + if(ENABLE_ONNX) - find_path(ONNXRUNTIME_INCLUDE_DIR - NAMES onnxruntime_cxx_api.h - PATH_SUFFIXES onnxruntime/core/session - ) - find_library(ONNXRUNTIME_LIBRARY NAMES onnxruntime) - if(ONNXRUNTIME_INCLUDE_DIR AND ONNXRUNTIME_LIBRARY) + if(AUTOMIX_FETCH_ORT) + include(cmake/FetchOnnxRuntime.cmake) target_include_directories(automix_core PUBLIC ${ONNXRUNTIME_INCLUDE_DIR}) - target_link_libraries(automix_core PUBLIC ${ONNXRUNTIME_LIBRARY}) + if(TARGET onnxruntime::onnxruntime) + target_link_libraries(automix_core PUBLIC onnxruntime::onnxruntime) + else() + target_link_libraries(automix_core PUBLIC ${ONNXRUNTIME_LIBRARY}) + endif() set(AUTOMIX_HAS_NATIVE_ORT ON) + else() + find_path(ONNXRUNTIME_INCLUDE_DIR + NAMES onnxruntime_cxx_api.h + PATH_SUFFIXES onnxruntime/core/session + ) + find_library(ONNXRUNTIME_LIBRARY NAMES onnxruntime) + if(ONNXRUNTIME_INCLUDE_DIR AND ONNXRUNTIME_LIBRARY) + target_include_directories(automix_core PUBLIC ${ONNXRUNTIME_INCLUDE_DIR}) + target_link_libraries(automix_core PUBLIC ${ONNXRUNTIME_LIBRARY}) + set(AUTOMIX_HAS_NATIVE_ORT ON) + message(WARNING "Using unpinned external ONNX Runtime. Recommended: configure with -DAUTOMIX_FETCH_ORT=ON") + endif() + endif() + + if(AUTOMIX_HAS_NATIVE_ORT) + target_compile_definitions(automix_core PUBLIC AUTOMIX_ORT_EXPECTED_VERSION="${AUTOMIX_ORT_VERSION}") # Feature-detect the optional APIs rather than parsing a version string out of # the headers. ONNX Runtime does not expose a reliable single version macro, and # a header either declares an entry point or it does not, which is exact where a # parsed version is only a guess. - file(GLOB _ort_api_headers "${ONNXRUNTIME_INCLUDE_DIR}/onnxruntime/core/session/onnxruntime_c*_api.h") + # The official release archives ship a flat include/ directory; source-tree + # installs nest the same headers under onnxruntime/core/session/. + file(GLOB _ort_api_headers + "${ONNXRUNTIME_INCLUDE_DIR}/onnxruntime_c*_api.h" + "${ONNXRUNTIME_INCLUDE_DIR}/onnxruntime/core/session/onnxruntime_c*_api.h") foreach(_ort_header IN LISTS _ort_api_headers) file(STRINGS "${_ort_header}" _ort_header_text) if(_ort_header_text MATCHES "RegisterExecutionProviderLibrary") @@ -197,6 +246,114 @@ if(ENABLE_ONNX) endif() endif() +set(AUTOMIX_WEBGPU_PLUGIN_DIR "" CACHE PATH "Directory holding the ONNX Runtime WebGPU plugin EP (onnxruntime_providers_webgpu + dxcompiler/dxil on Windows)") + +set(AUTOMIX_ORT_RUNTIME_FILES "") +if(AUTOMIX_HAS_NATIVE_ORT) + get_filename_component(_ort_lib_dir "${ONNXRUNTIME_LIBRARY}" DIRECTORY) + if(WIN32) + foreach(_core_dll onnxruntime.dll onnxruntime_providers_shared.dll onnxruntime_providers_cuda.dll) + if(EXISTS "${_ort_lib_dir}/${_core_dll}") + list(APPEND AUTOMIX_ORT_RUNTIME_FILES "${_ort_lib_dir}/${_core_dll}") + endif() + endforeach() + if(AUTOMIX_WEBGPU_PLUGIN_DIR) + foreach(_webgpu_dll onnxruntime_providers_webgpu.dll dxcompiler.dll dxil.dll) + set(_webgpu_file "${AUTOMIX_WEBGPU_PLUGIN_DIR}/${_webgpu_dll}") + if(EXISTS "${_webgpu_file}") + list(APPEND AUTOMIX_ORT_RUNTIME_FILES "${_webgpu_file}") + else() + message(WARNING "WebGPU plugin file missing: ${_webgpu_file}") + endif() + endforeach() + endif() + elseif(APPLE) + file(GLOB _ort_dylibs LIST_DIRECTORIES false "${_ort_lib_dir}/libonnxruntime*.dylib") + foreach(_dylib IN LISTS _ort_dylibs) + if(NOT _dylib MATCHES "providers_tensorrt") + list(APPEND AUTOMIX_ORT_RUNTIME_FILES "${_dylib}") + endif() + endforeach() + if(AUTOMIX_WEBGPU_PLUGIN_DIR) + set(_webgpu_dylib "${AUTOMIX_WEBGPU_PLUGIN_DIR}/libonnxruntime_providers_webgpu.dylib") + if(EXISTS "${_webgpu_dylib}") + list(APPEND AUTOMIX_ORT_RUNTIME_FILES "${_webgpu_dylib}") + endif() + endif() + elseif(UNIX) + file(GLOB _ort_sos LIST_DIRECTORIES false "${_ort_lib_dir}/libonnxruntime*.so*") + foreach(_so IN LISTS _ort_sos) + if(NOT _so MATCHES "providers_tensorrt") + list(APPEND AUTOMIX_ORT_RUNTIME_FILES "${_so}") + endif() + endforeach() + if(AUTOMIX_WEBGPU_PLUGIN_DIR) + set(_webgpu_so "${AUTOMIX_WEBGPU_PLUGIN_DIR}/libonnxruntime_providers_webgpu.so") + if(EXISTS "${_webgpu_so}") + list(APPEND AUTOMIX_ORT_RUNTIME_FILES "${_webgpu_so}") + else() + message(WARNING "WebGPU plugin file missing: ${_webgpu_so}") + endif() + endif() + endif() +endif() + +function(automix_stage_ort TARGET_NAME) + if(NOT AUTOMIX_HAS_NATIVE_ORT OR NOT AUTOMIX_ORT_RUNTIME_FILES) + return() + endif() + if(WIN32) + set(_staging_files ${AUTOMIX_ORT_RUNTIME_FILES}) + if(AUTOMIX_CUDA_RUNTIME_DIR) + file(GLOB _automix_cuda_dlls "${AUTOMIX_CUDA_RUNTIME_DIR}/*.dll") + list(APPEND _staging_files ${_automix_cuda_dlls}) + endif() + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${_staging_files} "$" + VERBATIM) + elseif(APPLE) + get_target_property(_is_bundle ${TARGET_NAME} MACOSX_BUNDLE) + if(_is_bundle) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/../Frameworks" + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${AUTOMIX_ORT_RUNTIME_FILES} "$/../Frameworks" + VERBATIM) + else() + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/lib" + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${AUTOMIX_ORT_RUNTIME_FILES} "$/lib" + VERBATIM) + endif() + elseif(UNIX) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/lib" + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${AUTOMIX_ORT_RUNTIME_FILES} "$/lib" + VERBATIM) + endif() +endfunction() + +function(automix_set_rpath TARGET_NAME) + if(APPLE) + get_target_property(_is_bundle ${TARGET_NAME} MACOSX_BUNDLE) + if(_is_bundle) + set_target_properties(${TARGET_NAME} PROPERTIES + BUILD_RPATH "@executable_path/../Frameworks" + INSTALL_RPATH "@executable_path/../Frameworks" + ) + else() + set_target_properties(${TARGET_NAME} PROPERTIES + BUILD_RPATH "@loader_path/lib" + INSTALL_RPATH "@loader_path/lib" + ) + endif() + elseif(UNIX AND NOT APPLE) + set_target_properties(${TARGET_NAME} PROPERTIES + BUILD_RPATH "$ORIGIN/lib" + INSTALL_RPATH "$ORIGIN/lib" + ) + endif() +endfunction() + target_link_libraries(automix_core PUBLIC nlohmann_json::nlohmann_json @@ -205,6 +362,11 @@ target_link_libraries(automix_core juce::juce_dsp ) +if(AUTOMIX_JUCE_USE_CURL) + find_package(CURL REQUIRED) + target_link_libraries(automix_core PUBLIC CURL::libcurl) +endif() + set(LIBEBUR128_BACKEND_AVAILABLE OFF) if(ENABLE_LIBEBUR128) if(DEFINED libebur128_SOURCE_DIR) @@ -230,7 +392,7 @@ target_compile_definitions(automix_core SESSION_SCHEMA_VERSION=2 DISTRIBUTION_MODE_${DISTRIBUTION_MODE}=1 JUCE_WEB_BROWSER=0 - JUCE_USE_CURL=0 + JUCE_USE_CURL=${AUTOMIX_JUCE_USE_CURL} $<$:ENABLE_PHASELIMITER=1> $<$:ENABLE_ONNX=1> $<$:ENABLE_RTNEURAL=1> @@ -268,7 +430,13 @@ if(ENABLE_SANITIZERS) endif() endif() +set(_automix_needs_curl FALSE) +if(AUTOMIX_JUCE_USE_CURL) + set(_automix_needs_curl TRUE) +endif() + juce_add_gui_app(AutoMixMasterApp + NEEDS_CURL ${_automix_needs_curl} PRODUCT_NAME "AutoMixMaster" COMPANY_NAME "AutoMixMaster" ICON_BIG "assets/icon.png" @@ -316,20 +484,31 @@ target_link_libraries(AutoMixMasterApp target_compile_definitions(AutoMixMasterApp PRIVATE JUCE_WEB_BROWSER=0 - JUCE_USE_CURL=0 + JUCE_USE_CURL=${AUTOMIX_JUCE_USE_CURL} ) -if(EXISTS "${CMAKE_SOURCE_DIR}/assets/phaselimiter") - add_custom_command(TARGET AutoMixMasterApp POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory "$/assets" - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${CMAKE_SOURCE_DIR}/assets/phaselimiter" - "$/assets/phaselimiter" - COMMENT "Copying bundled PhaseLimiter assets next to AutoMixMaster executable" - VERBATIM - ) +# Pinned PhaseLimiter fetching and staging +# Defaults ON whenever assets/phaselimiter/bin has no native binary for the platform key. +include(cmake/FetchPhaseLimiter.cmake) +automix_phaselimiter_platform_key(_automix_host_pl_key) +if(_automix_host_pl_key STREQUAL "windows-x64") + set(_automix_native_bin_name "phase_limiter.exe") +else() + set(_automix_native_bin_name "phase_limiter") endif() +if(EXISTS "${CMAKE_SOURCE_DIR}/assets/phaselimiter/bin/${_automix_native_bin_name}") + set(AUTOMIX_FETCH_PHASELIMITER_DEFAULT OFF) +else() + set(AUTOMIX_FETCH_PHASELIMITER_DEFAULT ON) +endif() +option(AUTOMIX_FETCH_PHASELIMITER "Fetch and stage pinned PhaseLimiter release archive" ${AUTOMIX_FETCH_PHASELIMITER_DEFAULT}) + +if(AUTOMIX_FETCH_PHASELIMITER) + include(cmake/FetchPhaseLimiter.cmake) +endif() + + if(EXISTS "${CMAKE_SOURCE_DIR}/assets/limiters") add_custom_command(TARGET AutoMixMasterApp POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory "$/assets" @@ -371,7 +550,11 @@ if(BUILD_TESTING) tests/unit/TestMain.cpp tests/unit/ControllerTests.cpp tests/unit/TaskCenterPanelTests.cpp + tests/unit/ControlDeckTests.cpp + tests/unit/UiDecisionTests.cpp tests/unit/SessionSerializationTests.cpp + tests/unit/LameDownloaderTests.cpp + src/app/ui/SessionManager.cpp tests/unit/AudioIoTests.cpp tests/unit/AnalysisTests.cpp tests/unit/StemOriginSafetyTests.cpp @@ -401,6 +584,7 @@ if(BUILD_TESTING) tests/unit/OfflineRenderPipelineTests.cpp tests/unit/OnnxModelInferenceTests.cpp tests/unit/StemSeparatorTests.cpp + tests/unit/TensorInferenceTests.cpp tests/unit/ModelBenchmarkTests.cpp tests/unit/GpuBenchmarkTests.cpp tests/regression/RegressionHarness.cpp @@ -410,6 +594,9 @@ if(BUILD_TESTING) tests/integration/PipelineIntegrationTests.cpp src/app/ui/TaskCenterPanel.cpp src/app/style/AutoMixLookAndFeel.cpp + src/app/ui/ControlDeck.cpp + src/app/ui/StemPanel.cpp + src/app/ui/GlowMeters.cpp src/app/controllers/ModelController.cpp src/app/controllers/ImportController.cpp src/app/controllers/ExportController.cpp @@ -420,7 +607,7 @@ if(BUILD_TESTING) src/app/controllers/OriginalMixController.cpp ) - target_include_directories(automix_tests PRIVATE src tests/regression tests/integration) + target_include_directories(automix_tests PRIVATE src tests/regression tests/integration tools) target_link_libraries(automix_tests PRIVATE automix_core Catch2::Catch2WithMain juce::juce_gui_extra) target_compile_definitions(automix_tests PRIVATE AUTOMIX_SOURCE_DIR="${CMAKE_SOURCE_DIR}" @@ -452,4 +639,167 @@ if(BUILD_TESTING) include(CTest) include(${Catch2_SOURCE_DIR}/extras/Catch.cmake) catch_discover_tests(automix_tests) + add_test(NAME cmake_phaselimiter_arch COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/tests/cmake/phaselimiter_arch_test.cmake) + add_test(NAME cmake_phaselimiter_install_guard COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/tests/cmake/phaselimiter_install_guard_test.cmake) + set_tests_properties(cmake_phaselimiter_install_guard PROPERTIES LABELS "slow") +endif() + +# Stage ONNX Runtime (and, optionally, CUDA/cuDNN) DLLs beside each executable, +# and configure rpath for Linux ($ORIGIN/lib) and macOS (@executable_path/../Frameworks, @loader_path/lib). +set(AUTOMIX_CUDA_RUNTIME_DIR "" CACHE PATH "Folder of CUDA runtime + cuDNN DLLs staged next to executables (optional)") + +automix_stage_ort(AutoMixMasterApp) +if(COMMAND automix_stage_phaselimiter) + automix_stage_phaselimiter(AutoMixMasterApp) +endif() +automix_set_rpath(AutoMixMasterApp) + +if(TARGET automix_dev_tools) + automix_stage_ort(automix_dev_tools) + if(COMMAND automix_stage_phaselimiter) + automix_stage_phaselimiter(automix_dev_tools) + endif() + automix_set_rpath(automix_dev_tools) +endif() + +if(TARGET automix_tests) + automix_stage_ort(automix_tests) + if(COMMAND automix_stage_phaselimiter) + automix_stage_phaselimiter(automix_tests) + endif() + automix_set_rpath(automix_tests) endif() + +if(TARGET automix_regression_cli) + automix_stage_ort(automix_regression_cli) + if(COMMAND automix_stage_phaselimiter) + automix_stage_phaselimiter(automix_regression_cli) + endif() + automix_set_rpath(automix_regression_cli) +endif() + +# ── Release packaging (Cross-platform portable package via CPack) ───────────── +# cmake --install --config Release --component application --prefix +# or: cpack -C Release. Only the "application" component is packaged. +install(TARGETS AutoMixMasterApp + RUNTIME DESTINATION . COMPONENT application + BUNDLE DESTINATION . COMPONENT application + RESOURCE DESTINATION . COMPONENT application +) + +if(AUTOMIX_HAS_NATIVE_ORT AND AUTOMIX_ORT_RUNTIME_FILES) + if(WIN32) + install(FILES ${AUTOMIX_ORT_RUNTIME_FILES} DESTINATION . COMPONENT application) + elseif(APPLE) + install(FILES ${AUTOMIX_ORT_RUNTIME_FILES} DESTINATION "AutoMixMaster.app/Contents/Frameworks" COMPONENT application) + elseif(UNIX) + install(FILES ${AUTOMIX_ORT_RUNTIME_FILES} DESTINATION lib COMPONENT application) + endif() +endif() + +if(APPLE) + set(_automix_pl_dest "AutoMixMaster.app/Contents/MacOS/assets/phaselimiter") +else() + set(_automix_pl_dest "assets/phaselimiter") +endif() + +set(_automix_pl_install_root "") +if(AUTOMIX_PHASELIMITER_ROOT) + set(_automix_pl_install_root "${AUTOMIX_PHASELIMITER_ROOT}") +elseif(EXISTS "${CMAKE_SOURCE_DIR}/assets/phaselimiter") + set(_automix_pl_install_root "${CMAKE_SOURCE_DIR}/assets/phaselimiter") +endif() + +if(_automix_pl_install_root) + foreach(_dir bin resource licenses) + if(EXISTS "${_automix_pl_install_root}/${_dir}") + install(DIRECTORY "${_automix_pl_install_root}/${_dir}" DESTINATION "${_automix_pl_dest}" COMPONENT application) + endif() + endforeach() + foreach(_file LICENSE README.md) + if(EXISTS "${_automix_pl_install_root}/${_file}") + install(FILES "${_automix_pl_install_root}/${_file}" DESTINATION "${_automix_pl_dest}" COMPONENT application) + endif() + endforeach() +endif() + +# Hard guard for PhaseLimiter: fail the install if no native PhaseLimiter binary or reference file is packaged +if(ENABLE_PHASELIMITER) + install(CODE " + set(_pl_bin_check \"\${CMAKE_INSTALL_PREFIX}/${_automix_pl_dest}/bin/${_automix_native_bin_name}\") + set(_pl_res_check \"\${CMAKE_INSTALL_PREFIX}/${_automix_pl_dest}/resource/mastering_reference.json\") + if(NOT EXISTS \"\${_pl_bin_check}\" OR NOT EXISTS \"\${_pl_res_check}\") + message(FATAL_ERROR \"Package has no native PhaseLimiter for ${_automix_host_pl_key}\") + endif() + " COMPONENT application) +endif() +if(EXISTS "${CMAKE_SOURCE_DIR}/assets/limiters") + install(DIRECTORY "${CMAKE_SOURCE_DIR}/assets/limiters" DESTINATION assets COMPONENT application) +endif() +foreach(_doc NOTICE LICENSE README.md) + if(EXISTS "${CMAKE_SOURCE_DIR}/${_doc}") + install(FILES "${CMAKE_SOURCE_DIR}/${_doc}" DESTINATION . COMPONENT application) + endif() +endforeach() + +# macOS arm64: re-sign every bundled dylib with runtime hardened options before final packaging +if(APPLE) + install(CODE [[ + file(GLOB _framework_dylibs "${CMAKE_INSTALL_PREFIX}/AutoMixMaster.app/Contents/Frameworks/*.dylib") + foreach(_dylib IN LISTS _framework_dylibs) + execute_process(COMMAND codesign --force --options runtime --sign - "${_dylib}") + endforeach() + ]] COMPONENT application) +endif() + +# Hard guard for NOTICE's promises: fail the install if a model file or an +# NVIDIA CUDA library got into the package. +install(CODE [[ + file(GLOB_RECURSE _automix_forbidden + "${CMAKE_INSTALL_PREFIX}/*.onnx" "${CMAKE_INSTALL_PREFIX}/*.onnx.data" + "${CMAKE_INSTALL_PREFIX}/*cudart*.dll" "${CMAKE_INSTALL_PREFIX}/*cublas*.dll" + "${CMAKE_INSTALL_PREFIX}/*cudnn*.dll" "${CMAKE_INSTALL_PREFIX}/*cufft*.dll" + "${CMAKE_INSTALL_PREFIX}/*libcudart*.so*" "${CMAKE_INSTALL_PREFIX}/*libcublas*.so*" + "${CMAKE_INSTALL_PREFIX}/*libcudnn*.so*" "${CMAKE_INSTALL_PREFIX}/*libcufft*.so*" + "${CMAKE_INSTALL_PREFIX}/*libcudart*.dylib*" "${CMAKE_INSTALL_PREFIX}/*libcublas*.dylib*" + "${CMAKE_INSTALL_PREFIX}/*libcudnn*.dylib*" "${CMAKE_INSTALL_PREFIX}/*libcufft*.dylib*") + if(_automix_forbidden) + message(FATAL_ERROR "Package must not contain model weights or NVIDIA CUDA libraries: ${_automix_forbidden}") + endif() +]] COMPONENT application) + +set(CPACK_PACKAGE_NAME "AutoMixMaster") +set(CPACK_PACKAGE_VENDOR "AutoMixMaster") + +if(WIN32) + set(CPACK_GENERATOR ZIP) + if(AUTOMIX_HAS_NATIVE_ORT) + get_filename_component(_ort_pkg_dir "${ONNXRUNTIME_LIBRARY}" DIRECTORY) + if(EXISTS "${_ort_pkg_dir}/onnxruntime_providers_cuda.dll" OR AUTOMIX_ORT_FLAVOR STREQUAL "cuda") + set(CPACK_PACKAGE_FILE_NAME "AutoMixMaster-${PROJECT_VERSION}-win64-cuda") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") + set(CPACK_PACKAGE_FILE_NAME "AutoMixMaster-${PROJECT_VERSION}-winarm64") + else() + set(CPACK_PACKAGE_FILE_NAME "AutoMixMaster-${PROJECT_VERSION}-win64") + endif() + else() + set(CPACK_PACKAGE_FILE_NAME "AutoMixMaster-${PROJECT_VERSION}-win64") + endif() +elseif(APPLE) + set(CPACK_GENERATOR TGZ) + set(CPACK_PACKAGE_FILE_NAME "AutoMixMaster-${PROJECT_VERSION}-macos-arm64") +elseif(UNIX) + set(CPACK_GENERATOR TGZ) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") + set(CPACK_PACKAGE_FILE_NAME "AutoMixMaster-${PROJECT_VERSION}-linux-arm64") + else() + set(CPACK_PACKAGE_FILE_NAME "AutoMixMaster-${PROJECT_VERSION}-linux-x64") + endif() +endif() + +set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY ON) +set(CPACK_COMPONENTS_ALL application) +set(CPACK_ARCHIVE_COMPONENT_INSTALL ON) +set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE) +include(CPack) + diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..426d881 --- /dev/null +++ b/NOTICE @@ -0,0 +1,185 @@ +AutoMixMaster — NOTICE +====================== + +This file records third-party material that AutoMixMaster can use, and the +terms it is offered under. It is maintained by hand alongside +docs/model-licensing-audit.json, which is the machine-readable manifest that +tests assert against. + + +1. AutoMixMaster itself +----------------------- + +AutoMixMaster is licensed under the GNU General Public License, version 3 +(GPLv3). The GPLv3 notice is reproduced in the LICENSE file at the root of the +source distribution. + + +2. No model weights are distributed with this software +----------------------------------------------------- + +AutoMixMaster ships NO model weights. No trained .onnx, .pt, .pth, +.safetensors or .ckpt file is committed to the repository or bundled into a +release build, and .gitignore excludes the model-hub install directories. + +The only .onnx files in the repository are the synthetic test graphs under +tests/fixtures/tensor/ (a few hundred bytes each). They contain no trained +weights - only fixed arithmetic such as an identity mask - and are regenerated +by tests/fixtures/tensor/make_fixtures.py. + +Every model listed below is downloaded at runtime, by the user, directly from +the publisher's own distribution point — a Hugging Face repository or a GitHub +release. AutoMixMaster does not mirror, re-host, or redistribute any of them. + +This separation is deliberate and load-bearing: under GPLv3 section 5, an +aggregate formed by a GPL-licensed program together with separately-licensed +material is not itself a single combined work. Because the weights are never +part of the distributed GPL work, a non-commercial or otherwise +restrictive weight licence does not retroactively restrict the licence of the +application, and equally does not relicense the weights. Users are +responsible for their own compliance with each weight's terms. + + +3. Attribution for the curated model catalogue +---------------------------------------------- + +These are the repositories AutoMixMaster offers in its curated catalog. The +"Terms" column reproduces the license reported by the publisher's model card as +of the audit date in docs/model-licensing-audit.json. + + Repository Terms Consent + ------------------------------------------------------ -------------- ------- + rysertio/Demucs-onnx MIT no + onnx-community/whisper-tiny.en MIT no + onnx-community/whisper-small.en MIT no + onnx-community/Musical-Instrument-Classification-ONNX MIT no + onnx-community/Speech-Emotion-Classification-ONNX UNVERIFIED YES + onnx-community/Musical-genres-Classification-Hubert-V1-ONNX UNVERIFIED YES + openai/whisper-tiny MIT no + laion/clap-htsat-unfused CC BY 4.0 no + pranjal-pravesh/PANNs_CNN14_ONNX MIT no + StemSplitio/htdemucs-ft-onnx MIT no + StemSplitio/htdemucs-6s-onnx MIT no + SonyCSLParis/music2latent CC BY-NC 4.0 YES + kramp/ito-master-onnx CC BY-NC 4.0 YES + xycld/BS-RoFormer-ONNX MIT no + MixDirective/open-unmix-umxhq-vocals-onnx MIT no + + GitHub release sources, terms not yet verified: + smartdaze/otowake-oto (htdemucs_6s.onnx) + instant-high/Resemble-Denoiser-ONNX (denoiser_fp16.onnx) + + +4. How "Consent: YES" behaves in the application +----------------------------------------------- + +A model marked "Consent: YES" is refused download and refused activation until +the user explicitly acknowledges its terms. The acknowledgement is recorded in +license_consents.json inside the model-hub directory, together with the model +id, repository, license identifier, license URL, attribution string and a UTC +timestamp. + +The consent decision is licence-driven rather than a fixed list: any model +whose reported license is non-commercial (CC BY-NC, CC BY-NC-SA, and the +like), or which declares no license at all, requires the same acknowledgement. +An undeclared license is treated as requiring consent because permitted use +cannot be established from it, and research-only use is the only defensible +default until the publisher clarifies. + +Where a publisher's terms conflict — for example laion/clap-htsat-unfused, +whose model card lists apache-2.0 while the LAION research repository is +CC BY 4.0 — the more restrictive reading should be assumed until the publisher +resolves it. + + +5. Known attribution requirements +--------------------------------- + + laion/clap-htsat-unfused (CC BY 4.0) + Attribution required. Credit LAION-AI / CLAP and link the licence: + https://creativecommons.org/licenses/by/4.0/ + + kramp/ito-master-onnx (CC BY-NC 4.0) + Non-commercial use only. Derived from ITO-Master by Sony + Research Center. Attribution required: + https://creativecommons.org/licenses/by-nc/4.0/ + + SonyCSLParis/music2latent (CC BY-NC 4.0) + Non-commercial use only. Attribution required: + https://creativecommons.org/licenses/by-nc/4.0/ + + xycld/BS-RoFormer-ONNX (MIT) + BS-RoFormer (Lu et al., arXiv 2309.02612), ONNX export by xycld. + https://opensource.org/license/mit + The catalog installs the fp32 build where a CUDA GPU session opens and + the uint8-quantized build otherwise; the quantized build's quality + relative to fp32 has not been formally measured. The fp32 export's + external weights are folded into one local .onnx file at install time. + No model file is re-hosted. The model separates vocals + only: the instrumental stem is the residual (mix - vocals), not a + second separation. + + MixDirective/open-unmix-umxhq-vocals-onnx (MIT) + Open-Unmix UMX-HQ vocals (Stoter, Uhlich, Liutkus, Mitsufuji; (c) 2019 + Inria), ONNX export by MixDirective. + https://opensource.org/license/mit + A 36 MB CPU-friendly alternative to BS-RoFormer for machines with little + memory; lower separation quality. No model file is re-hosted. The model + separates vocals only: the instrumental stem is the residual + (mix - vocals), not a second separation. + + +6. Optional GPU runtime (NVIDIA CUDA libraries) +----------------------------------------------- + +GPU acceleration of the vocal model uses NVIDIA's CUDA runtime, cuBLAS, cuFFT +and cuDNN. AutoMixMaster does NOT ship or re-host them. On a machine with a +suitable NVIDIA GPU the application offers a one-time, opt-in download of +NVIDIA's own redistributable packages from pypi.org (pinned by SHA-256), +installed for the current user under %LOCALAPPDATA%\AutoMixMaster\gpu-runtime. +They are NVIDIA software, governed by NVIDIA's licence terms (the CUDA Toolkit +EULA and the cuDNN Software License Agreement), not by this project's licence. +Declining the download leaves every feature working on the CPU. + +7. Third-party build dependencies +--------------------------------- + +The application links against ONNX Runtime (MIT), JUCE (AGPLv3 or +commercial), libebur128 (MIT) and nlohmann/json (MIT). Full third-party +dependency details are listed in docs/third_party_registry.json. + +Note that ONNX Runtime is an OPTIONAL dependency. Builds made without it fall +back to a deterministic adapter and model-backed features degrade to +heuristics rather than failing. + + +8. Optional external renderers: PhaseLimiter +-------------------------------------------- + +PhaseLimiter (https://github.com/ai-mastering/phaselimiter) is an optional +external mastering renderer executing as a standalone child process. On Windows +x64, it uses upstream release binaries. On Linux (x86_64, aarch64) and macOS +(Apple Silicon, x86_64), it uses modernized portable builds +(https://github.com/soficis/phaselimiter) release v0.2.0-native3. + +The PhaseLimiter portable distribution bundles third-party open-source components, +whose full license texts are preserved under the licenses/ subdirectory: + - PhaseLimiter: MIT (Copyright (c) 2018-2020 Bakuage Co., Ltd.) + - oneTBB: Apache License 2.0 (Copyright (c) 2005-2024 Intel Corporation) + - PocketFFT: BSD-3-Clause (Copyright (c) 2010-2019 Max-Planck-Society) + - Boost: Boost Software License 1.0 (BSL-1.0) + - Eigen: Mozilla Public License 2.0 (MPL-2.0) + - Armadillo: Apache License 2.0 (Copyright 2008-2024 Conrad Sanderson and Ryan Curtin) + - libpng: libpng license (Copyright (c) 1995-2024 The PNG Reference Library Authors) + - zlib: zlib license (Copyright (C) 1995-2024 Jean-loup Gailly and Mark Adler) + - gflags: BSD-3-Clause (Copyright (c) 2006 Google Inc.) + - picojson: BSD-2-Clause (Copyright 2009-2010 Kazuho Oku) + - hnswlib: Apache License 2.0 (Copyright (c) 2017 Yury Malkov) + - libsimdpp: Boost Software License 1.0 (Copyright (c) 2011-2015 Povilas Kanapickas) + - OptimLib: Apache License 2.0 (Copyright (c) 2017-present Keith O'Hara) + - CImg: CeCILL-C (Copyright (c) David Tschumperle) + +Neither upstream PhaseLimiter nor the portable v0.2.0-native3 distribution links +to or redistributes LGPL-licensed libsndfile: audio I/O uses an in-tree WAV +implementation, and encoding/decoding relies on the user's system FFmpeg binary. + diff --git a/README.md b/README.md index aa0a25f..6d33414 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ **Version 0.4.2** - -AutoMixMaster application interface +AutoMixMaster with a session loaded **FIXED-RULE AUDIO WORKFLOW FOR MIXING AND MASTERING MUSIC STEMS** @@ -15,11 +14,10 @@

Overview • -Feature Set • +What It Does • First Session • -Quick Start • -System Requirements • -Build + Install • +Install • +What You Need • Licensing

@@ -27,241 +25,145 @@ ## Overview -AutoMixMaster helps you turn raw stems into a cleaner, release-ready track with fewer manual steps. +Mixing and mastering take years to learn. AutoMixMaster does both with one button. -It focuses on predictable, fixed-rule processing so results are repeatable and beginner-friendly, with optional extras like AI stem separation, batch mode, and bundled renderer integrations. +Drop in your stems. Press **Mix + Master**. Get a finished track. ---- +The app works by fixed rules, so the same stems give the same result every time. The AI features are extras. Each one falls back to the fixed rules when you have no model installed. + +
+AutoMixMaster on first launch +
-## Feature Set +--- -> **Core capabilities at a glance** +## What It Does -| Module | Description | +| Feature | What you get | | :--- | :--- | -| **Auto Mix + Auto Master** | Deterministic stem balancing, gain staging, and limiting workflow. | -| **One-Click Pipeline** | `Mix + Master` (`Ctrl+Shift+M`) runs Auto Mix → Auto Master → Export. | -| **ITO-Master AI Mastering** | Experimental AI mastering strategy driving a 46-parameter native white-box FX chain with licensing consent gating. | -| **AI Stem Separation (Optional)** | Splits a single full-mix import into stems before processing when enabled (`Ctrl+Shift+A`). | -| **Task-Scoped Model Browser** | Install/uninstall models and set active packs per task (`mix`, `master`, `analysis`, `separation`) from Hugging Face or GitHub Releases. | -| **Batch Processing** | Queue folders, auto-group stems by filename role patterns, and render one mastered song per group (`_AutoMixMaster_YYYYMMDD_XX.`). Supports recursive discovery via UI toggle or `AUTOMIX_BATCH_RECURSIVE=1`. | -| **Renderer Integrations** | Built-in discovery for PhaseLimiter, FFmpeg, SoX, and rsgain; only available tools are shown (`*_BIN` env overrides supported). | -| **Verification Reporting** | Export verification report plus batch completion summary; optional per-export `.report.json` sidecar. | -| **Task Center + ETA** | Real-time progress tracking with batch ETA countdown, summary status row, timestamped activity log, and copy-log utility. | -| **Transport & Audio Preview** | Realtime-safe lock-free audio preview buffer, live peak/RMS meters, 0–1.5x gain control (+3.5 dB), and modal confirmation for session clearing. | -| **Shortcuts & Commands** | `ApplicationCommandManager` integration with built-in Keyboard Shortcuts Cheatsheet modal (`?`). | -| **Analysis Meters** | Live LUFS and peak metering via GlowMeters. | +| **Auto Mix + Auto Master** | The app balances your stems, sets their levels and limits the result. | +| **Mix + Master** | One button (`Ctrl+Shift+M`) mixes, masters and exports. | +| **Master presets** | Default Streaming, Broadcast, Udio Optimized or Custom. | +| **Platform targets** | Spotify, Apple Music, YouTube, Amazon Music, Tidal or Broadcast EBU R128. | +| **AI Stem Separation** | Optional. Splits one full mix into stems, so you can start from a single file. | +| **Vocal Model** | Optional. BS-RoFormer pulls the vocals out of a mix. It is slow without a strong NVIDIA card. | +| **Light vocal model** | Optional. Open-Unmix is a 36 MB download for weaker machines. It is fast, and rougher. | +| **AI mastering** | Experimental. The ITO-Master model picks the mastering settings. | +| **Models window** | `Ctrl+K`. Download, remove and choose models. | +| **Batch** | Point the app at folders. It groups the stems by file name and renders one mastered song per group. | +| **Export** | WAV, AIFF, FLAC, OGG or MP3. Each export comes with a report that checks the result. | +| **Preview** | Play the mix, watch the level and loudness meters, solo or mute any stem. | +| **Sessions** | Save, load, undo and redo. The app asks before it throws away unsaved work. | +| **Progress** | A progress bar, a time estimate for batches and a log you can copy. | + +### Shortcuts + +| Action | Keys | +| :--- | :--- | +| Import | `Ctrl+I` | +| Auto Mix | `Ctrl+M` | +| Auto Master | `Ctrl+Shift+A` | +| Mix + Master | `Ctrl+Shift+M` | +| Export | `Ctrl+E` | +| Models | `Ctrl+K` | +| Save / Load session | `Ctrl+S` / `Ctrl+O` | +| Undo / Redo | `Ctrl+Z` / `Ctrl+Y` | +| Play / Pause | `Space` | +| Show all shortcuts | `Ctrl+/` | --- ## First Session -Welcome to your first mixing and mastering session. AutoMixMaster simplifies the process into a few core steps: +1. **Import your audio.** Drop files on the waveform area, click it, or press `Ctrl+I`. The app reads WAV, AIFF, FLAC, MP3 and OGG. +2. **Only one file? Turn on AI Stem Separation.** The app splits a single full mix into stems first. With several files it treats them as stems and skips this step. +3. **Press Mix + Master.** The app mixes, masters and exports. -1. **Import audio**: Drag and drop files onto the waveform area, or click `Import`. Supported formats: WAV, AIFF, FLAC, MP3, OGG. -2. **(Optional) enable AI Stem Separation**: Toggle **AI Stem Separation** and check the badge beside it (`Separation model: `). - - If exactly one full-mix track is loaded, separation runs before Auto Mix. - - If multiple files are loaded, they are treated as regular stems and separation is skipped. -3. **Manage models in Model Browser**: Open **Models** to fetch catalog entries, install/uninstall models, and set active packs per task (`mix`, `master`, `analysis`, `separation`) using **Set Active** or **Use Selected for Task**. -4. **Auto Mix**: Click **Auto Mix** to analyze stems and apply deterministic balancing rules. -5. **Auto Master**: Click **Auto Master** to apply mastering strategy and limiting. -6. **One-click pipeline**: Click **Mix + Master** (`Ctrl+Shift+M`) to run Auto Mix → Auto Master → Export. If AI Stem Separation is enabled and one full mix is loaded, separation is performed first, then the pipeline continues automatically. -7. **Export**: Use **Export** (`Ctrl+E`) for manual output control, or rely on pipeline export. +Want more control? Run **Auto Mix**, **Auto Master** and **Export** one at a time. Pick a master preset and a platform target before you master. --- -## Quick Start - -Getting started with AutoMixMaster is simple. - -> ⚠️ **Testing disclaimer:** only the **Windows** version has been manually tested end-to-end so far. -> Linux, macOS, and ARM64 artifacts are currently provided as best-effort builds. - -### Windows (Pre-compiled Executable) - -Windows users can download the portable release zip (`AutoMixMaster-windows-.zip`), extract it, and run `AutoMixMaster.exe`. - -### macOS (Pre-compiled App Bundle) - -macOS users can download the release zip (`AutoMixMaster-macos-.zip`), extract it, and open `AutoMixMaster.app`. - -### Linux (Prebuilt Packages) - -Linux users can download either: - -- **AppImage** (`AutoMixMaster--.AppImage`) for a portable one-file launch. -- **Debian package** (`automixmaster__.deb`) for Ubuntu/Debian install. -- **Flatpak bundle** (`AutoMixMaster-linux-.flatpak`) for Flatpak-based installs. - -### Build From Source - -If you are on Linux, or prefer to build the application from source on Windows, refer to the [Build + Install](#build--install) section below for verified instructions. - -## Estimated System Requirements - -These are **practical estimates** for AI-heavy workflows (especially ONNX-based separation/mix/master inference), not strict hard limits. - -AutoMixMaster is designed to benefit from **GPU acceleration** via ONNX Runtime providers. - -### Minimum OS requirements (release artifacts) - -- **Windows:** **Windows 10 or Windows 11** (x64 or ARM64) -- **macOS (Apple Silicon / ARM64):** **macOS 14+** -- **macOS (Intel / x64):** **macOS 15+** -- **Linux:** **Ubuntu 24.04 LTS+** for current prebuilt `.deb`/AppImage artifacts - -> Note: Ubuntu 22.04 may still work if you build from source on 22.04 with compatible dependencies, but official CI/release packaging currently targets Ubuntu 24.04. - -### Minimum workable - -- **CPU:** modern **6-core / 12-thread** desktop CPU (Ryzen 5 5600 / Core i5-12400 class) -- **RAM:** **16 GB minimum** -- **GPU:** compatible acceleration path with ~**6 GB VRAM** - - Windows DirectML path: **DirectX 12-capable GPU** - - CUDA path: **NVIDIA CUDA-capable GPU** -- **Storage:** ~10 GB free (models, temp files, exports) - -### Recommended (smoother) - -- **CPU:** **8 cores / 16 threads or better** (Ryzen 7 / Core i7 class) -- **RAM:** **32 GB** -- **GPU:** **8–12 GB VRAM** - -### Heavy batch / long sessions - -- **CPU:** **12 cores+** strongly recommended -- **RAM:** **32–64 GB** -- **GPU:** **12 GB+ VRAM** - -### Why these estimates - -- GPU acceleration matters most: DirectML needs a **DirectX 12** GPU and CUDA needs an **NVIDIA CUDA-capable** GPU ([DirectML](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html), [CUDA](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html)). -- Demucs notes roughly **3 GB minimum** and around **7 GB typical** GPU memory, so **8 GB+ VRAM** is a safer real-world target; CPU-only runs work but are slower ([Demucs README](https://github.com/facebookresearch/demucs/blob/main/README.md)). +## Install -### ONNX Runtime - -ONNX Runtime is an **optional** dependency. When it is not found at configure time the build -falls back to a deterministic adapter and every model-dependent feature degrades to a -heuristic — it does not fail the build. See [docs/ito-master-validation.md](docs/ito-master-validation.md). - -| | | -|---|---| -| Validated against | **ORT 1.30.x** (1.30.0, 2026-09-10) | -| Minimum for the optional GPU paths | **1.22** | -| Release cadence | roughly monthly — pin a minor series, not a patch | - -The build locates ONNX Runtime with `find_path`/`find_library` and applies **no** version -constraint, so any installed SDK is used. Pin deliberately if you are validating a release. - -#### Provider status (as of 1.30.x) - -| Provider | Status | Notes | -|---|---|---| -| **CPU** | always available | The baseline. Every GPU path falls back here on OOM or device loss, so the app never loses inference capability. | -| **CUDA** | current | Default packages target **CUDA 13.0** since 1.27. CUDA 12.8 packages are deprecated but still published through 1.30. cuDNN is optional at runtime from 1.28. | -| **DirectML** | maintenance mode | The `Microsoft.ML.OnnxRuntime.DirectML` NuGet is **frozen at 1.24.4** and caps at **opset ≤ 20**. It will not gain newer opsets, so prefer the options below for new work. | -| **CoreML** | current | Also covers the **Apple Neural Engine** — there is no separate ANE provider. ANE-specific work goes through CoreML. | -| **OpenVINO** | split | Legacy wheel pinned at 1.24.1; the plugin `onnxruntime-ep-openvino` 1.7.0 requires ORT ≥ 1.23. | -| **Windows ML** | GA (2025-09-23) | The recommended path for new Windows work. C++ needs the **self-contained** NuGet; framework-dependent C/C++ packages are not published. | -| **WebGPU** | preview | Native plugin EP, v0.4.0. | - -AutoMixMaster probes available providers and walks its own priority chain — **ANE → CoreML → -CUDA → OpenVINO → DirectML → CPU** (`src/ai/GpuProvider.h`). If session creation or inference -fails, the provider is recorded as failed and the chain continues, so a broken or missing GPU -runtime degrades to CPU instead of failing the render. - -> **fp16 caveat:** the CPU execution provider does not run fp16 graphs. Quantize to int8 (QDQ -> format) for CPU-only deployment; 16-bit and 4-bit quantization additionally require **opset ≥ 21**. - -#### Optional runtime capabilities - -Two further runtime paths are detected at configure time and are **off unless the installed -ONNX Runtime exposes the matching API**. The provider priority chain above is unchanged either -way, and both features default to off. - -| Capability | Compile guard | Minimum ORT | Status | -|---|---|---|---| -| CUDA provider supplied as a plugin library | `AUTOMIX_HAS_EP_PLUGIN` | 1.23 | Policy implemented; the `RegisterExecutionProviderLibrary` call is not yet wired | -| Per-GPU compiled-model cache (EPContext) | `AUTOMIX_HAS_EP_CONTEXT` | 1.22 | Policy implemented; the `OrtCompileApi` call is not yet wired | - -`src/ai/GpuProvider.h` holds the deciding logic for both — `parseOrtVersion`, -`supportsEpPlugin`, `supportsEpContext`, `decidePluginEpAttempt` and -`compiledModelCacheKey` — as pure functions, so it is covered by the test suite even on a -build with no ONNX Runtime SDK present. The cache key covers the model digest, the provider, -the GPU architecture, the driver version and the ORT version, so recompiling for a different -card or driver can never reuse another card's artifact. A model digest that is not a valid -64-character SHA-256 yields no key at all, because a key that cannot distinguish two models -would alias their caches. - -To finish the wiring, the guarded code should ask `decidePluginEpAttempt(...)` and, when it -returns `attempt == false`, log its `reason` and continue down the existing priority chain; -`Ort::GetAvailableProviders()` already covers every built-in provider. +> ⚠️ Only the **Windows** version has been tested by hand from start to finish. The Linux, macOS and ARM64 builds may have rough edges. +| System | Download | Then | +| :--- | :--- | :--- | +| **Windows** | `AutoMixMaster-windows-.zip` | Extract it and run `AutoMixMaster.exe`. | +| **macOS** | `AutoMixMaster-macos-.zip` | Extract it and open `AutoMixMaster.app`. | +| **Linux** | `.AppImage`, `.deb` or `.flatpak` | Run the AppImage, or install the package. | ---- +**macOS:** the app is unsigned. If macOS refuses to open it, go to **System Settings → Privacy & Security** and click **Open Anyway**. -## Build + Install +
+Build it yourself ### Windows (Visual Studio 2026) -1. Configure - ```bash cmake -S . -B build -G "Visual Studio 18 2026" -A x64 +cmake --build build --config Release --parallel +ctest --test-dir build -C Release --output-on-failure ``` -1. Build +**Release package.** `packaging/windows/build-release.ps1` builds, runs the tests and writes a portable ZIP. Give it the CUDA build of ONNX Runtime so the package can use the GPU: -```bash -cmake --build build --config Release --parallel +```powershell +powershell -File packaging\windows\build-release.ps1 -OnnxRuntimeDir C:\lib\onnxruntime-win-x64-gpu_cuda13-1.30.0 ``` -### Ubuntu Linux (24.04+) +The ZIP never holds model files or NVIDIA's CUDA libraries. The install step fails if it finds one. To run a developer build on CUDA without the GPU pack, point `-DAUTOMIX_CUDA_RUNTIME_DIR` at a folder of those DLLs. -1. Install dependencies +### Ubuntu Linux (24.04+) ```bash sudo apt-get install -y \ - libasound2-dev libfreetype6-dev libx11-dev libxcomposite-dev \ - libxcursor-dev libxext-dev libxinerama-dev libxrandr-dev \ - libxrender-dev libwebkit2gtk-4.1-dev libglu1-mesa-dev mesa-common-dev -``` - -1. Configure + build + build-essential cmake pkg-config \ + libasound2-dev libjack-jackd2-dev libfreetype6-dev libfontconfig1-dev \ + libx11-dev libxcomposite-dev libxcursor-dev libxext-dev libxinerama-dev \ + libxrandr-dev libxrender-dev libwebkit2gtk-4.1-dev libgtk-3-dev \ + libglu1-mesa-dev mesa-common-dev libcurl4-openssl-dev -```bash cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build --parallel +ctest --test-dir build --output-on-failure ``` -1. Run tests (optional but recommended) +Do not skip `libcurl4-openssl-dev`. Without it the app cannot download models. + +**Packages.** `./tools/package_linux.sh` writes a `.deb` and an AppImage to `dist/linux/`. + +**Flatpak.** ```bash -ctest --test-dir build --output-on-failure +sudo apt-get install -y flatpak flatpak-builder +flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo +flatpak --user install -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08 +./tools/build_flatpak.sh ``` -### macOS (Apple Silicon + Intel) +The bundle lands in `dist/flatpak/AutoMixMaster.flatpak`. The manifest is `packaging/flatpak/io.automixmaster.AutoMixMaster.yml`. It fetches its sources ahead of time, so the build needs no network inside the sandbox. -1. Install build tools +### macOS (Apple Silicon + Intel) ```bash xcode-select --install brew install cmake ninja ``` -1. Configure + build (pick one architecture) +Pick one architecture: ```bash -# Apple Silicon (arm64) +# Apple Silicon cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DBUILD_TESTING=OFF -DBUILD_TOOLS=OFF -# Intel (x64) +# Intel cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DBUILD_TESTING=OFF -DBUILD_TOOLS=OFF cmake --build build --target AutoMixMasterApp --parallel ``` -1. Configure + build (universal binary: arm64 + x86_64) +Or build for both at once: ```bash cmake -S . -B build-universal \ @@ -273,7 +175,7 @@ cmake -S . -B build-universal \ cmake --build build-universal --target AutoMixMasterApp --parallel ``` -1. Install app bundle +Then install the app: ```bash APP_BUNDLE="$(find build build-universal -maxdepth 6 -type d -name 'AutoMixMaster.app' 2>/dev/null | head -n 1)" @@ -282,97 +184,154 @@ sudo cp -R "$APP_BUNDLE" /Applications/ open /Applications/AutoMixMaster.app ``` -### Linux Package Builds (.deb + AppImage) - -After building, create distributable Linux packages with: +### Extra render tools -```bash -./tools/package_linux.sh -``` +The app needs none of these. It finds them if you put them here or set the variable: -Output artifacts are written to `dist/linux/`. +- `assets/ffmpeg/bin/ffmpeg(.exe)` or `FFMPEG_BIN` +- `assets/sox/bin/sox(.exe)` or `SOX_BIN` +- `assets/rsgain/bin/rsgain(.exe)` or `RSGAIN_BIN` -### Flatpak +PhaseLimiter runs only when you select it, and it needs ffmpeg. -Manifest path: +
-`packaging/flatpak/io.automixmaster.AutoMixMaster.yml` +--- -> Note: the Flatpak manifest prefetches JUCE/nlohmann/libebur128 sources and passes `FETCHCONTENT_SOURCE_DIR_*` flags so CMake does not need live GitHub access inside the Flatpak sandbox. +## What You Need -Install Flatpak tooling: +The fixed-rule mix and master run on almost any recent computer. The numbers below are for the AI features. They are estimates, not hard limits. -```bash -sudo apt-get install -y flatpak flatpak-builder -``` +| | Works | Comfortable | Big batches | +| :--- | :--- | :--- | :--- | +| **Processor** | 6 cores | 8 cores | 12 cores or more | +| **Memory** | 16 GB | 32 GB | 32–64 GB | +| **Graphics memory** | 6 GB | 8–12 GB | 12 GB or more | +| **Free disk** | 10 GB | | | -Add Flathub and install required runtime/SDK: +**Systems the release builds support** -```bash -flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo -flatpak --user install -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08 -``` +- **Windows** 10 or 11, x64 or ARM64 +- **macOS** 14 or later on Apple Silicon; 15 or later on Intel +- **Linux:** Ubuntu 24.04 or later -Build bundle: +**Graphics cards** -```bash -./tools/build_flatpak.sh -``` +- **Windows and Linux:** an NVIDIA card is fastest. Other cards work if they support DirectX 12 or Vulkan. Linux needs `libvulkan1`. +- **Apple Silicon Macs:** the app uses the built-in graphics chip. +- **Intel Macs:** no AI features. Mixing and mastering still work. +- **No usable card:** the AI features run on the processor. They work, and they are slow. -Output: +**Macs with less than 12 GB of memory.** The Vocal Model runs slowly on them. The app warns you and points you to the light Open-Unmix model. -- `dist/flatpak/AutoMixMaster.flatpak` +### The Vocal Model on an NVIDIA card (Windows) -### Bundled Renderer Tools (Optional) +The Vocal Model runs on your graphics card when all of this is true: -The renderer registry can auto-discover optional CLI tools if you place binaries under: +- The card has at least 12 GB of memory, with 10 GB free. +- Your NVIDIA driver is version 580 or newer. +- You have installed the **GPU pack**. -- `assets/ffmpeg/bin/ffmpeg(.exe)` or set `FFMPEG_BIN` -- `assets/sox/bin/sox(.exe)` or set `SOX_BIN` -- `assets/rsgain/bin/rsgain(.exe)` or set `RSGAIN_BIN` +The GPU pack is a one-time download of about 1 GB. The app offers it when you switch on the Vocal Model. You can also install or remove it under **Settings → GPU acceleration**. It needs no admin rights and changes nothing else on your system. -If a tool is missing, it is hidden from selectable available renderers automatically. +Without these, the Vocal Model runs on the processor and the log tells you why. The difference is large: one 196-second track took 66 to 91 seconds on an RTX 5060 Ti and 687 seconds on the processor. --- ## Licensing -AutoMixMaster is distributed under the **GNU General Public License v3 (GPLv3)**. +AutoMixMaster is free software under the **GNU General Public License v3**. + +**The app ships with no AI models.** You download the ones you want from the Models window. The app shows each model's license and asks you to accept it first. + +**Some models are for non-commercial use only.** If you sell your music, use the MIT-licensed models or the fixed rules. -### Core & Libraries +| Model | Used for | License | Commercial use | +| :--- | :--- | :--- | :--- | +| BS-RoFormer | Vocal Model | MIT | Yes | +| Open-Unmix | Light vocal model | MIT | Yes | +| Whisper, CLAP, PANNs | Analysis | MIT | Yes | +| Demucs / HTDemucs | Stem separation | CC-BY-NC 4.0 weights | No | +| Denoiser | Vocal clean-up | CC-BY-NC 4.0 | No | +| ITO-Master | AI mastering | CC-BY-NC 4.0 | No | + +The full license record for every model is in [docs/model-licensing-audit.json](docs/model-licensing-audit.json). + +**Software the app is built on** | Component | License | Role | | :--- | :--- | :--- | -| JUCE 8.0.8 | AGPLv3 / Commercial | Audio & GUI Framework | -| libebur128 | MIT | EBU R128 Loudness Metering | -| nlohmann/json | MIT | JSON Serialization & Model Metadata | -| Catch2 3.7.1 | BSL-1.0 | Unit Testing Framework | -| PhaseLimiter | GPL-2.0 / Custom | Optional External Limiter | -| FFmpeg | GPL-compatible / LGPL | Optional External Audio Renderer | -| SoX | GPL-2.0-or-later | Optional External Processor | -| rsgain | BSD-2-Clause | Optional ReplayGain Tagging Stage | +| JUCE 8.0.8 | AGPLv3 / Commercial | Audio and interface framework | +| libebur128 | MIT | Loudness metering | +| nlohmann/json | MIT | Reading and writing JSON | +| Catch2 3.7.1 | BSL-1.0 | Tests | +| PhaseLimiter | GPL-2.0 / Custom | Optional limiter | +| FFmpeg | GPL-compatible / LGPL | Optional renderer | +| SoX | GPL-2.0-or-later | Optional processor | +| rsgain | BSD-2-Clause | Optional loudness tagging | -### AI Model Hub & Third-Party Weights +--- -Model weights are **not bundled** into the installer or executable binaries. Users can optionally download models on-demand through the built-in Model Hub (`ModelManager`), which preserves and displays upstream model licensing metadata (`license` / `cardData` tags): +## Developer Notes -| Model / Model Family | Upstream Author | License | Usage & Compatibility | -| :--- | :--- | :--- | :--- | -| **HTDemucs / Demucs 4-stem** | Meta Research | MIT (code), CC-BY-NC 4.0 (MUSDB18-HQ weights) | Stem Separation (Non-Commercial weights) | -| **HTDemucs 6-stem (`htdemucs_6s`)** | Meta / Community ONNX | CC-BY-NC 4.0 | 6-Stem Separation (Guitar/Piano/Drums/Bass/Vocal/Other) | -| **Denoiser (`dns64` / Speech Enhancer)** | Meta Research | CC-BY-NC 4.0 | Speech & Vocal Denoising (Non-Commercial evaluation) | -| **ITO-Master (`ito-master-v1`)** | Community / Open | CC-BY-NC 4.0 | AI Mastering (46-param native white-box FX chain) | -| **Whisper Tiny / Small** | OpenAI | MIT | Transcripts, Vocal Alignment & Pitch Analysis | -| **CLAP (`clap-htsat`)** | LAION | MIT | Style Retrieval & Audio Embeddings | -| **PANNs (`PANNs_CNN14`)** | Bio-DSP / Open | MIT | General Audio Tagging & Classification | +Most people can stop reading here. These notes are for building against ONNX Runtime or writing a model pack. + +
+ONNX Runtime and graphics providers + +### ONNX Runtime + +ONNX Runtime is optional. Without it, the build still succeeds and every model feature falls back to the fixed rules. + +| | | +|---|---| +| Validated against | **ORT 1.30.x** (1.30.0, 2026-09-10) | +| Minimum for the optional GPU paths | **1.22** | +| Release cadence | roughly monthly — pin a minor series, not a patch | + +The build finds ONNX Runtime with `find_package(onnxruntime 1.30.0 EXACT CONFIG)` when fetched via `AUTOMIX_FETCH_ORT=ON`, or with `find_path`/`find_library` for a system SDK. -> **Non-Commercial Notice**: Models licensed under **CC-BY-NC 4.0** (such as Meta Demucs, Denoiser, and ITO-Master weights) are restricted to personal, educational, and non-commercial evaluation use. Commercial workflows can use open-source MIT-licensed models (e.g. Whisper, CLAP) or the built-in deterministic heuristic DSP engines. User consent gating is enforced prior to model download and execution. +#### Provider status (as of 1.30.x) -> **Model Licensing Audit**: For complete machine-checkable model license metadata and audit reports, see [docs/model-licensing-audit.md](docs/model-licensing-audit.md) and [docs/model-licensing-audit.json](docs/model-licensing-audit.json). +| Provider | Status | Notes | +|---|---|---| +| **CPU** | always available | The baseline. Every GPU path falls back here on out-of-memory or device loss. | +| **CUDA** | current | Default packages target **CUDA 13.0** since 1.27. cuDNN and the CUDA runtime load at run time when present. | +| **WebGPU** | current | A plugin provider on Windows and Linux (`Microsoft.ML.OnnxRuntime.EP.WebGpu` 0.4.0) and in-tree on Apple Silicon. The default non-NVIDIA path on Windows and Linux. Needs `libvulkan1` on Linux. | +| **CoreML** | current | Built in on macOS. Covers the **Apple Neural Engine** (`MLComputeUnits=CPUAndNeuralEngine` or `ALL`). Intel Macs run no AI inference. | +| **DirectML** | maintenance mode | The `Microsoft.ML.OnnxRuntime.DirectML` NuGet is frozen at 1.24.4 and caps at opset ≤ 20. WebGPU replaces it. | +| **OpenVINO** | split | The legacy wheel is pinned at 1.24.1. The plugin `onnxruntime-ep-openvino` 1.7.0 needs ORT ≥ 1.23. | +| **Windows ML** | GA (2025-09-23) | Recommended for new Windows work. C++ needs the self-contained NuGet. | -#### Mix-Scope Model Contract +The app probes the providers and walks its own priority chain: **ANE → CoreML → CUDA → WebGPU → OpenVINO → DirectML → CPU** (`src/ai/GpuProvider.h`). A provider that fails is recorded and skipped. A broken GPU runtime slows the render down. It does not stop it. -No curated `mix` model ships today — the AI mix path is fully wired (`AutoMixStrategyAI`), so it activates as soon as a valid mix pack is installed, and otherwise falls back to the deterministic heuristic. A downloadable mix model must satisfy all of the following: +> **fp16 caveat:** the CPU provider does not run fp16 graphs. Quantize to int8 (QDQ format) for CPU-only use. 16-bit and 4-bit quantization also need opset ≥ 21. + +#### Optional runtime capabilities + +Two more paths are detected at configure time. Both are off unless the installed ONNX Runtime exposes the matching API. Neither changes the priority chain. + +| Capability | Compile guard | Minimum ORT | Status | +|---|---|---|---| +| WebGPU provider supplied as a plugin library | `AUTOMIX_HAS_EP_PLUGIN` | 1.23 | Wired for WebGPU via `OrtRuntime` and `RegisterExecutionProviderLibrary` | +| Per-GPU compiled-model cache (EPContext) | `AUTOMIX_HAS_EP_CONTEXT` | 1.22 | Policy implemented. The `OrtCompileApi` call is not wired yet. | + +`src/ai/GpuProvider.h` holds the deciding logic as pure functions: `parseOrtVersion`, `supportsEpPlugin`, `supportsEpContext`, `decidePluginEpAttempt` and `compiledModelCacheKey`. The tests cover them even on a build with no ONNX Runtime SDK. + +The cache key covers the model digest, the provider, the GPU architecture, the driver version and the ORT version. One card can never reuse another card's compiled model. A digest that is not a valid 64-character SHA-256 yields no key at all. + +To finish the wiring, the guarded code should call `decidePluginEpAttempt(...)`. When it returns `attempt == false`, log its `reason` and continue down the chain. + +
+ +
+Model pack contracts + +### Mix-Scope Model Contract + +No curated `mix` model ships today. The AI mix path is wired (`AutoMixStrategyAI`). It activates when you install a valid mix pack. Until then it uses the fixed rules. + +A mix model must meet all of these: | Requirement | Value | Enforced by | | :--- | :--- | :--- | @@ -380,18 +339,20 @@ No curated `mix` model ships today — the AI mix path is fully wired (`AutoMixS | Manifest metadata | non-empty `license`, `source`, `feature_schema_version` | `ModelPackLoader` | | `feature_schema_version` | `1.0.0` | `FeatureSchemaV1::isCompatible` | | Required output keys | `confidence`, `global_gain_db` (±12 dB), `global_pan_bias` (±1.0) | `ModelPackLoader` + `OnnxModelInference` | -| Optional per-stem keys | `stem_gain_db` (±24 dB), `stem_pan` (±1.0) — a superset of the required keys | `AutoMixStrategyAI` | -| Input features | **66 floats per stem, concatenated** — `input_feature_count` must equal `66 × stem count` exactly | `OnnxModelInference::run` | +| Optional per-stem keys | `stem_gain_db` (±24 dB), `stem_pan` (±1.0) | `AutoMixStrategyAI` | +| Input features | **66 floats per stem, concatenated** — `input_feature_count` must equal `66 × stem count` | `OnnxModelInference::run` | | `allowed_tasks` | must include `mix_parameters` | `OnnxModelInference::run` | -Two consequences worth knowing before authoring a pack: +Two things to know before you author a pack: + +- **The stem count is baked into the input width.** A pack trained for 4 stems (`input_feature_count: 264`) is rejected on a 3-stem session. A model for varying stem counts must accept a padded or per-stem input. +- **The leading public model is not plug-and-play.** `csteinmetz1/automix-toolkit` (Apache-2.0) predicts per-track gain and pan, which maps onto the `stem_*` keys. But its weights are PyTorch `.ckpt` files and its input is an audio encoder, not the 66-float vector. It needs an ONNX export and a host-side encoder, so it is not on the curated list. -- **The stem count is baked into the model's input width.** Because features are concatenated per stem, a pack trained for 4 stems (`input_feature_count: 264`) is rejected outright on a 3-stem session. A model intended for varying stem counts must accept a padded or per-stem input, not a fixed concatenation. -- **The leading public model is not plug-and-play.** `csteinmetz1/automix-toolkit` (Apache-2.0) is the best-licensed downloadable mixer — it predicts per-track gain and pan, which maps cleanly onto the `stem_gain_db` / `stem_pan` keys — but its published weights are PyTorch `.ckpt` checkpoints and its input is an audio encoder (log-mel/STFT), not the 66-float feature vector. Using it requires an ONNX export **and** a host-side audio-encoder frontend, so it is deliberately absent from the curated list rather than listed as broken. +### Model Inference Contracts -#### Model Inference Contract (all scopes) +The app has two inference interfaces. -`IModelInference` is a **features-in, scalars-out** interface. A request carries one flat `std::vector` (`InferenceRequest::features`); a response carries flat named scalars (`InferenceResult::outputs`). There is no audio-tensor path through it. These six tasks are the complete set: +**1. `IModelInference`: features in, scalars out.** A request carries one flat `std::vector`. A response carries named scalars. These tasks use it: | Task | Input | Output keys | Consumer | | :--- | :--- | :--- | :--- | @@ -400,23 +361,27 @@ Two consequences worth knowing before authoring a pack: | `role_classifier` | 66 floats per stem | `prob_vocals`, `prob_bass`, `prob_drums`, `prob_fx` | `StemRoleClassifierAI` | | `stem_separation` | per-4096-sample-frame feature vector | `stem_weight` \| `source_weight` \| `mask_` \| `_weight` | `StemSeparator` | | `mix_master_override` | all stems' features, concatenated | `dryWet`, `targetLufs`, `preGainDb` (legacy) | `ModelStrategy` | -| `ito_fxencoder`, `ito_predictor` | audio tensors `[1,2,N]` → `[1,2048]` → `[1,46]` | 46 normalized chain parameters | `ItoMasterModelRunner` | +| `ito_fxencoder`, `ito_predictor` | the first N stereo samples, flattened channel-major (encoder); the same vector plus the encoder's 2048 outputs (predictor) | 2048-dim embedding, then 46 normalized chain parameters | `ItoMasterModelRunner` | -**Consequence: a model whose input is raw audio, a complex STFT, or a multi-tensor bundle cannot be used through this interface.** That excludes essentially the whole published audio ecosystem — Demucs/HTDemucs, BS-Roformer and Mel-Band Roformer, Open-Unmix, Spleeter, Whisper, CLAP, PANNs, CED, Basic Pitch, CREPE, skey, beat-this, chordmini — regardless of license. Installing one yields a pack that validates and downloads, then either fails the `features.size() != input_feature_count` check or receives a feature vector where it expects audio. +This interface cannot carry audio. Its output is a `map`, and its input has no shape, channels or phase. A model that returns an audio-shaped tensor does not fit. -This applies to the three **already-curated** separation models (`rysertio/Demucs-onnx`, `StemSplitio/htdemucs-ft-onnx`, `StemSplitio/htdemucs-6s-onnx`): the separator feeds them a per-frame feature vector and reads back per-stem weights, so with no weight key in the response it applies its own heuristic. `StemSeparator` now reports that case honestly — `SeparationResult::usedModel` is `false` and the log says the fallback weights were used — rather than claiming "Model-backed overlap-add separation completed". +The three curated Demucs models (`rysertio/Demucs-onnx`, `StemSplitio/htdemucs-ft-onnx`, `StemSplitio/htdemucs-6s-onnx`) go through this interface. They return no weight keys, so the separator applies its own fixed rules. `StemSeparator` reports that honestly: `SeparationResult::usedModel` is `false`, and the log says it used fallback weights. -The verified-later candidates below are held back by that single missing frontend, not by their licenses (licenses confirmed against the Hugging Face model API; all ungated): +**2. `ITensorInference`: shaped tensors in and out.** `OnnxTensorInference` passes named float32 tensors to ONNX Runtime. `SeparationRunner` adds a host-side STFT around it. This is how the audio models run: -| Model | License | Why it is not curated yet | +| Model | Input the host builds | Output the host applies | | :--- | :--- | :--- | -| `xycld/BS-RoFormer-ONNX` | MIT | consumes a complex STFT; needs a tensor-level audio input | -| `musetric/skey-onnx` | MIT | expects 22.05 kHz audio, not the 66-float vector | -| `musetric/chordmini-onnx` | MIT | expects a 144-bin log-CQT the host does not compute | -| `musetric/beat-this-onnx` | MIT | expects a 128-bin log-mel the host does not compute | -| `mispeech/ced-base` | Apache-2.0 | expects 16 kHz waveform input | -| Basic Pitch `nmp.onnx` | Apache-2.0 | expects a 43844-sample CQT input | +| `xycld/BS-RoFormer-ONNX` | STFT of the mix | a real/imaginary mask | +| `MixDirective/open-unmix-umxhq-vocals-onnx` | STFT magnitudes | a ratio mask; the mix phase is kept | -**The unblock is one interface, not a bigger catalog.** `ItoMasterModelRunner` already drives a real audio→audio→parameters graph in-process, so the pattern is proven; generalising it into a tensor-level audio interface (multi-input/multi-output tensors alongside `IModelInference`) is what would make the entire download ecosystem reachable, and is the reason adding more curated ids before then only adds download size. +These candidates are still held back. Each needs a front end the host does not compute yet. Their licenses are fine (confirmed against the Hugging Face model API; all ungated): +| Model | License | What it needs | +| :--- | :--- | :--- | +| `musetric/skey-onnx` | MIT | 22.05 kHz audio input | +| `musetric/chordmini-onnx` | MIT | a 144-bin log-CQT | +| `musetric/beat-this-onnx` | MIT | a 128-bin log-mel | +| `mispeech/ced-base` | Apache-2.0 | 16 kHz waveform input | +| Basic Pitch `nmp.onnx` | Apache-2.0 | a 43844-sample CQT input | +
diff --git a/assets/lame-pins.json b/assets/lame-pins.json new file mode 100644 index 0000000..1a6b9f3 --- /dev/null +++ b/assets/lame-pins.json @@ -0,0 +1,41 @@ +{ + "schema": 1, + "sources": [ + { + "platform": "linux-arm", + "type": "deb", + "url": "https://deb.debian.org/debian/pool/main/l/lame/lame_3.100-6+b3_armhf.deb", + "sha256": "b7c5a96b803db07f67b74b7611240979797fb6eab0b4705e23eb0ce639b9078e" + }, + { + "platform": "linux-arm64", + "type": "deb", + "url": "https://deb.debian.org/debian/pool/main/l/lame/lame_3.100-6+b3_arm64.deb", + "sha256": "4c6c6ee693633c846de685902641dbda18ef98f73a136ebef7c09cf6ee683bcd" + }, + { + "platform": "linux-x64", + "type": "deb", + "url": "https://deb.debian.org/debian/pool/main/l/lame/lame_3.100-6+b3_amd64.deb", + "sha256": "feceeb296f9df9340f6e2ce48decce73c9cd6c70e7fe959495eb89ee59bc8d47" + }, + { + "platform": "win32-arm64", + "type": "zip", + "url": "https://www.rarewares.org/files/mp3/lame3.100.1-x64.zip", + "sha256": "9a9c815203316e5203847e93100c6acf0d5d7a5be7744c9018825ded037052e7" + }, + { + "platform": "win32-ia32", + "type": "zip", + "url": "https://www.rarewares.org/files/mp3/lame3.100.1-win32.zip", + "sha256": "2518e1138953c235fb2bfcefbc38883dd04538d6ae0a19692562576ba37bafec" + }, + { + "platform": "win32-x64", + "type": "zip", + "url": "https://www.rarewares.org/files/mp3/lame3.100.1-x64.zip", + "sha256": "9a9c815203316e5203847e93100c6acf0d5d7a5be7744c9018825ded037052e7" + } + ] +} diff --git a/assets/screenshots/main-empty.png b/assets/screenshots/main-empty.png new file mode 100644 index 0000000..52fa8ae Binary files /dev/null and b/assets/screenshots/main-empty.png differ diff --git a/assets/screenshots/main-session.png b/assets/screenshots/main-session.png new file mode 100644 index 0000000..cf2a4e8 Binary files /dev/null and b/assets/screenshots/main-session.png differ diff --git a/cmake/FetchOnnxRuntime.cmake b/cmake/FetchOnnxRuntime.cmake new file mode 100644 index 0000000..12777b6 --- /dev/null +++ b/cmake/FetchOnnxRuntime.cmake @@ -0,0 +1,152 @@ +# cmake/FetchOnnxRuntime.cmake +# Fetches pinned ONNX Runtime release archives and the WebGPU EP plugin. +# +# Pinned version: 1.30.0 (exact pin convention) +# Supported architectures: +# Windows x64 (cpu or cuda), Windows arm64 (cpu) +# Linux x64 (cpu or cuda), Linux aarch64 (cpu) +# macOS arm64 (CoreML and WebGPU in-tree) +# macOS x86_64 is explicitly unsupported for AI inference (FATAL_ERROR). + +include(FetchContent) + +set(AUTOMIX_ORT_VERSION "1.30.0" CACHE STRING "ONNX Runtime version to fetch") +set(AUTOMIX_ORT_FLAVOR "cpu" CACHE STRING "ONNX Runtime flavor (cpu or cuda)") + +# Check U1: Intel Macs do not support ONNX Runtime +if(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64") + message(FATAL_ERROR "ONNX Runtime is not supported on Intel macOS. Please configure with -DENABLE_ONNX=OFF.") +endif() + +# Determine OS and Architecture keys +if(WIN32) + set(_os "win") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64" + OR CMAKE_GENERATOR_PLATFORM MATCHES "ARM64|arm64" + OR CMAKE_VS_PLATFORM_NAME MATCHES "ARM64|arm64") + set(_arch "arm64") + else() + set(_arch "x64") + endif() +elseif(APPLE) + set(_os "osx") + set(_arch "arm64") +elseif(UNIX) + set(_os "linux") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") + set(_arch "aarch64") + else() + set(_arch "x64") + endif() +else() + message(FATAL_ERROR "Unsupported platform for AUTOMIX_FETCH_ORT: ${CMAKE_SYSTEM_NAME}") +endif() + +# Resolve archive URL and SHA-256 hash +set(_ort_url "") +set(_ort_hash "") + +if(_os STREQUAL "win") + if(_arch STREQUAL "x64") + if(AUTOMIX_ORT_FLAVOR STREQUAL "cuda") + set(_ort_url "https://github.com/microsoft/onnxruntime/releases/download/v1.30.0/onnxruntime-win-x64-gpu_cuda13-1.30.0.zip") + set(_ort_hash "SHA256=8fa4b08359af682cd605892cb59077049700b640128bb93fd2c7776cf9f55bdc") + else() + set(_ort_url "https://github.com/microsoft/onnxruntime/releases/download/v1.30.0/onnxruntime-win-x64-1.30.0.zip") + set(_ort_hash "SHA256=c6ba983baf5681af108599675d2a89c2d145512d02de28aed0bff177cd0ba949") + endif() + elseif(_arch STREQUAL "arm64") + set(_ort_url "https://github.com/microsoft/onnxruntime/releases/download/v1.30.0/onnxruntime-win-arm64-1.30.0.zip") + set(_ort_hash "SHA256=e53db8a50b23ae35be901cc93428baf997dc8d420333b097b2eae53d3ea9f2d3") + endif() +elseif(_os STREQUAL "linux") + if(_arch STREQUAL "x64") + if(AUTOMIX_ORT_FLAVOR STREQUAL "cuda") + set(_ort_url "https://github.com/microsoft/onnxruntime/releases/download/v1.30.0/onnxruntime-linux-x64-gpu_cuda13-1.30.0.tgz") + set(_ort_hash "SHA256=382d79133112388cf94ce5855789b7c9bef12bef76a08b6b277e5a317213adcd") + else() + set(_ort_url "https://github.com/microsoft/onnxruntime/releases/download/v1.30.0/onnxruntime-linux-x64-1.30.0.tgz") + set(_ort_hash "SHA256=a5ed5a3cac51fbb2e90da632ae43d19212faaa20e76484e62bcb7c23ddb3b3fd") + endif() + elseif(_arch STREQUAL "aarch64") + set(_ort_url "https://github.com/microsoft/onnxruntime/releases/download/v1.30.0/onnxruntime-linux-aarch64-1.30.0.tgz") + set(_ort_hash "SHA256=e16a27a8ed330bbc698df7330b0cf56e722f354e3bcc92118682c74ef3c3e3da") + endif() +elseif(_os STREQUAL "osx") + set(_ort_url "https://github.com/microsoft/onnxruntime/releases/download/v1.30.0/onnxruntime-osx-arm64-1.30.0.tgz") + set(_ort_hash "SHA256=6ebb5062a934537c352937821f9fe9718e7de1a2db1122a93dd363ffd53a7012") +endif() + +if(NOT _ort_url) + message(FATAL_ERROR "No ONNX Runtime 1.30.0 archive configured for ${_os}-${_arch} (flavor: ${AUTOMIX_ORT_FLAVOR})") +endif() + +message(STATUS "Fetching ONNX Runtime ${AUTOMIX_ORT_VERSION} (${AUTOMIX_ORT_FLAVOR}) for ${_os}-${_arch}...") + +FetchContent_Declare( + onnxruntime_fetched + URL "${_ort_url}" + URL_HASH "${_ort_hash}" + DOWNLOAD_EXTRACT_TIMESTAMP TRUE +) +FetchContent_MakeAvailable(onnxruntime_fetched) + +set(_ort_root "${onnxruntime_fetched_SOURCE_DIR}") + +# Fetch WebGPU EP plugin on Windows and Linux +if(WIN32 OR (UNIX AND NOT APPLE)) + message(STATUS "Fetching ONNX Runtime WebGPU EP plugin (NuGet 0.4.0)...") + FetchContent_Declare( + onnxruntime_webgpu_nupkg + URL "https://www.nuget.org/api/v2/package/Microsoft.ML.OnnxRuntime.EP.WebGpu/0.4.0" + URL_HASH "SHA256=cc2e093a2eaa760a630d6c3299f015f1dd86885b9eb5877257135308f7c5e923" + DOWNLOAD_NAME "Microsoft.ML.OnnxRuntime.EP.WebGpu.0.4.0.zip" + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + ) + FetchContent_MakeAvailable(onnxruntime_webgpu_nupkg) + + if(WIN32) + if(_arch STREQUAL "arm64") + set(AUTOMIX_WEBGPU_PLUGIN_DIR "${onnxruntime_webgpu_nupkg_SOURCE_DIR}/runtimes/win-arm64/native" CACHE PATH "Directory holding WebGPU plugin" FORCE) + else() + set(AUTOMIX_WEBGPU_PLUGIN_DIR "${onnxruntime_webgpu_nupkg_SOURCE_DIR}/runtimes/win-x64/native" CACHE PATH "Directory holding WebGPU plugin" FORCE) + endif() + elseif(UNIX AND NOT APPLE) + if(_arch STREQUAL "aarch64") + set(AUTOMIX_WEBGPU_PLUGIN_DIR "${onnxruntime_webgpu_nupkg_SOURCE_DIR}/runtimes/linux-arm64/native" CACHE PATH "Directory holding WebGPU plugin" FORCE) + else() + set(AUTOMIX_WEBGPU_PLUGIN_DIR "${onnxruntime_webgpu_nupkg_SOURCE_DIR}/runtimes/linux-x64/native" CACHE PATH "Directory holding WebGPU plugin" FORCE) + endif() + endif() +endif() + +# Configure ONNX Runtime CMake target and variables +if(EXISTS "${_ort_root}/lib/cmake/onnxruntime/onnxruntimeConfig.cmake") + set(onnxruntime_DIR "${_ort_root}/lib/cmake/onnxruntime") + if(EXISTS "${_ort_root}/lib" AND NOT EXISTS "${_ort_root}/lib64") + file(CREATE_LINK "${_ort_root}/lib" "${_ort_root}/lib64" SYMBOLIC) + endif() + if(NOT EXISTS "${_ort_root}/include/onnxruntime") + file(MAKE_DIRECTORY "${_ort_root}/include/onnxruntime") + endif() + find_package(onnxruntime 1.30.0 EXACT CONFIG REQUIRED NO_DEFAULT_PATH) + target_include_directories(onnxruntime::onnxruntime INTERFACE "${_ort_root}/include") + set(ONNXRUNTIME_INCLUDE_DIR "${_ort_root}/include") + if(APPLE) + set(ONNXRUNTIME_LIBRARY "${_ort_root}/lib/libonnxruntime.dylib") + else() + set(ONNXRUNTIME_LIBRARY "${_ort_root}/lib/libonnxruntime.so") + endif() +else() + # Windows zip archives do not bundle CMake config files + set(ONNXRUNTIME_INCLUDE_DIR "${_ort_root}/include") + set(ONNXRUNTIME_LIBRARY "${_ort_root}/lib/onnxruntime.lib") + if(NOT TARGET onnxruntime::onnxruntime) + add_library(onnxruntime::onnxruntime SHARED IMPORTED) + set_target_properties(onnxruntime::onnxruntime PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${ONNXRUNTIME_INCLUDE_DIR}" + IMPORTED_IMPLIB "${ONNXRUNTIME_LIBRARY}" + IMPORTED_LOCATION "${_ort_root}/lib/onnxruntime.dll" + ) + endif() +endif() diff --git a/cmake/FetchPhaseLimiter.cmake b/cmake/FetchPhaseLimiter.cmake new file mode 100644 index 0000000..d919ee1 --- /dev/null +++ b/cmake/FetchPhaseLimiter.cmake @@ -0,0 +1,193 @@ +# cmake/FetchPhaseLimiter.cmake +# Fetches pinned PhaseLimiter release archives and stages assets beside executables. + +include(FetchContent) + +function(automix_phaselimiter_platform_key OUT_VAR) + set(_os "${CMAKE_SYSTEM_NAME}") + if(NOT _os) + if(APPLE) + set(_os "Darwin") + elseif(WIN32) + set(_os "Windows") + else() + set(_os "${CMAKE_HOST_SYSTEM_NAME}") + endif() + endif() + + set(_proc_raw "${CMAKE_SYSTEM_PROCESSOR}") + if(NOT _proc_raw) + set(_proc_raw "${CMAKE_HOST_SYSTEM_PROCESSOR}") + endif() + if(NOT _proc_raw AND WIN32) + set(_proc_raw "$ENV{PROCESSOR_ARCHITECTURE}") + endif() + if(NOT _proc_raw AND (UNIX OR APPLE)) + execute_process(COMMAND uname -m OUTPUT_VARIABLE _uname_m OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + set(_proc_raw "${_uname_m}") + endif() + string(TOLOWER "${_proc_raw}" _proc) + + if(_os STREQUAL "Darwin") + if(CMAKE_OSX_ARCHITECTURES) + list(LENGTH CMAKE_OSX_ARCHITECTURES _num_archs) + if(_num_archs GREATER 1) + message(FATAL_ERROR "PhaseLimiter does not support universal macOS architectures: ${CMAKE_OSX_ARCHITECTURES}") + endif() + string(TOLOWER "${CMAKE_OSX_ARCHITECTURES}" _osx_arch) + if(_osx_arch STREQUAL "arm64" OR _osx_arch STREQUAL "aarch64") + set(${OUT_VAR} "macos-arm64" PARENT_SCOPE) + return() + elseif(_osx_arch STREQUAL "x86_64" OR _osx_arch STREQUAL "amd64") + set(${OUT_VAR} "macos-x86_64" PARENT_SCOPE) + return() + else() + message(FATAL_ERROR "Unsupported macOS architecture in CMAKE_OSX_ARCHITECTURES: ${CMAKE_OSX_ARCHITECTURES}") + endif() + endif() + + if(_proc MATCHES "arm64|aarch64") + set(${OUT_VAR} "macos-arm64" PARENT_SCOPE) + return() + elseif(_proc MATCHES "x86_64|amd64") + set(${OUT_VAR} "macos-x86_64" PARENT_SCOPE) + return() + else() + message(FATAL_ERROR "Unsupported macOS processor: ${_proc_raw}") + endif() + + elseif(_os STREQUAL "Linux") + if(_proc MATCHES "arm64|aarch64") + set(${OUT_VAR} "linux-arm64" PARENT_SCOPE) + return() + elseif(_proc MATCHES "x86_64|amd64") + set(${OUT_VAR} "linux-x64" PARENT_SCOPE) + return() + else() + message(FATAL_ERROR "Unsupported Linux processor: ${_proc_raw}") + endif() + + elseif(_os STREQUAL "Windows") + if(_proc MATCHES "x86_64|amd64|arm64|aarch64") + set(${OUT_VAR} "windows-x64" PARENT_SCOPE) + return() + else() + message(FATAL_ERROR "Unsupported Windows processor: ${_proc_raw}") + endif() + + else() + message(FATAL_ERROR "Unsupported platform for PhaseLimiter: ${_os}") + endif() +endfunction() + +if(CMAKE_SCRIPT_MODE_FILE OR NOT AUTOMIX_FETCH_PHASELIMITER) + return() +endif() + +set(AUTOMIX_PHASELIMITER_ROOT "") + +include("${CMAKE_CURRENT_LIST_DIR}/PhaseLimiterPins.cmake") +automix_phaselimiter_platform_key(_platform_key) + +set(_pl_url "") +set(_pl_hash "") + +if(_platform_key STREQUAL "windows-x64") + if(EXISTS "${CMAKE_SOURCE_DIR}/assets/phaselimiter/bin/phase_limiter.exe") + set(AUTOMIX_PHASELIMITER_ROOT "${CMAKE_SOURCE_DIR}/assets/phaselimiter") + else() + set(_pl_url "${AUTOMIX_PL_PIN_WINDOWS_X64_URL}") + if(AUTOMIX_PL_PIN_WINDOWS_X64_SHA256) + set(_pl_hash "SHA256=${AUTOMIX_PL_PIN_WINDOWS_X64_SHA256}") + endif() + endif() +elseif(_platform_key STREQUAL "linux-x64") + set(_pl_url "${AUTOMIX_PL_PIN_LINUX_X64_URL}") + if(AUTOMIX_PL_PIN_LINUX_X64_SHA256) + set(_pl_hash "SHA256=${AUTOMIX_PL_PIN_LINUX_X64_SHA256}") + endif() +elseif(_platform_key STREQUAL "linux-arm64") + set(_pl_url "${AUTOMIX_PL_PIN_LINUX_ARM64_URL}") + if(AUTOMIX_PL_PIN_LINUX_ARM64_SHA256) + set(_pl_hash "SHA256=${AUTOMIX_PL_PIN_LINUX_ARM64_SHA256}") + endif() +elseif(_platform_key STREQUAL "macos-arm64") + set(_pl_url "${AUTOMIX_PL_PIN_MACOS_ARM64_URL}") + if(AUTOMIX_PL_PIN_MACOS_ARM64_SHA256) + set(_pl_hash "SHA256=${AUTOMIX_PL_PIN_MACOS_ARM64_SHA256}") + endif() +elseif(_platform_key STREQUAL "macos-x86_64") + set(_pl_url "${AUTOMIX_PL_PIN_MACOS_X86_64_URL}") + if(AUTOMIX_PL_PIN_MACOS_X86_64_SHA256) + set(_pl_hash "SHA256=${AUTOMIX_PL_PIN_MACOS_X86_64_SHA256}") + endif() +endif() + +if(NOT AUTOMIX_PHASELIMITER_ROOT AND _pl_url) + message(STATUS "Fetching PhaseLimiter archive: ${_pl_url}") + FetchContent_Declare( + phaselimiter_fetched + URL "${_pl_url}" + URL_HASH "${_pl_hash}" + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + ) + FetchContent_MakeAvailable(phaselimiter_fetched) + + if(EXISTS "${phaselimiter_fetched_SOURCE_DIR}/bin") + set(AUTOMIX_PHASELIMITER_ROOT "${phaselimiter_fetched_SOURCE_DIR}") + elseif(EXISTS "${phaselimiter_fetched_SOURCE_DIR}/phaselimiter/bin") + set(AUTOMIX_PHASELIMITER_ROOT "${phaselimiter_fetched_SOURCE_DIR}/phaselimiter") + else() + file(GLOB _pl_subdirs LIST_DIRECTORIES true "${phaselimiter_fetched_SOURCE_DIR}/*") + foreach(_sub IN LISTS _pl_subdirs) + if(IS_DIRECTORY "${_sub}" AND EXISTS "${_sub}/bin") + set(AUTOMIX_PHASELIMITER_ROOT "${_sub}") + break() + endif() + endforeach() + endif() +endif() + +if(UNIX AND AUTOMIX_PHASELIMITER_ROOT) + file(GLOB _pl_bins "${AUTOMIX_PHASELIMITER_ROOT}/bin/*") + foreach(_bin IN LISTS _pl_bins) + if(NOT IS_DIRECTORY "${_bin}") + file(CHMOD "${_bin}" + PERMISSIONS + OWNER_READ OWNER_WRITE OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE + WORLD_READ WORLD_EXECUTE) + endif() + endforeach() +endif() + +function(automix_stage_phaselimiter TARGET_NAME) + if(NOT AUTOMIX_PHASELIMITER_ROOT) + return() + endif() + + set(_staging_commands) + foreach(_dir bin resource licenses) + if(EXISTS "${AUTOMIX_PHASELIMITER_ROOT}/${_dir}") + list(APPEND _staging_commands COMMAND ${CMAKE_COMMAND} -E copy_directory + "${AUTOMIX_PHASELIMITER_ROOT}/${_dir}" + "$/assets/phaselimiter/${_dir}") + endif() + endforeach() + foreach(_file LICENSE README.md) + if(EXISTS "${AUTOMIX_PHASELIMITER_ROOT}/${_file}") + list(APPEND _staging_commands COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${AUTOMIX_PHASELIMITER_ROOT}/${_file}" + "$/assets/phaselimiter/${_file}") + endif() + endforeach() + + if(_staging_commands) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/assets/phaselimiter" + ${_staging_commands} + COMMENT "Staging PhaseLimiter assets next to ${TARGET_NAME}" + VERBATIM + ) + endif() +endfunction() diff --git a/cmake/PhaseLimiterPins.cmake b/cmake/PhaseLimiterPins.cmake new file mode 100644 index 0000000..130827e --- /dev/null +++ b/cmake/PhaseLimiterPins.cmake @@ -0,0 +1,44 @@ +# cmake/PhaseLimiterPins.cmake +# Single source of truth for PhaseLimiter release downloads and SHA-256 integrity pins. + +set(AUTOMIX_PL_PIN_WINDOWS_X64_URL "https://github.com/ai-mastering/phaselimiter/releases/download/v0.2.0/phaselimiter-win.zip") +set(AUTOMIX_PL_PIN_WINDOWS_X64_SHA256 "cab2d30ad8d993a383749b30d9f6dc1911198d3aa309d5f631b70206e0162145") + +set(AUTOMIX_PL_PIN_LINUX_X64_URL "https://github.com/soficis/phaselimiter/releases/download/v0.2.0-native3/phaselimiter-0.2.0-native3-linux-x64.tar.xz") +set(AUTOMIX_PL_PIN_LINUX_X64_SHA256 "8ba28b31f823555c3c368a6d3b4757c1980c981b6abe726e80d724f050aeefaa") + +set(AUTOMIX_PL_PIN_LINUX_ARM64_URL "https://github.com/soficis/phaselimiter/releases/download/v0.2.0-native3/phaselimiter-0.2.0-native3-linux-arm64.tar.xz") +set(AUTOMIX_PL_PIN_LINUX_ARM64_SHA256 "714867eaf3ca9c33f68648ae3d073323b0990d816cca7b767ded41456461441c") + +set(AUTOMIX_PL_PIN_MACOS_ARM64_URL "https://github.com/soficis/phaselimiter/releases/download/v0.2.0-native3/phaselimiter-0.2.0-native3-macos-arm64.tar.xz") +set(AUTOMIX_PL_PIN_MACOS_ARM64_SHA256 "42614e95f2d185bf40498a6e1da199f547a2c7bd427e74ed74ee35c9d1512a43") + +set(AUTOMIX_PL_PIN_MACOS_X86_64_URL "https://github.com/soficis/phaselimiter/releases/download/v0.2.0-native3/phaselimiter-0.2.0-native3-macos-x86_64.tar.xz") +set(AUTOMIX_PL_PIN_MACOS_X86_64_SHA256 "e63e2b755fdaa80c104aa6ff5a0866e0f3754fbb79ed84efd696af583201afbe") + +function(automix_generate_phaselimiter_pins_header) + set(_entries "") + set(_platforms windows-x64 linux-x64 linux-arm64 macos-arm64 macos-x86_64) + set(_var_windows-x64 WINDOWS_X64) + set(_var_linux-x64 LINUX_X64) + set(_var_linux-arm64 LINUX_ARM64) + set(_var_macos-arm64 MACOS_ARM64) + set(_var_macos-x86_64 MACOS_X86_64) + + foreach(_key IN LISTS _platforms) + set(_var_key "${_var_${_key}}") + set(_url "${AUTOMIX_PL_PIN_${_var_key}_URL}") + set(_sha "${AUTOMIX_PL_PIN_${_var_key}_SHA256}") + if(_url AND _sha) + string(APPEND _entries " {\"${_key}\", \"${_url}\", \"${_sha}\"},\n") + endif() + endforeach() + + string(REGEX REPLACE "\n$" "" AUTOMIX_PHASELIMITER_PIN_ROWS "${_entries}") + + configure_file( + "${CMAKE_SOURCE_DIR}/src/renderers/PhaseLimiterPins.h.in" + "${CMAKE_BINARY_DIR}/generated/renderers/PhaseLimiterPins.h" + @ONLY + ) +endfunction() diff --git a/docs/model-licensing-audit.json b/docs/model-licensing-audit.json new file mode 100644 index 0000000..b896526 --- /dev/null +++ b/docs/model-licensing-audit.json @@ -0,0 +1,141 @@ +{ + "schemaVersion": 1, + "models": [ + { + "id": "rysertio/Demucs-onnx", + "license": "MIT", + "commercialUsable": true, + "attributionRequired": false, + "flagged": false, + "notes": "Demucs ONNX port; MIT license." + }, + { + "id": "onnx-community/whisper-tiny.en", + "license": "MIT", + "commercialUsable": true, + "attributionRequired": false, + "flagged": false, + "notes": "onnx-community export of OpenAI Whisper tiny.en; MIT license inherited." + }, + { + "id": "onnx-community/whisper-small.en", + "license": "MIT", + "commercialUsable": true, + "attributionRequired": false, + "flagged": false, + "notes": "onnx-community export of OpenAI Whisper small.en; MIT license inherited." + }, + { + "id": "onnx-community/Speech-Emotion-Classification-ONNX", + "license": "unverified", + "commercialUsable": false, + "attributionRequired": false, + "flagged": true, + "notes": "No license declared on HF card (checked 2026-07-31); wav2vec2-based, base prithivMLmods/Speech-Emotion-Classification. Verify before commercial ship." + }, + { + "id": "onnx-community/Musical-Instrument-Classification-ONNX", + "license": "MIT", + "commercialUsable": true, + "attributionRequired": false, + "flagged": false, + "notes": "HF card declares license:mit (checked 2026-07-31)." + }, + { + "id": "onnx-community/Musical-genres-Classification-Hubert-V1-ONNX", + "license": "unverified", + "commercialUsable": false, + "attributionRequired": false, + "flagged": true, + "notes": "No license declared on HF card (checked 2026-07-31); HuBERT-derived, base SeyedAli/Musical-genres-Classification-Hubert-V1. onnx-community HuBERT re-distributions are typically CC BY-NC 4.0; verify before commercial ship." + }, + { + "id": "openai/whisper-tiny", + "license": "MIT", + "commercialUsable": true, + "attributionRequired": false, + "flagged": false, + "notes": "OpenAI Whisper tiny; MIT license." + }, + { + "id": "laion/clap-htsat-unfused", + "license": "CC BY 4.0", + "commercialUsable": true, + "attributionRequired": true, + "flagged": true, + "notes": "CC BY 4.0 per LAION research repo; HF card metadata currently lists apache-2.0 (checked 2026-07-31). Attribution required; commercially usable with attribution." + }, + { + "id": "pranjal-pravesh/PANNs_CNN14_ONNX", + "license": "MIT", + "commercialUsable": true, + "attributionRequired": false, + "flagged": false, + "notes": "PANNs CNN14 ONNX port; MIT license." + }, + { + "id": "SonyCSLParis/music2latent", + "license": "CC BY-NC 4.0", + "commercialUsable": false, + "attributionRequired": true, + "flagged": true, + "notes": "Non-commercial; user opt-in download only, never bundled in a commercial installer." + }, + { + "id": "StemSplitio/htdemucs-ft-onnx", + "license": "MIT", + "commercialUsable": true, + "attributionRequired": false, + "flagged": false, + "notes": "Demucs 4-stem ONNX port; MIT license." + }, + { + "id": "StemSplitio/htdemucs-6s-onnx", + "license": "MIT", + "commercialUsable": true, + "attributionRequired": false, + "flagged": false, + "notes": "Demucs 6-stem ONNX port; MIT license." + }, + { + "id": "kramp/ito-master-onnx", + "license": "CC BY-NC 4.0", + "commercialUsable": false, + "attributionRequired": true, + "flagged": true, + "assets": [ + "fxencoder.onnx", + "mastering_tcn.onnx", + "config.json" + ], + "attribution": "ITO-Master, Koo et al., Sony Research (github.com/SonyResearch/ITO-Master); ONNX export by kramp (huggingface.co/kramp/ito-master-onnx)", + "notes": "ITO-Master ONNX; CC BY-NC 4.0 confirmed on HF card (checked 2026-07-31). Multi-file pack (fxencoder.onnx + mastering_tcn.onnx + config.json) consumed by the mastering-assistant route. User opt-in download only, never bundled in a commercial installer/redistribution; runtime hub download under the user's license keeps it legal." + }, + { + "id": "xycld/BS-RoFormer-ONNX", + "license": "MIT", + "commercialUsable": true, + "attributionRequired": false, + "flagged": false, + "assets": [ + "bs_roformer_ep317_sdr12.9755_quantized_uint8.onnx", + "bs_roformer_ep317_sdr12.9755.onnx", + "bs_roformer_ep317_sdr12.9755.onnx.data" + ], + "attribution": "BS-RoFormer, Lu et al., arXiv 2309.02612; ONNX export by xycld (huggingface.co/xycld/BS-RoFormer-ONNX)", + "notes": "BS-RoFormer vocal separation ONNX; MIT confirmed on HF card (cardData.license and license:mit tag, checked 2026-09-30). Catalog installs fp32 (~645 MB, external-data sidecar inlined locally at install) where a CUDA session opens, else the single-file uint8-quantized build (~166 MB). Measured on RTX 5060 Ti: fp32/CUDA 85-91 s vs quantized/CPU 687 s for a 196 s track. Graph outputs vocals only; instrumental is the residual mix - vocals. Download-only, never bundled." + }, + { + "id": "MixDirective/open-unmix-umxhq-vocals-onnx", + "license": "MIT", + "commercialUsable": true, + "attributionRequired": false, + "flagged": false, + "assets": [ + "model.onnx" + ], + "attribution": "Open-Unmix UMX-HQ, Stoter, Uhlich, Liutkus, Mitsufuji (c) 2019 Inria; ONNX export by MixDirective (huggingface.co/MixDirective/open-unmix-umxhq-vocals-onnx)", + "notes": "Open-Unmix UMX-HQ vocals ONNX (36 MB, bidirectional LSTM, magnitude STFT in/out); MIT confirmed on HF card and LICENSE file (checked 2026-10-04). Low-memory alternative to BS-RoFormer. Graph outputs vocals only; instrumental is the residual mix - vocals. Download-only, never bundled." + } + ] +} diff --git a/packaging/macos/codesign-bundled-dylibs.sh b/packaging/macos/codesign-bundled-dylibs.sh new file mode 100644 index 0000000..99709eb --- /dev/null +++ b/packaging/macos/codesign-bundled-dylibs.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_BUNDLE="${1:-}" +if [[ -z "$APP_BUNDLE" || ! -d "$APP_BUNDLE" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks" +if [[ -d "$FRAMEWORKS_DIR" ]]; then + echo "Re-signing bundled dylibs in $FRAMEWORKS_DIR..." + find "$FRAMEWORKS_DIR" -type f \( -name "*.dylib" -o -name "*.dylib.*" \) | while read -r dylib; do + echo " Signing $dylib" + codesign --force --sign - --options runtime "$dylib" + done +else + echo "No Frameworks directory found in $APP_BUNDLE; skipping dylib signing." +fi diff --git a/packaging/windows/build-release.ps1 b/packaging/windows/build-release.ps1 new file mode 100644 index 0000000..c8b10c3 --- /dev/null +++ b/packaging/windows/build-release.ps1 @@ -0,0 +1,103 @@ +# Builds the Windows release ZIP of AutoMixMaster. +# +# The package contains the app, its assets and the ONNX Runtime it links +# against. Point -OnnxRuntimeDir at the CUDA build +# (onnxruntime-win-x64-gpu_cuda13-) so GPU acceleration is available; +# users download NVIDIA's CUDA libraries on demand from inside the app, so they +# are never part of the package (the install step fails if they are). +# +# powershell -File packaging\windows\build-release.ps1 ` +# -OnnxRuntimeDir C:\lib\onnxruntime-win-x64-gpu_cuda13-1.30.0 +param( + [string]$OnnxRuntimeDir = "", + [string]$WebGpuPluginDir = "", + [string]$BuildDir = "build-release", + [string]$OutputDir = "", + [string]$Generator = "", + [switch]$SkipTests +) + +$ErrorActionPreference = "Stop" +$repo = Resolve-Path (Join-Path $PSScriptRoot "..\..") +Set-Location $repo + +if ($OutputDir -and -not [System.IO.Path]::IsPathRooted($OutputDir)) { + $OutputDir = [System.IO.Path]::GetFullPath((Join-Path $repo $OutputDir)) +} + +if (-not $WebGpuPluginDir) { + $defaultWebGpu = "C:\lib\webgpu-win-x64\runtimes\win-x64\native" + if (Test-Path $defaultWebGpu) { + $WebGpuPluginDir = $defaultWebGpu + } elseif ($OnnxRuntimeDir) { + Write-Warning "No WebGPU plugin: the package will use CUDA or CPU only." + } +} + +# A fresh directory: a reused cache could carry AUTOMIX_CUDA_RUNTIME_DIR or a +# different ONNX Runtime from a developer build. +if (Test-Path (Join-Path $BuildDir "CMakeCache.txt")) { + throw "$BuildDir already holds a CMake cache; pass a new -BuildDir for a clean release build." +} + +$cmakeExtraArgs = @() +if ($OnnxRuntimeDir) { + $include = Join-Path $OnnxRuntimeDir "include" + $library = Join-Path $OnnxRuntimeDir "lib\onnxruntime.lib" + if (-not (Test-Path $library)) { throw "ONNX Runtime library not found: $library" } + if (-not (Test-Path (Join-Path $OnnxRuntimeDir "lib\onnxruntime_providers_cuda.dll"))) { + Write-Warning "This ONNX Runtime has no CUDA provider; the package will run on CPU only." + } + $cmakeExtraArgs += "-DONNXRUNTIME_INCLUDE_DIR=$include" + $cmakeExtraArgs += "-DONNXRUNTIME_LIBRARY=$library" + if ($WebGpuPluginDir) { + $cmakeExtraArgs += "-DAUTOMIX_WEBGPU_PLUGIN_DIR=$WebGpuPluginDir" + } +} else { + $cmakeExtraArgs += "-DAUTOMIX_FETCH_ORT=ON" + $cmakeExtraArgs += "-DAUTOMIX_ORT_FLAVOR=cuda" + if ($WebGpuPluginDir) { + $cmakeExtraArgs += "-DAUTOMIX_WEBGPU_PLUGIN_DIR=$WebGpuPluginDir" + } +} + +$generatorArgs = @("-A", "x64") +if ($Generator) { + $generatorArgs = @("-G", $Generator, "-A", "x64") +} + +cmake -S . -B $BuildDir @generatorArgs ` + -DENABLE_ONNX=ON ` + @cmakeExtraArgs +if ($LASTEXITCODE -ne 0) { throw "configure failed" } + + +cmake --build $BuildDir --config Release --parallel +if ($LASTEXITCODE -ne 0) { throw "build failed" } + +if (-not $SkipTests) { + ctest --test-dir $BuildDir -C Release --output-on-failure + if ($LASTEXITCODE -ne 0) { throw "tests failed" } +} + +Push-Location $BuildDir +try { + cpack -C Release + if ($LASTEXITCODE -ne 0) { throw "packaging failed" } + Get-ChildItem -Filter "AutoMixMaster-*.zip" | ForEach-Object { + "Package: $($_.FullName) ($([math]::Round($_.Length / 1MB)) MB)" + if ($OutputDir) { + if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null + } + $destZip = Join-Path $OutputDir $_.Name + Copy-Item $_.FullName -Destination $destZip -Force + $hash = (Get-FileHash -Path $destZip -Algorithm SHA256).Hash.ToLower() + "$hash $($_.Name)" | Set-Content "$destZip.sha256" + Write-Host "Staged $destZip with SHA256 $hash" + } + } +} finally { + Pop-Location +} + diff --git a/src/ai/BsRoformerPack.h b/src/ai/BsRoformerPack.h new file mode 100644 index 0000000..9ecda16 --- /dev/null +++ b/src/ai/BsRoformerPack.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +#include "ai/ModelPackLoader.h" + +namespace automix::ai { + +// BS-RoFormer vocal separation (arXiv 2309.02612), ONNX export by xycld. MIT, +// download-only; attribution lives in NOTICE. +inline constexpr const char* kBsRoformerRepoId = "xycld/BS-RoFormer-ONNX"; +// Spec D4: the catalog installs the single-file uint8-quantized build. The repo's +// alphabetically-first .onnx is the fp32 graph, which is unusable without its +// ~640 MB external-data sidecar, so the primary file is pinned by name. +inline constexpr const char* kBsRoformerQuantizedFile = "bs_roformer_ep317_sdr12.9755_quantized_uint8.onnx"; +// The fp32 graph with its weights in ".data". Installed instead of the +// quantized build where a GPU session opens: the quantized build's integer ops +// have no CUDA kernels (ORT inserts ~870 Memcpy nodes), so on CUDA it took +// 303-504 s for a 196 s track, while fp32 on an RTX 5060 Ti took 73-91 s +// (quantized on CPU: 687 s). +// On CPU fp32 is ~45% slower than quantized, so CPU-only machines keep that. +// The installer inlines the sidecar (inlineExternalData) because ONNX Runtime +// cannot load this export with its weights external. +inline constexpr const char* kBsRoformerFp32File = "bs_roformer_ep317_sdr12.9755.onnx"; +// Device memory the fp32 build needs while separating: measured peak 9442 MiB +// above baseline (RTX 5060 Ti, CUDA 13, per-run arena shrinkage), rounded up to +// 10 GiB. Written into the pack manifest as gpu_memory_mb. +inline constexpr std::uint64_t kBsRoformerFp32GpuMemoryMb = 10240; +inline constexpr const char* kBsRoformerIntendedUse = + "Vocal separation (BS-RoFormer). The graph produces vocals only; the instrumental stem is the " + "residual mix - vocals, not a second separation. Quantized build: quality versus fp32 is not " + "yet measured."; +inline constexpr const char* kBsRoformerFp32IntendedUse = + "Vocal separation (BS-RoFormer). The graph produces vocals only; the instrumental stem is the " + "residual mix - vocals, not a second separation. fp32 build, installed for GPU (CUDA) inference."; + +// True for either BS-RoFormer build, judged by the pack's model file name. +inline bool isBsRoformerModelFile(const std::string& modelFile) { + const auto name = std::filesystem::path(modelFile).filename().string(); + return name == kBsRoformerQuantizedFile || name == kBsRoformerFp32File; +} + +// BS-RoFormer is restricted to CUDA until Task 8 measures other EPs. +// Peaks at 9.4 GiB device memory on GPU. +inline const std::vector& bsRoformerGpuProviders() { + static const std::vector providers = {"cuda"}; + return providers; +} + +// Catalog form of the pack's tensor contract (spec section 6). Tensor names are +// omitted because the repo does not publish them; the install-time probe fills +// them in, and until then checkTensorContract() matches positionally. +inline TensorContract bsRoformerCatalogContract() { + TensorContract contract; + contract.engine = "bs_roformer"; + contract.sampleRate = 44100; + contract.stereo = true; + contract.chunkSamples = 352800; + contract.overlapSamples = 88200; + contract.stft.nFft = 2048; + contract.stft.hopLength = 441; + contract.stft.winLength = 2048; + contract.stft.window = "hann"; + contract.stft.periodicWindow = true; + contract.stft.center = true; + contract.stft.padMode = "reflect"; + contract.stft.normalized = false; + contract.stft.zeroDc = true; + contract.inputLayout = "folded_stereo"; + contract.inputs = {{"", {1, 801, 4100}, "float32"}}; + contract.outputs = {{"", {1, 1, 2050, 801, 2}, "float32"}}; + contract.outputMode = "mask"; + contract.targetStem = "vocals"; + contract.stems = {{"vocals", ""}, {"instrumental", "vocals"}}; + return contract; +} + +} // namespace automix::ai diff --git a/src/ai/GitHubReleaseModelHub.cpp b/src/ai/GitHubReleaseModelHub.cpp index e5de8f1..9870fe8 100644 --- a/src/ai/GitHubReleaseModelHub.cpp +++ b/src/ai/GitHubReleaseModelHub.cpp @@ -14,6 +14,7 @@ #include #include "ai/ModelCatalogValidator.h" +#include "ai/ModelStorage.h" #include "util/Sha256.h" #include "util/StringUtils.h" @@ -636,7 +637,7 @@ HubInstallResult GitHubReleaseModelHub::installModel(const std::string& modelId, result.taskScope = compatibility.taskScope; result.revision = info.revision; - const auto destinationRoot = options.destinationRoot.empty() ? std::filesystem::path("assets/modelhub") : options.destinationRoot; + const auto destinationRoot = options.destinationRoot.empty() ? defaultModelHubRoot() : options.destinationRoot; const auto installKey = sanitizePathToken(info.modelId.empty() ? info.repoId : info.modelId); const auto installPath = destinationRoot / installKey; const auto primaryPath = installPath / std::filesystem::path(info.primaryFile).filename(); @@ -700,7 +701,7 @@ HubInstallResult GitHubReleaseModelHub::installModel(const std::string& modelId, writeJson(result.metadataPath, metadata); std::string manifestError; - if (!writeTurnkeyModelPackManifest(installPath, info, result, compatibility, &manifestError)) { + if (!writeTurnkeyModelPackManifest(installPath, info, result, compatibility, nullptr, &manifestError)) { std::filesystem::remove(primaryPath, error); result.message = "Failed writing turnkey model pack metadata: " + manifestError; appendInstallLog(destinationRoot, info, result); diff --git a/src/ai/GpuCapabilityDetector.cpp b/src/ai/GpuCapabilityDetector.cpp index c1930e4..124e2a6 100644 --- a/src/ai/GpuCapabilityDetector.cpp +++ b/src/ai/GpuCapabilityDetector.cpp @@ -239,11 +239,26 @@ bool GpuCapabilityDetector::hasDirectMlSupport() { #endif } +#if defined(__APPLE__) +#include + +static int getMacOsMajorVersion() { + char osversion[64] = {0}; + size_t len = sizeof(osversion) - 1; + if (sysctlbyname("kern.osproductversion", osversion, &len, nullptr, 0) == 0) { + int major = 0; + if (std::sscanf(osversion, "%d", &major) == 1) { + return major; + } + } + return 0; +} +#endif + bool GpuCapabilityDetector::hasCoreMlSupport() { #if defined(__APPLE__) - // CoreML is available on macOS 10.13+ (High Sierra) and later. - // At compile time we assume the deployment target is at least that. - return true; + // CoreML requires macOS >= 12 + return getMacOsMajorVersion() >= 12; #else return false; #endif @@ -251,8 +266,8 @@ bool GpuCapabilityDetector::hasCoreMlSupport() { bool GpuCapabilityDetector::hasAneSupport() { #if defined(__APPLE__) && defined(__arm64__) - // Apple Neural Engine is available on M1 and later. - return true; + // Apple Neural Engine requires Apple Silicon and macOS >= 13 + return getMacOsMajorVersion() >= 13; #else return false; #endif diff --git a/src/ai/GpuMemory.cpp b/src/ai/GpuMemory.cpp new file mode 100644 index 0000000..aa5ded9 --- /dev/null +++ b/src/ai/GpuMemory.cpp @@ -0,0 +1,97 @@ +#include "ai/GpuMemory.h" + +#include + +#include + +#include "ai/GpuRuntimePack.h" +#include "ai/OnnxTensorInference.h" + +namespace automix::ai { +namespace { + +// Headroom on top of a model's need when judging total memory: a Windows +// desktop held 1.6-2.2 GB on the 16 GB card this was measured on. Sized so a +// 12 GB card (reported as just under 12 GiB) still qualifies for a 10 GiB model. +constexpr std::uint64_t kInstallHeadroomBytes = 1536ull * 1024 * 1024; + +using CudaMemGetInfo = int (*)(std::size_t* freeBytes, std::size_t* totalBytes); + +} // namespace + +std::optional queryCudaDeviceMemory() { + GpuRuntimePack::preload(); // a per-user CUDA runtime, if installed +#if defined(_WIN32) + const char* candidates[] = {"cudart64_13.dll", "cudart64_12.dll", "cudart64_110.dll"}; +#elif defined(__APPLE__) + const char* candidates[] = {"libcudart.dylib"}; +#else + const char* candidates[] = {"libcudart.so.13", "libcudart.so.12", "libcudart.so"}; +#endif + for (const auto* name : candidates) { + juce::DynamicLibrary library; + if (!library.open(name)) { + continue; + } + const auto query = reinterpret_cast(library.getFunction("cudaMemGetInfo")); + if (query == nullptr) { + continue; + } + std::size_t freeBytes = 0; + std::size_t totalBytes = 0; + if (query(&freeBytes, &totalBytes) != 0 || totalBytes == 0) { // 0 == cudaSuccess + return std::nullopt; + } + return GpuMemoryInfo{freeBytes, totalBytes}; + } + return std::nullopt; +} + +bool gpuFitsModel(const std::optional& memory, const std::uint64_t requiredBytes) { + return memory.has_value() && memory->totalBytes >= requiredBytes + kInstallHeadroomBytes; +} + +bool gpuHasRoomNow(const std::optional& memory, const std::uint64_t requiredBytes) { + return !memory.has_value() || memory->freeBytes >= requiredBytes; +} + +bool coreMlAutoAllowed(const std::uint64_t physicalMemoryBytes, const bool explicitOptIn) { + return explicitOptIn || physicalMemoryBytes == 0 || physicalMemoryBytes >= kCoreMlAutoMinMemoryBytes; +} + +std::string vocalModelCpuWarning(const std::uint64_t physicalMemoryBytes, const bool gpuSessionUsable, + const bool isMac, const bool lightModelActive) { + if (lightModelActive || !isMac || gpuSessionUsable || physicalMemoryBytes == 0 || physicalMemoryBytes >= kCoreMlAutoMinMemoryBytes) { + return {}; + } + const auto gib = physicalMemoryBytes / (1024ull * 1024 * 1024); + return "This Mac has " + std::to_string(gib) + + " GB of memory and no usable GPU acceleration for the vocal model. It will run on the CPU: expect " + "several minutes per 20-second chunk (hours for a full song), heavy memory swapping, and possible " + "failure. Install the light vocal model instead (Open-Unmix, 36 MB, about 20x faster than real time on " + "a CPU) from the Model Hub, or leave Vocal Model off."; +} + +std::string vocalModelCpuWarning(const bool lightModelActive) { +#if JUCE_MAC + std::string provider; + const bool gpuUsable = gpuTensorSessionAvailable(&provider); + const auto physicalBytes = static_cast(juce::SystemStats::getMemorySizeInMegabytes()) * 1024 * 1024; + return vocalModelCpuWarning(physicalBytes, gpuUsable, true, lightModelActive); +#else + (void)lightModelActive; + return {}; +#endif +} + +bool coreMlAutoAllowed() { +#if JUCE_MAC + const bool optIn = juce::SystemStats::getEnvironmentVariable("AUTOMIX_ENABLE_COREML", "") == "1"; + const auto physicalBytes = static_cast(juce::SystemStats::getMemorySizeInMegabytes()) * 1024 * 1024; + return coreMlAutoAllowed(physicalBytes, optIn); +#else + return true; +#endif +} + +} // namespace automix::ai diff --git a/src/ai/GpuMemory.h b/src/ai/GpuMemory.h new file mode 100644 index 0000000..251c6d2 --- /dev/null +++ b/src/ai/GpuMemory.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +namespace automix::ai { + +struct GpuMemoryInfo { + std::uint64_t freeBytes = 0; + std::uint64_t totalBytes = 0; +}; + +// Free and total memory of CUDA device 0 (the device the CUDA execution +// provider uses), read through the CUDA runtime loaded at run time so nothing +// links against CUDA. Empty when no CUDA runtime is loadable or the query +// fails, e.g. on a machine without an NVIDIA GPU. +std::optional queryCudaDeviceMemory(); + +// Install-time choice: is the device big enough for a model that needs +// `requiredBytes` while it runs? Judged on total memory with headroom for what +// the desktop and other applications typically hold, since free memory at +// install time says little about free memory at separation time. +bool gpuFitsModel(const std::optional& memory, std::uint64_t requiredBytes); + +// Run-time choice: can a model needing `requiredBytes` run on the GPU right +// now without spilling into shared system memory? Unknown memory is treated as +// yes, because there is nothing better to go on and a failed GPU run is +// retried on CPU anyway. +bool gpuHasRoomNow(const std::optional& memory, std::uint64_t requiredBytes); + +// Apple's CoreML and Neural Engine providers are opt-in on Macs with little RAM. +// Measured on an 8 GiB MacBook Neo (BS-RoFormer, 1 run, nothing else running): +// both providers were killed by the OS (SIGKILL) during the first inference, and +// the CPU path did not finish a run in 5 minutes. "auto" therefore skips them +// below kCoreMlAutoMinMemoryBytes unless the user opts in with +// AUTOMIX_ENABLE_COREML=1. Naming the provider explicitly always works. +inline constexpr std::uint64_t kCoreMlAutoMinMemoryBytes = 12ull * 1024 * 1024 * 1024; + +// Pure policy: may "auto" try CoreML/ANE on a machine with this much RAM? +// Unknown memory (0) is allowed, because nothing better is known. +bool coreMlAutoAllowed(std::uint64_t physicalMemoryBytes, bool explicitOptIn); + +// Warning for the Vocal Model toggle: empty unless the model would run on the CPU of a +// low-memory Mac (below kCoreMlAutoMinMemoryBytes, no GPU session usable). The 8 GiB +// MacBook Neo needed over 5 minutes per chunk on the CPU against about 25 s on a desktop. +// Empty too when the light vocal model (Open-Unmix) is already the active separation pack. +std::string vocalModelCpuWarning(std::uint64_t physicalMemoryBytes, bool gpuSessionUsable, bool isMac, + bool lightModelActive); + +// Live version for this machine; probes for a usable GPU session. +std::string vocalModelCpuWarning(bool lightModelActive); + +// Live policy for this machine. Always true off macOS, where CoreML does not exist. +bool coreMlAutoAllowed(); + +} // namespace automix::ai diff --git a/src/ai/GpuProvider.h b/src/ai/GpuProvider.h index b07e422..fda1a87 100644 --- a/src/ai/GpuProvider.h +++ b/src/ai/GpuProvider.h @@ -1,8 +1,12 @@ #pragma once #include +#include +#include +#include #include #include +#include #include namespace automix::ai::gpu { @@ -11,6 +15,7 @@ inline constexpr const char* kProviderCpu = "cpu"; inline constexpr const char* kProviderAne = "ane"; inline constexpr const char* kProviderCoreMl = "coreml"; inline constexpr const char* kProviderCuda = "cuda"; +inline constexpr const char* kProviderWebGpu = "webgpu"; inline constexpr const char* kProviderOpenVino = "openvino"; inline constexpr const char* kProviderDirectMl = "directml"; @@ -19,6 +24,7 @@ inline const std::vector& providerPriorityChain() { kProviderAne, kProviderCoreMl, kProviderCuda, + kProviderWebGpu, kProviderOpenVino, kProviderDirectMl, kProviderCpu, @@ -36,6 +42,8 @@ inline std::string canonicalProviderName(const std::string& raw) { return kProviderAne; if (lower.find("coreml") != std::string::npos) return kProviderCoreMl; if (lower.find("cuda") != std::string::npos) return kProviderCuda; + if (lower.find("webgpu") != std::string::npos || lower.find("wgpu") != std::string::npos) + return kProviderWebGpu; if (lower.find("openvino") != std::string::npos || lower.find("vino") != std::string::npos) return kProviderOpenVino; if (lower.find("dml") != std::string::npos || lower.find("directml") != std::string::npos) @@ -46,13 +54,32 @@ inline std::string canonicalProviderName(const std::string& raw) { return lower; } +/// True when a bench run that asked for `requested` actually ran on `actual`. +/// "auto" matches anything; an empty or "unknown" actual provider never matches a concrete request. +inline bool benchProviderMatches(const std::string& requested, const std::string& actual) { + const auto wanted = canonicalProviderName(requested); + if (wanted == "auto") return true; + if (actual.empty() || actual == "unknown") return false; + return wanted == canonicalProviderName(actual); +} + +/// Nearest-rank percentile (p in (0, 1]); returns 0.0 for an empty input. +inline double nearestRankPercentile(std::vector values, double p) { + if (values.empty()) return 0.0; + std::sort(values.begin(), values.end()); + const auto n = static_cast(values.size()); + auto idx = static_cast(std::ceil(p * static_cast(n))) - 1; + idx = std::clamp(idx, 0, n - 1); + return values[static_cast(idx)]; +} + inline std::string platformPreferredProvider() { #if defined(__APPLE__) && defined(__arm64__) return kProviderAne; #elif defined(__APPLE__) return kProviderCoreMl; #elif defined(_WIN32) - return kProviderDirectMl; + return kProviderWebGpu; #else return kProviderCuda; #endif @@ -73,6 +100,123 @@ inline int providerPriority(const std::string& provider) { return static_cast(chain.size()); } +struct SessionTuning { + std::string hardwareTier = "standard"; + int intraOpThreads = 0; + int interOpThreads = 0; + bool memPattern = true; + bool cpuArena = true; + bool sequentialExecution = false; +}; + +inline SessionTuning sessionTuning(const std::string& provider, const int hardwareThreads) { + SessionTuning tuning; + const auto normalized = canonicalProviderName(provider); + const int clampedThreads = std::max(1, hardwareThreads); + if (clampedThreads <= 4) { + tuning.hardwareTier = "low"; + } else if (clampedThreads >= 12) { + tuning.hardwareTier = "high"; + } + + if (normalized == kProviderCuda) { + tuning.intraOpThreads = std::clamp(clampedThreads / 2, 1, 8); + tuning.interOpThreads = 1; + tuning.memPattern = false; + tuning.cpuArena = true; + tuning.sequentialExecution = false; + return tuning; + } + + if (normalized == kProviderDirectMl) { + tuning.intraOpThreads = std::clamp(clampedThreads / 2, 1, 4); + tuning.interOpThreads = 1; + tuning.memPattern = false; + tuning.cpuArena = false; + tuning.sequentialExecution = true; + return tuning; + } + + if (normalized == kProviderCoreMl || normalized == kProviderAne) { + tuning.intraOpThreads = std::clamp(clampedThreads / 2, 1, 4); + tuning.interOpThreads = 1; + tuning.memPattern = false; + tuning.cpuArena = false; + tuning.sequentialExecution = true; + return tuning; + } + + tuning.intraOpThreads = std::clamp(clampedThreads, 1, 16); + tuning.interOpThreads = std::clamp(clampedThreads / 2, 1, 8); + tuning.memPattern = true; + tuning.cpuArena = true; + tuning.sequentialExecution = false; + return tuning; +} + +// Exactly which SessionOptions calls configureSessionForProvider() makes. An empty +// optional means "leave ONNX Runtime's default". Pure: unit-testable without ORT. +struct SessionConfigPlan { + std::optional intraOpThreads; + std::optional interOpThreads; + std::optional sequentialExecution; + std::optional memPattern; + std::optional cpuArena; +}; + +// hardwareThreads > 0: full tuning (model path). <= 0: only what the provider +// requires to open a session at all - today that is DirectML's memory-pattern-off +// and sequential execution - so tensor sessions keep ORT's own thread defaults. +inline SessionConfigPlan sessionConfigPlan(const std::string& provider, int hardwareThreads) { + SessionConfigPlan plan; + const auto canonical = canonicalProviderName(provider); + if (hardwareThreads <= 0) { + if (canonical == kProviderDirectMl) { + plan.memPattern = false; + plan.sequentialExecution = true; + } + return plan; + } + const auto tuning = sessionTuning(canonical, hardwareThreads); + plan.intraOpThreads = std::max(1, tuning.intraOpThreads); + plan.interOpThreads = std::max(1, tuning.interOpThreads); + plan.sequentialExecution = tuning.sequentialExecution; + plan.memPattern = tuning.memPattern; + plan.cpuArena = tuning.cpuArena; + return plan; +} + +// Option maps for execution providers. +// Valid CoreML options per coreml_options.cc@v1.30.0:55-64: +// MLComputeUnits, ModelFormat, RequireStaticInputShapes, EnableOnSubgraphs, +// SpecializationStrategy, ProfileComputePlan, AllowLowPrecisionAccumulationOnGPU, ModelCacheDirectory +inline std::unordered_map providerOptionMap(const std::string& rawProvider) { + std::unordered_map options; + const auto canonical = canonicalProviderName(rawProvider); + if (canonical == kProviderDirectMl) { + options["device_id"] = "0"; + return options; + } + if (canonical == kProviderCoreMl) { + options["ModelFormat"] = "MLProgram"; + options["MLComputeUnits"] = "ALL"; + return options; + } + if (canonical == kProviderAne) { + // Apple Neural Engine via CoreML with Neural Engine compute units. + // (Note: older docs suggested a unit-count key which ORT 1.30 rejects with 'Unknown option') + options["ModelFormat"] = "MLProgram"; + options["MLComputeUnits"] = "CPUAndNeuralEngine"; + return options; + } + if (canonical == kProviderOpenVino) { + // OpenVINO provider for Intel NPU / GPU: device_type=CPU_FP32 is a CPU device, and OpenVINO is not shipped + options["device_type"] = "CPU_FP32"; + return options; + } + return options; +} + // --- Optional ONNX Runtime capabilities ------------------------------------- // The two optional runtime paths -- a CUDA provider supplied by a plugin // library, and a per-GPU compiled-model cache -- are guarded at compile time by @@ -157,6 +301,23 @@ inline PluginEpDecision decidePluginEpAttempt(bool compiledIn, return decision; } +inline std::string pluginLibraryFileName(const std::string& platform) { + auto lower = platform; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (lower.find("darwin") != std::string::npos || lower.find("macos") != std::string::npos || + lower.find("osx") != std::string::npos) { + return {}; + } + if (lower.find("win") != std::string::npos) { + return "onnxruntime_providers_webgpu.dll"; + } + if (lower.find("linux") != std::string::npos || lower.find("ubuntu") != std::string::npos) { + return "libonnxruntime_providers_webgpu.so"; + } + return {}; +} + inline bool isSha256Hex64(const std::string& text) { if (text.size() != 64) return false; for (const char c : text) { diff --git a/src/ai/GpuRuntimePack.cpp b/src/ai/GpuRuntimePack.cpp new file mode 100644 index 0000000..1f672db --- /dev/null +++ b/src/ai/GpuRuntimePack.cpp @@ -0,0 +1,409 @@ +#include "ai/GpuRuntimePack.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "util/Sha256.h" + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif + +namespace automix::ai::GpuRuntimePack { +namespace { + +constexpr const char* kMarkerFile = "runtime.json"; +constexpr const char* kPendingRemovalFile = "remove-pending"; +constexpr std::uint32_t kNvidiaVendorId = 0x10DE; + +std::filesystem::path binDirectory(const std::filesystem::path& root) { return root / "bin"; } + +bool endsWithDll(std::string name) { + std::transform(name.begin(), name.end(), name.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return name.size() > 4 && name.compare(name.size() - 4, 4, ".dll") == 0; +} + +std::optional> markerLibraries(const std::filesystem::path& root) { + try { + std::ifstream in(root / kMarkerFile); + if (!in.is_open()) { + return std::nullopt; + } + const auto marker = nlohmann::json::parse(in); + if (marker.value("version", "") != version() || !marker.contains("libraries")) { + return std::nullopt; + } + return marker.at("libraries").get>(); + } catch (...) { + return std::nullopt; + } +} + +// cudart first: everything else depends on it. The rest load with their own +// directory searched for dependencies, so their order does not matter. +int loadRank(const std::string& library) { return library.rfind("cudart", 0) == 0 ? 0 : 1; } + +} // namespace + +std::string version() { return "cuda13.4-cudnn9.27-1"; } + +const std::vector& archives() { +#if defined(_WIN32) + // NVIDIA redistributables (CUDA EULA / cuDNN SLA), as published by NVIDIA on + // PyPI. Pinned: a changed file fails its hash instead of being installed. + static const std::vector list = { + {"nvidia_cuda_runtime-13.4.92-py3-none-win_amd64.whl", + "https://files.pythonhosted.org/packages/86/00/d5436004268f049214193659ebc36550b5ef3925c3d13b4cc980e13be6f5/" + "nvidia_cuda_runtime-13.4.92-py3-none-win_amd64.whl", + "08dca5e4aba480c2fd5b55075c0fa71b84ef9dcf0521f2d58baa14a803a7311c", 2778543}, + {"nvidia_cublas-13.8.0.4-py3-none-win_amd64.whl", + "https://files.pythonhosted.org/packages/a3/df/f1246959833e2c437db8be3e5b477f66b87f8817821ed40de6c7561c9a36/" + "nvidia_cublas-13.8.0.4-py3-none-win_amd64.whl", + "8c5494423bb8a46822cb6b0cb95d7fa4be2d7b96a31155dff083839ec8297910", 423266897}, + {"nvidia_cufft-12.4.0.43-py3-none-win_amd64.whl", + "https://files.pythonhosted.org/packages/7b/cc/be7fe31058127336a66c88414a2ecc6beecf680baf85994a43eaf292b202/" + "nvidia_cufft-12.4.0.43-py3-none-win_amd64.whl", + "4ff7075f2d0b5f69291f70938d37a86ec632cbe5747184c74ba1f50f17accacc", 160953139}, + {"nvidia_cudnn_cu13-9.27.0.42-py3-none-win_amd64.whl", + "https://files.pythonhosted.org/packages/87/6a/e55ff0ac26a5c6e2b21f41c9d04ad096b4ed6da593fba7e25845c61b0532/" + "nvidia_cudnn_cu13-9.27.0.42-py3-none-win_amd64.whl", + "7d96f634adafd55c72231eb0500ca77ab109ec8ebff7b33000b76e081bc4558e", 436469905}, + }; +#else + static const std::vector list; // Windows only for now +#endif + return list; +} + +std::uint64_t downloadBytes() { + std::uint64_t total = 0; + for (const auto& archive : archives()) { + total += archive.bytes; + } + return total; +} + +std::filesystem::path defaultRoot() { +#if defined(_WIN32) + const auto base = juce::File::getSpecialLocation(juce::File::windowsLocalAppData); +#else + const auto base = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory); +#endif + return std::filesystem::path(base.getFullPathName().toWideCharPointer()) / "AutoMixMaster" / "gpu-runtime" / + version(); +} + +bool isInstalled(const std::filesystem::path& root) { + const auto libraries = markerLibraries(root); + if (!libraries.has_value() || libraries->empty()) { + return false; + } + std::error_code error; + for (const auto& library : *libraries) { + if (!std::filesystem::is_regular_file(binDirectory(root) / library, error) || error) { + return false; + } + } + return true; +} + +Fetcher httpFetcher() { + return [](const std::string& url, const std::filesystem::path& destination, + const std::function& progress) -> std::string { + int statusCode = 0; + const auto options = juce::URL::InputStreamOptions(juce::URL::ParameterHandling::inAddress) + .withConnectionTimeoutMs(60000) + .withNumRedirectsToFollow(8) + .withStatusCode(&statusCode); + const auto input = juce::URL(url).createInputStream(options); + if (input == nullptr) { + return "could not connect to " + url; + } + if (statusCode >= 400) { + return "HTTP " + std::to_string(statusCode) + " from " + url; + } + std::ofstream out(destination, std::ios::binary | std::ios::trunc); + if (!out.is_open()) { + return "cannot write " + destination.string(); + } + std::vector buffer(1 << 20); + std::uint64_t written = 0; + for (;;) { + const auto read = input->read(buffer.data(), static_cast(buffer.size())); + if (read <= 0) { + break; + } + out.write(buffer.data(), read); + if (!out) { + return "write failed for " + destination.string(); + } + written += static_cast(read); + if (progress && !progress(written)) { + return "cancelled"; + } + } + return {}; + }; +} + +std::string extractLibraries(const std::filesystem::path& wheel, + const std::filesystem::path& binDir, + std::vector& extracted) { + juce::ZipFile zip(juce::File(juce::String(wheel.wstring().c_str()))); + if (zip.getNumEntries() == 0) { + return "'" + wheel.filename().string() + "' is not a readable archive"; + } + std::error_code error; + std::filesystem::create_directories(binDir, error); + for (int i = 0; i < zip.getNumEntries(); ++i) { + const auto* entry = zip.getEntry(i); + const auto name = entry->filename.replaceCharacter('\\', '/').toStdString(); + const std::filesystem::path entryPath(name); + for (const auto& part : entryPath) { + if (part == "..") { + return "archive entry '" + name + "' escapes the archive"; + } + } + if (entryPath.is_absolute() || entryPath.has_root_name() || !endsWithDll(name)) { + continue; + } + const auto fileName = entryPath.filename(); + const auto& unused = unusedLibraries(); + if (std::find(unused.begin(), unused.end(), fileName.string()) != unused.end()) { + continue; + } + std::unique_ptr in(zip.createStreamForEntry(i)); + if (in == nullptr) { + return "cannot read '" + name + "' from " + wheel.filename().string(); + } + std::ofstream out(binDir / fileName, std::ios::binary | std::ios::trunc); + std::vector buffer(1 << 20); + for (;;) { + const auto read = in->read(buffer.data(), static_cast(buffer.size())); + if (read <= 0) { + break; + } + out.write(buffer.data(), read); + } + if (!out) { + return "cannot write " + (binDir / fileName).string(); + } + extracted.push_back(fileName.string()); + } + return {}; +} + +InstallResult install(const std::filesystem::path& root, + const std::function& progress, + const Fetcher& fetch) { + InstallResult result; + if (archives().empty()) { + result.message = "The GPU runtime pack is not available on this platform."; + return result; + } + std::error_code error; + const auto downloads = root / "downloads"; + std::filesystem::create_directories(downloads, error); + std::filesystem::remove(root / kMarkerFile, error); // invalid until this install completes + + const auto total = downloadBytes(); + std::uint64_t done = 0; + std::vector libraries; + for (const auto& archive : archives()) { + const auto wheel = downloads / archive.name; + const bool alreadyHere = std::filesystem::is_regular_file(wheel, error) && util::fileSha256(wheel) == archive.sha256; + if (!alreadyHere) { + const auto base = done; + const auto failure = fetch(archive.url, wheel, [&](std::uint64_t bytes) { + return !progress || progress(base + bytes, total); + }); + if (failure == "cancelled") { + result.cancelled = true; + result.message = "GPU runtime download cancelled."; + return result; + } + if (!failure.empty()) { + result.message = "Downloading " + archive.name + " failed: " + failure; + return result; + } + if (const auto actual = util::fileSha256(wheel); actual != archive.sha256) { + std::filesystem::remove(wheel, error); + result.message = "SHA-256 mismatch for " + archive.name + " (expected " + archive.sha256 + ", got " + + (actual.empty() ? "unreadable" : actual) + "); the download was discarded."; + return result; + } + } + done += archive.bytes; + if (progress && !progress(done, total)) { + result.cancelled = true; + result.message = "GPU runtime download cancelled."; + return result; + } + if (const auto failure = extractLibraries(wheel, binDirectory(root), libraries); !failure.empty()) { + result.message = "Unpacking " + archive.name + " failed: " + failure; + return result; + } + std::filesystem::remove(wheel, error); + } + std::filesystem::remove(downloads, error); + + std::sort(libraries.begin(), libraries.end()); + libraries.erase(std::unique(libraries.begin(), libraries.end()), libraries.end()); + const nlohmann::json marker = {{"version", version()}, {"libraries", libraries}}; + { + std::ofstream out(root / kMarkerFile, std::ios::trunc); + out << marker.dump(2); + if (!out) { + result.message = "Cannot write the GPU runtime marker in " + root.string(); + return result; + } + } + result.success = true; + result.message = "GPU runtime installed (" + std::to_string(libraries.size()) + " libraries)."; + return result; +} + +const std::vector& unusedLibraries() { + static const std::vector list = {"cufftw64_12.dll", "nvblas64_13.dll"}; + return list; +} + +namespace { + +// Only ever delete something that is recognisably ours: the versioned folder +// holding a bin/ directory, a marker or a pending-removal note. +bool looksLikePack(const std::filesystem::path& root) { + std::error_code error; + return root.filename() == version() && + (std::filesystem::is_directory(root / "bin", error) || std::filesystem::exists(root / kMarkerFile, error) || + std::filesystem::exists(root / kPendingRemovalFile, error)); +} + +} // namespace + +RemoveResult uninstall(const std::filesystem::path& root) { + RemoveResult result; + std::error_code error; + if (!std::filesystem::exists(root, error)) { + result.removedNow = true; + result.message = "The GPU runtime is not installed."; + return result; + } + if (!looksLikePack(root)) { + result.message = "Refusing to delete '" + root.string() + "': it does not look like a GPU runtime pack."; + return result; + } + std::filesystem::remove(root / kMarkerFile, error); // first: nothing may preload a half-deleted pack + std::filesystem::remove_all(root, error); + if (!error && !std::filesystem::exists(root, error)) { + result.removedNow = true; + result.message = "GPU runtime removed."; + return result; + } + // Libraries loaded by this process are locked on Windows. + std::ofstream(root / kPendingRemovalFile) << "remove at next start\n"; + result.message = "GPU runtime disabled; its remaining files are removed the next time AutoMixMaster starts."; + return result; +} + +void completePendingRemoval(const std::filesystem::path& root) { + std::error_code error; + if (std::filesystem::exists(root / kPendingRemovalFile, error) && looksLikePack(root)) { + std::filesystem::remove_all(root, error); + } +} +bool preload(const std::filesystem::path& root) { +#if defined(_WIN32) + static std::mutex mutex; + static bool loaded = false; + const std::scoped_lock lock(mutex); + if (loaded) { + return true; + } + if (!isInstalled(root)) { + return false; + } + auto libraries = *markerLibraries(root); + std::stable_sort(libraries.begin(), libraries.end(), + [](const std::string& a, const std::string& b) { return loadRank(a) < loadRank(b); }); + for (const auto& library : libraries) { + // With an absolute path, LOAD_WITH_ALTERED_SEARCH_PATH resolves the + // library's own dependencies from its directory first. Modules stay + // loaded for the life of the process, which is what the provider needs. + const auto path = binDirectory(root) / library; + if (LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH) == nullptr && + library.rfind("cudart", 0) == 0) { + return false; + } + } + loaded = true; + return true; +#else + (void)root; + return false; +#endif +} + +std::pair nvidiaDriverVersion(const std::uint64_t userModeDriverVersion) { + const auto c = static_cast((userModeDriverVersion >> 16) & 0xFFFF); + const auto d = static_cast(userModeDriverVersion & 0xFFFF); + const int combined = (c % 10) * 10000 + d; // the last five digits + return {combined / 100, combined % 100}; +} + +std::optional largestNvidiaAdapter() { +#if defined(_WIN32) + IDXGIFactory1* factory = nullptr; + if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), reinterpret_cast(&factory))) || factory == nullptr) { + return std::nullopt; + } + std::optional largest; + IDXGIAdapter1* adapter = nullptr; + for (UINT index = 0; factory->EnumAdapters1(index, &adapter) != DXGI_ERROR_NOT_FOUND; ++index) { + DXGI_ADAPTER_DESC1 desc{}; + if (SUCCEEDED(adapter->GetDesc1(&desc)) && desc.VendorId == kNvidiaVendorId && + (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) == 0) { + NvidiaAdapter found; + found.dedicatedBytes = static_cast(desc.DedicatedVideoMemory); + LARGE_INTEGER userModeVersion{}; + if (SUCCEEDED(adapter->CheckInterfaceSupport(__uuidof(IDXGIDevice), &userModeVersion))) { + const auto [major, minor] = nvidiaDriverVersion(static_cast(userModeVersion.QuadPart)); + found.driverMajor = major; + found.driverMinor = minor; + } + if (!largest.has_value() || found.dedicatedBytes > largest->dedicatedBytes) { + largest = found; + } + } + adapter->Release(); + } + factory->Release(); + return largest; +#else + return std::nullopt; +#endif +} + +bool shouldOffer(const std::uint64_t minimumDedicatedBytes, const std::filesystem::path& root) { + if (archives().empty() || isInstalled(root)) { + return false; + } + const auto adapter = largestNvidiaAdapter(); + // An unreported driver version is not proof of an old one, but 1 GB is too + // much to download on a guess: only offer what is known to work. + return adapter.has_value() && adapter->dedicatedBytes >= minimumDedicatedBytes && + adapter->driverMajor.value_or(0) >= kMinimumDriverMajor; +} +} // namespace automix::ai::GpuRuntimePack diff --git a/src/ai/GpuRuntimePack.h b/src/ai/GpuRuntimePack.h new file mode 100644 index 0000000..122a166 --- /dev/null +++ b/src/ai/GpuRuntimePack.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace automix::ai { + +// The NVIDIA CUDA libraries the CUDA execution provider needs (runtime, +// cuBLAS, cuFFT, cuDNN), installed on demand per user instead of shipping +// ~1.3 GB with every copy of the application. The files come from NVIDIA's own +// redistributable wheels on PyPI, pinned by URL and SHA-256; only their DLLs +// are kept. Nothing is installed system-wide and no PATH is changed: the DLLs +// are preloaded by full path before any CUDA session is created. +namespace GpuRuntimePack { + +struct Archive { + std::string name; // wheel file name + std::string url; // files.pythonhosted.org, pinned + std::string sha256; // lower-case hex + std::uint64_t bytes; // download size +}; + +// Pack identity, written into the completion marker; bump it whenever the +// archive list changes so an older pack is reinstalled rather than trusted. +std::string version(); +const std::vector& archives(); +std::uint64_t downloadBytes(); + +// %LOCALAPPDATA%\AutoMixMaster\gpu-runtime\ on Windows; the user +// application-data directory elsewhere. +std::filesystem::path defaultRoot(); + +// The marker matches this version and every library it lists is present. +bool isInstalled(const std::filesystem::path& root); + +// Fetches `url` into `destination`. `progress(bytesSoFar)` returns false to +// abort. Returns an empty string on success, else the reason. +using Fetcher = std::function& progress)>; +Fetcher httpFetcher(); + +struct InstallResult { + bool success = false; + bool cancelled = false; + std::string message; +}; + +// Downloads, verifies and unpacks every archive into `root`. `progress` gets +// (bytes done, bytes total) and returns false to cancel. The completion marker +// is written last, so an interrupted or failed install is never mistaken for a +// working one; archives are deleted once unpacked. +InstallResult install(const std::filesystem::path& root, + const std::function& progress, + const Fetcher& fetch = httpFetcher()); + +// Copies the .dll entries of a wheel (a zip) into `binDirectory`, flattening +// their paths; anything else is ignored. An entry whose name would escape the +// archive is an error. Exposed for tests. +std::string extractLibraries(const std::filesystem::path& wheel, + const std::filesystem::path& binDirectory, + std::vector& extracted); + +// Libraries in the wheels that ONNX Runtime never loads (an FFTW-compatible +// shim and a CPU-BLAS interposer); skipped when unpacking. Everything else is +// kept: one model loading only cuBLAS and three cuDNN parts does not make the +// rest unused (convolution, FFT and RNN graphs load others). +const std::vector& unusedLibraries(); + +struct RemoveResult { + bool removedNow = false; // false: finishes at next start (files were in use) + std::string message; +}; + +// Removes an installed pack. Libraries this process has loaded cannot be +// deleted on Windows, so the marker goes first (nothing preloads it again) and +// whatever remains is deleted by completePendingRemoval() at the next start. +// Refuses any directory that does not look like a GPU runtime pack. +RemoveResult uninstall(const std::filesystem::path& root = defaultRoot()); + +// Call once at startup, before anything can preload the pack. +void completePendingRemoval(const std::filesystem::path& root = defaultRoot()); + +// Loads the installed libraries by full path so the CUDA execution provider's +// later lookups by name resolve to them. Idempotent; true once loaded. Returns +// false (and changes nothing) when no installed pack is found. +bool preload(const std::filesystem::path& root = defaultRoot()); + +// Whether to offer the pack: an NVIDIA adapter with at least +// `minimumDedicatedBytes` of dedicated memory and a CUDA 13 capable driver +// (>= kMinimumDriverMajor) is present - both read through DXGI, so no CUDA is +// needed to decide - and the pack is not installed yet. +bool shouldOffer(std::uint64_t minimumDedicatedBytes, const std::filesystem::path& root = defaultRoot()); + +// CUDA 13 requires an NVIDIA driver of this major version or newer. +inline constexpr int kMinimumDriverMajor = 580; + +struct NvidiaAdapter { + std::uint64_t dedicatedBytes = 0; + // NVIDIA's own numbering (e.g. 610.74); empty when Windows did not report it. + std::optional driverMajor; + std::optional driverMinor; +}; + +// NVIDIA driver version from the Windows user-mode driver version DXGI +// reports (a.b.c.d). NVIDIA's number is the last five digits of c and d: +// 32.0.16.1074 -> 610.74. +std::pair nvidiaDriverVersion(std::uint64_t userModeDriverVersion); + +// The NVIDIA adapter with the most dedicated memory, if any (Windows only). +std::optional largestNvidiaAdapter(); + +} // namespace GpuRuntimePack + +} // namespace automix::ai diff --git a/src/ai/HuggingFaceModelHub.cpp b/src/ai/HuggingFaceModelHub.cpp index 51c0a1f..9bd4d07 100644 --- a/src/ai/HuggingFaceModelHub.cpp +++ b/src/ai/HuggingFaceModelHub.cpp @@ -13,8 +13,14 @@ #include #include +#include "ai/BsRoformerPack.h" +#include "ai/UmxPack.h" +#include "ai/GpuMemory.h" #include "ai/ItoMasterAdapter.h" #include "ai/ModelCatalogValidator.h" +#include "ai/ModelStorage.h" +#include "ai/OnnxExternalData.h" +#include "ai/OnnxTensorInference.h" #include "util/Sha256.h" #include "util/StringUtils.h" @@ -453,6 +459,78 @@ void appendInstallLog(const std::filesystem::path& root, out << event.dump() << "\n"; } +bool bsRoformerGpuBuildQualifies() { + // Probes whether CUDA tensor inference is usable on this machine, and + // checks whether device memory is sufficient for the ~10 GiB fp32 model. + return tensorProviderUsable("cuda") && + gpuFitsModel(queryCudaDeviceMemory(), kBsRoformerFp32GpuMemoryMb * 1024 * 1024); +} + +std::optional upgradeBsRoformerForGpu(const std::filesystem::path& destinationRoot) { + std::error_code error; + if (!std::filesystem::is_directory(destinationRoot, error) || error) { + return std::nullopt; + } + for (const auto& entry : std::filesystem::directory_iterator(destinationRoot, error)) { + const auto hubMetadataPath = entry.path() / "modelhub.json"; + if (!entry.is_directory(error) || !std::filesystem::is_regular_file(hubMetadataPath, error)) { + continue; + } + nlohmann::json hubMetadata; + nlohmann::json manifest; + try { + std::ifstream hubIn(hubMetadataPath); + hubMetadata = nlohmann::json::parse(hubIn); + std::ifstream manifestIn(entry.path() / "model.json"); + manifest = nlohmann::json::parse(manifestIn); + } catch (...) { + continue; + } + if (hubMetadata.value("repoId", "") != kBsRoformerRepoId || + manifest.value("model_file", "") != kBsRoformerQuantizedFile) { + continue; + } + if (!bsRoformerGpuBuildQualifies()) { + return std::nullopt; + } + HubInstallOptions options; + options.destinationRoot = destinationRoot; + options.overwrite = true; + options.downloadReadme = false; + auto result = HuggingFaceModelHub().installModel(kBsRoformerRepoId, options); + if (result.success && result.primaryFilePath.filename() != kBsRoformerQuantizedFile) { + // The pack keeps its id and directory; only the superseded CPU build goes. + std::filesystem::remove(entry.path() / kBsRoformerQuantizedFile, error); + result.message = "Vocal model switched to its GPU build."; + } + return result; + } + return std::nullopt; +} + +std::string primaryFileForRepo(const std::string& repoId, + const std::vector& files, + bool* hasOnnxOut, + const bool preferGpuBuild) { + auto primary = pickPrimaryFile(files, hasOnnxOut); + if (repoId == kBsRoformerRepoId) { + // Pinned by name (see kBsRoformerQuantizedFile / kBsRoformerFp32File). The + // fp32 graph is only chosen together with its sidecar; without both, and + // without the quantized file, the entry becomes undiscoverable instead of + // installing an unloadable graph. + const auto has = [&files](const std::string& name) { + return std::find(files.begin(), files.end(), name) != files.end(); + }; + const std::string fp32 = kBsRoformerFp32File; + if (preferGpuBuild && has(fp32) && has(fp32 + ".data")) { + primary = fp32; + } else { + primary = has(kBsRoformerQuantizedFile) ? kBsRoformerQuantizedFile : ""; + } + } + return primary; +} + std::vector curatedModelIds() { return { "rysertio/Demucs-onnx", @@ -468,16 +546,33 @@ std::vector curatedModelIds() { "StemSplitio/htdemucs-ft-onnx", "StemSplitio/htdemucs-6s-onnx", "kramp/ito-master-onnx", + "xycld/BS-RoFormer-ONNX", // == kBsRoformerRepoId; a literal because licensing tests parse this list + "MixDirective/open-unmix-umxhq-vocals-onnx", // == kUmxVocalsRepoId; literal for the same reason }; } +std::filesystem::path localAssetPath(const std::filesystem::path& installPath, const std::string& repoPath) { + const auto name = std::filesystem::path(repoPath).filename(); + if (name.empty() || name == "." || name == "..") { + return installPath; + } + return installPath / name; +} + // Artifacts that must accompany the primary model file to form a complete pack // (the ITO-Master mastering route consumes all three as one model pack). -std::vector auxiliaryAssetsForRepo(const std::string& repoId) { +std::vector auxiliaryAssetsFor(const std::string& repoId, + const std::string& primaryFile, + const std::vector& files) { + std::vector assets; if (repoId == kItoMasterRepoId) { - return {kItoMasterPredictorFile, kItoMasterConfigFile}; + assets = {kItoMasterPredictorFile, kItoMasterConfigFile}; + } + const auto sidecar = primaryFile + ".data"; + if (!primaryFile.empty() && std::find(files.begin(), files.end(), sidecar) != files.end()) { + assets.push_back(sidecar); } - return {}; + return assets; } std::vector HuggingFaceModelHub::defaultRecommendedSearchTerms() { @@ -518,6 +613,13 @@ std::string HuggingFaceModelHub::resolveToken(const std::string& explicitToken) return ""; } +void applyCuratedPin(HubModelInfo& info) { + if (info.repoId == kUmxVocalsRepoId) { + info.revision = kUmxVocalsRevision; + info.fileSha256[kUmxVocalsFile] = kUmxVocalsSha256; + } +} + std::optional HuggingFaceModelHub::modelInfo(const std::string& modelIdOrRepoId, const std::string& token) const { auto repoId = trim(modelIdOrRepoId); @@ -586,7 +688,11 @@ std::optional HuggingFaceModelHub::modelInfo(const std::string& mo } } - info.primaryFile = pickPrimaryFile(info.files, &info.hasOnnx); + applyCuratedPin(info); + + // Only repos with a GPU variant pay for the probe. + const bool preferGpuBuild = info.repoId == kBsRoformerRepoId && bsRoformerGpuBuildQualifies(); + info.primaryFile = primaryFileForRepo(info.repoId, info.files, &info.hasOnnx, preferGpuBuild); info.useCase = HuggingFaceModelHub::inferUseCase(info.repoId, info.tags, ""); const auto compatibility = validateCatalogModel(info); info.compatible = compatibility.compatible; @@ -751,10 +857,10 @@ HubInstallResult HuggingFaceModelHub::installModel(const std::string& modelIdOrR } result.taskScope = compatibility.taskScope; - const auto destinationRoot = options.destinationRoot.empty() ? std::filesystem::path("assets/modelhub") : options.destinationRoot; + const auto destinationRoot = options.destinationRoot.empty() ? defaultModelHubRoot() : options.destinationRoot; const auto installKey = sanitizeRepoId(info->modelId.empty() ? info->repoId : info->modelId); const auto installPath = destinationRoot / installKey; - const auto primaryPath = installPath / std::filesystem::path(info->primaryFile).filename(); + const auto primaryPath = localAssetPath(installPath, info->primaryFile); result.installPath = installPath; result.primaryFilePath = primaryPath; result.revision = info->revision.empty() ? "main" : info->revision; @@ -804,8 +910,9 @@ HubInstallResult HuggingFaceModelHub::installModel(const std::string& modelIdOrR // Fetch auxiliary artifacts so the pack is complete on disk (e.g. the // ITO-Master pack needs mastering_tcn.onnx + config.json alongside the // primary fxencoder.onnx). Each is SHA-256 verified when the repo exposes it. - for (const auto& auxiliaryAsset : auxiliaryAssetsForRepo(info->repoId)) { - const auto auxiliaryPath = installPath / auxiliaryAsset; + for (const auto& auxiliaryAsset : auxiliaryAssetsFor(info->repoId, info->primaryFile, info->files)) { + const auto auxiliaryPath = localAssetPath(installPath, auxiliaryAsset); + const auto auxiliaryName = auxiliaryPath.filename().string(); const auto auxiliaryUrl = "https://huggingface.co/" + info->repoId + "/resolve/" + revision + "/" + escapePathPreservingSlash(auxiliaryAsset); if (!downloadToFile(auxiliaryUrl, auxiliaryPath, effectiveToken, &detail)) { @@ -827,8 +934,36 @@ HubInstallResult HuggingFaceModelHub::installModel(const std::string& modelIdOrR return result; } } - result.downloadedFiles.push_back(auxiliaryAsset); - result.auxiliaryFiles.push_back(auxiliaryAsset); + result.downloadedFiles.push_back(auxiliaryName); + result.auxiliaryFiles.push_back(auxiliaryName); + } + + // ONNX Runtime cannot load some external-weight exports at all (shape + // inference cannot read external initializers), so ".data" is + // folded into the primary model in place and the sidecar dropped. The pack + // keeps its file name, so the already-installed check above still matches. + const auto sidecarName = primaryPath.filename().string() + ".data"; + if (const auto sidecarIt = std::find(result.auxiliaryFiles.begin(), result.auxiliaryFiles.end(), sidecarName); + sidecarIt != result.auxiliaryFiles.end()) { + auto inlinedPath = primaryPath; + inlinedPath += ".inlined"; + const auto inlined = inlineExternalData(primaryPath, inlinedPath); + if (!inlined.success) { + std::filesystem::remove(primaryPath, error); + std::filesystem::remove(installPath / sidecarName, error); + result.message = "Could not make " + primaryPath.filename().string() + " self-contained: " + inlined.error; + appendInstallLog(destinationRoot, info.value(), result); + return result; + } + std::filesystem::rename(inlinedPath, primaryPath, error); + if (error) { + result.message = "Could not replace " + primaryPath.filename().string() + " with its inlined form: " + + error.message(); + appendInstallLog(destinationRoot, info.value(), result); + return result; + } + std::filesystem::remove(installPath / sidecarName, error); + result.auxiliaryFiles.erase(sidecarIt); } if (options.downloadReadme) { @@ -869,8 +1004,24 @@ HubInstallResult HuggingFaceModelHub::installModel(const std::string& modelIdOrR result.metadataPath = installPath / "modelhub.json"; writeJson(result.metadataPath, metadata); + std::optional tensorContract; + if (info->repoId == kBsRoformerRepoId || info->repoId == kUmxVocalsRepoId) { + std::string probeError; + const auto catalogContract = + info->repoId == kBsRoformerRepoId ? bsRoformerCatalogContract() : umxVocalsCatalogContract(); + tensorContract = resolveInstalledTensorContract(catalogContract, primaryPath, probeError); + if (!tensorContract.has_value()) { + std::filesystem::remove(primaryPath, error); + result.message = "Downloaded model does not match its tensor contract: " + probeError; + appendInstallLog(destinationRoot, info.value(), result); + return result; + } + } + std::string manifestError; - if (!writeTurnkeyModelPackManifest(installPath, info.value(), result, compatibility, &manifestError)) { + if (!writeTurnkeyModelPackManifest(installPath, info.value(), result, compatibility, + tensorContract.has_value() ? &tensorContract.value() : nullptr, + &manifestError)) { std::filesystem::remove(primaryPath, error); result.message = "Failed writing turnkey model pack metadata: " + manifestError; appendInstallLog(destinationRoot, info.value(), result); diff --git a/src/ai/HuggingFaceModelHub.h b/src/ai/HuggingFaceModelHub.h index 2878ab5..a6fe880 100644 --- a/src/ai/HuggingFaceModelHub.h +++ b/src/ai/HuggingFaceModelHub.h @@ -88,8 +88,43 @@ class HuggingFaceModelHub { const std::string& fallbackQuery); }; +// Overrides the live revision and file hash with the pinned values for curated +// repos that have them (Open-Unmix); other repos are left untouched. +void applyCuratedPin(HubModelInfo& info); + // Curated model catalogue (catalog-only discovery source). Exposed for // license-coverage tests and downstream hub tooling. std::vector curatedModelIds(); +// The repo file an install downloads as the pack's model file; empty when the +// repo offers nothing installable. Generic preference order, except for repos +// whose correct file cannot be inferred from names (BS-RoFormer, spec D4). +// preferGpuBuild selects a GPU-oriented variant where a repo has one. +std::string primaryFileForRepo(const std::string& repoId, + const std::vector& files, + bool* hasOnnxOut = nullptr, + bool preferGpuBuild = false); + +// True when this machine should get BS-RoFormer's GPU (fp32) build: a CUDA +// session opens and the device is large enough for the model. +bool bsRoformerGpuBuildQualifies(); + +// After GPU support appears (e.g. the GPU runtime pack was installed), +// reinstalls a BS-RoFormer pack under `destinationRoot` that is still on its CPU +// (quantized) build as the GPU build, and removes the superseded file. Empty +// when there is no such pack or the machine does not qualify. +std::optional upgradeBsRoformerForGpu(const std::filesystem::path& destinationRoot); + +// Files that must be downloaded next to `primaryFile` to make a complete pack: +// per-repo extras, plus ".data" whenever the repo publishes one +// (ONNX external weights, which the installer then inlines). +std::vector auxiliaryAssetsFor(const std::string& repoId, + const std::string& primaryFile, + const std::vector& files); + +// Where a repo file lands on disk: always directly inside `installPath`, using +// only the file's own name. Repo subfolders ("onnx/model.onnx") are dropped and +// ".." segments can never escape the install directory. +std::filesystem::path localAssetPath(const std::filesystem::path& installPath, const std::string& repoPath); + } // namespace automix::ai diff --git a/src/ai/ITensorInference.h b/src/ai/ITensorInference.h new file mode 100644 index 0000000..cb39b41 --- /dev/null +++ b/src/ai/ITensorInference.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include + +#include "ai/TensorTypes.h" + +namespace automix::ai { + +// Sibling of IModelInference for graphs whose inputs and outputs are shaped +// float32 tensors rather than a feature vector and named scalars. The spec +// probes (inputSpecs/outputSpecs) are the load-bearing addition: tensor names +// are frequently unpublished, so binding by name requires reading them back +// from the loaded graph. +class ITensorInference { + public: + virtual ~ITensorInference() = default; + + virtual bool isAvailable() const = 0; + virtual bool loadModel(const std::filesystem::path& modelPath) = 0; + virtual std::vector inputSpecs() const = 0; + virtual std::vector outputSpecs() const = 0; + virtual TensorInferenceResult run(const std::vector& inputs) const = 0; + + // run(), but a backend able to abort an in-flight inference stops early once + // cancelRequested() returns true. cancelRequested may be called from another + // thread, so it must be thread-safe. The default cannot abort and just runs. + virtual TensorInferenceResult runCancellable(const std::vector& inputs, + const std::function& cancelRequested) const { + (void)cancelRequested; + return run(inputs); + } +}; + +class NullTensorInference final : public ITensorInference { + public: + bool isAvailable() const override { return false; } + bool loadModel(const std::filesystem::path&) override { + lastLog_ = "NullTensorInference: no tensor backend is configured."; + return false; + } + std::vector inputSpecs() const override { return {}; } + std::vector outputSpecs() const override { return {}; } + TensorInferenceResult run(const std::vector& inputs) const override { + TensorInferenceResult result; + result.usedModel = false; + result.logMessage = "NullTensorInference: skipped run with " + std::to_string(inputs.size()) + + " input binding(s) (no model loaded)."; + return result; + } + + const std::string& lastLog() const { return lastLog_; } + + private: + mutable std::string lastLog_; +}; + +} // namespace automix::ai diff --git a/src/ai/ModelCatalogValidator.cpp b/src/ai/ModelCatalogValidator.cpp index 881c106..d92879e 100644 --- a/src/ai/ModelCatalogValidator.cpp +++ b/src/ai/ModelCatalogValidator.cpp @@ -7,9 +7,16 @@ #include +#include "ai/BsRoformerPack.h" +#include "ai/UmxPack.h" #include "ai/ItoMasterAdapter.h" +#include "ai/OnnxTensorInference.h" #include "util/StringUtils.h" +#ifndef AUTOMIX_HAS_NATIVE_ORT +#define AUTOMIX_HAS_NATIVE_ORT 0 +#endif + namespace automix::ai { namespace { @@ -123,6 +130,10 @@ std::string inferTaskScope(const HubModelInfo& model) { if (isKnownTaskScope(model.taskScope)) { return model.taskScope; } + if (model.repoId == kUmxVocalsRepoId) { + // Pinned: the publisher's name ("MixDirective") would otherwise match the "mix" token. + return "separation"; + } std::string joined = toLower(model.useCase); joined += "|" + toLower(model.repoId); @@ -168,10 +179,40 @@ std::string normalizeModelIdForPack(const std::string& modelId) { return sanitizePackId(toLower(modelId)); } +std::optional resolveInstalledTensorContract(const TensorContract& catalogContract, + const std::filesystem::path& modelPath, + std::string& errorOut) { +#if AUTOMIX_HAS_NATIVE_ORT + OnnxTensorInference inference; + inference.setTensorContract(catalogContract); + if (!inference.loadModel(modelPath)) { + errorOut = inference.backendDiagnostics(); + return std::nullopt; + } + // loadModel() ran checkTensorContract(), so the probed and declared lists + // agree in count and position. + auto installed = catalogContract; + const auto inputs = inference.inputSpecs(); + const auto outputs = inference.outputSpecs(); + for (std::size_t i = 0; i < installed.inputs.size() && i < inputs.size(); ++i) { + installed.inputs[i].name = inputs[i].name; + } + for (std::size_t i = 0; i < installed.outputs.size() && i < outputs.size(); ++i) { + installed.outputs[i].name = outputs[i].name; + } + return installed; +#else + (void)modelPath; + (void)errorOut; + return catalogContract; +#endif +} + bool writeTurnkeyModelPackManifest(const std::filesystem::path& installPath, const HubModelInfo& model, const HubInstallResult& installResult, const ModelCompatibilityResult& compatibility, + const TensorContract* tensorContract, std::string* errorOut) { if (!compatibility.compatible) { if (errorOut != nullptr) { @@ -223,11 +264,27 @@ bool writeTurnkeyModelPackManifest(const std::filesystem::path& installPath, {"output_names", nlohmann::json::array()}, }; + if (tensorContract != nullptr) { + manifest["tensor_contract"] = tensorContractToJson(*tensorContract); + } + if (model.repoId == kItoMasterRepoId) { manifest["intended_use"] = std::string("Experimental ITO-Master AI mastering route (non-default, off by default). ") + "License: " + kItoMasterLicense + ". Attribution: " + kItoMasterAttribution; } + if (model.repoId == kBsRoformerRepoId) { + const bool fp32 = modelFileName == kBsRoformerFp32File; + manifest["intended_use"] = fp32 ? kBsRoformerFp32IntendedUse : kBsRoformerIntendedUse; + manifest["gpu_providers"] = bsRoformerGpuProviders(); + if (fp32) { + manifest["gpu_memory_mb"] = kBsRoformerFp32GpuMemoryMb; + } + } + + if (model.repoId == kUmxVocalsRepoId) { + manifest["intended_use"] = kUmxVocalsIntendedUse; + } const auto manifestPath = installPath / "model.json"; std::ofstream out(manifestPath); diff --git a/src/ai/ModelCatalogValidator.h b/src/ai/ModelCatalogValidator.h index 03ce69c..26f9705 100644 --- a/src/ai/ModelCatalogValidator.h +++ b/src/ai/ModelCatalogValidator.h @@ -1,10 +1,12 @@ #pragma once #include +#include #include #include #include "ai/HuggingFaceModelHub.h" +#include "ai/ModelPackLoader.h" namespace automix::ai { @@ -20,10 +22,19 @@ struct ModelCompatibilityResult { std::string inferTaskScope(const HubModelInfo& model); ModelCompatibilityResult validateCatalogModel(const HubModelInfo& model); std::string normalizeModelIdForPack(const std::string& modelId); +// Installed form of a catalog tensor contract: loads the downloaded graph +// against the contract and writes the graph's real tensor names in. Without +// native ONNX Runtime nothing can be probed, so the names stay omitted and +// load-time checks match positionally. With it, a graph that does not satisfy +// the contract fails here (errorOut names the mismatch) rather than at import. +std::optional resolveInstalledTensorContract(const TensorContract& catalogContract, + const std::filesystem::path& modelPath, + std::string& errorOut); bool writeTurnkeyModelPackManifest(const std::filesystem::path& installPath, const HubModelInfo& model, const HubInstallResult& installResult, const ModelCompatibilityResult& compatibility, + const TensorContract* tensorContract, std::string* errorOut); } // namespace automix::ai diff --git a/src/ai/ModelManager.cpp b/src/ai/ModelManager.cpp index 4a7bde8..508189d 100644 --- a/src/ai/ModelManager.cpp +++ b/src/ai/ModelManager.cpp @@ -1,4 +1,5 @@ #include "ai/ModelManager.h" +#include "ai/ModelStorage.h" #include #include @@ -17,6 +18,7 @@ using ::automix::util::toLower; std::vector defaultRoots() { return { + defaultModelHubRoot(), // per-user downloads (see ModelStorage.h) "ModelPacks", "modelhub", "assets/modelhub", diff --git a/src/ai/ModelPackLoader.cpp b/src/ai/ModelPackLoader.cpp index 477ec70..f76dd3e 100644 --- a/src/ai/ModelPackLoader.cpp +++ b/src/ai/ModelPackLoader.cpp @@ -2,11 +2,14 @@ #include #include +#include #include #include +#include "ai/BsRoformerPack.h" #include "ai/FeatureSchema.h" +#include "analysis/SpectrogramFrontEnd.h" #include "util/HashUtils.h" #include "util/Sha256.h" #include "util/StringUtils.h" @@ -113,8 +116,338 @@ bool hasRequiredOutputKeysForScope(const std::string& scope, const std::vector +bool readRequired(const nlohmann::json& object, const char* key, T& out, const std::string& where, std::string& errorOut) { + if (!object.contains(key)) { + errorOut = where + " is missing required field '" + key + "'"; + return false; + } + try { + out = object.at(key).get(); + } catch (const nlohmann::json::exception&) { + errorOut = where + " field '" + key + "' has the wrong type"; + return false; + } + return true; +} + +bool parseIoList(const nlohmann::json& block, + const char* key, + std::vector& out, + std::string& errorOut) { + const std::string where = std::string("tensor_contract.") + key; + if (!block.contains(key) || !block.at(key).is_array()) { + errorOut = where + " must be an array"; + return false; + } + for (const auto& entry : block.at(key)) { + TensorContract::Io io; + if (!entry.is_object()) { + errorOut = where + " entries must be objects"; + return false; + } + if (entry.contains("name")) { + if (!entry.at("name").is_string()) { + errorOut = where + " name must be a string"; + return false; + } + io.name = entry.at("name").get(); + } + if (!readRequired(entry, "shape", io.shape, where, errorOut) || + !readRequired(entry, "dtype", io.dtype, where, errorOut)) { + return false; + } + out.push_back(std::move(io)); + } + return true; +} + +// Can a graph whose probed dims are `probed` accept/produce `declared`? A +// dynamic probed axis accepts any declared extent. +bool graphAccepts(const std::vector& probed, const std::vector& declared) { + if (probed.size() != declared.size()) { + return false; + } + for (std::size_t axis = 0; axis < probed.size(); ++axis) { + if (probed[axis] != -1 && declared[axis] != -1 && probed[axis] != declared[axis]) { + return false; + } + } + return true; +} + +bool checkProbedSide(const char* side, + const std::vector& declared, + const std::vector& probed, + std::string& errorOut) { + const bool namesDeclared = std::any_of(declared.begin(), declared.end(), [](const auto& io) { return !io.name.empty(); }); + if (namesDeclared) { + std::set declaredNames; + std::set probedNames; + for (const auto& io : declared) { + declaredNames.insert(io.name); + } + for (const auto& spec : probed) { + probedNames.insert(spec.name); + } + for (const auto& name : declaredNames) { + if (probedNames.count(name) == 0) { + errorOut = std::string("declared ") + side + " '" + name + "' does not exist in the graph"; + return false; + } + } + for (const auto& name : probedNames) { + if (declaredNames.count(name) == 0) { + errorOut = std::string("graph ") + side + " '" + name + "' is not declared in tensor_contract"; + return false; + } + } + } + if (declared.size() != probed.size()) { + errorOut = std::string("tensor_contract declares ") + std::to_string(declared.size()) + " " + side + + "(s) but the graph has " + std::to_string(probed.size()); + return false; + } + for (std::size_t i = 0; i < declared.size(); ++i) { + const auto& io = declared[i]; + const auto match = namesDeclared ? std::find_if(probed.begin(), probed.end(), + [&](const TensorSpec& spec) { return spec.name == io.name; }) + : probed.begin() + static_cast(i); + if (!graphAccepts(match->dims, io.shape)) { + errorOut = std::string(side) + " '" + match->name + "' is " + describeShape(match->dims) + + " in the graph but declared " + describeShape(io.shape); + return false; + } + } + return true; +} + } // namespace +std::optional parseTensorContract(const nlohmann::json& block, std::string& errorOut) { + if (!block.is_object()) { + errorOut = "tensor_contract must be an object"; + return std::nullopt; + } + TensorContract contract; + const std::string where = "tensor_contract"; + if (!readRequired(block, "engine", contract.engine, where, errorOut) || + !readRequired(block, "sample_rate", contract.sampleRate, where, errorOut) || + !readRequired(block, "stereo", contract.stereo, where, errorOut) || + !readRequired(block, "chunk_samples", contract.chunkSamples, where, errorOut) || + !readRequired(block, "overlap_samples", contract.overlapSamples, where, errorOut) || + !readRequired(block, "input_layout", contract.inputLayout, where, errorOut) || + !readRequired(block, "output_mode", contract.outputMode, where, errorOut)) { + return std::nullopt; + } + if (!block.contains("stft") || !block.at("stft").is_object()) { + errorOut = "tensor_contract.stft must be an object"; + return std::nullopt; + } + const auto& stft = block.at("stft"); + const std::string stftWhere = "tensor_contract.stft"; + if (!readRequired(stft, "n_fft", contract.stft.nFft, stftWhere, errorOut) || + !readRequired(stft, "hop_length", contract.stft.hopLength, stftWhere, errorOut) || + !readRequired(stft, "win_length", contract.stft.winLength, stftWhere, errorOut) || + !readRequired(stft, "window", contract.stft.window, stftWhere, errorOut) || + !readRequired(stft, "periodic_window", contract.stft.periodicWindow, stftWhere, errorOut) || + !readRequired(stft, "center", contract.stft.center, stftWhere, errorOut) || + !readRequired(stft, "pad_mode", contract.stft.padMode, stftWhere, errorOut) || + !readRequired(stft, "normalized", contract.stft.normalized, stftWhere, errorOut) || + !readRequired(stft, "zero_dc", contract.stft.zeroDc, stftWhere, errorOut)) { + return std::nullopt; + } + if (!parseIoList(block, "inputs", contract.inputs, errorOut) || + !parseIoList(block, "outputs", contract.outputs, errorOut)) { + return std::nullopt; + } + if (block.contains("target_stem")) { + if (!readRequired(block, "target_stem", contract.targetStem, where, errorOut)) { + return std::nullopt; + } + } + if (!block.contains("stems") || !block.at("stems").is_array()) { + errorOut = "tensor_contract.stems must be an array"; + return std::nullopt; + } + for (const auto& entry : block.at("stems")) { + TensorContract::Stem stem; + if (!entry.is_object() || !readRequired(entry, "name", stem.name, "tensor_contract.stems", errorOut)) { + if (errorOut.empty()) { + errorOut = "tensor_contract.stems entries must be objects"; + } + return std::nullopt; + } + if (entry.contains("residual_of") && + !readRequired(entry, "residual_of", stem.residualOf, "tensor_contract.stems", errorOut)) { + return std::nullopt; + } + contract.stems.push_back(std::move(stem)); + } + return contract; +} + +nlohmann::json tensorContractToJson(const TensorContract& contract) { + const auto ioList = [](const std::vector& list) { + nlohmann::json out = nlohmann::json::array(); + for (const auto& io : list) { + nlohmann::json entry = {{"shape", io.shape}, {"dtype", io.dtype}}; + if (!io.name.empty()) { + entry["name"] = io.name; + } + out.push_back(entry); + } + return out; + }; + nlohmann::json stems = nlohmann::json::array(); + for (const auto& stem : contract.stems) { + nlohmann::json entry = {{"name", stem.name}}; + if (!stem.residualOf.empty()) { + entry["residual_of"] = stem.residualOf; + } + stems.push_back(entry); + } + nlohmann::json block = { + {"engine", contract.engine}, + {"sample_rate", contract.sampleRate}, + {"stereo", contract.stereo}, + {"chunk_samples", contract.chunkSamples}, + {"overlap_samples", contract.overlapSamples}, + {"stft", + {{"n_fft", contract.stft.nFft}, + {"hop_length", contract.stft.hopLength}, + {"win_length", contract.stft.winLength}, + {"window", contract.stft.window}, + {"periodic_window", contract.stft.periodicWindow}, + {"center", contract.stft.center}, + {"pad_mode", contract.stft.padMode}, + {"normalized", contract.stft.normalized}, + {"zero_dc", contract.stft.zeroDc}}}, + {"input_layout", contract.inputLayout}, + {"inputs", ioList(contract.inputs)}, + {"outputs", ioList(contract.outputs)}, + {"output_mode", contract.outputMode}, + {"stems", stems}, + }; + if (!contract.targetStem.empty()) { + block["target_stem"] = contract.targetStem; + } + return block; +} + +std::optional runnerConfigFromContract(const TensorContract& contract, std::string& errorOut) { + const auto layout = inputLayoutFromString(contract.inputLayout); + if (!layout.has_value()) { + errorOut = "tensor_contract input_layout '" + contract.inputLayout + + "' is not one of folded_stereo, split_channels, magnitude_channels"; + return std::nullopt; + } + const auto mode = outputModeFromString(contract.outputMode); + if (!mode.has_value()) { + errorOut = "tensor_contract output_mode '" + contract.outputMode + "' is not one of direct, mask, ratio_mask"; + return std::nullopt; + } + if (contract.stft.padMode != "reflect") { + errorOut = "tensor_contract.stft pad_mode '" + contract.stft.padMode + "' is not implemented (only reflect)"; + return std::nullopt; + } + if (contract.stft.window != "hann" || !contract.stft.periodicWindow) { + errorOut = "tensor_contract.stft window '" + contract.stft.window + + (contract.stft.periodicWindow ? "" : " (symmetric)") + "' is not implemented (only periodic hann)"; + return std::nullopt; + } + if (contract.sampleRate <= 0) { + errorOut = "tensor_contract sample_rate must be positive"; + return std::nullopt; + } + + RunnerConfig config; + config.chunkSamples = contract.chunkSamples; + config.overlapSamples = contract.overlapSamples; + config.sampleRate = static_cast(contract.sampleRate); + config.channels = contract.stereo ? 2 : 1; + config.stft.nFft = contract.stft.nFft; + config.stft.hopLength = contract.stft.hopLength; + config.stft.winLength = contract.stft.winLength; + config.stft.center = contract.stft.center; + config.stft.normalized = contract.stft.normalized; + config.stft.zeroDc = contract.stft.zeroDc; + config.inputLayout = *layout; + config.outputMode = *mode; + config.targetStem = contract.targetStem; + config.stems.clear(); + for (const auto& stem : contract.stems) { + config.stems.push_back({stem.name, stem.residualOf}); + } + if (const auto error = validateRunnerConfig(config); !error.empty()) { + errorOut = "tensor_contract: " + error; + return std::nullopt; + } + if (config.stft.nFft <= 0 || (config.stft.nFft & (config.stft.nFft - 1)) != 0 || config.stft.hopLength <= 0 || + config.stft.winLength <= 0 || config.stft.winLength > config.stft.nFft) { + errorOut = "tensor_contract.stft n_fft " + std::to_string(config.stft.nFft) + " / hop_length " + + std::to_string(config.stft.hopLength) + " / win_length " + std::to_string(config.stft.winLength) + + " is not a supported STFT"; + return std::nullopt; + } + return config; +} + +bool checkTensorContract(const TensorContract& contract, + const std::vector& probedInputs, + const std::vector& probedOutputs, + std::string& errorOut) { + const auto config = runnerConfigFromContract(contract, errorOut); + if (!config.has_value()) { + return false; + } + if (contract.inputs.size() != 1 || contract.outputs.size() != 1) { + errorOut = "tensor_contract must declare exactly one input and one output, got " + + std::to_string(contract.inputs.size()) + " and " + std::to_string(contract.outputs.size()); + return false; + } + + const int freqBins = config->stft.nFft / 2 + 1; + const int frames = analysis::stftFrameCount(config->chunkSamples, config->stft); + const int graphStems = static_cast(std::count_if(contract.stems.begin(), contract.stems.end(), + [](const auto& stem) { return stem.residualOf.empty(); })); + const int stemAxis = config->outputMode == OutputMode::Direct ? graphStems : 1; + const auto impliedInput = tensorInputDims(config->inputLayout, config->channels, freqBins, frames); + const auto impliedOutput = tensorOutputDims(config->inputLayout, stemAxis, config->channels, freqBins, frames); + const auto stftSummary = "n_fft " + std::to_string(config->stft.nFft) + " (" + std::to_string(freqBins) + + " bins), " + std::to_string(frames) + " frames per " + + std::to_string(config->chunkSamples) + "-sample chunk, " + + std::to_string(config->channels) + " channel(s), " + contract.inputLayout; + + const auto label = [](const TensorContract::Io& io, const char* fallback) { + return io.name.empty() ? std::string(fallback) : io.name; + }; + const auto& input = contract.inputs.front(); + const auto& output = contract.outputs.front(); + for (const auto* io : {&input, &output}) { + if (io->dtype != "float32") { + errorOut = "tensor '" + label(*io, io == &input ? "input" : "output") + "' declares dtype '" + io->dtype + + "'; only float32 is supported"; + return false; + } + } + const TensorSpec declaredInput{input.name, TensorElementType::Float32, input.shape}; + if (!shapesMatch(declaredInput, TensorSpec{input.name, TensorElementType::Float32, impliedInput})) { + errorOut = "input '" + label(input, "input") + "' declares " + describeShape(input.shape) + " but " + stftSummary + + " implies " + describeShape(impliedInput); + return false; + } + const TensorSpec declaredOutput{output.name, TensorElementType::Float32, output.shape}; + if (!shapesMatch(declaredOutput, TensorSpec{output.name, TensorElementType::Float32, impliedOutput})) { + errorOut = "output '" + label(output, "output") + "' declares " + describeShape(output.shape) + " but " + + stftSummary + " implies " + describeShape(impliedOutput); + return false; + } + + return checkProbedSide("input", contract.inputs, probedInputs, errorOut) && + checkProbedSide("output", contract.outputs, probedOutputs, errorOut); +} + std::optional ModelPackLoader::load(const std::filesystem::path& directory) const { const auto metadataPath = directory / "model.json"; if (!std::filesystem::exists(metadataPath)) { @@ -179,11 +512,34 @@ std::optional ModelPackLoader::load(const std::filesystem::path& dire pack.defaultInterOpThreads.reset(); } pack.enableProfiling = json.value("enableProfiling", json.value("enable_profiling", false)); + if (json.contains("gpu_memory_mb") && json.at("gpu_memory_mb").is_number_unsigned()) { + pack.gpuMemoryMb = json.at("gpu_memory_mb").get(); + } + if (json.contains("gpu_providers") && json.at("gpu_providers").is_array()) { + pack.gpuProviders = json.at("gpu_providers").get>(); + } else if (json.contains("gpuProviders") && json.at("gpuProviders").is_array()) { + pack.gpuProviders = json.at("gpuProviders").get>(); + } + + // Packs installed before gpu_providers existed carry no list; BS-RoFormer must + // still be held to the providers it was measured on. + if (pack.gpuProviders.empty() && isBsRoformerModelFile(pack.modelFile)) { + pack.gpuProviders = bsRoformerGpuProviders(); + } if (pack.featureSchemaVersion.empty() && json.contains("feature_schema") && json.at("feature_schema").is_object()) { pack.featureSchemaVersion = json.at("feature_schema").value("version", ""); } + if (json.contains("tensor_contract")) { + std::string contractError; + auto contract = parseTensorContract(json.at("tensor_contract"), contractError); + if (!contract.has_value()) { + return std::nullopt; + } + pack.tensorContract = std::move(contract); + } + pack.rootPath = directory; if (!hasRequiredMetadata(pack)) { @@ -235,7 +591,10 @@ std::optional ModelPackLoader::load(const std::filesystem::path& dire } else if (pack.checksum != computeLegacyChecksum(modelPath)) { return std::nullopt; } - if (!hasRequiredOutputKeysForScope(pack.taskScope, pack.expectedOutputKeys)) { + // A tensor pack's outputs are the named tensors of its contract, not scalar + // keys, so the scalar output-key requirement does not apply to it. + if (!pack.tensorContract.has_value() && + !hasRequiredOutputKeysForScope(pack.taskScope, pack.expectedOutputKeys)) { return std::nullopt; } diff --git a/src/ai/ModelPackLoader.h b/src/ai/ModelPackLoader.h index f25985c..084dca6 100644 --- a/src/ai/ModelPackLoader.h +++ b/src/ai/ModelPackLoader.h @@ -1,12 +1,74 @@ #pragma once +#include #include #include #include #include +#include + +#include "ai/SeparationRunner.h" +#include "ai/TensorTypes.h" + namespace automix::ai { +// Declared audio-tensor interface of a pack (spec section 6). Strings are kept +// verbatim from the manifest so that checkTensorContract() can name an +// unrecognised value instead of the parser silently mapping it to a default. +struct TensorContract { + struct Stft { + int nFft = 0; + int hopLength = 0; + int winLength = 0; + std::string window; + bool periodicWindow = true; + bool center = true; + std::string padMode; + bool normalized = false; + bool zeroDc = true; + }; + struct Io { + std::string name; // empty in the catalog form; the install-time probe fills it + std::vector shape; + std::string dtype; + }; + struct Stem { + std::string name; + std::string residualOf; + }; + + std::string engine; + int sampleRate = 0; + bool stereo = true; + int chunkSamples = 0; + int overlapSamples = 0; + Stft stft; + std::string inputLayout; + std::vector inputs; + std::vector outputs; + std::string outputMode; + std::string targetStem; + std::vector stems; +}; + +// Structural parse of a `tensor_contract` block. Fails (nullopt + reason) only +// on missing or mistyped fields; value checks belong to checkTensorContract(). +std::optional parseTensorContract(const nlohmann::json& block, std::string& errorOut); +nlohmann::json tensorContractToJson(const TensorContract& contract); + +// The spec 5.3 load-time cross-check: the declared contract must be internally +// consistent (STFT geometry vs declared shapes, layout, mode, stems) and must +// agree with the graph's probed inputs/outputs. Every failure names the tensor +// or field and prints both shapes where shapes are involved. +bool checkTensorContract(const TensorContract& contract, + const std::vector& probedInputs, + const std::vector& probedOutputs, + std::string& errorOut); + +// The runner configuration a (checked) contract describes. +std::optional runnerConfigFromContract(const TensorContract& contract, std::string& errorOut); + struct ModelPack { int schemaVersion = 1; std::string id; @@ -35,6 +97,16 @@ struct ModelPack { std::vector expectedOutputKeys; std::vector inputNames; std::vector outputNames; + // Absent for scalar-only packs; existing packs load unchanged. + std::optional tensorContract; + // Device memory the model needs while running on a GPU (MiB). When free + // memory is below it, GPU execution would spill into system memory, so + // callers run on CPU instead. Absent: no known requirement. + std::optional gpuMemoryMb; + // Per-pack GPU provider allow-list (e.g. {"cuda"}). When non-empty, GPU + // candidates outside this list are ignored and fallback to CPU. Empty means + // any runtime-supported GPU provider in the priority chain is allowed. + std::vector gpuProviders; std::filesystem::path rootPath; }; diff --git a/src/ai/ModelStorage.cpp b/src/ai/ModelStorage.cpp new file mode 100644 index 0000000..c1dd42c --- /dev/null +++ b/src/ai/ModelStorage.cpp @@ -0,0 +1,188 @@ +#include "ai/ModelStorage.h" + +#include +#include +#include + +#include +#include + +namespace automix::ai { +namespace { + +constexpr const char* kRegistryFile = "install_registry.json"; +constexpr const char* kConsentsFile = "license_consents.json"; +constexpr const char* kInstallLogFile = "install_log.jsonl"; +constexpr const char* kMigratedNote = "MIGRATED.txt"; + +std::filesystem::path toPath(const juce::File& file) { + return std::filesystem::path(file.getFullPathName().toWideCharPointer()); +} + +nlohmann::json readArray(const std::filesystem::path& path) { + try { + std::ifstream in(path); + if (in.is_open()) { + auto parsed = nlohmann::json::parse(in); + if (parsed.is_array()) { + return parsed; + } + } + } catch (...) { + } + return nlohmann::json::array(); +} + +std::string recordKey(const nlohmann::json& item) { + return item.is_object() ? item.value("modelId", item.value("repoId", "")) : std::string(); +} + +// rename() where possible; across volumes, copy then delete the source. +bool movePath(const std::filesystem::path& from, const std::filesystem::path& to, std::string& error) { + std::error_code code; + std::filesystem::rename(from, to, code); + if (!code) { + return true; + } + code.clear(); + std::filesystem::copy(from, to, std::filesystem::copy_options::recursive, code); + if (code) { + error = "cannot copy '" + from.string() + "' to '" + to.string() + "': " + code.message(); + std::error_code ignored; + std::filesystem::remove_all(to, ignored); + return false; + } + std::filesystem::remove_all(from, code); // the copy is complete; a leftover source is harmless + return true; +} + +// Target records win on the same key; legacy-only records are appended. +nlohmann::json mergeRecords(nlohmann::json target, const nlohmann::json& legacy) { + std::set present; + for (const auto& item : target) { + present.insert(recordKey(item)); + } + for (const auto& item : legacy) { + const auto key = recordKey(item); + if (key.empty() || present.insert(key).second) { + target.push_back(item); + } + } + return target; +} + +bool writeJson(const std::filesystem::path& path, const nlohmann::json& value) { + std::ofstream out(path, std::ios::trunc); + out << value.dump(2); + return static_cast(out); +} + +} // namespace + +std::filesystem::path defaultModelHubRoot() { + const auto overrideRoot = juce::SystemStats::getEnvironmentVariable("AUTOMIX_MODEL_HUB_ROOT", {}); + if (overrideRoot.isNotEmpty()) { + return std::filesystem::path(overrideRoot.toWideCharPointer()); + } +#if defined(_WIN32) + const auto base = juce::File::getSpecialLocation(juce::File::windowsLocalAppData); +#else + const auto base = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory); +#endif + return toPath(base) / "AutoMixMaster" / "modelhub"; +} + +std::filesystem::path legacyModelHubRoot() { return std::filesystem::path("assets") / "modelhub"; } + +bool isInsideDirectory(const std::filesystem::path& candidate, const std::filesystem::path& root) { + std::error_code error; + const auto child = std::filesystem::weakly_canonical(std::filesystem::absolute(candidate, error), error); + const auto parent = std::filesystem::weakly_canonical(std::filesystem::absolute(root, error), error); + if (error || child.empty() || parent.empty()) { + return false; + } + const auto relative = child.lexically_relative(parent); + return !relative.empty() && relative != "." && *relative.begin() != ".."; +} + +ModelHubMigration migrateModelHub(const std::filesystem::path& legacyRoot, const std::filesystem::path& targetRoot) { + ModelHubMigration result; + std::error_code error; + if (!std::filesystem::is_directory(legacyRoot, error) || std::filesystem::exists(legacyRoot / kMigratedNote, error)) { + return result; + } + if (std::filesystem::equivalent(legacyRoot, targetRoot, error) && !error) { + return result; // already the same place (e.g. AUTOMIX_MODEL_HUB_ROOT points at it) + } + error.clear(); + if (std::filesystem::is_empty(legacyRoot, error)) { + return result; + } + result.attempted = true; + std::filesystem::create_directories(targetRoot, error); + if (error) { + result.error = "cannot create '" + targetRoot.string() + "': " + error.message(); + return result; + } + + // Pack directories first; the records describing them follow. + for (const auto& entry : std::filesystem::directory_iterator(legacyRoot, error)) { + if (!entry.is_directory(error)) { + continue; + } + const auto name = entry.path().filename(); + if (std::filesystem::exists(targetRoot / name, error)) { + result.keptInPlace.push_back(name.string()); + continue; + } + if (!movePath(entry.path(), targetRoot / name, result.error)) { + return result; // nothing is marked migrated, so the next start retries + } + ++result.movedPacks; + } + + // Registry: merge, then point every installPath at the pack's directory here. + auto registry = mergeRecords(readArray(targetRoot / kRegistryFile), readArray(legacyRoot / kRegistryFile)); + for (auto& item : registry) { + if (!item.is_object() || !item.contains("installPath")) { + continue; + } + auto installPath = std::filesystem::path(item.value("installPath", "")).lexically_normal(); + if (installPath.filename().empty()) { + installPath = installPath.parent_path(); // trailing separator + } + const auto relocated = targetRoot / installPath.filename(); + if (!installPath.filename().empty() && std::filesystem::is_directory(relocated, error)) { + item["installPath"] = relocated.string(); + } + } + if (!registry.empty() && !writeJson(targetRoot / kRegistryFile, registry)) { + result.error = "cannot write " + (targetRoot / kRegistryFile).string(); + return result; + } + const auto consents = mergeRecords(readArray(targetRoot / kConsentsFile), readArray(legacyRoot / kConsentsFile)); + if (!consents.empty() && !writeJson(targetRoot / kConsentsFile, consents)) { + result.error = "cannot write " + (targetRoot / kConsentsFile).string(); + return result; + } + std::filesystem::remove(legacyRoot / kRegistryFile, error); + std::filesystem::remove(legacyRoot / kConsentsFile, error); + + // The install log is append-only history: keep both. + if (std::filesystem::exists(legacyRoot / kInstallLogFile, error)) { + std::ifstream in(legacyRoot / kInstallLogFile, std::ios::binary); + std::ofstream out(targetRoot / kInstallLogFile, std::ios::binary | std::ios::app); + out << in.rdbuf(); + in.close(); + if (out) { + std::filesystem::remove(legacyRoot / kInstallLogFile, error); + } + } + + std::ofstream note(legacyRoot / kMigratedNote, std::ios::trunc); + note << "Downloaded models moved to " << targetRoot.string() << "\n" + << "AutoMixMaster no longer reads this folder.\n"; + return result; +} + +} // namespace automix::ai diff --git a/src/ai/ModelStorage.h b/src/ai/ModelStorage.h new file mode 100644 index 0000000..626c338 --- /dev/null +++ b/src/ai/ModelStorage.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +namespace automix::ai { + +// Where downloaded models and their records (install_registry.json, +// license_consents.json) live: %LOCALAPPDATA%\AutoMixMaster\modelhub on +// Windows, /AutoMixMaster/modelhub elsewhere, or +// AUTOMIX_MODEL_HUB_ROOT when set (portable installs, tests). Per user and +// writable, unlike the application folder under Program Files - and unlike the +// old cwd-relative "assets/modelhub", which moved with the working directory. +std::filesystem::path defaultModelHubRoot(); + +// The pre-2026-10 location, relative to the working directory. +std::filesystem::path legacyModelHubRoot(); + +struct ModelHubMigration { + bool attempted = false; // a legacy hub with content was found + int movedPacks = 0; + std::vector keptInPlace; // already present at the target; left untouched + std::string error; +}; + +// Moves a legacy hub into `targetRoot`: every pack directory, plus the hub's +// records. install_registry.json and license_consents.json are merged by +// modelId (the target's entry wins), so recorded licence consents survive, and +// every registry installPath is rewritten to the pack's new directory. Packs +// moved across volumes are copied, then deleted at the source. Leaves a +// MIGRATED.txt note behind and is a no-op once that note exists. +ModelHubMigration migrateModelHub(const std::filesystem::path& legacyRoot, const std::filesystem::path& targetRoot); + +// True when `candidate` is strictly inside `root` (after normalisation). Used to +// refuse deleting anything a registry entry points at outside the hub. +bool isInsideDirectory(const std::filesystem::path& candidate, const std::filesystem::path& root); + +} // namespace automix::ai diff --git a/src/ai/OnnxExternalData.cpp b/src/ai/OnnxExternalData.cpp new file mode 100644 index 0000000..13a1b5d --- /dev/null +++ b/src/ai/OnnxExternalData.cpp @@ -0,0 +1,393 @@ +#include "ai/OnnxExternalData.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace automix::ai { +namespace { + +// onnx.proto field numbers this rewrite touches. Everything else is opaque. +constexpr std::uint32_t kModelGraph = 7; // ModelProto.graph +constexpr std::uint32_t kGraphInitializer = 5; // GraphProto.initializer +constexpr std::uint32_t kTensorRawData = 9; // TensorProto.raw_data +constexpr std::uint32_t kTensorExternalData = 13; // TensorProto.external_data +constexpr std::uint32_t kTensorDataLocation = 14; // TensorProto.data_location +constexpr std::uint64_t kDataLocationExternal = 1; +constexpr std::uint32_t kEntryKey = 1; // StringStringEntryProto.key +constexpr std::uint32_t kEntryValue = 2; // StringStringEntryProto.value + +constexpr std::uint32_t kWireVarint = 0; +constexpr std::uint32_t kWire64 = 1; +constexpr std::uint32_t kWireLength = 2; +constexpr std::uint32_t kWire32 = 5; + +// Protobuf refuses messages of 2 GB or more. +constexpr std::uint64_t kMaxMessageBytes = static_cast(std::numeric_limits::max()); + +struct Field { + std::uint32_t number = 0; + std::uint32_t wire = 0; + std::size_t begin = 0; // first byte of the tag + std::size_t end = 0; // one past the last byte of the field + std::size_t payloadBegin = 0; // length-delimited payload + std::size_t payloadEnd = 0; + std::uint64_t varint = 0; +}; + +bool readVarint(const std::string& bytes, std::size_t& pos, std::size_t end, std::uint64_t& value) { + value = 0; + for (int shift = 0; shift < 64; shift += 7) { + if (pos >= end) { + return false; + } + const auto byte = static_cast(bytes[pos++]); + value |= static_cast(byte & 0x7F) << shift; + if ((byte & 0x80) == 0) { + return true; + } + } + return false; +} + +void writeVarint(std::string& out, std::uint64_t value) { + while (value >= 0x80) { + out.push_back(static_cast((value & 0x7F) | 0x80)); + value >>= 7; + } + out.push_back(static_cast(value)); +} + +void writeLengthDelimitedHeader(std::string& out, std::uint32_t number, std::uint64_t length) { + writeVarint(out, (static_cast(number) << 3) | kWireLength); + writeVarint(out, length); +} + +// Reads the field starting at `pos` (which must be < end) and advances past it. +bool nextField(const std::string& bytes, std::size_t& pos, std::size_t end, Field& field, std::string& error) { + field = Field{}; + field.begin = pos; + std::uint64_t tag = 0; + if (!readVarint(bytes, pos, end, tag)) { + error = "truncated field tag at byte " + std::to_string(field.begin); + return false; + } + field.number = static_cast(tag >> 3); + field.wire = static_cast(tag & 0x7); + switch (field.wire) { + case kWireVarint: + if (!readVarint(bytes, pos, end, field.varint)) { + error = "truncated varint at byte " + std::to_string(field.begin); + return false; + } + break; + case kWire64: + case kWire32: { + const std::size_t width = field.wire == kWire64 ? 8 : 4; + if (end - pos < width) { + error = "truncated fixed-width field at byte " + std::to_string(field.begin); + return false; + } + pos += width; + break; + } + case kWireLength: { + std::uint64_t length = 0; + if (!readVarint(bytes, pos, end, length) || length > end - pos) { + error = "length-delimited field overruns its message at byte " + std::to_string(field.begin); + return false; + } + field.payloadBegin = pos; + field.payloadEnd = pos + static_cast(length); + pos = field.payloadEnd; + break; + } + default: + error = "unsupported protobuf wire type " + std::to_string(field.wire) + " at byte " + + std::to_string(field.begin); + return false; + } + field.end = pos; + return true; +} + +// Refuses absolute locations and any that leave the model's directory. +bool resolveLocation(const std::filesystem::path& modelDirectory, + const std::string& location, + std::filesystem::path& resolved, + std::string& error) { + const auto relative = std::filesystem::path(location); + if (location.empty() || relative.is_absolute() || relative.has_root_name() || relative.has_root_directory()) { + error = "external data location '" + location + "' is not a relative path"; + return false; + } + for (const auto& part : relative) { + if (part == "..") { + error = "external data location '" + location + "' leaves the model directory"; + return false; + } + } + resolved = modelDirectory / relative; + return true; +} + +class SidecarReader { + public: + explicit SidecarReader(std::filesystem::path directory) : directory_(std::move(directory)) {} + + bool read(const std::string& location, std::uint64_t offset, std::uint64_t length, bool lengthGiven, + std::string& out, std::string& error) { + auto it = files_.find(location); + if (it == files_.end()) { + std::filesystem::path resolved; + if (!resolveLocation(directory_, location, resolved, error)) { + return false; + } + auto stream = std::make_unique(resolved, std::ios::binary); + if (!stream->is_open()) { + error = "cannot open external data file '" + resolved.string() + "'"; + return false; + } + std::error_code sizeError; + const auto size = std::filesystem::file_size(resolved, sizeError); + if (sizeError) { + error = "cannot size external data file '" + resolved.string() + "'"; + return false; + } + it = files_.emplace(location, Open{std::move(stream), size}).first; + } + auto& file = it->second; + if (offset > file.size) { + error = "external data offset " + std::to_string(offset) + " is past the end of '" + location + "'"; + return false; + } + const auto available = file.size - offset; + const auto count = lengthGiven ? length : available; + if (count > available) { + error = "external data for '" + location + "' needs " + std::to_string(count) + " bytes at offset " + + std::to_string(offset) + " but only " + std::to_string(available) + " remain"; + return false; + } + const auto start = out.size(); + out.resize(start + static_cast(count)); + file.stream->seekg(static_cast(offset)); + file.stream->read(out.data() + start, static_cast(count)); + if (!*file.stream) { + error = "short read from external data file '" + location + "'"; + return false; + } + return true; + } + + private: + struct Open { + std::unique_ptr stream; + std::uintmax_t size = 0; + }; + std::filesystem::path directory_; + std::map files_; +}; + +bool parseUnsigned(const std::string& text, std::uint64_t& value) { + if (text.empty()) { + return false; + } + value = 0; + for (const char ch : text) { + if (ch < '0' || ch > '9') { + return false; + } + const auto digit = static_cast(ch - '0'); + if (value > (std::numeric_limits::max() - digit) / 10) { + return false; + } + value = value * 10 + digit; + } + return true; +} + +// Appends the rewritten TensorProto in [begin, end) to `out`. +bool rewriteTensor(const std::string& bytes, std::size_t begin, std::size_t end, SidecarReader& sidecars, + std::string& out, bool& inlined, std::string& error) { + std::map external; + bool isExternal = false; + std::string kept; + for (std::size_t pos = begin; pos < end;) { + Field field; + if (!nextField(bytes, pos, end, field, error)) { + return false; + } + if (field.number == kTensorDataLocation && field.wire == kWireVarint) { + isExternal = field.varint == kDataLocationExternal; + continue; + } + if (field.number == kTensorExternalData && field.wire == kWireLength) { + std::string key; + std::string value; + for (std::size_t entry = field.payloadBegin; entry < field.payloadEnd;) { + Field part; + if (!nextField(bytes, entry, field.payloadEnd, part, error)) { + return false; + } + if (part.wire != kWireLength) { + continue; + } + auto text = bytes.substr(part.payloadBegin, part.payloadEnd - part.payloadBegin); + if (part.number == kEntryKey) { + key = std::move(text); + } else if (part.number == kEntryValue) { + value = std::move(text); + } + } + external[key] = value; + continue; + } + kept.append(bytes, field.begin, field.end - field.begin); + } + + inlined = false; + if (!isExternal) { + out.append(bytes, begin, end - begin); // untouched, byte for byte + return true; + } + + const auto location = external.find("location"); + if (location == external.end()) { + error = "external tensor has no 'location' entry"; + return false; + } + std::uint64_t offset = 0; + std::uint64_t length = 0; + bool lengthGiven = false; + if (const auto it = external.find("offset"); it != external.end() && !parseUnsigned(it->second, offset)) { + error = "external tensor has a malformed offset '" + it->second + "'"; + return false; + } + if (const auto it = external.find("length"); it != external.end()) { + if (!parseUnsigned(it->second, length)) { + error = "external tensor has a malformed length '" + it->second + "'"; + return false; + } + lengthGiven = true; + } + + std::string raw; + if (!sidecars.read(location->second, offset, length, lengthGiven, raw, error)) { + return false; + } + out.append(kept); + writeLengthDelimitedHeader(out, kTensorRawData, raw.size()); + out.append(raw); + inlined = true; + return true; +} + +bool rewriteGraph(const std::string& bytes, std::size_t begin, std::size_t end, SidecarReader& sidecars, + std::string& out, int& inlinedCount, std::string& error) { + for (std::size_t pos = begin; pos < end;) { + Field field; + if (!nextField(bytes, pos, end, field, error)) { + return false; + } + if (field.number != kGraphInitializer || field.wire != kWireLength) { + out.append(bytes, field.begin, field.end - field.begin); + continue; + } + std::string tensor; + bool inlined = false; + if (!rewriteTensor(bytes, field.payloadBegin, field.payloadEnd, sidecars, tensor, inlined, error)) { + return false; + } + inlinedCount += inlined ? 1 : 0; + writeLengthDelimitedHeader(out, kGraphInitializer, tensor.size()); + out.append(tensor); + if (out.size() > kMaxMessageBytes) { + error = "inlined model would exceed protobuf's 2 GB limit; keep its external data"; + return false; + } + } + return true; +} + +bool readWholeFile(const std::filesystem::path& path, std::string& out) { + std::ifstream in(path, std::ios::binary); + if (!in.is_open()) { + return false; + } + in.seekg(0, std::ios::end); + out.resize(static_cast(in.tellg())); + in.seekg(0, std::ios::beg); + in.read(out.data(), static_cast(out.size())); + return static_cast(in); +} + +} // namespace + +ExternalDataInlineResult inlineExternalData(const std::filesystem::path& modelPath, + const std::filesystem::path& outputPath) { + ExternalDataInlineResult result; + std::string model; + if (!readWholeFile(modelPath, model)) { + result.error = "cannot read model '" + modelPath.string() + "'"; + return result; + } + + SidecarReader sidecars(modelPath.parent_path()); + std::string rewritten; + bool sawGraph = false; + for (std::size_t pos = 0; pos < model.size();) { + Field field; + if (!nextField(model, pos, model.size(), field, result.error)) { + result.error = "'" + modelPath.filename().string() + "' is not a valid ONNX model: " + result.error; + return result; + } + if (field.number != kModelGraph || field.wire != kWireLength) { + rewritten.append(model, field.begin, field.end - field.begin); + continue; + } + sawGraph = true; + std::string graph; + if (!rewriteGraph(model, field.payloadBegin, field.payloadEnd, sidecars, graph, result.tensorsInlined, + result.error)) { + return result; + } + writeLengthDelimitedHeader(rewritten, kModelGraph, graph.size()); + rewritten.append(graph); + } + if (!sawGraph) { + result.error = "'" + modelPath.filename().string() + "' has no graph"; + return result; + } + if (rewritten.size() > kMaxMessageBytes) { + result.error = "inlined model would exceed protobuf's 2 GB limit; keep its external data"; + return result; + } + + auto temporary = outputPath; + temporary += ".partial"; + { + std::ofstream out(temporary, std::ios::binary | std::ios::trunc); + out.write(rewritten.data(), static_cast(rewritten.size())); + if (!out) { + result.error = "cannot write '" + temporary.string() + "'"; + std::error_code ignored; + std::filesystem::remove(temporary, ignored); + return result; + } + } + std::error_code renameError; + std::filesystem::rename(temporary, outputPath, renameError); + if (renameError) { + result.error = "cannot move inlined model into place: " + renameError.message(); + std::filesystem::remove(temporary, renameError); + return result; + } + result.success = true; + return result; +} + +} // namespace automix::ai diff --git a/src/ai/OnnxExternalData.h b/src/ai/OnnxExternalData.h new file mode 100644 index 0000000..b24c6f2 --- /dev/null +++ b/src/ai/OnnxExternalData.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +namespace automix::ai { + +struct ExternalDataInlineResult { + bool success = false; + int tensorsInlined = 0; + std::string error; +}; + +// Rewrites an ONNX model whose graph initializers live in external-data files +// into one self-contained file, with each such initializer stored inline as +// raw_data. Everything else in the model is copied byte for byte. +// +// Why: ONNX Runtime's shape inference cannot read external initializers, so an +// export that keeps even a Split's size list external (BS-RoFormer fp32) fails +// to load at all. Inlining is only possible below protobuf's 2 GB message +// limit; larger models are refused. +// +// External locations must be relative paths inside the model's directory; a +// model naming an absolute path or one that escapes the directory is refused +// rather than having arbitrary files copied into it. The output is written to +// `outputPath` via a temporary file, so a failure never leaves a partial model. +ExternalDataInlineResult inlineExternalData(const std::filesystem::path& modelPath, + const std::filesystem::path& outputPath); + +} // namespace automix::ai diff --git a/src/ai/OnnxModelInference.cpp b/src/ai/OnnxModelInference.cpp index d24b753..0c4aa83 100644 --- a/src/ai/OnnxModelInference.cpp +++ b/src/ai/OnnxModelInference.cpp @@ -17,6 +17,7 @@ #include +#include "ai/GpuRuntimePack.h" #include "util/StringUtils.h" #ifndef AUTOMIX_HAS_NATIVE_ORT @@ -25,6 +26,8 @@ #if AUTOMIX_HAS_NATIVE_ORT #include +#include "ai/OrtRuntime.h" +#include "ai/OrtSessionProviders.h" #endif namespace automix::ai { @@ -42,7 +45,7 @@ std::string canonicalProviderName(const std::string& rawProvider) { return gpu::canonicalProviderName(rawProvider); } -std::string platformPreferredProvider() { +[[maybe_unused]] std::string platformPreferredProvider() { return gpu::platformPreferredProvider(); } @@ -92,61 +95,7 @@ std::filesystem::path pickQuantizedVariant(const std::filesystem::path& modelPat return modelPath; } -#if AUTOMIX_HAS_NATIVE_ORT -struct SessionTuning { - std::string hardwareTier = "standard"; - int intraOpThreads = 0; - int interOpThreads = 0; - bool memPattern = true; - bool cpuArena = true; - bool sequentialExecution = false; -}; - -SessionTuning tuningForProvider(const std::string& provider, const int hardwareThreads) { - SessionTuning tuning; - const auto normalized = canonicalProviderName(provider); - const int clampedThreads = std::max(1, hardwareThreads); - if (clampedThreads <= 4) { - tuning.hardwareTier = "low"; - } else if (clampedThreads >= 12) { - tuning.hardwareTier = "high"; - } - - if (normalized == "cuda") { - tuning.intraOpThreads = std::clamp(clampedThreads / 2, 1, 8); - tuning.interOpThreads = 1; - tuning.memPattern = false; - tuning.cpuArena = true; - tuning.sequentialExecution = false; - return tuning; - } - - if (normalized == "directml") { - tuning.intraOpThreads = std::clamp(clampedThreads / 2, 1, 4); - tuning.interOpThreads = 1; - tuning.memPattern = false; - tuning.cpuArena = false; - tuning.sequentialExecution = true; - return tuning; - } - if (normalized == "coreml") { - tuning.intraOpThreads = std::clamp(clampedThreads / 2, 1, 4); - tuning.interOpThreads = 1; - tuning.memPattern = false; - tuning.cpuArena = false; - tuning.sequentialExecution = true; - return tuning; - } - - tuning.intraOpThreads = std::clamp(clampedThreads, 1, 16); - tuning.interOpThreads = std::clamp(clampedThreads / 2, 1, 8); - tuning.memPattern = true; - tuning.cpuArena = true; - tuning.sequentialExecution = false; - return tuning; -} -#endif #if AUTOMIX_HAS_NATIVE_ORT @@ -166,64 +115,10 @@ std::string makeProfilePrefix(const std::filesystem::path& modelPath) { return (base / (stem + "_" + timeTag)).string(); } -void appendExecutionProvider(Ort::SessionOptions& options, const std::string& provider) { - const auto normalized = canonicalProviderName(provider); - if (normalized == "cpu" || normalized == "auto" || normalized.empty()) { - return; - } - std::unordered_map providerOptions; - if (normalized == gpu::kProviderCuda) { - // CUDA provider with default device ID 0 - providerOptions["device_id"] = "0"; - providerOptions["cudnn_conv_algo_search"] = "DEFAULT"; - options.AppendExecutionProvider("CUDA", providerOptions); - return; - } - if (normalized == gpu::kProviderDirectMl) { - // DirectML provider on default device - providerOptions["device_id"] = "0"; - options.AppendExecutionProvider("DML", providerOptions); - return; - } - if (normalized == gpu::kProviderCoreMl) { - providerOptions["ModelFormat"] = "MLProgram"; - options.AppendExecutionProvider("CoreML", providerOptions); - return; - } - if (normalized == gpu::kProviderAne) { - // Apple Neural Engine via CoreML with ANE override - providerOptions["ModelFormat"] = "MLProgram"; - providerOptions["ANEUnits"] = "256"; - options.AppendExecutionProvider("CoreML", providerOptions); - return; - } - if (normalized == gpu::kProviderOpenVino) { - // OpenVINO provider for Intel NPU / GPU - providerOptions["device_type"] = "CPU_FP32"; - options.AppendExecutionProvider("OpenVINO", providerOptions); - return; - } - if (normalized == "tensorrt") { - options.AppendExecutionProvider("Tensorrt", providerOptions); - return; - } -} std::vector discoverAvailableRuntimeProviders() { - std::vector providers = {"cpu"}; - try { - const auto runtimeProviders = Ort::GetAvailableProviders(); - providers.reserve(providers.size() + runtimeProviders.size()); - for (const auto& provider : runtimeProviders) { - providers.push_back(canonicalProviderName(provider)); - } - } catch (...) { - } - - std::sort(providers.begin(), providers.end()); - providers.erase(std::unique(providers.begin(), providers.end()), providers.end()); - return providers; + return OrtRuntime::instance().availableProviders(); } #endif @@ -232,7 +127,6 @@ std::vector discoverAvailableRuntimeProviders() { struct OnnxModelInference::NativeState { #if AUTOMIX_HAS_NATIVE_ORT - std::unique_ptr env; std::unique_ptr sessionOptions; std::unique_ptr session; std::vector inputNames; @@ -355,11 +249,11 @@ bool OnnxModelInference::loadModel(const std::filesystem::path& modelPath) { } if (availableExecutionProviders_.empty()) { - availableExecutionProviders_.push_back("cpu"); - const auto platformProvider = platformPreferredProvider(); - if (platformProvider != "cpu") { - availableExecutionProviders_.push_back(platformProvider); - } +#if AUTOMIX_HAS_NATIVE_ORT + availableExecutionProviders_ = OrtRuntime::instance().availableProviders(); +#else + availableExecutionProviders_ = {gpu::kProviderCpu}; +#endif } for (auto& provider : availableExecutionProviders_) { @@ -405,39 +299,23 @@ bool OnnxModelInference::loadModel(const std::filesystem::path& modelPath) { } auto nativeState = std::make_shared(); - nativeState->env = std::make_unique(ORT_LOGGING_LEVEL_WARNING, "AutoMixMaster"); nativeState->sessionOptions = std::make_unique(); const int hardwareThreads = static_cast(std::max(1u, std::thread::hardware_concurrency())); - auto tuning = tuningForProvider(activeExecutionProvider_, hardwareThreads); - if (intraOpThreads_ > 0) { - tuning.intraOpThreads = intraOpThreads_; - } - if (interOpThreads_ > 0) { - tuning.interOpThreads = interOpThreads_; - } - - nativeState->sessionOptions->SetIntraOpNumThreads(std::max(1, tuning.intraOpThreads)); - nativeState->sessionOptions->SetInterOpNumThreads(std::max(1, tuning.interOpThreads)); - nativeState->sessionOptions->SetExecutionMode( - tuning.sequentialExecution ? ExecutionMode::ORT_SEQUENTIAL : ExecutionMode::ORT_PARALLEL); + auto tuning = gpu::sessionTuning(activeExecutionProvider_, hardwareThreads); nativeState->sessionOptions->SetGraphOptimizationLevel( graphOptimizationEnabled_ ? GraphOptimizationLevel::ORT_ENABLE_ALL : GraphOptimizationLevel::ORT_DISABLE_ALL); - if (!tuning.memPattern) { - nativeState->sessionOptions->DisableMemPattern(); - } - if (!tuning.cpuArena) { - nativeState->sessionOptions->DisableCpuMemArena(); - } - if (profilingEnabled_) { nativeState->profilingPrefix = makeProfilePrefix(modelPath_); - nativeState->sessionOptions->EnableProfiling(nativeState->profilingPrefix.string().c_str()); + nativeState->sessionOptions->EnableProfiling(nativeState->profilingPrefix.c_str()); } + if (activeExecutionProvider_ != gpu::kProviderCpu) { + GpuRuntimePack::preload(); // per-user CUDA libraries, if installed + } try { - appendExecutionProvider(*nativeState->sessionOptions, activeExecutionProvider_); + configureSessionForProvider(*nativeState->sessionOptions, activeExecutionProvider_, hardwareThreads); } catch (const std::exception&) { providerFallbacks_.fetch_add(1); { @@ -445,14 +323,23 @@ bool OnnxModelInference::loadModel(const std::filesystem::path& modelPath) { failedProviders_.push_back(activeExecutionProvider_); } activeExecutionProvider_ = gpu::kProviderCpu; + tuning = gpu::sessionTuning(activeExecutionProvider_, hardwareThreads); + configureSessionForProvider(*nativeState->sessionOptions, activeExecutionProvider_, hardwareThreads); + } + + if (intraOpThreads_ > 0) { + nativeState->sessionOptions->SetIntraOpNumThreads(intraOpThreads_); + } + if (interOpThreads_ > 0) { + nativeState->sessionOptions->SetInterOpNumThreads(interOpThreads_); } #if defined(_WIN32) - nativeState->session = std::make_unique(*nativeState->env, + nativeState->session = std::make_unique(OrtRuntime::instance().env(), modelPath_.wstring().c_str(), *nativeState->sessionOptions); #else - nativeState->session = std::make_unique(*nativeState->env, + nativeState->session = std::make_unique(OrtRuntime::instance().env(), modelPath_.string().c_str(), *nativeState->sessionOptions); #endif @@ -515,7 +402,15 @@ bool OnnxModelInference::loadModel(const std::filesystem::path& modelPath) { gpuOomCount_.fetch_add(1); gpuRecoveryCount_.fetch_add(1); } - { + // A session that fails to open because of the model itself (missing, + // unparseable or invalid file) would fail on every provider; recording the + // GPU provider as failed would wrongly steer later resolution off it. CPU is + // the floor resolution always returns, so it is never recorded either. + const auto* ortError = dynamic_cast(&errorException); + const auto code = ortError != nullptr ? ortError->GetOrtErrorCode() : ORT_FAIL; + const bool modelProblem = code == ORT_NO_SUCHFILE || code == ORT_NO_MODEL || code == ORT_INVALID_PROTOBUF || + code == ORT_INVALID_GRAPH || code == ORT_MODEL_LOADED; + if (activeExecutionProvider_ != gpu::kProviderCpu && !modelProblem) { std::scoped_lock lock(failedProvidersMutex_); failedProviders_.push_back(activeExecutionProvider_); } @@ -908,6 +803,14 @@ void OnnxModelInference::recordProviderFailure(const std::string& provider, } } +void OnnxModelInference::pinExecutionProvidersForTesting(std::vector providers) { + availableExecutionProviders_ = providers; + pinnedProviders_ = std::move(providers); + if (loaded_) { + activeExecutionProvider_ = resolveExecutionProvider(); + } +} + void OnnxModelInference::setGraphOptimizationEnabled(const bool enabled) { graphOptimizationEnabled_ = enabled; } void OnnxModelInference::setWarmupEnabled(const bool enabled) { warmupEnabled_ = enabled; } @@ -978,12 +881,13 @@ std::string OnnxModelInference::resolveExecutionProvider() const { // Probe runtime providers and walk the priority chain std::vector runtimeProviders; + if (pinnedProviders_.has_value()) { + runtimeProviders = *pinnedProviders_; + } else { #if AUTOMIX_HAS_NATIVE_ORT - try { - runtimeProviders = Ort::GetAvailableProviders(); - } catch (...) { - } + runtimeProviders = OrtRuntime::instance().availableProviders(); #endif + } // If runtime probe succeeded, use it; otherwise fall back to metadata list const auto& probeProviders = runtimeProviders.empty() @@ -1062,25 +966,11 @@ void OnnxModelInference::captureProfilingArtifactIfNeeded() const { } std::vector OnnxModelInference::detectAvailableProviders() const { - std::vector providers = {gpu::kProviderCpu}; #if AUTOMIX_HAS_NATIVE_ORT - try { - const auto runtimeProviders = Ort::GetAvailableProviders(); - providers.reserve(1 + runtimeProviders.size()); - for (const auto& p : runtimeProviders) { - providers.push_back(gpu::canonicalProviderName(p)); - } - } catch (...) { - } - std::sort(providers.begin(), providers.end()); - providers.erase(std::unique(providers.begin(), providers.end()), providers.end()); - - std::stable_sort(providers.begin(), providers.end(), - [](const std::string& a, const std::string& b) { - return gpu::providerPriority(a) < gpu::providerPriority(b); - }); + return OrtRuntime::instance().availableProviders(); +#else + return {gpu::kProviderCpu}; #endif - return providers; } std::vector OnnxModelInference::failedProviders() const { diff --git a/src/ai/OnnxModelInference.h b/src/ai/OnnxModelInference.h index 700bf95..4841d6b 100644 --- a/src/ai/OnnxModelInference.h +++ b/src/ai/OnnxModelInference.h @@ -40,6 +40,11 @@ class OnnxModelInference final : public IModelInference { void recordProviderFailure(const std::string& provider, ProviderFailureKind kind); + // Test seam: replaces both the manifest provider list and the runtime probe, + // then re-resolves. Lets the provider-recovery bookkeeping be exercised on a + // machine whose ONNX Runtime has no GPU provider. + void pinExecutionProvidersForTesting(std::vector providers); + [[nodiscard]] std::string activeExecutionProvider() const; [[nodiscard]] std::string backendDiagnostics() const; [[nodiscard]] std::vector profilingArtifacts() const; @@ -75,6 +80,7 @@ class OnnxModelInference final : public IModelInference { std::vector outputNames_; std::vector allowedTasks_; std::vector availableExecutionProviders_; + std::optional> pinnedProviders_; std::string requestedExecutionProvider_ = "auto"; std::string activeExecutionProvider_ = "cpu"; std::string preferredPrecision_ = "auto"; diff --git a/src/ai/OnnxTensorInference.cpp b/src/ai/OnnxTensorInference.cpp new file mode 100644 index 0000000..3ec222e --- /dev/null +++ b/src/ai/OnnxTensorInference.cpp @@ -0,0 +1,574 @@ +#include "ai/OnnxTensorInference.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef AUTOMIX_HAS_NATIVE_ORT +#define AUTOMIX_HAS_NATIVE_ORT 0 +#endif + +#include + +#include "ai/GpuProvider.h" +#include "ai/GpuRuntimePack.h" +#include "ai/OrtRuntime.h" + +#if AUTOMIX_HAS_NATIVE_ORT +#include + +#include "ai/OrtSessionProviders.h" +#endif + +namespace automix::ai { +namespace { + +#if AUTOMIX_HAS_NATIVE_ORT + +std::string describeOnnxElementType(const ONNXTensorElementDataType type) { + switch (type) { + case ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT: return "float32"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8: return "uint8"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8: return "int8"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT16: return "uint16"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT16: return "int16"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32: return "int32"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64: return "int64"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_STRING: return "string"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_BOOL: return "bool"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT16: return "float16"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE: return "float64"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT32: return "uint32"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT64: return "uint64"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_COMPLEX64: return "complex64"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_COMPLEX128: return "complex128"; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16: return "bfloat16"; + default: return "onnx element type " + std::to_string(static_cast(type)); + } +} + +// Reads one graph input or output back as a TensorSpec. Anything that is not a +// float32 tensor is refused here, naming the tensor and what it actually is. +bool probeSpec(const std::string& role, + const std::string& name, + const Ort::TypeInfo& info, + std::vector& specs, + std::string& errorOut) { + if (info.GetONNXType() != ONNX_TYPE_TENSOR) { + errorOut = "graph " + role + " '" + name + "' is not a tensor (ONNX value type " + + std::to_string(static_cast(info.GetONNXType())) + "); only float32 tensors are supported."; + return false; + } + const auto tensorInfo = info.GetTensorTypeAndShapeInfo(); + const auto elementType = tensorInfo.GetElementType(); + if (elementType != ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT) { + errorOut = "graph " + role + " '" + name + "' has element type " + describeOnnxElementType(elementType) + + "; only float32 is supported."; + return false; + } + specs.push_back(TensorSpec{name, TensorElementType::Float32, tensorInfo.GetShape()}); + return true; +} + +bool isConcrete(const std::vector& dims) { + for (const auto dim : dims) { + if (dim <= 0) { + return false; + } + } + return true; +} + +#endif + +} // namespace + +std::vector tensorProviderCandidates(const std::string& requested, + const std::vector& runtimeProviders, + const std::vector& allowList, + const bool autoAllowCoreMl) { + std::vector reported; + for (const auto& provider : runtimeProviders) { + reported.push_back(gpu::canonicalProviderName(provider)); + } + const auto isReported = [&reported](const std::string& provider) { + return std::find(reported.begin(), reported.end(), provider) != reported.end(); + }; + + std::vector canonicalAllow; + for (const auto& a : allowList) { + canonicalAllow.push_back(gpu::canonicalProviderName(a)); + } + const auto isAllowed = [&canonicalAllow](const std::string& provider) { + if (canonicalAllow.empty()) return true; + return std::find(canonicalAllow.begin(), canonicalAllow.end(), provider) != canonicalAllow.end(); + }; + + std::vector candidates; + const auto wanted = gpu::canonicalProviderName(requested.empty() ? std::string("auto") : requested); + if (wanted == gpu::kProviderCpu) { + return {gpu::kProviderCpu}; + } + // A named GPU provider is tried first; if this runtime lacks it or it's not allowed, + // the request still means "a GPU" (e.g. a DirectML preference on a CUDA build), + // so the remaining reported GPU providers follow before CPU. + if (wanted != "auto" && isReported(wanted) && isAllowed(wanted)) { + candidates.push_back(wanted); + } + for (const auto& provider : gpu::providerPriorityChain()) { + // On low-memory Macs "auto" skips CoreML/ANE (see coreMlAutoAllowed); naming one still works. + const bool appleSkippedForAuto = + wanted == "auto" && !autoAllowCoreMl && (provider == gpu::kProviderCoreMl || provider == gpu::kProviderAne); + if (provider != gpu::kProviderCpu && provider != wanted && isReported(provider) && isAllowed(provider) && + !appleSkippedForAuto) { + candidates.push_back(provider); + } + } + candidates.emplace_back(gpu::kProviderCpu); + return candidates; +} + +namespace { + +#if AUTOMIX_HAS_NATIVE_ORT +void putVarint(std::string& out, std::uint64_t value) { + while (value >= 0x80) { + out.push_back(static_cast((value & 0x7F) | 0x80)); + value >>= 7; + } + out.push_back(static_cast(value)); +} +std::string varintField(std::uint32_t number, std::uint64_t value) { + std::string out; + putVarint(out, static_cast(number) << 3); + putVarint(out, value); + return out; +} +std::string bytesField(std::uint32_t number, const std::string& payload) { + std::string out; + putVarint(out, (static_cast(number) << 3) | 2); + putVarint(out, payload.size()); + return out + payload; +} + +// y = Identity(x), x and y float[1], IR 8 / opset 17: the smallest graph that +// makes a session initialise its execution provider and device. +std::string identityProbeModel() { + const auto valueInfo = [](const std::string& name) { + const auto dim = bytesField(1, varintField(1, 1)); // Dimension.dim_value = 1 + const auto tensorType = varintField(1, 1) + bytesField(2, dim); // elem_type FLOAT, shape + return bytesField(1, name) + bytesField(2, bytesField(1, tensorType)); + }; + const auto node = bytesField(1, "x") + bytesField(2, "y") + bytesField(4, "Identity"); + const auto graph = bytesField(1, node) + bytesField(2, "gpu_probe") + bytesField(11, valueInfo("x")) + + bytesField(12, valueInfo("y")); + return varintField(1, 8) + bytesField(8, bytesField(1, "") + varintField(2, 17)) + bytesField(7, graph); +} +#endif + +} // namespace + +bool runtimeReportsProvider(const std::string& provider) { +#if AUTOMIX_HAS_NATIVE_ORT + try { + const auto wanted = gpu::canonicalProviderName(provider); + for (const auto& reported : OrtRuntime::instance().availableProviders()) { + if (gpu::canonicalProviderName(reported) == wanted) { + return true; + } + } + } catch (...) { + } +#else + (void)provider; +#endif + return false; +} + +namespace { +std::mutex s_probeMutex; +std::unordered_set s_usableProviders; +std::unordered_set s_unusableProviders; +TensorProviderProbeFn s_testProbeFn = nullptr; +} // namespace + +void setTensorProviderProbeFunctionForTesting(TensorProviderProbeFn fn) { + const std::scoped_lock lock(s_probeMutex); + s_testProbeFn = std::move(fn); +} + +void invalidateTensorProviderProbeCache() { + const std::scoped_lock lock(s_probeMutex); + s_usableProviders.clear(); + s_unusableProviders.clear(); +} + +void resetTensorProviderProbeCacheForTesting() { + const std::scoped_lock lock(s_probeMutex); + s_usableProviders.clear(); + s_unusableProviders.clear(); + s_testProbeFn = nullptr; +} + +bool tensorProviderUsable(const std::string& canonical) { + const auto canon = gpu::canonicalProviderName(canonical); + if (canon == gpu::kProviderCpu) { + return true; + } + + const std::scoped_lock lock(s_probeMutex); + if (s_usableProviders.count(canon) > 0) { + return true; + } + if (s_unusableProviders.count(canon) > 0) { + return false; + } + + if (s_testProbeFn != nullptr) { + if (s_testProbeFn(canon)) { + s_usableProviders.insert(canon); + return true; + } + s_unusableProviders.insert(canon); + return false; + } + +#if AUTOMIX_HAS_NATIVE_ORT + const auto available = OrtRuntime::instance().availableProviders(); + if (std::find(available.begin(), available.end(), canon) == available.end()) { + s_unusableProviders.insert(canon); + return false; + } + + if (canon == gpu::kProviderCuda) { + GpuRuntimePack::preload(); + } + try { + const auto model = identityProbeModel(); + Ort::SessionOptions options; + // untuned: keep ORT's thread defaults (see gpu::sessionConfigPlan) + configureSessionForProvider(options, canon); + Ort::Session session(OrtRuntime::instance().env(), model.data(), model.size(), options); + s_usableProviders.insert(canon); + return true; + } catch (...) { + s_unusableProviders.insert(canon); + return false; + } +#else + return false; +#endif +} + +bool gpuTensorSessionAvailable(std::string* providerOut) { + for (const auto& candidate : gpu::providerPriorityChain()) { + if (candidate == gpu::kProviderCpu) { + break; + } + if (!coreMlAutoAllowed() && (candidate == gpu::kProviderCoreMl || candidate == gpu::kProviderAne)) { + continue; + } + if (tensorProviderUsable(candidate)) { + if (providerOut != nullptr) { + *providerOut = candidate; + } + return true; + } + } + if (providerOut != nullptr) { + providerOut->clear(); + } + return false; +} + +struct OnnxTensorInference::NativeState { +#if AUTOMIX_HAS_NATIVE_ORT + std::unique_ptr session; +#endif +}; + +OnnxTensorInference::OnnxTensorInference() = default; +OnnxTensorInference::~OnnxTensorInference() noexcept = default; + +void OnnxTensorInference::setTensorContract(std::optional contract) { contract_ = std::move(contract); } + +void OnnxTensorInference::setExecutionProvider(std::string provider) { requestedProvider_ = std::move(provider); } + +void OnnxTensorInference::setGpuProviderAllowList(std::vector allowList) { + gpuProviderAllowList_ = std::move(allowList); +} + +std::string OnnxTensorInference::activeExecutionProvider() const { return activeProvider_; } + +bool OnnxTensorInference::isAvailable() const { return nativeState_ != nullptr; } + +bool OnnxTensorInference::usingNativeSession() const { return nativeState_ != nullptr; } + +std::string OnnxTensorInference::backendDiagnostics() const { return diagnostics_; } + +std::vector OnnxTensorInference::inputSpecs() const { return inputs_; } + +std::vector OnnxTensorInference::outputSpecs() const { return outputs_; } + +void OnnxTensorInference::unload(std::string diagnostics) { + nativeState_.reset(); + activeProvider_.clear(); + inputs_.clear(); + outputs_.clear(); + diagnostics_ = std::move(diagnostics); +} + +bool OnnxTensorInference::loadModel(const std::filesystem::path& modelPath) { + std::error_code error; + if (!std::filesystem::is_regular_file(modelPath, error) || error) { + unload("ONNX tensor load failed: missing model file '" + modelPath.string() + "'."); + return false; + } + +#if AUTOMIX_HAS_NATIVE_ORT + const auto runtimeProviders = OrtRuntime::instance().availableProviders(); + if (gpu::canonicalProviderName(requestedProvider_) != gpu::kProviderCpu) { + GpuRuntimePack::preload(); // CUDA libraries installed per user, if any + } + const auto candidates = tensorProviderCandidates(requestedProvider_, runtimeProviders, gpuProviderAllowList_); + + std::unique_ptr state; + std::vector inputs; + std::vector outputs; + std::string provider; + std::string attempts; // why each provider before the winner was passed over + for (const auto& candidate : candidates) { + auto attempt = std::make_unique(); + try { + Ort::SessionOptions options; + options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); + // untuned: keep ORT's thread defaults (see gpu::sessionConfigPlan) + configureSessionForProvider(options, candidate); +#if defined(_WIN32) + attempt->session = std::make_unique(OrtRuntime::instance().env(), modelPath.wstring().c_str(), options); +#else + attempt->session = std::make_unique(OrtRuntime::instance().env(), modelPath.string().c_str(), options); +#endif + } catch (const std::exception& exception) { + if (candidate == gpu::kProviderCpu) { + // Session creation is where a missing external-data sidecar surfaces; ORT's + // message names the file it could not open. + unload("ONNX tensor load failed for '" + modelPath.string() + "': " + attempts + exception.what()); + return false; + } + attempts += candidate + " unavailable (" + exception.what() + "); "; + continue; + } + state = std::move(attempt); + provider = candidate; + break; + } + + try { + Ort::AllocatorWithDefaultOptions allocator; + std::string probeError; + for (std::size_t i = 0; i < state->session->GetInputCount(); ++i) { + const auto name = std::string(state->session->GetInputNameAllocated(i, allocator).get()); + if (!probeSpec("input", name, state->session->GetInputTypeInfo(i), inputs, probeError)) { + unload("ONNX tensor load failed for '" + modelPath.string() + "': " + probeError); + return false; + } + } + for (std::size_t i = 0; i < state->session->GetOutputCount(); ++i) { + const auto name = std::string(state->session->GetOutputNameAllocated(i, allocator).get()); + if (!probeSpec("output", name, state->session->GetOutputTypeInfo(i), outputs, probeError)) { + unload("ONNX tensor load failed for '" + modelPath.string() + "': " + probeError); + return false; + } + } + } catch (const std::exception& exception) { + unload("ONNX tensor load failed for '" + modelPath.string() + "': " + exception.what()); + return false; + } + if (contract_.has_value()) { + std::string contractError; + if (!checkTensorContract(*contract_, inputs, outputs, contractError)) { + unload("ONNX tensor load failed for '" + modelPath.string() + "': tensor contract mismatch: " + contractError); + return false; + } + } + + inputs_ = std::move(inputs); + outputs_ = std::move(outputs); + nativeState_ = std::move(state); + activeProvider_ = provider; + diagnostics_ = "backend=native_onnxruntime; provider=" + provider + "; model=" + modelPath.filename().string() + + "; inputs=" + std::to_string(inputs_.size()) + "; outputs=" + std::to_string(outputs_.size()) + + (attempts.empty() ? std::string() : "; fallback: " + attempts) + + "; runtime: " + OrtRuntime::instance().diagnostics(); + return true; +#else + unload("ONNX tensor load failed for '" + modelPath.string() + + "': this build has no native ONNX Runtime (AUTOMIX_HAS_NATIVE_ORT is off)."); + return false; +#endif +} + +TensorInferenceResult OnnxTensorInference::run(const std::vector& inputs) const { + return runImpl(inputs, nullptr); +} + +TensorInferenceResult OnnxTensorInference::runCancellable(const std::vector& inputs, + const std::function& cancelRequested) const { + return runImpl(inputs, cancelRequested ? &cancelRequested : nullptr); +} + +TensorInferenceResult OnnxTensorInference::runImpl(const std::vector& inputs, + const std::function* cancelRequested) const { + TensorInferenceResult result; + if (nativeState_ == nullptr) { + result.logMessage = "OnnxTensorInference: no model loaded (" + diagnostics_ + ")"; + return result; + } + +#if AUTOMIX_HAS_NATIVE_ORT + if (inputs.size() != inputs_.size()) { + result.logMessage = "OnnxTensorInference: graph declares " + std::to_string(inputs_.size()) + + " input(s) but " + std::to_string(inputs.size()) + " binding(s) were supplied."; + return result; + } + + // ORT takes a mutable pointer; copying keeps the caller's bindings const + // without casting it away. The copy is small next to the inference itself. + std::vector> buffers; + std::vector> shapes; + std::vector inputNames; + buffers.reserve(inputs_.size()); + shapes.reserve(inputs_.size()); + inputNames.reserve(inputs_.size()); + for (const auto& spec : inputs_) { + const TensorBinding* binding = nullptr; + for (const auto& candidate : inputs) { + if (candidate.expected.name == spec.name) { + binding = &candidate; + break; + } + } + if (binding == nullptr) { + result.logMessage = "OnnxTensorInference: no binding for graph input '" + spec.name + "'."; + return result; + } + const auto& dims = binding->expected.dims; + if (!isConcrete(dims) || !shapesMatch(spec, binding->expected)) { + result.logMessage = "OnnxTensorInference: binding for '" + spec.name + "' has shape " + describeShape(dims) + + " but the graph declares " + describeShape(spec.dims) + "."; + return result; + } + const auto expectedCount = elementCount(binding->expected); + if (!expectedCount.has_value() || *expectedCount != binding->data.size()) { + result.logMessage = "OnnxTensorInference: binding for '" + spec.name + "' carries " + + std::to_string(binding->data.size()) + " value(s) for shape " + describeShape(dims) + "."; + return result; + } + buffers.push_back(binding->data); + shapes.push_back(dims); + inputNames.push_back(spec.name.c_str()); + } + + std::vector outputNames; + outputNames.reserve(outputs_.size()); + for (const auto& spec : outputs_) { + outputNames.push_back(spec.name.c_str()); + } + + try { + const auto memoryInfo = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); + std::vector values; + values.reserve(buffers.size()); + for (std::size_t i = 0; i < buffers.size(); ++i) { + values.push_back(Ort::Value::CreateTensor( + memoryInfo, buffers[i].data(), buffers[i].size(), shapes[i].data(), shapes[i].size())); + } + + // A watcher polls the cancel check while the native run executes and + // terminates it; ORT then throws, which the catch below reports. + Ort::RunOptions runOptions; + if (activeProvider_ == gpu::kProviderCuda) { + // Return the run's unused arena memory to the device afterwards. Without + // it, the second BS-RoFormer chunk grows the arena past a 16 GB card and + // the driver spills into shared system memory (~5x slower per chunk). + // Requires arena_extend_strategy=kSameAsRequested (OrtSessionProviders.h). + runOptions.AddConfigEntry("memory.enable_memory_arena_shrinkage", "gpu:0"); + } + std::atomic runFinished{false}; + std::thread watcher; + if (cancelRequested != nullptr) { + watcher = std::thread([&runOptions, &runFinished, cancelRequested] { + while (!runFinished.load()) { + if ((*cancelRequested)()) { + runOptions.SetTerminate(); + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + } + }); + } + struct WatcherJoin { + std::atomic& finished; + std::thread& thread; + ~WatcherJoin() { + finished.store(true); + if (thread.joinable()) { + thread.join(); + } + } + } watcherJoin{runFinished, watcher}; + + auto produced = nativeState_->session->Run(runOptions, + inputNames.data(), + values.data(), + values.size(), + outputNames.data(), + outputNames.size()); + + for (std::size_t i = 0; i < produced.size(); ++i) { + const auto& value = produced[i]; + if (!value.IsTensor()) { + result.logMessage = "OnnxTensorInference: graph output '" + outputs_[i].name + "' is not a tensor."; + return result; + } + const auto info = value.GetTensorTypeAndShapeInfo(); + if (info.GetElementType() != ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT) { + result.logMessage = "OnnxTensorInference: graph output '" + outputs_[i].name + "' has element type " + + describeOnnxElementType(info.GetElementType()) + "; only float32 is supported."; + return result; + } + const float* data = value.GetTensorData(); + Tensor tensor; + tensor.spec = TensorSpec{outputs_[i].name, TensorElementType::Float32, info.GetShape()}; + tensor.data.assign(data, data + info.GetElementCount()); + result.outputs.push_back(std::move(tensor)); + } + } catch (const std::exception& exception) { + result.outputs.clear(); + result.logMessage = std::string("OnnxTensorInference: run failed: ") + exception.what(); + return result; + } + + result.usedModel = true; + result.logMessage = "OnnxTensorInference: ran " + std::to_string(inputs_.size()) + " input(s) -> " + + std::to_string(result.outputs.size()) + " output(s)."; + return result; +#else + static_cast(inputs); + static_cast(cancelRequested); + result.logMessage = "OnnxTensorInference: this build has no native ONNX Runtime."; + return result; +#endif +} + +} // namespace automix::ai diff --git a/src/ai/OnnxTensorInference.h b/src/ai/OnnxTensorInference.h new file mode 100644 index 0000000..e3f2f41 --- /dev/null +++ b/src/ai/OnnxTensorInference.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "ai/GpuMemory.h" +#include "ai/ITensorInference.h" +#include "ai/ModelPackLoader.h" + +namespace automix::ai { + +// Ordered execution providers a tensor session tries; always ends with "cpu". +// "auto" walks gpu::providerPriorityChain() over the providers this runtime +// build reports; a named provider goes first if reported, and is otherwise +// read as "any GPU" so the reported ones are still tried; "cpu" is CPU only. +// Reported is not the same as usable (CUDA may lack its DLLs), which is why +// loadModel() treats every non-CPU entry as an attempt that may fail. +std::vector tensorProviderCandidates(const std::string& requested, + const std::vector& runtimeProviders, + const std::vector& allowList = {}, + bool autoAllowCoreMl = coreMlAutoAllowed()); + +// True when a GPU execution provider (e.g. "cuda", "webgpu") is proven usable on this system +// by opening a real session on a tiny in-memory graph. Successes are cached per provider. +// CPU always returns true. Always false without native ONNX Runtime. +bool tensorProviderUsable(const std::string& canonical); + +// Test injection hooks for probe-cache semantics verification without hardware. +using TensorProviderProbeFn = std::function; +void setTensorProviderProbeFunctionForTesting(TensorProviderProbeFn fn); +void resetTensorProviderProbeCacheForTesting(); + +// Call after the GPU runtime pack is installed, removed or upgraded. +void invalidateTensorProviderProbeCache(); + +// True when a GPU tensor session can actually be opened here, proven by +// opening one on a tiny in-memory graph (a reported provider can still lack +// its DLLs or a device). Walks providerPriorityChain() and returns the first usable +// GPU provider. False without native ONNX Runtime or when no GPU session opens. +bool gpuTensorSessionAvailable(std::string* providerOut = nullptr); + +// Whether this ONNX Runtime build contains `provider` (e.g. "cuda") at all, +// regardless of whether its own libraries are present. False without native ORT. +bool runtimeReportsProvider(const std::string& provider); + +// ONNX Runtime backend for tensor graphs. Without the native SDK +// (AUTOMIX_HAS_NATIVE_ORT undefined) it is a deterministic no-op: every load +// fails with a diagnostic and nothing ever reports usedModel == true. There is +// no approximate fallback, because a tensor graph has none that is honest. +class OnnxTensorInference final : public ITensorInference { + public: + OnnxTensorInference(); + ~OnnxTensorInference() noexcept override; + + // Checked against the probed graph on the next loadModel(); a mismatch fails + // the load. Without a contract the graph only has to be float32 throughout. + void setTensorContract(std::optional contract); + // "auto" (default), "cpu", or a provider name such as "cuda". Applies to + // the next loadModel(). + void setExecutionProvider(std::string provider); + // Restricts GPU candidates to this list when non-empty. CPU is always kept. + void setGpuProviderAllowList(std::vector allowList); + // Provider the loaded session actually runs on ("cpu", "cuda", ...), or + // empty when nothing is loaded. + [[nodiscard]] std::string activeExecutionProvider() const; + + bool isAvailable() const override; + bool loadModel(const std::filesystem::path& modelPath) override; + std::vector inputSpecs() const override; + std::vector outputSpecs() const override; + TensorInferenceResult run(const std::vector& inputs) const override; + // Aborts an in-flight native run within ~25 ms of cancelRequested() turning true. + TensorInferenceResult runCancellable(const std::vector& inputs, + const std::function& cancelRequested) const override; + + [[nodiscard]] bool usingNativeSession() const; + [[nodiscard]] std::string backendDiagnostics() const; + + private: + struct NativeState; + + void unload(std::string diagnostics); + TensorInferenceResult runImpl(const std::vector& inputs, + const std::function* cancelRequested) const; + + std::optional contract_; + std::string requestedProvider_ = "auto"; + std::vector gpuProviderAllowList_; + std::string activeProvider_; + std::vector inputs_; + std::vector outputs_; + std::string diagnostics_; + std::unique_ptr nativeState_; +}; + +} // namespace automix::ai diff --git a/src/ai/OrtRuntime.cpp b/src/ai/OrtRuntime.cpp new file mode 100644 index 0000000..c6c9253 --- /dev/null +++ b/src/ai/OrtRuntime.cpp @@ -0,0 +1,220 @@ +#include "ai/OrtRuntime.h" + +#if AUTOMIX_HAS_NATIVE_ORT + +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +#include "ai/GpuProvider.h" + +namespace automix::ai { +namespace { + +std::filesystem::path resolveOrtCoreLibraryPath() { +#if defined(_WIN32) + HMODULE hModule = nullptr; + if (GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&OrtGetApiBase), + &hModule) && hModule != nullptr) { + std::vector path(32768, L'\0'); + const DWORD len = GetModuleFileNameW(hModule, path.data(), static_cast(path.size())); + if (len > 0 && len < path.size()) { + return std::filesystem::path(path.data()); + } + } +#else + Dl_info info; + if (dladdr(reinterpret_cast(&OrtGetApiBase), &info) && info.dli_fname != nullptr) { + return std::filesystem::path(info.dli_fname); + } +#endif + return {}; +} + +std::filesystem::path resolveWebGpuPluginPath() { + const auto corePath = resolveOrtCoreLibraryPath(); + if (corePath.empty()) { + return {}; + } +#if defined(_WIN32) + const auto candidate = corePath.parent_path() / "onnxruntime_providers_webgpu.dll"; + if (std::filesystem::exists(candidate)) { + return candidate; + } +#elif defined(__linux__) + const auto candidate = corePath.parent_path() / "libonnxruntime_providers_webgpu.so"; + if (std::filesystem::exists(candidate)) { + return candidate; + } +#endif + return {}; +} + +} // namespace + +OrtRuntime& OrtRuntime::instance() { + // Intentionally leaked to prevent Ort::Env being destroyed during static teardown + // while other statics might still hold Ort::Session instances. + static OrtRuntime* s_instance = new OrtRuntime(); + return *s_instance; +} + +OrtRuntime::OrtRuntime() = default; + +void OrtRuntime::warmUpAsync() { + std::lock_guard lock(warmUpMutex_); + if (warmUpThread_.joinable() || warmUpStarted_) { + return; + } + warmUpStarted_ = true; + warmUpThread_ = std::thread([this]() { + ensureInitialized(); + }); +} + +void OrtRuntime::joinWarmUp() { + std::lock_guard lock(warmUpMutex_); + if (warmUpThread_.joinable()) { + warmUpThread_.join(); + } +} + +void OrtRuntime::ensureInitialized() { + std::call_once(initOnce_, [this]() { + try { + env_ = std::make_unique(ORT_LOGGING_LEVEL_WARNING, "AutoMixMasterGlobal"); + } catch (const std::exception& e) { + initError_ = "Ort::Env initialization failed: " + std::string(e.what()); + diagnostics_ = initError_; + availableProviders_ = {gpu::kProviderCpu}; + return; + } catch (...) { + initError_ = "Ort::Env initialization failed with unknown exception"; + diagnostics_ = initError_; + availableProviders_ = {gpu::kProviderCpu}; + return; + } + + std::vector rawProviders; + try { + rawProviders = Ort::GetAvailableProviders(); + } catch (...) { + } + + for (const auto& p : rawProviders) { + availableProviders_.push_back(gpu::canonicalProviderName(p)); + } + + // Attempt WebGPU plugin registration on Windows / Linux + const auto pluginPath = resolveWebGpuPluginPath(); + const auto ortVersion = gpu::parseOrtVersion(Ort::GetVersionString()); + const bool compiledIn = (AUTOMIX_HAS_EP_PLUGIN != 0); + + const auto u8Plugin = pluginPath.u8string(); + const std::string pluginPathStr(u8Plugin.begin(), u8Plugin.end()); + const auto decision = gpu::decidePluginEpAttempt(compiledIn, ortVersion, "webgpu", pluginPathStr); + + diagnostics_ = "ORT version: " + std::string(Ort::GetVersionString()); +#ifdef AUTOMIX_ORT_EXPECTED_VERSION + const std::string runtimeVer = Ort::GetVersionString(); + if (runtimeVer != AUTOMIX_ORT_EXPECTED_VERSION) { + diagnostics_ += " (WARNING: runtime version " + runtimeVer + " != expected " + AUTOMIX_ORT_EXPECTED_VERSION + ")"; + } +#endif + +#if AUTOMIX_HAS_EP_PLUGIN + if (decision.attempt) { + try { + env_->RegisterExecutionProviderLibrary("webgpu_ep", pluginPath.c_str()); + diagnostics_ += "; WebGPU plugin registered from " + pluginPathStr; + + auto devices = env_->GetEpDevices(); + for (const auto& dev : devices) { + const std::string name = dev.EpName() ? dev.EpName() : ""; + if (name.find("WebGpu") != std::string::npos || name.find("webgpu") != std::string::npos) { + webGpuDevices_.push_back(dev); + } + } + + if (!webGpuDevices_.empty()) { + // TODO(measure): hybrid GPUs - confirm the WebGPU EP's first device is the discrete adapter (plan Phase F) + const auto& chosen = webGpuDevices_[0]; + const char* vendorStr = chosen.Device().Vendor() ? chosen.Device().Vendor() : "unknown"; + const auto devId = chosen.Device().DeviceId(); + const auto devType = static_cast(chosen.Device().Type()); + diagnostics_ += "; " + std::to_string(webGpuDevices_.size()) + " WebGPU device(s) found (chosen: vendor=" + + vendorStr + ", deviceId=" + std::to_string(devId) + ", type=" + std::to_string(devType) + ")"; + if (std::find(availableProviders_.begin(), availableProviders_.end(), gpu::kProviderWebGpu) == + availableProviders_.end()) { + availableProviders_.push_back(gpu::kProviderWebGpu); + } + } else { + diagnostics_ += "; no WebGPU device discovered (adapter unsupported or headless)"; + } + } catch (const std::exception& e) { + diagnostics_ += "; WebGPU registration failed (" + std::string(e.what()) + ")"; + } + } else { + diagnostics_ += "; WebGPU plugin registration skipped: " + decision.reason; + } +#else + diagnostics_ += "; WebGPU plugin: not compiled in (ONNX Runtime headers lack the plugin-EP API)"; +#endif + + std::sort(availableProviders_.begin(), availableProviders_.end()); + availableProviders_.erase( + std::unique(availableProviders_.begin(), availableProviders_.end()), + availableProviders_.end()); + std::stable_sort(availableProviders_.begin(), availableProviders_.end(), + [](const std::string& a, const std::string& b) { + return gpu::providerPriority(a) < gpu::providerPriority(b); + }); + }); +} + +Ort::Env& OrtRuntime::env() { + ensureInitialized(); + if (!env_) { + throw std::runtime_error(initError_.empty() ? "Ort::Env not initialized" : initError_); + } + return *env_; +} + +std::vector OrtRuntime::availableProviders() { + ensureInitialized(); + return availableProviders_; +} + +#if AUTOMIX_HAS_EP_PLUGIN +const std::vector& OrtRuntime::webGpuDevices() { + ensureInitialized(); + return webGpuDevices_; +} +#endif + +std::string OrtRuntime::diagnostics() { + ensureInitialized(); + return diagnostics_; +} + +} // namespace automix::ai + +#endif // AUTOMIX_HAS_NATIVE_ORT + diff --git a/src/ai/OrtRuntime.h b/src/ai/OrtRuntime.h new file mode 100644 index 0000000..7db557a --- /dev/null +++ b/src/ai/OrtRuntime.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include + +#ifndef AUTOMIX_HAS_NATIVE_ORT +#define AUTOMIX_HAS_NATIVE_ORT 0 +#endif + +#ifndef AUTOMIX_HAS_EP_PLUGIN +#define AUTOMIX_HAS_EP_PLUGIN 0 +#endif + +#if AUTOMIX_HAS_NATIVE_ORT +#include +#endif + +namespace automix::ai { + +#if AUTOMIX_HAS_NATIVE_ORT + +/// Process-wide ONNX Runtime environment and device discovery manager. +/// Owns the single Ort::Env instance, registers provider plugins (such as WebGPU), +/// and manages available device discovery. +class OrtRuntime { + public: + static OrtRuntime& instance(); + + void warmUpAsync(); + /// Blocks until the warm-up thread (if any) has finished. Safe to call repeatedly. + void joinWarmUp(); + + Ort::Env& env(); + std::vector availableProviders(); +#if AUTOMIX_HAS_EP_PLUGIN + const std::vector& webGpuDevices(); +#endif + std::string diagnostics(); + + private: + OrtRuntime(); + ~OrtRuntime() = default; + + OrtRuntime(const OrtRuntime&) = delete; + OrtRuntime& operator=(const OrtRuntime&) = delete; + + void ensureInitialized(); + + std::unique_ptr env_; + std::vector availableProviders_; +#if AUTOMIX_HAS_EP_PLUGIN + std::vector webGpuDevices_; +#endif + std::string diagnostics_; + std::string initError_; + std::once_flag initOnce_; + std::mutex warmUpMutex_; + std::thread warmUpThread_; + bool warmUpStarted_ = false; +}; + +#else + +class OrtRuntime { + public: + static OrtRuntime& instance() { + static OrtRuntime s_instance; + return s_instance; + } + + void warmUpAsync() {} + void joinWarmUp() {} + + std::vector availableProviders() { return {"cpu"}; } + std::string diagnostics() { return "ONNX Runtime native SDK not enabled."; } + + private: + OrtRuntime() = default; + ~OrtRuntime() = default; +}; + +#endif + +} // namespace automix::ai diff --git a/src/ai/OrtSessionProviders.h b/src/ai/OrtSessionProviders.h new file mode 100644 index 0000000..6a0d97a --- /dev/null +++ b/src/ai/OrtSessionProviders.h @@ -0,0 +1,103 @@ +#pragma once + +// Native-ORT builds only: include after checking AUTOMIX_HAS_NATIVE_ORT. + +#include +#include +#include +#include +#include + +#include + +#include "ai/GpuProvider.h" +#include "ai/OrtRuntime.h" + +namespace automix::ai { + +// Configures an Ort::SessionOptions with provider-specific tuning (threads, memory pattern, +// arena, execution mode) AND appends the execution provider. +// This unifies session configuration for both OnnxModelInference and OnnxTensorInference. +inline void configureSessionForProvider(Ort::SessionOptions& options, + const std::string& canonical, + int hardwareThreads = 0) { + const auto canon = gpu::canonicalProviderName(canonical); + const auto plan = gpu::sessionConfigPlan(canon, hardwareThreads); + if (plan.intraOpThreads) options.SetIntraOpNumThreads(*plan.intraOpThreads); + if (plan.interOpThreads) options.SetInterOpNumThreads(*plan.interOpThreads); + if (plan.sequentialExecution) + options.SetExecutionMode(*plan.sequentialExecution ? ExecutionMode::ORT_SEQUENTIAL + : ExecutionMode::ORT_PARALLEL); + if (plan.memPattern) { if (*plan.memPattern) options.EnableMemPattern(); else options.DisableMemPattern(); } + if (plan.cpuArena) { if (*plan.cpuArena) options.EnableCpuMemArena(); else options.DisableCpuMemArena(); } + + if (canon == gpu::kProviderCpu || canon == "auto" || canon.empty()) { + return; + } + + // CUDA and TensorRT are not accepted by the generic string-keyed + // AppendExecutionProvider(); they need their dedicated V2 option objects. + // In a build without them, Create*ProviderOptions fails and throws here. + if (canon == gpu::kProviderCuda) { + const auto& api = Ort::GetApi(); + OrtCUDAProviderOptionsV2* cuda = nullptr; + Ort::ThrowOnError(api.CreateCUDAProviderOptions(&cuda)); + const std::unique_ptr owned( + cuda, api.ReleaseCUDAProviderOptions); + // kSameAsRequested: the default power-of-two arena growth over-reserves + // VRAM on large graphs until the driver spills into shared system memory, + // which on BS-RoFormer made CUDA slower than the CPU. + const char* keys[] = {"device_id", "cudnn_conv_algo_search", "arena_extend_strategy"}; + const char* values[] = {"0", "DEFAULT", "kSameAsRequested"}; + Ort::ThrowOnError(api.UpdateCUDAProviderOptions(cuda, keys, values, 3)); + options.AppendExecutionProvider_CUDA_V2(*cuda); + return; + } + if (canon == "tensorrt") { + const auto& api = Ort::GetApi(); + OrtTensorRTProviderOptionsV2* tensorrt = nullptr; + Ort::ThrowOnError(api.CreateTensorRTProviderOptions(&tensorrt)); + const std::unique_ptr owned( + tensorrt, api.ReleaseTensorRTProviderOptions); + options.AppendExecutionProvider_TensorRT_V2(*tensorrt); + return; + } + + const auto providerOptions = gpu::providerOptionMap(canon); + if (canon == gpu::kProviderWebGpu) { +#if defined(__APPLE__) + options.AppendExecutionProvider("WebGPU", providerOptions); +#elif AUTOMIX_HAS_EP_PLUGIN + auto& runtime = OrtRuntime::instance(); + const auto& devices = runtime.webGpuDevices(); + if (devices.empty()) { + throw std::runtime_error("WebGPU execution provider requested but no WebGPU devices are available"); + } + std::vector selectedDevice = {devices[0]}; + options.AppendExecutionProvider_V2(runtime.env(), selectedDevice, providerOptions); +#else + throw std::runtime_error("WebGPU plugin EP not compiled in"); +#endif + return; + } + if (canon == gpu::kProviderDirectMl) { + options.AppendExecutionProvider("DML", providerOptions); + return; + } + if (canon == gpu::kProviderCoreMl || canon == gpu::kProviderAne) { + options.AppendExecutionProvider("CoreML", providerOptions); + return; + } + if (canon == gpu::kProviderOpenVino) { + // OpenVINO provider for Intel NPU / GPU: device_type=CPU_FP32 is a CPU device, and OpenVINO is not shipped + options.AppendExecutionProvider("OpenVINO", providerOptions); + return; + } +} + +// Deprecated: prefer configureSessionForProvider() to ensure session tuning is applied. +inline void appendOrtExecutionProvider(Ort::SessionOptions& options, const std::string& canonical) { + configureSessionForProvider(options, canonical, 0); +} + +} // namespace automix::ai diff --git a/src/ai/SeparationRunner.cpp b/src/ai/SeparationRunner.cpp new file mode 100644 index 0000000..1338cfe --- /dev/null +++ b/src/ai/SeparationRunner.cpp @@ -0,0 +1,428 @@ +#include "ai/SeparationRunner.h" + +#include +#include +#include +#include + +namespace automix::ai { +namespace { + +constexpr double kPi = 3.14159265358979323846; + +struct ChunkFailure : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +std::size_t volume(const std::vector& dims) { + std::size_t count = 1; + for (const auto dim : dims) { + count *= static_cast(dim); + } + return count; +} + +std::vector encodeInput(const analysis::Spectrogram& spec, const InputLayout layout) { + const int channels = spec.channels; + const int bins = spec.freqBins; + const int frames = spec.frames; + if (layout == InputLayout::MagnitudeChannels) { + std::vector magnitude(spec.real.size()); + for (std::size_t i = 0; i < magnitude.size(); ++i) { + magnitude[i] = std::hypot(spec.real[i], spec.imag[i]); + } + return magnitude; + } + std::vector data(static_cast(channels) * static_cast(bins) * + static_cast(frames) * 2); + for (int ch = 0; ch < channels; ++ch) { + for (int bin = 0; bin < bins; ++bin) { + for (int frame = 0; frame < frames; ++frame) { + const auto source = spec.index(ch, bin, frame); + std::size_t target = 0; + if (layout == InputLayout::FoldedStereo) { + const auto folded = static_cast(bin) * static_cast(channels) + + static_cast(ch); + target = (static_cast(frame) * static_cast(bins * channels) + folded) * 2; + } else { + target = source * 2; + } + data[target] = spec.real[source]; + data[target + 1] = spec.imag[source]; + } + } + } + return data; +} + +analysis::Spectrogram decodeOutputStem(const std::vector& data, + const int stemIndex, + const InputLayout layout, + const analysis::Spectrogram& geometry) { + analysis::Spectrogram spec; + spec.channels = geometry.channels; + spec.freqBins = geometry.freqBins; + spec.frames = geometry.frames; + spec.sampleRate = geometry.sampleRate; + const std::size_t plane = static_cast(spec.channels) * static_cast(spec.freqBins) * + static_cast(spec.frames); + spec.real.assign(plane, 0.0f); + spec.imag.assign(plane, 0.0f); + + const std::size_t stemOffset = static_cast(stemIndex) * plane * 2; + for (int ch = 0; ch < spec.channels; ++ch) { + for (int bin = 0; bin < spec.freqBins; ++bin) { + for (int frame = 0; frame < spec.frames; ++frame) { + const auto target = spec.index(ch, bin, frame); + std::size_t source = 0; + if (layout == InputLayout::FoldedStereo) { + const auto folded = static_cast(bin) * static_cast(spec.channels) + + static_cast(ch); + source = stemOffset + (folded * static_cast(spec.frames) + static_cast(frame)) * 2; + } else { + source = stemOffset + target * 2; + } + spec.real[target] = data[source]; + spec.imag[target] = data[source + 1]; + } + } + } + return spec; +} + +// Real mask = clip(estimated magnitude / input magnitude, 0, 1), bin by bin. +analysis::Spectrogram ratioMask(const std::vector& estimate, + const std::vector& inputMagnitude, + const analysis::Spectrogram& geometry) { + analysis::Spectrogram mask; + mask.channels = geometry.channels; + mask.freqBins = geometry.freqBins; + mask.frames = geometry.frames; + mask.sampleRate = geometry.sampleRate; + mask.real.resize(estimate.size()); + mask.imag.assign(estimate.size(), 0.0f); + for (std::size_t i = 0; i < estimate.size(); ++i) { + const float denominator = std::max(inputMagnitude[i], 1.0e-8f); + mask.real[i] = std::clamp(estimate[i] / denominator, 0.0f, 1.0f); + } + return mask; +} + +// Crossfade weight of sample `i` within a chunk. Consecutive chunks overlap by +// exactly `overlap` samples, and the fade-in of one chunk and the fade-out of +// the previous are sin^2 / cos^2 of the same phase, so they sum to one there. +double chunkWeight(const int i, const int chunkSamples, const int overlap, const bool fadeIn, const bool fadeOut) { + if (overlap <= 0) { + return 1.0; + } + if (fadeIn && i < overlap) { + const double phase = 0.5 * kPi * (static_cast(i) + 0.5) / static_cast(overlap); + return std::sin(phase) * std::sin(phase); + } + const int fromEnd = chunkSamples - 1 - i; + if (fadeOut && fromEnd < overlap) { + const int j = overlap - 1 - fromEnd; + const double phase = 0.5 * kPi * (static_cast(j) + 0.5) / static_cast(overlap); + return std::cos(phase) * std::cos(phase); + } + return 1.0; +} + +} // namespace + +std::string validateRunnerConfig(const RunnerConfig& config) { + if (config.chunkSamples <= 0 || config.overlapSamples < 0 || config.overlapSamples >= config.chunkSamples) { + return "invalid chunking: chunk_samples " + std::to_string(config.chunkSamples) + ", overlap_samples " + + std::to_string(config.overlapSamples); + } + if (config.channels <= 0) { + return "invalid channel count " + std::to_string(config.channels); + } + const auto isResidual = [](const SeparationStem& stem) { return !stem.residualOf.empty(); }; + const auto graphStems = std::count_if(config.stems.begin(), config.stems.end(), + [&](const SeparationStem& stem) { return !isResidual(stem); }); + if (graphStems == 0) { + return "the stem list names no graph-produced stem"; + } + if ((config.inputLayout == InputLayout::MagnitudeChannels) != (config.outputMode == OutputMode::RatioMask)) { + return "input_layout magnitude_channels and output_mode ratio_mask only work together"; + } + if (config.outputMode == OutputMode::Mask || config.outputMode == OutputMode::RatioMask) { + if (graphStems != 1) { + return "mask mode produces exactly one graph stem, but " + std::to_string(graphStems) + " are declared"; + } + const auto target = std::find_if(config.stems.begin(), config.stems.end(), + [&](const SeparationStem& stem) { return stem.name == config.targetStem; }); + if (target == config.stems.end() || isResidual(*target)) { + return "mask target stem '" + config.targetStem + "' is not a graph-produced stem in the stem list"; + } + } + for (const auto& stem : config.stems) { + if (!isResidual(stem)) { + continue; + } + const auto source = std::find_if(config.stems.begin(), config.stems.end(), + [&](const SeparationStem& other) { return other.name == stem.residualOf; }); + if (source == config.stems.end() || isResidual(*source)) { + return "stem '" + stem.name + "' is a residual of '" + stem.residualOf + + "', which is not a graph-produced stem"; + } + } + return {}; +} + +std::optional inputLayoutFromString(const std::string& value) { + if (value == "folded_stereo") { + return InputLayout::FoldedStereo; + } + if (value == "split_channels") { + return InputLayout::SplitChannels; + } + if (value == "magnitude_channels") { + return InputLayout::MagnitudeChannels; + } + return std::nullopt; +} + +std::optional outputModeFromString(const std::string& value) { + if (value == "direct") { + return OutputMode::Direct; + } + if (value == "mask") { + return OutputMode::Mask; + } + if (value == "ratio_mask") { + return OutputMode::RatioMask; + } + return std::nullopt; +} + +std::vector tensorInputDims(const InputLayout layout, const int channels, const int freqBins, const int frames) { + if (layout == InputLayout::FoldedStereo) { + return {1, frames, static_cast(freqBins) * channels * 2}; + } + if (layout == InputLayout::MagnitudeChannels) { + return {1, channels, freqBins, frames}; + } + return {1, channels, freqBins, frames, 2}; +} + +std::vector tensorOutputDims(const InputLayout layout, + const int stemAxis, + const int channels, + const int freqBins, + const int frames) { + if (layout == InputLayout::FoldedStereo) { + return {1, stemAxis, static_cast(freqBins) * channels, frames, 2}; + } + if (layout == InputLayout::MagnitudeChannels) { + return {1, channels, freqBins, frames}; + } + return {1, stemAxis, channels, freqBins, frames, 2}; +} + +int SeparationRunner::chunkCount(const int samples, const RunnerConfig& config) { + if (samples <= 0) { + return 0; + } + if (samples <= config.chunkSamples) { + return 1; + } + const int stride = config.chunkSamples - config.overlapSamples; + return 1 + (samples - config.chunkSamples + stride - 1) / stride; +} + +SeparationRunner::Result SeparationRunner::separate(const engine::AudioBuffer& mix, + const ITensorInference& inference, + const RunnerConfig& config) { + Result result; + const auto fail = [&](const std::string& reason) { + Result failed; + failed.usedModel = false; + failed.logMessage = "Tensor separation not used: " + reason; + return failed; + }; + + if (const auto configError = validateRunnerConfig(config); !configError.empty()) { + return fail(configError); + } + const int samples = mix.getNumSamples(); + if (mix.getNumChannels() <= 0 || samples <= 0) { + return fail("input mix has no audio samples"); + } + if (std::abs(mix.getSampleRate() - config.sampleRate) > 1.0e-6) { + return fail("model expects " + std::to_string(static_cast(config.sampleRate)) + + " Hz audio but the mix is " + std::to_string(static_cast(mix.getSampleRate())) + + " Hz; resampling is not implemented"); + } + if (mix.getNumChannels() != config.channels) { + return fail("model expects " + std::to_string(config.channels) + "-channel audio but the mix has " + + std::to_string(mix.getNumChannels()) + " channel(s); channels are never duplicated or mixed down"); + } + if (!inference.isAvailable()) { + return fail("tensor inference backend is unavailable"); + } + + const auto inputs = inference.inputSpecs(); + const auto outputs = inference.outputSpecs(); + if (inputs.size() != 1 || outputs.size() != 1) { + return fail("graph declares " + std::to_string(inputs.size()) + " input(s) and " + + std::to_string(outputs.size()) + " output(s); this runner binds exactly one of each"); + } + const auto& inputSpec = inputs.front(); + const auto& outputSpec = outputs.front(); + + std::vector graphStemIndex; // index into config.stems, in stem-axis order + for (std::size_t i = 0; i < config.stems.size(); ++i) { + if (config.stems[i].residualOf.empty()) { + graphStemIndex.push_back(i); + } + } + const int stemAxis = config.outputMode == OutputMode::Direct ? static_cast(graphStemIndex.size()) : 1; + + const int chunk = config.chunkSamples; + const int overlap = config.overlapSamples; + const int stride = chunk - overlap; + const int channels = config.channels; + const int totalChunks = chunkCount(samples, config); + + // Output stems accumulate in place. The crossfade weights of overlapping + // chunks sum to one, so no separate normalisation pass is needed. + std::vector stemAudio; + stemAudio.reserve(config.stems.size()); + for (std::size_t i = 0; i < config.stems.size(); ++i) { + stemAudio.emplace_back(channels, samples, mix.getSampleRate()); + } + + const auto cancelRequested = [&config] { return config.cancelRequested && config.cancelRequested(); }; + const auto cancelled = [&](const int chunksDone) { + Result stopped; + stopped.cancelled = true; + stopped.logMessage = "Tensor separation cancelled after " + std::to_string(chunksDone) + "/" + + std::to_string(totalChunks) + " chunk(s); no stems were produced."; + return stopped; + }; + + try { + for (int chunkIndex = 0; chunkIndex < totalChunks; ++chunkIndex) { + if (cancelRequested()) { + return cancelled(chunkIndex); + } + const int start = chunkIndex * stride; + const int valid = std::min(chunk, samples - start); + + // The graph's geometry is fixed, so a short final chunk is zero-padded to + // the full chunk length and its output trimmed back to `valid` samples. + engine::AudioBuffer chunkAudio(channels, chunk, mix.getSampleRate()); + for (int ch = 0; ch < channels; ++ch) { + const float* source = mix.getReadPointer(ch); + float* destination = chunkAudio.getWritePointer(ch); + std::copy(source + start, source + start + valid, destination); + } + + const auto spectrum = analysis::analyze(chunkAudio, config.stft); + TensorBinding binding; + binding.expected.name = inputSpec.name; + binding.expected.elementType = TensorElementType::Float32; + binding.expected.dims = tensorInputDims(config.inputLayout, channels, spectrum.freqBins, spectrum.frames); + if (!shapesMatch(inputSpec, binding.expected)) { + throw ChunkFailure("input '" + inputSpec.name + "' expects " + describeShape(inputSpec.dims) + + " but the STFT of a " + std::to_string(chunk) + "-sample chunk is " + + describeShape(binding.expected.dims)); + } + binding.data = encodeInput(spectrum, config.inputLayout); + + const auto inferred = inference.runCancellable({binding}, config.cancelRequested); + if (!inferred.usedModel && cancelRequested()) { + // A terminated run reports failure; the cause is the cancellation. + return cancelled(chunkIndex); + } + if (!inferred.usedModel) { + throw ChunkFailure("chunk " + std::to_string(chunkIndex + 1) + "/" + std::to_string(totalChunks) + + " inference failed: " + inferred.logMessage); + } + const auto produced = std::find_if(inferred.outputs.begin(), inferred.outputs.end(), + [&](const Tensor& tensor) { return tensor.spec.name == outputSpec.name; }); + if (produced == inferred.outputs.end()) { + throw ChunkFailure("graph did not return the declared output '" + outputSpec.name + "'"); + } + const auto expectedOutputDims = + tensorOutputDims(config.inputLayout, stemAxis, channels, spectrum.freqBins, spectrum.frames); + const TensorSpec expectedOutput{outputSpec.name, TensorElementType::Float32, expectedOutputDims}; + if (!shapesMatch(expectedOutput, produced->spec) || produced->data.size() != volume(expectedOutputDims)) { + throw ChunkFailure("output '" + outputSpec.name + "' has shape " + describeShape(produced->spec.dims) + + " with " + std::to_string(produced->data.size()) + " values; expected " + + describeShape(expectedOutputDims)); + } + + std::vector> chunkStems(config.stems.size()); + if (config.outputMode == OutputMode::RatioMask) { + const auto mask = ratioMask(produced->data, binding.data, spectrum); + const auto masked = analysis::complexMultiply(spectrum, mask, config.stft.zeroDc); + chunkStems[graphStemIndex.front()] = analysis::synthesize(masked, config.stft); + } else if (config.outputMode == OutputMode::Mask) { + const auto mask = decodeOutputStem(produced->data, 0, config.inputLayout, spectrum); + const auto masked = analysis::complexMultiply(spectrum, mask, config.stft.zeroDc); + chunkStems[graphStemIndex.front()] = analysis::synthesize(masked, config.stft); + } else { + for (std::size_t n = 0; n < graphStemIndex.size(); ++n) { + const auto stemSpec = decodeOutputStem(produced->data, static_cast(n), config.inputLayout, spectrum); + chunkStems[graphStemIndex[n]] = analysis::synthesize(stemSpec, config.stft); + } + } + for (std::size_t i = 0; i < config.stems.size(); ++i) { + if (config.stems[i].residualOf.empty()) { + continue; + } + const auto sourceIt = std::find_if(config.stems.begin(), config.stems.end(), [&](const SeparationStem& s) { + return s.name == config.stems[i].residualOf; + }); + const auto& source = *chunkStems[static_cast(std::distance(config.stems.begin(), sourceIt))]; + engine::AudioBuffer residual(channels, chunk, mix.getSampleRate()); + for (int ch = 0; ch < channels; ++ch) { + for (int s = 0; s < chunk; ++s) { + residual.setSample(ch, s, chunkAudio.getSample(ch, s) - source.getSample(ch, s)); + } + } + chunkStems[i] = std::move(residual); + } + + for (const auto& stem : chunkStems) { + if (!stem.has_value() || stem->getNumSamples() != chunk || stem->getNumChannels() != channels) { + throw ChunkFailure("chunk " + std::to_string(chunkIndex + 1) + " reconstructed " + + std::to_string(stem.has_value() ? stem->getNumSamples() : 0) + " samples, expected " + + std::to_string(chunk)); + } + } + + const bool fadeIn = chunkIndex > 0; + const bool fadeOut = chunkIndex + 1 < totalChunks; + for (std::size_t stemIndex = 0; stemIndex < chunkStems.size(); ++stemIndex) { + for (int ch = 0; ch < channels; ++ch) { + float* destination = stemAudio[stemIndex].getWritePointer(ch) + start; + for (int s = 0; s < valid; ++s) { + const double weight = chunkWeight(s, chunk, overlap, fadeIn, fadeOut); + destination[s] += static_cast(weight * static_cast(chunkStems[stemIndex]->getSample(ch, s))); + } + } + } + + if (config.progressCallback) { + config.progressCallback(chunkIndex + 1, totalChunks); + } + } + } catch (const std::exception& error) { + return fail(error.what()); + } + + result.stemAudio = std::move(stemAudio); + for (const auto& stem : config.stems) { + result.stemNames.push_back(stem.name); + } + result.usedModel = true; + result.logMessage = "Tensor separation completed (" + std::to_string(totalChunks) + " chunk(s), " + + std::to_string(config.stems.size()) + " stem(s))."; + return result; +} + +} // namespace automix::ai diff --git a/src/ai/SeparationRunner.h b/src/ai/SeparationRunner.h new file mode 100644 index 0000000..f691a11 --- /dev/null +++ b/src/ai/SeparationRunner.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "ai/ITensorInference.h" +#include "analysis/SpectrogramFrontEnd.h" +#include "engine/AudioBuffer.h" + +namespace automix::ai { + +// How stereo spectra are laid out in the graph's input and output tensors. +// +// FoldedStereo (ZFTurbo / upstream BS-RoFormer, 'b s f t c -> b (f s) t c'): +// input [1, T, F*C*2] element (t, (f*C + ch)*2 + reim) +// output [1, N, F*C, T, 2] element (n, f*C + ch, t, reim) +// SplitChannels (channels on their own axis): +// input [1, C, F, T, 2] +// output [1, N, C, F, T, 2] +// MagnitudeChannels (Open-Unmix style, magnitude only, no phase): +// input [1, C, F, T] |STFT| +// output [1, C, F, T] estimated target |STFT| (RatioMask mode only) +// +// N is the stem axis: 1 in Mask mode, one entry per graph-produced stem in +// Direct mode. For the complex layouts the trailing axis of length 2 is always +// (real, imag). +enum class InputLayout { FoldedStereo, SplitChannels, MagnitudeChannels }; + +// Mask: the graph returns one complex mask for the target stem; the caller +// multiplies it against the input spectrum. Direct: the graph returns one +// spectrogram per stem. RatioMask: the graph returns the target's magnitude; +// the caller divides it by the input magnitude, clips to [0, 1] and multiplies +// the complex input spectrum by that real mask (the mix phase is kept). +enum class OutputMode { Direct, Mask, RatioMask }; + +std::optional inputLayoutFromString(const std::string& value); +std::optional outputModeFromString(const std::string& value); + +std::vector tensorInputDims(InputLayout layout, int channels, int freqBins, int frames); +std::vector tensorOutputDims(InputLayout layout, int stemAxis, int channels, int freqBins, int frames); + +struct SeparationStem { + std::string name; + // Empty for a stem the graph produces. Otherwise the stem is computed + // host-side as (input mix - residualOf), e.g. instrumental = mix - vocals. + std::string residualOf; +}; + +struct RunnerConfig { + int chunkSamples = 352800; + int overlapSamples = 88200; // chunkSamples / 4 + double sampleRate = 44100.0; + int channels = 2; + analysis::StftParams stft{}; // stft.zeroDc is applied in Mask mode + InputLayout inputLayout = InputLayout::FoldedStereo; + OutputMode outputMode = OutputMode::Mask; + std::string targetStem = "vocals"; + std::vector stems{{"vocals", ""}, {"instrumental", "vocals"}}; + // JUCE-free on purpose: the runner stays testable with no message loop; the + // controller that adapts it to the UI owns the SafePointer marshalling. + std::function progressCallback; + // Polled before every chunk and, where the backend supports it, during + // inference (from a watcher thread, so it must be thread-safe). Returning + // true discards all work and yields Result::cancelled. + std::function cancelRequested; +}; + +// Empty when the configuration is runnable; otherwise the reason it is not. +std::string validateRunnerConfig(const RunnerConfig& config); + +class SeparationRunner final { + public: + struct Result { + bool usedModel = false; + // The caller asked to stop. Not a failure: callers must not fall back to + // another separator, because the user wants no separation at all. + bool cancelled = false; + std::vector stemAudio; + std::vector stemNames; + std::string logMessage; + }; + + // Chunks the track, runs every chunk through `inference`, and crossfades the + // chunks back together. All-or-nothing: any chunk failure discards every stem + // and returns usedModel == false with the reason. + static Result separate(const engine::AudioBuffer& mix, const ITensorInference& inference, const RunnerConfig& config); + + // Number of chunks separate() will run for a track of `samples` samples. + static int chunkCount(int samples, const RunnerConfig& config); +}; + +} // namespace automix::ai diff --git a/src/ai/StemSeparator.cpp b/src/ai/StemSeparator.cpp index feeb138..95ba969 100644 --- a/src/ai/StemSeparator.cpp +++ b/src/ai/StemSeparator.cpp @@ -14,7 +14,11 @@ #include +#include "ai/GpuMemory.h" +#include "ai/ModelPackLoader.h" #include "ai/OnnxModelInference.h" +#include "ai/OnnxTensorInference.h" +#include "ai/SeparationRunner.h" #include "domain/StemOrigin.h" #include "domain/StemRole.h" #include "engine/AudioFileIO.h" @@ -590,7 +594,7 @@ OverlapAddResult runModelBackedOverlapAdd(const engine::AudioBuffer& mixBuffer, return result; } - result.stems.reserve(result.stemCount); + result.stems.reserve(static_cast(result.stemCount)); for (int index = 0; index < result.stemCount; ++index) { result.stems.emplace_back(channels, samples, mixBuffer.getSampleRate()); } @@ -982,6 +986,142 @@ void writeQaBundle(const std::filesystem::path& path, out << qa.dump(2); } +// A tensor pack at `modelRoot`, or nullopt with the reason. Never throws: a +// malformed manifest is a reason to fall back, not a failed import. +std::optional loadTensorPack(const std::filesystem::path& modelRoot, std::string& reasonOut) { + try { + auto pack = ModelPackLoader().load(modelRoot); + if (!pack.has_value()) { + reasonOut = "no model pack manifest at " + modelRoot.string(); + return std::nullopt; + } + if (!pack->tensorContract.has_value()) { + reasonOut = "model pack '" + pack->id + "' has no tensor_contract"; + return std::nullopt; + } + std::error_code error; + if (pack->modelFile.empty() || !std::filesystem::is_regular_file(modelRoot / pack->modelFile, error) || error) { + reasonOut = "model pack '" + pack->id + "' is missing its model file '" + pack->modelFile + "'"; + return std::nullopt; + } + return pack; + } catch (const std::exception& exception) { + reasonOut = std::string("model pack manifest could not be read: ") + exception.what(); + return std::nullopt; + } +} + +domain::StemRole roleForTensorStem(const std::string& name) { + if (name == "vocals") { + return domain::StemRole::Vocals; + } + if (name == "instrumental") { + return domain::StemRole::Music; + } + return domain::StemRole::Unknown; +} + +// Runs the tensor pack end to end and writes one WAV per stem. On any failure +// returns success == false with the reason and leaves no stems in the result; +// the caller then takes the existing path. +StemSeparator::SeparationResult runTensorSeparation(const std::filesystem::path& modelRoot, + const engine::AudioBuffer& mix, + const std::filesystem::path& outputDir, + const StemSeparator::SeparationOptions& options) { + StemSeparator::SeparationResult result; + std::string reason; + const auto pack = loadTensorPack(modelRoot, reason); + if (!pack.has_value()) { + result.logMessage = reason; + return result; + } + + auto config = runnerConfigFromContract(*pack->tensorContract, reason); + if (!config.has_value()) { + result.logMessage = "tensor_contract rejected: " + reason; + return result; + } + config->progressCallback = options.tensorProgress; + config->cancelRequested = options.cancelRequested; + + // A GPU session can open and still fail mid-run (out of device memory, an + // unsupported kernel), so a GPU failure gets exactly one retry on CPU. + SeparationRunner::Result separated; + std::string provider; + std::string gpuFailure; + auto firstProvider = options.executionProvider; + if (firstProvider != "cpu" && pack->gpuMemoryMb.has_value()) { + // Not enough free device memory means the run would spill into shared + // system memory, which measured slower than running on CPU outright. + const auto requiredBytes = *pack->gpuMemoryMb * 1024 * 1024; + const auto memory = queryCudaDeviceMemory(); + if (!gpuHasRoomNow(memory, requiredBytes)) { + firstProvider = "cpu"; + gpuFailure = "GPU has " + std::to_string(memory->freeBytes / (1024 * 1024)) + " MiB free but the model needs " + + std::to_string(*pack->gpuMemoryMb) + " MiB; ran on cpu. "; + } + } + for (const auto& requested : {firstProvider, std::string("cpu")}) { + OnnxTensorInference inference; + inference.setGpuProviderAllowList(pack->gpuProviders); + inference.setTensorContract(pack->tensorContract); + inference.setExecutionProvider(requested); + if (!inference.loadModel(modelRoot / pack->modelFile)) { + result.logMessage = gpuFailure + "tensor model did not load: " + inference.backendDiagnostics(); + return result; + } + provider = inference.activeExecutionProvider(); + separated = SeparationRunner::separate(mix, inference, *config); + if (separated.cancelled) { + result.cancelled = true; + result.logMessage = separated.logMessage; + return result; + } + if (separated.usedModel || provider == "cpu") { + break; + } + gpuFailure = "on " + provider + ": " + separated.logMessage + "; retried on cpu. "; + } + if (!separated.usedModel) { + result.logMessage = "tensor separation failed: " + gpuFailure + separated.logMessage; + return result; + } + + util::WavWriter writer; + for (std::size_t index = 0; index < separated.stemAudio.size(); ++index) { + const auto& name = separated.stemNames[index]; + const auto stemPath = outputDir / ("stem_" + name + ".wav"); + writer.write(stemPath, separated.stemAudio[index], 24); + result.generatedFiles.push_back(stemPath); + + // No separationConfidence / separationArtifactRisk: the graph reports + // neither, and a hardcoded number would read as a measurement. + domain::Stem stem; + stem.id = "sep_" + name; + stem.name = "Separated " + titleCase(name); + stem.filePath = stemPath.string(); + stem.role = roleForTensorStem(name); + stem.origin = domain::StemOrigin::Separated; + stem.enabled = true; + result.stems.push_back(std::move(stem)); + } + + std::string residuals; + for (const auto& stem : pack->tensorContract->stems) { + if (!stem.residualOf.empty()) { + residuals += " '" + stem.name + "' is the residual (mix - " + stem.residualOf + "), not a second separation."; + } + } + + result.success = true; + result.usedModel = true; + result.stemVariantCount = static_cast(result.stems.size()); + result.qaMetrics = computeQaMetrics(mix, separated.stemAudio); + result.logMessage = "Tensor separation via pack '" + pack->id + "' on " + provider + "." + residuals + " " + + gpuFailure + separated.logMessage; + return result; +} + } // namespace StemSeparator::StemSeparator(std::filesystem::path modelRoot) : modelRoot_(std::move(modelRoot)) {} @@ -1010,6 +1150,16 @@ bool StemSeparator::isModelAvailable() const { return !resolveModelPath().empty(); } +bool StemSeparator::isTensorModelAvailable() const { + std::string reason; + return loadTensorPack(modelRoot_, reason).has_value(); +} + +StemSeparator::SeparationResult StemSeparator::separate(const std::filesystem::path& mixPath, + const std::filesystem::path& outputDir) const { + return separate(mixPath, outputDir, SeparationOptions{}); +} + StemSeparator::SeparationResult StemSeparator::separate(const std::filesystem::path& mixPath, const std::filesystem::path& outputDir, const SeparationOptions& options) const { @@ -1025,6 +1175,17 @@ StemSeparator::SeparationResult StemSeparator::separate(const std::filesystem::p std::filesystem::create_directories(outputDir); + std::string tensorFallbackNote; + if (options.useTensorModel) { + auto tensorResult = runTensorSeparation(modelRoot_, mixBuffer, outputDir, options); + if (tensorResult.success || tensorResult.cancelled) { + // Cancelled is returned as-is: falling back would run the separation + // the user just stopped. + return tensorResult; + } + tensorFallbackNote = "Tensor separation unavailable (" + tensorResult.logMessage + "); existing separator used. "; + } + auto variants = discoverModelVariants(modelRoot_); if (variants.empty()) { const auto fallbackModel = resolveModelPath(); @@ -1079,7 +1240,7 @@ StemSeparator::SeparationResult StemSeparator::separate(const std::filesystem::p result.stemVariantCount = separated.stemCount; result.qaMetrics = computeQaMetrics(mixBuffer, separated.stems); result.qaReportPath = outputDir / "separation_qa_report.json"; - result.logMessage = separated.logMessage; + result.logMessage = tensorFallbackNote + separated.logMessage; writeQaBundle(result.qaReportPath, result, separated.stemRoles, separated, selectedVariant); diff --git a/src/ai/StemSeparator.h b/src/ai/StemSeparator.h index d3e16ee..e99a074 100644 --- a/src/ai/StemSeparator.h +++ b/src/ai/StemSeparator.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include @@ -14,6 +16,20 @@ class StemSeparator final { std::optional targetStemCount; std::optional gpuMemoryBudgetMb; std::optional maxStreams; + // Set from RenderSettings::tensorSeparationEnabled. When the model root holds + // a pack with a tensor_contract, separation runs through SeparationRunner; + // any failure falls back to the existing path. Off: behaviour is unchanged. + bool useTensorModel = false; + // Tensor path only, called from the separating thread. JUCE-free so the + // caller owns any message-thread marshalling. + std::function tensorProgress; + // Tensor path only. Polled between chunks and during inference, possibly + // from a watcher thread: must be thread-safe. True stops separation with + // SeparationResult::cancelled and no fallback. + std::function cancelRequested; + // Tensor path only: "auto" (GPU when the runtime can open one, else CPU), + // "cpu", or a provider name such as "cuda". + std::string executionProvider = "auto"; }; struct SeparationQaMetrics { @@ -25,6 +41,8 @@ class StemSeparator final { struct SeparationResult { bool success = false; bool usedModel = false; + // The user stopped it; success is false and nothing was written. + bool cancelled = false; int stemVariantCount = 0; std::vector stems; std::vector generatedFiles; @@ -36,9 +54,14 @@ class StemSeparator final { explicit StemSeparator(std::filesystem::path modelRoot = "assets/models/stem-separator"); [[nodiscard]] bool isModelAvailable() const; + // True when the model root is a pack carrying a tensor_contract whose model + // file exists. Says nothing about whether ONNX Runtime can open it. + [[nodiscard]] bool isTensorModelAvailable() const; + SeparationResult separate(const std::filesystem::path& mixPath, + const std::filesystem::path& outputDir) const; SeparationResult separate(const std::filesystem::path& mixPath, const std::filesystem::path& outputDir, - const SeparationOptions& options = {}) const; + const SeparationOptions& options) const; private: [[nodiscard]] std::filesystem::path resolveModelPath() const; diff --git a/src/ai/TensorTypes.cpp b/src/ai/TensorTypes.cpp new file mode 100644 index 0000000..38c59bf --- /dev/null +++ b/src/ai/TensorTypes.cpp @@ -0,0 +1,56 @@ +#include "ai/TensorTypes.h" + +namespace automix::ai { + +std::optional elementCount(const TensorSpec& spec) { + std::size_t count = 1; + int dynamicAxes = 0; + for (const auto dim : spec.dims) { + if (dim == -1) { + ++dynamicAxes; + continue; + } + if (dim < 0) { + return std::nullopt; + } + count *= static_cast(dim); + } + if (dynamicAxes > 1) { + return std::nullopt; + } + return count; +} + +bool shapesMatch(const TensorSpec& expected, const TensorSpec& actual) { + if (expected.dims.size() != actual.dims.size()) { + return false; + } + for (std::size_t axis = 0; axis < expected.dims.size(); ++axis) { + if (expected.dims[axis] != -1 && expected.dims[axis] != actual.dims[axis]) { + return false; + } + } + return true; +} + +std::string describeShape(const std::vector& dims) { + std::string text = "["; + for (std::size_t axis = 0; axis < dims.size(); ++axis) { + if (axis > 0) { + text += ", "; + } + text += std::to_string(dims[axis]); + } + text += "]"; + return text; +} + +std::string describeDtype(const TensorElementType type) { + switch (type) { + case TensorElementType::Float32: + return "float32"; + } + return "unknown"; +} + +} // namespace automix::ai diff --git a/src/ai/TensorTypes.h b/src/ai/TensorTypes.h new file mode 100644 index 0000000..a634ead --- /dev/null +++ b/src/ai/TensorTypes.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace automix::ai { + +// Deliberately a single member. ONNX Runtime has no complex dtype, so complex +// spectra travel as float32 with an explicit real/imag axis; any other graph +// dtype is rejected at load rather than widened silently. +enum class TensorElementType { Float32 }; + +struct TensorSpec { + std::string name; + TensorElementType elementType = TensorElementType::Float32; + std::vector dims; // -1 == dynamic axis +}; + +struct TensorBinding { + TensorSpec expected; + std::vector data; +}; + +struct Tensor { + TensorSpec spec; + std::vector data; +}; + +struct TensorInferenceResult { + bool usedModel = false; + std::vector outputs; // named, never positional + std::string logMessage; +}; + +// Product of the static dims, treating a single dynamic axis as 1. nullopt when +// more than one axis is dynamic, because the volume is then undetermined. +std::optional elementCount(const TensorSpec& spec); + +// Equal rank, and every non-dynamic expected dim equals the actual dim. +bool shapesMatch(const TensorSpec& expected, const TensorSpec& actual); + +std::string describeShape(const std::vector& dims); +std::string describeDtype(TensorElementType type); + +} // namespace automix::ai diff --git a/src/ai/UmxPack.h b/src/ai/UmxPack.h new file mode 100644 index 0000000..e18d97a --- /dev/null +++ b/src/ai/UmxPack.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +#include "ai/ModelPackLoader.h" + +namespace automix::ai { + +// Open-Unmix UMX-HQ vocals (sigsep/open-unmix-pytorch, MIT), ONNX export by +// MixDirective. A 36 MB bidirectional-LSTM that runs on any CPU: the fast +// alternative to BS-RoFormer on machines that cannot run that model. On an 8 GiB +// Mac, BS-RoFormer needed over 5 minutes per chunk on the CPU and CoreML/ANE +// were killed by the OS. Its graph takes the STFT magnitude only (no phase), so +// the mix phase is kept and the model's output is applied as a ratio mask. +inline constexpr const char* kUmxVocalsRepoId = "MixDirective/open-unmix-umxhq-vocals-onnx"; +inline constexpr const char* kUmxVocalsFile = "model.onnx"; +// Pinned so a later push to the repo cannot change what installs; bump both deliberately. +inline constexpr const char* kUmxVocalsRevision = "e06097d3193a4c3168b5ddd2a479a680b1176c16"; +inline constexpr const char* kUmxVocalsSha256 = "f27742bb52b24cb039614dc76d0764e279df3eb2a9d6daa82c6daee3369ec747"; +inline constexpr const char* kUmxVocalsIntendedUse = + "Vocal separation (Open-Unmix UMX-HQ, 36 MB, CPU friendly). Lower quality than BS-RoFormer but " + "needs little memory. The graph produces vocals only; the instrumental stem is the residual " + "mix - vocals, not a second separation."; + +// Catalog form of the pack's tensor contract. ~10 s chunks (431 frames at hop +// 1024) with ~2 s of overlap, which also gives the LSTM context at chunk edges. +// The chunk is a whole number of hops (430 x 1024): the inverse STFT returns +// (frames - 1) x hop samples, so any other length would come back short. +inline TensorContract umxVocalsCatalogContract() { + TensorContract contract; + contract.engine = "open_unmix"; + contract.sampleRate = 44100; + contract.stereo = true; + contract.chunkSamples = 440320; + contract.overlapSamples = 87040; + contract.stft.nFft = 4096; + contract.stft.hopLength = 1024; + contract.stft.winLength = 4096; + contract.stft.window = "hann"; + contract.stft.periodicWindow = true; + contract.stft.center = true; + contract.stft.padMode = "reflect"; + contract.stft.normalized = false; + contract.stft.zeroDc = false; + contract.inputLayout = "magnitude_channels"; + contract.inputs = {{"", {1, 2, 2049, 431}, "float32"}}; + contract.outputs = {{"", {1, 2, 2049, 431}, "float32"}}; + contract.outputMode = "ratio_mask"; + contract.targetStem = "vocals"; + contract.stems = {{"vocals", ""}, {"instrumental", "vocals"}}; + return contract; +} + +} // namespace automix::ai diff --git a/src/analysis/SpectrogramFrontEnd.cpp b/src/analysis/SpectrogramFrontEnd.cpp new file mode 100644 index 0000000..fa35512 --- /dev/null +++ b/src/analysis/SpectrogramFrontEnd.cpp @@ -0,0 +1,251 @@ +#include "analysis/SpectrogramFrontEnd.h" + +#include +#include +#include + +#include + +namespace automix::analysis { +namespace { + +bool isPowerOfTwo(const int value) { + return value > 0 && (value & (value - 1)) == 0; +} + +int fftOrderFor(const int nFft) { + int order = 0; + while ((1 << order) < nFft) { + ++order; + } + return order; +} + +void validate(const StftParams& params) { + if (!isPowerOfTwo(params.nFft)) { + throw std::invalid_argument("STFT n_fft must be a power of two, got " + std::to_string(params.nFft)); + } + if (params.hopLength <= 0) { + throw std::invalid_argument("STFT hop_length must be positive, got " + std::to_string(params.hopLength)); + } + if (params.winLength <= 0 || params.winLength > params.nFft) { + throw std::invalid_argument("STFT win_length must be in (0, n_fft], got " + std::to_string(params.winLength)); + } +} + +// analyze() side of `center`: samples of reflect padding added at each end. +int centrePadSamples(const StftParams& params) { + return params.center ? params.nFft / 2 : 0; +} + +// synthesize() side of `center`: samples dropped from each end of the +// overlap-added signal. Numerically equal to the pad, deliberately named apart. +int centreTrimSamples(const StftParams& params) { + return params.center ? params.nFft / 2 : 0; +} + +// Periodic Hann of winLength (torch.hann_window(periodic=True)), zero-padded +// centred into nFft as torch.stft does when win_length < n_fft. JUCE's hann is +// the symmetric form, so the periodic window of length N is the first N taps of +// the symmetric window of length N + 1. +std::vector makeAnalysisWindow(const StftParams& params) { + std::vector symmetric(static_cast(params.winLength) + 1, 0.0f); + juce::dsp::WindowingFunction::fillWindowingTables( + symmetric.data(), symmetric.size(), juce::dsp::WindowingFunction::hann, false); + + std::vector window(static_cast(params.nFft), 0.0f); + const auto offset = static_cast((params.nFft - params.winLength) / 2); + for (std::size_t i = 0; i < static_cast(params.winLength); ++i) { + window[offset + i] = symmetric[i]; + } + return window; +} + +} // namespace + +int stftFrameCount(const int samples, const StftParams& params) { + if (params.center) { + return 1 + samples / params.hopLength; + } + if (samples < params.nFft) { + return 0; + } + return 1 + (samples - params.nFft) / params.hopLength; +} + +int istftLength(const int frames, const StftParams& params) { + const int overlapAdded = params.nFft + params.hopLength * (frames - 1); + return overlapAdded - 2 * centreTrimSamples(params); +} + +Spectrogram analyze(const engine::AudioBuffer& audio, const StftParams& params) { + validate(params); + const int channels = audio.getNumChannels(); + const int samples = audio.getNumSamples(); + const int pad = centrePadSamples(params); + if (params.center && samples <= pad) { + throw std::invalid_argument("STFT reflect padding of " + std::to_string(pad) + + " samples needs more input than the " + std::to_string(samples) + " samples given"); + } + const int frames = stftFrameCount(samples, params); + if (channels <= 0 || frames <= 0) { + throw std::invalid_argument("STFT input has no complete frame (" + std::to_string(channels) + " channels, " + + std::to_string(samples) + " samples)"); + } + + Spectrogram spectrogram; + spectrogram.channels = channels; + spectrogram.freqBins = params.nFft / 2 + 1; + spectrogram.frames = frames; + spectrogram.sampleRate = audio.getSampleRate(); + const std::size_t planeSize = + static_cast(channels) * static_cast(spectrogram.freqBins) * static_cast(frames); + spectrogram.real.assign(planeSize, 0.0f); + spectrogram.imag.assign(planeSize, 0.0f); + + const auto window = makeAnalysisWindow(params); + juce::dsp::FFT fft(fftOrderFor(params.nFft)); + const float scale = params.normalized ? 1.0f / std::sqrt(static_cast(params.nFft)) : 1.0f; + + const int paddedLength = samples + 2 * pad; + std::vector padded(static_cast(paddedLength), 0.0f); + std::vector fftData(static_cast(params.nFft) * 2, 0.0f); + + for (int ch = 0; ch < channels; ++ch) { + const float* source = audio.getReadPointer(ch); + for (int i = 0; i < samples; ++i) { + padded[static_cast(pad + i)] = source[i]; + } + // torch pad_mode='reflect': mirror about the edge sample, edge excluded. + for (int j = 0; j < pad; ++j) { + padded[static_cast(pad - 1 - j)] = source[j + 1]; + padded[static_cast(pad + samples + j)] = source[samples - 2 - j]; + } + + for (int frame = 0; frame < frames; ++frame) { + const std::size_t start = static_cast(frame) * static_cast(params.hopLength); + std::fill(fftData.begin(), fftData.end(), 0.0f); + for (std::size_t n = 0; n < static_cast(params.nFft); ++n) { + fftData[n] = padded[start + n] * window[n]; + } + fft.performRealOnlyForwardTransform(fftData.data(), true); + for (int bin = 0; bin < spectrogram.freqBins; ++bin) { + const auto index = spectrogram.index(ch, bin, frame); + spectrogram.real[index] = fftData[static_cast(2 * bin)] * scale; + spectrogram.imag[index] = fftData[static_cast(2 * bin + 1)] * scale; + } + } + } + return spectrogram; +} + +engine::AudioBuffer synthesize(const Spectrogram& spectrogram, const StftParams& params) { + validate(params); + if (spectrogram.freqBins != params.nFft / 2 + 1) { + throw std::invalid_argument("iSTFT expects " + std::to_string(params.nFft / 2 + 1) + " frequency bins for n_fft " + + std::to_string(params.nFft) + ", got " + std::to_string(spectrogram.freqBins)); + } + const std::size_t planeSize = static_cast(spectrogram.channels) * + static_cast(spectrogram.freqBins) * + static_cast(spectrogram.frames); + if (spectrogram.channels <= 0 || spectrogram.frames <= 0 || spectrogram.real.size() != planeSize || + spectrogram.imag.size() != planeSize) { + throw std::invalid_argument("iSTFT spectrogram planes do not match their declared geometry"); + } + + const int frames = spectrogram.frames; + const int nyquist = params.nFft / 2; + const int overlapAddedLength = params.nFft + params.hopLength * (frames - 1); + const int trim = centreTrimSamples(params); + const int outputLength = istftLength(frames, params); + if (outputLength <= 0) { + throw std::invalid_argument("iSTFT of " + std::to_string(frames) + " frames yields no samples"); + } + + const auto window = makeAnalysisWindow(params); + juce::dsp::FFT fft(fftOrderFor(params.nFft)); + const float scale = params.normalized ? std::sqrt(static_cast(params.nFft)) : 1.0f; + + // Window-squared envelope: frames overlap heavily (hop < win), so the + // overlap-added signal is divided by sum(w^2) at each sample, as torch.istft + // does. Identical for every channel. + std::vector envelope(static_cast(overlapAddedLength), 0.0); + for (int frame = 0; frame < frames; ++frame) { + const std::size_t start = static_cast(frame) * static_cast(params.hopLength); + for (std::size_t n = 0; n < static_cast(params.nFft); ++n) { + envelope[start + n] += static_cast(window[n]) * static_cast(window[n]); + } + } + + engine::AudioBuffer output(spectrogram.channels, outputLength, spectrogram.sampleRate); + std::vector accumulator(static_cast(overlapAddedLength), 0.0); + std::vector fftData(static_cast(params.nFft) * 2, 0.0f); + + for (int ch = 0; ch < spectrogram.channels; ++ch) { + std::fill(accumulator.begin(), accumulator.end(), 0.0); + for (int frame = 0; frame < frames; ++frame) { + std::fill(fftData.begin(), fftData.end(), 0.0f); + for (int bin = 0; bin <= nyquist; ++bin) { + const auto index = spectrogram.index(ch, bin, frame); + fftData[static_cast(2 * bin)] = spectrogram.real[index] * scale; + // irfft semantics: the imaginary parts of the DC and Nyquist bins of a + // real signal's spectrum are discarded, not folded into the output. + const bool realOnlyBin = bin == 0 || bin == nyquist; + fftData[static_cast(2 * bin + 1)] = realOnlyBin ? 0.0f : spectrogram.imag[index] * scale; + } + fft.performRealOnlyInverseTransform(fftData.data()); + const std::size_t start = static_cast(frame) * static_cast(params.hopLength); + for (std::size_t n = 0; n < static_cast(params.nFft); ++n) { + accumulator[start + n] += static_cast(fftData[n]) * static_cast(window[n]); + } + } + + float* destination = output.getWritePointer(ch); + for (int i = 0; i < outputLength; ++i) { + const auto source = static_cast(i + trim); + const double norm = envelope[source]; + destination[i] = norm > 1.0e-11 ? static_cast(accumulator[source] / norm) : 0.0f; + } + } + return output; +} + +Spectrogram complexMultiply(const Spectrogram& signal, const Spectrogram& mask, const bool zeroDc) { + const auto describe = [](const Spectrogram& s) { + return "[" + std::to_string(s.channels) + " ch, " + std::to_string(s.freqBins) + " bins, " + + std::to_string(s.frames) + " frames, 2 planes]"; + }; + const auto planeSize = [](const Spectrogram& s) { + return static_cast(s.channels) * static_cast(s.freqBins) * + static_cast(s.frames); + }; + if (signal.channels != mask.channels || signal.freqBins != mask.freqBins || signal.frames != mask.frames) { + throw std::invalid_argument("mask shape " + describe(mask) + " does not match signal shape " + describe(signal)); + } + if (signal.real.size() != planeSize(signal) || signal.imag.size() != planeSize(signal) || + mask.real.size() != planeSize(mask) || mask.imag.size() != planeSize(mask)) { + throw std::invalid_argument("real/imag planes do not match the declared shape " + describe(signal)); + } + + Spectrogram product = signal; + for (std::size_t i = 0; i < product.real.size(); ++i) { + const float a = signal.real[i]; + const float b = signal.imag[i]; + const float c = mask.real[i]; + const float d = mask.imag[i]; + product.real[i] = a * c - b * d; + product.imag[i] = a * d + b * c; + } + if (zeroDc) { + for (int ch = 0; ch < product.channels; ++ch) { + for (int frame = 0; frame < product.frames; ++frame) { + const auto index = product.index(ch, 0, frame); + product.real[index] = 0.0f; + product.imag[index] = 0.0f; + } + } + } + return product; +} + +} // namespace automix::analysis diff --git a/src/analysis/SpectrogramFrontEnd.h b/src/analysis/SpectrogramFrontEnd.h new file mode 100644 index 0000000..88c0b84 --- /dev/null +++ b/src/analysis/SpectrogramFrontEnd.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +#include "engine/AudioBuffer.h" + +namespace automix::analysis { + +// torch.stft / torch.istft conventions, named explicitly because each one +// silently changes the spectrum if misread. The window is always a periodic +// Hann (torch.hann_window default); padding under `center` is always reflect. +struct StftParams { + int nFft = 2048; + int hopLength = 441; + int winLength = 2048; + // One flag, two opposite operations: analyze() reflect-pads nFft/2 samples at + // each end, synthesize() drops nFft/2 samples at each end. + bool center = true; + // torch.stft(normalized=True) scales the forward transform by 1/sqrt(nFft). + bool normalized = false; + // Applied by complexMultiply(): zero bin 0 of the masked spectrum before + // synthesis. Carried here because it is a property of how a mask is applied. + bool zeroDc = true; +}; + +// Complex spectrum stored as two float planes, laid out [channel][bin][frame] +// (torch's (C, F, T) order). Keeps what StemAnalyzer discards: phase and +// channel identity. +struct Spectrogram { + int channels = 0; + int freqBins = 0; + int frames = 0; + double sampleRate = 44100.0; + std::vector real; + std::vector imag; + + [[nodiscard]] std::size_t index(int channel, int bin, int frame) const { + return (static_cast(channel) * static_cast(freqBins) + static_cast(bin)) * + static_cast(frames) + + static_cast(frame); + } +}; + +// Number of frames analyze() produces for `samples` input samples. +int stftFrameCount(int samples, const StftParams& params); + +// Exact synthesize() output length: hop * (frames - 1) under `center`. +int istftLength(int frames, const StftParams& params); + +// Throws std::invalid_argument for parameters outside the implemented set and +// for inputs too short to reflect-pad (torch raises in the same case). +Spectrogram analyze(const engine::AudioBuffer& audio, const StftParams& params); +engine::AudioBuffer synthesize(const Spectrogram& spectrogram, const StftParams& params); + +// Mask-mode application. Throws std::invalid_argument naming both shapes when +// the mask's geometry does not match the signal exactly: a plausible but wrong +// mask shape would otherwise yield numerically valid garbage. +Spectrogram complexMultiply(const Spectrogram& signal, const Spectrogram& mask, bool zeroDc); + +} // namespace automix::analysis diff --git a/src/app/Main.cpp b/src/app/Main.cpp index a79e571..aab14d7 100644 --- a/src/app/Main.cpp +++ b/src/app/Main.cpp @@ -1,5 +1,6 @@ #include +#include "ai/OrtRuntime.h" #include "app/style/AutoMixLookAndFeel.h" #include "app/ui/MainLayout.h" @@ -27,6 +28,13 @@ class MainWindow final : public juce::DocumentWindow { setVisible(true); } + bool requestQuit(std::function quitNow) { + if (auto* layout = dynamic_cast(getContentComponent())) + return layout->requestQuit(std::move(quitNow)); + quitNow(); + return true; + } + void closeButtonPressed() override { juce::JUCEApplication::getInstance()->systemRequestedQuit(); } @@ -45,9 +53,21 @@ class AutoMixMasterApplication final : public juce::JUCEApplication { lookAndFeel_ = std::make_unique(); juce::LookAndFeel::setDefaultLookAndFeel(lookAndFeel_.get()); mainWindow_ = std::make_unique(getApplicationName()); + // Warm up ONNX Runtime asynchronously to avoid UI stalls on adapter discovery + automix::ai::OrtRuntime::instance().warmUpAsync(); + } + + void systemRequestedQuit() override { + if (mainWindow_ == nullptr) { + quit(); + return; + } + mainWindow_->requestQuit([] { juce::JUCEApplication::quit(); }); } void shutdown() override { + // Let the ONNX Runtime warm-up thread finish before the window and libraries tear down. + automix::ai::OrtRuntime::instance().joinWarmUp(); mainWindow_.reset(); juce::LookAndFeel::setDefaultLookAndFeel(nullptr); lookAndFeel_.reset(); diff --git a/src/app/controllers/ImportController.cpp b/src/app/controllers/ImportController.cpp index 6dccf0b..c93c9fc 100644 --- a/src/app/controllers/ImportController.cpp +++ b/src/app/controllers/ImportController.cpp @@ -28,7 +28,8 @@ void ImportController::importFiles(std::vector files, const bool useSeparation, const int preferredStemCount, std::atomic_bool& cancelFlag, - std::optional separationModelRoot) { + std::optional separationModelRoot, + TensorSeparationRequest tensorSeparation) { if (files.empty()) { return; } @@ -67,6 +68,7 @@ void ImportController::importFiles(std::vector files, bool useSeparation; int preferredStemCount; std::optional separationModelRoot; + TensorSeparationRequest tensorSeparation; std::atomic_bool* cancelFlag; Callbacks callbacks; @@ -74,6 +76,7 @@ void ImportController::importFiles(std::vector files, bool sep, int stemCount, std::optional separationRoot, + TensorSeparationRequest tensor, std::atomic_bool* cancel, Callbacks cb) : juce::ThreadPoolJob("ImportJob"), @@ -81,6 +84,7 @@ void ImportController::importFiles(std::vector files, useSeparation(sep), preferredStemCount(stemCount), separationModelRoot(std::move(separationRoot)), + tensorSeparation(std::move(tensor)), cancelFlag(cancel), callbacks(std::move(cb)) {} @@ -128,7 +132,9 @@ void ImportController::importFiles(std::vector files, if (!result.cancelled) { ai::StemSeparator separator(separationModelRoot.value_or(std::filesystem::path("assets/models/stem-separator"))); if (separationModelRoot.has_value()) { - if (separator.isModelAvailable()) { + if (tensorSeparation.enabled && separator.isTensorModelAvailable()) { + importLines.push_back("Tensor separation pack: " + separationModelRoot->string()); + } else if (separator.isModelAvailable()) { importLines.push_back("Separation model pack: " + separationModelRoot->string()); } else { importLines.push_back("Separation model pack unavailable, falling back to bundled separator."); @@ -137,9 +143,25 @@ void ImportController::importFiles(std::vector files, } ai::StemSeparator::SeparationOptions separationOptions; separationOptions.targetStemCount = preferredStemCount; + separationOptions.useTensorModel = tensorSeparation.enabled; + separationOptions.executionProvider = tensorSeparation.executionProvider; + if (tensorSeparation.enabled) { + // Thread-safe: an atomic load plus ThreadPoolJob::shouldExit(). + separationOptions.cancelRequested = [this] { return isCancellationRequested(); }; + // Chunk progress fills the band between the 0.12 and 0.78 marks. + separationOptions.tensorProgress = [cb = callbacks](const int done, const int total) { + if (total > 0) { + emitProgress(cb, 0.12 + 0.66 * static_cast(done) / static_cast(total)); + } + }; + } const auto separationResult = separator.separate(mixPath, outputDir, separationOptions); emitProgress(callbacks, 0.78); - if (separationResult.success) { + if (separationResult.cancelled) { + requestCancellation(); + result.cancelled = true; + importLines.push_back(separationResult.logMessage); + } else if (separationResult.success) { importedStems = separationResult.stems; separatedFromSingleMix = true; importLines.push_back("Separated import from: " + mixPath.string()); @@ -224,7 +246,7 @@ void ImportController::importFiles(std::vector files, }; threadPool_.addJob( - new ImportJob(std::move(files), useSeparation, preferredStemCount, std::move(separationModelRoot), &cancelFlag, callbacks_), + new ImportJob(std::move(files), useSeparation, preferredStemCount, std::move(separationModelRoot), std::move(tensorSeparation), &cancelFlag, callbacks_), true); } diff --git a/src/app/controllers/ImportController.h b/src/app/controllers/ImportController.h index 44e302d..047e8c9 100644 --- a/src/app/controllers/ImportController.h +++ b/src/app/controllers/ImportController.h @@ -14,6 +14,13 @@ namespace automix::app { +// Opt-in tensor (vocal model) separation for single-mix import. +struct TensorSeparationRequest { + bool enabled = false; + // "auto", "cpu" or a provider such as "cuda"; see StemSeparator::SeparationOptions. + std::string executionProvider = "auto"; +}; + struct ImportResult { bool cancelled = false; std::vector stems; @@ -36,7 +43,8 @@ class ImportController { bool useSeparation, int preferredStemCount, std::atomic_bool& cancelFlag, - std::optional separationModelRoot = std::nullopt); + std::optional separationModelRoot = std::nullopt, + TensorSeparationRequest tensorSeparation = {}); private: juce::ThreadPool& threadPool_; diff --git a/src/app/controllers/ModelController.cpp b/src/app/controllers/ModelController.cpp index ec58ee5..3274abc 100644 --- a/src/app/controllers/ModelController.cpp +++ b/src/app/controllers/ModelController.cpp @@ -1,5 +1,7 @@ #include "app/controllers/ModelController.h" +#include "ai/ModelStorage.h" + #include #include #include @@ -16,7 +18,7 @@ namespace automix::app { namespace { std::filesystem::path defaultModelHubRoot() { - return std::filesystem::path("assets") / "modelhub"; + return ai::defaultModelHubRoot(); } nlohmann::json loadJsonIfPresent(const std::filesystem::path& path) { @@ -843,10 +845,21 @@ void ModelController::uninstallModel(const std::string& modelId, std::atomic_boo detail = "Model is not currently installed."; } else { std::error_code error; - if (!installPath.empty() && std::filesystem::exists(installPath, error)) { + // The path comes from install_registry.json: only ever delete inside + // the hub, whatever that file says. + // A stale entry whose directory is already gone (e.g. an unmigrated + // legacy path) is only unregistered, so it cannot get stuck. + const bool outsideHub = !installPath.empty() && !ai::isInsideDirectory(installPath, hubRoot) && + std::filesystem::exists(installPath, error); + error.clear(); + if (outsideHub) { + detail = "Refusing to remove '" + installPath.string() + "': it is outside the model hub " + hubRoot.string(); + } else if (!installPath.empty() && std::filesystem::exists(installPath, error)) { std::filesystem::remove_all(installPath, error); } - if (error) { + if (outsideHub) { + // leave the registry entry: nothing was removed + } else if (error) { detail = "Failed removing install directory: " + installPath.string(); } else { registry.erase(matchIt); diff --git a/src/app/controllers/ModelController.h b/src/app/controllers/ModelController.h index b6ad6a1..0f8254b 100644 --- a/src/app/controllers/ModelController.h +++ b/src/app/controllers/ModelController.h @@ -69,6 +69,7 @@ class ModelController { const std::vector& discoveredModels() const; std::set installedModelIds() const; void setModelHubRoot(const std::filesystem::path& root); + [[nodiscard]] const std::filesystem::path& modelHubRoot() const { return modelHubRoot_; } private: ai::ModelManager& modelManager_; diff --git a/src/app/style/AutoMixLookAndFeel.cpp b/src/app/style/AutoMixLookAndFeel.cpp index 10bb576..5d7591a 100644 --- a/src/app/style/AutoMixLookAndFeel.cpp +++ b/src/app/style/AutoMixLookAndFeel.cpp @@ -68,28 +68,58 @@ AutoMixLookAndFeel::AutoMixLookAndFeel() { // Button // ───────────────────────────────────────────────────────────────── +namespace { +juce::String variantOf(const juce::Button& button) { + return button.getProperties()[buttonVariant::key].toString(); +} +} // namespace + void AutoMixLookAndFeel::drawButtonBackground(juce::Graphics& g, juce::Button& button, const juce::Colour& /*backgroundColour*/, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) { auto bounds = button.getLocalBounds().toFloat().reduced(0.5f); auto cornerSize = metrics::cornerRadius; - - juce::Colour bg; - if (!button.isEnabled()) { - bg = colour(colours::surfaceBorder); - } else if (shouldDrawButtonAsDown) { - bg = colour(colours::primaryPressed); - } else if (shouldDrawButtonAsHighlighted) { - bg = colour(colours::primary).interpolatedWith(colour(colours::primaryHover), 0.6f); - } else if (button.getToggleState()) { - bg = colour(colours::primary); + const auto variant = variantOf(button); + const bool enabled = button.isEnabled(); + + if (!enabled) { + // Disabled: no fill, outline only for primary/secondary so it never out-shouts an enabled button. + if (variant != buttonVariant::quiet && variant != buttonVariant::danger) { + g.setColour(colour(colours::surfaceBorder)); + g.drawRoundedRectangle(bounds, cornerSize, 1.0f); + } + } else if (variant == buttonVariant::secondary) { + juce::Colour bg = colour(colours::surfaceLight); + if (enabled && shouldDrawButtonAsDown) + bg = bg.darker(0.1f); + else if (enabled && shouldDrawButtonAsHighlighted) + bg = bg.brighter(0.1f); + g.setColour(bg); + g.fillRoundedRectangle(bounds, cornerSize); + g.setColour(colour(colours::surfaceBorder)); + g.drawRoundedRectangle(bounds, cornerSize, 1.0f); + } else if (variant == buttonVariant::quiet || variant == buttonVariant::danger) { + if (enabled && (shouldDrawButtonAsHighlighted || shouldDrawButtonAsDown)) { + g.setColour(variant == buttonVariant::quiet ? colour(colours::surfaceLight) + : colour(colours::error).withAlpha(0.15f)); + g.fillRoundedRectangle(bounds, cornerSize); + } } else { - bg = colour(colours::primary); - } + juce::Colour bg; + if (shouldDrawButtonAsDown) { + bg = colour(colours::primaryPressed); + } else if (shouldDrawButtonAsHighlighted) { + bg = colour(colours::primary).interpolatedWith(colour(colours::primaryHover), 0.6f); + } else if (button.getToggleState()) { + bg = colour(colours::primary); + } else { + bg = colour(colours::primary); + } - g.setColour(bg); - g.fillRoundedRectangle(bounds, cornerSize); + g.setColour(bg); + g.fillRoundedRectangle(bounds, cornerSize); + } // Focus ring drawFocusRing(g, button); @@ -101,7 +131,14 @@ void AutoMixLookAndFeel::drawButtonText(juce::Graphics& g, juce::TextButton& but auto font = getTextButtonFont(button, button.getHeight()); g.setFont(font); - auto textColour = button.isEnabled() ? colour(colours::text) : colour(colours::textDisabled); + const auto variant = variantOf(button); + juce::Colour textColour = colour(colours::text); + if (variant == buttonVariant::quiet) + textColour = colour(colours::primaryHover); + else if (variant == buttonVariant::danger) + textColour = colour(colours::error); + if (!button.isEnabled()) + textColour = colour(colours::textDisabled); g.setColour(textColour); @@ -366,7 +403,9 @@ void AutoMixLookAndFeel::drawToggleButton(juce::Graphics& g, juce::ToggleButton& // Fonts // ───────────────────────────────────────────────────────────────── -juce::Font AutoMixLookAndFeel::getTextButtonFont(juce::TextButton& /*button*/, int /*buttonHeight*/) { +juce::Font AutoMixLookAndFeel::getTextButtonFont(juce::TextButton& button, int /*buttonHeight*/) { + if (static_cast(button.getProperties().getWithDefault("compact", false))) + return typography::caption(); return typography::body(); } diff --git a/src/app/style/AutoMixLookAndFeel.h b/src/app/style/AutoMixLookAndFeel.h index b6c5fde..359268c 100644 --- a/src/app/style/AutoMixLookAndFeel.h +++ b/src/app/style/AutoMixLookAndFeel.h @@ -6,6 +6,21 @@ namespace automix::app { +/// Button visual variants, selected via the "variant" component property. +/// No property (or an unknown value) means primary. +namespace buttonVariant { +inline const juce::Identifier key{"variant"}; +inline constexpr const char* primary = "primary"; +inline constexpr const char* secondary = "secondary"; +inline constexpr const char* quiet = "quiet"; +inline constexpr const char* danger = "danger"; +} // namespace buttonVariant + +inline void setButtonVariant(juce::Button& button, const char* variant) { + button.getProperties().set(buttonVariant::key, juce::String(variant)); + button.repaint(); +} + /// Custom LookAndFeel for AutoMixMaster. /// Applies the dark audio-app theme defined in Theme.h to all standard JUCE widgets. class AutoMixLookAndFeel final : public juce::LookAndFeel_V4 { diff --git a/src/app/ui/ControlDeck.cpp b/src/app/ui/ControlDeck.cpp index fd8376d..0198fbc 100644 --- a/src/app/ui/ControlDeck.cpp +++ b/src/app/ui/ControlDeck.cpp @@ -1,5 +1,6 @@ #include "app/ui/ControlDeck.h" +#include "app/style/AutoMixLookAndFeel.h" #include "app/ui/GlowMeters.h" #include "app/ui/StemPanel.h" @@ -25,38 +26,40 @@ ControlDeck::ControlDeck() { // Tooltips importButton_.setTooltip("Import Stems (Ctrl+I)"); autoMixButton_.setTooltip("Auto Mix (Ctrl+M)"); - autoMasterButton_.setTooltip("Auto Master"); + autoMasterButton_.setTooltip("Auto Master (Ctrl+Shift+A)"); autoMixMasterButton_.setTooltip("One-click: Auto Mix -> Auto Master -> Export (Ctrl+Shift+M)"); batchButton_.setTooltip("Batch Process"); exportButton_.setTooltip("Export (Ctrl+E)"); separatedStemsToggle_.setTooltip("Split a single imported full mix into stems using the active Separation model pack."); + tensorSeparationToggle_.setTooltip( + "Use a vocal-separation model pack (e.g. BS-RoFormer) when AI Stem Separation runs. Produces vocals plus an " + "instrumental that is the residual (mix - vocals), not a second separation. Takes minutes on CPU; falls back to " + "the standard separator if the pack cannot run."); + tensorSeparationToggle_.setEnabled(false); + rendererBox_.setTooltip("Engine that renders the exported file. BuiltIn needs no external tools."); + exportModeBox_.setTooltip("Final renders at full quality. Quick Preview renders faster for checking the result."); + masterPresetBox_.setTooltip("Mastering target. Default Streaming suits Spotify, Apple Music and YouTube."); + platformPresetBox_.setTooltip("Loudness target for the platform you will publish to."); + + // Accessibility titles (match the visible labels) + rendererBox_.setTitle("Renderer"); + profileBox_.setTitle("Profile"); + masterPresetBox_.setTitle("Master preset"); + platformPresetBox_.setTitle("Platform"); + exportFormatBox_.setTitle("Export format"); + exportModeBox_.setTitle("Render mode"); + rendererChainModeBox_.setTitle("Chain"); + residualBlendSlider_.setTitle("Residual blend"); batchRecursiveToggle_.setTooltip("Include subfolders when scanning batch input"); rendererChainToggle_.setTooltip("Run renderers in a staged chain"); rendererChainModeBox_.setTooltip("Renderer chain strategy"); residualBlendSlider_.setTooltip("Control residual audio blend"); - // Button visual hierarchy: primary > secondary > tertiary - // Primary — Mix+Master is the hero action; filled brand colour. - autoMixMasterButton_.setColour(juce::TextButton::buttonColourId, colour(colours::primary)); - autoMixMasterButton_.setColour(juce::TextButton::buttonOnColourId, colour(colours::primaryPressed)); - autoMixMasterButton_.setColour(juce::TextButton::textColourOffId, juce::Colours::white); - autoMixMasterButton_.setColour(juce::TextButton::textColourOnId, juce::Colours::white); - - // Secondary — Import and Export are the gateway/output actions; tinted text on dark surface. - for (juce::TextButton* btn : {&importButton_, &exportButton_}) { - btn->setColour(juce::TextButton::buttonColourId, colour(colours::surface)); - btn->setColour(juce::TextButton::buttonOnColourId, colour(colours::surfaceLight)); - btn->setColour(juce::TextButton::textColourOffId, colour(colours::primary)); - btn->setColour(juce::TextButton::textColourOnId, colour(colours::primaryHover)); - } - - // Tertiary — AutoMix, AutoMaster, Batch are advanced ops; visually receded. - for (juce::TextButton* btn : {&autoMixButton_, &autoMasterButton_, &batchButton_}) { - btn->setColour(juce::TextButton::buttonColourId, colour(colours::surface)); - btn->setColour(juce::TextButton::buttonOnColourId, colour(colours::surfaceLight)); - btn->setColour(juce::TextButton::textColourOffId, colour(colours::textMuted)); - btn->setColour(juce::TextButton::textColourOnId, colour(colours::text)); - } + // Button visual hierarchy: primary > secondary > quiet (see buttonVariant in AutoMixLookAndFeel.h). + setButtonVariant(autoMixMasterButton_, buttonVariant::primary); + for (juce::TextButton* btn : {&autoMixButton_, &autoMasterButton_, &batchButton_, &exportButton_}) + setButtonVariant(*btn, buttonVariant::secondary); + setButtonVariant(importButton_, buttonVariant::primary); // Keyboard focus on action buttons importButton_.setWantsKeyboardFocus(true); @@ -118,14 +121,14 @@ ControlDeck::ControlDeck() { rendererChainPreviewLabel_.setFont(typography::caption()); rendererChainPreviewLabel_.setColour(juce::Label::textColourId, colour(colours::textMuted)); rendererChainPreviewLabel_.setJustificationType(juce::Justification::centredLeft); - addAndMakeVisible(rendererChainPreviewLabel_); + addChildComponent(rendererChainPreviewLabel_); blendLabel_.setFont(typography::caption()); blendLabel_.setColour(juce::Label::textColourId, colour(colours::textMuted)); blendLabel_.setJustificationType(juce::Justification::centredLeft); separationModelStatusLabel_.setFont(typography::caption()); separationModelStatusLabel_.setColour(juce::Label::textColourId, colour(colours::warning)); separationModelStatusLabel_.setJustificationType(juce::Justification::centredLeft); - separationModelStatusLabel_.setText("Separation model: none", juce::dontSendNotification); + separationModelStatusLabel_.setText("Model: none installed", juce::dontSendNotification); residualBlendSlider_.setSliderStyle(juce::Slider::LinearHorizontal); residualBlendSlider_.setTextBoxStyle(juce::Slider::TextBoxRight, false, 48, 20); @@ -134,10 +137,7 @@ ControlDeck::ControlDeck() { rendererChainModeBox_.setEnabled(false); // Advanced disclosure toggle - advancedToggle_.setColour(juce::TextButton::buttonColourId, colour(colours::surface)); - advancedToggle_.setColour(juce::TextButton::buttonOnColourId, colour(colours::surfaceLight)); - advancedToggle_.setColour(juce::TextButton::textColourOffId, colour(colours::textMuted)); - advancedToggle_.setColour(juce::TextButton::textColourOnId, colour(colours::text)); + setButtonVariant(advancedToggle_, buttonVariant::quiet); advancedToggle_.onClick = [this] { advancedExpanded_ = !advancedExpanded_; advancedToggle_.setButtonText(advancedExpanded_ ? "v Advanced" : "> Advanced"); @@ -146,8 +146,6 @@ ControlDeck::ControlDeck() { }; // Always-visible settings - addAndMakeVisible(rendererLabel_); - addAndMakeVisible(rendererBox_); addAndMakeVisible(profileLabel_); addAndMakeVisible(profileBox_); addAndMakeVisible(masterPresetLabel_); @@ -155,10 +153,13 @@ ControlDeck::ControlDeck() { addAndMakeVisible(platformPresetLabel_); addAndMakeVisible(platformPresetBox_); addAndMakeVisible(separatedStemsToggle_); - addAndMakeVisible(separationModelStatusLabel_); + addChildComponent(tensorSeparationToggle_); + addChildComponent(separationModelStatusLabel_); addAndMakeVisible(advancedToggle_); // Advanced settings — hidden by default, revealed by advancedToggle_ + addChildComponent(rendererLabel_); + addChildComponent(rendererBox_); addChildComponent(exportFormatLabel_); addChildComponent(exportFormatBox_); addChildComponent(exportModeLabel_); @@ -169,6 +170,8 @@ ControlDeck::ControlDeck() { addChildComponent(blendLabel_); addChildComponent(residualBlendSlider_); addChildComponent(batchRecursiveToggle_); + + setHasStems(false); } ControlDeck::~ControlDeck() = default; @@ -185,8 +188,8 @@ void ControlDeck::resized() { auto area = getLocalBounds().reduced(static_cast(metrics::paddingMedium)); // Three-column layout - int stemWidth = std::max(200, area.getWidth() * 35 / 100); - int meterWidth = std::max(100, area.getWidth() * 15 / 100); + int stemWidth = juce::jlimit(200, 360, area.getWidth() * 28 / 100); + int meterWidth = juce::jlimit(110, 180, area.getWidth() * 13 / 100); auto stemArea = area.removeFromLeft(stemWidth); area.removeFromLeft(spacing::gapMedium); @@ -198,37 +201,60 @@ void ControlDeck::resized() { glowMeters_->setBounds(meterArea); // Action row - // Slots: import(1) | autoMix(1) | autoMaster(1) | Mix+Master(2) | batch(1) | export(1) = 7 slots + // Slots: import(1) | autoMix(1) | autoMaster(1) | Mix+Master(1.5) | batch(1) | export(1) = 6.5 slots auto actionRow = centerArea.removeFromTop(44); - const int slotW = actionRow.getWidth() / 7; + const int slotW = juce::roundToInt(static_cast(actionRow.getWidth()) / 6.5f); + const bool compact = slotW < 90; + for (auto* b : {&importButton_, &autoMixButton_, &autoMasterButton_, &autoMixMasterButton_, + &batchButton_, &exportButton_}) + b->getProperties().set("compact", compact); importButton_.setBounds(actionRow.removeFromLeft(slotW).reduced(2)); autoMixButton_.setBounds(actionRow.removeFromLeft(slotW).reduced(2)); autoMasterButton_.setBounds(actionRow.removeFromLeft(slotW).reduced(2)); - autoMixMasterButton_.setBounds(actionRow.removeFromLeft(slotW * 2).reduced(2)); + autoMixMasterButton_.setBounds(actionRow.removeFromLeft(juce::roundToInt(static_cast(slotW) * 1.5f)).reduced(2)); batchButton_.setBounds(actionRow.removeFromLeft(slotW).reduced(2)); exportButton_.setBounds(actionRow.reduced(2)); centerArea.removeFromTop(spacing::gapSmall); - // Always-visible context rows: Renderer/Profile and Master/Platform - auto settingsRow1 = centerArea.removeFromTop(28); - rendererLabel_.setBounds(settingsRow1.removeFromLeft(64).reduced(1)); - rendererBox_.setBounds(settingsRow1.removeFromLeft(160).reduced(1)); - profileLabel_.setBounds(settingsRow1.removeFromLeft(50).reduced(1)); - profileBox_.setBounds(settingsRow1.removeFromLeft(140).reduced(1)); - - auto settingsRow2 = centerArea.removeFromTop(28); - masterPresetLabel_.setBounds(settingsRow2.removeFromLeft(50).reduced(1)); - masterPresetBox_.setBounds(settingsRow2.removeFromLeft(140).reduced(1)); - platformPresetLabel_.setBounds(settingsRow2.removeFromLeft(64).reduced(1)); - platformPresetBox_.setBounds(settingsRow2.removeFromLeft(140).reduced(1)); - settingsRow2.removeFromLeft(spacing::gapSmall); - separatedStemsToggle_.setBounds(settingsRow2.removeFromLeft(180).reduced(1)); - settingsRow2.removeFromLeft(spacing::gapSmall); - separationModelStatusLabel_.setBounds(settingsRow2.reduced(1)); - - auto chainPreviewRow = centerArea.removeFromTop(24); - rendererChainPreviewLabel_.setBounds(chainPreviewRow.removeFromLeft(640).reduced(1)); + // Always-visible context rows: Profile/Master/Platform, then the separation row + // Pairs wrap onto a new row when they do not fit; combos shrink toward their minimum first. + { + struct Pair { + juce::Label* label; + juce::ComboBox* box; + int labelW, prefW, minW; + }; + const Pair pairs[] = {{&profileLabel_, &profileBox_, 50, 200, 150}, + {&masterPresetLabel_, &masterPresetBox_, 50, 170, 130}, + {&platformPresetLabel_, &platformPresetBox_, 64, 150, 120}}; + const int rowWidth = centerArea.getWidth(); + auto row = centerArea.removeFromTop(28); + int x = 0; + for (const auto& p : pairs) { + const int avail = rowWidth - x - p.labelW; + if (x > 0 && avail < p.minW) { + row = centerArea.removeFromTop(28); + x = 0; + } + const int comboW = std::min(p.prefW, std::max(p.minW, rowWidth - x - p.labelW)); + p.label->setBounds(row.getX() + x, row.getY(), p.labelW, row.getHeight()); + p.label->setBounds(p.label->getBounds().reduced(1)); + p.box->setBounds(juce::Rectangle(row.getX() + x + p.labelW, row.getY(), comboW, row.getHeight()).reduced(1)); + x += p.labelW + comboW; + } + } + + auto separationRow = centerArea.removeFromTop(28); + separatedStemsToggle_.setBounds(separationRow.removeFromLeft(180).reduced(1)); + tensorSeparationToggle_.setVisible(separationControlsVisible_); + separationModelStatusLabel_.setVisible(separationControlsVisible_); + if (separationControlsVisible_) { + separationRow.removeFromLeft(16); + tensorSeparationToggle_.setBounds(separationRow.removeFromLeft(120).reduced(1)); + separationRow.removeFromLeft(spacing::gapSmall); + separationModelStatusLabel_.setBounds(separationRow.reduced(1)); + } centerArea.removeFromTop(spacing::gapSmall); // Advanced disclosure row @@ -236,6 +262,9 @@ void ControlDeck::resized() { centerArea.removeFromTop(spacing::gapSmall); // Advanced settings — shown only when expanded + rendererLabel_.setVisible(advancedExpanded_); + rendererBox_.setVisible(advancedExpanded_); + rendererChainPreviewLabel_.setVisible(advancedExpanded_); exportFormatLabel_.setVisible(advancedExpanded_); exportFormatBox_.setVisible(advancedExpanded_); exportModeLabel_.setVisible(advancedExpanded_); @@ -248,6 +277,12 @@ void ControlDeck::resized() { batchRecursiveToggle_.setVisible(advancedExpanded_); if (advancedExpanded_) { + auto rendererRow = centerArea.removeFromTop(28); + rendererLabel_.setBounds(rendererRow.removeFromLeft(64).reduced(1)); + rendererBox_.setBounds(rendererRow.removeFromLeft(160).reduced(1)); + rendererRow.removeFromLeft(spacing::gapSmall); + rendererChainPreviewLabel_.setBounds(rendererRow.removeFromLeft(640).reduced(1)); + auto settingsRow3 = centerArea.removeFromTop(28); exportFormatLabel_.setBounds(settingsRow3.removeFromLeft(50).reduced(1)); exportFormatBox_.setBounds(settingsRow3.removeFromLeft(100).reduced(1)); @@ -275,7 +310,22 @@ void ControlDeck::setSeparationModelStatus(const juce::String& text, const bool separationModelStatusLabel_.setText(text, juce::dontSendNotification); separationModelStatusLabel_.setColour( juce::Label::textColourId, - ready ? colour(colours::success) : colour(colours::warning)); + ready ? colour(colours::textMuted) : colour(colours::warning)); +} + +void ControlDeck::setHasStems(const bool hasStems) { + autoMixButton_.setEnabled(hasStems); + autoMasterButton_.setEnabled(hasStems); + autoMixMasterButton_.setEnabled(hasStems); + exportButton_.setEnabled(hasStems); + batchButton_.setEnabled(true); + importButton_.setEnabled(true); + setButtonVariant(importButton_, hasStems ? buttonVariant::secondary : buttonVariant::primary); +} + +void ControlDeck::setSeparationControlsVisible(const bool visible) { + separationControlsVisible_ = visible; + resized(); } } // namespace automix::app diff --git a/src/app/ui/ControlDeck.h b/src/app/ui/ControlDeck.h index d8b7967..ada2f5a 100644 --- a/src/app/ui/ControlDeck.h +++ b/src/app/ui/ControlDeck.h @@ -42,9 +42,15 @@ class ControlDeck final : public juce::Component { juce::ComboBox& getRendererChainModeBox() { return rendererChainModeBox_; } juce::Slider& getResidualBlendSlider() { return residualBlendSlider_; } juce::ToggleButton& getSeparatedStemsToggle() { return separatedStemsToggle_; } + juce::ToggleButton& getTensorSeparationToggle() { return tensorSeparationToggle_; } juce::ToggleButton& getBatchRecursiveToggle() { return batchRecursiveToggle_; } void setRendererChainPreviewText(const juce::String& text); void setSeparationModelStatus(const juce::String& text, bool ready); + /// Enables Auto Mix, Auto Master, Mix + Master and Export only when stems exist; Import becomes the + /// primary action while the session is empty. + void setHasStems(bool hasStems); + /// Progressive disclosure: the Vocal Model toggle and model label are shown only while AI Stem Separation is on. + void setSeparationControlsVisible(bool visible); private: std::unique_ptr stemPanel_; @@ -78,12 +84,14 @@ class ControlDeck final : public juce::Component { juce::Label blendLabel_{"", "Residual Blend"}; juce::Slider residualBlendSlider_; juce::ToggleButton separatedStemsToggle_{"AI Stem Separation"}; - juce::Label separationModelStatusLabel_{"", "Separation model: none"}; + juce::ToggleButton tensorSeparationToggle_{"Vocal Model"}; + juce::Label separationModelStatusLabel_{"", "Model: none installed"}; juce::ToggleButton batchRecursiveToggle_{"Recursive Batch"}; // Advanced section toggle juce::TextButton advancedToggle_{"> Advanced"}; bool advancedExpanded_ = false; + bool separationControlsVisible_ = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ControlDeck) }; diff --git a/src/app/ui/GlowMeters.cpp b/src/app/ui/GlowMeters.cpp index 4bd161e..2206b74 100644 --- a/src/app/ui/GlowMeters.cpp +++ b/src/app/ui/GlowMeters.cpp @@ -7,7 +7,14 @@ namespace automix::app { using namespace theme; +juce::String GlowMeters::formatReadout(const juce::String& prefix, const double value, const juce::String& unit) { + if (!std::isfinite(value) || value <= kNoSignalDb) + return prefix + "--" + unit; + return prefix + juce::String(value, 1) + unit; +} + GlowMeters::GlowMeters() { + setTitle("Loudness meters"); lufsLabel_.setText("I: -- LUFS", juce::dontSendNotification); lufsLabel_.setFont(typography::caption()); lufsLabel_.setColour(juce::Label::textColourId, colour(colours::textMuted)); @@ -107,10 +114,10 @@ void GlowMeters::timerCallback() { } // Pre-allocated string updates (avoid ostringstream allocation per frame) - lufsText_ = "I: " + juce::String(integratedLufs_, 1) + " LUFS"; - stText_ = "S: " + juce::String(shortTermLufs_, 1) + " LUFS"; - tpText_ = "TP: " + juce::String(truePeakDbtp_, 1) + " dBTP"; - momentText_ = "M: " + juce::String(momentaryLufs_, 1) + " LUFS"; + lufsText_ = formatReadout("I: ", integratedLufs_, " LUFS"); + stText_ = formatReadout("S: ", shortTermLufs_, " LUFS"); + tpText_ = formatReadout("TP: ", truePeakDbtp_, " dBTP"); + momentText_ = formatReadout("M: ", momentaryLufs_, " LUFS"); lufsLabel_.setText(lufsText_, juce::dontSendNotification); shortTermLabel_.setText(stText_, juce::dontSendNotification); @@ -204,8 +211,11 @@ void GlowMeters::paint(juce::Graphics& g) { auto area = getLocalBounds().toFloat().reduced(metrics::paddingSmall); // Labels at bottom - float labelHeight = 68.0f; - auto meterArea = area.withTrimmedBottom(labelHeight); + // 16 px for the LUFS bar strip, then the four readouts and the bar caption. + float labelHeight = 84.0f; + // The top strip holds the L/R captions drawn above the bars. + const float channelCaptionHeight = 14.0f; + auto meterArea = area.withTrimmedBottom(labelHeight).withTrimmedTop(channelCaptionHeight); // Two meter bars side by side float meterWidth = std::min(32.0f, meterArea.getWidth() * 0.35f); @@ -224,8 +234,10 @@ void GlowMeters::paint(juce::Graphics& g) { // L/R labels g.setColour(colour(colours::textMuted)); g.setFont(typography::caption()); - g.drawText("L", leftBounds.withHeight(14.0f).translated(0.0f, -14.0f), juce::Justification::centred); - g.drawText("R", rightBounds.withHeight(14.0f).translated(0.0f, -14.0f), juce::Justification::centred); + g.drawText("L", leftBounds.withHeight(channelCaptionHeight).translated(0.0f, -channelCaptionHeight), + juce::Justification::centred); + g.drawText("R", rightBounds.withHeight(channelCaptionHeight).translated(0.0f, -channelCaptionHeight), + juce::Justification::centred); // LUFS bar below meters auto lufsBarArea = juce::Rectangle(area.getX(), meterArea.getBottom() + 6.0f, @@ -235,7 +247,8 @@ void GlowMeters::paint(juce::Graphics& g) { void GlowMeters::resized() { auto area = getLocalBounds().reduced(static_cast(metrics::paddingSmall)); - auto labelArea = area.removeFromBottom(68); + auto labelArea = area.removeFromBottom(84); + labelArea.removeFromTop(16); // the LUFS bar is painted here momentaryLabel_.setBounds(labelArea.removeFromTop(14)); shortTermLabel_.setBounds(labelArea.removeFromTop(14)); diff --git a/src/app/ui/GlowMeters.h b/src/app/ui/GlowMeters.h index 3e6c282..c188702 100644 --- a/src/app/ui/GlowMeters.h +++ b/src/app/ui/GlowMeters.h @@ -21,6 +21,9 @@ class GlowMeters final : public juce::Component, private juce::Timer { void setTruePeak(double truePeakDbtp); void setMomentaryLufs(double momentary); + /// Values at or below kNoSignalDb (or non-finite) read as "--". + static juce::String formatReadout(const juce::String& prefix, double value, const juce::String& unit); + private: void timerCallback() override; void drawMeter(juce::Graphics& g, juce::Rectangle bounds, float levelDb, float peakDb) const; @@ -62,6 +65,7 @@ class GlowMeters final : public juce::Component, private juce::Timer { juce::Label momentaryLabel_; juce::Label lufsBarLabel_; + static constexpr double kNoSignalDb = -69.95; static constexpr float kMinDb = -60.0f; static constexpr float kMaxDb = 6.0f; static constexpr int kPeakHoldFrames = 40; diff --git a/src/app/ui/HeaderBar.cpp b/src/app/ui/HeaderBar.cpp index e1362e9..42ec413 100644 --- a/src/app/ui/HeaderBar.cpp +++ b/src/app/ui/HeaderBar.cpp @@ -1,5 +1,7 @@ #include "app/ui/HeaderBar.h" +#include "app/style/AutoMixLookAndFeel.h" + namespace automix::app { using namespace theme; @@ -15,7 +17,13 @@ HeaderBar::HeaderBar() { addAndMakeVisible(loadButton_); addAndMakeVisible(modelsButton_); addAndMakeVisible(settingsButton_); - addAndMakeVisible(profileSelector_); + // Profile selection lives in the ControlDeck "Profile" combo; this selector is never populated. + addChildComponent(profileSelector_); + profileSelector_.setVisible(false); + + for (juce::Button* btn : {static_cast(&saveButton_), static_cast(&loadButton_), + static_cast(&modelsButton_), static_cast(&settingsButton_)}) + setButtonVariant(*btn, buttonVariant::quiet); saveButton_.setTooltip("Save Session (Ctrl+S)"); loadButton_.setTooltip("Load Session (Ctrl+O)"); @@ -78,9 +86,6 @@ void HeaderBar::resized() { loadButton_.setBounds(rightArea.removeFromRight(80).reduced(2)); saveButton_.setBounds(rightArea.reduced(2)); - auto profileArea = area.removeFromLeft(220); - profileSelector_.setBounds(profileArea.reduced(2)); - sessionNameLabel_.setBounds(area); } diff --git a/src/app/ui/HeroWaveform.cpp b/src/app/ui/HeroWaveform.cpp index 02702b0..a966704 100644 --- a/src/app/ui/HeroWaveform.cpp +++ b/src/app/ui/HeroWaveform.cpp @@ -127,6 +127,26 @@ void HeroWaveform::mouseDrag(const juce::MouseEvent& event) { onSeek(progress); } +void HeroWaveform::mouseUp(const juce::MouseEvent& event) { + if (shouldOpenImportOnClick(hasStems_, event.mods.isLeftButtonDown(), event.mouseWasClicked(), + getLocalBounds().contains(event.getPosition())) && + onImportRequested) + onImportRequested(); +} + +void HeroWaveform::setHasStems(bool hasStems) { + if (hasStems_ == hasStems) + return; + hasStems_ = hasStems; + repaint(); +} + +juce::MouseCursor HeroWaveform::getMouseCursor() { + if (!hasStems_) + return juce::MouseCursor::PointingHandCursor; + return juce::Component::getMouseCursor(); +} + void HeroWaveform::mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel) { const double zoomSensitivity = 0.3; double zoomDelta = -wheel.deltaY * zoomSensitivity; @@ -277,7 +297,8 @@ void HeroWaveform::drawZoomControls(juce::Graphics& g) { }; drawBtn(0, "+", zoomInHover_); - drawBtn(1, "\u2212", zoomOutHover_); + // UTF-8 bytes, not "\u2212": MSVC turns that escape into "?" in a narrow string. + drawBtn(1, juce::String(juce::CharPointer_UTF8("\xe2\x88\x92")), zoomOutHover_); drawBtn(2, "R", zoomResetHover_); // Zoom level text @@ -355,17 +376,39 @@ void HeroWaveform::paint(juce::Graphics& g) { float midY = h * 0.5f; if (waveformPeaks_.empty()) { + const auto zone = bounds.reduced(12.0f); + if (isDragOver_) { + g.setColour(colour(colours::primary).withAlpha(0.08f)); + g.fillRoundedRectangle(zone, metrics::cornerRadiusLarge); + g.setColour(colour(colours::primary)); + g.drawRoundedRectangle(zone, metrics::cornerRadiusLarge, 2.0f); + } else { + juce::Path zonePath; + zonePath.addRoundedRectangle(zone, metrics::cornerRadiusLarge); + juce::Path dashedPath; + const float dashes[] = {6.0f, 4.0f}; + juce::PathStrokeType(1.5f).createDashedStroke(dashedPath, zonePath, dashes, 2); + g.setColour(colour(colours::surfaceBorder)); + g.fillPath(dashedPath); + } + auto upperHalf = bounds.withHeight(h * 0.5f); g.setFont(typography::subhead()); - g.setColour(colour(colours::primary).withAlpha(0.85f)); - g.drawText("Drop stems here or click Import (Ctrl+I)", upperHalf, juce::Justification::centredBottom); - - auto lowerHalf = bounds.withY(h * 0.5f).withHeight(h * 0.5f); - g.setFont(typography::caption()); - g.setColour(colour(colours::textMuted)); - g.drawText("1 Import Stems -> 2 Mix + Master -> 3 Export\n" - "Tip: enable 'AI Stem Separation' in the control deck to split one full mix into stems.", - lowerHalf.reduced(0.0f, 8.0f), juce::Justification::centredTop); + g.setColour(colour(colours::text)); + g.drawText(hasStems_ ? "Preview unavailable" : "Drop stems here or click to import", upperHalf, + juce::Justification::centredBottom); + + if (!hasStems_) { + auto lowerHalf = bounds.withY(h * 0.5f).withHeight(h * 0.5f); + g.setFont(typography::caption()); + g.setColour(colour(colours::textMuted)); + const auto arrow = juce::String(juce::CharPointer_UTF8("\xe2\x86\x92")); + auto steps = lowerHalf.reduced(0.0f, 8.0f); + g.drawText("1 Import stems " + arrow + " 2 Mix + Master " + arrow + " 3 Export", + steps.removeFromTop(18.0f), juce::Justification::centredTop); + g.drawText("One file? Turn on AI Stem Separation first.", steps.removeFromTop(18.0f), + juce::Justification::centredTop); + } } else { // Draw stem overlay if we have per-stem data if (numStemGroups_ > 0) { @@ -440,7 +483,7 @@ void HeroWaveform::paint(juce::Graphics& g) { drawZoomControls(g); } - if (isDragOver_) { + if (isDragOver_ && !waveformPeaks_.empty()) { g.setColour(colour(colours::primary).withAlpha(0.12f)); g.fillAll(); g.setColour(colour(colours::primary)); diff --git a/src/app/ui/HeroWaveform.h b/src/app/ui/HeroWaveform.h index 1af1b33..75cd629 100644 --- a/src/app/ui/HeroWaveform.h +++ b/src/app/ui/HeroWaveform.h @@ -21,6 +21,8 @@ class HeroWaveform final : public juce::Component, void paint(juce::Graphics& g) override; void mouseDown(const juce::MouseEvent& event) override; void mouseDrag(const juce::MouseEvent& event) override; + void mouseUp(const juce::MouseEvent& event) override; + juce::MouseCursor getMouseCursor() override; void mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel) override; void resized() override; @@ -36,8 +38,11 @@ class HeroWaveform final : public juce::Component, void setLoopRange(bool enabled, double startProgress, double endProgress); struct StemGroup { std::vector peaks; juce::Colour colour; juce::String name; }; void setStemGroups(const std::vector& groups); + /// Whether the session has any stems; drives the empty-state prompt, click-to-import and cursor. + void setHasStems(bool hasStems); // Callbacks + std::function onImportRequested; // empty-state zone clicked std::function onSeek; // progress fraction 0..1 std::function onZoomChanged; // new zoom factor std::function)> onFilesDropped; // audio/preset files dropped @@ -59,6 +64,13 @@ class HeroWaveform final : public juce::Component, return {x, kZoomControlTop, kZoomButtonSize, kZoomButtonSize}; } + /// Pure decision backing click-to-import: only a plain left-button click (not a drag) that is + /// released inside the component, on a session with no stems, opens Import. + static constexpr bool shouldOpenImportOnClick(bool hasStems, bool leftButton, bool wasClicked, + bool insideBounds) { + return !hasStems && leftButton && wasClicked && insideBounds; + } + /// Pure dirty-check seam backing setPlayheadProgress: returns true when the playhead's /// x-pixel changed relative to `lastPixel` (updating `lastPixel`), i.e. a repaint is /// actually needed. Unchanged pixels skip the repaint. @@ -114,6 +126,7 @@ class HeroWaveform final : public juce::Component, int numStemGroups_ = 0; bool isDragOver_ = false; + bool hasStems_ = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HeroWaveform) }; diff --git a/src/app/ui/MainLayout.cpp b/src/app/ui/MainLayout.cpp index f018096..bea9361 100644 --- a/src/app/ui/MainLayout.cpp +++ b/src/app/ui/MainLayout.cpp @@ -13,6 +13,12 @@ #include "app/ui/TaskOrchestrator.h" #include "app/ui/TransportBar.h" #include "app/ui/VerificationEngine.h" +#include "ai/BsRoformerPack.h" +#include "ai/GpuRuntimePack.h" +#include "ai/HuggingFaceModelHub.h" +#include "ai/ModelStorage.h" +#include "ai/OnnxTensorInference.h" +#include "ai/UmxPack.h" #include "renderers/RendererPipeline.h" #include "util/FileUtils.h" @@ -64,6 +70,28 @@ void automix::app::detail::updateStemPanelFromSession(StemPanel& panel, const do MainLayout::MainLayout() { setWantsKeyboardFocus(true); + // A GPU runtime removed last run may still have files on disk (they were + // locked while loaded); finish that before anything can preload them. + ai::GpuRuntimePack::completePendingRemoval(); + + // Downloaded models used to live in a working-directory-relative + // assets/modelhub; move them (with their install registry and licence + // consents) to the per-user location once. The outcome is reported once the + // task history exists. + const auto modelHubMigration = ai::migrateModelHub(ai::legacyModelHubRoot(), ai::defaultModelHubRoot()); + if (modelHubMigration.attempted) { + juce::MessageManager::callAsync([safe = juce::Component::SafePointer(this), modelHubMigration] { + if (safe == nullptr || safe->taskOrchestrator_ == nullptr) { + return; + } + safe->taskOrchestrator_->appendHistory( + modelHubMigration.error.empty() + ? "Moved " + juce::String(modelHubMigration.movedPacks) + " downloaded model(s) to " + + juce::String(ai::defaultModelHubRoot().wstring().c_str()) + : "Model folder migration incomplete (will retry next start): " + juce::String(modelHubMigration.error)); + }); + } + // 1. Create UI components headerBar_ = std::make_unique(); heroWaveform_ = std::make_unique(); @@ -85,12 +113,9 @@ MainLayout::MainLayout() { addAndMakeVisible(*taskCenter_); // Destructive Clear: MainLayout-owned (not part of TransportBar), confirmed on click. - clearTracksButton_.setTooltip("Clear Imported Tracks (asks for confirmation)"); + clearTracksButton_.setTooltip("Remove all stems from the session"); clearTracksButton_.setWantsKeyboardFocus(true); - clearTracksButton_.setColour(juce::TextButton::buttonColourId, colour(colours::surface)); - clearTracksButton_.setColour(juce::TextButton::buttonOnColourId, colour(colours::surfaceLight)); - clearTracksButton_.setColour(juce::TextButton::textColourOffId, colour(colours::warning)); - clearTracksButton_.setColour(juce::TextButton::textColourOnId, colour(colours::warning)); + setButtonVariant(clearTracksButton_, buttonVariant::danger); clearTracksButton_.onClick = [this] { onClearTracks(); }; addAndMakeVisible(clearTracksButton_); @@ -121,6 +146,7 @@ MainLayout::MainLayout() { wireControlDeckCallbacks(); wireHeroWaveformCallbacks(); + taskCenter_->onPreferredHeightChanged = [this] { resized(); }; taskCenter_->onCancel = [this] { taskOrchestrator_->cancelActiveTask(); }; // 5. Audio device & transport @@ -133,6 +159,20 @@ MainLayout::MainLayout() { // 6. Application command manager: keyboard shortcuts are dispatched here commandManager_.setFirstCommandTarget(this); commandManager_.registerAllCommandsForTarget(this); + + sessionManager_.markSaved(); + refreshStemDependentUi(); +} + +void MainLayout::refreshStemDependentUi() { + const bool hasStems = !sessionManager_.session().stems.empty(); + if (controlDeck_ != nullptr) + controlDeck_->setHasStems(hasStems); + if (heroWaveform_ != nullptr) + heroWaveform_->setHasStems(hasStems); + if (transportBar_ != nullptr) + transportBar_->setHasMedia(hasStems); + clearTracksButton_.setEnabled(hasStems); } // ── Controllers factory ──────────────────────────────────────── @@ -262,6 +302,7 @@ void MainLayout::initControllers() { safe->sessionManager_.session().stems = result.stems; safe->sessionManager_.session().originalMixPath = result.originalMixPath; updateStemPanelFromSession(safe->controlDeck_->getStemPanel(), safe->sessionManager_.session()); + safe->refreshStemDependentUi(); safe->taskOrchestrator_->finishTaskCompleted(ActiveTask::Import, "Import complete"); safe->rebuildPreview(); @@ -471,16 +512,21 @@ void MainLayout::initControllers() { if (result.cancelled) { safe->taskOrchestrator_->finishTaskCancelled(ActiveTask::Session, "Session save cancelled"); + safe->finishPendingSave(false); return; } if (!result.success) { safe->taskOrchestrator_->finishTaskFailed(ActiveTask::Session, result.errorText.toStdString()); + safe->finishPendingSave(false); return; } safe->taskOrchestrator_->appendHistory("Session saved to " + juce::String(result.path)); - safe->headerBar_->setSessionName(juce::File(result.path).getFileNameWithoutExtension()); + safe->sessionManager_.markSaved(); + safe->sessionShownModified_ = false; + safe->setSessionDisplayName(juce::File(result.path).getFileNameWithoutExtension()); safe->taskOrchestrator_->finishTaskCompleted(ActiveTask::Session, "Session saved"); + safe->finishPendingSave(true); }); }; cb.onLoadComplete = [safe](SessionLoadResult result) { @@ -523,6 +569,7 @@ void MainLayout::initComboBoxes() { // ───────────────────────────────────────────────────────────────── MainLayout::~MainLayout() { + gpuRuntimeCancel_->store(true); taskOrchestrator_->cancelAll(); stopTimer(); audioDeviceManager_.removeAudioCallback(this); @@ -562,7 +609,7 @@ void MainLayout::resized() { fb.items.add(juce::FlexItem(*headerBar_).withHeight(static_cast(kHeaderHeight))); fb.items.add(juce::FlexItem(*heroWaveform_).withFlex(1.5f).withMinHeight(120.0f)); fb.items.add(juce::FlexItem(*controlDeck_).withFlex(2.5f).withMinHeight(180.0f)); - fb.items.add(juce::FlexItem(*taskCenter_).withFlex(1.2f).withMinHeight(160.0f)); + fb.items.add(juce::FlexItem(*taskCenter_).withHeight(static_cast(taskCenter_->getPreferredHeight()))); fb.performLayout(area); } @@ -645,7 +692,7 @@ class ShortcutsDialog final : public juce::Component { } closeButton_.setButtonText("Close"); - closeButton_.onClick = [this] { exitModalState(0); }; + closeButton_.onClick = [this] { closeHostWindow(); }; addAndMakeVisible(closeButton_); } @@ -671,13 +718,18 @@ class ShortcutsDialog final : public juce::Component { bool keyPressed(const juce::KeyPress& key) override { if (key == juce::KeyPress::escapeKey) { - exitModalState(0); + closeHostWindow(); return true; } return juce::Component::keyPressed(key); } private: + void closeHostWindow() { + if (auto* dw = findParentComponentOfClass()) + dw->exitModalState(0); + } + static constexpr int kDialogWidth = 460; static constexpr int kDialogHeight = 560; @@ -758,11 +810,14 @@ void MainLayout::onTogglePlayPause() { } void MainLayout::showShortcutsDialog() { - auto* dialog = new ShortcutsDialog(shortcutTable()); - dialog->setAlwaysOnTop(true); - dialog->centreWithSize(dialog->getWidth(), dialog->getHeight()); - dialog->setVisible(true); - dialog->enterModalState(true, nullptr, true); // modal manager owns + deletes it + juce::DialogWindow::LaunchOptions options; + options.content.setOwned(new ShortcutsDialog(shortcutTable())); + options.dialogTitle = "Keyboard Shortcuts"; + options.dialogBackgroundColour = colour(colours::surface); + options.escapeKeyTriggersCloseButton = true; + options.useNativeTitleBar = true; + options.resizable = false; + options.launchAsync(); } // ───────────────────────────────────────────────────────────────── @@ -772,6 +827,15 @@ void MainLayout::showShortcutsDialog() { void MainLayout::timerCallback() { updateTransportDisplay(); + if (++modifiedCheckTicks_ >= 30) { + modifiedCheckTicks_ = 0; + const bool modified = sessionManager_.isModified(); + if (modified != sessionShownModified_) { + sessionShownModified_ = modified; + refreshSessionTitle(); + } + } + // Reconcile the transport bar with the atomic play state (covers end-of-track // auto-stop, which is realtime-only and posts no change message). const bool playing = transportController_.isPlaying(); @@ -944,10 +1008,10 @@ void MainLayout::onClearTracks() { return; // Nothing imported; nothing to clear. juce::AlertWindow::showOkCancelBox( - juce::MessageBoxIconType::WarningIcon, "Clear imported tracks", - "Clear " + juce::String(numTracks) + - " imported tracks? This cannot be undone \xe2\x80\x94 use Undo after T4.1.", - "Clear", "Cancel", this, + juce::MessageBoxIconType::WarningIcon, "Remove all stems?", + "Remove all " + juce::String(numTracks) + (numTracks == 1 ? " stem" : " stems") + + " from this session? This can't be undone.", + "Remove stems", "Cancel", this, juce::ModalCallbackFunction::create([safe = safeAsync(this), numTracks](int result) { if (safe != nullptr && confirmClear(numTracks, result == 1)) safe->performClearTracks(); @@ -960,6 +1024,7 @@ void MainLayout::performClearTracks() { transportBar_->setTimeDisplay(0.0, 0.0); sessionManager_.session().stems.clear(); detail::updateStemPanelFromSession(controlDeck_->getStemPanel(), sessionManager_.session()); + refreshStemDependentUi(); } // ───────────────────────────────────────────────────────────────── @@ -1046,7 +1111,25 @@ void MainLayout::wireControlDeckCallbacks() { sessionManager_.session().residualBlend = controlDeck_->getResidualBlendSlider().getValue(); }; controlDeck_->getSeparatedStemsToggle().onClick = [this] { - sessionManager_.session().aiStemsEnabled = controlDeck_->getSeparatedStemsToggle().getToggleState(); + const bool enabled = controlDeck_->getSeparatedStemsToggle().getToggleState(); + sessionManager_.session().aiStemsEnabled = enabled; + controlDeck_->getTensorSeparationToggle().setEnabled(enabled); + controlDeck_->setSeparationControlsVisible(enabled); + }; + controlDeck_->getTensorSeparationToggle().onClick = [this] { + const bool enabled = controlDeck_->getTensorSeparationToggle().getToggleState(); + sessionManager_.session().renderSettings.tensorSeparationEnabled = enabled; + if (enabled) { + const auto separationPack = resolveActiveModelPackForTask("separation"); + const bool lightModelActive = separationPack.has_value() && separationPack->tensorContract.has_value() && + separationPack->tensorContract->engine == "open_unmix"; + if (const auto warning = ai::vocalModelCpuWarning(lightModelActive); !warning.empty()) { + taskOrchestrator_->appendHistory("Vocal Model warning: " + juce::String(warning)); + juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::WarningIcon, "Vocal Model will be slow", + juce::String(warning)); + } + offerGpuRuntimeIfUseful(); + } }; controlDeck_->getBatchRecursiveToggle().onClick = [this] { const bool enabled = controlDeck_->getBatchRecursiveToggle().getToggleState(); @@ -1064,6 +1147,7 @@ void MainLayout::wireHeroWaveformCallbacks() { heroWaveform_->onSeek = [this](double progress) { transportController_.seekToFraction(std::clamp(progress, 0.0, 1.0)); }; + heroWaveform_->onImportRequested = [this] { onImport(); }; heroWaveform_->onFilesDropped = [this](std::vector files) { importFiles(std::move(files)); }; @@ -1164,7 +1248,12 @@ void MainLayout::importFiles(std::vector files) { useSeparation, sessionManager_.session().preferredStemCount, taskOrchestrator_->cancelFlag(ActiveTask::Import), - std::move(separationModelRoot)); + std::move(separationModelRoot), + TensorSeparationRequest{ + .enabled = sessionManager_.session().renderSettings.tensorSeparationEnabled, + .executionProvider = sessionManager_.session().renderSettings.preferHardwareAcceleration + ? sessionManager_.session().renderSettings.gpuExecutionProvider + : std::string("cpu")}); } bool MainLayout::startAiSeparationBeforeAutoMixIfNeeded() { @@ -1489,9 +1578,13 @@ void MainLayout::startBatchVerification(const std::string& outputFolder) { // Action: Save Session // ───────────────────────────────────────────────────────────────── -void MainLayout::onSaveSession() { +void MainLayout::onSaveSession(std::function done) { + finishPendingSave(false); + pendingSaveCompletion_ = std::move(done); + if (taskOrchestrator_->isTaskRunning()) { taskOrchestrator_->setStatus("Busy", "A task is already running"); + finishPendingSave(false); return; } @@ -1505,11 +1598,13 @@ void MainLayout::onSaveSession() { const auto selected = chooser.getResult(); if (selected == juce::File()) { saveSessionChooser_.reset(); + finishPendingSave(false); return; } if (!taskOrchestrator_->beginTask(ActiveTask::Session, "Saving session", "", "Session save started")) { saveSessionChooser_.reset(); + finishPendingSave(false); return; } @@ -1521,6 +1616,73 @@ void MainLayout::onSaveSession() { }); } +void MainLayout::finishPendingSave(bool success) { + if (auto done = std::move(pendingSaveCompletion_)) { + pendingSaveCompletion_ = nullptr; + done(success); + } +} + +void MainLayout::setSessionDisplayName(const juce::String& name) { + sessionDisplayName_ = name; + refreshSessionTitle(); +} + +void MainLayout::refreshSessionTitle() { + headerBar_->setSessionName(sessionDisplayName_ + (sessionShownModified_ ? " *" : "")); +} + +bool MainLayout::requestQuit(std::function quitNow) { + return requestQuitStep(std::move(quitNow), false); +} + +bool MainLayout::requestQuitStep(std::function quitNow, bool taskQuitConfirmed) { + const auto step = nextQuitStep(taskOrchestrator_->isTaskRunning(), taskQuitConfirmed, + sessionManager_.isModified()); + if (step == QuitStep::QuitNow) { + quitNow(); + return true; + } + if (quitPromptOpen_) + return false; + quitPromptOpen_ = true; + + if (step == QuitStep::ConfirmRunningTask) { + juce::AlertWindow::showOkCancelBox( + juce::MessageBoxIconType::WarningIcon, "Task still running", + "A task is still running. Quitting now will cancel it, and an unfinished export may be left " + "incomplete. Quit anyway?", + "Quit", "Cancel", this, + juce::ModalCallbackFunction::create([safe = safeAsync(this), quitNow](int result) { + if (safe == nullptr) + return; + safe->quitPromptOpen_ = false; + if (result == 1) + safe->requestQuitStep(quitNow, true); + })); + return false; + } + + juce::AlertWindow::showYesNoCancelBox( + juce::MessageBoxIconType::QuestionIcon, "Save changes?", + "Save changes to " + sessionDisplayName_ + " before closing?", + "Save", "Don't Save", "Cancel", this, + juce::ModalCallbackFunction::create([safe = safeAsync(this), quitNow](int result) { + if (safe == nullptr) + return; + safe->quitPromptOpen_ = false; + if (result == 1) { + safe->onSaveSession([quitNow](bool saved) { + if (saved) + quitNow(); + }); + } else if (result == 2) { + quitNow(); + } + })); + return false; +} + // ───────────────────────────────────────────────────────────────── // Action: Load Session // ───────────────────────────────────────────────────────────────── @@ -1677,8 +1839,9 @@ void MainLayout::onModelsDialog() { options.escapeKeyTriggersCloseButton = true; options.useNativeTitleBar = true; options.resizable = true; - options.launchAsync(); - taskOrchestrator_->appendHistory("Model browser opened"); + panel->setSize(760, 560); + if (auto* dialog = options.launchAsync()) + dialog->setResizeLimits(640, 460, 4096, 4096); } // ───────────────────────────────────────────────────────────────── @@ -1695,8 +1858,9 @@ void MainLayout::onSettings() { taskOrchestrator_->appendHistory(enabled ? "Export sidecar JSON enabled (.report.json)" : "Export sidecar JSON disabled"); - }); - settingsPanel->setSize(540, 430); + }, + gpuRuntimeStatusText(), gpuRuntimeButtonText(), [this] { onGpuRuntimeButton(); }); + settingsPanel->setSize(540, 520); juce::DialogWindow::LaunchOptions options; options.content.setOwned(settingsPanel); @@ -1706,7 +1870,6 @@ void MainLayout::onSettings() { options.useNativeTitleBar = true; options.resizable = false; options.launchAsync(); - taskOrchestrator_->appendHistory("Settings dialog opened"); } // ───────────────────────────────────────────────────────────────── @@ -1772,8 +1935,10 @@ void MainLayout::updateMeterPanel(const automaster::MasteringReport& report) { void MainLayout::applyLoadedSession(domain::Session loadedSession, const juce::String& sourcePath) { sessionManager_.replaceSession(std::move(loadedSession)); const auto& session = sessionManager_.session(); - headerBar_->setSessionName(juce::File(sourcePath).getFileNameWithoutExtension()); + sessionShownModified_ = false; + setSessionDisplayName(juce::File(sourcePath).getFileNameWithoutExtension()); updateStemPanelFromSession(controlDeck_->getStemPanel(), session); + refreshStemDependentUi(); transportBar_->setLoopEnabled(session.timeline.loopEnabled); heroWaveform_->setZoom(session.timeline.zoom, 0.5); refreshRenderers(); @@ -1783,6 +1948,7 @@ void MainLayout::applyLoadedSession(domain::Session loadedSession, const juce::S refreshProjectProfiles(); applySessionUiSelections(); syncSessionUiSelections(); + sessionManager_.markSaved(); taskOrchestrator_->appendHistory("Session loaded: " + sourcePath); rebuildPreview(); } @@ -1886,7 +2052,7 @@ void MainLayout::updateRendererChainPreview() { // ───────────────────────────────────────────────────────────────── void MainLayout::refreshModelPacks() { - modelManager_.setRootPaths({std::filesystem::path("ModelPacks"), std::filesystem::path("assets/modelhub")}); + modelManager_.setRootPaths({ai::defaultModelHubRoot(), std::filesystem::path("ModelPacks")}); const auto packs = modelManager_.scan(); const auto pickDefaultForScope = [&](const std::string& scope) -> std::optional { @@ -1965,9 +2131,10 @@ void MainLayout::updateSeparationModelBadge() { return; } + const juce::String noneText(juce::CharPointer_UTF8("Model: none installed \xe2\x80\x94 open Models")); const auto activeId = modelManager_.activePackId("separation"); if (activeId.empty()) { - controlDeck_->setSeparationModelStatus("Separation model: none", false); + controlDeck_->setSeparationModelStatus(noneText, false); return; } @@ -1976,7 +2143,7 @@ void MainLayout::updateSeparationModelBadge() { return util::toLower(pack.taskScope) == "separation" && pack.id == activeId; }); if (selected == packs.end()) { - controlDeck_->setSeparationModelStatus("Separation model: none", false); + controlDeck_->setSeparationModelStatus(noneText, false); return; } @@ -1984,7 +2151,21 @@ void MainLayout::updateSeparationModelBadge() { const auto modelPath = selected->rootPath / selected->modelFile; const bool ready = std::filesystem::is_regular_file(modelPath, error) && !error; const auto displayName = selected->name.empty() ? selected->id : selected->name; - auto badgeText = juce::String("Separation model: ") + juce::String(displayName); + const auto mentions = [&](const char* repoId) { + const juce::String repo(repoId); + const auto sanitizedRepo = repo.replaceCharacter('/', '_'); + for (const auto& field : {selected->id, selected->name, selected->source}) { + const juce::String value(field); + if (value.containsIgnoreCase(repo) || value.containsIgnoreCase(sanitizedRepo)) + return true; + } + return false; + }; + juce::String badgeText = "Model: " + juce::String(displayName); + if (mentions(ai::kBsRoformerRepoId)) + badgeText = "Model: BS-RoFormer (best quality, GPU recommended)"; + else if (mentions(ai::kUmxVocalsRepoId)) + badgeText = "Model: Open-Unmix (fast, lower quality)"; if (!ready) { badgeText << " (missing)"; } @@ -2087,6 +2268,10 @@ void MainLayout::applySessionUiSelections() { controlDeck_->getResidualBlendSlider().setValue(std::clamp(session.residualBlend, 0.0, 10.0), juce::dontSendNotification); controlDeck_->getSeparatedStemsToggle().setToggleState(session.aiStemsEnabled, juce::dontSendNotification); + controlDeck_->getTensorSeparationToggle().setToggleState(session.renderSettings.tensorSeparationEnabled, + juce::dontSendNotification); + controlDeck_->getTensorSeparationToggle().setEnabled(session.aiStemsEnabled); + controlDeck_->setSeparationControlsVisible(session.aiStemsEnabled); controlDeck_->getBatchRecursiveToggle().setToggleState(session.batchRecursiveEnabled, juce::dontSendNotification); controlDeck_->getRendererChainToggle().setToggleState( session.renderSettings.rendererChainEnabled, @@ -2100,6 +2285,7 @@ void MainLayout::syncSessionUiSelections() { auto& session = sessionManager_.session(); session.residualBlend = controlDeck_->getResidualBlendSlider().getValue(); session.aiStemsEnabled = controlDeck_->getSeparatedStemsToggle().getToggleState(); + session.renderSettings.tensorSeparationEnabled = controlDeck_->getTensorSeparationToggle().getToggleState(); session.batchRecursiveEnabled = controlDeck_->getBatchRecursiveToggle().getToggleState(); if (const auto renderer = selectionState_.rendererIdForCombo(controlDeck_->getRendererBox().getSelectedId()); renderer.has_value()) { @@ -2217,4 +2403,151 @@ std::vector MainLayout::loadConfiguredExterna return automix::app::detail::loadConfiguredExternalRenderers(onError); } + + +void MainLayout::offerGpuRuntimeIfUseful() { + if (gpuRuntimeOffered_ || gpuRuntimeInstalling_) { + return; + } + if (!ai::runtimeReportsProvider("cuda") || !ai::GpuRuntimePack::shouldOffer(kGpuRuntimeMinimumAdapterBytes)) { + return; + } + gpuRuntimeOffered_ = true; + const auto megabytes = ai::GpuRuntimePack::downloadBytes() / (1024 * 1024); + const juce::String message = + "Your NVIDIA GPU can run the vocal model about 8x faster than the CPU.\n\n" + "This needs NVIDIA's CUDA libraries (CUDA runtime, cuBLAS, cuFFT, cuDNN): a one-time download of about " + + juce::String(static_cast(megabytes)) + + " MB from NVIDIA's packages on pypi.org, installed for your user only. They are NVIDIA software under " + "NVIDIA's own licence terms and are not part of AutoMixMaster.\n\n" + "Without them, separation keeps working on the CPU."; + juce::AlertWindow::showOkCancelBox( + juce::AlertWindow::QuestionIcon, "Enable GPU acceleration?", message, "Download", "Not now", nullptr, + juce::ModalCallbackFunction::create([safe = juce::Component::SafePointer(this)](int result) { + if (safe != nullptr && result == 1) { + safe->installGpuRuntime(); + } + })); +} + +void MainLayout::installGpuRuntime() { + gpuRuntimeInstalling_ = true; + taskOrchestrator_->appendHistory("Downloading the GPU runtime (NVIDIA CUDA libraries)..."); + juce::Thread::launch([safe = juce::Component::SafePointer(this), cancel = gpuRuntimeCancel_] { + int lastDecile = -1; + const auto result = ai::GpuRuntimePack::install( + ai::GpuRuntimePack::defaultRoot(), [&](std::uint64_t done, std::uint64_t total) { + const int percent = total > 0 ? static_cast(done * 100 / total) : 0; + if (percent / 10 != lastDecile) { + lastDecile = percent / 10; + juce::MessageManager::callAsync([safe, percent] { + if (safe != nullptr) { + safe->taskOrchestrator_->appendHistory("GPU runtime download " + juce::String(percent) + "%"); + } + }); + } + return !cancel->load(); + }); + juce::MessageManager::callAsync([safe, result] { + if (safe == nullptr) { + return; + } + safe->gpuRuntimeInstalling_ = false; + safe->taskOrchestrator_->appendHistory(juce::String(result.message)); + if (result.success) { + ai::invalidateTensorProviderProbeCache(); + safe->taskOrchestrator_->appendHistory("GPU acceleration is ready."); + safe->upgradeVocalModelForGpu(); + } + }); + }); +} + +juce::String MainLayout::gpuRuntimeStatusText() const { + namespace pack = ai::GpuRuntimePack; + if (gpuRuntimeInstalling_) { + return "GPU acceleration: downloading NVIDIA CUDA libraries..."; + } + if (pack::isInstalled(pack::defaultRoot())) { + return "GPU acceleration: installed (NVIDIA CUDA libraries)"; + } + const auto adapter = pack::largestNvidiaAdapter(); + if (!adapter.has_value()) { + return "GPU acceleration: needs an NVIDIA GPU (separation runs on the CPU)"; + } + if (adapter->driverMajor.value_or(0) < pack::kMinimumDriverMajor) { + return "GPU acceleration: update the NVIDIA driver to " + juce::String(pack::kMinimumDriverMajor) + + " or newer (found " + + (adapter->driverMajor.has_value() ? juce::String(*adapter->driverMajor) : juce::String("unknown")) + ")"; + } + if (adapter->dedicatedBytes < kGpuRuntimeMinimumAdapterBytes) { + return "GPU acceleration: the vocal model needs a 12 GB NVIDIA GPU (found " + + juce::String(static_cast(adapter->dedicatedBytes / (1024 * 1024 * 1024))) + " GB)"; + } + if (!ai::runtimeReportsProvider("cuda")) { + return "GPU acceleration: not included in this build of AutoMixMaster"; + } + return "GPU acceleration: available for your NVIDIA GPU (one-time download)"; +} + +juce::String MainLayout::gpuRuntimeButtonText() const { + namespace pack = ai::GpuRuntimePack; + if (gpuRuntimeInstalling_) { + return {}; + } + if (pack::isInstalled(pack::defaultRoot())) { + return "Remove"; + } + if (ai::runtimeReportsProvider("cuda") && pack::shouldOffer(kGpuRuntimeMinimumAdapterBytes)) { + return "Install (~" + juce::String(static_cast(pack::downloadBytes() / (1024 * 1024 * 1024) + 1)) + + " GB)"; + } + return {}; +} + +void MainLayout::onGpuRuntimeButton() { + namespace pack = ai::GpuRuntimePack; + if (pack::isInstalled(pack::defaultRoot())) { + juce::AlertWindow::showAsync( + juce::MessageBoxOptions::makeOptionsOkCancel( + juce::MessageBoxIconType::WarningIcon, + "Remove GPU runtime?", + "This deletes the downloaded NVIDIA libraries. The vocal model will run on the CPU until you install them " + "again.", + "Remove", + "Cancel"), + [safe = juce::Component::SafePointer(this)](const int result) { + if (result == 0 || safe == nullptr) { + return; + } + namespace pack = ai::GpuRuntimePack; + const auto removed = pack::uninstall(); + ai::invalidateTensorProviderProbeCache(); + safe->taskOrchestrator_->appendHistory(juce::String(removed.message)); + }); + return; + } + gpuRuntimeOffered_ = true; // asked explicitly; no need to offer again this run + installGpuRuntime(); +} + +void MainLayout::upgradeVocalModelForGpu() { + // Runs off the message thread: a re-install is a ~650 MB download. + juce::Thread::launch([safe = juce::Component::SafePointer(this), + hubRoot = modelController_->modelHubRoot()] { + const auto upgraded = ai::upgradeBsRoformerForGpu(hubRoot); + if (!upgraded.has_value()) { + return; + } + juce::MessageManager::callAsync([safe, upgraded] { + if (safe != nullptr) { + safe->taskOrchestrator_->appendHistory( + juce::String(upgraded->success ? upgraded->message : "Vocal model GPU upgrade failed: " + upgraded->message)); + if (upgraded->success) { + safe->refreshModelPacks(); // the scanned pack still names the deleted quantized file + } + } + }); + }); +} } // namespace automix::app diff --git a/src/app/ui/MainLayout.h b/src/app/ui/MainLayout.h index c840181..da3cd80 100644 --- a/src/app/ui/MainLayout.h +++ b/src/app/ui/MainLayout.h @@ -31,6 +31,17 @@ namespace automix::app { +/// Next step of the quit flow (pure decision, unit-tested; the prompts themselves are modal UI). +enum class QuitStep { QuitNow, ConfirmRunningTask, PromptSave }; + +/// A running task must be confirmed once before anything else; after that (or with no task) a +/// modified session gets the Save prompt and an unmodified one quits immediately. +inline QuitStep nextQuitStep(bool taskRunning, bool taskQuitConfirmed, bool modified) { + if (taskRunning && !taskQuitConfirmed) + return QuitStep::ConfirmRunningTask; + return modified ? QuitStep::PromptSave : QuitStep::QuitNow; +} + // ── Keyboard shortcut table (single source of truth) ──────────────────── // Command ids start at 1000 (0 is reserved as "no command" by JUCE). @@ -145,6 +156,11 @@ class MainLayout final : public juce::Component, void resized() override; bool keyPressed(const juce::KeyPress& key) override; + /// Runs quitNow immediately when no task is running and the session is unchanged (returns true); + /// otherwise confirms quitting over a running task and/or asks Save / Don't Save / Cancel and + /// returns false. + bool requestQuit(std::function quitNow); + private: // Timer / Listener overrides void timerCallback() override; @@ -172,7 +188,12 @@ class MainLayout final : public juce::Component, void onAutoMixMaster(); // Pipeline: Mix -> Master -> Export void onBatch(); void onExport(); - void onSaveSession(); + void onSaveSession(std::function done = {}); + void finishPendingSave(bool success); + void refreshSessionTitle(); + bool requestQuitStep(std::function quitNow, bool taskQuitConfirmed); + void refreshStemDependentUi(); + void setSessionDisplayName(const juce::String& name); void onLoadSession(); void onModelsDialog(); void onSettings(); @@ -257,6 +278,13 @@ class MainLayout final : public juce::Component, std::unique_ptr previewManager_; SessionManager sessionManager_; + // Unsaved-changes tracking (message thread only). + juce::String sessionDisplayName_{"Untitled Session"}; + bool sessionShownModified_ = false; + int modifiedCheckTicks_ = 0; + bool quitPromptOpen_ = false; + std::function pendingSaveCompletion_; + // State std::vector analysisEntries_; std::vector rendererInfos_; @@ -281,6 +309,22 @@ class MainLayout final : public juce::Component, // Stores the folder path to export into after mastering completes. std::string pendingPipelineExportFolder_; bool pendingAutoMixAfterSeparationImport_ = false; + + // GPU runtime pack (NVIDIA CUDA libraries, downloaded on demand). Offered at + // most once per run; the cancel flag is shared with the download thread so + // closing the window stops it without a dangling pointer. + void offerGpuRuntimeIfUseful(); + void installGpuRuntime(); + void upgradeVocalModelForGpu(); + void onGpuRuntimeButton(); + juce::String gpuRuntimeStatusText() const; + juce::String gpuRuntimeButtonText() const; + // The fp32 vocal model needs 10 GiB of device memory plus desktop headroom; + // offering ~1 GB of CUDA libraries to a smaller card would buy nothing. + static constexpr std::uint64_t kGpuRuntimeMinimumAdapterBytes = (10240ull + 1536ull) * 1024 * 1024; + bool gpuRuntimeOffered_ = false; + bool gpuRuntimeInstalling_ = false; + std::shared_ptr gpuRuntimeCancel_ = std::make_shared(false); bool skipNextAutoMixSeparationCheck_ = false; // Controllers diff --git a/src/app/ui/MainLayoutInternal.h b/src/app/ui/MainLayoutInternal.h index a39cc3d..832e007 100644 --- a/src/app/ui/MainLayoutInternal.h +++ b/src/app/ui/MainLayoutInternal.h @@ -18,6 +18,7 @@ #include #include +#include "app/style/AutoMixLookAndFeel.h" #include "ai/ModelManager.h" #include "ai/OnnxModelInference.h" #include "automaster/IAutoMasterStrategy.h" @@ -234,9 +235,37 @@ class SettingsPanel final : public juce::Component { public: SettingsPanel(juce::AudioDeviceManager& audioDeviceManager, const bool writeReportJsonSidecar, - std::function onWriteReportSidecarChanged) + std::function onWriteReportSidecarChanged, + const juce::String& gpuStatus = {}, + const juce::String& gpuButtonText = {}, + std::function onGpuButton = {}) : audioSelector_(audioDeviceManager, 0, 0, 0, 2, false, false, true, false), - onWriteReportSidecarChanged_(std::move(onWriteReportSidecarChanged)) { + onWriteReportSidecarChanged_(std::move(onWriteReportSidecarChanged)), + onGpuButton_(std::move(onGpuButton)) { + // GPU acceleration: status plus an optional action (install / remove the + // per-user NVIDIA CUDA runtime), both decided by the owner. + gpuStatusLabel_.setText(gpuStatus, juce::dontSendNotification); + gpuButton_.setButtonText(gpuButtonText); + gpuButton_.setVisible(gpuButtonText.isNotEmpty()); + setButtonVariant(gpuButton_, gpuButtonText == "Remove" ? buttonVariant::danger : buttonVariant::secondary); + gpuButton_.onClick = [this] { + gpuButton_.setEnabled(false); // one action per dialog + if (onGpuButton_) { + onGpuButton_(); + } + }; + addAndMakeVisible(gpuStatusLabel_); + addChildComponent(gpuButton_); + + for (auto* heading : {&exportHeading_, &gpuHeading_, &audioHeading_}) { + heading->setFont(theme::typography::subhead()); + heading->setColour(juce::Label::textColourId, theme::colour(theme::colours::textMuted)); + addAndMakeVisible(*heading); + } + exportHeading_.setText("Export", juce::dontSendNotification); + gpuHeading_.setText("GPU acceleration", juce::dontSendNotification); + audioHeading_.setText("Audio output", juce::dontSendNotification); + reportSidecarToggle_.setButtonText("Write .report.json sidecar next to each exported file"); reportSidecarToggle_.setTooltip("Disable to export only audio files without per-file JSON report sidecars."); reportSidecarToggle_.setToggleState(writeReportJsonSidecar, juce::dontSendNotification); @@ -251,16 +280,45 @@ class SettingsPanel final : public juce::Component { } void resized() override { + styleAudioSelectorButtons(audioSelector_); auto area = getLocalBounds().reduced(10); + exportHeading_.setBounds(area.removeFromTop(24)); reportSidecarToggle_.setBounds(area.removeFromTop(28)); area.removeFromTop(10); + gpuHeading_.setBounds(area.removeFromTop(24)); + auto gpuRow = area.removeFromTop(28); + if (gpuButton_.isVisible()) { + gpuButton_.setBounds(gpuRow.removeFromRight(130)); + gpuRow.removeFromRight(8); + } + gpuStatusLabel_.setBounds(gpuRow); + area.removeFromTop(10); + audioHeading_.setBounds(area.removeFromTop(24)); audioSelector_.setBounds(area); } private: + // The "Test" button lives inside JUCE's audio selector; find it by its text. + static void styleAudioSelectorButtons(juce::Component& parent) { + for (auto* child : parent.getChildren()) { + if (auto* button = dynamic_cast(child)) { + if (button->getButtonText() == "Test") + setButtonVariant(*button, buttonVariant::secondary); + } else if (child != nullptr) { + styleAudioSelectorButtons(*child); + } + } + } + juce::AudioDeviceSelectorComponent audioSelector_; juce::ToggleButton reportSidecarToggle_; std::function onWriteReportSidecarChanged_; + juce::Label exportHeading_; + juce::Label gpuHeading_; + juce::Label audioHeading_; + juce::Label gpuStatusLabel_; + juce::TextButton gpuButton_; + std::function onGpuButton_; }; // ── Stem string helpers ──────────────────────────────────────── diff --git a/src/app/ui/ModelBrowserPanel.cpp b/src/app/ui/ModelBrowserPanel.cpp index 65031a2..33e5132 100644 --- a/src/app/ui/ModelBrowserPanel.cpp +++ b/src/app/ui/ModelBrowserPanel.cpp @@ -264,12 +264,12 @@ ModelBrowserPanel::ModelBrowserPanel() { "Repository: " + juce::String(selected->repoId) + "\n" "License: " + juce::String(licenseId.empty() ? juce::String("not declared by the publisher") : juce::String(licenseId)); if (!licenseUrl.empty()) { - dialogMsg += "\nLicense terms: " + juce::String(licenseUrl); + dialogMsg += "\nLicense terms: " + juce::String(std::string(licenseUrl)); } if (!selected->revision.empty()) { dialogMsg += "\nRevision: " + juce::String(selected->revision); } - dialogMsg += "\n\n" + juce::String(ai::ModelLicensePolicy::consentReason(licenseId)) + + dialogMsg += "\n\n" + juce::String(std::string(ai::ModelLicensePolicy::consentReason(licenseId))) + "\n\nModel weights are downloaded at runtime from the publisher and are NOT part of the " "GPL-licensed AutoMixMaster distribution. Your acknowledgement is recorded against this " "model and applies to your own use of the downloaded weights.\n\n" diff --git a/src/app/ui/SessionManager.cpp b/src/app/ui/SessionManager.cpp index 9435f32..bf186e7 100644 --- a/src/app/ui/SessionManager.cpp +++ b/src/app/ui/SessionManager.cpp @@ -1,5 +1,7 @@ #include "app/ui/SessionManager.h" +#include "domain/JsonSerialization.h" + namespace automix::app { domain::Session& SessionManager::session() { @@ -14,4 +16,17 @@ void SessionManager::replaceSession(domain::Session session) { session_ = std::move(session); } +std::string SessionManager::snapshot() const { + const domain::Json json = session_; + return json.dump(); +} + +void SessionManager::markSaved() { + savedSnapshot_ = snapshot(); +} + +bool SessionManager::isModified() const { + return snapshot() != savedSnapshot_; +} + } // namespace automix::app diff --git a/src/app/ui/SessionManager.h b/src/app/ui/SessionManager.h index 536e022..f7e3e0a 100644 --- a/src/app/ui/SessionManager.h +++ b/src/app/ui/SessionManager.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "domain/Session.h" @@ -15,8 +16,15 @@ class SessionManager { void replaceSession(domain::Session session); + /// Records the current session as the saved baseline for isModified(). + void markSaved(); + bool isModified() const; + private: + std::string snapshot() const; + domain::Session session_; + std::string savedSnapshot_; }; } // namespace automix::app diff --git a/src/app/ui/TaskCenterPanel.cpp b/src/app/ui/TaskCenterPanel.cpp index 4b55df8..1773d96 100644 --- a/src/app/ui/TaskCenterPanel.cpp +++ b/src/app/ui/TaskCenterPanel.cpp @@ -1,5 +1,7 @@ #include "app/ui/TaskCenterPanel.h" +#include "app/style/AutoMixLookAndFeel.h" + #include #include @@ -11,16 +13,16 @@ namespace automix::app { using namespace theme; TaskCenterPanel::TaskCenterPanel() : progressBar_(progressValue_), batchProgressBar_(batchProgressValue_) { - taskLabel_.setText("Ready", juce::dontSendNotification); + taskLabel_.setText("", juce::dontSendNotification); taskLabel_.setFont(typography::body()); taskLabel_.setColour(juce::Label::textColourId, colour(colours::text)); taskLabel_.setJustificationType(juce::Justification::centredLeft); - stateBadge_.setText("IDLE", juce::dontSendNotification); + stateBadge_.setText("READY", juce::dontSendNotification); stateBadge_.setFont(typography::caption()); stateBadge_.setJustificationType(juce::Justification::centred); stateBadge_.setColour(juce::Label::backgroundColourId, stateColour(TaskState::Idle)); - stateBadge_.setColour(juce::Label::textColourId, juce::Colours::white); + stateBadge_.setColour(juce::Label::textColourId, stateTextColour(TaskState::Idle)); progressLabel_.setText("0%", juce::dontSendNotification); progressLabel_.setFont(typography::caption()); @@ -39,10 +41,15 @@ TaskCenterPanel::TaskCenterPanel() : progressBar_(progressValue_), batchProgress .withPointHeight(12.0f))); historyEditor_.setText("Task history will appear here."); + toggleLogButton_.onClick = [this] { setLogVisible(!logVisible_); }; + setButtonVariant(toggleLogButton_, buttonVariant::quiet); + copyLogButton_.onClick = [this] { juce::SystemClipboard::copyTextToClipboard(historyEditor_.getText()); }; + setButtonVariant(copyLogButton_, buttonVariant::quiet); + setButtonVariant(cancelButton_, buttonVariant::secondary); cancelButton_.setEnabled(false); cancelButton_.onClick = [this] { if (onCancel) @@ -76,6 +83,7 @@ TaskCenterPanel::TaskCenterPanel() : progressBar_(progressValue_), batchProgress addAndMakeVisible(stateBadge_); addAndMakeVisible(progressBar_); addAndMakeVisible(progressLabel_); + addAndMakeVisible(toggleLogButton_); addAndMakeVisible(copyLogButton_); addAndMakeVisible(cancelButton_); addAndMakeVisible(queueHeaderLabel_); @@ -86,6 +94,9 @@ TaskCenterPanel::TaskCenterPanel() : progressBar_(progressValue_), batchProgress addAndMakeVisible(batchProgressBar_); addAndMakeVisible(historyEditor_); + progressLabel_.setVisible(false); // Idle (READY) at start + progressBar_.setVisible(false); + historyEditor_.setVisible(false); // Log is collapsed by default updateBatchSummary(); } @@ -96,7 +107,7 @@ void TaskCenterPanel::paint(juce::Graphics& g) { g.setColour(colour(colours::surfaceBorder)); g.fillRect(0, 0, getWidth(), 1); - if (!queueItems_.empty()) { + if (batchActive_ && !queueItems_.empty()) { auto area = getLocalBounds().reduced(static_cast(metrics::paddingMedium)); // Mirror resized() layout: skip top row (24) + queue header row (18) area.removeFromTop(24); @@ -145,43 +156,92 @@ void TaskCenterPanel::drawQueueItem(juce::Graphics& g, juce::Rectangle bo void TaskCenterPanel::resized() { auto area = getLocalBounds().reduced(static_cast(metrics::paddingMedium)); - // Top row: state badge + task label + progress + cancel - auto topRow = area.removeFromTop(24); + // Top row: state badge + task label + progress + log toggle + copy log + cancel + auto topRow = area.removeFromTop(kTopRowHeight); cancelButton_.setBounds(topRow.removeFromRight(64).reduced(1)); copyLogButton_.setBounds(topRow.removeFromRight(84).reduced(1)); + toggleLogButton_.setBounds(topRow.removeFromRight(84).reduced(1)); progressLabel_.setBounds(topRow.removeFromRight(48).reduced(1)); auto progressArea = topRow.removeFromRight(std::min(220, topRow.getWidth() / 2)); progressBar_.setBounds(progressArea.reduced(2)); stateBadge_.setBounds(topRow.removeFromLeft(80).reduced(1)); taskLabel_.setBounds(topRow); - // Batch queue header row - auto queueHeaderRow = area.removeFromTop(18); - queueEtaLabel_.setBounds(queueHeaderRow.removeFromRight(100).reduced(1)); - queueThroughputLabel_.setBounds(queueHeaderRow.removeFromRight(80).reduced(1)); - queueHeaderLabel_.setBounds(queueHeaderRow.reduced(1)); + if (batchActive_) { + // Batch queue header row + auto queueHeaderRow = area.removeFromTop(18); + queueEtaLabel_.setBounds(queueHeaderRow.removeFromRight(100).reduced(1)); + queueThroughputLabel_.setBounds(queueHeaderRow.removeFromRight(80).reduced(1)); + queueHeaderLabel_.setBounds(queueHeaderRow.reduced(1)); + + // Queue items + if (!queueItems_.empty()) { + int queueHeight = std::min(static_cast(queueItems_.size()), kMaxVisibleQueueItems) * kQueueItemHeight; + area.removeFromTop(queueHeight + 4); + } - // Queue items - if (!queueItems_.empty()) { - int queueHeight = std::min(static_cast(queueItems_.size()), kMaxVisibleQueueItems) * kQueueItemHeight; - area.removeFromTop(queueHeight + 4); + // Batch ETA / summary row: ETA (left) + overall progress bar (middle) + counts (right) + auto summaryRow = area.removeFromTop(22); + etaLabel_.setBounds(summaryRow.removeFromLeft(92).reduced(1)); + batchCountsLabel_.setBounds(summaryRow.removeFromRight(190).reduced(1)); + batchProgressBar_.setBounds(summaryRow.reduced(2, 4)); } - // Batch ETA / summary row: ETA (left) + overall progress bar (middle) + counts (right) - auto summaryRow = area.removeFromTop(22); - etaLabel_.setBounds(summaryRow.removeFromLeft(92).reduced(1)); - batchCountsLabel_.setBounds(summaryRow.removeFromRight(190).reduced(1)); - batchProgressBar_.setBounds(summaryRow.reduced(2, 4)); - - // Remaining: history editor + // Remaining: history editor (only when the log is shown) area.removeFromTop(4); - historyEditor_.setBounds(area); + if (logVisible_) + historyEditor_.setBounds(area); } -void TaskCenterPanel::setQueueItems(const std::vector& items) { - queueItems_ = items; +int TaskCenterPanel::getPreferredHeight() const { + int height = 2 * static_cast(metrics::paddingMedium) + kTopRowHeight; + if (batchActive_) { + height += 18 + 22; + if (!queueItems_.empty()) + height += std::min(static_cast(queueItems_.size()), kMaxVisibleQueueItems) * kQueueItemHeight + 4; + } + if (logVisible_) + height += 4 + kLogAreaHeight; + return height; +} + +void TaskCenterPanel::setLogVisible(bool visible) { + if (logVisible_ == visible) + return; + logVisible_ = visible; + toggleLogButton_.setButtonText(logVisible_ ? "Hide log" : "Show log"); + historyEditor_.setVisible(logVisible_); + resized(); + repaint(); + if (onPreferredHeightChanged) + onPreferredHeightChanged(); +} + +bool TaskCenterPanel::isBatchStripVisible() const { + return batchActive_; +} + +bool TaskCenterPanel::isLogVisible() const { + return logVisible_; +} + +void TaskCenterPanel::refreshBatchVisibility() { + batchActive_ = batchDetail_.totalCount > 0 || !queueItems_.empty(); + queueHeaderLabel_.setVisible(batchActive_); + queueEtaLabel_.setVisible(batchActive_); + queueThroughputLabel_.setVisible(batchActive_); + etaLabel_.setVisible(batchActive_); + batchCountsLabel_.setVisible(batchActive_); + batchProgressBar_.setVisible(batchActive_); resized(); repaint(); + if (onPreferredHeightChanged) + onPreferredHeightChanged(); +} + +void TaskCenterPanel::setQueueItems(const std::vector& items) { + queueItems_ = items; + refreshBatchVisibility(); } void TaskCenterPanel::setQueueEta(const juce::String& eta) { @@ -210,8 +270,7 @@ void TaskCenterPanel::removeQueueItem(int index) { if (index < 0 || index >= static_cast(queueItems_.size())) return; queueItems_.erase(queueItems_.begin() + index); - resized(); - repaint(); + refreshBatchVisibility(); if (onQueueItemRemoved) onQueueItemRemoved(index); } @@ -273,13 +332,14 @@ juce::String TaskCenterPanel::formatEta(const juce::RelativeTime& eta) { } void TaskCenterPanel::updateBatchSummary() { + refreshBatchVisibility(); const size_t total = batchDetail_.totalCount; const size_t completed = batchDetail_.completedCount; const size_t failed = batchDetail_.failedCount; if (total == 0) { // No batch items: em dash "—". - etaLabel_.setText(juce::String(static_cast(0x2014)), juce::dontSendNotification); + etaLabel_.setText(juce::String::charToString(static_cast(0x2014)), juce::dontSendNotification); } else { const int currentIndex = static_cast(batchDetail_.itemIndex) + 1; if (currentIndex >= static_cast(total)) { @@ -301,8 +361,13 @@ void TaskCenterPanel::setCanCancel(bool canCancel) { void TaskCenterPanel::setTaskState(TaskState state) { currentState_ = state; + progressLabel_.setVisible(state != TaskState::Idle); + progressBar_.setVisible(state != TaskState::Idle); // the bar paints its own "0%" text + if (state == TaskState::Failed) + setLogVisible(true); stateBadge_.setText(juce::String(stateLabel(state)), juce::dontSendNotification); stateBadge_.setColour(juce::Label::backgroundColourId, stateColour(state)); + stateBadge_.setColour(juce::Label::textColourId, stateTextColour(state)); stateBadge_.repaint(); } @@ -369,9 +434,20 @@ juce::Colour TaskCenterPanel::stateColour(TaskState state) { return colour(colours::textMuted); } +juce::Colour TaskCenterPanel::stateTextColour(TaskState state) { + switch (state) { + case TaskState::Idle: + case TaskState::Cancelled: + case TaskState::Completed: return colour(colours::background); + case TaskState::Running: + case TaskState::Failed: return juce::Colours::white; + } + return colour(colours::background); +} + const char* TaskCenterPanel::stateLabel(TaskState state) { switch (state) { - case TaskState::Idle: return "IDLE"; + case TaskState::Idle: return "READY"; case TaskState::Running: return "RUNNING"; case TaskState::Cancelled: return "CANCELLED"; case TaskState::Completed: return "COMPLETED"; diff --git a/src/app/ui/TaskCenterPanel.h b/src/app/ui/TaskCenterPanel.h index bf3a79b..08bd239 100644 --- a/src/app/ui/TaskCenterPanel.h +++ b/src/app/ui/TaskCenterPanel.h @@ -55,7 +55,14 @@ class TaskCenterPanel final : public juce::Component { /// Current item-status counts text ("completed/failed/total"). juce::String batchCountsText() const; + /// Height needed for the top row, the batch strip (when active) and the log (when shown). + int getPreferredHeight() const; + bool isBatchStripVisible() const; + bool isLogVisible() const; + bool isProgressPercentVisible() const { return progressLabel_.isVisible(); } + // Callbacks + std::function onPreferredHeightChanged; std::function onCancel; std::function onQueueItemMoved; std::function onQueueItemRemoved; @@ -65,18 +72,24 @@ class TaskCenterPanel final : public juce::Component { static constexpr int kHistoryTrimChunkLines = 300; static constexpr int kQueueItemHeight = 24; static constexpr int kMaxVisibleQueueItems = 8; + static constexpr int kTopRowHeight = 24; + static constexpr int kLogAreaHeight = 160; static juce::Colour stateColour(TaskState state); + static juce::Colour stateTextColour(TaskState state); static const char* stateLabel(TaskState state); void drawQueueItem(juce::Graphics& g, juce::Rectangle bounds, const BatchQueueItem& item, int index); static juce::String formatEta(const juce::RelativeTime& eta); void updateBatchSummary(); + void refreshBatchVisibility(); + void setLogVisible(bool visible); juce::Label taskLabel_; juce::Label stateBadge_; double progressValue_ = 0.0; juce::ProgressBar progressBar_; juce::Label progressLabel_; + juce::TextButton toggleLogButton_{"Show log"}; juce::TextButton copyLogButton_{"Copy Log"}; juce::TextButton cancelButton_{"Cancel"}; juce::TextEditor historyEditor_; @@ -96,6 +109,8 @@ class TaskCenterPanel final : public juce::Component { juce::ProgressBar batchProgressBar_; TaskState currentState_ = TaskState::Idle; + bool batchActive_ = false; + bool logVisible_ = false; bool hasHistoryEntries_ = false; int historyLineCount_ = 0; diff --git a/src/app/ui/TransportBar.cpp b/src/app/ui/TransportBar.cpp index f5aec32..9aef5ff 100644 --- a/src/app/ui/TransportBar.cpp +++ b/src/app/ui/TransportBar.cpp @@ -1,5 +1,7 @@ #include "app/ui/TransportBar.h" +#include "app/style/AutoMixLookAndFeel.h" + #include #include @@ -19,6 +21,20 @@ TransportBar::TransportBar() { addAndMakeVisible(volumeSlider_); addAndMakeVisible(loopToggle_); + for (juce::Button* btn : {static_cast(&skipStartButton_), static_cast(&playPauseButton_), + static_cast(&stopButton_), static_cast(&skipEndButton_), + static_cast(&shortcutsButton_)}) + setButtonVariant(*btn, buttonVariant::secondary); + + // Accessibility titles + skipStartButton_.setTitle("Skip to start"); + playPauseButton_.setTitle("Play"); + stopButton_.setTitle("Stop"); + skipEndButton_.setTitle("Skip to end"); + shortcutsButton_.setTitle("Keyboard shortcuts"); + loopToggle_.setTitle("Loop"); + volumeSlider_.setTitle("Volume"); + // Tooltips skipStartButton_.setTooltip("Skip to Start (Home)"); playPauseButton_.setTooltip("Play / Pause (Space)"); @@ -110,6 +126,13 @@ void TransportBar::resized() { timeLabel_.setBounds(area); } +void TransportBar::setHasMedia(bool hasMedia) { + skipStartButton_.setEnabled(hasMedia); + playPauseButton_.setEnabled(hasMedia); + stopButton_.setEnabled(hasMedia); + skipEndButton_.setEnabled(hasMedia); +} + void TransportBar::setPlaying(bool playing) { isPlaying_ = playing; playPauseButton_.setButtonText(playing ? "Pause" : "Play"); diff --git a/src/app/ui/TransportBar.h b/src/app/ui/TransportBar.h index 236bf53..468dc15 100644 --- a/src/app/ui/TransportBar.h +++ b/src/app/ui/TransportBar.h @@ -17,6 +17,8 @@ class TransportBar final : public juce::Component, private juce::Slider::Listene bool keyPressed(const juce::KeyPress& key) override; void setPlaying(bool playing); + /// Enables the skip/play/stop buttons only when there is media to transport. + void setHasMedia(bool hasMedia); void setTimeDisplay(double currentSeconds, double totalSeconds); void setVolume(double volume); void setLoopEnabled(bool enabled); diff --git a/src/domain/JsonSerialization.cpp b/src/domain/JsonSerialization.cpp index eb82f6c..cccb667 100644 --- a/src/domain/JsonSerialization.cpp +++ b/src/domain/JsonSerialization.cpp @@ -81,6 +81,7 @@ void to_json(Json& j, const RenderSettings& value) { {"mp3VbrQuality", value.mp3VbrQuality}, {"processingThreads", value.processingThreads}, {"preferHardwareAcceleration", value.preferHardwareAcceleration}, + {"tensorSeparationEnabled", value.tensorSeparationEnabled}, {"metadataPolicy", value.metadataPolicy}, {"metadataTemplate", value.metadataTemplate}, {"rendererName", value.rendererName}, @@ -111,6 +112,8 @@ void from_json(const Json& j, RenderSettings& value) { value.mp3VbrQuality = std::clamp(j.value("mp3VbrQuality", 4), 0, 9); value.processingThreads = std::max(0, j.value("processingThreads", 0)); value.preferHardwareAcceleration = j.value("preferHardwareAcceleration", true); + // Absent in sessions saved before the toggle existed; must stay off for them. + value.tensorSeparationEnabled = j.value("tensorSeparationEnabled", false); value.metadataPolicy = j.value("metadataPolicy", "copy_all"); if (value.metadataPolicy != "copy_all" && value.metadataPolicy != "copy_common" && @@ -120,7 +123,7 @@ void from_json(const Json& j, RenderSettings& value) { value.metadataPolicy = "copy_all"; } value.metadataTemplate = j.value("metadataTemplate", std::map{}); - value.rendererName = j.value("rendererName", "PhaseLimiter"); + value.rendererName = j.value("rendererName", "BuiltIn"); value.rendererChainEnabled = j.value("rendererChainEnabled", false); value.rendererChainMode = j.value("rendererChainMode", "logical_all"); if (value.rendererChainMode != "logical_all" && value.rendererChainMode != "master_then_rsgain") { @@ -308,6 +311,8 @@ void from_json(const Json& j, Session& value) { value.residualBlend = std::clamp(j.value("residualBlend", 0.0), 0.0, 10.0); value.aiStemsEnabled = j.value("aiStemsEnabled", false); value.batchRecursiveEnabled = j.value("batchRecursiveEnabled", false); + // Session files saved before this field existed were mastered with Udio Optimized, so keep that + // fallback for them; new sessions get Default Streaming from the Session struct default instead. value.selectedMasterPreset = masterPresetFromString(j.value("selectedMasterPreset", "udio_optimized")); value.selectedPlatformPreset = masterPresetFromString(j.value("selectedPlatformPreset", "youtube")); value.stems = j.value("stems", std::vector{}); @@ -337,6 +342,16 @@ void from_json(const Json& j, Session& value) { } else { value.renderSettings = RenderSettings{}; } + // Schema 3 made PhaseLimiter opt-in. Earlier sessions stored "PhaseLimiter" + // as the default rather than a choice, and every such render fell back to + // BuiltIn (the renderer never ran correctly before schema 3), so BuiltIn is + // exactly what those sessions have been producing. + if (value.schemaVersion < 3) { + if (value.renderSettings.rendererName == "PhaseLimiter") { + value.renderSettings.rendererName = "BuiltIn"; + } + value.schemaVersion = 3; + } if (j.contains("mixPlan") && !j.at("mixPlan").is_null()) { value.mixPlan = j.at("mixPlan").get(); diff --git a/src/domain/ProjectProfile.cpp b/src/domain/ProjectProfile.cpp index 630d3ac..0aef4c4 100644 --- a/src/domain/ProjectProfile.cpp +++ b/src/domain/ProjectProfile.cpp @@ -14,7 +14,7 @@ ProjectProfile profileFromJson(const nlohmann::json& json) { profile.id = json.value("id", ""); profile.name = json.value("name", profile.id); profile.platformPreset = json.value("platformPreset", json.value("platform", "spotify")); - profile.rendererName = json.value("rendererName", "PhaseLimiter"); + profile.rendererName = json.value("rendererName", "BuiltIn"); profile.outputFormat = json.value("outputFormat", "wav"); profile.lossyBitrateKbps = std::clamp(json.value("lossyBitrateKbps", 320), 64, 320); profile.mp3UseVbr = json.value("mp3UseVbr", false); @@ -86,7 +86,7 @@ std::vector defaultProjectProfiles() { .id = "default", .name = "Default Balanced", .platformPreset = "spotify", - .rendererName = "PhaseLimiter", + .rendererName = "BuiltIn", .outputFormat = "wav", .lossyBitrateKbps = 320, .mp3UseVbr = false, @@ -99,13 +99,13 @@ std::vector defaultProjectProfiles() { .preferredStemCount = 4, .metadataPolicy = "copy_common", .metadataTemplate = {}, - .pinnedRendererIds = {"PhaseLimiter", "BuiltIn"}, + .pinnedRendererIds = {"BuiltIn", "PhaseLimiter"}, }, ProjectProfile{ .id = "streaming_spotify", .name = "Streaming Spotify", .platformPreset = "spotify", - .rendererName = "PhaseLimiter", + .rendererName = "BuiltIn", .outputFormat = "mp3", .lossyBitrateKbps = 256, .mp3UseVbr = true, @@ -118,7 +118,7 @@ std::vector defaultProjectProfiles() { .preferredStemCount = 4, .metadataPolicy = "copy_common", .metadataTemplate = {}, - .pinnedRendererIds = {"PhaseLimiter", "BuiltIn"}, + .pinnedRendererIds = {"BuiltIn", "PhaseLimiter"}, }, ProjectProfile{ .id = "mobile_fast_turn", diff --git a/src/domain/ProjectProfile.h b/src/domain/ProjectProfile.h index 7b8796f..501dc0f 100644 --- a/src/domain/ProjectProfile.h +++ b/src/domain/ProjectProfile.h @@ -18,7 +18,7 @@ struct ProjectProfile { std::string id; std::string name; std::string platformPreset = "spotify"; - std::string rendererName = "PhaseLimiter"; + std::string rendererName = "BuiltIn"; std::string outputFormat = "wav"; int lossyBitrateKbps = 320; bool mp3UseVbr = false; diff --git a/src/domain/RenderSettings.h b/src/domain/RenderSettings.h index 51cfff8..307b12a 100644 --- a/src/domain/RenderSettings.h +++ b/src/domain/RenderSettings.h @@ -27,9 +27,15 @@ struct RenderSettings { // additionally requires the experimental toggle, CC BY-NC consent and a complete // pack. Never default this on. bool itoMasteringEnabled = false; + // Tensor (BS-RoFormer) stem separation on single-mix import. Not sufficient on + // its own: StemSeparator additionally requires an active separation pack that + // carries a tensor_contract and a native ONNX Runtime build. Never default this on. + bool tensorSeparationEnabled = false; std::string metadataPolicy = "copy_all"; std::map metadataTemplate; - std::string rendererName = "PhaseLimiter"; + // PhaseLimiter is opt-in: selecting it (here or as a custom chain stage) is + // the only way it runs, including in the logical_all chain. + std::string rendererName = "BuiltIn"; bool rendererChainEnabled = false; std::string rendererChainMode = "logical_all"; std::vector rendererChain; diff --git a/src/domain/Session.h b/src/domain/Session.h index 2d72eb5..6ffda79 100644 --- a/src/domain/Session.h +++ b/src/domain/Session.h @@ -21,13 +21,13 @@ struct TimelineState { }; struct Session { - int schemaVersion = 2; + int schemaVersion = 3; // 3: PhaseLimiter opt-in (see JsonSerialization.cpp) std::string sessionName; std::optional originalMixPath; double residualBlend = 0.0; bool aiStemsEnabled = false; bool batchRecursiveEnabled = false; - MasterPreset selectedMasterPreset = MasterPreset::UdioOptimized; + MasterPreset selectedMasterPreset = MasterPreset::DefaultStreaming; MasterPreset selectedPlatformPreset = MasterPreset::YouTube; std::vector stems; std::vector buses; diff --git a/src/renderers/PhaseLimiterDiscovery.cpp b/src/renderers/PhaseLimiterDiscovery.cpp index 47bae99..c9fa4b7 100644 --- a/src/renderers/PhaseLimiterDiscovery.cpp +++ b/src/renderers/PhaseLimiterDiscovery.cpp @@ -1,4 +1,6 @@ #include "renderers/PhaseLimiterDiscovery.h" +#include "renderers/PhaseLimiterPins.h" +#include "renderers/FfmpegDiscovery.h" #include #include @@ -11,6 +13,7 @@ #include #include "util/FileUtils.h" +#include "util/Sha256.h" #include "util/StringUtils.h" namespace automix::renderers { @@ -74,7 +77,7 @@ std::vector executableNames() { #if defined(_WIN32) return {"phase_limiter.exe", "phaselimiter.exe", "phase_limiter", "phaselimiter"}; #else - return {"phase_limiter", "phaselimiter", "phase_limiter.bin", "phaselimiter.bin", "phase_limiter.exe"}; + return {"phase_limiter", "phaselimiter", "phase_limiter.bin", "phaselimiter.bin"}; #endif } @@ -163,6 +166,7 @@ std::vector baseAssetDirectories(const std::filesystem::p root / "resources" / "assets", root / "Resources" / "assets", root / "Contents" / "Resources" / "assets", + root / "Contents" / "MacOS" / "assets", }; } @@ -250,20 +254,92 @@ bool downloadToFile(const std::string& url, const std::filesystem::path& outputP return true; } -std::optional defaultDownloadUrl() { - if (const auto manual = readEnvironment("AUTOMIX_PHASELIMITER_DOWNLOAD_URL"); manual.has_value()) { - return manual; - } +} // namespace +std::string currentPhaseLimiterPlatformKey() { #if defined(_WIN32) - return std::string("https://github.com/ai-mastering/phaselimiter/releases/download/v0.2.0/phaselimiter-win.zip"); + #if defined(_M_X64) || defined(__x86_64__) + return "windows-x64"; + #elif defined(_M_ARM64) || defined(__aarch64__) + return "windows-arm64"; + #else + return "windows-x86"; + #endif +#elif defined(__APPLE__) + #if defined(__aarch64__) || defined(__arm64__) + return "macos-arm64"; + #elif defined(__x86_64__) + return "macos-x86_64"; + #else + return "macos-unknown"; + #endif #elif defined(__linux__) - return std::string("https://github.com/ai-mastering/phaselimiter/releases/download/v0.2.0/release.tar.xz"); + #if defined(__x86_64__) + return "linux-x64"; + #elif defined(__aarch64__) + return "linux-arm64"; + #else + return "linux-unknown"; + #endif #else - return std::nullopt; + return "unknown"; #endif } +std::string phaseLimiterPlatformKeyForResolution(const std::string& platformKey) { + if (platformKey == "windows-arm64") { + return "windows-x64"; + } + return platformKey; +} + +std::vector phaseLimiterDownloadPinTable() { + std::vector table; + table.reserve(std::size(kPhaseLimiterPins)); + for (const auto& pin : kPhaseLimiterPins) { + table.push_back({pin.platformKey, pin.url, pin.sha256}); + } + return table; +} + +std::optional defaultPhaseLimiterDownloadPin() { + if (const auto manual = readEnvironment("AUTOMIX_PHASELIMITER_DOWNLOAD_URL"); manual.has_value()) { + PhaseLimiterDownloadPin pin; + pin.platformKey = currentPhaseLimiterPlatformKey(); + pin.url = manual.value(); + if (const auto manualSha = readEnvironment("AUTOMIX_PHASELIMITER_DOWNLOAD_SHA256"); manualSha.has_value()) { + pin.sha256 = manualSha.value(); + } + return pin; + } + + const auto currentKey = currentPhaseLimiterPlatformKey(); + const auto resolutionKey = phaseLimiterPlatformKeyForResolution(currentKey); + const auto table = phaseLimiterDownloadPinTable(); + for (const auto& pin : table) { + if (pin.platformKey == resolutionKey) { + if (pin.url.empty() || pin.sha256.empty()) { + return std::nullopt; + } + return pin; + } + } + return std::nullopt; +} + +namespace { + +static std::optional gDownloadFetcher; +static bool gAttemptedDownload = false; +static std::mutex gDownloadMutex; + +bool fetchArchive(const std::string& url, const std::filesystem::path& destination) { + if (gDownloadFetcher.has_value()) { + return (*gDownloadFetcher)(url, destination); + } + return downloadToFile(url, destination); +} + bool isSafeArchivePath(const std::filesystem::path& relativePath) { if (relativePath.empty() || relativePath.is_absolute()) { return false; @@ -353,31 +429,80 @@ std::optional resolveFromCacheInstall() { return scanDirectoryRecursive(cacheInstallRoot(), 6); } -std::optional resolveFromAutoDownload() { +// Ordered by priority: AUTOMIX_ASSET_ROOT is an override, so it is searched +// first. (A std::set here once sorted roots lexicographically, which let the +// executable's own tree win whenever its path sorted before the override.) +std::vector defaultRoots() { + std::vector candidates; + if (const auto assetRoot = readEnvironment("AUTOMIX_ASSET_ROOT"); assetRoot.has_value()) { + candidates.emplace_back(assetRoot.value()); + } + candidates.push_back(std::filesystem::current_path()); + + const juce::File executable = juce::File::getSpecialLocation(juce::File::currentExecutableFile); + const std::filesystem::path executableDir(executable.getParentDirectory().getFullPathName().toStdString()); + candidates.push_back(executableDir); + candidates.push_back(executableDir / ".." / "Resources"); + + std::set seen; + std::vector output; + output.reserve(candidates.size()); + for (const auto& root : candidates) { + if (seen.insert(root).second) { + output.push_back(root); + } + } + return output; +} + +} // namespace + +void PhaseLimiterDiscovery::setDownloadFetcherForTesting(DownloadFetcher fetcher) { + gDownloadFetcher = std::move(fetcher); +} + +void PhaseLimiterDiscovery::resetDownloadFetcherForTesting() { + gDownloadFetcher.reset(); +} + +void PhaseLimiterDiscovery::resetAttemptedDownloadForTesting() { + std::scoped_lock lock(gDownloadMutex); + gAttemptedDownload = false; +} + +std::optional PhaseLimiterDiscovery::downloadAndInstall( + const std::optional& customPin) { if (flagEnabled("AUTOMIX_PHASELIMITER_SKIP_DOWNLOAD")) { return std::nullopt; } - const auto url = defaultDownloadUrl(); - if (!url.has_value()) { + const auto pin = customPin.has_value() ? customPin : defaultPhaseLimiterDownloadPin(); + if (!pin.has_value() || pin->url.empty()) { return std::nullopt; } - if (const auto cachedInfo = resolveFromCacheInstall(); cachedInfo.has_value()) { - return cachedInfo; + const bool isManualUrlOverride = readEnvironment("AUTOMIX_PHASELIMITER_DOWNLOAD_URL").has_value(); + if (pin->sha256.empty() && !isManualUrlOverride) { + juce::Logger::writeToLog("ERROR: PhaseLimiter download pin has empty SHA-256 hash. Rejected unverified download."); + return std::nullopt; } - static std::mutex downloadMutex; - static bool attemptedDownload = false; - std::scoped_lock lock(downloadMutex); + if (!customPin.has_value()) { + if (const auto cachedInfo = resolveFromCacheInstall(); cachedInfo.has_value()) { + return cachedInfo; + } + } - if (const auto cachedInfo = resolveFromCacheInstall(); cachedInfo.has_value()) { - return cachedInfo; + std::scoped_lock lock(gDownloadMutex); + if (!customPin.has_value()) { + if (const auto cachedInfo = resolveFromCacheInstall(); cachedInfo.has_value()) { + return cachedInfo; + } } - if (attemptedDownload) { + if (gAttemptedDownload) { return std::nullopt; } - attemptedDownload = true; + gAttemptedDownload = true; std::error_code error; std::filesystem::create_directories(cacheToolsRoot(), error); @@ -385,93 +510,86 @@ std::optional resolveFromAutoDownload() { return std::nullopt; } - const auto lowerUrl = toLower(url.value()); + const auto lowerUrl = toLower(pin->url); const std::filesystem::path archivePath = cacheToolsRoot() / "phaselimiter_download"; + std::filesystem::path destinationFile; + bool isArchive = false; + bool isZip = false; if (lowerUrl.ends_with(".zip")) { - const auto zipPath = archivePath.string() + ".zip"; - if (!downloadToFile(url.value(), zipPath)) { - return std::nullopt; - } - std::filesystem::remove_all(cacheInstallRoot(), error); - if (!extractZipArchive(zipPath, cacheToolsRoot())) { - return std::nullopt; - } + destinationFile = archivePath.string() + ".zip"; + isArchive = true; + isZip = true; } else if (lowerUrl.ends_with(".tar.xz") || lowerUrl.ends_with(".txz")) { - const auto tarPath = archivePath.string() + ".tar.xz"; - if (!downloadToFile(url.value(), tarPath)) { - return std::nullopt; - } - std::filesystem::remove_all(cacheInstallRoot(), error); - if (!extractTarXzArchive(tarPath, cacheToolsRoot())) { - return std::nullopt; - } - } else if (lowerUrl.ends_with(".sh")) { - if (!flagEnabled("AUTOMIX_PHASELIMITER_ALLOW_LINUX_INSTALL_SCRIPT")) { - return std::nullopt; - } -#if defined(__linux__) - const auto scriptPath = archivePath.string() + ".sh"; - if (!downloadToFile(url.value(), scriptPath)) { + destinationFile = archivePath.string() + ".tar.xz"; + isArchive = true; + isZip = false; + } else { + std::filesystem::create_directories(cacheInstallRoot() / "bin", error); + if (error) { return std::nullopt; } + destinationFile = cacheBinaryPath(); + isArchive = false; + } - juce::StringArray command; - command.add("bash"); - command.add(scriptPath); - juce::ChildProcess process; - if (!process.start(command) || !process.waitForProcessToFinish(180000) || process.getExitCode() != 0) { + if (!fetchArchive(pin->url, destinationFile)) { + std::filesystem::remove(destinationFile, error); + return std::nullopt; + } + + // SHA-256 verification before extraction + if (!pin->sha256.empty()) { + const auto actualSha = toLower(automix::util::fileSha256(destinationFile)); + const auto expectedSha = toLower(pin->sha256); + if (actualSha != expectedSha) { + std::filesystem::remove(destinationFile, error); + juce::Logger::writeToLog("PhaseLimiter download SHA-256 mismatch for " + + destinationFile.string() + " (expected " + expectedSha + + ", got " + (actualSha.empty() ? "unreadable" : actualSha) + + "); download discarded."); return std::nullopt; } -#else - return std::nullopt; -#endif } else { - std::filesystem::create_directories(cacheInstallRoot() / "bin", error); - if (error || !downloadToFile(url.value(), cacheBinaryPath())) { + juce::Logger::writeToLog("WARNING: PhaseLimiter downloaded without SHA-256 verification (unpinned URL)."); + } + + if (isArchive) { + std::filesystem::remove_all(cacheInstallRoot(), error); + const bool extracted = isZip ? extractZipArchive(destinationFile, cacheToolsRoot()) + : extractTarXzArchive(destinationFile, cacheToolsRoot()); + std::filesystem::remove(destinationFile, error); + if (!extracted) { return std::nullopt; } } if (const auto downloadedInfo = resolveFromCacheInstall(); downloadedInfo.has_value()) { +#if !defined(_WIN32) + const auto currentPermissions = std::filesystem::status(downloadedInfo->executablePath, error).permissions(); + if (!error) { + constexpr auto executeFlags = std::filesystem::perms::owner_exec | + std::filesystem::perms::group_exec | + std::filesystem::perms::others_exec; + std::filesystem::permissions(downloadedInfo->executablePath, currentPermissions | executeFlags, error); + } +#endif return downloadedInfo; } return std::nullopt; } -std::vector defaultRoots() { - std::set roots; - roots.insert(std::filesystem::current_path()); - - const juce::File executable = juce::File::getSpecialLocation(juce::File::currentExecutableFile); - const std::filesystem::path executableDir(executable.getParentDirectory().getFullPathName().toStdString()); - roots.insert(executableDir); - roots.insert(executableDir / ".." / "Resources"); - if (const auto assetRoot = readEnvironment("AUTOMIX_ASSET_ROOT"); assetRoot.has_value()) { - roots.insert(assetRoot.value()); - } - - std::vector output; - output.reserve(roots.size()); - for (const auto& root : roots) { - output.push_back(root); - } - return output; -} - -} // namespace - std::optional PhaseLimiterDiscovery::find() const { if (const auto fromEnv = resolveFromEnvironment(); fromEnv.has_value()) { return fromEnv; } - if (const auto cached = toBinaryInfo(cacheBinaryPath()); cached.has_value()) { - return cached; - } if (const auto fromLocal = findInRoots(defaultRoots()); fromLocal.has_value()) { return fromLocal; } - return resolveFromAutoDownload(); + if (const auto cached = resolveFromCacheInstall(); cached.has_value()) { + return cached; + } + return downloadAndInstall(); } std::optional PhaseLimiterDiscovery::findInRoots( @@ -491,4 +609,34 @@ std::optional PhaseLimiterDiscovery::findInRoots( return std::nullopt; } +namespace { +std::mutex s_ffmpegResolverMutex; +FfmpegResolver s_ffmpegResolverForTesting = nullptr; +} // namespace + +void setFfmpegResolverForTesting(FfmpegResolver resolver) { + std::lock_guard lock(s_ffmpegResolverMutex); + s_ffmpegResolverForTesting = std::move(resolver); +} + +void resetFfmpegResolverForTesting() { + std::lock_guard lock(s_ffmpegResolverMutex); + s_ffmpegResolverForTesting = nullptr; +} + +std::optional ffmpegForPhaseLimiter() { + { + std::lock_guard lock(s_ffmpegResolverMutex); + if (s_ffmpegResolverForTesting) { + return s_ffmpegResolverForTesting(); + } + } + + FfmpegDiscovery discovery; + if (const auto ffmpeg = discovery.find(); ffmpeg.has_value()) { + return ffmpeg->executablePath; + } + return std::nullopt; +} + } // namespace automix::renderers diff --git a/src/renderers/PhaseLimiterDiscovery.h b/src/renderers/PhaseLimiterDiscovery.h index 757832f..77ddce0 100644 --- a/src/renderers/PhaseLimiterDiscovery.h +++ b/src/renderers/PhaseLimiterDiscovery.h @@ -1,7 +1,10 @@ #pragma once #include +#include #include +#include +#include #include namespace automix::renderers { @@ -11,10 +14,63 @@ struct PhaseLimiterBinaryInfo { std::filesystem::path installRoot; }; +// The data files phase_limiter reads. Passed by absolute path: their built-in +// defaults ("./mastering_reference.json") resolve against the working +// directory and silently miss the copies under resource/, which made every +// run fail with "auto mastering error: syntax error". +inline std::filesystem::path masteringReferencePath(const PhaseLimiterBinaryInfo& info) { + return info.installRoot / "resource" / "mastering_reference.json"; +} +inline std::filesystem::path soundQualityCachePath(const PhaseLimiterBinaryInfo& info) { + return info.installRoot / "resource" / "sound_quality2_cache"; +} +inline std::filesystem::path licensesDirectoryPath(const PhaseLimiterBinaryInfo& info) { + return info.installRoot / "licenses"; +} + +// A binary without its mastering reference cannot master; treat it as absent. +inline bool isCompleteInstall(const PhaseLimiterBinaryInfo& info) { + std::error_code error; + return std::filesystem::is_regular_file(masteringReferencePath(info), error) && !error; +} + +struct PhaseLimiterDownloadPin { + std::string platformKey; + std::string url; + std::string sha256; +}; + +std::vector phaseLimiterDownloadPinTable(); +std::optional defaultPhaseLimiterDownloadPin(); +std::string currentPhaseLimiterPlatformKey(); +std::string phaseLimiterPlatformKeyForResolution(const std::string& platformKey); + +using FfmpegResolver = std::function()>; + +std::optional ffmpegForPhaseLimiter(); +void setFfmpegResolverForTesting(FfmpegResolver resolver); +void resetFfmpegResolverForTesting(); + class PhaseLimiterDiscovery { public: + using DownloadFetcher = std::function; + std::optional find() const; std::optional findInRoots(const std::vector& roots) const; + + static std::optional downloadAndInstall( + const std::optional& pin = std::nullopt); + + static void setDownloadFetcherForTesting(DownloadFetcher fetcher); + static void resetDownloadFetcherForTesting(); + static void resetAttemptedDownloadForTesting(); + + static void setFfmpegResolverForTesting(FfmpegResolver resolver) { + ::automix::renderers::setFfmpegResolverForTesting(std::move(resolver)); + } + static void resetFfmpegResolverForTesting() { + ::automix::renderers::resetFfmpegResolverForTesting(); + } }; } // namespace automix::renderers diff --git a/src/renderers/PhaseLimiterPins.h.in b/src/renderers/PhaseLimiterPins.h.in new file mode 100644 index 0000000..70a4a8b --- /dev/null +++ b/src/renderers/PhaseLimiterPins.h.in @@ -0,0 +1,20 @@ +// generated/renderers/PhaseLimiterPins.h +// Generated from src/renderers/PhaseLimiterPins.h.in by CMake. Do not edit. + +#pragma once + +#include + +namespace automix::renderers { + +struct PinRow { + const char* platformKey; + const char* url; + const char* sha256; +}; + +inline constexpr PinRow kPhaseLimiterPins[] = { +@AUTOMIX_PHASELIMITER_PIN_ROWS@ +}; + +} // namespace automix::renderers diff --git a/src/renderers/PhaseLimiterRenderer.cpp b/src/renderers/PhaseLimiterRenderer.cpp index 9ddee4c..3519eff 100644 --- a/src/renderers/PhaseLimiterRenderer.cpp +++ b/src/renderers/PhaseLimiterRenderer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -43,28 +44,61 @@ bool pathExists(const std::filesystem::path& path) { return std::filesystem::exists(path, error); } -class CurrentWorkingDirectoryGuard final { +// Per-render scratch space under the OS temp directory, removed on every way +// out of render(). It used to live in installRoot/tmp and was cleaned only on +// success, so each failed run leaked its input WAV and work directory (13 GB on +// one machine) - and installRoot is read-only in a Program Files install. +class ScratchDirectory final { public: - explicit CurrentWorkingDirectoryGuard(const std::filesystem::path& path) - : previous_(std::filesystem::current_path()), changed_(false) { + explicit ScratchDirectory(std::filesystem::path path) : path_(std::move(path)) { std::error_code error; - std::filesystem::current_path(path, error); - changed_ = !error; + std::filesystem::create_directories(path_ / "work", error); } - - ~CurrentWorkingDirectoryGuard() { - if (!changed_) { - return; - } + ~ScratchDirectory() { std::error_code error; - std::filesystem::current_path(previous_, error); + std::filesystem::remove_all(path_, error); } + ScratchDirectory(const ScratchDirectory&) = delete; + ScratchDirectory& operator=(const ScratchDirectory&) = delete; + [[nodiscard]] const std::filesystem::path& path() const { return path_; } private: - std::filesystem::path previous_; - bool changed_ = false; + std::filesystem::path path_; }; +std::filesystem::path scratchBase() { return std::filesystem::temp_directory_path() / "automix_phaselimiter"; } + +bool startsWith(const std::string& text, const std::string& prefix) { return text.rfind(prefix, 0) == 0; } + +// Once per process: remove what earlier versions leaked into installRoot/tmp +// (only their own file patterns, never anything else there) and scratch runs +// of ours older than a day, which only a crash leaves behind. +void sweepLeftoverScratch(const std::filesystem::path& installRoot) { + static std::once_flag once; + std::call_once(once, [&installRoot] { + std::error_code error; + const auto legacy = installRoot / "tmp"; + if (std::filesystem::is_directory(legacy, error)) { + for (const auto& entry : std::filesystem::directory_iterator(legacy, error)) { + const auto name = entry.path().filename().string(); + const bool leaked = (startsWith(name, "input_") && entry.path().extension() == ".wav") || + (startsWith(name, "phase_output_") && entry.path().extension() == ".wav") || + (startsWith(name, "work_") && entry.is_directory(error)); + if (leaked) { + std::filesystem::remove_all(entry.path(), error); + } + } + } + const auto cutoff = std::filesystem::file_time_type::clock::now() - std::chrono::hours(24); + if (std::filesystem::is_directory(scratchBase(), error)) { + for (const auto& entry : std::filesystem::directory_iterator(scratchBase(), error)) { + if (entry.is_directory(error) && std::filesystem::last_write_time(entry.path(), error) < cutoff) { + std::filesystem::remove_all(entry.path(), error); + } + } + } + }); +} std::string uniqueSuffix() { const auto value = std::chrono::high_resolution_clock::now().time_since_epoch().count(); return std::to_string(value); @@ -115,8 +149,8 @@ void drainProcessOutput(juce::ChildProcess& process, std::string& outputCapture) } // namespace bool PhaseLimiterRenderer::isAvailable() const { - PhaseLimiterDiscovery discovery; - return discovery.find().has_value(); + const auto found = PhaseLimiterDiscovery{}.find(); + return found.has_value() && isCompleteInstall(*found); } RenderResult PhaseLimiterRenderer::render(const domain::Session& session, @@ -134,6 +168,21 @@ RenderResult PhaseLimiterRenderer::render(const domain::Session& session, return fallbackToBuiltIn(session, settings, onProgress, cancelFlag, "PhaseLimiter binary not found in assets"); } + if (!isCompleteInstall(*binaryInfo)) { + return fallbackToBuiltIn(session, settings, onProgress, cancelFlag, + "PhaseLimiter install is incomplete: missing " + + pathToUtf8(masteringReferencePath(*binaryInfo))); + } + + const auto ffmpegPath = ffmpegForPhaseLimiter(); + if (!ffmpegPath.has_value()) { + RenderResult result; + result.success = false; + result.rendererName = "PhaseLimiter"; + result.logs.push_back( + "PhaseLimiter needs ffmpeg, which was not found. Install ffmpeg or set FFMPEG_BIN."); + return result; + } engine::OfflineRenderPipeline pipeline; auto renderState = pipeline.renderRawMix( @@ -184,33 +233,31 @@ RenderResult PhaseLimiterRenderer::render(const domain::Session& session, std::filesystem::create_directories(outputPath.parent_path()); } - const auto suffix = uniqueSuffix(); - const std::filesystem::path tempRoot = binaryInfo->installRoot / "tmp"; - const std::filesystem::path tempWorkDir = tempRoot / ("work_" + suffix); - const std::filesystem::path tempInputPath = tempRoot / ("input_" + suffix + ".wav"); - const std::filesystem::path tempPhaseOutputPath = tempRoot / ("phase_output_" + suffix + ".wav"); - const std::filesystem::path relativeTempWorkDir = std::filesystem::path("tmp") / ("work_" + suffix); - const std::filesystem::path relativeTempInputPath = std::filesystem::path("tmp") / ("input_" + suffix + ".wav"); - const std::filesystem::path relativeTempPhaseOutputPath = - std::filesystem::path("tmp") / ("phase_output_" + suffix + ".wav"); - std::filesystem::create_directories(tempWorkDir); + sweepLeftoverScratch(binaryInfo->installRoot); + const ScratchDirectory scratch(scratchBase() / uniqueSuffix()); + const std::filesystem::path tempWorkDir = scratch.path() / "work"; + const std::filesystem::path tempInputPath = scratch.path() / "input.wav"; + const std::filesystem::path tempPhaseOutputPath = scratch.path() / "phase_output.wav"; util::WavWriter writer; writer.write(tempInputPath, rawMix, kPhaseLimiterBitDepth); - CurrentWorkingDirectoryGuard workingDirectory(binaryInfo->installRoot); - + // Absolute paths throughout: no process-wide working-directory change, + // which raced with parallel renders. juce::StringArray command; command.add(pathToUtf8(binaryInfo->executablePath)); - command.add("-input=" + pathToUtf8(relativeTempInputPath)); - command.add("-output=" + pathToUtf8(relativeTempPhaseOutputPath)); + command.add("-input=" + pathToUtf8(tempInputPath)); + command.add("-output=" + pathToUtf8(tempPhaseOutputPath)); + command.add("-ffmpeg=" + pathToUtf8(*ffmpegPath)); + command.add("-mastering_reference_file=" + pathToUtf8(masteringReferencePath(*binaryInfo))); + command.add("-sound_quality2_cache=" + pathToUtf8(soundQualityCachePath(*binaryInfo))); command.add("-disable_input_encode=true"); command.add("-output_format=wav"); command.add("-sample_rate=44100"); command.add("-bit_depth=" + std::to_string(std::clamp(settings.outputBitDepth, 16, 24))); command.add("-ceiling=" + std::to_string(plan.limiterCeilingDb)); command.add("-mastering=true"); - command.add("-tmp=" + pathToUtf8(relativeTempWorkDir)); + command.add("-tmp=" + pathToUtf8(tempWorkDir)); juce::ChildProcess process; if (!process.start(command)) { @@ -238,9 +285,14 @@ RenderResult PhaseLimiterRenderer::render(const domain::Session& session, const auto exitCode = process.getExitCode(); const bool hasOutput = pathExists(tempPhaseOutputPath); if (!hasOutput) { - const std::string outputHint = processOutput.empty() - ? "" - : (" output=" + processOutput.substr(0, 240)); + std::string snippet; + if (processOutput.size() <= 500) { + snippet = processOutput; + } else { + snippet = processOutput.substr(0, 200) + "\n...[snip]...\n" + + processOutput.substr(processOutput.size() - 300); + } + const std::string outputHint = snippet.empty() ? "" : (" output=" + snippet); return fallbackToBuiltIn(session, settings, onProgress, cancelFlag, "phase_limiter failed (exit=" + std::to_string(exitCode) + ")" + outputHint); } @@ -330,10 +382,6 @@ RenderResult PhaseLimiterRenderer::render(const domain::Session& session, out << report.dump(2); } - std::error_code ignore; - std::filesystem::remove(tempInputPath, ignore); - std::filesystem::remove_all(tempWorkDir, ignore); - std::filesystem::remove(tempPhaseOutputPath, ignore); RenderResult result; result.success = true; diff --git a/src/renderers/RendererPipeline.cpp b/src/renderers/RendererPipeline.cpp index 6def6dd..affb9d5 100644 --- a/src/renderers/RendererPipeline.cpp +++ b/src/renderers/RendererPipeline.cpp @@ -49,7 +49,8 @@ bool isRendererAvailable(const std::string& rendererId) { return true; } if (rendererId == kPhaseLimiterRendererId) { - return PhaseLimiterDiscovery{}.find().has_value(); + const auto found = PhaseLimiterDiscovery{}.find(); + return found.has_value() && isCompleteInstall(*found); } if (rendererId == kFfmpegRendererId) { return FfmpegDiscovery{}.find().has_value(); @@ -130,6 +131,11 @@ std::vector resolveLogicalAllChain(const domain::RenderSettings& se if (rendererId == primaryRenderer) { continue; } + // PhaseLimiter is opt-in: it joins the chain only as the selected primary + // renderer (added above), never as an automatic stage. + if (rendererId == kPhaseLimiterRendererId) { + continue; + } if (!isRendererAvailable(rendererId)) { continue; } diff --git a/src/util/LameDownloader.cpp b/src/util/LameDownloader.cpp index 7ed0287..4e68a31 100644 --- a/src/util/LameDownloader.cpp +++ b/src/util/LameDownloader.cpp @@ -19,6 +19,7 @@ #include #include "util/FileUtils.h" +#include "util/Sha256.h" #include "util/StringUtils.h" namespace automix::util { @@ -38,11 +39,28 @@ enum class SourceType { Ghcr, }; +// Every source is an archive that ends up executed, so each one carries the +// SHA-256 of the exact file it must deliver. A source with no hash is refused. +// For Ghcr the hash is also the address: Homebrew bottles are stored as blobs +// named by their own SHA-256, so the pinned bottle is fetched directly. struct DownloadSource { SourceType type = SourceType::Zip; std::string url; - std::string ghcrOs; - std::string ghcrArch; + std::string sha256; +}; + +const std::string kGhcrBlobBaseUrl = "https://ghcr.io/v2/homebrew/core/lame/blobs/sha256:"; + +// The pin list on the default branch, kept current by tools/update_lame_pins.py. +// It lets installed copies follow upstream LAME updates without a release. It +// can only name files on the hosts below, so changing it is not enough to make +// the app run something else: the named host must also serve the matching file. +constexpr const char* kPinManifestUrl = + "https://raw.githubusercontent.com/soficis/AutoMixMaster/master/assets/lame-pins.json"; +constexpr int kPinManifestTimeoutMs = 8000; +constexpr const char* kAllowedDownloadPrefixes[] = { + "https://www.rarewares.org/files/mp3/", + "https://deb.debian.org/debian/pool/main/l/lame/", }; struct TempDirectory { @@ -152,65 +170,121 @@ std::string platformKey() { #endif } -std::vector platformSources() { - const auto version = readEnvironment("AUTOMIX_LAME_VERSION").value_or(kDefaultLameVersion); - if (const auto manualUrl = readEnvironment("AUTOMIX_LAME_DOWNLOAD_URL"); manualUrl.has_value()) { - const auto lower = toLower(*manualUrl); - if (lower.ends_with(".zip")) { - return {{SourceType::Zip, *manualUrl, "", ""}}; - } - if (lower.ends_with(".deb")) { - return {{SourceType::Debian, *manualUrl, "", ""}}; - } - return {{SourceType::DirectBinary, *manualUrl, "", ""}}; - } - - const auto key = platformKey(); - if (key == "win32-x64") { +// Sources for `key` at `version`. The hashes are only valid for the default +// version; platformSources() replaces them when the version is overridden. +// Hash origins: Debian's package index for the .deb files, the Homebrew bottle +// index for the Ghcr blobs. rarewares.org publishes no hashes, so the two ZIP +// pins were taken from the files as served on 2026-10-05. +// Only bottles whose encoder runs on its own are usable: the 3.100 macOS ones +// do. Linux bottles never do (their loader path is a Homebrew placeholder), and +// neither do the 4.0 macOS ones (they need Homebrew's libmpg123). +std::vector sourcesForPlatform(const std::string& key, const std::string& version) { + const std::string rarewares = "https://www.rarewares.org/files/mp3/lame" + version; + const std::string debian = "https://deb.debian.org/debian/pool/main/l/lame/lame_" + version + "-6_"; + if (key == "win32-x64" || key == "win32-arm64") { + // There is no ARM64 build to pin; Windows on ARM runs the x64 one. return { - {SourceType::Zip, "https://www.rarewares.org/files/mp3/lame" + version + ".1-x64.zip", "", ""}, + {SourceType::Zip, rarewares + ".1-x64.zip", "9a9c815203316e5203847e93100c6acf0d5d7a5be7744c9018825ded037052e7"}, }; } if (key == "win32-ia32") { return { - {SourceType::Zip, "https://www.rarewares.org/files/mp3/lame" + version + ".1-win32.zip", "", ""}, - }; - } - if (key == "win32-arm64") { - return { - {SourceType::Zip, "https://www.rarewares.org/files/mp3/LAME-" + version + "-Win-ARM64.zip", "", ""}, + {SourceType::Zip, rarewares + ".1-win32.zip", "2518e1138953c235fb2bfcefbc38883dd04538d6ae0a19692562576ba37bafec"}, }; } if (key == "linux-x64") { return { - {SourceType::Debian, "https://deb.debian.org/debian/pool/main/l/lame/lame_" + version + "-6_amd64.deb", "", ""}, - {SourceType::Ghcr, "", "linux", "amd64"}, + {SourceType::Debian, debian + "amd64.deb", "786ba06d2f222661e1f09b610de7b18c60f411a373d4fd3f595ec890f062089e"}, }; } if (key == "linux-arm64") { return { - {SourceType::Debian, "https://deb.debian.org/debian/pool/main/l/lame/lame_" + version + "-6_arm64.deb", "", ""}, - {SourceType::Ghcr, "", "linux", "arm64"}, + {SourceType::Debian, debian + "arm64.deb", "aba5023ffde46709e4bccc9e1c10142a7d77f2884d2a9af84cab6a28f8792bd2"}, }; } if (key == "linux-arm") { return { - {SourceType::Debian, "https://deb.debian.org/debian/pool/main/l/lame/lame_" + version + "-6_armhf.deb", "", ""}, + {SourceType::Debian, debian + "armhf.deb", "f77e72665a30bae6d83ca3719845309a2db12b1adf27c422415c4a12930ea76b"}, }; } if (key == "darwin-x64") { return { - {SourceType::Ghcr, "", "darwin", "amd64"}, + {SourceType::Ghcr, "", "737751faa513a68ac2499bb5cc607bc366e15dab8ff3bff5443567a455af5c3f"}, }; } if (key == "darwin-arm64") { return { - {SourceType::Ghcr, "", "darwin", "arm64"}, + {SourceType::Ghcr, "", "2ff2c6ad3cfd26e1ba53230631e2f04734a4638c344cce50ff0b8fc36b45c403"}, }; } return {}; } +std::optional fetchJson(const std::string& url, + const std::string& extraHeaders, + std::string* detail, + int timeoutMs = 45000); + +// Sources for this platform from the published pin list, or none when it cannot +// be fetched or does not validate. +std::vector publishedSources() { + const auto manifest = fetchJson(kPinManifestUrl, "", nullptr, kPinManifestTimeoutMs); + if (!manifest.has_value()) { + return {}; + } + + std::vector sources; + for (const auto& pin : LameDownloader::parsePinManifest(manifest->dump(), nullptr)) { + if (pin.platformKey != platformKey()) { + continue; + } + const auto type = pin.type == "zip" ? SourceType::Zip : pin.type == "deb" ? SourceType::Debian : SourceType::Ghcr; + sources.push_back({type, type == SourceType::Ghcr ? std::string() : pin.url, pin.sha256}); + } + return sources; +} + +// AUTOMIX_LAME_DOWNLOAD_URL and AUTOMIX_LAME_VERSION point at files the built-in +// hashes do not cover, so they only work together with AUTOMIX_LAME_DOWNLOAD_SHA256. +// With `allowPublished`, the published pin list is tried first and the built-in +// pins stay as the fallback (offline, list unreachable, or a newer build that +// does not run on this machine). +std::vector platformSources(const bool allowPublished = false) { + const auto manualSha = toLower(readEnvironment("AUTOMIX_LAME_DOWNLOAD_SHA256").value_or("")); + if (const auto manualUrl = readEnvironment("AUTOMIX_LAME_DOWNLOAD_URL"); manualUrl.has_value()) { + const auto lower = toLower(*manualUrl); + if (lower.ends_with(".zip")) { + return {{SourceType::Zip, *manualUrl, manualSha}}; + } + if (lower.ends_with(".deb")) { + return {{SourceType::Debian, *manualUrl, manualSha}}; + } + return {{SourceType::DirectBinary, *manualUrl, manualSha}}; + } + + const auto version = readEnvironment("AUTOMIX_LAME_VERSION").value_or(kDefaultLameVersion); + auto sources = sourcesForPlatform(platformKey(), version); + if (version != kDefaultLameVersion) { + for (auto& source : sources) { + source.sha256 = manualSha; + } + return sources; + } + + if (allowPublished && !flagEnabled("AUTOMIX_LAME_SKIP_PIN_UPDATE")) { + auto published = publishedSources(); + for (auto& source : sources) { + const bool known = std::any_of(published.begin(), published.end(), + [&source](const DownloadSource& other) { return other.sha256 == source.sha256; }); + if (!known) { + published.push_back(std::move(source)); + } + } + return published; + } + return sources; +} + bool runProcess(const juce::StringArray& command, const int timeoutMs, std::string* processOutput, @@ -306,13 +380,34 @@ bool downloadToFile(const std::string& url, return true; } +// Downloads a source and refuses it unless it matches its hash. Nothing from the +// file is extracted or run before this returns true. +bool downloadVerified(const std::string& url, + const std::string& sha256, + const std::filesystem::path& outputPath, + const std::string& extraHeaders, + std::string* detail) { + if (!isSha256Hex(sha256)) { + if (detail != nullptr) { + *detail = "No SHA-256 is pinned for this download (set AUTOMIX_LAME_DOWNLOAD_SHA256 when overriding " + "the URL or version): " + url; + } + return false; + } + if (!downloadToFile(url, outputPath, extraHeaders, detail)) { + return false; + } + return LameDownloader::verifyDownload(outputPath, sha256, detail); +} + std::optional fetchJson(const std::string& url, const std::string& extraHeaders, - std::string* detail) { + std::string* detail, + const int timeoutMs) { int statusCode = 0; const auto baseOptions = juce::URL::InputStreamOptions(juce::URL::ParameterHandling::inAddress) - .withConnectionTimeoutMs(45000) + .withConnectionTimeoutMs(timeoutMs) .withNumRedirectsToFollow(8) .withStatusCode(&statusCode); const auto input = juce::URL(url).createInputStream( @@ -561,7 +656,7 @@ bool copyBinaryToCache(const std::filesystem::path& source, const std::filesyste bool installFromZip(const DownloadSource& source, const std::filesystem::path& targetBinary, std::string* detail) { TempDirectory temp("automix_lame_zip"); const auto archivePath = temp.path / "lame.zip"; - if (!downloadToFile(source.url, archivePath, "", detail)) { + if (!downloadVerified(source.url, source.sha256, archivePath, "", detail)) { return false; } @@ -630,7 +725,7 @@ bool installFromDebian(const DownloadSource& source, const std::filesystem::path #else TempDirectory temp("automix_lame_deb"); const auto debPath = temp.path / "lame.deb"; - if (!downloadToFile(source.url, debPath, "", detail)) { + if (!downloadVerified(source.url, source.sha256, debPath, "", detail)) { return false; } @@ -681,8 +776,6 @@ bool installFromDebian(const DownloadSource& source, const std::filesystem::path } bool installFromGhcr(const DownloadSource& source, const std::filesystem::path& targetBinary, std::string* detail) { - const auto version = readEnvironment("AUTOMIX_LAME_VERSION").value_or(kDefaultLameVersion); - const auto tokenJson = fetchJson("https://ghcr.io/token?service=ghcr.io&scope=repository:homebrew/core/lame:pull", "", detail); if (!tokenJson.has_value() || !tokenJson->contains("token")) { if (detail != nullptr && detail->empty()) { @@ -699,67 +792,13 @@ bool installFromGhcr(const DownloadSource& source, const std::filesystem::path& return false; } - const std::string authHeader = "Authorization: Bearer " + token + "\n"; - const auto manifestList = fetchJson( - "https://ghcr.io/v2/homebrew/core/lame/manifests/" + version, - authHeader + "Accept: application/vnd.oci.image.index.v1+json\n", - detail); - if (!manifestList.has_value() || !manifestList->contains("manifests")) { - if (detail != nullptr && detail->empty()) { - *detail = "Failed to fetch GHCR manifest list."; - } - return false; - } - - std::string manifestDigest; - for (const auto& manifest : (*manifestList)["manifests"]) { - const auto platform = manifest.value("platform", nlohmann::json::object()); - if (platform.value("os", "") == source.ghcrOs && platform.value("architecture", "") == source.ghcrArch) { - manifestDigest = manifest.value("digest", ""); - break; - } - } - if (manifestDigest.empty()) { - if (detail != nullptr) { - *detail = "No GHCR manifest found for " + source.ghcrOs + "/" + source.ghcrArch; - } - return false; - } - - const auto manifest = fetchJson( - "https://ghcr.io/v2/homebrew/core/lame/manifests/" + manifestDigest, - authHeader + "Accept: application/vnd.oci.image.manifest.v1+json\n", - detail); - if (!manifest.has_value() || !manifest->contains("layers")) { - if (detail != nullptr && detail->empty()) { - *detail = "Failed to fetch GHCR image manifest."; - } - return false; - } - - std::string layerDigest; - std::string mediaType; - for (const auto& layer : (*manifest)["layers"]) { - mediaType = layer.value("mediaType", ""); - if (mediaType.find("tar") != std::string::npos) { - layerDigest = layer.value("digest", ""); - break; - } - } - if (layerDigest.empty()) { - if (detail != nullptr) { - *detail = "No tar layer found in GHCR image manifest."; - } - return false; - } - TempDirectory temp("automix_lame_ghcr"); - const bool gzipLayer = mediaType.find("gzip") != std::string::npos; - const auto layerPath = temp.path / (gzipLayer ? "layer.tar.gz" : "layer.tar"); - if (!downloadToFile("https://ghcr.io/v2/homebrew/core/lame/blobs/" + layerDigest, - layerPath, - authHeader + "Accept: application/octet-stream\n", - detail)) { + const auto layerPath = temp.path / "layer.tar.gz"; + if (!downloadVerified(kGhcrBlobBaseUrl + source.sha256, + source.sha256, + layerPath, + "Authorization: Bearer " + token + "\nAccept: application/octet-stream\n", + detail)) { return false; } @@ -773,7 +812,7 @@ bool installFromGhcr(const DownloadSource& source, const std::filesystem::path& return false; } - if (!extractTarArchive(layerPath, extractDir, gzipLayer ? "z" : "", "", detail)) { + if (!extractTarArchive(layerPath, extractDir, "z", "", detail)) { return false; } @@ -795,10 +834,114 @@ std::filesystem::path internalCacheBinaryPath() { return appData / "AutoMixMaster" / "codecs" / "lame" / key / binaryName(); } +// The cached binary is only trusted while this file, written after a verified +// install, still holds the binary's own hash. A binary cached before downloads +// were verified has no such file and is fetched again. +std::filesystem::path cacheMarkerPath(const std::filesystem::path& binary) { + return std::filesystem::path(binary.string() + ".sha256"); +} + +void writeCacheMarker(const std::filesystem::path& binary) { + std::ofstream marker(cacheMarkerPath(binary), std::ios::binary | std::ios::trunc); + marker << fileSha256(binary); +} + } // namespace std::filesystem::path LameDownloader::cacheBinaryPath() { return internalCacheBinaryPath(); } +bool LameDownloader::cachedBinaryIsVerified() { + const auto binary = cacheBinaryPath(); + if (!isRegularFile(binary)) { + return false; + } + std::ifstream marker(cacheMarkerPath(binary), std::ios::binary); + std::string recorded; + marker >> recorded; + return isSha256Hex(recorded) && recorded == fileSha256(binary); +} + +std::vector LameDownloader::pinnedSources() { + std::vector pins; + for (const char* key : {"win32-x64", "win32-ia32", "win32-arm64", "linux-x64", "linux-arm64", "linux-arm", + "darwin-x64", "darwin-arm64"}) { + for (const auto& source : sourcesForPlatform(key, kDefaultLameVersion)) { + const char* type = source.type == SourceType::Zip ? "zip" : source.type == SourceType::Debian ? "deb" : "ghcr"; + pins.push_back({key, type, source.type == SourceType::Ghcr ? kGhcrBlobBaseUrl + source.sha256 : source.url, + source.sha256}); + } + } + return pins; +} + +std::vector LameDownloader::parsePinManifest(const std::string& jsonText, + std::string* detail) { + const auto reject = [detail](const std::string& reason) { + if (detail != nullptr) { + *detail = "LAME pin list rejected: " + reason; + } + return std::vector{}; + }; + + const auto json = nlohmann::json::parse(jsonText, nullptr, false); + if (!json.is_object() || json.value("schema", 0) != 1 || !json.contains("sources") || !json["sources"].is_array()) { + return reject("not a schema 1 pin list."); + } + + std::vector pins; + for (const auto& entry : json["sources"]) { + if (!entry.is_object() || !entry.value("platform", nlohmann::json()).is_string() || + !entry.value("type", nlohmann::json()).is_string() || !entry.value("sha256", nlohmann::json()).is_string() || + !entry.value("url", nlohmann::json("")).is_string()) { + return reject("an entry has missing or non-text fields."); + } + + PinnedSource pin; + pin.platformKey = entry["platform"].get(); + pin.type = entry["type"].get(); + pin.sha256 = toLower(entry["sha256"].get()); + if (!isSha256Hex(pin.sha256)) { + return reject("an entry has no valid SHA-256."); + } + + if (pin.type == "ghcr") { + pin.url = kGhcrBlobBaseUrl + pin.sha256; + } else if (pin.type == "zip" || pin.type == "deb") { + pin.url = entry.value("url", ""); + const bool allowedHost = std::any_of(std::begin(kAllowedDownloadPrefixes), std::end(kAllowedDownloadPrefixes), + [&pin](const char* prefix) { return pin.url.rfind(prefix, 0) == 0; }); + const auto fileName = pin.url.substr(pin.url.find_last_of('/') + 1); + const bool plainFile = !fileName.empty() && fileName.ends_with("." + pin.type) && + pin.url.find_first_of("?#\\%") == std::string::npos && + pin.url.find("..") == std::string::npos; + if (!allowedHost || !plainFile) { + return reject("an entry points outside the known download locations: " + pin.url); + } + } else { + return reject("unknown source type '" + pin.type + "'."); + } + pins.push_back(std::move(pin)); + } + return pins; +} + +bool LameDownloader::verifyDownload(const std::filesystem::path& file, + const std::string& expectedSha256, + std::string* detail) { + const auto actual = fileSha256(file); + if (isSha256Hex(expectedSha256) && actual == toLower(expectedSha256)) { + return true; + } + + std::error_code error; + std::filesystem::remove(file, error); + if (detail != nullptr) { + *detail = "SHA-256 mismatch for " + file.filename().string() + " (expected " + expectedSha256 + ", got " + + (actual.empty() ? "unreadable file" : actual) + "); the download was discarded."; + } + return false; +} + bool LameDownloader::isSupportedOnCurrentPlatform() { return !platformSources().empty(); } LameDownloader::DownloadResult LameDownloader::ensureAvailable(const bool forceDownload) { @@ -807,7 +950,7 @@ LameDownloader::DownloadResult LameDownloader::ensureAvailable(const bool forceD DownloadResult result; const auto targetBinary = cacheBinaryPath(); - if (!forceDownload && !flagEnabled("AUTOMIX_LAME_FORCE_DOWNLOAD") && isRegularFile(targetBinary)) { + if (!forceDownload && !flagEnabled("AUTOMIX_LAME_FORCE_DOWNLOAD") && cachedBinaryIsVerified()) { std::string detail; if (ensureExecutable(targetBinary, &detail)) { result.success = true; @@ -822,7 +965,7 @@ LameDownloader::DownloadResult LameDownloader::ensureAvailable(const bool forceD return result; } - const auto sources = platformSources(); + const auto sources = platformSources(true); if (sources.empty()) { result.detail = "No fallback LAME downloader source configured for this platform."; return result; @@ -837,7 +980,7 @@ LameDownloader::DownloadResult LameDownloader::ensureAvailable(const bool forceD case SourceType::DirectBinary: { TempDirectory temp("automix_lame_direct"); const auto downloadedPath = temp.path / binaryName(); - if (downloadToFile(source.url, downloadedPath, "", &attemptDetail)) { + if (downloadVerified(source.url, source.sha256, downloadedPath, "", &attemptDetail)) { installed = copyBinaryToCache(downloadedPath, targetBinary, &attemptDetail); } break; @@ -854,6 +997,7 @@ LameDownloader::DownloadResult LameDownloader::ensureAvailable(const bool forceD } if (installed) { + writeCacheMarker(targetBinary); result.success = true; result.executablePath = targetBinary; result.detail = "Downloaded fallback LAME binary to " + targetBinary.string(); diff --git a/src/util/LameDownloader.h b/src/util/LameDownloader.h index dae5314..2a36db6 100644 --- a/src/util/LameDownloader.h +++ b/src/util/LameDownloader.h @@ -2,6 +2,7 @@ #include #include +#include namespace automix::util { @@ -14,7 +15,23 @@ class LameDownloader { std::string detail; }; + struct PinnedSource { + std::string platformKey; + std::string type; // "zip", "deb" or "ghcr" + std::string url; + std::string sha256; + }; + static std::filesystem::path cacheBinaryPath(); + /// True when the cached binary exists and is the one a verified install left there. + static bool cachedBinaryIsVerified(); + /// Every built-in download with the SHA-256 it must match, for all platforms. + static std::vector pinnedSources(); + /// Parses the published pin list (assets/lame-pins.json). Any entry that is malformed or + /// points outside the known download hosts rejects the whole list (returns empty). + static std::vector parsePinManifest(const std::string& jsonText, std::string* detail); + /// Checks a downloaded file against its pinned SHA-256; a mismatch deletes the file. + static bool verifyDownload(const std::filesystem::path& file, const std::string& expectedSha256, std::string* detail); static bool isSupportedOnCurrentPlatform(); static DownloadResult ensureAvailable(bool forceDownload = false); }; diff --git a/src/util/WavWriter.cpp b/src/util/WavWriter.cpp index 765569b..cfdd6ff 100644 --- a/src/util/WavWriter.cpp +++ b/src/util/WavWriter.cpp @@ -252,8 +252,8 @@ std::optional resolveBundledLameExecutable() { } std::optional findLameExecutable() { - if (const auto downloaded = LameDownloader::cacheBinaryPath(); isRegularFile(downloaded)) { - return downloaded; + if (LameDownloader::cachedBinaryIsVerified()) { + return LameDownloader::cacheBinaryPath(); } if (const auto bundled = resolveBundledLameExecutable(); bundled.has_value()) { diff --git a/tests/cmake/phaselimiter_arch_test.cmake b/tests/cmake/phaselimiter_arch_test.cmake new file mode 100644 index 0000000..cfa1230 --- /dev/null +++ b/tests/cmake/phaselimiter_arch_test.cmake @@ -0,0 +1,88 @@ +# tests/cmake/phaselimiter_arch_test.cmake + +cmake_minimum_required(VERSION 3.20) + +get_filename_component(_root_dir "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +include("${_root_dir}/cmake/FetchPhaseLimiter.cmake") + +if(TEST_SUBCASE STREQUAL "universal") + set(CMAKE_SYSTEM_NAME "Darwin") + set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") + set(CMAKE_SYSTEM_PROCESSOR "arm64") + automix_phaselimiter_platform_key(_key) + message(FATAL_ERROR "Should not reach here: universal arch did not trigger error") +endif() + +# Test 1: Darwin, no CMAKE_OSX_ARCHITECTURES, arm64 processor +set(CMAKE_SYSTEM_NAME "Darwin") +set(CMAKE_OSX_ARCHITECTURES "") +set(CMAKE_SYSTEM_PROCESSOR "arm64") +automix_phaselimiter_platform_key(_key) +if(NOT _key STREQUAL "macos-arm64") + message(FATAL_ERROR "Test 1 failed: expected macos-arm64, got '${_key}'") +endif() + +# Test 2: Darwin, CMAKE_OSX_ARCHITECTURES=x86_64 +set(CMAKE_SYSTEM_NAME "Darwin") +set(CMAKE_OSX_ARCHITECTURES "x86_64") +set(CMAKE_SYSTEM_PROCESSOR "arm64") +automix_phaselimiter_platform_key(_key) +if(NOT _key STREQUAL "macos-x86_64") + message(FATAL_ERROR "Test 2 failed: expected macos-x86_64, got '${_key}'") +endif() + +# Test 3: Darwin, CMAKE_OSX_ARCHITECTURES=arm64 with processor x86_64 (explicit arch wins) +set(CMAKE_SYSTEM_NAME "Darwin") +set(CMAKE_OSX_ARCHITECTURES "arm64") +set(CMAKE_SYSTEM_PROCESSOR "x86_64") +automix_phaselimiter_platform_key(_key) +if(NOT _key STREQUAL "macos-arm64") + message(FATAL_ERROR "Test 3 failed: expected macos-arm64, got '${_key}'") +endif() + +# Test 4: Linux, processor aarch64 +set(CMAKE_SYSTEM_NAME "Linux") +set(CMAKE_OSX_ARCHITECTURES "") +set(CMAKE_SYSTEM_PROCESSOR "aarch64") +automix_phaselimiter_platform_key(_key) +if(NOT _key STREQUAL "linux-arm64") + message(FATAL_ERROR "Test 4 failed: expected linux-arm64, got '${_key}'") +endif() + +# Test 5: Linux, processor AMD64 (case-insensitive) +set(CMAKE_SYSTEM_NAME "Linux") +set(CMAKE_OSX_ARCHITECTURES "") +set(CMAKE_SYSTEM_PROCESSOR "AMD64") +automix_phaselimiter_platform_key(_key) +if(NOT _key STREQUAL "linux-x64") + message(FATAL_ERROR "Test 5 failed: expected linux-x64, got '${_key}'") +endif() + +# Test 6: Windows, processor AMD64 +set(CMAKE_SYSTEM_NAME "Windows") +set(CMAKE_OSX_ARCHITECTURES "") +set(CMAKE_SYSTEM_PROCESSOR "AMD64") +automix_phaselimiter_platform_key(_key) +if(NOT _key STREQUAL "windows-x64") + message(FATAL_ERROR "Test 6 failed: expected windows-x64, got '${_key}'") +endif() + +# Test 7: Darwin, CMAKE_OSX_ARCHITECTURES="arm64;x86_64" -> FATAL_ERROR containing "universal" +execute_process( + COMMAND "${CMAKE_COMMAND}" -DTEST_SUBCASE=universal -P "${CMAKE_CURRENT_LIST_FILE}" + RESULT_VARIABLE _proc_res + OUTPUT_VARIABLE _proc_out + ERROR_VARIABLE _proc_err +) + +if(_proc_res EQUAL 0) + message(FATAL_ERROR "Test 7 failed: expected universal architecture to trigger non-zero exit code") +endif() + +set(_all_proc_output "${_proc_out}${_proc_err}") +string(TOLOWER "${_all_proc_output}" _lower_err) +if(NOT _lower_err MATCHES "universal") + message(FATAL_ERROR "Test 7 failed: expected 'universal' in error output, got:\n${_all_proc_output}") +endif() + +message(STATUS "All automix_phaselimiter_platform_key tests passed successfully.") diff --git a/tests/cmake/phaselimiter_install_guard_test.cmake b/tests/cmake/phaselimiter_install_guard_test.cmake new file mode 100644 index 0000000..8d048fc --- /dev/null +++ b/tests/cmake/phaselimiter_install_guard_test.cmake @@ -0,0 +1,67 @@ +# tests/cmake/phaselimiter_install_guard_test.cmake + +cmake_minimum_required(VERSION 3.20) + +get_filename_component(_root_dir "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +include("${_root_dir}/cmake/FetchPhaseLimiter.cmake") +automix_phaselimiter_platform_key(_host_key) + +set(_temp_dir "${CMAKE_CURRENT_BINARY_DIR}/install_guard_scratch") +file(REMOVE_RECURSE "${_temp_dir}") +file(MAKE_DIRECTORY "${_temp_dir}/install_prefix") + +# Verify that if binary or resource is missing, the guard fails with non-zero exit and expected error +set(_dummy_cmake_install "${_temp_dir}/test_guard.cmake") +file(WRITE "${_dummy_cmake_install}" " +set(CMAKE_INSTALL_PREFIX \"${_temp_dir}/install_prefix\") +set(_automix_pl_dest \"assets/phaselimiter\") +set(_automix_native_bin_name \"phase_limiter\") +if(\"${_host_key}\" STREQUAL \"windows-x64\") + set(_automix_native_bin_name \"phase_limiter.exe\") +endif() +set(_pl_bin_check \"\${CMAKE_INSTALL_PREFIX}/\${_automix_pl_dest}/bin/\${_automix_native_bin_name}\") +set(_pl_res_check \"\${CMAKE_INSTALL_PREFIX}/\${_automix_pl_dest}/resource/mastering_reference.json\") +if(NOT EXISTS \"\${_pl_bin_check}\" OR NOT EXISTS \"\${_pl_res_check}\") + message(FATAL_ERROR \"Package has no native PhaseLimiter for ${_host_key}\") +endif() +") + +execute_process( + COMMAND "${CMAKE_COMMAND}" -P "${_dummy_cmake_install}" + RESULT_VARIABLE _res + ERROR_VARIABLE _err + OUTPUT_VARIABLE _out +) + +if(_res EQUAL 0) + message(FATAL_ERROR "Expected guard script to fail on missing binary, but it succeeded!") +endif() + +set(_full_err "${_out}${_err}") +if(NOT _full_err MATCHES "Package has no native PhaseLimiter for ${_host_key}") + message(FATAL_ERROR "Expected error message to contain 'Package has no native PhaseLimiter for ${_host_key}', got:\n${_full_err}") +endif() + +# Verify that when binary and resource exist, the guard succeeds +file(MAKE_DIRECTORY "${_temp_dir}/install_prefix/assets/phaselimiter/bin") +file(MAKE_DIRECTORY "${_temp_dir}/install_prefix/assets/phaselimiter/resource") +set(_bin_ext "") +if("${_host_key}" STREQUAL "windows-x64") + set(_bin_ext ".exe") +endif() +file(WRITE "${_temp_dir}/install_prefix/assets/phaselimiter/bin/phase_limiter${_bin_ext}" "dummy binary") +file(WRITE "${_temp_dir}/install_prefix/assets/phaselimiter/resource/mastering_reference.json" "{}") + +execute_process( + COMMAND "${CMAKE_COMMAND}" -P "${_dummy_cmake_install}" + RESULT_VARIABLE _res2 + ERROR_VARIABLE _err2 + OUTPUT_VARIABLE _out2 +) + +if(NOT _res2 EQUAL 0) + message(FATAL_ERROR "Guard script failed when files were present:\n${_out2}${_err2}") +endif() + +file(REMOVE_RECURSE "${_temp_dir}") +message(STATUS "cmake_phaselimiter_install_guard test passed.") diff --git a/tests/fixtures/tensor/external_data.onnx b/tests/fixtures/tensor/external_data.onnx new file mode 100644 index 0000000..081e4e8 Binary files /dev/null and b/tests/fixtures/tensor/external_data.onnx differ diff --git a/tests/fixtures/tensor/external_data.onnx.data b/tests/fixtures/tensor/external_data.onnx.data new file mode 100644 index 0000000..8db91db Binary files /dev/null and b/tests/fixtures/tensor/external_data.onnx.data differ diff --git a/tests/fixtures/tensor/identity_mask.onnx b/tests/fixtures/tensor/identity_mask.onnx new file mode 100644 index 0000000..e59ebcc Binary files /dev/null and b/tests/fixtures/tensor/identity_mask.onnx differ diff --git a/tests/fixtures/tensor/int64_io.onnx b/tests/fixtures/tensor/int64_io.onnx new file mode 100644 index 0000000..9051e66 Binary files /dev/null and b/tests/fixtures/tensor/int64_io.onnx differ diff --git a/tests/fixtures/tensor/make_fixtures.py b/tests/fixtures/tensor/make_fixtures.py new file mode 100644 index 0000000..af9f632 --- /dev/null +++ b/tests/fixtures/tensor/make_fixtures.py @@ -0,0 +1,77 @@ +"""Generates the synthetic ONNX graphs used by the native tensor-inference tests. + +These stand in for the real BS-RoFormer export (never committed; download-only) +so that the native OnnxTensorInference path demonstrably executes in CI. + + python -m pip install onnx==1.23.1 + python tests/fixtures/tensor/make_fixtures.py + +Opset 17 / IR 8 are pinned so ONNX Runtime 1.30.0 loads every graph. +""" + +from pathlib import Path + +import numpy as np +import onnx +from onnx import TensorProto, helper, numpy_helper + +HERE = Path(__file__).resolve().parent +OPSET = [helper.make_opsetid("", 17)] +IR_VERSION = 8 + + +def save(graph, name, **kwargs): + model = helper.make_model(graph, opset_imports=OPSET, producer_name="automix-fixtures") + model.ir_version = IR_VERSION + onnx.checker.check_model(model) + onnx.save_model(model, str(HERE / name), **kwargs) + + +def identity_mask(): + # BS-RoFormer geometry (spec 5.4): folded-stereo spectrum in, one complex + # mask out. The mask is 1 + 0i everywhere, so separation is the identity. + # input [1, 801, 4100] -> output [1, 1, 2050, 801, 2] + x = helper.make_tensor_value_info("input", TensorProto.FLOAT, [1, 801, 4100]) + y = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1, 1, 2050, 801, 2]) + inits = [ + numpy_helper.from_array(np.array(0.0, dtype=np.float32), "zero"), + numpy_helper.from_array(np.array([1, 801, 2050, 2], dtype=np.int64), "split_shape"), + numpy_helper.from_array(np.array([1.0, 0.0], dtype=np.float32), "one_plus_zero_i"), + numpy_helper.from_array(np.array([1], dtype=np.int64), "stem_axis"), + ] + nodes = [ + helper.make_node("Mul", ["input", "zero"], ["zeroed"]), + helper.make_node("Reshape", ["zeroed", "split_shape"], ["reim"]), + helper.make_node("Add", ["reim", "one_plus_zero_i"], ["mask_tf"]), + helper.make_node("Transpose", ["mask_tf"], ["mask_ft"], perm=[0, 2, 1, 3]), + helper.make_node("Unsqueeze", ["mask_ft", "stem_axis"], ["output"]), + ] + save(helper.make_graph(nodes, "identity_mask", [x], [y], inits), "identity_mask.onnx") + + +def int64_io(): + # A quantized/tokenised export whose I/O is not float32: must be refused at load. + x = helper.make_tensor_value_info("tokens", TensorProto.INT64, [1, 4]) + y = helper.make_tensor_value_info("tokens_out", TensorProto.INT64, [1, 4]) + nodes = [helper.make_node("Identity", ["tokens"], ["tokens_out"])] + save(helper.make_graph(nodes, "int64_io", [x], [y]), "int64_io.onnx") + + +def external_data(): + # fp32 exports keep weights in a `.onnx.data` sidecar. + x = helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 4]) + y = helper.make_tensor_value_info("y", TensorProto.FLOAT, [1, 4]) + weight = numpy_helper.from_array(np.array([[1.0, 2.0, 3.0, 4.0]], dtype=np.float32), "w") + nodes = [helper.make_node("Add", ["x", "w"], ["y"])] + save(helper.make_graph(nodes, "external_data", [x], [y], [weight]), + "external_data.onnx", + save_as_external_data=True, + all_tensors_to_one_file=True, + location="external_data.onnx.data", + size_threshold=0) + + +if __name__ == "__main__": + identity_mask() + int64_io() + external_data() diff --git a/tests/integration/PipelineIntegrationTests.cpp b/tests/integration/PipelineIntegrationTests.cpp index 75400b0..b9316b5 100644 --- a/tests/integration/PipelineIntegrationTests.cpp +++ b/tests/integration/PipelineIntegrationTests.cpp @@ -8,6 +8,7 @@ #include "domain/Session.h" #include "domain/Stem.h" #include "engine/AudioBuffer.h" +#include "renderers/PhaseLimiterDiscovery.h" #include "renderers/RendererPipeline.h" namespace { @@ -59,6 +60,14 @@ TEST_CASE("Integration: rendering chain resolves correct default order", "[integ settings.rendererChainEnabled = true; settings.rendererChainMode = "logical_all"; + // PhaseLimiter must exist and run on every platform (owner requirement), so a + // missing install is a failure here, never a silent BuiltIn fallback. + const auto phaseLimiter = automix::renderers::PhaseLimiterDiscovery{}.find(); + INFO("PhaseLimiter not discovered on this platform: install a native, complete build " + "(binary + resource/mastering_reference.json) under assets/phaselimiter or the cache root"); + REQUIRE(phaseLimiter.has_value()); + REQUIRE(automix::renderers::isCompleteInstall(*phaseLimiter)); + const auto chain = automix::renderers::resolveRendererChain(settings); REQUIRE_FALSE(chain.empty()); REQUIRE(chain.front() == "PhaseLimiter"); diff --git a/tests/tools/test_check_repo_hygiene.py b/tests/tools/test_check_repo_hygiene.py new file mode 100644 index 0000000..23728c4 --- /dev/null +++ b/tests/tools/test_check_repo_hygiene.py @@ -0,0 +1,87 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent +SCRIPT_PATH = REPO_ROOT / "tools" / "check_repo_hygiene.py" + + +class TestCheckRepoHygiene(unittest.TestCase): + def run_hygiene(self, args): + cmd = [sys.executable, str(SCRIPT_PATH)] + args + proc = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True + ) + return proc.returncode, proc.stdout + + def test_mask_detected(self): + with tempfile.TemporaryDirectory() as tmpdir: + wf_path = Path(tmpdir) / "test_mask.yml" + wf_path.write_text("name: Test\njobs:\n build:\n steps:\n - run: ctest || true\n") + rc, out = self.run_hygiene(["--check-workflows", "--workflows-dir", tmpdir]) + self.assertEqual(rc, 1, f"Expected rc 1, got {rc}. Output:\n{out}") + self.assertIn("MASK:", out) + + def test_unpinned_action_detected(self): + with tempfile.TemporaryDirectory() as tmpdir: + wf_path = Path(tmpdir) / "test_unpinned.yml" + wf_path.write_text("name: Test\njobs:\n build:\n steps:\n - uses: actions/checkout@v4\n") + rc, out = self.run_hygiene(["--check-workflows", "--workflows-dir", tmpdir]) + self.assertEqual(rc, 1, f"Expected rc 1, got {rc}. Output:\n{out}") + self.assertIn("UNPINNED:", out) + + def test_pinned_action_ok(self): + with tempfile.TemporaryDirectory() as tmpdir: + wf_path = Path(tmpdir) / "test_pinned.yml" + wf_path.write_text("name: Test\njobs:\n build:\n steps:\n - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4\n") + rc, out = self.run_hygiene(["--check-workflows", "--workflows-dir", tmpdir]) + self.assertEqual(rc, 0, f"Expected rc 0, got {rc}. Output:\n{out}") + + def test_forbidden_paths(self): + forbidden_samples = [ + ".superpowers/x", + ".anchor/x", + "memory/x.md", + ".claude/x", + ".gemini/x", + ".opencode/x", + ".codex/x", + "GEMINI.md", + ".cursorrules", + "notes/SSH_HANDOFF.md", + "x/handoff-2.md", + "job-logs1.txt", + "a/b.jsonl", + "docs/architecture/x.md", + "docs/architecture/github_support_pr_purge_request.md", + ] + allowed_samples = [ + "docs/model-licensing-audit.json", + "README.md", + "assets/sox/README.md", + "tools/training/README.md", + ] + with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as f: + for p in forbidden_samples + allowed_samples: + f.write(p + "\n") + list_file = f.name + + try: + rc, out = self.run_hygiene(["--paths-from", list_file]) + self.assertEqual(rc, 1, f"Expected rc 1, got {rc}. Output:\n{out}") + for p in forbidden_samples: + self.assertIn(f"FORBIDDEN: {p}", out) + for p in allowed_samples: + self.assertNotIn(f"FORBIDDEN: {p}", out) + finally: + os.remove(list_file) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/AiExtensionTests.cpp b/tests/unit/AiExtensionTests.cpp index e3071f1..9a491a7 100644 --- a/tests/unit/AiExtensionTests.cpp +++ b/tests/unit/AiExtensionTests.cpp @@ -25,6 +25,7 @@ #include "ai/ModelManager.h" #include "ai/ModelPackLoader.h" #include "ai/ModelStrategy.h" +#include "ai/UmxPack.h" #include "app/controllers/ModelController.h" #include "ai/ModelLicensePolicy.h" #include "app/ui/HeroWaveform.h" @@ -875,7 +876,7 @@ bool waitForAsync(const std::function& predicate, const int timeoutMs = } struct ModelInstallProbe { - int installCalls = 0; + std::atomic installCalls{0}; }; automix::app::ModelController::ModelHubOps makeProbeModelHubOps(ModelInstallProbe& probe) { @@ -1009,6 +1010,25 @@ TEST_CASE("Stem separator only claims model-backed separation when a model weigh REQUIRE(claim - gateEnd < 400); } +TEST_CASE("Curated pin overrides the live revision and hash for Open-Unmix only", "[ai][hub]") { + automix::ai::HubModelInfo umx; + umx.repoId = automix::ai::kUmxVocalsRepoId; + umx.revision = "main"; + umx.fileSha256[automix::ai::kUmxVocalsFile] = "bogus"; + automix::ai::applyCuratedPin(umx); + CHECK(umx.revision == automix::ai::kUmxVocalsRevision); + CHECK(umx.fileSha256.at(automix::ai::kUmxVocalsFile) == automix::ai::kUmxVocalsSha256); + + automix::ai::HubModelInfo other; + other.repoId = "xycld/BS-RoFormer-ONNX"; + other.revision = "main"; + other.fileSha256["model.onnx"] = "bogus"; + automix::ai::applyCuratedPin(other); + CHECK(other.revision == "main"); + CHECK(other.fileSha256.at("model.onnx") == "bogus"); + CHECK(other.fileSha256.size() == 1); +} + TEST_CASE("Curated hub includes ITO-Master mapped to mastering-assistant with three-asset pack metadata", "[ai][licensing]") { const auto curated = automix::ai::curatedModelIds(); REQUIRE(std::find(curated.begin(), curated.end(), "kramp/ito-master-onnx") != curated.end()); @@ -1220,7 +1240,7 @@ TEST_CASE("Optional ORT provider plugin and compiled-model cache policy needs no REQUIRE(gpu::compiledModelCacheKey("not-a-digest", "cuda", "sm_90", "driver", "1.30.0").empty()); const auto& chain = gpu::providerPriorityChain(); - REQUIRE(chain.size() == 6); + REQUIRE(chain.size() == 7); REQUIRE(chain.front() == gpu::kProviderAne); REQUIRE(chain.back() == gpu::kProviderCpu); } @@ -1314,3 +1334,18 @@ TEST_CASE("Model strategy returns base plans unchanged when no model inference i REQUIRE(masterPass.targetLufs == Catch::Approx(-16.0)); REQUIRE(masterPass.decisionLog.size() == 1); } + +TEST_CASE("Linux builds enable libcurl so HTTPS downloads work", "[build-config]") { +#if defined(__linux__) + REQUIRE(JUCE_USE_CURL == 1); +#else + SUCCEED("JUCE_USE_CURL is only required on Linux; Windows and macOS use native HTTP stacks."); +#endif +} + +TEST_CASE("Hub assets are stored flat inside the install directory", "[ai][hub]") { + const std::filesystem::path installPath = std::filesystem::path("models") / "pack"; + CHECK(automix::ai::localAssetPath(installPath, "onnx/model.onnx.data") == installPath / "model.onnx.data"); + CHECK(automix::ai::localAssetPath(installPath, "model.onnx") == installPath / "model.onnx"); + CHECK(automix::ai::localAssetPath(installPath, "a/b/../../x.onnx") == installPath / "x.onnx"); +} diff --git a/tests/unit/AudioIoTests.cpp b/tests/unit/AudioIoTests.cpp index 5a1f2d4..f0c9bb4 100644 --- a/tests/unit/AudioIoTests.cpp +++ b/tests/unit/AudioIoTests.cpp @@ -177,7 +177,11 @@ TEST_CASE("Preview bridge meter targets round-trip without torn reads", "[audioi const float peak = leftPeak.load(std::memory_order_relaxed); seenLevels.push_back(level); seenPeaks.push_back(peak); - if (level < 0.0f || level > 1.0f || peak < 0.0f || peak > 2.0f) + // The reader can run before the writer's first store, so the initial + // -60 is a legitimate read; anything else outside the written range is not. + const bool levelOk = level == -60.0f || (level >= 0.0f && level <= 1.0f); + const bool peakOk = peak == -60.0f || (peak >= 0.0f && peak <= 2.0f); + if (!levelOk || !peakOk) readerOk.store(false); } }); @@ -196,7 +200,7 @@ TEST_CASE("Preview bridge meter targets round-trip without torn reads", "[audioi TEST_CASE("Preview bridge buffer swap never tears and keeps last writer", "[audioio][previewbridge]") { constexpr int kIterations = 2000; - std::atomic> buffer{nullptr}; + std::shared_ptr buffer{nullptr}; std::atomic done{false}; std::atomic readerOk{true}; @@ -205,14 +209,15 @@ TEST_CASE("Preview bridge buffer swap never tears and keeps last writer", "[audi auto next = std::make_shared(2, 256 + (i % 3), 44100.0); next->setSample(0, 0, static_cast(i)); - buffer.store(std::move(next), std::memory_order_release); + std::shared_ptr constNext = std::move(next); + std::atomic_store_explicit(&buffer, std::move(constNext), std::memory_order_release); } done.store(true); }); std::thread reader([&] { while (!done.load()) { - const auto current = buffer.load(std::memory_order_acquire); + const auto current = std::atomic_load_explicit(&buffer, std::memory_order_acquire); if (current == nullptr) continue; // While holding the shared_ptr the published buffer must be fully valid @@ -230,7 +235,7 @@ TEST_CASE("Preview bridge buffer swap never tears and keeps last writer", "[audi reader.join(); REQUIRE(readerOk.load()); - const auto finalBuffer = buffer.load(); + const auto finalBuffer = std::atomic_load(&buffer); REQUIRE(finalBuffer != nullptr); REQUIRE(finalBuffer->getNumSamples() == 256 + ((kIterations - 1) % 3)); const float lastSample = finalBuffer->getSample(0, 0); diff --git a/tests/unit/ControlDeckTests.cpp b/tests/unit/ControlDeckTests.cpp new file mode 100644 index 0000000..b5daf69 --- /dev/null +++ b/tests/unit/ControlDeckTests.cpp @@ -0,0 +1,98 @@ +#include +#include + +#include +#include + +#include "app/ui/ControlDeck.h" +#include "app/ui/GlowMeters.h" + +namespace { + +juce::Button* findButtonByText(juce::Component& parent, const juce::String& text) { + for (auto* child : parent.getChildren()) { + if (auto* button = dynamic_cast(child); button != nullptr && button->getButtonText() == text) + return button; + if (child != nullptr) { + if (auto* nested = findButtonByText(*child, text)) + return nested; + } + } + return nullptr; +} + +} // namespace + +TEST_CASE("ControlDeck gates stem-dependent actions on setHasStems", "[ui][controldeck]") { + juce::ScopedJuceInitialiser_GUI juceInit; + + automix::app::ControlDeck deck; + deck.setSize(1000, 300); + + const auto enabled = [&](const char* name) { + auto* button = findButtonByText(deck, name); + REQUIRE(button != nullptr); + return button->isEnabled(); + }; + + deck.setHasStems(false); + REQUIRE_FALSE(enabled("Auto Mix")); + REQUIRE_FALSE(enabled("Auto Master")); + REQUIRE_FALSE(enabled("Mix + Master")); + REQUIRE_FALSE(enabled("Export")); + REQUIRE(enabled("Batch")); + REQUIRE(enabled("Import")); + + deck.setHasStems(true); + REQUIRE(enabled("Auto Mix")); + REQUIRE(enabled("Auto Master")); + REQUIRE(enabled("Mix + Master")); + REQUIRE(enabled("Export")); + REQUIRE(enabled("Batch")); + REQUIRE(enabled("Import")); +} + +TEST_CASE("ControlDeck shows the Vocal Model toggle only with AI Stem Separation", "[ui][controldeck]") { + juce::ScopedJuceInitialiser_GUI juceInit; + + automix::app::ControlDeck deck; + deck.setSize(1000, 300); + + deck.setSeparationControlsVisible(false); + REQUIRE_FALSE(deck.getTensorSeparationToggle().isVisible()); + + deck.setSeparationControlsVisible(true); + REQUIRE(deck.getTensorSeparationToggle().isVisible()); +} + +TEST_CASE("GlowMeters readouts show -- for no signal", "[ui][glowmeters]") { + using automix::app::GlowMeters; + REQUIRE(GlowMeters::formatReadout("M: ", -70.0, " LUFS") == "M: -- LUFS"); + REQUIRE(GlowMeters::formatReadout("TP: ", -69.95, " dBTP") == "TP: -- dBTP"); + REQUIRE(GlowMeters::formatReadout("I: ", -std::numeric_limits::infinity(), " LUFS") == "I: -- LUFS"); + REQUIRE(GlowMeters::formatReadout("S: ", -14.0, " LUFS") == "S: -14.0 LUFS"); +} + +TEST_CASE("ControlDeck settings row keeps Platform combo visible and non-overlapping", "[ui][controldeck]") { + juce::ScopedJuceInitialiser_GUI juceInit; + + for (const int width : {940, 1500}) { + automix::app::ControlDeck deck; + deck.setSize(width, 420); + + const auto platform = deck.getPlatformPresetBox().getBounds(); + const auto profile = deck.getProfileBox().getBounds(); + const auto master = deck.getMasterPresetBox().getBounds(); + REQUIRE_FALSE(platform.isEmpty()); + REQUIRE(deck.getLocalBounds().contains(platform)); + REQUIRE(platform.getRight() <= deck.getWidth()); + REQUIRE_FALSE(platform.intersects(profile)); + REQUIRE_FALSE(platform.intersects(master)); + + auto* autoMaster = findButtonByText(deck, "Auto Master"); + auto* import = findButtonByText(deck, "Import"); + REQUIRE(autoMaster != nullptr); + REQUIRE(import != nullptr); + REQUIRE(autoMaster->getHeight() == import->getHeight()); + } +} diff --git a/tests/unit/ControllerTests.cpp b/tests/unit/ControllerTests.cpp index db09ad9..2e94c12 100644 --- a/tests/unit/ControllerTests.cpp +++ b/tests/unit/ControllerTests.cpp @@ -1,7 +1,9 @@ #include #include +#include #include #include +#include #include #include #include @@ -13,6 +15,7 @@ #include #include +#include #include "ai/ModelManager.h" #include "app/controllers/ExportController.h" @@ -349,6 +352,86 @@ TEST_CASE("ModelController uninstall respects pre-set cancel", "[controllers][mo REQUIRE(cancelled.value()); } +TEST_CASE("ModelController uninstall only deletes inside the model hub", "[controllers][model][uninstall]") { + juce::ScopedJuceInitialiser_GUI juceInit; + + const auto base = uniqueTempPath("model_uninstall_guard"); + const auto hub = base / "modelhub"; + const auto outside = base / "precious"; + std::filesystem::create_directories(hub / "inside-pack"); + std::filesystem::create_directories(outside); + std::ofstream(hub / "inside-pack" / "model.onnx") << "x"; + std::ofstream(outside / "keep.txt") << "keep"; + + const auto writeRegistry = [&](const std::filesystem::path& installPath) { + nlohmann::json registry = nlohmann::json::array(); + registry.push_back({{"modelId", "victim"}, {"installPath", installPath.string()}}); + std::ofstream(hub / "install_registry.json", std::ios::trunc) << registry.dump(); + }; + const auto registryEntries = [&] { + std::ifstream in(hub / "install_registry.json"); + return nlohmann::json::parse(in).size(); + }; + + juce::ThreadPool pool(1); + automix::ai::ModelManager modelManager; + FakeModelHubState fakeState; + std::optional done; + automix::app::ModelController::Callbacks callbacks; + callbacks.onUninstallComplete = [&](const bool value) { done = value; }; + automix::app::ModelController controller(modelManager, pool, std::move(callbacks), makeFakeModelHubOps(fakeState)); + controller.setModelHubRoot(hub); + + const auto uninstall = [&] { + done.reset(); + std::atomic_bool cancelFlag {false}; + controller.uninstallModel("victim", cancelFlag); + REQUIRE(waitFor([&]() { return done.has_value(); })); + REQUIRE_FALSE(done.value()); + }; + + SECTION("an existing directory outside the hub survives and stays registered") { + writeRegistry(outside); + uninstall(); + REQUIRE(std::filesystem::exists(outside / "keep.txt")); + REQUIRE(registryEntries() == 1); + } + + SECTION("a traversal path that resolves outside the hub survives") { + writeRegistry(hub / "inside-pack" / ".." / ".." / "precious"); + uninstall(); + REQUIRE(std::filesystem::exists(outside / "keep.txt")); + REQUIRE(registryEntries() == 1); + } + + SECTION("a stale entry whose directory is gone is unregistered") { + writeRegistry(std::filesystem::path("assets") / "modelhub" / "long-gone-pack"); + uninstall(); + REQUIRE(registryEntries() == 0); + } + + SECTION("a pack inside the hub is removed") { + writeRegistry(hub / "inside-pack"); + uninstall(); + REQUIRE_FALSE(std::filesystem::exists(hub / "inside-pack")); + REQUIRE(registryEntries() == 0); + } + +#if defined(_WIN32) + SECTION("a junction inside the hub pointing outside is not followed") { + const auto junction = hub / "junction-pack"; + const auto command = "mklink /J \"" + junction.string() + "\" \"" + outside.string() + "\" >NUL"; + REQUIRE(std::system(command.c_str()) == 0); + writeRegistry(junction); + uninstall(); + REQUIRE(std::filesystem::exists(outside / "keep.txt")); + std::filesystem::remove(junction); // removes the link only + } +#endif + + std::filesystem::remove_all(base); +} + TEST_CASE("ExportController returns cancelled result when cancel flag is pre-set", "[controllers][export][cancel]") { juce::ScopedJuceInitialiser_GUI juceInit; diff --git a/tests/unit/GpuBenchmarkTests.cpp b/tests/unit/GpuBenchmarkTests.cpp index 9b5716d..da4d446 100644 --- a/tests/unit/GpuBenchmarkTests.cpp +++ b/tests/unit/GpuBenchmarkTests.cpp @@ -10,6 +10,7 @@ #include "ai/GpuProvider.h" #include "ai/OnnxModelInference.h" +#include "ai/OnnxTensorInference.h" namespace automix { namespace ai { namespace test { @@ -78,6 +79,9 @@ TEST_CASE("GpuProvider canonical name mapping", "[gpu][provider]") { CHECK(canonicalProviderName("CpuExecutionProvider") == "cpu"); CHECK(canonicalProviderName("CUDA") == "cuda"); CHECK(canonicalProviderName("CudaExecutionProvider") == "cuda"); + CHECK(canonicalProviderName("WebGpu") == "webgpu"); + CHECK(canonicalProviderName("WebGpuExecutionProvider") == "webgpu"); + CHECK(canonicalProviderName("wgpu") == "webgpu"); CHECK(canonicalProviderName("DML") == "directml"); CHECK(canonicalProviderName("DirectML") == "directml"); CHECK(canonicalProviderName("CoreML") == "coreml"); @@ -95,25 +99,27 @@ TEST_CASE("GpuProvider priority chain order", "[gpu][provider]") { const auto& chain = providerPriorityChain(); - // Chain should be: ANE > CoreML > CUDA > OpenVINO > DirectML > CPU - REQUIRE(chain.size() == 6); + // Chain should be: ANE > CoreML > CUDA > WebGPU > OpenVINO > DirectML > CPU + REQUIRE(chain.size() == 7); CHECK(chain[0] == "ane"); CHECK(chain[1] == "coreml"); CHECK(chain[2] == "cuda"); - CHECK(chain[3] == "openvino"); - CHECK(chain[4] == "directml"); - CHECK(chain[5] == "cpu"); + CHECK(chain[3] == "webgpu"); + CHECK(chain[4] == "openvino"); + CHECK(chain[5] == "directml"); + CHECK(chain[6] == "cpu"); // Verify priority values (lower = higher priority) CHECK(providerPriority("ane") == 0); CHECK(providerPriority("coreml") == 1); CHECK(providerPriority("cuda") == 2); - CHECK(providerPriority("openvino") == 3); - CHECK(providerPriority("directml") == 4); - CHECK(providerPriority("cpu") == 5); + CHECK(providerPriority("webgpu") == 3); + CHECK(providerPriority("openvino") == 4); + CHECK(providerPriority("directml") == 5); + CHECK(providerPriority("cpu") == 6); // Unknown providers have lowest priority - CHECK(providerPriority("tensorrt") == 6); + CHECK(providerPriority("tensorrt") == 7); } TEST_CASE("GpuProvider isGpuProvider classification", "[gpu][provider]") { @@ -121,6 +127,7 @@ TEST_CASE("GpuProvider isGpuProvider classification", "[gpu][provider]") { CHECK_FALSE(isGpuProvider("cpu")); CHECK(isGpuProvider("cuda")); + CHECK(isGpuProvider("webgpu")); CHECK(isGpuProvider("directml")); CHECK(isGpuProvider("coreml")); CHECK(isGpuProvider("ane")); @@ -133,7 +140,178 @@ TEST_CASE("GpuProvider platform preferred provider", "[gpu][provider]") { const auto preferred = platformPreferredProvider(); // Should return one of the known provider names CHECK((preferred == "ane" || preferred == "coreml" || - preferred == "cuda" || preferred == "directml")); + preferred == "cuda" || preferred == "webgpu")); +#if defined(_WIN32) + CHECK(preferred == "webgpu"); +#endif +} + +TEST_CASE("CoreML and ANE are opt-in for auto on low-memory Macs", "[gpu][tensor][coreml]") { + using automix::ai::coreMlAutoAllowed; + using automix::ai::kCoreMlAutoMinMemoryBytes; + constexpr std::uint64_t GiB = 1024ull * 1024 * 1024; + const std::vector mac = {"ane", "coreml", "cpu"}; + + CHECK_FALSE(coreMlAutoAllowed(8 * GiB, false)); + CHECK(coreMlAutoAllowed(8 * GiB, true)); + CHECK(coreMlAutoAllowed(kCoreMlAutoMinMemoryBytes, false)); + CHECK(coreMlAutoAllowed(16 * GiB, false)); + CHECK(coreMlAutoAllowed(0, false)); + + // auto on a low-memory Mac goes straight to CPU; opted in or roomy keeps the Apple providers + CHECK(tensorProviderCandidates("auto", mac, {}, false) == std::vector{"cpu"}); + CHECK(tensorProviderCandidates("auto", mac, {}, true) == std::vector{"ane", "coreml", "cpu"}); + // naming the provider is always honoured + CHECK(tensorProviderCandidates("coreml", mac, {}, false).front() == "coreml"); + CHECK(tensorProviderCandidates("ane", mac, {}, false).front() == "ane"); +} + +TEST_CASE("Vocal model warns about CPU only on low-memory Macs", "[gpu][coreml]") { + using automix::ai::vocalModelCpuWarning; + constexpr std::uint64_t GiB = 1024ull * 1024 * 1024; + CHECK_FALSE(vocalModelCpuWarning(8 * GiB, false, true, false).empty()); + CHECK(vocalModelCpuWarning(8 * GiB, false, true, false).find("8 GB") != std::string::npos); + CHECK(vocalModelCpuWarning(8 * GiB, true, true, false).empty()); // a GPU session is usable + CHECK(vocalModelCpuWarning(16 * GiB, false, true, false).empty()); // roomy + CHECK(vocalModelCpuWarning(8 * GiB, false, false, false).empty()); // not a Mac: no measurement behind it + CHECK(vocalModelCpuWarning(0, false, true, false).empty()); // unknown memory + CHECK(vocalModelCpuWarning(8 * GiB, false, true, true).empty()); // Open-Unmix already active +} + +TEST_CASE("tensorProviderCandidates allow-list filtering", "[gpu][tensor]") { + // 1. Empty allow-list preserves all reported GPU candidates and keeps CPU last + const auto c1 = tensorProviderCandidates("auto", {"cuda", "webgpu", "cpu"}, {}); + const std::vector expected1 = {"cuda", "webgpu", "cpu"}; + CHECK(c1 == expected1); + + // 2. ["cuda"] on a webgpu-only runtime filters to [cpu], CPU always kept + const auto c2 = tensorProviderCandidates("auto", {"webgpu", "cpu"}, {"cuda"}); + const std::vector expected2 = {"cpu"}; + CHECK(c2 == expected2); + + // 3. Requested provider not in allow-list falls back to allowed candidates then CPU + const auto c3 = tensorProviderCandidates("webgpu", {"webgpu", "cpu"}, {"cuda"}); + CHECK(c3 == expected2); + + // 4. ["webgpu"] on a multi-GPU runtime filters to [webgpu, cpu] + const auto c4 = tensorProviderCandidates("auto", {"cuda", "webgpu", "cpu"}, {"webgpu"}); + const std::vector expected4 = {"webgpu", "cpu"}; + CHECK(c4 == expected4); +} + +TEST_CASE("GpuProvider providerOptionMap and sessionTuning", "[gpu][provider]") { + using namespace automix::ai::gpu; + + // 1. providerOptionMap("ane") + const auto aneOpts = providerOptionMap("ane"); + CHECK(aneOpts.at("ModelFormat") == "MLProgram"); + CHECK(aneOpts.at("MLComputeUnits") == "CPUAndNeuralEngine"); + + // Every CoreML/ANE key must be in the valid set documented in coreml_options.cc@v1.30.0:55-64 + const std::vector validCoreMlKeys = { + "MLComputeUnits", + "ModelFormat", + "RequireStaticInputShapes", + "EnableOnSubgraphs", + "SpecializationStrategy", + "ProfileComputePlan", + "AllowLowPrecisionAccumulationOnGPU", + "ModelCacheDirectory" + }; + for (const auto& [k, v] : aneOpts) { + CHECK((std::find(validCoreMlKeys.begin(), validCoreMlKeys.end(), k) != validCoreMlKeys.end())); + } + + // 2. providerOptionMap("coreml") + const auto coremlOpts = providerOptionMap("coreml"); + CHECK(coremlOpts.at("ModelFormat") == "MLProgram"); + CHECK(coremlOpts.at("MLComputeUnits") == "ALL"); + for (const auto& [k, v] : coremlOpts) { + CHECK((std::find(validCoreMlKeys.begin(), validCoreMlKeys.end(), k) != validCoreMlKeys.end())); + } + + // 3. sessionTuning("directml", n) gives sequential execution with mem pattern off + const auto dmlTuning = sessionTuning("directml", 8); + CHECK(dmlTuning.sequentialExecution == true); + CHECK(dmlTuning.memPattern == false); + CHECK(dmlTuning.cpuArena == false); + CHECK(dmlTuning.interOpThreads == 1); + CHECK(dmlTuning.intraOpThreads == 4); + + // 4. sessionTuning("cuda" | "cpu", n) golden values + const auto cudaTuning = sessionTuning("cuda", 8); + CHECK(cudaTuning.sequentialExecution == false); + CHECK(cudaTuning.memPattern == false); + CHECK(cudaTuning.cpuArena == true); + CHECK(cudaTuning.interOpThreads == 1); + CHECK(cudaTuning.intraOpThreads == 4); + CHECK(cudaTuning.hardwareTier == "standard"); + + const auto cpuTuningLow = sessionTuning("cpu", 2); + CHECK(cpuTuningLow.hardwareTier == "low"); + CHECK(cpuTuningLow.intraOpThreads == 2); + CHECK(cpuTuningLow.interOpThreads == 1); + CHECK(cpuTuningLow.memPattern == true); + CHECK(cpuTuningLow.cpuArena == true); + CHECK(cpuTuningLow.sequentialExecution == false); + + const auto cpuTuningHigh = sessionTuning("cpu", 16); + CHECK(cpuTuningHigh.hardwareTier == "high"); + CHECK(cpuTuningHigh.intraOpThreads == 16); + CHECK(cpuTuningHigh.interOpThreads == 8); + CHECK(cpuTuningHigh.memPattern == true); + CHECK(cpuTuningHigh.cpuArena == true); + CHECK(cpuTuningHigh.sequentialExecution == false); +} + +TEST_CASE("sessionConfigPlan leaves ORT defaults for untuned sessions", "[gpu][provider]") { + using namespace automix::ai::gpu; + + // Untuned CPU session leaves every field empty (ORT defaults) + const auto cpuUntuned = sessionConfigPlan("cpu", 0); + CHECK(!cpuUntuned.intraOpThreads.has_value()); + CHECK(!cpuUntuned.interOpThreads.has_value()); + CHECK(!cpuUntuned.sequentialExecution.has_value()); + CHECK(!cpuUntuned.memPattern.has_value()); + CHECK(!cpuUntuned.cpuArena.has_value()); + + // Untuned CUDA, CoreML, WebGPU: every field empty + for (const auto& prov : {"cuda", "coreml", "webgpu"}) { + const auto untuned = sessionConfigPlan(prov, 0); + CHECK(!untuned.intraOpThreads.has_value()); + CHECK(!untuned.interOpThreads.has_value()); + CHECK(!untuned.sequentialExecution.has_value()); + CHECK(!untuned.memPattern.has_value()); + CHECK(!untuned.cpuArena.has_value()); + } + + // Untuned DirectML: mandatory constraints only (memPattern=false, sequentialExecution=true) + const auto dmlUntuned = sessionConfigPlan("directml", 0); + CHECK(dmlUntuned.memPattern == false); + CHECK(dmlUntuned.sequentialExecution == true); + CHECK(!dmlUntuned.intraOpThreads.has_value()); + CHECK(!dmlUntuned.interOpThreads.has_value()); + CHECK(!dmlUntuned.cpuArena.has_value()); + + // Tuned CPU session (hardwareThreads > 0): equals sessionTuning field by field + const auto cpuTuned = sessionConfigPlan("cpu", 16); + const auto cpuExpectedTuning = sessionTuning("cpu", 16); + CHECK(cpuTuned.intraOpThreads == cpuExpectedTuning.intraOpThreads); + CHECK(cpuTuned.interOpThreads == cpuExpectedTuning.interOpThreads); + CHECK(cpuTuned.sequentialExecution == cpuExpectedTuning.sequentialExecution); + CHECK(cpuTuned.memPattern == cpuExpectedTuning.memPattern); + CHECK(cpuTuned.cpuArena == cpuExpectedTuning.cpuArena); + CHECK(cpuTuned.intraOpThreads == 16); + CHECK(cpuTuned.interOpThreads == 8); + CHECK(cpuTuned.sequentialExecution == false); + CHECK(cpuTuned.memPattern == true); + CHECK(cpuTuned.cpuArena == true); + + // Tuned DirectML session (hardwareThreads > 0) + const auto dmlTuned = sessionConfigPlan("directml", 8); + CHECK(dmlTuned.sequentialExecution == true); + CHECK(dmlTuned.memPattern == false); + CHECK(dmlTuned.cpuArena == false); } // ─── T3.4: detectAvailableProviders ───────────────────────────────────────── @@ -240,7 +418,12 @@ TEST_CASE("OnnxModelInference OOM failure triggers recovery counters and CPU re- const auto modelPath = createDummyModel(tempDir); createDummyMetadataWithGpu(modelPath); + // Load on CPU: these tests simulate GPU failures, so a real GPU attempt at + // load (which fails wherever CUDA's libraries are absent) must not pre-mark it. + inference.setExecutionProviderPreference("cpu"); REQUIRE(inference.loadModel(modelPath)); + inference.pinExecutionProvidersForTesting({"cuda", "cpu"}); + inference.setExecutionProviderPreference("auto"); // Setup proves the GPU provider is actually selectable before any failure: // the recovery assertions are only meaningful if resolution picks "cuda". @@ -285,7 +468,12 @@ TEST_CASE("OnnxModelInference device-lost failure triggers recovery counters and const auto modelPath = createDummyModel(tempDir); createDummyMetadataWithGpu(modelPath); + // Load on CPU: these tests simulate GPU failures, so a real GPU attempt at + // load (which fails wherever CUDA's libraries are absent) must not pre-mark it. + inference.setExecutionProviderPreference("cpu"); REQUIRE(inference.loadModel(modelPath)); + inference.pinExecutionProvidersForTesting({"cuda", "cpu"}); + inference.setExecutionProviderPreference("auto"); REQUIRE(inference.activeExecutionProvider() == "cuda"); // When: a GPU device-lost failure is simulated on the active provider. @@ -319,7 +507,12 @@ TEST_CASE("OnnxModelInference resolution skips failed provider on later re-resol const auto modelPath = createDummyModel(tempDir); createDummyMetadataWithGpu(modelPath); + // Load on CPU: these tests simulate GPU failures, so a real GPU attempt at + // load (which fails wherever CUDA's libraries are absent) must not pre-mark it. + inference.setExecutionProviderPreference("cpu"); REQUIRE(inference.loadModel(modelPath)); + inference.pinExecutionProvidersForTesting({"cuda", "cpu"}); + inference.setExecutionProviderPreference("auto"); REQUIRE(inference.activeExecutionProvider() == "cuda"); // Given: the GPU provider has failed once and resolution fell back to CPU. @@ -346,7 +539,12 @@ TEST_CASE("OnnxModelInference repeated failure counts each recovery but records const auto modelPath = createDummyModel(tempDir); createDummyMetadataWithGpu(modelPath); + // Load on CPU: these tests simulate GPU failures, so a real GPU attempt at + // load (which fails wherever CUDA's libraries are absent) must not pre-mark it. + inference.setExecutionProviderPreference("cpu"); REQUIRE(inference.loadModel(modelPath)); + inference.pinExecutionProvidersForTesting({"cuda", "cpu"}); + inference.setExecutionProviderPreference("auto"); REQUIRE(inference.activeExecutionProvider() == "cuda"); // When: the same provider fails twice. @@ -374,7 +572,12 @@ TEST_CASE("OnnxModelInference non-recoverable failure marks provider failed with const auto modelPath = createDummyModel(tempDir); createDummyMetadataWithGpu(modelPath); + // Load on CPU: these tests simulate GPU failures, so a real GPU attempt at + // load (which fails wherever CUDA's libraries are absent) must not pre-mark it. + inference.setExecutionProviderPreference("cpu"); REQUIRE(inference.loadModel(modelPath)); + inference.pinExecutionProvidersForTesting({"cuda", "cpu"}); + inference.setExecutionProviderPreference("auto"); REQUIRE(inference.activeExecutionProvider() == "cuda"); // When: a non-OOM / non-device-lost failure is simulated. @@ -454,3 +657,96 @@ TEST_CASE("Multiple task type benchmark", "[gpu][benchmark][onnx]") { std::filesystem::remove_all(tempDir); } + +TEST_CASE("GpuProvider pluginLibraryFileName platform mapping", "[gpu][provider]") { + using namespace automix::ai::gpu; + + CHECK(pluginLibraryFileName("windows") == "onnxruntime_providers_webgpu.dll"); + CHECK(pluginLibraryFileName("win") == "onnxruntime_providers_webgpu.dll"); + CHECK(pluginLibraryFileName("win-x64") == "onnxruntime_providers_webgpu.dll"); + CHECK(pluginLibraryFileName("Windows_NT") == "onnxruntime_providers_webgpu.dll"); + + CHECK(pluginLibraryFileName("linux") == "libonnxruntime_providers_webgpu.so"); + CHECK(pluginLibraryFileName("linux-x64") == "libonnxruntime_providers_webgpu.so"); + CHECK(pluginLibraryFileName("Ubuntu") == "libonnxruntime_providers_webgpu.so"); + + CHECK(pluginLibraryFileName("macos").empty()); + CHECK(pluginLibraryFileName("darwin").empty()); + CHECK(pluginLibraryFileName("").empty()); +} + +struct ProbeCacheResetGuard { + ProbeCacheResetGuard() { resetTensorProviderProbeCacheForTesting(); } + ~ProbeCacheResetGuard() { resetTensorProviderProbeCacheForTesting(); } +}; + +TEST_CASE("tensorProviderUsable per-provider probe cache semantics", "[gpu][tensor]") { + ProbeCacheResetGuard guard; + + int cudaProbeCalls = 0; + bool cudaAvailable = false; + bool webgpuAvailable = false; + + setTensorProviderProbeFunctionForTesting([&](const std::string& candidate) { + if (candidate == "webgpu") return webgpuAvailable; + if (candidate == "cuda") { + ++cudaProbeCalls; + return cudaAvailable; + } + return false; + }); + + // 1. Failure caching: with probe returning false for cuda, first call is false, + // and second call does NOT invoke probe (count remains 1). + cudaAvailable = false; + CHECK(tensorProviderUsable("cuda") == false); + CHECK(cudaProbeCalls == 1); + CHECK(tensorProviderUsable("cuda") == false); + CHECK(cudaProbeCalls == 1); + + // 2. Invalidate cache: after invalidateTensorProviderProbeCache(), probe is re-invoked + cudaAvailable = true; + invalidateTensorProviderProbeCache(); + CHECK(tensorProviderUsable("cuda") == true); + CHECK(cudaProbeCalls == 2); + // Subsequent calls use cached success + CHECK(tensorProviderUsable("cuda") == true); + CHECK(cudaProbeCalls == 2); + + // 3. R1 regression: probe returns true for webgpu and false for cuda + invalidateTensorProviderProbeCache(); + cudaAvailable = false; + webgpuAvailable = true; + std::string winner; + CHECK(gpuTensorSessionAvailable(&winner) == true); + CHECK(winner == "webgpu"); + + // Invalidate and make cuda succeed: CUDA is usable AND winner is "cuda" (precedes webgpu) + invalidateTensorProviderProbeCache(); + cudaAvailable = true; + CHECK(tensorProviderUsable("cuda") == true); + CHECK(gpuTensorSessionAvailable(&winner) == true); + CHECK(winner == "cuda"); +} + + +TEST_CASE("benchProviderMatches compares canonical provider names", "[gpu][bench]") { + using automix::ai::gpu::benchProviderMatches; + CHECK(benchProviderMatches("auto", "cpu") == true); + CHECK(benchProviderMatches("cuda", "cuda") == true); + CHECK(benchProviderMatches("webgpu", "cuda") == false); + CHECK(benchProviderMatches("webgpu", "cpu") == false); + CHECK(benchProviderMatches("cuda", "") == false); + CHECK(benchProviderMatches("cuda", "unknown") == false); + // canonicalProviderName maps "wgpu" to "webgpu" and ignores case. + CHECK(benchProviderMatches("WGPU", "webgpu") == true); +} + +TEST_CASE("nearestRankPercentile uses the nearest-rank method", "[gpu][bench]") { + using automix::ai::gpu::nearestRankPercentile; + CHECK(nearestRankPercentile({}, 0.9) == 0.0); + CHECK(nearestRankPercentile({5.0}, 0.9) == 5.0); + CHECK(nearestRankPercentile({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 0.9) == 9.0); + CHECK(nearestRankPercentile({3.0, 1.0, 2.0}, 0.5) == 2.0); + CHECK(nearestRankPercentile({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 1.0) == 10.0); +} diff --git a/tests/unit/LameDownloaderTests.cpp b/tests/unit/LameDownloaderTests.cpp new file mode 100644 index 0000000..3eee753 --- /dev/null +++ b/tests/unit/LameDownloaderTests.cpp @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include + +#include + +#include "util/LameDownloader.h" +#include "util/Sha256.h" + +using automix::util::LameDownloader; + +TEST_CASE("Every LAME download source is pinned to a SHA-256 over HTTPS", "[util][lame]") { + const auto pins = LameDownloader::pinnedSources(); + REQUIRE(!pins.empty()); + + std::set platforms; + for (const auto& pin : pins) { + INFO(pin.platformKey << " " << pin.url); + CHECK(automix::util::isSha256Hex(pin.sha256)); + CHECK(pin.url.rfind("https://", 0) == 0); + platforms.insert(pin.platformKey); + } + for (const char* key : {"win32-x64", "win32-ia32", "win32-arm64", "linux-x64", "linux-arm64", "linux-arm", + "darwin-x64", "darwin-arm64"}) { + INFO(key); + CHECK(platforms.count(key) == 1); + } +} + +TEST_CASE("The published LAME pin list in the repo is valid and names only known platforms", "[util][lame]") { + std::ifstream file(std::filesystem::path(AUTOMIX_SOURCE_DIR) / "assets" / "lame-pins.json", std::ios::binary); + REQUIRE(file.is_open()); + const std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + std::string detail; + const auto pins = LameDownloader::parsePinManifest(text, &detail); + INFO(detail); + REQUIRE(!pins.empty()); + + // A platform may be absent: the app then uses its built-in pins. + std::set known; + for (const auto& builtIn : LameDownloader::pinnedSources()) { + known.insert(builtIn.platformKey); + } + for (const auto& pin : pins) { + INFO(pin.platformKey); + CHECK(known.count(pin.platformKey) == 1); + } +} + +TEST_CASE("A LAME pin list that could redirect the download is rejected whole", "[util][lame]") { + const std::string sha(64, 'a'); + const auto list = [](const std::string& entry) { return R"({"schema":1,"sources":[)" + entry + "]}"; }; + const auto zip = [&sha](const std::string& url) { + return R"({"platform":"win32-x64","type":"zip","url":")" + url + R"(","sha256":")" + sha + R"("})"; + }; + const std::string good = zip("https://www.rarewares.org/files/mp3/lame4.0-x64.zip"); + + REQUIRE(LameDownloader::parsePinManifest(list(good), nullptr).size() == 1); + CHECK(LameDownloader::parsePinManifest(list(R"({"platform":"darwin-arm64","type":"ghcr","sha256":")" + sha + R"("})"), + nullptr) + .size() == 1); + + std::string detail; + for (const auto& bad : { + zip("https://evil.example/lame.zip"), + zip("http://www.rarewares.org/files/mp3/lame.zip"), + zip("https://www.rarewares.org.evil.example/files/mp3/lame.zip"), + zip("https://www.rarewares.org/files/mp3/../../x/lame.zip"), + zip("https://www.rarewares.org/files/mp3/lame.exe"), + zip("https://www.rarewares.org/files/mp3/lame.zip?x=.zip"), + std::string(R"({"platform":"win32-x64","type":"zip","url":"https://www.rarewares.org/files/mp3/l.zip","sha256":"abc"})"), + std::string(R"({"platform":"win32-x64","type":"exe","url":"https://www.rarewares.org/files/mp3/l.zip","sha256":")") + + sha + R"("})", + }) { + INFO(bad); + // One bad entry poisons the list even next to a good one. + CHECK(LameDownloader::parsePinManifest(list(good + "," + bad), &detail).empty()); + CHECK(!detail.empty()); + } + CHECK(LameDownloader::parsePinManifest("not json", &detail).empty()); + CHECK(LameDownloader::parsePinManifest(R"({"schema":2,"sources":[]})", &detail).empty()); +} + +TEST_CASE("A LAME download that does not match its pin is rejected and deleted", "[util][lame]") { + const auto dir = std::filesystem::temp_directory_path() / "automix_lame_pin_test"; + std::filesystem::create_directories(dir); + const auto file = dir / "lame.zip"; + const auto write = [&file]() { std::ofstream(file, std::ios::binary) << "not really lame"; }; + + write(); + const auto actual = automix::util::fileSha256(file); + std::string detail; + CHECK(LameDownloader::verifyDownload(file, actual, &detail)); + CHECK(std::filesystem::exists(file)); + + std::string wrong = actual; + wrong[0] = wrong[0] == '0' ? '1' : '0'; + CHECK_FALSE(LameDownloader::verifyDownload(file, wrong, &detail)); + CHECK(detail.find("mismatch") != std::string::npos); + CHECK_FALSE(std::filesystem::exists(file)); + + write(); + CHECK_FALSE(LameDownloader::verifyDownload(file, "", &detail)); + CHECK_FALSE(std::filesystem::exists(file)); + + std::filesystem::remove_all(dir); +} diff --git a/tests/unit/ModelBenchmarkTests.cpp b/tests/unit/ModelBenchmarkTests.cpp index 58da5a6..f7dcc51 100644 --- a/tests/unit/ModelBenchmarkTests.cpp +++ b/tests/unit/ModelBenchmarkTests.cpp @@ -44,3 +44,35 @@ TEST_CASE("ONNX model load failure is graceful", "[benchmark][onnx]") { REQUIRE_FALSE(loaded); REQUIRE_FALSE(inference.isAvailable()); } + +#include + +TEST_CASE("Model benchmark on demo-mix-v1 stub exits with code 3", "[benchmark][model]") { + const juce::File exe = juce::File::getSpecialLocation(juce::File::currentExecutableFile); + const auto devTools = exe.getSiblingFile("automix_dev_tools" +#if defined(_WIN32) + ".exe" +#endif + ); + if (!devTools.existsAsFile()) { + SKIP("automix_dev_tools executable not found in test directory"); + } + + juce::StringArray args; + args.add(devTools.getFullPathName()); + args.add("model"); + args.add("bench"); + args.add("--pack"); +#if defined(AUTOMIX_SOURCE_DIR) + const auto packPath = std::filesystem::path(AUTOMIX_SOURCE_DIR) / "assets" / "models" / "demo-mix-v1"; + args.add(juce::String(packPath.string())); +#else + args.add("assets/models/demo-mix-v1"); +#endif + + juce::ChildProcess process; + REQUIRE(process.start(args)); + REQUIRE(process.waitForProcessToFinish(30000)); + REQUIRE(process.getExitCode() == 3); +} + diff --git a/tests/unit/PhaseLimiterDiscoveryTests.cpp b/tests/unit/PhaseLimiterDiscoveryTests.cpp index 4bb358e..0a794b7 100644 --- a/tests/unit/PhaseLimiterDiscoveryTests.cpp +++ b/tests/unit/PhaseLimiterDiscoveryTests.cpp @@ -6,6 +6,9 @@ #include #include "renderers/PhaseLimiterDiscovery.h" +#include "renderers/PhaseLimiterPins.h" +#include +#include namespace { @@ -69,6 +72,24 @@ TEST_CASE("PhaseLimiter discovery finds binary inside assets folder", "[phaselim std::filesystem::remove_all(root); } +TEST_CASE("PhaseLimiter discovery finds binary inside macOS bundle layout", "[phaselimiter][discovery]") { + const std::filesystem::path appBundle = std::filesystem::temp_directory_path() / "AutoMixMaster.app"; + const std::filesystem::path binDir = appBundle / "Contents" / "MacOS" / "assets" / "phaselimiter" / "bin"; + const std::filesystem::path binary = binDir / binaryNameForPlatform(); + + std::filesystem::remove_all(appBundle); + std::filesystem::create_directories(binDir); + std::ofstream(binary).put('\n'); + + automix::renderers::PhaseLimiterDiscovery discovery; + const auto result = discovery.findInRoots({appBundle}); + REQUIRE(result.has_value()); + REQUIRE(lowerPath(result->executablePath) == lowerPath(binary)); + REQUIRE(lowerPath(result->installRoot) == lowerPath(appBundle / "Contents" / "MacOS" / "assets" / "phaselimiter")); + + std::filesystem::remove_all(appBundle); +} + TEST_CASE("PhaseLimiter discovery supports PHASELIMITER_BIN override", "[phaselimiter][discovery]") { const std::filesystem::path root = std::filesystem::temp_directory_path() / "automix_phaselimiter_discovery_env"; const std::filesystem::path binDir = root / "custom_bin"; @@ -136,3 +157,145 @@ TEST_CASE("PhaseLimiter discovery supports AUTOMIX_ASSET_ROOT override", "[phase setEnvValue("AUTOMIX_ASSET_ROOT", previousEnv); std::filesystem::remove_all(root); } + +TEST_CASE("PhaseLimiter download pin table contains target platforms and valid sha256 digests", "[phaselimiter][discovery]") { + const auto table = automix::renderers::phaseLimiterDownloadPinTable(); + REQUIRE_FALSE(table.empty()); + + const std::vector requiredPlatforms = { + "windows-x64", + "linux-x64", + "linux-arm64", + "macos-arm64", + "macos-x86_64", + }; + + for (const auto& required : requiredPlatforms) { + const auto it = std::find_if(table.begin(), table.end(), + [&](const automix::renderers::PhaseLimiterDownloadPin& pin) { + return pin.platformKey == required; + }); + INFO("Checking presence of required platform: " << required); + REQUIRE(it != table.end()); + REQUIRE(!it->url.empty()); + REQUIRE(it->sha256.size() == 64); + for (char c : it->sha256) { + REQUIRE(((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))); + } + if (required == "linux-x64") { + REQUIRE(it->sha256 == "8ba28b31f823555c3c368a6d3b4757c1980c981b6abe726e80d724f050aeefaa"); + } + } + + for (const auto& pin : table) { + REQUIRE(!pin.url.empty()); + REQUIRE(pin.sha256.size() == 64); + } + + const auto currentKey = automix::renderers::currentPhaseLimiterPlatformKey(); + REQUIRE_FALSE(currentKey.empty()); + REQUIRE(currentKey != "unknown"); +} + +TEST_CASE("PhaseLimiter auto-download rejects unverified table pin with empty hash", "[phaselimiter][discovery]") { + automix::renderers::PhaseLimiterDiscovery::resetAttemptedDownloadForTesting(); + + bool downloadAttempted = false; + automix::renderers::PhaseLimiterDiscovery::setDownloadFetcherForTesting( + [&](const std::string& /*url*/, const std::filesystem::path& destination) { + downloadAttempted = true; + std::ofstream out(destination, std::ios::binary); + out << "test-data"; + return true; + }); + + automix::renderers::PhaseLimiterDownloadPin emptyHashPin{ + "test-empty-hash", + "https://example.com/test_empty.zip", + "" + }; + + const auto result = automix::renderers::PhaseLimiterDiscovery::downloadAndInstall(emptyHashPin); + REQUIRE(!result.has_value()); + REQUIRE(!downloadAttempted); + + automix::renderers::PhaseLimiterDiscovery::resetDownloadFetcherForTesting(); + automix::renderers::PhaseLimiterDiscovery::resetAttemptedDownloadForTesting(); +} + +TEST_CASE("PhaseLimiter auto-download rejects corrupted archive before extraction and discards file", "[phaselimiter][discovery]") { + automix::renderers::PhaseLimiterDiscovery::resetAttemptedDownloadForTesting(); + + std::filesystem::path interceptedDestination; + automix::renderers::PhaseLimiterDiscovery::setDownloadFetcherForTesting( + [&](const std::string& /*url*/, const std::filesystem::path& destination) { + interceptedDestination = destination; + std::ofstream out(destination, std::ios::binary); + out << "corrupted-file-data-that-will-not-match-expected-hash"; + return true; + }); + + automix::renderers::PhaseLimiterDownloadPin testPin{ + "test-platform", + "https://example.com/test_corrupted.zip", + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + }; + + const auto result = automix::renderers::PhaseLimiterDiscovery::downloadAndInstall(testPin); + REQUIRE(!result.has_value()); + REQUIRE(!interceptedDestination.empty()); + REQUIRE(!std::filesystem::exists(interceptedDestination)); + + automix::renderers::PhaseLimiterDiscovery::resetDownloadFetcherForTesting(); + automix::renderers::PhaseLimiterDiscovery::resetAttemptedDownloadForTesting(); +} + +TEST_CASE("PhaseLimiter defaultPhaseLimiterDownloadPin supports AUTOMIX_PHASELIMITER_DOWNLOAD_URL override", "[phaselimiter][discovery]") { + std::string previousUrl; +#if defined(_WIN32) + char* oldValue = nullptr; + size_t oldLength = 0; + if (_dupenv_s(&oldValue, &oldLength, "AUTOMIX_PHASELIMITER_DOWNLOAD_URL") == 0 && oldValue != nullptr) { + previousUrl.assign(oldValue, oldLength > 0 ? oldLength - 1 : 0); + free(oldValue); + } +#else + if (const char* existing = std::getenv("AUTOMIX_PHASELIMITER_DOWNLOAD_URL"); existing != nullptr) { + previousUrl = existing; + } +#endif + + setEnvValue("AUTOMIX_PHASELIMITER_DOWNLOAD_URL", "https://example.com/custom_phaselimiter.zip"); + + const auto pin = automix::renderers::defaultPhaseLimiterDownloadPin(); + REQUIRE(pin.has_value()); + REQUIRE(pin->url == "https://example.com/custom_phaselimiter.zip"); + + setEnvValue("AUTOMIX_PHASELIMITER_DOWNLOAD_URL", previousUrl); +} + +TEST_CASE("PhaseLimiter pin table comes from the generated header", "[phaselimiter]") { + const auto table = automix::renderers::phaseLimiterDownloadPinTable(); + REQUIRE(table.size() == std::size(automix::renderers::kPhaseLimiterPins)); + + const std::set validKeys = { + "windows-x64", "macos-arm64", "macos-x86_64", "linux-x64", "linux-arm64" + }; + const std::regex shaRegex("^[0-9a-f]{64}$"); + + std::set seenKeys; + for (const auto& pin : table) { + REQUIRE(validKeys.find(pin.platformKey) != validKeys.end()); + REQUIRE(pin.url.rfind("https://github.com/", 0) == 0); + REQUIRE(std::regex_match(pin.sha256, shaRegex)); + + REQUIRE(seenKeys.find(pin.platformKey) == seenKeys.end()); + seenKeys.insert(pin.platformKey); + } + + REQUIRE(seenKeys.size() == 5); + for (const auto& key : validKeys) { + REQUIRE(seenKeys.find(key) != seenKeys.end()); + } +} + diff --git a/tests/unit/PhaseLimiterRendererTests.cpp b/tests/unit/PhaseLimiterRendererTests.cpp index 2f28f77..e4ed92d 100644 --- a/tests/unit/PhaseLimiterRendererTests.cpp +++ b/tests/unit/PhaseLimiterRendererTests.cpp @@ -1,14 +1,29 @@ #include #include +#include +#include #include #include "domain/Session.h" +#include "renderers/PhaseLimiterDiscovery.h" #include "renderers/PhaseLimiterRenderer.h" #include "util/WavWriter.h" namespace { +void setEnvValue(const char* key, const std::string& value) { +#if defined(_WIN32) + _putenv_s(key, value.c_str()); +#else + if (value.empty()) { + unsetenv(key); + } else { + setenv(key, value.c_str(), 1); + } +#endif +} + automix::engine::AudioBuffer makeTone(const double sampleRate, const int samples, const double frequency, @@ -68,3 +83,211 @@ TEST_CASE("PhaseLimiter renderer never crashes and always returns a valid render std::filesystem::remove_all(tempDir); } + +TEST_CASE("A selected PhaseLimiter really renders and leaves no scratch behind", "[phaselimiter][renderer]") { + automix::renderers::PhaseLimiterRenderer renderer; + if (!renderer.isAvailable()) { + SKIP("No complete PhaseLimiter install found"); + } + const auto installRoot = automix::renderers::PhaseLimiterDiscovery{}.find()->installRoot; + const auto countEntries = [](const std::filesystem::path& dir) { + std::error_code error; + std::size_t count = 0; + if (std::filesystem::is_directory(dir, error)) { + for ([[maybe_unused]] const auto& entry : std::filesystem::directory_iterator(dir, error)) { + ++count; + } + } + return count; + }; + + const std::filesystem::path tempDir = std::filesystem::temp_directory_path() / "automix_phaselimiter_real_render"; + std::filesystem::remove_all(tempDir); + std::filesystem::create_directories(tempDir); + automix::util::WavWriter writer; + writer.write(tempDir / "bass.wav", makeTone(44100.0, 44100, 110.0, 0.40), 24); + writer.write(tempDir / "lead.wav", makeTone(44100.0, 44100, 660.0, 0.20), 24); + automix::domain::Session session; + automix::domain::Stem bass; + bass.id = "bass"; + bass.name = "Bass"; + bass.filePath = (tempDir / "bass.wav").string(); + automix::domain::Stem lead; + lead.id = "lead"; + lead.name = "Lead"; + lead.filePath = (tempDir / "lead.wav").string(); + session.stems = {bass, lead}; + + automix::domain::RenderSettings settings; + settings.rendererName = "PhaseLimiter"; + settings.outputPath = (tempDir / "out.wav").string(); + + const auto scratchBase = std::filesystem::temp_directory_path() / "automix_phaselimiter"; + const auto workingDirectory = std::filesystem::current_path(); + const auto scratchBefore = countEntries(scratchBase); + const auto legacyBefore = countEntries(installRoot / "tmp"); + + const auto result = renderer.render(session, settings, {}, nullptr); + std::string logs; + for (const auto& line : result.logs) { + logs += line + "\n"; + } + INFO(logs); + REQUIRE(result.success); + REQUIRE(result.rendererName == "PhaseLimiter"); // not "PhaseLimiter (fallback BuiltIn)" + REQUIRE(logs.find("fallback") == std::string::npos); + REQUIRE(std::filesystem::exists(settings.outputPath)); + + REQUIRE(countEntries(scratchBase) <= scratchBefore); // its own run is gone + REQUIRE(countEntries(installRoot / "tmp") <= legacyBefore); // nothing new in the install + REQUIRE(std::filesystem::current_path() == workingDirectory); // no process-wide cwd change + std::filesystem::remove_all(tempDir); +} + +TEST_CASE("PhaseLimiter stays available without ffmpeg and fails the render with a clear message", "[phaselimiter][renderer]") { + const std::filesystem::path tempDir = std::filesystem::temp_directory_path() / "automix_pl_test_missing_ffmpeg"; + std::filesystem::remove_all(tempDir); + std::filesystem::create_directories(tempDir); + + const std::filesystem::path fakeInstall = tempDir / "fake_phaselimiter"; + const std::filesystem::path fakeBinDir = fakeInstall / "bin"; + const std::filesystem::path fakeResourceDir = fakeInstall / "resource"; + std::filesystem::create_directories(fakeBinDir); + std::filesystem::create_directories(fakeResourceDir); + +#if defined(_WIN32) + const std::filesystem::path fakeExe = fakeBinDir / "phase_limiter.exe"; +#else + const std::filesystem::path fakeExe = fakeBinDir / "phase_limiter"; +#endif + { + std::ofstream out(fakeExe); + out << "binary stub\n"; + } + { + std::ofstream out(fakeResourceDir / "mastering_reference.json"); + out << "{}\n"; + } +#if !defined(_WIN32) + std::filesystem::permissions(fakeExe, std::filesystem::perms::owner_exec | std::filesystem::perms::owner_read, std::filesystem::perm_options::add); +#endif + + setEnvValue("PHASELIMITER_BIN", fakeExe.string()); + automix::renderers::setFfmpegResolverForTesting([]() -> std::optional { + return std::nullopt; + }); + + struct CleanupGuard { + std::filesystem::path dir; + ~CleanupGuard() { + automix::renderers::resetFfmpegResolverForTesting(); + setEnvValue("PHASELIMITER_BIN", ""); + std::filesystem::remove_all(dir); + } + } cleanup{tempDir}; + + automix::util::WavWriter writer; + const auto stemA = makeTone(44100.0, 22050, 220.0, 0.40); + const auto stemPathA = tempDir / "tone.wav"; + writer.write(stemPathA, stemA, 24); + + automix::domain::Session session; + automix::domain::Stem s1; + s1.id = "s1"; + s1.name = "Tone"; + s1.filePath = stemPathA.string(); + session.stems.push_back(s1); + + automix::domain::RenderSettings settings; + settings.outputSampleRate = 44100; + settings.blockSize = 1024; + settings.outputBitDepth = 24; + settings.rendererName = "PhaseLimiter"; + settings.outputPath = (tempDir / "out.wav").string(); + + automix::renderers::PhaseLimiterRenderer renderer; + REQUIRE(renderer.isAvailable()); + const auto result = renderer.render(session, settings, {}, nullptr); + + REQUIRE(result.success == false); + REQUIRE(result.rendererName == "PhaseLimiter"); + const std::string expectedMessage = "PhaseLimiter needs ffmpeg, which was not found. Install ffmpeg or set FFMPEG_BIN."; + REQUIRE_FALSE(result.logs.empty()); + REQUIRE(result.logs.back() == expectedMessage); +} + +TEST_CASE("ffmpegForPhaseLimiter honours FFMPEG_BIN first", "[phaselimiter][discovery]") { + const std::filesystem::path tempDir = std::filesystem::temp_directory_path() / "automix_test_ffmpeg_bin"; + std::filesystem::remove_all(tempDir); + std::filesystem::create_directories(tempDir); + +#if defined(_WIN32) + const auto fakeFfmpeg = tempDir / "fake_ffmpeg.exe"; +#else + const auto fakeFfmpeg = tempDir / "fake_ffmpeg"; +#endif + { + std::ofstream out(fakeFfmpeg); + out << "stub\n"; + } +#if !defined(_WIN32) + std::filesystem::permissions(fakeFfmpeg, std::filesystem::perms::owner_exec | std::filesystem::perms::owner_read, std::filesystem::perm_options::add); +#endif + + automix::renderers::resetFfmpegResolverForTesting(); + setEnvValue("FFMPEG_BIN", fakeFfmpeg.string()); + + struct CleanupGuard { + std::filesystem::path dir; + ~CleanupGuard() { + setEnvValue("FFMPEG_BIN", ""); + std::filesystem::remove_all(dir); + } + } cleanup{tempDir}; + + const auto found = automix::renderers::ffmpegForPhaseLimiter(); + REQUIRE(found.has_value()); + REQUIRE(std::filesystem::equivalent(*found, fakeFfmpeg)); +} + +TEST_CASE("PhaseLimiter platform resolution maps windows-arm64 to windows-x64 emulation", "[phaselimiter][discovery]") { + REQUIRE(automix::renderers::phaseLimiterPlatformKeyForResolution("windows-arm64") == "windows-x64"); + REQUIRE(automix::renderers::phaseLimiterPlatformKeyForResolution("windows-x64") == "windows-x64"); + REQUIRE(automix::renderers::phaseLimiterPlatformKeyForResolution("linux-arm64") == "linux-arm64"); + REQUIRE(automix::renderers::phaseLimiterPlatformKeyForResolution("macos-arm64") == "macos-arm64"); +} + +TEST_CASE("PhaseLimiter installation contains third-party licenses when installed", "[phaselimiter][licenses]") { + automix::renderers::PhaseLimiterDiscovery discovery; + const auto info = discovery.find(); + if (!info.has_value()) { + SUCCEED("PhaseLimiter binary is not installed on this machine; skipping license check."); + return; + } + + const auto licensesDir = automix::renderers::licensesDirectoryPath(*info); + if (!std::filesystem::is_directory(licensesDir)) { + SUCCEED("Installed PhaseLimiter distribution does not bundle licenses/."); + return; + } + + // The portable v0.2.0-native3 distribution bundles 14 licenses including onetbb.txt & hnswlib.txt & pocketfft.txt. + // The legacy upstream Windows distribution bundles tbb.txt & hnsw.txt. + const bool isNativePortable = std::filesystem::is_regular_file(licensesDir / "onetbb.txt"); + const std::vector requiredLicenses = isNativePortable ? std::vector{ + "armadillo.txt", "boost.txt", "cimg.txt", "eigen.txt", "gflags.txt", + "hnswlib.txt", "libpng.txt", "libsimdpp.txt", "onetbb.txt", "optim.txt", + "phaselimiter.txt", "picojson.txt", "pocketfft.txt", "zlib.txt" + } : std::vector{ + "armadillo.txt", "boost.txt", "cimg.txt", "eigen.txt", "gflags.txt", + "hnsw.txt", "libpng.txt", "libsimdpp.txt", "tbb.txt", "optim.txt", + "phaselimiter.txt", "picojson.txt", "zlib.txt" + }; + + for (const auto& licFile : requiredLicenses) { + const auto filePath = licensesDir / licFile; + INFO("Checking license file: " << filePath.string()); + REQUIRE(std::filesystem::is_regular_file(filePath)); + REQUIRE(std::filesystem::file_size(filePath) > 0); + } +} \ No newline at end of file diff --git a/tests/unit/ProjectProfileTests.cpp b/tests/unit/ProjectProfileTests.cpp index 7bfb3df..f0a96a9 100644 --- a/tests/unit/ProjectProfileTests.cpp +++ b/tests/unit/ProjectProfileTests.cpp @@ -13,7 +13,7 @@ TEST_CASE("Project profile defaults are available", "[profile]") { const auto foundDefault = automix::domain::findProjectProfile(defaults, "default"); REQUIRE(foundDefault.has_value()); - REQUIRE(foundDefault->rendererName == "PhaseLimiter"); + REQUIRE(foundDefault->rendererName == "BuiltIn"); // PhaseLimiter is opt-in } TEST_CASE("Project profile loader merges asset profiles with defaults", "[profile]") { diff --git a/tests/unit/RendererPipelineTests.cpp b/tests/unit/RendererPipelineTests.cpp index 771696f..b157770 100644 --- a/tests/unit/RendererPipelineTests.cpp +++ b/tests/unit/RendererPipelineTests.cpp @@ -48,3 +48,31 @@ TEST_CASE("Renderer pipeline uses explicit chain list when provided", "[renderer REQUIRE(chain[0] == "BuiltIn"); REQUIRE(chain[1] == "rsgain"); } + +TEST_CASE("PhaseLimiter is opt-in: default renders and logical_all leave it out", "[renderer][pipeline]") { + automix::domain::RenderSettings defaults; + const auto single = automix::renderers::resolveRendererChain(defaults); + REQUIRE(single == std::vector{"BuiltIn"}); + + automix::domain::RenderSettings logicalAll; + logicalAll.rendererChainEnabled = true; + logicalAll.rendererChainMode = "logical_all"; + const auto chain = automix::renderers::resolveRendererChain(logicalAll); + REQUIRE_FALSE(chain.empty()); + REQUIRE(chain.front() == "BuiltIn"); + REQUIRE(std::find(chain.begin(), chain.end(), "PhaseLimiter") == chain.end()); +} + +TEST_CASE("Selecting PhaseLimiter opts it into the logical_all chain", "[renderer][pipeline]") { + automix::domain::RenderSettings settings; + settings.rendererName = "PhaseLimiter"; + settings.rendererChainEnabled = true; + settings.rendererChainMode = "logical_all"; + const auto chain = automix::renderers::resolveRendererChain(settings); + REQUIRE_FALSE(chain.empty()); + if (std::find(chain.begin(), chain.end(), "PhaseLimiter") != chain.end()) { + REQUIRE(chain.front() == "PhaseLimiter"); // available here: it leads, once + REQUIRE(std::count(chain.begin(), chain.end(), "PhaseLimiter") == 1); + } + REQUIRE(std::find(chain.begin(), chain.end(), "BuiltIn") != chain.end()); +} \ No newline at end of file diff --git a/tests/unit/SessionSerializationTests.cpp b/tests/unit/SessionSerializationTests.cpp index 7a2af53..813f2d7 100644 --- a/tests/unit/SessionSerializationTests.cpp +++ b/tests/unit/SessionSerializationTests.cpp @@ -3,12 +3,13 @@ #include #include +#include "app/ui/SessionManager.h" #include "domain/JsonSerialization.h" #include "engine/SessionRepository.h" TEST_CASE("Session serialization round trip preserves required fields", "[session]") { automix::domain::Session session; - session.schemaVersion = 2; + session.schemaVersion = 3; session.sessionName = "round_trip"; session.originalMixPath = "C:/audio/original_mix.wav"; session.residualBlend = 7.5; @@ -54,7 +55,7 @@ TEST_CASE("Session serialization round trip preserves required fields", "[sessio const automix::domain::Json json = session; const auto decoded = json.get(); - REQUIRE(decoded.schemaVersion == 2); + REQUIRE(decoded.schemaVersion == 3); REQUIRE(decoded.sessionName == "round_trip"); REQUIRE(decoded.originalMixPath.has_value()); REQUIRE(decoded.originalMixPath.value() == "C:/audio/original_mix.wav"); @@ -161,3 +162,53 @@ TEST_CASE("Render settings normalize unsupported renderer chain modes", "[sessio REQUIRE(decoded.renderSettings.rendererChainEnabled == true); REQUIRE(decoded.renderSettings.rendererChainMode == "logical_all"); } + +TEST_CASE("Sessions from before PhaseLimiter became opt-in load with BuiltIn", "[session][serialization]") { + // Schema 2 wrote rendererName "PhaseLimiter" as the default, and every such + // render fell back to BuiltIn, so BuiltIn reproduces what those sessions got. + const nlohmann::json legacy = {{"schemaVersion", 2}, {"renderSettings", {{"rendererName", "PhaseLimiter"}}}}; + const auto migrated = legacy.get(); + REQUIRE(migrated.renderSettings.rendererName == "BuiltIn"); + REQUIRE(migrated.schemaVersion == 3); + + const nlohmann::json legacyOther = {{"schemaVersion", 2}, {"renderSettings", {{"rendererName", "SoX"}}}}; + REQUIRE(legacyOther.get().renderSettings.rendererName == "SoX"); + + // From schema 3 on, PhaseLimiter in a session is an explicit choice and stays. + const nlohmann::json chosen = {{"schemaVersion", 3}, {"renderSettings", {{"rendererName", "PhaseLimiter"}}}}; + REQUIRE(chosen.get().renderSettings.rendererName == "PhaseLimiter"); + + REQUIRE(automix::domain::Session{}.schemaVersion == 3); + REQUIRE(automix::domain::RenderSettings{}.rendererName == "BuiltIn"); +} +TEST_CASE("Default session uses the Default Streaming master preset", "[session]") { + const automix::domain::Session session; + REQUIRE(session.selectedMasterPreset == automix::domain::MasterPreset::DefaultStreaming); +} + +TEST_CASE("Old session without selectedMasterPreset keeps the Udio Optimized fallback", "[session]") { + const nlohmann::json legacy = {{"schemaVersion", 1}, {"sessionName", "legacy"}}; + REQUIRE(legacy.get().selectedMasterPreset == + automix::domain::MasterPreset::UdioOptimized); +} + +TEST_CASE("Explicit default_streaming master preset round-trips", "[session]") { + automix::domain::Session session; + REQUIRE(session.selectedMasterPreset == automix::domain::MasterPreset::DefaultStreaming); + const nlohmann::json encoded = session; + REQUIRE(encoded.at("selectedMasterPreset").get() == "default_streaming"); + REQUIRE(encoded.get().selectedMasterPreset == + automix::domain::MasterPreset::DefaultStreaming); +} + +TEST_CASE("SessionManager tracks unsaved changes", "[session]") { + automix::app::SessionManager manager; + manager.markSaved(); + REQUIRE_FALSE(manager.isModified()); + + manager.session().selectedMasterPreset = automix::domain::MasterPreset::Broadcast; + REQUIRE(manager.isModified()); + + manager.markSaved(); + REQUIRE_FALSE(manager.isModified()); +} diff --git a/tests/unit/TaskCenterPanelTests.cpp b/tests/unit/TaskCenterPanelTests.cpp index 0202658..6a2164f 100644 --- a/tests/unit/TaskCenterPanelTests.cpp +++ b/tests/unit/TaskCenterPanelTests.cpp @@ -80,7 +80,7 @@ TEST_CASE("TaskCenterPanel batch ETA row state transitions", "[ui][taskcenter][e juce::ScopedJuceInitialiser_GUI juceInit; automix::app::TaskCenterPanel panel; - const juce::String emDash(static_cast(0x2014)); + const auto emDash = juce::String::charToString(static_cast(0x2014)); SECTION("no batch items shows dash") { REQUIRE(panel.batchEtaText() == emDash); @@ -232,3 +232,52 @@ TEST_CASE("confirmClear gates destructive clear on dialog result and non-empty s REQUIRE_FALSE(confirmClear(0, false)); } } + +TEST_CASE("TaskCenterPanel shows the batch strip only for a batch", "[ui][taskcenter][batch]") { + juce::ScopedJuceInitialiser_GUI juceInit; + + automix::app::TaskCenterPanel panel; + panel.setSize(900, 400); + REQUIRE_FALSE(panel.isBatchStripVisible()); + const int idleHeight = panel.getPreferredHeight(); + + automix::engine::BatchQueueRunner::ProgressDetail detail; + detail.totalCount = 3; + panel.setBatchProgress(detail, 0.0); + REQUIRE(panel.isBatchStripVisible()); + REQUIRE(panel.getPreferredHeight() > idleHeight); + + automix::engine::BatchQueueRunner::ProgressDetail cleared; + panel.setBatchProgress(cleared, 0.0); + REQUIRE_FALSE(panel.isBatchStripVisible()); +} + +TEST_CASE("TaskCenterPanel log is hidden by default and shown on failure", "[ui][taskcenter][log]") { + juce::ScopedJuceInitialiser_GUI juceInit; + + automix::app::TaskCenterPanel panel; + panel.setSize(900, 400); + auto* editor = findHistoryEditor(panel); + REQUIRE(editor != nullptr); + REQUIRE_FALSE(panel.isLogVisible()); + REQUIRE_FALSE(editor->isVisible()); + const int collapsedHeight = panel.getPreferredHeight(); + + panel.setTaskState(automix::app::TaskState::Failed); + REQUIRE(panel.isLogVisible()); + REQUIRE(editor->isVisible()); + REQUIRE(panel.getPreferredHeight() == collapsedHeight + 4 + 160); +} + +TEST_CASE("TaskCenterPanel hides the percent label while idle", "[ui][taskcenter]") { + juce::ScopedJuceInitialiser_GUI juceInit; + + automix::app::TaskCenterPanel panel; + REQUIRE_FALSE(panel.isProgressPercentVisible()); + + panel.setTaskState(automix::app::TaskState::Running); + REQUIRE(panel.isProgressPercentVisible()); + + panel.setTaskState(automix::app::TaskState::Idle); + REQUIRE_FALSE(panel.isProgressPercentVisible()); +} diff --git a/tests/unit/TensorInferenceTests.cpp b/tests/unit/TensorInferenceTests.cpp new file mode 100644 index 0000000..671bc9c --- /dev/null +++ b/tests/unit/TensorInferenceTests.cpp @@ -0,0 +1,1828 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "ai/BsRoformerPack.h" +#include "ai/GpuMemory.h" +#include "ai/GpuRuntimePack.h" +#include "ai/ITensorInference.h" +#include "ai/ModelCatalogValidator.h" +#include "ai/ModelLicensePolicy.h" +#include "ai/ModelPackLoader.h" +#include "ai/ModelStorage.h" +#include "ai/OnnxExternalData.h" +#include "ai/OnnxTensorInference.h" +#include "ai/SeparationRunner.h" +#include "ai/StemSeparator.h" +#include "ai/TensorTypes.h" +#include "ai/UmxPack.h" +#include "analysis/SpectrogramFrontEnd.h" +#include "domain/JsonSerialization.h" +#include "domain/RenderSettings.h" +#include "engine/AudioBuffer.h" +#include "engine/AudioFileIO.h" +#include "util/WavWriter.h" +#include "TorchStftGolden.h" + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace ai = automix::ai; +namespace analysis = automix::analysis; +namespace engine = automix::engine; + +namespace { + +// BS-RoFormer's STFT (spec 5.4.1). +analysis::StftParams roformerStft() { + analysis::StftParams params; + params.nFft = 2048; + params.hopLength = 441; + params.winLength = 2048; + params.center = true; + params.normalized = false; + params.zeroDc = true; + return params; +} + +engine::AudioBuffer makeTestSignal(const int channels, const int samples, const double sampleRate) { + engine::AudioBuffer buffer(channels, samples, sampleRate); + for (int ch = 0; ch < channels; ++ch) { + for (int i = 0; i < samples; ++i) { + const double t = static_cast(i) / sampleRate; + const double value = 0.4 * std::sin(2.0 * 3.14159265358979323846 * (220.0 + 110.0 * ch) * t) + + 0.2 * std::sin(2.0 * 3.14159265358979323846 * 3130.0 * t + 0.3 * ch) + + 0.05 * std::sin(0.37 * static_cast(i * (ch + 3))); + buffer.setSample(ch, i, static_cast(value)); + } + } + return buffer; +} + +float maxAbsDifference(const engine::AudioBuffer& a, const engine::AudioBuffer& b) { + float worst = 0.0f; + for (int ch = 0; ch < a.getNumChannels(); ++ch) { + for (int i = 0; i < a.getNumSamples(); ++i) { + worst = std::max(worst, std::abs(a.getSample(ch, i) - b.getSample(ch, i))); + } + } + return worst; +} + +// Records the bindings it is handed and answers with a scripted output. This is +// the stand-in for a real graph that lets the runner be proven with no ORT SDK. +class FakeTensorInference final : public ai::ITensorInference { + public: + using Script = std::function&)>; + + FakeTensorInference(std::vector inputs, std::vector outputs, Script script) + : inputs_(std::move(inputs)), outputs_(std::move(outputs)), script_(std::move(script)) {} + + bool isAvailable() const override { return true; } + bool loadModel(const std::filesystem::path&) override { return true; } + std::vector inputSpecs() const override { return inputs_; } + std::vector outputSpecs() const override { return outputs_; } + ai::TensorInferenceResult run(const std::vector& inputs) const override { + ++calls; + lastBindings = inputs; + return script_(inputs); + } + + mutable int calls = 0; + mutable std::vector lastBindings; + + private: + std::vector inputs_; + std::vector outputs_; + Script script_; +}; + +const ai::TensorSpec kRoformerInput{"mix_spec", ai::TensorElementType::Float32, {1, 801, 4100}}; +const ai::TensorSpec kRoformerOutput{"vocals_mask", ai::TensorElementType::Float32, {1, 1, 2050, 801, 2}}; + +// A mask of 1 + 0i: the masked spectrum is the input spectrum. +ai::TensorInferenceResult identityMask(const std::vector&) { + ai::TensorInferenceResult result; + result.usedModel = true; + ai::Tensor mask; + mask.spec = kRoformerOutput; + mask.data.assign(1 * 1 * 2050 * 801 * 2, 0.0f); + for (std::size_t i = 0; i < mask.data.size(); i += 2) { + mask.data[i] = 1.0f; + } + result.outputs.push_back(std::move(mask)); + return result; +} + +ai::RunnerConfig roformerRunnerConfig() { + ai::RunnerConfig config; + config.stft = roformerStft(); + config.inputLayout = ai::InputLayout::FoldedStereo; + config.outputMode = ai::OutputMode::Mask; + config.targetStem = "vocals"; + config.stems = {{"vocals", ""}, {"instrumental", "vocals"}}; + return config; +} + +// Spec section 6, installed form (names present). +nlohmann::json roformerContractJson() { + return nlohmann::json::parse(R"({ + "engine": "bs_roformer", + "sample_rate": 44100, + "stereo": true, + "chunk_samples": 352800, + "overlap_samples": 88200, + "stft": { "n_fft": 2048, "hop_length": 441, "win_length": 2048, + "window": "hann", "periodic_window": true, + "center": true, "pad_mode": "reflect", "normalized": false, "zero_dc": true }, + "input_layout": "folded_stereo", + "inputs": [ { "name": "input", "shape": [1, 801, 4100], "dtype": "float32" } ], + "outputs": [ { "name": "output", "shape": [1, 1, 2050, 801, 2], "dtype": "float32" } ], + "output_mode": "mask", + "target_stem": "vocals", + "stems": [ { "name": "vocals" }, { "name": "instrumental", "residual_of": "vocals" } ] + })"); +} + +ai::TensorContract parseContract(const nlohmann::json& json) { + std::string error; + const auto contract = ai::parseTensorContract(json, error); + REQUIRE(contract.has_value()); + REQUIRE(error.empty()); + return *contract; +} + +const std::vector kProbedInputs{{"input", ai::TensorElementType::Float32, {1, 801, 4100}}}; +const std::vector kProbedOutputs{{"output", ai::TensorElementType::Float32, {1, 1, 2050, 801, 2}}}; + +std::string contractError(const nlohmann::json& json, + const std::vector& inputs = kProbedInputs, + const std::vector& outputs = kProbedOutputs) { + const auto contract = parseContract(json); + std::string error; + const bool ok = ai::checkTensorContract(contract, inputs, outputs, error); + REQUIRE_FALSE(ok); + REQUIRE_FALSE(error.empty()); + return error; +} + +} // namespace + +TEST_CASE("Tensor contract validation table", "[ai][tensor]") { + const ai::TensorSpec dynamicBatch{"input", ai::TensorElementType::Float32, {-1, 801, 4100}}; + const ai::TensorSpec probed{"input", ai::TensorElementType::Float32, {1, 801, 4100}}; + const ai::TensorSpec probedBatch4{"input", ai::TensorElementType::Float32, {4, 801, 4100}}; + REQUIRE(ai::shapesMatch(dynamicBatch, probed)); + REQUIRE(ai::shapesMatch(dynamicBatch, probedBatch4)); + + const ai::TensorSpec wrongRank{"input", ai::TensorElementType::Float32, {1, 2, 1025, 801}}; + REQUIRE_FALSE(ai::shapesMatch(dynamicBatch, wrongRank)); + REQUIRE_FALSE(ai::shapesMatch(probed, wrongRank)); + + const ai::TensorSpec wrongStaticDim{"input", ai::TensorElementType::Float32, {1, 796, 4100}}; + REQUIRE_FALSE(ai::shapesMatch(probed, wrongStaticDim)); + REQUIRE_FALSE(ai::shapesMatch(dynamicBatch, wrongStaticDim)); + + // An expected dynamic axis matches anything, but a dynamic probed axis does not + // satisfy a static expectation: the graph promised less than was declared. + const ai::TensorSpec probedDynamic{"input", ai::TensorElementType::Float32, {1, -1, 4100}}; + REQUIRE_FALSE(ai::shapesMatch(probed, probedDynamic)); + + REQUIRE(ai::elementCount(probed) == std::optional(3284100)); + REQUIRE(ai::elementCount(dynamicBatch) == std::optional(3284100)); + const ai::TensorSpec twoDynamic{"input", ai::TensorElementType::Float32, {-1, -1, 4100}}; + REQUIRE_FALSE(ai::elementCount(twoDynamic).has_value()); + const ai::TensorSpec output{"output", ai::TensorElementType::Float32, {1, 1, 2050, 801, 2}}; + REQUIRE(ai::elementCount(output) == std::optional(3284100)); + REQUIRE(ai::elementCount(ai::TensorSpec{"scalar", ai::TensorElementType::Float32, {}}) == + std::optional(1)); + + REQUIRE(ai::describeShape({1, 801, 4100}) == "[1, 801, 4100]"); + REQUIRE(ai::describeShape({-1, 2}) == "[-1, 2]"); + REQUIRE(ai::describeShape({}) == "[]"); + REQUIRE(ai::describeDtype(ai::TensorElementType::Float32) == "float32"); +} + +TEST_CASE("Null tensor inference reports unavailable", "[ai][tensor]") { + ai::NullTensorInference null; + REQUIRE(!null.isAvailable()); + REQUIRE(!null.loadModel("any.onnx")); + REQUIRE(!null.lastLog().empty()); + REQUIRE(null.inputSpecs().empty()); + REQUIRE(null.outputSpecs().empty()); + const auto result = null.run({}); + REQUIRE(result.usedModel == false); + REQUIRE(result.outputs.empty()); + REQUIRE(result.logMessage.find("NullTensorInference") != std::string::npos); +} + +TEST_CASE("STFT round-trip identity with exact length", "[ai][tensor][stft]") { + const auto params = roformerStft(); + const auto input = makeTestSignal(2, 352800, 44100.0); + const auto spectrum = analysis::analyze(input, params); + REQUIRE(spectrum.channels == 2); + REQUIRE(spectrum.freqBins == 1025); + REQUIRE(spectrum.frames == 801); + + const auto synth = analysis::synthesize(spectrum, params); + // Equality, not tolerance: any drift here accumulates at every chunk boundary. + REQUIRE(synth.getNumSamples() == 441 * (801 - 1)); + REQUIRE(synth.getNumSamples() == input.getNumSamples()); + REQUIRE(synth.getNumChannels() == 2); + REQUIRE(maxAbsDifference(input, synth) < 1.0e-5f); +} + +TEST_CASE("Analytic DC check", "[ai][tensor][stft]") { + // Premise: reflect-padding a constant yields a constant, so every frame of a + // constant signal is constant and bin 0 equals sum(window) exactly. Switching + // the pad mode (e.g. to zero padding) invalidates this expected value and must + // be a deliberate, visible change. + const auto params = roformerStft(); + engine::AudioBuffer ones(2, 44100, 44100.0); + for (int ch = 0; ch < 2; ++ch) { + for (int i = 0; i < ones.getNumSamples(); ++i) { + ones.setSample(ch, i, 1.0f); + } + } + const auto spectrum = analysis::analyze(ones, params); + // Periodic Hann of length 2048 sums to exactly 1024. + constexpr double windowSum = 1024.0; + double worstDc = 0.0; + double worstDcImag = 0.0; + for (int ch = 0; ch < spectrum.channels; ++ch) { + for (int frame = 0; frame < spectrum.frames; ++frame) { + const auto index = spectrum.index(ch, 0, frame); + worstDc = std::max(worstDc, std::abs(static_cast(spectrum.real[index]) - windowSum)); + worstDcImag = std::max(worstDcImag, std::abs(static_cast(spectrum.imag[index]))); + } + } + REQUIRE(worstDc < 1.0e-3); + REQUIRE(worstDcImag < 1.0e-3); +} + +TEST_CASE("Complex multiply rejects a mask whose geometry differs", "[ai][tensor][stft]") { + const auto params = roformerStft(); + const auto spectrum = analysis::analyze(makeTestSignal(2, 8820, 44100.0), params); + auto shortMask = spectrum; + shortMask.frames -= 1; + bool threw = false; + try { + static_cast(analysis::complexMultiply(spectrum, shortMask, true)); + } catch (const std::invalid_argument& error) { + threw = true; + REQUIRE(std::string(error.what()).find("does not match") != std::string::npos); + } + REQUIRE(threw); + + // zeroDc clears bin 0 of the product and nothing else. + auto unitMask = spectrum; + std::fill(unitMask.real.begin(), unitMask.real.end(), 1.0f); + std::fill(unitMask.imag.begin(), unitMask.imag.end(), 0.0f); + const auto product = analysis::complexMultiply(spectrum, unitMask, true); + REQUIRE(product.real[product.index(1, 0, 3)] == 0.0f); + REQUIRE(product.real[product.index(1, 7, 3)] == spectrum.real[spectrum.index(1, 7, 3)]); + REQUIRE(product.imag[product.index(1, 7, 3)] == spectrum.imag[spectrum.index(1, 7, 3)]); +} + +TEST_CASE("Runner binds by declared name", "[ai][tensor][runner]") { + FakeTensorInference fake({kRoformerInput}, {kRoformerOutput}, identityMask); + const auto mix = makeTestSignal(2, 352800, 44100.0); + const auto result = ai::SeparationRunner::separate(mix, fake, roformerRunnerConfig()); + REQUIRE(result.usedModel); + REQUIRE(fake.calls == 1); + REQUIRE(fake.lastBindings.size() == 1); + const auto& binding = fake.lastBindings.front(); + REQUIRE(binding.expected.name == "mix_spec"); + REQUIRE(binding.expected.elementType == ai::TensorElementType::Float32); + REQUIRE(binding.expected.dims == std::vector{1, 801, 4100}); + REQUIRE(binding.data.size() == std::size_t{801} * 4100); + + // Folded-stereo placement: element (t, (f * channels + ch) * 2 + reim). + const auto spectrum = analysis::analyze(mix, roformerStft()); + for (const int frame : {0, 17, 800}) { + for (const int bin : {0, 5, 1024}) { + for (const int ch : {0, 1}) { + const auto base = static_cast(frame) * 4100 + (static_cast(bin) * 2 + static_cast(ch)) * 2; + REQUIRE(binding.data[base] == spectrum.real[spectrum.index(ch, bin, frame)]); + REQUIRE(binding.data[base + 1] == spectrum.imag[spectrum.index(ch, bin, frame)]); + } + } + } + + REQUIRE(result.stemNames == std::vector{"vocals", "instrumental"}); + REQUIRE(result.stemAudio.size() == 2); + REQUIRE(result.stemAudio[0].getNumSamples() == mix.getNumSamples()); +} + +TEST_CASE("Wrong-size output falls back honestly", "[ai][tensor][runner]") { + const auto mix = makeTestSignal(2, 352800, 44100.0); + + FakeTensorInference wrongFrames({kRoformerInput}, {kRoformerOutput}, [](const auto&) { + ai::TensorInferenceResult result; + result.usedModel = true; + ai::Tensor mask; + mask.spec = {"vocals_mask", ai::TensorElementType::Float32, {1, 1, 2050, 796, 2}}; + mask.data.assign(2050 * 796 * 2, 1.0f); + result.outputs.push_back(std::move(mask)); + return result; + }); + const auto wrongShape = ai::SeparationRunner::separate(mix, wrongFrames, roformerRunnerConfig()); + REQUIRE_FALSE(wrongShape.usedModel); + REQUIRE(wrongShape.stemAudio.empty()); + REQUIRE(wrongShape.logMessage.find("vocals_mask") != std::string::npos); + REQUIRE(wrongShape.logMessage.find("[1, 1, 2050, 796, 2]") != std::string::npos); + REQUIRE(wrongShape.logMessage.find("[1, 1, 2050, 801, 2]") != std::string::npos); + + FakeTensorInference wrongName({kRoformerInput}, {kRoformerOutput}, [](const auto& bindings) { + auto result = identityMask(bindings); + result.outputs.front().spec.name = "out_spec_real"; + return result; + }); + const auto misnamed = ai::SeparationRunner::separate(mix, wrongName, roformerRunnerConfig()); + REQUIRE_FALSE(misnamed.usedModel); + REQUIRE(misnamed.logMessage.find("vocals_mask") != std::string::npos); + + // All-or-nothing: a failure in a later chunk discards the chunks that worked. + const auto longMix = makeTestSignal(2, 352800 * 2, 44100.0); + int progressCalls = 0; + auto config = roformerRunnerConfig(); + config.progressCallback = [&](int, int) { ++progressCalls; }; + int call = 0; + FakeTensorInference failsOnSecondChunk({kRoformerInput}, {kRoformerOutput}, [&](const auto& bindings) { + ++call; + if (call == 2) { + ai::TensorInferenceResult failed; + failed.logMessage = "scripted failure on chunk 2"; + return failed; + } + return identityMask(bindings); + }); + const auto partial = ai::SeparationRunner::separate(longMix, failsOnSecondChunk, config); + REQUIRE_FALSE(partial.usedModel); + REQUIRE(partial.stemAudio.empty()); + REQUIRE(partial.logMessage.find("chunk 2/3") != std::string::npos); + REQUIRE(progressCalls == 1); +} + +TEST_CASE("Window-squared normalization under chunking", "[ai][tensor][runner]") { + // Three chunks (stride 264600): the constant must survive every chunk + // boundary and crossfade. zero_dc is off here because it would, correctly, + // remove a constant. + constexpr int samples = 352800 + 2 * 264600 - 1000; + engine::AudioBuffer constant(2, samples, 44100.0); + for (int ch = 0; ch < 2; ++ch) { + for (int i = 0; i < samples; ++i) { + constant.setSample(ch, i, 0.5f); + } + } + auto config = roformerRunnerConfig(); + config.stft.zeroDc = false; + std::vector> progress; + config.progressCallback = [&](int done, int total) { progress.emplace_back(done, total); }; + FakeTensorInference fake({kRoformerInput}, {kRoformerOutput}, identityMask); + + REQUIRE(ai::SeparationRunner::chunkCount(samples, config) == 3); + const auto result = ai::SeparationRunner::separate(constant, fake, config); + REQUIRE(result.usedModel); + REQUIRE(fake.calls == 3); + REQUIRE(progress == std::vector>{{1, 3}, {2, 3}, {3, 3}}); + REQUIRE(result.stemAudio[0].getNumSamples() == samples); + REQUIRE(maxAbsDifference(result.stemAudio[0], constant) < 1.0e-5f); + + engine::AudioBuffer silence(2, samples, 44100.0); + REQUIRE(maxAbsDifference(result.stemAudio[1], silence) < 1.0e-5f); + + // A non-constant signal also reconstructs through the same path, and the + // residual stem is exactly mix - target. + const auto music = makeTestSignal(2, samples, 44100.0); + const auto separated = ai::SeparationRunner::separate(music, fake, config); + REQUIRE(separated.usedModel); + REQUIRE(maxAbsDifference(separated.stemAudio[0], music) < 1.0e-4f); + float worstSum = 0.0f; + for (int ch = 0; ch < 2; ++ch) { + for (int i = 0; i < samples; ++i) { + const float sum = separated.stemAudio[0].getSample(ch, i) + separated.stemAudio[1].getSample(ch, i); + worstSum = std::max(worstSum, std::abs(sum - music.getSample(ch, i))); + } + } + REQUIRE(worstSum < 1.0e-5f); +} + +TEST_CASE("Runner refuses input the contract does not describe", "[ai][tensor][runner]") { + FakeTensorInference fake({kRoformerInput}, {kRoformerOutput}, identityMask); + + const auto at48k = makeTestSignal(2, 48000, 48000.0); + const auto wrongRate = ai::SeparationRunner::separate(at48k, fake, roformerRunnerConfig()); + REQUIRE_FALSE(wrongRate.usedModel); + REQUIRE(wrongRate.logMessage.find("44100") != std::string::npos); + REQUIRE(wrongRate.logMessage.find("48000") != std::string::npos); + + const auto mono = makeTestSignal(1, 44100, 44100.0); + const auto monoResult = ai::SeparationRunner::separate(mono, fake, roformerRunnerConfig()); + REQUIRE_FALSE(monoResult.usedModel); + REQUIRE(monoResult.logMessage.find("1 channel") != std::string::npos); + REQUIRE(fake.calls == 0); + + // A track shorter than one chunk is one zero-padded partial chunk; with no + // neighbour the crossfade is the identity. + const auto shortMix = makeTestSignal(2, 100000, 44100.0); + const auto shortResult = ai::SeparationRunner::separate(shortMix, fake, roformerRunnerConfig()); + REQUIRE(shortResult.usedModel); + REQUIRE(fake.calls == 1); + REQUIRE(shortResult.stemAudio[0].getNumSamples() == 100000); + auto noDc = roformerRunnerConfig(); + noDc.stft.zeroDc = false; + const auto shortIdentity = ai::SeparationRunner::separate(shortMix, fake, noDc); + REQUIRE(maxAbsDifference(shortIdentity.stemAudio[0], shortMix) < 1.0e-4f); +} + +TEST_CASE("Golden torch.stft fixture", "[ai][tensor][stft]") { + // Catches a consistent-but-wrong convention (window periodicity, pad mode, + // sign of the exponent, bin order) that the round-trip test cannot, because + // forward and inverse would share the misreading. See TorchStftGolden.h for + // the generating expression. + analysis::StftParams params; + params.nFft = 256; + params.hopLength = 64; + params.winLength = 256; + params.center = true; + params.normalized = false; + params.zeroDc = false; + + engine::AudioBuffer ramp(1, 4096, 44100.0); + for (int i = 0; i < 4096; ++i) { + ramp.setSample(0, i, static_cast(i) / 4096.0f); + } + const auto spectrum = analysis::analyze(ramp, params); + REQUIRE(spectrum.freqBins == torch_stft_golden::kFreqBins); + REQUIRE(spectrum.frames == torch_stft_golden::kFrames); + + const auto toFloat = [](const std::uint32_t bits) { + float value = 0.0f; + std::memcpy(&value, &bits, sizeof(value)); + return value; + }; + double worst = 0.0; + for (int bin = 0; bin < spectrum.freqBins; ++bin) { + for (int frame = 0; frame < spectrum.frames; ++frame) { + const auto golden = static_cast(bin * spectrum.frames + frame); + const auto index = spectrum.index(0, bin, frame); + worst = std::max(worst, std::abs(static_cast(spectrum.real[index]) - toFloat(torch_stft_golden::kReal[golden]))); + worst = std::max(worst, std::abs(static_cast(spectrum.imag[index]) - toFloat(torch_stft_golden::kImag[golden]))); + } + } + // Peak magnitude here is ~128 (bin 0, sum of a 256-tap Hann); 1e-5 absolute + // is float32 rounding at that scale, far below any convention error. + REQUIRE(worst < 1.0e-5); +} + +TEST_CASE("Tensor contract load-time failures", "[ai][tensor][contract]") { + std::string error; + REQUIRE(ai::checkTensorContract(parseContract(roformerContractJson()), kProbedInputs, kProbedOutputs, error)); + // A graph with a dynamic batch axis accepts the declared static shape. + REQUIRE(ai::checkTensorContract(parseContract(roformerContractJson()), + {{"input", ai::TensorElementType::Float32, {-1, 801, 4100}}}, kProbedOutputs, error)); + + auto wrongFft = roformerContractJson(); + wrongFft["stft"]["n_fft"] = 1024; + wrongFft["stft"]["win_length"] = 1024; + auto message = contractError(wrongFft); + REQUIRE(message.find("input 'input'") != std::string::npos); + REQUIRE(message.find("[1, 801, 4100]") != std::string::npos); + REQUIRE(message.find("[1, 801, 2052]") != std::string::npos); + + message = contractError(roformerContractJson(), {{"input", ai::TensorElementType::Float32, {1, 796, 4100}}}); + REQUIRE(message.find("'input'") != std::string::npos); + REQUIRE(message.find("[1, 796, 4100]") != std::string::npos); + REQUIRE(message.find("[1, 801, 4100]") != std::string::npos); + + message = contractError(roformerContractJson(), kProbedInputs, + {{"output", ai::TensorElementType::Float32, {1, 2, 2050, 801, 2}}}); + REQUIRE(message.find("'output'") != std::string::npos); + + message = contractError(roformerContractJson(), {{"mix", ai::TensorElementType::Float32, {1, 801, 4100}}}); + REQUIRE(message.find("'input'") != std::string::npos); + + message = contractError(roformerContractJson(), {{"input", ai::TensorElementType::Float32, {1, 801, 4100}}, + {"extra", ai::TensorElementType::Float32, {1}}}); + REQUIRE(message.find("'extra'") != std::string::npos); + + auto badLayout = roformerContractJson(); + badLayout["input_layout"] = "interleaved"; + REQUIRE(contractError(badLayout).find("'interleaved'") != std::string::npos); + + auto badMode = roformerContractJson(); + badMode["output_mode"] = "banana"; + REQUIRE(contractError(badMode).find("'banana'") != std::string::npos); + + auto badPad = roformerContractJson(); + badPad["stft"]["pad_mode"] = "constant"; + REQUIRE(contractError(badPad).find("'constant'") != std::string::npos); + + auto badDtype = roformerContractJson(); + badDtype["outputs"][0]["dtype"] = "float16"; + message = contractError(badDtype); + REQUIRE(message.find("'output'") != std::string::npos); + REQUIRE(message.find("float16") != std::string::npos); + + auto badTarget = roformerContractJson(); + badTarget["target_stem"] = "drums"; + REQUIRE(contractError(badTarget).find("'drums'") != std::string::npos); + + auto missingField = roformerContractJson(); + missingField["stft"].erase("zero_dc"); + REQUIRE_FALSE(ai::parseTensorContract(missingField, error).has_value()); + REQUIRE(error.find("zero_dc") != std::string::npos); +} + +TEST_CASE("Tensor contract manifest round-trips in installed and catalog form", "[ai][tensor][contract]") { + // Catalog form: names omitted, filled by the install-time probe. + auto catalog = roformerContractJson(); + catalog["inputs"][0].erase("name"); + catalog["outputs"][0].erase("name"); + const auto catalogContract = parseContract(catalog); + REQUIRE(catalogContract.inputs.front().name.empty()); + std::string error; + REQUIRE(ai::checkTensorContract(catalogContract, {{"whatever_the_export_called_it", ai::TensorElementType::Float32, + {1, 801, 4100}}}, + kProbedOutputs, error)); + REQUIRE(ai::tensorContractToJson(catalogContract) == catalog); + + const auto installed = parseContract(roformerContractJson()); + REQUIRE(ai::tensorContractToJson(installed) == roformerContractJson()); + + const auto config = ai::runnerConfigFromContract(installed, error); + REQUIRE(config.has_value()); + REQUIRE(config->chunkSamples == 352800); + REQUIRE(config->overlapSamples == 88200); + REQUIRE(config->channels == 2); + REQUIRE(config->stft.nFft == 2048); + REQUIRE(config->stft.hopLength == 441); + REQUIRE(config->stft.zeroDc); + REQUIRE(config->inputLayout == ai::InputLayout::FoldedStereo); + REQUIRE(config->outputMode == ai::OutputMode::Mask); + REQUIRE(config->stems.size() == 2); + REQUIRE(config->stems[1].residualOf == "vocals"); + + // Writer -> loader, end to end: the tensor pack loads, and a null contract + // leaves the manifest without the block (scalar packs unchanged). + const auto root = std::filesystem::temp_directory_path() / "automix_tensor_contract_manifest"; + std::filesystem::remove_all(root); + std::filesystem::create_directories(root); + { + std::ofstream model(root / "bs_roformer.onnx", std::ios::binary); + model << "not a real graph"; + } + ai::HubModelInfo info; + info.repoId = "xycld/BS-RoFormer-ONNX"; + info.modelId = "xycld/BS-RoFormer-ONNX"; + info.license = "mit"; + info.sourceUrl = "https://huggingface.co/xycld/BS-RoFormer-ONNX"; + ai::HubInstallResult install; + install.primaryFilePath = root / "bs_roformer.onnx"; + ai::ModelCompatibilityResult compatibility; + compatibility.compatible = true; + compatibility.taskScope = "separation"; + compatibility.packType = "separation_model"; + compatibility.engine = "bs_roformer"; + compatibility.expectedOutputKeys = {"output"}; + + REQUIRE(ai::writeTurnkeyModelPackManifest(root, info, install, compatibility, &installed, &error)); + ai::ModelPackLoader loader; + const auto pack = loader.load(root); + REQUIRE(pack.has_value()); + REQUIRE(pack->tensorContract.has_value()); + REQUIRE(ai::tensorContractToJson(*pack->tensorContract) == roformerContractJson()); + + REQUIRE(ai::writeTurnkeyModelPackManifest(root, info, install, compatibility, nullptr, &error)); + { + // Scoped: Windows cannot remove_all a directory holding an open file. + std::ifstream manifestIn(root / "model.json"); + REQUIRE_FALSE(nlohmann::json::parse(manifestIn).contains("tensor_contract")); + } + const auto scalarPack = loader.load(root); + REQUIRE(scalarPack.has_value()); + REQUIRE_FALSE(scalarPack->tensorContract.has_value()); + std::filesystem::remove_all(root); +} + +TEST_CASE("Onnx tensor inference without a model stays unavailable", "[ai][tensor][onnx]") { + ai::OnnxTensorInference inference; + REQUIRE_FALSE(inference.isAvailable()); + REQUIRE_FALSE(inference.loadModel(std::filesystem::temp_directory_path() / "automix_no_such_model.onnx")); + REQUIRE(inference.backendDiagnostics().find("automix_no_such_model.onnx") != std::string::npos); + REQUIRE(inference.inputSpecs().empty()); + const auto result = inference.run({}); + REQUIRE_FALSE(result.usedModel); + REQUIRE(result.outputs.empty()); +} + +#ifdef AUTOMIX_HAS_NATIVE_ORT + +namespace { + +// Synthetic graphs generated by tests/fixtures/tensor/make_fixtures.py. +std::filesystem::path tensorFixture(const char* name) { + return std::filesystem::path(AUTOMIX_SOURCE_DIR) / "tests" / "fixtures" / "tensor" / name; +} + +} // namespace + +TEST_CASE("Native tensor inference probes the real graph", "[ai][tensor][onnx][native]") { + // Weights are download-only and never committed. Point this at an installed + // BS-RoFormer export to run the probe against the real graph. + const char* modelPath = std::getenv("AUTOMIX_BS_ROFORMER_ONNX"); + if (modelPath == nullptr || !std::filesystem::is_regular_file(modelPath)) { + SKIP("AUTOMIX_BS_ROFORMER_ONNX does not name a BS-RoFormer .onnx file"); + } + + auto catalog = roformerContractJson(); + catalog["inputs"][0].erase("name"); + catalog["outputs"][0].erase("name"); + ai::OnnxTensorInference inference; + inference.setTensorContract(parseContract(catalog)); + const bool loaded = inference.loadModel(modelPath); + INFO(inference.backendDiagnostics()); + REQUIRE(loaded); + REQUIRE(inference.usingNativeSession()); + REQUIRE(inference.inputSpecs().size() == 1); + REQUIRE(inference.outputSpecs().size() == 1); + REQUIRE(ai::shapesMatch(inference.inputSpecs().front(), kRoformerInput)); + REQUIRE(ai::shapesMatch(inference.outputSpecs().front(), kRoformerOutput)); +} + +TEST_CASE("Native tensor inference separates through a synthetic mask graph", "[ai][tensor][onnx][native]") { + ai::OnnxTensorInference inference; + inference.setTensorContract(parseContract(roformerContractJson())); + const bool loaded = inference.loadModel(tensorFixture("identity_mask.onnx")); + INFO(inference.backendDiagnostics()); + REQUIRE(loaded); + REQUIRE(inference.usingNativeSession()); + REQUIRE(inference.inputSpecs().size() == 1); + REQUIRE(inference.inputSpecs().front().name == "input"); + REQUIRE(inference.inputSpecs().front().dims == std::vector{1, 801, 4100}); + REQUIRE(inference.outputSpecs().front().name == "output"); + REQUIRE(inference.outputSpecs().front().dims == std::vector{1, 1, 2050, 801, 2}); + + // Two chunks, so the native session runs more than once and the crossfade is exercised. + const int samples = 352800 + 120000; + const auto mix = makeTestSignal(2, samples, 44100.0); + // zero_dc is off so a 1 + 0i mask is an exact identity; with it on, each + // frame's DC bin is (correctly) removed and the vocals differ from the mix. + auto config = roformerRunnerConfig(); + config.stft.zeroDc = false; + REQUIRE(ai::SeparationRunner::chunkCount(samples, config) == 2); + const auto result = ai::SeparationRunner::separate(mix, inference, config); + INFO(result.logMessage); + REQUIRE(result.usedModel); + REQUIRE(result.stemNames == std::vector{"vocals", "instrumental"}); + REQUIRE(result.stemAudio[0].getNumSamples() == samples); + REQUIRE(maxAbsDifference(result.stemAudio[0], mix) < 1.0e-4f); + const engine::AudioBuffer silence(2, samples, 44100.0); + REQUIRE(maxAbsDifference(result.stemAudio[1], silence) < 1.0e-4f); + + // With the contract's zero_dc on, the residual still sums back to the mix exactly. + const auto zeroDc = ai::SeparationRunner::separate(mix, inference, roformerRunnerConfig()); + REQUIRE(zeroDc.usedModel); + float worstSum = 0.0f; + for (int ch = 0; ch < 2; ++ch) { + for (int i = 0; i < samples; ++i) { + const float sum = zeroDc.stemAudio[0].getSample(ch, i) + zeroDc.stemAudio[1].getSample(ch, i); + worstSum = std::max(worstSum, std::abs(sum - mix.getSample(ch, i))); + } + } + REQUIRE(worstSum < 1.0e-5f); + + // Bindings are matched by name; an unknown name is refused, never bound by position. + ai::TensorBinding wrongName{{"mix", ai::TensorElementType::Float32, {1, 801, 4100}}, + std::vector(801 * 4100, 0.0f)}; + const auto refused = inference.run({wrongName}); + REQUIRE_FALSE(refused.usedModel); + REQUIRE(refused.logMessage.find("'input'") != std::string::npos); + + ai::TensorBinding wrongShape{{"input", ai::TensorElementType::Float32, {1, 800, 4100}}, + std::vector(800 * 4100, 0.0f)}; + const auto misshapen = inference.run({wrongShape}); + REQUIRE_FALSE(misshapen.usedModel); + REQUIRE(misshapen.logMessage.find("[1, 800, 4100]") != std::string::npos); +} + +TEST_CASE("Native tensor inference refuses a contract the graph does not match", "[ai][tensor][onnx][native]") { + auto wrongGeometry = roformerContractJson(); + wrongGeometry["outputs"][0]["shape"] = {1, 2, 2050, 801, 2}; + wrongGeometry["output_mode"] = "direct"; + wrongGeometry["stems"] = {{{"name", "vocals"}}, {{"name", "instrumental"}}}; + ai::OnnxTensorInference inference; + inference.setTensorContract(parseContract(wrongGeometry)); + REQUIRE_FALSE(inference.loadModel(tensorFixture("identity_mask.onnx"))); + REQUIRE_FALSE(inference.isAvailable()); + REQUIRE(inference.backendDiagnostics().find("'output'") != std::string::npos); +} + +TEST_CASE("Native tensor inference rejects non-float32 graph I/O", "[ai][tensor][onnx][native]") { + ai::OnnxTensorInference inference; + REQUIRE_FALSE(inference.loadModel(tensorFixture("int64_io.onnx"))); + REQUIRE_FALSE(inference.isAvailable()); + const auto diagnostics = inference.backendDiagnostics(); + REQUIRE(diagnostics.find("'tokens'") != std::string::npos); + REQUIRE(diagnostics.find("int64") != std::string::npos); +} + +TEST_CASE("Native tensor inference names a missing external-data sidecar", "[ai][tensor][onnx][native]") { + const auto root = std::filesystem::temp_directory_path() / "automix_tensor_external_data"; + std::filesystem::remove_all(root); + std::filesystem::create_directories(root); + std::filesystem::copy_file(tensorFixture("external_data.onnx"), root / "external_data.onnx"); + std::filesystem::copy_file(tensorFixture("external_data.onnx.data"), root / "external_data.onnx.data"); + + ai::OnnxTensorInference present; + const bool loaded = present.loadModel(root / "external_data.onnx"); + INFO(present.backendDiagnostics()); + REQUIRE(loaded); + const auto ran = present.run({{{"x", ai::TensorElementType::Float32, {1, 4}}, {0.5f, 0.5f, 0.5f, 0.5f}}}); + REQUIRE(ran.usedModel); + REQUIRE(ran.outputs.size() == 1); + REQUIRE(ran.outputs.front().spec.name == "y"); + REQUIRE(ran.outputs.front().data == std::vector{1.5f, 2.5f, 3.5f, 4.5f}); + + std::filesystem::remove(root / "external_data.onnx.data"); + ai::OnnxTensorInference missing; + REQUIRE_FALSE(missing.loadModel(root / "external_data.onnx")); + REQUIRE_FALSE(missing.isAvailable()); + REQUIRE(missing.backendDiagnostics().find("external_data.onnx.data") != std::string::npos); + std::filesystem::remove_all(root); +} + +#endif + +// Spec test 9. Tensor separation is opt-in per session; a default-on flag would +// route every separated import through a new backend and move golden files. +TEST_CASE("Tensor separation defaults off", "[tensor][settings]") { + const automix::domain::RenderSettings settings; + REQUIRE_FALSE(settings.tensorSeparationEnabled); +} + +TEST_CASE("Tensor separation flag persists per session and stays off for older sessions", "[tensor][settings]") { + automix::domain::RenderSettings enabled; + enabled.tensorSeparationEnabled = true; + const nlohmann::json saved = enabled; + REQUIRE(saved.at("tensorSeparationEnabled") == true); + REQUIRE(saved.get().tensorSeparationEnabled); + + // A session written before the toggle existed has no key at all. + auto legacy = nlohmann::json(automix::domain::RenderSettings{}); + legacy.erase("tensorSeparationEnabled"); + REQUIRE_FALSE(legacy.get().tensorSeparationEnabled); +} + +namespace { + +// Real sibling list of xycld/BS-RoFormer-ONNX (HF API, 2026-09-30), in the +// alphabetical order the API returns. +const std::vector kBsRoformerSiblings = { + ".gitattributes", + "README.md", + "bs_roformer_ep317_sdr12.9755.onnx", + "bs_roformer_ep317_sdr12.9755.onnx.data", + "bs_roformer_ep317_sdr12.9755_quantized_uint8.onnx", +}; + +nlohmann::json roformerCatalogJson() { + auto catalog = roformerContractJson(); + catalog["inputs"][0].erase("name"); + catalog["outputs"][0].erase("name"); + return catalog; +} + +// An installed tensor pack as the hub writes it: model.json carrying the +// catalog contract, plus the model file (copied, or a stand-in that no backend +// can open). +std::filesystem::path writeTensorPack(const std::filesystem::path& root, const std::filesystem::path* modelSource) { + std::filesystem::remove_all(root); + std::filesystem::create_directories(root); + const auto modelPath = root / "bs_roformer.onnx"; + if (modelSource != nullptr) { + std::filesystem::copy_file(*modelSource, modelPath); + } else { + std::ofstream model(modelPath, std::ios::binary); + model << "not a real graph"; + } + ai::HubModelInfo info; + info.repoId = ai::kBsRoformerRepoId; + info.modelId = ai::kBsRoformerRepoId; + info.license = "mit"; + ai::HubInstallResult install; + install.primaryFilePath = modelPath; + ai::ModelCompatibilityResult compatibility; + compatibility.compatible = true; + compatibility.taskScope = "separation"; + compatibility.packType = "separation_model"; + compatibility.engine = "bs_roformer"; + const auto contract = ai::bsRoformerCatalogContract(); + std::string error; + REQUIRE(ai::writeTurnkeyModelPackManifest(root, info, install, compatibility, &contract, &error)); + return root; +} + +std::vector readBytes(const std::filesystem::path& path) { + std::ifstream in(path, std::ios::binary); + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} + +} // namespace + +TEST_CASE("BS-RoFormer catalog entry installs the quantized build as a separation pack", "[ai][tensor][catalog]") { + const auto curated = ai::curatedModelIds(); + REQUIRE(std::find(curated.begin(), curated.end(), std::string(ai::kBsRoformerRepoId)) != curated.end()); + + // The generic preference takes the first .onnx: the fp32 graph, which is + // useless without its ~640 MB sidecar. The pin is what prevents that. + bool hasOnnx = false; + REQUIRE(ai::primaryFileForRepo("someone/else", kBsRoformerSiblings, &hasOnnx) == + "bs_roformer_ep317_sdr12.9755.onnx"); + REQUIRE(ai::primaryFileForRepo(ai::kBsRoformerRepoId, kBsRoformerSiblings, &hasOnnx) == + ai::kBsRoformerQuantizedFile); + REQUIRE(hasOnnx); + const std::vector withoutQuantized(kBsRoformerSiblings.begin(), kBsRoformerSiblings.end() - 1); + REQUIRE(ai::primaryFileForRepo(ai::kBsRoformerRepoId, withoutQuantized).empty()); + + // Classified from the real card data, the way HuggingFaceModelHub::modelInfo does. + ai::HubModelInfo info; + info.repoId = ai::kBsRoformerRepoId; + info.tags = {"onnxruntime", "onnx", "music", "music-source-separation", "audio", "bs-roformer", "roformer", + "vocals", "instrumental", "quantized", "audio-to-audio", "license:mit"}; + info.files = kBsRoformerSiblings; + info.primaryFile = ai::primaryFileForRepo(info.repoId, info.files, &info.hasOnnx); + info.useCase = ai::HuggingFaceModelHub::inferUseCase(info.repoId, info.tags, ""); + const auto compatibility = ai::validateCatalogModel(info); + INFO(compatibility.reason); + REQUIRE(compatibility.compatible); + REQUIRE(compatibility.taskScope == "separation"); + + // MIT: licence-driven consent does not prompt; attribution is in NOTICE. + REQUIRE_FALSE(ai::ModelLicensePolicy::requiresUserConsent("mit")); +} + +TEST_CASE("BS-RoFormer catalog contract is the spec block with names omitted", "[ai][tensor][catalog]") { + const auto contract = ai::bsRoformerCatalogContract(); + REQUIRE(ai::tensorContractToJson(contract) == roformerCatalogJson()); + std::string error; + REQUIRE(ai::checkTensorContract(contract, kProbedInputs, kProbedOutputs, error)); +} + +TEST_CASE("Installed tensor contract takes the graph's names only when a graph can be probed", "[ai][tensor][catalog]") { + const auto catalog = ai::bsRoformerCatalogContract(); + std::string error; +#ifdef AUTOMIX_HAS_NATIVE_ORT + const auto fixture = std::filesystem::path(AUTOMIX_SOURCE_DIR) / "tests" / "fixtures" / "tensor" / "identity_mask.onnx"; + const auto installed = ai::resolveInstalledTensorContract(catalog, fixture, error); + INFO(error); + REQUIRE(installed.has_value()); + REQUIRE(ai::tensorContractToJson(*installed) == roformerContractJson()); + + // A graph that does not satisfy the contract fails the install, named. + const auto int64Graph = fixture.parent_path() / "int64_io.onnx"; + REQUIRE_FALSE(ai::resolveInstalledTensorContract(catalog, int64Graph, error).has_value()); + REQUIRE_FALSE(error.empty()); +#else + const auto installed = + ai::resolveInstalledTensorContract(catalog, std::filesystem::temp_directory_path() / "unprobed.onnx", error); + REQUIRE(installed.has_value()); + REQUIRE(ai::tensorContractToJson(*installed) == roformerCatalogJson()); +#endif +} + +TEST_CASE("Tensor flag off leaves separation byte-identical; on falls back honestly", "[ai][tensor][separator]") { + const auto tempRoot = std::filesystem::temp_directory_path() / "automix_tensor_flag_separation"; + std::filesystem::remove_all(tempRoot); + std::filesystem::create_directories(tempRoot); + const auto mixPath = tempRoot / "mix.wav"; + automix::util::WavWriter writer; + writer.write(mixPath, makeTestSignal(2, 44100, 44100.0), 24); + + const auto packRoot = writeTensorPack(tempRoot / "pack", nullptr); + const auto emptyRoot = tempRoot / "no_pack"; + std::filesystem::create_directories(emptyRoot); + + ai::StemSeparator withPack(packRoot); + ai::StemSeparator withoutPack(emptyRoot); + REQUIRE(withPack.isTensorModelAvailable()); + REQUIRE_FALSE(withoutPack.isTensorModelAvailable()); + // The legacy path never adopts a tensor pack's model file. + REQUIRE_FALSE(withPack.isModelAvailable()); + + const auto baseline = withoutPack.separate(mixPath, tempRoot / "out_baseline"); + const auto flagOff = withPack.separate(mixPath, tempRoot / "out_off"); + REQUIRE(baseline.success); + REQUIRE(flagOff.success); + REQUIRE(flagOff.logMessage == baseline.logMessage); + REQUIRE(flagOff.usedModel == baseline.usedModel); + REQUIRE(flagOff.generatedFiles.size() == baseline.generatedFiles.size()); + for (std::size_t i = 0; i < baseline.generatedFiles.size(); ++i) { + REQUIRE(flagOff.generatedFiles[i].filename() == baseline.generatedFiles[i].filename()); + REQUIRE(readBytes(flagOff.generatedFiles[i]) == readBytes(baseline.generatedFiles[i])); + } + + ai::StemSeparator::SeparationOptions tensorOn; + tensorOn.useTensorModel = true; + const auto fellBack = withPack.separate(mixPath, tempRoot / "out_on", tensorOn); + INFO(fellBack.logMessage); + REQUIRE(fellBack.success); + REQUIRE_FALSE(fellBack.usedModel); + REQUIRE(fellBack.logMessage.rfind("Tensor separation unavailable (tensor model did not load", 0) == 0); + REQUIRE(fellBack.stems.size() == baseline.stems.size()); + std::filesystem::remove_all(tempRoot); +} + +#ifdef AUTOMIX_HAS_NATIVE_ORT + +TEST_CASE("Tensor separation runs end to end through StemSeparator", "[ai][tensor][separator][native]") { + const auto tempRoot = std::filesystem::temp_directory_path() / "automix_tensor_separator_e2e"; + std::filesystem::remove_all(tempRoot); + std::filesystem::create_directories(tempRoot); + const auto mixPath = tempRoot / "mix.wav"; + automix::util::WavWriter writer; + writer.write(mixPath, makeTestSignal(2, 100000, 44100.0), 24); + + const auto fixture = std::filesystem::path(AUTOMIX_SOURCE_DIR) / "tests" / "fixtures" / "tensor" / "identity_mask.onnx"; + const auto packRoot = writeTensorPack(tempRoot / "pack", &fixture); + + ai::StemSeparator separator(packRoot); + ai::StemSeparator::SeparationOptions options; + options.useTensorModel = true; + int lastDone = -1; + int lastTotal = -1; + options.tensorProgress = [&](const int done, const int total) { + lastDone = done; + lastTotal = total; + }; + const auto result = separator.separate(mixPath, tempRoot / "out", options); + INFO(result.logMessage); + REQUIRE(result.success); + REQUIRE(result.usedModel); + REQUIRE(lastTotal == 1); + REQUIRE(lastDone == lastTotal); + REQUIRE(result.logMessage.find("'instrumental' is the residual (mix - vocals), not a second separation") != + std::string::npos); + + REQUIRE(result.stems.size() == 2); + REQUIRE(result.stems[0].role == automix::domain::StemRole::Vocals); + REQUIRE(result.stems[1].role == automix::domain::StemRole::Music); + for (const auto& stem : result.stems) { + REQUIRE(stem.origin == automix::domain::StemOrigin::Separated); + REQUIRE_FALSE(stem.separationConfidence.has_value()); + REQUIRE_FALSE(stem.separationArtifactRisk.has_value()); + REQUIRE(std::filesystem::is_regular_file(stem.filePath)); + } + + // zero_dc is on in the contract, so vocals are not the mix, but the residual + // sums back to it; the only error left is each stem's 24-bit quantization. + automix::engine::AudioFileIO io; + const auto mix = io.readAudioFile(mixPath); + const auto vocals = io.readAudioFile(result.stems[0].filePath); + const auto instrumental = io.readAudioFile(result.stems[1].filePath); + REQUIRE(vocals.getNumSamples() == mix.getNumSamples()); + float worst = 0.0f; + for (int ch = 0; ch < 2; ++ch) { + for (int i = 0; i < mix.getNumSamples(); ++i) { + worst = std::max(worst, std::abs(vocals.getSample(ch, i) + instrumental.getSample(ch, i) - mix.getSample(ch, i))); + } + } + REQUIRE(worst < 1.0e-5f); + std::filesystem::remove_all(tempRoot); +} + +#endif +TEST_CASE("Tensor provider candidates always end on CPU", "[ai][tensor][gpu]") { + using V = std::vector; + const V cudaBuild{"TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"}; + REQUIRE(ai::tensorProviderCandidates("auto", cudaBuild) == V{"cuda", "cpu"}); + REQUIRE(ai::tensorProviderCandidates("", cudaBuild) == V{"cuda", "cpu"}); + REQUIRE(ai::tensorProviderCandidates("cuda", cudaBuild) == V{"cuda", "cpu"}); + REQUIRE(ai::tensorProviderCandidates("CUDAExecutionProvider", cudaBuild) == V{"cuda", "cpu"}); + REQUIRE(ai::tensorProviderCandidates("cpu", cudaBuild) == V{"cpu"}); + // A provider the runtime does not contain is never attempted, but the GPU + // request is honoured with what is there (Windows defaults to DirectML). + REQUIRE(ai::tensorProviderCandidates("directml", cudaBuild) == V{"cuda", "cpu"}); + REQUIRE(ai::tensorProviderCandidates("directml", V{"CPUExecutionProvider"}) == V{"cpu"}); + REQUIRE(ai::tensorProviderCandidates("auto", V{"CPUExecutionProvider"}) == V{"cpu"}); + REQUIRE(ai::tensorProviderCandidates("auto", V{}) == V{"cpu"}); +} + +TEST_CASE("Runner stops before the next chunk when cancelled", "[ai][tensor][runner][cancel]") { + FakeTensorInference fake({kRoformerInput}, {kRoformerOutput}, identityMask); + const int samples = 352800 * 3; + const auto mix = makeTestSignal(2, samples, 44100.0); + auto config = roformerRunnerConfig(); + REQUIRE(ai::SeparationRunner::chunkCount(samples, config) > 2); + int progressCalls = 0; + config.progressCallback = [&](int, int) { ++progressCalls; }; + config.cancelRequested = [&] { return progressCalls >= 1; }; + + const auto result = ai::SeparationRunner::separate(mix, fake, config); + INFO(result.logMessage); + REQUIRE(result.cancelled); + REQUIRE_FALSE(result.usedModel); + REQUIRE(result.stemAudio.empty()); + REQUIRE(fake.calls == 1); + REQUIRE(result.logMessage.find("cancelled after 1/") != std::string::npos); +} + +namespace { + +// A backend whose inference can be aborted: it reports the terminated run as a +// failure, the way ONNX Runtime does after RunOptions::SetTerminate(). +class AbortableTensorInference final : public ai::ITensorInference { + public: + bool isAvailable() const override { return true; } + bool loadModel(const std::filesystem::path&) override { return true; } + std::vector inputSpecs() const override { return {kRoformerInput}; } + std::vector outputSpecs() const override { return {kRoformerOutput}; } + ai::TensorInferenceResult run(const std::vector& inputs) const override { + return identityMask(inputs); + } + ai::TensorInferenceResult runCancellable(const std::vector& inputs, + const std::function& cancelRequested) const override { + ++calls; + abortRequested = true; // the user presses Cancel while this chunk is running + if (cancelRequested && cancelRequested()) { + ai::TensorInferenceResult aborted; + aborted.logMessage = "Exiting due to terminate flag being set to true."; + return aborted; + } + return run(inputs); + } + mutable int calls = 0; + mutable bool abortRequested = false; +}; + +} // namespace + +TEST_CASE("Runner reports a terminated inference as cancelled, not failed", "[ai][tensor][runner][cancel]") { + AbortableTensorInference backend; + const auto mix = makeTestSignal(2, 352800 * 2, 44100.0); + auto config = roformerRunnerConfig(); + config.cancelRequested = [&] { return backend.abortRequested; }; + + const auto result = ai::SeparationRunner::separate(mix, backend, config); + INFO(result.logMessage); + REQUIRE(result.cancelled); + REQUIRE(backend.calls == 1); + REQUIRE(result.logMessage.find("cancelled after 0/") != std::string::npos); + REQUIRE(result.logMessage.find("failed") == std::string::npos); +} + +#ifdef AUTOMIX_HAS_NATIVE_ORT + +TEST_CASE("Cancelled tensor separation writes nothing and does not fall back", "[ai][tensor][separator][cancel][native]") { + const auto tempRoot = std::filesystem::temp_directory_path() / "automix_tensor_cancel"; + std::filesystem::remove_all(tempRoot); + std::filesystem::create_directories(tempRoot); + const auto mixPath = tempRoot / "mix.wav"; + automix::util::WavWriter writer; + writer.write(mixPath, makeTestSignal(2, 100000, 44100.0), 24); + const auto fixture = std::filesystem::path(AUTOMIX_SOURCE_DIR) / "tests" / "fixtures" / "tensor" / "identity_mask.onnx"; + const auto packRoot = writeTensorPack(tempRoot / "pack", &fixture); + + ai::StemSeparator separator(packRoot); + ai::StemSeparator::SeparationOptions options; + options.useTensorModel = true; + options.cancelRequested = [] { return true; }; + const auto result = separator.separate(mixPath, tempRoot / "out", options); + INFO(result.logMessage); + REQUIRE(result.cancelled); + REQUIRE_FALSE(result.success); + REQUIRE(result.stems.empty()); + REQUIRE(result.generatedFiles.empty()); + // The existing separator would have produced stems; it must not have run. + REQUIRE_FALSE(std::filesystem::exists(tempRoot / "out" / "separation_qa_report.json")); + std::filesystem::remove_all(tempRoot); +} + +TEST_CASE("Tensor session reports the provider it actually runs on", "[ai][tensor][gpu][native]") { + const auto fixture = std::filesystem::path(AUTOMIX_SOURCE_DIR) / "tests" / "fixtures" / "tensor" / "identity_mask.onnx"; + + ai::OnnxTensorInference cpu; + cpu.setExecutionProvider("cpu"); + REQUIRE(cpu.loadModel(fixture)); + REQUIRE(cpu.activeExecutionProvider() == "cpu"); + REQUIRE(cpu.backendDiagnostics().find("provider=cpu") != std::string::npos); + + // "auto" opens on whatever works here; either way it must say which. + ai::OnnxTensorInference automatic; + REQUIRE(automatic.loadModel(fixture)); + INFO(automatic.backendDiagnostics()); + REQUIRE_FALSE(automatic.activeExecutionProvider().empty()); + + const char* expectCuda = std::getenv("AUTOMIX_EXPECT_CUDA"); + if (expectCuda == nullptr || std::string(expectCuda) != "1") { + SKIP("Set AUTOMIX_EXPECT_CUDA=1 on a machine with a CUDA-capable GPU and runtime to require CUDA"); + } + ai::OnnxTensorInference cuda; + cuda.setExecutionProvider("cuda"); + REQUIRE(cuda.loadModel(fixture)); + INFO(cuda.backendDiagnostics()); + REQUIRE(cuda.activeExecutionProvider() == "cuda"); + // Same numbers on the GPU: the identity mask still reconstructs the mix. + const int samples = 352800; + const auto mix = makeTestSignal(2, samples, 44100.0); + auto config = roformerRunnerConfig(); + config.stft.zeroDc = false; + const auto separated = ai::SeparationRunner::separate(mix, cuda, config); + INFO(separated.logMessage); + REQUIRE(separated.usedModel); + REQUIRE(maxAbsDifference(separated.stemAudio[0], mix) < 1.0e-4f); +} + +#endif +namespace { + +std::filesystem::path tensorFixturePath(const char* name) { + return std::filesystem::path(AUTOMIX_SOURCE_DIR) / "tests" / "fixtures" / "tensor" / name; +} + +// Minimal protobuf encoding, enough to hand-build hostile ONNX models. +void putVarint(std::string& out, std::uint64_t value) { + while (value >= 0x80) { + out.push_back(static_cast((value & 0x7F) | 0x80)); + value >>= 7; + } + out.push_back(static_cast(value)); +} +std::string lengthField(std::uint32_t number, const std::string& payload) { + std::string out; + putVarint(out, (static_cast(number) << 3) | 2); + putVarint(out, payload.size()); + return out + payload; +} +std::string modelWithExternalLocation(const std::string& location) { + std::string tensor; + putVarint(tensor, (14u << 3) | 0); // data_location + putVarint(tensor, 1); // EXTERNAL + tensor += lengthField(13, lengthField(1, "location") + lengthField(2, location)); + return lengthField(7, lengthField(5, tensor)); // ModelProto.graph.initializer +} + +} // namespace + +TEST_CASE("External-data models are inlined into one self-contained file", "[ai][tensor][external-data]") { + const auto root = std::filesystem::temp_directory_path() / "automix_inline_external"; + std::filesystem::remove_all(root); + std::filesystem::create_directories(root); + std::filesystem::copy_file(tensorFixturePath("external_data.onnx"), root / "external_data.onnx"); + std::filesystem::copy_file(tensorFixturePath("external_data.onnx.data"), root / "external_data.onnx.data"); + + const auto inlinedPath = root / "inlined.onnx"; + const auto inlined = ai::inlineExternalData(root / "external_data.onnx", inlinedPath); + INFO(inlined.error); + REQUIRE(inlined.success); + REQUIRE(inlined.tensorsInlined == 1); + const auto bytes = readBytes(inlinedPath); + const std::string text(bytes.begin(), bytes.end()); + REQUIRE(text.find("external_data.onnx.data") == std::string::npos); + REQUIRE(text.find("location") == std::string::npos); + // The sidecar's 16 bytes now live in the model itself. + const auto sidecar = readBytes(root / "external_data.onnx.data"); + REQUIRE(text.find(std::string(sidecar.begin(), sidecar.end())) != std::string::npos); + REQUIRE_FALSE(std::filesystem::exists(root / "inlined.onnx.partial")); + + // A model with nothing external is copied unchanged. + const auto again = ai::inlineExternalData(inlinedPath, root / "again.onnx"); + REQUIRE(again.success); + REQUIRE(again.tensorsInlined == 0); + REQUIRE(readBytes(root / "again.onnx") == bytes); + +#ifdef AUTOMIX_HAS_NATIVE_ORT + // It still computes the same thing with the sidecar gone. + std::filesystem::remove(root / "external_data.onnx.data"); + ai::OnnxTensorInference session; + REQUIRE(session.loadModel(inlinedPath)); + const auto ran = session.run({{{"x", ai::TensorElementType::Float32, {1, 4}}, {0.5f, 0.5f, 0.5f, 0.5f}}}); + REQUIRE(ran.usedModel); + REQUIRE(ran.outputs.front().data == std::vector{1.5f, 2.5f, 3.5f, 4.5f}); +#endif + std::filesystem::remove_all(root); +} + +TEST_CASE("External-data inlining refuses locations outside the model directory", "[ai][tensor][external-data]") { + const auto root = std::filesystem::temp_directory_path() / "automix_inline_hostile"; + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "models"); + { + std::ofstream secret(root / "secret.bin", std::ios::binary); + secret << "do not copy me"; + } + for (const std::string location : {std::string("../secret.bin"), (root / "secret.bin").string(), std::string("")}) { + INFO("location: " << location); + const auto modelPath = root / "models" / "hostile.onnx"; + { + std::ofstream model(modelPath, std::ios::binary | std::ios::trunc); + const auto bytes = modelWithExternalLocation(location); + model.write(bytes.data(), static_cast(bytes.size())); + } + const auto result = ai::inlineExternalData(modelPath, root / "models" / "out.onnx"); + REQUIRE_FALSE(result.success); + REQUIRE(result.error.find("location") != std::string::npos); + REQUIRE_FALSE(std::filesystem::exists(root / "models" / "out.onnx")); + } + + // Truncated input is a clean error, not a crash. + { + std::ofstream model(root / "models" / "truncated.onnx", std::ios::binary | std::ios::trunc); + const auto bytes = modelWithExternalLocation("weights.bin"); + model.write(bytes.data(), static_cast(bytes.size() - 3)); + } + const auto truncated = ai::inlineExternalData(root / "models" / "truncated.onnx", root / "models" / "t.onnx"); + REQUIRE_FALSE(truncated.success); + REQUIRE(truncated.error.find("not a valid ONNX model") != std::string::npos); + std::filesystem::remove_all(root); +} +TEST_CASE("BS-RoFormer installs fp32 for GPU and quantized for CPU", "[ai][tensor][catalog][gpu]") { + bool hasOnnx = false; + REQUIRE(ai::primaryFileForRepo(ai::kBsRoformerRepoId, kBsRoformerSiblings, &hasOnnx, true) == + ai::kBsRoformerFp32File); + REQUIRE(hasOnnx); + REQUIRE(ai::primaryFileForRepo(ai::kBsRoformerRepoId, kBsRoformerSiblings, &hasOnnx, false) == + ai::kBsRoformerQuantizedFile); + + // fp32 without its weights is never chosen. + std::vector noSidecar = kBsRoformerSiblings; + noSidecar.erase(std::find(noSidecar.begin(), noSidecar.end(), "bs_roformer_ep317_sdr12.9755.onnx.data")); + REQUIRE(ai::primaryFileForRepo(ai::kBsRoformerRepoId, noSidecar, &hasOnnx, true) == ai::kBsRoformerQuantizedFile); + + // The sidecar is fetched with the fp32 graph and only with it. + REQUIRE(ai::auxiliaryAssetsFor(ai::kBsRoformerRepoId, ai::kBsRoformerFp32File, kBsRoformerSiblings) == + std::vector{"bs_roformer_ep317_sdr12.9755.onnx.data"}); + REQUIRE(ai::auxiliaryAssetsFor(ai::kBsRoformerRepoId, ai::kBsRoformerQuantizedFile, kBsRoformerSiblings).empty()); +} + +TEST_CASE("GPU probe agrees with the build", "[ai][tensor][gpu]") { + std::string provider; + const bool available = ai::gpuTensorSessionAvailable(&provider); + INFO("provider: " << provider); + REQUIRE(available == !provider.empty()); +#ifndef AUTOMIX_HAS_NATIVE_ORT + REQUIRE_FALSE(available); +#else + const char* expectCuda = std::getenv("AUTOMIX_EXPECT_CUDA"); + if (expectCuda != nullptr && std::string(expectCuda) == "1") { + REQUIRE(provider == "cuda"); + } +#endif +} +TEST_CASE("GPU memory policy for the fp32 model", "[ai][tensor][gpu]") { + constexpr std::uint64_t MiB = 1024 * 1024; + const std::uint64_t need = ai::kBsRoformerFp32GpuMemoryMb * MiB; + const auto card = [](std::uint64_t freeMiB, std::uint64_t totalMiB) { + return std::optional(ai::GpuMemoryInfo{freeMiB * 1024 * 1024, totalMiB * 1024 * 1024}); + }; + // Install: judged on total size. 12 GB cards report just under 12 GiB. + REQUIRE(ai::gpuFitsModel(card(14000, 16311), need)); + REQUIRE(ai::gpuFitsModel(card(10000, 12282), need)); + REQUIRE_FALSE(ai::gpuFitsModel(card(7000, 8188), need)); + REQUIRE_FALSE(ai::gpuFitsModel(std::nullopt, need)); // unknown size: keep the CPU build + // Run time: judged on what is free right now. + REQUIRE(ai::gpuHasRoomNow(card(14000, 16311), need)); + REQUIRE_FALSE(ai::gpuHasRoomNow(card(9000, 16311), need)); // e.g. a game holding 7 GB + REQUIRE(ai::gpuHasRoomNow(std::nullopt, need)); // unknown: try, the CPU retry covers it +} + +TEST_CASE("fp32 pack records its GPU memory need; quantized does not", "[ai][tensor][gpu][catalog]") { + const auto root = std::filesystem::temp_directory_path() / "automix_gpu_memory_manifest"; + std::filesystem::remove_all(root); + std::filesystem::create_directories(root); + for (const std::string file : {std::string(ai::kBsRoformerFp32File), std::string(ai::kBsRoformerQuantizedFile)}) { + { + std::ofstream model(root / file, std::ios::binary); + model << "not a real graph"; + } + ai::HubModelInfo info; + info.repoId = ai::kBsRoformerRepoId; + info.modelId = ai::kBsRoformerRepoId; + info.license = "mit"; + ai::HubInstallResult install; + install.primaryFilePath = root / file; + ai::ModelCompatibilityResult compatibility; + compatibility.compatible = true; + compatibility.taskScope = "separation"; + compatibility.packType = "separation_model"; + const auto contract = ai::bsRoformerCatalogContract(); + std::string error; + REQUIRE(ai::writeTurnkeyModelPackManifest(root, info, install, compatibility, &contract, &error)); + const auto pack = ai::ModelPackLoader().load(root); + REQUIRE(pack.has_value()); + INFO(file); + if (file == ai::kBsRoformerFp32File) { + REQUIRE(pack->gpuMemoryMb == std::optional(ai::kBsRoformerFp32GpuMemoryMb)); + } else { + REQUIRE_FALSE(pack->gpuMemoryMb.has_value()); + } + } + std::filesystem::remove_all(root); +} + +TEST_CASE("BS-RoFormer packs default to CUDA allow-list if missing from manifest", "[ai][tensor]") { + const auto root = std::filesystem::temp_directory_path() / "automix_bsroformer_allowlist_test"; + std::filesystem::remove_all(root); + std::filesystem::create_directories(root); + + const auto writePackWithManifest = [&](const std::string& modelFile, const std::optional>& gpuProviders) { + { + std::ofstream model(root / modelFile, std::ios::binary); + model << "not a real graph"; + } + ai::HubModelInfo info; + info.repoId = ai::kBsRoformerRepoId; + info.modelId = ai::kBsRoformerRepoId; + info.license = "mit"; + ai::HubInstallResult install; + install.primaryFilePath = root / modelFile; + ai::ModelCompatibilityResult compatibility; + compatibility.compatible = true; + compatibility.taskScope = "separation"; + compatibility.packType = "separation_model"; + const auto contract = ai::bsRoformerCatalogContract(); + std::string error; + REQUIRE(ai::writeTurnkeyModelPackManifest(root, info, install, compatibility, &contract, &error)); + + // Now edit model.json to control gpu_providers and/or model_file + std::ifstream in(root / "model.json"); + nlohmann::json manifest; + in >> manifest; + in.close(); + + manifest["model_file"] = modelFile; + if (gpuProviders.has_value()) { + manifest["gpu_providers"] = *gpuProviders; + } else { + manifest.erase("gpu_providers"); + manifest.erase("gpuProviders"); + } + + std::ofstream out(root / "model.json"); + out << manifest.dump(2); + }; + + ai::ModelPackLoader loader; + + // 1. Manifest with model_file = kBsRoformerQuantizedFile and no gpu_providers -> gpuProviders == {"cuda"} + writePackWithManifest(ai::kBsRoformerQuantizedFile, std::nullopt); + auto pack = loader.load(root); + REQUIRE(pack.has_value()); + CHECK(pack->gpuProviders == std::vector{"cuda"}); + + // 2. Same with kBsRoformerFp32File -> {"cuda"} + writePackWithManifest(ai::kBsRoformerFp32File, std::nullopt); + pack = loader.load(root); + REQUIRE(pack.has_value()); + CHECK(pack->gpuProviders == std::vector{"cuda"}); + + // 3. BS-RoFormer manifest with gpu_providers: ["cuda","webgpu"] -> kept as-is (explicit wins) + writePackWithManifest(ai::kBsRoformerQuantizedFile, std::vector{"cuda", "webgpu"}); + pack = loader.load(root); + REQUIRE(pack.has_value()); + CHECK(pack->gpuProviders == std::vector{"cuda", "webgpu"}); + + // 4. Non-BS-RoFormer manifest without the key -> empty + writePackWithManifest("other_model.onnx", std::nullopt); + pack = loader.load(root); + REQUIRE(pack.has_value()); + CHECK(pack->gpuProviders.empty()); + + std::filesystem::remove_all(root); +} + +#ifdef AUTOMIX_HAS_NATIVE_ORT + +#include "ai/OrtRuntime.h" + +TEST_CASE("OrtRuntime reports available providers and diagnostics", "[ai][tensor][native]") { + auto& runtime = ai::OrtRuntime::instance(); + const auto providers = runtime.availableProviders(); + INFO("OrtRuntime diagnostics: " << runtime.diagnostics()); + CHECK(std::find(providers.begin(), providers.end(), "cpu") != providers.end()); + CHECK(runtime.diagnostics().rfind("ORT version: 1.30", 0) == 0); + CHECK(!runtime.diagnostics().empty()); +} + +TEST_CASE("OrtRuntime warm-up can be requested twice and joined twice", "[ai][tensor][native]") { + auto& runtime = ai::OrtRuntime::instance(); + REQUIRE_NOTHROW(runtime.warmUpAsync()); + REQUIRE_NOTHROW(runtime.warmUpAsync()); + REQUIRE_NOTHROW(runtime.joinWarmUp()); + REQUIRE_NOTHROW(runtime.joinWarmUp()); +} + +TEST_CASE("Separation runs on CPU when the GPU lacks free memory for the model", "[ai][tensor][gpu][native]") { + const auto memory = ai::queryCudaDeviceMemory(); + const char* expectCuda = std::getenv("AUTOMIX_EXPECT_CUDA"); + if (expectCuda != nullptr && std::string(expectCuda) == "1") { + REQUIRE(memory.has_value()); + REQUIRE(memory->totalBytes > memory->freeBytes); + } + if (!memory.has_value()) { + SKIP("No CUDA runtime loadable here; the memory gate cannot engage"); + } + + const auto tempRoot = std::filesystem::temp_directory_path() / "automix_gpu_memory_gate"; + std::filesystem::remove_all(tempRoot); + std::filesystem::create_directories(tempRoot); + const auto mixPath = tempRoot / "mix.wav"; + automix::util::WavWriter writer; + writer.write(mixPath, makeTestSignal(2, 100000, 44100.0), 24); + const auto fixture = std::filesystem::path(AUTOMIX_SOURCE_DIR) / "tests" / "fixtures" / "tensor" / "identity_mask.onnx"; + const auto packRoot = writeTensorPack(tempRoot / "pack", &fixture); + { + // Declare a need no device has. + std::ifstream in(packRoot / "model.json"); + auto manifest = nlohmann::json::parse(in); + in.close(); + manifest["gpu_memory_mb"] = memory->totalBytes / (1024 * 1024) + 1; + std::ofstream(packRoot / "model.json") << manifest.dump(2); + } + + ai::StemSeparator separator(packRoot); + ai::StemSeparator::SeparationOptions options; + options.useTensorModel = true; + const auto result = separator.separate(mixPath, tempRoot / "out", options); + INFO(result.logMessage); + REQUIRE(result.success); + REQUIRE(result.usedModel); + REQUIRE(result.logMessage.find("MiB free but the model needs") != std::string::npos); + REQUIRE(result.logMessage.find(" on cpu.") != std::string::npos); + std::filesystem::remove_all(tempRoot); +} + +#endif +namespace { + +void writeZip(const std::filesystem::path& path, const std::vector>& entries) { + juce::ZipFile::Builder builder; + for (const auto& [name, content] : entries) { + builder.addEntry(new juce::MemoryInputStream(content.data(), content.size(), true), 0, name, juce::Time()); + } + std::filesystem::remove(path); + juce::FileOutputStream out(juce::File(juce::String(path.wstring().c_str()))); + REQUIRE(builder.writeToStream(out, nullptr)); +} + +} // namespace + +TEST_CASE("GPU runtime pack pins every archive", "[ai][gpu][runtime-pack]") { + const auto& archives = ai::GpuRuntimePack::archives(); +#if defined(_WIN32) + REQUIRE(archives.size() == 4); +#endif + for (const auto& archive : archives) { + INFO(archive.name); + REQUIRE(archive.url.rfind("https://files.pythonhosted.org/", 0) == 0); + REQUIRE(archive.url.size() > archive.name.size()); + REQUIRE(archive.url.compare(archive.url.size() - archive.name.size(), archive.name.size(), archive.name) == 0); + REQUIRE(archive.sha256.size() == 64); + REQUIRE(archive.sha256.find_first_not_of("0123456789abcdef") == std::string::npos); + REQUIRE(archive.bytes > 0); + } + REQUIRE_FALSE(ai::GpuRuntimePack::version().empty()); +} + +TEST_CASE("GPU runtime pack extracts only libraries and refuses escaping entries", "[ai][gpu][runtime-pack]") { + const auto root = std::filesystem::temp_directory_path() / "automix_runtime_pack_extract"; + std::filesystem::remove_all(root); + std::filesystem::create_directories(root); + + writeZip(root / "good.whl", {{"nvidia/cu13/bin/x64/cudart64_13.dll", "runtime"}, + {"nvidia/cu13/include/cuda.h", "header"}, + {"nvidia_cuda_runtime-13.4.92.dist-info/LICENSE.txt", "licence"}, + {"nvidia/cudnn/bin/cudnn64_9.DLL", "cudnn"}, + {"nvidia/cu13/bin/x64/nvblas64_13.dll", "never loaded by ORT"}}); + std::vector extracted; + REQUIRE(ai::GpuRuntimePack::extractLibraries(root / "good.whl", root / "bin", extracted).empty()); + std::sort(extracted.begin(), extracted.end()); + REQUIRE(extracted == std::vector{"cudart64_13.dll", "cudnn64_9.DLL"}); + REQUIRE(readBytes(root / "bin" / "cudart64_13.dll") == std::vector{'r', 'u', 'n', 't', 'i', 'm', 'e'}); + REQUIRE_FALSE(std::filesystem::exists(root / "bin" / "cuda.h")); + + writeZip(root / "evil.whl", {{"../../outside.dll", "payload"}}); + extracted.clear(); + const auto failure = ai::GpuRuntimePack::extractLibraries(root / "evil.whl", root / "bin", extracted); + REQUIRE(failure.find("escapes the archive") != std::string::npos); + REQUIRE(extracted.empty()); + REQUIRE_FALSE(std::filesystem::exists(root.parent_path() / "outside.dll")); + std::filesystem::remove_all(root); +} + +TEST_CASE("GPU runtime pack never marks a failed or cancelled install as installed", "[ai][gpu][runtime-pack]") { + if (ai::GpuRuntimePack::archives().empty()) { + SKIP("No GPU runtime pack on this platform"); + } + const auto root = std::filesystem::temp_directory_path() / "automix_runtime_pack_install"; + std::filesystem::remove_all(root); + + // A download whose bytes do not match the pinned hash is discarded. + const ai::GpuRuntimePack::Fetcher wrongBytes = [](const std::string&, const std::filesystem::path& destination, + const std::function& progress) { + std::ofstream(destination, std::ios::binary) << "not the wheel"; + progress(13); + return std::string(); + }; + const auto mismatch = ai::GpuRuntimePack::install(root, nullptr, wrongBytes); + REQUIRE_FALSE(mismatch.success); + REQUIRE(mismatch.message.find("SHA-256 mismatch") != std::string::npos); + REQUIRE_FALSE(ai::GpuRuntimePack::isInstalled(root)); + REQUIRE_FALSE(std::filesystem::exists(root / "downloads" / ai::GpuRuntimePack::archives().front().name)); + + // Cancelling mid-download stops at once and leaves nothing marked installed. + int fetches = 0; + const ai::GpuRuntimePack::Fetcher slow = [&](const std::string&, const std::filesystem::path&, + const std::function& progress) { + ++fetches; + return progress(1) ? std::string() : std::string("cancelled"); + }; + const auto cancelled = ai::GpuRuntimePack::install( + root, [](std::uint64_t, std::uint64_t) { return false; }, slow); + REQUIRE(cancelled.cancelled); + REQUIRE_FALSE(cancelled.success); + REQUIRE(fetches == 1); + REQUIRE_FALSE(ai::GpuRuntimePack::isInstalled(root)); + + // A marker for another pack version is not trusted. + std::filesystem::create_directories(root / "bin"); + std::ofstream(root / "bin" / "cudart64_13.dll") << "x"; + std::ofstream(root / "runtime.json") << R"({"version": "older", "libraries": ["cudart64_13.dll"]})"; + REQUIRE_FALSE(ai::GpuRuntimePack::isInstalled(root)); + std::ofstream(root / "runtime.json", std::ios::trunc) + << nlohmann::json{{"version", ai::GpuRuntimePack::version()}, {"libraries", {"cudart64_13.dll"}}}.dump(); + REQUIRE(ai::GpuRuntimePack::isInstalled(root)); + std::filesystem::remove(root / "bin" / "cudart64_13.dll"); + REQUIRE_FALSE(ai::GpuRuntimePack::isInstalled(root)); // a listed library went missing + std::filesystem::remove_all(root); +} +TEST_CASE("NVIDIA driver version is read from the Windows driver version", "[ai][gpu][runtime-pack]") { + const auto umd = [](std::uint64_t a, std::uint64_t b, std::uint64_t c, std::uint64_t d) { + return (a << 48) | (b << 32) | (c << 16) | d; + }; + REQUIRE(ai::GpuRuntimePack::nvidiaDriverVersion(umd(32, 0, 16, 1074)) == std::pair{610, 74}); // this machine + REQUIRE(ai::GpuRuntimePack::nvidiaDriverVersion(umd(32, 0, 15, 8088)) == std::pair{580, 88}); + REQUIRE(ai::GpuRuntimePack::nvidiaDriverVersion(umd(32, 0, 15, 6094)) == std::pair{560, 94}); + REQUIRE(ai::GpuRuntimePack::nvidiaDriverVersion(umd(31, 0, 15, 3742)) == std::pair{537, 42}); + REQUIRE(ai::GpuRuntimePack::kMinimumDriverMajor == 580); +} + +TEST_CASE("GPU runtime pack removal refuses foreign folders and finishes locked removals at startup", + "[ai][gpu][runtime-pack]") { + const auto base = std::filesystem::temp_directory_path() / "automix_runtime_pack_remove"; + std::filesystem::remove_all(base); + + // Not a pack: wrong folder name. + const auto foreign = base / "Documents"; + std::filesystem::create_directories(foreign / "bin"); + const auto refused = ai::GpuRuntimePack::uninstall(foreign); + REQUIRE_FALSE(refused.removedNow); + REQUIRE(refused.message.find("Refusing") != std::string::npos); + REQUIRE(std::filesystem::exists(foreign / "bin")); + + // A pack whose files are free goes at once. + const auto root = base / ai::GpuRuntimePack::version(); + std::filesystem::create_directories(root / "bin"); + std::ofstream(root / "bin" / "cudart64_13.dll") << "x"; + std::ofstream(root / "runtime.json") << "{}"; + REQUIRE(ai::GpuRuntimePack::uninstall(root).removedNow); + REQUIRE_FALSE(std::filesystem::exists(root)); + +#if defined(_WIN32) + // A library in use cannot be deleted on Windows: the pack is disabled now + // and removed at the next start. + std::filesystem::create_directories(root / "bin"); + std::ofstream(root / "bin" / "cudnn64_9.dll") << "x"; + std::ofstream(root / "runtime.json") << "{}"; + { + HANDLE locked = CreateFileW((root / "bin" / "cudnn64_9.dll").c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + REQUIRE(locked != INVALID_HANDLE_VALUE); + const auto deferred = ai::GpuRuntimePack::uninstall(root); + CloseHandle(locked); + REQUIRE_FALSE(deferred.removedNow); + REQUIRE_FALSE(std::filesystem::exists(root / "runtime.json")); // disabled at once + REQUIRE_FALSE(ai::GpuRuntimePack::isInstalled(root)); + } + ai::GpuRuntimePack::completePendingRemoval(root); + REQUIRE_FALSE(std::filesystem::exists(root)); +#endif + std::filesystem::remove_all(base); +} + +TEST_CASE("Vocal model GPU upgrade leaves non-matching packs alone", "[ai][gpu][catalog]") { + const auto root = std::filesystem::temp_directory_path() / "automix_gpu_upgrade_scan"; + std::filesystem::remove_all(root); + REQUIRE_FALSE(ai::upgradeBsRoformerForGpu(root).has_value()); // no hub at all + std::filesystem::create_directories(root / "other_model"); + std::ofstream(root / "other_model" / "modelhub.json") << R"({"repoId": "someone/else"})"; + std::ofstream(root / "other_model" / "model.json") << R"({"model_file": "bs_roformer_ep317_sdr12.9755_quantized_uint8.onnx"})"; + std::filesystem::create_directories(root / "already_gpu"); + std::ofstream(root / "already_gpu" / "modelhub.json") << R"({"repoId": "xycld/BS-RoFormer-ONNX"})"; + std::ofstream(root / "already_gpu" / "model.json") << R"({"model_file": "bs_roformer_ep317_sdr12.9755.onnx"})"; + REQUIRE_FALSE(ai::upgradeBsRoformerForGpu(root).has_value()); + std::filesystem::remove_all(root); +} +TEST_CASE("Tests use an isolated model hub, never the user's profile", "[ai][model-storage]") { + REQUIRE(ai::defaultModelHubRoot() == std::filesystem::temp_directory_path() / "automix_tests_modelhub"); +} + +TEST_CASE("Legacy model hub migrates with its registry and licence consents", "[ai][model-storage]") { + const auto base = std::filesystem::temp_directory_path() / "automix_modelhub_migration"; + std::filesystem::remove_all(base); + const auto legacy = base / "assets" / "modelhub"; + const auto target = base / "LocalAppData" / "modelhub"; + const auto writeText = [](const std::filesystem::path& path, const std::string& text) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream(path, std::ios::binary) << text; + }; + const auto readJson = [](const std::filesystem::path& path) { + std::ifstream in(path); + return nlohmann::json::parse(in); + }; + + writeText(legacy / "packA" / "model.json", "{}"); + writeText(legacy / "packB" / "model.json", R"({"from": "legacy"})"); + writeText(legacy / "install_registry.json", + R"([{"modelId": "huggingface:a", "installPath": "assets/modelhub/packA"}, + {"modelId": "huggingface:b", "installPath": "C:\\old\\place\\packB\\"}])"); + writeText(legacy / "license_consents.json", R"([{"modelId": "huggingface:a", "license": "CC BY-NC 4.0"}])"); + writeText(legacy / "install_log.jsonl", "{\"event\": \"legacy\"}\n"); + // The target already has its own packB and records: those must win. + writeText(target / "packB" / "model.json", R"({"from": "target"})"); + writeText(target / "install_registry.json", + nlohmann::json::array({{{"modelId", "huggingface:b"}, {"installPath", (target / "packB").string()}}}).dump()); + writeText(target / "license_consents.json", R"([{"modelId": "huggingface:c", "license": "unknown"}])"); + writeText(target / "install_log.jsonl", "{\"event\": \"target\"}\n"); + + const auto migration = ai::migrateModelHub(legacy, target); + INFO(migration.error); + REQUIRE(migration.attempted); + REQUIRE(migration.error.empty()); + REQUIRE(migration.movedPacks == 1); + REQUIRE(migration.keptInPlace == std::vector{"packB"}); + REQUIRE(std::filesystem::exists(target / "packA" / "model.json")); + REQUIRE_FALSE(std::filesystem::exists(legacy / "packA")); + REQUIRE(readJson(target / "packB" / "model.json").at("from") == "target"); + + const auto registry = readJson(target / "install_registry.json"); + REQUIRE(registry.size() == 2); + for (const auto& entry : registry) { + const auto id = entry.at("modelId").get(); + INFO(id); + REQUIRE(std::filesystem::path(entry.at("installPath").get()) == + target / (id == "huggingface:a" ? "packA" : "packB")); + } + const auto consents = readJson(target / "license_consents.json"); + REQUIRE(consents.size() == 2); // c from the target, a carried over from the legacy hub + std::ifstream log(target / "install_log.jsonl"); + const std::string logText((std::istreambuf_iterator(log)), std::istreambuf_iterator()); + REQUIRE(logText.find("target") != std::string::npos); + REQUIRE(logText.find("legacy") != std::string::npos); + REQUIRE(std::filesystem::exists(legacy / "MIGRATED.txt")); + + REQUIRE_FALSE(ai::migrateModelHub(legacy, target).attempted); // once only + REQUIRE_FALSE(ai::migrateModelHub(base / "missing", target).attempted); + log.close(); + std::filesystem::remove_all(base); +} + +TEST_CASE("Hub containment check refuses look-alike and escaping paths", "[ai][model-storage]") { + const auto hub = std::filesystem::temp_directory_path() / "automix_contain" / "modelhub"; + REQUIRE(ai::isInsideDirectory(hub / "huggingface_x", hub)); + REQUIRE(ai::isInsideDirectory(hub / "a" / "b", hub)); + REQUIRE_FALSE(ai::isInsideDirectory(hub, hub)); + REQUIRE_FALSE(ai::isInsideDirectory(hub.parent_path() / "modelhub2" / "x", hub)); + REQUIRE_FALSE(ai::isInsideDirectory(hub / ".." / "elsewhere", hub)); + REQUIRE_FALSE(ai::isInsideDirectory(std::filesystem::temp_directory_path(), hub)); +} +namespace { + +// Open-Unmix geometry shrunk to a test: 16384-sample chunks (17 frames at hop 1024). +ai::RunnerConfig umxTestConfig() { + ai::RunnerConfig config; + config.chunkSamples = 16384; + config.overlapSamples = 4096; + config.stft.nFft = 4096; + config.stft.hopLength = 1024; + config.stft.winLength = 4096; + config.stft.center = true; + config.stft.normalized = false; + config.stft.zeroDc = false; + config.inputLayout = ai::InputLayout::MagnitudeChannels; + config.outputMode = ai::OutputMode::RatioMask; + config.targetStem = "vocals"; + config.stems = {{"vocals", ""}, {"instrumental", "vocals"}}; + return config; +} + +// A graph that answers with `gain` times the input magnitude. +FakeTensorInference scaledMagnitudeGraph(const float gain) { + const ai::TensorSpec input{"magnitude", ai::TensorElementType::Float32, {1, 2, 2049, 17}}; + const ai::TensorSpec output{"vocals_magnitude", ai::TensorElementType::Float32, {1, 2, 2049, 17}}; + return FakeTensorInference({input}, {output}, [output, gain](const std::vector& bindings) { + ai::TensorInferenceResult result; + result.usedModel = true; + ai::Tensor tensor; + tensor.spec = output; + tensor.data = bindings.front().data; + for (auto& value : tensor.data) { + value *= gain; + } + result.outputs.push_back(std::move(tensor)); + return result; + }); +} + +} // namespace + +TEST_CASE("Ratio-mask runner keeps the mix phase and applies the magnitude ratio", "[ai][tensor][umx]") { + const auto mix = makeTestSignal(2, 40000, 44100.0); + const auto config = umxTestConfig(); + REQUIRE(ai::validateRunnerConfig(config).empty()); + + // Half the magnitude: vocals = mix / 2, and the residual instrumental is the other half. + auto half = scaledMagnitudeGraph(0.5f); + const auto halved = ai::SeparationRunner::separate(mix, half, config); + INFO(halved.logMessage); + REQUIRE(halved.usedModel); + REQUIRE(halved.stemNames == std::vector{"vocals", "instrumental"}); + const auto& vocals = halved.stemAudio[0]; + const auto& instrumental = halved.stemAudio[1]; + float worstVocal = 0.0f; + float worstSum = 0.0f; + for (int ch = 0; ch < 2; ++ch) { + for (int i = 0; i < mix.getNumSamples(); ++i) { + worstVocal = std::max(worstVocal, std::abs(vocals.getSample(ch, i) - 0.5f * mix.getSample(ch, i))); + worstSum = std::max(worstSum, std::abs(vocals.getSample(ch, i) + instrumental.getSample(ch, i) - mix.getSample(ch, i))); + } + } + REQUIRE(worstVocal < 2.0e-3f); + REQUIRE(worstSum < 2.0e-3f); + + // An estimate above the input is clipped to a mask of 1: vocals = mix. + auto loud = scaledMagnitudeGraph(3.0f); + const auto clipped = ai::SeparationRunner::separate(mix, loud, config); + REQUIRE(clipped.usedModel); + REQUIRE(maxAbsDifference(clipped.stemAudio[0], mix) < 2.0e-3f); + + // The graph is fed magnitudes, so no value is negative. + REQUIRE_FALSE(half.lastBindings.empty()); + REQUIRE(*std::min_element(half.lastBindings.front().data.begin(), half.lastBindings.front().data.end()) >= 0.0f); + REQUIRE(half.lastBindings.front().data.size() == 2u * 2049u * 17u); +} + +TEST_CASE("Magnitude layout and ratio mask only work together", "[ai][tensor][umx]") { + auto config = umxTestConfig(); + config.outputMode = ai::OutputMode::Mask; + REQUIRE_FALSE(ai::validateRunnerConfig(config).empty()); + config = umxTestConfig(); + config.inputLayout = ai::InputLayout::SplitChannels; + REQUIRE_FALSE(ai::validateRunnerConfig(config).empty()); + REQUIRE(ai::inputLayoutFromString("magnitude_channels") == std::optional(ai::InputLayout::MagnitudeChannels)); + REQUIRE(ai::outputModeFromString("ratio_mask") == std::optional(ai::OutputMode::RatioMask)); +} + +TEST_CASE("Open-Unmix catalog entry is a curated separation pack with a valid contract", "[ai][tensor][catalog][umx]") { + const auto curated = ai::curatedModelIds(); + REQUIRE(std::find(curated.begin(), curated.end(), std::string(ai::kUmxVocalsRepoId)) != curated.end()); + + ai::HubModelInfo info; + info.repoId = ai::kUmxVocalsRepoId; + info.tags = {"onnx", "open-unmix", "music-source-separation", "vocals", "license:mit"}; + info.files = {"model.onnx", "LICENSE", "README.md"}; + info.primaryFile = ai::primaryFileForRepo(info.repoId, info.files, &info.hasOnnx); + REQUIRE(info.primaryFile == ai::kUmxVocalsFile); + info.useCase = ai::HuggingFaceModelHub::inferUseCase(info.repoId, info.tags, ""); + const auto compatibility = ai::validateCatalogModel(info); + REQUIRE(compatibility.compatible); + // "MixDirective" must not make this a mix model. + REQUIRE(compatibility.taskScope == "separation"); + + const auto contract = ai::umxVocalsCatalogContract(); + const std::vector inputs{{"magnitude", ai::TensorElementType::Float32, {1, 2, 2049, 431}}}; + const std::vector outputs{{"vocals_magnitude", ai::TensorElementType::Float32, {1, 2, 2049, 431}}}; + std::string error; + REQUIRE(ai::checkTensorContract(contract, inputs, outputs, error)); + REQUIRE(error.empty()); + REQUIRE(ai::runnerConfigFromContract(contract, error).has_value()); +} diff --git a/tests/unit/TestMain.cpp b/tests/unit/TestMain.cpp index ab474b9..4ad8394 100644 --- a/tests/unit/TestMain.cpp +++ b/tests/unit/TestMain.cpp @@ -1 +1,31 @@ // Catch2 main is provided by Catch2::Catch2WithMain. + +#include +#include +#include + +#include +#include + +namespace { + +// Downloaded models default to the user's real profile +// (%LOCALAPPDATA%\AutoMixMaster\modelhub). Point every test at a throwaway +// hub instead, so test runs can never write fake packs or consents there. +class IsolatedModelHub final : public Catch::EventListenerBase { + public: + using Catch::EventListenerBase::EventListenerBase; + + void testRunStarting(const Catch::TestRunInfo&) override { + const auto root = std::filesystem::temp_directory_path() / "automix_tests_modelhub"; +#if defined(_WIN32) + _putenv_s("AUTOMIX_MODEL_HUB_ROOT", root.string().c_str()); +#else + setenv("AUTOMIX_MODEL_HUB_ROOT", root.string().c_str(), 1); +#endif + } +}; + +} // namespace + +CATCH_REGISTER_LISTENER(IsolatedModelHub) diff --git a/tests/unit/TorchStftGolden.h b/tests/unit/TorchStftGolden.h new file mode 100644 index 0000000..a3cbe78 --- /dev/null +++ b/tests/unit/TorchStftGolden.h @@ -0,0 +1,2128 @@ +#pragma once + +// Golden fixture for the "Golden torch.stft fixture" test in TensorInferenceTests.cpp. +// Generated with torch 2.14.0+cu130 (CPU), float32 bit patterns, planes laid out [bin][frame] +// (129 bins x 65 frames), by exactly: +// +// x = torch.arange(4096, dtype=torch.float32) / 4096.0 +// w = torch.hann_window(256, periodic=True) +// S = torch.stft(x, n_fft=256, hop_length=64, win_length=256, window=w, +// center=True, pad_mode='reflect', normalized=False, return_complex=False) +// real = S[..., 0], imag = S[..., 1] +// +// (return_complex=False is deprecated but still accepted by this torch version; +// the planes were verified bit-identical to torch.view_as_real of return_complex=True.) +// Do not regenerate with any other STFT implementation. + +#include + +namespace torch_stft_golden { + +inline constexpr int kFreqBins = 129; +inline constexpr int kFrames = 65; + +inline constexpr std::uint32_t kReal[] = { + 0x3f983decu, 0x40060f7au, 0x40800000u, 0x40c00000u, 0x41000000u, 0x41200000u, 0x41400000u, 0x41600000u, + 0x41800000u, 0x41900000u, 0x41a00000u, 0x41b00000u, 0x41c00000u, 0x41d00000u, 0x41e00000u, 0x41f00000u, + 0x42000000u, 0x42080000u, 0x42100000u, 0x42180000u, 0x42200000u, 0x42280000u, 0x42300000u, 0x42380000u, + 0x42400000u, 0x42480000u, 0x42500000u, 0x42580000u, 0x42600000u, 0x42680000u, 0x42700000u, 0x42780000u, + 0x42800000u, 0x42840000u, 0x42880000u, 0x428c0000u, 0x42900000u, 0x42940000u, 0x42980000u, 0x429c0000u, + 0x42a00000u, 0x42a40000u, 0x42a80000u, 0x42ac0000u, 0x42b00000u, 0x42b40000u, 0x42b80000u, 0x42bc0000u, + 0x42c00000u, 0x42c40000u, 0x42c80000u, 0x42cc0000u, 0x42d00000u, 0x42d40000u, 0x42d80000u, 0x42dc0000u, + 0x42e00000u, 0x42e40000u, 0x42e80000u, 0x42ec0000u, 0x42f00000u, 0x42f40000u, 0x42f80000u, 0x42fbcc8cu, + 0x42fd8ee8u, 0xbe41ef6bu, 0xbf722f72u, 0xc0000000u, 0xc0400000u, 0xc07fffffu, 0xc0a00000u, 0xc0c00000u, + 0xc0dfffffu, 0xc1000000u, 0xc1100000u, 0xc1200000u, 0xc1300000u, 0xc13fffffu, 0xc1500000u, 0xc1600000u, + 0xc1700000u, 0xc1800000u, 0xc1880000u, 0xc1900000u, 0xc197ffffu, 0xc1a00000u, 0xc1a80000u, 0xc1b00000u, + 0xc1b80000u, 0xc1bfffffu, 0xc1c80000u, 0xc1d00000u, 0xc1d80001u, 0xc1e00000u, 0xc1e80000u, 0xc1f00000u, + 0xc1f80000u, 0xc1ffffffu, 0xc2040000u, 0xc207ffffu, 0xc20c0000u, 0xc20fffffu, 0xc2140000u, 0xc2180000u, + 0xc21c0000u, 0xc2200000u, 0xc2240000u, 0xc2280000u, 0xc22c0000u, 0xc2300000u, 0xc2340000u, 0xc2380000u, + 0xc23c0000u, 0xc2400000u, 0xc2440000u, 0xc2480000u, 0xc24bffffu, 0xc2500000u, 0xc2540000u, 0xc2580000u, + 0xc25c0000u, 0xc2600000u, 0xc2640000u, 0xc2680000u, 0xc26c0000u, 0xc2700000u, 0xc2740000u, 0xc2780000u, + 0xc27c3971u, 0xc27f2dd1u, 0xbee69533u, 0xbcb8484du, 0x33b1a2f6u, 0x340a48d9u, 0x34314036u, 0x342243eau, + 0x34413efbu, 0x3472b2a5u, 0x3472ac7au, 0x3444bc83u, 0x34955952u, 0x34ac0ed6u, 0x34b9c949u, 0x34c691ecu, + 0x34e140a7u, 0x34ef06d2u, 0x34e988acu, 0x350f1c51u, 0x34b74cfdu, 0x34fa480eu, 0x34f29501u, 0x3516a0f1u, + 0x35261ac7u, 0x353cbac7u, 0x3522984fu, 0x35157c02u, 0x352e79dau, 0x3522c22du, 0x355a2d84u, 0x3586c5aeu, + 0x35469a78u, 0x353cf713u, 0x3571109au, 0x3563f04bu, 0x356fdff3u, 0x35786a6fu, 0x3581b5aeu, 0x3582464cu, + 0x359cd3fcu, 0x355cf3feu, 0x356e0ce6u, 0x35ab4271u, 0x35b50b0au, 0x35988184u, 0x35c8f893u, 0x35a30ce4u, + 0x35a2bf34u, 0x35b19909u, 0x35c7957du, 0x35b8eabbu, 0x35acaa1du, 0x35af055fu, 0x35abed33u, 0x35c0a6b9u, + 0x35b95fd2u, 0x359c60bfu, 0x35ceb980u, 0x35fa0105u, 0x35b64bd0u, 0x35cf666au, 0x3600cdd0u, 0x35ca3931u, + 0x35cdc4b8u, 0x3cd4779bu, 0x3ee6756cu, 0x3db88870u, 0xbd668fd8u, 0x33054f98u, 0x3025ef04u, 0x334e25fau, + 0x330bd25fu, 0x333e1554u, 0x336eb3d9u, 0x33b30947u, 0x33958286u, 0xb32c5d2bu, 0x33d5c0f4u, 0x33a24813u, + 0x3412eaf9u, 0x33ca6a92u, 0x33ee71e1u, 0x33763f51u, 0x3319c421u, 0x3493cc1fu, 0x33a21f79u, 0x34119642u, + 0x34271effu, 0x340549aeu, 0xb2be068cu, 0x3496fe4cu, 0x3370f72du, 0x3297e811u, 0x33cac73du, 0x34191612u, + 0x3498dad4u, 0xb403b9e2u, 0xb3310bf7u, 0x34761e2au, 0x34b93819u, 0xb3a6e0e5u, 0x3457f003u, 0x3352f72du, + 0xb3f2103fu, 0x33aff4d6u, 0x34f489e7u, 0x3481e652u, 0x338435acu, 0x34cba8bfu, 0x34c13de7u, 0x341d48cdu, + 0x34ecc924u, 0x34752439u, 0x33e508a0u, 0xb4cedf99u, 0x33c1a210u, 0xb352aa7du, 0x350fa40au, 0x34eaff6eu, + 0x34b0ee99u, 0x34e6accbu, 0x357b2880u, 0x35089cdcu, 0x345728a9u, 0x3118f95au, 0x351ce407u, 0x3539b370u, + 0x35361505u, 0x3522e4d6u, 0x3d74239bu, 0xbdb8086eu, 0xbd7b04a9u, 0xbcfb04aeu, 0xb1c26307u, 0x328509f6u, + 0x31cf4ee4u, 0xb2fd2203u, 0xb2e0ec2du, 0xb3310c13u, 0x31c173e9u, 0x33301392u, 0xb29a1c46u, 0xb3dd0fccu, + 0x331c69dcu, 0xb3d1e352u, 0xb4085420u, 0xb38b7fc6u, 0xb48fec67u, 0xb45aa600u, 0xb3e70555u, 0xb38b4adeu, + 0xb413a05au, 0xb48072f9u, 0xb3c59e7du, 0x34032901u, 0x32b3e2ebu, 0xb424dfe3u, 0xb41477d3u, 0x3232050cu, + 0xb42fef0bu, 0xb41372f1u, 0x338022d8u, 0xb493750eu, 0xb3ada88eu, 0xb523b6b7u, 0xb5537b71u, 0xb54e593fu, + 0xb5285c9fu, 0xb534be16u, 0xb5062c50u, 0xb5095281u, 0xb40264d1u, 0xb4e1c97fu, 0xb51da90du, 0xb4b311f8u, + 0xb414c189u, 0xb1dc23d1u, 0xb4b5a0a4u, 0xb498b11au, 0xb4d8688du, 0xb450b88au, 0xb4c1f952u, 0xb4dfd1e6u, + 0xb40ceff9u, 0xb31b19dcu, 0xb41ab5fcu, 0xb475ca5du, 0xb40cc019u, 0x33e4e907u, 0xb438a2e1u, 0xb511533cu, + 0xb4249a71u, 0xb4855127u, 0xb2a39571u, 0x3cf49569u, 0x3d7a0466u, 0x3d04f873u, 0x3ba250b9u, 0x313e4234u, + 0x3288d98cu, 0x32a9598cu, 0xb2cee125u, 0xb28d6214u, 0xb1be1adcu, 0x331be6ffu, 0x329e7e2eu, 0xb258f25eu, + 0xb144ced9u, 0xb38d1904u, 0xb40092bdu, 0x337adeb8u, 0xb3b88bb7u, 0x318543a5u, 0xb3b9ef98u, 0x33fa60ebu, + 0xb43acf1bu, 0x333c4a88u, 0x33f6139fu, 0x33b0cfdbu, 0x33777836u, 0x339d72f3u, 0x349b7ff4u, 0xb408ecf2u, + 0xb30223fau, 0x33925708u, 0xb30ec498u, 0xb3d63e02u, 0xb40d81ebu, 0x341db979u, 0xb39ddd12u, 0xb430b6a1u, + 0x34249a1au, 0xb2ce0cc5u, 0xb38db95fu, 0xb469cc69u, 0x33e93ee9u, 0xb0bc7764u, 0x34593f1eu, 0x345844a9u, + 0x30a590f0u, 0x34656466u, 0xb4ab5b82u, 0xb2bfcb6du, 0x3486fbe6u, 0xb3fca8d1u, 0xb4888ce4u, 0xb3af0c10u, + 0x34443cefu, 0xb394d915u, 0x33e91cb9u, 0xb4a6c04au, 0x348b48beu, 0x3416f4f3u, 0xb40a7aa1u, 0x34ae3c8au, + 0x350667c2u, 0x33d27bffu, 0xb4b7bea8u, 0xb491ef2cu, 0xbbe37106u, 0xbd03f884u, 0xbcc8e4f4u, 0x3c28ac15u, + 0x31ebda75u, 0x32ab55e8u, 0x3225ac5bu, 0x32826b34u, 0x32f1babeu, 0x331fb134u, 0x32f8f228u, 0x336cff22u, + 0x33d848d8u, 0x33c1f550u, 0x31aff290u, 0xb0cb8380u, 0x3384453fu, 0x332842b8u, 0x339448b9u, 0x3429c3c1u, + 0x331f5d89u, 0x339cff4fu, 0x33a55459u, 0x32fb244eu, 0x334536c6u, 0x32b45191u, 0xb3a1f153u, 0x32aa2758u, + 0xb191a65du, 0xb246e06fu, 0x33827647u, 0xb46253deu, 0x3416e5d5u, 0xb34ad798u, 0xb319e012u, 0xb3f9dec6u, + 0x34654992u, 0xb3cfb172u, 0x3422a165u, 0x3492dddfu, 0x347d1a67u, 0xb48634e9u, 0x34a0b0d7u, 0x3317ce44u, + 0x331a4e27u, 0x34c5bf79u, 0x34161e91u, 0x349711c3u, 0xb3fb1094u, 0x3485101fu, 0xb36d713du, 0xb12ba73eu, + 0x34a0865du, 0x347c01e8u, 0x347b9fe6u, 0x34a4f4afu, 0x341fb888u, 0x34e40db1u, 0x34d978b6u, 0x34850d9cu, + 0xb419c14du, 0xb487928fu, 0x34acd44fu, 0x34a1992fu, 0xb2d583bbu, 0xbc22000au, 0x3cc6e5a0u, 0x3c87d90cu, + 0xbb43bdfbu, 0xb2c62aa2u, 0x31742c9du, 0x337e5534u, 0xb15b95f0u, 0x317e74f4u, 0x3229d34bu, 0xb1a88e18u, + 0xb35e9660u, 0xb22c9d28u, 0x341367cfu, 0xb36710a9u, 0x32501eacu, 0x33df950au, 0x34480d92u, 0xb22c1a1bu, + 0x33b3dd0bu, 0xb302c489u, 0xb3887f51u, 0xb1077cb7u, 0x34c3f5c0u, 0xb44b14e8u, 0x30c5a3bfu, 0x3424784au, + 0x33f88771u, 0xb2ce0789u, 0xb2f8ace5u, 0x33b350e0u, 0x3394dbeeu, 0xb34f9bafu, 0x34df4e14u, 0x334158a2u, + 0xb30abfbau, 0x34b4ab43u, 0x345c04fcu, 0x333c1b50u, 0xb3ab65fcu, 0xb4a58364u, 0xb3bfc7eau, 0xb41fc0fau, + 0x33af53c0u, 0xb3e1ff15u, 0x34613560u, 0x34d069b0u, 0xb472b911u, 0xb40f9c69u, 0x34a804cdu, 0x341f46dau, + 0xb47c08e9u, 0x34a66d88u, 0x346a5ed5u, 0xb3714e71u, 0xb4753c5bu, 0x32dea6bdu, 0xb3b01fc3u, 0x33dd9ddeu, + 0xb566b37eu, 0x33b40174u, 0xb4f99b21u, 0x34b906d0u, 0xb0ed5dbfu, 0x34b22d88u, 0x3b904d7cu, 0xbc85d825u, + 0xbc5a28d1u, 0xbbda28bbu, 0x330b0ccbu, 0xb21957acu, 0x33a045d3u, 0xb2f4d963u, 0x337309d0u, 0x336175c0u, + 0x33bba841u, 0xb2f88f95u, 0x339f7b6bu, 0xb27e97d5u, 0x33ec5652u, 0x32ee9976u, 0x32461b54u, 0x342a9204u, + 0x33dd66a3u, 0x3316218cu, 0xb3afd3b2u, 0xb4098a72u, 0x34840e75u, 0x349de3dbu, 0xb42f0496u, 0x34b7e6efu, + 0x332daf0cu, 0x34d62070u, 0x34281b94u, 0x34b3bd3eu, 0x344b25e9u, 0x3494879bu, 0x34f01fdbu, 0x34aa7a98u, + 0x34dfacf4u, 0xb48b7b43u, 0x34d0779cu, 0x3477b2d5u, 0x3415c6d5u, 0x340fa1d4u, 0x34b135f1u, 0x34a9f68au, + 0x3388bdaau, 0xb3893391u, 0x342235f1u, 0x3232fcf0u, 0xb33d7f07u, 0xb4a5cd8eu, 0xb439fc08u, 0x33d9b88du, + 0x342459b8u, 0xb4ab6819u, 0x33e4d49bu, 0x34da7f1cu, 0xb5123e0cu, 0xb52519b5u, 0xb3e7edf7u, 0x3276c46au, + 0xb5259588u, 0xb4b4f658u, 0x342803f8u, 0x34d1291cu, 0xb4bcef11u, 0xb4c68263u, 0x34546a4eu, 0x3bd0f7b1u, + 0x3c56275du, 0x3c249fa0u, 0x3a7935cdu, 0xb1589b5bu, 0xb2375b2au, 0x3196f05du, 0xb204409cu, 0x33693a3du, + 0x3324503bu, 0x3329b490u, 0xb261fd3cu, 0x338f4002u, 0xb29577a1u, 0xb204ebd4u, 0xb3a2350eu, 0xb323f211u, + 0x343e1469u, 0x333b04c9u, 0x3399dbfeu, 0xb3222b61u, 0xb3161ac6u, 0xb311220du, 0x34de4a5eu, 0x33f558c6u, + 0x330be16bu, 0xb41859e3u, 0xb35d00fau, 0x33990abdu, 0xb4108512u, 0xb3df905au, 0x346ebb17u, 0x348783afu, + 0x33512162u, 0x335ca299u, 0x34e715afu, 0x35104f5eu, 0x34834b3du, 0xb49a0caau, 0xb32c6ebbu, 0xb4a5396au, + 0xb4b352cbu, 0xb532fcd6u, 0xb385e9fcu, 0x34809746u, 0x34ad2285u, 0x34b8ddddu, 0xb232af68u, 0x33a5fd52u, + 0xb37ffb44u, 0xb405590eu, 0x35033c03u, 0xb4086138u, 0xb3a531a2u, 0xb35ee485u, 0x342fd964u, 0x330451b9u, + 0xb5269d75u, 0xb4e01a9du, 0x3407f85fu, 0xb530bd83u, 0xb42412b2u, 0x35331917u, 0xb4759e03u, 0xb4a37680u, + 0xbb0662f2u, 0xbc209ee4u, 0xbc09861fu, 0x3b816bb1u, 0x31a8bac9u, 0xb26c70eeu, 0xb31ac90du, 0x32e59c1fu, + 0xb1c1c088u, 0x324a0ad3u, 0xb34b3f30u, 0x33266680u, 0xb10b73cbu, 0x32b33440u, 0xb2925645u, 0x32166938u, + 0x335591f4u, 0xb2e1abc8u, 0x322ec724u, 0xb4126a53u, 0x339df312u, 0xb45d229au, 0xb35d4057u, 0xb3f10882u, + 0xb4470b47u, 0x321ed74cu, 0xb41d2433u, 0xb3acb427u, 0xb40a99bbu, 0xb3a05a4fu, 0x32a9c6f3u, 0x33057fd3u, + 0x337fc64du, 0x33309ebeu, 0x33891766u, 0xb2c8368fu, 0x34538d6cu, 0x3401efbbu, 0x3357d6e7u, 0x343c683cu, + 0x345577c2u, 0x31c0c7efu, 0xb4584991u, 0xb39fb9d3u, 0xb3ab6097u, 0xb4a60232u, 0x3411970au, 0xb32080c5u, + 0xb267ab7fu, 0xb3d8c715u, 0xb23a7e09u, 0x347925b4u, 0x33f30da3u, 0xb2b54b44u, 0x3403c562u, 0x3230df65u, + 0xb3fe9517u, 0x34244a9du, 0x33e43856u, 0xb49ab3d6u, 0xb3e03bc5u, 0xb408187fu, 0xb4cf93cdu, 0xb381ac84u, + 0x34290b2fu, 0xbb7437b3u, 0x3c058700u, 0x3bdcd93au, 0xba386254u, 0x3287c4b4u, 0x3204279fu, 0x32cc5109u, + 0x330c870bu, 0x326d88b5u, 0xb2d964fdu, 0x3369b016u, 0xb37e984fu, 0x329871bcu, 0x3366d27eu, 0x33f2a0cfu, + 0x32f9e7f9u, 0xb2aac93eu, 0x33b6ee3cu, 0x33949b5bu, 0x346a698cu, 0xb3a414ddu, 0xb42c05eau, 0x339e9b78u, + 0x339e2bf4u, 0x33f3a73eu, 0x3483d0fdu, 0x32f5bee0u, 0x3463c466u, 0x3425dd7du, 0x3484fd44u, 0xb2c7f36cu, + 0x34c6f75au, 0xb2b014cdu, 0x340d7effu, 0x34129ab0u, 0x339d4e95u, 0x34915addu, 0x34d18d41u, 0x33496726u, + 0xb33a7f3fu, 0x340a55e3u, 0xb41e4dc8u, 0x34ee5ee5u, 0x34a91c27u, 0xb3f22b4fu, 0x352f945au, 0x33facfa3u, + 0x345afc7eu, 0x345046a9u, 0xb10c243bu, 0x350eeb80u, 0x3409e47fu, 0x33b9d4b5u, 0x34aad69au, 0xb4a03d3cu, + 0xb3e5c51au, 0x34848219u, 0x34d3f797u, 0xb437648au, 0x35110277u, 0x350a7047u, 0x35200753u, 0xb3c73f48u, + 0x340f99b9u, 0x354b1f18u, 0x3ad1ff62u, 0xbbd4d713u, 0xbbbdad23u, 0xbb3dad34u, 0xb10c2e84u, 0xb2558428u, + 0x32af0771u, 0xb281b81au, 0xb30b9400u, 0xb2a74288u, 0xb3896722u, 0xb39b378fu, 0x339624bbu, 0x33b36ad0u, + 0x331f450au, 0xb405989eu, 0xb43725c4u, 0xb45f141eu, 0x3344ce25u, 0xb4c1f28fu, 0xb4a95da9u, 0xb360e897u, + 0xb4affd78u, 0x31f15b7fu, 0x33e33296u, 0xb4168154u, 0xb4999e18u, 0xb4943d5fu, 0xb473fe01u, 0xb4356d7au, + 0x3289eb30u, 0xb399098cu, 0xb4901172u, 0xb422969eu, 0xb4c857b8u, 0xb51653fbu, 0xb445e18eu, 0x340d550du, + 0xb4ac09c6u, 0xb4b2c1fcu, 0xb44b7d34u, 0xb4c4fd1eu, 0xb526a2c4u, 0xb490604eu, 0xb51ea0b9u, 0xb530084eu, + 0xb4c99faau, 0xb3b3a00au, 0xb4d0258fu, 0xb3951495u, 0xb56291a8u, 0xb4b23709u, 0xb5323fafu, 0xb4aa24e5u, + 0xb500fe6cu, 0xb33d9b98u, 0xb445e3ebu, 0xb4893c7cu, 0xb48bc8aeu, 0xb50711fbu, 0xb518a19du, 0xb4c17060u, + 0xb5134430u, 0xb4cc2316u, 0xb4b3eb35u, 0x3b3110fdu, 0x3bb5b03du, 0x3b9e811cu, 0x39ad3233u, 0x327418d6u, + 0x32652240u, 0x331a3658u, 0x334d4cd5u, 0x32de2004u, 0x31d5b963u, 0x339669a2u, 0x32badb43u, 0x341808acu, + 0x301eeaecu, 0x34319e5eu, 0x3413b6d0u, 0xb184b66fu, 0x3426d1fcu, 0x32ecada2u, 0x3471052cu, 0x33191abbu, + 0x341e99dau, 0x33ee1ef2u, 0xb2deb274u, 0x3456acddu, 0x33c8a53au, 0x34294daeu, 0x327d6459u, 0x33c0a113u, + 0x34ab54b1u, 0x33440f9du, 0x33e882e2u, 0x348f9b65u, 0x32da22b0u, 0x341d2507u, 0x34007ca0u, 0x3473e318u, + 0x346b7421u, 0x34ab9578u, 0x342272ffu, 0x339fabb5u, 0x34b23a3bu, 0x3483d983u, 0xb4217872u, 0x35257291u, + 0xb38ac292u, 0x345e8f95u, 0x34a7aef6u, 0x341e60d7u, 0xb3bb2536u, 0x350f3d7eu, 0xb29b335au, 0x34886060u, + 0x33eab25bu, 0x34d086b3u, 0x324394e2u, 0x334fc79eu, 0x34b741b2u, 0x32881d31u, 0x34d27d8du, 0x3593ee13u, + 0x320be4d3u, 0x316b7f97u, 0xb3da15a7u, 0x3433dc4cu, 0xba8ea941u, 0xbb967be3u, 0xbb8af255u, 0x3b06c37du, + 0xb059ad27u, 0x322e04c7u, 0x33228e2du, 0x32968bcfu, 0x32b6f3dfu, 0x33171048u, 0xb1a124f0u, 0x33445ecfu, + 0x3392b49eu, 0x33b40ee1u, 0x335594d9u, 0x32965daau, 0xb18dc0b6u, 0x33e819c8u, 0x33dd82f3u, 0x3300a8d9u, + 0xb3c3825du, 0x3270c622u, 0x33117743u, 0x342f3f32u, 0x3310c508u, 0x33e81a66u, 0x340e57cau, 0x31513a08u, + 0x33e8ceb0u, 0x33922ab3u, 0xb403466du, 0x33a4b008u, 0xb37f2188u, 0x341f1846u, 0xb342888du, 0x34955798u, + 0x320435ccu, 0xb3aefcb6u, 0x3416bf3du, 0x3468b4a2u, 0x34b4ee97u, 0x344a692cu, 0xb4376d7bu, 0xb3d548e0u, + 0x346163d0u, 0x3471704eu, 0x33d51847u, 0x34971441u, 0x33a0f7a3u, 0xb2b2a4bbu, 0x34d8a3fau, 0x349af6a8u, + 0x344dcb65u, 0x349d5c8fu, 0x3311d31au, 0x34d37ecbu, 0x349d7c42u, 0x353b0f9fu, 0x3476ea0au, 0x34e02b3eu, + 0x341872a1u, 0x34c575a9u, 0x34ce892fu, 0x34f72a25u, 0x35298ac6u, 0xbaf6bb86u, 0x3b82f143u, 0x3b6ec7d3u, + 0xb98bb70au, 0xb2a486dau, 0xb304cca9u, 0xb2e4d884u, 0xb21c9a08u, 0xb35cc5e4u, 0x33546b57u, 0xb3fcbe3au, + 0xb3c51135u, 0xb48c2e67u, 0x33b98b69u, 0x32d403f1u, 0xb38979a5u, 0xb47058f6u, 0x33a44590u, 0xb3bf0e62u, + 0x32b69c32u, 0xb4fb328fu, 0xb4d2582cu, 0xb314ab89u, 0x3315e418u, 0x337f3eeeu, 0xb4dd1c85u, 0xb3ade3bau, + 0xb28004beu, 0x33283f03u, 0xb3b60b4cu, 0x3457b1e8u, 0xb50ced50u, 0xb43b33bfu, 0x34258aadu, 0xb4b26d02u, + 0xb47040a6u, 0x3496da8fu, 0xb558e385u, 0xb36ad8e8u, 0xb472eff5u, 0xb50744f9u, 0xb4fac6bau, 0xb48534ecu, + 0xb54571edu, 0xb4fa3c23u, 0xb49eef49u, 0xb563464cu, 0xb49733acu, 0xb2e44e69u, 0xb518aa7du, 0xb4f16649u, + 0x34f33c29u, 0x332f493bu, 0xb56a3b22u, 0xb4dd8328u, 0x34b85f63u, 0xb3597d82u, 0xb4a898ddu, 0x3454a950u, + 0xb51646b9u, 0xb54f0ef1u, 0x3380c1ddu, 0xb54abe3eu, 0xb591679fu, 0xb56dc76cu, 0x3a7200cfu, 0xbb5ec28eu, + 0xbb54a42du, 0xbad4a4e1u, 0xb2020becu, 0x33a17c20u, 0x3389e031u, 0x331980fdu, 0x33f3fe2du, 0x3392062du, + 0x32b6ff71u, 0xb2a9f861u, 0xb48fe4ccu, 0xb435040cu, 0xb40b7e69u, 0xb4d5a20cu, 0xb4b2ff35u, 0xb343f10au, + 0xb4377f6au, 0x348b7e0bu, 0xb41975c7u, 0x34d0c3d5u, 0xb3c3f085u, 0x345d9897u, 0xb26eac76u, 0xb40c6f69u, + 0xb26eac76u, 0x33a61b2du, 0xb3099da5u, 0xb4fdb72bu, 0xb4fdb72bu, 0xb26ebc27u, 0xb4ccbb2bu, 0xb48775e1u, + 0xb505bf59u, 0xb443eef8u, 0xb50b7dbeu, 0xb50b7dbeu, 0xb50b7dbeu, 0xb4b503fcu, 0xb56600beu, 0xac64663au, + 0xb4b50386u, 0xb443eef8u, 0x34261908u, 0xb4b503fcu, 0xb4b503fcu, 0x34261908u, 0xb443eef8u, 0xb3a9e078u, + 0xb443eef8u, 0x34ec8362u, 0x34b50404u, 0xb443eef8u, 0xb53504feu, 0x351fc7f1u, 0xb580dfb6u, 0x348a8be2u, + 0x2a00f780u, 0x33ddfd88u, 0xb50b7dbeu, 0xb3a9e078u, 0x34ec8362u, 0xb46ae83cu, 0x34a18762u, 0x3abf7854u, + 0x3b44b69du, 0x3b3a8052u, 0x391ed6d2u, 0xb311793au, 0xb3331901u, 0xb324e7bau, 0x332fc219u, 0xb3e30594u, + 0xb429cda9u, 0x3195738au, 0xb43a49fdu, 0xb45fa8ffu, 0xb3a19ef8u, 0xb4892cc6u, 0xb4426ffeu, 0xb341ece3u, + 0xb4417f4fu, 0xb41f6ebcu, 0xb3b31d68u, 0xb3d443a7u, 0xb36a74e5u, 0xb41c2aa1u, 0xb425b26du, 0xb4a674cau, + 0xb49f8fd7u, 0xb3469360u, 0xb442bb91u, 0x34275af8u, 0xb4d1d593u, 0x3485289bu, 0xb3c6215cu, 0x330de5aau, + 0xb4a9edb2u, 0xb3cdb9a3u, 0xb51fd21eu, 0xb3ecbd1cu, 0x33ee4cfbu, 0x343f1c58u, 0x31c1d75du, 0x3492e7ddu, + 0x336d1daau, 0x336151d8u, 0xb4e87ab7u, 0xb336c96cu, 0xb50c8e15u, 0xb581a304u, 0xb46eb9efu, 0xb58026d2u, + 0xb4d88a6cu, 0xb1b0df15u, 0xb41ac906u, 0xb4a9f346u, 0xb566db6au, 0x34263c3cu, 0xb4aeba6cu, 0xb562df80u, + 0xb5907d48u, 0xb5a039adu, 0xb4846874u, 0xb55b9595u, 0xb5bd6049u, 0xb2a408b4u, 0xb4793c5cu, 0xb53176bau, + 0xba3efaeeu, 0xbb2a7e5cu, 0xbb282c74u, 0x3aa51f68u, 0x3222cdacu, 0xb1f700e3u, 0xb21dc1b8u, 0x304b2056u, + 0xb26afde0u, 0xb1215f73u, 0x3189134au, 0x3351714eu, 0x3332cc1cu, 0x32b96517u, 0x318e9aa1u, 0xb3b08681u, + 0xb28303efu, 0xb387884cu, 0x33a272ddu, 0xb2b0feccu, 0x33f59d7fu, 0xb3357339u, 0xb33e5675u, 0xb41e07afu, + 0xb280cff3u, 0xb4630a3fu, 0xb3b7dec5u, 0xb3f81eb6u, 0xb456a64eu, 0x327b8953u, 0xb2dcb4d8u, 0x34632f21u, + 0xb30958beu, 0x3317fb16u, 0xb4d9415du, 0xb0de6ea3u, 0xb49bb3dcu, 0xb32e1885u, 0x3318980eu, 0x33895118u, + 0x33028587u, 0x34738873u, 0x3476ac93u, 0xb31fb6e0u, 0x342e493bu, 0x339a397eu, 0xb43c4833u, 0x3433ee5cu, + 0x343f4070u, 0xb34ddb1du, 0x342a7d8bu, 0xb4988f8fu, 0xb5090becu, 0xb3661e83u, 0x33fa0cc7u, 0x34a33201u, + 0xb3e34f93u, 0xb3375871u, 0xb40fa3fau, 0xb33c4513u, 0x342b6277u, 0xb43547dbu, 0x333244fcu, 0x34639e6du, + 0x341c3cd5u, 0xba910c9eu, 0x3b1827fcu, 0x3b15d8c9u, 0xb90663fbu, 0xb0957538u, 0x3358f773u, 0x3263f11cu, + 0x326ee9adu, 0x3253b54au, 0xb1bc53c4u, 0x3341d597u, 0x329050d1u, 0xb34d508cu, 0xb36f4d79u, 0x337ef749u, + 0x3383cae9u, 0x338790beu, 0xb41cdb50u, 0x33bc0366u, 0x3407ec34u, 0x3233ac25u, 0x33d859d1u, 0x33b32e9fu, + 0x331cd354u, 0x327914dbu, 0xb3d30ea5u, 0xb3a20b2du, 0xb39fd4fbu, 0x339c71d4u, 0xb408bec8u, 0xb484fa78u, + 0x33051bd8u, 0x322db200u, 0xb32f3719u, 0xb1d6ecc0u, 0xb3a4f49cu, 0x34dca7b5u, 0x33e3876cu, 0x33fd68d1u, + 0xb4998a93u, 0xb3b51bafu, 0xb318c3f5u, 0xb394dd4fu, 0x32485308u, 0xb39ba71fu, 0x351f3e3fu, 0x33354fffu, + 0x3480373cu, 0xb4995cd9u, 0xb4e46647u, 0x34b845ecu, 0x3413b329u, 0xb3a0b65au, 0x342aa71fu, 0xb4d9ec40u, + 0xb399b1b6u, 0x34a46e9fu, 0x346dfdd7u, 0x34102ad7u, 0xb490d873u, 0xb4b7626au, 0x34f5dffbu, 0x3230378au, + 0x3538a187u, 0xb4ae8c53u, 0x3a28529eu, 0xbb05db13u, 0xbb0880d3u, 0xba888059u, 0x31f44bb4u, 0x31350398u, + 0xb2a2ececu, 0x334a3f22u, 0x33027042u, 0x33abd741u, 0x334cf5eau, 0x33c331f8u, 0xb3a43263u, 0x339feb5bu, + 0x334f0192u, 0x333200b7u, 0xb3d62f13u, 0xb398e442u, 0x341a11a0u, 0xb4a77ab9u, 0x340cb4c3u, 0xb3f116e0u, + 0xb4358ec0u, 0x33be76d1u, 0x33cf0335u, 0xb39ed1e0u, 0x349342b1u, 0x34803ad9u, 0x33df910eu, 0x34212a94u, + 0x34473342u, 0x33c861e4u, 0x34720716u, 0x33a79f77u, 0x3477ffd7u, 0x34a7753bu, 0xb3b19c15u, 0xb4781f60u, + 0x34c8cd70u, 0x33c30c79u, 0xb2a7c81eu, 0x34e6ac34u, 0x341e8ab3u, 0x34ef3d43u, 0x34974696u, 0x340c9ee2u, + 0x3435c096u, 0x351dfd8bu, 0x32f9457du, 0x346d3c9bu, 0x33c60f0du, 0x345d1a5cu, 0xb46b0e9cu, 0x338a5de9u, + 0xb37a8fcbu, 0x34d3ccb5u, 0xb339dfe4u, 0x34586793u, 0x3417a528u, 0xb38c6e15u, 0x34795635u, 0x348b1d8fu, + 0xb326bbc3u, 0x34b1b2e8u, 0xb384c579u, 0x3a6a4affu, 0x3af109f0u, 0x3af651c8u, 0x38ab29c4u, 0x3228813bu, + 0xafef69d4u, 0xb27016c7u, 0xb205557fu, 0xb3393d1bu, 0xb2ec0f89u, 0x3292e6f9u, 0xb2374e51u, 0xb312e9beu, + 0xb43811a8u, 0xb238dc71u, 0xb2e1e1ecu, 0x3295bb54u, 0xb2f31441u, 0x336669ecu, 0x32e90f87u, 0xb1b74f1bu, + 0x33f67f9cu, 0xb383a484u, 0xb38a80f6u, 0xb3e3f6f1u, 0xb35cd925u, 0x33b909abu, 0x32c6aea3u, 0xb374e926u, + 0x33fb8a3du, 0xb465bc38u, 0xb388c687u, 0x3290ee0eu, 0xb2ace241u, 0x337d1765u, 0xb4dd7088u, 0x34761ec1u, + 0x31c11505u, 0xb48255d3u, 0xb4c1c52au, 0x33f62492u, 0xb446a894u, 0xb482a6e3u, 0x341221deu, 0xb4bc856au, + 0xb3d5db10u, 0xb44dd1b0u, 0xb49dfe1bu, 0xb443d9c2u, 0xb42a99a8u, 0xb3de85a5u, 0x328b57a0u, 0xb51b66a4u, + 0xb4188011u, 0xb472da09u, 0xb4983afeu, 0x33cf9308u, 0xb2dec81fu, 0xb4d70b55u, 0xb52044d4u, 0xb42af3e9u, + 0x331350f2u, 0xb3e99db0u, 0xb4ce4badu, 0xb4fc1de3u, 0xba0ed53cu, 0xbad66452u, 0xbae249b5u, 0x3a5f8e50u, + 0xb0ee7dc5u, 0x326ad57bu, 0x3361317bu, 0x3332be34u, 0x331851ceu, 0xb17db714u, 0x33ed41bau, 0x33f4e64du, + 0xb2ea27c8u, 0xb2362712u, 0x341a6521u, 0x33c28fcbu, 0x33491591u, 0x33445fb2u, 0x34422014u, 0xb28f1aeeu, + 0x33a41e7eu, 0x33ef6a88u, 0x334abff3u, 0x33f67cb4u, 0x340cbf19u, 0x34a8479du, 0xb2bf838cu, 0x33795ccau, + 0x32b74db4u, 0xb422096du, 0x33c92216u, 0x3499df7cu, 0xb3659fb8u, 0x34536dd5u, 0x333b6281u, 0x34575440u, + 0xb32dfa92u, 0xb409dfd6u, 0x34f0c884u, 0x333a0889u, 0xb4588e23u, 0x33dcd1a3u, 0x31ff990du, 0x34718911u, + 0x343ac464u, 0x33e06e17u, 0x34221422u, 0x33de27beu, 0x347b1254u, 0x34509583u, 0xb3f93750u, 0x335d17fau, + 0x338b59b5u, 0x34878d02u, 0x333b0aa4u, 0x3413cd21u, 0x349647ebu, 0x34c79973u, 0xb1693932u, 0x343167d4u, + 0x34056871u, 0x34fd7560u, 0x31a62b64u, 0x338490e4u, 0x352967fdu, 0xba3a05ccu, 0x3ac25235u, 0x3ace4187u, + 0xb89554d4u, 0xb27392f1u, 0xb36f2f68u, 0xb3451b69u, 0xb2cccfcau, 0xb39d7f10u, 0xb3a2ab35u, 0xb4351853u, + 0xb41b7c7cu, 0xb39e4ca8u, 0xb4258c8du, 0xb4102dfcu, 0xb387ec14u, 0xb42c49e8u, 0xb443ba5eu, 0xb3ad74e8u, + 0xb457b666u, 0xb40f1218u, 0xb423af52u, 0xb3ca0d2du, 0xb4d958e1u, 0xb4b5d90fu, 0xb3c9b4ffu, 0xb432a4d6u, + 0x3221d521u, 0xb3c24b00u, 0xb3bf15e8u, 0xb4a32bcbu, 0xb3422f00u, 0xb4130184u, 0xb50833cfu, 0xb4433a89u, + 0xb4d42f11u, 0xb4440d4cu, 0xb48a6f2bu, 0xb506cbdeu, 0xb4b6668eu, 0xb5111caeu, 0xb4b3914au, 0xb404c2f5u, + 0xb53fde02u, 0xb4f0c1e2u, 0xb54c89e6u, 0xb5193c5bu, 0xb57ea3cfu, 0xb483542au, 0xb519e471u, 0xb5a6c6e1u, + 0xb53242bfu, 0xb495cc26u, 0xb50f5772u, 0xb4c8c596u, 0xb55475a1u, 0x33671de2u, 0xb55957a3u, 0x343da1c1u, + 0xb50e3a21u, 0xb5893280u, 0xb4edf97bu, 0xb51fec73u, 0xb5483a14u, 0xb4eb28dfu, 0x3a00f14eu, 0xbaae54e4u, + 0xbabed717u, 0xba3ed6b1u, 0xb2a402cdu, 0xb386cad7u, 0x33661e7eu, 0x3195ad50u, 0x32f3eaf5u, 0xb275a422u, + 0xb372ef40u, 0xb35ba9bbu, 0x33ad0e08u, 0x32cd7f95u, 0xb3bdcda8u, 0xb3c67958u, 0x3249bfe3u, 0xb3ade0f8u, + 0x340185e2u, 0x34657beau, 0x33f60677u, 0x3433d940u, 0x3455a79bu, 0x3480410du, 0xb2295013u, 0x347c798au, + 0x32d7924eu, 0x34698348u, 0x341d54e1u, 0x343b11a4u, 0x34b122abu, 0x331ec58eu, 0xb2bb2709u, 0x33b20385u, + 0x3451e382u, 0x34d0c83cu, 0x33db5f8du, 0xb3e4aef8u, 0x33f484fcu, 0x34c7ca31u, 0xb3d88f96u, 0xb45f2727u, + 0xb4eb797eu, 0x336da156u, 0x348e82c4u, 0x3492c6f9u, 0x34ec2ed1u, 0x33771aa6u, 0x3409e354u, 0x34e7f480u, + 0x3539bab3u, 0x34890a39u, 0x355d4ad2u, 0x3596fbe0u, 0x3497e2cfu, 0x351b641eu, 0x3504eeccu, 0x33ac27f6u, + 0x33f132e0u, 0x3558f83au, 0x341d66a8u, 0x35073fe0u, 0x33f8932eu, 0x3454459du, 0x35222788u, 0x3a1a629fu, + 0x3a9ecdcau, 0x3aaf6c90u, 0x384d1584u, 0x326b507fu, 0x3271af78u, 0xb218bb8au, 0x32ab2ef9u, 0x321cef0bu, + 0x32866eaau, 0xb1a7cd93u, 0x316a4eeeu, 0xb38ad15eu, 0xb34670e9u, 0x33d28f8cu, 0x33b05c83u, 0x32be0091u, + 0xb36be17cu, 0xb347ac14u, 0x3459a962u, 0xb3bdddfdu, 0x3372637eu, 0x338b9e52u, 0xb4ad7e74u, 0xb418a50eu, + 0xb35da101u, 0x3150eca1u, 0x34144b14u, 0xb38ce99du, 0xb46a1d66u, 0xb3f34958u, 0xb3886948u, 0xb38187d1u, + 0xb31bf3cau, 0xb2e27b7eu, 0x3412c534u, 0xb49e3efdu, 0x340e7829u, 0xb4556554u, 0x344a6245u, 0xb370789du, + 0x347f7767u, 0xb51b60a8u, 0x3476694bu, 0xb3ed81bcu, 0x3332e89fu, 0xb416516cu, 0x339727f2u, 0x34934a3eu, + 0x34265c91u, 0xb333a301u, 0x33f43fc3u, 0xb44bd9dbu, 0x3484ebf9u, 0x34535e44u, 0xb4354a2fu, 0x34acc4c4u, + 0x348c6cf9u, 0x351494b9u, 0xb502785du, 0x348e08abu, 0x348ddfc5u, 0xb47f9854u, 0x34a0d5e6u, 0xb45e5982u, + 0xb9e3b093u, 0xba8f6e70u, 0xbaa34eedu, 0x3a21e869u, 0x3162c299u, 0x30047e6au, 0x31e233f4u, 0xb2093afdu, + 0x32ca12c6u, 0xb2b9ef7eu, 0x325e5901u, 0x331960b5u, 0x323857a9u, 0xb39b1de2u, 0xb3aae0a3u, 0xb34b32e3u, + 0x32aefafbu, 0xb35c5143u, 0x322c18afu, 0xb3eb52beu, 0x3303a0cfu, 0xb39abd8bu, 0xb282a54au, 0xb3ef2cfdu, + 0x33ece0d7u, 0x3204e832u, 0x34152f86u, 0xb30216d9u, 0xb44a30e3u, 0xb3beee3eu, 0xb393b196u, 0xb4501a0bu, + 0xb2c84cffu, 0xb269eba1u, 0xb46e21aeu, 0xb3fab229u, 0x32cb39e4u, 0xb3fc4f0au, 0xb265856fu, 0xb43b8649u, + 0x33bdccc4u, 0x33f00ebau, 0x31a54d60u, 0xb4e459ddu, 0xb4937124u, 0xb39262d6u, 0xb49812a4u, 0x34316579u, + 0x313441b6u, 0xb356de64u, 0x34535491u, 0xb41d9606u, 0x320ba38cu, 0xb45d1f3fu, 0xb3a1aeb1u, 0xb4a09a3eu, + 0xb429b767u, 0xb417cf36u, 0xb4a544d3u, 0xb487eb20u, 0xb438afd1u, 0xb42692bbu, 0xb53fdf49u, 0x32c25ecbu, + 0xb45fd9a9u, 0xb9fbf3d3u, 0x3a8354f7u, 0x3a9730ccu, 0xb836a5d4u, 0x3168b591u, 0xb3086e87u, 0x33362476u, + 0x31f87239u, 0x334a48b8u, 0x3356df77u, 0x33b6d908u, 0x33bda3d8u, 0x330d959du, 0x338c7dc5u, 0x33a3dd0au, + 0xb3a9e821u, 0x32ea5105u, 0x33bb7d23u, 0x338a88aeu, 0x33d65564u, 0xb31f8371u, 0x32a40ae7u, 0x33d84160u, + 0x33f3c017u, 0x34050b22u, 0x3400eb4cu, 0xb3668464u, 0x344936b0u, 0xb3942e73u, 0x3423c021u, 0x32f65d5eu, + 0x33b674e2u, 0x342e3b40u, 0x340a3f06u, 0x31994c98u, 0xb47c54b8u, 0x3432e3f3u, 0xb39a6156u, 0xb42d7505u, + 0x3430cfefu, 0x34c82535u, 0x33b6c3f3u, 0xb4c77667u, 0x34b478aeu, 0x34a17a6eu, 0x3507daafu, 0x341db3e8u, + 0x349329bau, 0x350bde5au, 0x35300a52u, 0x34e55ef2u, 0x34ec144au, 0xb3d480c9u, 0x34a0e739u, 0xb35a89dbu, + 0x343dca80u, 0x33a23e7au, 0x34c572a4u, 0x33d09667u, 0x34abf054u, 0xb2363cceu, 0x3540852eu, 0xb32d5182u, + 0x34e40e80u, 0x353d6e70u, 0x39d0e418u, 0xba6e4c8bu, 0xba8d7e99u, 0xba0d7eeeu, 0xb14c9971u, 0xb18505cdu, + 0xb2aff441u, 0x332302b9u, 0xb2885bf7u, 0xb30808beu, 0x3248166au, 0xb2801423u, 0x33472cb8u, 0x319fbc56u, + 0xb38ef4d0u, 0x31cfb5ecu, 0x33eee281u, 0xb3e31428u, 0x34566a5au, 0xb383e40du, 0x3308c4e8u, 0x347d888eu, + 0x3412c9f1u, 0x34a92f13u, 0x34fc47feu, 0x34aaebc7u, 0x343e6893u, 0x3396c560u, 0x3481c7ecu, 0x348ffab6u, + 0x3421d142u, 0x33eee214u, 0x33a90523u, 0x34f6e2dcu, 0xb4b44dd9u, 0x348b2ea2u, 0x348f73a1u, 0xb437246bu, + 0x33ace767u, 0x344ec56eu, 0x344f4129u, 0x33b01da5u, 0x34faf4fau, 0x33f1b0b6u, 0x34171aa6u, 0x33f50355u, + 0xb4fb49cbu, 0x33e588e4u, 0x349240acu, 0x33d565fau, 0xb43bbf82u, 0x342404abu, 0x341cbd2fu, 0x355a457fu, + 0x34b4af0fu, 0x3488cfdfu, 0x34a8ca3fu, 0x345bccc4u, 0x345fa521u, 0x3272feb7u, 0x32d0725cu, 0x34d1e6b5u, + 0x34961f1eu, 0x34c00f6fu, 0x34aba399u, 0x39d40c4du, 0x3a5af98fu, 0x3a83ccb6u, 0x38044cabu, 0xb27a8f00u, + 0xb3044078u, 0xb2baec56u, 0xb390547eu, 0x32d05048u, 0xb3259f4cu, 0xb31609eau, 0xb3d75e71u, 0xb3d0df98u, + 0xb3de1bf8u, 0xb3dc5790u, 0xb43413d3u, 0xb3ee0503u, 0xb4813529u, 0xb3e29413u, 0xb3aeeb96u, 0xb44348a3u, + 0xb477d711u, 0xb3cebd75u, 0xb455f794u, 0xb46d1080u, 0xb41b267du, 0xb4162bf9u, 0xb3a5bf3eu, 0xb4951e05u, + 0xb44459cbu, 0xb4d15b55u, 0xb4ead60eu, 0xb4e7df28u, 0xb399b4ffu, 0xb490efedu, 0xb49df53bu, 0xb41c5777u, + 0xb4a53123u, 0xb48b34bbu, 0xb490aee5u, 0xb3dbf2f3u, 0xb4a94c66u, 0xb4cd8ccdu, 0xb2838b7au, 0xb4a57ad8u, + 0xb48c9430u, 0xb4f0150fu, 0xb3a34716u, 0xb48aa589u, 0xb50a3ba8u, 0xb44f3fb8u, 0xb4c9f63au, 0xb4d744a4u, + 0xb4a36c88u, 0xb53d1176u, 0xb4dd0286u, 0xb54903eau, 0xb5432afcu, 0xb4582181u, 0xb48a7e16u, 0xb4eba570u, + 0xb527a1f0u, 0x344af236u, 0xb5099d79u, 0xb5698951u, 0xb9bd0951u, 0xba47bbbau, 0xba77d846u, 0x39f645e7u, + 0x3262d099u, 0x3283dc2cu, 0x332ad5f5u, 0x32f3177eu, 0x33a049d0u, 0x32dbf1a4u, 0x32c3b456u, 0x339a1e83u, + 0x33261b7du, 0x33c4a8f2u, 0x3443dbdau, 0x33d35053u, 0x33500590u, 0x3392786au, 0x341ce0dau, 0x3421050cu, + 0x3414d2acu, 0x34444973u, 0x34774126u, 0x34799a85u, 0x347e0aa5u, 0x33f0be6eu, 0x346ed563u, 0x34397401u, + 0x344a4036u, 0x3441ad70u, 0x34228eccu, 0x3448233bu, 0x345e86f1u, 0x3505b09fu, 0x348d62d8u, 0x348da54fu, + 0x34e43025u, 0x3503acbbu, 0x34de9a4fu, 0x344e224eu, 0x339f520bu, 0x34c8b63bu, 0x349ad968u, 0x344aa4ccu, + 0x34a12b42u, 0x323887a6u, 0x34c404a7u, 0x3497b071u, 0x351486e6u, 0x34f1597fu, 0x3455e5ddu, 0x34dc2718u, + 0x34d769a8u, 0x34b43abeu, 0x34ffb61cu, 0x34bb95f6u, 0x34c55df8u, 0x3525b5cbu, 0x34f02077u, 0x351faef9u, + 0x353a13bbu, 0x35625c07u, 0x3541b7eau, 0x341da5beu, 0x355c7f16u, 0xb9b0010du, 0x3a381408u, 0x3a6817c3u, + 0xb7ef53a1u, 0x32834eb6u, 0xb32a79a5u, 0xb25eee52u, 0x3425eda7u, 0xb318d908u, 0xb2b5ff4au, 0x332de29du, + 0xb477dc32u, 0x338bd67cu, 0x33746abeu, 0xb4c4c39au, 0xb3d53504u, 0xb40bbab2u, 0xb02c8000u, 0x326c09f0u, + 0x34579640u, 0x3287a0e8u, 0x344a5c5fu, 0x33f5c43eu, 0xb3de02fcu, 0xb4d89920u, 0x34a99290u, 0xb25e6610u, + 0x32f16ff0u, 0x34c87f41u, 0xb435ae06u, 0xb5399362u, 0xb3bac142u, 0x342b81e1u, 0x33b80f42u, 0xb48b6472u, + 0x355da2b0u, 0xb52031a0u, 0x34b262deu, 0xb3978008u, 0xb3978008u, 0xb4aea8a2u, 0x34938840u, 0x34b262deu, + 0x34938840u, 0xb55df900u, 0x33db507cu, 0xb5947d20u, 0xb46ab484u, 0xb56495f0u, 0xb49434e0u, 0xb49434e0u, + 0xb46ab484u, 0x34081c00u, 0xb5897800u, 0x353c8280u, 0xb52d6b82u, 0x35193e70u, 0xb4829423u, 0xb3978008u, + 0xb49434e0u, 0x354552dfu, 0xb52d6b82u, 0xb507e1d0u, 0x35a3c859u, 0xb4aea8a2u, 0x39ae81dfu, 0xba27f369u, + 0xba5b1f8du, 0xb9db1ed8u, 0x335a82f7u, 0x337ffff7u, 0x33da827cu, 0x28800000u, 0x33b504f5u, 0x33b504f5u, + 0x34b504f7u, 0x34b504f4u, 0x343504f4u, 0x351a827au, 0x343504e7u, 0xb43504ffu, 0xaa400000u, 0xaa400000u, + 0x34da827au, 0x353504f0u, 0xaa400000u, 0x353504f6u, 0x34b504edu, 0x353504f6u, 0x34b504fau, 0x2a600000u, + 0x355a827du, 0x355a8276u, 0x353504f6u, 0x34b504edu, 0x34b504edu, 0x355a8276u, 0x359a8278u, 0x34b504edu, + 0x34b504edu, 0xaa400000u, 0xaa400000u, 0xaa400000u, 0xaa400000u, 0x353504f0u, 0xaa400000u, 0xaa400000u, + 0xaa400000u, 0xaa400000u, 0xaa400000u, 0x353504f0u, 0x353504f0u, 0xaa400000u, 0xaa400000u, 0xb53504f6u, + 0xaa400000u, 0xaa400000u, 0x353504f0u, 0xaa400000u, 0x353504f0u, 0x3495f614u, 0xb53504f6u, 0xb53504f6u, + 0x353504f0u, 0xaa400000u, 0x357ffffdu, 0xb53504f6u, 0xaa400000u, 0xb53504f6u, 0xaa400000u, 0x399672f3u, + 0x3a1aefbbu, 0x3a4e258fu, 0x37b4afcau, 0x33283358u, 0x33086b4fu, 0x33298e0cu, 0x340bb900u, 0x33c2a16fu, + 0x340df23du, 0x34040400u, 0x335f8518u, 0xb26bbd79u, 0x341fc2f7u, 0x34388317u, 0x32de8aceu, 0x3434111cu, + 0x346d2912u, 0xb3bd9587u, 0xb4583cbfu, 0xb4077f98u, 0x33cdbfb4u, 0x34645364u, 0x341b8064u, 0xb34aee22u, + 0xb3809cafu, 0x348f0617u, 0x33fe1aa9u, 0xb39ccff4u, 0xb4c39498u, 0xb42d06c4u, 0x3407f006u, 0x358fe2fcu, + 0x33f956c7u, 0x34d695a1u, 0x3525ed83u, 0x342647e9u, 0x34a2dacdu, 0x3526dffeu, 0x35a206bcu, 0x348bcbc1u, + 0x358a9d0fu, 0xb28474bbu, 0x3511e68bu, 0xb4b23feeu, 0x35242e28u, 0x34eed62fu, 0x34575006u, 0x354779f2u, + 0x35c80719u, 0x358e282bu, 0x357a4275u, 0x34f01946u, 0x34e0dac0u, 0x3499bb94u, 0x33623e05u, 0x333ee38fu, + 0xb3457a98u, 0xb322904au, 0x358e282bu, 0x35504f80u, 0x35289f54u, 0x3580ea6cu, 0x35a438e3u, 0xb54c54a5u, + 0xb99fc9edu, 0xba0e05fdu, 0xba435724u, 0x39c25f36u, 0xb3167dabu, 0xb2fe0a25u, 0xb2e16080u, 0xb3856033u, + 0xb3b1244fu, 0xb3cbf65au, 0xb3485420u, 0xb401619bu, 0xb39708ddu, 0xb3c7b1c9u, 0xb45c1902u, 0xb409d32eu, + 0xb3a6e939u, 0xb430e6b8u, 0xb4664857u, 0xb205fa06u, 0xb4668effu, 0xb4197664u, 0xb3b5315fu, 0xb49704fcu, + 0xb33e8b4du, 0xb484275cu, 0xb402b1dcu, 0xb449f291u, 0xb3b728d5u, 0xb2ce182cu, 0xb4be1acau, 0xb4ab40a2u, + 0xb4ace482u, 0xb4a554b5u, 0xb4bcd2cfu, 0xb472e68au, 0xb4a39730u, 0xb503526fu, 0xb4e1c00du, 0xb4e14713u, + 0xb4eb08b4u, 0xb4df1752u, 0xb452d12bu, 0xb488a9a3u, 0xb4c7ddf8u, 0xb536fcdau, 0xb4bb576du, 0xb4824defu, + 0xb4a01172u, 0xb3f03839u, 0xb4e1fb60u, 0xb4f44f6au, 0xb548b8b5u, 0xb4fb695eu, 0xb52fa4cdu, 0xb433256cu, + 0xb4c5e834u, 0xb5293a6eu, 0xb4e4124fu, 0xb4ef1c1au, 0xb5131c49u, 0xb5331ff9u, 0xb532b67au, 0xb5068975u, + 0xb57ccd38u, 0xb97c4c1du, 0x3a03165fu, 0x3a388842u, 0xb7a52ad1u, 0x329a36d5u, 0xb2175d05u, 0x32d69e1eu, + 0x3181d7cfu, 0x330b23f5u, 0x32cffba9u, 0x32535e06u, 0x32219258u, 0xb332273fu, 0x329b393eu, 0x338bc8bbu, + 0xb37653e9u, 0x3295e26du, 0xb29479dcu, 0xb22d8e2bu, 0x33582a38u, 0xb3b2fb45u, 0xb2f1c4d7u, 0xb4906bd9u, + 0x33242027u, 0x3186651du, 0xb3e77d4cu, 0xb44378a0u, 0x339392bfu, 0xb2084a13u, 0xb406f60du, 0xb18caf57u, + 0x3429ae2au, 0xb42ff427u, 0xb3faf180u, 0x33729be6u, 0xb3bb8846u, 0x33582857u, 0x33cc1618u, 0x34358721u, + 0xb36d564fu, 0xb45efc18u, 0xb356ecc1u, 0x3387b404u, 0xb3d00102u, 0x33d8e992u, 0xb41b1c32u, 0x344e3fb9u, + 0x33e9d2c4u, 0x33f753b1u, 0x33b472d9u, 0x346823bcu, 0x338cf0eau, 0xb3e62701u, 0xb4287a6fu, 0xb433972du, + 0xb4240058u, 0xb44d74f0u, 0x33206f34u, 0x324f5971u, 0x34874a13u, 0xb4897086u, 0xb4ab3417u, 0xb50803e4u, + 0x3507de2cu, 0xb4a64adau, 0x3995c874u, 0xb9f13ff3u, 0xba2f6c6bu, 0xb9af64f0u, 0x33dbdd44u, 0x340e66b9u, + 0x344469adu, 0x34680d17u, 0x348414e6u, 0x3454afcdu, 0x34a43330u, 0x347a494du, 0x34d5c90eu, 0x34c6c21cu, + 0x34cd550bu, 0x34d3796eu, 0x34ab7072u, 0x3508de16u, 0x351599d0u, 0x353f49d2u, 0x358805b1u, 0x354f4959u, + 0x35831309u, 0x353bbfceu, 0x354d3217u, 0x3572bd77u, 0x3563f069u, 0x355bb951u, 0x358f0ae1u, 0x3589c33au, + 0x358407b8u, 0x35896778u, 0x35904040u, 0x35678435u, 0x35aaa765u, 0x3575de55u, 0x35adee22u, 0x35817116u, + 0x35b4870cu, 0x35b5f95bu, 0x35b99b5du, 0x35977a54u, 0x35c1c5d7u, 0x35900ab1u, 0x359b2170u, 0x359e650eu, + 0x3601e7a6u, 0x35cf5102u, 0x3602a4afu, 0x35fbc7fcu, 0x35cb0af1u, 0x35f6b772u, 0x35cff647u, 0x35c5e769u, + 0x35ea5d28u, 0x35f5fb21u, 0x35f40480u, 0x36030469u, 0x35f17e37u, 0x35cf09b8u, 0x35ee5069u, 0x35dd5d6cu, + 0x36093e24u, 0x360ff0f4u, 0x3602c257u, 0x3958a1c6u, 0x39dfeb07u, 0x3a2655abu, 0x378095d6u, 0xb0063530u, + 0xb236d938u, 0xb232ed0au, 0xb2aa6eafu, 0xb311a325u, 0xb290ddb5u, 0xb28784b1u, 0xb10a105eu, 0x31cab619u, + 0xb2cde34du, 0x33814305u, 0xb2db99f5u, 0x33893164u, 0xb3eba48bu, 0x32c92c5bu, 0x33813ad9u, 0xb2ccb1d1u, + 0xb362208cu, 0x33b5b26au, 0xb3189d64u, 0x34523710u, 0xb20781bau, 0x33b40e72u, 0x335801fdu, 0x3412f25du, + 0xaf9521f7u, 0x3494cca6u, 0x323d649bu, 0x33b86c16u, 0x33d02f96u, 0x30f3ff46u, 0x33f0a3b1u, 0x34109294u, + 0xb3f019b8u, 0xb1aa6386u, 0xb3eb44a7u, 0x33326d75u, 0x314077a0u, 0xb2bbb4a4u, 0x348726c6u, 0x344c07d6u, + 0x34e90d54u, 0x3366cbb8u, 0x338bbdaau, 0x349cb099u, 0x344ddbf0u, 0x335e4570u, 0x34dfc903u, 0x346d673eu, + 0x3489eb06u, 0x32987795u, 0xb4ef54bcu, 0x34c23ab2u, 0xb4d25ff5u, 0x34f78b49u, 0xb3e05a61u, 0x3480caa3u, + 0xb5112063u, 0x33f8c4deu, 0xb3ba3ffbu, 0x3402fe93u, 0xb98ad204u, 0xb9cc6604u, 0xba1e9d0du, 0x399df48cu, + 0xb3c52173u, 0xb417824cu, 0xb4374903u, 0xb4607255u, 0xb473299bu, 0xb49bb91eu, 0xb4912fdbu, 0xb4bede69u, + 0xb4cc7997u, 0xb4caf8b8u, 0xb4b34636u, 0xb4e1bd08u, 0xb4fc745bu, 0xb4f2cb82u, 0xb5184037u, 0xb5023ab5u, + 0xb5251cceu, 0xb52f9775u, 0xb53e9279u, 0xb5428979u, 0xb506976cu, 0xb55ddcf0u, 0xb547b489u, 0xb541d139u, + 0xb573710fu, 0xb5586ff4u, 0xb56ca35bu, 0xb5860cd0u, 0xb57272a2u, 0xb588b1f1u, 0xb579690bu, 0xb5a09237u, + 0xb57a0e16u, 0xb5b9132bu, 0xb5bba779u, 0xb59f24c0u, 0xb58e527eu, 0xb5aa4ac6u, 0xb58703d2u, 0xb5b1b1a4u, + 0xb5b22171u, 0xb5b13f73u, 0xb59c5cfcu, 0xb5ca94a1u, 0xb5de6662u, 0xb5bbdf8du, 0xb5b978b1u, 0xb5d61ce9u, + 0xb5ec4964u, 0xb5d178b2u, 0xb5d82cf9u, 0xb5d18019u, 0xb5d59a70u, 0xb5f07638u, 0xb5f9dae3u, 0xb5ce1732u, + 0xb5c4bc4au, 0xb5ef40dcu, 0xb5dc1febu, 0xb5df137au, 0xb61f28d2u, 0xb936ba0fu, 0x39bc360cu, 0x3a16de86u, + 0xb76e4392u, 0xb2d3a2deu, 0xb3c1d688u, 0xb3f29088u, 0xb4063af5u, 0xb428d063u, 0xb3fc2684u, 0xb3d7a90au, + 0xb316dbe4u, 0xb41ed27bu, 0xb43df859u, 0xb4889711u, 0xb4257f36u, 0xb42152aeu, 0xb4c83687u, 0xb433bcb6u, + 0xb49f0799u, 0xb49c93b3u, 0xb49c7d73u, 0xb4c8aef2u, 0xb502f076u, 0xb4dd437bu, 0xb492d639u, 0xb4c7fcd8u, + 0xb53e99b7u, 0xb50c07dbu, 0xb4c0f2a9u, 0xb4b97413u, 0xb4b25e8fu, 0xb4caaa16u, 0xb4c48c9eu, 0xb009422fu, + 0xb51604abu, 0xb5740c40u, 0xb524847au, 0xb47af639u, 0xb5a89529u, 0xb509e1c0u, 0xb45e731au, 0xb54f1f5eu, + 0xb513e4abu, 0xb53c4c98u, 0xb5396969u, 0xb4d1d566u, 0xb58f497au, 0xb50af573u, 0xb51226c5u, 0xb5a2b61au, + 0xb57c48d5u, 0xb55acd05u, 0xb562a600u, 0xb58227c1u, 0xb51ed0f7u, 0xb504b275u, 0xb54c4b22u, 0xb59b3277u, + 0xb534580cu, 0xb46df18bu, 0xb5349a56u, 0xb5a91a37u, 0xb5756da9u, 0xb55f6a6cu, 0x39825b22u, 0xb9ae4748u, + 0xba1040a7u, 0xb9903fa5u, 0x32ff744eu, 0x32b694b7u, 0xb29b368fu, 0x30d9e72bu, 0x33972565u, 0xb1fd0ef1u, + 0x33be6800u, 0x340fa0c8u, 0x32d936dcu, 0x3440ca51u, 0x33c29781u, 0x34a93f1bu, 0x345149ffu, 0x34ad1f18u, + 0x3455c0adu, 0x35285be5u, 0x35169682u, 0x350513eau, 0x351702e3u, 0x350664d5u, 0x35086f8fu, 0x348b6a6au, + 0x3511cf21u, 0x3552db28u, 0x350bf181u, 0x354a2c5fu, 0x3534593du, 0x34f1e116u, 0x3527ef35u, 0x34a5ea88u, + 0x351f9287u, 0xb35e1ae3u, 0x34aca132u, 0x34acebebu, 0x33da9df9u, 0x352c4d06u, 0x34d393a4u, 0x3436e8d9u, + 0x3534870eu, 0x35246dcau, 0x3420591au, 0x3486c9d7u, 0xb485a038u, 0x345ce20bu, 0x33867bd8u, 0x33b88134u, + 0x342f4e01u, 0xb45ee2d2u, 0xb316076du, 0x353a553eu, 0xb5031613u, 0xb33de58eu, 0x34c3407au, 0x34cd3ea8u, + 0x34924de0u, 0x343bca43u, 0x350bf0efu, 0x34d02b74u, 0x345a4e35u, 0x3548cf5cu, 0x3446f4c5u, 0x391a2f18u, + 0x39a120b1u, 0x3a09a249u, 0x373d90f2u, 0x315a8273u, 0xb2ce8486u, 0xb2a3c9f1u, 0x3208e609u, 0xb2b0793du, + 0x31fac999u, 0xb350553cu, 0x3376470eu, 0x32bd09d1u, 0x33ae482cu, 0xb3e91a0cu, 0x32c89ac7u, 0xb3174172u, + 0xb36c305au, 0xb3aa025bu, 0xb3925e32u, 0xb4877902u, 0xb40695dcu, 0x3401dfd6u, 0xb3162612u, 0x3265808fu, + 0xafd5b927u, 0xb35cd67fu, 0xb0634151u, 0xb45294a8u, 0xb493f339u, 0xb3de6d6du, 0xb3e1b527u, 0xb4b0bec8u, + 0x331e0830u, 0xb2b7a7ffu, 0xb31c5084u, 0xb41977beu, 0xb4546389u, 0xb4923d42u, 0x318ac38fu, 0xb4150262u, + 0x33ef9db4u, 0xb45693efu, 0x34bb7944u, 0xb3e9ea27u, 0xb3261457u, 0xb40d0bc4u, 0x33ab9de3u, 0xb59765cdu, + 0xb359d366u, 0xb4954494u, 0x3427f169u, 0xb522890cu, 0xb43f6774u, 0xb500d45eu, 0x34bd3f6eu, 0xb53aa9d4u, + 0x34dd5954u, 0xb4f43c54u, 0x34ca620au, 0xb5140fdcu, 0x33980181u, 0xb5489ce9u, 0x3445fbb5u, 0xb579c923u, + 0xb9747b5au, 0xb9937ddeu, 0xba03eb89u, 0x398385afu, 0x324320fbu, 0x31ce8d8du, 0x33253d48u, 0x3324a49cu, + 0x32daaeffu, 0x335e5452u, 0x33d5e34fu, 0x337151c8u, 0x33e342a7u, 0x33f76aecu, 0x330a552au, 0x32cfb95du, + 0xb329167fu, 0x33b49cfeu, 0x33a8d2c8u, 0x348e7ca4u, 0x338504cbu, 0x3328cb95u, 0x3460537bu, 0x3235b878u, + 0xb2947f72u, 0x343f79bcu, 0x331912ecu, 0x345ea123u, 0x345a2b7du, 0x343a2484u, 0x33d96922u, 0x3467b8f3u, + 0x3418ac8cu, 0x3293431au, 0x3421a470u, 0x34c704b8u, 0x34b3c66cu, 0x341804bbu, 0x346e30bbu, 0x341cef98u, + 0x346e87f7u, 0x349ccb88u, 0x34ebe086u, 0x351ae034u, 0x34568bf3u, 0x327a3d2fu, 0x34821665u, 0x34946811u, + 0x35027a70u, 0x3492d405u, 0x33eb8da3u, 0x34f00ce8u, 0x3537b6c0u, 0x34dd5fd7u, 0x34c09651u, 0x34edfb5cu, + 0x34f6af91u, 0x34db9909u, 0x34a6cc54u, 0x34a3879cu, 0x348ce141u, 0x35063f7du, 0x341fa45eu, 0x3515569cu, + 0x350d7116u, 0xb9005a11u, 0x3987e5b2u, 0x39fc69ecu, 0xb7305a40u, 0xb22da052u, 0xb1ede638u, 0xb3151982u, + 0xb2815791u, 0x32fb072au, 0x2e8aa329u, 0xb337409fu, 0x32ef4fbeu, 0xb41aeb2eu, 0x33d8dc09u, 0xb3b74059u, + 0xb330a2d2u, 0xb30d6c33u, 0xb309f8d4u, 0x331c7bd2u, 0xb3b74e46u, 0x34132aa4u, 0xb35221a7u, 0xb425bd1bu, + 0xb3af4717u, 0xb326911fu, 0x3312c40au, 0x32c5018du, 0x3304213bu, 0x31243e95u, 0xb32d4e97u, 0xb3da4ac7u, + 0x3256021du, 0x334d30ecu, 0xb427365du, 0xb4179ca7u, 0x33509c55u, 0xb4945101u, 0xb40b0c89u, 0xb4b61745u, + 0xb42b0c2eu, 0x332c4eafu, 0xb47296f9u, 0xb485dd28u, 0xb2c12eb3u, 0x340eccd9u, 0xb259b4a0u, 0xb360ec45u, + 0xb3d3d01au, 0xb3b8301fu, 0xb4de37c5u, 0x34c5d551u, 0xb499c88au, 0x3465bcb7u, 0x34e62bc2u, 0xb483075eu, + 0xb4be1a92u, 0xb5096389u, 0xb49d11acu, 0xb50ffff4u, 0xb4319074u, 0xb380512du, 0xb4890041u, 0x341589d3u, + 0xb452971bu, 0x34adc286u, 0x3966179eu, 0xb978d477u, 0xb9f27ea6u, 0xb9727862u, 0xb10e634au, 0xb2c8f780u, + 0x30b0edfau, 0x31d3e358u, 0x33266a81u, 0x324fd2eeu, 0x3343777au, 0x33a16a52u, 0x32fb33bbu, 0x33180a2du, + 0xb3673e01u, 0x343462f9u, 0x32ab2508u, 0x32fe0fc9u, 0x319d7bd0u, 0xb34700fdu, 0xb4c5c500u, 0x339bfc26u, + 0xb4976eb8u, 0xb49fcd59u, 0xb4ab658fu, 0xb4afbac9u, 0xb4aa124fu, 0xb38f0a60u, 0xb371c48du, 0xb49fdc24u, + 0xb49c4980u, 0xb4af4fa1u, 0xb4221872u, 0xb485de94u, 0x342037c3u, 0x33a06f45u, 0xb478182cu, 0xb4059814u, + 0x32fd1526u, 0xb40ba53cu, 0x335020a2u, 0xb49aa226u, 0xb501979du, 0xb2557414u, 0x3489e9e3u, 0xb47f0db4u, + 0x3483602du, 0xb3033b3bu, 0x34b214acu, 0x320d81f2u, 0x32df3779u, 0x34df8816u, 0x344b8fd6u, 0xb45b8a6au, + 0x34986404u, 0xb283f5dau, 0x34088e58u, 0xb12c8436u, 0xb19e13dau, 0x3406c0dfu, 0xb4ace89cu, 0xb49c49f3u, + 0x33e34653u, 0xb386cfc5u, 0xb41b1710u, 0x38d80c0du, 0x3964e1e9u, 0x39e89045u, 0x370f53c0u, 0xb236fba4u, + 0xb2996ccbu, 0xb30bf26bu, 0xb2c76386u, 0xb275b62fu, 0xb396154bu, 0xb18ca24fu, 0x31f99c8fu, 0xb3761587u, + 0xb3e2827eu, 0x3260a2cdu, 0x338c2dd1u, 0x3398d1abu, 0xb2368818u, 0xb324ab97u, 0xb30bc479u, 0xb3d95f47u, + 0xb1aa9d17u, 0xb355f108u, 0xb4701a42u, 0xb436f305u, 0xb37a7c65u, 0xb415659du, 0xb3a722a3u, 0x32a9f175u, + 0xb3a36422u, 0xb4d63a27u, 0xb436569fu, 0xb44bf9ebu, 0xb40b56cdu, 0xb40572b1u, 0xb38dd295u, 0xb4af9056u, + 0xb4dfc137u, 0xb46376a1u, 0xb2b5fa45u, 0xb512cde1u, 0xb4227a14u, 0xb4822453u, 0xb46e52b5u, 0xb5013b01u, + 0xb409beb5u, 0xb454b56du, 0xb4a2c6cbu, 0x3427d47bu, 0xb4b7d9f1u, 0xb4cb5f0du, 0xb4b28293u, 0xb4d21169u, + 0xb4a9e709u, 0xb42d67e4u, 0xb46ef21bu, 0x334e1c34u, 0xb4557127u, 0xb3cf375au, 0xb4ae7b82u, 0xb3a5354bu, + 0xb440cb4bu, 0xb413565cu, 0xb50b1255u, 0xb3c9a20fu, 0xb959b3ebu, 0xb951bdf5u, 0xb9dfe393u, 0x395f53d3u, + 0x3316e6ceu, 0xb0caeb65u, 0x32c4c416u, 0x32970e65u, 0xb2beba13u, 0xb280585fu, 0x3384130du, 0x33ca142bu, + 0x310c1096u, 0x32444f8au, 0x3383fb29u, 0x33bdc4ffu, 0x3354781cu, 0x33e3f902u, 0x33657a8au, 0x33d1baa5u, + 0x34050a9cu, 0x33e42f84u, 0x3184a52eu, 0x346e69cdu, 0x33ebf247u, 0x33e5c2f1u, 0x34cedf64u, 0x345f5406u, + 0x3417970fu, 0x34595dafu, 0x33fc4c67u, 0x349beb59u, 0x34864cd6u, 0x341465d5u, 0x345f44b4u, 0x3463535fu, + 0x33088fd4u, 0x34f6f167u, 0x344d7597u, 0x344f5121u, 0x34a9cc37u, 0x348d7cdau, 0x34f3538fu, 0x35139869u, + 0x353e3c94u, 0x34b2a549u, 0x34f38959u, 0x34c0d5b6u, 0x350d6c79u, 0x3529a567u, 0x34fc1e46u, 0x35066798u, + 0x3423138cu, 0x34bcc2fdu, 0x34b0b8d3u, 0x352a3312u, 0x346a5eb4u, 0x34e4982eu, 0xb39eaddbu, 0x3538015au, + 0x34d5098fu, 0x34d886fau, 0x34db72ecu, 0x34db4bceu, 0x34ef6a9fu, 0xb8b22cacu, 0x39401402u, 0x39d735f4u, + 0xb7069997u, 0xb1d407d1u, 0xb322f4bdu, 0xb23119c7u, 0x3392147fu, 0xb2dfcdfau, 0xb3c59f62u, 0xb3b7944fu, + 0xb3df3cd7u, 0xb39b80b5u, 0xb3c3288du, 0xb18ceb0bu, 0xb494211cu, 0x323fc5b6u, 0xb2f2fac9u, 0xb4802b6eu, + 0x33eee6f0u, 0xb46c5d68u, 0x34bb4896u, 0xb46ba8a0u, 0xb4bed110u, 0xb41f7950u, 0x31827a3fu, 0x32b7cf59u, + 0xb41f7720u, 0xb42d165du, 0x33f8d24cu, 0x349f3885u, 0xb49e22c1u, 0xb4b579e7u, 0xb4a00e96u, 0x345983d7u, + 0xb5581240u, 0x3325eeccu, 0xb5624feau, 0xb49cb416u, 0x34298941u, 0xb446d9e6u, 0x3527fb10u, 0xb4074568u, + 0xb4f833ccu, 0xb4c5fa37u, 0xb4c1cf2au, 0x34daffb2u, 0xb42bcb53u, 0x34423191u, 0xb41060fau, 0xb4cccd06u, + 0x34a00851u, 0xb46e7e69u, 0xb5ae3ec8u, 0x341e2b7au, 0x32ec5bc3u, 0x34bcaa15u, 0x32834e34u, 0xb5030e1fu, + 0xb5be52d0u, 0xb50924c4u, 0xb4f85be0u, 0xb3f83b8cu, 0x341ae054u, 0xb491db93u, 0x394d4897u, 0xb92dab24u, + 0xb9cf973eu, 0xb94f9f2bu, 0x301fd1aeu, 0x33463998u, 0xb2c699deu, 0x3229eb24u, 0xb2c02259u, 0x33bc7acau, + 0x33c7f90fu, 0x3480e028u, 0xb44cba90u, 0x34350550u, 0x3450c487u, 0x31813044u, 0x342b46f7u, 0x33ec840fu, + 0xb4cb7e0du, 0xb4d0c3f6u, 0xb329e30du, 0xb3a61a14u, 0x346c83b7u, 0x34c2fcdcu, 0x34920116u, 0x353c00e6u, + 0x34920116u, 0x335dfb1du, 0x3405f9b7u, 0xb377d59du, 0xb377d59du, 0x34920113u, 0xb4953c64u, 0x33100899u, + 0x342188b1u, 0x34ec838au, 0x3550c43au, 0x3550c43au, 0x3550c43au, 0x34b50513u, 0x3595a35cu, 0x2bc3bfdau, + 0x34b50521u, 0x34ec838au, 0x33ddfa4eu, 0x34b50513u, 0x34b50513u, 0x33ddfa66u, 0x34ec838au, 0x3580e001u, + 0x34ec838au, 0x3443ef68u, 0xb4b504c6u, 0x34ec838au, 0x35350509u, 0x34997624u, 0x351fc878u, 0x35273d8au, + 0x2bbec8acu, 0x350b7e4fu, 0x3550c43au, 0x3580e001u, 0x3443ef68u, 0x341cf628u, 0xb529869eu, 0x38939ba0u, + 0x391f23f7u, 0x39c7f7e7u, 0x36dd307au, 0x32aec9edu, 0xb28a667eu, 0xb3941ba4u, 0xb20301cbu, 0x333a2618u, + 0x336d09dau, 0xb41379fau, 0xb3b7fc3du, 0xb32160c6u, 0x33e79c83u, 0x33cc6e4bu, 0xb4ab74f8u, 0xb46cbb63u, + 0xb41255eeu, 0xb41f8573u, 0xb32fbe9cu, 0xb4a67539u, 0xb4926c62u, 0x339a2de5u, 0xb006b599u, 0xb43773c3u, + 0x32ea087eu, 0xb27bb73cu, 0x327783a5u, 0xb44980e8u, 0x34be189bu, 0xb1fe3849u, 0x31e6f7fbu, 0xb5134a3du, + 0xb4bb3b00u, 0x34b67f16u, 0x339e81dau, 0x3432dc0cu, 0xb5ac5748u, 0xb51cb469u, 0x349c2832u, 0xb4a7fb1du, + 0xb5915eaau, 0xb41eeed0u, 0xb507d2b6u, 0xb5346ef8u, 0x320d9607u, 0x34dd0433u, 0xb4d0e0c5u, 0xb4057308u, + 0xb4e31adau, 0xb52e7430u, 0xb49c21dau, 0xb4748a74u, 0xb5256aaeu, 0xb42ca8f0u, 0x35706706u, 0xb555fab2u, + 0xb3599c91u, 0xb4fbb4ccu, 0xb57b2cd6u, 0xb549ea9cu, 0xb4a3662bu, 0x3548c8f9u, 0x33e5cf5eu, 0xb4867750u, + 0xb94107d7u, 0xb91159f9u, 0xb9c13c69u, 0x3940d207u, 0xb2343f00u, 0x31deaabbu, 0xb2e8fe2eu, 0xb26cac2cu, + 0xb2d3c0b9u, 0xb1d6dcedu, 0x32052867u, 0x3356fb72u, 0x323bfd29u, 0xb3af527eu, 0xb1faad8du, 0x338b4f2du, + 0xb3c316edu, 0xb35cd201u, 0x330e1d25u, 0xb45f559bu, 0xb470778cu, 0xb41e4f10u, 0xb34ddcfbu, 0xb3732c67u, + 0xb431de52u, 0x3306b2e1u, 0xb3d43e0bu, 0xb48610c2u, 0xb384e4ecu, 0xb4243adeu, 0xb26b881cu, 0xb29f9d12u, + 0x325c7b7au, 0x3296bb69u, 0x3431ce59u, 0xb48425a1u, 0x33dcf0d4u, 0x32b072f5u, 0xb443233au, 0xb45b6b19u, + 0xb3c2ba80u, 0xb2ac20ceu, 0xb4183569u, 0xb4409d8fu, 0xb4bce864u, 0x34626f17u, 0xb447237du, 0xb4d1555au, + 0x33b42260u, 0xb3a60e16u, 0xb4013134u, 0xb3c804d8u, 0xb4a19403u, 0xb2f1629du, 0x34351175u, 0xb41274a0u, + 0xb21747adu, 0x338400aeu, 0x33d66b54u, 0xb3f452e3u, 0xb4a260d9u, 0x3459d2d9u, 0xb3efa34cu, 0x33fd0b59u, + 0xb484b1d2u, 0xb870c379u, 0x3902429cu, 0x39ba80a7u, 0xb6d0a494u, 0x3218c1e8u, 0xb220a90fu, 0xaf96336du, + 0x31d7e49du, 0xb27c2afeu, 0xb3a0a8fdu, 0xb3c27517u, 0xb24f3641u, 0xb25782f7u, 0x3383a576u, 0xb37bf3c2u, + 0xb3acd173u, 0xb350d885u, 0x32d6ae49u, 0xb2c34c0eu, 0xb2d26c5fu, 0x32810e1eu, 0x3330ba72u, 0xb297a13bu, + 0xb2ddd338u, 0xb29cac52u, 0x33026f20u, 0x340aec4eu, 0x34aaea08u, 0x32e0ed50u, 0x339485d7u, 0x343df1b2u, + 0xb46075b6u, 0xb40fbc31u, 0xb3a6bedeu, 0x34208f33u, 0xb327e029u, 0xb494fb96u, 0xb33325feu, 0x34b28859u, + 0x34d136cau, 0x34539535u, 0xb28d2f28u, 0x3464ac34u, 0xb4085aa9u, 0xb011d9f1u, 0xb4af9dbeu, 0x3488d671u, + 0x33efdb22u, 0x335bcc34u, 0x34d8bc53u, 0x3404dd05u, 0xb51bc951u, 0xb39c5aa9u, 0x349c8ef7u, 0xb4f8daf2u, + 0xb351d31bu, 0xb33023ccu, 0xb3edd2a3u, 0x34059352u, 0x33229d1au, 0xb510dbfau, 0xb4348cb0u, 0xb516de99u, + 0xb3ba8f8cu, 0xb3718382u, 0x393783e2u, 0xb8e9ee60u, 0xb9b486cbu, 0xb9348777u, 0xb2a9f6f4u, 0xb1824391u, + 0xb1825563u, 0xb2288e3cu, 0xb2cb1204u, 0xb32fdeaeu, 0x3271e0dfu, 0xb2f4c156u, 0x338ea4afu, 0xb373c7f3u, + 0xb37218f9u, 0x31962c12u, 0x33756ed8u, 0x336c00c7u, 0x31e186a8u, 0x343b7e19u, 0x3308c245u, 0x33cf10a5u, + 0x33e4235au, 0x343190ebu, 0x34429f7cu, 0xb28b5297u, 0xb37887a3u, 0xb33a1a20u, 0xb39476b8u, 0x344acca9u, + 0x31b8e614u, 0x342b5d94u, 0xb4202070u, 0xaf8a2c77u, 0x3454cc5du, 0x3487d0a7u, 0x3471fc44u, 0x34877710u, + 0xb490b9ffu, 0xb4460de5u, 0xb4cc4c55u, 0x343a1544u, 0x33d22235u, 0xb39acdb2u, 0x340834aeu, 0x34df6506u, + 0x33ac41f9u, 0x349566ccu, 0x345f49f2u, 0xb4b1279eu, 0xb17d682cu, 0x342c81a3u, 0x344e17a1u, 0xb335cb65u, + 0x33fa79c2u, 0xb4b3dcf8u, 0xb39dd36du, 0x3445056fu, 0x32aed16au, 0x34842cf1u, 0xb2eb4d31u, 0xb48ab6f8u, + 0x34c5c71fu, 0xb4cae138u, 0x34eed973u, 0x383fb84fu, 0x38d25867u, 0x39ae8eb2u, 0x36af217eu, 0x326c3823u, + 0x321e9a7cu, 0x33066d41u, 0x3265a932u, 0x33267ac0u, 0xb1e0e99fu, 0x3291a5ebu, 0x33835394u, 0xb3166352u, + 0x327dcb93u, 0xb1b3dca8u, 0x339c927du, 0x3320bdb5u, 0x3372a80cu, 0x338dccfau, 0xb3fc3fc2u, 0x3431bebcu, + 0x329f7364u, 0x3269b855u, 0x341523ebu, 0x33d739a7u, 0xb3dee722u, 0xb398d581u, 0x3422166au, 0xb3b3fbb0u, + 0x33f34177u, 0x341f8e9du, 0x33d1aed9u, 0x3402d13du, 0xb35e2906u, 0x33e184a0u, 0x330cfdb9u, 0x342aa698u, + 0xb36e0426u, 0xb43305f1u, 0x34cb973cu, 0x34371606u, 0x33e010c6u, 0x349c376du, 0xb31c814au, 0xb48145c5u, + 0xb49f2945u, 0x325f815bu, 0x34177e40u, 0x318539d9u, 0x33f6e345u, 0x2f958da1u, 0x33b4d06cu, 0x33888208u, + 0x325a16a9u, 0x346954b3u, 0x34c92969u, 0x33dea439u, 0x34dec398u, 0xb3c479b9u, 0x32157a48u, 0x35072fe0u, + 0x348f75ddu, 0xb434b7dau, 0x344e5abau, 0x34c7b249u, 0xb92e0a5du, 0xb8b98d33u, 0xb9a93bc9u, 0x3928efb4u, + 0x316aa63bu, 0xb21cf133u, 0xb34200a8u, 0xb35ff666u, 0x32a01549u, 0x33523e5au, 0x30971410u, 0xb3091425u, + 0xb3da18c4u, 0xb3acf4c8u, 0xb3f18ff5u, 0x32af8dd1u, 0xb294a039u, 0xb3b6c008u, 0xb3ed219cu, 0x3253f1c0u, + 0xb38146f5u, 0xb416a698u, 0xaf5eb42eu, 0xb2691ccbu, 0xb3a1a7ccu, 0x337aa5aeu, 0xb37d4b10u, 0x33d5cfaau, + 0xb2d5e7f0u, 0xb35c4b61u, 0x341e73d8u, 0xb36f5a2bu, 0x337daf28u, 0xb42400afu, 0x33e25b51u, 0xb2e2d225u, + 0x34992aa9u, 0x348a9af4u, 0x33996464u, 0xb4a52bdeu, 0xb2b97080u, 0xb3a3a690u, 0x3181c943u, 0xb36562fbu, + 0xb44f3a65u, 0x33dc2fa1u, 0x340fdd57u, 0x33c2ab59u, 0x3481bdf9u, 0xb3bdf51du, 0x32b8bd55u, 0x340b2effu, + 0xb45141ccu, 0x347649ccu, 0xb3e2edf0u, 0x331cf4c5u, 0xb448994au, 0x335f2b53u, 0xb4cc1013u, 0xb48d0402u, + 0xb3b49fffu, 0x3365f90eu, 0xb4e444a6u, 0xb47495e5u, 0xb4bd82a7u, 0xb8154949u, 0x38a4877cu, 0x39a3e9deu, + 0xb6a5a057u, 0xb1fec1d9u, 0xb216af3eu, 0x3310a8edu, 0x330a5ed6u, 0x3346e27bu, 0x337618d2u, 0xb293ae39u, + 0x3365c8afu, 0x32354d69u, 0xb3c5e3bau, 0xb2bd403cu, 0x33127f9du, 0x34370bdbu, 0x3409b496u, 0x330a228fu, + 0xb244b74bu, 0x344678deu, 0x32f4607bu, 0x336a4229u, 0xb231c378u, 0x345a85c7u, 0xb2ee7197u, 0xb34cb4cau, + 0xb400357du, 0x347ccba7u, 0x34e50821u, 0xb43c2e10u, 0x338ae285u, 0xb2ebaf1eu, 0x33e55413u, 0x32f41ce1u, + 0xb4477f80u, 0xb286fcb3u, 0x34f90286u, 0x340557ceu, 0xb3d16efdu, 0x34bc29f1u, 0x337d7240u, 0xb3e5e6fbu, + 0x33f124bbu, 0xb3a4528bu, 0xb505f7b5u, 0x347adbdeu, 0xb217f05au, 0xb1cc348fu, 0x34a0a298u, 0xb3117baau, + 0xb44380a1u, 0x32754eeeu, 0x3499aa23u, 0x3556b7c0u, 0x3452f388u, 0x350212e4u, 0x35125637u, 0x3520f823u, + 0x34739d1bu, 0x3331a441u, 0xb45c8480u, 0x35985ffeu, 0xb4da35b5u, 0x350a16ceu, 0x39253c2bu, 0xb88f4a40u, + 0xb99f2744u, 0xb91f2b8bu, 0xb12e9c5fu, 0xae9121dfu, 0xb37121dfu, 0xb3f622b5u, 0xb401f3cfu, 0xb40f5f1eu, + 0xb3a95bcdu, 0x33d47077u, 0x3385542du, 0x3355609au, 0x33e89c47u, 0x32212a1bu, 0xb1d78535u, 0x3356189eu, + 0xb388b2b3u, 0xb46a7becu, 0xb481b9b8u, 0xb4c42e3fu, 0xb4fb5fa7u, 0xb342ada6u, 0xb528a646u, 0xb4927436u, + 0xb4e40ea9u, 0xb40f02bbu, 0xb46514fbu, 0xb51a3246u, 0xb4934c08u, 0xb504307cu, 0xb404b5f9u, 0xb48f1e7au, + 0xb54fe7f2u, 0xb3a611b2u, 0x34288e0du, 0xb4e3dbe2u, 0xb4b2ed2au, 0xb4ee3d2eu, 0x34935048u, 0x32032f24u, + 0x33eda742u, 0x31eefb2bu, 0x34a66f49u, 0xb5361425u, 0xb5036b23u, 0xb58a346au, 0xb4c18789u, 0xb422d0e4u, + 0xb5111d70u, 0xb55d9bbbu, 0xb4a4b6d0u, 0xb5318a78u, 0xb3afedadu, 0xb56a6ae6u, 0xb5018ab1u, 0xb53b61c0u, + 0xb56838dfu, 0xb559ca4fu, 0xb4650e80u, 0x3513379du, 0xb57d1370u, 0xb4f3fd51u, 0xb4d5a7a3u, 0x37d51ba8u, + 0x38770b6fu, 0x399a64c7u, 0x368b9c96u, 0xb1c44d1fu, 0xb2cb3490u, 0x3237a93eu, 0xaf6c87e6u, 0xb39350d7u, + 0xb333a24eu, 0xb3816721u, 0x33c9b9a5u, 0xb38b2e30u, 0x32c82f2fu, 0xb3cd8aa9u, 0xb3915922u, 0x338b3aa9u, + 0xb3b19ae9u, 0x33e50f38u, 0xb483a564u, 0xb32ada8du, 0xb36726cau, 0x3437bdaau, 0xb22feab0u, 0xb39f2764u, + 0x33b4396eu, 0xb42bddc2u, 0xb464c104u, 0x34336f31u, 0xb43201bfu, 0xb45f3207u, 0xb4bf1f37u, 0xb48cd3bau, + 0x3402dcd4u, 0xb423733fu, 0xb4c0f7adu, 0xb3682898u, 0xb47ed430u, 0xb403714eu, 0xb4ae82fcu, 0xb38b3ce1u, + 0xb4ee76a9u, 0xb29989d5u, 0xb4e41010u, 0xb46e3c4cu, 0xb3cd9ab4u, 0xb4f679c5u, 0xb4f1b318u, 0xb3b6eb28u, + 0xb498d02du, 0xb4887f39u, 0xb3c9309du, 0xb4b84d47u, 0xb443a714u, 0xb4a0b6a0u, 0xb52e550fu, 0x34a6a6c5u, + 0x344fbb25u, 0xb54cea39u, 0xb547238cu, 0x33b1d7eeu, 0xb492914du, 0xb4cae231u, 0xb4ee3c62u, 0xb4e2bd97u, + 0xb91cdec8u, 0xb8537c6du, 0xb9961ff5u, 0x3915e7c5u, 0xb1d42c06u, 0x30263f5au, 0x32375a7au, 0x31bf7978u, + 0xb15bef07u, 0x3140f2eau, 0xb24d384fu, 0x33074cabu, 0xb1990468u, 0xb2bede64u, 0x32f22dfeu, 0x32efedc5u, + 0x333dba45u, 0x32fb6daeu, 0x338d3276u, 0x34108e15u, 0x33c35950u, 0x3478f0bau, 0x3240dd6bu, 0x3444d34cu, + 0x330064dcu, 0xb30068e9u, 0x330b1b55u, 0x3418b7ecu, 0x342db3adu, 0xb2f5caabu, 0x33f5362fu, 0xb1b47e93u, + 0xb3e7db32u, 0x33f8e9ecu, 0x33602d7au, 0x342a4765u, 0xb400f485u, 0x322ede9cu, 0xb340af47u, 0xb3d30779u, + 0x33da62e8u, 0xb4402045u, 0xb428e0c4u, 0xb2c236a3u, 0x348c5c41u, 0x336ede29u, 0xb4414a1bu, 0x338eec5du, + 0x33632da0u, 0xb332b6f3u, 0xb2e696a0u, 0x32d0e280u, 0x34003db9u, 0x3490dcb7u, 0x33d009e8u, 0x3407e08fu, + 0xb424919du, 0x3450a500u, 0x341d56adu, 0x32ec6b32u, 0x347ecbdbu, 0x33fcd5c1u, 0x347f49b4u, 0xb432fa93u, + 0x33abd76au, 0xb7903609u, 0x3831227bu, 0x3991da92u, 0xb684f7a9u, 0x316b2d67u, 0x3185edc9u, 0x329676ccu, + 0xb325ad86u, 0xb32037a4u, 0xb2347b21u, 0xb36a8cb6u, 0xb319a6f6u, 0x30cd22d6u, 0xb3175ce5u, 0x33c38a59u, + 0x30b2442cu, 0xb18e10deu, 0xb28b6dd8u, 0xb381f9dfu, 0x3164da3au, 0x324b585du, 0x334ca045u, 0x336e4d52u, + 0x3379f391u, 0x328d0942u, 0x345f0a32u, 0xb315be30u, 0x3236ba1du, 0xb43cff16u, 0xb409c541u, 0x33024a5eu, + 0x33b860e5u, 0xb32dac45u, 0xb3c009edu, 0xb3b8ef9fu, 0x3497e1f0u, 0x33db4545u, 0x3426946au, 0x3307714cu, + 0x336bac31u, 0xb491eb17u, 0xb39f210fu, 0x342ca96eu, 0xb3d88d20u, 0x3342bb53u, 0x342e83cau, 0xb4de6de7u, + 0x3456c887u, 0x3318f4ecu, 0xb4533b64u, 0xb42bdde6u, 0x34b96dacu, 0xb38de7ceu, 0x347c1f8eu, 0xb504547du, + 0xb43e6a0fu, 0xb3a2a040u, 0x345599dcu, 0x334bcaa7u, 0xb3186c82u, 0xb356691du, 0x343794bcu, 0x33b08011u, + 0x330d6667u, 0xb3da68f7u, 0x3914aae9u, 0xb80e3066u, 0xb98e030eu, 0xb90e00d5u, 0x32ed38e0u, 0x3298c95du, + 0x323ded66u, 0xb32527a9u, 0xb2a76b0fu, 0xb31f249au, 0xb25f49c1u, 0x33437589u, 0x33fa8564u, 0xb0b8db42u, + 0x33424532u, 0x32891f93u, 0x2f072021u, 0x33386008u, 0xb455b314u, 0x32a3ce58u, 0x340b1c87u, 0x3480d054u, + 0xb403695au, 0x33855de5u, 0x322d1b94u, 0xb402f200u, 0xb31069b1u, 0x347db1d8u, 0x3404bae7u, 0x3421a027u, + 0x317d6df0u, 0x3436dddfu, 0xb381cca1u, 0xb3df8065u, 0x3501ff63u, 0x35125e22u, 0x328ec6dbu, 0xb304035du, + 0x341b1212u, 0x34e18c84u, 0x3046c432u, 0x3423dd9du, 0xb3bfb0f0u, 0x3383477fu, 0xb3b8a6dau, 0x3499e057u, + 0xb4646ceau, 0xb4936ab8u, 0xb506c8a7u, 0xb4eeb3ccu, 0xb4e8da59u, 0xb422979au, 0x33f1279eu, 0xb3e246c3u, + 0xb32e9cf7u, 0xb3aed5e9u, 0xb2891690u, 0xb47f60d8u, 0xb2a7b976u, 0xb2898d58u, 0xb51c12e9u, 0xb4f8030cu, + 0xb4f1292fu, 0xb51739d8u, 0xb461302du, 0x372d738fu, 0x37e3c518u, 0x398a2a4cu, 0x36634b72u, 0x31668574u, + 0xb1d2d249u, 0x3277033bu, 0x32780ad1u, 0x330a028bu, 0x33108c21u, 0xb31bb77eu, 0x3166a9afu, 0x33e8319cu, + 0x3303401eu, 0xb2ecbff9u, 0x324936b7u, 0xb227f12au, 0x32c87cf9u, 0xb3c24474u, 0x338a4e55u, 0x330b1777u, + 0x3376abdcu, 0xb41bf789u, 0x332870cdu, 0xb33e70f1u, 0x3442d47cu, 0x334dce00u, 0x33828adfu, 0x3293322cu, + 0x326fb912u, 0x343d022au, 0x3443f6cbu, 0x34d49a60u, 0x35060c75u, 0x33caf08au, 0x33e8085au, 0x341a9c84u, + 0xb432a715u, 0xb431d2bcu, 0xb3e431aeu, 0xb332ed21u, 0x340cd0acu, 0x32f23452u, 0xb21f087bu, 0x344d84edu, + 0x348a0ccau, 0x3399cbbeu, 0xb217d736u, 0xb39dc6d1u, 0xb329b694u, 0x33af229bu, 0xb3c07d79u, 0x34f7a681u, + 0xb2aa9295u, 0x34c28b10u, 0x3410cbeeu, 0xb3bdb19fu, 0x329ae8cdu, 0x3500f0dfu, 0x34ce0e52u, 0x3416f85du, + 0x33c62b17u, 0x3126fe11u, 0x349bfe46u, 0xb4829eb9u, 0xb90d4cccu, 0xb7a0d2c5u, 0xb986afe6u, 0x390680deu, + 0xb1c0fa9fu, 0xb2f24968u, 0x31964bc7u, 0xb3290495u, 0xb32ac77eu, 0x3303f95eu, 0xb35d6136u, 0xb32204f6u, + 0xb3843705u, 0xb385c435u, 0xb3021749u, 0xb227007eu, 0xb366bab7u, 0xb3f265fau, 0xb364750au, 0xb3c85e62u, + 0x337d6a68u, 0x33685633u, 0xb1d52e8cu, 0xb4394839u, 0xb40e4fe5u, 0xb1b2cbceu, 0xb1080be8u, 0xb3db2419u, + 0x33f1a9cbu, 0xb295e74du, 0xb4419cceu, 0xb4925a3eu, 0xb4020252u, 0xb458b37cu, 0xb34f9124u, 0x3392c7e2u, + 0xb2dff87cu, 0xb3da2fdeu, 0xb410439au, 0x33c3b3ffu, 0x3304a12au, 0xb2da5757u, 0xb1842379u, 0xb4f979f6u, + 0xb429e63eu, 0x33831c54u, 0xb40c40c0u, 0xb4c1395fu, 0xb453427bu, 0xb464aee1u, 0xb4af14beu, 0x33dccde3u, + 0x33801480u, 0xb4e45d3fu, 0xb537da7eu, 0xb50bf2c3u, 0xb4c8fc3eu, 0xb508cc2cu, 0xb2647009u, 0xb467e624u, + 0xb522cbc1u, 0xb49c178bu, 0xb457b388u, 0xb50842b0u, 0xb0eaf64au, 0xb65a3598u, 0x375347a4u, 0x398337c6u, + 0xb6573722u, 0x336d85d4u, 0x336f7231u, 0xb168aee6u, 0x33f9a3b7u, 0x33f96aa3u, 0xb154fa33u, 0x33f5ffdau, + 0x340055f7u, 0x33fb1d09u, 0x34025900u, 0xb40294cbu, 0xb4c134aau, 0xb40395adu, 0x33f95e6eu, 0x33fa3f52u, + 0x33fa5b0au, 0xb4c0d26cu, 0x34026bf3u, 0x33f791deu, 0x3402469bu, 0x34098570u, 0xb4c328e0u, 0x33e8aadeu, + 0x351e20f6u, 0xb4b9cb9cu, 0x33fb68a0u, 0xb4bd80a4u, 0x33ff94fbu, 0x33f02ff7u, 0x33fd99b1u, 0x3404b253u, + 0x3590b167u, 0xb56285efu, 0x3407bf01u, 0x33e6849bu, 0x340e4a87u, 0x33f3c323u, 0x35905e36u, 0x340ef688u, + 0x3409a4b1u, 0x35904dbbu, 0x3608ee08u, 0x3403d8b7u, 0x33d265f8u, 0x33e0d34du, 0x340fa222u, 0x34080d3du, + 0xb55dab54u, 0xb55fad50u, 0x33eb612fu, 0x33ff728bu, 0xb55f25afu, 0x340195edu, 0xb56183d5u, 0x33fca3d8u, + 0x33f047f4u, 0xb563cd6eu, 0xb5607e86u, 0x340f4362u, 0xb5f1e315u, 0xb55d6d5eu, 0x39072a67u, 0xb6d7006fu, + 0xb9801400u, 0xb9001000u, 0x00000000u, 0x00000000u, 0x34800000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x34800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x36000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xb6000000u, + 0x00000000u, 0x3979de6du, 0x363a7111u, 0x32fe2356u, 0x332871cfu, 0x34065ffcu, 0x33d059c9u, 0xb402b612u, + 0x31eb111bu, 0xb307ebb4u, 0x34915a44u, 0x3414febcu, 0xb3b59681u, 0xb23d8156u, 0x33c6fea7u, 0x34df1c36u, + 0xb3724ddbu, 0x34993f8bu, 0xb3bc17cau, 0x34ada22cu, 0x33f776dau, 0xb4e191d8u, 0x3425e1a5u, 0xb49e6ab8u, + 0x3385097fu, 0xb3913f5eu, 0x34ebdcf4u, 0xb4f5db83u, 0xb4387ad0u, 0xb55bd446u, 0xb38b29fbu, 0x356eae49u, + 0x34ec4613u, 0xb4a3779au, 0xb508d016u, 0xb4ac8fb2u, 0xb52c1e08u, 0xb58eee76u, 0xb4ebfef3u, 0xb4cf8688u, + 0xb2cc5b92u, 0x35253b6eu, 0x343ec74fu, 0x34c03e04u, 0xb594b191u, 0x33bc2e11u, 0x34e49862u, 0xb46d91e7u, + 0xb59d0850u, 0x3505fb89u, 0x3587660eu, 0xb3d8a702u, 0x34e1e734u, 0x35964c2bu, 0x34ff92bdu, 0xb42e932du, + 0x34298614u, 0xb5814ce2u, 0xb35e2666u, 0x360c86bbu, 0xb4486fa8u, 0x3507bb9fu, 0xb5a8caa7u, 0x35cd9edfu, + 0xb900ea41u, 0x36bf5a6fu, 0xb9742a5bu, 0x38f3e4c8u, 0x32598557u, 0x324e14d7u, 0x32ec6f13u, 0xb3090e6bu, + 0xb2a155feu, 0x31aa2111u, 0xb1610caau, 0x3329a6f2u, 0x33ed6746u, 0x32e7efd3u, 0x33aecd65u, 0xb12075f5u, + 0x32d56d70u, 0x33a29db9u, 0x33855b85u, 0x34437d50u, 0x348f7603u, 0xb309f033u, 0xb3a75c1au, 0x340f397au, + 0x340ae804u, 0x2fc8fc16u, 0xb2d3cf82u, 0x349d5de7u, 0xb2599859u, 0x33830494u, 0x3403944fu, 0x33334bf1u, + 0x34474a12u, 0x33bddaf8u, 0x32cef346u, 0xb4c36359u, 0xb28c2279u, 0x3376e3c3u, 0x33b33a36u, 0xb3023e7fu, + 0x3480020bu, 0xb2855fafu, 0xb40eb8e3u, 0x343a562bu, 0xb41025e1u, 0x308ddac3u, 0x33501d80u, 0x33ea38fdu, + 0xb4cd3c02u, 0xb50137a0u, 0x335f8e6au, 0x33e90addu, 0x345d5580u, 0x32548be5u, 0xb415f8c6u, 0xb4b5408au, + 0xb3eddc0au, 0xb44ffe92u, 0xb4057381u, 0xb2e11bd9u, 0xb4584b9eu, 0x33a9e8e8u, 0xb21f1b78u, 0xb2d5c508u, + 0xb4398693u, 0x370cbe36u, 0xb73bde44u, 0x396e75d0u, 0xb63285f2u, 0x30b2c481u, 0x32c0db89u, 0x3330f32fu, + 0x3067529eu, 0x326911e7u, 0xb332c49du, 0xb23f83f7u, 0xb20dac69u, 0x322d0b12u, 0xb3d65bd1u, 0xb36e8efeu, + 0x32b68ba4u, 0x339ba6a3u, 0xb3a7c282u, 0x32d54fd5u, 0xb3ea7715u, 0x3337cb09u, 0xb2acc4c7u, 0xb3e0deeeu, + 0xb41f0a54u, 0xb34b3d0du, 0xb38b44b7u, 0xb2ce42fau, 0x32d5c982u, 0xb38beacdu, 0xb3ac06e4u, 0x33df24acu, + 0x33bc4daau, 0xb3a91a01u, 0x32ca0463u, 0x3416197bu, 0x34672cf2u, 0x3319426eu, 0xb3b2a2dcu, 0xb49a0052u, + 0x3461b457u, 0xb513e6e6u, 0xb42a36ebu, 0x339f732au, 0x34724ee8u, 0xb46e8c2du, 0x335df135u, 0xb3a6f9bdu, + 0x337b334fu, 0x34270947u, 0x344986e5u, 0x34a26f09u, 0x347a307du, 0x33acc1fdu, 0x3490103au, 0x34fc5230u, + 0x34b2c6b9u, 0x34fdd568u, 0xb486a48du, 0xb4bf308du, 0x348396aeu, 0xb3cd4efbu, 0x34ce944au, 0xb26d3784u, + 0xb387ca17u, 0xb3898220u, 0x38f45c51u, 0x378d6625u, 0xb9694525u, 0xb8e949edu, 0xb0cf8dd4u, 0x32473b4au, + 0xb29487afu, 0x336d06acu, 0xb1e88429u, 0x33128716u, 0x32a301dbu, 0x33497c7bu, 0x3416a38du, 0xb33123a1u, + 0x335cc34fu, 0xb11cf4a9u, 0x33fc5f70u, 0xb2e36b0cu, 0xb2f9bc54u, 0xb4393eaau, 0xb239f086u, 0xb4155268u, + 0xb3344b19u, 0x3415d41du, 0x34ae340bu, 0x33ad0d44u, 0xb410f634u, 0x34433ae8u, 0xb441ef66u, 0xb0a60b6du, + 0x33d9d091u, 0x31c3a653u, 0x346a89c1u, 0xb3d7579bu, 0xb42c13dbu, 0xb4308669u, 0x3345b394u, 0xb40ff5c8u, + 0x34a359b7u, 0x351d15beu, 0x34e3b9feu, 0x34e3e9a9u, 0xb3bb1ff0u, 0x3450b34fu, 0x34a6b91fu, 0x34cf8741u, + 0x35947981u, 0x350baf88u, 0x3492d026u, 0x3432a29au, 0x34908d91u, 0x351cbbc3u, 0x34365de2u, 0xb3f0e3e0u, + 0x340ae82du, 0x354db96du, 0x3464d8b3u, 0x358601d7u, 0x34e8b8f8u, 0x35003097u, 0x35516105u, 0x351ab4deu, + 0x34e0bea7u, 0x3533bda9u, 0x35758914u, 0xb76d23c0u, 0xb7b55458u, 0x39641239u, 0x3619efe4u, 0xb2a6ba1du, + 0xb2ced48bu, 0xb2f34d31u, 0xb34786c8u, 0xb33cb0dau, 0xb356ad28u, 0x32ec53ecu, 0x332c8a34u, 0x3360f8b3u, + 0xb2a10bc7u, 0xb385fbb6u, 0xb2f60bb9u, 0x32e71740u, 0x3316972cu, 0xb4591af1u, 0xb438d4a6u, 0x3306a270u, + 0xb327c639u, 0xb45df324u, 0xb41aadf3u, 0xb3df9cb0u, 0xb28c2910u, 0xb4aa6ecau, 0xb435c521u, 0xb3035fb2u, + 0xb3c4cc9fu, 0xb425cafau, 0xb4998901u, 0xb2e80f88u, 0xb44f3f29u, 0xb4a63b38u, 0xb4779aedu, 0xb526f0e9u, + 0x32d8eaafu, 0xb4c68ca1u, 0xb4c4a0b7u, 0xb271dcebu, 0xb423b728u, 0xb4dd1737u, 0xb46c2ab1u, 0xb4c1e9a2u, + 0xb4c8b3edu, 0xb3166b90u, 0xb42d0039u, 0xb3d3e7f6u, 0x32829921u, 0xb3e1feeeu, 0xb51dcfd6u, 0xb4281317u, + 0xb45065eeu, 0x32e4daa6u, 0xb455e12eu, 0xb2984d0au, 0xb4a2041fu, 0xb539719au, 0xb26f8ae9u, 0xb4165368u, + 0xb4bb002du, 0xb4042fd8u, 0xb4cc8b43u, 0xb4697734u, 0xb8e7badfu, 0x37dbaea3u, 0xb95f5eadu, 0x38df176bu, + 0xb2d60c7cu, 0xb2e97780u, 0xb39d4793u, 0xb39168f8u, 0xb3b217cau, 0xb3be26b8u, 0xb3e8e91au, 0xb3d485f4u, + 0xb4165b6bu, 0xb2ab099bu, 0xb3dfea3fu, 0xb42f3c39u, 0xb420bc22u, 0xb40cd7a6u, 0xb494ba8du, 0xb480b682u, + 0xb4aeb9d4u, 0xb4837de7u, 0xb417a1b8u, 0xb2a0afe1u, 0xb420d148u, 0xb3f1dfa6u, 0xb4e26289u, 0xb4a92bb5u, + 0xb4b3c047u, 0xb500e35bu, 0xb4bcbafcu, 0xb481885eu, 0xb474677au, 0xb4c6a51bu, 0xb4ba7640u, 0xb4eb4432u, + 0xb5158487u, 0xb4e19d65u, 0xb522b714u, 0xb501b868u, 0xb56487deu, 0xb54bfc47u, 0xb446dfcdu, 0xb4cd77c6u, + 0xb371a88eu, 0xb541998bu, 0xb508e881u, 0xb4f2ce98u, 0xb5666951u, 0xb511d828u, 0xb4e6a7c5u, 0xb5863baeu, + 0xb52aaa96u, 0xb55f6f2cu, 0xb54409c4u, 0xb50aa6e4u, 0xb567ed80u, 0xb584b5ecu, 0xb4c1bc46u, 0xb4cabb05u, + 0xb4ecbbaeu, 0xb55cfb99u, 0xb56daf2du, 0xb4c77bf6u, 0xb58ba1bbu, 0x37937ea1u, 0xb805cdd3u, 0x395aa09eu, + 0xb612134bu, 0x32ce9b3eu, 0x3314f982u, 0x33b50494u, 0x33868ea3u, 0x33881239u, 0x337abed0u, 0x34580effu, + 0x33ce7177u, 0x32ad09bdu, 0x33b24a36u, 0x33f4e5c5u, 0x345e911eu, 0xb2ecef1fu, 0x33df48c8u, 0x33f764c5u, + 0x331f5715u, 0x34a84131u, 0x340eb125u, 0x3440d3b4u, 0x34c4f01eu, 0x34ac391fu, 0x34b0951bu, 0x349fbe39u, + 0x34b81832u, 0x34debb50u, 0x34773d00u, 0x34d8082du, 0x3484c9a7u, 0x34b6fa92u, 0x351270f3u, 0x35015118u, + 0x347419c8u, 0x34a5715au, 0x3519df80u, 0x34c00f0cu, 0x34b3345au, 0x3581347au, 0x357c92fdu, 0x349a4b2eu, + 0x34ac2a50u, 0xb3e3ebc2u, 0x34e22c85u, 0x348c7cc4u, 0x34fe1bc1u, 0x34d878aau, 0x34d9094fu, 0x3510951cu, + 0x33c51386u, 0x33de78a4u, 0x34f64a22u, 0x3530d8c7u, 0x34f3b2ccu, 0x3478fab9u, 0x359561f7u, 0x349405adu, + 0x350e45bcu, 0x350bd9eeu, 0x356ed176u, 0x3425710du, 0x35610351u, 0x358b1caau, 0x38ddeee2u, 0x3817264cu, + 0xb95651e8u, 0xb8d65ae5u, 0xb2d70f91u, 0xb2d67034u, 0xb2e37b3bu, 0xb38bd7cdu, 0xb330d3f0u, 0xb18b35c5u, + 0xb39ead77u, 0x33a27fcbu, 0xb407deaau, 0x33558598u, 0x33a1f638u, 0xb41d2343u, 0xb3a7802fu, 0xb2bc182eu, + 0xb43d7537u, 0xb4618d02u, 0xb4489901u, 0xb4d2d208u, 0xb4c2e142u, 0xb4cdb083u, 0xb45e7577u, 0xb4fad57au, + 0xb17e6c94u, 0xb469f96au, 0xb4a47d73u, 0xb3f48aeeu, 0xb4de37b9u, 0xb48ea541u, 0xb4744617u, 0x31e4f69fu, + 0xb5101881u, 0xb3a62bb1u, 0xb52064c3u, 0xb4bdad66u, 0xb48cb1b0u, 0xb4b355fbu, 0xb5242d19u, 0xb552d1b0u, + 0xb59a58d8u, 0xb3b8ddd6u, 0xb52dbc49u, 0xb4975639u, 0xb51c6186u, 0xb516de0fu, 0xb4f37dbfu, 0x3422e10bu, + 0xb47e9f23u, 0xb4e65dd0u, 0xb4fcdf17u, 0xb54a827du, 0xb5138c09u, 0xb56a6887u, 0xb5337df5u, 0xb5a8f183u, + 0xb4fb95f4u, 0xb5027987u, 0xb4a47598u, 0xb4db7522u, 0xb58817acu, 0xb5778739u, 0xb516ebc2u, 0xb7c29110u, + 0xb82621c7u, 0x3951fa90u, 0x36005d81u, 0xb2c6089cu, 0xb2a18986u, 0xb350c8e5u, 0xb3c31fb7u, 0xb2ee4c9cu, + 0xb4212129u, 0xb357c3a2u, 0xb34c2e5au, 0xb40b9bccu, 0xb32ad638u, 0xb2aedea9u, 0x33250ae4u, 0xb2fe5eb2u, + 0xb49c3b23u, 0x338bc1e6u, 0xb4113de9u, 0xb33efdb0u, 0xb45519e1u, 0xb4d05dcbu, 0xb4566a6au, 0xb432d2bbu, + 0xb4ca6adfu, 0xb15b70f4u, 0xb438cd62u, 0x331f41dfu, 0x33c75100u, 0xb49c6c01u, 0xb450b9b3u, 0xb4a69188u, + 0xb42d1e39u, 0xb4933460u, 0xb5084985u, 0x3405d9d9u, 0xb46286a7u, 0x349eec99u, 0xb4781fcdu, 0xb33eafd5u, + 0x33f247fau, 0x34050bceu, 0xb571b6dcu, 0x34dff230u, 0xb4fb418du, 0xb4d40c25u, 0xb4039a19u, 0xb38517d3u, + 0xb500d531u, 0xb3709657u, 0xb440b523u, 0xb5094521u, 0xb51c7175u, 0xb57b0438u, 0x33c9f84fu, 0xb4cde067u, + 0xb5226ff4u, 0xb516ff88u, 0xb5127f0bu, 0x320010e7u, 0x34b88458u, 0xb4c25cfeu, 0xb4abf384u, 0x340af05au, + 0xb8d330feu, 0x3838f553u, 0xb94e0d66u, 0x38cddc60u, 0xb142d42au, 0xb29fd301u, 0xb30efd73u, 0xb36ba1fcu, + 0xb345bb9bu, 0xb3661a2au, 0xb2970082u, 0xb3d09657u, 0xb29c9a88u, 0x328b2d7fu, 0xb393ea72u, 0xb3d69577u, + 0xb4325e64u, 0xb3ea1c05u, 0xb2618c09u, 0xb4454327u, 0xb40340bfu, 0xb4194a44u, 0xb447ad5cu, 0xb397347du, + 0xb33db065u, 0xb40ddb67u, 0xb45d81e8u, 0xb14ab73du, 0xb476994bu, 0xb42d99d8u, 0xb3c6ba0fu, 0xb338829bu, + 0xb4a4f1f5u, 0xb45367b3u, 0xb4962314u, 0xb4a53aebu, 0xb491bfedu, 0xb1c1b787u, 0xb39e52ddu, 0xb4eda60bu, + 0xb53496e4u, 0xb42f6a09u, 0xb496046bu, 0xb283fe95u, 0xb5033787u, 0xb49cb146u, 0xb4770ca7u, 0xb524bd58u, + 0xb508e607u, 0xb463dc22u, 0xb524db1du, 0xb5045280u, 0xb4458bbeu, 0xb4b3a8f2u, 0xb5143ae2u, 0xb4aa82cfu, + 0xb524f40au, 0xb51eaef5u, 0xb54425feu, 0xb41e5cdcu, 0xb4cd8c48u, 0xb52bf9abu, 0xb4d53960u, 0xb57704aeu, + 0xb51d3039u, 0x37dc139cu, 0xb8490564u, 0x394a1e10u, 0xb5f37e17u, 0x3306669fu, 0x337c7512u, 0x3346a43au, + 0x33ac9901u, 0x33d72d15u, 0x3435f4c2u, 0x3397e89au, 0x3404b459u, 0x343a98cbu, 0x3430e5bbu, 0x347a2786u, + 0x3449800au, 0x34a264afu, 0x347475f8u, 0x345d13f0u, 0x349961c6u, 0x34a41e86u, 0x349def17u, 0x34f77946u, + 0x34d9826du, 0x34e73fd6u, 0x347e19acu, 0x344981d2u, 0x34abb74du, 0x34a33027u, 0x352939eau, 0x3495750fu, + 0x34e88648u, 0x34a0eb6du, 0x34d0f196u, 0x351c4850u, 0x34dd2827u, 0x34e0e38au, 0x34442c57u, 0x3520578cu, + 0x354c4211u, 0x3528797fu, 0x3548aea7u, 0x3532c119u, 0x352ca1f5u, 0x353e7ec5u, 0x351c2052u, 0x344a7485u, + 0x350aab81u, 0x35823d6eu, 0x34fda208u, 0x356d85c2u, 0x3551af27u, 0x3592acaeu, 0x34d648e5u, 0x350a01a0u, + 0x35166098u, 0x353186c1u, 0x358caf04u, 0x3582e1b0u, 0x35783a71u, 0x356ac32cu, 0x3551ca91u, 0x3556fc1au, + 0x355b0e51u, 0x34f34578u, 0x38c8c7adu, 0x385c2faau, 0xb9467671u, 0xb8c66fceu, 0x332f7601u, 0x3340d938u, + 0x3399cf96u, 0x33c2e75eu, 0x33a2c447u, 0x3419672cu, 0x3448f46cu, 0x3481fcdcu, 0x348f790bu, 0x347a7010u, + 0x3443ea61u, 0x3454f9e0u, 0x34a9b976u, 0x3464ced3u, 0x34130ab5u, 0x3416bb69u, 0x34b150c7u, 0x34e612bfu, + 0x34f46ff6u, 0x34d9bd8du, 0x344f4c2bu, 0x34d5f617u, 0x34cba8adu, 0x34c45e5cu, 0x3536cbd8u, 0x34f32eadu, + 0x352373f3u, 0x35323995u, 0x3513298bu, 0x3537ae75u, 0x355b7cacu, 0x358fa48eu, 0x357d5f7bu, 0x358f22f8u, + 0x3585230cu, 0x35618f43u, 0x35b250ebu, 0x35670ea9u, 0x3595268bu, 0x351c6c5au, 0x35a50016u, 0x359ce04au, + 0x35ac9c79u, 0x35a072fdu, 0x35e2454au, 0x35acad2cu, 0x3540ec3cu, 0x35b00ef1u, 0x35b77cd0u, 0x35bbdcb1u, + 0x35cd3c8au, 0x358d62e2u, 0x35945667u, 0x35d0928cu, 0x35ac698bu, 0x35ab95f4u, 0x35a54c1fu, 0x35dfc65eu, + 0x35c67e06u, 0x35a9f828u, 0x35d3ce9fu, 0xb7f13863u, 0xb862a2c7u, 0x3942de1eu, 0x35d75d94u, 0xb2d2d963u, + 0xb36074c6u, 0xb38ad6f8u, 0xb2da09ceu, 0xb3c23a45u, 0xb22329b7u, 0xb3ce9553u, 0xb3f21873u, 0xb2c5a5f6u, + 0xb3f669b1u, 0xb444bf00u, 0xb3680f91u, 0xb49fd10au, 0xb20bb162u, 0xb43244c0u, 0xb3f4d56bu, 0xb42c711cu, + 0xb3ac69dcu, 0xb4908a73u, 0xb4a2758fu, 0xb41bff43u, 0xb481f848u, 0xb4b0befbu, 0xb346e40eu, 0xb456e73bu, + 0xb4997b1fu, 0xb46dcbccu, 0xb482bfbau, 0xb48e4249u, 0xb4b1bd14u, 0xb455cc1fu, 0xb48e3fa3u, 0x332e2f43u, + 0xb4c832fcu, 0xb41c9afeu, 0xb4b45fedu, 0xb53bb0d3u, 0xb4f623f9u, 0xb4e59839u, 0xb413bf5eu, 0xb50d5825u, + 0xb5009a78u, 0xb5151fe7u, 0xb54d8261u, 0xb3e01527u, 0xb4adf775u, 0xb5845e7du, 0xb506e3b9u, 0xb4111e3bu, + 0xb50749b9u, 0x33eb33f2u, 0xb5176484u, 0xb5193c4du, 0xb5874e67u, 0xb50adca5u, 0xb48a5f88u, 0xb3ef7227u, + 0xb573061du, 0xb4d61cd2u, 0xb574ce72u, 0xb503407au, 0xb8bdd64du, 0x386fb405u, 0xb93f8fffu, 0x38bf6a72u, + 0xb28b6d60u, 0xb10b7318u, 0xb2f4913cu, 0x33041295u, 0xb30a9dcdu, 0xb1ff37dfu, 0x32cfa4d4u, 0x3332357eu, + 0x33505ae7u, 0x331f2382u, 0x332886d6u, 0xb3f3c201u, 0xb372c3feu, 0xb38b3334u, 0xb45a33b9u, 0xb3b0cebau, + 0xb1341a5fu, 0x32953999u, 0xb3207d0au, 0xb3e0ad4cu, 0xb39f5251u, 0xb4860800u, 0xb35ec3f1u, 0xb4147f40u, + 0xb3d05dddu, 0xb44dd8a0u, 0xb40d254cu, 0xb47c681eu, 0xb46343c4u, 0x3410cefcu, 0x3409ffd8u, 0xb45016c4u, + 0x33d2c1b4u, 0xb19c69f2u, 0xb1edb211u, 0xb110d4e2u, 0xb318bfedu, 0xb2856bf2u, 0x3252bc17u, 0xb4a876a8u, + 0xb340900bu, 0xb4649bdeu, 0xb40a8275u, 0xb4362292u, 0xb448d715u, 0xb4a0c0e6u, 0xb3a10f63u, 0x33b59557u, + 0x348c0d78u, 0xb3c060e4u, 0xb4ad99beu, 0xb4892940u, 0x3413e3ebu, 0x3411b785u, 0x33a2be35u, 0xb48b6b77u, + 0xb2c47fb7u, 0xb4aacbfau, 0x33cb3143u, 0xb354c7c3u, 0x32381c2au, 0x380bb5ecu, 0xb880fe70u, 0x393c3f73u, + 0xb5d4efc8u, 0xb2088b90u, 0xb38b56e8u, 0xb373a9aeu, 0xb355a319u, 0x338742b8u, 0xb3c1b6a3u, 0xb318f41du, + 0x34c10566u, 0x33e24762u, 0x33aa72ffu, 0x33eaa606u, 0x33ba65e8u, 0x341586d2u, 0xb419a756u, 0x31b9cecau, + 0x3434bb16u, 0x3340e128u, 0x343026b4u, 0x33b26416u, 0x3429522bu, 0x338b7983u, 0xb3adf01cu, 0xb4d43f0cu, + 0x34395341u, 0x33d28eb7u, 0xb307a0eeu, 0x3453b83eu, 0xb266c4d0u, 0x306f7578u, 0x34eba0bfu, 0xb4b51e4du, + 0xb45cf7f4u, 0xb36e87b0u, 0x34530d1du, 0x3554c91eu, 0xb4670c6au, 0xb3938182u, 0xb48d60d7u, 0xb4b06913u, + 0x346993ceu, 0xb324001cu, 0x340a7484u, 0x349ba7bbu, 0xb51d3e9bu, 0x33c981f7u, 0x343183a7u, 0x351a4ca0u, + 0xb4853c27u, 0xb3a66cfcu, 0xb4b7061cu, 0xb4590e17u, 0x34db5672u, 0xb408c74au, 0x3479b513u, 0x34fbd952u, + 0x3414014au, 0x344ffd4cu, 0xb5937f33u, 0x33449485u, 0xb3d7337eu, 0x3492a5acu, 0x38b3d589u, 0x3886f41du, + 0xb93939d3u, 0xb8b939fbu, 0x330d0855u, 0x336ecb95u, 0xb31cb2ebu, 0x339fc7abu, 0x33e50d9au, 0xb16eb88eu, + 0xb33bde09u, 0xb39976a4u, 0x3401bf8fu, 0x3435048fu, 0xb2ddfc12u, 0x33cc00bau, 0xb3827a24u, 0xb3ec8404u, + 0xb4530dc1u, 0x335df838u, 0xb49fc891u, 0xb48b7e69u, 0xb46c83b1u, 0xb53c00e5u, 0xb5238305u, 0xb4c2fcd9u, + 0xb5238305u, 0xb4d0c451u, 0xb57c00e5u, 0xb54b0520u, 0xb54b0520u, 0xb5238301u, 0xb50fe449u, 0xb5638301u, + 0xb49bbf68u, 0xb4ec8389u, 0xb3ddfa42u, 0xb3ddfa42u, 0xb3ddfa42u, 0x34b504cdu, 0x347b0cbfu, 0xabbd718fu, + 0x34b504c1u, 0xb4ec8389u, 0xb550c438u, 0x34b504cdu, 0x34b504cdu, 0xb550c43cu, 0xb4ec8389u, 0xb4997629u, + 0xb4ec8389u, 0xb443ef75u, 0xb4b5051bu, 0xb4ec8389u, 0x353504dfu, 0xb580e002u, 0x33a9e3beu, 0xb5273d8cu, + 0xabca6c10u, 0x34261a7bu, 0xb3ddfa42u, 0xb4997629u, 0xb443ef75u, 0xb5939ec6u, 0xb585bf2bu, 0xb8159583u, + 0xb88e3032u, 0x39362e80u, 0x35ba9ca9u, 0x323da66bu, 0x33acbc6du, 0x337d4ce1u, 0x338e3272u, 0x33466ae0u, + 0x3356c7b6u, 0x33c594f4u, 0x33bcc4e4u, 0x34074339u, 0x3357579eu, 0x344dade2u, 0x344f42a8u, 0xb27f70f1u, + 0x330a83efu, 0x34745c43u, 0x33be28afu, 0x34735dcdu, 0xb32a2f69u, 0x34b3d6feu, 0x34fb927fu, 0x34188397u, + 0x34c9f8c8u, 0x3505a23bu, 0xb3be0bfeu, 0x341e9947u, 0x34afce23u, 0x348e8dd8u, 0x3408c379u, 0xb38d08eau, + 0x34ad527bu, 0x34a881f8u, 0x32fa4b2bu, 0x3590d136u, 0x34b71b11u, 0x345c688bu, 0x34e44301u, 0x34f4ed24u, + 0x32c8f03du, 0x34284fb8u, 0x34f3b3fcu, 0x3522fe6eu, 0x35266b60u, 0x34740fdau, 0x355ef42au, 0x3528f767u, + 0x34e73811u, 0x355667dau, 0x35afbfd4u, 0xb361dd1cu, 0x35815d99u, 0x3363536du, 0x34babf5du, 0x349e5d82u, + 0x354de959u, 0x354350aeu, 0x35c49241u, 0x3580f3a9u, 0x35c758e1u, 0x3557c787u, 0x35c064a0u, 0x351cb056u, + 0xb8a88030u, 0x3895744du, 0xb933628eu, 0x38b3434au, 0xb2958bb0u, 0xb31ae73eu, 0xb36cfce9u, 0xb25adcceu, + 0x328b49f2u, 0xb39ee314u, 0xb32f08f0u, 0xb3585f5au, 0xb39f78eeu, 0xb3d85741u, 0xb411b731u, 0xb392924bu, + 0xb32cdd48u, 0xb395ac39u, 0xb3e8a48fu, 0x2f5aa0dfu, 0x32010d8du, 0xb4222a64u, 0xb40a85b3u, 0xb45e2472u, + 0xb455cd64u, 0xb3938fffu, 0xb2fb6db5u, 0xb2f96bfau, 0x3372ea37u, 0x33cf0888u, 0x342bbffcu, 0xb44b1e0eu, + 0xb36560cau, 0xb3efea94u, 0xb42ac263u, 0xb3b149f3u, 0x339c30a6u, 0xb2bd6cbfu, 0xb45a0f03u, 0x34470ca6u, + 0xb4ed8826u, 0xb2496512u, 0xb50c4221u, 0xb4323c5au, 0xb4c7bf68u, 0xb44e99aau, 0xb51024a9u, 0xb51d57feu, + 0xb4d3ccdeu, 0xb3bd7044u, 0xb3ee4423u, 0xb4993ef3u, 0xb43fcd4cu, 0xb462d260u, 0xb3e77278u, 0xb4aafcdbu, + 0xb45f1045u, 0xb45c4922u, 0xb4ae090bu, 0xb4e2e661u, 0xb488be81u, 0xb515f9f3u, 0xb4bed107u, 0xb4be3be2u, + 0xb4a875b0u, 0x38219f41u, 0xb89a744cu, 0x3930995bu, 0xb5ae184du, 0x32292051u, 0x321d19e1u, 0xb1694f33u, + 0xb3397432u, 0x33721eedu, 0x3359ac97u, 0xb2c5dac4u, 0x33d4f4b2u, 0x338859acu, 0xb27fee3au, 0x3416aef3u, + 0x338f8392u, 0x342ad694u, 0x324a3f9fu, 0xb30e4df2u, 0x3436f100u, 0x3412273bu, 0x331d43ccu, 0x33ee5064u, + 0x344acebcu, 0xb3c7ed42u, 0x34023abeu, 0x3465357fu, 0x3329f51bu, 0xb38bb323u, 0xb3fcf62fu, 0x343f414fu, + 0x331ef862u, 0x34a042dbu, 0x34ca3584u, 0x344cbe4cu, 0x33c2581fu, 0x343f4aadu, 0x3445cd2au, 0x3454783eu, + 0xb4350c76u, 0x34a5229cu, 0x32744cc8u, 0x343ecaa3u, 0x34dd9168u, 0x34d16e80u, 0x33be291au, 0x3548698du, + 0x34c70b9cu, 0x3333beeeu, 0x34d7d71fu, 0x34546242u, 0x34019486u, 0x338f2a92u, 0x333111cbu, 0xb42f3441u, + 0x34964606u, 0x34db5baau, 0x34b5ee41u, 0x348f2bf9u, 0x3409f2fdu, 0x34773bf8u, 0x34a1b6dcu, 0xb3ae5e24u, + 0x351fec92u, 0x34161a3eu, 0x38a19b8fu, 0x38a08860u, 0xb92e0973u, 0xb8ae0dedu, 0xb303eb59u, 0xb117b493u, + 0xb3722089u, 0xb3c59740u, 0xb2d626e5u, 0xb3a50e5eu, 0xb336d361u, 0xb416e775u, 0xb37631a2u, 0xb4273114u, + 0xb469c8fau, 0xb3884e05u, 0x333402ecu, 0xb3641f09u, 0x320775f9u, 0x31b6a33fu, 0xb36743f8u, 0xb4556b8eu, + 0xb33f74c2u, 0xb4dfb8d6u, 0xb4752080u, 0xb4a54238u, 0xb4ba1a8du, 0xb476aad7u, 0xb46ee448u, 0xb329bf5bu, + 0xb4880863u, 0xb48caa0fu, 0xb480cc57u, 0xb4800b12u, 0xb4e20d2du, 0xb4248efdu, 0xb4dea959u, 0xb520a6cfu, + 0xb4c2a340u, 0xb357ca98u, 0xb48c5467u, 0xb4bf9305u, 0xb521da11u, 0xb5284c6fu, 0xb493fe60u, 0xb53a144cu, + 0xb266cbe2u, 0xb53a0d17u, 0xb51ccd97u, 0xb520aa60u, 0x32589151u, 0xb40ce91eu, 0xb496ae56u, 0x335673b3u, + 0xb31677f2u, 0xb4dd04e1u, 0xb4a4adb8u, 0xb48a7aa9u, 0xb56e2e49u, 0xb58c6ad6u, 0xb532c2b9u, 0xb596fbceu, + 0xb51eebfdu, 0xb5211ba1u, 0xb513f6deu, 0xb82e9b22u, 0xb8a3593du, 0x392b79ecu, 0x3599086cu, 0xb236c3cdu, + 0xb225511eu, 0x32876266u, 0xb2e89814u, 0xb0dc3115u, 0x32284abfu, 0xb28f0c8au, 0x32ccbb82u, 0xb3877bbdu, + 0xb284df03u, 0xb3d41b57u, 0xb2def5ddu, 0x3352f275u, 0x322b726au, 0xb38e3f45u, 0x33915940u, 0xb23e3fccu, + 0xb319a659u, 0xb435912eu, 0x3402a714u, 0x32c970f5u, 0x3476de07u, 0x33bb161du, 0x325a6b0eu, 0x33d478f7u, + 0xb477d6ffu, 0x33c0398bu, 0x33c6f53du, 0xb3ca5995u, 0x3374080eu, 0xb4884310u, 0x327f50ddu, 0xb46cd2b6u, + 0xb43c1e04u, 0x33a6e413u, 0xb3b012c2u, 0xb4330a2du, 0x31d6d14bu, 0xb0d62233u, 0x33f7585bu, 0x3478ceffu, + 0xb30529c9u, 0xb47fcf8cu, 0xb4095948u, 0xb470bce7u, 0xb2597ba1u, 0x33303e96u, 0xb427e1a5u, 0x3369e75fu, + 0xb42e94b9u, 0x345c19cbu, 0xb428af13u, 0xb46767e0u, 0xb44e4d12u, 0x34200b70u, 0xb4ab920bu, 0xb4fc1b7au, + 0x312a6f87u, 0xb4c03609u, 0x2f8408c1u, 0x349c6ed7u, 0xb8987210u, 0x38a7fcb8u, 0xb92920fdu, 0x38a9014bu, + 0xb14bb073u, 0x3250ca8du, 0xb272d9c7u, 0x331dfea3u, 0xb2148693u, 0xb2559f4du, 0xb2faaaf9u, 0xb23a2900u, + 0x32bd64b9u, 0xb19a8372u, 0xb2e967cfu, 0xb354bc82u, 0x327c6e3cu, 0xb1a07811u, 0xb2b2b975u, 0xb393c54au, + 0xb3e065bfu, 0xb4060915u, 0xb33ae0d3u, 0x33ad3b17u, 0x318e9a25u, 0xb151d0d1u, 0x3361f51du, 0xb42cc816u, + 0xb46d1284u, 0xb38da2eau, 0xb3fb6e33u, 0xb41caf3du, 0xb43b8b3eu, 0xb407121fu, 0xb404ab4cu, 0xb4713006u, + 0xb396de2fu, 0x3413da2eu, 0x34029438u, 0xb27e0d18u, 0x340b8f6eu, 0xb4507643u, 0x32d23253u, 0xb4a7140du, + 0xb4a835d0u, 0x33f0a4dau, 0xb46c0984u, 0xb4ab8412u, 0xb3b73291u, 0xb41bf983u, 0xb3b0135eu, 0x333ddb4au, + 0xb4a12e67u, 0x3403ba63u, 0x342a629fu, 0xb444a57bu, 0x34878c0au, 0x33cd1e10u, 0xb4991dedu, 0xb338013fu, + 0xb417b0b7u, 0xb3486c1cu, 0x342b63e8u, 0xb28213cfu, 0xb3bf8419u, 0x3835b4c3u, 0xb8ae0392u, 0x3926c63eu, + 0xb59289e5u, 0x31e26110u, 0x331907d8u, 0x3281be11u, 0xb3b54fccu, 0xb3610442u, 0x33ce5f0bu, 0xb1234fc1u, + 0x32b1da7au, 0xb245d831u, 0x32fea16fu, 0x32d099a8u, 0xb36a5568u, 0x330f8bbfu, 0x31952dfcu, 0xb1e2ed06u, + 0xb3bf0684u, 0x33e64671u, 0xb3323a63u, 0x341ac72cu, 0x33b68968u, 0x349fae1bu, 0xb3c53336u, 0x33dd2858u, + 0x339d04ffu, 0xb3fb33eeu, 0x33764f28u, 0x348a02a6u, 0x32d62aaeu, 0x3497266fu, 0xb20bf8c0u, 0xb49039d9u, + 0x3440c65eu, 0x3362d638u, 0x348d68dau, 0x31b936c5u, 0x32431aabu, 0x348963ccu, 0xb484c94eu, 0xb39d44dfu, + 0xb41e5f04u, 0x3486dfaeu, 0xb3978da6u, 0xb41fdc88u, 0x34a8bee4u, 0xb468991eu, 0xb44c2899u, 0x330c8d43u, + 0xb5155dbau, 0x33d40cdfu, 0xb2199bf6u, 0x33f64c94u, 0x357cfda6u, 0x34b526aau, 0x33d89d28u, 0x349f1d34u, + 0xb413275du, 0xb5370378u, 0x34d1defau, 0x35214c2cu, 0x340cbd81u, 0x329973e1u, 0x388e9a10u, 0x38b31281u, + 0xb9249d04u, 0xb8a495b0u, 0x32bdb0bdu, 0x3284d8d8u, 0xb28aa8d9u, 0x3263b023u, 0x33465af6u, 0xb2ab307eu, + 0x342cb73bu, 0x33914a1bu, 0xb44275a0u, 0x3440ebdfu, 0x3445eab8u, 0x3282ae12u, 0xb3ed8b0cu, 0x32921f82u, + 0xb357c8acu, 0xb395c4dfu, 0xb17111b3u, 0x333b0446u, 0x34b8c840u, 0x3373e986u, 0xb2b159b4u, 0x34e524ceu, + 0x32752fd6u, 0xb43d621fu, 0x3489a806u, 0x344f6189u, 0x34935a5cu, 0x33be974au, 0xb28d9cc1u, 0x33ab6369u, + 0x3523b676u, 0xb35e1f00u, 0xb5074ec6u, 0xb433c421u, 0xb4d023fau, 0xb502c66bu, 0xb3746224u, 0xb2866acau, + 0x356d00bbu, 0x34ba9e8eu, 0x3449b2a5u, 0x34b8b9d1u, 0xb509ebedu, 0xb44cc73eu, 0xb4594212u, 0xb3b8a671u, + 0xb2138093u, 0xb3f602c5u, 0x344ba0a0u, 0x34e670d9u, 0xb3c55afeu, 0xb33dde58u, 0xb4326d3fu, 0x3509ee76u, + 0x34c438a5u, 0x354b53deu, 0x34899f82u, 0x34e4d7eau, 0x3539f839u, 0x35999d86u, 0x34dc0ae2u, 0xb83effe9u, + 0xb8b7f408u, 0x39227458u, 0x3581e405u, 0xb1815558u, 0x31d958d2u, 0x32a329a2u, 0x338ba611u, 0x3343c0afu, + 0x3355e2ffu, 0x3187ce92u, 0x303c324eu, 0x3400369au, 0x32f13579u, 0x30a7b660u, 0xb25d0fa4u, 0xb3b88c5au, + 0x336cc533u, 0x3320beecu, 0xb3439912u, 0xb38f272eu, 0x33e70758u, 0x3445e0d6u, 0x342c3161u, 0x3477a839u, + 0xb33d4dc3u, 0x33af3505u, 0x33320bbcu, 0xb4420569u, 0x33cc57b5u, 0x33160464u, 0xb1f771c2u, 0x33071f93u, + 0xb2496a98u, 0x3417f9a4u, 0x34039057u, 0x34f2afe5u, 0x3324e8a3u, 0x341eb9fdu, 0x33dfc91du, 0x348dd56au, + 0x345392f4u, 0x34d7444du, 0xb31993a8u, 0xb2405205u, 0x33f7e7e0u, 0xb2cbb986u, 0x344581ddu, 0x342d218au, + 0x34a8b988u, 0xb4a70ad6u, 0x34dd6254u, 0x34af3ef7u, 0xb3591804u, 0x33e8b158u, 0x33fdcc7bu, 0xb40e8e10u, + 0x346e74afu, 0x34819968u, 0x35377ee3u, 0xb4228c4du, 0x345c2c12u, 0xb4d2c420u, 0x34b358abu, 0xb4e0fa19u, + 0xb885f443u, 0x38bbf22au, 0xb9207998u, 0x38a06421u, 0xb235bf82u, 0x318fa782u, 0x3287fd91u, 0x3151f6deu, + 0xb19a1b21u, 0xb306dbf1u, 0xb251ebb5u, 0x324b616fu, 0xb2e796ccu, 0x339f58dcu, 0xb12e2c39u, 0x331c4844u, + 0xb215f8b7u, 0xb2464f28u, 0xb18b6583u, 0xb38b63cfu, 0xb39e6503u, 0xb3d050bfu, 0xb2e390e2u, 0xb4651ee4u, + 0xb353f77eu, 0xb37f0ae6u, 0xb350c6aeu, 0xb433eba2u, 0x32047cd7u, 0x32265be3u, 0xb4368b1cu, 0xb30532e8u, + 0x32f5960bu, 0x34147ebeu, 0xb486b938u, 0xb4331034u, 0xb4cbccccu, 0xb43c121eu, 0xb4b0fdddu, 0xb479bb0eu, + 0xb47f5ea7u, 0x32e8f13au, 0xb40052ebu, 0xb4e2edb6u, 0xb312a402u, 0x343215b0u, 0xb2fed974u, 0xb45938b2u, + 0xb3eb84b9u, 0xb4b8c494u, 0xb4586379u, 0xb43848d0u, 0xb30fc9bdu, 0xb53bdb84u, 0xb4ccd59eu, 0xb50ae1adu, + 0xb4d94dc0u, 0xb4ea43a0u, 0xb496e4aau, 0xb4998601u, 0x33b61d18u, 0xb3500aa2u, 0xb41f1991u, 0xb512ffbdu, + 0x331fdc76u, 0x38461ef4u, 0xb8bef4feu, 0x391e7dc8u, 0xb57a4984u, 0xb1c8b901u, 0xb20b3da4u, 0xb2927170u, + 0x3233351cu, 0x3311a6d0u, 0xb350e608u, 0xb26d91fdu, 0xb2f9565au, 0x32ab2c4du, 0xb38a2fd5u, 0x3310a43au, + 0xb415f4bdu, 0xb378790bu, 0xb3fd0b76u, 0xb2eff781u, 0x33a3ebb1u, 0xb339b938u, 0xb3c9abd1u, 0x33f1e3f6u, + 0xb0ba6bc8u, 0x34136d19u, 0xb46ca161u, 0x31240f12u, 0x344cbdfeu, 0xb41a6d66u, 0x34790864u, 0xb413a5f6u, + 0xb42c6103u, 0xb1a65053u, 0x349f8d9cu, 0x34326779u, 0x349c212du, 0xb2cbc6edu, 0x33d0f52du, 0x341be16au, + 0x34a73c71u, 0x338b1223u, 0xb3f5b03au, 0x33f87b56u, 0xb44540f7u, 0x3382fe2du, 0xb4ca86abu, 0x334c2055u, + 0xb4374867u, 0xb3875173u, 0x34bb4680u, 0xb3b96c61u, 0xb3c9664au, 0xaf52dbb7u, 0x34153087u, 0xb4d7e76fu, + 0x33d6d5e1u, 0xb4de39cau, 0xb4eabb8cu, 0xb3c813f1u, 0xb434833au, 0x3477f702u, 0x3306f3e3u, 0x3462cd9du, + 0xb2e49a4eu, 0xb47c979fu, 0x387dbab4u, 0x38c3552au, 0xb91cac5cu, 0xb89cacd0u, 0x31318419u, 0xb2aa8a3au, + 0x31841a9fu, 0x337ad170u, 0x32fec67bu, 0x336cc6efu, 0x335a0c80u, 0xb3be2cfau, 0xb40672e0u, 0xb3a99361u, + 0x3308d755u, 0xb3098d99u, 0x33ae19cbu, 0x31f074b0u, 0xb42ca2b5u, 0xb204bb2eu, 0x2f17019eu, 0xb3b3d155u, + 0xb3e6de99u, 0xb1f6902au, 0xb4075614u, 0xb4ad1334u, 0xb474ab92u, 0xb42c56f6u, 0xb310ba01u, 0xb3884bb2u, + 0xb40b44fbu, 0xb3d765e3u, 0xb33e52c5u, 0x3333f2b1u, 0x34a56d89u, 0x340c2a24u, 0x3450d571u, 0xb3609e69u, + 0x32be3349u, 0x34b4e2abu, 0xb484492eu, 0x33ebb720u, 0x34d4647cu, 0x35100b11u, 0x34a7e6e3u, 0x34e6a649u, + 0x34d3c285u, 0x34b7f13eu, 0x3517415bu, 0x34addc89u, 0xb45e4eb0u, 0x3436c124u, 0xb454b886u, 0xb3c59b02u, + 0xb39740e2u, 0x33ceb9e6u, 0x34faf9d3u, 0xb45d497bu, 0x3489ef5au, 0xb390cf84u, 0xb345d1c1u, 0xb3318302u, + 0x34b2ad9du, 0x3513a7a0u, 0xb416b48eu, 0xb84e0caau, 0xb8c7e24bu, 0x391adbffu, 0x355b76c3u, 0x3247b205u, + 0x3238ddddu, 0xb2fb00dau, 0x30e1db9eu, 0x31b26addu, 0xb2c07075u, 0x3317f87bu, 0x3398ff79u, 0x3310dc91u, + 0xb2cdb60eu, 0x33f68aa1u, 0x33ea7af0u, 0x33d597edu, 0x33b1c96bu, 0x3186a764u, 0x338c041bu, 0xb430d430u, + 0xb139df62u, 0x343180e6u, 0x33e7a11eu, 0xb40ead5bu, 0x33b3934eu, 0xb2b51443u, 0xb2bb2d3bu, 0xb35118a2u, + 0xb35349b9u, 0x3491494au, 0x33be9db2u, 0x340a0359u, 0x338c79aeu, 0x3486c6fcu, 0xb0936a98u, 0xb4417f77u, + 0x345466eau, 0x31cef76fu, 0x33d666f7u, 0x33b776e7u, 0x341e1f58u, 0x33f7f6edu, 0x344d1a9fu, 0x322496ecu, + 0x3491dedcu, 0xb41859c6u, 0x34bd3340u, 0x34c9165cu, 0x351cc279u, 0x3307b15eu, 0x33bd4ccbu, 0x348997bcu, + 0x34a94bc6u, 0xb4b1535du, 0x341b7e43u, 0x34a2dc57u, 0x34baa132u, 0x349edb81u, 0xb43705ceu, 0xb4d0b03au, + 0x3432b348u, 0x3545cfe1u, 0x332dbe4bu, 0x349f563cu, 0xb86b760au, 0x38ca1399u, 0xb91930c7u, 0x38991589u, + 0xb23b893eu, 0x31b7369eu, 0x3252bfceu, 0x32ff1bb9u, 0x32d9265eu, 0xb1e9b5dbu, 0xb131acaau, 0xb36e024eu, + 0x32f1dcd4u, 0xb2235079u, 0x33b68bf7u, 0xb3659b19u, 0x33fa482fu, 0x2f2575bbu, 0x33ae5959u, 0xb2fe6284u, + 0x331a3f94u, 0x337c7e56u, 0x3445743au, 0x348feef8u, 0x319f471fu, 0x3458a7deu, 0x339172bau, 0x34589a7fu, + 0x343552f6u, 0x339f5816u, 0x348e2d77u, 0x33945566u, 0x342d234cu, 0x33857376u, 0x340b667eu, 0x3453485eu, + 0xb380c1abu, 0xb2c16d75u, 0xb4327397u, 0xb2d78daau, 0xb3de6a36u, 0xb40bc724u, 0x3332da94u, 0x34c918acu, + 0x349fa4cfu, 0x3436ace8u, 0xb422bc56u, 0xb312e704u, 0x34939a38u, 0x3454b50cu, 0xb48a685cu, 0x338c9570u, + 0xb3a5f1b5u, 0x33c3a4c2u, 0x34ba836du, 0xb4cc4402u, 0xb2d4976au, 0xb42b1f17u, 0x34a1ecafu, 0xb433a5a7u, + 0x34bd0e94u, 0x34fa1a48u, 0xb31d3419u, 0x33640e9fu, 0xb3a86176u, 0x385624b6u, 0xb8ccd077u, 0x391784a2u, + 0xb54dd7dcu, 0x328909f0u, 0xb10f428du, 0xb3751228u, 0x326a74aau, 0xb42172dfu, 0x3246e4bcu, 0xb184c4e2u, + 0xb2ec082fu, 0x34291c0bu, 0xb32b8027u, 0xb2f11426u, 0x339313cau, 0xb2cd87abu, 0x33c4783fu, 0x33f25674u, + 0x343d9849u, 0x34b66d6bu, 0xb401843bu, 0x34b588e0u, 0xb417160bu, 0x34e68df7u, 0xb45a566au, 0xb4c1d5ecu, + 0x32da9665u, 0x34f46416u, 0xb4328a3au, 0x338ece7au, 0x32938055u, 0xb464b787u, 0xb4624b18u, 0xb4d460a7u, + 0xb4b3afc9u, 0x34482c1fu, 0xb50557c4u, 0x34b474fau, 0x35178f87u, 0x342eb50au, 0x34d7c0f9u, 0x342aeec9u, + 0x33a58c15u, 0xb525c593u, 0x321a78aau, 0x352f2cf3u, 0xb373bfceu, 0x355f1e14u, 0x350093ccu, 0xb4816691u, + 0x34c2b530u, 0x343a2d8bu, 0xb549d55cu, 0x35875f94u, 0x33c3a111u, 0x34cb49c3u, 0x3532efb3u, 0xb2887bc3u, + 0xb4816691u, 0xb426ddf5u, 0xb201d396u, 0x34b39070u, 0xb4d9a971u, 0x34779f06u, 0x385ebe1bu, 0x38d3354eu, + 0xb91601cdu, 0xb89604a1u, 0x3215f5f6u, 0x337ffff7u, 0x3295f608u, 0x28800000u, 0xb3b504feu, 0xb3b504feu, + 0xb4b504fcu, 0xb4b50500u, 0xb43504ffu, 0xb3d413feu, 0xb435050cu, 0x343504f4u, 0xaa400000u, 0xaa400000u, + 0x3395f602u, 0xb53504f3u, 0xaa400000u, 0xb53504ecu, 0xb4b504e6u, 0xb53504ecu, 0xb4b504f9u, 0x2a600000u, + 0x3415f60eu, 0x3415f634u, 0xb53504ecu, 0xb4b504e6u, 0xb4b504e6u, 0x3415f634u, 0xb45413ccu, 0xb4b504e6u, + 0xb4b504e6u, 0xaa400000u, 0xaa400000u, 0xaa400000u, 0xaa400000u, 0xb53504f3u, 0xaa400000u, 0xaa400000u, + 0xaa400000u, 0xaa400000u, 0xaa400000u, 0xb53504f3u, 0xb53504f3u, 0xaa400000u, 0xaa400000u, 0x353504edu, + 0xaa400000u, 0xaa400000u, 0xb53504f3u, 0xaa400000u, 0xb53504f3u, 0x35da8276u, 0x353504edu, 0x353504edu, + 0xb53504f3u, 0xaa400000u, 0x357ffffdu, 0x353504edu, 0xaa400000u, 0x353504edu, 0xaa400000u, 0xb8639790u, + 0xb8d37dd4u, 0x391479f2u, 0x353973efu, 0x327962f3u, 0xb2ab0ba9u, 0xb3a42210u, 0xb317b76du, 0x34263642u, + 0x33ad7ff3u, 0x34148728u, 0xb40823beu, 0x343a14d4u, 0xb43d1adbu, 0x3484c3b0u, 0x34b54d47u, 0xb3e88a59u, + 0xb47f536cu, 0xb40ec09bu, 0xb457964au, 0x34f785f8u, 0x349ad1d8u, 0xb3f5c414u, 0xb5643fa2u, 0xb4a766d9u, + 0xb3a649cfu, 0x3486f347u, 0xb2f17050u, 0xb4c87f45u, 0x3596b5beu, 0x34f326cau, 0xb4229f2cu, 0x33a8fc1du, + 0xb4ae03d6u, 0x32b6489cu, 0x35912ea6u, 0xb5afe730u, 0x3526ce8eu, 0x3512effcu, 0x3512effcu, 0x35abaa24u, + 0x3458ef71u, 0x3526ce8eu, 0x3458ef71u, 0xb5d10381u, 0x34c92bd8u, 0x3423e8e2u, 0xb48aa5c2u, 0xb51b6a14u, + 0xb535e592u, 0xb535e592u, 0xb48aa5c2u, 0xb5220701u, 0xb4da2008u, 0xb53c8280u, 0x352d6b7bu, 0x35f360ccu, + 0x35414a0cu, 0x3512effcu, 0xb535e592u, 0x353aad1fu, 0x352d6b7bu, 0xb5781e34u, 0xb5a3c858u, 0x35abaa24u, + 0xb8500ef9u, 0x38d79b42u, 0xb91312e8u, 0x38930464u, 0xb2816752u, 0xb2b3dc07u, 0x30a53716u, 0x3239cec5u, + 0x325dafb7u, 0x3190363du, 0xb384ece3u, 0x333bc36du, 0x324795d3u, 0x3356ae13u, 0xb0edd151u, 0x32057ac5u, + 0x31ffd110u, 0x331b0f31u, 0x33347cbfu, 0x32d7d7ccu, 0x33be5aa8u, 0x33876d2fu, 0x338d7d7fu, 0xb1b34f4cu, + 0xb200abd2u, 0x3397414du, 0xb1356737u, 0xb1ae832au, 0x3316ff3fu, 0x33d0a52cu, 0xb4228ebfu, 0xb3a44655u, + 0x343778f8u, 0xb0427ffeu, 0x349b9d11u, 0x34bf5ab2u, 0x34199fadu, 0x34afa673u, 0x348665cdu, 0x330776c5u, + 0x345a5707u, 0x32c49b65u, 0x346c4d33u, 0x3483ad9fu, 0x3417a98eu, 0xb41d88a0u, 0xb378249cu, 0x34b44f7cu, + 0x33cfc8f0u, 0x34074d0bu, 0x346a1a4bu, 0x343bb1d5u, 0x348b9658u, 0x334e29f7u, 0x344493beu, 0xb354afe4u, + 0x346d440cu, 0x34ba9463u, 0x34bfdf75u, 0x34afa1fcu, 0x34a0d875u, 0x34168fefu, 0x34919028u, 0x340e5a45u, + 0x345003b8u, 0x3860926du, 0xb8d8b444u, 0x3911af12u, 0xb52fba26u, 0xb2c2b7edu, 0x3220fef9u, 0x3292eb38u, + 0xb24d5b71u, 0x32b7b020u, 0xb36e609eu, 0xb237d79au, 0x30d7957fu, 0xb2bc811bu, 0xb30fc878u, 0xb405545bu, + 0x32193ae8u, 0xb383fb04u, 0xb42315c7u, 0xb38b6bd4u, 0xb3642958u, 0xb38e6e66u, 0xb16a4295u, 0xb272121eu, + 0xb4388889u, 0xb42aef7au, 0xb34765b6u, 0xb21d3eefu, 0xb4ef1047u, 0xb480e1f3u, 0xb3974cbcu, 0x343936bbu, + 0x33c95832u, 0x32b9f18bu, 0xb4cdd2c2u, 0xb43ea02du, 0x34ae3532u, 0xb44e287fu, 0x3411624fu, 0xb3b42cdfu, + 0x33497759u, 0x32bbcc3cu, 0xb46b6721u, 0xb380cd3eu, 0xb2ec7329u, 0x3381ebb1u, 0x320a899au, 0xb36f580bu, + 0xb4505c7cu, 0xb33cd3dbu, 0xb4ef48bdu, 0x33d37f81u, 0xb3304dcdu, 0xb4fafb5cu, 0x341558f9u, 0xb3fd746eu, + 0xb423fb05u, 0x34bdc7e7u, 0xb4b2a9f7u, 0xb4e5af44u, 0x3303f0aau, 0xb424b54bu, 0xb4e5fc28u, 0xb52fdc89u, + 0xb33027a4u, 0xb3d6b550u, 0x3842069au, 0x38dc4c4fu, 0xb9106adbu, 0xb89066afu, 0x325b28b5u, 0x328b4263u, + 0x320fe67du, 0x3394fe61u, 0xb11d25bcu, 0x33fe8460u, 0x33947cfcu, 0x33ff84e5u, 0x33f56996u, 0x339683d4u, + 0x3306ea6au, 0x348ea118u, 0x3487475du, 0xb1d6bc6eu, 0x346b95a8u, 0x34daf903u, 0x33d99dbeu, 0x35220ddcu, + 0x34989afdu, 0xb3e3bc59u, 0x34567004u, 0x3515ea21u, 0xb37ca1cau, 0x342a5d4du, 0x34fed825u, 0x3483e532u, + 0x3410aed0u, 0x34520ef8u, 0x3289ebe4u, 0x34a53d18u, 0x344fdbd4u, 0x347b62c6u, 0x32018c59u, 0x34e67239u, + 0x34e68623u, 0xb3ee0acau, 0x349bbf5du, 0x34a098aeu, 0x34924af7u, 0x34b513d6u, 0x33f34a97u, 0x33a87c7cu, + 0xb45dec69u, 0x34989db5u, 0x33fffdafu, 0xb373cc57u, 0xb437008du, 0x34c9ddaeu, 0x340302d5u, 0x3349a812u, + 0x33b44411u, 0x3485f010u, 0x32895c85u, 0x34afb990u, 0x34bccd6du, 0xb4096fe2u, 0xb36d3895u, 0xb278d88du, + 0x348340eeu, 0x34eeb098u, 0x3533fe35u, 0xb867336au, 0xb8df64b8u, 0x390f26ccu, 0x35146ca6u, 0xb2d5174au, + 0xb2c82293u, 0xb2189285u, 0xb2f01cd7u, 0xb3cd6413u, 0xb3a1af9eu, 0x305372a9u, 0xb39d241fu, 0x332969aeu, + 0xb17fbe5au, 0x328e8b7bu, 0xb3be97d9u, 0xb3a81471u, 0x339442bfu, 0x33aaf75du, 0x336c555bu, 0xb41e5ec4u, + 0xb3e84334u, 0xb38ac10bu, 0xb44fa03au, 0x3384aa04u, 0xb48755bau, 0xb39c3e53u, 0xb25f6b1bu, 0xb2b6469cu, + 0xb3cbbf9eu, 0xb3cbd6dcu, 0xb4063a8du, 0x320c47d9u, 0xb3a93de4u, 0xb296527du, 0xb4ab15cfu, 0xb4790417u, + 0xb103e0acu, 0x33aea989u, 0xb44bd03fu, 0xb485e545u, 0xb47dc1fdu, 0x341cad3cu, 0xb5364c6au, 0xb4c02a89u, + 0xb42c6a9fu, 0xb32bcf54u, 0xb30c4da4u, 0xb3d372fdu, 0xb4bd04a0u, 0xb4525dd5u, 0x3415376bu, 0xb4b9afbbu, + 0x353c4c52u, 0xb5067759u, 0xb38ad4f9u, 0xb4ce8f7au, 0xb4360577u, 0xb4a29561u, 0x350fd7e3u, 0x3424a3e8u, + 0xb3c3696cu, 0x34ecaa2bu, 0xb412fd2fu, 0x33b94c38u, 0xb83106dfu, 0x38e1d99au, 0xb90dfe45u, 0x388df54fu, + 0x31e898d1u, 0x32a1dc3cu, 0x325bea2au, 0x33990691u, 0x3231d5eau, 0x33b55c17u, 0x332c2905u, 0x32e0bed8u, + 0xb2015766u, 0xb37504abu, 0x338440bau, 0x338118fbu, 0x3354c14eu, 0x338048f2u, 0x3259e48bu, 0x341a197eu, + 0x34ab9be6u, 0x3480bf64u, 0xb3596df7u, 0x34319653u, 0xb3654213u, 0xb2ee72a4u, 0x346b0091u, 0x3405a5b3u, + 0x3466c12cu, 0x3422b708u, 0x34982462u, 0x34a0ecf3u, 0x33937348u, 0x344c2e7eu, 0x34e350ddu, 0x3496746du, + 0xb23f6c24u, 0x34da33a8u, 0x34b5c43du, 0x31cad309u, 0x3440896eu, 0x3502be2fu, 0x34f19ab8u, 0x3456f3a7u, + 0x33e2a4f1u, 0x3491a8d5u, 0x35013d5du, 0x34e1d521u, 0x33b6fe52u, 0x33e30ea4u, 0x351ce6d1u, 0x3508b58bu, + 0x34b50b1du, 0x34a36771u, 0x34f433d2u, 0x355ee117u, 0x350fc1e8u, 0x34bfbf94u, 0x35172659u, 0x33731774u, + 0x34380013u, 0x3483b153u, 0x33c8ba7du, 0x33a24788u, 0x34c56cdcu, 0x386bc4e9u, 0xb8e30b4du, 0x390cd672u, + 0xb50ef8d9u, 0xb2cde8c5u, 0xb2c7d9b3u, 0xb3121188u, 0x32f48ff4u, 0xb3559c34u, 0xb3cbec5eu, 0xb16262afu, + 0x32ee36a8u, 0x336f01ccu, 0xb3ac2832u, 0xb3bc8ffbu, 0xb3a64cc3u, 0x329f000fu, 0xb400b74fu, 0xb40aed00u, + 0xb405c15cu, 0xb4838478u, 0xb405f93eu, 0xb3d06e49u, 0xb448430du, 0x334f94ceu, 0xb45f7f56u, 0xb40b03a3u, + 0x3392ea22u, 0xb40cdac4u, 0xb4992559u, 0xb4d641dcu, 0xb35e8e82u, 0xb49ffe01u, 0xb4355acdu, 0xb4ae2449u, + 0xb41ffcd7u, 0xb50ec475u, 0x326302a8u, 0x33b51b54u, 0x3402c59eu, 0xb4470220u, 0xb4e607b0u, 0x34817d58u, + 0xb49f48ccu, 0xb421072du, 0xb4b3851bu, 0xb45b368du, 0xb4d6e9f7u, 0xb480c650u, 0xb52c4b29u, 0xb4304f1du, + 0xb49c0c1eu, 0xb45d9e3au, 0xb509d5fau, 0xb4c80f49u, 0x340b81cdu, 0xb4c938cdu, 0xb483e0eau, 0xb3acd66fu, + 0x338102e8u, 0xb484d4bdu, 0xb4525794u, 0xb48c1bc7u, 0x327cc0bdu, 0xb4a12b41u, 0x38239b48u, 0x38e396a4u, + 0xb90bcd7cu, 0xb88bcdebu, 0xb24979c2u, 0xb3104ad1u, 0xb2a67cdcu, 0xb3683661u, 0x32cad239u, 0xb3a68a9du, + 0xb2d15fa1u, 0xb4215562u, 0x33a1625bu, 0x32ccc192u, 0x335e9c44u, 0xb47f939au, 0x339ba7b9u, 0xb2acbbc5u, + 0xb3d88c5du, 0xb3cff7fau, 0x311b4c0du, 0xb508b657u, 0xb4e793d3u, 0xb4030a02u, 0x33741476u, 0xb484c0d5u, + 0x33c51a90u, 0xb50362dfu, 0xb4862e9fu, 0xb3ea4353u, 0xb48e26bbu, 0xb4cb5ca8u, 0xb486518bu, 0xb41081ecu, + 0xb4d075d9u, 0x34d0c3dau, 0x33ac7fc5u, 0x352811ecu, 0x3563336cu, 0x341e7369u, 0x34eb2c0bu, 0x35434bcdu, + 0x3478eb03u, 0xb4b72826u, 0x339a049eu, 0xb4509633u, 0x333ac91du, 0x34ad20d3u, 0xb48fe9bcu, 0xb4e7f463u, + 0xb438fb39u, 0xb464146cu, 0xb4a399b9u, 0x3543864du, 0x329ad3dcu, 0x351b61edu, 0xb49fd57cu, 0xb51c08f4u, + 0xb4bec4afu, 0xb4d27499u, 0xb4862b3cu, 0x34a68031u, 0xb525966eu, 0xb3907b30u, 0xb50ceb8eu, 0xb86ee44eu, + 0xb8e6917cu, 0x390ac753u, 0x34fa1c54u, 0x31bd1dc2u, 0x3370afc0u, 0x32edd85au, 0x33754778u, 0x33da1f5au, + 0x33c11b4fu, 0x33ea116fu, 0xb3c09779u, 0x33fcd4c8u, 0x3418cc8eu, 0x336b3764u, 0x340861cbu, 0x3417ee34u, + 0x3426c647u, 0x3445aa78u, 0x34c7fd53u, 0x33c7d42cu, 0x343ec74bu, 0x34ea1d49u, 0x34917f16u, 0x34d77912u, + 0x3473c2a6u, 0x34ace668u, 0xb2858dd9u, 0x35041752u, 0x348d6174u, 0xb30c8276u, 0x34247b90u, 0x352b2d59u, + 0x34291edfu, 0x34b25b26u, 0x348f1909u, 0x34c0b6d0u, 0x34be7d3du, 0x351333d7u, 0x339131d6u, 0x343efe5du, + 0x3505e7afu, 0x34e949a0u, 0x3421ac3au, 0x34bc8ff2u, 0x34fa9dc8u, 0x3551af63u, 0x34875392u, 0x31e30910u, + 0x341a6e3au, 0x344e133au, 0x34385308u, 0x3588f691u, 0x33eb1c00u, 0x34828761u, 0x3502f4acu, 0x3539f72fu, + 0x3546a290u, 0x34aa98e9u, 0x34c28c27u, 0x3560e0fbu, 0x35847266u, 0x3513f88eu, 0x34c90c24u, 0x35995d28u, + 0xb815011cu, 0x38ebc0aau, 0xb909d246u, 0x3889cbfcu, 0x324f91d4u, 0x322366ffu, 0x3316eeb9u, 0xb182f234u, + 0x33342db3u, 0x32d8f748u, 0xb29c6744u, 0xb2c598c4u, 0xb1d259a4u, 0xb2271b82u, 0xb2b7e903u, 0x34145028u, + 0x330b6a04u, 0x3226bfb6u, 0xb37a6fd0u, 0xb3966967u, 0xb338ec90u, 0x34439aa8u, 0xb3c917aau, 0x33045524u, + 0xb456d72au, 0xb40332c9u, 0x334a41b4u, 0xb36afb99u, 0xb41d5186u, 0xb0cb221au, 0xb28fc81cu, 0xb39295dfu, + 0xb2dda09cu, 0xb419a5d7u, 0xb2738b31u, 0x33b68fc1u, 0xb3f5fa8fu, 0xb353ff09u, 0x340bef2bu, 0xb39ca3f0u, + 0xb47799efu, 0x348d4d9du, 0x334ffb9bu, 0x341696dau, 0x340aab74u, 0xb363fc5cu, 0xb49f03fau, 0xb4c6ae1eu, + 0xb38124c8u, 0x33a24cf3u, 0xb417e0abu, 0x341e45d7u, 0x33de266du, 0xb34097ceu, 0x34ec74adu, 0x341a2ad6u, + 0x34d62c11u, 0xb4353711u, 0xb3e09dd5u, 0xb432843bu, 0xb426c0b6u, 0xb2ed99d5u, 0x3392ed47u, 0xb442809eu, + 0xb3255064u, 0x3872101cu, 0xb8ec0cedu, 0x3908e491u, 0xb4e60864u, 0x330f7897u, 0x3345ed2au, 0x32e2db5du, + 0x3380dea6u, 0x33a7d35fu, 0x336ab0ecu, 0x339529e3u, 0x332d53d1u, 0x33cb08ddu, 0x33cf432fu, 0x33c7bbdcu, + 0xb33a0081u, 0x32ec1545u, 0x330c9ab7u, 0x34348347u, 0x340477f2u, 0x349b4b2du, 0x334e3791u, 0x33f278afu, + 0x31bb562bu, 0xb34d325eu, 0xb23b78fau, 0x33f79db9u, 0x345399c8u, 0xb3f29ed7u, 0x343782f7u, 0x3481721du, + 0x34148b9cu, 0x340323fau, 0xb3c76715u, 0xb006301cu, 0x348ab176u, 0x33e7f63au, 0x328a2ce0u, 0x34b4f4acu, + 0x34161c6du, 0x3494a3a1u, 0x335e9ae6u, 0x3420cfe6u, 0x34b33bf3u, 0x34c83890u, 0x3481e8d7u, 0xb35781a4u, + 0x352e4f8du, 0xb36337b1u, 0x33ed0ae1u, 0xb47114c4u, 0x33a391cau, 0xb3087e16u, 0x349d4ef0u, 0xb4d939fau, + 0x3504e5f9u, 0x34fa442cu, 0x347bab22u, 0x341ca845u, 0x32b64b03u, 0xb4176a01u, 0x34af9fe2u, 0x33ea9a36u, + 0x353eb3d7u, 0x353bff03u, 0x380b3989u, 0x38ee485bu, 0xb9080f10u, 0xb88812ceu, 0x326924a8u, 0xb2797f51u, + 0xb1f7721bu, 0xb344907du, 0xb3c35f11u, 0xb3778f3du, 0xb3f1c98fu, 0xb3d7d1c4u, 0xb458431au, 0xb40045ceu, + 0xb3458eddu, 0xb4017b82u, 0xb40ccc76u, 0xb3402927u, 0xb38c6b36u, 0xb4f81f68u, 0xb4aeac43u, 0xb3d7122eu, + 0xb3ae6a88u, 0x34078f72u, 0x32b751a2u, 0xb31c6a1bu, 0xb49f968du, 0xb436c5cbu, 0xb39be175u, 0xb4dcc1a3u, + 0x33d1e2f2u, 0x326c2ff5u, 0x343d28a1u, 0x33ed8888u, 0xb3a6386eu, 0x34346dd1u, 0xb517c169u, 0xb527fb44u, + 0x32c845f1u, 0xb4c476d5u, 0xb4af53a5u, 0xb4b895f9u, 0xb468032fu, 0xb443f0a4u, 0xb4a25283u, 0xb4563eefu, + 0xb532854cu, 0xb4d019cau, 0xb51596e0u, 0xb4f05687u, 0xb542ede5u, 0xb4d1c12cu, 0xb485b2b0u, 0xb5431fd7u, + 0xb49261ebu, 0xb4cf87a9u, 0xb52afef0u, 0xb4f9002bu, 0xb3ea7263u, 0xb4523557u, 0xb54cddf2u, 0xb498059fu, + 0xb4894276u, 0xb42ed025u, 0xb511aa2fu, 0xb8778837u, 0xb8f0bddcu, 0x3907397du, 0x34bcc784u, 0xb2076e21u, + 0x33187f03u, 0xb209d221u, 0xb3049de6u, 0xb24e92c9u, 0xb267a911u, 0xb282b523u, 0x32c16f1eu, 0x3346a644u, + 0xb33faa0fu, 0x33991093u, 0xb33cf31du, 0xb3f30b54u, 0x32a8f67eu, 0x33a87cf6u, 0xb246e31du, 0xb374cb3eu, + 0xb2c14b6du, 0xb28e2a84u, 0x33da2eb5u, 0x340258d1u, 0x342ceaf6u, 0xb39093a2u, 0x3361eeb5u, 0xb30e736au, + 0x3313b5b9u, 0x342d2691u, 0x34e56764u, 0x34d44b5fu, 0x341fba23u, 0x34436b69u, 0x34989b2bu, 0x333c1af8u, + 0x32ff83bfu, 0xb4b09a38u, 0x33cdc1afu, 0xb3cffc2fu, 0xb4ce4c9du, 0x32ab6599u, 0xb2057658u, 0xb3547643u, + 0x343eb107u, 0x3474ba41u, 0xb4467795u, 0x34ac7c3fu, 0x33b9a25cu, 0x339ba4f0u, 0xb4504126u, 0x3428e95fu, + 0x348db2aau, 0x34993a51u, 0xb22c3039u, 0x34adf248u, 0x34a2cc1bu, 0x34ac0d41u, 0x3422348eu, 0x345ea372u, + 0xb3a5bf87u, 0x343b80b9u, 0xb418efd5u, 0x34e2e888u, 0xb7f85745u, 0x38f2435cu, 0xb9067706u, 0x38867a8au, + 0x33232bcau, 0x33800f8eu, 0x33c90802u, 0x339c0177u, 0x33e64019u, 0x33e401f6u, 0x343f6f3du, 0x339d5f5cu, + 0x3420d92cu, 0x3432bacdu, 0x345546ddu, 0x3458ab7eu, 0x34be3de7u, 0x34914e13u, 0x3459badbu, 0x34808608u, + 0x33c02efbu, 0x349db693u, 0x34bbfad9u, 0x33b7847eu, 0x35147bf1u, 0x348df6e5u, 0x34359f2cu, 0x3455e75eu, + 0x34942afcu, 0x3484b15eu, 0x34e4d7b4u, 0x34cc0b78u, 0x34c88907u, 0x348bc8aeu, 0x34f0ffa6u, 0x351543a6u, + 0x353d4aeeu, 0x3500549bu, 0x35031c75u, 0x354dbdc7u, 0x3536b5d2u, 0x34d609d4u, 0x34bfd96fu, 0x353af262u, + 0x354390bcu, 0x34e518b2u, 0x353871e8u, 0x358cb8b9u, 0x353429e2u, 0x3550e628u, 0x35661a8du, 0x349aff8au, + 0x355dbaf2u, 0x353cb50cu, 0x3531d08au, 0x356d3264u, 0x35349618u, 0x3520bb83u, 0x3568436au, 0x3522b626u, + 0x356181e0u, 0x354288f3u, 0x356f0ac0u, 0x35648feau, 0x3590bb61u, 0x387e51c7u, 0xb8f1a74bu, 0x3905bcffu, + 0xb4b4e29au, 0xb30370beu, 0x32f92c9fu, 0xb230632eu, 0xb3705c84u, 0x33b7c2d3u, 0xb3244630u, 0x335151f1u, + 0xb43479eeu, 0xb3b3b688u, 0xb15c229cu, 0xb35bb61eu, 0x32db8125u, 0xb44b88f5u, 0x343d3cbcu, 0xb4aa4c57u, + 0xb3f9f62au, 0xb41cddc7u, 0x346e9761u, 0xb45fdca9u, 0xb453d1e6u, 0xb42c2010u, 0x342717acu, 0x318419edu, + 0xb37b1e09u, 0xb418df64u, 0x3447a2d7u, 0xb4b2ef8bu, 0xb4696b4fu, 0xb51c805eu, 0xb531abb7u, 0xb443aaedu, + 0xb50fee1fu, 0xb50e692eu, 0xb3b8348du, 0xb3c43ba1u, 0xb51e072bu, 0xb391a093u, 0xb4745110u, 0xb516570eu, + 0xb3182aa3u, 0xb45251d7u, 0xb538f56fu, 0xb41cf81du, 0x32f5b2dbu, 0xb4b26ba4u, 0xb5633e3au, 0x338b06f3u, + 0xb30d1584u, 0xb4c2907eu, 0xb47d8a54u, 0x35905871u, 0xb50d6547u, 0xb49547d5u, 0xb531091au, 0xb4ff9ea0u, + 0xb54a4ac0u, 0xb3e96871u, 0xb517c367u, 0x3498c07du, 0xb569329bu, 0xb4093107u, 0x37cf19d6u, 0x38f4ad88u, + 0xb90514e9u, 0xb88519a9u, 0xb3b77df7u, 0xb377f7cbu, 0xb404effbu, 0xb400dffbu, 0xb4547edeu, 0xb4238319u, + 0xb4a5f40cu, 0xb41fc91bu, 0xb4153e04u, 0xb435038eu, 0xb3262863u, 0xb43ec3deu, 0xb482050cu, 0x3343ef9du, + 0x3490c3b9u, 0x33a6186du, 0x3500df6bu, 0xb35e014eu, 0x33c3ef59u, 0x340c6e93u, 0x34bc7949u, 0xb45d996du, + 0x34bc7949u, 0x348b7cd6u, 0xb3e722dau, 0x34b2bad6u, 0x34b2bad6u, 0x34bc7a10u, 0x3481bf11u, 0x33f1e840u, + 0xb4a9859fu, 0x3443eeadu, 0xb4261953u, 0xb4261953u, 0xb4261953u, 0xb4b50434u, 0xb5040955u, 0x2ca6fff6u, + 0xb4b50459u, 0x3443eeadu, 0x350b7dabu, 0xb4b50434u, 0xb4b50434u, 0x350b7dabu, 0x3443eeadu, 0xb51fc815u, + 0x3443eeadu, 0xb4ec83b4u, 0x34b503ccu, 0x3443eeadu, 0xb535051au, 0x33a9df59u, 0x349976d6u, 0xb48a8c2au, + 0xabd0e724u, 0xb550c3dau, 0xb4261953u, 0xb51fc815u, 0xb4ec83b4u, 0xb5454615u, 0xb51bbfdau, 0xb880561cu, + 0xb8f6eb74u, 0x39046e10u, 0x3493d79du, 0xb2c6024au, 0xb31f4e09u, 0x330e4ebfu, 0x338ecc3eu, 0xb36bd643u, + 0xb2781c6du, 0xb407e874u, 0x343c7cedu, 0xb341c3a5u, 0x33b46434u, 0xb3ec1134u, 0x3379d046u, 0x338f177bu, + 0x3459dd65u, 0xb1d80a4cu, 0xb41f9b0du, 0x3298ab51u, 0x32cb21dau, 0x33fdc3e1u, 0x3346e90au, 0x33479e25u, + 0xb486069eu, 0x3272dd2bu, 0xb438cce9u, 0x3358af88u, 0xb4ffa08bu, 0x33b915e9u, 0x336babbdu, 0xb4789402u, + 0x3479ae93u, 0x33c4328eu, 0xb520822cu, 0x33b61c97u, 0xb50331bbu, 0xb4259969u, 0x3421ab1au, 0x34db659fu, + 0xb5192e7eu, 0x34851528u, 0x3512e020u, 0x346f3309u, 0x342a8dc8u, 0x3526f04fu, 0xb43adbfau, 0xb4e16329u, + 0xb408a629u, 0xb57325fcu, 0xb4d1dd86u, 0x348df713u, 0xb514d6f8u, 0x34a05ef4u, 0xb4690515u, 0xb4f47840u, + 0x340de281u, 0xb5353c8eu, 0x35b639d4u, 0xb3d83149u, 0xb517663fu, 0xb32542beu, 0x35d07de8u, 0xb5cc27a6u, + 0xb7bf313au, 0x38f7119fu, 0xb903e08eu, 0x3883d1d9u, 0xb18ea80au, 0x32458effu, 0xb2e7a6b1u, 0xb2887393u, + 0xb30ef7d1u, 0xb2b87eb3u, 0x324c28b7u, 0xb392c60cu, 0xb3b25d3au, 0xb3609e1fu, 0xb346c823u, 0xb3b12679u, + 0xb2faa96fu, 0x33fd86c3u, 0x333fa63fu, 0xb311bb9eu, 0x32109536u, 0xb382c983u, 0xb2b629f7u, 0xb2a10358u, + 0x332d58aau, 0xb413aabcu, 0xb20887d0u, 0xb2e51a14u, 0x341037fcu, 0xb250174du, 0x341325ceu, 0xb38bee53u, + 0xb20bc121u, 0xb312fda5u, 0xb42b2952u, 0xb4605060u, 0x325df962u, 0xb5046b51u, 0xb5348c19u, 0x337becdau, + 0x3316c636u, 0xb4931b7bu, 0xb5066263u, 0x320305e3u, 0xb4656ff4u, 0x339d7eb6u, 0x33d636c7u, 0x34a2523au, + 0xb33ecc1au, 0x345edb9cu, 0xb40f90aau, 0xb312d839u, 0xb414c082u, 0xb380bdd9u, 0xb40854c4u, 0xb40c357du, + 0xb3d2cfa1u, 0xb415f542u, 0xb28c1474u, 0x33f55294u, 0xb409228cu, 0x3422d76fu, 0x34dd9524u, 0x34d62d84u, + 0xb39898d0u, 0x387ffa2fu, 0xb8f89b6fu, 0x3903517bu, 0xb4885858u, 0x32406b96u, 0xb1853ff5u, 0x32f94c32u, + 0x32a55957u, 0x32e5198au, 0x32fec9bau, 0xb0d58582u, 0x32623404u, 0x31e4e6ebu, 0x330b0a97u, 0xb28ef776u, + 0x339c6379u, 0x33a0eb25u, 0x32596c49u, 0x32e4cb6au, 0xb39b9a0au, 0xb309d4feu, 0x34087502u, 0xb2787bf9u, + 0x34893ed1u, 0x34857255u, 0x3415fd2fu, 0xb2e1f5b4u, 0x33e9237eu, 0xb445808fu, 0x33401914u, 0xb38058b7u, + 0x3344d8ddu, 0x34131194u, 0x34169cacu, 0xb33bb66au, 0x34b9f982u, 0xb21fc8c0u, 0x336eecadu, 0x351639a1u, + 0xb378873cu, 0xb202609cu, 0x34187b75u, 0xb414abecu, 0xb4466f83u, 0x34342e95u, 0x3290090eu, 0xb3df7f3bu, + 0x33be8430u, 0x33ab6e2eu, 0x339a14b3u, 0x34611a2bu, 0x33883b1bu, 0xb3d4ee78u, 0x3482975fu, 0x3334253au, + 0xb4c514adu, 0x330018fdu, 0x33774ea9u, 0x34a369a5u, 0xb4716b95u, 0x350e501du, 0xb362799fu, 0x341b01f7u, + 0xb325bb12u, 0x33361205u, 0x37a8fe46u, 0x38fa0fe3u, 0xb902d559u, 0xb882d08bu, 0xb2370e58u, 0x333bde33u, + 0x32f03fccu, 0x33be2115u, 0x33854b41u, 0x33f3ee34u, 0xb1cb9d06u, 0x316f0353u, 0x333499d5u, 0x341a4989u, + 0x32006314u, 0x345ba8eeu, 0x33e3abe7u, 0x34200421u, 0x345373e3u, 0x3448e5dau, 0x3317b201u, 0x341c1242u, + 0x341c81a6u, 0x33835c7eu, 0x3475f5d1u, 0xb305f7dau, 0xb2c79ea1u, 0xb289f4dau, 0x34188d2eu, 0x33917af1u, + 0x3484d8d6u, 0x34b99dffu, 0x341419cbu, 0x34cdaef4u, 0xb4838846u, 0xb46277b8u, 0xb4c42f43u, 0x340ae112u, + 0xb4489355u, 0xb48cd5a0u, 0x33ab9a65u, 0x3437ea86u, 0x34377b3eu, 0x34de3be6u, 0x34748a90u, 0x34bbad04u, + 0x35080e13u, 0x34c453e5u, 0x35084529u, 0x3460a987u, 0x335716cbu, 0xb20496aeu, 0xb372eb99u, 0x33706507u, + 0x34211a03u, 0x34a3068bu, 0x351956c7u, 0x34931058u, 0x34c8d4e1u, 0x34ec1fe6u, 0x350325b5u, 0x353415e2u, + 0x353ff416u, 0x35126378u, 0x3437ad2bu, 0xb87ee1b7u, 0xb8f87c8au, 0x39025d17u, 0x34463837u, 0xb2cd826au, + 0x326f5cb0u, 0xb3715564u, 0xb2af4d77u, 0xb31e6001u, 0x311ee5f3u, 0x331cb164u, 0xb1b33767u, 0xb3c9e0d2u, + 0xb3524fb0u, 0xb2c408d2u, 0xb3d20728u, 0xb3980e1au, 0xb37b5f37u, 0xb2dce5e1u, 0xb47aa957u, 0x3364b6f5u, + 0xb3c257e8u, 0x32e86761u, 0x336dab5bu, 0x330e292fu, 0xb3ea5174u, 0x340c2f55u, 0xb2e72d48u, 0x33987517u, + 0xb3adbb12u, 0x348c54cdu, 0x329d3a8bu, 0xb43783f5u, 0x347be195u, 0xb37970c5u, 0xb4c2ec0fu, 0x342b7b05u, + 0x33b4a5c1u, 0x349088eeu, 0xb4034fd0u, 0xb3926bbfu, 0x3417aafau, 0x340c326fu, 0xb43a002fu, 0x34b2128fu, + 0x31c91081u, 0xb4c9ec12u, 0xb4d3d54bu, 0xb45f9f3du, 0x34271639u, 0x3458cdfbu, 0x351b3cc5u, 0x338ed4b6u, + 0xb4b25eaeu, 0xb4345dfcu, 0xb4fef94bu, 0xb4539541u, 0xb3674755u, 0xb4a38145u, 0xb45a4956u, 0x343e31b6u, + 0x344c5411u, 0xb40076e9u, 0xb37a1e84u, 0xb4bbc589u, 0xb78eb078u, 0x38fa6dfau, 0xb901f7fau, 0x3881ed37u, + 0xb1dc86f8u, 0xb238806bu, 0xb2ba63afu, 0x324cb2b7u, 0x32a288c1u, 0x321e13e0u, 0xb34cb85eu, 0xb386f3c5u, + 0xb1dc4ce4u, 0x317d83a3u, 0x3320701fu, 0x32fdb5beu, 0x334cf09au, 0x326335f2u, 0xb396bb02u, 0xb23b46afu, + 0xb3b88d81u, 0x3396e9edu, 0xb32441b8u, 0xb2e7db0cu, 0xb46315cfu, 0xb38cb920u, 0x32284b1au, 0x3433abfdu, + 0xb2a8b96au, 0x3247ad20u, 0xb3f0727eu, 0x32c8f689u, 0xb2d0fb0cu, 0xb3c87235u, 0xb35329f3u, 0x340d87d8u, + 0x338eaa7bu, 0xb490388eu, 0x327ca39du, 0x33adf11bu, 0x31c51ef8u, 0x34271680u, 0xb40619f4u, 0xb44032fbu, + 0xb1083d3eu, 0x348f6252u, 0x349bf331u, 0xb419efe7u, 0x34552c16u, 0x33650911u, 0x31526bf5u, 0xb48602e4u, + 0xb3abf14cu, 0xb40437fcu, 0xb3a44b86u, 0xb4188a11u, 0x32af52c4u, 0xb3c214c2u, 0xb0a55050u, 0xb312a127u, + 0xb3ba8491u, 0xb464c8c2u, 0x345a761bu, 0x34806b14u, 0x33be2b40u, 0x3881c763u, 0xb8fcc619u, 0x39019108u, + 0xb43f17d1u, 0xb263a97cu, 0xb208c6fau, 0x32c5c358u, 0xb34beb73u, 0xb3738135u, 0xb33ad585u, 0xb29ed85cu, + 0xb3273b19u, 0x338b22a1u, 0xb34979a6u, 0xb434cf0bu, 0x33b87668u, 0xb3e9e93bu, 0xb3194f99u, 0x34342048u, + 0xb45bfaf5u, 0x339f9a8bu, 0x3409094fu, 0xb3a0aceau, 0xb41e4395u, 0xb3aab407u, 0x32a63890u, 0xb3331da0u, + 0xb46f011du, 0x337f32eeu, 0xb414dd85u, 0x33899149u, 0x3317cec6u, 0xb3ff8f9au, 0xb2e63b9du, 0xb4a12433u, + 0xb4601bb9u, 0x33b14a14u, 0xb489bc79u, 0xb30b26f1u, 0xb4d7a2d4u, 0xb3e4bbedu, 0x33e509d4u, 0x3384a6dcu, + 0xb4973e62u, 0xb41e0e53u, 0xb426652bu, 0xb4960e0bu, 0xb3f7ca62u, 0xb4d1009cu, 0xb45720b4u, 0xb4c5e4dfu, + 0xb4be98e5u, 0xb4885072u, 0xb508ce2bu, 0xb3649fcbu, 0x34c1a3d9u, 0xb51f78acu, 0xb5038f78u, 0xb40d0c1fu, + 0xb37062aau, 0x3486b395u, 0xb2e5654du, 0xb2bf17dau, 0x33bf5e9bu, 0xb465d5f8u, 0x375efb7bu, 0x38fc23edu, + 0xb90145a1u, 0xb881475cu, 0xb2dd13d0u, 0xb2db50c3u, 0x3209a31fu, 0x319fbe52u, 0xb3874971u, 0xb3126abdu, + 0xb3dda416u, 0xb41c6f2eu, 0xb227d023u, 0xb27d6c20u, 0xb36d3027u, 0x33ea597fu, 0xb3cdbf39u, 0x335db18eu, + 0xb307c967u, 0xb496d486u, 0xb43016adu, 0xb48e4a17u, 0xb3bf76eeu, 0xb510f187u, 0xb5037f3au, 0xb494a642u, + 0xb499b6d8u, 0xb4cea08du, 0xb4a70e9fu, 0xb5309f6cu, 0xb509891du, 0xb533c3deu, 0xb4e89f6au, 0xb4d3fa20u, + 0xb3faf384u, 0xb48b7307u, 0x3360bedau, 0xb408934du, 0xb46a898bu, 0xb30304afu, 0xb53322b9u, 0xb464ede5u, + 0xb4c84f9au, 0xb50ea9f4u, 0xb5031579u, 0x3493d71fu, 0xb5096793u, 0xb4d7b2ffu, 0xb52c08c3u, 0xb3d97502u, + 0xb35564feu, 0xb3694707u, 0xb15b538eu, 0xb3823c1au, 0xb4f2846du, 0xb5251d83u, 0xb58c04e1u, 0xb4c155aeu, + 0xb4bce35fu, 0x3499651fu, 0xb4a71185u, 0xb4862a93u, 0x31d41dffu, 0xb4948ecdu, 0x33bd2564u, 0xb8830fcfu, + 0xb8fefd75u, 0x3900f417u, 0x33fd4254u, 0xaf539ed8u, 0xb1de4187u, 0xb20141b5u, 0xb22728aau, 0x338792d7u, + 0x33d8ba52u, 0x31d4dd3au, 0x33914cbbu, 0x32db4b84u, 0x340a748eu, 0x3445891du, 0x340f6388u, 0x33ae6918u, + 0x341c6fc4u, 0x31dc4876u, 0x343d8ce7u, 0xb2ab9cb2u, 0xb2b61b9au, 0x3446429fu, 0x3419179au, 0x3485fa7eu, + 0xb0f20043u, 0x342527feu, 0x34806180u, 0x34af1edcu, 0x3362e73cu, 0x3496dd3eu, 0xb414d3dbu, 0xb35d6804u, + 0xb41d6317u, 0x34fbf312u, 0x33faa020u, 0xb40359c5u, 0x34eb3d5eu, 0x33305bfbu, 0x330b3a78u, 0x3488e447u, + 0xb44143b2u, 0x3445ddd5u, 0x348d9d1cu, 0xb2d81766u, 0x351c818du, 0x3545233cu, 0x339334d8u, 0x344f0180u, + 0x34ed2dcdu, 0x34a8ba4fu, 0x34252957u, 0x35075933u, 0x34883192u, 0x350ab386u, 0x33b37cc1u, 0x34472908u, + 0x32df95feu, 0x3350425eu, 0x353dec51u, 0xb4f86136u, 0x347ed5f0u, 0xb4217045u, 0xb429c618u, 0xb37a5434u, + 0xb72c4d73u, 0x38ff5bbdu, 0xb900b811u, 0x3880b385u, 0x325dd044u, 0x321545a6u, 0xb175e002u, 0xb2e28f99u, + 0x31f8e37eu, 0x337d4e74u, 0x328af04du, 0xb2f1f148u, 0xb2b559b4u, 0xb19e82fdu, 0xb34afb66u, 0x32397538u, + 0xb2a0f79cu, 0xb3ac1976u, 0x32c8c41au, 0x30a1144du, 0x339549f5u, 0x337ff2a3u, 0x31464802u, 0x31caf026u, + 0x34282f81u, 0x3401779fu, 0x343bb655u, 0x332eee81u, 0x3486c6fbu, 0x3477aeeeu, 0xb2abc9fcu, 0x3404d2ccu, + 0x319377b5u, 0x33f2738du, 0x339169eeu, 0x332fbb29u, 0x3302e83au, 0x3468986cu, 0x34829062u, 0x348dc310u, + 0x33c4cc5bu, 0x33d0ca5du, 0x3402dcc0u, 0x322cca59u, 0xb42b135au, 0xb34609d7u, 0xb21e0fb0u, 0x346ce1c3u, + 0x33bb9155u, 0xb33b8018u, 0x34066373u, 0x3358b8ddu, 0x33ed6719u, 0xb2dfeb29u, 0x3492f071u, 0x34241a15u, + 0xb2e5ad4du, 0x34096ba7u, 0x33c51b1bu, 0xb3e22aaau, 0x3494de0eu, 0x3302ad89u, 0xb212670fu, 0x34559103u, + 0x340ab240u, 0x3882a36cu, 0xb8feba2au, 0x39007b33u, 0xb3ed4e37u, 0x31efae97u, 0xb1840440u, 0xb14de0feu, + 0xb3307f92u, 0xb28ea443u, 0xb3324a2du, 0x32084bf6u, 0x32c57ad4u, 0xb27682dbu, 0x32417a3au, 0xb3ccd1afu, + 0xb2c79d80u, 0xb360a889u, 0x32a22540u, 0x30493d11u, 0xb2d4b2b6u, 0xb38a63b7u, 0x31d9d904u, 0xb32a3871u, + 0xb41f977bu, 0xb3056171u, 0xb3bac517u, 0xb38d7112u, 0xb4457e45u, 0x326ec9c5u, 0x339b1306u, 0xb3e86688u, + 0x343e2e64u, 0x3322700fu, 0xb41f80ccu, 0xb30d0a2du, 0xb32a201eu, 0xb49ba8cbu, 0x33cec611u, 0xb42fc5b3u, + 0x345e5a99u, 0x34654e01u, 0xb465a175u, 0xb42d81d9u, 0xb25431dau, 0x3295e3b1u, 0x3457a92cu, 0x32951bb6u, + 0x32e9a398u, 0xb47e8764u, 0x33ec25e2u, 0xb4ea9bfeu, 0xb19cdb92u, 0xb440fb8du, 0xb3b578d7u, 0x34c3f769u, + 0xb4ac0a64u, 0xb44e862eu, 0xb417882eu, 0x340596abu, 0x3424fa9fu, 0xb0fdabfeu, 0xb40c1894u, 0xb49d9f9du, + 0xb4688364u, 0xb4f9d1c9u, 0x36fc40e2u, 0x38ff3717u, 0xb9005544u, 0xb8805b20u, 0xb25e95edu, 0x3311a8e9u, + 0x32f47b79u, 0x331e692cu, 0xb0ea52dfu, 0x32f24f84u, 0xb33003adu, 0x32cfaf63u, 0x34174de4u, 0x3294702bu, + 0x3389ce3au, 0x33c9e1b2u, 0x3380b496u, 0x335aeed3u, 0x3427e253u, 0xb2ab3d93u, 0x34b13d47u, 0x338934d9u, + 0x342e9e37u, 0xb350ba4eu, 0x33a59a0du, 0xb3584a39u, 0x33b31bc8u, 0xb0885cb1u, 0x339f07ffu, 0x34b06a41u, + 0x3461de37u, 0x32bbcdbbu, 0x34997200u, 0x33d3b7efu, 0xb3647465u, 0xb4c008edu, 0xb4c4908au, 0xb47eaf48u, + 0xb3991814u, 0x3451f1afu, 0x33b38bfau, 0xb37ad965u, 0x32ab02b0u, 0xb48eb00eu, 0xb4da2c0au, 0xb50a38cdu, + 0x3438c81cu, 0x3389cf6fu, 0x3484a51au, 0xb35e51f5u, 0x34976ea3u, 0xb451477cu, 0x33be04e9u, 0x32ad4b5au, + 0x350db601u, 0x343acafdu, 0x351db1c9u, 0xb4744b47u, 0x34bf6313u, 0xb4a1c586u, 0x351ae4dau, 0x33989ae4u, + 0xb413634du, 0xb3d2c2f0u, 0xb474822du, 0xb881d698u, 0xb8ff7efau, 0x39002abeu, 0x33212c2au, 0xb15711ffu, + 0x32758638u, 0xb27879c8u, 0xb1a04812u, 0xb2fc3ce4u, 0xb1f5bc6du, 0xb0c41c95u, 0xb3ed671bu, 0xb2e75f2fu, + 0xb062dd3eu, 0xb3e23b03u, 0xb2a706feu, 0x3352c739u, 0xb3c6946du, 0xb3d70eeau, 0x338304fdu, 0x3351864fu, + 0x32e7fde8u, 0xb439a37au, 0x33f5d934u, 0xb4053986u, 0xb4783515u, 0xb3bff708u, 0xb4244b70u, 0xb3f5e6ccu, + 0xb4b0ad4cu, 0x319a2633u, 0xb3e77efeu, 0xb4a1256au, 0xb44bc74du, 0xb4aa12bcu, 0xb452848cu, 0x30c05457u, + 0xb40e0d6eu, 0xb42ab4c7u, 0xb287cfdbu, 0xb473f713u, 0x34fb754eu, 0xb439bd81u, 0x32ef6e9au, 0xb3155b12u, + 0x338af511u, 0x333ae676u, 0xb397240bu, 0xb42d2b48u, 0xb4c3457du, 0xb416358bu, 0x33ce208du, 0xb437533fu, + 0xb526a59du, 0xb417fa98u, 0xb178a38cu, 0xb4eab740u, 0xb28515a8u, 0xb4c13a8bu, 0xb50646b9u, 0xb5389b0du, + 0x34206455u, 0xb559b249u, 0xb5661756u, 0xb499ca63u, 0xb695e1ebu, 0x38fee0c6u, 0xb9001a42u, 0x38800c2bu, + 0x31d9e48fu, 0xb1179514u, 0xb28caac9u, 0xb225e545u, 0xb2115592u, 0xb32a9d3eu, 0xb3a54e9fu, 0xb2a586dcu, + 0x32558b08u, 0xb38fcfc1u, 0xb3e706c6u, 0x32975537u, 0xb41aa750u, 0xb317c5b2u, 0xb2966bfdu, 0xb3f8beecu, + 0xb39d4e9fu, 0xb1898bbeu, 0x341ad78fu, 0x31ad7912u, 0xb390beecu, 0xb13e2d42u, 0xb41a55eau, 0xb3ebbca9u, + 0xb3a39678u, 0x33c9c236u, 0xb450a871u, 0xb38b85a4u, 0x33565bf8u, 0xb3a7cfc1u, 0xb352770eu, 0xb3e0728cu, + 0xb4f3dca3u, 0xb429716cu, 0xb3fb3944u, 0xb383abd4u, 0x32cb08d7u, 0xb35098b7u, 0xb417cc5eu, 0x32f08fc6u, + 0xb470a871u, 0xb40bcc5eu, 0xb447f28eu, 0xb49c3794u, 0xb425e902u, 0xb4be7cebu, 0xb4ce5286u, 0xb47741b0u, + 0xb4bad7ddu, 0x347033a2u, 0xb45fa62eu, 0xb4be6f2au, 0x3313f275u, 0xb422dd33u, 0xb3ec74cfu, 0xb467ef29u, + 0xb42a73b0u, 0xb472cfc3u, 0xb4d678b6u, 0xb421e902u, 0xb4b6e6c0u, 0x3882c573u, 0xb8ffe6d9u, 0x39000518u, + 0xb338aa5au, 0x333cd6c6u, 0xb33e3488u, 0xb410e20bu, 0xb472c909u, 0xb39ccf64u, 0xb4d82758u, 0xb24ed590u, + 0xb3e3f104u, 0xb44a7300u, 0x344f81d7u, 0xb4cac4a9u, 0x328be180u, 0xb38dfc9cu, 0xb42c8977u, 0xb488ca50u, + 0xb4aeff3au, 0xb4e7cd24u, 0xb50d495cu, 0xb52043d1u, 0x34862a74u, 0x342749c0u, 0xb56603dau, 0xb5814568u, + 0xb3f88e20u, 0xb43ba038u, 0x353480fdu, 0xb4cfcbf0u, 0x35c0edb4u, 0x34d75992u, 0x34b78cfeu, 0xb5e0483bu, + 0x342463aau, 0xb5fcbf30u, 0xb318caa0u, 0xb60652bdu, 0xb484f528u, 0x35d6ba91u, 0xb4c496d0u, 0xb50b54c8u, + 0xb51b3d12u, 0xb52b235cu, 0xb54de2a6u, 0x358de35du, 0xb57a47e6u, 0x3562d0d0u, 0xb59672bdu, 0x353cbbe6u, + 0xb5afcd87u, 0x350a1652u, 0xb5c5fc27u, 0x34bb7226u, 0x3482643cu, 0x34456f50u, 0xb5fbcde6u, 0xb2f242c0u, + 0xb605da18u, 0xb41d7aa8u, 0xb4a0ec94u, 0xb4c0d928u, 0xb4f9a712u, 0xb5131228u, 0x365a8c42u, 0x38ff3196u, + 0xb9000000u, 0xb8804000u, 0x00000000u, 0x00000000u, 0xb4800000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x35800000u, 0x35800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x35800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x36800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x36800000u, 0x00000000u, 0x00000000u, 0x36800000u, 0x00000000u, 0x36800000u, 0xb8800000u, + 0xb9000000u, +}; + +inline constexpr std::uint32_t kImag[] = { + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0xb3cceb7au, 0x3f61fd32u, 0x3f747642u, 0x3f747640u, 0x3f74763eu, 0x3f74763eu, 0x3f747639u, + 0x3f74763cu, 0x3f747634u, 0x3f747635u, 0x3f747630u, 0x3f747637u, 0x3f747635u, 0x3f747630u, 0x3f747627u, + 0x3f747626u, 0x3f747628u, 0x3f747623u, 0x3f74762eu, 0x3f747628u, 0x3f747623u, 0x3f747626u, 0x3f747629u, + 0x3f74761bu, 0x3f747626u, 0x3f747629u, 0x3f747614u, 0x3f747617u, 0x3f747629u, 0x3f74761cu, 0x3f747617u, + 0x3f747602u, 0x3f747614u, 0x3f7475ffu, 0x3f747629u, 0x3f747605u, 0x3f7475ffu, 0x3f74761au, 0x3f7475f6u, + 0x3f7475efu, 0x3f74761au, 0x3f747605u, 0x3f747610u, 0x3f74760au, 0x3f747616u, 0x3f747600u, 0x3f74760cu, + 0x3f747606u, 0x3f747600u, 0x3f74760au, 0x3f7475f6u, 0x3f7475f0u, 0x3f7475fcu, 0x3f747626u, 0x3f7475f0u, + 0x3f74761au, 0x3f7475f6u, 0x3f7475e0u, 0x3f7475dcu, 0x3f7475e6u, 0x3f7475f2u, 0x3f7475dcu, 0x3f7475f6u, + 0x3f60aeebu, 0xbca30128u, 0xb29af392u, 0xbe928cb9u, 0xbe594cb1u, 0xbe594cafu, 0xbe594cb1u, 0xbe594cafu, + 0xbe594cb2u, 0xbe594cafu, 0xbe594cb1u, 0xbe594cb2u, 0xbe594cadu, 0xbe594cb3u, 0xbe594cb9u, 0xbe594cadu, + 0xbe594cafu, 0xbe594cb0u, 0xbe594cb9u, 0xbe594cafu, 0xbe594cb2u, 0xbe594cacu, 0xbe594cb1u, 0xbe594ca9u, + 0xbe594cb0u, 0xbe594ca3u, 0xbe594caeu, 0xbe594cb2u, 0xbe594ca9u, 0xbe594cb3u, 0xbe594cabu, 0xbe594caau, + 0xbe594cb4u, 0xbe594cabu, 0xbe594ca8u, 0xbe594cadu, 0xbe594ca5u, 0xbe594ca9u, 0xbe594c9fu, 0xbe594cafu, + 0xbe594cb7u, 0xbe594c9eu, 0xbe594ca6u, 0xbe594ca0u, 0xbe594ca7u, 0xbe594c9au, 0xbe594ca6u, 0xbe594ca0u, + 0xbe594ca3u, 0xbe594c9eu, 0xbe594cacu, 0xbe594cadu, 0xbe594ca8u, 0xbe594cbdu, 0xbe594ca2u, 0xbe594cb6u, + 0xbe594cb4u, 0xbe594ca3u, 0xbe594ca6u, 0xbe594cacu, 0xbe594cc2u, 0xbe594cbbu, 0xbe594cacu, 0xbe594cafu, + 0xbe594cb8u, 0xbe943f30u, 0x3c5944f0u, 0x30be92b4u, 0xbd929762u, 0xbd594ca6u, 0xbd594ca3u, 0xbd594ca9u, + 0xbd594ca2u, 0xbd594c9au, 0xbd594c90u, 0xbd594c90u, 0xbd594c88u, 0xbd594c99u, 0xbd594c93u, 0xbd594c6au, + 0xbd594c88u, 0xbd594c88u, 0xbd594c87u, 0xbd594c61u, 0xbd594c90u, 0xbd594c61u, 0xbd594ca7u, 0xbd594c72u, + 0xbd594c5fu, 0xbd594c6au, 0xbd594c67u, 0xbd594c65u, 0xbd594c71u, 0xbd594c81u, 0xbd594c93u, 0xbd594c5au, + 0xbd594c72u, 0xbd594cb0u, 0xbd594c5au, 0xbd594c60u, 0xbd594c28u, 0xbd594c4au, 0xbd594c84u, 0xbd594c4au, + 0xbd594c41u, 0xbd594c1cu, 0xbd594c5eu, 0xbd594c75u, 0xbd594c86u, 0xbd594c08u, 0xbd594c66u, 0xbd594c15u, + 0xbd594c33u, 0xbd594c4au, 0xbd594bf1u, 0xbd594c3fu, 0xbd594bf2u, 0xbd594c85u, 0xbd594bffu, 0xbd594bddu, + 0xbd594be6u, 0xbd594bccu, 0xbd594befu, 0xbd594cccu, 0xbd594c1eu, 0xbd594c96u, 0xbd594c3cu, 0xbd594bf0u, + 0xbd594c5eu, 0xbd594bbfu, 0xbd8ef1ccu, 0xbbd930d8u, 0xb11b71bau, 0x3b7905f5u, 0xbcadd6f4u, 0xbcadd6f2u, + 0xbcadd71du, 0xbcadd6fbu, 0xbcadd701u, 0xbcadd6e9u, 0xbcadd71au, 0xbcadd6e7u, 0xbcadd6d7u, 0xbcadd708u, + 0xbcadd742u, 0xbcadd708u, 0xbcadd6eeu, 0xbcadd72eu, 0xbcadd71au, 0xbcadd773u, 0xbcadd737u, 0xbcadd728u, + 0xbcadd769u, 0xbcadd73du, 0xbcadd746u, 0xbcadd773u, 0xbcadd6edu, 0xbcadd7d4u, 0xbcadd7a8u, 0xbcadd6b5u, + 0xbcadd753u, 0xbcadd71au, 0xbcadd794u, 0xbcadd717u, 0xbcadd6e2u, 0xbcadd6d7u, 0xbcadd768u, 0xbcadd6fau, + 0xbcadd668u, 0xbcadd73bu, 0xbcadd7bdu, 0xbcadd70eu, 0xbcadd751u, 0xbcadd78au, 0xbcadd779u, 0xbcadd7edu, + 0xbcadd6e5u, 0xbcadd65eu, 0xbcadd6fdu, 0xbcadd632u, 0xbcadd6b1u, 0xbcadd68fu, 0xbcadd6ffu, 0xbcadd5c1u, + 0xbcadd78eu, 0xbcadd676u, 0xbcadd6a0u, 0xbcadd5e0u, 0xbcadd6a6u, 0xbcadd72du, 0xbcadd637u, 0xbcadd6cdu, + 0xbcadd63du, 0xbcadd660u, 0xbcadd60du, 0x3bd35cfau, 0x3badb473u, 0xb1d6b746u, 0x3c330560u, 0xbc2dd6eeu, + 0xbc2dd6e5u, 0xbc2dd705u, 0xbc2dd6fdu, 0xbc2dd6e4u, 0xbc2dd6e4u, 0xbc2dd6ecu, 0xbc2dd6e2u, 0xbc2dd708u, + 0xbc2dd70fu, 0xbc2dd6ebu, 0xbc2dd6d8u, 0xbc2dd78au, 0xbc2dd649u, 0xbc2dd719u, 0xbc2dd6fbu, 0xbc2dd6adu, + 0xbc2dd707u, 0xbc2dd78bu, 0xbc2dd784u, 0xbc2dd6cdu, 0xbc2dd712u, 0xbc2dd760u, 0xbc2dd71du, 0xbc2dd5c1u, + 0xbc2dd735u, 0xbc2dd7bfu, 0xbc2dd721u, 0xbc2dd7a8u, 0xbc2dd6dau, 0xbc2dd6e3u, 0xbc2dd73du, 0xbc2dd6e4u, + 0xbc2dd705u, 0xbc2dd7dbu, 0xbc2dd71cu, 0xbc2dd74du, 0xbc2dd74fu, 0xbc2dd6f2u, 0xbc2dd660u, 0xbc2dd5d3u, + 0xbc2dd56au, 0xbc2dd77cu, 0xbc2dd601u, 0xbc2dd7e3u, 0xbc2dd6cfu, 0xbc2dda08u, 0xbc2dd669u, 0xbc2dd7c5u, + 0xbc2dd838u, 0xbc2dd853u, 0xbc2dd5c6u, 0xbc2dd837u, 0xbc2dd60au, 0xbc2dd664u, 0xbc2dd5d6u, 0xbc2dd69cu, + 0xbc2dd5c1u, 0xbc2dd741u, 0xbc2dd6d3u, 0xbc2dd635u, 0x3c2b966au, 0xbb823b5cu, 0xb13fcb7cu, 0xbbe56e1du, + 0xbbc6ac77u, 0xbbc6ac37u, 0xbbc6ac6bu, 0xbbc6aca1u, 0xbbc6acb8u, 0xbbc6aca9u, 0xbbc6aca8u, 0xbbc6ad58u, + 0xbbc6ac18u, 0xbbc6ad49u, 0xbbc6adaeu, 0xbbc6ad1eu, 0xbbc6acb7u, 0xbbc6ad07u, 0xbbc6aca8u, 0xbbc6ac2fu, + 0xbbc6abd9u, 0xbbc6ac3eu, 0xbbc6ac7eu, 0xbbc6ac6fu, 0xbbc6ad40u, 0xbbc6ad2du, 0xbbc6ad1cu, 0xbbc6acfbu, + 0xbbc6ade9u, 0xbbc6aca7u, 0xbbc6ad6bu, 0xbbc6ad1du, 0xbbc6add3u, 0xbbc6ad20u, 0xbbc6abdau, 0xbbc6ad74u, + 0xbbc6af8au, 0xbbc6ae77u, 0xbbc6ae66u, 0xbbc6ae40u, 0xbbc6af36u, 0xbbc6aed3u, 0xbbc6ad5du, 0xbbc6ad76u, + 0xbbc6aeedu, 0xbbc6addbu, 0xbbc6ad1bu, 0xbbc6b006u, 0xbbc6ae57u, 0xbbc6ad4eu, 0xbbc6af4fu, 0xbbc6aec7u, + 0xbbc6ac18u, 0xbbc6aec5u, 0xbbc6adc9u, 0xbbc6ae72u, 0xbbc6af77u, 0xbbc6b208u, 0xbbc6aba8u, 0xbbc6ad63u, + 0xbbc6ae36u, 0xbbc6ad2cu, 0xbbc6ac66u, 0xbbc6b033u, 0xbbc6aee1u, 0xbc0d0f5bu, 0x3b5f1949u, 0xb08b414eu, + 0xbc26e3ffu, 0xbb785749u, 0xbb785776u, 0xbb785679u, 0xbb785722u, 0xbb78567eu, 0xbb7856a9u, 0xbb785694u, + 0xbb785700u, 0xbb7858c1u, 0xbb78562bu, 0xbb7858c6u, 0xbb785737u, 0xbb78589du, 0xbb785760u, 0xbb785876u, + 0xbb7858e0u, 0xbb7855ddu, 0xbb78578du, 0xbb7857adu, 0xbb7858dfu, 0xbb78562au, 0xbb78585fu, 0xbb78549au, + 0xbb785960u, 0xbb785827u, 0xbb7855afu, 0xbb785b66u, 0xbb785698u, 0xbb785919u, 0xbb7858d7u, 0xbb785440u, + 0xbb785a03u, 0xbb785619u, 0xbb785853u, 0xbb785423u, 0xbb78595au, 0xbb785a14u, 0xbb7851ccu, 0xbb785c4au, + 0xbb785092u, 0xbb78532eu, 0xbb785be2u, 0xbb78543du, 0xbb78570au, 0xbb785bfau, 0xbb785ba8u, 0xbb785077u, + 0xbb785241u, 0xbb78543bu, 0xbb7851ffu, 0xbb78551cu, 0xbb78530du, 0xbb785a73u, 0xbb786096u, 0xbb785aa3u, + 0xbb785d8eu, 0xbb784913u, 0xbb784b81u, 0xbb784c53u, 0xbb7854f5u, 0xbb785819u, 0xbc210344u, 0xbb39c884u, + 0x31744982u, 0x39c255b9u, 0xbb258f3bu, 0xbb258fe0u, 0xbb259014u, 0xbb258fbau, 0xbb259028u, 0xbb259073u, + 0xbb25901du, 0xbb258d99u, 0xbb258ce3u, 0xbb2590fbu, 0xbb25917eu, 0xbb258fd6u, 0xbb258c11u, 0xbb258e35u, + 0xbb258cfbu, 0xbb258e93u, 0xbb258db0u, 0xbb2590b9u, 0xbb258efeu, 0xbb258d99u, 0xbb258e91u, 0xbb258dc6u, + 0xbb258defu, 0xbb258ae7u, 0xbb258c40u, 0xbb25924eu, 0xbb258b45u, 0xbb258ceeu, 0xbb258c11u, 0xbb258a0fu, + 0xbb25980eu, 0xbb2597c4u, 0xbb259c0eu, 0xbb2596beu, 0xbb2598b4u, 0xbb258cfau, 0xbb259466u, 0xbb259cfeu, + 0xbb25981bu, 0xbb25a178u, 0xbb259ba5u, 0xbb2593b5u, 0xbb258b95u, 0xbb259ce5u, 0xbb2591cdu, 0xbb259cbdu, + 0xbb259942u, 0xbb2599abu, 0xbb259687u, 0xbb259c3eu, 0xbb258d31u, 0xbb25954cu, 0xbb259259u, 0xbb259e6au, + 0xbb258d28u, 0xbb2599f0u, 0xbb25917fu, 0xbb258f56u, 0xbb259a4fu, 0xbb259dfeu, 0xbb259491u, 0x3ad5b749u, + 0x3b250869u, 0x3200d7f2u, 0x3b87a69cu, 0xbae7c750u, 0xbae7c654u, 0xbae7c897u, 0xbae7c682u, 0xbae7c5d6u, + 0xbae7c62cu, 0xbae7c35bu, 0xbae7c193u, 0xbae7c55au, 0xbae7c3c0u, 0xbae7c571u, 0xbae7c2c6u, 0xbae7c5a5u, + 0xbae7c357u, 0xbae7c2b8u, 0xbae7c2b4u, 0xbae7c631u, 0xbae7bb72u, 0xbae7c967u, 0xbae7c1d2u, 0xbae7c2e6u, + 0xbae7c719u, 0xbae7b952u, 0xbae7b9b2u, 0xbae7bbc5u, 0xbae7c093u, 0xbae7c210u, 0xbae7bdd3u, 0xbae7cc18u, + 0xbae7bc78u, 0xbae7c1e6u, 0xbae7b79du, 0xbae7b6e2u, 0xbae7b7bfu, 0xbae7be13u, 0xbae7b9bau, 0xbae7c123u, + 0xbae7bf1cu, 0xbae7b8a7u, 0xbae7ad20u, 0xbae7b9fdu, 0xbae7be9fu, 0xbae7c2f6u, 0xbae7cb70u, 0xbae7a649u, + 0xbae7c71au, 0xbae7b01cu, 0xbae7c0fdu, 0xbae7c13cu, 0xbae7bbc2u, 0xbae7b964u, 0xbae7bbc2u, 0xbae7acd5u, + 0xbae7c48fu, 0xbae7c02fu, 0xbae7bd03u, 0xbae7b957u, 0xbae7b3a1u, 0xbae79635u, 0xbae7d29eu, 0xbae7c9e9u, + 0x3b800799u, 0xbb104198u, 0xb0bdb6dau, 0xbac0aef7u, 0xbaa891feu, 0xbaa89120u, 0xbaa891c9u, 0xbaa89242u, + 0xbaa892c3u, 0xbaa89173u, 0xbaa8912du, 0xbaa891e2u, 0xbaa8930eu, 0xbaa89048u, 0xbaa88f17u, 0xbaa891b1u, + 0xbaa88eb3u, 0xbaa8902fu, 0xbaa890f4u, 0xbaa8922au, 0xbaa88e97u, 0xbaa89468u, 0xbaa88da7u, 0xbaa88ddfu, + 0xbaa88987u, 0xbaa88d57u, 0xbaa8915bu, 0xbaa88b52u, 0xbaa8908cu, 0xbaa88ca7u, 0xbaa88f3au, 0xbaa891dfu, + 0xbaa88eedu, 0xbaa88d78u, 0xbaa89164u, 0xbaa8929cu, 0xbaa89288u, 0xbaa89442u, 0xbaa89598u, 0xbaa8997eu, + 0xbaa893f3u, 0xbaa891cau, 0xbaa88c8au, 0xbaa89151u, 0xbaa88730u, 0xbaa89935u, 0xbaa88d16u, 0xbaa8899cu, + 0xbaa88d0cu, 0xbaa89bfau, 0xbaa88b40u, 0xbaa8914au, 0xbaa88694u, 0xbaa89570u, 0xbaa88dbau, 0xbaa895c6u, + 0xbaa89514u, 0xbaa89876u, 0xbaa88c33u, 0xbaa89512u, 0xbaa890a8u, 0xbaa88975u, 0xbaa8884eu, 0xbaa888d8u, + 0xbaa88e77u, 0xbb208956u, 0x3b030f8au, 0xb1b0a033u, 0xbb7c74d4u, 0xba7cda97u, 0xba7cdafau, 0xba7cdafcu, + 0xba7cdd65u, 0xba7cda5bu, 0xba7cda98u, 0xba7cdddau, 0xba7cdd3au, 0xba7cddf8u, 0xba7cdc42u, 0xba7ce38au, + 0xba7cd9f3u, 0xba7cddaau, 0xba7cdd46u, 0xba7cdd47u, 0xba7ce3fau, 0xba7cdb6fu, 0xba7cdd2eu, 0xba7ce084u, + 0xba7cd1f6u, 0xba7ce62bu, 0xba7cd99eu, 0xba7ce191u, 0xba7ccefcu, 0xba7cdddau, 0xba7cd9a1u, 0xba7cd4cfu, + 0xba7cd0afu, 0xba7ce245u, 0xba7ce530u, 0xba7cda6du, 0xba7ce164u, 0xba7cefc3u, 0xba7ce726u, 0xba7ce3b3u, + 0xba7cf699u, 0xba7cfba6u, 0xba7cda26u, 0xba7ce1bfu, 0xba7ce468u, 0xba7cec19u, 0xba7ce104u, 0xba7ce643u, + 0xba7ce668u, 0xba7cfe63u, 0xba7ce834u, 0xba7cf117u, 0xba7ced29u, 0xba7cf259u, 0xba7ce6ccu, 0xba7ce13eu, + 0xba7cd10cu, 0xba7cf35cu, 0xba7ccbf9u, 0xba7ce131u, 0xba7cc9a5u, 0xba7cf11fu, 0xba7d044bu, 0xba7ce7a5u, + 0xba7cc8a5u, 0xba7cf3c7u, 0xbb6e87a4u, 0xbaeba3a0u, 0x31696065u, 0x38db81d7u, 0xba427fd1u, 0xba428028u, + 0xba427fd8u, 0xba427cd0u, 0xba428089u, 0xba427c47u, 0xba4277fdu, 0xba427d43u, 0xba428306u, 0xba427bd0u, + 0xba427c9eu, 0xba427be3u, 0xba42836fu, 0xba427f25u, 0xba428394u, 0xba428454u, 0xba427b17u, 0xba4276f5u, + 0xba4276f4u, 0xba428517u, 0xba427fdcu, 0xba42881cu, 0xba428764u, 0xba42785cu, 0xba428a23u, 0xba428ec9u, + 0xba428099u, 0xba427c1bu, 0xba427ac8u, 0xba4279b8u, 0xba428902u, 0xba4269b5u, 0xba4274d8u, 0xba426c15u, + 0xba426d20u, 0xba428313u, 0xba42828eu, 0xba428327u, 0xba428b5bu, 0xba426d47u, 0xba42606du, 0xba4288b6u, + 0xba429fa6u, 0xba4272a4u, 0xba42735fu, 0xba42743au, 0xba428b71u, 0xba427543u, 0xba428558u, 0xba428eb1u, + 0xba429199u, 0xba4276d7u, 0xba427ccdu, 0xba427d60u, 0xba427a33u, 0xba428454u, 0xba42a04eu, 0xba42892au, + 0xba4284e0u, 0xba428e50u, 0xba427096u, 0x3a74b695u, 0x3ad93b57u, 0xb0978ea3u, 0x3b0b66e5u, 0xba18d243u, + 0xba18d245u, 0xba18d1a5u, 0xba18d22fu, 0xba18d059u, 0xba18d470u, 0xba18cd1fu, 0xba18cb81u, 0xba18cf63u, + 0xba18d16du, 0xba18d36bu, 0xba18da79u, 0xba18cc9au, 0xba18d48au, 0xba18d20au, 0xba18d0a5u, 0xba18c92cu, + 0xba18caf6u, 0xba18d12cu, 0xba18d44fu, 0xba18c929u, 0xba18d26cu, 0xba18cccfu, 0xba18ccefu, 0xba18eb78u, + 0xba18da47u, 0xba18d846u, 0xba18d0d8u, 0xba18d4ecu, 0xba18e9ccu, 0xba18dbe2u, 0xba18e189u, 0xba18bbafu, + 0xba18c3dfu, 0xba18cb1du, 0xba18c3adu, 0xba18cf55u, 0xba18c6d9u, 0xba18cb66u, 0xba18c5b7u, 0xba18c9f1u, + 0xba18bc11u, 0xba18da85u, 0xba18c55eu, 0xba18f740u, 0xba18c723u, 0xba18dfaau, 0xba18d9b3u, 0xba18d662u, + 0xba18df95u, 0xba18df8eu, 0xba18b600u, 0xba18dce6u, 0xba18caf0u, 0xba18dc29u, 0xba18b9afu, 0xba18db63u, + 0xba18b2a8u, 0xba18bd91u, 0xba18d27fu, 0xba18dc04u, 0x3aff94ebu, 0xbac6ebf4u, 0x3164b065u, 0xba0b5b62u, + 0xb9f48244u, 0xb9f48408u, 0xb9f481b1u, 0xb9f484c6u, 0xb9f47dc5u, 0xb9f48700u, 0xb9f4846eu, 0xb9f47b42u, + 0xb9f483c1u, 0xb9f487bau, 0xb9f48b45u, 0xb9f48a5bu, 0xb9f4829eu, 0xb9f48b27u, 0xb9f48845u, 0xb9f47458u, + 0xb9f4832eu, 0xb9f47a7fu, 0xb9f480a3u, 0xb9f47b86u, 0xb9f4871eu, 0xb9f48263u, 0xb9f47ceau, 0xb9f489c5u, + 0xb9f48a33u, 0xb9f4915au, 0xb9f48886u, 0xb9f4936du, 0xb9f495f5u, 0xb9f47cffu, 0xb9f49af7u, 0xb9f46958u, + 0xb9f46a97u, 0xb9f49e06u, 0xb9f48958u, 0xb9f47ad2u, 0xb9f4881du, 0xb9f49332u, 0xb9f487b3u, 0xb9f45ed9u, + 0xb9f4723du, 0xb9f4a48bu, 0xb9f4add3u, 0xb9f49dd0u, 0xb9f4c62bu, 0xb9f4879du, 0xb9f47cf2u, 0xb9f459e8u, + 0xb9f4adfcu, 0xb9f44dc2u, 0xb9f49182u, 0xb9f469ddu, 0xb9f48b04u, 0xb9f4811au, 0xb9f44cbbu, 0xb9f48969u, + 0xb9f4b419u, 0xb9f49b50u, 0xb9f461d4u, 0xb9f48304u, 0xb9f48b5au, 0xbaa17470u, 0x3ab96120u, 0x302ea30eu, + 0xbb03ce13u, 0xb9c6a912u, 0xb9c6ace7u, 0xb9c6b2d3u, 0xb9c6ad20u, 0xb9c6ad98u, 0xb9c6b06bu, 0xb9c6a916u, + 0xb9c6ac41u, 0xb9c6a6ddu, 0xb9c6a1cau, 0xb9c6a1ddu, 0xb9c6a3edu, 0xb9c69127u, 0xb9c69830u, 0xb9c6840cu, + 0xb9c6acc2u, 0xb9c6ace2u, 0xb9c6b272u, 0xb9c6aa05u, 0xb9c69d1cu, 0xb9c6936bu, 0xb9c69b5cu, 0xb9c6a4ebu, + 0xb9c6925eu, 0xb9c67df6u, 0xb9c68b87u, 0xb9c68646u, 0xb9c68d38u, 0xb9c6cdd6u, 0xb9c6884cu, 0xb9c6b9c4u, + 0xb9c6a4a9u, 0xb9c65faeu, 0xb9c690f9u, 0xb9c6ade9u, 0xb9c6b0b0u, 0xb9c6c15au, 0xb9c68056u, 0xb9c6504au, + 0xb9c64ae5u, 0xb9c65420u, 0xb9c69080u, 0xb9c6c2e2u, 0xb9c65b7fu, 0xb9c69b37u, 0xb9c69628u, 0xb9c6e284u, + 0xb9c6cc15u, 0xb9c611d0u, 0xb9c68bdfu, 0xb9c63cb7u, 0xb9c6e915u, 0xb9c6fb26u, 0xb9c69816u, 0xb9c61ee5u, + 0xb9c6dee5u, 0xb9c6643fu, 0xb9c6a900u, 0xb9c6782du, 0xb9c726deu, 0xb9c6b0f6u, 0xbaf122b7u, 0xbaabab68u, + 0x318afc15u, 0x38362ee3u, 0xb9a39a33u, 0xb9a3a160u, 0xb9a3a2adu, 0xb9a39c1fu, 0xb9a397cau, 0xb9a3a693u, + 0xb9a3a3fbu, 0xb9a3a573u, 0xb9a3ac63u, 0xb9a39f0au, 0xb9a3ab92u, 0xb9a38b5au, 0xb9a3a042u, 0xb9a3b6e3u, + 0xb9a392cbu, 0xb9a37f0bu, 0xb9a392cbu, 0xb9a3905cu, 0xb9a395abu, 0xb9a3553bu, 0xb9a368fcu, 0xb9a3827cu, + 0xb9a388fcu, 0xb9a3bf0au, 0xb9a3553bu, 0xb9a38db9u, 0xb9a38db9u, 0xb9a3a17au, 0xb9a39028u, 0xb9a3817au, + 0xb9a3b788u, 0xb9a42b1bu, 0xb9a3fdd9u, 0xb9a43dd9u, 0xb9a3fdd9u, 0xb9a3c2b9u, 0xb9a3d098u, 0xb9a40bb9u, + 0xb9a41e78u, 0xb9a42b1bu, 0xb9a3fdd9u, 0xb9a3c2b9u, 0xb9a3c2b9u, 0xb9a3e357u, 0xb9a41098u, 0xb9a3bbd4u, + 0xb9a3d098u, 0xb9a3adf5u, 0xb9a36836u, 0xb9a41098u, 0xb9a39578u, 0xb9a37bd4u, 0xb9a3cad9u, 0xb9a42916u, + 0xb9a37af5u, 0xb9a41b36u, 0xb9a3bdd9u, 0xb9a3fbd4u, 0xb9a3edf5u, 0xb9a42916u, 0xb9a3c0b4u, 0x3a2cbecau, + 0x3aa17721u, 0xb1b388abu, 0x3aa975a1u, 0xb9884eb6u, 0xb9885244u, 0xb9884d71u, 0xb9884afeu, 0xb9885828u, + 0xb9884b3fu, 0xb98858c9u, 0xb98845ddu, 0xb9884bc9u, 0xb98844dau, 0xb9882faau, 0xb988395cu, 0xb988622eu, + 0xb988586du, 0xb9883162u, 0xb9884f5du, 0xb9887c63u, 0xb9885e07u, 0xb9885beau, 0xb988390du, 0xb9880892u, + 0xb98851fcu, 0xb9884957u, 0xb988643bu, 0xb9883849u, 0xb9882712u, 0xb98842b4u, 0xb988812cu, 0xb98841f8u, + 0xb9886f7eu, 0xb9887cf3u, 0xb988469bu, 0xb9885baau, 0xb9888db1u, 0xb9880d37u, 0xb988621au, 0xb98862ddu, + 0xb988e0a2u, 0xb988246au, 0xb988983fu, 0xb988056fu, 0xb98841adu, 0xb9886540u, 0xb98831a1u, 0xb9885a77u, + 0xb988422du, 0xb987cfabu, 0xb987fcaeu, 0xb9883ba3u, 0xb9889796u, 0xb987f95fu, 0xb988dbfcu, 0xb988cdb6u, + 0xb9884cc6u, 0xb9884f2bu, 0xb987fd3du, 0xb987f61au, 0xb987fae8u, 0xb9887a2cu, 0xb989129fu, 0xb987bdf9u, + 0x3a9548c2u, 0xba9704ceu, 0xb12d635eu, 0xb982b53du, 0xb9659c32u, 0xb965a03cu, 0xb965a2a8u, 0xb965ac1fu, + 0xb965a0aeu, 0xb9659f55u, 0xb96592b5u, 0xb965a5a3u, 0xb965b1d6u, 0xb965a24du, 0xb965b8e4u, 0xb96593b8u, + 0xb965a391u, 0xb965ab87u, 0xb965815au, 0xb965b8efu, 0xb965934fu, 0xb96592eau, 0xb965ba9bu, 0xb9658534u, + 0xb965a9afu, 0xb965c147u, 0xb965b7dau, 0xb965a1d8u, 0xb965b1f9u, 0xb965925cu, 0xb965a181u, 0xb9659f91u, + 0xb965c8b0u, 0xb965cf6au, 0xb965b129u, 0xb965a97fu, 0xb965a1f8u, 0xb965b524u, 0xb965ffd1u, 0xb965e3cfu, + 0xb965986bu, 0xb965ffe8u, 0xb965dbd8u, 0xb965b5bfu, 0xb965acc0u, 0xb965e90du, 0xb965b488u, 0xb965a94au, + 0xb965e36cu, 0xb965c7fbu, 0xb9659d57u, 0xb965ecd4u, 0xb965e90bu, 0xb96635bau, 0xb96572a4u, 0xb965b134u, + 0xb965a9c3u, 0xb965baf9u, 0xb965a9f8u, 0xb965eca6u, 0xb965b206u, 0xb9660f81u, 0xb9661ec4u, 0xb9660c70u, + 0xb965b707u, 0xba4f6d19u, 0x3a8eef22u, 0xb0835c57u, 0xbaa20b2cu, 0xb943222bu, 0xb9432a8eu, 0xb943274du, + 0xb9432968u, 0xb94333dau, 0xb94317dfu, 0xb9432ec6u, 0xb9432a64u, 0xb9431c6du, 0xb9432c72u, 0xb9433be9u, + 0xb9433268u, 0xb94313cdu, 0xb9433e35u, 0xb94355eeu, 0xb94327c7u, 0xb9431959u, 0xb9436c3au, 0xb943507bu, + 0xb9432cbdu, 0xb943103du, 0xb943505bu, 0xb94355efu, 0xb9434f3au, 0xb943164au, 0xb9432ef7u, 0xb94339d1u, + 0xb943397eu, 0xb9433fbbu, 0xb9437315u, 0xb9436187u, 0xb942ed18u, 0xb9430fdcu, 0xb94321a6u, 0xb9432c08u, + 0xb94347edu, 0xb9439017u, 0xb9433db8u, 0xb9436a4du, 0xb943abb2u, 0xb9435609u, 0xb94383e3u, 0xb9439974u, + 0xb942f896u, 0xb943773au, 0xb9432ce8u, 0xb942d4cbu, 0xb943a5c6u, 0xb9436b0fu, 0xb9431eafu, 0xb94385a6u, + 0xb9430f90u, 0xb9432c50u, 0xb9433349u, 0xb94344c8u, 0xb9439b5fu, 0xb943d056u, 0xb94376d1u, 0xb94322b8u, + 0xb9431ed2u, 0xb943769bu, 0xba8e3135u, 0xba86b70eu, 0x32354e92u, 0x37b8f7f2u, 0xb9274045u, 0xb92735dcu, + 0xb9272d28u, 0xb9273698u, 0xb92725c2u, 0xb92732d3u, 0xb9272030u, 0xb9272827u, 0xb9273d26u, 0xb9275270u, + 0xb92734ffu, 0xb927299fu, 0xb9272999u, 0xb9272b21u, 0xb9270e97u, 0xb9272fcdu, 0xb927269du, 0xb92746a2u, + 0xb9272aa2u, 0xb9270c91u, 0xb926f36eu, 0xb927680bu, 0xb927104eu, 0xb9270884u, 0xb9272151u, 0xb926e8dbu, + 0xb9271960u, 0xb9271f7eu, 0xb9271182u, 0xb92732f3u, 0xb9266ef8u, 0xb9274f96u, 0xb926c4aeu, 0xb926da17u, + 0xb926b7b4u, 0xb9274b9fu, 0xb927413eu, 0xb9266c1bu, 0xb9268bafu, 0xb9268b21u, 0xb9265e88u, 0xb9261b3au, + 0xb9266188u, 0xb926ca20u, 0xb9263b7bu, 0xb926b98bu, 0xb926c1ecu, 0xb926e93eu, 0xb926faeau, 0xb927358du, + 0xb9275018u, 0xb926faecu, 0xb9274406u, 0xb926d93eu, 0xb92736c8u, 0xb9273275u, 0xb9268debu, 0xb926a034u, + 0xb9270453u, 0xb9269746u, 0xb9262c69u, 0x3a0603e4u, 0x3a801f2eu, 0xb091774au, 0x3a6442f3u, 0xb9107063u, + 0xb91073e6u, 0xb9107aa1u, 0xb9106b1fu, 0xb9105ea2u, 0xb9106ff7u, 0xb9107fa8u, 0xb9106c75u, 0xb9107872u, + 0xb910702fu, 0xb9106b8bu, 0xb9107840u, 0xb9107399u, 0xb9108abdu, 0xb91080b3u, 0xb9108712u, 0xb9104abau, + 0xb9108c56u, 0xb9105454u, 0xb910655eu, 0xb9107c3fu, 0xb9107650u, 0xb91077e0u, 0xb91079b5u, 0xb9103137u, + 0xb91085edu, 0xb91074a9u, 0xb9109429u, 0xb9107e9du, 0xb910596du, 0xb9108327u, 0xb91055d5u, 0xb9105208u, + 0xb9105cf5u, 0xb910949cu, 0xb910512eu, 0xb910b586u, 0xb910cde1u, 0xb91056b6u, 0xb9107f51u, 0xb91096c1u, + 0xb9108a1fu, 0xb9107537u, 0xb9109343u, 0xb910494bu, 0xb9102bf6u, 0xb9103651u, 0xb910ffffu, 0xb9105ca2u, + 0xb910939du, 0xb910d246u, 0xb910be9fu, 0xb91067b4u, 0xb910d51fu, 0xb9105a6cu, 0xb9110dacu, 0xb9109728u, + 0xb9109ed4u, 0xb91050c4u, 0xb910777eu, 0xb910963fu, 0x3a3e7b9fu, 0xba72c74cu, 0xb0908f0bu, 0xb90eebdcu, + 0xb8fb327bu, 0xb8fb313du, 0xb8fb37b4u, 0xb8fb2f9eu, 0xb8fb3e55u, 0xb8fb438au, 0xb8fb357du, 0xb8fb456cu, + 0xb8fb49bau, 0xb8fb4f02u, 0xb8fb4a16u, 0xb8fb6476u, 0xb8fb2eb1u, 0xb8fb3f42u, 0xb8fb24a3u, 0xb8fb4d59u, + 0xb8fb6377u, 0xb8fb5718u, 0xb8faf8beu, 0xb8fb777du, 0xb8fb8593u, 0xb8fb9994u, 0xb8fb8d82u, 0xb8fb7888u, + 0xb8fb7ef1u, 0xb8fb8136u, 0xb8fb33ebu, 0xb8fb9f7bu, 0xb8fb5b02u, 0xb8fb6cacu, 0xb8fbb883u, 0xb8fba0deu, + 0xb8fab8d9u, 0xb8fb6be5u, 0xb8fba9d2u, 0xb8fba494u, 0xb8fbe316u, 0xb8fb7900u, 0xb8fbf558u, 0xb8fb7cd1u, + 0xb8fc6d90u, 0xb8fba69cu, 0xb8fc8fc3u, 0xb8fb56aeu, 0xb8fc849cu, 0xb8fba81au, 0xb8fbf6bau, 0xb8fb6b71u, + 0xb8fbf37bu, 0xb8fba24cu, 0xb8fc1c57u, 0xb8fb0909u, 0xb8fb7a51u, 0xb8fb6b1bu, 0xb8fbedb1u, 0xb8fac20au, + 0xb8faff2du, 0xb8fb2a88u, 0xb8fb6e9eu, 0xb8fae74cu, 0xb8fb087cu, 0xba173368u, 0x3a67bd17u, 0x312899e7u, + 0xba5bfd51u, 0xb8dbbee8u, 0xb8dbad34u, 0xb8dbb976u, 0xb8db94cdu, 0xb8dbb202u, 0xb8db9976u, 0xb8db8ff4u, + 0xb8dbb03fu, 0xb8db808fu, 0xb8dbb18eu, 0xb8dba604u, 0xb8db5c5eu, 0xb8dbcf0eu, 0xb8db224du, 0xb8db4ea3u, + 0xb8dbaea9u, 0xb8db91a7u, 0xb8db7191u, 0xb8db70a3u, 0xb8db5ae0u, 0xb8db1bbfu, 0xb8dc29d3u, 0xb8db1ef4u, + 0xb8db8413u, 0xb8dbb283u, 0xb8dc14e7u, 0xb8db10c0u, 0xb8db0bf3u, 0xb8db0edbu, 0xb8db21bfu, 0xb8db3c83u, + 0xb8db350bu, 0xb8dac862u, 0xb8db5380u, 0xb8db8c0eu, 0xb8dbaacbu, 0xb8db7c28u, 0xb8dc65fdu, 0xb8db7319u, + 0xb8da7d08u, 0xb8db0223u, 0xb8dae6c0u, 0xb8db61b0u, 0xb8dae351u, 0xb8dacbf3u, 0xb8db3c64u, 0xb8dae2a2u, + 0xb8da5309u, 0xb8db8df3u, 0xb8db5668u, 0xb8db0620u, 0xb8db0139u, 0xb8daaddeu, 0xb8db0a5eu, 0xb8dbda94u, + 0xb8dbed65u, 0xb8db91acu, 0xb8db6465u, 0xb8da7282u, 0xb8da24dbu, 0xb8dafaafu, 0xba368828u, 0xba5c9c6au, + 0xb1ff4ceeu, 0x3754e42fu, 0xb8c1655du, 0xb8c1799eu, 0xb8c19a1au, 0xb8c18f38u, 0xb8c1a24fu, 0xb8c1b5cfu, + 0xb8c18443u, 0xb8c1617du, 0xb8c16e52u, 0xb8c16e2eu, 0xb8c16a56u, 0xb8c16c8bu, 0xb8c1c960u, 0xb8c17a44u, + 0xb8c1743fu, 0xb8c1abedu, 0xb8c1c9e3u, 0xb8c151bcu, 0xb8c1a74fu, 0xb8c1c56fu, 0xb8c15f19u, 0xb8c175c4u, + 0xb8c178f5u, 0xb8c10613u, 0xb8c1ee6fu, 0xb8c1cc7bu, 0xb8c23822u, 0xb8c21472u, 0xb8c1b4e5u, 0xb8c2233eu, + 0xb8c1cb8bu, 0xb8c22decu, 0xb8c1fb37u, 0xb8c24192u, 0xb8c32793u, 0xb8c2f542u, 0xb8c318e4u, 0xb8c27335u, + 0xb8c22ae0u, 0xb8c28956u, 0xb8c2c770u, 0xb8c1f43cu, 0xb8c38e36u, 0xb8c2b515u, 0xb8c35067u, 0xb8c20284u, + 0xb8c3a823u, 0xb8c25a3au, 0xb8c30bc0u, 0xb8c2440cu, 0xb8c26664u, 0xb8c3242cu, 0xb8c30580u, 0xb8c2683fu, + 0xb8c2ad55u, 0xb8c25222u, 0xb8c39f6au, 0xb8c1e076u, 0xb8c2082eu, 0xb8c371e7u, 0xb8c225c4u, 0x39d9aba2u, + 0x3a530448u, 0x2ff22ec9u, 0x3a24bb71u, 0xb8ab0e01u, 0xb8ab13abu, 0xb8ab0b4bu, 0xb8ab2ddau, 0xb8ab4062u, + 0xb8ab271au, 0xb8aaff42u, 0xb8ab13f6u, 0xb8ab1cbbu, 0xb8ab3a17u, 0xb8ab1f70u, 0xb8aaebeeu, 0xb8aaf825u, + 0xb8aa92adu, 0xb8aae35cu, 0xb8aafa94u, 0xb8ab4c4fu, 0xb8ab4432u, 0xb8ab269cu, 0xb8aad614u, 0xb8aad9c8u, + 0xb8ab51fdu, 0xb8ab1821u, 0xb8aaf62fu, 0xb8aaf9a8u, 0xb8aaa1f2u, 0xb8aa82fdu, 0xb8aa8c80u, 0xb8ab699bu, + 0xb8ab7b8cu, 0xb8aa0e9bu, 0xb8aac7d4u, 0xb8aa8bc6u, 0xb8aaf5e9u, 0xb8aab30cu, 0xb8ab12b9u, 0xb8ab0680u, + 0xb8abe14au, 0xb8aac146u, 0xb8aa3932u, 0xb8aa80b0u, 0xb8ab5b1eu, 0xb8ab4d7au, 0xb8ab599du, 0xb8abc701u, + 0xb8aac496u, 0xb8ab4e8bu, 0xb8a99695u, 0xb8ab5687u, 0xb8ab588cu, 0xb8aa5aa9u, 0xb8abd698u, 0xb8aab22bu, + 0xb8aab897u, 0xb8aaf6d9u, 0xb8ab563bu, 0xb8ac9bffu, 0xb8aa9fb4u, 0xb8ab8e5eu, 0xb8a9fa17u, 0xb8ab8823u, + 0x3a009d1bu, 0xba49ef17u, 0x31afd8d6u, 0xb8acf72fu, 0xb8980b74u, 0xb8980306u, 0xb89813a1u, 0xb897f6afu, + 0xb89818e1u, 0xb897ef83u, 0xb8981159u, 0xb898316fu, 0xb897eaf8u, 0xb8980355u, 0xb897fc50u, 0xb89802a0u, + 0xb897fe7du, 0xb897e0ebu, 0xb897c223u, 0xb897ded3u, 0xb897e426u, 0xb8981b7fu, 0xb897fce2u, 0xb8982c8fu, + 0xb897b709u, 0xb897f2abu, 0xb897bb5fu, 0xb897ca51u, 0xb897c64du, 0xb8987e55u, 0xb8980687u, 0xb89860e2u, + 0xb89840eau, 0xb8983a27u, 0xb898261du, 0xb899116fu, 0xb8984a4cu, 0xb8988f5au, 0xb89963f5u, 0xb8987be8u, + 0xb89912cbu, 0xb8994033u, 0xb899324du, 0xb898a18au, 0xb898b779u, 0xb89898a9u, 0xb8986dc6u, 0xb898d928u, + 0xb89810fau, 0xb898cd1cu, 0xb8981891u, 0xb899077bu, 0xb897e425u, 0xb8981672u, 0xb8980c0cu, 0xb897f93fu, + 0xb897d1dfu, 0xb8984e41u, 0xb898497du, 0xb897d1e8u, 0xb8986634u, 0xb897c559u, 0xb8980963u, 0xb897a723u, + 0xb897a80du, 0xb9eccb5au, 0x3a4214afu, 0xb11d0792u, 0xba1fac0bu, 0xb887bbc2u, 0xb887c1cbu, 0xb887b7f4u, + 0xb887d692u, 0xb887ac06u, 0xb887d0d0u, 0xb8879132u, 0xb887ac25u, 0xb887ad4du, 0xb887cc8cu, 0xb8878d91u, + 0xb887d081u, 0xb88804efu, 0xb887a07fu, 0xb887e307u, 0xb8879ce3u, 0xb887b899u, 0xb8878c4fu, 0xb887a248u, + 0xb887a229u, 0xb88794e6u, 0xb8876e78u, 0xb887aa60u, 0xb8876834u, 0xb886fef5u, 0xb8876b0cu, 0xb8882373u, + 0xb8877649u, 0xb88745b7u, 0xb8872629u, 0xb88795eeu, 0xb887a6d2u, 0xb88787f0u, 0xb8875e03u, 0xb887005du, + 0xb887e66bu, 0xb88838ebu, 0xb8879082u, 0xb887d008u, 0xb887de53u, 0xb887fbf3u, 0xb887d223u, 0xb8878aafu, + 0xb8879fecu, 0xb887f4aau, 0xb887adf3u, 0xb8874576u, 0xb888c113u, 0xb88731aeu, 0xb887da7du, 0xb887cd31u, + 0xb887af54u, 0xb887de64u, 0xb887b2c2u, 0xb8878f06u, 0xb887d1dcu, 0xb8879b0bu, 0xb887a884u, 0xb887005fu, + 0xb887d578u, 0xb887910cu, 0xb9f7d2a7u, 0xba3a2604u, 0xb0cb59f9u, 0x370601d7u, 0xb8735c7bu, 0xb87342aau, + 0xb87371ccu, 0xb8736b85u, 0xb8738d7au, 0xb873401bu, 0xb8734435u, 0xb87383f2u, 0xb872f929u, 0xb8739ff0u, + 0xb87324f9u, 0xb8738522u, 0xb872fa1du, 0xb87382ccu, 0xb8732613u, 0xb872c86bu, 0xb873c1d1u, 0xb873a8b2u, + 0xb873997eu, 0xb873227eu, 0xb8730725u, 0xb872eb51u, 0xb872bb84u, 0xb87324b2u, 0xb87277d9u, 0xb8737d5au, + 0xb8733123u, 0xb871cf57u, 0xb8729883u, 0xb87246eeu, 0xb8726828u, 0xb87110f9u, 0xb870fa5fu, 0xb8722c46u, + 0xb872080du, 0xb870cf45u, 0xb87202cdu, 0xb87300ebu, 0xb871fa7cu, 0xb8717da0u, 0xb8722f5cu, 0xb8736f84u, + 0xb8732791u, 0xb8715a17u, 0xb8723d96u, 0xb8725098u, 0xb872330du, 0xb871bd85u, 0xb872ca92u, 0xb870ff01u, + 0xb87238b3u, 0xb871431eu, 0xb8727555u, 0xb872c996u, 0xb8723471u, 0xb8728c2eu, 0xb8726213u, 0xb8752b4au, + 0xb8744036u, 0xb87385f6u, 0xb8751377u, 0x39b74b31u, 0x3a333b2au, 0xb1c28c8bu, 0x39f9e951u, 0xb85b0146u, + 0xb85b021eu, 0xb85b35deu, 0xb85b1a28u, 0xb85b2b3cu, 0xb85b35b2u, 0xb85b32dau, 0xb85b4e7du, 0xb85b5203u, + 0xb85b1e61u, 0xb85b3855u, 0xb85bc1ffu, 0xb85b5a9cu, 0xb85b16abu, 0xb85b26e3u, 0xb85b9a58u, 0xb85b15aau, + 0xb85ad277u, 0xb85ae751u, 0xb85b67f4u, 0xb85b51c1u, 0xb85ada81u, 0xb85bcd13u, 0xb85ae648u, 0xb85a8fb0u, + 0xb85c057bu, 0xb85beab4u, 0xb85abe6cu, 0xb85b74fdu, 0xb85be38fu, 0xb85bd648u, 0xb85c3d43u, 0xb85ad192u, + 0xb85aa345u, 0xb85b11f3u, 0xb85bdc34u, 0xb85c0583u, 0xb85ced58u, 0xb85b734bu, 0xb85a9cb2u, 0xb85c4c69u, + 0xb8598dcbu, 0xb85bb0f1u, 0xb85bda13u, 0xb85b15edu, 0xb85caab2u, 0xb85bb219u, 0xb85ba9e1u, 0xb85c8334u, + 0xb85c86f0u, 0xb85bb405u, 0xb859d6dau, 0xb85af588u, 0xb85ca254u, 0xb85affaeu, 0xb85e313fu, 0xb85aa1e9u, + 0xb85b471eu, 0xb85c60c8u, 0xb85ad4feu, 0xb858e1deu, 0x39b38921u, 0xba2c4252u, 0xb0b17f00u, 0xb860f88au, + 0xb845ccc2u, 0xb845c9f7u, 0xb845ec86u, 0xb845c437u, 0xb845cf40u, 0xb8460545u, 0xb845de04u, 0xb84600cdu, + 0xb845b25eu, 0xb84624b1u, 0xb84623eau, 0xb8460ccfu, 0xb845d57fu, 0xb845f845u, 0xb8467b56u, 0xb845d8e8u, + 0xb8460c44u, 0xb8463652u, 0xb8466c69u, 0xb8459eacu, 0xb846c72cu, 0xb8467e9cu, 0xb8461517u, 0xb8463a13u, + 0xb846e52fu, 0xb845aa9du, 0xb84772ceu, 0xb846d8edu, 0xb845ffb2u, 0xb8463a8eu, 0xb845a1a1u, 0xb846c53bu, + 0xb846a321u, 0xb845811cu, 0xb8468b0cu, 0xb8471878u, 0xb84584a8u, 0xb846beb5u, 0xb8473bb6u, 0xb8465058u, + 0xb845f345u, 0xb846315au, 0xb84859a7u, 0xb84720a7u, 0xb847de7du, 0xb846c45fu, 0xb846ced1u, 0xb84535e2u, + 0xb8483bd9u, 0xb8457d97u, 0xb8468447u, 0xb8470ed8u, 0xb8470702u, 0xb84622bfu, 0xb84827f1u, 0xb846ca7fu, + 0xb8459ab1u, 0xb8467f2fu, 0xb8461199u, 0xb8469445u, 0xb84765c3u, 0xb9c1e655u, 0x3a260887u, 0xb1a8c8eeu, + 0xb9f34ac6u, 0xb8330847u, 0xb83313ddu, 0xb8334c23u, 0xb832fa78u, 0xb83348d8u, 0xb8330237u, 0xb8331cb3u, + 0xb83334f9u, 0xb8339a7eu, 0xb832b088u, 0xb832e128u, 0xb83353e6u, 0xb833069fu, 0xb8334b04u, 0xb8334467u, + 0xb831ee50u, 0xb8341eefu, 0xb833cf75u, 0xb831160du, 0xb8339ed5u, 0xb833046cu, 0xb832b959u, 0xb8342c29u, + 0xb8343dcau, 0xb8339ed5u, 0xb8344f90u, 0xb83188edu, 0xb83372c1u, 0xb833dcafu, 0xb8332347u, 0xb831c23bu, + 0xb8356121u, 0xb830cafau, 0xb8308d44u, 0xb83146acu, 0xb83146acu, 0xb83372e5u, 0xb834a7b8u, 0xb8308d44u, + 0xb834a7b8u, 0xb83334e8u, 0xb832f732u, 0xb831236bu, 0xb82fd3dcu, 0xb832f732u, 0xb82fee50u, 0xb82fee50u, + 0xb82fd3dcu, 0xb83334e8u, 0xb83546acu, 0xb8302c04u, 0xb8344f90u, 0xb8338d59u, 0xb8339627u, 0xb83146acu, + 0xb82fee50u, 0xb833d3dcu, 0xb8344f90u, 0xb832583du, 0xb8313d96u, 0xb83372e5u, 0xb9ad2175u, 0xba202de1u, + 0x3234f4beu, 0x36b2a181u, 0xb822b42au, 0xb8230acau, 0xb8225d89u, 0xb822f048u, 0xb8234acbu, 0xb8234acbu, + 0xb823254du, 0xb824da52u, 0xb823254du, 0xb823da52u, 0xb8258f57u, 0xb823254du, 0xb823da52u, 0xb824da52u, + 0xb825254du, 0xb827ae66u, 0xb824da52u, 0xb824da52u, 0xb828445cu, 0xb824da52u, 0xb8237048u, 0xb822063eu, + 0xb8237048u, 0xb826445cu, 0xb824da52u, 0xb826445cu, 0xb826445cu, 0xb826445cu, 0xb827ae66u, 0xb826445cu, + 0xb824445cu, 0xb824da52u, 0xb824da52u, 0xb824da52u, 0xb824da52u, 0xb827ae66u, 0xb820da52u, 0xb824da52u, + 0xb820da52u, 0xb824da52u, 0xb824da52u, 0xb827ae66u, 0xb827ae66u, 0xb828da52u, 0xb824da52u, 0xb81e063eu, + 0xb824da52u, 0xb824da52u, 0xb82bae66u, 0xb824da52u, 0xb823ae66u, 0xb826063eu, 0xb81e063eu, 0xb822063eu, + 0xb827ae66u, 0xb824da52u, 0xb824da52u, 0xb81e063eu, 0xb824da52u, 0xb822063eu, 0xb828da52u, 0x399d9201u, + 0x3a1a5538u, 0x31ba786bu, 0x39c4de5fu, 0xb8145cf4u, 0xb8148d62u, 0xb81450f0u, 0xb814a326u, 0xb8145b6du, + 0xb81488aau, 0xb813b220u, 0xb814117du, 0xb813e0ebu, 0xb81332edu, 0xb8141311u, 0xb813cfe6u, 0xb8154808u, + 0xb8129225u, 0xb81318b9u, 0xb8132dd6u, 0xb8147797u, 0xb8144b82u, 0xb8127c0eu, 0xb8138171u, 0xb815070bu, + 0xb815a9c2u, 0xb81450e5u, 0xb814c39cu, 0xb81241b3u, 0xb813c618u, 0xb813d53du, 0xb81559afu, 0xb815c3b6u, + 0xb810bd5bu, 0xb812ada0u, 0xb8158a48u, 0xb8121318u, 0xb8133d60u, 0xb8138171u, 0xb8111bfau, 0xb816535cu, + 0xb812d0ebu, 0xb8105c36u, 0xb81238f4u, 0xb8138171u, 0xb814efcbu, 0xb8133012u, 0xb817a050u, 0xb81650d6u, + 0xb80d0192u, 0xb8108ecbu, 0xb8173a2cu, 0xb8153662u, 0xb81aeabbu, 0xb8137423u, 0xb813130cu, 0xb8130f42u, + 0xb8120a29u, 0xb8133298u, 0xb8108ecbu, 0xb814a932u, 0xb8141beeu, 0xb81185e8u, 0xb8124833u, 0xb8139c71u, + 0x397fce20u, 0xba14c4deu, 0x313850bau, 0xb81a4fe7u, 0xb807a346u, 0xb80796c8u, 0xb807abc5u, 0xb807c449u, + 0xb807ccf4u, 0xb8078f28u, 0xb807d732u, 0xb8079a69u, 0xb807afd6u, 0xb8079888u, 0xb807782eu, 0xb8078b69u, + 0xb807670bu, 0xb806f242u, 0xb807ae38u, 0xb806f438u, 0xb807714cu, 0xb8072ea6u, 0xb806c607u, 0xb8069d90u, + 0xb806d97bu, 0xb8068c53u, 0xb8068e95u, 0xb806acb2u, 0xb806db8du, 0xb806251au, 0xb807566eu, 0xb8072398u, + 0xb8070111u, 0xb806ed52u, 0xb806636bu, 0xb806c8eau, 0xb805ec3cu, 0xb806b8e9u, 0xb807a9e4u, 0xb806cc50u, + 0xb806f67cu, 0xb8069eb3u, 0xb8065ab0u, 0xb8068d28u, 0xb8073557u, 0xb80603a4u, 0xb8062753u, 0xb805137eu, + 0xb8072367u, 0xb8052dc9u, 0xb8074372u, 0xb8079c47u, 0xb8064f96u, 0xb8043f73u, 0xb8069542u, 0xb808144fu, + 0xb8066bf5u, 0xb80757edu, 0xb8069f27u, 0xb805c5c6u, 0xb8056fafu, 0xb806ba68u, 0xb806091eu, 0xb8065743u, + 0xb80638cfu, 0xb9a35ebau, 0x3a1099b9u, 0xb2b28361u, 0xb9c05355u, 0xb7f95b5eu, 0xb7f959d4u, 0xb7f9a370u, + 0xb7f97867u, 0xb7fa6e15u, 0xb7fa1c6du, 0xb7faa3b9u, 0xb7faf919u, 0xb7fba425u, 0xb7fb7d84u, 0xb7fb878cu, + 0xb7fb82ccu, 0xb7fbb71au, 0xb7fb9311u, 0xb7fc2e64u, 0xb7fc719eu, 0xb7fc6191u, 0xb7fc9515u, 0xb7fc7a92u, + 0xb7fd9030u, 0xb7fb91f7u, 0xb7fdbbb3u, 0xb7fe37c6u, 0xb7fc991cu, 0xb7fdde67u, 0xb7ff5f29u, 0xb7ff17d0u, + 0xb7fe0b9au, 0xb7ff09a1u, 0xb7ff59e7u, 0xb80045f8u, 0xb8008cbeu, 0xb800137bu, 0xb7fef9b6u, 0xb7ff64f7u, + 0xb8013f00u, 0xb7ff8178u, 0xb7ffb332u, 0xb7fe7055u, 0xb8003f24u, 0xb7ffbe1cu, 0xb7ffd1b5u, 0xb800befdu, + 0xb8012afbu, 0xb801bfa5u, 0xb8021a10u, 0xb8021611u, 0xb8029013u, 0xb8004126u, 0xb802a13fu, 0xb8011098u, + 0xb800c693u, 0xb801ab77u, 0xb802e0cau, 0xb801c5cdu, 0xb800a884u, 0xb802891du, 0xb8015a93u, 0xb80413ffu, + 0xb803eed8u, 0xb803e091u, 0xb978fe15u, 0xba0c06e5u, 0x328f8f47u, 0x367d5535u, 0xb7e3fc09u, 0xb7e3b67fu, + 0xb7e40c6eu, 0xb7e418fau, 0xb7e3c4b1u, 0xb7e434e1u, 0xb7e351c1u, 0xb7e35e7eu, 0xb7e409afu, 0xb7e457e4u, + 0xb7e35c9bu, 0xb7e408c3u, 0xb7e44f07u, 0xb7e32c98u, 0xb7e474abu, 0xb7e24e4fu, 0xb7e3a18eu, 0xb7e25de5u, + 0xb7e34cb8u, 0xb7dfacdfu, 0xb7e204b5u, 0xb7e17ba6u, 0xb7e0e0e8u, 0xb7df926fu, 0xb7e19627u, 0xb7e08f52u, + 0xb7e076c8u, 0xb7df6c1fu, 0xb7ddcaa4u, 0xb7e083ebu, 0xb7e04b8au, 0xb7e268b8u, 0xb7e28128u, 0xb7dfed8au, + 0xb7e21caau, 0xb7e018a5u, 0xb7e1af9du, 0xb7e0c750u, 0xb7e04591u, 0xb7e00279u, 0xb7e18cbbu, 0xb7e21bdeu, + 0xb7dbe711u, 0xb7dcbe7eu, 0xb7df5730u, 0xb7e10fc6u, 0xb7e092f1u, 0xb7e07c80u, 0xb7e298dau, 0xb7db2c5bu, + 0xb7e2ca3fu, 0xb7df5101u, 0xb7df4b34u, 0xb7de203au, 0xb7e325ffu, 0xb7dd9b83u, 0xb7e05efau, 0xb7e0df5au, + 0xb7ddc42fu, 0xb7db481du, 0xb7db7ad8u, 0x3989fab8u, 0x3a07b294u, 0x33409b87u, 0x399fcc52u, 0xb7d15423u, + 0xb7d0f71au, 0xb7d09a41u, 0xb7cff430u, 0xb7cfdb0du, 0xb7cff1f6u, 0xb7cfd319u, 0xb7ced8ccu, 0xb7cec8e6u, + 0xb7ce454eu, 0xb7ceff84u, 0xb7cf72d7u, 0xb7ccf5efu, 0xb7cce680u, 0xb7ccd700u, 0xb7cca109u, 0xb7cccd22u, + 0xb7cb35cfu, 0xb7cc8de5u, 0xb7cb6392u, 0xb7c9cd0bu, 0xb7ca3b24u, 0xb7c98608u, 0xb7c879eau, 0xb7cbb916u, + 0xb7ca5bf8u, 0xb7c9872eu, 0xb7c82d49u, 0xb7c95a01u, 0xb7c6f913u, 0xb7c985eeu, 0xb7c7f92eu, 0xb7c66af6u, + 0xb7c78376u, 0xb7c587e3u, 0xb7c6e214u, 0xb7c5ac98u, 0xb7c6f21fu, 0xb7c30aa2u, 0xb7c6c7d7u, 0xb7c79d48u, + 0xb7c51656u, 0xb7c3cc8bu, 0xb7c56f3cu, 0xb7c1966au, 0xb7c59a9au, 0xb7c49c88u, 0xb7c41df6u, 0xb7c194c2u, + 0xb7c28b7bu, 0xb7bf78fau, 0xb7c3d8a0u, 0xb7c41464u, 0xb7c1696fu, 0xb7c29bc2u, 0xb7c25184u, 0xb7be9b9bu, + 0xb7bfb785u, 0xb7be2f03u, 0xb7b7fc1bu, 0xb7bfdfa2u, 0x3939dca2u, 0xba027431u, 0x324086cdu, 0xb7dc8cb8u, + 0xb7c1e1a6u, 0xb7c1c2f3u, 0xb7c18e37u, 0xb7c1b950u, 0xb7c1bebbu, 0xb7c210b1u, 0xb7c1524au, 0xb7c172b4u, + 0xb7c1a957u, 0xb7c1b12bu, 0xb7c145dfu, 0xb7c17ebcu, 0xb7c130f9u, 0xb7c1ed8fu, 0xb7c05f5cu, 0xb7c0a839u, + 0xb7c03226u, 0xb7c1d835u, 0xb7c14eb9u, 0xb7c0a5e5u, 0xb7c1438eu, 0xb7c0a683u, 0xb7c257a1u, 0xb7bff578u, + 0xb7bfc72au, 0xb7c1db2fu, 0xb7c05643u, 0xb7bfef33u, 0xb7bfc23fu, 0xb7c00627u, 0xb7c1775eu, 0xb7bf585eu, + 0xb7c1703eu, 0xb7c18c4au, 0xb7c31879u, 0xb7c1c52cu, 0xb7c1e3f5u, 0xb7c21deau, 0xb7c0dcc6u, 0xb7c2400bu, + 0xb7c1025fu, 0xb7bfd420u, 0xb7bd2784u, 0xb7befc45u, 0xb7bd8969u, 0xb7bfdc24u, 0xb7bfe849u, 0xb7bf03bau, + 0xb7bc2fabu, 0xb7be396eu, 0xb7bf3806u, 0xb7c24ea8u, 0xb7bd7b61u, 0xb7bff814u, 0xb7bdec7du, 0xb7bf5de4u, + 0xb7bcacbbu, 0xb7c188d6u, 0xb7c0576bu, 0xb7bdabe2u, 0xb7bcb0c8u, 0xb98c9fbeu, 0x39fea8beu, 0xb28d52d5u, + 0xb99c816au, 0xb7b41e2au, 0xb7b42473u, 0xb7b41e28u, 0xb7b48ab9u, 0xb7b3e420u, 0xb7b54f4du, 0xb7b4924eu, + 0xb7b528fdu, 0xb7b5ee92u, 0xb7b5251du, 0xb7b5a361u, 0xb7b57895u, 0xb7b5fbbau, 0xb7b6b647u, 0xb7b62cf9u, + 0xb7b5f6ffu, 0xb7b5a277u, 0xb7b560ffu, 0xb7b6830du, 0xb7b5de57u, 0xb7b81256u, 0xb7b8a14eu, 0xb7b78cd8u, + 0xb7b77e49u, 0xb7b67e5cu, 0xb7b71430u, 0xb7b9d501u, 0xb7b7338eu, 0xb7b87545u, 0xb7b64572u, 0xb7b76c69u, + 0xb7babb82u, 0xb7ba2ed2u, 0xb7babefeu, 0xb7ba8f2eu, 0xb7bb1623u, 0xb7b9fd0du, 0xb7bcde0du, 0xb7bbe8ceu, + 0xb7ba0b5cu, 0xb7b90913u, 0xb7bce7b6u, 0xb7b68802u, 0xb7b8fb2du, 0xb7bc716bu, 0xb7b7797du, 0xb7bdbbc3u, + 0xb7b8ec30u, 0xb7bd2760u, 0xb7bc8f50u, 0xb7b99cbfu, 0xb7bd2638u, 0xb7bb2afeu, 0xb7bbab31u, 0xb7bedea8u, + 0xb7b9ef8bu, 0xb7ba8e03u, 0xb7c056a7u, 0xb7bd3593u, 0xb7bfd927u, 0xb7ba10e6u, 0xb932754du, 0xb9f78b9du, + 0xb20014cau, 0x36361960u, 0xb7a640ddu, 0xb7a6282bu, 0xb7a6d124u, 0xb7a6d2d1u, 0xb7a6e67cu, 0xb7a6cad3u, + 0xb7a67c8cu, 0xb7a776a7u, 0xb7a6bec3u, 0xb7a754b9u, 0xb7a5c248u, 0xb7a7e994u, 0xb7a6c45eu, 0xb7a73ce4u, + 0xb7aa8ec0u, 0xb7a8a670u, 0xb7a85dbbu, 0xb7a9cdc7u, 0xb7a794b9u, 0xb7a88ce5u, 0xb7a82941u, 0xb7a80856u, + 0xb7aa87efu, 0xb7a94601u, 0xb7a9de5bu, 0xb7a9a2feu, 0xb7a7548au, 0xb7a8c9bfu, 0xb7a851beu, 0xb7aab591u, + 0xb7a8b8beu, 0xb7a654b3u, 0xb7a8239au, 0xb7a72b46u, 0xb7a51b2fu, 0xb7a97f7eu, 0xb7a50195u, 0xb7a769d1u, + 0xb7a2567du, 0xb7a84fa2u, 0xb7a427efu, 0xb7a9702du, 0xb7ab8aecu, 0xb7a3cb11u, 0xb7ab87d2u, 0xb7a6ae6au, + 0xb7a4e46au, 0xb7a95ffbu, 0xb7a9ed34u, 0xb7a73a84u, 0xb7a924acu, 0xb7a62dd2u, 0xb7a81560u, 0xb7a7d941u, + 0xb7a3f432u, 0xb7a6f379u, 0xb7a8c3dau, 0xb7aae847u, 0xb7a825a9u, 0xb7a49e97u, 0xb7abfd68u, 0x3972bb21u, + 0x39ef8e0bu, 0xb1adf352u, 0x3984cf64u, 0xb79a9104u, 0xb79a6ec3u, 0xb79a50b5u, 0xb79a6ac8u, 0xb79a79a2u, + 0xb79a69abu, 0xb79a38a6u, 0xb799c034u, 0xb79b5479u, 0xb79a7dc8u, 0xb79b551eu, 0xb79a3eb7u, 0xb79b0954u, + 0xb79a9ab3u, 0xb79ab1f0u, 0xb79a9578u, 0xb799e5d7u, 0xb799698eu, 0xb79b307bu, 0xb79afc86u, 0xb79a27a6u, + 0xb79903a8u, 0xb79c6ceeu, 0xb79aa281u, 0xb799ac80u, 0xb799c2f8u, 0xb79bc73au, 0xb7989123u, 0xb79a1d07u, + 0xb799d3e8u, 0xb79cd8efu, 0xb797c014u, 0xb79a5421u, 0xb79a0fcau, 0xb79b8892u, 0xb79aff4au, 0xb79b2a71u, + 0xb79aa415u, 0xb79bff5du, 0xb797edd5u, 0xb79ea423u, 0xb797b5f6u, 0xb798756eu, 0xb79d2b93u, 0xb79ae71cu, + 0xb79d56d6u, 0xb79b6c4cu, 0xb79c2ec3u, 0xb798fb0cu, 0xb79d0273u, 0xb79c8c76u, 0xb798fcd6u, 0xb799d832u, + 0xb797a24fu, 0xb79d61ffu, 0xb79bde80u, 0xb79c9a09u, 0xb79b7d16u, 0xb79e549eu, 0xb79c8820u, 0xb797317au, + 0x3902dbfeu, 0xb9e87b0cu, 0x30b6ad3bu, 0xb7a329bfu, 0xb78f87e0u, 0xb78f99bfu, 0xb78f6b95u, 0xb78f1c35u, + 0xb78feb98u, 0xb78fd2ebu, 0xb7902b79u, 0xb78f6701u, 0xb78f1d22u, 0xb78fd3e1u, 0xb78faa4eu, 0xb78f8d6eu, + 0xb78f160du, 0xb7902c18u, 0xb790835cu, 0xb790d2b2u, 0xb7910c63u, 0xb78f4c3cu, 0xb79083afu, 0xb78ea997u, + 0xb78fddd2u, 0xb78de92fu, 0xb78ec9d1u, 0xb78fc1c1u, 0xb78ecbf2u, 0xb78f2aeau, 0xb7906ce2u, 0xb78f9c2au, + 0xb78f19e9u, 0xb78e3795u, 0xb7903a7bu, 0xb78dbb78u, 0xb78ead6bu, 0xb78faf70u, 0xb78f837bu, 0xb791caffu, + 0xb790e50eu, 0xb78ecd85u, 0xb78f021du, 0xb78f0740u, 0xb790d399u, 0xb78f781fu, 0xb7900857u, 0xb78fda01u, + 0xb7903ae7u, 0xb78ff62fu, 0xb78f9d3cu, 0xb78e5793u, 0xb78f5e35u, 0xb78fdce6u, 0xb78fed4fu, 0xb78cebcau, + 0xb78fd31bu, 0xb78feff3u, 0xb78fd133u, 0xb78edd9au, 0xb78febacu, 0xb78fe47cu, 0xb78eeb22u, 0xb79394dbu, + 0xb78f1b42u, 0xb9763193u, 0x39e218c4u, 0x31dc21d7u, 0xb98261bcu, 0xb78584a9u, 0xb7854b44u, 0xb7858e95u, + 0xb7852744u, 0xb7853b78u, 0xb7858d60u, 0xb7845178u, 0xb78598c6u, 0xb78484efu, 0xb7857ea1u, 0xb784d01fu, + 0xb7855471u, 0xb784c64au, 0xb7856539u, 0xb7850b10u, 0xb7842584u, 0xb783580au, 0xb784f116u, 0xb7857fa5u, + 0xb784fe99u, 0xb7842b09u, 0xb7831047u, 0xb783aeb8u, 0xb7821f7eu, 0xb783f5b8u, 0xb78387c5u, 0xb7840c38u, + 0xb7850659u, 0xb7852047u, 0xb7828e66u, 0xb7827ddfu, 0xb7810c2eu, 0xb7824f4au, 0xb782a0c8u, 0xb782e57du, + 0xb782c232u, 0xb77f3899u, 0xb7844b94u, 0xb7842539u, 0xb7811d75u, 0xb780b37bu, 0xb7807302u, 0xb7837dcbu, + 0xb785a427u, 0xb7807c40u, 0xb782d9a1u, 0xb7807263u, 0xb7841cb7u, 0xb780db87u, 0xb783d6e2u, 0xb7819cc6u, + 0xb78378c1u, 0xb783abb9u, 0xb7849ccau, 0xb783c46au, 0xb77d77efu, 0xb77cd4b3u, 0xb7841f7au, 0xb77e4a06u, + 0xb7810e62u, 0xb7857ba9u, 0xb8fbf920u, 0xb9db3430u, 0x318e8e8au, 0x36090345u, 0xb7794a09u, 0xb779710cu, + 0xb778da7bu, 0xb778cca2u, 0xb77857cdu, 0xb77829d1u, 0xb77a1e6bu, 0xb7788fe4u, 0xb7792228u, 0xb778e3dbu, + 0xb779a04fu, 0xb77a3ec4u, 0xb7780070u, 0xb7762746u, 0xb777bc53u, 0xb77777a9u, 0xb77a834bu, 0xb776b4feu, + 0xb77afe21u, 0xb77383b0u, 0xb772d934u, 0xb775b9c6u, 0xb777b2b9u, 0xb7789dd7u, 0xb779e752u, 0xb778e574u, + 0xb77bbaa9u, 0xb7765fefu, 0xb77c77c7u, 0xb7794c3eu, 0xb778e6f9u, 0xb779d7ebu, 0xb77bd27du, 0xb77cac8au, + 0xb77a3545u, 0xb77eba0du, 0xb77c51dbu, 0xb77a21d1u, 0xb7814ecbu, 0xb77684aeu, 0xb7763759u, 0xb7741836u, + 0xb7749c2au, 0xb7789c10u, 0xb777a74cu, 0xb77a19fau, 0xb77d2010u, 0xb779f342u, 0xb77de251u, 0xb7802f0au, + 0xb77c6485u, 0xb7759c35u, 0xb781c273u, 0xb7816694u, 0xb7803015u, 0xb77265dfu, 0xb778c191u, 0xb778f217u, + 0xb778e0f5u, 0xb77d0f41u, 0xb775efa3u, 0x3957bc68u, 0x39d62649u, 0x318fff7au, 0x39614afeu, 0xb7691d71u, + 0xb76870b8u, 0xb7689785u, 0xb768f9aeu, 0xb7690a9bu, 0xb768aa2fu, 0xb768e9a7u, 0xb767daf1u, 0xb767a537u, + 0xb7688bc7u, 0xb7667895u, 0xb7679d5bu, 0xb7662b98u, 0xb768c46bu, 0xb767b5e1u, 0xb768b3b1u, 0xb768f168u, + 0xb7677486u, 0xb76982e1u, 0xb769939cu, 0xb7658353u, 0xb76908d7u, 0xb76749a9u, 0xb7697a54u, 0xb7668864u, + 0xb7684ac9u, 0xb76a4636u, 0xb7686e60u, 0xb7645bb6u, 0xb76a12f9u, 0xb7669494u, 0xb76394d3u, 0xb767f70eu, + 0xb767ec3cu, 0xb764b94bu, 0xb76c574fu, 0xb768e926u, 0xb764aa3bu, 0xb76c70eau, 0xb7672c8bu, 0xb76b84acu, + 0xb762e1bau, 0xb76972bau, 0xb768c39au, 0xb75e8613u, 0xb765690fu, 0xb7670e61u, 0xb762200eu, 0xb768db32u, + 0xb76497efu, 0xb76661beu, 0xb7664893u, 0xb766f73au, 0xb76872b7u, 0xb76ed277u, 0xb765538bu, 0xb76bcfeau, + 0xb761b1cau, 0xb768bb02u, 0xb7692f34u, 0xb76c8a29u, 0x38b8b50bu, 0xb9cf899au, 0x321b7b85u, 0xb777d209u, + 0xb75a0e9eu, 0xb75a5770u, 0xb75ab583u, 0xb75a365au, 0xb759dc19u, 0xb75aaa5eu, 0xb75a2b32u, 0xb75b01e8u, + 0xb75a1421u, 0xb75bef1fu, 0xb75b045eu, 0xb75aaa8cu, 0xb759e4e4u, 0xb7594e8cu, 0xb75c4864u, 0xb759d797u, + 0xb75b5ce5u, 0xb759fa5au, 0xb7599cecu, 0xb759f31fu, 0xb75d2297u, 0xb75ceff3u, 0xb75b302cu, 0xb75b3c68u, + 0xb75cf714u, 0xb75bc7a3u, 0xb759fab1u, 0xb75d8bb7u, 0xb75bf8f0u, 0xb75acebdu, 0xb75e34a7u, 0xb75ba9afu, + 0xb7579ecau, 0xb7601d4au, 0xb75be3bau, 0xb75aa6ddu, 0xb76098e1u, 0xb75a575au, 0xb75c55a0u, 0xb75b2cc6u, + 0xb75f260au, 0xb7591083u, 0xb75a3445u, 0xb75affffu, 0xb75e2812u, 0xb75bd490u, 0xb760018fu, 0xb75b456du, + 0xb75def80u, 0xb75fa828u, 0xb75b24d7u, 0xb75f1208u, 0xb7604e0au, 0xb75b5f0eu, 0xb75f75b2u, 0xb75c2ac1u, + 0xb75ae0ceu, 0xb75e1a42u, 0xb75954bfu, 0xb7615ce3u, 0xb7595788u, 0xb959ac34u, 0x39ca40acu, 0xb089758fu, + 0xb95d97d7u, 0xb74bbe19u, 0xb74b6e93u, 0xb74b4556u, 0xb74a37eeu, 0xb74b916du, 0xb74a55d2u, 0xb74d43d9u, + 0xb74a9a7cu, 0xb748f835u, 0xb74cf8a3u, 0xb74b1620u, 0xb74b5840u, 0xb74e699eu, 0xb7487bfau, 0xb74ae0bbu, + 0xb74870bbu, 0xb74baaabu, 0xb74dae2bu, 0xb74d20bbu, 0xb74ba8c0u, 0xb74b03f0u, 0xb74a6d6fu, 0xb74c7e28u, + 0xb74ba982u, 0xb748d820u, 0xb7462c93u, 0xb745a449u, 0xb74c3301u, 0xb73cad73u, 0xb745b6dcu, 0xb7453d9bu, + 0xb7402c69u, 0xb74c7033u, 0xb74749f8u, 0xb7471affu, 0xb740eb26u, 0xb7534f78u, 0xb746e1c9u, 0xb74c3d3cu, + 0xb751ce56u, 0xb7479c05u, 0xb748f5fbu, 0xb745069au, 0xb753b76du, 0xb7474d0bu, 0xb74c49e0u, 0xb7484a2au, + 0xb74db752u, 0xb7437ee4u, 0xb74cd9f7u, 0xb74c2f38u, 0xb745ba5cu, 0xb746ec4cu, 0xb74ca2f7u, 0xb74cc539u, + 0xb748bc5bu, 0xb743f111u, 0xb7466098u, 0xb7488b8du, 0xb744d9b7u, 0xb744194du, 0xb8af7461u, 0xb9c4876bu, + 0xb22711b2u, 0x35cc10d0u, 0xb73f1274u, 0xb73fc7f7u, 0xb73fac38u, 0xb73d6373u, 0xb73eb1efu, 0xb73dee00u, + 0xb73e257eu, 0xb73e66f4u, 0xb73a0b02u, 0xb7401202u, 0xb7403ffbu, 0xb73dc0d9u, 0xb73ed5f1u, 0xb741aa05u, + 0xb73c5ff4u, 0xb7441202u, 0xb73c5ff4u, 0xb73ed816u, 0xb746e616u, 0xb741e7d2u, 0xb73a35c4u, 0xb7478ff9u, + 0xb73635c4u, 0xb73c1202u, 0xb741e7d2u, 0xb7454bedu, 0xb7454bedu, 0xb73d99e0u, 0xb742d3cbu, 0xb74199e0u, + 0xb747761cu, 0xb753225du, 0xb74d7a35u, 0xb7457a35u, 0xb74d7a35u, 0xb75089f1u, 0xb747d20eu, 0xb744c252u, + 0xb7371a2au, 0xb753225du, 0xb74d7a35u, 0xb75089f1u, 0xb75089f1u, 0xb73a29e6u, 0xb73fd20eu, 0xb7498e01u, + 0xb747d20eu, 0xb75245e5u, 0xb74539a2u, 0xb73fd20eu, 0xb74ae1cau, 0xb7518e01u, 0xb732c5cbu, 0xb7473629u, + 0xb737917bu, 0xb74fee0cu, 0xb7557a35u, 0xb7418e01u, 0xb74a45e5u, 0xb7473629u, 0xb7449dbdu, 0x394071b5u, + 0x39bf4b56u, 0x315f8ca5u, 0x3942569bu, 0xb734027au, 0xb7336cc8u, 0xb734a2f6u, 0xb732410fu, 0xb7337d69u, + 0xb73236ffu, 0xb732a384u, 0xb7331df5u, 0xb73466cdu, 0xb736ccbau, 0xb732ea97u, 0xb731e4bbu, 0xb735590fu, + 0xb732e9d0u, 0xb733fc26u, 0xb73099d0u, 0xb734551bu, 0xb7325697u, 0xb72e5b99u, 0xb73978ceu, 0xb731fda3u, + 0xb7335091u, 0xb7370f57u, 0xb7314ce5u, 0xb731a7acu, 0xb7379a8au, 0xb73176d8u, 0xb7329463u, 0xb72b69b9u, + 0xb7379df2u, 0xb72a4cfcu, 0xb733bb55u, 0xb72e6a05u, 0xb72e0820u, 0xb730c41bu, 0xb73036f3u, 0xb72f608fu, + 0xb7239b26u, 0xb7383dc0u, 0xb72d699fu, 0xb73417b3u, 0xb7379694u, 0xb72c52f0u, 0xb73a7802u, 0xb732c08eu, + 0xb7304f70u, 0xb72ab175u, 0xb734d819u, 0xb7299d81u, 0xb73c1479u, 0xb73416f1u, 0xb728ff83u, 0xb732dbf4u, + 0xb73fbc56u, 0xb7378a94u, 0xb73ac969u, 0xb734757cu, 0xb73c27feu, 0xb7291039u, 0xb736c0f1u, 0xb739833du, + 0x387916a2u, 0xb9bac9b3u, 0x31fb4f6au, 0xb740552eu, 0xb729246cu, 0xb729121cu, 0xb729498fu, 0xb7285c22u, + 0xb728e9c8u, 0xb728b9b8u, 0xb7298eacu, 0xb728901du, 0xb729446fu, 0xb729904bu, 0xb7282076u, 0xb728e1dfu, + 0xb7273712u, 0xb7278caau, 0xb726f455u, 0xb7271375u, 0xb727bbaeu, 0xb7276ad9u, 0xb72a7b19u, 0xb7294e40u, + 0xb725eb1cu, 0xb725a32bu, 0xb7263c99u, 0xb726af48u, 0xb727271au, 0xb723703bu, 0xb72618a5u, 0xb727998au, + 0xb724492fu, 0xb724810fu, 0xb725b398u, 0xb724d874u, 0xb71fa535u, 0xb722e365u, 0xb7271e9eu, 0xb7230153u, + 0xb725d3f1u, 0xb72793e8u, 0xb7240dfdu, 0xb723aea5u, 0xb7287791u, 0xb72b0022u, 0xb72867a0u, 0xb727cf79u, + 0xb721544fu, 0xb721837bu, 0xb727220fu, 0xb725a9fdu, 0xb721b160u, 0xb72210fdu, 0xb722e2c3u, 0xb720ff9fu, + 0xb7201fa2u, 0xb723e986u, 0xb720ffbau, 0xb7222d22u, 0xb7276409u, 0xb722359fu, 0xb722b4a0u, 0xb72271ebu, + 0xb7269b65u, 0xb9413358u, 0x39b5f28eu, 0x30954190u, 0xb93f7d53u, 0xb71f6ebfu, 0xb71f0299u, 0xb71ef011u, + 0xb71fd94fu, 0xb71e8798u, 0xb71ed61au, 0xb71f1ccdu, 0xb71fa236u, 0xb71fc5ffu, 0xb72015fcu, 0xb71f2894u, + 0xb71fda36u, 0xb7205792u, 0xb71e1ab7u, 0xb71f40efu, 0xb71f04ffu, 0xb71ed5cfu, 0xb71e9086u, 0xb71db2d8u, + 0xb71dd991u, 0xb72277a4u, 0xb722dc50u, 0xb71d822cu, 0xb7200986u, 0xb71efa8eu, 0xb723816au, 0xb721f3ddu, + 0xb722d603u, 0xb71f9d95u, 0xb72226b6u, 0xb71f6475u, 0xb71db2deu, 0xb721bc99u, 0xb720c001u, 0xb71a4954u, + 0xb71fd326u, 0xb7213e06u, 0xb72220e2u, 0xb7216ec6u, 0xb72189d8u, 0xb71d999fu, 0xb722a8c7u, 0xb7242ee2u, + 0xb724ae79u, 0xb71f6967u, 0xb7247e2bu, 0xb71de950u, 0xb71fc9edu, 0xb71f25ecu, 0xb71f6c9cu, 0xb71dc7e0u, + 0xb71ae87fu, 0xb723fe45u, 0xb71b4686u, 0xb72499d0u, 0xb71f3e5bu, 0xb729c5c3u, 0xb7213d74u, 0xb7256bf9u, + 0xb71fa5cfu, 0xb72311ecu, 0xb86a45feu, 0xb9b11f17u, 0x32061302u, 0x35a52a5du, 0xb7161d01u, 0xb716ce6du, + 0xb717283du, 0xb7162700u, 0xb7172428u, 0xb715d655u, 0xb7171aa9u, 0xb715a3cfu, 0xb7167f2bu, 0xb7173464u, + 0xb7168930u, 0xb717115au, 0xb7152360u, 0xb715449cu, 0xb716753fu, 0xb715c46cu, 0xb7169c5cu, 0xb7149da2u, + 0xb7152c49u, 0xb7144f24u, 0xb717e615u, 0xb7161c57u, 0xb71afff3u, 0xb714d0e0u, 0xb7164746u, 0xb717cde7u, + 0xb713591cu, 0xb7140d7bu, 0xb7135626u, 0xb7157fc3u, 0xb7198e9fu, 0xb71e60afu, 0xb719509cu, 0xb719e6f6u, + 0xb71885feu, 0xb71b4d87u, 0xb71d1a81u, 0xb71808a6u, 0xb720dd53u, 0xb71d255cu, 0xb71dbc7cu, 0xb7127eadu, + 0xb71c10ecu, 0xb7197268u, 0xb71e26b4u, 0xb71dc518u, 0xb71da706u, 0xb71ae540u, 0xb71612b2u, 0xb712e251u, + 0xb71b56eau, 0xb70f2c17u, 0xb71c7f63u, 0xb7153dc4u, 0xb7168229u, 0xb71ac37cu, 0xb7127b15u, 0xb71aedc5u, + 0xb71a5f8fu, 0xb715a520u, 0xb7164ab1u, 0x392d6decu, 0x39acadcbu, 0xb1412358u, 0x392a20b9u, 0xb70d7c25u, + 0xb70da359u, 0xb70df920u, 0xb70e13acu, 0xb70ce56eu, 0xb70eeddeu, 0xb70cd9fdu, 0xb70dcb8eu, 0xb70f1881u, + 0xb70f1329u, 0xb70d8db6u, 0xb70df6f7u, 0xb70cdd67u, 0xb710d781u, 0xb70ef13fu, 0xb70f57a2u, 0xb70e5e88u, + 0xb70ea17du, 0xb70ed84eu, 0xb70e9874u, 0xb70e04a3u, 0xb70f6b7du, 0xb70ded1eu, 0xb70ed4c7u, 0xb713efdfu, + 0xb70d025cu, 0xb70f5b7fu, 0xb70e04e5u, 0xb7112331u, 0xb70f9763u, 0xb710e3c2u, 0xb70b2e24u, 0xb712b079u, + 0xb7131e84u, 0xb7142c49u, 0xb70a1be7u, 0xb70db30cu, 0xb70bfd57u, 0xb71250b3u, 0xb70a8239u, 0xb7163e17u, + 0xb70f38bdu, 0xb709b8ebu, 0xb7135622u, 0xb712269cu, 0xb710f099u, 0xb7136cafu, 0xb70cf155u, 0xb7129004u, + 0xb7109205u, 0xb710d2adu, 0xb70eb77eu, 0xb70c5b93u, 0xb70f3774u, 0xb7142d66u, 0xb70f1fcdu, 0xb70d0c29u, + 0xb706c1f9u, 0xb71569cbu, 0xb711ebeeu, 0xb714b08au, 0x3817fac0u, 0xb9a83a1bu, 0x31acc59bu, 0xb71812cbu, + 0xb705978bu, 0xb70591f7u, 0xb7054848u, 0xb7051c17u, 0xb7057dc7u, 0xb705d69bu, 0xb705ea5cu, 0xb704fe8cu, + 0xb7050ab7u, 0xb7057495u, 0xb7059f44u, 0xb7059594u, 0xb70561b5u, 0xb704c21fu, 0xb7068f5eu, 0xb7066767u, + 0xb706c40cu, 0xb70607aeu, 0xb700fc3cu, 0xb7033976u, 0xb7037dc8u, 0xb704cea1u, 0xb7067681u, 0xb704aa8bu, + 0xb7070b1fu, 0xb7067fb1u, 0xb70463edu, 0xb705c06au, 0xb707e455u, 0xb7057eb4u, 0xb705369au, 0xb70428d2u, + 0xb7067ab2u, 0xb703106au, 0xb703d904u, 0xb7052852u, 0xb70423eeu, 0xb704fcbdu, 0xb70748b3u, 0xb7094e59u, + 0xb70263fcu, 0xb70058c1u, 0xb7097775u, 0xb6fea8b5u, 0xb70c2d8au, 0xb70211ceu, 0xb70f276eu, 0xb7025ffdu, + 0xb706991bu, 0xb70336fdu, 0xb701414fu, 0xb702bc54u, 0xb701b894u, 0xb704b715u, 0xb7075c20u, 0xb7012c08u, + 0xb700f6a6u, 0xb6ff1dc7u, 0xb70ad1d3u, 0xb70316efu, 0xb70192c2u, 0xb92d2cc0u, 0x39a42d5cu, 0xb04db00du, + 0xb927dc29u, 0xb6fc7c6cu, 0xb6fc10b1u, 0xb6fd5c5eu, 0xb6fc0deau, 0xb6fa418du, 0xb6feba68u, 0xb6fa3f67u, + 0xb6f9c244u, 0xb6f797dau, 0xb6fb006au, 0xb6fa447fu, 0xb6fcf7f5u, 0xb6facbc2u, 0xb6fbd1c2u, 0xb6fc81bfu, + 0xb6f70accu, 0xb6f6e416u, 0xb6ffb70au, 0xb6ff765du, 0xb6fc2c81u, 0xb7009674u, 0xb6f5cd9eu, 0xb6fd42c4u, + 0xb6f9585cu, 0xb6f9b613u, 0xb6f79921u, 0xb6f8fa56u, 0xb6f1edfeu, 0xb6f1a66eu, 0xb6fc8cc5u, 0xb6f17af5u, + 0xb6fce2f3u, 0xb6f2a650u, 0xb6fbb76du, 0xb7031051u, 0xb6f29fc1u, 0xb6eb5477u, 0xb6ee97d4u, 0xb6ffc4c3u, + 0xb6eefdb6u, 0xb6fbfd80u, 0xb6f82df3u, 0xb6eeac84u, 0xb6f2f577u, 0xb701523fu, 0xb6f21a8du, 0xb70181c2u, + 0xb6f37c00u, 0xb6fd53ccu, 0xb6f8b034u, 0xb6e68204u, 0xb6f2f7fau, 0xb6f40539u, 0xb70029f6u, 0xb6ed7fe1u, + 0xb6fbcf4cu, 0xb6f7c151u, 0xb6fe7ba9u, 0xb6e3872du, 0xb701b56bu, 0xb6f5a0c2u, 0xb811254du, 0xb99fe301u, + 0xb1d3ae04u, 0x3582fb58u, 0xb6efc7afu, 0xb6ef6e08u, 0xb6ed3558u, 0xb6ec8e51u, 0xb6eec543u, 0xb6eccd1au, + 0xb6eef84cu, 0xb6ecd0a1u, 0xb6f4bf00u, 0xb6f6dfb9u, 0xb6f6d97bu, 0xb6f46eecu, 0xb6f2a35fu, 0xb6f5bfdbu, + 0xb6f325abu, 0xb6f7ee52u, 0xb6e9e81du, 0xb6f89fbcu, 0xb6f90612u, 0xb6f1ad60u, 0xb6e0c83bu, 0xb6e726c2u, + 0xb6eb74d3u, 0xb6f22968u, 0xb6f4a281u, 0xb6e8ff89u, 0xb6f1e23cu, 0xb6f09af5u, 0xb6f8fb32u, 0xb6f3a9b9u, + 0xb6f6888au, 0xb6e35dcdu, 0xb7033569u, 0xb6ff2379u, 0xb6ec6e8bu, 0xb6fcfd09u, 0xb6f5b967u, 0xb6edd161u, + 0xb704609bu, 0xb6fd0e7au, 0xb6f89be9u, 0xb7020a11u, 0xb6e88124u, 0xb6fdec1bu, 0xb7039467u, 0xb6fcb474u, + 0xb6fd3aeau, 0xb6e9602fu, 0xb6f2c829u, 0xb6f29a7eu, 0xb6dc09e1u, 0xb6ee6d5eu, 0xb7019d3cu, 0xb705d529u, + 0xb6fe7b8cu, 0xb6fc6d0fu, 0xb70b466du, 0xb6fb0833u, 0xb6ff6003u, 0xb7049c95u, 0xb702be0eu, 0x391c87a0u, + 0x399bf6b1u, 0x31178dfeu, 0x3916de8eu, 0xb6e1a01cu, 0xb6e2039fu, 0xb6e1964eu, 0xb6e08f82u, 0xb6df66e5u, + 0xb6df8412u, 0xb6de67f9u, 0xb6e09b22u, 0xb6de4614u, 0xb6dd776cu, 0xb6deddb3u, 0xb6e21334u, 0xb6e04f29u, + 0xb6dccbddu, 0xb6de28bbu, 0xb6dc296cu, 0xb6da813fu, 0xb6e07697u, 0xb6d80f9du, 0xb6de73fdu, 0xb6dcc617u, + 0xb6dccfa0u, 0xb6e27255u, 0xb6d628fcu, 0xb6da4c2fu, 0xb6daaa4cu, 0xb6dbd470u, 0xb6d9808au, 0xb6e522abu, + 0xb6d2a3a5u, 0xb6dc4710u, 0xb6d75d88u, 0xb6dbe140u, 0xb6d7269bu, 0xb6da934eu, 0xb6e350edu, 0xb6d91c7eu, + 0xb6cbb291u, 0xb6e69245u, 0xb6de0465u, 0xb6dc71efu, 0xb6c6d801u, 0xb6dae38eu, 0xb6cb61f0u, 0xb6d4d21fu, + 0xb6d5d29eu, 0xb6ee6fe2u, 0xb6ebfb25u, 0xb6d94eb9u, 0xb6d36a85u, 0xb6c6192au, 0xb6d076c6u, 0xb6e0d22fu, + 0xb6cd9eecu, 0xb6e1fb69u, 0xb6c41adbu, 0xb6d7168fu, 0xb6caec85u, 0xb6cbdaccu, 0xb6d3e994u, 0xb6ecd3cbu, + 0x379dc9a8u, 0xb997defbu, 0x319ab9bcu, 0xb6f40cc4u, 0xb6d6f737u, 0xb6d72d48u, 0xb6d77c58u, 0xb6d8c446u, + 0xb6d7bdf9u, 0xb6d6fc94u, 0xb6d87026u, 0xb6d5ec65u, 0xb6d6e959u, 0xb6d76eb9u, 0xb6d2ee55u, 0xb6d69b0eu, + 0xb6d3c690u, 0xb6d5f470u, 0xb6d9f3a2u, 0xb6d4c26eu, 0xb6d5e1b5u, 0xb6d8b2fdu, 0xb6d8b44fu, 0xb6db2694u, + 0xb6e08167u, 0xb6d1fedau, 0xb6d4acaeu, 0xb6d78277u, 0xb6d3c866u, 0xb6d50cfbu, 0xb6dbee42u, 0xb6dc44c5u, + 0xb6dd9d61u, 0xb6db07adu, 0xb6d99081u, 0xb6d98044u, 0xb6d8d5adu, 0xb6d80825u, 0xb6dda32du, 0xb6dc2bafu, + 0xb6e06cc5u, 0xb6d1f47au, 0xb6d97109u, 0xb6d4577bu, 0xb6d349feu, 0xb6d3951bu, 0xb6d7bab0u, 0xb6caf36cu, + 0xb6dc13c2u, 0xb6df729eu, 0xb6d664ffu, 0xb6d2825au, 0xb6d36b9cu, 0xb6d00f0au, 0xb6dc00adu, 0xb6d5cd08u, + 0xb6dfc7adu, 0xb6da22bbu, 0xb6d80044u, 0xb6db4b5au, 0xb6d68652u, 0xb6dcc8eau, 0xb6da979cu, 0xb6dee84du, + 0xb6d96bb2u, 0xb91bacb6u, 0x3994409cu, 0xb166cf17u, 0xb9150888u, 0xb6cc0116u, 0xb6cbaabeu, 0xb6caf380u, + 0xb6cbc030u, 0xb6cbd2c9u, 0xb6cbaf96u, 0xb6cd0eafu, 0xb6c97915u, 0xb6cd130fu, 0xb6cb07f3u, 0xb6d15c61u, + 0xb6c89ae5u, 0xb6c5c5a0u, 0xb6cb3d5cu, 0xb6cd9526u, 0xb6c54f1du, 0xb6cc1b26u, 0xb6cc09dcu, 0xb6cbe711u, + 0xb6cf8e34u, 0xb6cccde6u, 0xb6cccadeu, 0xb6cbe54cu, 0xb6ce3f76u, 0xb6c3978bu, 0xb6cccd09u, 0xb6ce9138u, + 0xb6c99542u, 0xb6cf2d60u, 0xb6cb00b5u, 0xb6d07ac3u, 0xb6d10ca9u, 0xb6d81779u, 0xb6ca04bbu, 0xb6d072ebu, + 0xb6cecb92u, 0xb6cc50feu, 0xb6d591bfu, 0xb6ca682eu, 0xb6d315f4u, 0xb6cd4c74u, 0xb6ce0e73u, 0xb6d68747u, + 0xb6c7b941u, 0xb6cb5a27u, 0xb6d9b966u, 0xb6d0ed66u, 0xb6d0f674u, 0xb6c7b0a0u, 0xb6d32413u, 0xb6d48308u, + 0xb6d4c1a1u, 0xb6d7594au, 0xb6c35a4du, 0xb6bda6ccu, 0xb6dce92du, 0xb6cb56c7u, 0xb6d4d1f7u, 0xb6d160d4u, + 0xb6d7cbf3u, 0xb6d4c94fu, 0xb78dcb36u, 0xb990fa9du, 0xb231a103u, 0x3551333eu, 0xb6c12707u, 0xb6c229abu, + 0xb6c11408u, 0xb6bff97bu, 0xb6c14411u, 0xb6c19065u, 0xb6c3a55cu, 0xb6bcf1d5u, 0xb6bfb955u, 0xb6c2863fu, + 0xb6c0de15u, 0xb6c0ca0au, 0xb6bdd0b9u, 0xb6bfd614u, 0xb6bf32e6u, 0xb6cac60fu, 0xb6d152bfu, 0xb6c49fe4u, + 0xb6c47fc8u, 0xb6c31ea2u, 0xb6c4ad0bu, 0xb6cd1565u, 0xb6bfe9d0u, 0xb6c6b449u, 0xb6d027e1u, 0xb6c48f14u, + 0xb6c8dbb4u, 0xb6c76d26u, 0xb6c84336u, 0xb6c8a235u, 0xb6c82495u, 0xb6d320d3u, 0xb6d26bc0u, 0xb6d3efdcu, + 0xb6d6cacfu, 0xb6cd2bd8u, 0xb6d5b654u, 0xb6cf8777u, 0xb6c04276u, 0xb6ca32a0u, 0xb6c7814cu, 0xb6be3ad9u, + 0xb6cb03a2u, 0xb6bd25c0u, 0xb6c19c7cu, 0xb6ce74f2u, 0xb6c24e48u, 0xb6d5b2c4u, 0xb6caa257u, 0xb6c6b067u, + 0xb6cd2a09u, 0xb6c3a9c9u, 0xb6c22e76u, 0xb6c56601u, 0xb6b60006u, 0xb6bfd2e2u, 0xb6c61114u, 0xb6cad740u, + 0xb6bbb8d8u, 0xb6b89a09u, 0xb6c939f7u, 0x390e67a2u, 0x398d6eddu, 0x3118f5e8u, 0x39074c0au, 0xb6b71e52u, + 0xb6b66fa9u, 0xb6b6c7b3u, 0xb6b68258u, 0xb6b758c5u, 0xb6b71f4fu, 0xb6b4cddfu, 0xb6b79b97u, 0xb6baa191u, + 0xb6b8f613u, 0xb6b696a5u, 0xb6b4c9fbu, 0xb6b8618eu, 0xb6b75f98u, 0xb6b66b86u, 0xb6b48feau, 0xb6b64a04u, + 0xb6b7cf0bu, 0xb6b3ea27u, 0xb6b2d260u, 0xb6b9d09du, 0xb6b21d92u, 0xb6b59e66u, 0xb6b00091u, 0xb6ba4dc1u, + 0xb6b78234u, 0xb6ad1553u, 0xb6b5995fu, 0xb6b212b6u, 0xb6af9f1eu, 0xb6a7ea04u, 0xb6bbb571u, 0xb6b5e386u, + 0xb6ad51dau, 0xb6b1328cu, 0xb6aff7f0u, 0xb6b38441u, 0xb6ae4a80u, 0xb6ae6823u, 0xb6b06af2u, 0xb6b42254u, + 0xb6b2174au, 0xb6af12c9u, 0xb6ab91e2u, 0xb6ad474au, 0xb6bac851u, 0xb6a804edu, 0xb6b188acu, 0xb6bace63u, + 0xb6b41b5au, 0xb6b8cb39u, 0xb6b48997u, 0xb6ac505fu, 0xb6bb29cdu, 0xb6a68f4cu, 0xb6b7eb92u, 0xb6ac270au, + 0xb6ad106au, 0xb6b7947cu, 0xb6afb39cu, 0xb6a8c304u, 0x369371f4u, 0xb989c1cfu, 0xb0a8e546u, 0xb6c65c18u, + 0xb6af34f2u, 0xb6af1985u, 0xb6adb5b9u, 0xb6ae529eu, 0xb6ae3563u, 0xb6ae2b77u, 0xb6af7bccu, 0xb6ab40e3u, + 0xb6b17628u, 0xb6b1048au, 0xb6b148f1u, 0xb6adf291u, 0xb6b0a283u, 0xb6b239cfu, 0xb6ad0995u, 0xb6b3ac4eu, + 0xb6b03e0eu, 0xb6b44081u, 0xb6b262a0u, 0xb6aaf1c1u, 0xb6ac6843u, 0xb6b0925bu, 0xb6aecc4bu, 0xb6aa23b9u, + 0xb6b3d80au, 0xb6b14248u, 0xb6b7b29cu, 0xb6aef46du, 0xb6b0f28du, 0xb6ad0f57u, 0xb6a64000u, 0xb6aab69bu, + 0xb6aef38du, 0xb6b012d2u, 0xb6af60f0u, 0xb69fb519u, 0xb6a55cb3u, 0xb6b69549u, 0xb6b55a2du, 0xb6b17847u, + 0xb6aab6f6u, 0xb6aee45bu, 0xb6b7d98fu, 0xb6aa5579u, 0xb6af2a84u, 0xb6af98c2u, 0xb6bcc198u, 0xb6b0d329u, + 0xb6b4d12du, 0xb6b1ecddu, 0xb6bfc7b9u, 0xb6b9990fu, 0xb6c18b0cu, 0xb6add2b1u, 0xb6b39bffu, 0xb6b57be2u, + 0xb6c48afdu, 0xb6b0403bu, 0xb6b89c50u, 0xb6ad18a6u, 0xb6ad0ea1u, 0xb90ce1fbu, 0x398666dbu, 0xb1c328c5u, + 0xb905d226u, 0xb6a60967u, 0xb6a60a6bu, 0xb6a419c3u, 0xb6a80a3bu, 0xb6a7f32bu, 0xb6a80d49u, 0xb6a7fcaau, + 0xb6a81c8bu, 0xb6a80ea1u, 0xb6aff716u, 0xb6a817f8u, 0xb6a03c09u, 0xb6a844f2u, 0xb6a7fa0du, 0xb6a81e18u, + 0xb69ff6c4u, 0xb6a0545du, 0xb6a00c26u, 0xb69fd3b6u, 0xb6c01049u, 0xb6afe0e5u, 0xb6a038ccu, 0xb69ff8deu, + 0xb68ffc0bu, 0xb6c001feu, 0xb69fede3u, 0xb6afdbefu, 0xb69ff92bu, 0xb6a05dbcu, 0xb6a02e65u, 0xb6bfdfe7u, + 0xb69f65bcu, 0xb6a042a7u, 0xb69fbc68u, 0xb69f8fbcu, 0xb69fd1a9u, 0xb69fd741u, 0xb6bf98f4u, 0xb6c040e4u, + 0xb6a012bbu, 0xb69fc137u, 0xb6bec223u, 0xb6c0093du, 0xb6a02ce3u, 0xb69fe8adu, 0xb69f84a9u, 0xb6c0349eu, + 0xb6a0cee2u, 0xb6c059e6u, 0xb6c02c5cu, 0xb6a0760du, 0xb6a096b8u, 0xb69feedcu, 0xb6a0752eu, 0xb6bf9a72u, + 0xb67ff518u, 0xb6814149u, 0xb6a050dbu, 0xb6e0354eu, 0xb680447fu, 0xb6a1060au, 0xb630b611u, 0xb9831dd0u, + 0x33000000u, 0x35200000u, 0xb6a00000u, 0xb6a00000u, 0xb6a00000u, 0xb6980000u, 0xb6a00000u, 0xb6a00000u, + 0xb6a00000u, 0xb6800000u, 0xb6a00000u, 0xb6a00000u, 0xb6900000u, 0xb6900000u, 0xb6900000u, 0xb6900000u, + 0xb6a00000u, 0xb6a00000u, 0xb6a00000u, 0xb6800000u, 0xb6a00000u, 0xb6800000u, 0xb6800000u, 0xb6800000u, + 0xb6800000u, 0xb6a00000u, 0xb6800000u, 0xb6a00000u, 0xb6a00000u, 0xb6a00000u, 0xb6a00000u, 0xb6a00000u, + 0xb6a00000u, 0xb7000000u, 0xb7000000u, 0xb7000000u, 0xb7000000u, 0xb6c00000u, 0xb7000000u, 0xb7000000u, + 0xb7000000u, 0xb7000000u, 0xb7000000u, 0xb6c00000u, 0xb6c00000u, 0xb7000000u, 0xb7000000u, 0xb6c00000u, + 0xb7000000u, 0xb6800000u, 0xb6c00000u, 0xb7000000u, 0xb6c00000u, 0xb6c00000u, 0xb6c00000u, 0xb6c00000u, + 0xb6c00000u, 0xb6800000u, 0xb7000000u, 0xb6c00000u, 0xb6800000u, 0xb6c00000u, 0xb6800000u, 0x39000000u, + 0x39800000u, 0xb234345fu, 0x38f51d94u, 0xb6942babu, 0xb6956519u, 0xb698acddu, 0xb695d5beu, 0xb6958d13u, + 0xb6957594u, 0xb6913352u, 0xb69ec9bdu, 0xb697b623u, 0xb69be932u, 0xb6975c3bu, 0xb69fcae5u, 0xb694ce1eu, + 0xb6957d87u, 0xb6969bdbu, 0xb69ebf26u, 0xb6a1bf37u, 0xb6ac1972u, 0xb69b3327u, 0xb68bba5bu, 0xb6ae2d69u, + 0xb695d778u, 0xb68839fdu, 0xb6a2dfa0u, 0xb6a58808u, 0xb6a01661u, 0xb6a2a6e4u, 0xb6a56695u, 0xb691ccfau, + 0xb6a2e0b7u, 0xb691e461u, 0xb6b40ca8u, 0xb68c3bb9u, 0xb6b9a842u, 0xb6857d68u, 0xb6ca52bbu, 0xb6965a26u, + 0xb68d6e01u, 0xb6ac08b6u, 0xb6be7cffu, 0xb6ac1e40u, 0xb6abd0b5u, 0xb68fba45u, 0xb657bf7eu, 0xb67c7c02u, + 0xb6cdbdabu, 0xb69a6f78u, 0xb6bdac30u, 0xb689390du, 0xb6575bf3u, 0xb6a53abdu, 0xb6ae9e7cu, 0xb6a9f876u, + 0xb696809cu, 0xb681a7b5u, 0xb6b1eb84u, 0xb69f3417u, 0xb6a0e61fu, 0xb68ccc68u, 0xb69f7e0fu, 0xb6c0234cu, + 0xb6efe3a8u, 0xb979cff2u, 0xb257fca4u, 0xb6a30797u, 0xb68f34feu, 0xb68f01d1u, 0xb68f1641u, 0xb68fcc0au, + 0xb68f5c89u, 0xb68ef6f9u, 0xb6926036u, 0xb6900eaeu, 0xb692137au, 0xb69176acu, 0xb68f2060u, 0xb68ff05au, + 0xb6935e05u, 0xb6927b79u, 0xb6929f99u, 0xb6926632u, 0xb6952fb8u, 0xb6946977u, 0xb6903ecau, 0xb694a8e9u, + 0xb691d944u, 0xb68e656eu, 0xb68f75feu, 0xb6901363u, 0xb6933148u, 0xb6916604u, 0xb691ac4eu, 0xb6967c83u, + 0xb692d38bu, 0xb6985dc9u, 0xb695d679u, 0xb697afd7u, 0xb6920becu, 0xb68fa660u, 0xb69ba3bfu, 0xb68f27fcu, + 0xb6982c5fu, 0xb6958051u, 0xb6a4d643u, 0xb6915ffdu, 0xb698f74au, 0xb68eee0eu, 0xb6983ee9u, 0xb69e1bf7u, + 0xb6929bfdu, 0xb6904c1eu, 0xb686e1b2u, 0xb693398cu, 0xb6916f51u, 0xb68965cau, 0xb696c7dbu, 0xb68ce5acu, + 0xb68c6f24u, 0xb68a4393u, 0xb699ab31u, 0xb69b1240u, 0xb6974a74u, 0xb695e656u, 0xb69832aeu, 0xb69d263cu, + 0xb69ba398u, 0xb8fe2df0u, 0x3973c7dbu, 0x3107f651u, 0xb8f2bd2du, 0xb688d671u, 0xb6888bf5u, 0xb689ec1bu, + 0xb6887faeu, 0xb687a7a5u, 0xb68932a1u, 0xb6882f7bu, 0xb689301fu, 0xb689dff7u, 0xb687dd69u, 0xb68ade59u, + 0xb68b28c8u, 0xb6894b7fu, 0xb68a8c92u, 0xb689e7deu, 0xb68accf0u, 0xb688fa62u, 0xb689cf2bu, 0xb6875ce1u, + 0xb68916d4u, 0xb68d9b29u, 0xb6838993u, 0xb685067eu, 0xb687229bu, 0xb687ee16u, 0xb68b34c0u, 0xb68aa2bbu, + 0xb68b664du, 0xb692e8e0u, 0xb68c012eu, 0xb68ea488u, 0xb6917a79u, 0xb6868bb2u, 0xb67a923fu, 0xb6943286u, + 0xb6874a9fu, 0xb6874801u, 0xb68ef126u, 0xb68209efu, 0xb67b0f35u, 0xb6862548u, 0xb6916f6au, 0xb6828251u, + 0xb68d98dcu, 0xb6928bf7u, 0xb689ade3u, 0xb6813e8du, 0xb68235a8u, 0xb6846d0bu, 0xb68fabdeu, 0xb68da817u, + 0xb67fdfd2u, 0xb693f4f5u, 0xb69b3323u, 0xb6841cbbu, 0xb6646bf1u, 0xb68488ceu, 0xb68a51fau, 0xb69002dau, + 0xb685f390u, 0xb689f58eu, 0x371fc73au, 0xb96d66f6u, 0xb15c0461u, 0x3512ec3fu, 0xb6822ea7u, 0xb682c4e5u, + 0xb6835a5eu, 0xb6815e39u, 0xb681215bu, 0xb680618fu, 0xb680ad4au, 0xb6831189u, 0xb683c434u, 0xb6806971u, + 0xb682ca34u, 0xb6800144u, 0xb67e1acfu, 0xb67a0937u, 0xb6843d48u, 0xb6849d48u, 0xb6794889u, 0xb6857340u, + 0xb6831eaau, 0xb67c5b60u, 0xb680ecdfu, 0xb684993du, 0xb6855eebu, 0xb682fdecu, 0xb6757d95u, 0xb67c799du, + 0xb6729318u, 0xb6751799u, 0xb6715aa1u, 0xb66cb4ebu, 0xb65e77bdu, 0xb6545f66u, 0xb6481fffu, 0xb63817dcu, + 0xb64cd9aau, 0xb65c5175u, 0xb650c78cu, 0xb65331f6u, 0xb6599ec8u, 0xb662d9f0u, 0xb64dbae1u, 0xb6751526u, + 0xb65a3463u, 0xb671c6e5u, 0xb63e5108u, 0xb6444248u, 0xb6311ed1u, 0xb6548db5u, 0xb6539f19u, 0xb66c3daau, + 0xb65caa8au, 0xb66a8d1bu, 0xb63d4948u, 0xb65c4beeu, 0xb67a500cu, 0xb65f7e5du, 0xb655bd54u, 0xb6465f0du, + 0xb65c03e2u, 0xb66d8ea3u, 0xb64492c1u, 0x38e94f8cu, 0x39687ffau, 0x3232eba3u, 0x38e033f1u, 0xb676a27bu, + 0xb6781094u, 0xb679cda3u, 0xb6759355u, 0xb6754c31u, 0xb675b79fu, 0xb6756888u, 0xb677accdu, 0xb67705b2u, + 0xb678fb97u, 0xb674290au, 0xb6742e45u, 0xb66ec819u, 0xb66e6860u, 0xb6749be0u, 0xb673fa41u, 0xb66db4abu, + 0xb6774e2fu, 0xb66a01b1u, 0xb67adb04u, 0xb6714453u, 0xb67001a4u, 0xb67879ecu, 0xb67c7909u, 0xb66a5d66u, + 0xb6724776u, 0xb66ff898u, 0xb66e4f4fu, 0xb6666316u, 0xb67cf501u, 0xb678744du, 0xb67c3a7fu, 0xb6713a6au, + 0xb672276au, 0xb66b5a7bu, 0xb66882e1u, 0xb66a6422u, 0xb6675f05u, 0xb6698233u, 0xb673de67u, 0xb665b53cu, + 0xb6755b03u, 0xb66335d2u, 0xb66a8293u, 0xb66ac7e2u, 0xb67a206fu, 0xb66fdfdfu, 0xb668a3b3u, 0xb65c229cu, + 0xb6506cb2u, 0xb6526ac3u, 0xb679fa2bu, 0xb65b2f8cu, 0xb65f57feu, 0xb6612f8cu, 0xb66b2ee7u, 0xb65099d8u, + 0xb685165cu, 0xb66b3623u, 0xb682b87fu, 0xb64e5851u, 0xb7944947u, 0xb9618b50u, 0xb1500edcu, 0xb6870c9du, + 0xb66e8061u, 0xb66dbd80u, 0xb66dbf78u, 0xb66cf67bu, 0xb6701cf5u, 0xb6718664u, 0xb66cebf0u, 0xb66ffeabu, + 0xb66fb676u, 0xb66b0745u, 0xb66660c2u, 0xb66e78bcu, 0xb66d40a4u, 0xb66b8186u, 0xb66f36a5u, 0xb671f346u, + 0xb662651fu, 0xb66d1642u, 0xb66a17efu, 0xb665ee94u, 0xb66c2d80u, 0xb6659eb7u, 0xb66a2414u, 0xb66fcc92u, + 0xb66b3967u, 0xb66bd98du, 0xb66b6606u, 0xb66a5f3fu, 0xb659a28du, 0xb66a861bu, 0xb66554eau, 0xb66e43f9u, + 0xb670828cu, 0xb668926au, 0xb67af9e7u, 0xb6773fb5u, 0xb667a204u, 0xb66c5f59u, 0xb6633938u, 0xb65ea921u, + 0xb65ab0e3u, 0xb6734d4du, 0xb672383cu, 0xb65768bdu, 0xb66164cdu, 0xb665dc5cu, 0xb66825b2u, 0xb663181bu, + 0xb67aace7u, 0xb668fc22u, 0xb66fe32eu, 0xb67cc2ffu, 0xb664e4dfu, 0xb678763fu, 0xb6796d15u, 0xb66dfd8fu, + 0xb6539069u, 0xb6659477u, 0xb660a584u, 0xb671f3aeu, 0xb65c31f7u, 0xb8e4bf60u, 0x395cefc3u, 0xb2156c98u, + 0xb8de3247u, 0xb665d99du, 0xb668499fu, 0xb666fe74u, 0xb665f3e1u, 0xb667e671u, 0xb66a524cu, 0xb66aeda5u, + 0xb66be2c9u, 0xb66c7e50u, 0xb6693371u, 0xb66c5ae2u, 0xb6768409u, 0xb671c88au, 0xb675e9b2u, 0xb66b68beu, + 0xb663e8ddu, 0xb67b285cu, 0xb67d1f2au, 0xb66f9459u, 0xb66fc427u, 0xb66dd23eu, 0xb676e3a8u, 0xb67fdf6au, + 0xb6755c62u, 0xb68631f5u, 0xb67c83e4u, 0xb671010du, 0xb66ef543u, 0xb6806b3eu, 0xb6825709u, 0xb679e263u, + 0xb6703567u, 0xb67c8450u, 0xb68574efu, 0xb686b3d0u, 0xb6811955u, 0xb6805c80u, 0xb68614efu, 0xb6810295u, + 0xb668062du, 0xb66f5aaeu, 0xb67e58d3u, 0xb699cb46u, 0xb66fea88u, 0xb680a973u, 0xb68383a2u, 0xb680e2eau, + 0xb68117e5u, 0xb68f9571u, 0xb6852dbbu, 0xb6877a2au, 0xb6903619u, 0xb67d50dfu, 0xb6a0643cu, 0xb687e43fu, + 0xb6a4b85du, 0xb6741856u, 0xb68347d4u, 0xb68ca278u, 0xb68e29e3u, 0xb682fc4du, 0x3795a8c4u, 0xb9583395u, + 0x31a9cf58u, 0x34e3e99eu, 0xb657bc7bu, 0xb6585247u, 0xb6565762u, 0xb654456au, 0xb651e0e4u, 0xb6539812u, + 0xb657ceb4u, 0xb65188c1u, 0xb65c748du, 0xb64e261fu, 0xb6431a43u, 0xb651fca1u, 0xb6623aa3u, 0xb65c031bu, + 0xb6475b8cu, 0xb6690849u, 0xb6639c65u, 0xb65ba2e3u, 0xb653c6c8u, 0xb64c877bu, 0xb6460cb9u, 0xb63ed0c4u, + 0xb6501622u, 0xb6436752u, 0xb653a937u, 0xb647b2e0u, 0xb6497874u, 0xb63b9a33u, 0xb6410ae9u, 0xb6452f2eu, + 0xb6453558u, 0xb647bfccu, 0xb64d68f7u, 0xb628398du, 0xb64da1ebu, 0xb6312dbeu, 0xb64ee5dau, 0xb632d731u, + 0xb6033551u, 0xb633c0a0u, 0xb654aa88u, 0xb5e915dcu, 0xb645c79bu, 0xb63c6d42u, 0xb62c0603u, 0xb6463b1du, + 0xb651a9d7u, 0xb629f3f9u, 0xb63cc3deu, 0xb648a788u, 0xb63916bdu, 0xb6319f0eu, 0xb6534723u, 0xb61cc708u, + 0xb60c38dfu, 0xb61bdd2fu, 0xb60d3e61u, 0xb62c4272u, 0xb604326au, 0xb602455du, 0xb6175ad4u, 0x38d46df7u, + 0x3951de70u, 0xaed6df3du, 0x38ceb8d5u, 0xb64eaa79u, 0xb6507727u, 0xb64f1cf0u, 0xb64b91afu, 0xb64c1c08u, + 0xb64cafc4u, 0xb649154eu, 0xb649f6f8u, 0xb6497f4cu, 0xb64c7b12u, 0xb649799du, 0xb64ce65bu, 0xb64d7c7fu, + 0xb64b97c7u, 0xb652e31fu, 0xb647d136u, 0xb6383e51u, 0xb64bb41fu, 0xb645d5d1u, 0xb6481523u, 0xb654e0f9u, + 0xb6550e32u, 0xb63d89aeu, 0xb643a105u, 0xb6462142u, 0xb63695beu, 0xb64bae7au, 0xb640d281u, 0xb6346342u, + 0xb6524b40u, 0xb645b2e7u, 0xb63ebeefu, 0xb647dcb2u, 0xb6478b64u, 0xb6385a1eu, 0xb63cf5eau, 0xb63803cau, + 0xb62e0962u, 0xb638d81eu, 0xb646ac56u, 0xb64c7ccbu, 0xb64c6496u, 0xb64b8654u, 0xb628b41du, 0xb62eca1au, + 0xb6318153u, 0xb6517fb5u, 0xb6409e6au, 0xb6466e52u, 0xb631d22du, 0xb64977acu, 0xb6361c07u, 0xb638cf21u, + 0xb6102557u, 0xb628e070u, 0xb642f9a5u, 0xb63fe748u, 0xb665bbeeu, 0xb6543f2cu, 0xb634a8b2u, 0xb63802dfu, + 0xb7d9179eu, 0xb94c42f8u, 0xb28cbdcau, 0xb6646661u, 0xb64a8742u, 0xb64bc25au, 0xb64ea918u, 0xb64ca9c2u, + 0xb6525334u, 0xb650e40eu, 0xb6530ed3u, 0xb653d7cdu, 0xb65c8d56u, 0xb6543ea4u, 0xb65f5845u, 0xb65bf8e2u, + 0xb65e3103u, 0xb65e769bu, 0xb65bcf9bu, 0xb65f26d4u, 0xb65d47e3u, 0xb6587298u, 0xb6611b8au, 0xb66e2111u, + 0xb66b4b27u, 0xb6640bd0u, 0xb6732b6au, 0xb66ae19bu, 0xb66d8ddfu, 0xb66ff081u, 0xb66d5880u, 0xb6678b95u, + 0xb6827067u, 0xb6783482u, 0xb66fb8eau, 0xb68484a6u, 0xb67e47cbu, 0xb6730567u, 0xb67a4000u, 0xb67e245eu, + 0xb680bf51u, 0xb689195bu, 0xb681617fu, 0xb68ca968u, 0xb689569fu, 0xb68bc640u, 0xb6870670u, 0xb6867cb9u, + 0xb685cf11u, 0xb68f9a8cu, 0xb68a152bu, 0xb67d3a65u, 0xb6874815u, 0xb68bd545u, 0xb6878c74u, 0xb689717du, + 0xb690bf93u, 0xb68b7f10u, 0xb6782104u, 0xb6992eb6u, 0xb68d02c0u, 0xb69834ebu, 0xb6941b44u, 0xb6952ea3u, + 0xb68aca27u, 0xb8d201d9u, 0x39464717u, 0xb1ac451du, 0xb8cd19d5u, 0xb63e9d45u, 0xb63e3785u, 0xb6401454u, + 0xb63e2553u, 0xb63e1295u, 0xb63d0215u, 0xb63a89d6u, 0xb63c8522u, 0xb63ffdecu, 0xb6412196u, 0xb63bcc97u, + 0xb642438bu, 0xb641850bu, 0xb6407d9fu, 0xb63fd448u, 0xb64074e1u, 0xb639f96eu, 0xb63213e4u, 0xb62d9e54u, + 0xb646ab74u, 0xb6419796u, 0xb6388238u, 0xb632ff7fu, 0xb63f99e5u, 0xb64723bcu, 0xb63e4162u, 0xb64ef4f9u, + 0xb63a05acu, 0xb6389644u, 0xb6402a8du, 0xb6434efcu, 0xb63a4568u, 0xb6383bdfu, 0xb632ad32u, 0xb655be95u, + 0xb63ecc30u, 0xb626cc0du, 0xb64ae6a1u, 0xb664f762u, 0xb63119f3u, 0xb643e825u, 0xb653e310u, 0xb652f8f4u, + 0xb6412b6fu, 0xb62f92c1u, 0xb635ece2u, 0xb64cb539u, 0xb6391f32u, 0xb6339bbbu, 0xb631605au, 0xb63a9b06u, + 0xb6631cb6u, 0xb662c79du, 0xb623fbc0u, 0xb652522du, 0xb6442bfcu, 0xb64b049bu, 0xb63744eau, 0xb618b7bau, + 0xb6390156u, 0xb63a81b3u, 0x37e185e5u, 0xb942bc46u, 0x3210cebfu, 0x34d28fd6u, 0xb6320bbfu, 0xb6339fa6u, + 0xb6328326u, 0xb630be33u, 0xb62f5130u, 0xb62f990au, 0xb631719eu, 0xb6345f5au, 0xb63861d4u, 0xb6379852u, + 0xb63952a9u, 0xb62a66dcu, 0xb62c43d9u, 0xb62f2126u, 0xb641bc87u, 0xb623197au, 0xb62e8dacu, 0xb62b4d70u, + 0xb625b122u, 0xb6356b02u, 0xb61d958fu, 0xb620c059u, 0xb61bece8u, 0xb61a5c9au, 0xb6207d85u, 0xb60e2aefu, + 0xb60d6d9au, 0xb618a326u, 0xb6094ba1u, 0xb62a2f51u, 0xb5ed8e42u, 0xb5f730e0u, 0xb5bfad01u, 0xb605b931u, + 0xb605427cu, 0xb604fef9u, 0xb5f81cd4u, 0xb60bf2b0u, 0xb602ebb2u, 0xb60da220u, 0xb6113f94u, 0xb61f7385u, + 0xb616c219u, 0xb5db3eabu, 0xb60a6d20u, 0xb5cf921eu, 0xb608deebu, 0xb5a0b1a0u, 0xb5fcb612u, 0xb5d96009u, + 0xb5d5ae50u, 0xb5fb66f2u, 0xb5edd91du, 0xb5d0a070u, 0xb5f599afu, 0xb5e516aau, 0xb6118be9u, 0xb611581fu, + 0xb61d0a3eu, 0xb62a6d0eu, 0xb6255f01u, 0x38bfa892u, 0x393f2607u, 0x3190430eu, 0x38c02b4fu, 0xb62d5715u, + 0xb62ddbdbu, 0xb62d2cbeu, 0xb62ae550u, 0xb62d127cu, 0xb62bf246u, 0xb6324f58u, 0xb629d4b1u, 0xb631fd7du, + 0xb627dbd9u, 0xb632405au, 0xb62d2bfau, 0xb630fbf4u, 0xb62ad6b8u, 0xb62cf91du, 0xb62caf42u, 0xb62dede8u, + 0xb62f2e63u, 0xb62a7931u, 0xb623a57fu, 0xb62bf7e2u, 0xb62ba2a6u, 0xb6214f42u, 0xb632e862u, 0xb6327145u, + 0xb62522edu, 0xb62d2b79u, 0xb632d3e2u, 0xb6308f2eu, 0xb6287a67u, 0xb6322bf0u, 0xb639e329u, 0xb62e1d9eu, + 0xb635f9c3u, 0xb6347f5eu, 0xb6286a30u, 0xb632fbddu, 0xb633e595u, 0xb651362bu, 0xb64d2529u, 0xb62ef192u, + 0xb62c511du, 0xb627e5d5u, 0xb635f841u, 0xb62b7663u, 0xb631a1c6u, 0xb636abc7u, 0xb6239d09u, 0xb6316ebbu, + 0xb6454e64u, 0xb61a7edcu, 0xb64884c8u, 0xb63c20d4u, 0xb63dd8c7u, 0xb649d988u, 0xb644c953u, 0xb626d20bu, + 0xb630a5d8u, 0xb619aeacu, 0xb640b441u, 0xb64c9e89u, 0xb8096217u, 0xb9393afbu, 0x31eba991u, 0xb63c1f4bu, + 0xb624ea1bu, 0xb624e74bu, 0xb6239465u, 0xb621e1f5u, 0xb621f10fu, 0xb621f827u, 0xb61fc3d3u, 0xb624f41au, + 0xb61f089eu, 0xb622d0a6u, 0xb6213614u, 0xb61a504au, 0xb61d581du, 0xb61f6a3cu, 0xb61d35fbu, 0xb61a8532u, + 0xb61e5040u, 0xb618a861u, 0xb611ddb0u, 0xb61e993fu, 0xb617adacu, 0xb61a4a55u, 0xb6175ec5u, 0xb61c284eu, + 0xb6141ff1u, 0xb619c197u, 0xb616f96du, 0xb62652cau, 0xb617240cu, 0xb60be495u, 0xb61bbd88u, 0xb617b442u, + 0xb607a54eu, 0xb61b3c86u, 0xb60ae5bau, 0xb60d4014u, 0xb617600eu, 0xb612274fu, 0xb5eb211au, 0xb5f6be2du, + 0xb60e8d7du, 0xb6086281u, 0xb5fbbf1du, 0xb60c74b1u, 0xb614eb8eu, 0xb5dd1769u, 0xb6064cfcu, 0xb5e33276u, + 0xb5fdddb7u, 0xb6208df6u, 0xb5daf918u, 0xb6050b98u, 0xb603fc52u, 0xb60aa724u, 0xb5df92bcu, 0xb5f17d3au, + 0xb622012cu, 0xb60aa3f9u, 0xb5ed3ef3u, 0xb5e5603cu, 0xb5f3bbacu, 0xb8b8f74du, 0x3934fe61u, 0x3108edddu, + 0xb8bebfdau, 0xb61ce4edu, 0xb61c7e9eu, 0xb61aed4eu, 0xb6194fbbu, 0xb61a4124u, 0xb61c277bu, 0xb620c397u, + 0xb617d948u, 0xb61b50bau, 0xb616945au, 0xb624b3f7u, 0xb610204au, 0xb60d99c2u, 0xb60e08b4u, 0xb622f670u, + 0xb5fb489au, 0xb61189f3u, 0xb61a8fe4u, 0xb630f43cu, 0xb609f09bu, 0xb62bfc62u, 0xb5ffcefeu, 0xb616db0bu, + 0xb6251145u, 0xb6022c0fu, 0xb6054f5eu, 0xb60968c3u, 0xb60a2f3cu, 0xb5d386a6u, 0xb6094529u, 0xb5e36200u, + 0xb62aa2dcu, 0xb5fb9a9bu, 0xb5cc6acbu, 0xb6066556u, 0xb5f50209u, 0xb60f77c6u, 0xb5b90305u, 0xb622cd26u, + 0xb60b7ca4u, 0xb61024a7u, 0xb5c47f11u, 0xb5ea2334u, 0xb606a5e2u, 0xb603e76fu, 0xb5ef45f0u, 0xb61feb0cu, + 0xb5f649fcu, 0xb603cc2cu, 0xb5d34d99u, 0xb62ad28du, 0xb61fb2f5u, 0xb602b466u, 0xb5ebed26u, 0xb5c17e6cu, + 0xb5bc15b8u, 0xb61fcc17u, 0xb5c24922u, 0xb5e46cdbu, 0xb5bc11f7u, 0xb60d81dbu, 0x380bc911u, 0xb92fb43du, + 0xb2270f21u, 0x34930bd8u, 0xb61a5db1u, 0xb6229dc7u, 0xb61f5abbu, 0xb61bdfcbu, 0xb62119b6u, 0xb61e09f9u, + 0xb624901bu, 0xb622459fu, 0xb627767au, 0xb6244226u, 0xb629a9bau, 0xb62afd84u, 0xb62f51e2u, 0xb6240192u, + 0xb61579f0u, 0xb6144228u, 0xb61579f0u, 0xb5feb4f3u, 0xb608f1d7u, 0xb60b9969u, 0xb60cd133u, 0xb5e9f199u, + 0xb61cd133u, 0xb6344226u, 0xb60b9969u, 0xb63929d6u, 0xb63929d6u, 0xb63a61a0u, 0xb64f494fu, 0xb62a61a0u, + 0xb641d292u, 0xb61f6081u, 0xb636011du, 0xb656011cu, 0xb636011du, 0xb642400du, 0xb64ca1bcu, 0xb64062c8u, + 0xb6770368u, 0xb61f6081u, 0xb636011du, 0xb642400du, 0xb642400du, 0xb681a12eu, 0xb66ca1bcu, 0xb6806964u, + 0xb64ca1bcu, 0xb6767118u, 0xb66f814du, 0xb66ca1bcu, 0xb658e0adu, 0xb660d2c7u, 0xb665b1f0u, 0xb6851914u, + 0xb69310f6u, 0xb67fd079u, 0xb616011du, 0xb6906964u, 0xb68b388cu, 0xb6851914u, 0xb69688dcu, 0x38aaba4bu, + 0x3929aeccu, 0xb04356bfu, 0x38b3d6a9u, 0xb61279b0u, 0xb614611eu, 0xb611903au, 0xb619c4a0u, 0xb6122e66u, + 0xb611e823u, 0xb60a0e09u, 0xb60c64d1u, 0xb617afc8u, 0xb60eb542u, 0xb60fab37u, 0xb61207cbu, 0xb5fcf9ecu, + 0xb60cc2a2u, 0xb60fd1dfu, 0xb607e9bdu, 0xb60f294fu, 0xb6096703u, 0xb6223145u, 0xb61ac9cbu, 0xb6068e66u, + 0xb6256412u, 0xb618df6du, 0xb6057c89u, 0xb610179cu, 0xb631c13du, 0xb6188840u, 0xb60ff2ccu, 0xb61ccd84u, + 0xb60ad28au, 0xb634dd3au, 0xb61ee135u, 0xb632600cu, 0xb61b76dau, 0xb60a5b8cu, 0xb60d4bddu, 0xb5fdd86eu, + 0xb61df57du, 0xb60903e9u, 0xb5f80e1du, 0xb60ccf57u, 0xb5d2ad28u, 0xb5f4b228u, 0xb621bbbbu, 0xb5fbe5a7u, + 0xb6212628u, 0xb608c7e9u, 0xb6131befu, 0xb60a8a89u, 0xb61bb632u, 0xb5c2566du, 0xb60777c8u, 0xb5c39e74u, + 0xb6050a85u, 0xb5d6467au, 0xb624714du, 0xb60a933du, 0xb5bf834au, 0xb644fc8du, 0xb628461fu, 0xb60174c3u, + 0xb822974du, 0xb9262105u, 0x31c4dd59u, 0xb61fe2beu, 0xb60bced1u, 0xb60cb8fau, 0xb60a64acu, 0xb60c0527u, + 0xb60c0983u, 0xb60e12adu, 0xb60fa806u, 0xb60de69au, 0xb60b93cbu, 0xb608b3f0u, 0xb60f349au, 0xb60a9de0u, + 0xb6100483u, 0xb6124149u, 0xb61b47beu, 0xb612a8fbu, 0xb6105ae2u, 0xb618970eu, 0xb60c5c43u, 0xb618752au, + 0xb611ae77u, 0xb60d1322u, 0xb60eb233u, 0xb60fa0a9u, 0xb6151359u, 0xb622b14cu, 0xb6136c89u, 0xb6162142u, + 0xb615fa8fu, 0xb618b3c0u, 0xb607819du, 0xb60b4b26u, 0xb60d83d3u, 0xb60a2ec7u, 0xb6053bf8u, 0xb60d263cu, + 0xb618596bu, 0xb611245bu, 0xb620334eu, 0xb60d6c97u, 0xb6212c6cu, 0xb60ee314u, 0xb610b0e4u, 0xb609a30fu, + 0xb61a2747u, 0xb61a68deu, 0xb60852b4u, 0xb61d158fu, 0xb6363313u, 0xb60a42edu, 0xb616c5b2u, 0xb609199eu, + 0xb62007d1u, 0xb5e3625fu, 0xb6095199u, 0xb60cfdd8u, 0xb60f7fb2u, 0xb628f751u, 0xb61eb19fu, 0xb5f91ce4u, + 0xb6040f2cu, 0xb8a75768u, 0x39222595u, 0x31681380u, 0xb8b2ada4u, 0xb607518bu, 0xb6082f22u, 0xb607386du, + 0xb609697fu, 0xb6032f55u, 0xb6094ae9u, 0xb6066d81u, 0xb607b10eu, 0xb607f58fu, 0xb605187cu, 0xb605e061u, + 0xb608499du, 0xb608dc1fu, 0xb6077ecbu, 0xb60b3346u, 0xb606e46fu, 0xb60a3d8cu, 0xb605eecdu, 0xb60cf0dfu, + 0xb6021afau, 0xb605edfau, 0xb5f6ae65u, 0xb6052013u, 0xb609efabu, 0xb60af903u, 0xb60376e4u, 0xb61155f3u, + 0xb602e801u, 0xb601dcd7u, 0xb615eee7u, 0xb6079c3fu, 0xb613c16du, 0xb610ba23u, 0xb61b356bu, 0xb602afecu, + 0xb60426d4u, 0xb60e5466u, 0xb5e9ee33u, 0xb5fa8d62u, 0xb5ffc411u, 0xb618637fu, 0xb5f34e9eu, 0xb5fac430u, + 0xb60d2cddu, 0xb5ffa656u, 0xb61d9423u, 0xb6190f3cu, 0xb60c775fu, 0xb6216266u, 0xb61b0399u, 0xb5fa7b03u, + 0xb600a782u, 0xb601f158u, 0xb60e402cu, 0xb5dbad89u, 0xb6020fcbu, 0xb6251cf1u, 0xb6112e92u, 0xb6121953u, + 0xb639138fu, 0xb6247eeau, 0x38233c29u, 0xb91daf73u, 0x31a2d9ccu, 0x348a25d3u, 0xb60065ffu, 0xb6010524u, + 0xb5fbee40u, 0xb6014078u, 0xb602e12eu, 0xb601a482u, 0xb5fe5f53u, 0xb5fad2ddu, 0xb5f93ab3u, 0xb5fa4c47u, + 0xb601ae3au, 0xb5f91de8u, 0xb5ea033fu, 0xb5f70341u, 0xb5f4c59eu, 0xb60b24e6u, 0xb6071bbcu, 0xb5f96e7fu, + 0xb60c4ae4u, 0xb5fe87bfu, 0xb5eb7088u, 0xb5e83e73u, 0xb5d6d079u, 0xb5d0294au, 0xb5d5f6f3u, 0xb5ffee3au, + 0xb5dee5e9u, 0xb5f218c7u, 0xb5f27c79u, 0xb5d39548u, 0xb5ddc92fu, 0xb5ce3177u, 0xb5e08ee9u, 0xb5f825dau, + 0xb5d48070u, 0xb5df5b1eu, 0xb5c97b9cu, 0xb5e53636u, 0xb5ba2bf2u, 0xb5c5f904u, 0xb5ffc8c6u, 0xb5e2f0efu, + 0xb5fcb7bcu, 0xb5d41de6u, 0xb5f48ca3u, 0xb59ef0c9u, 0xb6119202u, 0xb5bf19beu, 0xb5c94475u, 0xb5af9222u, + 0xb5bc799cu, 0xb5fa86c4u, 0xb5fa7478u, 0xb5beb6bdu, 0xb5d2f0bcu, 0xb5e42b97u, 0xb60ce208u, 0xb5b2e0a5u, + 0xb5cbc20bu, 0xb5bce205u, 0xb5bbb8c7u, 0x3899fde5u, 0x391a0242u, 0xb1508944u, 0x38a98ab7u, 0xb5f6fba5u, + 0xb5f5653eu, 0xb5f61709u, 0xb5f8da63u, 0xb5f3c40eu, 0xb5fdb3dau, 0xb5f6a14bu, 0xb5fc3f83u, 0xb5ef742cu, + 0xb5f7b77au, 0xb5fa6a50u, 0xb5fb8932u, 0xb5fa0839u, 0xb5f8fab0u, 0xb60217b3u, 0xb5f3aec4u, 0xb5ed71a2u, + 0xb5f44a5bu, 0xb5f50b5du, 0xb5f26597u, 0xb5f23bf1u, 0xb5f736efu, 0xb5e7cdd7u, 0xb5fb2d34u, 0xb606eed0u, + 0xb60b8feeu, 0xb5e8ebd3u, 0xb607a955u, 0xb5f002edu, 0xb5d7875eu, 0xb6017274u, 0xb6038674u, 0xb6010d4du, + 0xb5e233a5u, 0xb5fb3699u, 0xb5fe5eecu, 0xb60e0fe2u, 0xb5deca09u, 0xb60297a2u, 0xb5f7aac8u, 0xb609e9d4u, + 0xb60ae435u, 0xb5f712f0u, 0xb6078315u, 0xb60337e8u, 0xb5fe2ec9u, 0xb5fd8f51u, 0xb5fd808fu, 0xb60b12dcu, + 0xb5ee7d86u, 0xb62132bcu, 0xb5fb7647u, 0xb62b168eu, 0xb5ad6930u, 0xb5f94ae4u, 0xb5ced20au, 0xb5cf7b0cu, + 0xb5cf2bc5u, 0xb614a8afu, 0xb5bdca3fu, 0xb5dbe9ddu, 0xb8366970u, 0xb915bbc6u, 0xb208d00fu, 0xb6069323u, + 0xb5ed8a19u, 0xb5f1fe47u, 0xb5f12c69u, 0xb5ef9263u, 0xb5f12505u, 0xb5f0c43eu, 0xb5f36c86u, 0xb5fe7552u, + 0xb5f4279eu, 0xb5ec9ac3u, 0xb600aaa2u, 0xb5e505c0u, 0xb5eee875u, 0xb5e9b6bfu, 0xb5f6e963u, 0xb5f8085du, + 0xb5f083fcu, 0xb5f3ef8bu, 0xb600fc60u, 0xb600f197u, 0xb5ff70bdu, 0xb5eef128u, 0xb5e7861cu, 0xb60822bau, + 0xb5fc70e6u, 0xb6010d4eu, 0xb6082f64u, 0xb60fcd3eu, 0xb5fafe6cu, 0xb606751eu, 0xb607dea4u, 0xb6114112u, + 0xb6011546u, 0xb6075f88u, 0xb5eacae3u, 0xb60ff25au, 0xb619e227u, 0xb5d9efcau, 0xb6114a76u, 0xb608ca8du, + 0xb60393c7u, 0xb601a752u, 0xb600c5bau, 0xb619efe4u, 0xb5f2f2d2u, 0xb5f13824u, 0xb618625du, 0xb5f75b4du, + 0xb5e8bcedu, 0xb60164b9u, 0xb614fe44u, 0xb603c13du, 0xb60dbfa2u, 0xb60cd3abu, 0xb61570cdu, 0xb60fde62u, + 0xb618216au, 0xb6148f5fu, 0xb607fbe6u, 0xb61cd0d8u, 0xb60b6899u, 0xb8961ae0u, 0x391063dbu, 0xb0ce203cu, + 0xb8a888a9u, 0xb5e347c6u, 0xb5e2ecd5u, 0xb5e12a31u, 0xb5e115a2u, 0xb5dc6583u, 0xb5e12b5cu, 0xb5eb01b2u, + 0xb5e3faf4u, 0xb5e45ac1u, 0xb5e38a41u, 0xb5e79526u, 0xb5ee5981u, 0xb5db1fabu, 0xb5d8b2a7u, 0xb5f7c382u, + 0xb5edfe9cu, 0xb5d55195u, 0xb5ed2773u, 0xb5c03508u, 0xb60b087fu, 0xb6015bf6u, 0xb5df2ef6u, 0xb5d51f18u, + 0xb5fcdc48u, 0xb5ea717bu, 0xb5f945f3u, 0xb5d18a62u, 0xb5e7fbafu, 0xb5e127d3u, 0xb5dda060u, 0xb5f9d3d3u, + 0xb608f27fu, 0xb5bea3dbu, 0xb5c7e278u, 0xb5f46c4au, 0xb5b06b8du, 0xb5b701c7u, 0xb6091b21u, 0xb5cf468bu, + 0xb5f97865u, 0xb5f8052eu, 0xb6000173u, 0xb5e2859bu, 0xb5d45cebu, 0xb60c64c0u, 0xb612dd07u, 0xb5cde756u, + 0xb5dd2320u, 0xb5c4b98au, 0xb5f918afu, 0xb61c83b2u, 0xb5ef7a7cu, 0xb5ff4248u, 0xb5d1c8dfu, 0xb5cee20du, + 0xb611c025u, 0xb60a0331u, 0xb620b37eu, 0xb6012c97u, 0xb5ce39c7u, 0xb5c43934u, 0x38375889u, 0xb90d36c9u, + 0x32465439u, 0x347a25fcu, 0xb5d96c93u, 0xb5d9a81bu, 0xb5d4d6a0u, 0xb5d13c21u, 0xb5d4cf6du, 0xb5de63d8u, + 0xb5cd94a4u, 0xb5ced5ecu, 0xb5e21f13u, 0xb5bedeecu, 0xb5ead71au, 0xb5e24450u, 0xb5d8e21au, 0xb5d159d7u, + 0xb5d8ad8eu, 0xb5be2246u, 0xb5e6d76eu, 0xb5dcd68au, 0xb5e315f1u, 0xb5dc87efu, 0xb5cced48u, 0xb5de0dcfu, + 0xb5dc3933u, 0xb5bdb8a9u, 0xb5dddfd4u, 0xb5f0c70au, 0xb5ebe195u, 0xb5ec938bu, 0xb5ce737bu, 0xb5e871a0u, + 0xb5faa345u, 0xb5d2879eu, 0xb59f9031u, 0xb5459277u, 0xb583cc90u, 0xb6114884u, 0xb5d5d40cu, 0xb5c13674u, + 0xb5eeddcdu, 0xb5e80fddu, 0xb5c83827u, 0xb5df9591u, 0xb5eac96cu, 0xb53413a2u, 0xb55adf2bu, 0xb5c35ee0u, + 0xb598e140u, 0xb5e09781u, 0xb5e96c06u, 0xb5f95ff4u, 0xb5ff85bfu, 0xb5d4f5bau, 0xb5d9ce3eu, 0xb5cbe708u, + 0xb5f0393bu, 0xb5a50e10u, 0xb599ae5fu, 0xb5d9bc35u, 0xb5d0ae90u, 0xb5ca5a8du, 0xb5c86a16u, 0x388985ffu, + 0x39084835u, 0x31f24a29u, 0x38a0d925u, 0xb5cb958fu, 0xb5ce8cffu, 0xb5cea3a4u, 0xb5c4e30cu, 0xb5ccabe3u, + 0xb5da53bfu, 0xb5cfbae0u, 0xb5cf45ccu, 0xb5c44c14u, 0xb5d196f8u, 0xb5c6c9f5u, 0xb5d9c0dcu, 0xb5c04721u, + 0xb5ce467du, 0xb5bd4a38u, 0xb5befcb3u, 0xb5c58fbbu, 0xb5c10bd2u, 0xb5bb34d9u, 0xb5b7610fu, 0xb5d9011eu, + 0xb5d3fda8u, 0xb5be5ff0u, 0xb5be5914u, 0xb5df1e9du, 0xb5c04d5cu, 0xb5a90578u, 0xb5c5d86au, 0xb5e38dabu, + 0xb5b57c2cu, 0xb5b6d6b0u, 0xb5c3fd65u, 0xb5d8c511u, 0xb6020478u, 0xb5cc261du, 0xb5cf06d0u, 0xb5e3368fu, + 0xb5933ef5u, 0xb5b09e93u, 0xb5ad3660u, 0xb5d1d35cu, 0xb5b03330u, 0xb5bd5fa5u, 0xb5d84c27u, 0xb5dc8141u, + 0xb5ac0197u, 0xb5b58f96u, 0xb5b41d63u, 0xb5a63bedu, 0xb5a6ce84u, 0xb58db164u, 0xb58b2be2u, 0xb589527eu, + 0xb5af14e6u, 0xb5864721u, 0xb5c79770u, 0xb5641326u, 0xb6081338u, 0xb59c73e6u, 0xb533b828u, 0xb5d37fe5u, + 0xb84726aau, 0xb903de4fu, 0x3185f4f9u, 0xb5e1c09fu, 0xb5c5395du, 0xb5c54c9du, 0xb5c235dcu, 0xb5be10e3u, + 0xb5c0cd94u, 0xb5c0c0beu, 0xb5c43343u, 0xb5be7b3cu, 0xb5bfbb07u, 0xb5b77c4fu, 0xb5bf4afdu, 0xb5ba7048u, + 0xb5ba42f2u, 0xb5b212a5u, 0xb5bc4119u, 0xb5c3795cu, 0xb5bc1b36u, 0xb5b15ca5u, 0xb5cf634bu, 0xb5b621ceu, + 0xb5b9a5e5u, 0xb5ab5b3bu, 0xb5bcbc59u, 0xb5ca11c3u, 0xb5b650fdu, 0xb5bc285au, 0xb5c49c29u, 0xb5ace46cu, + 0xb5b43838u, 0xb5bc5834u, 0xb5aea268u, 0xb5ae13b9u, 0xb592cdfcu, 0xb5abd125u, 0xb58eac0fu, 0xb59b9058u, + 0xb5bb40aeu, 0xb57478bcu, 0xb5a683a9u, 0xb5c00969u, 0xb5bca7cbu, 0xb5aa4d17u, 0xb5c959fau, 0xb5cb6d96u, + 0xb5bf0214u, 0xb5960a05u, 0xb5ad7a11u, 0xb5a41e66u, 0xb58a3211u, 0xb5c8f217u, 0xb59161e1u, 0xb593cc45u, + 0xb580686eu, 0xb598b599u, 0xb5a3520fu, 0xb593985au, 0xb583f249u, 0xb593570fu, 0xb5b24f0eu, 0xb5b5b7ffu, + 0xb5a5e11fu, 0xb8836abeu, 0x3901766bu, 0xb168927du, 0xb89ffd2eu, 0xb5bdb82eu, 0xb5bf3f79u, 0xb5c5a257u, + 0xb5c4b63au, 0xb5cd297bu, 0xb5c31fa5u, 0xb5caac54u, 0xb5ca2ce2u, 0xb5c1a91cu, 0xb5cdf8c1u, 0xb5b4da0eu, + 0xb5be9316u, 0xb5c0b5b2u, 0xb5c9e10bu, 0xb5bd9d5fu, 0xb5cf801au, 0xb5c48e57u, 0xb5c50ae4u, 0xb5c1674du, + 0xb5c3efc7u, 0xb5bff8b4u, 0xb5ce4b44u, 0xb5c3bfeau, 0xb5d1c6c3u, 0xb5e62345u, 0xb5d04d7du, 0xb5cbff67u, + 0xb5cbaa39u, 0xb5cc4673u, 0xb5ca0eb4u, 0xb5c9c9b4u, 0xb5cb8946u, 0xb5de5969u, 0xb5e4f7b9u, 0xb5b95285u, + 0xb5e0285du, 0xb5e52b11u, 0xb5a2029eu, 0xb5cc0e1cu, 0xb5cacc66u, 0xb5e89c40u, 0xb57f41f8u, 0xb5ea8d08u, + 0xb5e170d6u, 0xb5a02587u, 0xb5afc522u, 0xb5c2af2eu, 0xb5a30a2bu, 0xb5c440c7u, 0xb593ce4cu, 0xb5f03681u, + 0xb6001ddeu, 0xb5bb0152u, 0xb5f83affu, 0xb5d34ea2u, 0xb5d4a277u, 0xb5f1eda8u, 0xb5e85c4fu, 0xb5d2a0a8u, + 0xb60292c5u, 0xb5b7edcau, 0x38473e27u, 0xb8f9ec65u, 0xb17036c1u, 0x343ba9b5u, 0xb5b9119eu, 0xb5b90cadu, + 0xb5b156beu, 0xb5aeb17du, 0xb5b847d8u, 0xb5abe39bu, 0xb5c5b78au, 0xb5baddceu, 0xb5ba9e7au, 0xb5b17338u, + 0xb5be0235u, 0xb5a40b7cu, 0xb5af7cecu, 0xb5aaff09u, 0xb5a1980bu, 0xb5b6beb6u, 0xb5b889c3u, 0xb59660a4u, + 0xb5b29787u, 0xb59a53d5u, 0xb5a8f1f8u, 0xb5d71571u, 0xb5dad8c4u, 0xb5c7cb96u, 0xb5cf3bebu, 0xb5b03a01u, + 0xb5b15e2du, 0xb5b0f1b5u, 0xb5df3a9fu, 0xb5cec3bbu, 0xb5db71bdu, 0xb597e0beu, 0xb58b8ab8u, 0xb5e86a28u, + 0xb5aa157fu, 0xb558eefcu, 0xb58f1c88u, 0xb5be854bu, 0xb5b7f3c8u, 0xb5b732fbu, 0xb5ba0d93u, 0xb5f5c9d8u, + 0xb5b07c93u, 0xb5d2e563u, 0xb5ee6cc1u, 0xb5baa489u, 0xb598501cu, 0xb5a378a4u, 0xb5ce1021u, 0xb60979c7u, + 0xb5ac557bu, 0xb5d4d94du, 0xb5e80079u, 0xb5d070ecu, 0xb5aebf17u, 0xb5aa0495u, 0xb5a94042u, 0xb5ba679eu, + 0xb5b7f036u, 0xb6078cb3u, 0xb5b6f4bau, 0x3871dc89u, 0x38f2f6e9u, 0xae140afcu, 0x389974dbu, 0xb5aed7eeu, + 0xb5af0e2eu, 0xb5aec8dbu, 0xb5b1b8d9u, 0xb5abe800u, 0xb5ad386au, 0xb5b4da83u, 0xb5af580fu, 0xb5aeff84u, + 0xb5ac4d46u, 0xb59f9b98u, 0xb5b3e832u, 0xb5b40ddcu, 0xb597f9f1u, 0xb5baa7bcu, 0xb5c54ee6u, 0xb5b5338au, + 0xb5a5592fu, 0xb5adda24u, 0xb5afb293u, 0xb5b90945u, 0xb5ade590u, 0xb5a89c9cu, 0xb5bd8bddu, 0xb5a896c4u, + 0xb5bda407u, 0xb5aef0d9u, 0xb5a17063u, 0xb5b4c06cu, 0xb5cbd5d7u, 0xb59eca3fu, 0xb5a1a6d2u, 0xb5a8caceu, + 0xb5a2c27fu, 0xb5ac0cb0u, 0xb5afc6c8u, 0xb5aab968u, 0xb5ac10a7u, 0xb5cf11fcu, 0xb5abef77u, 0xb5a8c7cbu, + 0xb5d9647cu, 0xb5cacd50u, 0xb5c1b6ceu, 0xb5bbaec2u, 0xb5b27b91u, 0xb589380fu, 0xb5e1fce1u, 0xb5b0618eu, + 0xb5c0863eu, 0xb5bdc334u, 0xb5c3875au, 0xb5be40ceu, 0xb5c08315u, 0xb5cb3c48u, 0xb5f01cabu, 0xb5bce2f0u, + 0xb592b3eau, 0xb5f6ccf0u, 0xb5b3dffau, 0xb5ad7071u, 0xb8557f92u, 0xb8ebfe77u, 0xb0717061u, 0xb5c01cbbu, + 0xb5a8e735u, 0xb5a78187u, 0xb5a78225u, 0xb5a86d24u, 0xb5ab6ca8u, 0xb5a6637cu, 0xb5a4952bu, 0xb5a6d693u, + 0xb5a8f764u, 0xb5ad8003u, 0xb5ad89aeu, 0xb5a634bcu, 0xb5a4d12eu, 0xb5a4dba8u, 0xb59e2dd2u, 0xb5ab4f8cu, + 0xb5958b9fu, 0xb5ae7e18u, 0xb59f9f47u, 0xb5954cb4u, 0xb59af337u, 0xb5a5441cu, 0xb5a971d4u, 0xb57deadfu, + 0xb59cfe95u, 0xb5954280u, 0xb5a25e7eu, 0xb58f7087u, 0xb5ac6d47u, 0xb5a9eed8u, 0xb5c8226fu, 0xb5a51a0du, + 0xb5e878dbu, 0xb5c6f21fu, 0xb5cb584eu, 0xb5b6a557u, 0xb5a03435u, 0xb5abd68cu, 0xb5aeb9c0u, 0xb5ad39f5u, + 0xb5b62f9au, 0xb5883f07u, 0xb5c33817u, 0xb58e6a28u, 0xb5bae7a7u, 0xb5777d33u, 0xb5b5c8d4u, 0xb5b78f72u, + 0xb5ad12c0u, 0xb5478b28u, 0xb6005b5bu, 0xb5c2e227u, 0xb5a2261fu, 0xb5e64652u, 0xb5b49be9u, 0xb5bc1aa1u, + 0xb5a7e08cu, 0xb5967a37u, 0xb5a676c9u, 0xb590830du, 0xb5cabd51u, 0xb869a295u, 0x38e2c687u, 0x2f7204a2u, + 0xb898c31du, 0xb5a4247au, 0xb5a48a1bu, 0xb59b5394u, 0xb5974281u, 0xb5aafb2du, 0xb583f30du, 0xb5a4721eu, + 0xb58d0d74u, 0xb586fb1fu, 0xb5a7dc15u, 0xb5869f4fu, 0xb5a4da8eu, 0xb58d3e3fu, 0xb5ba639fu, 0xb58495a4u, + 0xb5fdd9d1u, 0xb599d097u, 0xb5689bfcu, 0xb5c7a0b5u, 0xb591ac92u, 0xb5c25fedu, 0xb5c9757eu, 0xb53cb3cbu, + 0xb5ecb0bbu, 0xb5b6f5fbu, 0xb5ad0039u, 0xb5aee4cfu, 0xb57ee5f2u, 0xb58cb3f6u, 0xb5a66b10u, 0xb597325eu, + 0xb57089e8u, 0xb522453fu, 0xb6049531u, 0xb591ac9du, 0xb5647ddfu, 0xb61f5461u, 0xb57737c9u, 0xb59a4667u, + 0xb60aef21u, 0xb591ac92u, 0xb5d23685u, 0xb5f4c1e0u, 0xb5a8473au, 0xb5fe57f2u, 0xb512e8c4u, 0xb5e09900u, + 0xb2002330u, 0xb59b0981u, 0xb60e47d2u, 0xb57a880fu, 0xb52d3d7du, 0xb5b0a7e8u, 0xb56b04d1u, 0xb61a895du, + 0xb5e09900u, 0xb604b1c9u, 0xb4deec25u, 0xb5bf7cb0u, 0xb5d7c608u, 0xb5049baeu, 0x38598e32u, 0xb8dd1fa1u, + 0x32351035u, 0x345430e0u, 0xb5968538u, 0xb5915950u, 0xb59bb128u, 0xb57c1200u, 0xb589594fu, 0xb589594fu, + 0xb5a4a99eu, 0xb59b4a3du, 0xb5a4a99eu, 0xb5bb4a3du, 0xb5b1eaddu, 0xb5a4a9a0u, 0xb5bb4a3eu, 0xb59b4a3eu, + 0xb549533du, 0xb5f5ccb8u, 0xb59b4a3eu, 0xb59b4a3bu, 0xb5888b7bu, 0xb59b4a3bu, 0xb55c11fdu, 0xb5018f84u, + 0xb55c11fdu, 0xb5c88b7bu, 0xb59b4a3bu, 0xb5c88b7bu, 0xb5c88b7bu, 0xb5c88b7bu, 0xb5f5ccb8u, 0xb5c88b7bu, + 0xb60445beu, 0xb59b4a3eu, 0xb59b4a3eu, 0xb59b4a3eu, 0xb59b4a3eu, 0xb5f5ccb8u, 0xb60da51fu, 0xb59b4a3eu, + 0xb60da51fu, 0xb59b4a3eu, 0xb59b4a3eu, 0xb5f5ccb8u, 0xb5f5ccb8u, 0xb45a51f4u, 0xb59b4a3eu, 0xb5c0c7c5u, + 0xb59b4a3eu, 0xb59b4a3eu, 0xb56b9970u, 0xb59b4a3eu, 0xb63ae65cu, 0x34fce0ecu, 0xb5c0c7c5u, 0xb5018f8au, + 0xb5f5ccb8u, 0xb59b4a3eu, 0xb59b4a3eu, 0xb5c0c7c5u, 0xb59b4a3eu, 0xb5018f8au, 0xb45a51f4u, 0x384c9007u, + 0x38d2a9c2u, 0xb1a93472u, 0x38934beau, 0xb59908ddu, 0xb59a7badu, 0xb58d8449u, 0xb5974ef3u, 0xb5891b09u, + 0xb5a846edu, 0xb5bb965bu, 0xb5969f36u, 0xb5669f91u, 0xb5a6110au, 0xb58c24ffu, 0xb5aa7cd1u, 0xb590d3f6u, + 0xb5a96080u, 0xb5888cdcu, 0xb5bdc9f8u, 0xb547bbdau, 0xb599eea4u, 0xb5c2c19cu, 0xb5b3daafu, 0xb5e08d88u, + 0xb56e5639u, 0xb5a5852cu, 0xb50f7281u, 0xb5b3daafu, 0xb513e3c4u, 0xb4c4768au, 0xb58e5824u, 0xb5db95e4u, + 0xb5c468dcu, 0xb598476du, 0xb5ac2408u, 0xb532be61u, 0xb591a889u, 0xb5a8d591u, 0xb5a8d591u, 0xb5ee5ca9u, + 0xb594f700u, 0xb591a889u, 0xb594f700u, 0xb54d39e0u, 0xb59ee649u, 0xb63236b0u, 0xb574f702u, 0xb59ee649u, + 0xb5fdc9f8u, 0xb5fdc9f8u, 0xb574f702u, 0xb54d39e0u, 0xb4a35645u, 0xb5c5809fu, 0xb513e3c4u, 0xb638d590u, + 0xb4cb1368u, 0xb5a8d591u, 0xb5fdc9f8u, 0x33308fd8u, 0xb513e3c4u, 0xb60583dcu, 0xb613d963u, 0xb5ee5ca9u, + 0xb8610ba6u, 0xb8cd6f0bu, 0x3126b953u, 0xb59e1147u, 0xb58ad853u, 0xb58b7ee1u, 0xb589d0c4u, 0xb58b46d3u, + 0xb58be7f7u, 0xb586a896u, 0xb58b807eu, 0xb58759a7u, 0xb5920bc1u, 0xb583d622u, 0xb58b7d43u, 0xb58b19cau, + 0xb58fafddu, 0xb58c48a0u, 0xb58c6abbu, 0xb5915ce8u, 0xb5848892u, 0xb589ca41u, 0xb5769a31u, 0xb5662b02u, + 0xb587258du, 0xb58ad383u, 0xb589e2f0u, 0xb58d0253u, 0xb58ae5c7u, 0xb58a5394u, 0xb558b35cu, 0xb585dd9bu, + 0xb58c364du, 0xb578a389u, 0xb571684fu, 0xb5614eccu, 0xb5714865u, 0xb595e37du, 0xb585a18eu, 0xb5a30ef3u, + 0xb5841503u, 0xb57b2d62u, 0xb59476b8u, 0xb59d4af0u, 0xb586689au, 0xb5cd6b38u, 0xb58234d1u, 0xb58c14efu, + 0xb5421f61u, 0xb56597b7u, 0xb5b01a10u, 0xb587fc55u, 0xb5923b2eu, 0xb59032e8u, 0xb56011bbu, 0xb57835e8u, + 0xb570c091u, 0xb4e25f90u, 0xb568fc16u, 0xb5701fd6u, 0xb582d61eu, 0xb5a5e5ddu, 0xb570663du, 0xb57c115bu, + 0xb584f86fu, 0xb849a0abu, 0x38c6263bu, 0xb231341au, 0xb892c77cu, 0xb58a18dbu, 0xb58a83a7u, 0xb58f6bc5u, + 0xb587150eu, 0xb594976cu, 0xb58da63fu, 0xb594cb36u, 0xb599af8cu, 0xb5941049u, 0xb597ac25u, 0xb5948a9cu, + 0xb5a5ffe0u, 0xb598236cu, 0xb5963556u, 0xb5848c5eu, 0xb5a20b03u, 0xb5a98549u, 0xb590feccu, 0xb59cba0bu, + 0xb5a5ce89u, 0xb598a820u, 0xb59b4021u, 0xb5b9b265u, 0xb5a52902u, 0xb5a185f1u, 0xb5c14f64u, 0xb58f2674u, + 0xb59c7d7du, 0xb5d33f9cu, 0xb5a9d1dbu, 0xb5b91900u, 0xb5c74853u, 0xb5af5240u, 0xb5cd68b0u, 0xb5b23e6eu, + 0xb5b05690u, 0xb5c7f041u, 0xb5d66b0au, 0xb58a995fu, 0xb5de7649u, 0xb5da7d2cu, 0xb5981964u, 0xb5d42e0cu, + 0xb5d78272u, 0xb5b1ddadu, 0xb5c5b63fu, 0xb5c9131eu, 0xb5dd3c19u, 0xb5d93681u, 0xb5c47df9u, 0xb5aa4089u, + 0xb5951b47u, 0xb5ee9116u, 0xb5bfea85u, 0xb5e095c2u, 0xb5be97edu, 0xb5ea8d2du, 0xb60771dfu, 0xb5fe1916u, + 0xb5d8efb4u, 0xb5d73bc2u, 0x385f31d6u, 0xb8bfcc12u, 0xb105eb56u, 0x33ececc9u, 0xb57b3eaau, 0xb57a5a7au, + 0xb5813174u, 0xb57a410du, 0xb5833732u, 0xb57886c3u, 0xb582cea3u, 0xb550cc76u, 0xb58b1517u, 0xb576cbfcu, + 0xb589ef0cu, 0xb564587au, 0xb581e3b6u, 0xb56882f9u, 0xb5817a67u, 0xb562aad7u, 0xb560942du, 0xb5580c52u, + 0xb56ecf7eu, 0xb594a7c0u, 0xb5786922u, 0xb58c9a2bu, 0xb59950a0u, 0xb58ac649u, 0xb56ef662u, 0xb566068au, + 0xb55858b0u, 0xb58daae4u, 0xb53ff0beu, 0xb588f5b4u, 0xb560ca1cu, 0xb5819f1fu, 0xb59e13feu, 0xb58998cau, + 0xb5a4a192u, 0xb5b4f097u, 0xb578833eu, 0xb599e564u, 0xb5853f76u, 0xb5c6ec0cu, 0xb59a3b74u, 0xb5b8008eu, + 0xb5c65229u, 0xb5becaddu, 0xb5e0e2c5u, 0xb5cf1b12u, 0xb5d85199u, 0xb5b3a899u, 0xb5857a45u, 0xb5686024u, + 0xb58e7e73u, 0xb5cc2bc4u, 0xb5427546u, 0xb59442d3u, 0xb579ac2bu, 0xb595edadu, 0xb5c73a6bu, 0xb599a953u, + 0xb5d2e6a8u, 0xb5d436c7u, 0xb5bc4eebu, 0x38379505u, 0x38b7a58eu, 0x3009ef40u, 0x388e366eu, 0xb5710927u, + 0xb569ddbcu, 0xb5744a00u, 0xb56ab928u, 0xb575aaf5u, 0xb572f7deu, 0xb568c8b1u, 0xb5731272u, 0xb5606e89u, + 0xb5719df9u, 0xb570d899u, 0xb537689au, 0xb5598fa8u, 0xb5426f9cu, 0xb5394bd2u, 0xb54cd949u, 0xb539dc62u, + 0xb54c8766u, 0xb555ec36u, 0xb54d4476u, 0xb54862e3u, 0xb55bbc01u, 0xb540b83au, 0xb53621d9u, 0xb5880938u, + 0xb5509648u, 0xb54501d4u, 0xb559c45cu, 0xb5404b2cu, 0xb53d9460u, 0xb58c5795u, 0xb4e661cau, 0xb57c2010u, + 0xb53d4164u, 0xb54b76abu, 0xb57b1db7u, 0xb53f95b0u, 0xb54650dfu, 0xb58331ffu, 0xb58540b3u, 0xb52ed170u, + 0xb5a390dfu, 0xb52cff91u, 0xb5126e2bu, 0xb50594f6u, 0xb563a977u, 0xb4d17608u, 0xb533919du, 0xb515f721u, + 0xb533ae5cu, 0xb5959059u, 0xb55c49dau, 0xb500ba14u, 0xb581d07du, 0xb4f6c5bbu, 0xb5846313u, 0xb4ccdaa6u, + 0xb550722cu, 0xb4f29f75u, 0xb5278c12u, 0xb564fdf9u, 0xb86ac21au, 0xb8af07e3u, 0xb1700612u, 0xb58446bcu, + 0xb56845b3u, 0xb57102e2u, 0xb56d709du, 0xb5732bb3u, 0xb56ea83au, 0xb569a15du, 0xb580d241u, 0xb5844fb4u, + 0xb5848211u, 0xb5809325u, 0xb57f93e1u, 0xb5790010u, 0xb58c8d3eu, 0xb584b6c7u, 0xb57dd17fu, 0xb58f6eb0u, + 0xb57d6b1fu, 0xb586e9b9u, 0xb5811878u, 0xb581cdaau, 0xb575f473u, 0xb59102e5u, 0xb57a97bbu, 0xb56910b6u, + 0xb57e7e8bu, 0xb5927931u, 0xb58899d4u, 0xb5821e7bu, 0xb57ddd4eu, 0xb5670f76u, 0xb551f267u, 0xb57dcf53u, + 0xb5911103u, 0xb58ede89u, 0xb58c2729u, 0xb57ff41cu, 0xb58d7cadu, 0xb54a79a6u, 0xb58c4b5bu, 0xb5a08a53u, + 0xb5b6a42eu, 0xb56b4890u, 0xb56e3af0u, 0xb5b8f1e1u, 0xb5a390a2u, 0xb5b1f513u, 0xb559d08bu, 0xb59120c8u, + 0xb5324e5du, 0xb5b2147du, 0xb57c5203u, 0xb58e21e0u, 0xb5973db5u, 0xb59dac30u, 0xb58b1744u, 0xb5a1d5f4u, + 0xb58f6111u, 0xb599f248u, 0xb5a0ccb3u, 0xb5d0f2beu, 0xb57ee26cu, 0xb82eaa2cu, 0x38a8a657u, 0x312d7c21u, + 0xb88db477u, 0xb556007fu, 0xb55bed56u, 0xb567a588u, 0xb54ae916u, 0xb552b0c2u, 0xb55bb6feu, 0xb5628720u, + 0xb557f0e5u, 0xb55dd74bu, 0xb5763b68u, 0xb551abb3u, 0xb554bf00u, 0xb55b185au, 0xb554cc79u, 0xb5600e2au, + 0xb576a5d3u, 0xb531b5a7u, 0xb53774d6u, 0xb53637f6u, 0xb56165e4u, 0xb53c19feu, 0xb5696830u, 0xb55d6e95u, + 0xb54ebb48u, 0xb572d61bu, 0xb52196d4u, 0xb52cca69u, 0xb57517b5u, 0xb4e4b309u, 0xb5644c1au, 0xb56e2984u, + 0xb50ca9dfu, 0xb5674943u, 0xb4b41d62u, 0xb576e821u, 0xb5521430u, 0xb502a7b7u, 0xb551d917u, 0xb543194du, + 0xb537151fu, 0xb54085e3u, 0xb52ea335u, 0xb56a670bu, 0xb56ec05eu, 0xb52f2656u, 0xb51804ddu, 0xb4e12307u, + 0xb5802a48u, 0xb520599bu, 0xb55b260cu, 0xb583f247u, 0xb59e8121u, 0xb4ccfb3cu, 0xb51e91a6u, 0xb586194eu, + 0xb5469d84u, 0x343352bfu, 0x32e386f0u, 0xb56f152bu, 0xb4f15357u, 0xb563d571u, 0x386b67acu, 0xb8a0b51bu, + 0x3093e935u, 0x33af3803u, 0xb559508eu, 0xb54e0522u, 0xb55f7ceeu, 0xb552a3e2u, 0xb5577fb7u, 0xb54a9f5eu, + 0xb56741ccu, 0xb566645cu, 0xb56a5b1au, 0xb57e4eedu, 0xb58cae69u, 0xb58232b3u, 0xb5577455u, 0xb57f042au, + 0xb50db558u, 0xb54438bfu, 0xb4f16a68u, 0xb52f9c2fu, 0xb59704cfu, 0xb510f79fu, 0xb4e11076u, 0xb57cbe27u, + 0xb52aba8eu, 0xb53709b3u, 0xb57df756u, 0xb53fb986u, 0xb535910au, 0xb550f4c4u, 0xb54e7064u, 0xb56d211eu, + 0xb5703f9cu, 0xb5433402u, 0xb53f19a5u, 0xb59e6277u, 0xb549c798u, 0xb4c335b2u, 0xb5b4d70au, 0xb520946fu, + 0xb59791f7u, 0xb55c9aeau, 0xb5a078dfu, 0xb54713fcu, 0xb57d18b2u, 0xb58e012bu, 0xb5a6f5a3u, 0xb59c7dfcu, + 0xb59d85b7u, 0xb514131cu, 0xb56cda4bu, 0xb57f4207u, 0xb55f722du, 0xb4902804u, 0xb5dd3d17u, 0xb5abc9cau, + 0xb59fb02cu, 0xb58dc470u, 0xb593356au, 0xb5c13e8eu, 0xb593c452u, 0xb50ba58bu, 0xb5afebffu, 0x381a4e50u, + 0x389c920bu, 0xb1e3da7eu, 0x3889f89eu, 0xb5419e33u, 0xb5468ccau, 0xb54ab5e4u, 0xb54f398bu, 0xb54f73d1u, + 0xb54b29c2u, 0xb54d0916u, 0xb5484c41u, 0xb54cb4f0u, 0xb54d9942u, 0xb55a8e03u, 0xb5710e56u, 0xb5420ef5u, + 0xb57042c5u, 0xb514ba7du, 0xb54878aau, 0xb573c168u, 0xb544c888u, 0xb5664f59u, 0xb58c6f1fu, 0xb58019beu, + 0xb5847c28u, 0xb568c81bu, 0xb55db26au, 0xb56069cfu, 0xb5938a65u, 0xb5837c77u, 0xb55adf86u, 0xb574efa7u, + 0xb5705d66u, 0xb542189bu, 0xb574357cu, 0xb587323du, 0xb54e0af4u, 0xb5c29b99u, 0xb580eeb3u, 0xb55e40cau, + 0xb579d284u, 0xb51fec86u, 0xb54df0d3u, 0xb54cdb81u, 0xb57e0d03u, 0xb58f73dau, 0xb540507du, 0xb5c7f8f0u, + 0xb52edb27u, 0xb57d8ccdu, 0xb52fe867u, 0xb57f558fu, 0xb53c74deu, 0xb5a18406u, 0xb53c44dbu, 0xb58d4f95u, + 0xb5982c61u, 0xb509d2fbu, 0xb57032aau, 0xb5aa27f3u, 0xb5de5449u, 0xb5a5ff7du, 0xb5d0e4fbu, 0xb58a7a55u, + 0xb872af06u, 0xb8950d04u, 0xb2372377u, 0xb55a1baeu, 0xb541556bu, 0xb542f643u, 0xb5425d3au, 0xb53e38b3u, + 0xb543bdb3u, 0xb542b01du, 0xb53d14a9u, 0xb5342b81u, 0xb54fc0dbu, 0xb56deefdu, 0xb554cc17u, 0xb580da63u, + 0xb5650c84u, 0xb54ccab1u, 0xb5616fb8u, 0xb53fca82u, 0xb54a5b6cu, 0xb55b3fb2u, 0xb55e3afau, 0xb57dde83u, + 0xb5701b44u, 0xb56429b2u, 0xb556a7f9u, 0xb57d59ccu, 0xb56fef67u, 0xb54fa700u, 0xb58a4d06u, 0xb59515e3u, + 0xb58c4ddbu, 0xb590fa73u, 0xb58af863u, 0xb5b9dc82u, 0xb5a6a540u, 0xb592815au, 0xb57c6607u, 0xb5908ea0u, + 0xb58c6f36u, 0xb5a44177u, 0xb598e111u, 0xb5824f2eu, 0xb597722cu, 0xb59519afu, 0xb57d957du, 0xb50b072eu, + 0xb58d1e87u, 0xb5a09419u, 0xb57d7e0bu, 0xb5bfc53fu, 0xb5ca5e49u, 0xb5a37d83u, 0xb559da29u, 0xb5aea60au, + 0xb59529acu, 0xb5e4c69fu, 0xb5b7c3c8u, 0xb5b52ff9u, 0xb58c5f5du, 0xb5bb1702u, 0xb57dff20u, 0xb5c7826du, + 0xb5874822u, 0xb8148640u, 0x388d7fddu, 0x322911efu, 0xb8899198u, 0xb52a6ce5u, 0xb52df859u, 0xb52bc4c5u, + 0xb539ab4du, 0xb5341b60u, 0xb524ec3au, 0xb5288c00u, 0xb523227fu, 0xb51ec535u, 0xb51d5a39u, 0xb52d6b9fu, + 0xb528fba6u, 0xb51977aeu, 0xb51cdc3du, 0xb503b819u, 0xb50d5730u, 0xb51d7c3au, 0xb51ab7f9u, 0xb50e7cf6u, + 0xb51ee4d7u, 0xb52b0d4fu, 0xb500a54du, 0xb520ebbau, 0xb5249800u, 0xb50882c1u, 0xb5565d77u, 0xb4f9b304u, + 0xb502c306u, 0xb5667158u, 0xb462c1c4u, 0xb4ca07adu, 0xb4cf101au, 0xb54fa334u, 0xb5107353u, 0xb5605939u, + 0x338ad976u, 0xb4d84985u, 0xb5283a04u, 0xb49897d0u, 0xb4c62110u, 0xb49ac37du, 0xb536ef25u, 0xb52b139au, + 0xb53801e6u, 0xb4ef44cfu, 0xb454004eu, 0xb5730356u, 0xb52e31d4u, 0xb4e6b865u, 0xb4a9c755u, 0xb53627e2u, + 0xb479ec8du, 0xb342f00au, 0xb4b0c210u, 0xb409d2adu, 0xb4ba027du, 0xb4c0086fu, 0xb4a41cafu, 0xb5401763u, + 0xb3c478ccu, 0xb4d5abdfu, 0x3876105du, 0xb8853b6du, 0x32a7428du, 0x33ee23c8u, 0xb51ccbc2u, 0xb51c0ec7u, + 0xb51b3ce8u, 0xb519cf88u, 0xb511ba56u, 0xb50a46e2u, 0xb518080cu, 0xb4dc1bdfu, 0xb4e5129au, 0xb4c115a8u, + 0xb4cae53eu, 0xb4c01e01u, 0xb4e0326fu, 0xb48c660eu, 0xb4c1fb67u, 0xb4f68eb7u, 0xb4cd2f11u, 0xb4c91fb6u, + 0xb48496b5u, 0xb4b3cb0eu, 0xb4dc75adu, 0xb48c158fu, 0xb4330e9bu, 0xb405e5aau, 0xb480926eu, 0xb47a7c3du, + 0x30761bf2u, 0xb425be78u, 0xb3b7eeeeu, 0xb388c6c4u, 0xb3e7d69fu, 0xb312a6f9u, 0x3408b110u, 0x3330713du, + 0xb4a521bbu, 0xb3c12bc2u, 0x3403e5ffu, 0x33dd2b15u, 0xb41f7cefu, 0x33307980u, 0x33cd61e7u, 0x33b83c7eu, + 0x33a8cf43u, 0x33b3b349u, 0xb4e0a038u, 0x33a82398u, 0x342b7528u, 0xb366dd24u, 0x31535649u, 0xb3cb8ac1u, + 0x34357a7cu, 0x342fab75u, 0xb41bbfe2u, 0x34aa6f0eu, 0xb4481c8du, 0xb4be1c96u, 0xb4622c69u, 0xb48d857fu, + 0x33c4bf3bu, 0x34a214e3u, 0x348b5d3cu, 0x3806b070u, 0x38829589u, 0xb13aa862u, 0x388695c5u, 0xb520d9f0u, + 0xb5291faau, 0xb521559du, 0xb5341391u, 0xb529ae09u, 0xb52e0815u, 0xb529e163u, 0xb52b5864u, 0xb518af7au, + 0xb53bb7c5u, 0xb534010eu, 0xb545d95eu, 0xb52443efu, 0xb523894fu, 0xb52c9c90u, 0xb5201da2u, 0xb54c779cu, + 0xb55aad06u, 0xb528f02fu, 0xb542eebfu, 0xb5329ef8u, 0xb55ba943u, 0xb53f1481u, 0xb52e73ccu, 0xb53cc7b1u, + 0xb52525e4u, 0xb532a551u, 0xb529580fu, 0xb52952aau, 0xb54a6124u, 0xb5356ee9u, 0xb5431045u, 0xb529344fu, + 0xb564f812u, 0xb53972b1u, 0xb4fb2762u, 0xb53135c5u, 0xb5584fdau, 0xb51858fcu, 0xb53bb23cu, 0xb4d2d77eu, + 0xb540046du, 0xb5613046u, 0xb575d058u, 0xb51d4549u, 0xb58df32au, 0xb4f6c483u, 0xb53c42a8u, 0xb56a464au, + 0xb5a56f42u, 0xb519bfacu, 0xb51864dbu, 0xb55340c6u, 0xb4a13f01u, 0xb54dfb47u, 0xb5a2b323u, 0xb520aab3u, + 0xb55aee11u, 0xb411b564u, 0xb55e4d03u, 0xb585aa2cu, 0xb87b691eu, 0xb87481cdu, 0xb1155868u, 0xb528e398u, + 0xb5160461u, 0xb51428efu, 0xb514e324u, 0xb51ecd8eu, 0xb50c924eu, 0xb51b7ab2u, 0xb510a14bu, 0xb50a9e86u, + 0xb516e2c4u, 0xb50b2aa8u, 0xb526f0d9u, 0xb4fe0e08u, 0xb51bd501u, 0xb51ba275u, 0xb52df2d5u, 0xb54299d3u, + 0xb50bed97u, 0xb5391f50u, 0xb528bdc5u, 0xb529d740u, 0xb53a3f6bu, 0xb50b149fu, 0xb53d1fefu, 0xb527937bu, + 0xb514d52eu, 0xb522a6d8u, 0xb53907f3u, 0xb54fda4eu, 0xb54e8a93u, 0xb504cd88u, 0xb52de465u, 0xb5489056u, + 0xb5529a5eu, 0xb50f8a17u, 0xb525061eu, 0xb513f96eu, 0xb4ea8414u, 0xb52b82eau, 0xb50e4b46u, 0xb527a50fu, + 0xb52e9024u, 0xb58a88a5u, 0xb50b86cbu, 0xb51f5701u, 0xb544881au, 0xb52955c4u, 0xb504879eu, 0xb52e5263u, + 0xb533df4au, 0xb4fd88bcu, 0xb4e9da73u, 0xb572f95au, 0xb55601f5u, 0xb55ecd6au, 0xb56e6324u, 0xb51cf0e3u, + 0xb51a77b7u, 0xb577bb24u, 0xb4aa7c07u, 0xb5535c51u, 0xb56f4bcfu, 0xb7eeeb11u, 0x38663d28u, 0x3284c63cu, + 0xb8864694u, 0xb5037239u, 0xb4fe481du, 0xb514ccceu, 0xb4f31f0eu, 0xb4dfe535u, 0xb4f185deu, 0xb4cb1727u, + 0xb4e74c3au, 0xb506cee5u, 0xb4e9fc6au, 0xb4993ee9u, 0xb4caa9f0u, 0xb45edc08u, 0xb4b84670u, 0xb4a8020au, + 0xb45d5ad4u, 0xb46afdbeu, 0xb48d8fccu, 0x34a2e652u, 0xb40ad551u, 0x329c58b5u, 0x34353daau, 0xb4dc933cu, + 0xb40f3e50u, 0xb4c6db7au, 0xb4e4bf4fu, 0xb3544afeu, 0xb4fde6f4u, 0xb4313c48u, 0xb43b61e1u, 0x32cfe0adu, + 0xb505f410u, 0xb539cea1u, 0xb3a4e651u, 0xb5522b66u, 0x33aa81cdu, 0xb3472db2u, 0x345414fcu, 0xb4244058u, + 0x34f89356u, 0xb488b3c2u, 0x34cd5448u, 0xb4c6f2d8u, 0x34a28a8bu, 0xb45fa4bau, 0x346eabfbu, 0xb477452eu, + 0x33ad4fc2u, 0x33b3f183u, 0x343e6f23u, 0xb451dd41u, 0x3505d0d4u, 0x3498396bu, 0xb4c71065u, 0x31b9833bu, + 0xb5007945u, 0x34223aa9u, 0xb50eced6u, 0x35006e06u, 0xb4f17391u, 0xb5402f51u, 0x387d69bbu, 0xb854e265u, + 0x31892446u, 0x3351f643u, 0xb50cb572u, 0xb510b952u, 0xb50800eau, 0xb51185fbu, 0xb508dd34u, 0xb5266d94u, + 0xb537dd0du, 0xb4db1b23u, 0xb555efc3u, 0xb54dfcb2u, 0xb4f39831u, 0xb4cd398eu, 0xb5106c18u, 0xb4c655cfu, + 0xb5357ecfu, 0xb586fe9au, 0xb5357eb6u, 0xb5984fbcu, 0xb560bb67u, 0xb53a5e38u, 0xb4c3c0acu, 0xb4bfb871u, + 0xb4078157u, 0xb50dfc98u, 0xb53a5e38u, 0xb52b5989u, 0xb52b5989u, 0xb4a5b757u, 0xb440e2a2u, 0xb512dbacu, + 0xb57ef8ceu, 0x34bcdd2au, 0x327ae536u, 0x3503eb95u, 0x327ae536u, 0x34f45aaau, 0xb4ad2cd6u, 0xb54cd86au, + 0xb5275b9bu, 0x34bcdd2au, 0x327ae536u, 0x34f45aaau, 0x34f45aaau, 0xb44465adu, 0x3425a653u, 0xb5621530u, + 0xb4ad2cd6u, 0xb38a8383u, 0xb46b5eadu, 0x3425a653u, 0x33fd5aa7u, 0xb5b10a98u, 0xafed2f37u, 0xb2f2460du, + 0xb3aacd59u, 0x35493190u, 0xb4f828d6u, 0xb4c42a61u, 0x34dd5f1fu, 0xb2f2460du, 0x33a1647du, 0x37cf34c0u, + 0x384b44a4u, 0xb1d54288u, 0x3883ed97u, 0xb4fb1f1au, 0xb4e9f7ebu, 0xb4de5862u, 0xb4f6dd85u, 0xb51c5c84u, + 0xb50e04d0u, 0xb4d6050au, 0xb4cc0f25u, 0xb4a92000u, 0xb4e232aau, 0xb507e1fau, 0xb50a0e71u, 0xb523247au, + 0xb4f3cc9fu, 0xb5342f7du, 0xb54677eeu, 0xb51a9939u, 0xb4d3737au, 0xb4e4429cu, 0xb54a6c60u, 0xb536ed22u, + 0xb48298fdu, 0xb5744ea4u, 0xb5243266u, 0xb52a21c1u, 0xb4e5430bu, 0xb51ac242u, 0xb519e842u, 0xb341e07bu, + 0xb53bce0eu, 0xb3abdd56u, 0xb4609eb7u, 0x344042edu, 0xb55a45abu, 0xb4d24ca7u, 0xb48fe972u, 0xb4951315u, + 0xb59c7fd8u, 0xb41fb206u, 0xb4da44d4u, 0xb41d6157u, 0xb5984a3au, 0xb5ca6e63u, 0xb5950aeau, 0xb58d0138u, + 0xb505e35bu, 0xb5cccdebu, 0xb5373c0au, 0xb58c7c3au, 0x323b8ed4u, 0xb5b1a508u, 0x347c0a3cu, 0xb5535f45u, + 0xb50c40c6u, 0xb5481da6u, 0xb584af01u, 0xb5802a6eu, 0xb5141359u, 0xb420c268u, 0xb4b3a246u, 0xb4ff7ea4u, + 0xb87da469u, 0xb841f053u, 0xb20f7ba5u, 0xb502af24u, 0xb4e2d423u, 0xb4e215efu, 0xb4f234deu, 0xb4e2a164u, + 0xb4ce64b3u, 0xb4e38ceau, 0xb4f5fed1u, 0xb4bfa1d5u, 0xb4c6bb4du, 0xb4d7641fu, 0xb4ebb2ecu, 0xb4fcb324u, + 0xb4f1577du, 0xb4d6581bu, 0xb5021e7du, 0xb4f01c50u, 0xb4e5c352u, 0xb4f8e579u, 0xb4fb1201u, 0xb4f7168au, + 0xb4f47dafu, 0xb4e07c99u, 0xb4c80ae4u, 0xb5133560u, 0xb50973f2u, 0xb511b7b2u, 0xb4c1cb77u, 0xb50d0cc1u, + 0xb5146851u, 0xb4e60117u, 0xb4e55413u, 0xb4b4f15fu, 0xb4e5dfb0u, 0xb4ee90f5u, 0xb533dcebu, 0xb50b8d18u, + 0xb486f781u, 0xb5084363u, 0xb4cf91e3u, 0xb4f68a0du, 0xb521b4e9u, 0xb49c39c3u, 0xb51c2aeau, 0xb3afdfefu, + 0xb53cbfccu, 0xb3898856u, 0xb473df9du, 0xb46c5a76u, 0xb5449e2du, 0xb5002ba1u, 0xb4d69489u, 0xb48add05u, + 0xb31fe37bu, 0xb4f86f60u, 0xb4a0413cu, 0xb4cdf618u, 0xb5367456u, 0xb487a9a6u, 0xb2bb7464u, 0xb4e45287u, + 0xb512f4abu, 0xb7b5125au, 0x3832491cu, 0xb19dff53u, 0xb883ba68u, 0xb4d7f4bbu, 0xb4d0ee3fu, 0xb4eaca71u, + 0xb4cd3a6au, 0xb4c1e96au, 0xb4c38f7du, 0xb4d9d5e8u, 0xb4ceac08u, 0xb4abcb08u, 0xb4f5c855u, 0xb4d5890du, + 0xb508513eu, 0xb4b5c477u, 0xb4aa6ff5u, 0xb4e102fbu, 0xb505fe80u, 0xb4be96b2u, 0xb4d60f0bu, 0xb50a06fdu, + 0xb4d6edc4u, 0xb4915942u, 0xb4b8e6b5u, 0xb50f5484u, 0xb4b9c790u, 0xb4d8371eu, 0xb51151efu, 0xb506a5d0u, + 0xb5076fd1u, 0xb4c06715u, 0xb4e8dd51u, 0xb4bfe2cau, 0xb4bc09adu, 0xb4941361u, 0xb53c6395u, 0xb512db7cu, + 0xb4afdc11u, 0xb494082cu, 0xb5098b90u, 0xb50704a2u, 0xb4754daeu, 0xb4f9a037u, 0xb5565aaau, 0xb49036bdu, + 0xb49f5c3au, 0xb4ddbbd8u, 0xb50cf0e4u, 0xb4978823u, 0xb542019au, 0xb53b03a6u, 0xb55525fbu, 0xb4c4ecacu, + 0xb4e79ae3u, 0xb43b5774u, 0xb500fce4u, 0xb5466c6cu, 0xb5333e59u, 0xb582cbb6u, 0xb5341f96u, 0xb53dba6cu, + 0x341bb60au, 0xb5134a9au, 0x3880296du, 0xb82532bdu, 0xb21a4c8eu, 0x3345c5d0u, 0xb4bc65b1u, 0xb4bd8ee1u, + 0xb4af12abu, 0xb4a44d47u, 0xb4b9f1d0u, 0xb49f2ec5u, 0xb4e361b6u, 0xb4cc189au, 0xb50dfb11u, 0xb50fda9eu, + 0xb4cc5b5cu, 0xb4e169abu, 0xb5125543u, 0xb4a99f8eu, 0xb4d6a265u, 0xb4cb1f48u, 0xb4e19523u, 0xb4c69fceu, + 0xb487da84u, 0xb4c1a775u, 0xb4ad51c4u, 0xb5130a5bu, 0xb4aa069du, 0xb50e14c0u, 0xb4ca9451u, 0xb486e580u, + 0xb49e19e7u, 0xb5061ddbu, 0xb505ad3fu, 0xb4d74203u, 0xb529d62au, 0xb5619bdau, 0xb5159f1fu, 0xb516065cu, + 0xb5191aaau, 0xb4e53de0u, 0xb53b7856u, 0xb54e8988u, 0xb50ab8d8u, 0xb5911cdcu, 0xb5a72d77u, 0xb3d6e411u, + 0xb5350656u, 0xb52d9673u, 0xb51db242u, 0x33a297b9u, 0xb5587d68u, 0xb4e2146du, 0xb540406bu, 0xb34c0ee9u, + 0xb54ef512u, 0xb42c178du, 0xb5251dccu, 0xb506961au, 0xb554fbdfu, 0xb4f94971u, 0xb530a99eu, 0xb49bb39cu, + 0xb4fc1fa2u, 0xb2a85448u, 0xb56059d9u, 0x3794c08eu, 0x381959f9u, 0x322bc877u, 0x3882087du, 0xb4a7e99bu, + 0xb49a9f62u, 0xb4952abdu, 0xb48f452du, 0xb4ae6861u, 0xb494a612u, 0xb44bc15au, 0xb4ab58cbu, 0xb46d242eu, + 0xb49670f5u, 0xb49d75d4u, 0xb3f810ecu, 0xb4647fcau, 0xb4020b75u, 0xb40b42c6u, 0xb49cb1a1u, 0xb450f488u, + 0xb3272f40u, 0xb4877473u, 0xb45eb57cu, 0xb4c089fbu, 0xb436de34u, 0xb3ebc9d3u, 0xb2592a10u, 0x33bb9ea2u, + 0xb39ee95cu, 0xb492142du, 0x3437306au, 0xb4550d4au, 0xb3a08714u, 0x33e0520au, 0xb1230511u, 0x3413eea6u, + 0x32bfec5du, 0xb38d25c8u, 0xb39e2a09u, 0xb40c017fu, 0x338ab616u, 0xb42df924u, 0x34b96842u, 0xb4c65165u, + 0xb326f8a2u, 0x3379f927u, 0x34077dd2u, 0xb22f925du, 0x336ea08fu, 0xb3b12276u, 0x3457b3d8u, 0x34406df1u, + 0xb4b9755au, 0x33e9c258u, 0x348e1a91u, 0x3449cdbeu, 0x2f429f50u, 0xb4410d20u, 0x3558a19fu, 0x33c3a9e4u, + 0x351cbc79u, 0x3525f9e1u, 0x34c0f6b1u, 0xb467fc54u, 0xb8807215u, 0xb8095b84u, 0x2ff5f85au, 0xb4b299eau, + 0xb49d5cb6u, 0xb4991f61u, 0xb4a43df1u, 0xb490c6e8u, 0xb49934b8u, 0xb4a40a42u, 0xb49c46f3u, 0xb4a54f9eu, + 0xb4837e67u, 0xb49ef39au, 0xb4804974u, 0xb4b3158au, 0xb499806bu, 0xb4881375u, 0xb4aed0b9u, 0xb4f1dfaau, + 0xb4ab32beu, 0xb4c8fecfu, 0xb4af6cf1u, 0xb4db0758u, 0xb4c995b2u, 0xb4e5059fu, 0xb4409e45u, 0xb4a4ea1bu, + 0xb4d18b63u, 0xb4a838abu, 0xb4788d7du, 0xb4e4305fu, 0xb4e0debbu, 0xb515cc6cu, 0xb4f876d7u, 0xb4bb00f8u, + 0xb4c1c093u, 0xb55597edu, 0xb5157ab1u, 0xb47c3addu, 0xb51c59b5u, 0xb4493199u, 0xb477327bu, 0xb4b3ff7au, + 0xb4b98903u, 0xb50b8b35u, 0xb5190d54u, 0xb52a5f14u, 0xb310af31u, 0xb4c6cb47u, 0xb4db6787u, 0xb4f43c1du, + 0xb4297e9fu, 0xb5476482u, 0xb4e28195u, 0xb4d0b44fu, 0xb5138e97u, 0xb4b6ad81u, 0xb4ff2d9au, 0xb508098au, + 0xb4b24854u, 0xb4a6a069u, 0xb4f3ec16u, 0xb50dbfceu, 0xb510a7a6u, 0xb7817b5bu, 0x37fd0178u, 0xb1a02694u, + 0xb881daa5u, 0xb4925aa9u, 0xb4913cd1u, 0xb469ab0au, 0xb49a092fu, 0xb49cf8e8u, 0xb498dcdbu, 0xb47997e7u, + 0xb4af4b29u, 0xb44b252fu, 0xb4e0d954u, 0xb4bd4df5u, 0xb48afb55u, 0xb4d09230u, 0xb4751b24u, 0xb3d80274u, + 0xb469fa16u, 0xb482eb34u, 0xb0d789f8u, 0xb4a3ba03u, 0xb5027564u, 0xb3e65f28u, 0xb4980841u, 0xb4d2de3eu, + 0xb4d46315u, 0xb5193c2bu, 0xb1defb0au, 0xb4cdca38u, 0xb45fad3bu, 0xb50fd428u, 0xb498a30du, 0xb3c1a507u, + 0xb51399ccu, 0xb4bec69du, 0x339e14bfu, 0xb540fbaau, 0xb4df000du, 0xb50b11b1u, 0xb5094f12u, 0xb50983dcu, + 0xb4ac9b78u, 0xb53530f7u, 0x34c88fffu, 0xb4079d0fu, 0xb530673au, 0x3311fc55u, 0xb510ce5fu, 0xb53a90b1u, + 0xb4b48c43u, 0xb40472d3u, 0xb49f0e4bu, 0xb462751du, 0xb4cefb2fu, 0xb541483au, 0xb39c76ecu, 0xb4c9903bu, + 0xb4895b62u, 0xb5475cb5u, 0xb5334c28u, 0x333083ecu, 0xb4e2c5a3u, 0x331f147du, 0x3880b136u, 0xb7df862du, + 0x319a638fu, 0x330efe91u, 0xb4809ecfu, 0xb48d84eeu, 0xb44f8d36u, 0xb42ccd44u, 0xb49ac054u, 0xb46da8d0u, + 0xb485a0e4u, 0xb47ffd92u, 0xb4c191aau, 0xb48aa510u, 0xb451259eu, 0xb422995au, 0xb49cdfaau, 0xb430bc34u, + 0xb48fd63au, 0xb4a96ff6u, 0x339fdcdfu, 0xb46fc012u, 0x321735dfu, 0xb5050974u, 0xb41d459bu, 0xb41519ccu, + 0xb494dc37u, 0xb3414bd2u, 0x333a7021u, 0xb42017d2u, 0x33086509u, 0xb47d11bbu, 0xb4bd6de7u, 0xb4f365ebu, + 0xb35c0ef7u, 0xb4a44a97u, 0xb493ece3u, 0xb49f0c01u, 0xb4407802u, 0x3501becdu, 0x34b410d6u, 0x31bd7603u, + 0x3442fbfeu, 0x34405961u, 0xb3cf91cdu, 0xb422bf7au, 0x3521d27eu, 0xb4aee74au, 0x34421dafu, 0xb505f74cu, + 0x347524fdu, 0x345ac086u, 0x34d1ef32u, 0xb40e069fu, 0x34ae14c3u, 0xb4f34c27u, 0x34012f5eu, 0x34f97667u, + 0x34d333fau, 0x349c3b91u, 0x342e9dafu, 0xb320767bu, 0x353e6cfdu, 0x350efec8u, 0x34932f32u, 0x3749e19au, + 0x37cc1b56u, 0xb1828c5fu, 0x3880b2e6u, 0xb45325ebu, 0xb47acad5u, 0xb477002au, 0xb470bde2u, 0xb459d7d2u, + 0xb428687fu, 0xb4a4e27fu, 0xb4808e28u, 0xb481d373u, 0xb445e3c1u, 0xb42fdd8bu, 0xb4995265u, 0xb484cffcu, + 0xb47a8e6au, 0xb48da917u, 0xb499478fu, 0xb4a844d9u, 0xb4778043u, 0xb499c15fu, 0xb44b59b7u, 0xb4b9c188u, + 0xb47bb9bfu, 0xb4a3a3a4u, 0xb4c723f8u, 0xb4e316f6u, 0xb41d9751u, 0xb329bf80u, 0xb4050e53u, 0xb4d9a632u, + 0xb4517895u, 0xb4c5bdc0u, 0xb44caea9u, 0xb4a1e0f0u, 0xb47797e0u, 0x3434bea4u, 0xb5289d24u, 0xb4637b97u, + 0xb51c5a38u, 0xb436f3f0u, 0x33a89dcbu, 0xb4dac09cu, 0xb4e7808bu, 0xb4dd8f54u, 0xb50463beu, 0xb50864d2u, + 0x34b085e6u, 0xb4bcf400u, 0xb4a215c8u, 0xb4ba8663u, 0xb5517f6du, 0xb56bb996u, 0xb55b5991u, 0xb355b7b7u, + 0xb52e5a96u, 0xb53a4198u, 0xb528f322u, 0xb4c04e1du, 0xb50a6899u, 0xb5171233u, 0xb5059062u, 0xb4d86970u, + 0xb882ada6u, 0xb7b2d9cbu, 0xb22fdffcu, 0xb45a8a26u, 0xb431f30bu, 0xb4376b60u, 0xb436755eu, 0xb42d8a76u, + 0xb410d251u, 0xb42181eeu, 0xb4311068u, 0xb3f41877u, 0xb409cd2bu, 0xb41592a6u, 0xb388188au, 0xb44f92dbu, + 0xb4906454u, 0xb405baceu, 0xb4113a8cu, 0xb49a6697u, 0xb48fc4d7u, 0xb45a0da0u, 0xb476e75fu, 0xb409c5e2u, + 0xb4a48842u, 0xb46ba37au, 0xb4ddece6u, 0xb4d4dc97u, 0xb4b4476cu, 0xb4891f21u, 0xb4c32cf7u, 0xb42432cfu, + 0xb46e6d03u, 0xb42938ddu, 0xb48f97d6u, 0x31804ac1u, 0x32beeff5u, 0xb47f4b2du, 0xb496fac2u, 0x33d9c569u, + 0xb37027d5u, 0x324515b6u, 0xb2995ad7u, 0xb3979beau, 0xb4684e7cu, 0x33b9d923u, 0x33f00209u, 0x341a9f68u, + 0xb4c060ddu, 0x332fea60u, 0xb4849406u, 0xb4b73013u, 0xb20591bfu, 0x33bf541eu, 0xb48e087au, 0xb3b07416u, + 0xb336705eu, 0xb3f7bccfu, 0xb420f54bu, 0x348b6b19u, 0xb4c679a4u, 0x335481bdu, 0xb45da556u, 0xb44629f7u, + 0xb415d063u, 0xb71916a0u, 0x37990683u, 0xb128b816u, 0xb8809d1eu, 0xb40d4eceu, 0xb4430c88u, 0xb409a488u, + 0xb3f15b07u, 0xb41e1efbu, 0xb41c7863u, 0xb3fbc011u, 0xb3c72a40u, 0xb405c1fdu, 0xb2daccccu, 0xb4289de1u, + 0xb47c11ceu, 0xb45abcc2u, 0xb45edfc0u, 0xb42105abu, 0xb466670cu, 0x33de6f74u, 0xb3b14438u, 0xb461fa0au, + 0xb46ccc67u, 0xb3dd97b9u, 0xb3f952e3u, 0xb4cad26du, 0x33455861u, 0xb4a4eeebu, 0xb48449d3u, 0xb3d1a417u, + 0xb44eb0eau, 0xb46a969au, 0xb44f5b86u, 0xb430fcdau, 0x32afba33u, 0x32e6dd21u, 0xb47fd7a1u, 0xb4b98285u, + 0xb49bb97du, 0xb48c4f15u, 0xb4051bd2u, 0xb4aff603u, 0xb4b57f22u, 0xb4e22399u, 0xb4302229u, 0xb5387083u, + 0xb4fd663du, 0xb43745d1u, 0xb48860a5u, 0xb31ab1d1u, 0x348c34feu, 0xb3e37a1du, 0xb4852d3bu, 0xb4e286cbu, + 0xb2730eaeu, 0xb4aba7c4u, 0xb4dc557du, 0x34076995u, 0xb494451bu, 0xb4456d23u, 0xb41bfc06u, 0xb3c9f291u, + 0xb365a0cdu, 0xb4212dabu, 0x38824420u, 0xb780a58du, 0x30b44f94u, 0x328ac646u, 0xb3e5a4c7u, 0xb380a8bau, + 0xb4077e5fu, 0xb40302e1u, 0xb3a23e8bu, 0xb432e1c9u, 0xb4232fe2u, 0xb42a657du, 0xb40afd87u, 0xb45f75c1u, + 0xb3eeb4bfu, 0xb3ced360u, 0xb40fa3dfu, 0x3302fd64u, 0xb3966077u, 0xb40a6fb6u, 0xb4a29d7cu, 0xb4532391u, + 0xb4894bffu, 0x345106b1u, 0xb4602fe8u, 0xb4756f46u, 0xb3d42caau, 0xb30d229cu, 0xb3dc3a4du, 0xb4916786u, + 0xb49b5626u, 0xb4834c97u, 0xb41460f5u, 0xb4a93ff7u, 0xb2bd58a2u, 0xb4f174bdu, 0xb46b16cdu, 0x33a5f41au, + 0xb429a390u, 0xb39bf13au, 0x34672387u, 0xb427d520u, 0xb47ec706u, 0xb5023f65u, 0xb5152d9eu, 0xb3ec5d7du, + 0xb41114afu, 0x33b70083u, 0xb4dbb956u, 0xb44071a2u, 0x3433a6bcu, 0x35106c60u, 0xb496a263u, 0xb2d5ea12u, + 0xb4098afau, 0x3522443du, 0x33afe989u, 0x34dc2708u, 0xb36107edu, 0x347db77fu, 0x31a5b664u, 0xb4f6f653u, + 0xb4a41c45u, 0xb4829638u, 0xb4f71bbau, 0x36b53895u, 0x3747c6c0u, 0xb1251655u, 0x388016b5u, 0xb3b34faeu, + 0xb3b78464u, 0xb3958ca8u, 0xb306b408u, 0xb4073753u, 0xb30260aau, 0xb31de8a0u, 0xb38e753bu, 0xb279edd6u, + 0x33180365u, 0xb38e3692u, 0xb308fbd7u, 0x31c63a63u, 0xb370926cu, 0x327fcde3u, 0x33c21e40u, 0x33bac120u, + 0xb318a358u, 0x32bf8e25u, 0x3420c97bu, 0xb3f3a5d8u, 0x340814e3u, 0xb400a709u, 0x33f80672u, 0x34919758u, + 0xb4190502u, 0xb345368fu, 0xb2fb9ae3u, 0x34711536u, 0x34bb6f58u, 0xb325b7dau, 0x34b6cfd7u, 0x3467bfc2u, + 0x34552046u, 0xb33fe31cu, 0x348244e0u, 0xb415d93du, 0x3438f675u, 0xb33180bcu, 0x333ae1f0u, 0xb4990c2eu, + 0x30006da4u, 0xb3cf3a52u, 0x337150b9u, 0x34a8b210u, 0xb3b7d459u, 0xb4972b91u, 0xb47d50e0u, 0xb4110079u, + 0x34b9ca63u, 0xb3682c86u, 0x33bfa430u, 0x33bdc6a4u, 0x35067dc1u, 0xb4af963cu, 0x3589cbc7u, 0xb275fe54u, + 0x341969fbu, 0x3479c843u, 0x341dc775u, 0x343409d4u, 0xb8833b0fu, 0xb7194fc0u, 0xb1283748u, 0xb39c7bfau, + 0xb3b03357u, 0xb39302c6u, 0xb3a5b235u, 0xb3b494bcu, 0xb39616b3u, 0xb3542736u, 0xb2b0283bu, 0xb3a6dfaeu, + 0xb3aa139bu, 0xb3e09ff2u, 0xb30850b8u, 0xb41f5cbfu, 0xb399c73au, 0xb472d529u, 0xb46a266du, 0xb4a7a467u, + 0xb20f6b55u, 0xb45a78acu, 0xb4764985u, 0xb192e3f5u, 0xb49c2204u, 0xb3c1180fu, 0xb488f2ffu, 0xb35e8c32u, + 0xb3446b10u, 0xb41b9e24u, 0xb4bc5e4au, 0xb4423c4bu, 0xb4a43723u, 0xb4815b7du, 0xb3fdec40u, 0xb35c817bu, + 0xb451875fu, 0xb4c55d84u, 0xb349b12eu, 0xb488ada8u, 0xb3e899fcu, 0xb404e56du, 0xb4a49889u, 0x3401548eu, + 0xb4ebd39cu, 0xb507de3bu, 0xb52eaac9u, 0xb3cb288eu, 0xb52092a6u, 0xb4e52634u, 0x34806ff9u, 0x333ed128u, + 0xb3c21962u, 0x32eb79b8u, 0xb3ca5f93u, 0xb375185bu, 0xb419a19eu, 0xb3e1484au, 0xb4d4d2d6u, 0x334d3b57u, + 0xb4e335ffu, 0xb5334ca5u, 0xb4cda7e0u, 0xb4e8f915u, 0xb49bd1a4u, 0xb65410b3u, 0x36c8fefbu, 0x30c15da4u, + 0xb87ff982u, 0x331f6208u, 0xb20ba920u, 0x3312c968u, 0x34121f46u, 0x338ba90fu, 0xb2983710u, 0x331bdad4u, + 0x34aa31d0u, 0x30e84680u, 0xb370c770u, 0x34b25532u, 0x32f53fe0u, 0xb34fac60u, 0x34bcc0eau, 0x3509bbb0u, + 0xb54ae915u, 0xb3fc6ed0u, 0x350bcd61u, 0xb548d764u, 0x34c99859u, 0x35102768u, 0xb5a562dau, 0xb51f6a78u, + 0x35123918u, 0xb5426bacu, 0x34de6fc8u, 0x35144acau, 0x3545ee5au, 0x34e2932au, 0xb5635b30u, 0xb54490a0u, + 0xb34a4ba0u, 0x351225d6u, 0x34942378u, 0xb605c8edu, 0x35d06419u, 0x35ac5a0cu, 0xb58440acu, 0x35d16cf2u, + 0x349c6a3cu, 0xb6419beau, 0x3524eb94u, 0xb5232886u, 0xb28bbec0u, 0x362ce37cu, 0x34a4b100u, 0x33244660u, + 0x35290ef6u, 0x35b07d6eu, 0x357fc56cu, 0xb4909d5cu, 0xb5ce79bau, 0xb5719838u, 0x35d6992cu, 0x35b28f1fu, + 0x327a3380u, 0xb6110ad3u, 0x3619cbfcu, 0xb56d74d6u, 0x35def332u, 0xb516be60u, 0x388177fbu, 0xb622e285u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, +}; + +} // namespace torch_stft_golden diff --git a/tests/unit/UiDecisionTests.cpp b/tests/unit/UiDecisionTests.cpp new file mode 100644 index 0000000..36a6b95 --- /dev/null +++ b/tests/unit/UiDecisionTests.cpp @@ -0,0 +1,34 @@ +#include + +#include "app/ui/HeroWaveform.h" +#include "app/ui/MainLayout.h" + +using automix::app::HeroWaveform; +using automix::app::QuitStep; +using automix::app::nextQuitStep; + +TEST_CASE("Quit flow confirms a running task before anything else", "[ui][quit]") { + // No task: modified -> save prompt, unmodified -> quit now. + REQUIRE(nextQuitStep(false, false, false) == QuitStep::QuitNow); + REQUIRE(nextQuitStep(false, false, true) == QuitStep::PromptSave); + REQUIRE(nextQuitStep(false, true, false) == QuitStep::QuitNow); + REQUIRE(nextQuitStep(false, true, true) == QuitStep::PromptSave); + // Task running and not yet confirmed: always ask first. + REQUIRE(nextQuitStep(true, false, false) == QuitStep::ConfirmRunningTask); + REQUIRE(nextQuitStep(true, false, true) == QuitStep::ConfirmRunningTask); + // Confirmed: continue into the unsaved-changes flow. + REQUIRE(nextQuitStep(true, true, false) == QuitStep::QuitNow); + REQUIRE(nextQuitStep(true, true, true) == QuitStep::PromptSave); +} + +TEST_CASE("Click-to-import only fires for a left click inside an empty session", "[ui][import]") { + for (int bits = 0; bits < 16; ++bits) { + const bool hasStems = (bits & 1) != 0; + const bool left = (bits & 2) != 0; + const bool clicked = (bits & 4) != 0; + const bool inside = (bits & 8) != 0; + const bool expected = !hasStems && left && clicked && inside; + INFO("hasStems=" << hasStems << " left=" << left << " clicked=" << clicked << " inside=" << inside); + REQUIRE(HeroWaveform::shouldOpenImportOnClick(hasStems, left, clicked, inside) == expected); + } +} diff --git a/tools/build_flatpak.sh b/tools/build_flatpak.sh index 84c17fe..8acdf99 100644 --- a/tools/build_flatpak.sh +++ b/tools/build_flatpak.sh @@ -30,16 +30,22 @@ if ! flatpak remote-ls --user flathub 2>/dev/null | grep -q "org.freedesktop.Pla flatpak remote-add --user --if-not-exists flathub "$FLATHUB_REMOTE_URL" fi +FLATPAK_ARCH="${FLATPAK_ARCH:-${1:-}}" +if [[ "$FLATPAK_ARCH" == "arm64" ]]; then + FLATPAK_ARCH="aarch64" +elif [[ "$FLATPAK_ARCH" == "x64" || "$FLATPAK_ARCH" == "x86_64" ]]; then + FLATPAK_ARCH="x86_64" +fi + EXTRA_ARGS=() if flatpak-builder --help 2>&1 | grep -q -- "--disable-rofiles-fuse"; then EXTRA_ARGS+=(--disable-rofiles-fuse) fi -if [[ "${AUTOMIX_FLATPAK_DISABLE_SANDBOX:-0}" == "1" ]]; then - if flatpak-builder --help 2>&1 | grep -q -- "--disable-sandbox"; then - EXTRA_ARGS+=(--disable-sandbox) - else - echo "Warning: this flatpak-builder does not support --disable-sandbox; continuing without it." - fi +if flatpak-builder --help 2>&1 | grep -q -- "--ccache"; then + EXTRA_ARGS+=(--ccache) +fi +if [[ -n "$FLATPAK_ARCH" ]]; then + EXTRA_ARGS+=(--arch="$FLATPAK_ARCH") fi flatpak-builder \ @@ -51,7 +57,12 @@ flatpak-builder \ "$BUILD_DIR" \ "$MANIFEST" +BUILD_BUNDLE_ARGS=() +if [[ -n "$FLATPAK_ARCH" ]]; then + BUILD_BUNDLE_ARGS+=(--arch="$FLATPAK_ARCH") +fi + BUNDLE_PATH="$DIST_DIR/AutoMixMaster.flatpak" -flatpak build-bundle "$REPO_DIR" "$BUNDLE_PATH" "$APP_ID" +flatpak build-bundle "${BUILD_BUNDLE_ARGS[@]}" "$REPO_DIR" "$BUNDLE_PATH" "$APP_ID" echo "Built Flatpak bundle: $BUNDLE_PATH" diff --git a/tools/check_repo_hygiene.py b/tools/check_repo_hygiene.py new file mode 100644 index 0000000..7fbaaf7 --- /dev/null +++ b/tools/check_repo_hygiene.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +""" +tools/check_repo_hygiene.py + +Repository hygiene verification script. +Ensures no forbidden files, sensitive IP patterns, private tokens, +or developer notes are committed or staged in the repository. +Also verifies GitHub Actions workflow pinning and failure mask rules. +""" + +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path + +# Paths/patterns that must NEVER exist in the repo +FORBIDDEN_PATH_PATTERNS = [ + re.compile(r"^diff_output\.txt$", re.IGNORECASE), + re.compile(r"CLAUDE\.md$", re.IGNORECASE), + re.compile(r"AGENTS\.md$", re.IGNORECASE), + re.compile(r"^\.omo(/|$)", re.IGNORECASE), + re.compile(r"^assets/phaselimiter/assets(/|$)", re.IGNORECASE), + # Under docs/, ONLY model-licensing-audit.json is permitted + re.compile(r"^docs/(?!model-licensing-audit\.json$).+", re.IGNORECASE), + # Extended patterns from Round 5 + re.compile(r"(^|/)\.superpowers(/|$)", re.IGNORECASE), + re.compile(r"(^|/)\.anchor(/|$)", re.IGNORECASE), + re.compile(r"(^|/)memory(/|$)", re.IGNORECASE), + re.compile(r"(^|/)\.claude(/|$)", re.IGNORECASE), + re.compile(r"(^|/)\.gemini(/|$)", re.IGNORECASE), + re.compile(r"(^|/)\.opencode(/|$)", re.IGNORECASE), + re.compile(r"(^|/)\.codex(/|$)", re.IGNORECASE), + re.compile(r"(^|/)GEMINI\.md$", re.IGNORECASE), + re.compile(r"(^|/)\.cursorrules$", re.IGNORECASE), + re.compile(r"(^|/)[^/]*handoff[^/]*\.md$", re.IGNORECASE), + re.compile(r"(^|/)job-logs.*\.txt$", re.IGNORECASE), + re.compile(r"\.jsonl$", re.IGNORECASE), +] + +# Sensitive content regex patterns +SENSITIVE_CONTENT_PATTERNS = [ + ("Private Key", re.compile(r"-----BEGIN (RSA|OPENSSH|EC|DSA) PRIVATE KEY-----")), + ("GitHub Personal Access Token (classic)", re.compile(r"\bghp_[A-Za-z0-9]{36}\b")), + ("GitHub Fine-Grained PAT", re.compile(r"\bgithub_pat_[A-Za-z0-9_]{82}\b")), + ("Slack Token", re.compile(r"\bxox[baprs]-[A-Za-z0-9-]+\b")), + ("Local Network IP", re.compile(r"\b192\.168\.0\.\d{1,3}\b")), + ("Windows User Path", re.compile(r"[C-Zc-z]:[/\\]Users[/\\](?!runneradmin|Default|Public)[A-Za-z0-9_]+")), +] + +# Files/extensions exempt from binary or content inspection +EXEMPT_EXTENSIONS = { + ".onnx", ".png", ".jpg", ".jpeg", ".ico", ".wav", ".mp3", ".zip", ".tar", + ".gz", ".xz", ".exe", ".dll", ".lib", ".so", ".dylib", ".bin", ".dat" +} + +EXEMPT_PATHS = { + "tools/check_repo_hygiene.py", # self-exempt for pattern literals + "tests/tools/test_check_repo_hygiene.py", +} + + +def get_git_files(staged_only=False): + """Retrieve list of files from git.""" + cmd = ["git", "diff", "--cached", "--name-only"] if staged_only else ["git", "ls-files"] + result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + return [line.strip() for line in result.stdout.splitlines() if line.strip()] + + +def check_path_hygiene(file_path): + """Check if a path violates repository hygiene rules.""" + normalized = file_path.replace("\\", "/").lstrip("/") + for pat in FORBIDDEN_PATH_PATTERNS: + if pat.search(normalized): + return f"FORBIDDEN: {file_path} (matched '{pat.pattern}')" + return None + + +def check_file_content(file_path): + """Scan a text file for forbidden content patterns.""" + normalized = file_path.replace("\\", "/") + if normalized in EXEMPT_PATHS: + return [] + + _, ext = os.path.splitext(file_path) + if ext.lower() in EXEMPT_EXTENSIONS: + return [] + + if not os.path.isfile(file_path): + return [] + + findings = [] + try: + with open(file_path, "r", encoding="utf-8", errors="replace") as f: + for line_no, line in enumerate(f, start=1): + for name, pat in SENSITIVE_CONTENT_PATTERNS: + if pat.search(line): + findings.append(f"{file_path}:{line_no}: detected {name}") + except Exception as e: + findings.append(f"{file_path}: unable to read file: {e}") + + return findings + + +def check_workflow_file(file_path): + """Check a GitHub Actions workflow file for failure masks and unpinned actions.""" + findings = [] + normalized_path = os.path.normpath(file_path).replace("\\", "/") + try: + with open(file_path, "r", encoding="utf-8", errors="replace") as f: + for line_no, line in enumerate(f, start=1): + # 1. Mask detection: || true, continue-on-error: true, if: false + if (re.search(r"\|\|\s*true\b", line) or + re.search(r"continue-on-error:\s*true\b", line, re.IGNORECASE) or + re.search(r"^\s*if:\s*false\b", line, re.IGNORECASE)): + findings.append(f"MASK: {normalized_path}:{line_no}: {line.strip()}") + + # 2. Unpinned action detection: uses: owner/repo@ref + m = re.search(r"uses:\s*([^\s#]+)", line) + if m: + action_ref = m.group(1) + if not action_ref.startswith("./"): + if "@" in action_ref: + _, ref = action_ref.split("@", 1) + if not re.fullmatch(r"[0-9a-fA-F]{40}", ref): + findings.append(f"UNPINNED: {normalized_path}:{line_no}: {action_ref}") + else: + findings.append(f"UNPINNED: {normalized_path}:{line_no}: {action_ref}") + except Exception as e: + findings.append(f"ERROR: {normalized_path}: unable to read file: {e}") + + return findings + + +def check_workflows_dir(workflows_dir): + """Scan all YAML workflow files in workflows_dir.""" + p = Path(workflows_dir) + if not p.is_dir(): + return [f"ERROR: Workflows directory '{workflows_dir}' not found"] + + findings = [] + yaml_files = sorted(list(p.glob("*.yml")) + list(p.glob("*.yaml"))) + for yml in yaml_files: + findings.extend(check_workflow_file(str(yml))) + return findings + + +def main(): + parser = argparse.ArgumentParser(description="Check repository hygiene and scan for sensitive content.") + parser.add_argument("--staged", action="store_true", help="Scan only git staged files.") + parser.add_argument("--path", nargs="*", help="Specific file paths to scan.") + parser.add_argument("--paths-from", type=str, help="File containing newline-separated paths to scan.") + parser.add_argument("--check-workflows", action="store_true", help="Check workflows for failure masks and unpinned actions.") + parser.add_argument("--workflows-dir", type=str, default=".github/workflows", help="Directory containing workflow files to check.") + args = parser.parse_args() + + has_errors = False + + if args.check_workflows: + wf_errors = check_workflows_dir(args.workflows_dir) + if wf_errors: + has_errors = True + for err in wf_errors: + print(err) + + files_to_check = None + if args.paths_from: + with open(args.paths_from, "r", encoding="utf-8") as f: + files_to_check = [line.strip() for line in f if line.strip()] + elif args.path: + files_to_check = args.path + elif not args.check_workflows: + files_to_check = get_git_files(staged_only=args.staged) + + if files_to_check is not None: + path_errors = [] + content_errors = [] + + for file_path in files_to_check: + err = check_path_hygiene(file_path) + if err: + path_errors.append(err) + + content_errs = check_file_content(file_path) + content_errors.extend(content_errs) + + if path_errors: + has_errors = True + print("[ERROR] Hygiene: Forbidden paths found:", file=sys.stderr) + for err in path_errors: + print(f" - {err}", file=sys.stderr) + + if content_errors: + has_errors = True + print("[ERROR] Hygiene: Sensitive patterns found in file contents:", file=sys.stderr) + for err in content_errors: + print(f" - {err}", file=sys.stderr) + + if has_errors: + sys.exit(1) + + if args.check_workflows and files_to_check is None: + print("[OK] Workflow hygiene check passed: 0 masks, 0 unpinned actions.") + else: + print(f"[OK] Repository hygiene check passed: {len(files_to_check or [])} files scanned, 0 violations.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/tools/commands/ModelCommands.cpp b/tools/commands/ModelCommands.cpp index 754a63e..dcae4d5 100644 --- a/tools/commands/ModelCommands.cpp +++ b/tools/commands/ModelCommands.cpp @@ -2,14 +2,21 @@ #include "commands/DevToolsUtils.h" #include +#include #include #include #include #include "ai/FeatureSchema.h" +#include "ai/GpuProvider.h" +#include "ai/GpuRuntimePack.h" #include "ai/HuggingFaceModelHub.h" #include "ai/ModelPackLoader.h" +#include "ai/ModelStorage.h" #include "ai/OnnxModelInference.h" +#include "ai/OnnxTensorInference.h" +#include "ai/OrtRuntime.h" +#include "ai/StemSeparator.h" #include "renderers/ExternalLimiterRenderer.h" #include "util/LameDownloader.h" @@ -395,7 +402,7 @@ int commandModelInstall(const CommandArgs& args) { } automix::ai::HubInstallOptions options; - options.destinationRoot = argValue(args, "--dest").value_or("assets/modelhub"); + options.destinationRoot = argValue(args, "--dest").value_or(automix::ai::defaultModelHubRoot().string()); options.overwrite = hasFlag(args, "--force"); options.downloadReadme = !hasFlag(args, "--no-readme"); if (const auto tokenEnvArg = argValue(args, "--token-env"); tokenEnvArg.has_value()) { @@ -435,7 +442,7 @@ int commandModelInstall(const CommandArgs& args) { } int commandModelHealth(const CommandArgs& args) { - const std::filesystem::path root = argValue(args, "--root").value_or("assets/modelhub"); + const std::filesystem::path root = argValue(args, "--root").value_or(automix::ai::defaultModelHubRoot().string()); const auto registryPath = root / "install_registry.json"; const auto registry = loadJsonFile(registryPath); if (!registry.has_value() || !registry->is_array()) { @@ -510,6 +517,457 @@ int commandModelHealth(const CommandArgs& args) { return failed == 0 ? 0 : 1; } +// separate --mix --out [--pack ] [--tensor] [--provider auto|cpu|cuda] [--cancel-after-ms N] [--json] +// Runs StemSeparator exactly as single-mix import does, outside the UI. +int commandSeparate(const std::vector& args) { + const auto mixArg = argValue(args, "--mix"); + const auto outArg = argValue(args, "--out"); + if (!mixArg.has_value() || !outArg.has_value()) { + std::cerr << "Usage: separate --mix --out [--pack ] [--tensor] [--provider auto|cpu|cuda] [--cancel-after-ms N] [--json]\n"; + return 2; + } + + automix::ai::StemSeparator separator(argValue(args, "--pack").value_or("assets/models/stem-separator")); + automix::ai::StemSeparator::SeparationOptions options; + options.useTensorModel = hasFlag(args, "--tensor"); + options.executionProvider = argValue(args, "--provider").value_or("auto"); + const bool jsonOutput = hasFlag(args, "--json"); + // --cancel-after-ms N: request cancellation N ms after start, to measure how + // quickly a running separation actually stops. + const auto started = std::chrono::steady_clock::now(); + if (const auto cancelAfter = parseIntArg(args, "--cancel-after-ms"); cancelAfter.has_value()) { + const auto deadline = started + std::chrono::milliseconds(*cancelAfter); + options.cancelRequested = [deadline] { return std::chrono::steady_clock::now() >= deadline; }; + } + if (options.useTensorModel && !jsonOutput) { + options.tensorProgress = [](const int done, const int total) { + std::cout << " chunk " << done << "/" << total << "\n" << std::flush; + }; + } + + const auto result = separator.separate(*mixArg, *outArg, options); + const double seconds = + std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + + nlohmann::json stems = nlohmann::json::array(); + for (const auto& stem : result.stems) { + stems.push_back({{"name", stem.name}, {"path", stem.filePath}, {"role", automix::domain::toString(stem.role)}}); + } + const nlohmann::json payload = { + {"success", result.success}, + {"usedModel", result.usedModel}, + {"cancelled", result.cancelled}, + {"seconds", seconds}, + {"stems", stems}, + {"energyLeakage", result.qaMetrics.energyLeakage}, + {"residualDistortion", result.qaMetrics.residualDistortion}, + {"transientRetention", result.qaMetrics.transientRetention}, + {"log", result.logMessage}, + }; + if (jsonOutput) { + std::cout << payload.dump(2) << "\n"; + } else { + std::cout << "Separation: success=" << (result.success ? "yes" : "no") + << " usedModel=" << (result.usedModel ? "yes" : "no") << " seconds=" << seconds << "\n"; + for (const auto& stem : result.stems) { + std::cout << " " << automix::domain::toString(stem.role) << " -> " << stem.filePath << "\n"; + } + std::cout << " " << result.logMessage << "\n"; + } + return result.success ? 0 : 1; +} + +// gpu-runtime status|install|remove|upgrade-models [--root ] [--hub ] +// The per-user NVIDIA CUDA libraries the CUDA execution provider loads. +int commandGpuRuntime(const std::vector& args) { + namespace pack = automix::ai::GpuRuntimePack; + const auto has = [&args](const char* word) { return std::find(args.begin(), args.end(), word) != args.end(); }; + const std::string action = has("install") ? "install" + : has("remove") ? "remove" + : has("upgrade-models") ? "upgrade-models" + : (has("status") || args.size() <= 1) ? "status" + : ""; + const std::filesystem::path root = argValue(args, "--root").value_or(pack::defaultRoot().string()); + if (action == "status") { + const auto adapter = pack::largestNvidiaAdapter(); + const auto driver = adapter.has_value() && adapter->driverMajor.has_value() + ? std::to_string(*adapter->driverMajor) + "." + + (*adapter->driverMinor < 10 ? "0" : "") + std::to_string(*adapter->driverMinor) + : std::string("unknown"); + std::cout << "GPU runtime " << pack::version() << " at " << root.string() << "\n" + << " installed: " << (pack::isInstalled(root) ? "yes" : "no") << "\n" + << " largest NVIDIA adapter: " + << (adapter.has_value() ? std::to_string(adapter->dedicatedBytes / (1024 * 1024)) + " MiB, driver " + driver + : std::string("none")) + << " (CUDA 13 needs >= " << pack::kMinimumDriverMajor << ")\n" + << " runtime build has CUDA: " << (automix::ai::runtimeReportsProvider("cuda") ? "yes" : "no") << "\n" + << " preload: " << (pack::preload(root) ? "ok" : "not loaded") << "\n"; + std::string provider; + const bool gpu = automix::ai::gpuTensorSessionAvailable(&provider); + std::cout << " GPU tensor session: " << (gpu ? provider : std::string("unavailable")) << "\n"; + std::cout << " ORT diagnostics: " << automix::ai::OrtRuntime::instance().diagnostics() << "\n"; + return 0; + } + if (action == "install") { + int lastDecile = -1; + const auto result = pack::install(root, [&](std::uint64_t done, std::uint64_t total) { + const int percent = total > 0 ? static_cast(done * 100 / total) : 0; + if (percent / 10 != lastDecile) { + lastDecile = percent / 10; + std::cout << " " << percent << "%\n" << std::flush; + } + return true; + }); + std::cout << result.message << "\n"; + if (result.success) { + automix::ai::invalidateTensorProviderProbeCache(); + } + return result.success ? 0 : 1; + } + if (action == "remove") { + const auto removed = pack::uninstall(root); + automix::ai::invalidateTensorProviderProbeCache(); + std::cout << removed.message << "\n"; + return removed.removedNow ? 0 : 3; + } + if (action == "upgrade-models") { + const std::filesystem::path hub = argValue(args, "--hub").value_or(automix::ai::defaultModelHubRoot().string()); + const auto upgraded = automix::ai::upgradeBsRoformerForGpu(hub); + std::cout << (upgraded.has_value() ? upgraded->message : std::string("Nothing to upgrade.")) << "\n"; + return !upgraded.has_value() || upgraded->success ? 0 : 1; + } + std::cerr << "Usage: gpu-runtime status|install|remove|upgrade-models [--root ] [--hub ]\n"; + return 2; +} + +// model bench --provider [--pack ] [--runs N] [--warmup N] [--json] [--out ] +// [--skip-cpu-baseline] +// Tensor packs print one progress line per run to stderr. --skip-cpu-baseline skips the CPU +// reference (no parity check, no speedup); `--provider cpu` runs the CPU model once, not twice. +// Exit codes: 0 ok, 1 pack load failure, 3 model load/execution failure, +// 4 the model ran on a different provider than the one requested, +// 5 one or more inference calls failed (latencies and parity are invalid). +int commandModelBench(const CommandArgs& args) { + const auto packArg = argValue(args, "--pack").value_or("assets/models/demo-mix-v1"); + const auto providerArg = argValue(args, "--provider").value_or("auto"); + const int warmupRuns = std::clamp(parseIntArg(args, "--warmup").value_or(2), 0, 50); + const int timedRuns = std::clamp(parseIntArg(args, "--runs").value_or(10), 1, 1000); + const bool jsonOutput = hasFlag(args, "--json"); + const bool skipCpuBaseline = hasFlag(args, "--skip-cpu-baseline"); + + const std::filesystem::path packDir(packArg); + automix::ai::ModelPackLoader loader; + const auto maybePack = loader.load(packDir); + if (!maybePack.has_value()) { + std::cerr << "Model benchmark failed: could not load model pack at " << packDir.string() << "\n"; + return 1; + } + const auto& pack = maybePack.value(); + + std::vector provLatencies; + std::vector cpuLatencies; + std::string actualProvider = "unknown"; + std::string providerBackendDiagnostics; + double maxDelta = 0.0; + const bool isTensor = pack.tensorContract.has_value(); + // A run that throws inside ORT still returns quickly with usedModel=false; its + // latency is meaningless, so every call is checked and any failure voids the bench. + int failedRuns = 0; + bool parityChecked = false; + std::string firstFailure; + const auto checkRun = [&](const auto& result) { + if (!result.usedModel) { + if (failedRuns == 0) firstFailure = result.logMessage; + ++failedRuns; + } + }; + + const bool requestedIsCpu = + automix::ai::gpu::canonicalProviderName(providerArg) == automix::ai::gpu::kProviderCpu; + + // The CPU reference feeds parity and speedup. Requesting cpu makes the reference the measurement. + const bool runCpuBaseline = !skipCpuBaseline || requestedIsCpu; + + if (isTensor) { + const auto timedRun = [&](auto& inference, const auto& bindings, const char* label, int index, int total, + std::vector& latencies) { + const auto start = std::chrono::steady_clock::now(); + auto result = inference.run(bindings); + const double ms = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + checkRun(result); + latencies.push_back(ms); + // stderr keeps --json stdout parseable. + std::cerr << "[bench] " << label << " " << index << "/" << total << ": " << ms << " ms" << std::endl; + return result; + }; + const auto makeBindings = [](const automix::ai::OnnxTensorInference& inference) { + std::vector bindings; + for (const auto& spec : inference.inputSpecs()) { + size_t count = automix::ai::elementCount(spec).value_or(1024); + bindings.push_back(automix::ai::TensorBinding{ + .expected = spec, + .data = std::vector(count, 0.1f), + }); + } + return bindings; + }; + + std::vector bindings; + automix::ai::TensorInferenceResult cpuResult; + automix::ai::OnnxTensorInference cpuInference; + if (runCpuBaseline) { + cpuInference.setTensorContract(pack.tensorContract); + cpuInference.setExecutionProvider("cpu"); + if (!cpuInference.loadModel(pack.rootPath / pack.modelFile)) { + std::cerr << "ERROR: could not load tensor model on CPU: " << cpuInference.backendDiagnostics() << "\n"; + return 3; + } + bindings = makeBindings(cpuInference); + for (int i = 0; i < warmupRuns; ++i) { + checkRun(cpuInference.run(bindings)); + std::cerr << "[bench] cpu warmup " << (i + 1) << "/" << warmupRuns << " done" << std::endl; + } + for (int i = 0; i < timedRuns; ++i) { + cpuResult = timedRun(cpuInference, bindings, "cpu", i + 1, timedRuns, cpuLatencies); + } + } + + if (requestedIsCpu) { + actualProvider = "cpu"; + providerBackendDiagnostics = cpuInference.backendDiagnostics(); + provLatencies = cpuLatencies; + } else { + automix::ai::OnnxTensorInference provInference; + provInference.setTensorContract(pack.tensorContract); + provInference.setExecutionProvider(providerArg); + // The bench deliberately ignores the pack's allow-list so it measures the requested provider. + provInference.setGpuProviderAllowList({}); + if (!provInference.loadModel(pack.rootPath / pack.modelFile)) { + std::cerr << "ERROR: could not load tensor model with provider '" << providerArg + << "': " << provInference.backendDiagnostics() << "\n"; + return 3; + } + actualProvider = provInference.activeExecutionProvider(); + providerBackendDiagnostics = provInference.backendDiagnostics(); + if (bindings.empty()) { + bindings = makeBindings(provInference); + } + + for (int i = 0; i < warmupRuns; ++i) { + checkRun(provInference.run(bindings)); + std::cerr << "[bench] " << actualProvider << " warmup " << (i + 1) << "/" << warmupRuns << " done" + << std::endl; + } + automix::ai::TensorInferenceResult provResult; + for (int i = 0; i < timedRuns; ++i) { + provResult = timedRun(provInference, bindings, actualProvider.c_str(), i + 1, timedRuns, provLatencies); + } + + for (size_t i = 0; i < cpuResult.outputs.size() && i < provResult.outputs.size(); ++i) { + const auto& cOut = cpuResult.outputs[i]; + const auto& pOut = provResult.outputs[i]; + const size_t sz = std::min(cOut.data.size(), pOut.data.size()); + for (size_t j = 0; j < sz; ++j) { + double diff = std::abs(static_cast(cOut.data[j]) - static_cast(pOut.data[j])); + if (diff > maxDelta) maxDelta = diff; + } + } + } + parityChecked = runCpuBaseline; + } else { + automix::ai::OnnxModelInference cpuInference; + cpuInference.setExecutionProviderPreference("cpu"); + if (!cpuInference.loadModel(pack.rootPath / pack.modelFile)) { + std::cerr << "ERROR: could not load model on CPU: " << cpuInference.backendDiagnostics() << "\n"; + return 3; + } + const size_t featureCount = pack.inputFeatureCount.value_or(automix::ai::FeatureSchemaV1::featureCount()); + const automix::ai::InferenceRequest request{ + .task = taskFromModelType(pack.type), + .features = deterministicFeatures(featureCount), + }; + + const auto cpuProbe = cpuInference.run(request); + if (!cpuProbe.usedModel || !cpuInference.usingNativeSession()) { + std::cerr << "ERROR: model did not execute (deterministic fallback); nothing measured\n"; + return 3; + } + + for (int i = 0; i < warmupRuns; ++i) { + checkRun(cpuInference.run(request)); + } + automix::ai::InferenceResult cpuResult; + for (int i = 0; i < timedRuns; ++i) { + const auto start = std::chrono::steady_clock::now(); + cpuResult = cpuInference.run(request); + const auto end = std::chrono::steady_clock::now(); + checkRun(cpuResult); + cpuLatencies.push_back(std::chrono::duration(end - start).count()); + } + + automix::ai::OnnxModelInference provInference; + provInference.setExecutionProviderPreference(providerArg); + if (!provInference.loadModel(pack.rootPath / pack.modelFile)) { + std::cerr << "ERROR: could not load model with provider '" << providerArg + << "': " << provInference.backendDiagnostics() << "\n"; + return 3; + } + actualProvider = provInference.activeExecutionProvider(); + providerBackendDiagnostics = provInference.backendDiagnostics(); + + const auto provProbe = provInference.run(request); + if (!provProbe.usedModel || !provInference.usingNativeSession()) { + std::cerr << "ERROR: model did not execute (deterministic fallback); nothing measured\n"; + return 3; + } + + for (int i = 0; i < warmupRuns; ++i) { + checkRun(provInference.run(request)); + } + automix::ai::InferenceResult provResult; + for (int i = 0; i < timedRuns; ++i) { + const auto start = std::chrono::steady_clock::now(); + provResult = provInference.run(request); + const auto end = std::chrono::steady_clock::now(); + checkRun(provResult); + provLatencies.push_back(std::chrono::duration(end - start).count()); + } + + for (const auto& [k, v] : cpuResult.outputs) { + auto it = provResult.outputs.find(k); + if (it != provResult.outputs.end()) { + double diff = std::abs(static_cast(v) - static_cast(it->second)); + if (diff > maxDelta) maxDelta = diff; + } + } + } + + auto computeMedian = [](std::vector v) -> double { + if (v.empty()) return 0.0; + std::sort(v.begin(), v.end()); + if (v.size() % 2 == 1) return v[v.size() / 2]; + return 0.5 * (v[v.size() / 2 - 1] + v[v.size() / 2]); + }; + const double medianMs = computeMedian(provLatencies); + const double cpuMedianMs = computeMedian(cpuLatencies); + const double minMs = provLatencies.empty() ? 0.0 : *std::min_element(provLatencies.begin(), provLatencies.end()); + const double maxMs = provLatencies.empty() ? 0.0 : *std::max_element(provLatencies.begin(), provLatencies.end()); + + const bool providerMatches = automix::ai::gpu::benchProviderMatches(providerArg, actualProvider); + const bool allRunsSucceeded = failedRuns == 0; + if (!isTensor) parityChecked = true; + const bool shouldReportSpeedup = allRunsSucceeded && parityChecked && + (automix::ai::gpu::canonicalProviderName(actualProvider) != automix::ai::gpu::kProviderCpu) && providerMatches; + const double p90Ms = automix::ai::gpu::nearestRankPercentile(provLatencies, 0.9); + const double cpuP90Ms = automix::ai::gpu::nearestRankPercentile(cpuLatencies, 0.9); + const auto canonicalRequested = automix::ai::gpu::canonicalProviderName(providerArg); + const bool outsideAllowList = + canonicalRequested != "auto" && canonicalRequested != automix::ai::gpu::kProviderCpu && !pack.gpuProviders.empty() && + std::none_of(pack.gpuProviders.begin(), pack.gpuProviders.end(), [&](const std::string& allowed) { + return automix::ai::gpu::canonicalProviderName(allowed) == canonicalRequested; + }); + const std::optional speedup = shouldReportSpeedup && (medianMs > 1e-6) + ? std::optional(cpuMedianMs / medianMs) + : std::nullopt; + + const auto ortDiagnostics = automix::ai::OrtRuntime::instance().diagnostics(); + + nlohmann::json payload = { + {"model", pack.id}, + {"requestedProvider", providerArg}, + {"actualProvider", actualProvider}, + {isTensor ? "loaded" : "usedModel", true}, + {"diagnostics", ortDiagnostics}, + {"warmupRuns", warmupRuns}, + {"timedRuns", timedRuns}, + {"medianLatencyMs", medianMs}, + {"minLatencyMs", minMs}, + {"maxLatencyMs", maxMs}, + {"parityChecked", parityChecked}, + {"latenciesMs", provLatencies}, + {"passedParity", allRunsSucceeded && parityChecked && maxDelta < 1e-3}, + {"failedRuns", failedRuns}, + {"providerMatches", providerMatches}, + {"packAllowList", pack.gpuProviders}, + {"p90LatencyMs", p90Ms}, + }; + if (parityChecked) { + payload["maxDeltaVsCpu"] = maxDelta; + payload["cpuMedianLatencyMs"] = cpuMedianMs; + payload["cpuP90LatencyMs"] = cpuP90Ms; + } + + if (speedup.has_value()) { + payload["speedupVsCpu"] = *speedup; + } + + if (const auto outArg = argValue(args, "--out"); outArg.has_value()) { + writeJsonFile(*outArg, payload); + std::cout << "Model benchmark report: " << *outArg << "\n"; + } + + if (jsonOutput) { + std::cout << payload.dump(2) << "\n"; + } else { + std::cout << "Model benchmark: " << pack.id << "\n"; + std::cout << " Requested provider: " << providerArg << "\n"; + std::cout << " Actual provider: " << actualProvider << "\n"; + std::cout << " " << (isTensor ? "Loaded" : "Used real model") << ": true\n"; + std::cout << " ORT diagnostics: " << ortDiagnostics << "\n"; + std::cout << " Warm-up runs: " << warmupRuns << "\n"; + std::cout << " Timed runs: " << timedRuns << "\n"; + std::cout << " Median latency: " << medianMs << " ms (min: " << minMs << " ms, max: " << maxMs << " ms)\n"; + std::cout << " p90: " << p90Ms << " ms\n"; + if (parityChecked) { + std::cout << " CPU median latency: " << cpuMedianMs << " ms\n"; + } else { + std::cout << " CPU baseline: skipped (no parity check, no speedup)\n"; + } + std::cout << " Provider match: " << (providerMatches ? "yes" : "no") << "\n"; + if (outsideAllowList) { + std::cout << " Note: '" << providerArg << "' is outside this pack's allow-list; measured anyway\n"; + } + if (speedup.has_value()) { + std::cout << " Speedup vs CPU: " << *speedup << "x\n"; + } + if (parityChecked) { + std::cout << " Max |Δ| vs CPU: " << maxDelta << "\n"; + } + std::cout << " Failed runs: " << failedRuns << "\n"; + } + + if (!allRunsSucceeded) { + std::cerr << "ERROR: " << failedRuns << " inference call(s) failed on '" << actualProvider + << "'; latencies and parity are invalid. First failure: " << firstFailure << "\n"; + return 5; + } + + if (!providerMatches) { + std::cerr << "ERROR: requested provider '" << providerArg << "' but the model ran on '" << actualProvider << "'. " + << providerBackendDiagnostics << "\n"; + return 4; + } + + return 0; +} + +int commandModel(const CommandArgs& args) { + if (args.size() > 1) { + const std::string& sub = args[1]; + CommandArgs subArgs; + subArgs.push_back(sub); + for (size_t i = 2; i < args.size(); ++i) { + subArgs.push_back(args[i]); + } + if (sub == "bench") return commandModelBench(subArgs); + if (sub == "browse") return commandModelBrowse(subArgs); + if (sub == "install") return commandModelInstall(subArgs); + if (sub == "health") return commandModelHealth(subArgs); + } + std::cerr << "Usage: model bench|browse|install|health [options]\n"; + return 2; +} + } // namespace void registerModelCommands(automix::devtools::CommandRegistry& registry) { @@ -521,7 +979,11 @@ void registerModelCommands(automix::devtools::CommandRegistry& registry) { registry.add("validate-modelpack", commandValidateModelPack); registry.add("validate-external-limiter", commandValidateExternalLimiter); registry.add("external-limiter-compat", commandExternalLimiterCompat); + registry.add("model", commandModel); + registry.add("model-bench", commandModelBench); registry.add("model-browse", commandModelBrowse); registry.add("model-install", commandModelInstall); registry.add("model-health", commandModelHealth); + registry.add("separate", commandSeparate); + registry.add("gpu-runtime", commandGpuRuntime); } diff --git a/tools/package_linux.sh b/tools/package_linux.sh index 22a82d9..9524423 100644 --- a/tools/package_linux.sh +++ b/tools/package_linux.sh @@ -97,11 +97,12 @@ if [[ $SKIP_BUILD -eq 0 ]]; then if command -v ccache >/dev/null 2>&1; then ccache_launcher=(-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache) fi - cmake -S "$REPO_ROOT" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DBUILD_TOOLS=OFF "${ccache_launcher[@]}" + cmake -S "$REPO_ROOT" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release -DENABLE_ONNX=ON -DAUTOMIX_FETCH_ORT=ON -DAUTOMIX_ORT_FLAVOR=cpu -DBUILD_TESTING=OFF -DBUILD_TOOLS=OFF "${ccache_launcher[@]}" cmake --build "$BUILD_DIR" --config Release --target AutoMixMasterApp --parallel 3 if command -v ccache >/dev/null 2>&1; then ccache --show-stats fi + fi BINARY_PATH="" @@ -179,6 +180,13 @@ build_deb() { cp -a "$SOURCE_ASSETS_PATH" "$app_root/assets" sanitize_linux_assets "$app_root" + # Bundle ONNX Runtime and WebGPU plugin libraries if present + if [[ -d "$BUILD_DIR/lib" ]]; then + cp -a "$BUILD_DIR/lib" "$app_root/lib" + elif [[ -d "$(dirname "$BINARY_PATH")/lib" ]]; then + cp -a "$(dirname "$BINARY_PATH")/lib" "$app_root/lib" + fi + install -Dm755 /dev/null "$stage_dir/usr/bin/$PACKAGE_NAME" cat > "$stage_dir/usr/bin/$PACKAGE_NAME" <<'WRAPPER' #!/bin/sh @@ -200,7 +208,8 @@ Section: sound Priority: optional Architecture: $deb_arch Maintainer: AutoMixMaster -Depends: libc6 (>= 2.31), libstdc++6 (>= 11), libgcc-s1, libasound2, libfontconfig1, libfreetype6, libexpat1, zlib1g, libbz2-1.0, libpng16-16, libbrotli1 +Depends: libc6 (>= 2.31), libstdc++6 (>= 11), libgcc-s1, libasound2, libfontconfig1, libfreetype6, libexpat1, zlib1g, libbz2-1.0, libpng16-16, libbrotli1, libcurl4t64 | libcurl4 +Recommends: libvulkan1 Installed-Size: $installed_size Description: Deterministic auto mix and mastering desktop app AutoMixMaster provides one-click mix/master workflows, batch processing, @@ -242,12 +251,22 @@ build_appimage() { cp -a "$SOURCE_ASSETS_PATH" "$stage_dir/usr/bin/assets" sanitize_linux_assets "$stage_dir/usr/bin" + # Bundle ONNX Runtime and WebGPU plugin libraries beside the binary or in usr/lib + if [[ -d "$BUILD_DIR/lib" ]]; then + mkdir -p "$stage_dir/usr/bin/lib" + cp -a "$BUILD_DIR/lib/." "$stage_dir/usr/bin/lib/" + elif [[ -d "$(dirname "$BINARY_PATH")/lib" ]]; then + mkdir -p "$stage_dir/usr/bin/lib" + cp -a "$(dirname "$BINARY_PATH")/lib/." "$stage_dir/usr/bin/lib/" + fi + install -Dm644 "$REPO_ROOT/packaging/linux/automixmaster.svg" "$stage_dir/$PACKAGE_NAME.svg" install -Dm644 "$REPO_ROOT/packaging/linux/automixmaster.svg" "$stage_dir/usr/share/icons/hicolor/scalable/apps/$PACKAGE_NAME.svg" create_desktop_file "$stage_dir/$PACKAGE_NAME.desktop" "$APP_NAME" "$PACKAGE_NAME" cp "$stage_dir/$PACKAGE_NAME.desktop" "$stage_dir/usr/share/applications/$PACKAGE_NAME.desktop" + ln -sf "$PACKAGE_NAME.svg" "$stage_dir/.DirIcon" cat > "$stage_dir/AppRun" <<'APPRUN' @@ -307,17 +326,7 @@ APPRUN } local appimagetool - - # In QEMU-emulated containers the AppImage runtime cannot be executed - # (execve fails with "Exec format error"). The CI workflow signals this - # via AUTOMIX_QEMU_EMULATED=1 so we can cross-build with the x86_64 - # appimagetool, whose static binary runs natively on the host kernel. - if [[ "${AUTOMIX_QEMU_EMULATED:-}" == "1" && "$arch" == "aarch64" ]]; then - echo "QEMU-emulated arm64 build detected; cross-building AppImage with x86_64 appimagetool" >&2 - appimagetool="$(fetch_appimagetool "x86_64")" - else - appimagetool="$(fetch_appimagetool "$arch")" - fi + appimagetool="$(fetch_appimagetool "$arch")" local output="$DIST_DIR/${APP_NAME}-${VERSION}-${arch}.AppImage" APPIMAGE_EXTRACT_AND_RUN=1 ARCH="$arch" "$appimagetool" "$stage_dir" "$output" >/dev/null diff --git a/tools/update_lame_pins.py b/tools/update_lame_pins.py new file mode 100644 index 0000000..2d15ca5 --- /dev/null +++ b/tools/update_lame_pins.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Regenerate assets/lame-pins.json, the list of LAME downloads the app trusts. + +The app fetches that file from the default branch before downloading an MP3 +encoder, so merging a change to it updates every installed copy without a +release. Run by .github/workflows/lame_pins.yml, which opens a pull request +when the output changes; a person reviews it before it is merged. + +Where each hash comes from: + - Debian: the SHA256 field of the `lame` entry in Debian stable's package index. + - Homebrew (macOS): the bottle digests in the GHCR image index for the current + version. Each candidate bottle is downloaded, checked against its digest and + inspected: the app copies only bin/lame out of it, so a bottle whose encoder + still points at a Homebrew path cannot run and is not published. With no + usable bottle the platform is left out and the app uses its built-in pins. + Linux bottles are never usable this way, so Linux relies on Debian. + - rarewares.org (Windows): no index or published hash exists, so the known + files are downloaded and hashed. A changed hash there has no independent + confirmation and needs a careful look before merging. + +A source that cannot be refreshed keeps its current entry and is reported. +""" + +import hashlib +import io +import json +import lzma +import pathlib +import sys +import tarfile +import urllib.request + +MANIFEST = pathlib.Path(__file__).resolve().parent.parent / "assets" / "lame-pins.json" +GHCR = "https://ghcr.io/v2/homebrew/core/lame" +DEBIAN = "https://deb.debian.org/debian/" + +# platform key -> Debian architecture / Homebrew (os, architecture) / rarewares file +DEBIAN_ARCH = {"linux-x64": "amd64", "linux-arm64": "arm64", "linux-arm": "armhf"} +BOTTLE = { + "darwin-x64": ("darwin", "amd64"), + "darwin-arm64": ("darwin", "arm64"), +} +WINDOWS_ZIP = { + "win32-x64": "https://www.rarewares.org/files/mp3/lame3.100.1-x64.zip", + "win32-arm64": "https://www.rarewares.org/files/mp3/lame3.100.1-x64.zip", + "win32-ia32": "https://www.rarewares.org/files/mp3/lame3.100.1-win32.zip", +} + + +def fetch(url, headers=None): + request = urllib.request.Request(url, headers={"User-Agent": "automix-lame-pins", **(headers or {})}) + with urllib.request.urlopen(request, timeout=120) as response: + return response.read() + + +def debian_sources(): + sources = [] + for platform, arch in DEBIAN_ARCH.items(): + index = lzma.decompress(fetch(f"{DEBIAN}dists/stable/main/binary-{arch}/Packages.xz")).decode("utf-8") + stanza = next(s for s in index.split("\n\n") if s.startswith("Package: lame\n")) + fields = dict(line.split(": ", 1) for line in stanza.splitlines() if ": " in line and not line.startswith(" ")) + sources.append({"platform": platform, "type": "deb", "url": DEBIAN + fields["Filename"], "sha256": fields["SHA256"]}) + return sources + + +def os_version(manifest): + digits = "".join(c if c.isdigit() or c == "." else " " for c in manifest["platform"].get("os.version", "0")) + return tuple(int(part) for part in digits.split()[0].split(".") if part) if digits.split() else (0,) + + +def runs_standalone(digest, auth): + blob = fetch(f"{GHCR}/blobs/sha256:{digest}", auth) + if hashlib.sha256(blob).hexdigest() != digest: + raise ValueError(f"bottle {digest[:12]} does not match its digest") + with tarfile.open(fileobj=io.BytesIO(blob)) as bottle: + encoder = next(m for m in bottle.getmembers() if m.name.endswith("/bin/lame")) + return b"@@HOMEBREW" not in bottle.extractfile(encoder).read() + + +def homebrew_sources(): + version = json.loads(fetch("https://formulae.brew.sh/api/formula/lame.json"))["versions"]["stable"] + token = json.loads(fetch("https://ghcr.io/token?service=ghcr.io&scope=repository:homebrew/core/lame:pull"))["token"] + auth = {"Authorization": f"Bearer {token}"} + index = json.loads(fetch(f"{GHCR}/manifests/{version}", {**auth, "Accept": "application/vnd.oci.image.index.v1+json"})) + + sources = [] + for platform, (os_name, arch) in BOTTLE.items(): + bottles = [m for m in index["manifests"] + if m["platform"]["os"] == os_name and m["platform"]["architecture"] == arch] + # The bottle built for the oldest OS release runs on the widest range of machines. + for bottle in sorted(bottles, key=os_version): + digest = bottle["annotations"]["sh.brew.bottle.digest"] + if runs_standalone(digest, auth): + sources.append({"platform": platform, "type": "ghcr", "sha256": digest}) + break + else: + print(f"note: no LAME {version} bottle for {platform} runs on its own; the app keeps its built-in pin") + return sources + + +def windows_sources(current): + sources = [] + hashes = {} + for platform, default_url in WINDOWS_ZIP.items(): + url = next((s["url"] for s in current if s["platform"] == platform and s["type"] == "zip"), default_url) + if url not in hashes: + hashes[url] = hashlib.sha256(fetch(url)).hexdigest() + sources.append({"platform": platform, "type": "zip", "url": url, "sha256": hashes[url]}) + return sources + + +def main(): + current = json.loads(MANIFEST.read_text(encoding="utf-8"))["sources"] if MANIFEST.exists() else [] + sources = [] + failed = False + for name, kind, refresh in (("Debian", "deb", debian_sources), + ("Homebrew", "ghcr", homebrew_sources), + ("rarewares.org", "zip", lambda: windows_sources(current))): + try: + sources += refresh() + except Exception as error: # keep the entries we already trust + failed = True + print(f"warning: could not refresh {name} pins ({error}); keeping the current ones", file=sys.stderr) + sources += [s for s in current if s["type"] == kind] + + for old in current: + new = next((s for s in sources if (s["platform"], s["type"]) == (old["platform"], old["type"])), None) + if new is not None and new["sha256"] != old["sha256"]: + note = " -- NO independent confirmation, review before merging" if old["type"] == "zip" else "" + print(f"changed: {old['platform']} {old['type']} {old['sha256'][:12]} -> {new['sha256'][:12]}{note}") + + sources.sort(key=lambda s: (s["platform"], s["type"])) + MANIFEST.write_text(json.dumps({"schema": 1, "sources": sources}, indent=2) + "\n", encoding="utf-8", newline="\n") + return 1 if failed and not sources else 0 + + +if __name__ == "__main__": + sys.exit(main())