diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..42a1551 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: sdevil7th diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bfeb616..2a2e28a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,16 +12,35 @@ on: release_notes_file: description: "Path to a markdown file in the repo for release notes" required: false - default: "packaging/release-notes-template.md" + default: "" permissions: contents: read jobs: + validate-release-notes: + runs-on: ubuntu-24.04 + outputs: + notes_file: ${{ steps.notes.outputs.notes_file }} + env: + VERSION: ${{ github.event.inputs.version || github.ref_name }} + NOTES_FILE: ${{ github.event.inputs.release_notes_file }} + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - name: Validate exact-version release notes before building + id: notes + run: python3 tools/validate-release-notes.py --version "$VERSION" --notes-file "$NOTES_FILE" --github-output "$GITHUB_OUTPUT" + build-windows: + needs: validate-release-notes runs-on: windows-latest + permissions: + contents: read + actions: read env: + WINDOWS_SIGNING_PROVIDER: ${{ vars.OPENSTUDIO_WINDOWS_SIGNING_PROVIDER || 'certificate' }} VERSION: ${{ github.event.inputs.version || github.ref_name }} + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} BUILD_DIR: build-release-windows ASIO_SDK_DIR: thirdparty/asio ONNXRUNTIME_VERSION: 1.24.4 @@ -34,6 +53,25 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - name: Validate Windows signing configuration + shell: pwsh + env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} + SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }} + SIGNPATH_SIGNING_POLICY_SLUG: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + run: | + if ($env:WINDOWS_SIGNING_PROVIDER -notin @('certificate', 'signpath')) { + throw "OPENSTUDIO_WINDOWS_SIGNING_PROVIDER must be certificate or signpath." + } + if ($env:WINDOWS_SIGNING_PROVIDER -eq 'signpath') { + foreach ($name in @('SIGNPATH_API_TOKEN', 'SIGNPATH_ORGANIZATION_ID', 'SIGNPATH_PROJECT_SLUG', 'SIGNPATH_SIGNING_POLICY_SLUG')) { + if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) { + throw "SignPath is enabled but $name is missing. See docs/release-runbook.md#windows-signing-with-signpath." + } + } + } + - name: Normalize version shell: pwsh run: | @@ -143,6 +181,14 @@ jobs: shell: pwsh run: cmake --build $env:BUILD_DIR --config Release --target OpenStudio + - name: Verify signed updater (Windows Release) + shell: pwsh + run: | + $exe = Join-Path $env:GITHUB_WORKSPACE "$env:BUILD_DIR/OpenStudio_artefacts/Release/OpenStudio.exe" + $report = Join-Path $env:RUNNER_TEMP "updater-regression" + $process = Start-Process -FilePath $exe -ArgumentList @("--updater-self-test", "`"$report`"") -WindowStyle Hidden -Wait -PassThru + if ($process.ExitCode -ne 0) { throw "Signed updater regression failed." } + - name: Validate Windows runtime bundle shell: pwsh run: ./tools/validate-runtime-bundle.ps1 -Platform windows -BundlePath "$env:BUILD_DIR/OpenStudio_artefacts/Release" -ExpectedVersion $env:VERSION -EnforceLeanBundle @@ -165,7 +211,50 @@ jobs: $report = Join-Path $env:RUNNER_TEMP "OpenStudio_WindowLifecycleHarness.json" ./tools/run-window-lifecycle-smoke.ps1 -AppPath $exePath -ReportPath $report -TimeoutSeconds 180 + - name: Stage Windows payload for SignPath + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + shell: pwsh + run: | + . ./tools/windows-signing.ps1 + Copy-OpenStudioSigningFiles -SourceDirectory "$env:BUILD_DIR/OpenStudio_artefacts/Release" ` + -DestinationDirectory "$env:RUNNER_TEMP/signpath-payload-unsigned" -Version $env:VERSION -RequireEmptyDestination + + - name: Upload unsigned Windows payload + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + id: signpath-payload + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: signpath-payload-unsigned-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/signpath-payload-unsigned/* + if-no-files-found: error + retention-days: 7 + + - name: Sign Windows payload + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2.3 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + artifact-configuration-slug: windows-payload + github-artifact-id: ${{ steps.signpath-payload.outputs.artifact-id }} + wait-for-completion: true + wait-for-completion-timeout-in-seconds: 3600 + output-artifact-directory: ${{ runner.temp }}/signpath-payload-signed + parameters: | + version: ${{ toJSON(env.VERSION) }} + + - name: Verify and restore signed Windows payload + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + shell: pwsh + run: | + . ./tools/windows-signing.ps1 + Copy-OpenStudioSigningFiles -SourceDirectory "$env:RUNNER_TEMP/signpath-payload-signed" ` + -DestinationDirectory "$env:BUILD_DIR/OpenStudio_artefacts/Release" -Version $env:VERSION -RequireSignature + - name: Prepare Windows signing certificate + if: env.WINDOWS_SIGNING_PROVIDER == 'certificate' shell: pwsh env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} @@ -189,6 +278,7 @@ jobs: "WINDOWS_CODESIGN_CERT_PATH=$certPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Package Windows installer + if: env.WINDOWS_SIGNING_PROVIDER == 'certificate' shell: pwsh env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} @@ -215,6 +305,7 @@ jobs: $env:WINDOWS_TIMESTAMP_URL = "" $arguments = @{ Version = $env:VERSION + NotesFile = $env:RELEASE_NOTES_FILE SourceDir = "$env:BUILD_DIR/OpenStudio_artefacts/Release" } @@ -236,6 +327,52 @@ jobs: ./tools/package-windows-release.ps1 @arguments + - name: Package Windows installer with signed payload + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + shell: pwsh + run: | + ./tools/package-windows-release.ps1 -Version $env:VERSION -NotesFile $env:RELEASE_NOTES_FILE ` + -SourceDir "$env:BUILD_DIR/OpenStudio_artefacts/Release" -RequireSignedPayload + . ./tools/windows-signing.ps1 + Copy-OpenStudioSigningFiles -SourceDirectory "dist/windows" -Kind installer ` + -DestinationDirectory "$env:RUNNER_TEMP/signpath-installer-unsigned" -Version $env:VERSION -RequireEmptyDestination + + - name: Upload unsigned Windows installer + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + id: signpath-installer + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: signpath-installer-unsigned-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/signpath-installer-unsigned/* + if-no-files-found: error + retention-days: 7 + + - name: Sign Windows installer + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2.3 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + artifact-configuration-slug: windows-installer + github-artifact-id: ${{ steps.signpath-installer.outputs.artifact-id }} + wait-for-completion: true + wait-for-completion-timeout-in-seconds: 3600 + output-artifact-directory: ${{ runner.temp }}/signpath-installer-signed + parameters: | + version: ${{ toJSON(env.VERSION) }} + + - name: Verify and restore signed Windows installer + if: env.WINDOWS_SIGNING_PROVIDER == 'signpath' + shell: pwsh + run: | + . ./tools/windows-signing.ps1 + Copy-OpenStudioSigningFiles -SourceDirectory "$env:RUNNER_TEMP/signpath-installer-signed" -Kind installer ` + -DestinationDirectory "dist/windows" -Version $env:VERSION -RequireSignature + Assert-OpenStudioSigningFiles -Directory "$env:BUILD_DIR/OpenStudio_artefacts/Release" ` + -Version $env:VERSION -RequireSignature + - name: Verify Windows release outputs shell: pwsh run: | @@ -254,6 +391,33 @@ jobs: shell: pwsh run: ./tools/setup-ffmpeg.ps1 -CorrespondingSourceDestination "dist/windows/OpenStudio-FFmpeg-8.0.1-complete-corresponding-source.zip" + - name: Build validated Microsoft Store package + if: vars.OPENSTUDIO_STORE_ENABLED == 'true' + shell: pwsh + env: + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} + run: | + $storeVersion = python tools/submit_store_release.py --version $env:VERSION --print-package-version + if ($LASTEXITCODE -ne 0) { throw 'Invalid Store release version.' } + ./tools/package-windows-store.ps1 -Version $storeVersion -NotesFile $env:RELEASE_NOTES_FILE ` + -SourceDir "$env:BUILD_DIR/OpenStudio_artefacts/Release" -OutputDir dist/store + python tools/submit_store_release.py --version $env:VERSION --package-dir dist/store ` + --notes-file $env:RELEASE_NOTES_FILE --report dist/store/validation.json + if ($LASTEXITCODE -ne 0) { throw 'Store submission preflight failed.' } + + - name: Retain Microsoft Store submission artifact + if: vars.OPENSTUDIO_STORE_ENABLED == 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: microsoft-store-package + path: | + dist/store/*.msix + dist/store/package-report.json + dist/store/validation.json + compression-level: 0 + retention-days: 30 + if-no-files-found: error + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: windows-release @@ -263,9 +427,11 @@ jobs: if-no-files-found: error build-macos: + needs: validate-release-notes runs-on: macos-14 env: VERSION: ${{ github.event.inputs.version || github.ref_name }} + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} BUILD_DIR: build-release-macos AI_RUNTIME_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_VERSION || github.event.inputs.version || github.ref_name }} AI_RUNTIME_STANDALONE_RELEASE_TAG: ${{ vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_RELEASE_TAG != '' && vars.OPENSTUDIO_AI_RUNTIME_STANDALONE_RELEASE_TAG || '20260325' }} @@ -385,6 +551,7 @@ jobs: unset DOPPLER_TOKEN TONE3000_PUBLISHABLE_KEY OPENSTUDIO_TONE3000_CLIENT_ID_VALUE OPENSTUDIO_TONE3000_CLIENT_ID cmake -S . -B "$BUILD_DIR" \ + -DCMAKE_BUILD_TYPE=Release \ -DOPENSTUDIO_APP_VERSION="$VERSION" \ -DOPENSTUDIO_UPDATE_MANIFEST_URL_VALUE="$RELEASE_SITE_URL/releases/stable/latest.json" \ -DOPENSTUDIO_UPDATE_APPCAST_URL_VALUE="$RELEASE_SITE_URL/appcast/macos-stable.xml" \ @@ -398,6 +565,12 @@ jobs: shell: bash run: cmake --build "$BUILD_DIR" --config Release --target OpenStudio + - name: Verify signed updater (macOS Release) + shell: bash + run: | + "$BUILD_DIR/OpenStudio_artefacts/Release/OpenStudio.app/Contents/MacOS/OpenStudio" --updater-self-test "$RUNNER_TEMP/updater-regression" + "$BUILD_DIR/OpenStudio_artefacts/Release/OpenStudio.app/Contents/Helpers/OpenStudioUpdateInstaller" --self-test "$HOME/updater-installer-regression" + - name: Validate macOS runtime bundle shell: pwsh run: | @@ -449,7 +622,7 @@ jobs: echo "OpenStudio.app was not found in the build output." >&2 exit 1 fi - ./tools/package-macos-release.sh "$APP_PATH" "$VERSION" + ./tools/package-macos-release.sh "$APP_PATH" "$VERSION" dist/macos "$RELEASE_NOTES_FILE" if [[ -n "$MACOS_CODESIGN_IDENTITY" ]]; then echo "OPENSTUDIO_MACOS_PACKAGE_SIGNED=true" >> "$GITHUB_ENV" fi @@ -502,9 +675,11 @@ jobs: if-no-files-found: error build-linux: + needs: validate-release-notes runs-on: ubuntu-24.04 env: VERSION: ${{ github.event.inputs.version || github.ref_name }} + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} BUILD_DIR: build-release-linux ONNXRUNTIME_VERSION: 1.24.4 ONNXRUNTIME_LINUX_X64_SHA256: 3a211fbea252c1e66290658f1b735b772056149f28321e71c308942cdb54b747 @@ -540,7 +715,7 @@ jobs: - name: Download ONNX Runtime (Linux CPU) run: | URL="https://github.com/microsoft/onnxruntime/releases/download/v${ONNXRUNTIME_VERSION}/onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}.tgz" - wget -q "$URL" -O onnxruntime.tgz + curl --fail --location --show-error --retry 3 --retry-delay 2 --output onnxruntime.tgz "$URL" echo "${ONNXRUNTIME_LINUX_X64_SHA256} onnxruntime.tgz" | sha256sum --check --strict mkdir -p thirdparty/onnxruntime tar -xzf onnxruntime.tgz --strip-components=1 -C thirdparty/onnxruntime @@ -603,6 +778,12 @@ jobs: - name: Build OpenStudio run: cmake --build "$BUILD_DIR" --config Release --target OpenStudio + - name: Verify signed updater (Linux Release) + shell: bash + run: | + xvfb-run -a "$BUILD_DIR/OpenStudio_artefacts/Release/OpenStudio" --updater-self-test "$RUNNER_TEMP/updater-regression" + "$BUILD_DIR/OpenStudio_artefacts/Release/OpenStudioUpdateInstaller" --self-test "$HOME/updater-installer-regression" + - name: Validate Linux runtime bundle shell: pwsh run: ./tools/validate-runtime-bundle.ps1 -Platform linux -BundlePath "$env:BUILD_DIR/OpenStudio_artefacts/Release" -ExpectedVersion $env:VERSION -EnforceLeanBundle @@ -610,7 +791,7 @@ jobs: - name: Package AppImage run: | chmod +x ./tools/package-linux-release.sh - bash ./tools/package-linux-release.sh "$VERSION" "$BUILD_DIR" + bash ./tools/package-linux-release.sh "$VERSION" "$BUILD_DIR" "$RELEASE_NOTES_FILE" - name: Verify Linux release outputs run: test -f "dist/linux/OpenStudio-${VERSION}-linux-x86_64.AppImage" @@ -642,6 +823,7 @@ jobs: publish: runs-on: ubuntu-latest needs: + - validate-release-notes - build-windows - build-macos - build-linux @@ -649,7 +831,7 @@ jobs: contents: write env: VERSION: ${{ github.event.inputs.version || github.ref_name }} - RELEASE_NOTES_FILE: ${{ github.event.inputs.release_notes_file || 'packaging/release-notes-template.md' }} + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} AI_RUNTIME_VERSION: ${{ vars.OPENSTUDIO_AI_RUNTIME_VERSION != '' && vars.OPENSTUDIO_AI_RUNTIME_VERSION || github.event.inputs.version || github.ref_name }} AI_RUNTIME_RELEASE_TAG: ${{ vars.OPENSTUDIO_AI_RUNTIME_RELEASE_TAG }} WEBSITE_REPO: ${{ vars.OPENSTUDIO_WEBSITE_REPO != '' && vars.OPENSTUDIO_WEBSITE_REPO || 'sdevil7th/OpenStudioWebsite' }} @@ -794,6 +976,20 @@ jobs: -LinuxCudaInstallPlanPath "tools/ai-runtime-install-plan-linux-cuda.json" ` -LinuxRocmInstallPlanPath "tools/ai-runtime-install-plan-linux-rocm.json" + - name: Install updater signing dependency + run: python3 -m pip install "cryptography==46.0.3" + + - name: Sign application update manifests + shell: bash + env: + OPENSTUDIO_UPDATE_SIGNING_SEED: ${{ secrets.OPENSTUDIO_UPDATE_SIGNING_SEED }} + run: | + if [ -z "$OPENSTUDIO_UPDATE_SIGNING_SEED" ]; then + echo "OPENSTUDIO_UPDATE_SIGNING_SEED is required for authenticated app updates." >&2 + exit 1 + fi + python3 tools/updater_manifest.py sign --metadata-dir dist/release-metadata + - name: Prepare uniquely named release metadata assets shell: pwsh run: | @@ -849,3 +1045,46 @@ jobs: event-type: ${{ env.WEBSITE_DISPATCH_EVENT_TYPE }} client-payload: >- {"tag":"${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', github.event.inputs.version) }}","channel":"stable","desktopRepo":"${{ github.repository }}"} + + submit-store: + name: Submit Microsoft Store update + needs: [publish, build-windows, validate-release-notes] + if: vars.OPENSTUDIO_STORE_ENABLED == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 25 + environment: microsoft-store + concurrency: + group: openstudio-microsoft-store-submission + cancel-in-progress: false + permissions: + contents: read + actions: read + env: + VERSION: ${{ github.event.inputs.version || github.ref_name }} + RELEASE_NOTES_FILE: ${{ needs.validate-release-notes.outputs.notes_file }} + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: microsoft-store-package + path: dist/store + + - name: Submit the exact release package and notes + env: + MS_STORE_TENANT_ID: ${{ secrets.MS_STORE_TENANT_ID }} + MS_STORE_CLIENT_ID: ${{ secrets.MS_STORE_CLIENT_ID }} + MS_STORE_CLIENT_SECRET: ${{ secrets.MS_STORE_CLIENT_SECRET }} + run: >- + python3 tools/submit_store_release.py --version "$VERSION" + --package-dir dist/store --notes-file "$RELEASE_NOTES_FILE" + --report output/store-submission.json --submit + + - name: Retain sanitized submission status + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: microsoft-store-submission-${{ github.run_attempt }} + path: output/store-submission.json + if-no-files-found: warn + retention-days: 30 diff --git a/.github/workflows/updater-installer.yml b/.github/workflows/updater-installer.yml new file mode 100644 index 0000000..d338479 --- /dev/null +++ b/.github/workflows/updater-installer.yml @@ -0,0 +1,42 @@ +name: Updater installer safety +on: + push: + paths: + - 'Source/UpdateInstaller*' + - 'Source/UpdateManifest*' + - 'Source/UpdatePublicKey.h' + - 'thirdparty/monocypher/**' + - 'tests/updater-installer/**' + - '.github/workflows/updater-installer.yml' + pull_request: + paths: + - 'Source/UpdateInstaller*' + - 'Source/UpdateManifest*' + - 'Source/UpdatePublicKey.h' + - 'thirdparty/monocypher/**' + - 'tests/updater-installer/**' + - '.github/workflows/updater-installer.yml' + workflow_dispatch: +permissions: + contents: read +jobs: + native-helper: + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-15, macos-15-intel] + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - name: Build the actual helper with warnings as errors + run: | + cmake -S tests/updater-installer -B build-installer -DCMAKE_BUILD_TYPE=Release + cmake --build build-installer --parallel 3 + - name: Exercise native replacement and recovery primitives + run: ./build-installer/OpenStudioUpdateInstaller --self-test "$HOME/updater-installer-qualification" + - uses: actions/upload-artifact@v4 + if: always() + with: + name: installer-${{ matrix.os }} + path: ~/updater-installer-qualification/installer-result.json diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 4619afb..82fb9e7 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -17,12 +17,44 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: '3.12' + + - name: Install audio routing test dependencies + run: python -m pip install numpy==2.4.2 soundfile==0.13.1 cryptography==46.0.3 + + - name: Check first-party product names + run: python3 tools/check-product-naming.py + - name: Run Python tooling unit tests run: >- python3 -m unittest + tests.test_release_notes + tests.test_updater_manifest + tests.test_store_submission tests.test_ai_runtime_dependency_pins tests.test_install_ai_tools_feature_gating tests.test_install_ai_tools_runtime_repair + tests.test_prepare_diffusers_audio + tests.test_diffusers_memory_policy + + verify-ai-policy: + name: AI execution policy (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, windows-latest, macos-14] + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: '3.12' + - name: Install lightweight audio test dependencies + run: python -m pip install numpy==2.4.2 soundfile==0.13.1 + - name: Verify device budgets and model routing without GPU dependencies + run: python -m unittest tests.test_ai_execution_policy tests.test_ai_benchmark tests.test_diffusers_memory_policy tests.test_ace_diffusers_generation tests.test_ai_generation_preflight verify-input-profiles: name: Input profiles (${{ matrix.os }}) @@ -66,6 +98,14 @@ jobs: steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - name: Test Windows signing safeguards + shell: pwsh + run: ./tools/test-windows-signing.ps1 + + - name: Test Microsoft Store packaging safeguards + shell: pwsh + run: ./tools/test-windows-store-package.ps1 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version-file: frontend/.nvmrc @@ -171,6 +211,14 @@ jobs: throw "Windows startup self-test failed." } + - name: Verify signed updater and restart persistence (Windows Release) + shell: pwsh + run: | + $exe = Join-Path $env:GITHUB_WORKSPACE "$env:BUILD_DIR/OpenStudio_artefacts/Release/OpenStudio.exe" + $report = Join-Path $env:RUNNER_TEMP "updater-regression" + $process = Start-Process -FilePath $exe -ArgumentList @("--updater-self-test", "`"$report`"") -WindowStyle Hidden -Wait -PassThru + if ($process.ExitCode -ne 0) { throw "Signed updater regression failed." } + - name: Run render and export regression matrix shell: pwsh run: | @@ -299,13 +347,14 @@ jobs: shell: bash run: | cmake -S . -B "$BUILD_DIR" \ + -DCMAKE_BUILD_TYPE=Release \ -DOPENSTUDIO_APP_VERSION="$VERSION" \ -DOPENSTUDIO_ENABLE_EXTERNAL_PYTHON_AI_FALLBACK=OFF \ -DFETCHCONTENT_UPDATES_DISCONNECTED=ON - name: Build OpenStudio shell: bash - run: cmake --build "$BUILD_DIR" --config Release --target OpenStudio + run: cmake --build "$BUILD_DIR" --config Release --target OpenStudio --parallel 2 - name: Find app bundle shell: bash @@ -341,6 +390,12 @@ jobs: exit 1 fi + - name: Verify signed updater and restart persistence (macOS Release) + shell: bash + run: | + "$APP_BUNDLE/Contents/MacOS/OpenStudio" --updater-self-test "$RUNNER_TEMP/updater-regression" + "$APP_BUNDLE/Contents/Helpers/OpenStudioUpdateInstaller" --self-test "$HOME/updater-installer-regression" + - name: Run macOS native-window lifecycle smoke test shell: pwsh run: | @@ -359,13 +414,17 @@ jobs: verify-linux: runs-on: ubuntu-24.04 env: - VERSION: 0.0.0 + # AppImage verification exercises the real release-notes gate. + VERSION: 0.1.02 BUILD_DIR: build-verify-linux ONNXRUNTIME_VERSION: 1.24.4 ONNXRUNTIME_LINUX_X64_SHA256: 3a211fbea252c1e66290658f1b735b772056149f28321e71c308942cdb54b747 steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - name: Validate AppImage release notes + run: python3 tools/validate-release-notes.py --version "$VERSION" + - name: Install system dependencies shell: bash run: | @@ -389,7 +448,7 @@ jobs: shell: bash run: | URL="https://github.com/microsoft/onnxruntime/releases/download/v${ONNXRUNTIME_VERSION}/onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}.tgz" - wget -q "$URL" -O onnxruntime.tgz + curl --fail --location --show-error --retry 3 --retry-delay 2 --output onnxruntime.tgz "$URL" echo "${ONNXRUNTIME_LINUX_X64_SHA256} onnxruntime.tgz" | sha256sum --check --strict mkdir -p thirdparty/onnxruntime tar -xzf onnxruntime.tgz --strip-components=1 -C thirdparty/onnxruntime @@ -427,6 +486,12 @@ jobs: exit 1 fi + - name: Verify signed updater and restart persistence (Linux Release) + shell: bash + run: | + xvfb-run -a "$BUILD_DIR/OpenStudio_artefacts/Release/OpenStudio" --updater-self-test "$RUNNER_TEMP/updater-regression" + "$BUILD_DIR/OpenStudio_artefacts/Release/OpenStudioUpdateInstaller" --self-test "$HOME/updater-installer-regression" + - name: Package AppImage shell: bash run: | diff --git a/.gitignore b/.gitignore index 2e01647..cc0295a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /build-*/ /out /tmp/ +/tmp_* /cmake-build-* CMakeCache.txt CMakeFiles @@ -177,3 +178,7 @@ yarn-error.log* Thumbs.db ehthumbs.db Desktop.ini + +# Local cabinet presentation captures +/tmp_cab_showcase/ +/tmp_cab_showcase_v2/ diff --git a/AGENTS.md b/AGENTS.md index 4f0a046..b88a9ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # OpenStudio -A hybrid DAW (Digital Audio Workstation) with a **JUCE C++ backend** for audio processing and a **React/TypeScript frontend** rendered in WebView2. +A hybrid DAW (Digital Audio Workstation) with a **JUCE C++ backend** for audio processing and a **React/TypeScript frontend** rendered in a platform WebView (WebView2 on Windows). ## Architecture @@ -21,7 +21,7 @@ C++ (JUCE) Backend React/TypeScript Frontend - **C++ backend** handles: audio I/O, recording to disk, clip playback with sample-rate conversion, VST3 plugin hosting, MIDI device management, metering, offline render/export - **React frontend** handles: all UI, state management (Zustand), canvas-based timeline (Konva/react-konva), keyboard shortcuts, drag-and-drop, project save/load -- **Communication**: synchronous bridge via `window.__JUCE__.backend.*` functions (defined in NativeBridge.ts, exposed in MainComponent.cpp) +- **Communication**: asynchronous bridge via `window.__JUCE__.backend.*` functions (wrapped as Promises in NativeBridge.ts, exposed in MainComponent.cpp) ## Directory Structure @@ -41,7 +41,7 @@ OpenStudio/ │ ├── MIDIClip.h/cpp # MIDI note event storage and time-range queries │ ├── Metronome.h/cpp # Click track generation (BPM, time sig, accent patterns) │ ├── AudioConverter.h/cpp # Channel/sample-rate conversion utilities -│ ├── PeakCache.h/cpp # REAPER-style multi-resolution peak cache (.s13peaks sidecar files) +│ ├── PeakCache.h/cpp # REAPER-style multi-resolution peak cache (.ospeaks sidecar files) │ ├── AudioAnalyzer.h/cpp # Audio analysis utilities │ ├── BuiltInEffects.h/cpp # Built-in audio effects (EQ, compressor, etc.) │ ├── BuiltInEffects2.h/cpp # Additional built-in effects @@ -58,13 +58,13 @@ OpenStudio/ │ ├── HarmonicMaskGenerator.h/cpp # Wiener-filter soft masks at harmonic positions for poly separation │ ├── SpectralPitchShifter.h/cpp # Phase vocoder on masked spectrograms with cepstral formant preservation │ ├── SpectralProcessor.h/cpp # STFT/ISTFT utilities for spectral processing -│ ├── S13PitchCorrector.h/cpp # Real-time inline pitch corrector (auto-tune style) +│ ├── OpenStudioPitchCorrector.h/cpp # Real-time inline pitch corrector (auto-tune style) │ │ │ │ # Plugin System -│ ├── S13FXProcessor.h/cpp # JSFX/Lua script-based audio processor (wraps YSFX) -│ ├── S13FXGfxEditor.h/cpp # JSFX @gfx rendering via juce::Image framebuffer at 30fps -│ ├── S13PluginEditors.h/cpp # Built-in plugin editor windows -│ ├── S13ScriptWindow.h/cpp # Lua gfx API framebuffer window +│ ├── JSFXProcessor.h/cpp # JSFX/Lua script-based audio processor (wraps YSFX) +│ ├── JSFXGfxEditor.h/cpp # JSFX @gfx rendering via juce::Image framebuffer at 30fps +│ ├── OpenStudioPluginEditors.h/cpp # Built-in plugin editor windows +│ ├── OpenStudioScriptWindow.h/cpp # Lua gfx API framebuffer window │ ├── ScriptEngine.h/cpp # Lua scripting engine (sol2) │ │ │ │ # Other Features @@ -115,7 +115,7 @@ OpenStudio/ │ │ ├── PitchCorrectorPanel.tsx # Real-time inline corrector (auto-tune style, key/scale/retune) │ │ ├── PitchEditorLowerZone.tsx # Graphical pitch editor: canvas host, tools, controls, interaction handlers │ │ ├── PitchEditorCanvas.ts # Imperative canvas renderer (60fps RAF loop): notes, contour, grid, piano keys -│ │ ├── S13PitchEditor.tsx # Pitch editor wrapper/container +│ │ ├── OpenStudioPitchEditor.tsx # Pitch editor wrapper/container │ │ ├── pitchCorrectorPresets.ts # Preset definitions for real-time pitch corrector │ │ │ │ │ │ # Other @@ -146,6 +146,22 @@ OpenStudio/ ## Build & Dev +### Release notes are a required release gate + +Before preparing or publishing any release, review the exact Git range since the +previous application tag and write `docs/releases/.md`. Describe concrete +user-visible changes, fixes, known limitations and upgrade steps. Distinguish +verified changes in the tagged source from unshipped working-tree changes; never +copy a template, a raw commit list, or claim hardware/platform testing not run. +Include a comparison/commit/PR link, review the prose, then run +`python tools/validate-release-notes.py --version ` before building or +publishing. The same notes feed GitHub and updater metadata. CI and local release +entry points must fail closed when notes are missing, mismatched or unfinished. +Do not bypass the gate or fabricate notes just to make a release pass. +For a cumulative release page, review its main release PR as well as later +hotfixes. Cover the main features and label the hotfix-only comparison separately; +the immediately preceding tag can already contain the main feature release. + ```bash # Full dev (installs deps, builds C++ Debug, starts Vite HMR, launches app) python build.py dev --run @@ -159,11 +175,11 @@ cmake --build build --config Debug # C++ rebuild only — Release cmake --build build --config Release -# Production (builds frontend + Release C++, single .exe with embedded frontend) -python build.py prod +# Production (reviewed notes required; builds frontend + Release C++ and copies webui/runtime files) +python build.py prod --version ``` -**No feature flags** — all features (ASIO, WASAPI, DirectSound, VST3 hosting, WebView2) are always enabled via hardcoded `target_compile_definitions` in CMakeLists.txt. The `build.py dev` mode uses Debug config; `build.py prod` uses Release. +`build.py dev` uses Debug and `build.py prod` uses Release. Read `CMakeLists.txt` and the platform packaging scripts for current dependency, feature and signing options; do not assume every backend is enabled on every platform. ### Manual Testing Handoff Requirement @@ -180,6 +196,29 @@ Before asking for manual testing: - Stop any Codex-started dev servers, harness browsers, or background Vite/npm processes before handing off. Verify port `5183` is not left occupied by a Codex-started process. - In the handoff, state that the CMake Debug build was completed and that no pre-running server is required. +### Branding and website documentation + +- The approved 2160 px master is `assets/branding/openstudio-logo-source.png`. + Run `node tools/generate-icons.mjs` after installing frontend dependencies to + regenerate native, frontend, menu-bar and README icons. Follow + [docs/branding.md](docs/branding.md) for all consumers and cache versions. +- Keep the master synchronized with `../openstudio-website/assets/branding/`. + The website owns its favicon, social card and Store promotional exports; the + app owns native package icons and MSIX resources. New source icons do not update + already-built or installed binaries. Follow the existing release smoke checklist. +- Product guides describe the source checkout unless tied to a released tag. + Update the app manual and the relevant website guides when public behavior + changes; the website lives in the separate `../openstudio-website` repository. + Its guides record an app commit and release/development status. +- Verify menu names and bindings against `MenuBar.tsx`, `actionRegistry.ts`, + `shortcutProfiles.ts` and `mouseBehaviorProfiles.ts`. The current window is + **Keyboard, Mouse & Trackpad** in Options and Help. `F1` opens Help Reference. + Keyboard and mouse profiles are independent; platform, scope and custom + overrides can change a displayed shortcut. See [docs/input-profiles.md](docs/input-profiles.md). +- The website owns shared legal policy and public app/update metadata. Keep + stable download/appcast contracts compatible with shipped clients, and only + advertise new models/features as released after checking the actual app tag. + ## Key Technical Details ### State Management @@ -215,7 +254,7 @@ For **continuous edits** (faders, knobs), use the begin/commit pattern: `beginXE ### Timeline Rendering - **Konva** (react-konva) for canvas-based rendering -- Waveform peaks fetched from C++ via `getWaveformPeaks(filePath, samplesPerPixel, numPixels)` — backed by PeakCache (`.s13peaks` files), never reads audio files directly +- Waveform peaks fetched from C++ via `getWaveformPeaks(filePath, samplesPerPixel, numPixels)` — backed by PeakCache (`.ospeaks` files), never reads audio files directly - `samplesPerPixel` uses the clip's `sampleRate` (not hardcoded) with power-of-2 quantization for cache stability - Zoom: exponential scaling via `Math.exp(-deltaY * sensitivity)`, anchored to cursor position - Zoom debounce: suppresses waveform re-fetches during active zoom (`isZoomingRef`, 200ms timeout) @@ -246,7 +285,7 @@ Polyphonic pipeline: PolyPitchDetector (Basic-Pitch ONNX) -> PolyNotes -> HarmonicMaskGenerator (Wiener) -> SpectralPitchShifter -> PolyResynthesizer Real-time corrector: - S13PitchCorrector (per-block, key/scale aware) -> inserted as FX plugin on track + OpenStudioPitchCorrector (per-block, key/scale aware) -> inserted as FX plugin on track ``` **Key data flow** (graphical editor): @@ -288,11 +327,11 @@ Real-time corrector: - **Cached pan gains**: `TrackProcessor` pre-computes `cos`/`sin` pan gains as `std::atomic` (`cachedPanL`, `cachedPanR`) when `setPan()` or `setVolume()` is called on the message thread. `processBlock()` on the audio thread loads these atomics cheaply — no trig computation per callback. - **AudioRecorder::writeBlock()** also uses `ScopedTryLock` — same pattern. -### PeakCache System (.s13peaks) +### PeakCache System (.ospeaks) -- REAPER-inspired multi-resolution peak cache stored as `.s13peaks` sidecar files alongside audio files +- REAPER-inspired multi-resolution peak cache stored as `.ospeaks` sidecar files alongside audio files - 4 mipmap levels at strides: 64, 256, 1024, 4096 samples per peak -- File format: `PeakFileHeader` (magic `0x53313350` / "S13P", version, source file size/timestamp for invalidation, sample rate, channels, level count) followed by flat float arrays per level +- File format: `PeakFileHeader` (magic `0x4f53504b` / "OSPK", version, source file size/timestamp for invalidation, sample rate, channels, level count) followed by flat float arrays per level - `AudioEngine::getWaveformPeaks()` reads from PeakCache — never reads audio files directly. First call generates the cache synchronously; subsequent calls are instant (memory-cached mipmap lookup) - Peak generation is triggered automatically in the background when recording stops (`peakCache.generateAsync()` for each completed clip) - `PeakCache::buildPeaks()` reads the audio file in a single pass, computing all 4 mipmap levels simultaneously using per-level accumulators @@ -337,11 +376,11 @@ Real-time corrector: ## Coding Preferences - Prefer targeted, minimal fixes over large refactors -- Frontend changes don't require C++ rebuild — just refresh the WebView +- During Vite development, frontend edits update through HMR without recompiling C++; follow the Debug build/copy requirement above before a manual-testing handoff. - C++ changes require `cmake --build build --config Debug` (or Release) - C++ builds should compile with **zero warnings** (`/W4` is enabled) — use `juce::ignoreUnused()` for required-but-unused params, avoid C macro name collisions, use `const auto&` for rvalue refs -- TypeScript has some pre-existing errors in MenuBar, Playhead, ProjectSettingsModal, TrackHeader — these are known -- Use `npx tsc --noEmit` to check for new TS errors after changes +- Run `npm run build` in `frontend` for the dependency-notice check, TypeScript and Vite build. Do not waive diagnostics based on a historical list of known errors. +- Use `npx tsc --noEmit` in `frontend` for a focused TypeScript check during iteration. ## Known Pitfalls & Past Issues @@ -377,11 +416,16 @@ In Konva, `onMouseDown` fires before `onClick`. Handle all selection logic in `o Some VST3 plugins (e.g., Amplitube) expect specific channel counts — `TrackProcessor` and render path expand buffers before `processBlock()` if needed (`safeRenderFX` lambda). The render path must also re-prepare all FX plugins with render block size (512) and `reset()` them, then restore original state after. Without this, plugins overflow internal buffers and produce noise. -### Render Modal — What Actually Works in Backend +### Render and Export Documentation -- **Working**: format (wav/aiff/flac), bit depth (16/24/32), channels (stereo/mono), normalize, tail -- **Ignored**: sample rate (always renders at device rate) -- **Not implemented**: "selected_tracks"/"stems" source (always master mix), dither +- The current renderer supports selected output sample rates, selected-track/stem + sources and dither; the old notes saying these are ignored are obsolete. +- Verify source selection and queue behavior in `frontend/src/store/actions/rendering.ts` + and `frontend/src/utils/renderJobPlanning.ts`, and native rendering in + `Source/AudioEngine.h/cpp`. Do not infer behavior solely from visible controls. +- Keep [the user manual](docs/USER_MANUAL.md#12-rendering-and-exporting) and + [the implemented-feature inventory](docs/implemented_features.md) aligned. + Metadata fields remain a disabled placeholder; do not claim they are written. ### C++ Naming Conflicts with C Standard Library Macros diff --git a/CLAUDE.md b/CLAUDE.md index b987957..218d9a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,359 +1,8 @@ -# OpenStudio +# OpenStudio instructions for Claude -A hybrid DAW (Digital Audio Workstation) with a **JUCE C++ backend** for audio processing and a **React/TypeScript frontend** rendered in WebView2. +@AGENTS.md -## Architecture - -``` -C++ (JUCE) Backend React/TypeScript Frontend -┌─────────────────────┐ ┌──────────────────────────┐ -│ AudioEngine │◄───►│ NativeBridge.ts │ -│ PlaybackEngine │ │ (window.__JUCE__) │ -│ AudioRecorder │ ├──────────────────────────┤ -│ TrackProcessor │ │ useDAWStore.ts (Zustand) │ -│ PluginManager │ ├──────────────────────────┤ -│ MIDIManager │ │ Timeline.tsx (Konva) │ -│ Metronome │ │ MixerPanel / ChannelStrip │ -│ MainComponent │ │ TransportBar / MenuBar │ -│ (WebBrowserComponent) │ FXChainPanel / PianoRoll │ -└─────────────────────┘ └──────────────────────────┘ -``` - -- **C++ backend** handles: audio I/O, recording to disk, clip playback with sample-rate conversion, VST3 plugin hosting, MIDI device management, metering, offline render/export -- **React frontend** handles: all UI, state management (Zustand), canvas-based timeline (Konva/react-konva), keyboard shortcuts, drag-and-drop, project save/load -- **Communication**: synchronous bridge via `window.__JUCE__.backend.*` functions (defined in NativeBridge.ts, exposed in MainComponent.cpp) - -## Directory Structure - -``` -OpenStudio/ -├── Source/ # C++ backend -│ ├── Main.cpp # JUCE app entry point -│ ├── MainComponent.h/cpp # Hosts WebBrowserComponent + AudioEngine, exposes native functions to JS -│ ├── AudioEngine.h/cpp # Core audio callback, device management, track graph, render, pitch correction entry point -│ ├── PlaybackEngine.h/cpp # Clip playback scheduling, sample-rate-aware mixing, replaceClipAudioFile -│ ├── AudioRecorder.h/cpp # Thread-safe recording via juce::AudioFormatWriter::ThreadedWriter -│ ├── TrackProcessor.h/cpp # Per-track juce::AudioProcessor: metering, FX chain, input monitoring -│ ├── PluginManager.h/cpp # VST3/CLAP/LV2 plugin scanning and loading -│ ├── PluginWindowManager.h/cpp# Native plugin editor window management -│ ├── CLAPPluginFormat.h/cpp # CLAP plugin hosting (parameter discovery, GUI, state) -│ ├── MIDIManager.h/cpp # MIDI device enumeration and input routing -│ ├── MIDIClip.h/cpp # MIDI note event storage and time-range queries -│ ├── Metronome.h/cpp # Click track generation (BPM, time sig, accent patterns) -│ ├── AudioConverter.h/cpp # Channel/sample-rate conversion utilities -│ ├── PeakCache.h/cpp # REAPER-style multi-resolution peak cache (.s13peaks sidecar files) -│ ├── AudioAnalyzer.h/cpp # Audio analysis utilities -│ ├── BuiltInEffects.h/cpp # Built-in audio effects (EQ, compressor, etc.) -│ ├── BuiltInEffects2.h/cpp # Additional built-in effects -│ │ -│ │ # Pitch Editor / Correction Pipeline (see "Pitch Editor Subsystem" below) -│ ├── PitchAnalyzer.h/cpp # YIN monophonic pitch detection, note segmentation, pitchDrift -│ ├── PitchDetector.h/cpp # Low-level YIN pitch detection algorithm -│ ├── PitchMapper.h/cpp # Maps detected pitch to corrected pitch (scale/key snapping) -│ ├── PitchShifter.h/cpp # Phase vocoder pitch shifter (FFT 2048, hop 512, FIFO-based) -│ ├── PitchResynthesizer.h/cpp # Offline graphical pitch correction via native VSF pitch-only renderer -│ ├── FormantPreserver.h/cpp # WORLD vocoder (DIO+StoneMask+CheapTrick+D4C) for formant-preserving pitch shift (fallback) -│ ├── PolyPitchDetector.h/cpp # Polyphonic pitch detection via Basic-Pitch ONNX model -│ ├── PolyResynthesizer.h/cpp # Polyphonic pipeline: STFT→Wiener masks→per-note shift→accumulate→ISTFT -│ ├── HarmonicMaskGenerator.h/cpp # Wiener-filter soft masks at harmonic positions for poly separation -│ ├── SpectralPitchShifter.h/cpp # Phase vocoder on masked spectrograms with cepstral formant preservation -│ ├── SpectralProcessor.h/cpp # STFT/ISTFT utilities for spectral processing -│ ├── S13PitchCorrector.h/cpp # Real-time inline pitch corrector (auto-tune style) -│ │ -│ │ # Plugin System -│ ├── S13FXProcessor.h/cpp # JSFX/Lua script-based audio processor (wraps YSFX) -│ ├── S13FXGfxEditor.h/cpp # JSFX @gfx rendering via juce::Image framebuffer at 30fps -│ ├── S13PluginEditors.h/cpp # Built-in plugin editor windows -│ ├── S13ScriptWindow.h/cpp # Lua gfx API framebuffer window -│ ├── ScriptEngine.h/cpp # Lua scripting engine (sol2) -│ │ -│ │ # Other Features -│ ├── StemSeparator.h/cpp # AI stem separation (vocals/drums/bass/other) -│ └── ARAHostController.h/cpp # ARA plugin hosting controller -│ -├── frontend/ -│ ├── src/ -│ │ ├── App.tsx # Main layout: MenuBar → MainToolbar → workspace(TCP + Timeline) → TransportBar → LowerZone → Modals -│ │ ├── main.tsx # React entry point -│ │ ├── index.css # Tailwind theme with daw-* custom colors -│ │ ├── store/ -│ │ │ ├── useDAWStore.ts # Zustand store — all app state and actions (~3400 lines) -│ │ │ ├── actionRegistry.ts # Centralized action defs for Command Palette, shortcuts, menus -│ │ │ ├── pitchEditorStore.ts # Pitch editor Zustand store: notes, viewport, tools, undo, poly mode -│ │ │ ├── automationParams.ts # Automation parameter definitions -│ │ │ └── commands/ # Undo/redo: CommandManager.ts, TrackCommands.ts, ClipCommands.ts -│ │ ├── services/ -│ │ │ └── NativeBridge.ts # Type-safe bridge to C++ backend (with mock fallbacks for dev) -│ │ ├── utils/ -│ │ │ └── snapToGrid.ts # Musical grid snapping (bar, beat, subdivisions) -│ │ └── components/ -│ │ ├── Timeline.tsx # Konva canvas: clips, waveforms, rulers, zoom, drag, selection (~2100 lines) -│ │ ├── TransportBar.tsx # Bottom bar: play/stop/record, BPM, time display, loop, metronome -│ │ ├── MixerPanel.tsx # Horizontal mixer with ChannelStrip components -│ │ ├── ChannelStrip.tsx # Track volume fader, pan, solo/mute, FX, meter -│ │ ├── ChannelStripEQModal.tsx # Per-channel EQ editor -│ │ ├── TrackHeader.tsx # Track name, arm, solo, mute, input selector -│ │ ├── TrackRoutingModal.tsx # Track I/O routing configuration -│ │ ├── SortableTrackHeader.tsx # @dnd-kit wrapper for track reordering + context menu -│ │ ├── FXChainPanel.tsx # Plugin browser + FX chain management (input FX + track FX) -│ │ ├── PianoRoll.tsx # MIDI note editor (Konva canvas) -│ │ ├── VirtualPianoKeyboard.tsx # 88-key on-screen MIDI keyboard -│ │ ├── MainToolbar.tsx # Top toolbar: transport, undo/redo, snap, mixer toggle, settings -│ │ ├── MenuBar.tsx # File/Edit/View/Insert/Help dropdown menus -│ │ ├── SettingsModal.tsx # Audio device configuration (driver, I/O, sample rate, buffer) -│ │ ├── RenderModal.tsx # Export dialog (format, bit depth, channels, normalize, tail) -│ │ ├── PreferencesModal.tsx # Tabbed prefs: General, Editing, Display, Backup -│ │ ├── ProjectSettingsModal.tsx # Project-level settings -│ │ ├── KeyboardShortcutsModal.tsx # Searchable action/shortcut reference (from actionRegistry) -│ │ ├── CommandPalette.tsx # Ctrl+Shift+P fuzzy action search -│ │ ├── PluginBrowser.tsx # VST3/CLAP/LV2 plugin selection UI -│ │ ├── LowerZone.tsx # Bottom panel container (pitch editor, clip properties, etc.) -│ │ ├── ClipPropertiesPanel.tsx # Clip property inspector -│ │ ├── ClipLauncherView.tsx # Ableton-style clip launcher grid -│ │ │ -│ │ │ # Pitch Editor UI -│ │ ├── PitchCorrectorPanel.tsx # Real-time inline corrector (auto-tune style, key/scale/retune) -│ │ ├── PitchEditorLowerZone.tsx # Graphical pitch editor: canvas host, tools, controls, interaction handlers -│ │ ├── PitchEditorCanvas.ts # Imperative canvas renderer (60fps RAF loop): notes, contour, grid, piano keys -│ │ ├── S13PitchEditor.tsx # Pitch editor wrapper/container -│ │ ├── pitchCorrectorPresets.ts # Preset definitions for real-time pitch corrector -│ │ │ -│ │ │ # Other -│ │ ├── StemSeparationModal.tsx # AI stem separation UI -│ │ ├── EnvelopeManagerModal.tsx # Automation envelope management -│ │ ├── TimecodeSettingsPanel.tsx # Timecode display settings -│ │ ├── ThemeEditor.tsx # Theme customization + REAPER theme import -│ │ ├── GettingStartedGuide.tsx # First-run guide -│ │ ├── HelpOverlay.tsx # Contextual help overlay -│ │ ├── MasterTrackHeader.tsx # Master track control strip -│ │ ├── Playhead.tsx # Timeline playhead cursor -│ │ ├── PeakMeter.tsx # Audio level meters -│ │ ├── icons.tsx # SVG icon components -│ │ ├── menus/ # MenuDropdown.tsx, EditMenu.tsx -│ │ └── ui/ # Base components: Button, Input, Select, NativeSelect, Slider, Modal, Checkbox, Textarea, TimeSignatureInput -│ ├── package.json -│ ├── vite.config.ts -│ └── tsconfig.json -│ -├── tools/ # FFmpeg runtime setup, stem_separator.py, setup scripts -├── resources/ # ONNX models, presets, resources -├── build/ # CMake build output -├── CMakeLists.txt # C++ build: JUCE 9.0.1, ASIO SDK, WebView2, VST3, ONNX Runtime -├── build.py # Python orchestrator: cmake + npm + vite dev server -├── docs/roadmap.md # Open release/product work; completed implementation history stays in Git -└── WORKFLOWS.md # Dev workflow docs -``` - -## Build & Dev - -```bash -# Full dev (installs deps, builds C++ Debug, starts Vite HMR, launches app) -python build.py dev --run - -# Frontend only (no C++ rebuild needed for UI changes) -cd frontend && npm run dev - -# C++ rebuild only — Debug (for Source/ changes, skips cmake configure if already done) -cmake --build build --config Debug - -# C++ rebuild only — Release -cmake --build build --config Release - -# Production (builds frontend + Release C++, single .exe with embedded frontend) -python build.py prod -``` - -**No feature flags** — all features (ASIO, WASAPI, DirectSound, VST3 hosting, WebView2) are always enabled via hardcoded `target_compile_definitions` in CMakeLists.txt. The `build.py dev` mode uses Debug config; `build.py prod` uses Release. - -## Key Technical Details - -### State Management -- **Zustand** store in `useDAWStore.ts` holds all application state (~3400 lines) -- `useShallow` selectors prevent unnecessary re-renders -- Undo/redo via CommandManager pattern (TrackCommands, ClipCommands) -- Multi-clip selection: `selectedClipIds: string[]`, multi-track: `selectedTrackIds: string[]` -- Clipboard supports single and multi-clip with track position info -- **Action Registry** (`actionRegistry.ts`): centralized list of all actions with id, name, category, shortcut, execute. Used by CommandPalette, KeyboardShortcutsModal, and menus -- **Modal state pattern**: each modal follows `showX: boolean` + `toggleX()` in store + useShallow selector in App.tsx + keyboard shortcut + menu item + action registry entry - -### Undo/Redo Requirement (IMPORTANT) - -**Every new action/function that modifies clip or track data MUST be tracked via `commandManager.push()` or `commandManager.execute()`.** This includes but is not limited to: - -- Adding, removing, moving, splitting, resizing clips -- Changing clip properties: volume, pan, fades, color, mute, lock, groupId, reverse -- Paste, nudge, quantize, normalize operations -- Time selection operations (cut, delete, insert silence) -- Razor edit content deletion -- Track property changes (name, color, volume, pan, mute, solo, armed) - -**Pattern for adding undo support:** - -1. **Before** the `set()` call, capture old state (snapshot or specific values) -2. **After** the `set()` call, capture new state -3. Call `commandManager.push({ type, description, timestamp, execute: () => set(newState), undo: () => set(oldState) })` -4. Call `set({ canUndo: commandManager.canUndo(), canRedo: commandManager.canRedo() })` - -For **continuous edits** (faders, knobs), use the begin/commit pattern: `beginXEdit()` captures initial state, intermediate `setX()` calls update live without undo, `commitXEdit()` pushes a single undo command covering the full range. - -**Do not skip undo tracking** — users expect Ctrl+Z to undo any data-modifying action. - -### Timeline Rendering -- **Konva** (react-konva) for canvas-based rendering -- Waveform peaks fetched from C++ via `getWaveformPeaks(filePath, samplesPerPixel, numPixels)` — backed by PeakCache (`.s13peaks` files), never reads audio files directly -- `samplesPerPixel` uses the clip's `sampleRate` (not hardcoded) with power-of-2 quantization for cache stability -- Zoom: exponential scaling via `Math.exp(-deltaY * sensitivity)`, anchored to cursor position -- Zoom debounce: suppresses waveform re-fetches during active zoom (`isZoomingRef`, 200ms timeout) -- Scroll debounce: suppresses waveform re-fetches during active scrolling (`isScrollingRef`, 200ms timeout) -- In-flight request dedup: `inFlightRef` prevents duplicate concurrent bridge calls for the same waveform cache key -- Zoom range: 1–1000 pixels/second (clamped in both Timeline.tsx and store's `setZoom`) -- Waveform peak data is parsed from flat C++ arrays into `WaveformPeak[]` objects via `parseFlatPeaks()` in NativeBridge.ts — format: `[numChannels, min_ch0_px0, max_ch0_px0, min_ch1_px0, max_ch1_px0, ...]` - -### Audio Engine -- Sample rate conversion in PlaybackEngine: linear interpolation when file rate != device rate -- Render uses same PlaybackEngine.fillTrackBuffer() — automatically handles rate conversion -- FX plugins: per-track input FX chain + track FX chain + master FX chain -- Render re-prepares plugins for offline block size, restores state after - -### Pitch Editor Subsystem - -The pitch editor enables vocal pitch correction with both real-time (auto-tune style) and graphical (Melodyne-style) modes. Current open work and release decisions live in `docs/roadmap.md`; renderer evidence is retained only in the dedicated pitch research notes. - -**Architecture**: -``` -Monophonic graphical pitch pipeline: - PitchAnalyzer (YIN) → PitchNotes → PitchResynthesizer::processMultiChannel() → native VSF pitch-only renderer - -Polyphonic pipeline: - PolyPitchDetector (Basic-Pitch ONNX) → PolyNotes → HarmonicMaskGenerator (Wiener) → SpectralPitchShifter → PolyResynthesizer - -Real-time corrector: - S13PitchCorrector (per-block, key/scale aware) → inserted as FX plugin on track -``` - -**Key data flow** (graphical editor): -1. User opens pitch editor → `pitchEditorStore.open()` → `nativeBridge.analyzePitchContourDirect()` -2. C++ `PitchAnalyzer` runs YIN detection, returns frames (per-hop pitch data) + notes (segmented note objects) -3. Frontend renders notes as blobs on canvas (`PitchEditorCanvas.ts`), user edits correctedPitch/drift/vibrato/etc. -4. On edit commit (400ms auto-apply debounce) → `nativeBridge.applyPitchCorrection(trackId, clipId, notes)` -5. C++ `AudioEngine::applyPitchCorrection()`: - - Loads original clip audio → extracts window around edited notes - - Runs the native VSF pitch-only renderer and composites the corrected region back into the clip - - Deletes old output file, writes new rotating output file → `PlaybackEngine::replaceClipAudioFile()` swaps it in (resets `clip.offset = 0`) - -**PitchResynthesizer engine** (`PitchEngine` enum): native VSF pitch-only offline apply. Preview/scrub audio and the realtime pitch-corrector FX use Signalsmith Stretch directly. - -**PitchShifter FIFO latency**: The phase vocoder has 2048-sample FIFO latency. The first fftSize output samples are zeros. `PitchResynthesizer` compensates by flushing the FIFO with silence and trimming the latency from the output. - -**WORLD vocoder** (`FormantPreserver`): Kept as fallback engine. Decomposes audio into F0 + spectral envelope + aperiodicity. Mono-only — when used via `processMultiChannel`, creates mono mix and duplicates to all channels. - -**Polyphonic detection**: Uses Spotify's Basic-Pitch ONNX model (22050 Hz, 256 hop). Thresholds are tunable: `noteThreshold=0.15`, `onsetThreshold=0.3`. Onset bypass accepts sustained notes ≥200ms even without detected onset. - -**Frontend state**: `pitchEditorStore.ts` (separate Zustand store from main DAW store) — holds notes, viewport, selectedNoteIds, tools (Select/Pitch/Drift/Vibrato/Transition), undo/redo stack, poly mode toggle, auto-apply debounce. - -### Audio Thread Safety (REAPER-inspired) - -- **PlaybackEngine::fillTrackBuffer()** uses `ScopedTryLock` (not `ScopedLock`) — returns silence if the message thread holds the lock (adding/removing clips). This is rare and inaudible. Never use blocking locks on the audio thread. -- **Pre-allocated buffers**: `PlaybackEngine` has a `reusableFileBuffer` member that is reused across clips/callbacks. Never heap-allocate (`new`, `AudioBuffer(...)`) on the audio thread. -- **Pre-loaded readers**: `addClip()` calls `preloadReader()` on the message thread so `AudioFormatReader` objects are cached before the audio thread needs them. The audio thread uses `getCachedReader()` which only does a map lookup — never creates readers or does disk I/O. -- **Cached pan gains**: `TrackProcessor` pre-computes `cos`/`sin` pan gains as `std::atomic` (`cachedPanL`, `cachedPanR`) when `setPan()` or `setVolume()` is called on the message thread. `processBlock()` on the audio thread loads these atomics cheaply — no trig computation per callback. -- **AudioRecorder::writeBlock()** also uses `ScopedTryLock` — same pattern. - -### PeakCache System (.s13peaks) - -- REAPER-inspired multi-resolution peak cache stored as `.s13peaks` sidecar files alongside audio files -- 4 mipmap levels at strides: 64, 256, 1024, 4096 samples per peak -- File format: `PeakFileHeader` (magic `0x53313350` / "S13P", version, source file size/timestamp for invalidation, sample rate, channels, level count) followed by flat float arrays per level -- `AudioEngine::getWaveformPeaks()` reads from PeakCache — never reads audio files directly. First call generates the cache synchronously; subsequent calls are instant (memory-cached mipmap lookup) -- Peak generation is triggered automatically in the background when recording stops (`peakCache.generateAsync()` for each completed clip) -- `PeakCache::buildPeaks()` reads the audio file in a single pass, computing all 4 mipmap levels simultaneously using per-level accumulators -- Background generation uses a `juce::ThreadPool` with 1 thread; completion callbacks fire on the message thread via `juce::MessageManager::callAsync` - -### Bridge Pattern -- `NativeBridge.ts` wraps `window.__JUCE__.backend.*` calls -- All methods have mock fallbacks for frontend-only development -- Real-time metering via event system (`addEventListener`/`removeEventListener`) -- Async: all bridge calls return Promises - -### Theme -- Tailwind CSS v4 with custom `daw-*` color tokens defined in `index.css` -- Dark theme: `daw-dark` (#121212), `daw-panel` (#1a1a1a), `daw-accent` (#0078d4) -- Semantic colors: `daw-record` (red), `daw-mute` (green), `daw-solo` (yellow), `daw-fx` (lime) -- UI components in `components/ui/` use variant pattern (default, primary, success, danger, etc.) - -### Frontend Styling and Visual QA - -- Do not generate stylesheet strings at runtime, mount JSX `