diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d59e4c488..5241e1d93 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,8 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - lfs: true + with: + fetch-depth: 100 - uses: actions/setup-node@v6 with: node-version: 'lts/*' @@ -31,8 +31,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - lfs: true + with: + fetch-depth: 100 - uses: actions/setup-node@v6 with: node-version: 'lts/*' @@ -43,7 +43,7 @@ jobs: - run: npm run build-language-server - run: npm run build-monaco id: test - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: ${{ failure() && steps.test.conclusion == 'failure' }} with: name: test-results-web @@ -56,8 +56,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - lfs: true + with: + fetch-depth: 100 - uses: actions/setup-node@v6 with: node-version: 'lts/*' @@ -69,7 +69,7 @@ jobs: - run: npm run build-csharp - run: npm run test-csharp id: test - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: ${{ failure() && steps.test.conclusion == 'failure' }} with: name: test-results-csharp @@ -80,19 +80,10 @@ jobs: build_kotlin: name: Build and Test Kotlin runs-on: ubuntu-latest - env: - OSSRH_USERNAME: ${{secrets.OSSRH_USERNAME}} - OSSRH_PASSWORD: ${{secrets.OSSRH_PASSWORD}} - OSSRH_USERTOKEN_USERNAME: ${{secrets.OSSRH_USERTOKEN_USERNAME}} - OSSRH_USERTOKEN_PASSWORD: ${{secrets.OSSRH_USERTOKEN_PASSWORD}} - SONATYPE_STAGING_PROFILE_ID: ${{secrets.SONATYPE_STAGING_PROFILE_ID}} - SONATYPE_SIGNING_KEY_ID: ${{secrets.SONATYPE_SIGNING_KEY_ID}} - SONATYPE_SIGNING_PASSWORD: ${{secrets.SONATYPE_SIGNING_PASSWORD}} - SONATYPE_SIGNING_KEY: ${{secrets.SONATYPE_SIGNING_KEY}} steps: - uses: actions/checkout@v6 - with: - lfs: true + with: + fetch-depth: 100 - uses: actions/setup-node@v6 with: node-version: 'lts/*' @@ -101,14 +92,14 @@ jobs: with: java-version: '19' distribution: 'temurin' - - uses: gradle/actions/setup-gradle@v5 + - uses: gradle/actions/setup-gradle@v6 with: cache-read-only: false - run: npm ci - run: npm run build-kotlin - run: npm run test-kotlin id: test - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: ${{ failure() && steps.test.conclusion == 'failure' }} with: name: test-results-kotlin diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 441cf161d..9e2f8a23c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,139 +6,21 @@ on: workflow_dispatch: jobs: - check_sha: - name: check_sha - runs-on: ubuntu-latest - outputs: - hit: ${{ steps.cache.outputs.cache-hit == 'true' }} - steps: - - run: touch dummy.txt - - - uses: actions/cache@v5 - id: cache - with: - path: dummy.txt - key: check-sha-${{ github.sha }} - - nighty_web: - name: Web - needs: check_sha - if: needs.check_sha.outputs.hit == 'false' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - lfs: true - - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - cache: 'npm' - - - run: npm run update-version -- alpha ${{github.run_number}} - - run: npm ci - - run: npm run build-web - - run: npm run build-language-server - - run: npm run build-monaco - - run: npm pack - working-directory: ./packages/alphatab/ - - run: npm pack - working-directory: ./packages/vite/ - - run: npm pack - working-directory: ./packages/webpack/ - - run: npm pack - working-directory: ./packages/lsp/ - - run: npm pack - working-directory: ./packages/monaco/ - - - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - registry-url: https://registry.npmjs.org/ - - - run: npm publish --access public --tag alpha - working-directory: ./packages/alphatab/ - env: - NODE_AUTH_TOKEN: ${{secrets.NPMJS_AUTH_TOKEN}} - - - run: npm publish --access public --tag alpha - working-directory: ./packages/vite/ - env: - NODE_AUTH_TOKEN: ${{secrets.NPMJS_AUTH_TOKEN}} - - - run: npm publish --access public --tag alpha - working-directory: ./packages/webpack/ - env: - NODE_AUTH_TOKEN: ${{secrets.NPMJS_AUTH_TOKEN}} - - - run: npm publish --access public --tag alpha - working-directory: ./packages/lsp/ - env: - NODE_AUTH_TOKEN: ${{secrets.NPMJS_AUTH_TOKEN}} - - - run: npm publish --access public --tag alpha - working-directory: ./packages/monaco/ - env: - NODE_AUTH_TOKEN: ${{secrets.NPMJS_AUTH_TOKEN}} - - nightly_csharp: - name: C# - needs: check_sha - if: needs.check_sha.outputs.hit == 'false' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - lfs: true - - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - cache: 'npm' - - - uses: actions/setup-dotnet@v5 - with: - dotnet-version: "8" - - - run: npm run update-version -- alpha ${{github.run_number}} - - run: npm ci - - run: npm run build-csharp - - - run: dotnet nuget push AlphaTab/bin/Release/*.nupkg -k ${{secrets.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json - working-directory: ./packages/csharp/src/ - - run: dotnet nuget push AlphaTab.Windows/bin/Release/*.nupkg -k ${{secrets.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json - working-directory: ./packages/csharp/src/ - - nightly_kotlin_android: - name: Kotlin (Android) - needs: check_sha - if: needs.check_sha.outputs.hit == 'false' - runs-on: ubuntu-latest - env: - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{secrets.OSSRH_USERTOKEN_USERNAME}} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{secrets.OSSRH_USERTOKEN_PASSWORD}} - ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{secrets.SONATYPE_SIGNING_KEY_ID}} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{secrets.SONATYPE_SIGNING_PASSWORD}} - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{secrets.SONATYPE_SIGNING_KEY}} - steps: - - uses: actions/checkout@v6 - with: - lfs: true - - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - cache: 'npm' - - - uses: actions/setup-java@v5 - with: - java-version: "19" - distribution: "temurin" - - - run: npm run update-version -- alpha ${{github.run_number}} - - run: npm ci - - run: npm run build-kotlin - - - run: ./gradlew publishToMavenCentral - working-directory: ./packages/kotlin/src/ - - - run: ./gradlew --stop - working-directory: ./packages/kotlin/src/ - + web: + uses: ./.github/workflows/~publish_web.yml + secrets: inherit + with: + version: alpha ${{github.run_number}} + npm_tag: alpha + + dotnet: + uses: ./.github/workflows/~publish_dotnet.yml + secrets: inherit + with: + version: alpha ${{github.run_number}} + + kotlin: + uses: ./.github/workflows/~publish_kotlin.yml + secrets: inherit + with: + version: alpha ${{github.run_number}} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cad9beecc..44492ba0f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,116 +19,27 @@ on: default: true jobs: - release_web: - name: Web - runs-on: ubuntu-latest + web: + uses: ./.github/workflows/~publish_web.yml + secrets: inherit if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_web == 'true') - steps: - - uses: actions/checkout@v6 - with: - lfs: true - - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - - - run: npm run update-version -- ${{github.run_number}} - - run: npm ci - - run: npm run build-web - - run: npm run build-language-server - - run: npm run build-monaco - - run: npm pack - working-directory: ./packages/alphatab/ - - run: npm pack - working-directory: ./packages/vite/ - - run: npm pack - working-directory: ./packages/webpack/ - - run: npm pack - working-directory: ./packages/lsp/ - - run: npm pack - working-directory: ./packages/monaco/ - - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - registry-url: https://registry.npmjs.org/ - - name: Publish to NPM (alphaTab release) - run: npm publish --access public - working-directory: ./packages/alphatab/ - env: - NODE_AUTH_TOKEN: ${{secrets.NPMJS_AUTH_TOKEN}} - - name: Publish to NPM (Vite Plugin release) - run: npm publish --access public - working-directory: ./packages/vite/ - env: - NODE_AUTH_TOKEN: ${{secrets.NPMJS_AUTH_TOKEN}} - - name: Publish to NPM (Webpack release) - run: npm publish --access public - working-directory: ./packages/webpack/ - env: - NODE_AUTH_TOKEN: ${{secrets.NPMJS_AUTH_TOKEN}} - - name: Publish to NPM (Language Server release) - run: npm publish --access public - working-directory: ./packages/lsp/ - env: - NODE_AUTH_TOKEN: ${{secrets.NPMJS_AUTH_TOKEN}} - - name: Publish to NPM (Monaco release) - run: npm publish --access public - working-directory: ./packages/monaco/ - env: - NODE_AUTH_TOKEN: ${{secrets.NPMJS_AUTH_TOKEN}} - - release_csharp: - name: C# - runs-on: ubuntu-latest + with: + version: ${{github.run_number}} + npm_tag: latest + force: true + + dotnet: + uses: ./.github/workflows/~publish_dotnet.yml + secrets: inherit if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_csharp == 'true') - steps: - - uses: actions/checkout@v6 - with: - lfs: true - - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - - uses: actions/setup-dotnet@v5 - with: - dotnet-version: '8' - env: - NUGET_AUTH_TOKEN: ${{secrets.NUGET_API_KEY}} - - run: npm run update-version -- ${{github.run_number}} - - run: npm ci - - run: npm run build-csharp - - run: dotnet nuget push AlphaTab/bin/Release/*.nupkg -k ${{secrets.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json --skip-duplicate - working-directory: ./packages/csharp/src/ - - run: dotnet nuget push AlphaTab.Windows/bin/Release/*.nupkg -k ${{secrets.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json --skip-duplicate - working-directory: ./packages/csharp/src/ + with: + version: ${{github.run_number}} + force: true - release_kotlin_android: - name: Kotlin (Android) - runs-on: windows-latest + kotlin: + uses: ./.github/workflows/~publish_kotlin.yml + secrets: inherit if: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_kotlin_android == 'true') - env: - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{secrets.OSSRH_USERTOKEN_USERNAME}} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{secrets.OSSRH_USERTOKEN_PASSWORD}} - ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{secrets.SONATYPE_SIGNING_KEY_ID}} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{secrets.SONATYPE_SIGNING_PASSWORD}} - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{secrets.SONATYPE_SIGNING_KEY}} - steps: - - uses: actions/checkout@v6 - with: - lfs: true - - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - - - uses: actions/setup-java@v5 - with: - java-version: "19" - distribution: "temurin" - - - run: npm run update-version -- ${{github.run_number}} - - run: npm ci - - run: npm run build-kotlin - - run: ./gradlew publishToMavenCentral - working-directory: ./packages/kotlin/src/ - - - run: ./gradlew --stop - working-directory: ./packages/kotlin/src/ - + with: + version: ${{github.run_number}} + force: true \ No newline at end of file diff --git a/.github/workflows/~publish_dotnet.yml b/.github/workflows/~publish_dotnet.yml new file mode 100644 index 000000000..5655bc8f8 --- /dev/null +++ b/.github/workflows/~publish_dotnet.yml @@ -0,0 +1,50 @@ +on: + workflow_call: + inputs: + version: + type: string + required: true + force: + type: boolean + default: false + +jobs: + check: + runs-on: ubuntu-latest + outputs: + has_changes: ${{ steps.check.outputs.has_changes }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 100 + - uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + cache: 'npm' + - run: npm run nightly-check -- --platform dotnet --force ${{ inputs.force }} + id: check + + build: + needs: check + if: needs.check.outputs.has_changes == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 100 + - uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + cache: 'npm' + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: "8" + + - run: npm run update-version -- ${{inputs.version}} + - run: npm ci + - run: npm run build-csharp + + - run: npm run nightly-pack -- --platform dotnet --force ${{ inputs.force }} + - run: npm run nightly-publish -- --platform dotnet --force ${{ inputs.force }} + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} diff --git a/.github/workflows/~publish_kotlin.yml b/.github/workflows/~publish_kotlin.yml new file mode 100644 index 000000000..25bc40a78 --- /dev/null +++ b/.github/workflows/~publish_kotlin.yml @@ -0,0 +1,55 @@ +on: + workflow_call: + inputs: + version: + type: string + required: true + force: + type: boolean + default: false + +jobs: + check: + runs-on: ubuntu-latest + outputs: + has_changes: ${{ steps.check.outputs.has_changes }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 100 + - uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + cache: 'npm' + - run: npm run nightly-check -- --platform kotlin --force ${{ inputs.force }} + id: check + + build: + needs: check + if: needs.check.outputs.has_changes == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 100 + - uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + cache: 'npm' + - uses: actions/setup-java@v5 + with: + java-version: "19" + distribution: "temurin" + + - run: npm run update-version -- ${{inputs.version}} + - run: npm ci + - run: npm run build-kotlin + + - run: npm run nightly-pack -- --platform kotlin --force ${{ inputs.force }} + - run: npm run nightly-publish -- --platform kotlin --force ${{ inputs.force }} + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{secrets.OSSRH_USERTOKEN_USERNAME}} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{secrets.OSSRH_USERTOKEN_PASSWORD}} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{secrets.SONATYPE_SIGNING_KEY_ID}} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{secrets.SONATYPE_SIGNING_PASSWORD}} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{secrets.SONATYPE_SIGNING_KEY}} diff --git a/.github/workflows/~publish_web.yml b/.github/workflows/~publish_web.yml new file mode 100644 index 000000000..4b79f8d0b --- /dev/null +++ b/.github/workflows/~publish_web.yml @@ -0,0 +1,54 @@ +on: + workflow_call: + inputs: + version: + type: string + required: true + force: + type: boolean + default: false + npm_tag: + type: string + required: true + + +jobs: + check: + runs-on: ubuntu-latest + outputs: + has_changes: ${{ steps.check.outputs.has_changes }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 100 + - uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + cache: 'npm' + - run: npm run nightly-check -- --platform web --force ${{ inputs.force }} + id: check + + build: + needs: check + if: needs.check.outputs.has_changes == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 100 + - uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + cache: 'npm' + registry-url: https://registry.npmjs.org/ + + - run: npm run update-version -- ${{inputs.version}} + - run: npm ci + - run: npm run build-web + - run: npm run build-language-server + - run: npm run build-monaco + + - run: npm run nightly-pack -- --platform web --force ${{ inputs.force }} + - run: npm run nightly-publish -- --platform web --force ${{ inputs.force }} --tag ${{ inputs.npm_tag }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPMJS_AUTH_TOKEN }} diff --git a/README.md b/README.md index 44e3b7659..e8e76cfb1 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,7 @@ [![Official Site](https://img.shields.io/badge/site-alphatab.net-blue.svg)](https://www.alphatab.net/) [![Documentation](https://img.shields.io/badge/docs-alphatab.net-blue.svg)](https://www.alphatab.net/docs/introduction) [![License MPL-2.0](https://img.shields.io/badge/license-MPL--2.0-green.svg)](https://www.mozilla.org/en-US/MPL/2.0/) -[![Twitter](https://img.shields.io/badge/twitter-alphaTabMusic-blue.svg)](https://twitter.com/alphaTabMusic) -[![Facebook](https://img.shields.io/badge/facebook-alphaTabMusic-blue.svg)](https://facebook.com/alphaTabMusic) -[![Build](https://github.com/CoderLine/alphaTab/workflows/Build/badge.svg?branch=develop)](https://github.com/CoderLine/alphaTab/actions/workflows/build.yml) +[![Build](https://github.com/CoderLine/alphaTab/actions/workflows/build.yml/badge.svg?branch=develop)](https://github.com/CoderLine/alphaTab/actions/workflows/build.yml) alphaTab is a cross platform music notation and guitar tablature rendering library. You can use alphaTab within your own website or application to load and display music sheets from data sources like Guitar Pro or the built in markup language named alphaTex. @@ -22,9 +20,9 @@ To get started follow our guides and tutorials at: alphaTab mostly focuses on web based platforms allowing music notation to be embedded into websites and browser based apps but is also designed to be used on various other platforms like .net and Android either as a platform native integration or through runtime specific JavaScript engines. -alphaTab can load music notation from various sources like Guitar Pro 3-7, AlphaTex and MusicXML (experimental) and render them into beautiful music sheets right within your browser (or application). Using a built in midi synthesizer named alphaSynth the music sheets can also be played in your browser. +alphaTab can load music notation from various sources like Guitar Pro 3-7, AlphaTex and MusicXML and render them into beautiful music sheets right within your browser (or application). Using a built in midi synthesizer named alphaSynth the music sheets can also be played in your browser. -* Load GuitarPro 3-5, GuitarPro 6, Guitar Pro 7, AlphaTex or MusicXML (experimental) +* Load GuitarPro 3-5, GuitarPro 6, Guitar Pro 7, AlphaTex or MusicXML * Render as SVG or Raster Graphics (raster graphics depends on platform: HTML5 canvas, GDI+, SkiaSharp, Android Canvas)... * Display single or multiple instruments as standard music notation and guitar tablatures consisting of song information, repeats, alternate endings, guitar tunints, clefs, key signatures, time signatures, notes, rests, accidentals, drum tabs, piano grand staff, tied notes, grace notes, dead notes, ghost notes, markers, tempos, lyrics, chords, vibratos, dynamics, tap/slap/pop, fade-in, let-ring, palm-mute, string bends, whammy bar, tremolo picking, strokes, slides, trills, pick strokes, tuplets, fingering, triplet feels,... * Adapt to your responsive design by dynamic resizing diff --git a/package-lock.json b/package-lock.json index be765f55d..3df881617 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coderline/alphatab-monorepo", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@coderline/alphatab-monorepo", - "version": "1.8.1", + "version": "1.9.0", "workspaces": [ "packages/*" ], @@ -473,7 +473,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.3.11", + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.10.tgz", + "integrity": "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -487,18 +489,88 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.11", - "@biomejs/cli-darwin-x64": "2.3.11", - "@biomejs/cli-linux-arm64": "2.3.11", - "@biomejs/cli-linux-arm64-musl": "2.3.11", - "@biomejs/cli-linux-x64": "2.3.11", - "@biomejs/cli-linux-x64-musl": "2.3.11", - "@biomejs/cli-win32-arm64": "2.3.11", - "@biomejs/cli-win32-x64": "2.3.11" + "@biomejs/cli-darwin-arm64": "2.4.10", + "@biomejs/cli-darwin-x64": "2.4.10", + "@biomejs/cli-linux-arm64": "2.4.10", + "@biomejs/cli-linux-arm64-musl": "2.4.10", + "@biomejs/cli-linux-x64": "2.4.10", + "@biomejs/cli-linux-x64-musl": "2.4.10", + "@biomejs/cli-win32-arm64": "2.4.10", + "@biomejs/cli-win32-x64": "2.4.10" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.10.tgz", + "integrity": "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.10.tgz", + "integrity": "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.10.tgz", + "integrity": "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.10.tgz", + "integrity": "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.11", + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.10.tgz", + "integrity": "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==", "cpu": [ "x64" ], @@ -513,7 +585,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.3.11", + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.10.tgz", + "integrity": "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==", "cpu": [ "x64" ], @@ -527,8 +601,44 @@ "node": ">=14.21.3" } }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.10.tgz", + "integrity": "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.10.tgz", + "integrity": "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@coderline/alphaskia": { - "version": "3.4.135", + "version": "3.5.147", + "resolved": "https://registry.npmjs.org/@coderline/alphaskia/-/alphaskia-3.5.147.tgz", + "integrity": "sha512-9RDWV6cralGAvKFU/aozCPAverWcBTQJHpUh6Y7gBlggEwsVjorIp3tuzVCDfV+SqkSMsJtZOiJ9R2CdJKJIVw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -558,7 +668,9 @@ } }, "node_modules/@coderline/alphaskia-windows": { - "version": "3.4.135", + "version": "3.5.147", + "resolved": "https://registry.npmjs.org/@coderline/alphaskia-windows/-/alphaskia-windows-3.5.147.tgz", + "integrity": "sha512-T8G4oE2bacNuI1mDoR6l7pXpbrR4ww2tdPlshrm/8En//Y4kY0EbDRP3Vd9qr3E8HP1Zy8peMXoE0e7Rt3c4Mg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -610,7 +722,9 @@ "link": true }, "node_modules/@discoveryjs/json-ext": { - "version": "0.6.3", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.0.0.tgz", + "integrity": "sha512-dDlz3W405VMFO4w5kIP9DOmELBcvFQGmLoKSdIRstBDubKFYwaNHV1NnlzMCQpXQFGWVALmeMORAuiLx18AvZQ==", "dev": true, "license": "MIT", "engines": { @@ -646,31 +760,14 @@ } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "7.1.0", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz", + "integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==", "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "dev": true, @@ -689,6 +786,8 @@ }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "license": "ISC", "dependencies": { @@ -711,7 +810,9 @@ } }, "node_modules/@jest/diff-sequences": { - "version": "30.0.1", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", "dev": true, "license": "MIT", "engines": { @@ -719,7 +820,9 @@ } }, "node_modules/@jest/expect-utils": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", "dev": true, "license": "MIT", "dependencies": { @@ -731,6 +834,8 @@ }, "node_modules/@jest/get-type": { "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, "license": "MIT", "engines": { @@ -739,6 +844,8 @@ }, "node_modules/@jest/pattern": { "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, "license": "MIT", "dependencies": { @@ -751,6 +858,8 @@ }, "node_modules/@jest/schemas": { "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { @@ -761,11 +870,13 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -776,6 +887,8 @@ }, "node_modules/@jest/snapshot-utils/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -790,6 +903,8 @@ }, "node_modules/@jest/snapshot-utils/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -805,6 +920,8 @@ }, "node_modules/@jest/snapshot-utils/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -815,22 +932,23 @@ } }, "node_modules/@jest/transform": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@jridgewell/trace-mapping": "^0.3.25", "babel-plugin-istanbul": "^7.0.1", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "jest-haste-map": "30.3.0", "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "micromatch": "^4.0.8", + "jest-util": "30.3.0", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" @@ -841,6 +959,8 @@ }, "node_modules/@jest/transform/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -855,6 +975,8 @@ }, "node_modules/@jest/transform/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -870,6 +992,8 @@ }, "node_modules/@jest/transform/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -880,7 +1004,9 @@ } }, "node_modules/@jest/types": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, "license": "MIT", "dependencies": { @@ -898,6 +1024,8 @@ }, "node_modules/@jest/types/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -912,6 +1040,8 @@ }, "node_modules/@jest/types/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -927,6 +1057,8 @@ }, "node_modules/@jest/types/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -972,20 +1104,22 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.55.2", + "version": "7.57.7", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.57.7.tgz", + "integrity": "sha512-kmnmVs32MFWbV5X6BInC1/TfCs7y1ugwxv1xHsAIj/DyUfoe7vtO0alRUgbQa57+yRGHBBjlNcEk33SCAt5/dA==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.32.2", + "@microsoft/api-extractor-model": "7.33.4", "@microsoft/tsdoc": "~0.16.0", - "@microsoft/tsdoc-config": "~0.18.0", - "@rushstack/node-core-library": "5.19.1", - "@rushstack/rig-package": "0.6.0", - "@rushstack/terminal": "0.19.5", - "@rushstack/ts-command-line": "5.1.5", + "@microsoft/tsdoc-config": "~0.18.1", + "@rushstack/node-core-library": "5.20.3", + "@rushstack/rig-package": "0.7.2", + "@rushstack/terminal": "0.22.3", + "@rushstack/ts-command-line": "5.3.3", "diff": "~8.0.2", - "lodash": "~4.17.15", - "minimatch": "10.0.3", + "lodash": "~4.17.23", + "minimatch": "10.2.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", @@ -996,13 +1130,15 @@ } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.32.2", + "version": "7.33.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.4.tgz", + "integrity": "sha512-u1LTaNTikZAQ9uK6KG1Ms7nvNedsnODnspq/gH2dcyETWvH4hVNGNDvRAEutH66kAmxA4/necElqGNs1FggC8w==", "dev": true, "license": "MIT", "dependencies": { "@microsoft/tsdoc": "~0.16.0", - "@microsoft/tsdoc-config": "~0.18.0", - "@rushstack/node-core-library": "5.19.1" + "@microsoft/tsdoc-config": "~0.18.1", + "@rushstack/node-core-library": "5.20.3" } }, "node_modules/@microsoft/api-extractor/node_modules/diff": { @@ -1027,16 +1163,20 @@ }, "node_modules/@microsoft/tsdoc": { "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", "dev": true, "license": "MIT" }, "node_modules/@microsoft/tsdoc-config": { - "version": "0.18.0", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.18.1.tgz", + "integrity": "sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==", "dev": true, "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.16.0", - "ajv": "~8.12.0", + "ajv": "~8.18.0", "jju": "~1.4.0", "resolve": "~1.22.2" } @@ -1094,16 +1234,18 @@ } }, "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", "dev": true, "license": "MIT", "dependencies": { - "serialize-javascript": "^6.0.1", + "serialize-javascript": "^7.0.3", "smob": "^1.0.0", "terser": "^5.17.4" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" @@ -1114,6 +1256,16 @@ } } }, + "node_modules/@rollup/plugin-terser/node_modules/serialize-javascript": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@rollup/plugin-typescript": { "version": "12.3.0", "dev": true, @@ -1183,11 +1335,13 @@ ] }, "node_modules/@rushstack/node-core-library": { - "version": "5.19.1", + "version": "5.20.3", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.20.3.tgz", + "integrity": "sha512-95JgEPq2k7tHxhF9/OJnnyHDXfC9cLhhta0An/6MlkDsX2A6dTzDrTUG18vx4vjc280V0fi0xDH9iQczpSuWsw==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "~8.13.0", + "ajv": "~8.18.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", "fs-extra": "~11.3.0", @@ -1205,23 +1359,10 @@ } } }, - "node_modules/@rushstack/node-core-library/node_modules/ajv": { - "version": "8.13.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@rushstack/problem-matcher": { - "version": "0.1.1", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@rushstack/problem-matcher/-/problem-matcher-0.2.1.tgz", + "integrity": "sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1234,7 +1375,9 @@ } }, "node_modules/@rushstack/rig-package": { - "version": "0.6.0", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.7.2.tgz", + "integrity": "sha512-9XbFWuqMYcHUso4mnETfhGVUSaADBRj6HUAAEYk50nMPn8WRICmBuCphycQGNB3duIR6EEZX3Xj3SYc2XiP+9A==", "dev": true, "license": "MIT", "dependencies": { @@ -1243,12 +1386,14 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.19.5", + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.22.3.tgz", + "integrity": "sha512-gHC9pIMrUPzAbBiI4VZMU7Q+rsCzb8hJl36lFIulIzoceKotyKL3Rd76AZ2CryCTKEg+0bnTj406HE5YY5OQvw==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.19.1", - "@rushstack/problem-matcher": "0.1.1", + "@rushstack/node-core-library": "5.20.3", + "@rushstack/problem-matcher": "0.2.1", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -1261,23 +1406,29 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "5.1.5", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.3.tgz", + "integrity": "sha512-c+ltdcvC7ym+10lhwR/vWiOhsrm/bP3By2VsFcs5qTKv+6tTmxgbVrtJ5NdNjANiV5TcmOZgUN+5KYQ4llsvEw==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.19.5", + "@rushstack/terminal": "0.22.3", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "node_modules/@sinclair/typebox": { - "version": "0.34.41", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, "node_modules/@types/argparse": { "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", "dev": true, "license": "MIT" }, @@ -1339,6 +1490,8 @@ }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "license": "MIT", "dependencies": { @@ -1347,6 +1500,8 @@ }, "node_modules/@types/istanbul-reports": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1363,12 +1518,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.6.tgz", - "integrity": "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/resolve": { @@ -1386,6 +1541,8 @@ }, "node_modules/@types/stack-utils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true, "license": "MIT" }, @@ -1395,14 +1552,16 @@ "optional": true }, "node_modules/@types/vscode": { - "version": "1.108.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.108.1.tgz", - "integrity": "sha512-DerV0BbSzt87TbrqmZ7lRDIYaMiqvP8tmJTzW2p49ZBVtGUnGAu2RGQd1Wv4XMzEVUpaHbsemVM5nfuQJj7H6w==", + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", "dev": true, "license": "MIT" }, "node_modules/@types/yargs": { - "version": "17.0.33", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, "license": "MIT", "dependencies": { @@ -1411,11 +1570,15 @@ }, "node_modules/@types/yargs-parser": { "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true, "license": "MIT" }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, "license": "ISC" }, @@ -1653,47 +1816,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@webpack-cli/configtest": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "license": "BSD-3-Clause" @@ -1703,7 +1825,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.15.0", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "peer": true, "bin": { @@ -1732,14 +1856,16 @@ } }, "node_modules/ajv": { - "version": "8.12.0", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -1748,6 +1874,8 @@ }, "node_modules/ajv-draft-04": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1761,6 +1889,8 @@ }, "node_modules/ajv-formats": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1777,6 +1907,8 @@ }, "node_modules/ajv-keywords": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" @@ -1802,6 +1934,8 @@ }, "node_modules/ansi-styles": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { @@ -1886,6 +2020,8 @@ }, "node_modules/babel-plugin-istanbul": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", "dev": true, "license": "BSD-3-Clause", "workspaces": [ @@ -2030,6 +2166,8 @@ }, "node_modules/bser": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2211,6 +2349,8 @@ }, "node_modules/camelcase": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", "engines": { @@ -2276,7 +2416,9 @@ } }, "node_modules/ci-info": { - "version": "4.3.0", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -2460,6 +2602,8 @@ }, "node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, @@ -2770,11 +2914,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.3", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -2904,6 +3050,8 @@ }, "node_modules/esprima": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "license": "BSD-2-Clause", "bin": { @@ -2958,16 +3106,18 @@ } }, "node_modules/expect": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.2.0", + "@jest/expect-utils": "30.3.0", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2979,9 +3129,27 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "dev": true, @@ -2992,6 +3160,8 @@ }, "node_modules/fb-watchman": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3081,7 +3251,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.2", + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", "dev": true, "license": "MIT", "dependencies": { @@ -3095,6 +3267,8 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, @@ -3172,6 +3346,8 @@ }, "node_modules/get-package-type": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", "engines": { @@ -3233,6 +3409,8 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/minimatch": { @@ -3270,7 +3448,9 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "license": "MIT", "dependencies": { "minimist": "^1.2.5", @@ -3376,7 +3556,9 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.5", + "version": "5.6.6", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.6.tgz", + "integrity": "sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw==", "dev": true, "license": "MIT", "dependencies": { @@ -3476,6 +3658,8 @@ }, "node_modules/import-lazy": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true, "license": "MIT", "engines": { @@ -3502,6 +3686,8 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -3510,6 +3696,9 @@ }, "node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", "dependencies": { @@ -3749,6 +3938,8 @@ }, "node_modules/istanbul-lib-instrument": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3813,14 +4004,16 @@ } }, "node_modules/jest-diff": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.0.1", + "@jest/diff-sequences": "30.3.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3828,6 +4021,8 @@ }, "node_modules/jest-diff/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -3842,6 +4037,8 @@ }, "node_modules/jest-diff/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -3857,6 +4054,8 @@ }, "node_modules/jest-diff/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -3867,19 +4066,21 @@ } }, "node_modules/jest-haste-map": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", "walker": "^1.0.8" }, "engines": { @@ -3890,14 +4091,16 @@ } }, "node_modules/jest-matcher-utils": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3905,6 +4108,8 @@ }, "node_modules/jest-matcher-utils/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -3919,6 +4124,8 @@ }, "node_modules/jest-matcher-utils/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -3934,6 +4141,8 @@ }, "node_modules/jest-matcher-utils/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -3944,17 +4153,19 @@ } }, "node_modules/jest-message-util": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -3964,6 +4175,8 @@ }, "node_modules/jest-message-util/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -3978,6 +4191,8 @@ }, "node_modules/jest-message-util/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -3993,6 +4208,8 @@ }, "node_modules/jest-message-util/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -4003,13 +4220,15 @@ } }, "node_modules/jest-mock": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-util": "30.2.0" + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4017,6 +4236,8 @@ }, "node_modules/jest-regex-util": { "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, "license": "MIT", "engines": { @@ -4024,7 +4245,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4033,20 +4256,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.2.0", + "@jest/expect-utils": "30.3.0", "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.2.0", + "expect": "30.3.0", "graceful-fs": "^4.2.11", - "jest-diff": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "pretty-format": "30.2.0", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -4106,16 +4329,18 @@ } }, "node_modules/jest-util": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" + "picomatch": "^4.0.3" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4123,6 +4348,8 @@ }, "node_modules/jest-util/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -4137,6 +4364,8 @@ }, "node_modules/jest-util/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -4152,6 +4381,8 @@ }, "node_modules/jest-util/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -4162,13 +4393,15 @@ } }, "node_modules/jest-worker": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", + "jest-util": "30.3.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -4178,6 +4411,8 @@ }, "node_modules/jju": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", "dev": true, "license": "MIT" }, @@ -4188,6 +4423,8 @@ }, "node_modules/js-yaml": { "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -4230,6 +4467,8 @@ }, "node_modules/jsonfile": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { @@ -4289,7 +4528,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -4387,6 +4628,8 @@ }, "node_modules/makeerror": { "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4415,29 +4658,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime-db": { "version": "1.52.0", "license": "MIT", @@ -4467,19 +4687,44 @@ } }, "node_modules/minimatch": { - "version": "10.0.3", + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", + "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimatch/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/minimist": { "version": "1.2.8", "license": "MIT", @@ -4488,9 +4733,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -4655,6 +4902,8 @@ }, "node_modules/natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, @@ -4673,6 +4922,8 @@ }, "node_modules/node-int64": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, "license": "MIT" }, @@ -4753,6 +5004,8 @@ }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { @@ -4879,7 +5132,9 @@ } }, "node_modules/p-map": { - "version": "7.0.3", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", "engines": { @@ -4953,6 +5208,8 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { @@ -5008,6 +5265,8 @@ }, "node_modules/pirates": { "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, "license": "MIT", "engines": { @@ -5069,7 +5328,9 @@ } }, "node_modules/pretty-format": { - "version": "30.2.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5086,15 +5347,9 @@ "dev": true, "license": "MIT" }, - "node_modules/punycode": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/randombytes": { "version": "2.1.0", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -5109,6 +5364,8 @@ }, "node_modules/react-is": { "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, "license": "MIT" }, @@ -5270,11 +5527,13 @@ } }, "node_modules/rimraf": { - "version": "6.1.2", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^13.0.0", + "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { @@ -5288,45 +5547,37 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "13.0.0", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.2.4", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "10.1.1", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, "engines": { "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rimraf/node_modules/path-scurry": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -5334,7 +5585,7 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5380,18 +5631,20 @@ } }, "node_modules/rollup-plugin-license": { - "version": "3.6.0", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-3.7.0.tgz", + "integrity": "sha512-RvvOIF+GH3fBR3wffgc/vmjQn6qOn72WjppWVDp/v+CLpT0BbcRBdSkPeeIOL6U5XccdYgSIMjUyXgxlKEEFcw==", "dev": true, "license": "MIT", "dependencies": { - "commenting": "~1.1.0", + "commenting": "^1.1.0", "fdir": "^6.4.3", - "lodash": "~4.17.21", - "magic-string": "~0.30.0", - "moment": "~2.30.1", - "package-name-regex": "~2.0.6", - "spdx-expression-validate": "~2.0.0", - "spdx-satisfies": "~5.0.1" + "lodash": "^4.17.21", + "magic-string": "^0.30.0", + "moment": "^2.30.1", + "package-name-regex": "^2.0.6", + "spdx-expression-validate": "^2.0.0", + "spdx-satisfies": "^5.0.1" }, "engines": { "node": ">=14.0.0" @@ -5431,6 +5684,7 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", + "dev": true, "funding": [ { "type": "github", @@ -5465,6 +5719,8 @@ }, "node_modules/schema-utils": { "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -5482,6 +5738,8 @@ }, "node_modules/schema-utils/node_modules/ajv-formats": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -5564,6 +5822,7 @@ }, "node_modules/serialize-javascript": { "version": "6.0.2", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" @@ -5665,6 +5924,8 @@ }, "node_modules/slash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { @@ -5762,6 +6023,8 @@ }, "node_modules/stack-utils": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5773,6 +6036,8 @@ }, "node_modules/stack-utils/node_modules/escape-string-regexp": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, "license": "MIT", "engines": { @@ -5812,6 +6077,8 @@ }, "node_modules/string-argv": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, "license": "MIT", "engines": { @@ -5967,7 +6234,9 @@ } }, "node_modules/terser": { - "version": "5.44.1", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -5983,13 +6252,14 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -6016,6 +6286,8 @@ }, "node_modules/terser-webpack-plugin/node_modules/jest-worker": { "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -6032,6 +6304,8 @@ }, "node_modules/test-exclude": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "license": "ISC", "dependencies": { @@ -6045,6 +6319,8 @@ }, "node_modules/test-exclude/node_modules/brace-expansion": { "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -6054,6 +6330,9 @@ }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -6072,7 +6351,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6098,6 +6379,8 @@ }, "node_modules/tmpl": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true, "license": "BSD-3-Clause" }, @@ -6194,11 +6477,15 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/universalify": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { @@ -6233,13 +6520,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util": { "version": "0.12.5", "dev": true, @@ -6276,9 +6556,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", "peer": true, "dependencies": { @@ -6351,20 +6631,26 @@ } }, "node_modules/vite-plugin-static-copy": { - "version": "3.1.4", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.3.0.tgz", + "integrity": "sha512-XiAtZcev7nppxNFgKoD55rfL+ukVp/RtrnTJONRwRuzv/B2FK2h2ZRCYjvxhwBV/Oarse83SiyXBSxMTfeEM0Q==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^3.6.0", - "p-map": "^7.0.3", + "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15" }, "engines": { "node": "^18.0.0 || >=20.0.0" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/sapphi-red" + }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/vite-plugin-static-copy/node_modules/chokidar": { @@ -6413,24 +6699,18 @@ } }, "node_modules/vite-tsconfig-paths": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.0.4.tgz", - "integrity": "sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", + "integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", - "tsconfck": "^3.0.3", - "vite": "*" + "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } } }, "node_modules/vscode-jsonrpc": { @@ -6495,13 +6775,15 @@ "license": "MIT" }, "node_modules/vscode-textmate": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.3.1.tgz", - "integrity": "sha512-U19nFkCraZF9/bkQKQYsb9mRqM9NwpToQQFl40nGiioZTH9gRtdtCHwp48cubayVfreX3ivnoxgxQgNwrTVmQg==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.3.2.tgz", + "integrity": "sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q==", "license": "MIT" }, "node_modules/walker": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6509,7 +6791,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.4", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -6520,7 +6804,9 @@ } }, "node_modules/webpack": { - "version": "5.104.1", + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "license": "MIT", "peer": true, "dependencies": { @@ -6530,11 +6816,11 @@ "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.4", + "enhanced-resolve": "^5.20.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -6546,9 +6832,9 @@ "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -6567,18 +6853,15 @@ } }, "node_modules/webpack-cli": { - "version": "6.0.1", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-7.0.2.tgz", + "integrity": "sha512-dB0R4T+C/8YuvM+fabdvil6QE44/ChDXikV5lOOkrUeCkW5hTJv2pGLE3keh+D5hjYw8icBaJkZzpFoaHV4T+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@discoveryjs/json-ext": "^0.6.1", - "@webpack-cli/configtest": "^3.0.1", - "@webpack-cli/info": "^3.0.1", - "@webpack-cli/serve": "^3.0.1", - "colorette": "^2.0.14", - "commander": "^12.1.0", - "cross-spawn": "^7.0.3", + "@discoveryjs/json-ext": "^1.0.0", + "commander": "^14.0.3", + "cross-spawn": "^7.0.6", "envinfo": "^7.14.0", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", @@ -6590,14 +6873,16 @@ "webpack-cli": "bin/cli.js" }, "engines": { - "node": ">=18.12.0" + "node": ">=20.9.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.82.0" + "webpack": "^5.101.0", + "webpack-bundle-analyzer": "^4.0.0 || ^5.0.0", + "webpack-dev-server": "^5.0.0" }, "peerDependenciesMeta": { "webpack-bundle-analyzer": { @@ -6608,17 +6893,14 @@ } } }, - "node_modules/webpack-cli/node_modules/colorette": { - "version": "2.0.20", - "dev": true, - "license": "MIT" - }, "node_modules/webpack-cli/node_modules/commander": { - "version": "12.1.0", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/webpack-merge": { @@ -6635,7 +6917,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -6786,11 +7070,15 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "license": "ISC", "dependencies": { @@ -6914,27 +7202,27 @@ }, "packages/alphatab": { "name": "@coderline/alphatab", - "version": "1.8.1", + "version": "1.9.0", "license": "MPL-2.0", "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@coderline/alphaskia": "^3.4.135", + "@biomejs/biome": "^2.4.10", + "@coderline/alphaskia": "^3.5.147", "@coderline/alphaskia-linux": "^3.4.135", - "@coderline/alphaskia-windows": "^3.4.135", + "@coderline/alphaskia-windows": "^3.5.147", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", + "@types/node": "^25.5.0", "assert": "^2.1.0", "chai": "^6.2.2", "chalk": "^5.6.2", - "jest-snapshot": "^30.2.0", + "jest-snapshot": "^30.3.0", "mocha": "^11.7.5", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vite": "^7.3.1", - "vite-plugin-static-copy": "^3.1.4" + "vite": "^7.3.2", + "vite-plugin-static-copy": "^3.3.0" }, "engines": { "node": ">=6.0.0" @@ -6942,9 +7230,9 @@ }, "packages/alphatex": { "name": "@coderline/alphatab-alphatex", - "version": "1.8.1", + "version": "1.9.0", "devDependencies": { - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" @@ -6952,22 +7240,22 @@ }, "packages/csharp": { "name": "@coderline/alphatab-csharp", - "version": "1.8.1", + "version": "1.9.0", "devDependencies": { "@coderline/alphatab-transpiler": "*", - "rimraf": "^6.1.2" + "rimraf": "^6.1.3" } }, "packages/kotlin": { "name": "@coderline/alphatab-kotlin", - "version": "1.8.1" + "version": "1.9.0" }, "packages/lsp": { "name": "@coderline/alphatab-language-server", - "version": "1.8.1", + "version": "1.9.0", "license": "MPL-2.0", "dependencies": { - "@coderline/alphatab": "^1.8.1", + "@coderline/alphatab": "^1.9.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12" }, @@ -6975,15 +7263,15 @@ "alphatab-language-server": "dist/server.mjs" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@microsoft/api-extractor": "^7.55.2", + "@biomejs/biome": "^2.4.10", + "@microsoft/api-extractor": "^7.57.7", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", + "@types/node": "^25.5.0", "assert": "^2.1.0", "chai": "^6.2.2", "mocha": "^11.7.4", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" @@ -6991,29 +7279,29 @@ }, "packages/monaco": { "name": "@coderline/alphatab-monaco", - "version": "1.8.1", + "version": "1.9.0", "license": "MPL-2.0", "dependencies": { - "@coderline/alphatab": "^1.8.1", - "@coderline/alphatab-language-server": "^1.8.1", + "@coderline/alphatab": "^1.9.0", + "@coderline/alphatab-language-server": "^1.9.0", "monaco-editor": "^0.55.1", "vscode-languageserver-types": "^3.17.5", "vscode-oniguruma": "^2.0.1", - "vscode-textmate": "^9.3.1" + "vscode-textmate": "^9.3.2" }, "bin": { "alphatab-monaco": "dist/alphaTab.monaco.mjs" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@microsoft/api-extractor": "^7.55.2", + "@biomejs/biome": "^2.4.10", + "@microsoft/api-extractor": "^7.57.7", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", + "@types/node": "^25.5.0", "assert": "^2.1.0", "chai": "^6.2.2", "mocha": "^11.7.4", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" @@ -7021,16 +7309,16 @@ }, "packages/playground": { "name": "@coderline/alphatab-playground", - "version": "1.8.1", + "version": "1.9.0", "dependencies": { "@coderline/alphatab": "*", "@fontsource/noto-sans": "^5.2.10", "@fontsource/noto-serif": "^5.2.9", - "@fortawesome/fontawesome-free": "^7.1.0", + "@fortawesome/fontawesome-free": "^7.2.0", "@popperjs/core": "^2.11.8", "@types/serve-static": "^2.2.0", "bootstrap": "^5.3.8", - "handlebars": "^4.7.8", + "handlebars": "^4.7.9", "monaco-editor": "^0.55.1", "serve-static": "^2.2.1" }, @@ -7039,45 +7327,45 @@ "split.js": "^1.6.5", "tslib": "^2.8.1", "typescript": "^5.9.3", - "vite": "^7.3.1", - "vite-tsconfig-paths": "^6.0.4" + "vite": "^7.3.2", + "vite-tsconfig-paths": "^6.1.1" } }, "packages/tooling": { "name": "@coderline/alphatab-tooling", - "version": "1.8.1", + "version": "1.9.0", "devDependencies": { - "@microsoft/api-extractor": "^7.55.2", - "@rollup/plugin-terser": "^0.4.4", + "@microsoft/api-extractor": "^7.57.7", + "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "rollup-plugin-license": "^3.6.0", + "rollup-plugin-license": "^3.7.0", "typescript": "^5.9.3" } }, "packages/transpiler": { "name": "@coderline/alphatab-transpiler", - "version": "1.8.1" + "version": "1.9.0" }, "packages/vite": { "name": "@coderline/alphatab-vite", - "version": "1.8.1", + "version": "1.9.0", "license": "MPL-2.0", "dependencies": { "magic-string": "^0.30.21", - "vite": "^7.3.1" + "vite": "^7.3.2" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@microsoft/api-extractor": "^7.55.2", + "@biomejs/biome": "^2.4.10", + "@microsoft/api-extractor": "^7.57.7", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", + "@types/node": "^25.5.0", "assert": "^2.1.0", "chai": "^6.2.2", "mocha": "^11.7.5", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "rollup-plugin-node-externals": "^8.1.2", - "terser": "^5.44.1", + "terser": "^5.46.1", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" @@ -7088,22 +7376,22 @@ }, "packages/vscode": { "name": "alphatab-vscode", - "version": "1.8.1", + "version": "1.9.0", "license": "MPL-2.0", "devDependencies": { - "@biomejs/biome": "^2.3.11", + "@biomejs/biome": "^2.4.10", "@rollup/plugin-node-resolve": "^16.0.3", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", - "@types/vscode": "^1.108.1", + "@types/node": "^25.5.0", + "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "assert": "^2.1.0", "chai": "^6.2.2", "concurrently": "^9.2.1", "mocha": "^11.7.4", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3", @@ -7115,25 +7403,25 @@ }, "packages/webpack": { "name": "@coderline/alphatab-webpack", - "version": "1.8.1", + "version": "1.9.0", "license": "MPL-2.0", "dependencies": { - "webpack": "^5.104.1" + "webpack": "^5.105.4" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", + "@biomejs/biome": "^2.4.10", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", + "@types/node": "^25.5.0", "assert": "^2.1.0", "chai": "^6.2.2", - "html-webpack-plugin": "^5.6.5", + "html-webpack-plugin": "^5.6.6", "mocha": "^11.7.5", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3", - "webpack-cli": "^6.0.1" + "webpack-cli": "^7.0.2" }, "engines": { "node": ">=20.19.0" diff --git a/package.json b/package.json index 061a73ed1..9026be2d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-monorepo", - "version": "1.8.1", + "version": "1.9.0", "description": "Monorepo for alphaTab and its related packages", "private": true, "type": "module", @@ -40,7 +40,11 @@ "build-webpack": "npm run build --workspace=packages/webpack", "test-webpack": "npm run test --workspace=packages/webpack", - "build-monaco": "npm run build --workspace=packages/monaco" + "build-monaco": "npm run build --workspace=packages/monaco", + + "nightly-check": "node ./scripts/nightly.mts --mode check", + "nightly-pack": "node ./scripts/nightly.mts --mode pack", + "nightly-publish": "node ./scripts/nightly.mts --mode publish" }, "devDependencies": { "concurrently": "^9.2.1" diff --git a/packages/alphatab/package.json b/packages/alphatab/package.json index b0bb8888f..9636bebd1 100644 --- a/packages/alphatab/package.json +++ b/packages/alphatab/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab", - "version": "1.8.1", + "version": "1.9.0", "description": "alphaTab is a music notation and guitar tablature rendering library", "keywords": [ "guitar", @@ -58,24 +58,24 @@ "test-accept-reference": "tsx scripts/accept-new-reference-files.ts" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@coderline/alphaskia": "^3.4.135", + "@biomejs/biome": "^2.4.10", + "@coderline/alphaskia": "^3.5.147", "@coderline/alphaskia-linux": "^3.4.135", - "@coderline/alphaskia-windows": "^3.4.135", + "@coderline/alphaskia-windows": "^3.5.147", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", + "@types/node": "^25.5.0", "assert": "^2.1.0", "chai": "^6.2.2", "chalk": "^5.6.2", - "jest-snapshot": "^30.2.0", + "jest-snapshot": "^30.3.0", "mocha": "^11.7.5", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vite": "^7.3.1", - "vite-plugin-static-copy": "^3.1.4" + "vite": "^7.3.2", + "vite-plugin-static-copy": "^3.3.0" }, "files": [ "/dist/alphaTab*.js", diff --git a/packages/alphatab/src/AlphaTabApiBase.ts b/packages/alphatab/src/AlphaTabApiBase.ts index 6efea4815..b80c8f7dd 100644 --- a/packages/alphatab/src/AlphaTabApiBase.ts +++ b/packages/alphatab/src/AlphaTabApiBase.ts @@ -352,14 +352,16 @@ export class AlphaTabApiBase { this.container = uiFacade.rootContainer; this.activeBeatsChanged = new EventEmitterOfT(() => { - if (this._player.state === PlayerState.Playing && this._currentBeat) { - return new ActiveBeatsChangedEventArgs(this._currentBeat!.beatLookup.highlightedBeats.map(h => h.beat)); + const currentBeat = this._currentBeat; + if (this._player.state === PlayerState.Playing && currentBeat) { + return new ActiveBeatsChangedEventArgs(currentBeat.beatLookup.highlightedBeats.map(h => h.beat)); } return null; }); this.playedBeatChanged = new EventEmitterOfT(() => { - if (this._player.state === PlayerState.Playing && this._currentBeat) { - return this._currentBeat.beat; + const currentBeat = this._currentBeat; + if (this._player.state === PlayerState.Playing && currentBeat) { + return currentBeat.beat; } return null; }); @@ -395,7 +397,7 @@ export class AlphaTabApiBase { } this.container.resize.on( - Environment.throttle(() => { + this.uiFacade.throttle(() => { if (this._isDestroyed) { return; } @@ -2192,8 +2194,9 @@ export class AlphaTabApiBase { this._isInitialBeatCursorUpdate = true; } - if (this._currentBeat !== null) { - this._cursorUpdateBeat(this._currentBeat!, false, this._previousTick > 10, 1, true); + const currentBeat = this._currentBeat; + if (currentBeat) { + this._cursorUpdateBeat(currentBeat, false, this._previousTick > 10, 1, true); } } @@ -2351,7 +2354,15 @@ export class AlphaTabApiBase { this._previousStateForCursor = this._player.state; this.uiFacade.beginInvoke(() => { - this._internalCursorUpdateBeat(lookupResult, stop, cache!, beatBoundings!, shouldScroll, cursorSpeed); + this._internalCursorUpdateBeat( + lookupResult, + stop, + cache!, + beatBoundings!, + shouldScroll, + cursorSpeed, + forceUpdate + ); }); } @@ -2376,7 +2387,8 @@ export class AlphaTabApiBase { boundsLookup: BoundsLookup, beatBoundings: BeatBounds, shouldScroll: boolean, - cursorSpeed: number + cursorSpeed: number, + forceUpdate: boolean ) { const beat = lookupResult.beat; const nextBeat = lookupResult.nextBeat?.beat; @@ -2418,9 +2430,12 @@ export class AlphaTabApiBase { let startBeatX = beatBoundings.onNotesX; if (beatCursor) { const animationWidth = nextBeatX - beatBoundings.onNotesX; - const relativePosition = this._previousTick - this._currentBeat!.start; - const ratioPosition = - this._currentBeat!.tickDuration > 0 ? relativePosition / this._currentBeat!.tickDuration : 0; + const relativePosition = this._previousTick - lookupResult!.start; + let ratioPosition = lookupResult.tickDuration > 0 ? relativePosition / lookupResult.tickDuration : 0; + // state got out-of-sync + if (ratioPosition > 1) { + ratioPosition = 1; + } startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition; duration -= duration * ratioPosition; @@ -2431,6 +2446,7 @@ export class AlphaTabApiBase { // we do not "reset" the cursor if we are smoothly moving from left to right. const jumpCursor = !previousBeatBounds || + forceUpdate || this._isInitialBeatCursorUpdate || barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y || startBeatX < previousBeatBounds.onNotesX || @@ -3605,6 +3621,9 @@ export class AlphaTabApiBase { this._currentBeat = null; this._cursorUpdateTick(this._previousTick, false, 1, true, true); + if(this._selectionStart) { + this.highlightPlaybackRange(this._selectionStart.beat, this._selectionEnd!.beat); + } (this.postRenderFinished as EventEmitter).trigger(); this.uiFacade.triggerEvent(this.container, 'postRenderFinished', null); @@ -4024,12 +4043,9 @@ export class AlphaTabApiBase { return; } - const currentTick = e.currentTick; - - this._previousTick = currentTick; this.uiFacade.beginInvoke(() => { const cursorSpeed = e.modifiedTempo / e.originalTempo; - this._cursorUpdateTick(currentTick, false, cursorSpeed, false, e.isSeek); + this._cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek); }); this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e); diff --git a/packages/alphatab/src/CursorHandler.ts b/packages/alphatab/src/CursorHandler.ts index d2edad57e..6e3888de6 100644 --- a/packages/alphatab/src/CursorHandler.ts +++ b/packages/alphatab/src/CursorHandler.ts @@ -118,11 +118,11 @@ export class NonAnimatingCursorHandler implements ICursorHandler { // nothing to do } - public placeBeatCursor(beatCursor: IContainer, beatBounds: BeatBounds, startBeatX: number): void { + public placeBeatCursor(beatCursor: IContainer, beatBounds: BeatBounds, _startBeatX: number): void { const barBoundings = beatBounds.barBounds.masterBarBounds; const barBounds = barBoundings.visualBounds; - beatCursor.transitionToX(0, startBeatX); - beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h); + beatCursor.transitionToX(0, beatBounds.onNotesX); + beatCursor.setBounds(beatBounds.onNotesX, barBounds.y, 1, barBounds.h); } public placeBarCursor(barCursor: IContainer, beatBounds: BeatBounds): void { diff --git a/packages/alphatab/src/Environment.ts b/packages/alphatab/src/Environment.ts index 6eb7d74cb..70cebd901 100644 --- a/packages/alphatab/src/Environment.ts +++ b/packages/alphatab/src/Environment.ts @@ -14,13 +14,17 @@ import { GolpeType } from '@coderline/alphatab/model/GolpeType'; import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { AlphaSynthWebWorklet } from '@coderline/alphatab/platform/javascript/AlphaSynthAudioWorkletOutput'; -import { AlphaSynthWebWorker } from '@coderline/alphatab/platform/javascript/AlphaSynthWebWorker'; -import { AlphaTabWebWorker } from '@coderline/alphatab/platform/javascript/AlphaTabWebWorker'; +import { BrowserUiFacade } from '@coderline/alphatab/platform/javascript/BrowserUiFacade'; import { Html5Canvas } from '@coderline/alphatab/platform/javascript/Html5Canvas'; import { JQueryAlphaTab } from '@coderline/alphatab/platform/javascript/JQueryAlphaTab'; import { WebPlatform } from '@coderline/alphatab/platform/javascript/WebPlatform'; import { SkiaCanvas } from '@coderline/alphatab/platform/skia/SkiaCanvas'; import { CssFontSvgCanvas } from '@coderline/alphatab/platform/svg/CssFontSvgCanvas'; +import { AlphaSynthWebWorker } from '@coderline/alphatab/platform/worker/AlphaSynthWebWorker'; +import { AlphaTabWebWorker } from '@coderline/alphatab/platform/worker/AlphaTabWebWorker'; +import type { + IAlphaTabWorkerGlobalScope +} from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol'; import { EffectBandMode, type BarRendererFactory } from '@coderline/alphatab/rendering/BarRendererFactory'; import { AlternateEndingsEffectInfo } from '@coderline/alphatab/rendering/effects/AlternateEndingsEffectInfo'; import { BeatBarreEffectInfo } from '@coderline/alphatab/rendering/effects/BeatBarreEffectInfo'; @@ -167,6 +171,15 @@ export class Environment { return Environment._globalThis; } + /** + * @target web + * @internal + * @partial + */ + public static getGlobalWorkerScope(): IAlphaTabWorkerGlobalScope { + return Environment.globalThis; + } + /** * @target web */ @@ -206,30 +219,6 @@ export class Environment { return 'AudioWorkletGlobalScope' in Environment.globalThis; } - /** - * @target web - * @internal - */ - public static createWebWorker: (settings: Settings) => Worker; - - /** - * @target web - * @internal - */ - public static createAudioWorklet: (context: AudioContext, settings: Settings) => Promise; - - /** - * @target web - * @partial - */ - public static throttle(action: () => void, delay: number): () => void { - let timeoutId: number = 0; - return () => { - Environment.globalThis.clearTimeout(timeoutId); - timeoutId = Environment.globalThis.setTimeout(action, delay); - }; - } - /** * @target web */ @@ -422,7 +411,7 @@ export class Environment { renderEngines.set( 'skia', - new RenderEngineFactory(false, () => { + new RenderEngineFactory(true, () => { return new SkiaCanvas(); }) ); @@ -637,7 +626,7 @@ export class Environment { * @target web */ public static initializeMain( - createWebWorker: (settings: Settings) => Worker, + createWebWorker: (settings: Settings, nameHint: string) => Worker, createAudioWorklet: (context: AudioContext, settings: Settings) => Promise ) { if (Environment.isRunningInWorker || Environment.isRunningInAudioWorklet) { @@ -650,8 +639,9 @@ export class Environment { Environment.highDpiFactor = window.devicePixelRatio; } - Environment.createWebWorker = createWebWorker; - Environment.createAudioWorklet = createAudioWorklet; + BrowserUiFacade.createAlphaTabWebWorker = s => createWebWorker(s, 'alphaTab Renderer'); + BrowserUiFacade.createAlphaSynthWebWorker = s => createWebWorker(s, 'alphaSynth Worker'); + BrowserUiFacade.createAlphaSynthAudioWorklet = createAudioWorklet; } /** @@ -682,9 +672,6 @@ export class Environment { } AlphaTabWebWorker.init(); AlphaSynthWebWorker.init(); - Environment.createWebWorker = _ => { - throw new AlphaTabError(AlphaTabErrorType.General, 'Nested workers are not supported'); - }; } /** @@ -828,6 +815,7 @@ export class Environment { * create proxy objects for all objects used. This code handles the necessary unwrapping. * @internal * @target web + * @partial */ public static prepareForPostMessage(object: T): T { if (!object) { diff --git a/packages/alphatab/src/importer/GpifParser.ts b/packages/alphatab/src/importer/GpifParser.ts index ceffcb228..b7bd46907 100644 --- a/packages/alphatab/src/importer/GpifParser.ts +++ b/packages/alphatab/src/importer/GpifParser.ts @@ -2393,26 +2393,26 @@ export class GpifParser { case 'HarmonicType': const htype = c.findChildElement('HType'); if (htype) { - switch (htype.innerText) { - case 'NoHarmonic': + switch (htype.innerText.toLowerCase()) { + case 'noharmonic': note.harmonicType = HarmonicType.None; break; - case 'Natural': + case 'natural': note.harmonicType = HarmonicType.Natural; break; - case 'Artificial': + case 'artificial': note.harmonicType = HarmonicType.Artificial; break; - case 'Pinch': + case 'pinch': note.harmonicType = HarmonicType.Pinch; break; - case 'Tap': + case 'tap': note.harmonicType = HarmonicType.Tap; break; - case 'Semi': + case 'semi': note.harmonicType = HarmonicType.Semi; break; - case 'Feedback': + case 'feedback': note.harmonicType = HarmonicType.Feedback; break; } diff --git a/packages/alphatab/src/model/JsonConverter.ts b/packages/alphatab/src/model/JsonConverter.ts index 611961a22..c1fec2d99 100644 --- a/packages/alphatab/src/model/JsonConverter.ts +++ b/packages/alphatab/src/model/JsonConverter.ts @@ -76,7 +76,7 @@ export class JsonConverter { * @param score The score object to serialize * @returns A serialized score object without ciruclar dependencies that can be used for further serializations. */ - public static scoreToJsObject(score: Score): unknown { + public static scoreToJsObject(score: Score): Map|null { return ScoreSerializer.toJson(score); } diff --git a/packages/alphatab/src/platform/IUiFacade.ts b/packages/alphatab/src/platform/IUiFacade.ts index a0772ea55..93ed37f62 100644 --- a/packages/alphatab/src/platform/IUiFacade.ts +++ b/packages/alphatab/src/platform/IUiFacade.ts @@ -127,6 +127,19 @@ export interface IUiFacade { */ beginInvoke(action: () => void): void; + /** + * Creates a throttled/debounced version of the provided action. + * @param action The action to call. + * @param delay The delay to wait for additional call before actually executing. + * @returns A function which executes the provided action after the given delay. + * If multiple calls are made before the action is started, the already scheduled + * action is cancelled and a new one is scheduled after the given delay. + * If called endlessly, the action is never executed. + * + * Already executing actions will not be cancelled but will complete before another action executes. + */ + throttle(action: () => void, delay: number): () => void; + /** * Tells the UI layer to remove all highlights from highlighted music notation elements. */ @@ -176,7 +189,7 @@ export interface IUiFacade { scrollToX(scrollElement: IContainer, offset: number, speed: number): void; /** - * Stops any ongoing scrolling of the given element. + * Stops any ongoing scrolling of the given element. * @param scrollElement The element which might be scrolling dynamically. */ stopScrolling(scrollElement: IContainer): void; @@ -208,7 +221,7 @@ export interface IUiFacade { * Without these overflows we might not have enough scroll space * and we cannot reach a "sticky cursor" behavior. */ - setCanvasOverflow(canvasElement:IContainer, overflow: number, isVertical: boolean): void; + setCanvasOverflow(canvasElement: IContainer, overflow: number, isVertical: boolean): void; /** * This events is fired when the {@link canRender} property changes. diff --git a/packages/alphatab/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts b/packages/alphatab/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts index b69d2975a..a0db21058 100644 --- a/packages/alphatab/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts +++ b/packages/alphatab/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts @@ -1,17 +1,27 @@ -import { CircularSampleBuffer } from '@coderline/alphatab/synth/ds/CircularSampleBuffer'; import { Environment } from '@coderline/alphatab/Environment'; import { Logger } from '@coderline/alphatab/Logger'; -import { AlphaSynthWorkerSynthOutput } from '@coderline/alphatab/platform/javascript/AlphaSynthWorkerSynthOutput'; +import type { Settings } from '@coderline/alphatab/Settings'; import { AlphaSynthWebAudioOutputBase } from '@coderline/alphatab/platform/javascript/AlphaSynthWebAudioOutputBase'; +import { BrowserUiFacade } from '@coderline/alphatab/platform/javascript/BrowserUiFacade'; +import type { + IAlphaSynthWorkerMessage, + IAlphaTabWorker +} from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; -import type { Settings } from '@coderline/alphatab/Settings'; +import { CircularSampleBuffer } from '@coderline/alphatab/synth/ds/CircularSampleBuffer'; + +/** + * @target web + * @internal + */ +type AudioWorkletProcessorMessagePort = Omit, 'terminate'> & Pick; /** * @target web * @internal */ interface AudioWorkletProcessor { - readonly port: MessagePort; + readonly port: AudioWorkletProcessorMessagePort; process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record): boolean; } @@ -24,6 +34,14 @@ declare let AudioWorkletProcessor: { new (options?: AudioWorkletNodeOptions): AudioWorkletProcessor; }; +/** + * @target web + * @internal + */ +interface AudioWorkletNode extends AudioNode { + readonly port: AudioWorkletProcessorMessagePort; +} + // Bug 646: Safari 14.1 is buggy regarding audio worklets // globalThis cannot be used to access registerProcessor or samplerate // we need to really use them as globals @@ -76,22 +94,23 @@ export class AlphaSynthWebWorklet { AlphaSynthWebWorkletProcessor.BufferSize * this._bufferCount ); - this.port.onmessage = this._handleMessage.bind(this); + this.port.addEventListener('message', e => this._handleMessage(e)); + this.port.start(); } - private _handleMessage(e: MessageEvent) { - const data: any = e.data; - const cmd: any = data.cmd; + private _handleMessage(e: MessageEvent) { + const data = e.data; + const cmd = data.cmd; switch (cmd) { - case AlphaSynthWorkerSynthOutput.CmdOutputAddSamples: + case 'alphaSynth.output.addSamples': const f: Float32Array = data.samples; this._circularBuffer.write(f, 0, f.length); this._requestedBufferCount--; break; - case AlphaSynthWorkerSynthOutput.CmdOutputResetSamples: + case 'alphaSynth.output.resetSamples': this._circularBuffer.clear(); break; - case AlphaSynthWorkerSynthOutput.CmdOutputStop: + case 'alphaSynth.output.stop': this._isStopped = true; break; } @@ -139,7 +158,7 @@ export class AlphaSynthWebWorklet { } this.port.postMessage({ - cmd: AlphaSynthWorkerSynthOutput.CmdOutputSamplesPlayed, + cmd: 'alphaSynth.output.samplesPlayed', samples: samplesFromBuffer / SynthConstants.AudioChannels }); this._requestBuffers(); @@ -161,7 +180,7 @@ export class AlphaSynthWebWorklet { if (bufferedSamples < halfSamples) { for (let i: number = 0; i < halfBufferCount; i++) { this.port.postMessage({ - cmd: AlphaSynthWorkerSynthOutput.CmdOutputSampleRequest + cmd: 'alphaSynth.output.sampleRequest' }); } this._requestedBufferCount += halfBufferCount; @@ -179,13 +198,17 @@ export class AlphaSynthWebWorklet { * @internal */ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase { - private _worklet: AudioWorkletNode | null = null; + private _worklet: AudioWorkletNode | null = null; private _bufferTimeInMilliseconds: number = 0; private readonly _settings: Settings; + private _boundHandleMessage: (e: MessageEvent) => void; + + private _pendingEvents?: IAlphaSynthWorkerMessage[]; public constructor(settings: Settings) { super(); this._settings = settings; + this._boundHandleMessage = e => this._handleMessage(e); } public override open(bufferTimeInMilliseconds: number) { @@ -198,7 +221,7 @@ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase { super.play(); const ctx = this.context!; // create a script processor node which will replace the silence with the generated audio - Environment.createAudioWorklet(ctx, this._settings).then( + BrowserUiFacade.createAlphaSynthAudioWorklet(ctx, this._settings).then( () => { this._worklet = new AudioWorkletNode(ctx!, 'alphatab', { numberOfOutputs: 1, @@ -206,26 +229,36 @@ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase { processorOptions: { bufferTimeInMilliseconds: this._bufferTimeInMilliseconds } - }); - this._worklet.port.onmessage = this._handleMessage.bind(this); + }) as AudioWorkletNode; + + this._worklet.port.addEventListener('message', this._boundHandleMessage); + this._worklet.port.start(); this.source!.connect(this._worklet); this.source!.start(0); this._worklet.connect(ctx!.destination); + + const pending = this._pendingEvents; + if (pending) { + for (const e of pending) { + this._worklet.port.postMessage(e); + } + this._pendingEvents = undefined; + } }, - reason => { + (reason: any) => { Logger.error('WebAudio', `Audio Worklet creation failed: reason=${reason}`); } ); } - private _handleMessage(e: MessageEvent) { - const data: any = e.data; - const cmd: any = data.cmd; + private _handleMessage(e: MessageEvent) { + const data = e.data; + const cmd = data.cmd; switch (cmd) { - case AlphaSynthWorkerSynthOutput.CmdOutputSamplesPlayed: + case 'alphaSynth.output.samplesPlayed': this.onSamplesPlayed(data.samples); break; - case AlphaSynthWorkerSynthOutput.CmdOutputSampleRequest: + case 'alphaSynth.output.sampleRequest': this.onSampleRequest(); break; } @@ -235,24 +268,35 @@ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase { super.pause(); if (this._worklet) { this._worklet.port.postMessage({ - cmd: AlphaSynthWorkerSynthOutput.CmdOutputStop + cmd: 'alphaSynth.output.stop' }); - this._worklet.port.onmessage = null; + this._worklet.port.removeEventListener('message', this._boundHandleMessage); this._worklet.disconnect(); } this._worklet = null; + this._pendingEvents = undefined; + } + + private _postWorkerMessage(message: IAlphaSynthWorkerMessage) { + const worklet = this._worklet; + if (worklet) { + worklet.port.postMessage(message); + } else { + this._pendingEvents ??= []; + this._pendingEvents.push(message); + } } public addSamples(f: Float32Array): void { - this._worklet?.port.postMessage({ - cmd: AlphaSynthWorkerSynthOutput.CmdOutputAddSamples, + this._postWorkerMessage({ + cmd: 'alphaSynth.output.addSamples', samples: Environment.prepareForPostMessage(f) }); } public resetSamples(): void { - this._worklet?.port.postMessage({ - cmd: AlphaSynthWorkerSynthOutput.CmdOutputResetSamples + this._postWorkerMessage({ + cmd: 'alphaSynth.output.resetSamples' }); } } diff --git a/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts b/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts index b499755a3..c3205d2b4 100644 --- a/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts +++ b/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts @@ -18,9 +18,9 @@ import { Logger } from '@coderline/alphatab/Logger'; import type { IMouseEventArgs } from '@coderline/alphatab/platform/IMouseEventArgs'; import type { IUiFacade } from '@coderline/alphatab/platform/IUiFacade'; import { AlphaSynthScriptProcessorOutput } from '@coderline/alphatab/platform/javascript/AlphaSynthScriptProcessorOutput'; -import { AlphaSynthWebWorkerApi } from '@coderline/alphatab/platform/javascript/AlphaSynthWebWorkerApi'; +import { AlphaSynthWebWorkerApi } from '@coderline/alphatab/platform/worker/AlphaSynthWebWorkerApi'; import type { AlphaTabApi } from '@coderline/alphatab/platform/javascript/AlphaTabApi'; -import { AlphaTabWorkerScoreRenderer } from '@coderline/alphatab/platform/javascript/AlphaTabWorkerScoreRenderer'; +import { AlphaTabWorkerScoreRenderer } from '@coderline/alphatab/platform/worker/AlphaTabWorkerScoreRenderer'; import type { BrowserMouseEventArgs } from '@coderline/alphatab/platform/javascript/BrowserMouseEventArgs'; import { Cursors } from '@coderline/alphatab/platform/Cursors'; import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; @@ -35,7 +35,9 @@ import { AudioElementBackingTrackSynthOutput } from '@coderline/alphatab/platfor import { BackingTrackPlayer } from '@coderline/alphatab/synth/BackingTrackPlayer'; import { CoreSettings, FontFileFormat } from '@coderline/alphatab/CoreSettings'; import type { IAudioExporterWorker } from '@coderline/alphatab/synth/IAudioExporter'; -import { AlphaSynthAudioExporterWorkerApi } from '@coderline/alphatab/platform/javascript/AlphaSynthAudioExporterWorkerApi'; +import { AlphaSynthAudioExporterWorkerApi } from '@coderline/alphatab/platform/worker/AlphaSynthAudioExporterWorkerApi'; +import { IAlphaTabRenderingWorker, IAlphaSynthWorker } from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol'; +import { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; /** * @target web @@ -65,6 +67,7 @@ interface ResultPlaceholder extends HTMLElement { */ interface RegisteredWebFont { hash: number; + familyName: string; cssSource: string; elements: Map< HTMLDocument, @@ -185,7 +188,18 @@ export class BrowserUiFacade implements IUiFacade { } public createWorkerRenderer(): IScoreRenderer { - return new AlphaTabWorkerScoreRenderer(this._api, this._api.settings); + let worker: IAlphaTabRenderingWorker | undefined; + try { + worker = BrowserUiFacade.createAlphaTabWebWorker(this._api.settings); + return new AlphaTabWorkerScoreRenderer(this._api, worker); + } catch (e) { + Logger.error( + 'Renderer', + 'Failed to create worker for background rendering, fallback to non-worker rendering', + e + ); + return new ScoreRenderer(this._api.settings); + } } public initialize(api: AlphaTabApiBase, raw: SettingsJson | Settings): void { @@ -213,6 +227,7 @@ export class BrowserUiFacade implements IUiFacade { element.element.innerText = ''; } this._createStyleElements(settings); + settings.display.resources.smuflFontFamilyName = this._webFont.familyName; this._file = settings.core.file; } @@ -444,10 +459,9 @@ export class BrowserUiFacade implements IUiFacade { this._fontCheckers.set(familyName, checker); checker.checkForFontAvailability(); - settings.display.resources.smuflFontFamilyName = familyName; - const webFont: RegisteredWebFont = { hash, + familyName, elements: new Map(), fontSuffix, checker, @@ -713,16 +727,33 @@ export class BrowserUiFacade implements IUiFacade { if (supportsAudioWorklets && this._api.settings.player.outputMode === PlayerOutputMode.WebAudioAudioWorklets) { Logger.debug('Player', 'Will use webworkers for synthesizing and web audio api with worklets for playback'); + let worker: IAlphaSynthWorker | undefined; + try { + worker = BrowserUiFacade.createAlphaSynthWebWorker(this._api.settings); + } catch (e) { + Logger.error('Player', 'Failed to create worker for synthesizing audio', e); + return null; + } + player = new AlphaSynthWebWorkerApi( new AlphaSynthAudioWorkletOutput(this._api.settings), - this._api.settings + this._api.settings, + worker ); } else if (supportsScriptProcessor) { Logger.debug( 'Player', 'Will use webworkers for synthesizing and web audio api with ScriptProcessor for playback' ); - player = new AlphaSynthWebWorkerApi(new AlphaSynthScriptProcessorOutput(), this._api.settings); + let worker: IAlphaSynthWorker | undefined; + try { + worker = BrowserUiFacade.createAlphaSynthWebWorker(this._api.settings); + } catch (e) { + Logger.error('Player', 'Failed to create worker for synthesizing audio', e); + return null; + } + + player = new AlphaSynthWebWorkerApi(new AlphaSynthScriptProcessorOutput(), this._api.settings, worker); } if (!player) { @@ -1017,4 +1048,28 @@ export class BrowserUiFacade implements IUiFacade { this._api.settings.player.bufferTimeInMilliseconds ); } + + public throttle(action: () => void, delay: number): () => void { + let timeoutId: number = 0; + return () => { + Environment.globalThis.clearTimeout(timeoutId); + timeoutId = Environment.globalThis.setTimeout(action, delay); + }; + } + + /** + * @internal + */ + public static createAlphaTabWebWorker: (settings: Settings) => IAlphaTabRenderingWorker; + + /** + * @internal + */ + public static createAlphaSynthWebWorker: (settings: Settings) => IAlphaSynthWorker; + + /** + * @target web + * @internal + */ + public static createAlphaSynthAudioWorklet: (context: AudioContext, settings: Settings) => Promise; } diff --git a/packages/alphatab/src/platform/javascript/IWorkerScope.ts b/packages/alphatab/src/platform/javascript/IWorkerScope.ts deleted file mode 100644 index 8b33db253..000000000 --- a/packages/alphatab/src/platform/javascript/IWorkerScope.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @target web - * @internal - */ -export interface IWorkerScope { - addEventListener(eventType: string, listener: (e: MessageEvent) => void, capture?: boolean): void; - postMessage(message: unknown): void; -} diff --git a/packages/alphatab/src/platform/javascript/AlphaSynthAudioExporterWorkerApi.ts b/packages/alphatab/src/platform/worker/AlphaSynthAudioExporterWorkerApi.ts similarity index 78% rename from packages/alphatab/src/platform/javascript/AlphaSynthAudioExporterWorkerApi.ts rename to packages/alphatab/src/platform/worker/AlphaSynthAudioExporterWorkerApi.ts index 8ae2ca72c..6f17c4bf9 100644 --- a/packages/alphatab/src/platform/javascript/AlphaSynthAudioExporterWorkerApi.ts +++ b/packages/alphatab/src/platform/worker/AlphaSynthAudioExporterWorkerApi.ts @@ -2,7 +2,8 @@ import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabEr import { Environment } from '@coderline/alphatab/Environment'; import type { MidiFile } from '@coderline/alphatab/midi/MidiFile'; import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; -import type { AlphaSynthWebWorkerApi } from '@coderline/alphatab/platform/javascript/AlphaSynthWebWorkerApi'; +import type { AlphaSynthWebWorkerApi } from '@coderline/alphatab/platform/worker/AlphaSynthWebWorkerApi'; +import type { IAlphaSynthWorkerMessage } from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol'; import type { BackingTrackSyncPoint } from '@coderline/alphatab/synth/IAlphaSynth'; import type { AudioExportChunk, @@ -11,7 +12,6 @@ import type { } from '@coderline/alphatab/synth/IAudioExporter'; /** - * @target web * @internal */ export class AlphaSynthAudioExporterWorkerApi implements IAudioExporterWorker { @@ -21,7 +21,7 @@ export class AlphaSynthAudioExporterWorkerApi implements IAudioExporterWorker { private _exporterId: number; private _ownsWorker: boolean; - private _promise: PromiseWithResolvers | null = null; + private _promise: PromiseWithResolvers | null = null; public constructor(synthWorker: AlphaSynthWebWorkerApi, ownsWorker: boolean) { this._exporterId = AlphaSynthAudioExporterWorkerApi._nextExporterId++; @@ -35,10 +35,10 @@ export class AlphaSynthAudioExporterWorkerApi implements IAudioExporterWorker { syncPoints: BackingTrackSyncPoint[], transpositionPitches: Map ): Promise { - const onmessage = this.handleWorkerMessage.bind(this); - this._worker.worker.addEventListener('message', onmessage, false); + const onmessage: (ev: MessageEvent) => void = e => this.handleWorkerMessage(e); + this._worker.worker.addEventListener('message', onmessage); this._unsubscribe = () => { - this._worker.worker.removeEventListener('message', onmessage, false); + this._worker.worker.removeEventListener('message', onmessage); }; this._promise = Promise.withResolvers(); @@ -53,25 +53,32 @@ export class AlphaSynthAudioExporterWorkerApi implements IAudioExporterWorker { await this._promise.promise; } - public handleWorkerMessage(e: MessageEvent): void { - const data: any = e.data; + public handleWorkerMessage(e: MessageEvent): void { + const data = e.data; - // for us? - if (data.exporterId !== this._exporterId) { - return; - } - - const cmd: string = data.cmd; - switch (cmd) { + switch (data.cmd) { case 'alphaSynth.exporter.initialized': + // for us? + if (data.exporterId !== this._exporterId) { + return; + } + this._promise?.resolve(null); this._promise = null; break; case 'alphaSynth.exporter.error': + // for us? + if (data.exporterId !== this._exporterId) { + return; + } this._promise?.reject(data.error); this._promise = null; break; case 'alphaSynth.exporter.rendered': + // for us? + if (data.exporterId !== this._exporterId) { + return; + } this._promise?.resolve(data.chunk); this._promise = null; break; @@ -96,7 +103,8 @@ export class AlphaSynthAudioExporterWorkerApi implements IAudioExporterWorker { exporterId: this._exporterId, milliseconds: milliseconds }); - return (await this._promise.promise) as AudioExportChunk | undefined; + const result = await this._promise.promise; + return result as AudioExportChunk | undefined; } destroy(): void { diff --git a/packages/alphatab/src/platform/javascript/AlphaSynthWebWorker.ts b/packages/alphatab/src/platform/worker/AlphaSynthWebWorker.ts similarity index 67% rename from packages/alphatab/src/platform/javascript/AlphaSynthWebWorker.ts rename to packages/alphatab/src/platform/worker/AlphaSynthWebWorker.ts index 26f26b464..aecb1beb2 100644 --- a/packages/alphatab/src/platform/javascript/AlphaSynthWebWorker.ts +++ b/packages/alphatab/src/platform/worker/AlphaSynthWebWorker.ts @@ -1,68 +1,62 @@ -import { AlphaSynth, type IAlphaSynthAudioExporter } from '@coderline/alphatab/synth/AlphaSynth'; -import type { PlayerStateChangedEventArgs } from '@coderline/alphatab/synth/PlayerStateChangedEventArgs'; -import type { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs'; -import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; -import { AlphaSynthWorkerSynthOutput } from '@coderline/alphatab/platform/javascript/AlphaSynthWorkerSynthOutput'; -import type { IWorkerScope } from '@coderline/alphatab/platform/javascript/IWorkerScope'; -import { Logger } from '@coderline/alphatab/Logger'; import { Environment } from '@coderline/alphatab/Environment'; +import { Logger } from '@coderline/alphatab/Logger'; +import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; +import { AlphaSynthWorkerSynthOutput } from '@coderline/alphatab/platform/worker/AlphaSynthWorkerSynthOutput'; +import type { + IAlphaSynthWorkerMessage, + IAlphaTabWorkerGlobalScope +} from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol'; +import { AlphaSynth, type IAlphaSynthAudioExporter } from '@coderline/alphatab/synth/AlphaSynth'; import type { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs'; import type { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/PlaybackRangeChangedEventArgs'; +import type { PlayerStateChangedEventArgs } from '@coderline/alphatab/synth/PlayerStateChangedEventArgs'; +import type { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs'; /** * This class implements a HTML5 WebWorker based version of alphaSynth * which can be controlled via WebWorker messages. - * @target web * @internal + * @partial */ export class AlphaSynthWebWorker { - private _player: AlphaSynth; - private _main: IWorkerScope; + private _player!: AlphaSynth; + private _main: IAlphaTabWorkerGlobalScope; private _exporter: Map = new Map(); - public constructor(main: IWorkerScope, bufferTimeInMilliseconds: number) { + public constructor(main: IAlphaTabWorkerGlobalScope) { this._main = main; - this._main.addEventListener('message', this.handleMessage.bind(this)); - - this._player = new AlphaSynth(new AlphaSynthWorkerSynthOutput(), bufferTimeInMilliseconds); - this._player.positionChanged.on(this.onPositionChanged.bind(this)); - this._player.stateChanged.on(this.onPlayerStateChanged.bind(this)); - this._player.finished.on(this.onFinished.bind(this)); - this._player.soundFontLoaded.on(this.onSoundFontLoaded.bind(this)); - this._player.soundFontLoadFailed.on(this.onSoundFontLoadFailed.bind(this)); - this._player.soundFontLoadFailed.on(this.onSoundFontLoadFailed.bind(this)); - this._player.midiLoaded.on(this.onMidiLoaded.bind(this)); - this._player.midiLoadFailed.on(this.onMidiLoadFailed.bind(this)); - this._player.readyForPlayback.on(this.onReadyForPlayback.bind(this)); - this._player.midiEventsPlayed.on(this.onMidiEventsPlayed.bind(this)); - this._player.playbackRangeChanged.on(this.onPlaybackRangeChanged.bind(this)); - this._main.postMessage({ - cmd: 'alphaSynth.ready' - }); + main.addEventListener('message', e => this.handleMessage(e)); } public static init(): void { - const main: IWorkerScope = Environment.globalThis as IWorkerScope; - main.addEventListener('message', e => { - const data: any = e.data; - const cmd: string = data.cmd; - switch (cmd) { - case 'alphaSynth.initialize': - AlphaSynthWorkerSynthOutput.preferredSampleRate = data.sampleRate; - Logger.logLevel = data.logLevel; - Environment.globalThis.alphaSynthWebWorker = new AlphaSynthWebWorker( - main, - data.bufferTimeInMilliseconds - ); - break; - } - }); + new AlphaSynthWebWorker(Environment.getGlobalWorkerScope()); } - public handleMessage(e: MessageEvent): void { - const data: any = e.data; - const cmd: string = data.cmd; - switch (cmd) { + public handleMessage(e: MessageEvent): void { + const data = e.data; + switch (data.cmd) { + case 'alphaSynth.initialize': + AlphaSynthWorkerSynthOutput.preferredSampleRate = data.sampleRate; + Logger.logLevel = data.logLevel; + this._player = new AlphaSynth( + new AlphaSynthWorkerSynthOutput(this._main), + data.bufferTimeInMilliseconds + ); + this._player.positionChanged.on(e => this.onPositionChanged(e)); + this._player.stateChanged.on(e => this.onPlayerStateChanged(e)); + this._player.finished.on(() => this.onFinished()); + this._player.soundFontLoaded.on(() => this.onSoundFontLoaded()); + this._player.soundFontLoadFailed.on(e => this.onSoundFontLoadFailed(e)); + this._player.midiLoaded.on(e => this.onMidiLoaded(e)); + this._player.midiLoadFailed.on(e => this.onMidiLoadFailed(e)); + this._player.readyForPlayback.on(() => this.onReadyForPlayback()); + this._player.midiEventsPlayed.on(e => this.onMidiEventsPlayed(e)); + this._player.playbackRangeChanged.on(e => this.onPlaybackRangeChanged(e)); + this._main.postMessage({ + cmd: 'alphaSynth.ready' + }); + + break; case 'alphaSynth.setLogLevel': Logger.logLevel = data.value; break; @@ -139,21 +133,24 @@ export class AlphaSynthWebWorker { }); break; case 'alphaSynth.applyTranspositionPitches': - this._player.applyTranspositionPitches(new Map(JSON.parse(data.transpositionPitches))); + this._player.applyTranspositionPitches(data.transpositionPitches); break; } - if (cmd.startsWith('alphaSynth.exporter')) { + if (data.cmd.startsWith('alphaSynth.exporter')) { this._handleExporterMessage(e); } } - private _handleExporterMessage(e: MessageEvent) { - const data: any = e.data; - const cmd: string = data.cmd; + private _handleExporterMessage(ev: MessageEvent) { + const data = ev.data; + const cmd = data.cmd; + let exporter:IAlphaSynthAudioExporter|undefined = undefined; + let exporterId = 0; try { switch (cmd) { case 'alphaSynth.exporter.initialize': - const exporter = this._player.exportAudio( + exporterId = data.exporterId; + exporter = this._player.exportAudio( data.options, JsonConverter.jsObjectToMidiFile(data.midi), data.syncPoints, @@ -168,8 +165,9 @@ export class AlphaSynthWebWorker { break; case 'alphaSynth.exporter.render': + exporterId = data.exporterId; if (this._exporter.has(data.exporterId)) { - const exporter = this._exporter.get(data.exporterId)!; + exporter = this._exporter.get(data.exporterId)!; const chunk = exporter.render(data.milliseconds); this._main.postMessage({ cmd: 'alphaSynth.exporter.rendered', @@ -186,14 +184,15 @@ export class AlphaSynthWebWorker { break; case 'alphaSynth.exporter.destroy': + exporterId = data.exporterId; this._exporter.delete(data.exporterId); break; } } catch (e) { this._main.postMessage({ cmd: 'alphaSynth.exporter.error', - exporterId: data.exporterId, - error: e + exporterId: exporterId, + error: e as Error }); } } @@ -201,13 +200,7 @@ export class AlphaSynthWebWorker { public onPositionChanged(e: PositionChangedEventArgs): void { this._main.postMessage({ cmd: 'alphaSynth.positionChanged', - currentTime: e.currentTime, - endTime: e.endTime, - currentTick: e.currentTick, - endTick: e.endTick, - isSeek: e.isSeek, - originalTempo: e.originalTempo, - modifiedTempo: e.modifiedTempo + args: e }); } @@ -234,41 +227,21 @@ export class AlphaSynthWebWorker { public onSoundFontLoadFailed(e: any): void { this._main.postMessage({ cmd: 'alphaSynth.soundFontLoadFailed', - error: this._serializeException(Environment.prepareForPostMessage(e)) + error: e }); } - private _serializeException(e: any): unknown { - const error: any = JSON.parse(JSON.stringify(e)); - if (e.message) { - error.message = e.message; - } - if (e.stack) { - error.stack = e.stack; - } - if (e.constructor && e.constructor.name) { - error.type = e.constructor.name; - } - return error; - } - public onMidiLoaded(e: PositionChangedEventArgs): void { this._main.postMessage({ cmd: 'alphaSynth.midiLoaded', - currentTime: e.currentTime, - endTime: e.endTime, - currentTick: e.currentTick, - endTick: e.endTick, - isSeek: e.isSeek, - originalTempo: e.originalTempo, - modifiedTempo: e.modifiedTempo + args: e }); } public onMidiLoadFailed(e: any): void { this._main.postMessage({ - cmd: 'alphaSynth.midiLoaded', - error: this._serializeException(Environment.prepareForPostMessage(e)) + cmd: 'alphaSynth.midiLoadFailed', + error: e }); } diff --git a/packages/alphatab/src/platform/javascript/AlphaSynthWebWorkerApi.ts b/packages/alphatab/src/platform/worker/AlphaSynthWebWorkerApi.ts similarity index 90% rename from packages/alphatab/src/platform/javascript/AlphaSynthWebWorkerApi.ts rename to packages/alphatab/src/platform/worker/AlphaSynthWebWorkerApi.ts index 3c7a48948..2c757fd2c 100644 --- a/packages/alphatab/src/platform/javascript/AlphaSynthWebWorkerApi.ts +++ b/packages/alphatab/src/platform/worker/AlphaSynthWebWorkerApi.ts @@ -1,30 +1,38 @@ +import { Environment } from '@coderline/alphatab/Environment'; +import { + EventEmitter, + EventEmitterOfT, + type IEventEmitter, + type IEventEmitterOfT +} from '@coderline/alphatab/EventEmitter'; +import { Logger } from '@coderline/alphatab/Logger'; +import type { LogLevel } from '@coderline/alphatab/LogLevel'; +import type { MidiEventType } from '@coderline/alphatab/midi/MidiEvent'; import type { MidiFile } from '@coderline/alphatab/midi/MidiFile'; +import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import type { Score } from '@coderline/alphatab/model/Score'; +import type { + IAlphaSynthWorker, + IAlphaSynthWorkerMessage +} from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol'; +import type { Settings } from '@coderline/alphatab/Settings'; import type { BackingTrackSyncPoint, IAlphaSynth } from '@coderline/alphatab/synth/IAlphaSynth'; import type { ISynthOutput } from '@coderline/alphatab/synth/ISynthOutput'; +import { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs'; import type { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange'; +import { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/PlaybackRangeChangedEventArgs'; import { PlayerState } from '@coderline/alphatab/synth/PlayerState'; import { PlayerStateChangedEventArgs } from '@coderline/alphatab/synth/PlayerStateChangedEventArgs'; import { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs'; -import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@coderline/alphatab/EventEmitter'; -import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; -import { Logger } from '@coderline/alphatab/Logger'; -import type { LogLevel } from '@coderline/alphatab/LogLevel'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; -import { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs'; -import type { MidiEventType } from '@coderline/alphatab/midi/MidiEvent'; -import { Environment } from '@coderline/alphatab/Environment'; -import { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/PlaybackRangeChangedEventArgs'; -import type { Settings } from '@coderline/alphatab/Settings'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import type { Score } from '@coderline/alphatab/model/Score'; /** * a WebWorker based alphaSynth which uses the given player as output. - * @target web * @internal */ export class AlphaSynthWebWorkerApi implements IAlphaSynth { - private _synth!: Worker; + private _synth!: IAlphaSynthWorker; private _output: ISynthOutput; private _workerIsReadyForPlayback: boolean = false; private _workerIsReady: boolean = false; @@ -60,7 +68,7 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { return Logger.logLevel; } - public get worker(): Worker { + public get worker(): IAlphaSynthWorker { return this._synth; } @@ -221,7 +229,7 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { }); } - public constructor(player: ISynthOutput, settings: Settings) { + public constructor(player: ISynthOutput, settings: Settings, synthWorker: IAlphaSynthWorker) { this._workerIsReadyForPlayback = false; this._workerIsReady = false; this._outputIsReady = false; @@ -236,12 +244,8 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { this._output.samplesPlayed.on(this.onOutputSamplesPlayed.bind(this)); this._output.sampleRequest.on(this.onOutputSampleRequest.bind(this)); this._output.open(settings.player.bufferTimeInMilliseconds); - try { - this._synth = Environment.createWebWorker(settings); - } catch (e) { - Logger.error('AlphaSynth', `Failed to create WebWorker: ${e}`); - } - this._synth.addEventListener('message', this.handleWorkerMessage.bind(this), false); + this._synth = synthWorker; + this._synth.addEventListener('message', e => this.handleWorkerMessage(e)); this._synth.postMessage({ cmd: 'alphaSynth.initialize', sampleRate: this._output.sampleRate, @@ -319,9 +323,7 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { public applyTranspositionPitches(transpositionPitches: Map): void { this._synth.postMessage({ cmd: 'alphaSynth.applyTranspositionPitches', - transpositionPitches: JSON.stringify( - Array.from(Environment.prepareForPostMessage(transpositionPitches).entries()) - ) + transpositionPitches: Environment.prepareForPostMessage(transpositionPitches) }); } @@ -364,10 +366,9 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { }); } - public handleWorkerMessage(e: MessageEvent): void { - const data: any = e.data; - const cmd: string = data.cmd; - switch (cmd) { + public handleWorkerMessage(e: MessageEvent): void { + const data = e.data; + switch (data.cmd) { case 'alphaSynth.ready': this._workerIsReady = true; this._checkReady(); @@ -380,20 +381,12 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { this._checkReadyForPlayback(); break; case 'alphaSynth.positionChanged': - this._currentPosition = new PositionChangedEventArgs( - data.currentTime, - data.endTime, - data.currentTick, - data.endTick, - data.isSeek, - data.originalTempo, - data.modifiedTempo - ); + this._currentPosition = data.args; (this.positionChanged as EventEmitterOfT).trigger(this._currentPosition); break; case 'alphaSynth.midiEventsPlayed': (this.midiEventsPlayed as EventEmitterOfT).trigger( - new MidiEventsPlayedEventArgs((data.events as unknown[]).map(JsonConverter.jsObjectToMidiEvent)) + new MidiEventsPlayedEventArgs(data.events.map(JsonConverter.jsObjectToMidiEvent)) ); break; case 'alphaSynth.playerStateChanged': @@ -403,7 +396,7 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { ); break; case 'alphaSynth.playbackRangeChanged': - this._playbackRange = (data as PlaybackRangeChangedEventArgs).playbackRange; + this._playbackRange = data.playbackRange; (this.playbackRangeChanged as EventEmitterOfT).trigger( new PlaybackRangeChangedEventArgs(this._playbackRange) ); @@ -419,15 +412,7 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { break; case 'alphaSynth.midiLoaded': this._checkReadyForPlayback(); - this._loadedMidiInfo = new PositionChangedEventArgs( - data.currentTime, - data.endTime, - data.currentTick, - data.endTick, - data.isSeek, - data.originalTempo, - data.modifiedTempo - ); + this._loadedMidiInfo = data.args; (this.midiLoaded as EventEmitterOfT).trigger(this._loadedMidiInfo); break; case 'alphaSynth.midiLoadFailed': diff --git a/packages/alphatab/src/platform/javascript/AlphaSynthWorkerSynthOutput.ts b/packages/alphatab/src/platform/worker/AlphaSynthWorkerSynthOutput.ts similarity index 53% rename from packages/alphatab/src/platform/javascript/AlphaSynthWorkerSynthOutput.ts rename to packages/alphatab/src/platform/worker/AlphaSynthWorkerSynthOutput.ts index edac8d5e7..d773bdacb 100644 --- a/packages/alphatab/src/platform/javascript/AlphaSynthWorkerSynthOutput.ts +++ b/packages/alphatab/src/platform/worker/AlphaSynthWorkerSynthOutput.ts @@ -1,56 +1,54 @@ -import type { ISynthOutput, ISynthOutputDevice } from '@coderline/alphatab/synth/ISynthOutput'; -import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@coderline/alphatab/EventEmitter'; -import type { IWorkerScope } from '@coderline/alphatab/platform/javascript/IWorkerScope'; -import { Logger } from '@coderline/alphatab/Logger'; import { Environment } from '@coderline/alphatab/Environment'; +import { + EventEmitter, + EventEmitterOfT, + type IEventEmitter, + type IEventEmitterOfT +} from '@coderline/alphatab/EventEmitter'; +import { Logger } from '@coderline/alphatab/Logger'; +import type { + IAlphaSynthWorkerMessage, + IAlphaTabWorkerGlobalScope +} from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol'; +import type { ISynthOutput, ISynthOutputDevice } from '@coderline/alphatab/synth/ISynthOutput'; /** - * @target web * @internal */ export class AlphaSynthWorkerSynthOutput implements ISynthOutput { - public static readonly CmdOutputPrefix: string = 'alphaSynth.output.'; - public static readonly CmdOutputAddSamples: string = `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}addSamples`; - public static readonly CmdOutputPlay: string = `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}play`; - public static readonly CmdOutputPause: string = `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}pause`; - public static readonly CmdOutputResetSamples: string = `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}resetSamples`; - public static readonly CmdOutputStop: string = `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}stop`; - public static readonly CmdOutputSampleRequest: string = - `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}sampleRequest`; - public static readonly CmdOutputSamplesPlayed: string = - `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}samplesPlayed`; - // this value is initialized by the alphaSynth WebWorker wrapper // that also includes the alphaSynth library into the worker. public static preferredSampleRate: number = 0; - private _worker!: IWorkerScope; + private _main: IAlphaTabWorkerGlobalScope; public get sampleRate(): number { return AlphaSynthWorkerSynthOutput.preferredSampleRate; } - public open(): void { + public constructor(main: IAlphaTabWorkerGlobalScope) { + this._main = main; + } + + public open(_sampleRate: number): void { Logger.debug('AlphaSynth', 'Initializing synth worker'); - this._worker = Environment.globalThis as IWorkerScope; - this._worker.addEventListener('message', this._handleMessage.bind(this)); + this._main.addEventListener('message', this._handleMessage.bind(this)); (this.ready as EventEmitter).trigger(); } public destroy(): void { - this._worker.postMessage({ + this._main.postMessage({ cmd: 'alphaSynth.output.destroy' }); } - private _handleMessage(e: MessageEvent): void { - const data: any = e.data; - const cmd: any = data.cmd; - switch (cmd) { - case AlphaSynthWorkerSynthOutput.CmdOutputSampleRequest: + private _handleMessage(e: MessageEvent): void { + const data = e.data; + switch (data.cmd) { + case 'alphaSynth.output.sampleRequest': (this.sampleRequest as EventEmitter).trigger(); break; - case AlphaSynthWorkerSynthOutput.CmdOutputSamplesPlayed: + case 'alphaSynth.output.samplesPlayed': (this.samplesPlayed as EventEmitterOfT).trigger(data.samples); break; } @@ -61,26 +59,26 @@ export class AlphaSynthWorkerSynthOutput implements ISynthOutput { public readonly sampleRequest: IEventEmitter = new EventEmitter(); public addSamples(samples: Float32Array): void { - this._worker.postMessage({ + this._main.postMessage({ cmd: 'alphaSynth.output.addSamples', samples: Environment.prepareForPostMessage(samples) }); } public play(): void { - this._worker.postMessage({ + this._main.postMessage({ cmd: 'alphaSynth.output.play' }); } public pause(): void { - this._worker.postMessage({ + this._main.postMessage({ cmd: 'alphaSynth.output.pause' }); } public resetSamples(): void { - this._worker.postMessage({ + this._main.postMessage({ cmd: 'alphaSynth.output.resetSamples' }); } @@ -90,7 +88,7 @@ export class AlphaSynthWorkerSynthOutput implements ISynthOutput { } public async enumerateOutputDevices(): Promise { - return []; + return [] as ISynthOutputDevice[]; } public async setOutputDevice(_device: ISynthOutputDevice | null): Promise {} public async getOutputDevice(): Promise { diff --git a/packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts b/packages/alphatab/src/platform/worker/AlphaTabWebWorker.ts similarity index 78% rename from packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts rename to packages/alphatab/src/platform/worker/AlphaTabWebWorker.ts index 62038c4df..191cdbceb 100644 --- a/packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts +++ b/packages/alphatab/src/platform/worker/AlphaTabWebWorker.ts @@ -3,35 +3,38 @@ import { SettingsSerializer } from '@coderline/alphatab/generated/SettingsSerial import { Logger } from '@coderline/alphatab/Logger'; import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; import type { Score } from '@coderline/alphatab/model/Score'; -import type { IWorkerScope } from '@coderline/alphatab/platform/javascript/IWorkerScope'; import { type FontSizeDefinition, FontSizes } from '@coderline/alphatab/platform/svg/FontSizes'; +import type { + IAlphaTabWorkerGlobalScope, + IAlphaTabWorkerMessage +} from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol'; import type { RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; import { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import type { Settings } from '@coderline/alphatab/Settings'; /** - * @target web - * @public + * @internal + * @partial */ export class AlphaTabWebWorker { private _renderer!: ScoreRenderer; - private _main: IWorkerScope; + private _main: IAlphaTabWorkerGlobalScope; - public constructor(main: IWorkerScope) { + public constructor(main: IAlphaTabWorkerGlobalScope) { this._main = main; - this._main.addEventListener('message', this._handleMessage.bind(this), false); + main.addEventListener('message', e => this._handleMessage(e)); } public static init(): void { - (Environment.globalThis as any).alphaTabWebWorker = new AlphaTabWebWorker( - Environment.globalThis as IWorkerScope - ); + new AlphaTabWebWorker(Environment.getGlobalWorkerScope()); } - private _handleMessage(e: MessageEvent): void { - const data: any = e.data; - const cmd: any = data ? data.cmd : ''; - switch (cmd) { + private _handleMessage(e: MessageEvent): void { + const data = e.data; + if (!data?.cmd) { + return; + } + switch (data.cmd) { case 'alphaTab.initialize': const settings: Settings = JsonConverter.jsObjectToSettings(data.settings); Logger.logLevel = settings.core.logLevel; @@ -82,7 +85,7 @@ export class AlphaTabWebWorker { break; case 'alphaTab.renderScore': this._updateFontSizes(data.fontSizes); - const score: any = + const score = data.score == null ? null : JsonConverter.jsObjectToScore(data.score, this._renderer.settings); this._renderMultiple(score, data.trackIndexes); break; @@ -92,19 +95,9 @@ export class AlphaTabWebWorker { } } - private _updateFontSizes(fontSizes: { [key: string]: FontSizeDefinition } | Map): void { - if (!(fontSizes instanceof Map)) { - const obj = fontSizes; - fontSizes = new Map(); - for (const font in obj) { - fontSizes.set(font, obj[font]); - } - } - - if (fontSizes) { - for (const [k, v] of fontSizes) { - FontSizes.fontSizeLookupTables.set(k, v); - } + private _updateFontSizes(fontSizes: Map): void { + for (const [k, v] of fontSizes) { + FontSizes.fontSizeLookupTables.set(k, v); } } diff --git a/packages/alphatab/src/platform/worker/AlphaTabWorkerProtocol.ts b/packages/alphatab/src/platform/worker/AlphaTabWorkerProtocol.ts new file mode 100644 index 000000000..c855a0780 --- /dev/null +++ b/packages/alphatab/src/platform/worker/AlphaTabWorkerProtocol.ts @@ -0,0 +1,142 @@ +import type { LogLevel } from '@coderline/alphatab/LogLevel'; +import type { MidiEventType } from '@coderline/alphatab/midi/MidiEvent'; +import type { FontSizeDefinition } from '@coderline/alphatab/platform/_barrel'; +import type { RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; +import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; +import type { BackingTrackSyncPoint } from '@coderline/alphatab/synth/IAlphaSynth'; +import type { AudioExportChunk, AudioExportOptions } from '@coderline/alphatab/synth/IAudioExporter'; +import type { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange'; +import type { PlayerState } from '@coderline/alphatab/synth/PlayerState'; +import type { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs'; + +/** + * @internal + * @discriminated cmd alphaTab. + */ +export type IAlphaTabWorkerMessage = + // main -> worker + | { cmd: 'alphaTab.initialize'; settings: Map } + | { cmd: 'alphaTab.updateSettings'; settings: Map } + | { cmd: 'alphaTab.render'; renderHints: RenderHints | undefined } + | { cmd: 'alphaTab.resizeRender' } + | { cmd: 'alphaTab.renderResult'; resultId: string } + | { cmd: 'alphaTab.setWidth'; width: number } + | { + cmd: 'alphaTab.renderScore'; + score: Map | null; + trackIndexes: number[] | null; + fontSizes: Map; + renderHints: RenderHints | undefined; + } + // worker -> main + | { cmd: 'alphaTab.preRender'; resize: boolean } + | { cmd: 'alphaTab.partialRenderFinished'; result: RenderFinishedEventArgs } + | { cmd: 'alphaTab.partialLayoutFinished'; result: RenderFinishedEventArgs } + | { cmd: 'alphaTab.renderFinished'; result: RenderFinishedEventArgs } + | { cmd: 'alphaTab.postRenderFinished'; boundsLookup: Map | null } + | { cmd: 'alphaTab.error'; error: Error }; + +/** + * @internal + */ +export interface IAlphaTabWorker { + postMessage(message: T): void; + addEventListener(event: 'message', handler: (ev: MessageEvent) => void): void; + removeEventListener(event: 'message', handler: (ev: MessageEvent) => void): void; + terminate(): void; +} + +/** + * @internal + */ +export interface IAlphaTabWorkerGlobalScope { + postMessage(message: T): void; + addEventListener(event: 'message', handler: (ev: MessageEvent) => void): void; + removeEventListener(event: 'message', handler: (ev: MessageEvent) => void): void; +} + +/** + * @internal + * @discriminated cmd alphaSynth. + */ +export type IAlphaSynthWorkerMessage = + /* main -> worker */ + | { cmd: 'alphaSynth.initialize'; sampleRate: number; logLevel: LogLevel; bufferTimeInMilliseconds: number } + | { cmd: 'alphaSynth.setLogLevel'; value: LogLevel } + | { cmd: 'alphaSynth.setMasterVolume'; value: number } + | { cmd: 'alphaSynth.setMetronomeVolume'; value: number } + | { cmd: 'alphaSynth.setPlaybackSpeed'; value: number } + | { cmd: 'alphaSynth.setTickPosition'; value: number } + | { cmd: 'alphaSynth.setTimePosition'; value: number } + | { cmd: 'alphaSynth.setPlaybackRange'; value: PlaybackRange | null } + | { cmd: 'alphaSynth.setIsLooping'; value: boolean } + | { cmd: 'alphaSynth.setCountInVolume'; value: number } + | { cmd: 'alphaSynth.setMidiEventsPlayedFilter'; value: MidiEventType[] } + | { cmd: 'alphaSynth.play' } + | { cmd: 'alphaSynth.pause' } + | { cmd: 'alphaSynth.playPause' } + | { cmd: 'alphaSynth.stop' } + | { cmd: 'alphaSynth.playOneTimeMidiFile'; midi: unknown } + | { cmd: 'alphaSynth.loadSoundFontBytes'; data: Uint8Array; append: boolean } + | { cmd: 'alphaSynth.resetSoundFonts' } + | { cmd: 'alphaSynth.loadMidi'; midi: unknown } + | { cmd: 'alphaSynth.setChannelMute'; channel: number; mute: boolean } + | { cmd: 'alphaSynth.setChannelTranspositionPitch'; channel: number; semitones: number } + | { cmd: 'alphaSynth.setChannelSolo'; channel: number; solo: boolean } + | { cmd: 'alphaSynth.setChannelVolume'; channel: number; volume: number } + | { cmd: 'alphaSynth.resetChannelStates' } + | { cmd: 'alphaSynth.destroy' } + | { cmd: 'alphaSynth.applyTranspositionPitches'; transpositionPitches: Map } + /* worker -> main */ + | { cmd: 'alphaSynth.ready' } + | { cmd: 'alphaSynth.destroyed' } + | { + cmd: 'alphaSynth.positionChanged'; + args: PositionChangedEventArgs; + } + | { cmd: 'alphaSynth.playerStateChanged'; state: PlayerState; stopped: boolean } + | { cmd: 'alphaSynth.finished' } + | { cmd: 'alphaSynth.soundFontLoaded' } + | { cmd: 'alphaSynth.soundFontLoadFailed'; error: Error } + | { cmd: 'alphaSynth.midiLoaded'; args: PositionChangedEventArgs } + | { cmd: 'alphaSynth.midiLoadFailed'; error: Error } + | { cmd: 'alphaSynth.readyForPlayback' } + | { cmd: 'alphaSynth.midiEventsPlayed'; events: Map[] } + | { cmd: 'alphaSynth.playbackRangeChanged'; playbackRange: PlaybackRange | null } + + /* main -> exporter */ + | { + cmd: 'alphaSynth.exporter.initialize'; + options: AudioExportOptions; + midi: unknown; + syncPoints: BackingTrackSyncPoint[]; + transpositionPitches: Map; + exporterId: number; + } + | { cmd: 'alphaSynth.exporter.render'; exporterId: number; milliseconds: number } + | { cmd: 'alphaSynth.exporter.destroy'; exporterId: number } + /* exporter -> main */ + | { cmd: 'alphaSynth.exporter.initialized'; exporterId: number } + | { cmd: 'alphaSynth.exporter.rendered'; exporterId: number; chunk: AudioExportChunk | undefined } + | { cmd: 'alphaSynth.exporter.error'; exporterId: number; error: Error } + /* output -> worker */ + | { cmd: 'alphaSynth.output.sampleRequest' } + | { cmd: 'alphaSynth.output.samplesPlayed'; samples: number } + + /* worker -> output */ + | { cmd: 'alphaSynth.output.addSamples'; samples: Float32Array } + | { cmd: 'alphaSynth.output.play' } + | { cmd: 'alphaSynth.output.pause' } + | { cmd: 'alphaSynth.output.stop' } + | { cmd: 'alphaSynth.output.destroy' } + | { cmd: 'alphaSynth.output.resetSamples' }; + +/** + * @internal + */ +export interface IAlphaTabRenderingWorker extends IAlphaTabWorker {} + +/** + * @internal + */ +export interface IAlphaSynthWorker extends IAlphaTabWorker {} diff --git a/packages/alphatab/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts b/packages/alphatab/src/platform/worker/AlphaTabWorkerScoreRenderer.ts similarity index 81% rename from packages/alphatab/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts rename to packages/alphatab/src/platform/worker/AlphaTabWorkerScoreRenderer.ts index 0ef29268e..720575ad0 100644 --- a/packages/alphatab/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts +++ b/packages/alphatab/src/platform/worker/AlphaTabWorkerScoreRenderer.ts @@ -1,45 +1,41 @@ import type { AlphaTabApiBase } from '@coderline/alphatab/AlphaTabApiBase'; +import { Environment } from '@coderline/alphatab/Environment'; import { EventEmitter, - type IEventEmitterOfT, + EventEmitterOfT, type IEventEmitter, - EventEmitterOfT + type IEventEmitterOfT } from '@coderline/alphatab/EventEmitter'; import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; import type { Score } from '@coderline/alphatab/model/Score'; import { FontSizes } from '@coderline/alphatab/platform/svg/FontSizes'; +import type { + IAlphaTabRenderingWorker, + IAlphaTabWorkerMessage +} from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol'; import type { IScoreRenderer, RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; import { BoundsLookup } from '@coderline/alphatab/rendering/utils/BoundsLookup'; import type { Settings } from '@coderline/alphatab/Settings'; -import { Logger } from '@coderline/alphatab/Logger'; -import { Environment } from '@coderline/alphatab/Environment'; /** - * @target web - * @public + * @internal */ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { private _api: AlphaTabApiBase; - private _worker!: Worker; + private _worker!: IAlphaTabRenderingWorker; private _width: number = 0; public boundsLookup: BoundsLookup | null = null; - public constructor(api: AlphaTabApiBase, settings: Settings) { + public constructor(api: AlphaTabApiBase, worker: IAlphaTabRenderingWorker) { this._api = api; - - try { - this._worker = Environment.createWebWorker(settings); - } catch (e) { - Logger.error('Rendering', `Failed to create WebWorker: ${e}`); - return; - } + this._worker = worker; this._worker.postMessage({ cmd: 'alphaTab.initialize', - settings: this._serializeSettingsForWorker(settings) + settings: this._serializeSettingsForWorker(api.settings) }); - this._worker.addEventListener('message', this._handleWorkerMessage.bind(this)); + this._worker.addEventListener('message', e => this._handleWorkerMessage(e)); } public destroy(): void { @@ -53,7 +49,7 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { }); } - private _serializeSettingsForWorker(settings: Settings): unknown { + private _serializeSettingsForWorker(settings: Settings): Map { const jsObject = JsonConverter.settingsToJsObject(Environment.prepareForPostMessage(settings))!; // cut out player settings, they are only needed on UI thread side jsObject.delete('player'); @@ -92,9 +88,9 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { }); } - private _handleWorkerMessage(e: MessageEvent): void { - const data: any = e.data; - const cmd: string = data.cmd; + private _handleWorkerMessage(e: MessageEvent): void { + const data = e.data; + const cmd = data.cmd; switch (cmd) { case 'alphaTab.preRender': (this.preRender as EventEmitterOfT).trigger(data.resize); @@ -109,8 +105,11 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { (this.renderFinished as EventEmitterOfT).trigger(data.result); break; case 'alphaTab.postRenderFinished': - this.boundsLookup = BoundsLookup.fromJson(data.boundsLookup, this._api.score!); - this.boundsLookup.finish(); + const score = this._api.score; + if (score && data.boundsLookup) { + this.boundsLookup = BoundsLookup.fromJson(data.boundsLookup, this._api.score!); + this.boundsLookup?.finish(); + } (this.postRenderFinished as EventEmitter).trigger(); break; case 'alphaTab.error': @@ -120,7 +119,7 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { } public renderScore(score: Score | null, trackIndexes: number[] | null, renderHints?: RenderHints): void { - const jsObject: unknown = + const jsObject: Map | null = score == null ? null : JsonConverter.scoreToJsObject(Environment.prepareForPostMessage(score)); this._worker.postMessage({ cmd: 'alphaTab.renderScore', diff --git a/packages/alphatab/src/rendering/LineBarRenderer.ts b/packages/alphatab/src/rendering/LineBarRenderer.ts index 5c3d11ebf..bfee20a8e 100644 --- a/packages/alphatab/src/rendering/LineBarRenderer.ts +++ b/packages/alphatab/src/rendering/LineBarRenderer.ts @@ -261,8 +261,9 @@ export abstract class LineBarRenderer extends BarRendererBase { s = []; const zero = MusicFontSymbol.Tuplet0 as number; if (num > 10) { - s.push((zero + Math.floor(num / 10)) as MusicFontSymbol); - s.push((zero + (num - 10)) as MusicFontSymbol); + const tens = Math.floor(num / 10); + s.push((zero + tens) as MusicFontSymbol); + s.push((zero + (num - 10 * tens)) as MusicFontSymbol); } else { s.push((zero + num) as MusicFontSymbol); } @@ -270,8 +271,9 @@ export abstract class LineBarRenderer extends BarRendererBase { s.push(MusicFontSymbol.TupletColon); if (den > 10) { - s.push((zero + Math.floor(den / 10)) as MusicFontSymbol); - s.push((zero + (den - 10)) as MusicFontSymbol); + const tens = Math.floor(den / 10); + s.push((zero + tens) as MusicFontSymbol); + s.push((zero + (den - 10 * tens)) as MusicFontSymbol); } else { s.push((zero + den) as MusicFontSymbol); } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts index f7d2329e1..695fd2478 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts @@ -264,12 +264,14 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { const group: GlyphGroup = new GlyphGroup(0, 0); group.renderer = this.renderer; for (const note of this.container.beat.notes) { - const g = this._createBeatDot(sr.getNoteSteps(note), group); - g.colorOverride = ElementStyleHelper.noteColor( - sr.resources, - NoteSubElement.StandardNotationEffects, - note - ); + if (note.isVisible) { + const g = this._createBeatDot(sr.getNoteSteps(note), group); + g.colorOverride = ElementStyleHelper.noteColor( + sr.resources, + NoteSubElement.StandardNotationEffects, + note + ); + } } this.addEffect(group); } diff --git a/packages/alphatab/src/rendering/utils/BoundsLookup.ts b/packages/alphatab/src/rendering/utils/BoundsLookup.ts index 83788b4e9..396a7837f 100644 --- a/packages/alphatab/src/rendering/utils/BoundsLookup.ts +++ b/packages/alphatab/src/rendering/utils/BoundsLookup.ts @@ -13,105 +13,110 @@ import { StaffSystemBounds } from '@coderline/alphatab/rendering/utils/StaffSyst * @public */ export class BoundsLookup { - /** - * @target web - */ - public toJson(): unknown { - const json: any = {} as any; - const systems: StaffSystemBounds[] = []; - json.staffSystems = systems; + public toJson(): Map { + const json = new Map(); + const systems: Map[] = []; + json.set('staffSystems', systems); for (const system of this.staffSystems) { - const g: StaffSystemBounds = {} as any; - g.visualBounds = this._boundsToJson(system.visualBounds); - g.realBounds = this._boundsToJson(system.realBounds); - g.bars = []; + const g = new Map(); + g.set('visualBounds', BoundsLookup._boundsToJson(system.visualBounds)); + g.set('realBounds', BoundsLookup._boundsToJson(system.realBounds)); + const gBars: Map[] = []; + g.set('bars', gBars); + for (const masterBar of system.bars) { - const mb: MasterBarBounds = {} as any; - mb.lineAlignedBounds = this._boundsToJson(masterBar.lineAlignedBounds); - mb.visualBounds = this._boundsToJson(masterBar.visualBounds); - mb.realBounds = this._boundsToJson(masterBar.realBounds); - mb.index = masterBar.index; - mb.isFirstOfLine = masterBar.isFirstOfLine; - mb.bars = []; + const mb = new Map(); + mb.set('lineAlignedBounds', BoundsLookup._boundsToJson(masterBar.lineAlignedBounds)); + mb.set('visualBounds', BoundsLookup._boundsToJson(masterBar.visualBounds)); + mb.set('realBounds', BoundsLookup._boundsToJson(masterBar.realBounds)); + mb.set('index', masterBar.index); + mb.set('isFirstOfLine', masterBar.isFirstOfLine); + const mbBars: Map[] = []; + mb.set('bars', mbBars); for (const bar of masterBar.bars) { - const b: BarBounds = {} as any; - b.visualBounds = this._boundsToJson(bar.visualBounds); - b.realBounds = this._boundsToJson(bar.realBounds); - b.beats = []; + const b = new Map(); + b.set('visualBounds', BoundsLookup._boundsToJson(bar.visualBounds)); + b.set('realBounds', BoundsLookup._boundsToJson(bar.realBounds)); + const bBeats: Map[] = []; + b.set('beats', bBeats); for (const beat of bar.beats) { - const bb: BeatBounds = {} as any; - bb.visualBounds = this._boundsToJson(beat.visualBounds); - bb.realBounds = this._boundsToJson(beat.realBounds); - bb.onNotesX = beat.onNotesX; - const bbd: any = bb; - bbd.beatIndex = beat.beat.index; - bbd.voiceIndex = beat.beat.voice.index; - bbd.barIndex = beat.beat.voice.bar.index; - bbd.staffIndex = beat.beat.voice.bar.staff.index; - bbd.trackIndex = beat.beat.voice.bar.staff.track.index; + const bb = new Map(); + bb.set('visualBounds', BoundsLookup._boundsToJson(beat.visualBounds)); + bb.set('realBounds', BoundsLookup._boundsToJson(beat.realBounds)); + bb.set('onNotesX', beat.onNotesX); + bb.set('beatIndex', beat.beat.index); + bb.set('voiceIndex', beat.beat.voice.index); + bb.set('barIndex', beat.beat.voice.bar.index); + bb.set('staffIndex', beat.beat.voice.bar.staff.index); + bb.set('trackIndex', beat.beat.voice.bar.staff.track.index); if (beat.notes) { - const notes: NoteBounds[] = []; - bb.notes = notes; + const notes: Map[] = []; + bb.set('notes', notes); for (const note of beat.notes) { - const n: NoteBounds = {} as any; - const nd: any = n; - nd.index = note.note.index; - n.noteHeadBounds = this._boundsToJson(note.noteHeadBounds); + const n = new Map(); + n.set('index', note.note.index); + n.set('noteHeadBounds', BoundsLookup._boundsToJson(note.noteHeadBounds)); notes.push(n); } } - b.beats.push(bb); + bBeats.push(bb); } - mb.bars.push(b); + mbBars.push(b); } - g.bars.push(mb); + gBars.push(mb); } systems.push(g); } return json; } - /** - * @target web - */ - public static fromJson(json: unknown, score: Score): BoundsLookup { + public static fromJson(json: Map | null, score: Score): BoundsLookup | null { + if (json === null) { + return null; + } const lookup: BoundsLookup = new BoundsLookup(); - const staffSystems: StaffSystemBounds[] = (json as any).staffSystems; + const staffSystems = json.get('staffSystems')! as Map[]; for (const staffSystem of staffSystems) { const sg: StaffSystemBounds = new StaffSystemBounds(); - sg.visualBounds = BoundsLookup._boundsFromJson(staffSystem.visualBounds); - sg.realBounds = BoundsLookup._boundsFromJson(staffSystem.realBounds); + sg.visualBounds = BoundsLookup._boundsFromJson(staffSystem.get('visualBounds') as Map); + sg.realBounds = BoundsLookup._boundsFromJson(staffSystem.get('realBounds') as Map); lookup.addStaffSystem(sg); - for (const masterBar of staffSystem.bars) { + for (const masterBar of staffSystem.get('bars') as Map[]) { const mb: MasterBarBounds = new MasterBarBounds(); - mb.index = masterBar.index; - mb.isFirstOfLine = masterBar.isFirstOfLine; - mb.lineAlignedBounds = BoundsLookup._boundsFromJson(masterBar.lineAlignedBounds); - mb.visualBounds = BoundsLookup._boundsFromJson(masterBar.visualBounds); - mb.realBounds = BoundsLookup._boundsFromJson(masterBar.realBounds); + mb.index = masterBar.get('index') as number; + mb.isFirstOfLine = masterBar.get('isFirstOfLine') as boolean; + mb.lineAlignedBounds = BoundsLookup._boundsFromJson( + masterBar.get('lineAlignedBounds') as Map + ); + mb.visualBounds = BoundsLookup._boundsFromJson(masterBar.get('visualBounds') as Map); + mb.realBounds = BoundsLookup._boundsFromJson(masterBar.get('realBounds') as Map); lookup.addMasterBar(mb); - for (const bar of masterBar.bars) { + for (const bar of masterBar.get('bars') as Map[]) { const b: BarBounds = new BarBounds(); - b.visualBounds = BoundsLookup._boundsFromJson(bar.visualBounds); - b.realBounds = BoundsLookup._boundsFromJson(bar.realBounds); + b.visualBounds = BoundsLookup._boundsFromJson(bar.get('visualBounds') as Map); + b.realBounds = BoundsLookup._boundsFromJson(bar.get('realBounds') as Map); mb.addBar(b); - for (const beat of bar.beats) { + for (const beat of bar.get('beats') as Map[]) { const bb: BeatBounds = new BeatBounds(); - bb.visualBounds = BoundsLookup._boundsFromJson(beat.visualBounds); - bb.realBounds = BoundsLookup._boundsFromJson(beat.realBounds); - bb.onNotesX = beat.onNotesX; - const bd: any = beat; + bb.visualBounds = BoundsLookup._boundsFromJson( + beat.get('visualBounds') as Map + ); + bb.realBounds = BoundsLookup._boundsFromJson(beat.get('realBounds') as Map); + bb.onNotesX = beat.get('onNotesX') as number; bb.beat = - score.tracks[bd.trackIndex].staves[bd.staffIndex].bars[bd.barIndex].voices[ - bd.voiceIndex - ].beats[bd.beatIndex]; - if (beat.notes) { + score.tracks[beat.get('trackIndex') as number].staves[ + beat.get('staffIndex') as number + ].bars[beat.get('barIndex') as number].voices[beat.get('voiceIndex') as number].beats[ + beat.get('beatIndex') as number + ]; + if (beat.has('notes')) { bb.notes = []; - for (const note of beat.notes) { + for (const note of beat.get('notes') as Map[]) { const n: NoteBounds = new NoteBounds(); - const nd: any = note; - n.note = bb.beat.notes[nd.index]; - n.noteHeadBounds = BoundsLookup._boundsFromJson(note.noteHeadBounds); + n.note = bb.beat.notes[note.get('index') as number]; + n.noteHeadBounds = BoundsLookup._boundsFromJson( + note.get('noteHeadBounds') as Map + ); bb.addNote(n); } } @@ -123,27 +128,21 @@ export class BoundsLookup { return lookup; } - /** - * @target web - */ - private static _boundsFromJson(boundsRaw: Bounds): Bounds { + private static _boundsFromJson(boundsRaw: Map): Bounds { const b = new Bounds(); - b.x = boundsRaw.x; - b.y = boundsRaw.y; - b.w = boundsRaw.w; - b.h = boundsRaw.h; + b.x = boundsRaw.get('x') as number; + b.y = boundsRaw.get('y') as number; + b.w = boundsRaw.get('w') as number; + b.h = boundsRaw.get('h') as number; return b; } - /** - * @target web - */ - private _boundsToJson(bounds: Bounds): Bounds { - const json: Bounds = {} as any; - json.x = bounds.x; - json.y = bounds.y; - json.w = bounds.w; - json.h = bounds.h; + private static _boundsToJson(bounds: Bounds): Map { + const json = new Map(); + json.set('x', bounds.x); + json.set('y', bounds.y); + json.set('w', bounds.w); + json.set('h', bounds.h); return json; } diff --git a/packages/alphatab/src/synth/AlphaSynth.ts b/packages/alphatab/src/synth/AlphaSynth.ts index 6a5366ca2..e20f07332 100644 --- a/packages/alphatab/src/synth/AlphaSynth.ts +++ b/packages/alphatab/src/synth/AlphaSynth.ts @@ -1,29 +1,38 @@ +import { + EventEmitter, + EventEmitterOfT, + type IEventEmitter, + type IEventEmitterOfT +} from '@coderline/alphatab/EventEmitter'; +import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; +import { Logger } from '@coderline/alphatab/Logger'; +import type { LogLevel } from '@coderline/alphatab/LogLevel'; +import type { MidiEvent, MidiEventType } from '@coderline/alphatab/midi/MidiEvent'; import type { MidiFile } from '@coderline/alphatab/midi/MidiFile'; +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import type { Score } from '@coderline/alphatab/model/Score'; +import { Queue } from '@coderline/alphatab/synth/ds/Queue'; import type { BackingTrackSyncPoint, IAlphaSynth } from '@coderline/alphatab/synth/IAlphaSynth'; +import { + AudioExportChunk, + type AudioExportOptions, + type IAudioExporter +} from '@coderline/alphatab/synth/IAudioExporter'; +import type { IAudioSampleSynthesizer } from '@coderline/alphatab/synth/IAudioSampleSynthesizer'; import type { ISynthOutput } from '@coderline/alphatab/synth/ISynthOutput'; +import { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs'; import { MidiFileSequencer } from '@coderline/alphatab/synth/MidiFileSequencer'; import type { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange'; +import { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/PlaybackRangeChangedEventArgs'; import { PlayerState } from '@coderline/alphatab/synth/PlayerState'; import { PlayerStateChangedEventArgs } from '@coderline/alphatab/synth/PlayerStateChangedEventArgs'; import { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs'; import { Hydra } from '@coderline/alphatab/synth/soundfont/Hydra'; -import { TinySoundFont } from '@coderline/alphatab/synth/synthesis/TinySoundFont'; -import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@coderline/alphatab/EventEmitter'; -import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; -import { Logger } from '@coderline/alphatab/Logger'; -import type { LogLevel } from '@coderline/alphatab/LogLevel'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; -import type { SynthEvent } from '@coderline/alphatab/synth/synthesis/SynthEvent'; -import { Queue } from '@coderline/alphatab/synth/ds/Queue'; -import { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs'; -import type { MidiEvent, MidiEventType } from '@coderline/alphatab/midi/MidiEvent'; -import { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/PlaybackRangeChangedEventArgs'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import type { Score } from '@coderline/alphatab/model/Score'; -import type { IAudioSampleSynthesizer } from '@coderline/alphatab/synth/IAudioSampleSynthesizer'; -import { AudioExportChunk, type IAudioExporter, type AudioExportOptions } from '@coderline/alphatab/synth/IAudioExporter'; import type { Preset } from '@coderline/alphatab/synth/synthesis/Preset'; -import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import type { SynthEvent } from '@coderline/alphatab/synth/synthesis/SynthEvent'; +import { TinySoundFont } from '@coderline/alphatab/synth/synthesis/TinySoundFont'; /** * This is the base class for synthesizer components which can be used to @@ -284,6 +293,20 @@ export class AlphaSynthBase implements IAlphaSynth { } this._notPlayedSamples += samples.length; this.output.addSamples(samples); + + + // if the sequencer finished, we instantly force a noteOff on all + // voices to complete playback and stop voices fast. + // Doing this in the samplePlayed callback is too late as we might + // continue generating audio for long-release notes (especially percussion like cymbals) + + // we still have checkForFinish which takes care of the counterpart + // on the sample played area to ensure we seek back. + // but thanks to this code we ensure the output will complete fast as we won't + // be adding more samples beside a 0.1s ramp-down + if (this.sequencer.isFinished) { + this.synthesizer.noteOffAll(true); + } } else { // Tell output that there is no data left for it. const samples: Float32Array = new Float32Array(0); @@ -302,7 +325,7 @@ export class AlphaSynthBase implements IAlphaSynth { if (this._countInVolume > 0) { Logger.debug('AlphaSynth', 'Starting countin'); this.sequencer.startCountIn(); - this.synthesizer.setupMetronomeChannel(this._countInVolume); + this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this._countInVolume); this.updateTimePosition(0, true); } @@ -317,7 +340,7 @@ export class AlphaSynthBase implements IAlphaSynth { } Logger.debug('AlphaSynth', 'Starting playback'); - this.synthesizer.setupMetronomeChannel(this.metronomeVolume); + this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this.metronomeVolume); this._synthStopping = false; this.state = PlayerState.Playing; (this.stateChanged as EventEmitterOfT).trigger( @@ -419,7 +442,7 @@ export class AlphaSynthBase implements IAlphaSynth { private _checkReadyForPlayback(): void { if (this.isReadyForPlayback) { - this.synthesizer.setupMetronomeChannel(this.metronomeVolume); + this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this.metronomeVolume); const programs = this.sequencer.instrumentPrograms; const percussionKeys = this.sequencer.percussionKeys; let append = false; @@ -852,7 +875,7 @@ export class AlphaSynthAudioExporter implements IAlphaSynthAudioExporter { private _generatedAudioEndTime: number = 0; public setup() { - this._synth.setupMetronomeChannel(this._synth.metronomeVolume); + this._synth.setupMetronomeChannel(this._sequencer.metronomeChannel, this._synth.metronomeVolume); const syncPoints = this._sequencer.currentSyncPoints; const alphaTabEndTime = this._sequencer.currentEndTime; diff --git a/packages/alphatab/src/synth/BackingTrackPlayer.ts b/packages/alphatab/src/synth/BackingTrackPlayer.ts index 730250a9c..fb813fe8f 100644 --- a/packages/alphatab/src/synth/BackingTrackPlayer.ts +++ b/packages/alphatab/src/synth/BackingTrackPlayer.ts @@ -81,7 +81,7 @@ class BackingTrackAudioSynthesizer implements IAudioSampleSynthesizer { // not supported, ignore } - public setupMetronomeChannel(_metronomeVolume: number): void { + public setupMetronomeChannel(_metronomeChannel: number, _metronomeVolume: number): void { // not supported, ignore } diff --git a/packages/alphatab/src/synth/IAudioExporter.ts b/packages/alphatab/src/synth/IAudioExporter.ts index 5dd29603e..5523b451e 100644 --- a/packages/alphatab/src/synth/IAudioExporter.ts +++ b/packages/alphatab/src/synth/IAudioExporter.ts @@ -119,6 +119,7 @@ export interface IAudioExporter extends Disposable { * slightly longer audio is contained in the result. * * When the song ends, the chunk might contain less than the requested duration. + * @async */ render(milliseconds: number): Promise; @@ -141,6 +142,7 @@ export interface IAudioExporterWorker extends IAudioExporter { * @param midi The midi file to load * @param syncPoints The sync points of the song (if any) * @param transpositionPitches The initial transposition pitches for the midi file. + * @async */ initialize( options: AudioExportOptions, diff --git a/packages/alphatab/src/synth/IAudioSampleSynthesizer.ts b/packages/alphatab/src/synth/IAudioSampleSynthesizer.ts index 19d41847a..d8d1ce405 100644 --- a/packages/alphatab/src/synth/IAudioSampleSynthesizer.ts +++ b/packages/alphatab/src/synth/IAudioSampleSynthesizer.ts @@ -68,9 +68,10 @@ export interface IAudioSampleSynthesizer { /** * Configures the channel used to generate metronome sounds. + * @param metronomeChannel The midi hannel to use for playing the metronome (to avoid overlaps with instruments). * @param metronomeVolume The volume for the channel. */ - setupMetronomeChannel(metronomeVolume: number): void; + setupMetronomeChannel(metronomeChannel: number, metronomeVolume: number): void; /** * Synthesizes the given number of samples without producing an output (e.g. on seeking) diff --git a/packages/alphatab/src/synth/MidiFileSequencer.ts b/packages/alphatab/src/synth/MidiFileSequencer.ts index 1feac5b3e..f3073d26a 100644 --- a/packages/alphatab/src/synth/MidiFileSequencer.ts +++ b/packages/alphatab/src/synth/MidiFileSequencer.ts @@ -51,6 +51,7 @@ class MidiSequencerState { public endTime: number = 0; public currentTempo: number = 0; public syncPointTempo: number = 0; + public metronomeChannel: number = SynthConstants.DefaultChannelCount - 1; } /** @@ -65,6 +66,10 @@ export class MidiFileSequencer { private _oneTimeState: MidiSequencerState | null = null; private _countInState: MidiSequencerState | null = null; + public get metronomeChannel() { + return this._mainState.metronomeChannel; + } + public get isPlayingMain(): boolean { return this._currentState === this._mainState; } @@ -171,7 +176,7 @@ export class MidiFileSequencer { const metronomeVolume: number = this._synthesizer.metronomeVolume; this._synthesizer.noteOffAll(true); this._synthesizer.resetSoft(); - this._synthesizer.setupMetronomeChannel(metronomeVolume); + this._synthesizer.setupMetronomeChannel(this.metronomeChannel, metronomeVolume); } this._mainSilentProcess(timePosition); } @@ -239,6 +244,8 @@ export class MidiFileSequencer { let metronomeTick: number = midiFile.tickShift; // shift metronome to content let metronomeTime: number = 0.0; + let maxChannel = 0; + let previousTick: number = 0; for (const mEvent of midiFile.events) { const synthData: SynthEvent = new SynthEvent(state.synthData.length, mEvent); @@ -287,6 +294,9 @@ export class MidiFileSequencer { if (!state.firstProgramEventPerChannel.has(channel)) { state.firstProgramEventPerChannel.set(channel, synthData); } + if (channel > maxChannel) { + maxChannel = channel; + } const isPercussion = channel === SynthConstants.PercussionChannel; if (!isPercussion) { this.instrumentPrograms.add(programChange.program); @@ -297,6 +307,9 @@ export class MidiFileSequencer { if (isPercussion) { this.percussionKeys.add(noteOn.noteKey); } + if (noteOn.channel > maxChannel) { + maxChannel = noteOn.channel; + } } } @@ -314,6 +327,7 @@ export class MidiFileSequencer { }); state.endTime = absTime; state.endTick = absTick; + state.metronomeChannel = maxChannel + 1; return state; } diff --git a/packages/alphatab/src/synth/SynthConstants.ts b/packages/alphatab/src/synth/SynthConstants.ts index 805498f97..b5082f0db 100644 --- a/packages/alphatab/src/synth/SynthConstants.ts +++ b/packages/alphatab/src/synth/SynthConstants.ts @@ -8,7 +8,6 @@ */ export class SynthConstants { public static readonly DefaultChannelCount: number = 16 + 1; - public static readonly MetronomeChannel: number = SynthConstants.DefaultChannelCount - 1; public static readonly MetronomeKey: number = 33; public static readonly AudioChannels: number = 2; public static readonly MinVolume: number = 0; @@ -35,4 +34,9 @@ export class SynthConstants { public static readonly MicroBufferCount: number = 32; public static readonly MicroBufferSize: number = 64; + + /** + * approximately -60 dB, which is inaudible to humans + */ + public static readonly AudibleLevelThreshold: number = 1e-3; } diff --git a/packages/alphatab/src/synth/_barrel.ts b/packages/alphatab/src/synth/_barrel.ts index c4e2555b5..e5ec3a878 100644 --- a/packages/alphatab/src/synth/_barrel.ts +++ b/packages/alphatab/src/synth/_barrel.ts @@ -10,7 +10,7 @@ export { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/Playbac export { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs'; export { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs'; export { ActiveBeatsChangedEventArgs } from '@coderline/alphatab/synth/ActiveBeatsChangedEventArgs'; -export { AlphaSynthWebWorkerApi } from '@coderline/alphatab/platform/javascript/AlphaSynthWebWorkerApi'; +export { AlphaSynthWebWorkerApi } from '@coderline/alphatab/platform/worker/AlphaSynthWebWorkerApi'; export { AlphaSynthWebAudioOutputBase } from '@coderline/alphatab/platform/javascript/AlphaSynthWebAudioOutputBase'; export { AlphaSynthScriptProcessorOutput } from '@coderline/alphatab/platform/javascript/AlphaSynthScriptProcessorOutput'; export { AlphaSynthAudioWorkletOutput } from '@coderline/alphatab/platform/javascript/AlphaSynthAudioWorkletOutput'; diff --git a/packages/alphatab/src/synth/synthesis/TinySoundFont.ts b/packages/alphatab/src/synth/synthesis/TinySoundFont.ts index 49e107d05..b70b98a72 100644 --- a/packages/alphatab/src/synth/synthesis/TinySoundFont.ts +++ b/packages/alphatab/src/synth/synthesis/TinySoundFont.ts @@ -63,6 +63,7 @@ export class TinySoundFont implements IAudioSampleSynthesizer { public currentTempo: number = 0; public timeSignatureNumerator: number = 0; public timeSignatureDenominator: number = 0; + private _metronomeChannel: number = SynthConstants.DefaultChannelCount - 1; public constructor(sampleRate: number) { this.outSampleRate = sampleRate; @@ -188,8 +189,8 @@ export class TinySoundFont implements IAudioSampleSynthesizer { while (!this._midiEventQueue.isEmpty) { const m: SynthEvent = this._midiEventQueue.dequeue()!; if (m.isMetronome && this.metronomeVolume > 0) { - this.channelNoteOff(SynthConstants.MetronomeChannel, SynthConstants.MetronomeKey); - this.channelNoteOn(SynthConstants.MetronomeChannel, SynthConstants.MetronomeKey, 95 / 127); + this.channelNoteOff(this._metronomeChannel, SynthConstants.MetronomeKey); + this.channelNoteOn(this._metronomeChannel, SynthConstants.MetronomeKey, 95 / 127); } else if (m.event) { this.processMidiMessage(m.event); } @@ -204,7 +205,7 @@ export class TinySoundFont implements IAudioSampleSynthesizer { // exception. metronome is implicitly added in solo const isChannelMuted: boolean = this._mutedChannels.has(channel) || - (anySolo && channel !== SynthConstants.MetronomeChannel && !this._soloChannels.has(channel)); + (anySolo && channel !== this._metronomeChannel && !this._soloChannels.has(channel)); if (!buffer) { voice.kill(); @@ -261,18 +262,19 @@ export class TinySoundFont implements IAudioSampleSynthesizer { } public get metronomeVolume(): number { - return this.channelGetMixVolume(SynthConstants.MetronomeChannel); + return this.channelGetMixVolume(this._metronomeChannel); } public set metronomeVolume(value: number) { - this.setupMetronomeChannel(value); + this.setupMetronomeChannel(this._metronomeChannel, value); } - public setupMetronomeChannel(volume: number): void { - this.channelSetMixVolume(SynthConstants.MetronomeChannel, volume); + public setupMetronomeChannel(channel:number, volume: number): void { + this._metronomeChannel = channel; + this.channelSetMixVolume(channel, volume); if (volume > 0) { - this.channelSetVolume(SynthConstants.MetronomeChannel, 1); - this.channelSetPresetNumber(SynthConstants.MetronomeChannel, 0, true); + this.channelSetVolume(channel, 1); + this.channelSetPresetNumber(channel, 0, true); } } diff --git a/packages/alphatab/src/synth/synthesis/Voice.ts b/packages/alphatab/src/synth/synthesis/Voice.ts index 3d06cfec2..150455d31 100644 --- a/packages/alphatab/src/synth/synthesis/Voice.ts +++ b/packages/alphatab/src/synth/synthesis/Voice.ts @@ -213,20 +213,19 @@ export class Voice { noteGain = SynthHelper.decibelsToGain(this.noteGainDb + this.modLfo.level * tmpModLfoToVolume); } - gainMono = noteGain * this.ampEnv.level; + // Update EG. + this.ampEnv.process(blockSamples, f.outSampleRate); + if (updateModEnv) { + this.modEnv.process(blockSamples, f.outSampleRate); + } + gainMono = noteGain * this.ampEnv.level; if (isMuted) { gainMono = 0; } else { gainMono *= this.mixVolume; } - // Update EG. - this.ampEnv.process(blockSamples, f.outSampleRate); - if (updateModEnv) { - this.modEnv.process(blockSamples, f.outSampleRate); - } - // Update LFOs. if (updateModLFO) { this.modLfo.process(blockSamples); @@ -321,7 +320,15 @@ export class Voice { break; } - if (tmpSourceSamplePosition >= tmpSampleEndDbl || this.ampEnv.segment === VoiceEnvelopeSegment.Done) { + const inaudible = + this.ampEnv.segment === VoiceEnvelopeSegment.Release && + Math.abs(gainMono) < SynthConstants.AudibleLevelThreshold; + if ( + tmpSourceSamplePosition >= tmpSampleEndDbl || + this.ampEnv.segment === VoiceEnvelopeSegment.Done || + // Check if voice is inaudible during release to terminate early + inaudible + ) { this.kill(); return; } diff --git a/packages/alphatab/test-data/audio/export-silent-with-metronome.pcm b/packages/alphatab/test-data/audio/export-silent-with-metronome.pcm index 36f55e7a3..8bccb95c4 100644 Binary files a/packages/alphatab/test-data/audio/export-silent-with-metronome.pcm and b/packages/alphatab/test-data/audio/export-silent-with-metronome.pcm differ diff --git a/packages/alphatab/test-data/audio/export-sync-points.pcm b/packages/alphatab/test-data/audio/export-sync-points.pcm index 06db056ce..39c6e11e2 100644 Binary files a/packages/alphatab/test-data/audio/export-sync-points.pcm and b/packages/alphatab/test-data/audio/export-sync-points.pcm differ diff --git a/packages/alphatab/test-data/audio/export-test.pcm b/packages/alphatab/test-data/audio/export-test.pcm index 31c501d84..e7d72fc87 100644 Binary files a/packages/alphatab/test-data/audio/export-test.pcm and b/packages/alphatab/test-data/audio/export-test.pcm differ diff --git a/packages/alphatab/test-data/guitarpro8/harmonics-lowercase.gp b/packages/alphatab/test-data/guitarpro8/harmonics-lowercase.gp new file mode 100644 index 000000000..dfaf764d5 Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/harmonics-lowercase.gp differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/hidden-dots.mxml b/packages/alphatab/test-data/visual-tests/effects-and-annotations/hidden-dots.mxml new file mode 100644 index 000000000..9fd53015e --- /dev/null +++ b/packages/alphatab/test-data/visual-tests/effects-and-annotations/hidden-dots.mxml @@ -0,0 +1,74 @@ + + + + + + Piano + + Piano + + + + 1 + 1 + 78.7402 + 0 + + + + + + + 480 + + -5 + + + 2 + + G + 2 + + + G + 2 + + + + + F + 4 + + 480 + + 5 + quarter + + none + 2 + + + + + + + F + 4 + + 1440 + + 5 + half + + up + 2 + + + + + + + diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/hidden-dots.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/hidden-dots.png new file mode 100644 index 000000000..d731131b3 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/hidden-dots.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets-huge.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets-huge.png new file mode 100644 index 000000000..884d778a5 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets-huge.png differ diff --git a/packages/alphatab/test/TestPlatform.ts b/packages/alphatab/test/TestPlatform.ts index f887fb796..14ccd0018 100644 --- a/packages/alphatab/test/TestPlatform.ts +++ b/packages/alphatab/test/TestPlatform.ts @@ -7,6 +7,17 @@ import path from 'node:path'; * @internal */ export class TestPlatform { + /** + * @target web + * @partial + */ + public static throttle(action: () => void, delay: number): () => void { + let timeoutId: NodeJS.Timeout | undefined = undefined; + return () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(action, delay); + }; + } /** * @target web * @partial diff --git a/packages/alphatab/test/audio/SyncPoint.test.ts b/packages/alphatab/test/audio/SyncPoint.test.ts index 81cb80cce..22f657cb9 100644 --- a/packages/alphatab/test/audio/SyncPoint.test.ts +++ b/packages/alphatab/test/audio/SyncPoint.test.ts @@ -1,4 +1,9 @@ -import { type IEventEmitterOfT, type IEventEmitter, EventEmitterOfT, EventEmitter } from '@coderline/alphatab/EventEmitter'; +import { + type IEventEmitterOfT, + type IEventEmitter, + EventEmitterOfT, + EventEmitter +} from '@coderline/alphatab/EventEmitter'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler'; import { MidiFile } from '@coderline/alphatab/midi/MidiFile'; @@ -464,7 +469,7 @@ class EmptyAudioSynthesizer implements IAudioSampleSynthesizer { _percussionKeys: Set, _append: boolean ): void {} - public setupMetronomeChannel(_metronomeVolume: number): void {} + public setupMetronomeChannel(_metronomeChannel: number, _metronomeVolume: number): void {} public synthesizeSilent(_sampleCount: number): void {} public dispatchEvent(_synthEvent: SynthEvent): void {} public synthesize(_buffer: Float32Array, _bufferPos: number, _ampleCount: number): SynthEvent[] { diff --git a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts index 4faf7e7a2..77f9c86ae 100644 --- a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts +++ b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts @@ -56,10 +56,13 @@ describe('AlphaTexExporterTest', () => { } } - async function testRoundTripFolderEqual(name: string): Promise { + async function testRoundTripFolderEqual(name: string, ignoredFiles?: string[]): Promise { const files: string[] = await TestPlatform.listDirectory(`test-data/${name}`); + const ignoredFilesLookup = new Set(ignoredFiles); for (const file of files.filter(f => !f.endsWith('.png'))) { - await testRoundTripEqual(`${name}/${file}`, null); + if (!ignoredFilesLookup.has(file) && !file.endsWith('.png')) { + await testRoundTripEqual(`${name}/${file}`, null); + } } } @@ -133,7 +136,7 @@ describe('AlphaTexExporterTest', () => { }); it('visual-effects-and-annotations', async () => { - await testRoundTripFolderEqual('visual-tests/effects-and-annotations'); + await testRoundTripFolderEqual('visual-tests/effects-and-annotations', ['hidden-dots.mxml']); }); it('visual-general', async () => { diff --git a/packages/alphatab/test/exporter/Gp7Exporter.test.ts b/packages/alphatab/test/exporter/Gp7Exporter.test.ts index a68cf0334..5c1f95695 100644 --- a/packages/alphatab/test/exporter/Gp7Exporter.test.ts +++ b/packages/alphatab/test/exporter/Gp7Exporter.test.ts @@ -77,7 +77,7 @@ describe('Gp7ExporterTest', () => { }); it('visual-effects-and-annotations', async () => { - await testRoundTripFolderEqual('visual-tests/effects-and-annotations'); + await testRoundTripFolderEqual('visual-tests/effects-and-annotations', ['hidden-dots.mxml']); }); it('visual-general', async () => { diff --git a/packages/alphatab/test/importer/Gp8Importer.test.ts b/packages/alphatab/test/importer/Gp8Importer.test.ts index 9fe94ed4e..0ef7ffa1d 100644 --- a/packages/alphatab/test/importer/Gp8Importer.test.ts +++ b/packages/alphatab/test/importer/Gp8Importer.test.ts @@ -504,4 +504,10 @@ describe('Gp8ImporterTest', () => { expect(score.masterBars[5].beamingRules!.groups.has(Duration.Eighth)).to.be.true; expect(score.masterBars[5].beamingRules!.groups.get(Duration.Eighth)!.join(',')).to.be.equal('4,4'); }); + + it('harmonics-lowercase', async () => { + const reader = await prepareImporterWithFile('guitarpro8/harmonics-lowercase.gp'); + const score = reader.readScore(); + GpImporterTestHelper.checkHarmonics(score); + }); }); diff --git a/packages/alphatab/test/visualTests/TestUiFacade.ts b/packages/alphatab/test/visualTests/TestUiFacade.ts index 6e9efc966..1b8ad03dd 100644 --- a/packages/alphatab/test/visualTests/TestUiFacade.ts +++ b/packages/alphatab/test/visualTests/TestUiFacade.ts @@ -331,6 +331,10 @@ export class TestUiFacade implements IUiFacade { return null; } + public throttle(action: () => void, delay: number): () => void { + return TestPlatform.throttle(action, delay); + } + public readonly canRenderChanged: IEventEmitter = new EventEmitter(); public readonly rootContainerBecameVisible: IEventEmitter = new EventEmitter(); } diff --git a/packages/alphatab/test/visualTests/VisualTestHelper.ts b/packages/alphatab/test/visualTests/VisualTestHelper.ts index d840d46d7..996744259 100644 --- a/packages/alphatab/test/visualTests/VisualTestHelper.ts +++ b/packages/alphatab/test/visualTests/VisualTestHelper.ts @@ -198,7 +198,7 @@ export class VisualTestHelper { throw errors[0]; } if (errors.length > 0) { - const errorMessages = errors.map(e => e.message ?? 'Unknown error').join('\n'); + const errorMessages = errors.map(e => `${e.message ?? 'Unknown error'}\n${e.stack}`).join('\n'); throw new Error(errorMessages); } } finally { diff --git a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts index 91452df36..1d096492a 100644 --- a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -119,6 +119,17 @@ describe('EffectsAndAnnotationsTests', () => { await VisualTestHelper.runVisualTest('effects-and-annotations/tuplets.gp'); }); + it('tuplets-huge', async () => { + await VisualTestHelper.runVisualTestTex( + 'C4 {tu 12 27} * 12', + 'test-data/visual-tests/effects-and-annotations/tuplets-huge.png' + ); + }); + + it('hidden-dots', async () => { + await VisualTestHelper.runVisualTest('effects-and-annotations/hidden-dots.mxml'); + }); + it('tuplets-advanced', async () => { await VisualTestHelper.runVisualTest('effects-and-annotations/tuplets-advanced.gp', undefined, o => { o.tracks = [0, 1, 2]; diff --git a/packages/alphatex/package.json b/packages/alphatex/package.json index 737389fde..66969dd80 100644 --- a/packages/alphatex/package.json +++ b/packages/alphatex/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-alphatex", - "version": "1.8.1", + "version": "1.9.0", "private": true, "scripts": { "lint": "biome lint", @@ -8,7 +8,7 @@ "generate": "tsx scripts/generate.ts" }, "devDependencies": { - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" diff --git a/packages/csharp/package.json b/packages/csharp/package.json index 6e43893f7..27612d9e8 100644 --- a/packages/csharp/package.json +++ b/packages/csharp/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-csharp", - "version": "1.8.1", + "version": "1.9.0", "description": "The C# target of alphaTab.", "private": true, "type": "module", @@ -13,6 +13,6 @@ }, "devDependencies": { "@coderline/alphatab-transpiler": "*", - "rimraf": "^6.1.2" + "rimraf": "^6.1.3" } } diff --git a/packages/csharp/src/AlphaTab.Test/TestPlatform.cs b/packages/csharp/src/AlphaTab.Test/TestPlatform.cs index c8ca09f64..1115b5acd 100644 --- a/packages/csharp/src/AlphaTab.Test/TestPlatform.cs +++ b/packages/csharp/src/AlphaTab.Test/TestPlatform.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.IO; @@ -6,6 +6,7 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; namespace AlphaTab; @@ -29,6 +30,23 @@ static partial class TestPlatform $"Could not find repository root via working dir {System.Environment.CurrentDirectory}"); }); + public static Action Throttle(Action action, double delay) + { + CancellationTokenSource? cancellationTokenSource = null; + return () => + { + cancellationTokenSource?.Cancel(); + cancellationTokenSource = new CancellationTokenSource(); + + Task.Run(async () => + { + await Task.Delay((int)delay, cancellationTokenSource.Token); + action(); + }, + cancellationTokenSource.Token); + }; + } + public static async Task LoadFile(string path) { await using var fs = @@ -120,6 +138,7 @@ public override ArrayTuple Read( { throw new JsonException(); } + var v0 = _keyConverter.Read(ref reader, _keyType, options)!; if (!reader.Read()) @@ -136,7 +155,8 @@ public override ArrayTuple Read( return new ArrayTuple(v0, v1); } - public override void Write(Utf8JsonWriter writer, ArrayTuple value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ArrayTuple value, + JsonSerializerOptions options) { writer.WriteStartArray(); _keyConverter.Write(writer, value.V0, options); @@ -167,7 +187,8 @@ public static async Task SaveFileAsString(string name, string data) { var path = Path.Combine(AlphaTabProjectRoot.Value, name); Directory.CreateDirectory(Path.GetDirectoryName(path)!); - await using var fs = new StreamWriter(new FileStream(path, FileMode.Create, FileAccess.ReadWrite)); + await using var fs = + new StreamWriter(new FileStream(path, FileMode.Create, FileAccess.ReadWrite)); await fs.WriteAsync(data); } @@ -272,6 +293,7 @@ public static string CurrentTestName { return ""; } + var testName = testMethodInfo.MethodInfo.GetCustomAttribute()! .DisplayName; return testName ?? ""; diff --git a/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs b/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs index bd4518ffd..dcfbef075 100644 --- a/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs +++ b/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs @@ -245,7 +245,7 @@ public override void DestroyCursors() return null; } - public override void BeginInvoke(Action action) + protected override void PostToUIThread(Action action) { SettingsContainer.BeginInvoke(action); } diff --git a/packages/csharp/src/AlphaTab.Windows/Wpf/AlphaTab.cs b/packages/csharp/src/AlphaTab.Windows/Wpf/AlphaTab.cs index 4c7668ebc..190306b9f 100644 --- a/packages/csharp/src/AlphaTab.Windows/Wpf/AlphaTab.cs +++ b/packages/csharp/src/AlphaTab.Windows/Wpf/AlphaTab.cs @@ -29,8 +29,8 @@ static AlphaTab() /// Identifies the dependency property. /// public static readonly DependencyProperty TracksProperty = - DependencyProperty.Register("Tracks", typeof(IEnumerable), typeof(AlphaTab), - new PropertyMetadata(default(IEnumerable), OnTracksChanged)); + DependencyProperty.Register(nameof(Tracks), typeof(IEnumerable), typeof(AlphaTab), + new PropertyMetadata(null, OnTracksChanged)); private static void OnTracksChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -38,16 +38,16 @@ private static void OnTracksChanged(DependencyObject d, var observable = e.OldValue as INotifyCollectionChanged; if (observable != null) { - ((AlphaTab) d).UnregisterObservableCollection(observable); + ((AlphaTab)d).UnregisterObservableCollection(observable); } observable = e.NewValue as INotifyCollectionChanged; if (observable != null) { - ((AlphaTab) d).RegisterObservableCollection(observable); + ((AlphaTab)d).RegisterObservableCollection(observable); } - ((AlphaTab) d).RenderTracks(); + ((AlphaTab)d).RenderTracks(); } private void RegisterObservableCollection(INotifyCollectionChanged collection) @@ -66,9 +66,9 @@ private void OnTracksChanged(object? sender, NotifyCollectionChangedEventArgs e) } /// - public IEnumerable Tracks + public IEnumerable? Tracks { - get => (IEnumerable) GetValue(TracksProperty); + get => (IEnumerable)GetValue(TracksProperty); set => SetValue(TracksProperty, value); } @@ -86,13 +86,13 @@ public IEnumerable Tracks private static void OnSettingsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - ((AlphaTab) d).SettingsChanged?.Invoke((Settings) e.NewValue); + ((AlphaTab)d).SettingsChanged?.Invoke((Settings)e.NewValue); } /// public Settings Settings { - get => (Settings) GetValue(SettingsProperty); + get => (Settings)GetValue(SettingsProperty); set => SetValue(SettingsProperty, value); } @@ -113,7 +113,7 @@ public Settings Settings /// public Brush BarCursorFill { - get => (Brush) GetValue(BarCursorFillProperty); + get => (Brush)GetValue(BarCursorFillProperty); set => SetValue(BarCursorFillProperty, value); } @@ -134,7 +134,7 @@ public Brush BarCursorFill /// public Brush BeatCursorFill { - get => (Brush) GetValue(BeatCursorFillProperty); + get => (Brush)GetValue(BeatCursorFillProperty); set => SetValue(BeatCursorFillProperty, value); } @@ -155,16 +155,32 @@ public Brush BeatCursorFill /// public Brush SelectionFill { - get => (Brush) GetValue(SelectionCursorFillProperty); + get => (Brush)GetValue(SelectionCursorFillProperty); set => SetValue(SelectionCursorFillProperty, value); } #endregion + private static readonly DependencyPropertyKey ApiKey = + DependencyProperty.RegisterReadOnly( + nameof(Api), + typeof(AlphaTabApiBase), + typeof(AlphaTab), + new FrameworkPropertyMetadata(null)); + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty ApiProperty = ApiKey.DependencyProperty; + /// /// Gets the alphaTab API object. /// - public AlphaTabApiBase Api { get; private set; } + public AlphaTabApiBase? Api + { + get => GetValue(ApiProperty) as AlphaTabApiBase; + private set => SetValue(ApiKey, value); + } /// /// Initializes a new instance of the class. @@ -184,7 +200,7 @@ public AlphaTab() public override void OnApplyTemplate() { base.OnApplyTemplate(); - _scrollView = (ScrollViewer) Template.FindName("PART_ScrollView", this); + _scrollView = (ScrollViewer)Template.FindName("PART_ScrollView", this); Api = new AlphaTabApiBase(new WpfUiFacade(_scrollView), this); } diff --git a/packages/csharp/src/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs b/packages/csharp/src/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs index 13af754de..b6ee7c317 100644 --- a/packages/csharp/src/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs +++ b/packages/csharp/src/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs @@ -120,6 +120,7 @@ public void AppendChild(IContainer child) private double _targetX = 0; + public void StopAnimation() { Control.Dispatcher.BeginInvoke((Action)(() => @@ -134,8 +135,16 @@ public void TransitionToX(double duration, double x) _targetX = x; Control.Dispatcher.BeginInvoke((Action)(() => { - Control.BeginAnimation(Canvas.LeftProperty, - new DoubleAnimation(x, new Duration(TimeSpan.FromMilliseconds(duration)))); + if (duration > 0) + { + Control.BeginAnimation(Canvas.LeftProperty, + new DoubleAnimation(x, new Duration(TimeSpan.FromMilliseconds(duration)))); + } + else + { + Control.BeginAnimation(Canvas.LeftProperty, null); + Canvas.SetLeft(Control, x); + } })); } diff --git a/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs b/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs index 042051e27..b4a760c8d 100644 --- a/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs +++ b/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs @@ -32,10 +32,22 @@ internal class WpfUiFacade : ManagedUiFacade public override IEventEmitter RootContainerBecameVisible { get; } + private static double GetDpiScale(Visual visual) + { + var source = PresentationSource.FromVisual(visual); + if (source?.CompositionTarget != null) + { + var transformToDevice = source.CompositionTarget.TransformToDevice; + return transformToDevice.M11; + } + return 1.0; + } + public WpfUiFacade(ScrollViewer scrollViewer) { _scrollViewer = scrollViewer; RootContainer = new FrameworkElementContainer(scrollViewer); + Environment.HighDpiFactor = GetDpiScale(scrollViewer); RootContainerBecameVisible = new DelegatedEventEmitter( value => { @@ -109,7 +121,7 @@ protected override ISynthOutput CreateSynthOutput() return new NAudioSynthOutput(); } - public override IAlphaSynth? CreateBackingTrackPlayer() + public override IAlphaSynth CreateBackingTrackPlayer() { return new BackingTrackPlayer( new NAudioBackingTrackOutput(BeginInvoke), @@ -215,7 +227,7 @@ public override void BeginAppendRenderResults(RenderFinishedEventArgs? r) { placeholder = new Image { - Stretch = Stretch.None, + Stretch = Stretch.Fill, SnapsToDevicePixels = true }; panel.Children.Add(placeholder); @@ -294,7 +306,7 @@ public override Cursors CreateCursors() ); } - public override void BeginInvoke(Action action) + protected override void PostToUIThread(Action action) { SettingsContainer.Dispatcher?.BeginInvoke(action); } diff --git a/packages/csharp/src/AlphaTab/Core/EcmaScript/MessageEvent.cs b/packages/csharp/src/AlphaTab/Core/EcmaScript/MessageEvent.cs new file mode 100644 index 000000000..6fcdbf3fa --- /dev/null +++ b/packages/csharp/src/AlphaTab/Core/EcmaScript/MessageEvent.cs @@ -0,0 +1,11 @@ +namespace AlphaTab.Core.EcmaScript; + +internal class MessageEvent +{ + public MessageEvent(T data) + { + Data = data; + } + + public T Data { get; } +} diff --git a/packages/csharp/src/AlphaTab/Core/EcmaScript/Promise.cs b/packages/csharp/src/AlphaTab/Core/EcmaScript/Promise.cs index 7cffa6a2d..5965bc965 100644 --- a/packages/csharp/src/AlphaTab/Core/EcmaScript/Promise.cs +++ b/packages/csharp/src/AlphaTab/Core/EcmaScript/Promise.cs @@ -12,4 +12,15 @@ public static async Task Race(IList tasks) var completed = await Task.WhenAny(tasks); await completed; } + + public static PromiseWithResolvers WithResolvers() + { + return new PromiseWithResolvers(); + } + + + public static PromiseWithResolvers WithResolvers() + { + return new PromiseWithResolvers(); + } } diff --git a/packages/csharp/src/AlphaTab/Core/EcmaScript/PromiseWithResolvers.cs b/packages/csharp/src/AlphaTab/Core/EcmaScript/PromiseWithResolvers.cs new file mode 100644 index 000000000..02f1d65ba --- /dev/null +++ b/packages/csharp/src/AlphaTab/Core/EcmaScript/PromiseWithResolvers.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace AlphaTab.Core.EcmaScript; + +public class PromiseWithResolvers +{ + private readonly TaskCompletionSource _taskCompletionSource = new(); + public Task Promise => _taskCompletionSource.Task; + + public void Resolve(T o) + { + _taskCompletionSource.TrySetResult(o); + } + + public void Reject(Error error) + { + _taskCompletionSource.TrySetException(error); + } +} diff --git a/packages/csharp/src/AlphaTab/Environment.cs b/packages/csharp/src/AlphaTab/Environment.cs index 0948893bc..46e9bfb37 100644 --- a/packages/csharp/src/AlphaTab/Environment.cs +++ b/packages/csharp/src/AlphaTab/Environment.cs @@ -1,21 +1,19 @@ using System; -using System.Collections; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; using AlphaTab.Collections; using AlphaTab.Platform; using AlphaTab.Platform.CSharp; +using AlphaTab.Platform.Worker; namespace AlphaTab; partial class Environment { - public const bool SupportsTextDecoder = true; - - public static void PlatformInit() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T PrepareForPostMessage(T o) { + return o; } private static void _printPlatformInfo(System.Action print) @@ -26,23 +24,6 @@ private static void _printPlatformInfo(System.Action print) print($"OS Arch: {RuntimeInformation.OSArchitecture}"); } - public static Action Throttle(Action action, double delay) - { - CancellationTokenSource? cancellationTokenSource = null; - return () => - { - cancellationTokenSource?.Cancel(); - cancellationTokenSource = new CancellationTokenSource(); - - Task.Run(async () => - { - await Task.Delay((int)delay, cancellationTokenSource.Token); - action(); - }, - cancellationTokenSource.Token); - }; - } - private static void _createPlatformSpecificRenderEngines( IMap renderEngines) { @@ -63,4 +44,21 @@ internal static void SortDescending(System.Collections.Generic.IList lis { list.Sort((a, b) => b - a); } + + + internal static IAlphaTabWorkerGlobalScope GetGlobalWorkerScope() + { + if (typeof(T) == typeof(IAlphaSynthWorkerMessage)) + { + return (IAlphaTabWorkerGlobalScope)ManagedThreadAlphaSynthWorker.CurrentThreadWorker; + } + + if (typeof(T) == typeof(IAlphaTabWorkerMessage)) + { + return (IAlphaTabWorkerGlobalScope)ManagedThreadAlphaTabRendererWorker + .CurrentThreadWorker; + } + + throw new InvalidOperationException("Unsupported worker scope kind"); + } } diff --git a/packages/csharp/src/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs b/packages/csharp/src/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs deleted file mode 100644 index f79c64142..000000000 --- a/packages/csharp/src/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs +++ /dev/null @@ -1,273 +0,0 @@ -using System; -using System.Collections.Generic; -using AlphaTab.Collections; -using AlphaTab.Midi; -using AlphaTab.Model; -using AlphaTab.Synth; - -namespace AlphaTab.Platform.CSharp; - -internal abstract class AlphaSynthWorkerApiBase : IAlphaSynth -{ - private LogLevel _logLevel; - private readonly double _bufferTimeInMilliseconds; - - public AlphaSynth? Player { get; private set; } - - protected AlphaSynthWorkerApiBase(ISynthOutput output, LogLevel logLevel, double bufferTimeInMilliseconds) - { - Output = output; - _logLevel = logLevel; - _bufferTimeInMilliseconds = bufferTimeInMilliseconds; - Player = null!; - } - - public ISynthOutput Output { get; } - - public abstract void Destroy(); - protected abstract void DispatchOnUiThread(Action action); - protected internal abstract void DispatchOnWorkerThread(Action action); - - protected void Initialize() - { - Player = new AlphaSynth(Output, _bufferTimeInMilliseconds); - Player.PositionChanged.On(OnPositionChanged); - Player.StateChanged.On(OnStateChanged); - Player.Finished.On(OnFinished); - Player.SoundFontLoaded.On(OnSoundFontLoaded); - Player.SoundFontLoadFailed.On(OnSoundFontLoadFailed); - Player.MidiLoaded.On(OnMidiLoaded); - Player.MidiLoadFailed.On(OnMidiLoadFailed); - Player.ReadyForPlayback.On(OnReadyForPlayback); - Player.MidiEventsPlayed.On(OnMidiEventsPlayed); - Player.PlaybackRangeChanged.On(OnPlaybackRangeChanged); - - DispatchOnUiThread(OnReady); - } - - public bool IsReady => Player?.IsReady ?? false; - public bool IsReadyForPlayback => Player?.IsReadyForPlayback ?? false; - - public PlayerState State => Player?.State ?? PlayerState.Paused; - - public LogLevel LogLevel - { - get => _logLevel; - set - { - _logLevel = value; - DispatchOnWorkerThread(() => { Player.LogLevel = value; }); - } - } - - public double MasterVolume - { - get => Player.MasterVolume; - set => DispatchOnWorkerThread(() => { Player.MasterVolume = value; }); - } - - public double CountInVolume - { - get => Player.CountInVolume; - set => DispatchOnWorkerThread(() => { Player.CountInVolume = value; }); - } - - public IList MidiEventsPlayedFilter - { - get => Player.MidiEventsPlayedFilter; - set => DispatchOnWorkerThread(() => { Player.MidiEventsPlayedFilter = value; }); - } - - public double MetronomeVolume - { - get => Player.MetronomeVolume; - set => DispatchOnWorkerThread(() => { Player.MetronomeVolume = value; }); - } - - public double PlaybackSpeed - { - get => Player.PlaybackSpeed; - set => DispatchOnWorkerThread(() => { Player.PlaybackSpeed = value; }); - } - - public double TickPosition - { - get => Player.TickPosition; - set => DispatchOnWorkerThread(() => { Player.TickPosition = value; }); - } - - public double TimePosition - { - get => Player.TimePosition; - set => DispatchOnWorkerThread(() => { Player.TimePosition = value; }); - } - - public PlaybackRange? PlaybackRange - { - get => Player.PlaybackRange; - set => DispatchOnWorkerThread(() => { Player.PlaybackRange = value; }); - } - - public bool IsLooping - { - get => Player.IsLooping; - set => DispatchOnWorkerThread(() => { Player.IsLooping = value; }); - } - - public PositionChangedEventArgs? LoadedMidiInfo => Player.LoadedMidiInfo; - public PositionChangedEventArgs CurrentPosition => Player.CurrentPosition; - - public bool Play() - { - if (State == PlayerState.Playing || !IsReadyForPlayback) - { - return false; - } - - DispatchOnWorkerThread(() => { Player.Play(); }); - return true; - } - - public void Pause() - { - DispatchOnWorkerThread(() => { Player.Pause(); }); - } - - public void PlayOneTimeMidiFile(MidiFile midiFile) - { - DispatchOnWorkerThread(() => { Player.PlayOneTimeMidiFile(midiFile); }); - } - - public void PlayPause() - { - DispatchOnWorkerThread(() => { Player.PlayPause(); }); - } - - public void Stop() - { - DispatchOnWorkerThread(() => { Player.Stop(); }); - } - - public void ResetSoundFonts() - { - DispatchOnWorkerThread(() => { Player.ResetSoundFonts(); }); - } - - public void LoadSoundFont(Uint8Array data, bool append) - { - DispatchOnWorkerThread(() => { Player.LoadSoundFont(data, append); }); - } - - public void LoadMidiFile(MidiFile midi) - { - DispatchOnWorkerThread(() => { Player.LoadMidiFile(midi); }); - } - - public void ApplyTranspositionPitches(IValueTypeMap transpositionPitches) - { - DispatchOnWorkerThread(() => { Player.ApplyTranspositionPitches(transpositionPitches); }); - } - - public void SetChannelMute(double channel, bool mute) - { - DispatchOnWorkerThread(() => { Player.SetChannelMute(channel, mute); }); - } - - public void ResetChannelStates() - { - DispatchOnWorkerThread(() => { Player.ResetChannelStates(); }); - } - - public void SetChannelSolo(double channel, bool solo) - { - DispatchOnWorkerThread(() => { Player.SetChannelSolo(channel, solo); }); - } - - public void SetChannelVolume(double channel, double volume) - { - DispatchOnWorkerThread(() => { Player.SetChannelVolume(channel, volume); }); - } - - public void SetChannelTranspositionPitch(double channel, double semitones) - { - DispatchOnWorkerThread(() => { Player.SetChannelTranspositionPitch(channel, semitones); }); - } - - public void LoadBackingTrack(Score score) - { - DispatchOnWorkerThread(() => { Player.LoadBackingTrack(score); }); - } - - public void UpdateSyncPoints( IList syncPoints) - { - DispatchOnWorkerThread(() => { Player.UpdateSyncPoints(syncPoints); }); - } - - public IEventEmitter Ready { get; } = new EventEmitter(); - public IEventEmitter ReadyForPlayback { get; } = new EventEmitter(); - public IEventEmitter Finished { get; } = new EventEmitter(); - public IEventEmitter SoundFontLoaded { get; } = new EventEmitter(); - public IEventEmitterOfT SoundFontLoadFailed { get; } = new EventEmitterOfT(); - public IEventEmitterOfT MidiLoad { get; } = new EventEmitterOfT(); - public IEventEmitterOfT MidiLoaded { get; } = new EventEmitterOfT(); - public IEventEmitterOfT MidiLoadFailed { get; } = new EventEmitterOfT(); - public IEventEmitterOfT StateChanged { get; } = new EventEmitterOfT(); - public IEventEmitterOfT PositionChanged { get; } = new EventEmitterOfT(); - public IEventEmitterOfT MidiEventsPlayed { get; } = new EventEmitterOfT(); - public IEventEmitterOfT PlaybackRangeChanged { get; } = new EventEmitterOfT(); - - protected virtual void OnReady() - { - DispatchOnUiThread(() => ((EventEmitter)Ready).Trigger()); - } - - protected virtual void OnReadyForPlayback() - { - DispatchOnUiThread(() => ((EventEmitter)ReadyForPlayback).Trigger()); - } - - protected virtual void OnFinished() - { - DispatchOnUiThread(() => ((EventEmitter)Finished).Trigger()); - } - - protected virtual void OnSoundFontLoaded() - { - DispatchOnUiThread(() => ((EventEmitter)SoundFontLoaded).Trigger()); - } - - protected virtual void OnSoundFontLoadFailed(Error e) - { - DispatchOnUiThread(() => ((EventEmitterOfT)SoundFontLoadFailed).Trigger(e)); - } - - protected virtual void OnMidiLoaded(PositionChangedEventArgs args) - { - DispatchOnUiThread(() => ((EventEmitterOfT)MidiLoaded).Trigger(args)); - } - - protected virtual void OnMidiLoadFailed(Error e) - { - DispatchOnUiThread(() => ((EventEmitterOfT)MidiLoadFailed).Trigger(e)); - } - - protected virtual void OnMidiEventsPlayed(MidiEventsPlayedEventArgs e) - { - DispatchOnUiThread(() => ((EventEmitterOfT)MidiEventsPlayed).Trigger(e)); - } - - protected virtual void OnStateChanged(PlayerStateChangedEventArgs obj) - { - DispatchOnUiThread(() => ((EventEmitterOfT)StateChanged).Trigger(obj)); - } - - protected virtual void OnPositionChanged(PositionChangedEventArgs obj) - { - DispatchOnUiThread(() => ((EventEmitterOfT)PositionChanged).Trigger(obj)); - } - - protected virtual void OnPlaybackRangeChanged(PlaybackRangeChangedEventArgs obj) - { - DispatchOnUiThread(() => ((EventEmitterOfT)PlaybackRangeChanged).Trigger(obj)); - } -} diff --git a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthAudioExporter.cs b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthAudioExporter.cs deleted file mode 100644 index 984478225..000000000 --- a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthAudioExporter.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using AlphaTab.Collections; -using AlphaTab.Synth; - -namespace AlphaTab.Platform.CSharp; - -internal class ManagedThreadAlphaSynthAudioExporter : IAudioExporterWorker -{ - private readonly ManagedThreadAlphaSynthWorkerApi _worker; - private readonly bool _ownsWorker; - private TaskCompletionSource? _promise; - private IAlphaSynthAudioExporter? _exporter; - - public ManagedThreadAlphaSynthAudioExporter(ManagedThreadAlphaSynthWorkerApi synthWorker, - bool ownsWorker) - { - _worker = synthWorker; - _ownsWorker = ownsWorker; - } - - private async Task DispatchAsyncOnWorkerThread(Action action) - { - if (_promise != null) - { - throw new AlphaTabError( - AlphaTabErrorType.General, - "There is already an ongoing operation, wait for previous operation to complete before proceeding" - ); - } - - _promise = new TaskCompletionSource(); - try - { - _worker.DispatchOnWorkerThread(() => - { - try - { - action(); - _promise.SetResult(1); - } - catch (Error e) - { - _promise.SetException(e); - } - }); - - await _promise.Task; - } - finally - { - _promise = null; - } - } - - public async Task Initialize( - AudioExportOptions options, - Midi.MidiFile midi, - IList syncPoints, - IValueTypeMap transpositionPitches) - { - await DispatchAsyncOnWorkerThread(() => - { - _exporter = _worker.Player.ExportAudio( - options, - midi, - syncPoints, - transpositionPitches - ); - }); - } - - public async Task Render(double milliseconds) - { - AudioExportChunk? chunk = null; - await DispatchAsyncOnWorkerThread(() => - { - if (_exporter == null) - { - throw new AlphaTabError(AlphaTabErrorType.General, - "Exporter was already destroyed"); - } - - chunk = _exporter.Render(milliseconds); - }); - return chunk; - } - - public void Destroy() - { - _exporter = null; - if (_ownsWorker) - { - _worker.Destroy(); - } - } - - public void Dispose() - { - Destroy(); - } -} diff --git a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthWorker.cs b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthWorker.cs new file mode 100644 index 000000000..8d0bddad7 --- /dev/null +++ b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthWorker.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using AlphaTab.Platform.Worker; + +namespace AlphaTab.Platform.CSharp; + +internal abstract class ManagedThreadWorkerBase : IAlphaTabWorker +{ + private readonly Action _postToMain; + private readonly BlockingCollection _workerQueue; + private readonly CancellationTokenSource _workerCancellationToken; + private readonly ManualResetEventSlim? _threadStartedEvent; + private readonly ConcurrentDictionary>,Action>> _listenerInsideWorker = new(); + private readonly ConcurrentDictionary>,Action>> _listenerOutsideWorker = new(); + + protected Thread WorkerThread { get; } + + protected ManagedThreadWorkerBase(Action postToMain) + { + _postToMain = postToMain; + _threadStartedEvent = new ManualResetEventSlim(false); + _workerQueue = new BlockingCollection(); + _workerCancellationToken = new CancellationTokenSource(); + + WorkerThread = new Thread(DoWork) + { + IsBackground = true + }; + WorkerThread.Start(); + + _threadStartedEvent.Wait(); + _threadStartedEvent.Dispose(); + _threadStartedEvent = null; + } + + protected abstract void OnStartInsideWorker(); + + private void DoWork() + { + _threadStartedEvent.Set(); + OnStartInsideWorker(); + while (_workerQueue.TryTake(out var action, Timeout.Infinite, + _workerCancellationToken.Token)) + { + if (_workerCancellationToken.IsCancellationRequested) + { + break; + } + + action(); + } + } + + + public void PostMessage(T message) + { + var ev = new MessageEvent(message); + if (Thread.CurrentThread.ManagedThreadId == WorkerThread.ManagedThreadId) + { + // Inside Worker -> Post to main + _postToMain(() => + { + foreach (var listener in _listenerOutsideWorker) + { + listener.Value(ev); + } + }); + } + else + { + // Outside Worker -> Post to worker + PostToWorker(() => + { + foreach (var listener in _listenerInsideWorker) + { + listener.Value(ev); + } + }); + } + } + + public void PostToWorker(Action action) + { + _workerQueue.Add(action); + } + + public void AddEventListener(string @event, Action> handler) + { + if (@event != "message") return; + var listeners = Thread.CurrentThread.ManagedThreadId == WorkerThread.ManagedThreadId + ? _listenerInsideWorker + : _listenerOutsideWorker; + listeners[handler] = handler; + } + + public void RemoveEventListener(string @event, Action> handler) + { + if (@event != "message") return; + var listeners = Thread.CurrentThread.ManagedThreadId == WorkerThread.ManagedThreadId + ? _listenerInsideWorker + : _listenerOutsideWorker; + listeners.TryRemove(handler, out _); + } + + public virtual void Terminate() + { + _workerCancellationToken.Cancel(); + WorkerThread.Join(); + while (_workerQueue.Count > 0) + { + _workerQueue.Take(); + } + } +} + +internal class ManagedThreadAlphaTabRendererWorker : + ManagedThreadWorkerBase, + IAlphaTabRenderingWorker, + IAlphaTabWorkerGlobalScope +{ + private static readonly ConcurrentDictionary + WorkerLookup = new(); + + public static ManagedThreadAlphaTabRendererWorker? CurrentThreadWorker => + WorkerLookup.TryGetValue(Thread.CurrentThread.ManagedThreadId, out var v) ? v : null; + + public ManagedThreadAlphaTabRendererWorker(Action postToMain) : base(postToMain) + { + } + + protected override void OnStartInsideWorker() + { + WorkerLookup[Thread.CurrentThread.ManagedThreadId] = this; + AlphaTabWebWorker.Init(); + } + + public override void Terminate() + { + base.Terminate(); + WorkerLookup.TryRemove(WorkerThread.ManagedThreadId, out _); + } +} + +internal class ManagedThreadAlphaSynthWorker : + ManagedThreadWorkerBase, + IAlphaSynthWorker, + IAlphaTabWorkerGlobalScope +{ + private static readonly ConcurrentDictionary + WorkerLookup = new(); + + public static ManagedThreadAlphaSynthWorker? CurrentThreadWorker => + WorkerLookup.TryGetValue(Thread.CurrentThread.ManagedThreadId, out var v) ? v : null; + + public ManagedThreadAlphaSynthWorker(Action postToMain) : base(postToMain) + { + } + + protected override void OnStartInsideWorker() + { + WorkerLookup[Thread.CurrentThread.ManagedThreadId] = this; + AlphaSynthWebWorker.Init(); + } + + public override void Terminate() + { + base.Terminate(); + WorkerLookup.TryRemove(WorkerThread.ManagedThreadId, out _); + } +} diff --git a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthWorkerApi.cs b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthWorkerApi.cs deleted file mode 100644 index d1a2ea355..000000000 --- a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthWorkerApi.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using AlphaTab.Synth; - -namespace AlphaTab.Platform.CSharp; - -internal class ManagedThreadAlphaSynthWorkerApi : AlphaSynthWorkerApiBase -{ - private readonly Action _uiInvoke; - private readonly Thread _workerThread; - private readonly BlockingCollection _workerQueue; - private readonly CancellationTokenSource _workerCancellationToken; - private readonly ManualResetEventSlim? _threadStartedEvent; - - public ManagedThreadAlphaSynthWorkerApi(ISynthOutput output, LogLevel logLevel, Action uiInvoke, double bufferTimeInMilliseconds) - : base(output, logLevel, bufferTimeInMilliseconds) - { - _uiInvoke = uiInvoke; - - _threadStartedEvent = new ManualResetEventSlim(false); - _workerQueue = new BlockingCollection(); - _workerCancellationToken = new CancellationTokenSource(); - - _workerThread = new Thread(DoWork) - { - IsBackground = true - }; - _workerThread.Start(); - - _threadStartedEvent.Wait(); - _workerQueue.Add(Initialize); - _threadStartedEvent.Dispose(); - _threadStartedEvent = null; - } - - public override void Destroy() - { - _workerCancellationToken.Cancel(); - _workerThread.Join(); - } - - protected override void DispatchOnUiThread(Action action) - { - _uiInvoke(action); - } - - private bool CheckAccess() - { - return Thread.CurrentThread == _workerThread; - } - - protected internal override void DispatchOnWorkerThread(Action action) - { - if (CheckAccess()) - { - action(); - } - else - { - _workerQueue.Add(action); - } - } - - private void DoWork() - { - _threadStartedEvent.Set(); - while (_workerQueue.TryTake(out var action, Timeout.Infinite, _workerCancellationToken.Token)) - { - if (_workerCancellationToken.IsCancellationRequested) - { - break; - } - - action(); - } - } -} diff --git a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs deleted file mode 100644 index 0ba2a22eb..000000000 --- a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using AlphaTab.Model; -using AlphaTab.Rendering; -using AlphaTab.Rendering.Utils; - -namespace AlphaTab.Platform.CSharp; - -internal class ManagedThreadScoreRenderer : IScoreRenderer -{ - private readonly Action _uiInvoke; - - private readonly Thread _workerThread; - private readonly BlockingCollection _workerQueue; - private readonly ManualResetEventSlim? _threadStartedEvent; - private readonly CancellationTokenSource _workerCancellationToken; - private ScoreRenderer _renderer; - private double _width; - - public BoundsLookup? BoundsLookup { get; private set; } - - public ManagedThreadScoreRenderer(Settings settings, Action uiInvoke) - { - _uiInvoke = uiInvoke; - _renderer = null!; - _threadStartedEvent = new ManualResetEventSlim(false); - _workerQueue = new BlockingCollection(); - _workerCancellationToken = new CancellationTokenSource(); - - _workerThread = new Thread(DoWork) - { - IsBackground = true - }; - _workerThread.Start(); - - _threadStartedEvent.Wait(); - - _workerQueue.Add(() => Initialize(settings)); - _threadStartedEvent.Dispose(); - _threadStartedEvent = null; - } - - - private void DoWork() - { - _threadStartedEvent.Set(); - while (_workerQueue.TryTake(out var action, Timeout.Infinite, - _workerCancellationToken.Token)) - { - if (_workerCancellationToken.IsCancellationRequested) - { - break; - } - - action(); - } - } - - private void Initialize(Settings settings) - { - _renderer = new ScoreRenderer(settings); - _renderer.PartialRenderFinished.On(result => - _uiInvoke(() => OnPartialRenderFinished(result))); - _renderer.PartialLayoutFinished.On(result => - _uiInvoke(() => OnPartialLayoutFinished(result))); - _renderer.RenderFinished.On(result => _uiInvoke(() => OnRenderFinished(result))); - _renderer.PostRenderFinished.On(() => - _uiInvoke(() => OnPostFinished(_renderer.BoundsLookup))); - _renderer.PreRender.On(resize => _uiInvoke(() => OnPreRender(resize))); - _renderer.Error.On(e => _uiInvoke(() => OnError(e))); - } - - private void OnPostFinished(BoundsLookup boundsLookup) - { - BoundsLookup = boundsLookup; - OnPostRenderFinished(); - } - - public void Destroy() - { - _workerCancellationToken.Cancel(); - _workerThread.Join(); - } - - public void UpdateSettings(Settings settings) - { - if (CheckAccess()) - { - _renderer.UpdateSettings(settings); - } - else - { - _workerQueue.Add(() => UpdateSettings(settings)); - } - } - - private bool CheckAccess() - { - return Thread.CurrentThread == _workerThread; - } - - public void Render(RenderHints? renderHints = null) - { - if (CheckAccess()) - { - _renderer.Render(renderHints); - } - else - { - _workerQueue.Add(() => Render(renderHints)); - } - } - - public void RenderResult(string resultId) - { - if (CheckAccess()) - { - _renderer.RenderResult(resultId); - } - else - { - _workerQueue.Add(() => RenderResult(resultId)); - } - } - - public double Width - { - get => _width; - set - { - _width = value; - if (CheckAccess()) - { - _renderer.Width = value; - } - else - { - _workerQueue.Add(() => _renderer.Width = value); - } - } - } - - public void ResizeRender() - { - if (CheckAccess()) - { - _renderer.ResizeRender(); - } - else - { - _workerQueue.Add(ResizeRender); - } - } - - public void RenderScore(Score? score, IList? trackIndexes, RenderHints? renderHints = null) - { - if (CheckAccess()) - { - _renderer.RenderScore(score, trackIndexes, renderHints); - } - else - { - _workerQueue.Add(() => - RenderScore(score, - trackIndexes, renderHints)); - } - } - - public IEventEmitterOfT PreRender { get; } = new EventEmitterOfT(); - - protected virtual void OnPreRender(bool isResize) - { - ((EventEmitterOfT)PreRender).Trigger(isResize); - } - - public IEventEmitterOfT PartialRenderFinished { get; } = - new EventEmitterOfT(); - - protected virtual void OnPartialRenderFinished(RenderFinishedEventArgs obj) - { - ((EventEmitterOfT)PartialRenderFinished).Trigger(obj); - } - - public IEventEmitterOfT PartialLayoutFinished { get; } = - new EventEmitterOfT(); - - protected virtual void OnPartialLayoutFinished(RenderFinishedEventArgs obj) - { - ((EventEmitterOfT)PartialLayoutFinished).Trigger(obj); - } - - public IEventEmitterOfT RenderFinished { get; } = - new EventEmitterOfT(); - - protected virtual void OnRenderFinished(RenderFinishedEventArgs obj) - { - ((EventEmitterOfT)RenderFinished).Trigger(obj); - } - - public IEventEmitterOfT Error { get; } = new EventEmitterOfT(); - - protected virtual void OnError(Error details) - { - ((EventEmitterOfT)Error).Trigger(details); - } - - public IEventEmitter PostRenderFinished { get; } = new EventEmitter(); - - protected virtual void OnPostRenderFinished() - { - ((EventEmitter)PostRenderFinished).Trigger(); - } -} diff --git a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedUiFacade.cs b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedUiFacade.cs index 527ee4638..2f5627ecf 100644 --- a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedUiFacade.cs +++ b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedUiFacade.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Concurrent; using System.IO; +using System.Threading; +using System.Threading.Tasks; using AlphaTab.Synth; using AlphaTab.Importer; using AlphaTab.Model; +using AlphaTab.Platform.Worker; using AlphaTab.Rendering; using AlphaTab.Rendering.Utils; @@ -34,41 +37,46 @@ public virtual void Initialize(AlphaTabApiBase api, TSettings setting } public abstract void StopScrolling(IContainer scrollElement); + public abstract void SetCanvasOverflow(IContainer canvasElement, double overflow, bool isVertical); public IScoreRenderer CreateWorkerRenderer() { - return new ManagedThreadScoreRenderer(Api.Settings, BeginInvoke); + var worker = new ManagedThreadAlphaTabRendererWorker(PostToUIThread); + return new AlphaTabWorkerScoreRenderer(Api, worker); } + protected abstract void PostToUIThread(Action action); + protected abstract Stream? OpenDefaultSoundFont(); public IAlphaSynth CreateWorkerPlayer() { - var player = new ManagedThreadAlphaSynthWorkerApi(CreateSynthOutput(), - Api.Settings.Core.LogLevel, BeginInvoke, Api.Settings.Player.BufferTimeInMilliseconds); + var player = new AlphaSynthWebWorkerApi( + CreateSynthOutput(), + Api.Settings, + new ManagedThreadAlphaSynthWorker(PostToUIThread) + ); player.Ready.On(() => { - using (var sf = OpenDefaultSoundFont()) - using (var ms = new MemoryStream()) - { - sf.CopyTo(ms); - player.LoadSoundFont(new Uint8Array(ms.ToArray()), false); - } + using var sf = OpenDefaultSoundFont(); + using var ms = new MemoryStream(); + sf.CopyTo(ms); + player.LoadSoundFont(new Uint8Array(ms.ToArray()), false); }); return player; } public IAudioExporterWorker CreateWorkerAudioExporter(IAlphaSynth? synth) { - var needNewWorker = synth == null || synth is not ManagedThreadAlphaSynthWorkerApi; + var needNewWorker = synth is not AlphaSynthWebWorkerApi; if (needNewWorker) { synth = CreateWorkerPlayer(); } - return new ManagedThreadAlphaSynthAudioExporter((ManagedThreadAlphaSynthWorkerApi)synth, needNewWorker); + return new AlphaSynthAudioExporterWorkerApi(synth as AlphaSynthWebWorkerApi, needNewWorker); } public abstract IAlphaSynth? CreateBackingTrackPlayer(); @@ -86,8 +94,7 @@ public abstract void TriggerEvent(IContainer container, string eventName, public virtual void InitialRender() { - Api.Renderer.PreRender.On(resize => { TotalResultCount.Enqueue(new Counter()); }); - + Api.Renderer.PreRender.On(_ => { TotalResultCount.Enqueue(new Counter()); }); RootContainerBecameVisible.On(() => { @@ -106,7 +113,45 @@ public virtual void InitialRender() public abstract void BeginUpdateRenderResults(RenderFinishedEventArgs? renderResults); public abstract void DestroyCursors(); public abstract Cursors? CreateCursors(); - public abstract void BeginInvoke(Action action); + + public void BeginInvoke(Action action) + { + // post to "own" event loop if running inside worker + var synthWorker = ManagedThreadAlphaSynthWorker.CurrentThreadWorker; + if (synthWorker != null) + { + synthWorker.PostToWorker(action); + return; + } + + var renderWorker = ManagedThreadAlphaTabRendererWorker.CurrentThreadWorker; + if (renderWorker != null) + { + renderWorker.PostToWorker(action); + return; + } + + // not in worker -> run on main + PostToUIThread(action); + } + + public Action Throttle(Action action, double delay) + { + CancellationTokenSource? cancellationTokenSource = null; + return () => + { + cancellationTokenSource?.Cancel(); + cancellationTokenSource = new CancellationTokenSource(); + + Task.Run(async () => + { + await Task.Delay((int)delay, cancellationTokenSource.Token); + PostToUIThread(action); + }, + cancellationTokenSource.Token); + }; + } + public abstract void RemoveHighlights(); public abstract void HighlightElements(string groupId, double masterBarIndex); public abstract IContainer? CreateSelectionElement(); @@ -126,16 +171,14 @@ public bool Load(object? data, Action success, Action error) success(ScoreLoader.LoadScoreFromBytes(new Uint8Array(b), Api.Settings)); return true; case Stream s: - { - using (var ms = new MemoryStream()) - { - s.CopyTo(ms); - success(ScoreLoader.LoadScoreFromBytes(new Uint8Array(ms.ToArray()), - Api.Settings)); - } - - return true; - } + { + using var ms = new MemoryStream(); + s.CopyTo(ms); + success(ScoreLoader.LoadScoreFromBytes(new Uint8Array(ms.ToArray()), + Api.Settings)); + + return true; + } default: return false; } @@ -149,15 +192,13 @@ public bool LoadSoundFont(object? data, bool append) Api.Player.LoadSoundFont(new Uint8Array(bytes), append); return true; case Stream stream: - { - using (var ms = new MemoryStream()) - { - stream.CopyTo(ms); - Api.Player.LoadSoundFont(new Uint8Array(ms.ToArray()), append); - } - - return true; - } + { + using var ms = new MemoryStream(); + stream.CopyTo(ms); + Api.Player.LoadSoundFont(new Uint8Array(ms.ToArray()), append); + + return true; + } default: return false; } diff --git a/packages/csharp/src/Directory.Build.props b/packages/csharp/src/Directory.Build.props index 66883954d..f8ef9003c 100644 --- a/packages/csharp/src/Directory.Build.props +++ b/packages/csharp/src/Directory.Build.props @@ -2,8 +2,8 @@ portable true - 1.8.1 - 1.8.1.0 + 1.9.0 + 1.9.0.0 $(AssemblyVersion) Danielku15 CoderLine diff --git a/packages/kotlin/package.json b/packages/kotlin/package.json index 5cdc5a1e8..073763e4e 100644 --- a/packages/kotlin/package.json +++ b/packages/kotlin/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-kotlin", - "version": "1.8.1", + "version": "1.9.0", "description": "The Kotlin target of alphaTab.", "private": true, "type": "module", diff --git a/packages/kotlin/src/android/build.gradle.kts b/packages/kotlin/src/android/build.gradle.kts index 171364f61..174095eda 100644 --- a/packages/kotlin/src/android/build.gradle.kts +++ b/packages/kotlin/src/android/build.gradle.kts @@ -39,7 +39,7 @@ var libAuthorId = "danielku15" var libAuthorName = "Daniel Kuschny" var libOrgUrl = "https://github.com/coderline" var libCompany = "CoderLine" -var libVersion = "1.8.1-SNAPSHOT" +var libVersion = "1.9.0-SNAPSHOT" var libProjectUrl = "https://github.com/CoderLine/alphaTab" var libGitUrlHttp = "https://github.com/CoderLine/alphaTab.git" var libGitUrlGit = "scm:git:git://github.com/CoderLine/alphaTab.git" diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/AlphaTabView.kt b/packages/kotlin/src/android/src/main/java/alphaTab/AlphaTabView.kt index 373fbfbd2..fbdc18c4a 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/AlphaTabView.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/AlphaTabView.kt @@ -78,8 +78,8 @@ class AlphaTabView : RelativeLayout { } override fun onDetachedFromWindow() { - super.onDetachedFromWindow() _api.destroy() + super.onDetachedFromWindow() } private fun init(context: Context) { diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt b/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt index d3f534bcc..d2b5ee8f6 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt @@ -4,14 +4,16 @@ import alphaTab.collections.DoubleList import alphaTab.platform.Json import alphaTab.platform.android.AndroidCanvas import alphaTab.platform.android.AndroidEnvironment +import alphaTab.platform.android.JavaThreadAlphaSynthWorker +import alphaTab.platform.android.JavaThreadAlphaTabRendererWorker +import alphaTab.platform.worker.IAlphaSynthWorkerMessage +import alphaTab.platform.worker.IAlphaTabWorkerGlobalScope +import alphaTab.platform.worker.IAlphaTabWorkerMessage +import alphaTab.synth.AudioExportOptions import android.os.Build -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlin.contracts.ExperimentalContracts + @ExperimentalContracts @ExperimentalUnsignedTypes internal class EnvironmentPartials { @@ -27,9 +29,6 @@ internal class EnvironmentPartials { ) } - internal fun platformInit() { - } - internal fun _printPlatformInfo(print: (message: String) -> Unit) { print("OS Name: ${System.getProperty("os.name")}"); print("OS Version: ${System.getProperty("os.version")}"); @@ -39,24 +38,21 @@ internal class EnvironmentPartials { print("Screen Size: ${AndroidEnvironment.screenWidth}x${AndroidEnvironment.screenHeight}"); } - private val throttleScope = CoroutineScope(Dispatchers.Default) - internal fun throttle(toThrottle: () -> Unit, delay: Double): () -> Unit { - var job: Job? = null - return { - job?.cancel() - job = throttleScope.launch { - delay(delay.toLong()) - toThrottle() - } - } - } - @Suppress("NOTHING_TO_INLINE") internal inline fun quoteJsonString(string: String) = Json.quoteJsonString(string) @Suppress("NOTHING_TO_INLINE") internal inline fun sortDescending(list: DoubleList) = list.sortDescending() + internal inline fun getGlobalWorkerScope(): IAlphaTabWorkerGlobalScope { + @Suppress("UNCHECKED_CAST") + return when (T::class) { + IAlphaSynthWorkerMessage::class -> JavaThreadAlphaSynthWorker.currentThreadWorker as IAlphaTabWorkerGlobalScope + IAlphaTabWorkerMessage::class -> JavaThreadAlphaTabRendererWorker.currentThreadWorker as IAlphaTabWorkerGlobalScope + else -> throw UnsupportedOperationException("Unsupported worker scope kind ${T::class::qualifiedName}") + } + } + inline fun prepareForPostMessage(v:T) = v } } diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/MessageEvent.kt b/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/MessageEvent.kt new file mode 100644 index 000000000..acf26ace0 --- /dev/null +++ b/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/MessageEvent.kt @@ -0,0 +1,3 @@ +package alphaTab.core.ecmaScript + +internal class MessageEvent(val data: T) diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/Promise.kt b/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/Promise.kt index dc4619e93..21a1e582f 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/Promise.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/Promise.kt @@ -1,5 +1,7 @@ package alphaTab.core.ecmaScript +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.cancelChildren @@ -20,5 +22,13 @@ internal class Promise { } } } + + fun withResolvers(): PromiseWithResolvers { + return PromiseWithResolvers(); + } + + fun resolve(value: T): Deferred { + return CompletableDeferred(value) + } } } diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/PromiseWithResolvers.kt b/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/PromiseWithResolvers.kt new file mode 100644 index 000000000..5de2b4765 --- /dev/null +++ b/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/PromiseWithResolvers.kt @@ -0,0 +1,19 @@ +package alphaTab.core.ecmaScript + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred + +class PromiseWithResolvers { + private val _deferred = CompletableDeferred() + + public val promise: Deferred + get() = _deferred + + fun resolve(v: T) { + _deferred.complete(v) + } + + fun reject(e: Error) { + _deferred.completeExceptionally(e) + } +} diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt index 3a5c8abb8..26af6404b 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt @@ -56,23 +56,28 @@ internal class AndroidAudioWorker( private fun writeSamples() { while (!_stopped) { - if (_track.playState == AudioTrack.PLAYSTATE_PLAYING) { - val samplesFromBuffer = _output.read(_buffer, 0, _buffer.size) - if (_previousPosition == -1) { - _previousPosition = _track.playbackHeadPosition - _track.getTimestamp(_timestamp) + try { + if (_track.playState == AudioTrack.PLAYSTATE_PLAYING) { + val samplesFromBuffer = _output.read(_buffer, 0, _buffer.size) + if (_previousPosition == -1) { + _previousPosition = _track.playbackHeadPosition + _track.getTimestamp(_timestamp) + } + _track.write(_buffer, 0, samplesFromBuffer, AudioTrack.WRITE_BLOCKING) + } else { + _playingSemaphore.acquire() // wait for playing to start + _playingSemaphore.release() // release semaphore for others } - _track.write(_buffer, 0, samplesFromBuffer, AudioTrack.WRITE_BLOCKING) - } else { - _playingSemaphore.acquire() // wait for playing to start - _playingSemaphore.release() // release semaphore for others + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + break; } } } fun close() { - _playingSemaphore.release() // proceed thread _stopped = true + _playingSemaphore.release() // proceed thread _track.stop() _writeThread!!.interrupt() _writeThread!!.join() diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt index fb18f5d30..0bd11f42e 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt @@ -19,8 +19,7 @@ import kotlin.math.min @ExperimentalUnsignedTypes @ExperimentalContracts internal class AndroidSynthOutput( - private val context: Context, - private val synthInvoke: (action: (() -> Unit)) -> Unit + private val context: Context ) : ISynthOutput { companion object { private const val BufferSize = 4096 @@ -56,9 +55,7 @@ internal class AndroidSynthOutput( } private fun onReady() { - synthInvoke { - (ready as EventEmitter).trigger() - } + (ready as EventEmitter).trigger() } override fun destroy() { @@ -103,15 +100,11 @@ internal class AndroidSynthOutput( } private fun onSampleRequest() { - synthInvoke { - (sampleRequest as EventEmitter).trigger() - } + (sampleRequest as EventEmitter).trigger() } internal fun onSamplesPlayed(samples: Int) { - synthInvoke { - (samplesPlayed as EventEmitterOfT).trigger(samples.toDouble()) - } + (samplesPlayed as EventEmitterOfT).trigger(samples.toDouble()) } fun read(buffer: FloatArray, offset: Int, sampleCount: Int): Int { diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthAudioExporter.kt b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthAudioExporter.kt deleted file mode 100644 index 631234e97..000000000 --- a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthAudioExporter.kt +++ /dev/null @@ -1,92 +0,0 @@ -package alphaTab.platform.android - -import alphaTab.AlphaTabError -import alphaTab.AlphaTabErrorType -import alphaTab.collections.DoubleDoubleMap -import alphaTab.collections.List -import alphaTab.midi.MidiFile -import alphaTab.synth.IAlphaSynthAudioExporter -import alphaTab.synth.AudioExportChunk -import alphaTab.synth.AudioExportOptions -import alphaTab.synth.BackingTrackSyncPoint -import alphaTab.synth.IAudioExporterWorker -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import kotlin.contracts.ExperimentalContracts - -@ExperimentalContracts -@ExperimentalUnsignedTypes -internal class AndroidThreadAlphaSynthAudioExporter( - private val worker: AndroidThreadAlphaSynthWorkerPlayer, - private val ownsWorker: Boolean -) : IAudioExporterWorker { - - private var _exporter: IAlphaSynthAudioExporter? = null - private var _deferred: Deferred<*>? = null - - override fun initialize( - options: AudioExportOptions, - midi: MidiFile, - syncPoints: List, - transpositionPitches: DoubleDoubleMap - ): Deferred { - return dispatchAsyncOnWorkerThread { - val player = worker.player - ?: throw AlphaTabError( - AlphaTabErrorType.General, - "The player was destroyed prematurely" - ) - _exporter = player.exportAudio( - options, - midi, - syncPoints, - transpositionPitches - ) - } - } - - override fun render(milliseconds: Double): Deferred { - return dispatchAsyncOnWorkerThread { - val exporter = _exporter - ?: throw AlphaTabError( - AlphaTabErrorType.General, - "The exporter was destroyed prematurely" - ) - exporter.render(milliseconds) - } - } - - override fun destroy() { - _exporter = null - if (ownsWorker) { - worker.destroy() - } - } - - override fun close() { - destroy() - } - - private fun dispatchAsyncOnWorkerThread(action: () -> T): Deferred { - if (_deferred != null) { - throw AlphaTabError( - AlphaTabErrorType.General, - "There is already an ongoing operation, wait for previous operation to complete before proceeding" - ); - } - - val deferred = CompletableDeferred() - _deferred = deferred - worker.addToWorker { - try { - deferred.complete(action()) - } catch (e: Throwable) { - deferred.completeExceptionally(e) - } finally { - _deferred = null - } - } - return deferred - } - -} diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt deleted file mode 100644 index a31d033dc..000000000 --- a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt +++ /dev/null @@ -1,344 +0,0 @@ -package alphaTab.platform.android - -import alphaTab.* -import alphaTab.EventEmitter -import alphaTab.collections.List -import alphaTab.core.ecmaScript.Error -import alphaTab.core.ecmaScript.Uint8Array -import alphaTab.midi.MidiEventType -import alphaTab.midi.MidiFile -import alphaTab.model.Score -import alphaTab.synth.* -import android.util.Log -import java.util.concurrent.BlockingQueue -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.Semaphore -import java.util.concurrent.TimeUnit -import kotlin.contracts.ExperimentalContracts - -@ExperimentalUnsignedTypes -@ExperimentalContracts -internal class AndroidThreadAlphaSynthWorkerPlayer : IAlphaSynth, Runnable { - private val _uiInvoke: (action: (() -> Unit)) -> Unit - - private val _workerThread: Thread - private val _workerQueue: BlockingQueue<() -> Unit> - private val _threadStartedEvent: Semaphore - private var _isCancelled = false - - private var _player: AlphaSynth? = null - private val _output: ISynthOutput - private var _logLevel: LogLevel - private var _bufferTimeInMilliseconds: Double - - val player: AlphaSynth? - get() = _player - - override val output: ISynthOutput - get() = _output - - constructor( - logLevel: LogLevel, - output: ISynthOutput, - uiInvoke: (action: (() -> Unit)) -> Unit, - bufferTimeInMilliseconds: Double - ) { - _logLevel = logLevel - _bufferTimeInMilliseconds = bufferTimeInMilliseconds - _output = output - _uiInvoke = uiInvoke - _threadStartedEvent = Semaphore(1) - _threadStartedEvent.acquire() - _workerQueue = LinkedBlockingQueue() - - _workerThread = Thread(this) - _workerThread.name = "alphaSynthWorkerThread" - _workerThread.isDaemon = true - _workerThread.start() - - _threadStartedEvent.acquire() - - _workerQueue.add { initialize() } - } - - public fun addToWorker(action: () -> Unit) { - _workerQueue.add(action) - } - - override fun destroy() { - _isCancelled = true - _workerThread.interrupt() - _workerThread.join() - } - - override fun run() { - _threadStartedEvent.release() - try { - Log.d("AlphaTab", "AlphaSynth worker started") - do { - val item = _workerQueue.poll(500, TimeUnit.MILLISECONDS) - if (!_isCancelled && item != null) { - item() - } - } while (!_isCancelled) - } catch (e: InterruptedException) { - Log.d("AlphaTab", "AlphaSynth worker stopped") - // finished - } - } - - private fun initialize() { - val player = AlphaSynth(_output, _bufferTimeInMilliseconds) - _player = player - player.positionChanged.on { - _uiInvoke { onPositionChanged(it) } - } - player.stateChanged.on { - _uiInvoke { onStateChanged(it) } - } - player.finished.on { - _uiInvoke { onFinished() } - } - player.soundFontLoaded.on { - _uiInvoke { onSoundFontLoaded() } - } - player.soundFontLoadFailed.on { - _uiInvoke { onSoundFontLoadFailed(it) } - } - player.midiLoaded.on { - _uiInvoke { onMidiLoaded(it) } - } - player.midiLoadFailed.on { - _uiInvoke { onMidiLoadFailed(it) } - } - player.readyForPlayback.on { - _uiInvoke { onReadyForPlayback() } - } - player.midiEventsPlayed.on { - _uiInvoke { onMidiEventsPlayed(it) } - } - player.playbackRangeChanged.on { - _uiInvoke { onPlaybackRangeChanged(it) } - } - - _uiInvoke { onReady() } - } - - override val isReady: Boolean - get() = _player?.isReady ?: false - - override val isReadyForPlayback: Boolean - get() = _player?.isReadyForPlayback ?: false - - override val state: PlayerState - get() = _player?.state ?: PlayerState.Paused - - override var logLevel: LogLevel - get() = _logLevel - set(value) { - _logLevel = value - _workerQueue.add { _player?.logLevel = value } - } - - override var masterVolume: Double - get() = _player?.masterVolume ?: 0.0 - set(value) { - _workerQueue.add { _player?.masterVolume = value } - } - - override var countInVolume: Double - get() = _player?.countInVolume ?: 0.0 - set(value) { - _workerQueue.add { _player?.countInVolume = value } - } - - override var midiEventsPlayedFilter: List - get() = _player?.midiEventsPlayedFilter ?: List() - set(value) { - _workerQueue.add { _player?.midiEventsPlayedFilter = value } - } - - override var metronomeVolume: Double - get() = _player?.metronomeVolume ?: 0.0 - set(value) { - _workerQueue.add { _player?.metronomeVolume = value } - } - - override var playbackSpeed: Double - get() = _player?.playbackSpeed ?: 0.0 - set(value) { - _workerQueue.add { _player?.playbackSpeed = value } - } - - override var tickPosition: Double - get() = _player?.tickPosition ?: 0.0 - set(value) { - _workerQueue.add { _player?.tickPosition = value } - } - - override val currentPosition: PositionChangedEventArgs - get() = _player?.currentPosition ?: PositionChangedEventArgs( - 0.0, 0.0, 0.0, 0.0, false, 120.0, 120.0 - ) - - override val loadedMidiInfo: PositionChangedEventArgs? - get() = _player?.loadedMidiInfo - - override var timePosition: Double - get() = _player?.timePosition ?: 0.0 - set(value) { - _workerQueue.add { _player?.timePosition = value } - } - - - override var playbackRange: PlaybackRange? - get() = _player?.playbackRange - set(value) { - _workerQueue.add { _player?.playbackRange = value } - } - - - override var isLooping: Boolean - get() = _player?.isLooping ?: false - set(value) { - _workerQueue.add { _player?.isLooping = value } - } - - - override fun play(): Boolean { - if (state == PlayerState.Playing || !isReadyForPlayback) { - return false - } - - _workerQueue.add { _player?.play() } - return true - } - - override fun pause() { - _workerQueue.add { _player?.pause() } - } - - override fun playOneTimeMidiFile(midi: MidiFile) { - _workerQueue.add { _player?.playOneTimeMidiFile(midi) } - } - - override fun playPause() { - _workerQueue.add { _player?.playPause() } - } - - override fun stop() { - _workerQueue.add { _player?.stop() } - } - - override fun resetSoundFonts() { - _workerQueue.add { _player?.resetSoundFonts() } - } - - override fun loadSoundFont(data: Uint8Array, append: Boolean) { - _workerQueue.add { _player?.loadSoundFont(data, append) } - } - - override fun loadMidiFile(midi: MidiFile) { - _workerQueue.add { _player?.loadMidiFile(midi) } - } - - override fun applyTranspositionPitches(transpositionPitches: alphaTab.collections.DoubleDoubleMap) { - _workerQueue.add { _player?.applyTranspositionPitches(transpositionPitches) } - } - - override fun setChannelMute(channel: Double, mute: Boolean) { - _workerQueue.add { _player?.setChannelMute(channel, mute) } - } - - override fun resetChannelStates() { - _workerQueue.add { _player?.resetChannelStates() } - } - - override fun setChannelSolo(channel: Double, solo: Boolean) { - _workerQueue.add { _player?.setChannelSolo(channel, solo) } - } - - override fun setChannelVolume(channel: Double, volume: Double) { - _workerQueue.add { _player?.setChannelVolume(channel, volume) } - } - - override fun setChannelTranspositionPitch(channel: Double, semitones: Double) { - _workerQueue.add { _player?.setChannelTranspositionPitch(channel, semitones) } - } - - override fun loadBackingTrack(score: Score) { - _workerQueue.add { _player?.loadBackingTrack(score) } - } - - override fun updateSyncPoints(syncPoints: List) { - _workerQueue.add { _player?.updateSyncPoints(syncPoints) } - } - - override val ready: IEventEmitter = EventEmitter() - override val readyForPlayback: IEventEmitter = EventEmitter() - override val finished: IEventEmitter = EventEmitter() - override val soundFontLoaded: IEventEmitter = EventEmitter() - override val soundFontLoadFailed: IEventEmitterOfT = - EventEmitterOfT() - override val midiLoaded: IEventEmitterOfT = - EventEmitterOfT() - override val midiLoadFailed: IEventEmitterOfT = - EventEmitterOfT() - override val stateChanged: IEventEmitterOfT = - EventEmitterOfT() - override val positionChanged: IEventEmitterOfT = - EventEmitterOfT() - override val midiEventsPlayed: IEventEmitterOfT = - EventEmitterOfT() - override val playbackRangeChanged: IEventEmitterOfT = - EventEmitterOfT() - - - private fun onReady() { - _uiInvoke { (ready as EventEmitter).trigger() } - } - - private fun onReadyForPlayback() { - _uiInvoke { (readyForPlayback as EventEmitter).trigger() } - } - - private fun onFinished() { - _uiInvoke { (finished as EventEmitter).trigger() } - } - - private fun onSoundFontLoaded() { - _uiInvoke { (soundFontLoaded as EventEmitter).trigger() } - } - - private fun onSoundFontLoadFailed(e: Error) { - _uiInvoke { (soundFontLoadFailed as EventEmitterOfT).trigger(e) } - } - - private fun onMidiLoaded(args: PositionChangedEventArgs) { - _uiInvoke { (midiLoaded as EventEmitterOfT).trigger(args) } - } - - private fun onMidiLoadFailed(e: Error) { - _uiInvoke { (midiLoadFailed as EventEmitterOfT).trigger(e) } - } - - private fun onMidiEventsPlayed(e: MidiEventsPlayedEventArgs) { - _uiInvoke { (midiEventsPlayed as EventEmitterOfT).trigger(e) } - } - - private fun onStateChanged(obj: PlayerStateChangedEventArgs) { - _uiInvoke { (stateChanged as EventEmitterOfT).trigger(obj) } - } - - private fun onPositionChanged(obj: PositionChangedEventArgs) { - _uiInvoke { (positionChanged as EventEmitterOfT).trigger(obj) } - } - - private fun onPlaybackRangeChanged(obj: PlaybackRangeChangedEventArgs) { - _uiInvoke { - (playbackRangeChanged as EventEmitterOfT).trigger( - obj - ) - } - } -} diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidThreadScoreRenderer.kt b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidThreadScoreRenderer.kt deleted file mode 100644 index b344357da..000000000 --- a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidThreadScoreRenderer.kt +++ /dev/null @@ -1,188 +0,0 @@ -package alphaTab.platform.android - -import alphaTab.* -import alphaTab.collections.DoubleList -import alphaTab.core.ecmaScript.Error -import alphaTab.model.Score -import alphaTab.rendering.IScoreRenderer -import alphaTab.rendering.RenderFinishedEventArgs -import alphaTab.rendering.RenderHints -import alphaTab.rendering.ScoreRenderer -import alphaTab.rendering.utils.BoundsLookup -import java.util.concurrent.BlockingQueue -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.Semaphore -import java.util.concurrent.TimeUnit -import kotlin.contracts.ExperimentalContracts - -@ExperimentalContracts -@ExperimentalUnsignedTypes -internal class AndroidThreadScoreRenderer : IScoreRenderer, Runnable { - private val _uiInvoke: ( action: (() -> Unit) ) -> Unit - - private val _workerThread: Thread - private val _workerQueue: BlockingQueue<() -> Unit> - private val _threadStartedEvent: Semaphore - private var _isCancelled = false - public lateinit var renderer: ScoreRenderer - private var _width: Double = 0.0 - - public constructor(settings: Settings, uiInvoke: ( action: (() -> Unit) ) -> Unit) { - _uiInvoke = uiInvoke - _threadStartedEvent = Semaphore(1) - _threadStartedEvent.acquire() - _workerQueue = LinkedBlockingQueue() - - _workerThread = Thread(this) - _workerThread.name = "alphaTabRenderThread" - _workerThread.isDaemon = true - _workerThread.start() - - _threadStartedEvent.acquire() - - _workerQueue.add { initialize(settings) } - } - - override fun run() { - _threadStartedEvent.release() - try { - do { - val item = _workerQueue.poll(500, TimeUnit.MILLISECONDS) - if (!_isCancelled && item != null) { - item() - } - } while (!_isCancelled) - } catch (e: InterruptedException) { - // finished - } - } - - private fun initialize(settings: Settings) { - renderer = ScoreRenderer(settings) - renderer.partialRenderFinished.on { - _uiInvoke { onPartialRenderFinished(it) } - } - renderer.partialLayoutFinished.on { - _uiInvoke { onPartialLayoutFinished(it) } - } - renderer.renderFinished.on { - _uiInvoke { onRenderFinished(it) } - } - renderer.postRenderFinished.on { - _uiInvoke { onPostFinished(renderer.boundsLookup) } - } - renderer.preRender.on { - _uiInvoke { onPreRender(it) } - } - renderer.error.on { - _uiInvoke { onError(it) } - } - } - - private fun onPostFinished(boundsLookup: BoundsLookup?) { - this.boundsLookup = boundsLookup - onPostRenderFinished() - } - - override var boundsLookup: BoundsLookup? = null - override var width: Double - get() = _width - set(value) { - _width = value - if (checkAccess()) { - renderer.width = value - } else { - _workerQueue.add { renderer.width = value } - } - } - - override fun render(renderHints: RenderHints?) { - if (checkAccess()) { - renderer.render(renderHints) - } else { - _workerQueue.add { render(renderHints) } - } - } - - override fun renderResult(resultId:String) { - if (checkAccess()) { - renderer.renderResult(resultId) - } else { - _workerQueue.add { renderResult(resultId) } - } - } - - override fun resizeRender() { - if (checkAccess()) { - renderer.resizeRender() - } else { - _workerQueue.add { resizeRender() } - } - } - - override fun renderScore(score: Score?, trackIndexes: DoubleList?, renderHints: RenderHints?) { - if (checkAccess()) { - renderer.renderScore(score, trackIndexes, renderHints) - } else { - _workerQueue.add { - renderScore( - score, - trackIndexes, - renderHints - ) - } - } - } - - override fun updateSettings(settings: Settings) { - if (checkAccess()) { - renderer.updateSettings(settings) - } else { - _workerQueue.add { updateSettings(settings) } - } - } - - private fun checkAccess(): Boolean { - return Thread.currentThread().id == _workerThread.id - } - - override fun destroy() { - _isCancelled = true - _workerThread.interrupt() - _workerThread.join() - } - - override val preRender: IEventEmitterOfT = EventEmitterOfT() - private fun onPreRender(isResize: Boolean) { - (preRender as EventEmitterOfT).trigger(isResize) - } - - override val renderFinished: IEventEmitterOfT = EventEmitterOfT() - private fun onRenderFinished(args: RenderFinishedEventArgs) { - (renderFinished as EventEmitterOfT).trigger(args) - } - - override val partialRenderFinished: IEventEmitterOfT = - EventEmitterOfT() - - private fun onPartialRenderFinished(args: RenderFinishedEventArgs) { - (partialRenderFinished as EventEmitterOfT).trigger(args) - } - - override val partialLayoutFinished: IEventEmitterOfT = - EventEmitterOfT() - - private fun onPartialLayoutFinished(args: RenderFinishedEventArgs) { - (partialLayoutFinished as EventEmitterOfT).trigger(args) - } - - override val postRenderFinished: IEventEmitter = EventEmitter() - private fun onPostRenderFinished() { - (postRenderFinished as EventEmitter).trigger() - } - - override val error: IEventEmitterOfT = EventEmitterOfT() - private fun onError(e: Error) { - (error as EventEmitterOfT).trigger(e) - } -} diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidUiFacade.kt b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidUiFacade.kt index 479b980fc..e8bca30eb 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidUiFacade.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidUiFacade.kt @@ -14,6 +14,9 @@ import alphaTab.platform.IContainer import alphaTab.platform.IMouseEventArgs import alphaTab.platform.IUiFacade import alphaTab.platform.skia.AlphaSkiaImage +import alphaTab.platform.worker.AlphaSynthAudioExporterWorkerApi +import alphaTab.platform.worker.AlphaSynthWebWorkerApi +import alphaTab.platform.worker.AlphaTabWorkerScoreRenderer import alphaTab.rendering.IScoreRenderer import alphaTab.rendering.RenderFinishedEventArgs import alphaTab.rendering.utils.Bounds @@ -23,12 +26,19 @@ import alphaTab.synth.IAudioExporterWorker import android.annotation.SuppressLint import android.graphics.Bitmap import android.os.Handler +import android.os.Looper import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.widget.RelativeLayout import androidx.core.view.children +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import java.io.InputStream import java.nio.ByteBuffer @@ -39,13 +49,14 @@ import kotlin.contracts.ExperimentalContracts @ExperimentalUnsignedTypes @SuppressLint("ClickableViewAccessibility") internal class AndroidUiFacade : IUiFacade { - private var _handler: Handler private var _internalRootContainerBecameVisible: EventEmitter? = EventEmitter() private val _outerScroll: SuspendableHorizontalScrollView private val _innerScroll: SuspendableScrollView private val _renderSurface: AlphaTabRenderSurface private val _renderWrapper: RelativeLayout + private val _uiLooper:Handler; + public constructor( outerScroll: SuspendableHorizontalScrollView, innerScroll: SuspendableScrollView, @@ -56,10 +67,10 @@ internal class AndroidUiFacade : IUiFacade { _innerScroll = innerScroll _renderSurface = renderSurface _renderWrapper = renderWrapper + _uiLooper = Handler(Looper.getMainLooper()) rootContainer = AndroidRootViewContainer(outerScroll, innerScroll, renderSurface, this::beginInvoke) - _handler = Handler(outerScroll.context.mainLooper) rootContainerBecameVisible = object : IEventEmitter, ViewTreeObserver.OnGlobalLayoutListener, View.OnLayoutChangeListener { @@ -151,7 +162,12 @@ internal class AndroidUiFacade : IUiFacade { } override fun createWorkerRenderer(): IScoreRenderer { - return AndroidThreadScoreRenderer(api.settings, this::beginInvoke) + val worker = JavaThreadAlphaTabRendererWorker(this::postToUIThread) + return AlphaTabWorkerScoreRenderer(api, worker) + } + + private fun postToUIThread(action: () -> Unit) { + _uiLooper.post(action) } private fun openDefaultSoundFont(): InputStream { @@ -159,15 +175,12 @@ internal class AndroidUiFacade : IUiFacade { } override fun createWorkerPlayer(): IAlphaSynth { - var player: AndroidThreadAlphaSynthWorkerPlayer? = null - player = AndroidThreadAlphaSynthWorkerPlayer( - api.settings.core.logLevel, - AndroidSynthOutput(_renderWrapper.context) { - player!!.addToWorker(it) - }, - this::beginInvoke, - api.settings.player.bufferTimeInMilliseconds + val player = AlphaSynthWebWorkerApi( + AndroidSynthOutput(_renderWrapper.context), + api.settings, + JavaThreadAlphaSynthWorker(this::postToUIThread) ) + player.ready.on { val soundFont = openDefaultSoundFont() val bos = ByteArrayOutputStream() @@ -180,16 +193,16 @@ internal class AndroidUiFacade : IUiFacade { } override fun createWorkerAudioExporter(synth: IAlphaSynth?): IAudioExporterWorker { - val needNewWorker = synth == null || synth !is AndroidThreadAlphaSynthWorkerPlayer + val needNewWorker = synth == null || synth !is AlphaSynthWebWorkerApi var synthToUse = synth if (needNewWorker) { - synthToUse = this.createWorkerPlayer(); + synthToUse = this.createWorkerPlayer() } - return AndroidThreadAlphaSynthAudioExporter( - synthToUse as AndroidThreadAlphaSynthWorkerPlayer, + return AlphaSynthAudioExporterWorkerApi( + synthToUse as AlphaSynthWebWorkerApi, needNewWorker - ); + ) } @@ -232,7 +245,7 @@ internal class AndroidUiFacade : IUiFacade { } override fun beginAppendRenderResults(renderResults: RenderFinishedEventArgs?) { - _handler.post { + postToUIThread { if (renderResults != null) { _renderSurface.addPlaceholder(renderResults) } @@ -240,7 +253,7 @@ internal class AndroidUiFacade : IUiFacade { } override fun beginUpdateRenderResults(renderResults: RenderFinishedEventArgs) { - _handler.post { + postToUIThread { // convert AlphaSkia image to Android Bitmap val renderResult = renderResults.renderResult if (renderResult is AlphaSkiaImage) { @@ -345,12 +358,42 @@ internal class AndroidUiFacade : IUiFacade { } override fun beginInvoke(action: () -> Unit) { - _handler.post(action) + // post to "own" event loop if running inside worker + val synthWorker = JavaThreadAlphaSynthWorker.currentThreadWorker + if (synthWorker != null) { + synthWorker.postToWorker(action) + return + } + + val renderWorker = JavaThreadAlphaTabRendererWorker.currentThreadWorker + if (renderWorker != null) { + renderWorker.postToWorker(action) + return + } + + // not in worker -> run on main + postToUIThread(action) } override fun removeHighlights() { } + // The main throttle scope is not on Main but we then trigger the actual logic via postToUIThread. + // this way we do not load the UI thread unnecessarily until we have actual work. + private val throttleScope = CoroutineScope(Dispatchers.Default) + override fun throttle(action: () -> Unit, delay: Double): () -> Unit { + // we schedule delayed jobs + var job: Job? = null + return { + job?.cancel() + @Suppress("AssignedValueIsNeverRead") + job = throttleScope.launch { + delay(delay.toLong()) + postToUIThread(action) + } + } + } + override fun createSelectionElement(): IContainer? { val selection = object : View(_renderWrapper.context) { override fun onTouchEvent(event: MotionEvent?): Boolean { @@ -403,7 +446,7 @@ internal class AndroidUiFacade : IUiFacade { overflow: Double, isVertical: Boolean ) { - val view = (canvasElement as AndroidViewContainer).view; + val view = (canvasElement as AndroidViewContainer).view if (view is AlphaTabRenderSurface) { if (isVertical) { view.setPadding(0, 0, 0, overflow.toInt()) diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/JavaThreadWorkers.kt b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/JavaThreadWorkers.kt new file mode 100644 index 000000000..8e99ac5ec --- /dev/null +++ b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/JavaThreadWorkers.kt @@ -0,0 +1,164 @@ +package alphaTab.platform.android + +import alphaTab.core.ecmaScript.MessageEvent +import alphaTab.platform.worker.AlphaSynthWebWorker +import alphaTab.platform.worker.AlphaTabWebWorker +import alphaTab.platform.worker.IAlphaSynthWorker +import alphaTab.platform.worker.IAlphaSynthWorkerMessage +import alphaTab.platform.worker.IAlphaTabRenderingWorker +import alphaTab.platform.worker.IAlphaTabWorker +import alphaTab.platform.worker.IAlphaTabWorkerGlobalScope +import alphaTab.platform.worker.IAlphaTabWorkerMessage +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.Semaphore +import kotlin.contracts.ExperimentalContracts + +@OptIn(ExperimentalContracts::class, ExperimentalUnsignedTypes::class) +internal abstract class JavaThreadWorkerBase : IAlphaTabWorker, Runnable { + private val _postToMain: (action: () -> Unit) -> Unit + protected val workerThread: Thread + private val _workerQueue = LinkedBlockingQueue<() -> Unit>() + private var _isCancelled = false + private val _threadStartedEvent = Semaphore(1) + private val _listenerInsideWorker = + ConcurrentHashMap<(ev: MessageEvent) -> Unit, (ev: MessageEvent) -> Unit>() + private val _listenerOutsideWorker = + ConcurrentHashMap<(ev: MessageEvent) -> Unit, (ev: MessageEvent) -> Unit>() + + protected constructor(postToMain: (action: () -> Unit) -> Unit) { + _postToMain = postToMain; + + workerThread = Thread(this) + workerThread.isDaemon = true + workerThread.start() + + _threadStartedEvent.acquire() + } + + protected abstract fun onStartInsideWorker() + + override fun run() { + _threadStartedEvent.release(); + onStartInsideWorker(); + while (!_isCancelled) { + try { + val item = _workerQueue.take(); + if (!_isCancelled && item != null) { + item() + } + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + break; + } + } + } + + override fun postMessage(message: T) { + val ev = MessageEvent(message); + if (Thread.currentThread().id == workerThread.id) { + // Inside Worker -> Post to main + _postToMain( + { + for (listener in _listenerOutsideWorker) { + listener.value(ev); + } + }); + } else { + // Outside Worker -> Post to worker + postToWorker( + { + for (listener in _listenerInsideWorker) { + listener.value(ev); + } + }); + } + } + + public fun postToWorker(action: () -> Unit) { + _workerQueue.add(action); + } + + public override fun addEventListener(event: String, handler: (ev: MessageEvent) -> Unit) { + if (event != "message") { + return; + } + val listeners = if (Thread.currentThread().id == workerThread.id) { + _listenerInsideWorker + } else { + _listenerOutsideWorker + }; + listeners[handler] = handler; + } + + override fun removeEventListener(event: String, handler: (arg1: MessageEvent) -> Unit) { + if (event != "message") { + return; + } + val listeners = if (Thread.currentThread().id == workerThread.id) { + _listenerInsideWorker + } else { + _listenerOutsideWorker + }; + listeners.remove(handler); + } + + override fun terminate() { + _isCancelled = true + workerThread.interrupt() + workerThread.join() + _workerQueue.clear() + } +} + +@OptIn(ExperimentalContracts::class, ExperimentalUnsignedTypes::class) +internal class JavaThreadAlphaTabRendererWorker(postToMain: (action: () -> Unit) -> Unit) : + JavaThreadWorkerBase(postToMain), + IAlphaTabRenderingWorker, + IAlphaTabWorkerGlobalScope { + companion object { + private val workerLookup = ConcurrentHashMap() + + val currentThreadWorker: JavaThreadAlphaTabRendererWorker? + get() { + return workerLookup.getOrDefault(Thread.currentThread().id, null) + } + } + + @OptIn(ExperimentalContracts::class, ExperimentalUnsignedTypes::class) + override fun onStartInsideWorker() { + workerLookup[Thread.currentThread().id] = this; + AlphaTabWebWorker.init(); + } + + override fun terminate() { + super.terminate() + workerLookup.remove(workerThread.id) + } +} + +@OptIn(ExperimentalContracts::class, ExperimentalUnsignedTypes::class) +internal class JavaThreadAlphaSynthWorker(postToMain: (action: () -> Unit) -> Unit) : + JavaThreadWorkerBase(postToMain), + IAlphaSynthWorker, + IAlphaTabWorkerGlobalScope { + companion object { + private val workerLookup = ConcurrentHashMap() + + val currentThreadWorker: JavaThreadAlphaSynthWorker? + get() { + return workerLookup.getOrDefault(Thread.currentThread().id, null) + } + } + + @OptIn(ExperimentalContracts::class, ExperimentalUnsignedTypes::class) + override fun onStartInsideWorker() { + workerLookup[Thread.currentThread().id] = this; + AlphaSynthWebWorker.init(); + } + + override fun terminate() { + super.terminate() + workerLookup.remove(workerThread.id) + } +} diff --git a/packages/kotlin/src/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt b/packages/kotlin/src/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt index cb8aa34c4..4f61d4461 100644 --- a/packages/kotlin/src/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt +++ b/packages/kotlin/src/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt @@ -23,6 +23,11 @@ import com.beust.klaxon.JsonValue import com.beust.klaxon.Klaxon import com.beust.klaxon.KlaxonException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.junit.Assert import java.io.ByteArrayOutputStream import java.io.File @@ -325,6 +330,18 @@ class TestPlatformPartials { return testMethod!! } + private val throttleScope = CoroutineScope(Dispatchers.Default) + internal fun throttle(toThrottle: () -> Unit, delay: Double): () -> Unit { + var job: Job? = null + return { + job?.cancel() + job = throttleScope.launch { + delay(delay.toLong()) + toThrottle() + } + } + } + internal val currentTestName: String get() { val testMethodInfo = findTestMethod() diff --git a/packages/lsp/package.json b/packages/lsp/package.json index ca08ad823..110250308 100644 --- a/packages/lsp/package.json +++ b/packages/lsp/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-language-server", - "version": "1.8.1", + "version": "1.9.0", "description": "A language server for alphaTab providing coding assistance for alphaTex.", "keywords": [ "guitar", @@ -34,20 +34,20 @@ "test": "mocha" }, "dependencies": { - "@coderline/alphatab": "^1.8.1", + "@coderline/alphatab": "^1.9.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@microsoft/api-extractor": "^7.55.2", + "@biomejs/biome": "^2.4.10", + "@microsoft/api-extractor": "^7.57.7", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", + "@types/node": "^25.5.0", "assert": "^2.1.0", "chai": "^6.2.2", "mocha": "^11.7.4", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" diff --git a/packages/monaco/package.json b/packages/monaco/package.json index c199cfb98..86ca97117 100644 --- a/packages/monaco/package.json +++ b/packages/monaco/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-monaco", - "version": "1.8.1", + "version": "1.9.0", "description": "A Monaco editor integration for alphaTab providing coding assistance for alphaTex.", "keywords": [ "guitar", @@ -31,23 +31,23 @@ "test": "mocha" }, "dependencies": { - "@coderline/alphatab": "^1.8.1", - "@coderline/alphatab-language-server": "^1.8.1", + "@coderline/alphatab": "^1.9.0", + "@coderline/alphatab-language-server": "^1.9.0", "monaco-editor": "^0.55.1", "vscode-languageserver-types": "^3.17.5", "vscode-oniguruma": "^2.0.1", - "vscode-textmate": "^9.3.1" + "vscode-textmate": "^9.3.2" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@microsoft/api-extractor": "^7.55.2", + "@biomejs/biome": "^2.4.10", + "@microsoft/api-extractor": "^7.57.7", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", + "@types/node": "^25.5.0", "assert": "^2.1.0", "chai": "^6.2.2", "mocha": "^11.7.4", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" diff --git a/packages/playground/package.json b/packages/playground/package.json index fcdb5ba1b..431308171 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-playground", - "version": "1.8.1", + "version": "1.9.0", "description": "A development playground for alphaTab to test features while developing", "private": true, "type": "module", @@ -8,11 +8,11 @@ "@coderline/alphatab": "*", "@fontsource/noto-sans": "^5.2.10", "@fontsource/noto-serif": "^5.2.9", - "@fortawesome/fontawesome-free": "^7.1.0", + "@fortawesome/fontawesome-free": "^7.2.0", "@popperjs/core": "^2.11.8", "@types/serve-static": "^2.2.0", "bootstrap": "^5.3.8", - "handlebars": "^4.7.8", + "handlebars": "^4.7.9", "monaco-editor": "^0.55.1", "serve-static": "^2.2.1" }, @@ -26,7 +26,7 @@ "split.js": "^1.6.5", "tslib": "^2.8.1", "typescript": "^5.9.3", - "vite": "^7.3.1", - "vite-tsconfig-paths": "^6.0.4" + "vite": "^7.3.2", + "vite-tsconfig-paths": "^6.1.1" } } diff --git a/packages/tooling/package.json b/packages/tooling/package.json index 86eff5678..8967bca2a 100644 --- a/packages/tooling/package.json +++ b/packages/tooling/package.json @@ -1,14 +1,14 @@ { "name": "@coderline/alphatab-tooling", - "version": "1.8.1", + "version": "1.9.0", "type": "module", "description": "Additional build tooling for alphaTab like common build configurations", "private": true, "devDependencies": { - "@microsoft/api-extractor": "^7.55.2", - "@rollup/plugin-terser": "^0.4.4", + "@microsoft/api-extractor": "^7.57.7", + "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "rollup-plugin-license": "^3.6.0", + "rollup-plugin-license": "^3.7.0", "typescript": "^5.9.3" } } diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index f74f8f2fe..73e2af725 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-transpiler", - "version": "1.8.1", + "version": "1.9.0", "type": "module", "description": "The transpiler toolkit to translate alphaTab to C# and Kotlin", "private": true, diff --git a/packages/transpiler/src/AstPrinterBase.ts b/packages/transpiler/src/AstPrinterBase.ts index 34764cd0e..af8ddfc68 100644 --- a/packages/transpiler/src/AstPrinterBase.ts +++ b/packages/transpiler/src/AstPrinterBase.ts @@ -256,7 +256,7 @@ export default abstract class AstPrinterBase { if (expr.nullSafe) { this.write('?.'); - this.write(this._context.toMethodName('invoke')); + this.write(this._context.toMethodNameCase('invoke')); } this.write('('); diff --git a/packages/transpiler/src/csharp/CSharpAst.ts b/packages/transpiler/src/csharp/CSharpAst.ts index 09e2714db..e7c2d17f2 100644 --- a/packages/transpiler/src/csharp/CSharpAst.ts +++ b/packages/transpiler/src/csharp/CSharpAst.ts @@ -161,7 +161,7 @@ export interface ClassDeclaration extends NamedTypeDeclaration { interfaces?: TypeNode[]; isAbstract: boolean; members: ClassMember[]; - isRecord?:boolean; + isRecord?: boolean; } export type ClassMember = @@ -458,6 +458,7 @@ export interface NewExpression extends Node { nodeType: SyntaxKind.NewExpression; type: TypeNode; arguments: Expression[]; + objectInitializers?: LabeledExpression[]; } export interface CastExpression extends Node { diff --git a/packages/transpiler/src/csharp/CSharpAstPrinter.ts b/packages/transpiler/src/csharp/CSharpAstPrinter.ts index 37a863382..0b0f28ffa 100644 --- a/packages/transpiler/src/csharp/CSharpAstPrinter.ts +++ b/packages/transpiler/src/csharp/CSharpAstPrinter.ts @@ -879,6 +879,19 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.write('('); this.writeCommaSeparated(expr.arguments, a => this.writeExpression(a)); this.write(')'); + if (expr.objectInitializers?.length) { + this.writeLine(); + this.beginBlock(); + + for (const a of expr.objectInitializers!) { + this.write(a.label); + this.write(' = '); + this.writeExpression(a.expression); + this.writeLine(","); + } + + this.endBlock(); + } } protected writeCastExpression(expr: cs.CastExpression) { @@ -1068,7 +1081,7 @@ export default class CSharpAstPrinter extends AstPrinterBase { } protected writeUsing(using: cs.UsingDeclaration) { - if (using.skipEmit) { + if (using.skipEmit) { return; } @@ -1105,7 +1118,7 @@ export default class CSharpAstPrinter extends AstPrinterBase { protected override writeThrowStatement(s: cs.ThrowStatement) { this.write('throw'); - const currentException = this._currentCatchClauseIdentifier[this._currentCatchClauseIdentifier.length -1]; + const currentException = this._currentCatchClauseIdentifier[this._currentCatchClauseIdentifier.length - 1]; if (s.expression && (!cs.isIdentifier(s.expression) || s.expression.text !== currentException)) { this.write(' '); this.writeExpression(s.expression); diff --git a/packages/transpiler/src/csharp/CSharpAstTransformer.ts b/packages/transpiler/src/csharp/CSharpAstTransformer.ts index 8c94a04f2..c98b6a68d 100644 --- a/packages/transpiler/src/csharp/CSharpAstTransformer.ts +++ b/packages/transpiler/src/csharp/CSharpAstTransformer.ts @@ -44,7 +44,7 @@ export default class CSharpAstTransformer { namespace: { parent: null, nodeType: cs.SyntaxKind.NamespaceDeclaration, - namespace: this.context.toPascalCase('alphaTab'), + namespace: this.context.toNamespaceNameCase('alphaTab'), declarations: [] } }; @@ -268,7 +268,7 @@ export default class CSharpAstTransformer { } // TODO: make root namespace configurable from outside. - const folders = path + const folders: string[] = path .dirname( path.relative( path.resolve(this.context.compilerOptions.baseUrl!), @@ -281,7 +281,8 @@ export default class CSharpAstTransformer { folders.shift(); } this.csharpFile.namespace.namespace = - this.context.toPascalCase('alphaTab') + folders.map(f => `.${this.context.toPascalCase(f)}`).join(''); + this.context.toNamespaceNameCase('alphaTab') + + folders.map(f => `.${this.context.toNamespaceNameCase(f)}`).join(''); if (defaultExport) { this.visit( @@ -427,6 +428,8 @@ export default class CSharpAstTransformer { this.visitFunctionTypeAliasDeclaration(node); } else if (ts.isTypeLiteralNode(node.type)) { this.visitTypeLiteralAliasDeclaration(node); + } else if (this.context.isDiscriminatedUnion(node)) { + this.visitDiscriminatedUnion(node); } else if (isExported && !shouldSkip) { this.context.addTsNodeDiagnostics( node, @@ -504,6 +507,208 @@ export default class CSharpAstTransformer { this.visitRecordDeclaration(node); } + protected visitDiscriminatedUnion(node: ts.TypeAliasDeclaration) { + const tag = ts.getJSDocTags(node).find(t => t.tagName.text === 'discriminated')!; + const values = (tag.comment as string).split(' '); + const discriminatorField = values[0]; + const discriminatorValuePrefix = values[1]; + + const unionType = this.context.typeChecker.getTypeAtLocation(node.type); + if (!unionType.isUnion()) { + this.context.addTsNodeDiagnostics( + node, + `Discriminated union must be a union type`, + ts.DiagnosticCategory.Error + ); + return; + } + + // Create base interface + const baseInterface = this.createDiscriminatedUnionBaseInterface(node, discriminatorField); + this.csharpFile.namespace.declarations.push(baseInterface); + this.context.registerSymbol(baseInterface); + + const typeNamePrefix = baseInterface.name.startsWith('I') + ? baseInterface.name.substring(1) + : baseInterface.name; + + // Create classes for each union member + for (const memberType of unionType.types) { + const properties = this.context.typeChecker.getPropertiesOfType(memberType); + const discriminatorProp = properties.find(p => p.name === discriminatorField); + if (!discriminatorProp) { + continue; + } + + const discriminatorType = this.context.typeChecker.getTypeOfSymbolAtLocation(discriminatorProp, node); + if (!discriminatorType.isStringLiteral()) { + continue; + } + + const discriminatorValue = discriminatorType.value; + + // Compute class name + const suffix = discriminatorValue.startsWith(discriminatorValuePrefix) + ? discriminatorValue.substring(discriminatorValuePrefix.length) + : discriminatorValue; + const className = + typeNamePrefix + + suffix + .split('.') + .map(p => this.context.toTypeNameCase(p)) + .join(''); + + const csClass = this.createDiscriminatedUnionClass( + node, + className, + memberType, + baseInterface, + discriminatorField, + discriminatorValue + ); + this.csharpFile.namespace.declarations.push(csClass); + this.context.registerSymbol(csClass); + } + } + protected createDiscriminatedUnionClass( + node: ts.TypeAliasDeclaration, + className: string, + memberType: ts.Type, + baseInterface: cs.InterfaceDeclaration, + discriminatorField: string, + discriminatorValue: string + ) { + // Create class + const csClass: cs.ClassDeclaration = { + visibility: this.getVisibility(node), + name: className, + nodeType: cs.SyntaxKind.ClassDeclaration, + parent: this.csharpFile.namespace, + isAbstract: false, + members: [], + skipEmit: this.shouldSkip(node, false), + partial: false, + tsNode: memberType.symbol.declarations![0], + tsSymbol: memberType.symbol, + hasVirtualMembersOrSubClasses: false, + isRecord: true + }; + + // Add interface implementation + csClass.interfaces = [ + { + nodeType: cs.SyntaxKind.TypeReference, + parent: csClass, + reference: this.context.makeTypeName(baseInterface.name), + isAsync: false + } as cs.TypeReference + ]; + + // Add discriminator property + const discProp: cs.PropertyDeclaration = { + visibility: cs.Visibility.Public, + name: this.context.toPropertyNameCase(discriminatorField), + nodeType: cs.SyntaxKind.PropertyDeclaration, + parent: csClass, + isVirtual: false, + isOverride: false, + isAbstract: false, + isStatic: false, + type: this.createUnresolvedTypeNode(null, node.type, this.context.typeChecker.getStringType()), + initializer: { + nodeType: cs.SyntaxKind.StringLiteral, + text: discriminatorValue + } as cs.StringLiteral, + getAccessor: { + keyword: 'get' + } as cs.PropertyAccessorDeclaration, + setAccessor: { + keyword: 'set' + } as cs.PropertyAccessorDeclaration, + tsNode: node, + skipEmit: false + }; + discProp.initializer!.parent = discProp; + + csClass.members.push(discProp); + + // Add other properties + const properties = this.context.typeChecker.getPropertiesOfType(memberType); + const otherProperties = properties.filter(p => p.name !== discriminatorField); + for (const prop of otherProperties) { + const propType = this.context.typeChecker.getTypeOfSymbolAtLocation(prop, node); + + // Create property + const csProperty: cs.PropertyDeclaration = { + visibility: cs.Visibility.Public, + name: this.context.toPropertyNameCase(prop.name), + nodeType: cs.SyntaxKind.PropertyDeclaration, + parent: csClass, + isVirtual: false, + isOverride: false, + isAbstract: false, + isStatic: false, + type: this.createUnresolvedTypeNode(null, node.type, propType), + tsNode: prop.valueDeclaration ?? prop.declarations![0], + getAccessor: { + keyword: 'get' + } as cs.PropertyAccessorDeclaration, + setAccessor: { + keyword: 'set' + } as cs.PropertyAccessorDeclaration, + skipEmit: false + }; + + csClass.members.push(csProperty); + } + return csClass; + } + + protected createDiscriminatedUnionBaseInterface(node: ts.TypeAliasDeclaration, discriminatorField: string) { + const baseInterface: cs.InterfaceDeclaration = { + visibility: this.getVisibility(node), + name: node.name.text, + nodeType: cs.SyntaxKind.InterfaceDeclaration, + parent: this.csharpFile.namespace, + members: [], + tsNode: node, + skipEmit: this.shouldSkip(node, false), + partial: false, + tsSymbol: this.context.getSymbolForDeclaration(node), + hasVirtualMembersOrSubClasses: false + }; + + if (node.name) { + baseInterface.documentation = this.visitDocumentation(node.name); + } + + this._visitDocumentationAttributes(baseInterface, node); + + // Add discriminator property to interface + const discriminatorProperty: cs.PropertyDeclaration = { + visibility: cs.Visibility.Public, + name: this.context.toPropertyNameCase(discriminatorField), + nodeType: cs.SyntaxKind.PropertyDeclaration, + parent: baseInterface, + isVirtual: false, + isOverride: false, + isAbstract: false, + isStatic: false, + type: this.createUnresolvedTypeNode(null, node.type, this.context.typeChecker.getStringType()), + tsNode: node, + getAccessor: { + keyword: 'get' + } as cs.PropertyAccessorDeclaration, + setAccessor: { + keyword: 'set' + } as cs.PropertyAccessorDeclaration, + skipEmit: false + }; + + baseInterface.members.push(discriminatorProperty); + return baseInterface; + } + protected visitInterfaceDeclaration(node: ts.InterfaceDeclaration) { if (this.context.isRecord(node)) { this.visitRecordDeclaration(node); @@ -703,7 +908,7 @@ export default class CSharpAstTransformer { isOverride: false, isStatic: false, isVirtual: false, - name: this.context.toPascalCase(m.name.getText()), + name: this.context.toPropertyNameCase(m.name.getText()), type: this.createUnresolvedTypeNode(null, m.type ?? m, type), visibility: cs.Visibility.Public, tsNode: m, @@ -833,7 +1038,7 @@ export default class CSharpAstTransformer { nodeType: cs.SyntaxKind.ThisLiteral, parent: stmt.expression } as cs.ThisLiteral, - member: this.context.toPascalCase(p.name.getText()) + member: this.context.toPropertyNameCase(p.name.getText()) } as cs.MemberAccessExpression; ((stmt.expression as cs.BinaryExpression).left as cs.MemberAccessExpression).expression.parent = ( @@ -922,7 +1127,7 @@ export default class CSharpAstTransformer { const testClassName = (d.arguments[0] as ts.StringLiteral).text; const csClass: cs.ClassDeclaration = { visibility: cs.Visibility.Public, - name: this.context.toPascalCase(this.context.toIdentifier(testClassName)), + name: this.context.toTypeNameCase(this.context.toIdentifier(testClassName)), tsNode: d, nodeType: cs.SyntaxKind.ClassDeclaration, parent: this.csharpFile.namespace, @@ -1011,7 +1216,7 @@ export default class CSharpAstTransformer { isVirtual: false, isGeneratorFunction: false, partial: !!ts.getJSDocTags(d).find(t => t.tagName.text === 'partial'), - name: this.context.toPascalCase((d.name as ts.Identifier).text), + name: this.context.toMethodNameCase((d.name as ts.Identifier).text), parameters: [], returnType: this.createUnresolvedTypeNode(null, d.type ?? d, returnType), visibility: this.mapVisibility(d, cs.Visibility.Private), @@ -1049,7 +1254,7 @@ export default class CSharpAstTransformer { } } - name = this.context.toMethodName(name); + name = this.context.toMethodNameCase(name); const csMethod: cs.MethodDeclaration = { parent: parent, nodeType: cs.SyntaxKind.MethodDeclaration, @@ -1172,7 +1377,7 @@ export default class CSharpAstTransformer { isTestMethod: false, isGeneratorFunction: false, partial: !!ts.getJSDocTags(d).find(t => t.tagName.text === 'partial'), - name: this.context.toPascalCase(d.name.getText()), + name: this.context.toMethodNameCase(d.name.getText()), returnType: {} as cs.TypeNode, visibility: cs.Visibility.Private, tsNode: d, @@ -1216,7 +1421,7 @@ export default class CSharpAstTransformer { isOverride: false, isStatic: true, isVirtual: false, - name: this.context.toPascalCase(d.name.getText()), + name: this.context.toPropertyNameCase(d.name.getText()), type: this.createUnresolvedTypeNode(null, d.type ?? d, type), visibility: cs.Visibility.Private, tsNode: d @@ -1545,7 +1750,7 @@ export default class CSharpAstTransformer { isOverride: false, isStatic: false, isVirtual: false, - name: this.context.toPascalCase((classElement.name as ts.Identifier).text), + name: this.context.toPropertyNameCase((classElement.name as ts.Identifier).text), type: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, type), visibility: cs.Visibility.None, tsNode: classElement, @@ -1588,7 +1793,7 @@ export default class CSharpAstTransformer { } protected visitGetAccessor(parent: cs.ClassDeclaration, classElement: ts.GetAccessorDeclaration) { - const propertyName = this.context.toPascalCase(classElement.name.getText()); + const propertyName = this.context.toPropertyNameCase(classElement.name.getText()); const member = parent.members.find(m => m.name === propertyName); if (member && cs.isPropertyDeclaration(member)) { member.getAccessor = { @@ -1654,7 +1859,7 @@ export default class CSharpAstTransformer { } protected visitSetAccessor(parent: cs.ClassDeclaration, classElement: ts.SetAccessorDeclaration) { - const propertyName = this.context.toPascalCase(classElement.name.getText()); + const propertyName = this.context.toPropertyNameCase(classElement.name.getText()); const member = parent.members.find(m => m.name === propertyName); if (member && cs.isPropertyDeclaration(member)) { member.setAccessor = { @@ -1808,7 +2013,7 @@ export default class CSharpAstTransformer { isOverride: false, isStatic: false, isVirtual: false, - name: this.context.toPropertyName(classElement.name.getText()), + name: this.context.toPropertyNameCase(classElement.name.getText()), type: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, type), visibility: visibility, tsNode: classElement, @@ -1963,13 +2168,13 @@ export default class CSharpAstTransformer { } switch (csMethod.name) { - case this.context.toMethodName('toString'): + case this.context.toMethodNameCase('toString'): if (csMethod.parameters.length === 0) { csMethod.isVirtual = false; csMethod.isOverride = true; } break; - case this.context.toMethodName('equals'): + case this.context.toMethodNameCase('equals'): if (csMethod.parameters.length === 1) { csMethod.isVirtual = false; csMethod.isOverride = true; @@ -2371,13 +2576,17 @@ export default class CSharpAstTransformer { } protected visitReturnStatement(parent: cs.Node, s: ts.ReturnStatement) { - if(this.currentClassElement && ts.isMethodDeclaration(this.currentClassElement) && !!this.currentClassElement.asteriskToken) { + if ( + this.currentClassElement && + ts.isMethodDeclaration(this.currentClassElement) && + !!this.currentClassElement.asteriskToken + ) { const yieldExpressionStmt = { expression: null!, nodeType: cs.SyntaxKind.ExpressionStatement, parent: parent, tsNode: s - } as cs.ExpressionStatement + } as cs.ExpressionStatement; const yieldExpression = { expression: null, parent: yieldExpressionStmt, @@ -2859,7 +3068,7 @@ export default class CSharpAstTransformer { parent: parent, expression: null!, tsSymbol: enumMember.symbol, - member: this.context.toPropertyName(enumMember.symbol.name) + member: this.context.toPropertyNameCase(enumMember.symbol.name) } as cs.MemberAccessExpression; const identifier = { @@ -2975,7 +3184,7 @@ export default class CSharpAstTransformer { csExpr.expression = this.makeMemberAccess( csExpr, this.context.makeTypeName('alphaTab.core.TypeHelper'), - this.context.toMethodName('typeOf') + this.context.toMethodNameCase('typeOf') ); const e = this.visitExpression(csExpr, expression.expression); if (e) { @@ -3032,7 +3241,7 @@ export default class CSharpAstTransformer { csExpr.expression = this.makeMemberAccess( csExpr, this.context.makeTypeName('alphaTab.core.TypeHelper'), - this.context.toMethodName('in') + this.context.toMethodNameCase('in') ); let e = this.visitExpression(csExpr, expression.left)!; @@ -3504,7 +3713,7 @@ export default class CSharpAstTransformer { tsNode: expression.tsNode, nodeType: cs.SyntaxKind.MemberAccessExpression, expression: {} as cs.Expression, - member: this.context.toMethodName('isTruthy') + member: this.context.toMethodNameCase('isTruthy') } as cs.MemberAccessExpression; call.expression = access; @@ -3679,7 +3888,7 @@ export default class CSharpAstTransformer { csExpr.expression = this.makeMemberAccess( csExpr, this.context.makeTypeName('alphaTab.core.TypeHelper'), - this.context.toMethodName('createRegex') + this.context.toMethodNameCase('createRegex') ); csExpr.arguments.push({ parent: csExpr, @@ -3977,7 +4186,7 @@ export default class CSharpAstTransformer { const memberAccess = { expression: {} as cs.Expression, - member: this.context.toPropertyName(expression.name.text), + member: this.context.toPropertyNameCase(expression.name.text), parent: parent, tsNode: expression, tsSymbol: tsSymbol, @@ -3990,7 +4199,9 @@ export default class CSharpAstTransformer { if (this.context.isMethodSymbol(memberAccess.tsSymbol)) { memberAccess.member = this.context.buildMethodName(expression.name); } else if (this.context.isPropertySymbol(memberAccess.tsSymbol)) { - memberAccess.member = this.context.toPropertyName(expression.name.text); + memberAccess.member = this.context.toPropertyNameCase(expression.name.text); + } else if (memberAccess.tsSymbol.flags & ts.SymbolFlags.EnumMember) { + memberAccess.member = expression.name.text; } } @@ -4095,6 +4306,13 @@ export default class CSharpAstTransformer { protected visitObjectLiteralExpression(parent: cs.Node, expression: ts.ObjectLiteralExpression) { let type = this.context.typeChecker.getContextualType(expression); let isRecord = type?.symbol?.declarations?.some(d => this.context.isRecord(d)) || this._recordCreation > 0; + const isDiscriminatedUnion = + type?.symbol?.declarations?.some(d => this.context.isDiscriminatedUnion(d)) || + type?.aliasSymbol?.declarations?.some(d => this.context.isDiscriminatedUnion(d)); + + if (isDiscriminatedUnion) { + return this.visitDiscriminatedUnionCreate(parent, expression); + } // assignment of object literal to property without giving type // -> try to use specific type of property @@ -4332,6 +4550,104 @@ export default class CSharpAstTransformer { return objectLiteral; } + protected visitDiscriminatedUnionCreate(parent: cs.Node, expression: ts.ObjectLiteralExpression) { + const unionType = this.context.typeChecker.getContextualType(expression)! as ts.UnionType; + + // find concrete type within unionType with which the expression matches + + const tag = ts + .getJSDocTags(unionType.aliasSymbol!.declarations![0]) + .find(t => t.tagName.text === 'discriminated')!; + const values = (tag.comment as string).split(' '); + const discriminatorField = values[0]; + + const discriminatorProp = expression.properties.find( + p => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === discriminatorField + ) as ts.PropertyAssignment; + + const discriminatorValue = ts.isStringLiteral(discriminatorProp!.initializer) + ? discriminatorProp.initializer.text + : undefined; + + const matching = unionType.types.find(memberType => { + const prop = memberType.getProperty(discriminatorField); + if (!prop) { + return false; + } + const propType = this.context.typeChecker.getTypeOfSymbolAtLocation(prop, expression); + return ( + propType.flags & ts.TypeFlags.StringLiteral && + (propType as ts.StringLiteralType).value === discriminatorValue + ); + }); + + if (!matching) { + this.context.addCsNodeDiagnostics( + parent, + 'Could not resolve concrete union type', + ts.DiagnosticCategory.Error + ); + return null; + } + + const newObject = { + nodeType: cs.SyntaxKind.NewExpression, + type: this.createUnresolvedTypeNode(null, expression, matching), + arguments: [], + parent: parent, + objectInitializers: [] + } as cs.NewExpression; + + for (const p of expression.properties) { + const assignment = { + parent: newObject, + nodeType: cs.SyntaxKind.LabeledExpression, + label: '', + expression: {} as cs.Expression + } as cs.LabeledExpression; + + if (ts.isPropertyAssignment(p)) { + assignment.label = this.context.toPropertyNameCase(p.name.getText()); + assignment.expression = this.visitExpression(assignment, p.initializer)!; + newObject.objectInitializers!.push(assignment); + } else if (ts.isShorthandPropertyAssignment(p)) { + assignment.label = this.context.toPropertyNameCase(p.name.getText()); + if (p.objectAssignmentInitializer) { + assignment.expression = this.visitExpression(assignment, p.objectAssignmentInitializer)!; + } else { + assignment.expression = { + nodeType: cs.SyntaxKind.Identifier, + parent: assignment, + tsNode: p.name, + text: p.name.getText() + } as cs.Identifier; + } + newObject.objectInitializers!.push(assignment); + } else if (ts.isSpreadAssignment(p)) { + this.context.addTsNodeDiagnostics(p, 'Spread operator not supported', ts.DiagnosticCategory.Error); + } else if (ts.isMethodDeclaration(p)) { + this.context.addTsNodeDiagnostics( + p, + 'Method declarations in object literals not supported', + ts.DiagnosticCategory.Error + ); + } else if (ts.isGetAccessorDeclaration(p)) { + this.context.addTsNodeDiagnostics( + p, + 'Get accessor declarations in object literals not supported', + ts.DiagnosticCategory.Error + ); + } else if (ts.isSetAccessorDeclaration(p)) { + this.context.addTsNodeDiagnostics( + p, + 'Set accessor declarations in object literals not supported', + ts.DiagnosticCategory.Error + ); + } + } + + return newObject; + } protected toInvariantString(expr: cs.Expression): cs.Expression { const callExpr = { @@ -4343,7 +4659,7 @@ export default class CSharpAstTransformer { } as cs.InvocationExpression; const memberAccess = { expression: null!, - member: this.context.toPascalCase('toInvariantString'), + member: this.context.toMethodNameCase('toInvariantString'), parent: callExpr, tsNode: expr.tsNode, nodeType: cs.SyntaxKind.MemberAccessExpression @@ -4380,7 +4696,7 @@ export default class CSharpAstTransformer { const memberAccess = { expression: {} as cs.Expression, - member: this.context.toPascalCase('toString'), + member: this.context.toMethodNameCase('toString'), parent: callExpr, tsNode: expression, nodeType: cs.SyntaxKind.MemberAccessExpression @@ -4406,7 +4722,7 @@ export default class CSharpAstTransformer { callExpr.expression = this.makeMemberAccess( callExpr, this.context.makeTypeName('alphaTab.core.TypeHelper'), - this.context.toMethodName('parseEnum') + this.context.toMethodNameCase('parseEnum') ); const enumType = this.context.typeChecker.getTypeAtLocation(expression.expression); @@ -4436,7 +4752,7 @@ export default class CSharpAstTransformer { const memberAccess = { nodeType: cs.SyntaxKind.MemberAccessExpression, expression: {} as cs.Expression, - member: this.context.toMethodName(elementAccessMethod), + member: this.context.toMethodNameCase(elementAccessMethod), parent: parent, tsNode: expression, nullSafe: !!expression.questionDotToken @@ -4512,7 +4828,7 @@ export default class CSharpAstTransformer { if (ts.isNumericLiteral(index)) { return { expression: elementAccess.expression, - member: this.context.toPropertyName(`v${index.text}`), + member: this.context.toPropertyNameCase(`v${index.text}`), parent: parent, tsNode: expression, nodeType: cs.SyntaxKind.MemberAccessExpression, @@ -4626,7 +4942,7 @@ export default class CSharpAstTransformer { parent: callExpression, tsNode: expression.expression, nodeType: cs.SyntaxKind.Identifier, - text: `TestGlobals.${this.context.toPascalCase('expect')}` + text: `TestGlobals.${this.context.toMethodNameCase('expect')}` } as cs.Identifier; } else { callExpression.expression = this.visitExpression(callExpression, expression.expression)!; @@ -4685,7 +5001,7 @@ export default class CSharpAstTransformer { invocation.expression = this.makeMemberAccess( invocation, this.context.makeTypeName('alphaTab.core.TypeHelper'), - this.context.toMethodName('createPromise') + this.context.toMethodNameCase('createPromise') ); const e = this.visitExpression(invocation, expression.arguments![0]); @@ -4799,7 +5115,7 @@ export default class CSharpAstTransformer { csExpr.expression = this.makeMemberAccess( csExpr, this.context.makeTypeName('alphaTab.core.TypeHelper'), - this.context.toMethodName('unknownToNumber') + this.context.toMethodNameCase('unknownToNumber') ); const e = this.visitExpression(csExpr, expression.expression); if (e) { @@ -4898,6 +5214,14 @@ export default class CSharpAstTransformer { identifier.text = this.getIdentifierName(identifier, expression); if (identifier.tsSymbol) { + if (identifier.tsSymbol) { + if (this.context.isMethodSymbol(identifier.tsSymbol)) { + identifier.text = this.context.toMethodNameCase(identifier.text); + } else if (this.context.isPropertySymbol(identifier.tsSymbol)) { + identifier.text = this.context.toPropertyNameCase(identifier.text); + } + } + switch (expression.parent.kind) { case ts.SyntaxKind.PropertyAccessExpression: case ts.SyntaxKind.BinaryExpression: diff --git a/packages/transpiler/src/csharp/CSharpEmitterContext.ts b/packages/transpiler/src/csharp/CSharpEmitterContext.ts index 49ab75bf8..03e71f7d8 100644 --- a/packages/transpiler/src/csharp/CSharpEmitterContext.ts +++ b/packages/transpiler/src/csharp/CSharpEmitterContext.ts @@ -14,7 +14,6 @@ export default class CSharpEmitterContext { private _unresolvedTypeNodes: cs.UnresolvedTypeNode[] = []; private _program: ts.Program; public typeChecker: ts.TypeChecker; - public noPascalCase: boolean = false; public csharpFiles: cs.SourceFile[] = []; public processingSkippedElement: boolean = false; @@ -30,14 +29,22 @@ export default class CSharpEmitterContext { return this.typeChecker.getTypeAtLocation(n); } - public toMethodName(text: string): string { + public toMethodNameCase(text: string): string { return this.toPascalCase(this.toIdentifier(text)); } - public toPropertyName(text: string): string { + public toPropertyNameCase(text: string): string { return this.toPascalCase(this.toIdentifier(text)); } + public toNamespaceNameCase(text: string): string { + return this.toPascalCase(text); + } + + public toTypeNameCase(text: string): string { + return this.toPascalCase(text); + } + public toIdentifier(text: string): string { // kebab-case and "spaced name" to camelCase const parts = text.split(/[ -]/g); @@ -65,7 +72,18 @@ export default class CSharpEmitterContext { } public isPropertySymbol(tsSymbol: ts.Symbol) { - return (tsSymbol.flags & ts.SymbolFlags.Property) !== 0; + if ( + (tsSymbol.flags & (ts.SymbolFlags.Property | ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) !== 0 + ) { + return true; + } + + // globals + if((tsSymbol.flags & ts.SymbolFlags.FunctionScopedVariable) !== 0 && this.isGlobalVariable(tsSymbol)) { + return true; + } + + return false; } public isTypeAssignable(targetType: ts.Type, contextualTypeNullable: ts.Type, actualType: ts.Type) { @@ -136,7 +154,7 @@ export default class CSharpEmitterContext { if (expr.tsSymbol.flags & ts.SymbolFlags.Function) { if (this.isTestFunction(expr.tsSymbol)) { - return `${this.toPascalCase('alphaTab.test')}.Globals.${this.toPascalCase(expr.tsSymbol.name)}`; + return `${this.toNamespaceNameCase('alphaTab.test')}.Globals.${this.toMethodNameCase(expr.tsSymbol.name)}`; } if (expr.tsSymbol.valueDeclaration && expr.tsNode) { @@ -146,20 +164,20 @@ export default class CSharpEmitterContext { } } - return `${this.toPascalCase('alphaTab.core')}.Globals.${this.toPascalCase(expr.tsSymbol.name)}`; + return `${this.toNamespaceNameCase('alphaTab.core')}.Globals.${this.toMethodNameCase(expr.tsSymbol.name)}`; } if ( (expr.tsSymbol.flags & ts.SymbolFlags.FunctionScopedVariable && this.isGlobalVariable(expr.tsSymbol)) || (expr.tsSymbol.flags & ts.SymbolFlags.NamespaceModule && this.isKnownModule(expr.tsSymbol)) ) { - return `${this.toPascalCase('alphaTab.core')}.Globals.${this.toPascalCase(expr.tsSymbol.name)}`; + return `${this.toNamespaceNameCase('alphaTab.core')}.Globals.${this.toPropertyNameCase(expr.tsSymbol.name)}`; } if (expr.tsSymbol) { const externalModule = this.resolveExternalModuleOfType(expr.tsSymbol); if (externalModule) { - return externalModule + this.toPascalCase(expr.tsSymbol.name); + return externalModule + this.toTypeNameCase(expr.tsSymbol.name); } } } @@ -346,6 +364,11 @@ export default class CSharpEmitterContext { (ts.isTypeAliasDeclaration(d) && ts.isTypeLiteralNode(d.type)) ); } + + isDiscriminatedUnion(node: ts.Declaration) { + return ts.getJSDocTags(node).some(t => t.tagName.text === 'discriminated'); + } + private getTypeFromTsType( node: cs.Node, tsType: ts.Type, @@ -821,6 +844,13 @@ export default class CSharpEmitterContext { return null; } + if ('typeArguments' in pTsType && cs.isTypeReference(pType)) { + const args = this.typeChecker.getTypeArguments(pTsType as ts.TypeReference); + if (args.length > 0) { + pType.typeArguments = args.map(a => this.getTypeFromTsType(pType, a)!); + } + } + parameterTypes.push(pType); } @@ -1280,9 +1310,9 @@ export default class CSharpEmitterContext { result += '.'; } if (i === parts.length - 1) { - result += parts[i]; + result += this.toTypeNameCase(parts[i]); } else { - result += this.toPascalCase(parts[i]); + result += this.toNamespaceNameCase(parts[i]); } } return result; @@ -1293,7 +1323,7 @@ export default class CSharpEmitterContext { if (aliasSymbol) { if (aliasSymbol.name === 'Map') { - return `${this.toPascalCase('alphaTab.collections') + suffix}.`; + return `${this.toNamespaceNameCase('alphaTab.collections') + suffix}.`; } if (aliasSymbol.name === 'Error') { @@ -1308,18 +1338,18 @@ export default class CSharpEmitterContext { if (fileName.length) { suffix = fileName.split('.').map(s => { if (s.match(/webworker/i)) { - return `.${this.toPascalCase('ecmaScript')}`; + return `.${this.toNamespaceNameCase('ecmaScript')}`; } if (s.match(/esnext/)) { - return `.${this.toPascalCase('ecmaScript')}`; + return `.${this.toNamespaceNameCase('ecmaScript')}`; } if (s.match(/es[0-9]{4}/)) { - return `.${this.toPascalCase('ecmaScript')}`; + return `.${this.toNamespaceNameCase('ecmaScript')}`; } if (s.match(/es[0-9]{1}/)) { - return `.${this.toPascalCase('ecmaScript')}`; + return `.${this.toNamespaceNameCase('ecmaScript')}`; } - return `.${this.toPascalCase(s)}`; + return `.${this.toNamespaceNameCase(s)}`; })[0]; } } @@ -1327,7 +1357,7 @@ export default class CSharpEmitterContext { } } - return `${this.toPascalCase('alphaTab.core') + suffix}.`; + return `${this.toNamespaceNameCase('alphaTab.core') + suffix}.`; } protected toCoreTypeName(s: string) { if (s === 'Map') { @@ -1341,10 +1371,6 @@ export default class CSharpEmitterContext { return this.kebabCaseToPascalCase(text); } - if (this.noPascalCase) { - return text; - } - if (!text) { return ''; } @@ -1387,7 +1413,7 @@ export default class CSharpEmitterContext { } public registerSymbol(node: cs.NamedElement & cs.Node) { - const symbol = this.getSymbolForDeclaration(node.tsNode!); + const symbol = node.tsSymbol ?? this.getSymbolForDeclaration(node.tsNode!); if (symbol) { const symbolKey = this.getSymbolKey(symbol); this._symbolLookup.set(symbolKey, node); @@ -1426,10 +1452,15 @@ export default class CSharpEmitterContext { ? symbol.declarations[0] : undefined; + let name = symbol.name; + if (name.startsWith('__')) { + name = '__'; + } + if (declaration) { - return `${symbol.name}_${declaration.getSourceFile().fileName}_${declaration.pos}`; + return `${name}_${declaration.getSourceFile().fileName}_${declaration.pos}`; } - return symbol.name; + return name; } public getSymbolForDeclaration(node: ts.Node): ts.Symbol | undefined { @@ -1451,7 +1482,7 @@ export default class CSharpEmitterContext { } if (symbol.name === 'iterator' && (!parent || parent.name === 'SymbolConstructor')) { - return this.toMethodName('getEnumerator'); + return this.toMethodNameCase('getEnumerator'); } return ''; @@ -1814,9 +1845,16 @@ export default class CSharpEmitterContext { if (contextualType.symbol) { switch (contextualType.symbol.name) { case 'ArrayLike': - case '__type': return true; } + + // empty object type {} (basically object) + if ( + contextualType.flags & ts.TypeFlags.Object && + (contextualType as ts.ObjectType).getProperties().length === 0 + ) { + return true; + } } return false; @@ -2101,7 +2139,10 @@ export default class CSharpEmitterContext { } public getDefaultUsings(): string[] { - return [this.toPascalCase('system'), `${this.toPascalCase('alphaTab')}.${this.toPascalCase('core')}`]; + return [ + this.toNamespaceNameCase('system'), + `${this.toNamespaceNameCase('alphaTab')}.${this.toNamespaceNameCase('core')}` + ]; } public buildMethodName(propertyName: ts.PropertyName) { @@ -2131,7 +2172,7 @@ export default class CSharpEmitterContext { this.addTsNodeDiagnostics(propertyName, 'Unsupported method name syntax', ts.DiagnosticCategory.Error); } - return this.toMethodName(methodName); + return this.toMethodNameCase(methodName); } public isSymbolArrayTupleInstance(expression: ts.Expression) { diff --git a/packages/transpiler/src/kotlin/KotlinAstPrinter.ts b/packages/transpiler/src/kotlin/KotlinAstPrinter.ts index 5f77d5643..e1da51960 100644 --- a/packages/transpiler/src/kotlin/KotlinAstPrinter.ts +++ b/packages/transpiler/src/kotlin/KotlinAstPrinter.ts @@ -241,6 +241,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { } protected writeInterfaceDeclaration(d: cs.InterfaceDeclaration) { + this._currentType = d; this.writeDocumentation(d); this.writeLine('@kotlin.contracts.ExperimentalContracts'); @@ -264,9 +265,11 @@ export default class KotlinAstPrinter extends AstPrinterBase { } this.endBlock(); + this._currentType = undefined; } protected writeEnumDeclaration(d: cs.EnumDeclaration) { + this._currentType = d; this._forceInteger = true; this.writeDocumentation(d); this.writeAttributes(d); @@ -332,9 +335,12 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.endBlock(); this._forceInteger = false; + this._currentType = undefined; } + private _currentType: cs.NamedTypeDeclaration | undefined = undefined; protected writeClassDeclaration(d: cs.ClassDeclaration) { + this._currentType = d; this.writeDocumentation(d); this.writeAttributes(d); this.writeLine('@kotlin.contracts.ExperimentalContracts'); @@ -465,6 +471,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { } this.endBlock(); + this._currentType = undefined; } protected writeAttribute(a: cs.Attribute): void { @@ -523,6 +530,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.write(' = iterator'); this._thisScope.push((d.parent as cs.NamedTypeDeclaration).name); } + this.writeBody(d.body); if (d.isGeneratorFunction) { this._thisScope.pop(); @@ -627,7 +635,9 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeType(d.type); const needsInitializer = - isAutoProperty && d.type.isNullable && d.parent!.nodeType !== cs.SyntaxKind.InterfaceDeclaration && + isAutoProperty && + d.type.isNullable && + d.parent!.nodeType !== cs.SyntaxKind.InterfaceDeclaration && !d.isAbstract; let initializerWritten = false; @@ -1372,6 +1382,25 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.write('('); this.writeCommaSeparated(expr.arguments, a => this.writeExpression(a)); this.write(')'); + + if (expr.objectInitializers?.length) { + this.write('.apply '); + this.beginBlock(); + + this._thisScope.push(this._currentType!.name); + + for (const a of expr.objectInitializers!) { + this.write('this.'); + this.write(a.label); + this.write(' = '); + this.writeExpression(a.expression); + this.writeLine(); + } + + this._thisScope.pop(); + + this.endBlock(); + } } protected writeCastExpression(expr: cs.CastExpression) { @@ -2043,6 +2072,14 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.write('>'); } this.writeBlock(expr.arguments[0] as cs.Block); + } else if ( + expr.arguments.length === 1 && + cs.isMemberAccessExpression(expr.expression) && + expr.expression.member === 'suspendToDeferred' + ) { + this._thisScope.push(this._currentType!.name); + super.writeInvocationExpression(expr); + this._thisScope.pop(); } else { super.writeInvocationExpression(expr); } diff --git a/packages/transpiler/src/kotlin/KotlinAstTransformer.ts b/packages/transpiler/src/kotlin/KotlinAstTransformer.ts index c2edc4464..ae7844e48 100644 --- a/packages/transpiler/src/kotlin/KotlinAstTransformer.ts +++ b/packages/transpiler/src/kotlin/KotlinAstTransformer.ts @@ -221,7 +221,7 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { } override visitCallExpression(parent: cs.Node, expression: ts.CallExpression) { - const invocation = super.visitCallExpression(parent, expression); + let invocation = super.visitCallExpression(parent, expression); if (!invocation) { return invocation; } @@ -252,7 +252,7 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { (body.right.text === 'undefined' || body.right.text === 'null'))) ) { (invocation.expression as cs.MemberAccessExpression).member = - this.context.toMethodName('filterNotNull'); + this.context.toMethodNameCase('filterNotNull'); invocation.arguments = []; } } @@ -272,10 +272,11 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { nodeType: cs.SyntaxKind.InvocationExpression } as cs.InvocationExpression; + suspendToDeferred.expression = this.makeMemberAccess( suspendToDeferred, this.context.makeTypeName('alphaTab.core.TypeHelper'), - this.context.toMethodName('suspendToDeferred') + this.context.toMethodNameCase('suspendToDeferred') ); suspendToDeferred.arguments = [ @@ -730,4 +731,64 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { return d; } + + protected override createDiscriminatedUnionClass( + node: ts.TypeAliasDeclaration, + className: string, + memberType: ts.Type, + baseInterface: cs.InterfaceDeclaration, + discriminatorField: string, + discriminatorValue: string + ): cs.ClassDeclaration { + const csClass = super.createDiscriminatedUnionClass( + node, + className, + memberType, + baseInterface, + discriminatorField, + discriminatorValue + ); + + // need initializers or late init + for (const p of csClass.members) { + if (cs.isPropertyDeclaration(p)) { + if (!p.initializer) { + const tsProp = p.tsNode as ts.PropertyDeclaration; + const type = this.context.getType(tsProp); + const isNullable = this.context.isNullableType(type); + if (type === this.context.typeChecker.getNumberType()) { + p.initializer = { + nodeType: cs.SyntaxKind.NumericLiteral, + parent: p, + value: '0.0' + } as cs.NumericLiteral; + } else if (type === this.context.typeChecker.getBooleanType()) { + p.initializer = { + nodeType: cs.SyntaxKind.FalseLiteral, + parent: p + } as cs.BooleanLiteral; + } else if (isNullable || type === this.context.typeChecker.getUnknownType()) { + // default null + p.initializer = { + nodeType: cs.SyntaxKind.NullLiteral, + parent: p + } as cs.NullLiteral; + } else if (!isNullable) { + // lateinit + p.initializer = { + nodeType: cs.SyntaxKind.NonNullExpression, + parent: p, + expression: { + nodeType: cs.SyntaxKind.NullLiteral + } as cs.NullLiteral + } as cs.NonNullExpression; + } + } else if (p.name === this.context.toPropertyNameCase(discriminatorField)) { + p.isOverride = true; + } + } + } + + return csClass; + } } diff --git a/packages/transpiler/src/kotlin/KotlinEmitterContext.ts b/packages/transpiler/src/kotlin/KotlinEmitterContext.ts index e77a87c9d..d4bf22b91 100644 --- a/packages/transpiler/src/kotlin/KotlinEmitterContext.ts +++ b/packages/transpiler/src/kotlin/KotlinEmitterContext.ts @@ -3,11 +3,6 @@ import * as cs from '../csharp/CSharpAst'; import CSharpEmitterContext from '../csharp/CSharpEmitterContext'; export default class KotlinEmitterContext extends CSharpEmitterContext { - public constructor(program: ts.Program, srcOutDir: string, testOutDir: string) { - super(program, srcOutDir, testOutDir); - this.noPascalCase = true; - } - public override get targetTag(): string { return 'kotlin'; } @@ -41,7 +36,7 @@ export default class KotlinEmitterContext extends CSharpEmitterContext { } public override getDefaultUsings(): string[] { - return [`${this.toPascalCase('alphaTab')}.${this.toPascalCase('core')}`]; + return [`${this.toNamespaceNameCase('alphaTab')}.${this.toNamespaceNameCase('core')}`]; } public override makeExceptionType(): string { @@ -77,7 +72,7 @@ export default class KotlinEmitterContext extends CSharpEmitterContext { } if (symbol.name === 'iterator' && (!parent || parent.name === 'SymbolConstructor')) { - return this.toMethodName('iterator'); + return this.toMethodNameCase('iterator'); } return ''; @@ -94,4 +89,20 @@ export default class KotlinEmitterContext extends CSharpEmitterContext { public override makeIteratorType(): string { return this.makeTypeName('kotlin.collections.Iterator'); } + + public override toMethodNameCase(text: string): string { + return this.toIdentifier(text); + } + + public override toPropertyNameCase(text: string): string { + return this.toIdentifier(text); + } + + public override toNamespaceNameCase(text: string): string { + return text; + } + + public override toTypeNameCase(text: string): string { + return this.toPascalCase(text); + } } diff --git a/packages/vite/package.json b/packages/vite/package.json index 0b17ba405..2040c4850 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-vite", - "version": "1.8.1", + "version": "1.9.0", "description": "A plugin for Vite to bundle alphaTab into your webapps.", "keywords": [ "guitar", @@ -43,23 +43,23 @@ }, "dependencies": { "magic-string": "^0.30.21", - "vite": "^7.3.1" + "vite": "^7.3.2" }, "engines": { "node": ">=20.19.0" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@microsoft/api-extractor": "^7.55.2", + "@biomejs/biome": "^2.4.10", + "@microsoft/api-extractor": "^7.57.7", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", + "@types/node": "^25.5.0", "assert": "^2.1.0", "chai": "^6.2.2", "mocha": "^11.7.5", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "rollup-plugin-node-externals": "^8.1.2", - "terser": "^5.44.1", + "terser": "^5.46.1", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 858c05f69..1fcd00f5c 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -1,6 +1,6 @@ { "name": "alphatab-vscode", - "version": "1.8.1", + "version": "1.9.0", "private": true, "description": "A Visual Studio Code extension for alphaTab providing coding assistance for alphaTex.", "keywords": [ @@ -34,19 +34,19 @@ "test": "vscode-test" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", + "@biomejs/biome": "^2.4.10", "@rollup/plugin-node-resolve": "^16.0.3", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", - "@types/vscode": "^1.108.1", + "@types/node": "^25.5.0", + "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "assert": "^2.1.0", "chai": "^6.2.2", "concurrently": "^9.2.1", "mocha": "^11.7.4", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3", diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 0ce37134b..c38c8cdc8 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-webpack", - "version": "1.8.1", + "version": "1.9.0", "description": "A plugin for WebPack to bundle alphaTab into your webapps.", "keywords": [ "guitar", @@ -42,25 +42,25 @@ "test": "mocha" }, "dependencies": { - "webpack": "^5.104.1" + "webpack": "^5.105.4" }, "engines": { "node": ">=20.19.0" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", + "@biomejs/biome": "^2.4.10", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^25.0.6", + "@types/node": "^25.5.0", "assert": "^2.1.0", "chai": "^6.2.2", - "html-webpack-plugin": "^5.6.5", + "html-webpack-plugin": "^5.6.6", "mocha": "^11.7.5", - "rimraf": "^6.1.2", + "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3", - "webpack-cli": "^6.0.1" + "webpack-cli": "^7.0.2" }, "files": [ "/dist/**", diff --git a/scripts/nightly.mts b/scripts/nightly.mts new file mode 100644 index 000000000..ab59d22f7 --- /dev/null +++ b/scripts/nightly.mts @@ -0,0 +1,191 @@ +import { type ExecSyncOptionsWithBufferEncoding, execSync } from 'node:child_process'; +import { appendFileSync, existsSync, readdirSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { exit } from 'node:process'; +import { type ParseArgsOptionsConfig, parseArgs } from 'node:util'; + +const options: ParseArgsOptionsConfig = { + platform: { + type: 'string' + }, + mode: { + type: 'string' + }, + force: { + type: 'string' + }, + tag: { + type: 'string' + } +}; + +function getChangedPackages(force: boolean) { + const packages = new Set(); + if (!force) { + // get all commits since 26 hours + const featureCommits = execSync( + "git log --since='26 hours ago' --pretty=format:'%H;%an <%ae>' --perl-regexp --author='^((?!dependabot).*)$'", + { + encoding: 'utf-8' + } + ) + .split('\n') + .filter(l => l.trim().length > 0) + .map(l => { + const p = l.split(';'); + return { hash: p[0], author: p[1] }; + }); + + console.info('Detected following feature commits', featureCommits); + if (featureCommits.length === 0) { + console.info('No feature commits, no packages changed'); + return packages; + } + + // translate commits to changed packages + for (const commit of featureCommits) { + const files = execSync(`git show --name-only ${commit.hash} --pretty=""`, { + encoding: 'utf-8' + }).split('\n'); + + for (const file of files) { + const packageName = file.split('/')[1]; + if (packageName) { + packages.add(packageName); + } + } + } + } else { + for (const pkg of readdirSync(resolve(import.meta.dirname, '../packages/'), { withFileTypes: true })) { + if (pkg.isDirectory()) { + packages.add(pkg.name); + } + } + } + + for (const packageName of Array.from(packages)) { + const packageJson = resolve(import.meta.dirname, '../packages/', packageName, 'package.json'); + if (!existsSync(packageJson)) { + packages.delete(packageName); + continue; + } + + const isPrivate = JSON.parse(readFileSync(packageJson, 'utf-8')).private; + if (isPrivate) { + packages.delete(packageName); + } + } + + console.info('Detected following changed packages', packages); + return packages; +} + +const DRY_RUN = process.env.DRY_RUN; +function execDryRun(command: string, options: ExecSyncOptionsWithBufferEncoding) { + console.info(`executing command ${command} in ${options.cwd}`); + if (DRY_RUN) { + console.warn('nothing executed, dry run active'); + } else { + execSync(command, options); + } +} + +function publish(platform: string, packages: Set) { + switch (platform) { + case 'web': + for (const packageName of packages) { + execDryRun(`npm publish --access public --tag ${args.tag}`, { + stdio: 'inherit', + cwd: resolve(import.meta.dirname, '../packages', packageName) + }); + } + break; + case 'dotnet': + const dotnetPackages = ['AlphaTab', 'AlphaTab.Windows']; + for (const pkg of dotnetPackages) { + execDryRun( + `dotnet nuget push ${pkg}/bin/Release/*.nupkg -k ${process.env.NUGET_API_KEY} -s https://api.nuget.org/v3/index.json`, + { + stdio: 'inherit', + cwd: resolve(import.meta.dirname, '../packages/csharp/src') + } + ); + } + break; + case 'kotlin': + execDryRun(`./gradlew publishToMavenCentral`, { + stdio: 'inherit', + cwd: resolve(import.meta.dirname, '../packages/kotlin/src/') + }); + break; + } +} + +function pack(platform: string, packages: Set) { + switch (platform) { + case 'web': + for (const packageName of packages) { + console.info('npm pack', packageName); + execSync('npm pack', { + stdio: 'inherit', + cwd: resolve(import.meta.dirname, '../packages', packageName) + }); + } + break; + case 'dotnet': + console.info('dotnet pack skipped, nupkgs are created automatically'); + break; + case 'kotlin': + console.info('kotlin pack skipped, there is no pack step'); + break; + } +} + +function check(platform: string, packages: Set) { + let hasChanges = false; + switch (platform) { + case 'web': + hasChanges = packages.size > 0; + break; + case 'dotnet': + hasChanges = packages.has('alphatab') || packages.has('csharp'); + if (!hasChanges) { + console.info('dotnet build skipped, no relevant package changed'); + } + break; + case 'kotlin': + hasChanges = packages.has('alphatab') || packages.has('kotlin'); + if (!hasChanges) { + console.info('kotlin build skipped, no relevant package changed'); + } + break; + } + + const hasChangesOutput = `has_changes=${hasChanges ? 'true' : 'false'}`; + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, hasChangesOutput); + } else { + console.info(hasChangesOutput); + } +} + +const args = parseArgs({ + options, + strict: true +}).values; +const packages = getChangedPackages(args.force === 'true'); + +switch (args.mode) { + case 'pack': + pack(args.platform as string, packages); + break; + case 'publish': + publish(args.platform as string, packages); + break; + case 'check': + check(args.platform as string, packages); + break; + default: + console.error('Invalid mode', args.mode); + exit(1); +}