diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f9d932b6b..d59e4c488 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: - run: npm run build-language-server - run: npm run build-monaco id: test - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 if: ${{ failure() && steps.test.conclusion == 'failure' }} with: name: test-results-web @@ -69,7 +69,7 @@ jobs: - run: npm run build-csharp - run: npm run test-csharp id: test - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 if: ${{ failure() && steps.test.conclusion == 'failure' }} with: name: test-results-csharp @@ -108,7 +108,7 @@ jobs: - run: npm run build-kotlin - run: npm run test-kotlin id: test - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 if: ${{ failure() && steps.test.conclusion == 'failure' }} with: name: test-results-kotlin diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a5f8e7292..441cf161d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: steps: - run: touch dummy.txt - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: cache with: path: dummy.txt diff --git a/package-lock.json b/package-lock.json index 942836673..9e488fd0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coderline/alphatab-monorepo", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@coderline/alphatab-monorepo", - "version": "1.7.0", + "version": "1.8.0", "workspaces": [ "packages/*" ], @@ -16,8 +16,6 @@ }, "node_modules/@ampproject/remapping": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -30,8 +28,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { @@ -45,8 +41,6 @@ }, "node_modules/@babel/compat-data": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "license": "MIT", "engines": { @@ -55,8 +49,6 @@ }, "node_modules/@babel/core": { "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", "peer": true, @@ -87,8 +79,6 @@ }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -97,8 +87,6 @@ }, "node_modules/@babel/generator": { "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { @@ -114,8 +102,6 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { @@ -131,8 +117,6 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -141,8 +125,6 @@ }, "node_modules/@babel/helper-globals": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", "engines": { @@ -151,8 +133,6 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "license": "MIT", "dependencies": { @@ -165,8 +145,6 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { @@ -183,8 +161,6 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { @@ -193,8 +169,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -203,8 +177,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { @@ -213,8 +185,6 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -223,8 +193,6 @@ }, "node_modules/@babel/helpers": { "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, "license": "MIT", "dependencies": { @@ -237,8 +205,6 @@ }, "node_modules/@babel/parser": { "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, "license": "MIT", "dependencies": { @@ -253,8 +219,6 @@ }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "license": "MIT", "dependencies": { @@ -266,8 +230,6 @@ }, "node_modules/@babel/plugin-syntax-bigint": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "license": "MIT", "dependencies": { @@ -279,8 +241,6 @@ }, "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "license": "MIT", "dependencies": { @@ -292,8 +252,6 @@ }, "node_modules/@babel/plugin-syntax-class-static-block": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, "license": "MIT", "dependencies": { @@ -308,8 +266,6 @@ }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, "license": "MIT", "dependencies": { @@ -324,8 +280,6 @@ }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, "license": "MIT", "dependencies": { @@ -337,8 +291,6 @@ }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "license": "MIT", "dependencies": { @@ -350,8 +302,6 @@ }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dev": true, "license": "MIT", "dependencies": { @@ -366,8 +316,6 @@ }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, "license": "MIT", "dependencies": { @@ -379,8 +327,6 @@ }, "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -392,8 +338,6 @@ }, "node_modules/@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "license": "MIT", "dependencies": { @@ -405,8 +349,6 @@ }, "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "license": "MIT", "dependencies": { @@ -418,8 +360,6 @@ }, "node_modules/@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -431,8 +371,6 @@ }, "node_modules/@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, "license": "MIT", "dependencies": { @@ -444,8 +382,6 @@ }, "node_modules/@babel/plugin-syntax-private-property-in-object": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, "license": "MIT", "dependencies": { @@ -460,8 +396,6 @@ }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "license": "MIT", "dependencies": { @@ -476,8 +410,6 @@ }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -492,8 +424,6 @@ }, "node_modules/@babel/template": { "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { @@ -507,8 +437,6 @@ }, "node_modules/@babel/traverse": { "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, "license": "MIT", "dependencies": { @@ -526,8 +454,6 @@ }, "node_modules/@babel/types": { "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -539,15 +465,15 @@ } }, "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/@biomejs/biome": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.6.tgz", - "integrity": "sha512-yKTCNGhek0rL5OEW1jbLeZX8LHaM8yk7+3JRGv08my+gkpmtb5dDE+54r2ZjZx0ediFEn1pYBOJSmOdDP9xtFw==", + "version": "2.3.11", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -561,88 +487,18 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.2.6", - "@biomejs/cli-darwin-x64": "2.2.6", - "@biomejs/cli-linux-arm64": "2.2.6", - "@biomejs/cli-linux-arm64-musl": "2.2.6", - "@biomejs/cli-linux-x64": "2.2.6", - "@biomejs/cli-linux-x64-musl": "2.2.6", - "@biomejs/cli-win32-arm64": "2.2.6", - "@biomejs/cli-win32-x64": "2.2.6" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.6.tgz", - "integrity": "sha512-UZPmn3M45CjTYulgcrFJFZv7YmK3pTxTJDrFYlNElT2FNnkkX4fsxjExTSMeWKQYoZjvekpH5cvrYZZlWu3yfA==", - "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.2.6", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.6.tgz", - "integrity": "sha512-HOUIquhHVgh/jvxyClpwlpl/oeMqntlteL89YqjuFDiZ091P0vhHccwz+8muu3nTyHWM5FQslt+4Jdcd67+xWQ==", - "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.2.6", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.6.tgz", - "integrity": "sha512-BpGtuMJGN+o8pQjvYsUKZ+4JEErxdSmcRD/JG3mXoWc6zrcA7OkuyGFN1mDggO0Q1n7qXxo/PcupHk8gzijt5g==", - "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.2.6", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.6.tgz", - "integrity": "sha512-TjCenQq3N6g1C+5UT3jE1bIiJb5MWQvulpUngTIpFsL4StVAUXucWD0SL9MCW89Tm6awWfeXBbZBAhJwjyFbRQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" + "@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" } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.6.tgz", - "integrity": "sha512-1HaM/dpI/1Z68zp8ZdT6EiBq+/O/z97a2AiHMl+VAdv5/ELckFt9EvRb8hDHpk8hUMoz03gXkC7VPXOVtU7faA==", + "version": "2.3.11", "cpu": [ "x64" ], @@ -657,9 +513,7 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.6.tgz", - "integrity": "sha512-1ZcBux8zVM3JhWN2ZCPaYf0+ogxXG316uaoXJdgoPZcdK/rmRcRY7PqHdAos2ExzvjIdvhQp72UcveI98hgOog==", + "version": "2.3.11", "cpu": [ "x64" ], @@ -673,44 +527,8 @@ "node": ">=14.21.3" } }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.6.tgz", - "integrity": "sha512-h3A88G8PGM1ryTeZyLlSdfC/gz3e95EJw9BZmA6Po412DRqwqPBa2Y9U+4ZSGUAXCsnSQE00jLV8Pyrh0d+jQw==", - "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.2.6", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.6.tgz", - "integrity": "sha512-yx0CqeOhPjYQ5ZXgPfu8QYkgBhVJyvWe36as7jRuPrKPO5ylVDfwVtPQ+K/mooNTADW0IhxOZm3aPu16dP8yNQ==", - "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", - "resolved": "https://registry.npmjs.org/@coderline/alphaskia/-/alphaskia-3.4.135.tgz", - "integrity": "sha512-vKDE9cyC0BhFuMAPaQUryPD+rnbcP4J43bp69mxd3kM0hnpaDubpDcP5lwSlhfkEzkTQDD+a8kk5letJ4Jeu1g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -724,8 +542,6 @@ }, "node_modules/@coderline/alphaskia-linux": { "version": "3.4.135", - "resolved": "https://registry.npmjs.org/@coderline/alphaskia-linux/-/alphaskia-linux-3.4.135.tgz", - "integrity": "sha512-vApv3lijxD034eKUwR1QwUJOWFh3ztmdKk40JQ3iOyAcHqrKQqgLhJXFSrPH4EcZkaxFklhbezWDbrDz2DYAtw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -734,8 +550,6 @@ }, "node_modules/@coderline/alphaskia-macos": { "version": "3.4.135", - "resolved": "https://registry.npmjs.org/@coderline/alphaskia-macos/-/alphaskia-macos-3.4.135.tgz", - "integrity": "sha512-UuLF0cO4Hyoqm3F/eHEZinaZbJyMSIr6fFN1mBz/gs/ENWOh5WSLJl6HQr1eEmXfJ6DOyS8+BMnaPV6Pi0DYbA==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -745,8 +559,6 @@ }, "node_modules/@coderline/alphaskia-windows": { "version": "3.4.135", - "resolved": "https://registry.npmjs.org/@coderline/alphaskia-windows/-/alphaskia-windows-3.4.135.tgz", - "integrity": "sha512-6LqOcHcGo5Jo3eSg3rRYFNzNV2MExyypPPrfigz15UxCAejb7lvIaqgTeIejYWtHSiZiyOLCjG/bxkFpN1uoeA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -799,274 +611,14 @@ }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", - "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", "dev": true, "license": "MIT", "engines": { "node": ">=14.17.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.27.2", "cpu": [ "x64" ], @@ -1079,163 +631,15 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@fontsource/noto-sans": { "version": "5.2.10", - "resolved": "https://registry.npmjs.org/@fontsource/noto-sans/-/noto-sans-5.2.10.tgz", - "integrity": "sha512-J58RVfS/C0Z2VBF+PoU260Tx8cdRGYuS+e3yQe4hYaIYDl0sEVn5CzlLo5zVRvQD0HaIUTV8AZMfqR7rtdEpqQ==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" } }, "node_modules/@fontsource/noto-serif": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource/noto-serif/-/noto-serif-5.2.8.tgz", - "integrity": "sha512-q8iGVFYAj/13OicYggr0+2gtL0a4CDl+24JIc+B+71RUSRtGcKz90oz9GMvjcO7jxiDGpZfm98yBmmyF+rmbDQ==", + "version": "5.2.9", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -1243,8 +647,6 @@ }, "node_modules/@fortawesome/fontawesome-free": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz", - "integrity": "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==", "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" @@ -1252,8 +654,6 @@ }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", "dev": true, "license": "MIT", "engines": { @@ -1262,8 +662,6 @@ }, "node_modules/@isaacs/brace-expansion": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", "dev": true, "license": "MIT", "dependencies": { @@ -1275,8 +673,6 @@ }, "node_modules/@isaacs/cliui": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "license": "ISC", "dependencies": { @@ -1293,8 +689,6 @@ }, "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": { @@ -1310,8 +704,6 @@ }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "license": "MIT", "engines": { @@ -1320,8 +712,6 @@ }, "node_modules/@jest/diff-sequences": { "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", "dev": true, "license": "MIT", "engines": { @@ -1330,8 +720,6 @@ }, "node_modules/@jest/expect-utils": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", - "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", "dev": true, "license": "MIT", "dependencies": { @@ -1343,8 +731,6 @@ }, "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": { @@ -1353,8 +739,6 @@ }, "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": { @@ -1367,8 +751,6 @@ }, "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": { @@ -1380,8 +762,6 @@ }, "node_modules/@jest/snapshot-utils": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", - "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", "dev": true, "license": "MIT", "dependencies": { @@ -1396,8 +776,6 @@ }, "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": { @@ -1412,8 +790,6 @@ }, "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": { @@ -1429,8 +805,6 @@ }, "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": { @@ -1442,8 +816,6 @@ }, "node_modules/@jest/transform": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "dev": true, "license": "MIT", "dependencies": { @@ -1469,8 +841,6 @@ }, "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": { @@ -1485,8 +855,6 @@ }, "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": { @@ -1502,8 +870,6 @@ }, "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": { @@ -1515,8 +881,6 @@ }, "node_modules/@jest/types": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "license": "MIT", "dependencies": { @@ -1534,8 +898,6 @@ }, "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": { @@ -1550,8 +912,6 @@ }, "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": { @@ -1567,8 +927,6 @@ }, "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": { @@ -1580,8 +938,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1590,8 +946,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1599,8 +953,6 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1609,14 +961,10 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1624,19 +972,18 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.53.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.53.3.tgz", - "integrity": "sha512-p2HmQaMSVqMBj3bH3643f8xApKAqrF1jNpPsMCTQOYCYgfwLnvzsve8c+bgBWzCOBBgLK54PB6ZLIWMGLg8CZA==", + "version": "7.55.2", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.31.3", - "@microsoft/tsdoc": "~0.15.1", - "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.18.0", + "@microsoft/api-extractor-model": "7.32.2", + "@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.3", - "@rushstack/ts-command-line": "5.1.3", + "@rushstack/terminal": "0.19.5", + "@rushstack/ts-command-line": "5.1.5", + "diff": "~8.0.2", "lodash": "~4.17.15", "minimatch": "10.0.3", "resolve": "~1.22.1", @@ -1649,21 +996,25 @@ } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.31.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.31.3.tgz", - "integrity": "sha512-dv4quQI46p0U03TCEpasUf6JrJL3qjMN7JUAobsPElxBv4xayYYvWW9aPpfYV+Jx6hqUcVaLVOeV7+5hxsyoFQ==", + "version": "7.32.2", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "~0.15.1", - "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.18.0" + "@microsoft/tsdoc": "~0.16.0", + "@microsoft/tsdoc-config": "~0.18.0", + "@rushstack/node-core-library": "5.19.1" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/diff": { + "version": "8.0.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" } }, "node_modules/@microsoft/api-extractor/node_modules/typescript": { "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1675,20 +1026,16 @@ } }, "node_modules/@microsoft/tsdoc": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", - "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "version": "0.16.0", "dev": true, "license": "MIT" }, "node_modules/@microsoft/tsdoc-config": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.1.tgz", - "integrity": "sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==", + "version": "0.18.0", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "0.15.1", + "@microsoft/tsdoc": "0.16.0", "ajv": "~8.12.0", "jju": "~1.4.0", "resolve": "~1.22.2" @@ -1696,8 +1043,6 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "license": "MIT", "optional": true, @@ -1707,8 +1052,6 @@ }, "node_modules/@pkgr/core": { "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, "license": "MIT", "engines": { @@ -1720,8 +1063,6 @@ }, "node_modules/@popperjs/core": { "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", "peer": true, "funding": { @@ -1731,9 +1072,8 @@ }, "node_modules/@rollup/plugin-node-resolve": { "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", - "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", @@ -1755,8 +1095,6 @@ }, "node_modules/@rollup/plugin-terser": { "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1778,8 +1116,6 @@ }, "node_modules/@rollup/plugin-typescript": { "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", - "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==", "dev": true, "license": "MIT", "dependencies": { @@ -1801,230 +1137,31 @@ "tslib": { "optional": true } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", - "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.0.tgz", - "integrity": "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.0.tgz", - "integrity": "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.0.tgz", - "integrity": "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.0.tgz", - "integrity": "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.0.tgz", - "integrity": "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.0.tgz", - "integrity": "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.0.tgz", - "integrity": "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.0.tgz", - "integrity": "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.0.tgz", - "integrity": "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.0.tgz", - "integrity": "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.0.tgz", - "integrity": "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.0.tgz", - "integrity": "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.0.tgz", - "integrity": "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.0.tgz", - "integrity": "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.0.tgz", - "integrity": "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==", - "cpu": [ - "s390x" - ], + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.0.tgz", - "integrity": "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==", "cpu": [ "x64" ], @@ -2036,8 +1173,6 @@ }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.0.tgz", - "integrity": "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==", "cpu": [ "x64" ], @@ -2047,62 +1182,8 @@ "linux" ] }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.0.tgz", - "integrity": "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.0.tgz", - "integrity": "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.0.tgz", - "integrity": "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.0.tgz", - "integrity": "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rushstack/node-core-library": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.18.0.tgz", - "integrity": "sha512-XDebtBdw5S3SuZIt+Ra2NieT8kQ3D2Ow1HxhDQ/2soinswnOu9e7S69VSwTOLlQnx5mpWbONu+5JJjDxMAb6Fw==", + "version": "5.19.1", "dev": true, "license": "MIT", "dependencies": { @@ -2126,8 +1207,6 @@ }, "node_modules/@rushstack/node-core-library/node_modules/ajv": { "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", - "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", "dev": true, "license": "MIT", "dependencies": { @@ -2143,8 +1222,6 @@ }, "node_modules/@rushstack/problem-matcher": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@rushstack/problem-matcher/-/problem-matcher-0.1.1.tgz", - "integrity": "sha512-Fm5XtS7+G8HLcJHCWpES5VmeMyjAKaWeyZU5qPzZC+22mPlJzAsOxymHiWIfuirtPckX3aptWws+K2d0BzniJA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2158,8 +1235,6 @@ }, "node_modules/@rushstack/rig-package": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.6.0.tgz", - "integrity": "sha512-ZQmfzsLE2+Y91GF15c65L/slMRVhF6Hycq04D4TwtdGaUAbIXXg9c5pKA5KFU7M4QMaihoobp9JJYpYcaY3zOw==", "dev": true, "license": "MIT", "dependencies": { @@ -2168,13 +1243,11 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.19.3", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.19.3.tgz", - "integrity": "sha512-0P8G18gK9STyO+CNBvkKPnWGMxESxecTYqOcikHOVIHXa9uAuTK+Fw8TJq2Gng1w7W6wTC9uPX6hGNvrMll2wA==", + "version": "0.19.5", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.18.0", + "@rushstack/node-core-library": "5.19.1", "@rushstack/problem-matcher": "0.1.1", "supports-color": "~8.1.1" }, @@ -2188,13 +1261,11 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.1.3.tgz", - "integrity": "sha512-Kdv0k/BnnxIYFlMVC1IxrIS0oGQd4T4b7vKfx52Y2+wk2WZSDFIvedr7JrhenzSlm3ou5KwtoTGTGd5nbODRug==", + "version": "5.1.5", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.19.3", + "@rushstack/terminal": "0.19.5", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -2202,22 +1273,16 @@ }, "node_modules/@sinclair/typebox": { "version": "0.34.41", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", - "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "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" }, "node_modules/@types/bootstrap": { "version": "5.2.10", - "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz", - "integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -2226,8 +1291,6 @@ }, "node_modules/@types/chai": { "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -2237,15 +1300,11 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/eslint": { "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", "dependencies": { "@types/estree": "*", @@ -2254,8 +1313,6 @@ }, "node_modules/@types/eslint-scope": { "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", "dependencies": { "@types/eslint": "*", @@ -2264,33 +1321,24 @@ }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/http-errors": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true, "license": "MIT" }, "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": { @@ -2299,8 +1347,6 @@ }, "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": { @@ -2309,36 +1355,29 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, "node_modules/@types/mocha": { "version": "10.0.10", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", - "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", - "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "version": "25.0.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.6.tgz", + "integrity": "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==", "license": "MIT", "dependencies": { - "undici-types": "~7.14.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/resolve": { "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/serve-static": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -2347,22 +1386,23 @@ }, "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" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "license": "MIT", + "optional": true + }, "node_modules/@types/vscode": { - "version": "1.106.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.106.1.tgz", - "integrity": "sha512-R/HV8u2h8CAddSbX8cjpdd7B8/GnE4UjgjpuGuHcbp1xV6yh4OeqU4L1pKjlwujCrSFS0MOpwJAIs/NexMB1fQ==", + "version": "1.108.1", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.108.1.tgz", + "integrity": "sha512-DerV0BbSzt87TbrqmZ7lRDIYaMiqvP8tmJTzW2p49ZBVtGUnGAu2RGQd1Wv4XMzEVUpaHbsemVM5nfuQJj7H6w==", "dev": true, "license": "MIT" }, "node_modules/@types/yargs": { "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "license": "MIT", "dependencies": { @@ -2371,32 +1411,27 @@ }, "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" }, "node_modules/@vscode/test-cli": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.11.tgz", - "integrity": "sha512-qO332yvzFqGhBMJrp6TdwbIydiHgCtxXc2Nl6M58mbH/Z+0CyLR76Jzv4YWPEthhrARprzCRJUqzFvTHFhTj7Q==", + "version": "0.0.12", "dev": true, + "license": "MIT", "dependencies": { - "@types/mocha": "^10.0.2", - "c8": "^9.1.0", - "chokidar": "^3.5.3", - "enhanced-resolve": "^5.15.0", + "@types/mocha": "^10.0.10", + "c8": "^10.1.3", + "chokidar": "^3.6.0", + "enhanced-resolve": "^5.18.3", "glob": "^10.3.10", "minimatch": "^9.0.3", - "mocha": "^11.1.0", - "supports-color": "^9.4.0", + "mocha": "^11.7.4", + "supports-color": "^10.2.2", "yargs": "^17.7.2" }, "bin": { @@ -2408,9 +1443,8 @@ }, "node_modules/@vscode/test-cli/node_modules/chokidar": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2432,9 +1466,8 @@ }, "node_modules/@vscode/test-cli/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2447,9 +1480,8 @@ }, "node_modules/@vscode/test-cli/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -2459,9 +1491,8 @@ }, "node_modules/@vscode/test-cli/node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -2470,12 +1501,11 @@ } }, "node_modules/@vscode/test-cli/node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "version": "10.2.2", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/supports-color?sponsor=1" @@ -2483,9 +1513,8 @@ }, "node_modules/@vscode/test-electron": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", - "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", "dev": true, + "license": "MIT", "dependencies": { "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", @@ -2499,9 +1528,8 @@ }, "node_modules/@vscode/test-electron/node_modules/semver": { "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2511,8 +1539,6 @@ }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", @@ -2521,26 +1547,18 @@ }, "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", @@ -2550,14 +1568,10 @@ }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -2568,8 +1582,6 @@ }, "node_modules/@webassemblyjs/ieee754": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" @@ -2577,8 +1589,6 @@ }, "node_modules/@webassemblyjs/leb128": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" @@ -2586,14 +1596,10 @@ }, "node_modules/@webassemblyjs/utf8": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -2608,8 +1614,6 @@ }, "node_modules/@webassemblyjs/wasm-gen": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -2621,8 +1625,6 @@ }, "node_modules/@webassemblyjs/wasm-opt": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -2633,8 +1635,6 @@ }, "node_modules/@webassemblyjs/wasm-parser": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -2647,8 +1647,6 @@ }, "node_modules/@webassemblyjs/wast-printer": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -2657,8 +1655,6 @@ }, "node_modules/@webpack-cli/configtest": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", - "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", "dev": true, "license": "MIT", "engines": { @@ -2671,8 +1667,6 @@ }, "node_modules/@webpack-cli/info": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", - "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", "dev": true, "license": "MIT", "engines": { @@ -2685,8 +1679,6 @@ }, "node_modules/@webpack-cli/serve": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", - "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", "dev": true, "license": "MIT", "engines": { @@ -2704,20 +1696,14 @@ }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "license": "Apache-2.0" }, "node_modules/acorn": { "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "peer": true, "bin": { @@ -2729,8 +1715,6 @@ }, "node_modules/acorn-import-phases": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -2741,17 +1725,14 @@ }, "node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/ajv": { "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "license": "MIT", "peer": true, "dependencies": { @@ -2767,8 +1748,6 @@ }, "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": { @@ -2782,8 +1761,6 @@ }, "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": { @@ -2800,8 +1777,6 @@ }, "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" @@ -2816,8 +1791,6 @@ }, "node_modules/ansi-regex": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", "dev": true, "license": "MIT", "engines": { @@ -2829,8 +1802,6 @@ }, "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": { @@ -2842,8 +1813,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "license": "ISC", "dependencies": { @@ -2856,8 +1825,6 @@ }, "node_modules/anymatch/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { @@ -2869,8 +1836,6 @@ }, "node_modules/argparse": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { @@ -2879,8 +1844,6 @@ }, "node_modules/array-find-index": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", "dev": true, "license": "MIT", "engines": { @@ -2889,8 +1852,6 @@ }, "node_modules/assert": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", "dev": true, "license": "MIT", "dependencies": { @@ -2903,8 +1864,6 @@ }, "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -2913,8 +1872,6 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2929,8 +1886,6 @@ }, "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": [ @@ -2949,8 +1904,6 @@ }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, "license": "MIT", "dependencies": { @@ -2976,15 +1929,18 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.7", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { @@ -2996,14 +1952,11 @@ }, "node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/bootstrap": { "version": "5.3.8", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", - "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", "funding": [ { "type": "github", @@ -3021,8 +1974,6 @@ }, "node_modules/brace-expansion": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3031,8 +1982,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { @@ -3044,15 +1993,11 @@ }, "node_modules/browser-stdout": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true, "license": "ISC" }, "node_modules/browserslist": { - "version": "4.25.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", - "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "version": "4.28.1", "funding": [ { "type": "opencollective", @@ -3070,10 +2015,11 @@ "license": "MIT", "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001737", - "electron-to-chromium": "^1.5.211", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -3084,8 +2030,6 @@ }, "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": { @@ -3094,24 +2038,21 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, "node_modules/c8": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", - "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", + "version": "10.1.3", "dev": true, + "license": "ISC", "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", + "@bcoe/v8-coverage": "^1.0.1", "@istanbuljs/schema": "^0.1.3", "find-up": "^5.0.0", "foreground-child": "^3.1.1", "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.1.6", - "test-exclude": "^6.0.0", + "test-exclude": "^7.0.1", "v8-to-istanbul": "^9.0.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1" @@ -3120,14 +2061,21 @@ "c8": "bin/c8.js" }, "engines": { - "node": ">=14.14.0" + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } } }, "node_modules/c8/node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3141,9 +2089,8 @@ }, "node_modules/c8/node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -3154,11 +2101,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/c8/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/c8/node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -3171,9 +2131,8 @@ }, "node_modules/c8/node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -3184,10 +2143,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/c8/node_modules/test-exclude": { + "version": "7.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/call-bind": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { @@ -3205,8 +2175,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3219,8 +2187,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { @@ -3236,9 +2202,8 @@ }, "node_modules/camel-case": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", "dev": true, + "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -3246,8 +2211,6 @@ }, "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": { @@ -3255,9 +2218,7 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001739", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", - "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "version": "1.0.30001760", "funding": [ { "type": "opencollective", @@ -3275,9 +2236,7 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "version": "6.2.2", "dev": true, "license": "MIT", "engines": { @@ -3286,8 +2245,6 @@ }, "node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { @@ -3299,8 +2256,6 @@ }, "node_modules/chokidar": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { @@ -3315,8 +2270,6 @@ }, "node_modules/chrome-trace-event": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", "engines": { "node": ">=6.0" @@ -3324,8 +2277,6 @@ }, "node_modules/ci-info": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", "dev": true, "funding": [ { @@ -3340,9 +2291,8 @@ }, "node_modules/clean-css": { "version": "5.3.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", - "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dev": true, + "license": "MIT", "dependencies": { "source-map": "~0.6.0" }, @@ -3352,9 +2302,8 @@ }, "node_modules/cli-cursor": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" }, @@ -3367,9 +2316,8 @@ }, "node_modules/cli-spinners": { "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -3379,8 +2327,6 @@ }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3394,8 +2340,6 @@ }, "node_modules/cliui/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -3404,8 +2348,6 @@ }, "node_modules/cliui/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": { @@ -3420,15 +2362,11 @@ }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -3442,8 +2380,6 @@ }, "node_modules/cliui/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -3455,8 +2391,6 @@ }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3473,8 +2407,6 @@ }, "node_modules/clone-deep": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3488,8 +2420,6 @@ }, "node_modules/clone-deep/node_modules/is-plain-object": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "license": "MIT", "dependencies": { @@ -3501,8 +2431,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3514,38 +2442,29 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/commander": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/commenting": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/commenting/-/commenting-1.1.0.tgz", - "integrity": "sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA==", "dev": true, "license": "MIT" }, "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" }, "node_modules/concurrently": { "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, "license": "MIT", "dependencies": { @@ -3569,8 +2488,6 @@ }, "node_modules/concurrently/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": { @@ -3585,8 +2502,6 @@ }, "node_modules/concurrently/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": { @@ -3602,8 +2517,6 @@ }, "node_modules/concurrently/node_modules/chalk/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": { @@ -3615,21 +2528,16 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/core-util-is": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -3643,9 +2551,8 @@ }, "node_modules/css-select": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", @@ -3659,9 +2566,8 @@ }, "node_modules/css-what": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -3671,8 +2577,6 @@ }, "node_modules/debug": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3688,8 +2592,6 @@ }, "node_modules/decamelize": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, "license": "MIT", "engines": { @@ -3701,17 +2603,14 @@ }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { @@ -3728,8 +2627,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -3746,8 +2643,6 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -3755,8 +2650,6 @@ }, "node_modules/diff": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -3765,18 +2658,16 @@ }, "node_modules/dom-converter": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "dev": true, + "license": "MIT", "dependencies": { "utila": "~0.4" } }, "node_modules/dom-serializer": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "dev": true, + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -3788,21 +2679,19 @@ }, "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" }, @@ -3814,16 +2703,16 @@ } }, "node_modules/dompurify": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", - "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)" + "version": "3.2.7", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/domutils": { "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -3835,9 +2724,8 @@ }, "node_modules/dot-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dev": true, + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -3845,8 +2733,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { @@ -3860,34 +2746,24 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.214", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz", - "integrity": "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==", + "version": "1.5.267", "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -3895,8 +2771,6 @@ }, "node_modules/enhanced-resolve": { "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -3908,17 +2782,14 @@ }, "node_modules/entities": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true, + "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/envinfo": { "version": "7.14.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", - "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "dev": true, "license": "MIT", "bin": { @@ -3930,8 +2801,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { @@ -3940,8 +2809,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -3949,15 +2816,11 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -3968,9 +2831,7 @@ } }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.27.2", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3980,38 +2841,36 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -4019,14 +2878,10 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -4038,8 +2893,6 @@ }, "node_modules/eslint-scope": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -4051,8 +2904,6 @@ }, "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": { @@ -4065,8 +2916,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -4077,8 +2926,6 @@ }, "node_modules/esrecurse/node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -4086,8 +2933,6 @@ }, "node_modules/estraverse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -4095,15 +2940,11 @@ }, "node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, "license": "MIT" }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -4111,8 +2952,6 @@ }, "node_modules/events": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", "engines": { "node": ">=0.8.x" @@ -4120,8 +2959,6 @@ }, "node_modules/expect": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", "dev": true, "license": "MIT", "dependencies": { @@ -4138,21 +2975,15 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "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/fastest-levenshtein": { "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, "license": "MIT", "engines": { @@ -4161,8 +2992,6 @@ }, "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": { @@ -4171,8 +3000,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", "engines": { "node": ">=12.0.0" @@ -4188,8 +3015,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -4201,8 +3026,6 @@ }, "node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", "dependencies": { @@ -4215,8 +3038,6 @@ }, "node_modules/flat": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, "license": "BSD-3-Clause", "bin": { @@ -4225,8 +3046,6 @@ }, "node_modules/for-each": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { @@ -4241,8 +3060,6 @@ }, "node_modules/foreground-child": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { @@ -4258,8 +3075,6 @@ }, "node_modules/fresh": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -4267,8 +3082,6 @@ }, "node_modules/fs-extra": { "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", "dev": true, "license": "MIT", "dependencies": { @@ -4282,8 +3095,6 @@ }, "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" }, @@ -4303,8 +3114,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -4313,8 +3122,6 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { @@ -4323,8 +3130,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { @@ -4333,9 +3138,8 @@ }, "node_modules/get-east-asian-width": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -4345,8 +3149,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4370,8 +3172,6 @@ }, "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": { @@ -4380,8 +3180,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { @@ -4394,8 +3192,6 @@ }, "node_modules/get-tsconfig": { "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4406,9 +3202,7 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", "dev": true, "license": "ISC", "dependencies": { @@ -4428,8 +3222,6 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -4441,14 +3233,10 @@ }, "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": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -4463,15 +3251,11 @@ }, "node_modules/globrex": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true, "license": "MIT" }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { @@ -4483,14 +3267,10 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/handlebars": { "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "license": "MIT", "dependencies": { "minimist": "^1.2.5", @@ -4510,8 +3290,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { "node": ">=8" @@ -4519,8 +3297,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { @@ -4532,8 +3308,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -4545,8 +3319,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { @@ -4561,8 +3333,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4574,8 +3344,6 @@ }, "node_modules/he": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, "license": "MIT", "bin": { @@ -4584,15 +3352,13 @@ }, "node_modules/html-escaper": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/html-minifier-terser": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", "dev": true, + "license": "MIT", "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", @@ -4611,9 +3377,8 @@ }, "node_modules/html-webpack-plugin": { "version": "5.6.5", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz", - "integrity": "sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g==", "dev": true, + "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", @@ -4643,8 +3408,6 @@ }, "node_modules/htmlparser2": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -4653,6 +3416,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", @@ -4662,8 +3426,6 @@ }, "node_modules/http-errors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -4678,8 +3440,6 @@ }, "node_modules/http-errors/node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -4687,9 +3447,8 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -4700,9 +3459,8 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -4713,14 +3471,11 @@ }, "node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true + "dev": true, + "license": "MIT" }, "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": { @@ -4729,8 +3484,6 @@ }, "node_modules/import-local": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "license": "MIT", "dependencies": { @@ -4749,8 +3502,6 @@ }, "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": { @@ -4759,9 +3510,6 @@ }, "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": { @@ -4771,14 +3519,10 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/interpret": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, "license": "MIT", "engines": { @@ -4787,8 +3531,6 @@ }, "node_modules/is-arguments": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "dev": true, "license": "MIT", "dependencies": { @@ -4804,8 +3546,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", "dependencies": { @@ -4817,8 +3557,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", "engines": { @@ -4830,8 +3568,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -4846,8 +3582,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -4856,8 +3590,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -4866,8 +3598,6 @@ }, "node_modules/is-generator-function": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4885,8 +3615,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -4898,9 +3626,8 @@ }, "node_modules/is-interactive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -4910,14 +3637,11 @@ }, "node_modules/is-module": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-nan": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", "dev": true, "license": "MIT", "dependencies": { @@ -4933,8 +3657,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -4943,8 +3665,6 @@ }, "node_modules/is-path-inside": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, "license": "MIT", "engines": { @@ -4953,8 +3673,6 @@ }, "node_modules/is-plain-obj": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, "license": "MIT", "engines": { @@ -4963,8 +3681,6 @@ }, "node_modules/is-regex": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { @@ -4982,8 +3698,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4998,8 +3712,6 @@ }, "node_modules/is-unicode-supported": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, "license": "MIT", "engines": { @@ -5011,21 +3723,16 @@ }, "node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, "license": "MIT", "engines": { @@ -5034,8 +3741,6 @@ }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -5044,8 +3749,6 @@ }, "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": { @@ -5061,9 +3764,8 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -5075,9 +3777,8 @@ }, "node_modules/istanbul-lib-report/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": { "has-flag": "^4.0.0" }, @@ -5087,9 +3788,8 @@ }, "node_modules/istanbul-reports": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -5100,8 +3800,6 @@ }, "node_modules/jackspeak": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -5116,8 +3814,6 @@ }, "node_modules/jest-diff": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", "dev": true, "license": "MIT", "dependencies": { @@ -5132,8 +3828,6 @@ }, "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": { @@ -5148,8 +3842,6 @@ }, "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": { @@ -5165,8 +3857,6 @@ }, "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": { @@ -5178,8 +3868,6 @@ }, "node_modules/jest-haste-map": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", "dev": true, "license": "MIT", "dependencies": { @@ -5203,8 +3891,6 @@ }, "node_modules/jest-matcher-utils": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", "dev": true, "license": "MIT", "dependencies": { @@ -5219,8 +3905,6 @@ }, "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": { @@ -5235,8 +3919,6 @@ }, "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": { @@ -5252,8 +3934,6 @@ }, "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": { @@ -5265,8 +3945,6 @@ }, "node_modules/jest-message-util": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "dev": true, "license": "MIT", "dependencies": { @@ -5286,8 +3964,6 @@ }, "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": { @@ -5302,8 +3978,6 @@ }, "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": { @@ -5319,8 +3993,6 @@ }, "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": { @@ -5332,8 +4004,6 @@ }, "node_modules/jest-mock": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "dev": true, "license": "MIT", "dependencies": { @@ -5347,8 +4017,6 @@ }, "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": { @@ -5357,8 +4025,6 @@ }, "node_modules/jest-snapshot": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", - "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", "dev": true, "license": "MIT", "dependencies": { @@ -5390,8 +4056,6 @@ }, "node_modules/jest-snapshot/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": { @@ -5406,8 +4070,6 @@ }, "node_modules/jest-snapshot/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": { @@ -5423,8 +4085,6 @@ }, "node_modules/jest-snapshot/node_modules/semver": { "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -5436,8 +4096,6 @@ }, "node_modules/jest-snapshot/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": { @@ -5449,8 +4107,6 @@ }, "node_modules/jest-util": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, "license": "MIT", "dependencies": { @@ -5467,8 +4123,6 @@ }, "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": { @@ -5483,8 +4137,6 @@ }, "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": { @@ -5500,8 +4152,6 @@ }, "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": { @@ -5513,8 +4163,6 @@ }, "node_modules/jest-worker": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "dev": true, "license": "MIT", "dependencies": { @@ -5530,22 +4178,16 @@ }, "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" }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", "dev": true, "license": "MIT", "dependencies": { @@ -5558,8 +4200,6 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -5571,20 +4211,14 @@ }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -5596,8 +4230,6 @@ }, "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": { @@ -5609,9 +4241,8 @@ }, "node_modules/jszip": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -5621,8 +4252,6 @@ }, "node_modules/kind-of": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "license": "MIT", "engines": { @@ -5631,26 +4260,25 @@ }, "node_modules/lie": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dev": true, + "license": "MIT", "dependencies": { "immediate": "~3.0.5" } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", "dependencies": { @@ -5662,15 +4290,11 @@ }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true, "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "license": "MIT", "dependencies": { @@ -5686,8 +4310,6 @@ }, "node_modules/log-symbols/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": { @@ -5702,8 +4324,6 @@ }, "node_modules/log-symbols/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": { @@ -5719,8 +4339,6 @@ }, "node_modules/log-symbols/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": { @@ -5732,17 +4350,14 @@ }, "node_modules/lower-case": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.0.3" } }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", "dependencies": { @@ -5751,17 +4366,15 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -5774,8 +4387,6 @@ }, "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": { @@ -5784,8 +4395,6 @@ }, "node_modules/marked": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", - "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -5796,8 +4405,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -5806,14 +4413,10 @@ }, "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -5826,8 +4429,6 @@ }, "node_modules/micromatch/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { @@ -5839,8 +4440,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -5848,8 +4447,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -5860,9 +4457,8 @@ }, "node_modules/mimic-function": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -5872,8 +4468,6 @@ }, "node_modules/minimatch": { "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, "license": "ISC", "dependencies": { @@ -5888,8 +4482,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5897,8 +4489,6 @@ }, "node_modules/minipass": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "license": "ISC", "engines": { @@ -5907,8 +4497,6 @@ }, "node_modules/mocha": { "version": "11.7.5", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", - "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "dev": true, "license": "MIT", "dependencies": { @@ -5944,15 +4532,11 @@ }, "node_modules/mocha/node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/mocha/node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -5967,9 +4551,7 @@ } }, "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", "dev": true, "license": "MIT", "dependencies": { @@ -5981,8 +4563,6 @@ }, "node_modules/mocha/node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -5997,8 +4577,6 @@ }, "node_modules/mocha/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -6013,8 +4591,6 @@ }, "node_modules/mocha/node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6029,8 +4605,6 @@ }, "node_modules/mocha/node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -6045,8 +4619,6 @@ }, "node_modules/moment": { "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "dev": true, "license": "MIT", "engines": { @@ -6054,24 +4626,19 @@ } }, "node_modules/monaco-editor": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", - "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", + "version": "0.55.1", + "license": "MIT", "dependencies": { - "dompurify": "3.1.7", + "dompurify": "3.2.7", "marked": "14.0.0" } }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -6088,22 +4655,17 @@ }, "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" }, "node_modules/neo-async": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, "node_modules/no-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dev": true, + "license": "MIT", "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -6111,21 +4673,15 @@ }, "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" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", "engines": { @@ -6134,9 +4690,8 @@ }, "node_modules/nth-check": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, @@ -6146,8 +4701,6 @@ }, "node_modules/object-is": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6163,8 +4716,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", "engines": { @@ -6173,8 +4724,6 @@ }, "node_modules/object.assign": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { @@ -6194,8 +4743,6 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -6206,8 +4753,6 @@ }, "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": { @@ -6216,9 +4761,8 @@ }, "node_modules/onetime": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" }, @@ -6231,9 +4775,8 @@ }, "node_modules/ora": { "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", @@ -6254,15 +4797,13 @@ }, "node_modules/ora/node_modules/emoji-regex": { "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ora/node_modules/is-unicode-supported": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -6272,9 +4813,8 @@ }, "node_modules/ora/node_modules/log-symbols": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" @@ -6288,9 +4828,8 @@ }, "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6300,9 +4839,8 @@ }, "node_modules/ora/node_modules/string-width": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -6317,8 +4855,6 @@ }, "node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { @@ -6333,8 +4869,6 @@ }, "node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", "dependencies": { @@ -6346,8 +4880,6 @@ }, "node_modules/p-map": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", - "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", "dev": true, "license": "MIT", "engines": { @@ -6359,8 +4891,6 @@ }, "node_modules/p-try": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, "license": "MIT", "engines": { @@ -6369,15 +4899,11 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/package-name-regex": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/package-name-regex/-/package-name-regex-2.0.6.tgz", - "integrity": "sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA==", "dev": true, "license": "MIT", "engines": { @@ -6389,15 +4915,13 @@ }, "node_modules/pako": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "dev": true, + "license": "(MIT AND Zlib)" }, "node_modules/param-case": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", "dev": true, + "license": "MIT", "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -6405,8 +4929,6 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -6414,9 +4936,8 @@ }, "node_modules/pascal-case": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dev": true, + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -6424,8 +4945,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -6434,8 +4953,6 @@ }, "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": { @@ -6444,8 +4961,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -6454,15 +4969,11 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -6478,21 +4989,15 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -6503,8 +5008,6 @@ }, "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": { @@ -6513,8 +5016,6 @@ }, "node_modules/pkg-dir": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6526,8 +5027,6 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { @@ -6536,8 +5035,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -6564,9 +5061,8 @@ }, "node_modules/pretty-error": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", "dev": true, + "license": "MIT", "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" @@ -6574,8 +5070,6 @@ }, "node_modules/pretty-format": { "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, "license": "MIT", "dependencies": { @@ -6589,14 +5083,11 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", "engines": { "node": ">=6" @@ -6604,8 +5095,6 @@ }, "node_modules/randombytes": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -6613,8 +5102,6 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6622,16 +5109,13 @@ }, "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" }, "node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6644,14 +5128,11 @@ }, "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/readdirp": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { @@ -6664,8 +5145,6 @@ }, "node_modules/rechoir": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6677,18 +5156,16 @@ }, "node_modules/relateurl": { "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/renderkid": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", "dev": true, + "license": "MIT", "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", @@ -6699,18 +5176,16 @@ }, "node_modules/renderkid/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/renderkid/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6720,8 +5195,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { @@ -6730,8 +5203,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6739,8 +5210,6 @@ }, "node_modules/resolve": { "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { @@ -6760,8 +5229,6 @@ }, "node_modules/resolve-cwd": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, "license": "MIT", "dependencies": { @@ -6773,8 +5240,6 @@ }, "node_modules/resolve-from": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", "engines": { @@ -6783,8 +5248,6 @@ }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "devOptional": true, "license": "MIT", "funding": { @@ -6793,9 +5256,8 @@ }, "node_modules/restore-cursor": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" @@ -6808,13 +5270,11 @@ } }, "node_modules/rimraf": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.0.tgz", - "integrity": "sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==", + "version": "6.1.2", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^11.0.3", + "glob": "^13.0.0", "package-json-from-dist": "^1.0.1" }, "bin": { @@ -6828,22 +5288,14 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "13.0.0", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, "engines": { "node": "20 || >=22" }, @@ -6851,36 +5303,30 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.2.4", "dev": true, "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, "engines": { "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", - "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.1.1", "dev": true, - "license": "ISC", + "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.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "2.0.1", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -6896,8 +5342,6 @@ }, "node_modules/rollup": { "version": "4.50.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz", - "integrity": "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==", "license": "MIT", "peer": true, "dependencies": { @@ -6937,8 +5381,6 @@ }, "node_modules/rollup-plugin-license": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-3.6.0.tgz", - "integrity": "sha512-1ieLxTCaigI5xokIfszVDRoy6c/Wmlot1fDEnea7Q/WXSR8AqOjYljHDLObAx7nFxHC2mbxT3QnTSPhaic2IYw==", "dev": true, "license": "MIT", "dependencies": { @@ -6960,8 +5402,6 @@ }, "node_modules/rollup-plugin-node-externals": { "version": "8.1.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-node-externals/-/rollup-plugin-node-externals-8.1.2.tgz", - "integrity": "sha512-EuB6/lolkMLK16gvibUjikERq5fCRVIGwD2xue/CrM8D0pz5GXD2V6N8IrgxegwbcUoKkUFI8VYCEEv8MMvgpA==", "dev": true, "funding": [ { @@ -6973,6 +5413,7 @@ "url": "https://paypal.me/septh07" } ], + "license": "MIT", "engines": { "node": ">= 21 || ^20.6.0 || ^18.19.0" }, @@ -6982,8 +5423,6 @@ }, "node_modules/rxjs": { "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6992,8 +5431,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -7012,8 +5449,6 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", "dependencies": { @@ -7029,9 +5464,7 @@ } }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -7049,8 +5482,6 @@ }, "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" @@ -7066,8 +5497,6 @@ }, "node_modules/semver": { "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "license": "ISC", "dependencies": { @@ -7082,8 +5511,6 @@ }, "node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", "dependencies": { @@ -7095,15 +5522,11 @@ }, "node_modules/semver/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" }, "node_modules/send": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { "debug": "^4.3.5", @@ -7124,8 +5547,6 @@ }, "node_modules/send/node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7133,8 +5554,6 @@ }, "node_modules/send/node_modules/mime-types": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -7145,17 +5564,13 @@ }, "node_modules/serialize-javascript": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -7165,12 +5580,14 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/set-function-length": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { @@ -7187,20 +5604,15 @@ }, "node_modules/setimmediate": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, "node_modules/shallow-clone": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, "license": "MIT", "dependencies": { @@ -7212,8 +5624,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -7225,8 +5635,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -7235,8 +5643,6 @@ }, "node_modules/shell-quote": { "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", "engines": { @@ -7248,8 +5654,6 @@ }, "node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", "engines": { @@ -7261,8 +5665,6 @@ }, "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": { @@ -7271,15 +5673,11 @@ }, "node_modules/smob": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "dev": true, "license": "MIT" }, "node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7287,8 +5685,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7296,8 +5692,6 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -7306,8 +5700,6 @@ }, "node_modules/spdx-compare": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", - "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", "dev": true, "license": "MIT", "dependencies": { @@ -7318,15 +5710,11 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7336,8 +5724,6 @@ }, "node_modules/spdx-expression-validate": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz", - "integrity": "sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==", "dev": true, "license": "(MIT AND CC-BY-3.0)", "dependencies": { @@ -7346,22 +5732,16 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "dev": true, "license": "CC0-1.0" }, "node_modules/spdx-ranges": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", - "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", "dev": true, "license": "(MIT AND CC-BY-3.0)" }, "node_modules/spdx-satisfies": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-5.0.1.tgz", - "integrity": "sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==", "dev": true, "license": "MIT", "dependencies": { @@ -7372,22 +5752,16 @@ }, "node_modules/split.js": { "version": "1.6.5", - "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz", - "integrity": "sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==", "dev": true, "license": "MIT" }, "node_modules/sprintf-js": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, "license": "BSD-3-Clause" }, "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": { @@ -7399,8 +5773,6 @@ }, "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": { @@ -7409,8 +5781,6 @@ }, "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -7418,9 +5788,8 @@ }, "node_modules/stdin-discarder": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -7430,23 +5799,19 @@ }, "node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "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": { @@ -7455,8 +5820,6 @@ }, "node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { @@ -7474,8 +5837,6 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -7489,8 +5850,6 @@ }, "node_modules/string-width-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -7499,15 +5858,11 @@ }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -7519,8 +5874,6 @@ }, "node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7536,8 +5889,6 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -7549,8 +5900,6 @@ }, "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -7559,8 +5908,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -7572,8 +5919,6 @@ }, "node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -7587,8 +5932,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { @@ -7600,8 +5943,6 @@ }, "node_modules/synckit": { "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", "dev": true, "license": "MIT", "dependencies": { @@ -7615,9 +5956,7 @@ } }, "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "version": "2.3.0", "license": "MIT", "engines": { "node": ">=6" @@ -7629,8 +5968,6 @@ }, "node_modules/terser": { "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -7646,9 +5983,7 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", @@ -7681,8 +6016,6 @@ }, "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": "*", @@ -7695,14 +6028,10 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, "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": { @@ -7716,8 +6045,6 @@ }, "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": { @@ -7727,9 +6054,6 @@ }, "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": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -7749,8 +6073,6 @@ }, "node_modules/test-exclude/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -7762,8 +6084,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -7778,15 +6098,11 @@ }, "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" }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7798,8 +6114,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { "node": ">=0.6" @@ -7807,8 +6121,6 @@ }, "node_modules/tree-kill": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", "bin": { @@ -7817,8 +6129,6 @@ }, "node_modules/tsconfck": { "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", - "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", "dev": true, "license": "MIT", "bin": { @@ -7838,19 +6148,15 @@ }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "version": "4.21.0", "devOptional": true, "license": "MIT", "dependencies": { - "esbuild": "~0.25.0", + "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -7865,8 +6171,6 @@ }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -7880,8 +6184,6 @@ }, "node_modules/uglify-js": { "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "license": "BSD-2-Clause", "optional": true, "bin": { @@ -7893,14 +6195,10 @@ }, "node_modules/undici-types": { "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "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": { @@ -7908,9 +6206,7 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.2", "funding": [ { "type": "opencollective", @@ -7939,8 +6235,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -7948,8 +6242,6 @@ }, "node_modules/util": { "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "dev": true, "license": "MIT", "dependencies": { @@ -7962,21 +6254,18 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/utila": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/v8-to-istanbul": { "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, + "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -7987,13 +6276,13 @@ } }, "node_modules/vite": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", - "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", "peer": true, "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -8063,9 +6352,8 @@ }, "node_modules/vite-plugin-static-copy": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.4.tgz", - "integrity": "sha512-iCmr4GSw4eSnaB+G8zc2f4dxSuDjbkjwpuBLLGvQYR9IW7rnDzftnUjOH5p4RYR+d4GsiBqXRvzuFhs5bnzVyw==", "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.3", @@ -8081,8 +6369,6 @@ }, "node_modules/vite-plugin-static-copy/node_modules/chokidar": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -8106,8 +6392,6 @@ }, "node_modules/vite-plugin-static-copy/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { @@ -8119,8 +6403,6 @@ }, "node_modules/vite-plugin-static-copy/node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", "dependencies": { @@ -8131,15 +6413,16 @@ } }, "node_modules/vite-tsconfig-paths": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", - "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.0.4.tgz", + "integrity": "sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", - "tsconfck": "^3.0.3" + "tsconfck": "^3.0.3", + "vite": "*" }, "peerDependencies": { "vite": "*" @@ -8152,16 +6435,13 @@ }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", - "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", "dev": true, "license": "MIT", "dependencies": { @@ -8175,9 +6455,8 @@ }, "node_modules/vscode-languageclient/node_modules/minimatch": { "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8187,8 +6466,7 @@ }, "node_modules/vscode-languageserver": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, @@ -8198,8 +6476,6 @@ }, "node_modules/vscode-languageserver-protocol": { "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "license": "MIT", "dependencies": { "vscode-jsonrpc": "8.2.0", @@ -8208,30 +6484,24 @@ }, "node_modules/vscode-languageserver-textdocument": { "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" + "license": "MIT" }, "node_modules/vscode-languageserver-types": { "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + "license": "MIT" }, "node_modules/vscode-oniguruma": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-2.0.1.tgz", - "integrity": "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==", "license": "MIT" }, "node_modules/vscode-textmate": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.1.tgz", - "integrity": "sha512-eXiUi2yYFv9bdvgrYtJynA7UemCEkpVNE50S9iBBA08LYG5t9+/TB+8IRS/YoYOubCez2OkSyZ1Q12eQMwzbrw==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.3.1.tgz", + "integrity": "sha512-U19nFkCraZF9/bkQKQYsb9mRqM9NwpToQQFl40nGiioZTH9gRtdtCHwp48cubayVfreX3ivnoxgxQgNwrTVmQg==", "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": { @@ -8240,8 +6510,6 @@ }, "node_modules/watchpack": { "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -8252,9 +6520,7 @@ } }, "node_modules/webpack": { - "version": "5.101.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", - "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "version": "5.104.1", "license": "MIT", "peer": true, "dependencies": { @@ -8266,22 +6532,22 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.17.4", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, "bin": { @@ -8302,8 +6568,6 @@ }, "node_modules/webpack-cli": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", - "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", "peer": true, @@ -8346,15 +6610,11 @@ }, "node_modules/webpack-cli/node_modules/colorette": { "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, "node_modules/webpack-cli/node_modules/commander": { "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { @@ -8363,8 +6623,6 @@ }, "node_modules/webpack-merge": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "dev": true, "license": "MIT", "dependencies": { @@ -8378,8 +6636,6 @@ }, "node_modules/webpack-sources": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -8387,8 +6643,6 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -8403,8 +6657,6 @@ }, "node_modules/which-typed-array": { "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", "dependencies": { @@ -8425,28 +6677,20 @@ }, "node_modules/wildcard": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true, "license": "MIT" }, "node_modules/wordwrap": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, "node_modules/workerpool": { "version": "9.3.3", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", - "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", "dev": true, "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8464,8 +6708,6 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8482,8 +6724,6 @@ }, "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -8492,8 +6732,6 @@ }, "node_modules/wrap-ansi-cjs/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": { @@ -8508,15 +6746,11 @@ }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -8530,8 +6764,6 @@ }, "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -8543,8 +6775,6 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", "engines": { @@ -8556,15 +6786,11 @@ }, "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": { @@ -8577,8 +6803,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { @@ -8587,15 +6811,11 @@ }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { @@ -8613,8 +6833,6 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { @@ -8623,8 +6841,6 @@ }, "node_modules/yargs-unparser": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "license": "MIT", "dependencies": { @@ -8639,8 +6855,6 @@ }, "node_modules/yargs-unparser/node_modules/camelcase": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", "engines": { @@ -8652,8 +6866,6 @@ }, "node_modules/yargs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -8662,15 +6874,11 @@ }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -8684,8 +6892,6 @@ }, "node_modules/yargs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -8697,8 +6903,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -8710,26 +6914,26 @@ }, "packages/alphatab": { "name": "@coderline/alphatab", - "version": "1.7.0", + "version": "1.8.0", "license": "MPL-2.0", "devDependencies": { - "@biomejs/biome": "^2.2.6", + "@biomejs/biome": "^2.3.11", "@coderline/alphaskia": "^3.4.135", "@coderline/alphaskia-linux": "^3.4.135", "@coderline/alphaskia-windows": "^3.4.135", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^24.10.0", + "@types/node": "^25.0.6", "assert": "^2.1.0", - "chai": "^6.2.1", + "chai": "^6.2.2", "chalk": "^5.6.2", "jest-snapshot": "^30.2.0", "mocha": "^11.7.5", - "rimraf": "^6.1.0", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3", - "vite": "^7.2.6", + "vite": "^7.3.1", "vite-plugin-static-copy": "^3.1.4" }, "engines": { @@ -8738,32 +6942,32 @@ }, "packages/alphatex": { "name": "@coderline/alphatab-alphatex", - "version": "1.7.0", + "version": "1.8.0", "devDependencies": { - "rimraf": "^6.0.1", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3" } }, "packages/csharp": { "name": "@coderline/alphatab-csharp", - "version": "1.7.0", + "version": "1.8.0", "devDependencies": { "@coderline/alphatab-transpiler": "*", - "rimraf": "^6.1.0" + "rimraf": "^6.1.2" } }, "packages/kotlin": { "name": "@coderline/alphatab-kotlin", - "version": "1.7.0" + "version": "1.8.0" }, "packages/lsp": { "name": "@coderline/alphatab-language-server", - "version": "1.7.0", + "version": "1.8.0", "license": "MPL-2.0", "dependencies": { - "@coderline/alphatab": "^1.7.0", + "@coderline/alphatab": "^1.8.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12" }, @@ -8771,79 +6975,79 @@ "alphatab-language-server": "dist/server.mjs" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@microsoft/api-extractor": "^7.53.1", + "@biomejs/biome": "^2.3.11", + "@microsoft/api-extractor": "^7.55.2", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^24.8.1", + "@types/node": "^25.0.6", "assert": "^2.1.0", - "chai": "^6.2.0", + "chai": "^6.2.2", "mocha": "^11.7.4", - "rimraf": "^6.0.1", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3" } }, "packages/monaco": { "name": "@coderline/alphatab-monaco", - "version": "1.7.0", + "version": "1.8.0", "license": "MPL-2.0", "dependencies": { - "@coderline/alphatab": "^1.7.0", - "@coderline/alphatab-language-server": "^1.7.0", - "monaco-editor": "^0.54.0", + "@coderline/alphatab": "^1.8.0", + "@coderline/alphatab-language-server": "^1.8.0", + "monaco-editor": "^0.55.1", "vscode-languageserver-types": "^3.17.5", "vscode-oniguruma": "^2.0.1", - "vscode-textmate": "^9.2.1" + "vscode-textmate": "^9.3.1" }, "bin": { "alphatab-monaco": "dist/alphaTab.monaco.mjs" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@microsoft/api-extractor": "^7.53.1", + "@biomejs/biome": "^2.3.11", + "@microsoft/api-extractor": "^7.55.2", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^24.8.1", + "@types/node": "^25.0.6", "assert": "^2.1.0", - "chai": "^6.2.0", + "chai": "^6.2.2", "mocha": "^11.7.4", - "rimraf": "^6.0.1", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3" } }, "packages/playground": { "name": "@coderline/alphatab-playground", - "version": "1.7.0", + "version": "1.8.0", "dependencies": { "@coderline/alphatab": "*", "@fontsource/noto-sans": "^5.2.10", - "@fontsource/noto-serif": "^5.2.8", + "@fontsource/noto-serif": "^5.2.9", "@fortawesome/fontawesome-free": "^7.1.0", "@popperjs/core": "^2.11.8", "@types/serve-static": "^2.2.0", "bootstrap": "^5.3.8", "handlebars": "^4.7.8", - "monaco-editor": "^0.54.0", - "serve-static": "^2.2.0" + "monaco-editor": "^0.55.1", + "serve-static": "^2.2.1" }, "devDependencies": { "@types/bootstrap": "^5.2.10", "split.js": "^1.6.5", "tslib": "^2.8.1", "typescript": "^5.9.3", - "vite": "^7.2.6", - "vite-tsconfig-paths": "^5.1.4" + "vite": "^7.3.1", + "vite-tsconfig-paths": "^6.0.4" } }, "packages/tooling": { "name": "@coderline/alphatab-tooling", - "version": "1.7.0", + "version": "1.8.0", "devDependencies": { - "@microsoft/api-extractor": "^7.53.3", + "@microsoft/api-extractor": "^7.55.2", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.3.0", "rollup-plugin-license": "^3.6.0", @@ -8852,30 +7056,30 @@ }, "packages/transpiler": { "name": "@coderline/alphatab-transpiler", - "version": "1.7.0" + "version": "1.8.0" }, "packages/vite": { "name": "@coderline/alphatab-vite", - "version": "1.7.0", + "version": "1.8.0", "license": "MPL-2.0", "dependencies": { "magic-string": "^0.30.21", - "vite": "^7.2.6" + "vite": "^7.3.1" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@microsoft/api-extractor": "^7.53.3", + "@biomejs/biome": "^2.3.11", + "@microsoft/api-extractor": "^7.55.2", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^24.10.0", + "@types/node": "^25.0.6", "assert": "^2.1.0", - "chai": "^6.2.1", + "chai": "^6.2.2", "mocha": "^11.7.5", - "rimraf": "^6.1.0", + "rimraf": "^6.1.2", "rollup-plugin-node-externals": "^8.1.2", "terser": "^5.44.1", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3" }, "engines": { @@ -8884,24 +7088,24 @@ }, "packages/vscode": { "name": "alphatab-vscode", - "version": "1.7.0", + "version": "1.8.0", "license": "MPL-2.0", "devDependencies": { - "@biomejs/biome": "^2.2.6", + "@biomejs/biome": "^2.3.11", "@rollup/plugin-node-resolve": "^16.0.3", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^24.8.1", - "@types/vscode": "^1.106.1", - "@vscode/test-cli": "^0.0.11", + "@types/node": "^25.0.6", + "@types/vscode": "^1.108.1", + "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "assert": "^2.1.0", - "chai": "^6.2.0", + "chai": "^6.2.2", "concurrently": "^9.2.1", "mocha": "^11.7.4", - "rimraf": "^6.0.1", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3", "vscode-languageclient": "^9.0.1" }, @@ -8911,35 +7115,29 @@ }, "packages/webpack": { "name": "@coderline/alphatab-webpack", - "version": "1.7.0", + "version": "1.8.0", "license": "MPL-2.0", "dependencies": { - "webpack": "^5.101.3" + "webpack": "^5.104.1" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", + "@biomejs/biome": "^2.3.11", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^24.10.0", + "@types/node": "^25.0.6", "assert": "^2.1.0", - "chai": "^6.2.1", + "chai": "^6.2.2", "html-webpack-plugin": "^5.6.5", "mocha": "^11.7.5", - "rimraf": "^6.1.0", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3", "webpack-cli": "^6.0.1" }, "engines": { "node": ">=20.19.0" } - }, - "packages/website": { - "name": "@coderline/alphatab-website", - "version": "1.7.0", - "extraneous": true, - "license": "MPL-2.0" } } } diff --git a/package.json b/package.json index b0bab22d3..da562f7d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-monorepo", - "version": "1.7.1", + "version": "1.8.0", "description": "Monorepo for alphaTab and its related packages", "private": true, "type": "module", @@ -41,7 +41,6 @@ "test-webpack": "npm run test --workspace=packages/webpack", "build-monaco": "npm run build --workspace=packages/monaco" - }, "devDependencies": { "concurrently": "^9.2.1" diff --git a/packages/alphatab/.env b/packages/alphatab/.env index 72295dc04..33ccbd705 100644 --- a/packages/alphatab/.env +++ b/packages/alphatab/.env @@ -1,2 +1,2 @@ FORCE_COLOR=1 -NODE_OPTIONS=--expose-gc +NODE_OPTIONS=--expose-gc \ No newline at end of file diff --git a/packages/alphatab/package.json b/packages/alphatab/package.json index b2df5505e..216c76264 100644 --- a/packages/alphatab/package.json +++ b/packages/alphatab/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab", - "version": "1.7.1", + "version": "1.8.0", "description": "alphaTab is a music notation and guitar tablature rendering library", "keywords": [ "guitar", @@ -58,23 +58,23 @@ "test-accept-reference": "tsx scripts/accept-new-reference-files.ts" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", + "@biomejs/biome": "^2.3.11", "@coderline/alphaskia": "^3.4.135", "@coderline/alphaskia-linux": "^3.4.135", "@coderline/alphaskia-windows": "^3.4.135", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^24.10.0", + "@types/node": "^25.0.6", "assert": "^2.1.0", - "chai": "^6.2.1", + "chai": "^6.2.2", "chalk": "^5.6.2", "jest-snapshot": "^30.2.0", "mocha": "^11.7.5", - "rimraf": "^6.1.0", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3", - "vite": "^7.2.6", + "vite": "^7.3.1", "vite-plugin-static-copy": "^3.1.4" }, "files": [ diff --git a/packages/alphatab/scripts/JsonDeclarationEmitter.ts b/packages/alphatab/scripts/JsonDeclarationEmitter.ts index 23bfb7037..ec1fbdd36 100644 --- a/packages/alphatab/scripts/JsonDeclarationEmitter.ts +++ b/packages/alphatab/scripts/JsonDeclarationEmitter.ts @@ -7,7 +7,7 @@ import * as ts from 'typescript'; import createEmitter, { generateFile } from './EmitterBase'; import { getTypeWithNullableInfo, type TypeWithNullableInfo } from './TypeSchema'; -function createDefaultJsonTypeNode(checker: ts.TypeChecker, type: TypeWithNullableInfo, isNullable: boolean) { +function createDefaultJsonTypeNode(checker: ts.TypeChecker, type: TypeWithNullableInfo, isNullable: boolean): ts.TypeNode { if (isNullable) { const notNullable = createDefaultJsonTypeNode(checker, type, false); return ts.factory.createUnionTypeNode([notNullable, ts.factory.createLiteralTypeNode(ts.factory.createNull())]); diff --git a/packages/alphatab/scripts/Serializer.setProperty.ts b/packages/alphatab/scripts/Serializer.setProperty.ts index 658226707..ff75b644f 100644 --- a/packages/alphatab/scripts/Serializer.setProperty.ts +++ b/packages/alphatab/scripts/Serializer.setProperty.ts @@ -170,6 +170,10 @@ function generateSetPropertyBody(serializable: TypeSchema, importer: (name: stri `${mapValueTypeInfo.typeAsString}.fromJson(v)`, ts.SyntaxKind.CallExpression ); + + if(!mapValueTypeInfo.isOptional && !mapValueTypeInfo.isNullable) { + mapValue = ts.factory.createNonNullExpression(mapValue); + } } else { itemSerializer = `${mapValueTypeInfo.typeAsString}Serializer`; importer(itemSerializer, findSerializerModule(mapValueTypeInfo)); @@ -181,15 +185,17 @@ function generateSetPropertyBody(serializable: TypeSchema, importer: (name: stri .filter(t => t.tagName.text === 'json_add') .map(t => t.comment ?? '')[0] as string; - caseStatements.push( - assignField( - ts.factory.createNewExpression( - ts.factory.createIdentifier('Map'), - prop.type.typeArguments!.map(t => t.createTypeNode()), - [] + if(!prop.isReadOnly) { + caseStatements.push( + assignField( + ts.factory.createNewExpression( + ts.factory.createIdentifier('Map'), + prop.type.typeArguments!.map(t => t.createTypeNode()), + [] + ) ) - ) - ); + ); + } caseStatements.push( ts.factory.createExpressionStatement( diff --git a/packages/alphatab/scripts/Serializer.toJson.ts b/packages/alphatab/scripts/Serializer.toJson.ts index 09346273e..ed2c5656b 100644 --- a/packages/alphatab/scripts/Serializer.toJson.ts +++ b/packages/alphatab/scripts/Serializer.toJson.ts @@ -1,5 +1,5 @@ -import * as ts from 'typescript'; import { createNodeFromSource, setMethodBody } from '@coderline/alphatab-transpiler/src/BuilderHelpers'; +import * as ts from 'typescript'; import { findSerializerModule } from './Serializer.common'; import type { TypeSchema } from './TypeSchema'; @@ -30,7 +30,7 @@ function generateToJsonBody(serializable: TypeSchema, importer: (name: string, m const fieldName = prop.name; const jsonName = prop.jsonNames.filter(n => n !== '')[0]; - if (!jsonName || prop.isReadOnly) { + if (!jsonName || prop.isJsonReadOnly) { continue; } @@ -153,6 +153,17 @@ function generateToJsonBody(serializable: TypeSchema, importer: (name: string, m }`, ts.SyntaxKind.Block ); + } else if(prop.type.typeArguments![1].isArray && prop.type.typeArguments![1].arrayItemType!.isPrimitiveType) { + serializeBlock = createNodeFromSource( + `{ + const m = new Map(); + o.set(${JSON.stringify(jsonName)}, m); + for(const [k, v] of obj.${fieldName}!) { + m.set(k.toString(), v); + } + }`, + ts.SyntaxKind.Block + ); } else { const itemSerializer = `${prop.type.typeArguments![1].typeAsString}Serializer`; importer(itemSerializer, findSerializerModule(prop.type.typeArguments![1])); diff --git a/packages/alphatab/scripts/TypeSchema.ts b/packages/alphatab/scripts/TypeSchema.ts index 0b8628d3c..0a7e0cdbd 100644 --- a/packages/alphatab/scripts/TypeSchema.ts +++ b/packages/alphatab/scripts/TypeSchema.ts @@ -62,7 +62,8 @@ export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration) asRaw, partialNames: !!jsDoc.find(t => t.tagName.text === 'json_partial_names'), target: jsDoc.find(t => t.tagName.text === 'target')?.comment as string, - isReadOnly: !!jsDoc.find(t => t.tagName.text === 'json_read_only'), + isJsonReadOnly: isReadonly, + isReadOnly: propertyDeclaration.modifiers!.some(m => m.kind == ts.SyntaxKind.ReadonlyKeyword), name: (member.name as ts.Identifier).text, jsDocTags: jsDoc, type: getTypeWithNullableInfo( @@ -379,6 +380,7 @@ export interface TypeProperty { jsonNames: string[]; asRaw: boolean; target?: string; + isJsonReadOnly: boolean; isReadOnly: boolean; } diff --git a/packages/alphatab/scripts/smufl-metadata.ts b/packages/alphatab/scripts/smufl-metadata.ts index b837ac941..ce6029e56 100644 --- a/packages/alphatab/scripts/smufl-metadata.ts +++ b/packages/alphatab/scripts/smufl-metadata.ts @@ -1,18 +1,19 @@ import fs from 'node:fs'; -import url from 'node:url'; import path from 'node:path'; -import { MusicFontSymbol } from '../src/model/MusicFontSymbol'; -import { SmuflMetrics } from '../src/SmuflMetrics'; +import url from 'node:url'; +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { SmuflMetadata } from '@coderline/alphatab/SmuflMetadata'; const input = process.argv[2]; const output = process.argv[3]; -const metadata = JSON.parse(await fs.promises.readFile(input, 'utf-8')); +const metadata: SmuflMetadata = JSON.parse(await fs.promises.readFile(input, 'utf-8')); -const outputMetadata = { +const outputMetadata:SmuflMetadata = { engravingDefaults: metadata.engravingDefaults, - glyphBBoxes: {}, - glyphsWithAnchors: {} + glyphBBoxes: {} as SmuflMetadata['glyphBBoxes'], + glyphsWithAnchors: {} as SmuflMetadata['glyphsWithAnchors'] }; const alphaTabUsedGlyphs = new Set(); @@ -20,20 +21,20 @@ for(const [_,name] of Object.entries(MusicFontSymbol).filter(e => typeof e[1] == alphaTabUsedGlyphs.add(name.toString().toLowerCase()); } -for(const [k,_] of SmuflMetrics.smuflNameToGlyphNameMapping) { +for(const [k,_] of EngravingSettings.smuflNameToGlyphNameMapping) { alphaTabUsedGlyphs.add(k.toLowerCase()); } -for(const name of Object.keys(metadata.glyphBBoxes)) { +for(const name of Object.keys(metadata.glyphBBoxes!)) { if(alphaTabUsedGlyphs.has(name.toLowerCase())) { - const alphaTabName = SmuflMetrics.smuflNameToGlyphNameMapping.has(name) ? SmuflMetrics.smuflNameToGlyphNameMapping.get(name)! : name; - outputMetadata.glyphBBoxes[alphaTabName] = metadata.glyphBBoxes[name]; + const alphaTabName = EngravingSettings.smuflNameToGlyphNameMapping.has(name) ? EngravingSettings.smuflNameToGlyphNameMapping.get(name)! : name; + outputMetadata.glyphBBoxes![alphaTabName] = metadata.glyphBBoxes![name]; } } for(const name of Object.keys(metadata.glyphsWithAnchors)) { if(alphaTabUsedGlyphs.has(name.toLowerCase())) { - const alphaTabName = SmuflMetrics.smuflNameToGlyphNameMapping.has(name) ? SmuflMetrics.smuflNameToGlyphNameMapping.get(name)! : name; + const alphaTabName = EngravingSettings.smuflNameToGlyphNameMapping.has(name) ? EngravingSettings.smuflNameToGlyphNameMapping.get(name)! : name; outputMetadata.glyphsWithAnchors[alphaTabName] = metadata.glyphsWithAnchors[name]; } } @@ -44,7 +45,7 @@ await fs.promises.writeFile(output, if(process.argv[4] === 'true') { const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); - const smuflMetricsFile = path.resolve(__dirname, '..', 'src', 'SmuflMetrics.ts'); + const smuflMetricsFile = path.resolve(__dirname, '..', 'src', 'EngravingSettings.ts'); let source = await fs.promises.readFile(smuflMetricsFile, 'utf-8'); const beginMarker = 'begin bravura_alphatab_metadata'; const endMarker = 'end bravura_alphatab_metadata'; diff --git a/packages/alphatab/src/AlphaTabApiBase.ts b/packages/alphatab/src/AlphaTabApiBase.ts index e7f58361d..288d0271e 100644 --- a/packages/alphatab/src/AlphaTabApiBase.ts +++ b/packages/alphatab/src/AlphaTabApiBase.ts @@ -1,11 +1,16 @@ import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; import type { CoreSettings } from '@coderline/alphatab/CoreSettings'; import { Environment } from '@coderline/alphatab/Environment'; -import { EventEmitter, EventEmitterOfT, type IEventEmitter, type IEventEmitterOfT } from '@coderline/alphatab/EventEmitter'; +import { + EventEmitter, + EventEmitterOfT, + type IEventEmitter, + type IEventEmitterOfT +} from '@coderline/alphatab/EventEmitter'; import { AlphaTexImporter } from '@coderline/alphatab/importer/AlphaTexImporter'; import { Logger } from '@coderline/alphatab/Logger'; import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler'; -import type { BeatTickLookupItem } from '@coderline/alphatab/midi/BeatTickLookup'; +import type { BeatTickLookupItem, IBeatVisibilityChecker } from '@coderline/alphatab/midi/BeatTickLookup'; import type { MetaDataEvent, MetaEvent, @@ -48,21 +53,34 @@ import type { IMouseEventArgs } from '@coderline/alphatab/platform/IMouseEventAr import type { IUiFacade } from '@coderline/alphatab/platform/IUiFacade'; import { ResizeEventArgs } from '@coderline/alphatab/ResizeEventArgs'; import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; -import type { IScoreRenderer } from '@coderline/alphatab/rendering/IScoreRenderer'; +import type { IScoreRenderer, RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; import { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { ScoreRendererWrapper } from '@coderline/alphatab/rendering/ScoreRendererWrapper'; import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; -import type { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; +import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; import type { BoundsLookup } from '@coderline/alphatab/rendering/utils/BoundsLookup'; import type { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds'; import type { StaffSystemBounds } from '@coderline/alphatab/rendering/utils/StaffSystemBounds'; +import { + HorizontalContinuousScrollHandler, + HorizontalOffScreenScrollHandler, + HorizontalSmoothScrollHandler, + type IScrollHandler, + VerticalContinuousScrollHandler, + VerticalOffScreenScrollHandler, + VerticalSmoothScrollHandler +} from '@coderline/alphatab/ScrollHandlers'; import type { Settings } from '@coderline/alphatab/Settings'; import { ActiveBeatsChangedEventArgs } from '@coderline/alphatab/synth/ActiveBeatsChangedEventArgs'; import { AlphaSynthWrapper } from '@coderline/alphatab/synth/AlphaSynthWrapper'; import { ExternalMediaPlayer } from '@coderline/alphatab/synth/ExternalMediaPlayer'; import type { IAlphaSynth } from '@coderline/alphatab/synth/IAlphaSynth'; -import { AudioExportOptions, type IAudioExporter, type IAudioExporterWorker } from '@coderline/alphatab/synth/IAudioExporter'; +import { + AudioExportOptions, + type IAudioExporter, + type IAudioExporterWorker +} from '@coderline/alphatab/synth/IAudioExporter'; import type { ISynthOutputDevice } from '@coderline/alphatab/synth/ISynthOutput'; import type { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs'; import { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange'; @@ -71,15 +89,61 @@ import { PlayerState } from '@coderline/alphatab/synth/PlayerState'; import type { PlayerStateChangedEventArgs } from '@coderline/alphatab/synth/PlayerStateChangedEventArgs'; import type { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs'; +/** + * @internal + * @record + */ +interface SelectionInfo { + beat: Beat; + bounds?: BeatBounds; +} + +/** + * Holds information about the highlights shown for the playback range. + * @public + * @record + */ +export interface PlaybackHighlightChangeEventArgs { + /** + * The beat where the selection starts. undefined if there is no selection. + */ + startBeat?: Beat; + + /** + * The bounds of the start beat to determine its location and size. + */ + startBeatBounds?: BeatBounds; + + /** + * The beat where the selection ends. undefined if there is no selection. + */ + endBeat?: Beat; + + /** + * The bounds of the end beat to determine its location and size. + */ + endBeatBounds?: BeatBounds; + + /** + * A list of the individual rectangular areas where highlight blocks are placed. + * If a selection spans multiple lines this array will hold all items. + */ + highlightBlocks?: Bounds[]; +} + /** * @internal */ -class SelectionInfo { - public beat: Beat; - public bounds: BeatBounds | null = null; +class BoundsLookupVisibilityChecker implements IBeatVisibilityChecker { + public bounds: BoundsLookup | null = null; + + public isVisible(beat: Beat): boolean { + const bounds = this.bounds; + if (!bounds) { + return false; + } - public constructor(beat: Beat) { - this.beat = beat; + return bounds.findBeat(beat) !== null; } } @@ -93,6 +157,7 @@ export class AlphaTabApiBase { private _startTime: number = 0; private _trackIndexes: number[] | null = null; private _trackIndexLookup: Set | null = null; + private readonly _beatVisibilityChecker = new BoundsLookupVisibilityChecker(); private _isDestroyed: boolean = false; private _score: Score | null = null; private _tracks: Track[] = []; @@ -100,6 +165,17 @@ export class AlphaTabApiBase { private _player!: AlphaSynthWrapper; private _renderer: ScoreRendererWrapper; + private _defaultScrollHandler?: IScrollHandler; + + /** + * An indicator by how many midi-ticks the song contents are shifted. + * Grace beats at start might require a shift for the first beat to start at 0. + * This information can be used to translate back the player time axis to the music notation. + */ + public get midiTickShift() { + return this._player.midiTickShift; + } + /** * The actual player mode which is currently active. * @remarks @@ -555,6 +631,7 @@ export class AlphaTabApiBase { * @param score The score containing the tracks to be rendered. * @param trackIndexes The indexes of the tracks from the song that should be rendered. If not provided, the first track of the * song will be shown. + * @param renderHints Additional hints to respect during layouting and rendering. * * @category Methods - Core * @since 0.9.4 @@ -579,7 +656,7 @@ export class AlphaTabApiBase { * api.renderScore(generateScore(), alphaTab.collections.DoubleList(2, 3)); * ``` */ - public renderScore(score: Score, trackIndexes?: number[]): void { + public renderScore(score: Score, trackIndexes?: number[], renderHints?: RenderHints): void { const tracks: Track[] = []; if (!trackIndexes) { if (score.tracks.length > 0) { @@ -602,12 +679,13 @@ export class AlphaTabApiBase { } } } - this._internalRenderTracks(score, tracks); + this._internalRenderTracks(score, tracks, renderHints); } /** * Renders the given list of tracks. * @param tracks The tracks to render. They must all belong to the same score. + * @param renderHints Additional hints to respect during layouting and rendering. * * @category Methods - Core * @since 0.9.4 @@ -638,7 +716,7 @@ export class AlphaTabApiBase { * } * ``` */ - public renderTracks(tracks: Track[]): void { + public renderTracks(tracks: Track[], renderHints?: RenderHints): void { if (tracks.length > 0) { const score: Score = tracks[0].score; for (const track of tracks) { @@ -652,11 +730,11 @@ export class AlphaTabApiBase { return; } } - this._internalRenderTracks(score, tracks); + this._internalRenderTracks(score, tracks, renderHints); } } - private _internalRenderTracks(score: Score, tracks: Track[]): void { + private _internalRenderTracks(score: Score, tracks: Track[], renderHints: RenderHints | undefined): void { ModelUtils.applyPitchOffsets(this.settings, score); if (score !== this.score) { this._score = score; @@ -669,7 +747,7 @@ export class AlphaTabApiBase { this._trackIndexLookup = new Set(this._trackIndexes); this._onScoreLoaded(score); this.loadMidiForScore(); - this.render(); + this.render(renderHints); } else { this._tracks = tracks; @@ -685,7 +763,7 @@ export class AlphaTabApiBase { } this._trackIndexLookup = new Set(this._trackIndexes); - this.render(); + this.render(renderHints); } } @@ -873,6 +951,7 @@ export class AlphaTabApiBase { /** * Initiates a re-rendering of the current setup. + * @param renderHints Additional hints to respect during layouting and rendering. * @remarks * If rendering is not yet possible, it will be deferred until the UI changes to be ready for rendering. * @@ -899,18 +978,55 @@ export class AlphaTabApiBase { * api.render() * ``` */ - public render(): void { + public render(renderHints?: RenderHints): void { if (this.uiFacade.canRender) { // when font is finally loaded, start rendering this._renderer.width = this.container.width; - this._renderer.renderScore(this.score, this._trackIndexes); + this._renderer.renderScore(this.score, this._trackIndexes, renderHints); } else { - this.uiFacade.canRenderChanged.on(() => this.render()); + this.uiFacade.canRenderChanged.on(() => this.render(renderHints)); } } private _tickCache: MidiTickLookup | null = null; + /** + * A custom scroll handler which will be used to handle scrolling operations during playback. + * + * @category Properties - Player + * @since 1.8.0 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.customScrollHandler = { + * forceScrollTo(currentBeatBounds) { + * const scroll = api.uiFacade.getScrollElement(); + * api.uiFacade.scrollToY(scroll, currentBeatBounds.barBounds.masterBarBounds.realBounds.y, 0); + * }, + * onBeatCursorUpdating(startBeat, endBeat, cursorMode, relativePosition, actualBeatCursorStartX, actualBeatCursorEndX, actualBeatCursorTransitionDuration) { + * const scroll = api.uiFacade.getScrollElement(); + * api.uiFacade.scrollToY(scroll, startBeat.barBounds.masterBarBounds.realBounds.y, 0); + * } + * } + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.CustomScrollHandler = new CustomScrollHandler(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.customScrollHandler = CustomScrollHandler(); + * ``` + */ + public customScrollHandler?: IScrollHandler; + /** * The tick cache allowing lookup of midi ticks to beats. * @remarks @@ -1537,6 +1653,7 @@ export class AlphaTabApiBase { this._onMidiLoad(midiFile); const player = this._player; + player.midiTickShift = handler.tickShift; player.loadMidiFile(midiFile); player.loadBackingTrack(score); player.updateSyncPoints(generator.syncPoints); @@ -1948,7 +2065,6 @@ export class AlphaTabApiBase { private _isInitialBeatCursorUpdate = true; private _previousStateForCursor: PlayerState = PlayerState.Paused; private _previousCursorCache: BoundsLookup | null = null; - private _lastScroll: number = 0; private _destroyCursors(): void { if (!this._cursorWrapper) { @@ -1961,28 +2077,83 @@ export class AlphaTabApiBase { this._selectionWrapper = null; } + private _createCursors() { + if (this._cursorWrapper) { + return; + } + const cursors = this.uiFacade.createCursors(); + if (cursors) { + // store options and created elements for fast access + this._cursorWrapper = cursors.cursorWrapper; + this._barCursor = cursors.barCursor; + this._beatCursor = cursors.beatCursor; + this._selectionWrapper = cursors.selectionWrapper; + this._isInitialBeatCursorUpdate = true; + } + if (this._currentBeat !== null) { + this._cursorUpdateBeat(this._currentBeat!, false, this._previousTick > 10, 1, true); + } + } + private _updateCursors() { + this._updateScrollHandler(); + const enable = this._hasCursor; - if (enable && !this._cursorWrapper) { - // - // Create cursors - const cursors = this.uiFacade.createCursors(); - if (cursors) { - // store options and created elements for fast access - this._cursorWrapper = cursors.cursorWrapper; - this._barCursor = cursors.barCursor; - this._beatCursor = cursors.beatCursor; - this._selectionWrapper = cursors.selectionWrapper; - this._isInitialBeatCursorUpdate = true; - } - if (this._currentBeat !== null) { - this._cursorUpdateBeat(this._currentBeat!, false, this._previousTick > 10, 1, true); - } + if (enable) { + this._createCursors(); } else if (!enable && this._cursorWrapper) { this._destroyCursors(); } } + private _scrollHandlerMode = ScrollMode.Off; + private _scrollHandlerVertical = true; + private _updateScrollHandler() { + const currentHandler = this._defaultScrollHandler; + const scrollMode = this.settings.player.scrollMode; + const isVertical = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical; + + // no change + if (this._scrollHandlerMode === scrollMode && this._scrollHandlerVertical === isVertical) { + return; + } + + // destroy current handler in favor of new one + if (currentHandler) { + currentHandler[Symbol.dispose](); + const scroll = this.uiFacade.getScrollContainer(); + this.uiFacade.stopScrolling(scroll); + } + + switch (scrollMode) { + case ScrollMode.Off: + this._defaultScrollHandler = undefined; + break; + case ScrollMode.Continuous: + if (isVertical) { + this._defaultScrollHandler = new VerticalContinuousScrollHandler(this); + } else { + this._defaultScrollHandler = new HorizontalContinuousScrollHandler(this); + } + break; + case ScrollMode.OffScreen: + if (isVertical) { + this._defaultScrollHandler = new VerticalOffScreenScrollHandler(this); + } else { + this._defaultScrollHandler = new HorizontalOffScreenScrollHandler(this); + } + break; + + case ScrollMode.Smooth: + if (isVertical) { + this._defaultScrollHandler = new VerticalSmoothScrollHandler(this); + } else { + this._defaultScrollHandler = new HorizontalSmoothScrollHandler(this); + } + break; + } + } + /** * updates the cursors to highlight the beat at the specified tick position * @param tick @@ -2000,18 +2171,19 @@ export class AlphaTabApiBase { const cache: MidiTickLookup | null = this._tickCache; if (cache) { - const tracks = this._trackIndexLookup; - if (tracks != null && tracks.size > 0) { - const beat: MidiTickLookupFindBeatResult | null = cache.findBeat(tracks, tick, this._currentBeat); - if (beat) { - this._cursorUpdateBeat( - beat, - stop, - shouldScroll, - cursorSpeed, - forceUpdate || this.playerState === PlayerState.Paused - ); - } + const beat: MidiTickLookupFindBeatResult | null = cache.findBeatWithChecker( + this._beatVisibilityChecker, + tick, + this._currentBeat + ); + if (beat) { + this._cursorUpdateBeat( + beat, + stop, + shouldScroll, + cursorSpeed, + forceUpdate || this.playerState === PlayerState.Paused + ); } } } @@ -2086,69 +2258,9 @@ export class AlphaTabApiBase { public scrollToCursor() { const beatBounds = this._currentBeatBounds; if (beatBounds) { - this._internalScrollToCursor(beatBounds.barBounds.masterBarBounds); - } - } - - private _internalScrollToCursor(barBoundings: MasterBarBounds) { - const scrollElement: IContainer = this.uiFacade.getScrollContainer(); - const isVertical: boolean = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical; - const mode: ScrollMode = this.settings.player.scrollMode; - if (isVertical) { - // when scrolling on the y-axis, we preliminary check if the new beat/bar have - // moved on the y-axis - const y: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; - if (y !== this._lastScroll) { - this._lastScroll = y; - switch (mode) { - case ScrollMode.Continuous: - const elementOffset: Bounds = this.uiFacade.getOffset(scrollElement, this.container); - this.uiFacade.scrollToY(scrollElement, elementOffset.y + y, this.settings.player.scrollSpeed); - break; - case ScrollMode.OffScreen: - const elementBottom: number = - scrollElement.scrollTop + this.uiFacade.getOffset(null, scrollElement).h; - if ( - barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom || - barBoundings.visualBounds.y < scrollElement.scrollTop - ) { - const scrollTop: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; - this.uiFacade.scrollToY(scrollElement, scrollTop, this.settings.player.scrollSpeed); - } - break; - } - } - } else { - // when scrolling on the x-axis, we preliminary check if the new bar has - // moved on the x-axis - const x: number = barBoundings.visualBounds.x; - if (x !== this._lastScroll) { - this._lastScroll = x; - switch (mode) { - case ScrollMode.Continuous: - const scrollLeftContinuous: number = - barBoundings.realBounds.x + this.settings.player.scrollOffsetX; - this._lastScroll = barBoundings.visualBounds.x; - this.uiFacade.scrollToX(scrollElement, scrollLeftContinuous, this.settings.player.scrollSpeed); - break; - case ScrollMode.OffScreen: - const elementRight: number = - scrollElement.scrollLeft + this.uiFacade.getOffset(null, scrollElement).w; - if ( - barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight || - barBoundings.visualBounds.x < scrollElement.scrollLeft - ) { - const scrollLeftOffScreen: number = - barBoundings.realBounds.x + this.settings.player.scrollOffsetX; - this._lastScroll = barBoundings.visualBounds.x; - this.uiFacade.scrollToX( - scrollElement, - scrollLeftOffScreen, - this.settings.player.scrollSpeed - ); - } - break; - } + const handler = this.customScrollHandler ?? this._defaultScrollHandler; + if (handler) { + handler.forceScrollTo(beatBounds); } } } @@ -2181,11 +2293,12 @@ export class AlphaTabApiBase { const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop; let nextBeatX: number = barBoundings.visualBounds.x + barBoundings.visualBounds.w; + let nextBeatBoundings: BeatBounds | null = null; // get position of next beat on same system if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) { // if we are moving within the same bar or to the next bar // transition to the next beat, otherwise transition to the end of the bar. - const nextBeatBoundings: BeatBounds | null = cache.findBeat(nextBeat); + nextBeatBoundings = cache.findBeat(nextBeat); if ( nextBeatBoundings && nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds @@ -2219,23 +2332,28 @@ export class AlphaTabApiBase { beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h); } + // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks) + // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time. + // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX); + const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1; + nextBeatX = startBeatX + (nextBeatX - startBeatX) * factor; + duration = (duration / cursorSpeed) * factor; + // we need to put the transition to an own animation frame // otherwise the stop animation above is not applied. this.uiFacade.beginInvoke(() => { - // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks) - // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time. - // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX); - const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1; - const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * factor; - beatCursor!.transitionToX((duration / cursorSpeed) * factor, doubleEndBeatX); + beatCursor!.transitionToX(duration, nextBeatX); }); } else { - beatCursor.transitionToX(0, startBeatX); + duration = 0; + beatCursor.transitionToX(duration, nextBeatX); beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h); } } else { // ticking cursor - beatCursor.transitionToX(0, startBeatX); + duration = 0; + nextBeatX = startBeatX; + beatCursor.transitionToX(duration, nextBeatX); beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h); } @@ -2262,7 +2380,17 @@ export class AlphaTabApiBase { } if (shouldScroll && !this._isBeatMouseDown && this.settings.player.scrollMode !== ScrollMode.Off) { - this._internalScrollToCursor(barBoundings); + const handler = this.customScrollHandler ?? this._defaultScrollHandler; + if (handler) { + handler.onBeatCursorUpdating( + beatBoundings, + nextBeatBoundings === null ? undefined : nextBeatBoundings, + cursorMode, + startBeatX, + nextBeatX, + duration + ); + } } // trigger an event for others to indicate which beat/bar is played @@ -2368,8 +2496,8 @@ export class AlphaTabApiBase { private _isBeatMouseDown: boolean = false; private _isNoteMouseDown: boolean = false; - private _selectionStart: SelectionInfo | null = null; - private _selectionEnd: SelectionInfo | null = null; + private _selectionStart?: SelectionInfo; + private _selectionEnd?: SelectionInfo; /** * This event is fired whenever a the user presses the mouse button on a beat. @@ -2621,8 +2749,8 @@ export class AlphaTabApiBase { } if (this._hasCursor && this.settings.player.enableUserInteraction) { - this._selectionStart = new SelectionInfo(beat); - this._selectionEnd = null; + this._selectionStart = { beat }; + this._selectionEnd = undefined; } this._isBeatMouseDown = true; (this.beatMouseDown as EventEmitterOfT).trigger(beat); @@ -2646,7 +2774,7 @@ export class AlphaTabApiBase { if (this.settings.player.enableUserInteraction) { if (!this._selectionEnd || this._selectionEnd.beat !== beat) { - this._selectionEnd = new SelectionInfo(beat); + this._selectionEnd = { beat }; this._cursorSelectRange(this._selectionStart, this._selectionEnd); } } @@ -2669,51 +2797,7 @@ export class AlphaTabApiBase { } if (this._hasCursor && this.settings.player.enableUserInteraction) { - if (this._selectionEnd) { - const startTick: number = - this._tickCache?.getBeatStart(this._selectionStart!.beat) ?? - this._selectionStart!.beat.absolutePlaybackStart; - const endTick: number = - this._tickCache?.getBeatStart(this._selectionEnd!.beat) ?? - this._selectionEnd!.beat.absolutePlaybackStart; - if (endTick < startTick) { - const t: SelectionInfo = this._selectionStart!; - this._selectionStart = this._selectionEnd; - this._selectionEnd = t; - } - } - if (this._selectionStart && this._tickCache) { - // get the start and stop ticks (which consider properly repeats) - const tickCache: MidiTickLookup = this._tickCache; - const realMasterBarStart: number = tickCache.getMasterBarStart( - this._selectionStart.beat.voice.bar.masterBar - ); - // move to selection start - this._currentBeat = null; // reset current beat so it is updating the cursor - if (this._player.state === PlayerState.Paused) { - this._cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1); - } - this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart; - // set playback range - if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) { - const realMasterBarEnd: number = tickCache.getMasterBarStart( - this._selectionEnd.beat.voice.bar.masterBar - ); - - const range = new PlaybackRange(); - range.startTick = realMasterBarStart + this._selectionStart.beat.playbackStart; - range.endTick = - realMasterBarEnd + - this._selectionEnd.beat.playbackStart + - this._selectionEnd.beat.playbackDuration - - 50; - this.playbackRange = range; - } else { - this._selectionStart = null; - this.playbackRange = null; - this._cursorSelectRange(this._selectionStart, this._selectionEnd); - } - } + this.applyPlaybackRangeFromHighlight(); } (this.beatMouseUp as EventEmitterOfT).trigger(beat); @@ -2739,12 +2823,12 @@ export class AlphaTabApiBase { const startBeat = this._tickCache.findBeat(this._trackIndexLookup!, range.startTick); const endBeat = this._tickCache.findBeat(this._trackIndexLookup!, range.endTick); if (startBeat && endBeat) { - const selectionStart = new SelectionInfo(startBeat.beat); - const selectionEnd = new SelectionInfo(endBeat.beat); + const selectionStart: SelectionInfo = { beat: startBeat.beat }; + const selectionEnd: SelectionInfo = { beat: endBeat.beat }; this._cursorSelectRange(selectionStart, selectionEnd); } } else { - this._cursorSelectRange(null, null); + this._cursorSelectRange(undefined, undefined); } } @@ -2817,27 +2901,245 @@ export class AlphaTabApiBase { }); } - private _cursorSelectRange(startBeat: SelectionInfo | null, endBeat: SelectionInfo | null): void { + /** + * Places the highlight markers at the specified start and end-beat range. + * @param startBeat The start beat where the selection should start + * @param endBeat The end beat where the selection should end. + * + * @remarks + * Unlike actually setting {@link playbackRange} this method only places the selection markers without actually + * changing the playback range. This method can be used when building custom selection systems (e.g. having draggable handles). + * + * @category Methods - Player + * @since 1.8.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * const startBeat = api.score.tracks[0].staves[0].bars[0].voices[0].beats[0]; + * const endBeat = api.score.tracks[0].staves[0].bars[3].voices[0].beats[0]; + * api.highlightPlaybackRange(startBeat, endBeat); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.ChangeTrackVolume(new Track[] { api.Score.Tracks[0], api.Score.Tracks[1] }, 1.5); + * api.ChangeTrackVolume(new Track[] { api.Score.Tracks[2] }, 0.5); + * var startBeat = api.Score.Tracks[0].Staves[0].Bars[0].Voices[0].Beats[0]; + * var endBeat = api.Score.Tracks[0].Staves[0].Bars[3].Voices[0].Beats[0]; + * api.HighlightPlaybackRange(startBeat, endBeat); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * val startBeat = api.score.tracks[0].staves[0].bars[0].voices[0].beats[0] + * val endBeat = api.score.tracks[0].staves[0].bars[3].voices[0].beats[0] + * api.highlightPlaybackRange(startBeat, endBeat) + * ``` + */ + public highlightPlaybackRange(startBeat: Beat, endBeat: Beat) { + this._selectionStart = { beat: startBeat }; + this._selectionEnd = { beat: endBeat }; + this._cursorSelectRange(this._selectionStart, this._selectionEnd); + } + + /** + * Applies the playback range from the currently highlighted range. + * + * @remarks + * This method can be used when building custom selection systems (e.g. having draggable handles). + * + * @category Methods - Player + * @since 1.8.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * const startBeat = api.score.tracks[0].staves[0].bars[0].voices[0].beats[0]; + * const endBeat = api.score.tracks[0].staves[0].bars[3].voices[0].beats[0]; + * api.highlightPlaybackRange(startBeat, endBeat); + * api.applyPlaybackRangeFromHighlight(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.ChangeTrackVolume(new Track[] { api.Score.Tracks[0], api.Score.Tracks[1] }, 1.5); + * api.ChangeTrackVolume(new Track[] { api.Score.Tracks[2] }, 0.5); + * var startBeat = api.Score.Tracks[0].Staves[0].Bars[0].Voices[0].Beats[0]; + * var endBeat = api.Score.Tracks[0].Staves[0].Bars[3].Voices[0].Beats[0]; + * api.HighlightPlaybackRange(startBeat, endBeat); + * api.ApplyPlaybackRangeFromHighlight(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * val startBeat = api.score.tracks[0].staves[0].bars[0].voices[0].beats[0] + * val endBeat = api.score.tracks[0].staves[0].bars[3].voices[0].beats[0] + * api.highlightPlaybackRange(startBeat, endBeat) + * api.applyPlaybackRangeFromHighlight() + * ``` + */ + public applyPlaybackRangeFromHighlight() { + if (this._selectionEnd) { + const startTick: number = + this._tickCache?.getBeatStart(this._selectionStart!.beat) ?? + this._selectionStart!.beat.absolutePlaybackStart; + const endTick: number = + this._tickCache?.getBeatStart(this._selectionEnd!.beat) ?? + this._selectionEnd!.beat.absolutePlaybackStart; + if (endTick < startTick) { + const t: SelectionInfo = this._selectionStart!; + this._selectionStart = this._selectionEnd; + this._selectionEnd = t; + } + } + if (this._selectionStart && this._tickCache) { + // get the start and stop ticks (which consider properly repeats) + const tickCache: MidiTickLookup = this._tickCache; + const realMasterBarStart: number = tickCache.getMasterBarStart( + this._selectionStart.beat.voice.bar.masterBar + ); + // move to selection start + this._currentBeat = null; // reset current beat so it is updating the cursor + if (this._player.state === PlayerState.Paused) { + this._cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1); + } + this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart; + // set playback range + if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) { + const realMasterBarEnd: number = tickCache.getMasterBarStart( + this._selectionEnd.beat.voice.bar.masterBar + ); + + const range = new PlaybackRange(); + range.startTick = realMasterBarStart + this._selectionStart.beat.playbackStart; + range.endTick = + realMasterBarEnd + + this._selectionEnd.beat.playbackStart + + this._selectionEnd.beat.playbackDuration - + 50; + this.playbackRange = range; + } else { + this._selectionStart = undefined; + this.playbackRange = null; + this._cursorSelectRange(this._selectionStart, this._selectionEnd); + } + } + } + + /** + * Clears the highlight markers marking the currently selected playback range. + * + * @remarks + * Unlike actually setting {@link playbackRange} this method only clears the selection markers without actually + * changing the playback range. This method can be used when building custom selection systems (e.g. having draggable handles). + * + * @category Methods - Player + * @since 1.8.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.clearPlaybackRangeHighlight(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.clearPlaybackRangeHighlight(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.clearPlaybackRangeHighlight() + * ``` + */ + public clearPlaybackRangeHighlight() { + this._cursorSelectRange(undefined, undefined); + } + + /** + * This event is fired the shown highlights for the selected playback range changes. + * + * @remarks + * This event is fired already during selection and not only when the selection is completed. + * This event can be used to place additional custom selection markers (like drag handles). + * + * @eventProperty + * @category Events - Player + * @since 1.8.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playbackRangeHighlightChanged.on(e => { + * updateSelectionHandles(e); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlaybackRangeHighlightChanged.On(e => + * { + * UpdateSelectionHandles(e); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playbackRangeHighlightChanged.on { e -> + * updateSelectionHandles(e) + * } + * ``` + * + */ + public readonly playbackRangeHighlightChanged: IEventEmitterOfT = + new EventEmitterOfT(); + + private _cursorSelectRange(startBeat: SelectionInfo | undefined, endBeat: SelectionInfo | undefined): void { const cache: BoundsLookup | null = this._renderer.boundsLookup; if (!cache) { + (this.playbackRangeHighlightChanged as EventEmitterOfT).trigger({}); return; } const selectionWrapper: IContainer | null = this._selectionWrapper; if (!selectionWrapper) { + (this.playbackRangeHighlightChanged as EventEmitterOfT).trigger({}); return; } selectionWrapper.clear(); if (!startBeat || !endBeat || startBeat.beat === endBeat.beat) { + (this.playbackRangeHighlightChanged as EventEmitterOfT).trigger({}); return; } if (!startBeat.bounds) { - startBeat.bounds = cache.findBeat(startBeat.beat); + startBeat.bounds = cache.findBeat(startBeat.beat) ?? undefined; } if (!endBeat.bounds) { - endBeat.bounds = cache.findBeat(endBeat.beat); + endBeat.bounds = cache.findBeat(endBeat.beat) ?? undefined; } + const startTick: number = this._tickCache?.getBeatStart(startBeat.beat) ?? startBeat.beat.absolutePlaybackStart; const endTick: number = this._tickCache?.getBeatStart(endBeat.beat) ?? endBeat.beat.absolutePlaybackStart; if (endTick < startTick) { @@ -2845,7 +3147,20 @@ export class AlphaTabApiBase { startBeat = endBeat; endBeat = t; } - const startX: number = startBeat.bounds!.realBounds.x; + + const eventArgs: PlaybackHighlightChangeEventArgs = { + startBeat: startBeat.beat, + startBeatBounds: startBeat.bounds, + endBeat: endBeat.beat, + endBeatBounds: endBeat.bounds, + highlightBlocks: [] + }; + + let startX: number = startBeat.bounds!.realBounds.x; + if (startBeat.beat.index === 0) { + startX = startBeat.bounds!.barBounds.masterBarBounds.realBounds.x; + } + let endX: number = endBeat.bounds!.realBounds.x + endBeat.bounds!.realBounds.w; if (endBeat.beat.index === endBeat.beat.voice.beats.length - 1) { endX = @@ -2865,45 +3180,75 @@ export class AlphaTabApiBase { startBeat.bounds!.barBounds.masterBarBounds.staffSystemBounds!.visualBounds.x + startBeat.bounds!.barBounds.masterBarBounds.staffSystemBounds!.visualBounds.w; const startSelection: IContainer = this.uiFacade.createSelectionElement()!; - startSelection.setBounds( + const startSelectionBounds = new Bounds( startX, startBeat.bounds!.barBounds.masterBarBounds.visualBounds.y, staffEndX - startX, startBeat.bounds!.barBounds.masterBarBounds.visualBounds.h ); + startSelection.setBounds( + startSelectionBounds.x, + startSelectionBounds.y, + startSelectionBounds.w, + startSelectionBounds.h + ); + eventArgs.highlightBlocks!.push(startSelectionBounds); + selectionWrapper.appendChild(startSelection); const staffStartIndex: number = startBeat.bounds!.barBounds.masterBarBounds.staffSystemBounds!.index + 1; const staffEndIndex: number = endBeat.bounds!.barBounds.masterBarBounds.staffSystemBounds!.index; for (let staffIndex: number = staffStartIndex; staffIndex < staffEndIndex; staffIndex++) { const staffBounds: StaffSystemBounds = cache.staffSystems[staffIndex]; const middleSelection: IContainer = this.uiFacade.createSelectionElement()!; - middleSelection.setBounds( + const middleSelectionBounds = new Bounds( staffStartX, staffBounds.visualBounds.y, staffEndX - staffStartX, staffBounds.visualBounds.h ); + eventArgs.highlightBlocks!.push(middleSelectionBounds); + + middleSelection.setBounds( + middleSelectionBounds.x, + middleSelectionBounds.y, + middleSelectionBounds.w, + middleSelectionBounds.h + ); selectionWrapper.appendChild(middleSelection); } const endSelection: IContainer = this.uiFacade.createSelectionElement()!; - endSelection.setBounds( + const endSelectionBounds = new Bounds( staffStartX, endBeat.bounds!.barBounds.masterBarBounds.visualBounds.y, endX - staffStartX, endBeat.bounds!.barBounds.masterBarBounds.visualBounds.h ); + eventArgs.highlightBlocks!.push(endSelectionBounds); + + endSelection.setBounds( + endSelectionBounds.x, + endSelectionBounds.y, + endSelectionBounds.w, + endSelectionBounds.h + ); selectionWrapper.appendChild(endSelection); } else { // if the beats are on the same staff, we simply highlight from the startbeat to endbeat const selection: IContainer = this.uiFacade.createSelectionElement()!; - selection.setBounds( + const selectionBounds = new Bounds( startX, startBeat.bounds!.barBounds.masterBarBounds.visualBounds.y, endX - startX, startBeat.bounds!.barBounds.masterBarBounds.visualBounds.h ); + + selection.setBounds(selectionBounds.x, selectionBounds.y, selectionBounds.w, selectionBounds.h); + eventArgs.highlightBlocks!.push(selectionBounds); + selectionWrapper.appendChild(selection); } + + (this.playbackRangeHighlightChanged as EventEmitterOfT).trigger(eventArgs); } /** @@ -3158,6 +3503,8 @@ export class AlphaTabApiBase { return; } + this._beatVisibilityChecker.bounds = this.boundsLookup; + this._currentBeat = null; this._cursorUpdateTick(this._previousTick, false, 1, true, true); @@ -3527,6 +3874,7 @@ export class AlphaTabApiBase { const tickCache = this._tickCache; if (currentBeat && tickCache) { this._player.tickPosition = tickCache.getBeatStart(currentBeat.beat); + this.scrollToCursor(); } } @@ -3578,10 +3926,12 @@ export class AlphaTabApiBase { return; } - this._previousTick = e.currentTick; + const currentTick = e.currentTick; + + this._previousTick = currentTick; this.uiFacade.beginInvoke(() => { const cursorSpeed = e.modifiedTempo / e.originalTempo; - this._cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek); + this._cursorUpdateTick(currentTick, false, cursorSpeed, false, e.isSeek); }); this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e); diff --git a/packages/alphatab/src/DisplaySettings.ts b/packages/alphatab/src/DisplaySettings.ts index 40f807db1..3e2f526e9 100644 --- a/packages/alphatab/src/DisplaySettings.ts +++ b/packages/alphatab/src/DisplaySettings.ts @@ -71,6 +71,7 @@ export class DisplaySettings { * @remarks * AlphaTab has various stave profiles that define which staves will be shown in for the rendered tracks. Its recommended * to keep this on {@link StaveProfile.Default} and rather rely on the options available ob {@link Staff} level + * @deprecated Set the notation visibility by modifying the {@link Staff} properties. */ public staveProfile: StaveProfile = StaveProfile.Default; @@ -190,13 +191,15 @@ export class DisplaySettings { */ public padding: number[] = [35, 35]; + // system paddings + /** * The top padding applied to first system. * @since 1.4.0 * @category Display - * @defaultValue `5` + * @defaultValue `0` */ - public firstSystemPaddingTop: number = 5; + public firstSystemPaddingTop: number = 0; /** * The top padding applied systems beside the first one. @@ -210,17 +213,16 @@ export class DisplaySettings { * The bottom padding applied to systems beside the last one. * @since 1.4.0 * @category Display - * @defaultValue `20` + * @defaultValue `10` */ public systemPaddingBottom: number = 10; - /** * The bottom padding applied to the last system. * @since 1.4.0 * @category Display - * @defaultValue `0` + * @defaultValue `5` */ - public lastSystemPaddingBottom: number = 0; + public lastSystemPaddingBottom: number = 5; /** * The padding left to the track name label of the system. @@ -246,27 +248,47 @@ export class DisplaySettings { */ public accoladeBarPaddingRight: number = 3; + // Staff padding + /** - * The bottom padding applied to main notation staves (standard, tabs, numbered, slash). + * The top padding applied to the first main notation staff (standard, tabs, numbered, slash). + * @since 1.8.0 + * @category Display + * @defaultValue `0` + */ + public firstNotationStaffPaddingTop: number = 0; + + /** + * The bottom padding applied to last main notation staff (standard, tabs, numbered, slash). + * @since 1.8.0 + * @category Display + * @defaultValue `0` + */ + public lastNotationStaffPaddingBottom: number = 0; + + /** + * The top padding applied to main notation staves (standard, tabs, numbered, slash). * @since 1.4.0 * @category Display - * @defaultValue `5` + * @defaultValue `0` */ - public notationStaffPaddingTop: number = 5; + public notationStaffPaddingTop: number = 0; /** * The bottom padding applied to main notation staves (standard, tabs, numbered, slash). * @since 1.4.0 * @category Display - * @defaultValue `5` + * @defaultValue `0` */ - public notationStaffPaddingBottom: number = 5; + public notationStaffPaddingBottom: number = 0; /** * The top padding applied to effect annotation staffs. * @since 1.4.0 * @category Display * @defaultValue `0` + * @deprecated Effect staves do not exist anymore, effects are now part of the main notation staves. This value has no effect anymore. + * Use {@link effectBandPaddingBottom} to control the padding after effect bands. */ public effectStaffPaddingTop: number = 0; @@ -275,6 +297,8 @@ export class DisplaySettings { * @since 1.4.0 * @category Display * @defaultValue `0` + * @deprecated Effect staves do not exist anymore, effects are now part of the main notation staves. This value has no effect anymore. + * Use {@link effectBandPaddingBottom} to control the padding after effect bands. */ public effectStaffPaddingBottom: number = 0; @@ -295,13 +319,29 @@ export class DisplaySettings { public staffPaddingLeft: number = 2; /** - * The padding between individual effect bands. + * The padding between individual effect bands. * @since 1.7.0 * @category Display * @defaultValue `2` */ public effectBandPaddingBottom = 2; + /** + * The additional padding to apply between the staves of two separate tracks. + * @since 1.8.0 + * @category Display + * @defaultValue `5` + */ + public trackStaffPaddingBetween = 5; + + /** + * The additional padding to apply between multiple lyric lines. + * @since 1.8.0 + * @category Display + * @defaultValue `5` + */ + public lyricLinesPaddingBetween = 5; + /** * The mode used to arrange staves and systems. * @since 1.3.0 @@ -364,6 +404,7 @@ export class DisplaySettings { * * * Comparing files against each other (top/bottom comparison) * * Aligning the playback of multiple files on one screen assuming the same tempo (e.g. one file per track). + * @deprecated Use the {@link LayoutMode.Parchment} to display a music sheet respecting the systems layout. */ public systemsLayoutMode: SystemsLayoutMode = SystemsLayoutMode.Automatic; } diff --git a/packages/alphatab/src/EngravingSettings.ts b/packages/alphatab/src/EngravingSettings.ts index 072722f39..5517fb094 100644 --- a/packages/alphatab/src/EngravingSettings.ts +++ b/packages/alphatab/src/EngravingSettings.ts @@ -2,8 +2,7 @@ import { EngravingSettingsCloner } from '@coderline/alphatab/generated/Engraving import { JsonHelper } from '@coderline/alphatab/io/JsonHelper'; import { Logger } from '@coderline/alphatab/Logger'; import { Duration } from '@coderline/alphatab/model/Duration'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { MusicFontSymbol, MusicFontSymbolLookup } from '@coderline/alphatab/model/MusicFontSymbol'; import type { SmuflMetadata } from '@coderline/alphatab/SmuflMetadata'; /** @@ -53,6 +52,12 @@ export class EngravingStemInfo { export class EngravingSettings { private static _bravuraDefaults?: EngravingSettings; + // NOTE: configurable in future? + /** + * @internal + */ + public static readonly GraceScale: number = 0.75; + /** * A {@link EngravingSettings} copy filled with the settings of the Bravura font used by default in alphaTab. */ @@ -352,6 +357,22 @@ export class EngravingSettings { this.stemFlagOffsets.set(Duration.OneHundredTwentyEighth, 0); this.stemFlagOffsets.set(Duration.TwoHundredFiftySixth, 0); + // Workaround for: https://github.com/w3c/smufl/issues/203 + // There is no clear anchor for the height of flags on the stem side yet. + // These aproximations are tested with bravura + + this.stemFlagHeight.set(Duration.QuadrupleWhole, 0); + this.stemFlagHeight.set(Duration.DoubleWhole, 0); + this.stemFlagHeight.set(Duration.Whole, 0); + this.stemFlagHeight.set(Duration.Half, 0); + this.stemFlagHeight.set(Duration.Quarter, 0); + this.stemFlagHeight.set(Duration.Eighth, 1 * this.oneStaffSpace); + this.stemFlagHeight.set(Duration.Sixteenth, 1.5 * this.oneStaffSpace); + this.stemFlagHeight.set(Duration.ThirtySecond, 2 * this.oneStaffSpace); + this.stemFlagHeight.set(Duration.SixtyFourth, 3 * this.oneStaffSpace); + this.stemFlagHeight.set(Duration.OneHundredTwentyEighth, 3.5 * this.oneStaffSpace); + this.stemFlagHeight.set(Duration.TwoHundredFiftySixth, 4.2 * this.oneStaffSpace); + for (const [g, v] of Object.entries(smufl.glyphsWithAnchors)) { const symbol = EngravingSettings._smuflNameToMusicFontSymbol(g); if (symbol) { @@ -431,7 +452,7 @@ export class EngravingSettings { MusicFontSymbol.NoteheadNull ]); - for (const symbol of ModelUtils.getAllMusicFontSymbols()) { + for (const symbol of MusicFontSymbolLookup.getAllMusicFontSymbols()) { if (!handledSymbols.has(symbol)) { if (!ignoredSymbols.has(symbol)) { Logger.warning( @@ -449,7 +470,7 @@ export class EngravingSettings { // // custom alphatab sizes this.numberedBarRendererBarSize = this.staffLineThickness * 2; - this.numberedBarRendererBarSpacing = this.beamSpacing + this.numberedBarRendererBarSize; + this.numberedBarRendererBarSpacing = this.beamSpacing; this.preNoteEffectPadding = 0.4 * this.oneStaffSpace; this.postNoteEffectPadding = 0.2 * this.oneStaffSpace; this.lineRangedGlyphDashGap = 0.5 * this.oneStaffSpace; @@ -476,6 +497,7 @@ export class EngravingSettings { this.songBookWhammyDipHeight = 0.6 * this.oneStaffSpace; this.tabWhammyPerHalfHeight = 0.6 * this.oneStaffSpace; + this.tabBendStaffPadding = 0.5 * this.oneStaffSpace; this.tabBendPerValueHeight = 0.6 * this.oneStaffSpace; this.tabBendLabelPadding = 0.3 * this.oneStaffSpace; @@ -497,6 +519,7 @@ export class EngravingSettings { this.tripletFeelBracketPadding = 0.2 * this.oneStaffSpace; this.accidentalPadding = 0.1 * this.oneStaffSpace; this.preBeatGlyphSpacing = 0.5 * this.oneStaffSpace; + this.multiVoiceDisplacedNoteHeadSpacing = 0.2 * this.oneStaffSpace; this.tuningGlyphStringRowPadding = 0.2 * this.oneStaffSpace; } @@ -532,7 +555,7 @@ export class EngravingSettings { public numberedBarRendererBarSpacing = 0; /** - * The size of the dashed drawn in numbered notation to indicate the durations. + * The padding minimum between the duration dashes. */ public numberedDashGlyphPadding = 0; @@ -553,21 +576,20 @@ export class EngravingSettings { public lineRangedGlyphDashSize = 0; /** - * The padding between effects and glyphs placed before the note heads, e.g. accidentals or brushes + * The padding between effects and glyphs placed before the note heads, e.g. accidentals or brushes */ public preNoteEffectPadding = 0; /** - * The padding between effects and glyphs placed after the note heads, e.g. slides or bends + * The padding between effects and glyphs placed after the note heads, e.g. slides or bends */ public postNoteEffectPadding = 0; /** - * The padding between effects and glyphs placed above/blow the note heads e.g. staccato + * The padding between effects and glyphs placed above/blow the note heads e.g. staccato */ public onNoteEffectPadding = 0; - /** * The padding between the circles around string numbers. */ @@ -599,7 +621,7 @@ export class EngravingSettings { public tieHeight = 0; /** - * The padding between the border and text of beat timers. + * The padding between the border and text of beat timers. */ public beatTimerPadding = 0; @@ -629,7 +651,7 @@ export class EngravingSettings { public tabWhammyTextPadding = 0; /** - * The height applied per half-note whammy. + * The height applied per half-note whammy. */ public tabWhammyPerHalfHeight = 0; @@ -657,9 +679,15 @@ export class EngravingSettings { * The size of the dashes on bends (e.g. on holds) */ public tabBendDashSize = 0; - + + /** + * The additional padding between the staff and the point + * where bend values are calculated from. + */ + public tabBendStaffPadding = 0; + /** - * The height applied per quarter-note. + * The height applied per quarter-note. */ public tabBendPerValueHeight = 0; @@ -707,7 +735,7 @@ export class EngravingSettings { public chordDiagramLineWidth = 0; /** - * The padding between the bracket lines and numbers of tuplets + * The padding between the bracket lines and numbers of tuplets */ public tripletFeelBracketPadding = 0; @@ -746,6 +774,27 @@ export class EngravingSettings { */ public directionsScale = 0.6; + /** + * The spacing between displaced displaced note heads + * in case of multi-voice note head overlaps. + */ + public multiVoiceDisplacedNoteHeadSpacing = 0; + + /** + * Calculates the stem height for a note of the given duration. + * @param duration The duration to calculate the height respecting flag sizes. + * @param hasFlag True if we need to respect flags, false if we have beams. + * @returns The total stem height + */ + public getStemLength(duration: Duration, hasFlag: boolean) { + return this.standardStemLength + (hasFlag ? this.stemFlagOffsets.get(duration)! : 0); + } + + /** + * The space needed by flags on the stem-side from top to bottom to place. + */ + public stemFlagHeight: Map = new Map(); + // Idea: maybe we can encode and pack this large metadata into a more compact format (e.g. BSON or a custom binary blob?) // This metadata below is updated automatically from the bravura_metadata.json via npm script @@ -879,6 +928,10 @@ export class EngravingSettings { bBoxNE: [1.876, 1.18], bBoxSW: [0, 0] }, + buzzRoll: { + bBoxNE: [0.624, 0.464], + bBoxSW: [-0.62, -0.464] + }, cClef: { bBoxNE: [2.796, 2.024], bBoxSW: [0, -2.024] @@ -1835,6 +1888,14 @@ export class EngravingSettings { bBoxNE: [0.6, 1.112], bBoxSW: [-0.6, -1.12] }, + tremolo4: { + bBoxNE: [0.6, 1.496], + bBoxSW: [-0.6, -1.48] + }, + tremolo5: { + bBoxNE: [0.6, 1.88], + bBoxSW: [-0.604, -1.84] + }, tuplet0: { bBoxNE: [1.2731041262817027, 1.5], bBoxSW: [-0.001204330173715796, -0.032] diff --git a/packages/alphatab/src/Environment.ts b/packages/alphatab/src/Environment.ts index cacd16a8a..6eb7d74cb 100644 --- a/packages/alphatab/src/Environment.ts +++ b/packages/alphatab/src/Environment.ts @@ -21,8 +21,7 @@ import { JQueryAlphaTab } from '@coderline/alphatab/platform/javascript/JQueryAl 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 type { BarRendererFactory } from '@coderline/alphatab/rendering/BarRendererFactory'; -import { EffectBarRendererFactory } from '@coderline/alphatab/rendering/EffectBarRendererFactory'; +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'; import { BeatTimerEffectInfo } from '@coderline/alphatab/rendering/effects/BeatTimerEffectInfo'; @@ -42,14 +41,17 @@ import { LetRingEffectInfo } from '@coderline/alphatab/rendering/effects/LetRing import { LyricsEffectInfo } from '@coderline/alphatab/rendering/effects/LyricsEffectInfo'; import { MarkerEffectInfo } from '@coderline/alphatab/rendering/effects/MarkerEffectInfo'; import { NoteOrnamentEffectInfo } from '@coderline/alphatab/rendering/effects/NoteOrnamentEffectInfo'; +import { NumberedBarKeySignatureEffectInfo } from '@coderline/alphatab/rendering/effects/NumberedBarKeySignatureEffectInfo'; import { OttaviaEffectInfo } from '@coderline/alphatab/rendering/effects/OttaviaEffectInfo'; import { PalmMuteEffectInfo } from '@coderline/alphatab/rendering/effects/PalmMuteEffectInfo'; import { PickSlideEffectInfo } from '@coderline/alphatab/rendering/effects/PickSlideEffectInfo'; import { PickStrokeEffectInfo } from '@coderline/alphatab/rendering/effects/PickStrokeEffectInfo'; import { RasgueadoEffectInfo } from '@coderline/alphatab/rendering/effects/RasgueadoEffectInfo'; +import { SimpleDipWhammyBarEffectInfo } from '@coderline/alphatab/rendering/effects/SimpleDipWhammyBarEffectInfo'; import { SlightBeatVibratoEffectInfo } from '@coderline/alphatab/rendering/effects/SlightBeatVibratoEffectInfo'; import { SlightNoteVibratoEffectInfo } from '@coderline/alphatab/rendering/effects/SlightNoteVibratoEffectInfo'; import { SustainPedalEffectInfo } from '@coderline/alphatab/rendering/effects/SustainPedalEffectInfo'; +import { TabWhammyEffectInfo } from '@coderline/alphatab/rendering/effects/TabWhammyEffectInfo'; import { TapEffectInfo } from '@coderline/alphatab/rendering/effects/TapEffectInfo'; import { TempoEffectInfo } from '@coderline/alphatab/rendering/effects/TempoEffectInfo'; import { TextEffectInfo } from '@coderline/alphatab/rendering/effects/TextEffectInfo'; @@ -61,11 +63,14 @@ import { WideBeatVibratoEffectInfo } from '@coderline/alphatab/rendering/effects import { WideNoteVibratoEffectInfo } from '@coderline/alphatab/rendering/effects/WideNoteVibratoEffectInfo'; import { HorizontalScreenLayout } from '@coderline/alphatab/rendering/layout/HorizontalScreenLayout'; import { PageViewLayout } from '@coderline/alphatab/rendering/layout/PageViewLayout'; +import { ParchmentLayout } from '@coderline/alphatab/rendering/layout/ParchmentLayout'; import type { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; +import { NumberedBarRenderer } from '@coderline/alphatab/rendering/NumberedBarRenderer'; import { NumberedBarRendererFactory } from '@coderline/alphatab/rendering/NumberedBarRendererFactory'; import { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import { ScoreBarRendererFactory } from '@coderline/alphatab/rendering/ScoreBarRendererFactory'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; +import { SlashBarRenderer } from '@coderline/alphatab/rendering/SlashBarRenderer'; import { SlashBarRendererFactory } from '@coderline/alphatab/rendering/SlashBarRendererFactory'; import { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; import { TabBarRendererFactory } from '@coderline/alphatab/rendering/TabBarRendererFactory'; @@ -122,14 +127,6 @@ export class RenderEngineFactory { * @public */ export class Environment { - private static readonly _staffIdBeforeSlashAlways = 'before-slash-always'; - private static readonly _staffIdBeforeScoreAlways = 'before-score-always'; - private static readonly _staffIdBeforeScoreHideable = 'before-score-hideable'; - private static readonly _staffIdBeforeNumberedAlways = 'before-numbered-always'; - private static readonly _staffIdBeforeTabAlways = 'before-tab-always'; - private static readonly _staffIdBeforeTabHideable = 'before-tab-hideable'; - private static readonly _staffIdBeforeEndAlways = 'before-end-always'; - /** * The scaling factor to use when rending raster graphics for sharper rendering on high-dpi displays. * @internal @@ -379,8 +376,7 @@ export class Environment { /** * @internal */ - public static readonly staveProfiles: Map = - Environment._createDefaultStaveProfiles(); + public static readonly staveProfiles: Map> = Environment._createDefaultStaveProfiles(); public static getRenderEngineFactory(engine: string): RenderEngineFactory { if (!engine || !Environment.renderEngines.has(engine)) { @@ -466,158 +462,149 @@ export class Environment { ); } - private static _createDefaultRenderers(): BarRendererFactory[] { - return [ - // - // Slash - new EffectBarRendererFactory(Environment._staffIdBeforeSlashAlways, [ - new TempoEffectInfo(), - new TripletFeelEffectInfo(), - new MarkerEffectInfo(), - new DirectionsEffectInfo(), - new AlternateEndingsEffectInfo(), - new FreeTimeEffectInfo(), - new TextEffectInfo(), - new BeatTimerEffectInfo(), - new ChordsEffectInfo() - ]), - // no before-slash-hideable - new SlashBarRendererFactory(), - - // - // Score (standard notation) - new EffectBarRendererFactory(Environment._staffIdBeforeScoreAlways, [ - new FermataEffectInfo(), - new BeatBarreEffectInfo(), - new NoteOrnamentEffectInfo(), - new RasgueadoEffectInfo(), - new WahPedalEffectInfo() - ]), - new EffectBarRendererFactory( - Environment._staffIdBeforeScoreHideable, - [ - new WhammyBarEffectInfo(), - new TrillEffectInfo(), - new OttaviaEffectInfo(true), - new WideBeatVibratoEffectInfo(), - new SlightBeatVibratoEffectInfo(), - new WideNoteVibratoEffectInfo(), - new SlightNoteVibratoEffectInfo(false), - new LeftHandTapEffectInfo(), - new GolpeEffectInfo(GolpeType.Finger) - ], - (_, staff) => staff.showStandardNotation - ), - new ScoreBarRendererFactory(), - - // - // Numbered - new EffectBarRendererFactory(Environment._staffIdBeforeNumberedAlways, [ - new CrescendoEffectInfo(), - new OttaviaEffectInfo(false), - new DynamicsEffectInfo(), - new GolpeEffectInfo(GolpeType.Thumb, (_s, b) => b.voice.bar.staff.showStandardNotation), - new SustainPedalEffectInfo() - ]), - // no before-numbered-hideable - new NumberedBarRendererFactory(), - - // - // Tabs - new EffectBarRendererFactory(Environment._staffIdBeforeTabAlways, [new LyricsEffectInfo()]), - new EffectBarRendererFactory( - Environment._staffIdBeforeTabHideable, - [ - // TODO: whammy line effect - new TrillEffectInfo(), - new WideBeatVibratoEffectInfo(), - new SlightBeatVibratoEffectInfo(), - new WideNoteVibratoEffectInfo(), - new SlightNoteVibratoEffectInfo(true), - new TapEffectInfo(), - new FadeEffectInfo(), - new HarmonicsEffectInfo(HarmonicType.Natural), - new HarmonicsEffectInfo(HarmonicType.Artificial), - new HarmonicsEffectInfo(HarmonicType.Pinch), - new HarmonicsEffectInfo(HarmonicType.Tap), - new HarmonicsEffectInfo(HarmonicType.Semi), - new HarmonicsEffectInfo(HarmonicType.Feedback), - new LetRingEffectInfo(), - new CapoEffectInfo(), - new FingeringEffectInfo(), - new PalmMuteEffectInfo(), - new PickStrokeEffectInfo(), - new PickSlideEffectInfo(), - new LeftHandTapEffectInfo(), - new GolpeEffectInfo(GolpeType.Finger, (_s, b) => !b.voice.bar.staff.showStandardNotation) - ], - (_, staff) => staff.showTablature - ), - new TabBarRendererFactory(), - new EffectBarRendererFactory(Environment._staffIdBeforeEndAlways, [ - new GolpeEffectInfo(GolpeType.Thumb, (_s, b) => !b.voice.bar.staff.showStandardNotation) - ]) - ]; - } + /** + * @internal + */ + public static readonly defaultRenderers: BarRendererFactory[] = [ + // + // Slash + new SlashBarRendererFactory([ + { effect: new TempoEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new TripletFeelEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new MarkerEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new DirectionsEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new FreeTimeEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new TextEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new BeatTimerEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new ChordsEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new AlternateEndingsEffectInfo(), mode: EffectBandMode.SharedTop, order: 1000 } + ]), + + // + // Score (standard notation) + new ScoreBarRendererFactory([ + { effect: new CapoEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new FermataEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new BeatBarreEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new NoteOrnamentEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new RasgueadoEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new WahPedalEffectInfo(), mode: EffectBandMode.SharedTop }, + { effect: new WhammyBarEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new SimpleDipWhammyBarEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new TrillEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new OttaviaEffectInfo(true), mode: EffectBandMode.OwnedTop }, + { effect: new LeftHandTapEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new TapEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new WideBeatVibratoEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new SlightBeatVibratoEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new WideNoteVibratoEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new SlightNoteVibratoEffectInfo(false), mode: EffectBandMode.OwnedTop }, + { + effect: new FadeEffectInfo(), + mode: EffectBandMode.OwnedTop, + shouldCreate: staff => !staff.showTablature + }, + { + effect: new LetRingEffectInfo(), + mode: EffectBandMode.OwnedTop, + shouldCreate: staff => !staff.showTablature + }, + { + effect: new PickStrokeEffectInfo(), + mode: EffectBandMode.OwnedTop, + shouldCreate: staff => !staff.showTablature + }, + { + effect: new PickSlideEffectInfo(), + mode: EffectBandMode.OwnedTop, + shouldCreate: staff => !staff.showTablature + }, + + { effect: new GolpeEffectInfo(GolpeType.Finger), mode: EffectBandMode.OwnedTop }, + + { effect: new GolpeEffectInfo(GolpeType.Thumb), mode: EffectBandMode.OwnedBottom }, + { effect: new CrescendoEffectInfo(), mode: EffectBandMode.SharedBottom }, + // NOTE: all octave signs are currently shown above, but 8vb could be shown as 8va below the staff + // { effect: new OttaviaEffectInfo(false), mode: EffectBandMode.SharedBottom }, + { effect: new DynamicsEffectInfo(), mode: EffectBandMode.SharedBottom }, + { effect: new SustainPedalEffectInfo(), mode: EffectBandMode.SharedBottom } + ]), + + // + // Numbered + new NumberedBarRendererFactory([ + { effect: new NumberedBarKeySignatureEffectInfo(), mode: EffectBandMode.OwnedTop, order: 1000 } + ]), + + // + // Tabs + new TabBarRendererFactory([ + { effect: new LyricsEffectInfo(), mode: EffectBandMode.SharedTop }, + + { effect: new TabWhammyEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new TrillEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new WideBeatVibratoEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new SlightBeatVibratoEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new WideNoteVibratoEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new SlightNoteVibratoEffectInfo(true), mode: EffectBandMode.OwnedTop }, + { effect: new TapEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new FadeEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new HarmonicsEffectInfo(HarmonicType.Natural), mode: EffectBandMode.OwnedTop }, + { effect: new HarmonicsEffectInfo(HarmonicType.Artificial), mode: EffectBandMode.OwnedTop }, + { effect: new HarmonicsEffectInfo(HarmonicType.Pinch), mode: EffectBandMode.OwnedTop }, + { effect: new HarmonicsEffectInfo(HarmonicType.Tap), mode: EffectBandMode.OwnedTop }, + { effect: new HarmonicsEffectInfo(HarmonicType.Semi), mode: EffectBandMode.OwnedTop }, + { effect: new HarmonicsEffectInfo(HarmonicType.Feedback), mode: EffectBandMode.OwnedTop }, + { effect: new LetRingEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new FingeringEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new PalmMuteEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new PickStrokeEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new PickSlideEffectInfo(), mode: EffectBandMode.OwnedTop }, + { effect: new LeftHandTapEffectInfo(), mode: EffectBandMode.OwnedTop }, + { + effect: new GolpeEffectInfo(GolpeType.Finger), + mode: EffectBandMode.OwnedTop, + shouldCreate: staff => !staff.showStandardNotation + }, + + { + effect: new GolpeEffectInfo(GolpeType.Thumb), + mode: EffectBandMode.OwnedBottom, + shouldCreate: staff => !staff.showStandardNotation + } + ]) + ]; - private static _createDefaultStaveProfiles(): Map { - const staveProfiles = new Map(); + private static _createDefaultStaveProfiles(): Map> { + const staveProfiles = new Map>(); // the general layout is repeating the same pattern across the different notation staffs: // * general effects before notation renderer, shown also if notation renderer is hidden (`before-xxxx-always`) // * effects specific to the notation renderer, hidden if the nottation renderer is hidden (`before-xxxx-hideable`) // * the notation renderer itself, hidden based on settings (`xxxx`) - const defaultRenderers = Environment._createDefaultRenderers(); - staveProfiles.set(StaveProfile.Default, defaultRenderers); - staveProfiles.set(StaveProfile.ScoreTab, defaultRenderers); - - const scoreRenderers = new Set([ - Environment._staffIdBeforeSlashAlways, - Environment._staffIdBeforeScoreAlways, - Environment._staffIdBeforeNumberedAlways, - Environment._staffIdBeforeTabAlways, - ScoreBarRenderer.StaffId, - Environment._staffIdBeforeEndAlways - ]); staveProfiles.set( - StaveProfile.Score, - defaultRenderers.filter(r => scoreRenderers.has(r.staffId)) + StaveProfile.Default, + new Set([ + SlashBarRenderer.StaffId, + ScoreBarRenderer.StaffId, + NumberedBarRenderer.StaffId, + TabBarRenderer.StaffId + ]) ); - - const tabRenderers = new Set([ - Environment._staffIdBeforeSlashAlways, - Environment._staffIdBeforeScoreAlways, - Environment._staffIdBeforeNumberedAlways, - Environment._staffIdBeforeTabAlways, - TabBarRenderer.StaffId, - Environment._staffIdBeforeEndAlways - ]); staveProfiles.set( - StaveProfile.Tab, - Environment._createDefaultRenderers().filter(r => { - if (r instanceof TabBarRendererFactory) { - const tab = r as TabBarRendererFactory; - tab.showTimeSignature = true; - tab.showRests = true; - tab.showTiedNotes = true; - } - return tabRenderers.has(r.staffId); - }) + StaveProfile.ScoreTab, + new Set([ + SlashBarRenderer.StaffId, + ScoreBarRenderer.StaffId, + NumberedBarRenderer.StaffId, + TabBarRenderer.StaffId + ]) ); - staveProfiles.set( - StaveProfile.TabMixed, - Environment._createDefaultRenderers().filter(r => { - if (r instanceof TabBarRendererFactory) { - const tab = r as TabBarRendererFactory; - tab.showTimeSignature = false; - tab.showRests = false; - tab.showTiedNotes = false; - } - return tabRenderers.has(r.staffId); - }) - ); + staveProfiles.set(StaveProfile.Score, new Set([ScoreBarRenderer.StaffId])); + staveProfiles.set(StaveProfile.Tab, new Set([TabBarRenderer.StaffId])); + staveProfiles.set(StaveProfile.TabMixed, new Set([TabBarRenderer.StaffId])); return staveProfiles; } @@ -637,6 +624,12 @@ export class Environment { return new HorizontalScreenLayout(r); }) ); + engines.set( + LayoutMode.Parchment, + new LayoutEngineFactory(true, r => { + return new ParchmentLayout(r); + }) + ); return engines; } @@ -714,6 +707,15 @@ export class Environment { try { // @ts-expect-error if (typeof __webpack_require__ === 'function') { + // check if webpack plugin was used + // @ts-expect-error + if (typeof __ALPHATAB_WEBPACK__ !== 'boolean') { + Logger.warning( + 'WebPack', + `Detected bundling with WebPack but @coderline/alphatab-webpack was not used! To ensure alphaTab works as expected use our bundler plugins. Learn more at https://www.alphatab.net/docs/getting-started/installation-webpack` + ); + } + return true; } } catch { @@ -729,6 +731,15 @@ export class Environment { try { // @ts-expect-error if (typeof __BASE__ === 'string') { + // check if vite plugin was used + // @ts-expect-error + if (typeof __ALPHATAB_VITE__ !== 'boolean') { + Logger.warning( + 'Vite', + `Detected bundling with Vite but @coderline/alphatab-vite was not used! To ensure alphaTab works as expected use our bundler plugins. Learn more at https://www.alphatab.net/docs/getting-started/installation-vite` + ); + } + return true; } } catch { @@ -848,4 +859,16 @@ export class Environment { public static quoteJsonString(text: string) { return JSON.stringify(text); } + + /** + * @internal + * @target web + * @partial + */ + public static sortDescending(array: number[]) { + // java is a joke: + // no primitive sorting of arrays with custom comparer in 2025 + // so we need to declare this specific helper function and implement it in Kotlin ourselves. + array.sort((a, b) => b - a); + } } diff --git a/packages/alphatab/src/LayoutMode.ts b/packages/alphatab/src/LayoutMode.ts index f690a2963..fc9fa2274 100644 --- a/packages/alphatab/src/LayoutMode.ts +++ b/packages/alphatab/src/LayoutMode.ts @@ -9,6 +9,43 @@ export enum LayoutMode { Page = 0, /** * Bars are aligned horizontally in [one horizontally endless system (row)](https://alphatab.net/docs/showcase/layouts#horizontal-layout) + * + * alphaTab holds following information in the data model and developers can change those values (e.g. by tapping into the `scoreLoaded`) event. + * These widths are respected when using this layout. + * + * **Used when single tracks are rendered:** + * + * * `score.tracks[index].staves[index].bars[index].displayWidth` - The absolute size of this bar when displayed. + * + * **Used when multiple tracks are rendered:** + * + * * `score.masterBars[index].displayWidth` - Like the `displayWidth` on bar level. */ - Horizontal = 1 + Horizontal = 1, + /** + * The bars are aligned in an [vertically endless page-style fashion](https://alphatab.net/docs/showcase/layouts#parchment) + * respecting the configured systems layout. + * + * The parchment layout uses the `systemsLayout` and `defaultSystemsLayout` to decide how many bars go into a single system (row). + * Additionally when sizing the bars within the system the `displayScale` is used. This scale is rather a ratio than an absolute percentage value but percentages work also: + * + * ![Parchment Layout](https://alphatab.net/img/reference/property/systems-layout-page-examples.png) + * + * File formats like Guitar Pro embed information about the layout in the file and alphaTab can read and use this information. + * + * alphaTab holds following information in the data model and developers can change those values (e.g. by tapping into the `scoreLoaded`) event. + * + * **Used when single tracks are rendered:** + * + * * `score.tracks[index].systemsLayout` - An array of numbers describing how many bars should be placed within each system (row). + * * `score.tracks[index].defaultSystemsLayout` - The number of bars to place in a system (row) when no value is defined in the `systemsLayout`. + * * `score.tracks[index].staves[index].bars[index].displayScale` - The relative size of this bar in the system it is placed. Note that this is not directly a percentage value. e.g. if there are 3 bars and all define scale 1, they are sized evenly. + * + * **Used when multiple tracks are rendered:** + * + * * `score.systemsLayout` - Like the `systemsLayout` on track level. + * * `score.defaultSystemsLayout` - Like the `defaultSystemsLayout` on track level. + * * `score.masterBars[index].displayScale` - Like the `displayScale` on bar level. + */ + Parchment = 2 } diff --git a/packages/alphatab/src/NotationSettings.ts b/packages/alphatab/src/NotationSettings.ts index 77f4840df..51514e260 100644 --- a/packages/alphatab/src/NotationSettings.ts +++ b/packages/alphatab/src/NotationSettings.ts @@ -341,7 +341,38 @@ export enum NotationElement { /** * The absolute playback time of beats. */ - EffectBeatTimer = 49 + EffectBeatTimer = 49, + + /** + * The whammy bar line effect shown above the tab staff + */ + EffectWhammyBarLine = 50, + + /** + * The key signature for numbered notation staff. + */ + EffectNumberedNotationKeySignature = 51, + + /** + * The fretboard numbers shown in chord diagrams. + */ + ChordDiagramFretboardNumbers = 52, + + /** + * The bar numbers. + */ + BarNumber = 53, + + /** + * The repeat count indicator shown above the thick bar line to describe + * how many repeats should be played. + */ + RepeatCount = 54, + + /** + * The slurs shown on bend effects within the score staff. + */ + ScoreBendSlur = 55 } /** @@ -479,11 +510,11 @@ export class NotationSettings { * Controls how high the ryhthm notation is rendered below the tab staff * @since 0.9.6 * @category Notation - * @defaultValue `15` + * @defaultValue `25` * @remarks * This setting can be used in combination with the {@link rhythmMode} setting to control how high the rhythm notation should be rendered below the tab staff. */ - public rhythmHeight: number = 15; + public rhythmHeight: number = 25; /** * The transposition pitch offsets for the individual tracks used for rendering and playback. diff --git a/packages/alphatab/src/PlayerSettings.ts b/packages/alphatab/src/PlayerSettings.ts index 9e8b9d782..0e8fc9413 100644 --- a/packages/alphatab/src/PlayerSettings.ts +++ b/packages/alphatab/src/PlayerSettings.ts @@ -14,7 +14,13 @@ export enum ScrollMode { /** * Scrolling happens as soon the cursors exceed the displayed range. */ - OffScreen = 2 + OffScreen = 2, + /** + * Scrolling happens constantly in a smooth fashion. + * This will disable the use of any native scroll optimizations but + * manually scroll the scroll container in the required speed. + */ + Smooth = 3 } /** @@ -219,25 +225,25 @@ export class PlayerSettings { * @category Player * @remarks * This setting configures whether the player feature is enabled or not. Depending on the platform enabling the player needs some additional actions of the developer. - * + * * **Synthesizer** - * + * * If the synthesizer is used (via {@link PlayerMode.EnabledAutomatic} or {@link PlayerMode.EnabledSynthesizer}) a sound font is needed so that the midi synthesizer can produce the audio samples. - * + * * For the JavaScript version the [player.soundFont](/docs/reference/settings/player/soundfont) property must be set to the URL of the sound font that should be used or it must be loaded manually via API. * For .net manually the soundfont must be loaded. - * + * * **Backing Track** - * - * For a built-in backing track of the input file no additional data needs to be loaded (assuming everything is filled via the input file). + * + * For a built-in backing track of the input file no additional data needs to be loaded (assuming everything is filled via the input file). * Otherwise the `score.backingTrack` needs to be filled before loading and the related sync points need to be configured. - * + * * **External Media** - * + * * For synchronizing alphaTab with an external media no data needs to be loaded into alphaTab. The configured sync points on the MasterBars are used * as reference to synchronize the external media with the internal time axis. Then the related APIs on the AlphaTabApi object need to be used * to update the playback state and exterrnal audio position during playback. - * + * * **User Interface** * * AlphaTab does not ship a default UI for the player. The API must be hooked up to some UI controls to allow the user to interact with the player. diff --git a/packages/alphatab/src/RenderingResources.ts b/packages/alphatab/src/RenderingResources.ts index 3ac850b5d..df711ecd7 100644 --- a/packages/alphatab/src/RenderingResources.ts +++ b/packages/alphatab/src/RenderingResources.ts @@ -1,7 +1,8 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { Color } from '@coderline/alphatab/model/Color'; import { Font, FontStyle, FontWeight } from '@coderline/alphatab/model/Font'; import { ScoreSubElement } from '@coderline/alphatab/model/Score'; -import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * This public class contains central definitions for controlling the visual appearance. @@ -13,6 +14,48 @@ export class RenderingResources { private static _sansFont: string = 'Arial, sans-serif'; private static _serifFont: string = 'Georgia, serif'; + private static _effectFont = new Font(RenderingResources._serifFont, 12, FontStyle.Italic); + + /** + * The default fonts for notation elements if not specified by the user. + */ + public static defaultFonts: Map = new Map([ + [NotationElement.ScoreTitle, new Font(RenderingResources._serifFont, 32, FontStyle.Plain)], + [NotationElement.ScoreSubTitle, new Font(RenderingResources._serifFont, 20, FontStyle.Plain)], + [NotationElement.ScoreArtist, new Font(RenderingResources._serifFont, 20, FontStyle.Plain)], + [NotationElement.ScoreAlbum, new Font(RenderingResources._serifFont, 20, FontStyle.Plain)], + [NotationElement.ScoreWords, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)], + [NotationElement.ScoreMusic, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)], + [NotationElement.ScoreWordsAndMusic, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)], + [NotationElement.ScoreCopyright, new Font(RenderingResources._sansFont, 12, FontStyle.Plain, FontWeight.Bold)], + [NotationElement.EffectBeatTimer, new Font(RenderingResources._serifFont, 12, FontStyle.Plain)], + [NotationElement.EffectDirections, new Font(RenderingResources._serifFont, 14, FontStyle.Plain)], + [NotationElement.ChordDiagramFretboardNumbers, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)], + [NotationElement.EffectFingering, new Font(RenderingResources._serifFont, 14, FontStyle.Plain)], + [NotationElement.EffectMarker, new Font(RenderingResources._serifFont, 14, FontStyle.Plain, FontWeight.Bold)], + [NotationElement.EffectCapo, RenderingResources._effectFont], + [NotationElement.EffectFreeTime, RenderingResources._effectFont], + [NotationElement.EffectLyrics, RenderingResources._effectFont], + [NotationElement.EffectTap, RenderingResources._effectFont], + [NotationElement.ChordDiagrams, RenderingResources._effectFont], + [NotationElement.EffectChordNames, RenderingResources._effectFont], + [NotationElement.EffectText, RenderingResources._effectFont], + [NotationElement.EffectPalmMute, RenderingResources._effectFont], + [NotationElement.EffectLetRing, RenderingResources._effectFont], + [NotationElement.EffectBeatBarre, RenderingResources._effectFont], + [NotationElement.EffectTripletFeel, RenderingResources._effectFont], + [NotationElement.EffectHarmonics, RenderingResources._effectFont], + [NotationElement.EffectPickSlide, RenderingResources._effectFont], + [NotationElement.GuitarTuning, RenderingResources._effectFont], + [NotationElement.EffectRasgueado, RenderingResources._effectFont], + [NotationElement.EffectWhammyBar, RenderingResources._effectFont], + [NotationElement.TrackNames, RenderingResources._effectFont], + [NotationElement.RepeatCount, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)], + [NotationElement.BarNumber, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)], + [NotationElement.ScoreBendSlur, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)], + [NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)] + ]); + /** * The name of the SMuFL Font to use for rendering music symbols. * @@ -41,57 +84,189 @@ export class RenderingResources { * The font to use for displaying the songs copyright information in the header of the music sheet. * @defaultValue `bold 12px Arial, sans-serif` * @since 0.9.6 + * @deprecated use {@link elementFonts} with {@link NotationElement.ScoreCopyright} + */ + public get copyrightFont(): Font { + return this.elementFonts.get(NotationElement.ScoreCopyright)!; + } + /** + * @deprecated use {@link elementFonts} with {@link NotationElement.ScoreCopyright} */ - public copyrightFont: Font = new Font(RenderingResources._sansFont, 12, FontStyle.Plain, FontWeight.Bold); + public set copyrightFont(value: Font) { + this.elementFonts.set(NotationElement.ScoreCopyright, value); + } /** * The font to use for displaying the songs title in the header of the music sheet. * @defaultValue `32px Georgia, serif` * @since 0.9.6 + * @deprecated use {@link elementFonts} with {@link NotationElement.ScoreTitle} + */ + public get titleFont(): Font { + return this.elementFonts.get(NotationElement.ScoreTitle)!; + } + + /** + * @deprecated use {@link elementFonts} with {@link NotationElement.ScoreTitle} */ - public titleFont: Font = new Font(RenderingResources._serifFont, 32, FontStyle.Plain); + public set titleFont(value: Font) { + this.elementFonts.set(NotationElement.ScoreTitle, value); + } /** * The font to use for displaying the songs subtitle in the header of the music sheet. * @defaultValue `20px Georgia, serif` * @since 0.9.6 + * @deprecated use {@link elementFonts} with {@link NotationElement.ScoreSubTitle} */ - public subTitleFont: Font = new Font(RenderingResources._serifFont, 20, FontStyle.Plain); + public get subTitleFont(): Font { + return this.elementFonts.get(NotationElement.ScoreSubTitle)!; + } + + /** + * @deprecated use {@link elementFonts} with {@link NotationElement.ScoreSubTitle} + */ + public set subTitleFont(value: Font) { + this.elementFonts.set(NotationElement.ScoreSubTitle, value); + } /** * The font to use for displaying the lyrics information in the header of the music sheet. * @defaultValue `15px Arial, sans-serif` * @since 0.9.6 + * @deprecated use {@link elementFonts} with {@link NotationElement.ScoreWords} */ - public wordsFont: Font = new Font(RenderingResources._serifFont, 15, FontStyle.Plain); + public get wordsFont(): Font { + return this.elementFonts.get(NotationElement.ScoreWords)!; + } /** - * The font to use for displaying certain effect related elements in the music sheet. - * @defaultValue `italic 12px Georgia, serif` - * @since 0.9.6 + * @deprecated use {@link elementFonts} with {@link NotationElement.ScoreWords} */ - public effectFont: Font = new Font(RenderingResources._serifFont, 12, FontStyle.Italic); + public set wordsFont(value: Font) { + this.elementFonts.set(NotationElement.ScoreWords, value); + } /** * The font to use for displaying beat time information in the music sheet. * @defaultValue `12px Georgia, serif` * @since 1.4.0 + * @deprecated use {@link elementFonts} with {@link NotationElement.EffectBeatTimer} */ - public timerFont: Font = new Font(RenderingResources._serifFont, 12, FontStyle.Plain); + public get timerFont(): Font { + return this.elementFonts.get(NotationElement.EffectBeatTimer)!; + } + + /** + * @deprecated use {@link elementFonts} with {@link NotationElement.EffectBeatTimer} + */ + public set timerFont(value: Font) { + this.elementFonts.set(NotationElement.EffectBeatTimer, value); + } /** * The font to use for displaying the directions texts. * @defaultValue `14px Georgia, serif` * @since 1.4.0 + * @deprecated use {@link elementFonts} with {@link NotationElement.EffectDirections} */ - public directionsFont: Font = new Font(RenderingResources._serifFont, 14, FontStyle.Plain); + public get directionsFont(): Font { + return this.elementFonts.get(NotationElement.EffectDirections)!; + } + + /** + * @deprecated use {@link elementFonts} with {@link NotationElement.EffectDirections} + */ + public set directionsFont(value: Font) { + this.elementFonts.set(NotationElement.EffectDirections, value); + } /** * The font to use for displaying the fretboard numbers in chord diagrams. * @defaultValue `11px Arial, sans-serif` * @since 0.9.6 + * @deprecated use {@link elementFonts} with {@link NotationElement.ChordDiagramFretboardNumbers} + */ + public get fretboardNumberFont(): Font { + return this.elementFonts.get(NotationElement.ChordDiagramFretboardNumbers)!; + } + + /** + * @deprecated use {@link elementFonts} with {@link NotationElement.ChordDiagramFretboardNumbers} */ - public fretboardNumberFont: Font = new Font(RenderingResources._sansFont, 11, FontStyle.Plain); + public set fretboardNumberFont(value: Font) { + this.elementFonts.set(NotationElement.ChordDiagramFretboardNumbers, value); + } + + /** + * Unused, see deprecation note. + * @defaultValue `14px Georgia, serif` + * @since 0.9.6 + * @deprecated Since 1.7.0 alphaTab uses the glyphs contained in the SMuFL font + * @json_ignore + */ + public fingeringFont: Font = RenderingResources._effectFont; + + /** + * Unused, see deprecation note. + * @defaultValue `12px Georgia, serif` + * @since 1.4.0 + * @deprecated Since 1.7.0 alphaTab uses the glyphs contained in the SMuFL font + * @json_ignore + */ + public inlineFingeringFont: Font = RenderingResources._effectFont; + + /** + * The font to use for section marker labels shown above the music sheet. + * @defaultValue `bold 14px Georgia, serif` + * @since 0.9.6 + * @deprecated use {@link elementFonts} with {@link NotationElement.EffectMarker} + */ + public get markerFont(): Font { + return this.elementFonts.get(NotationElement.EffectMarker)!; + } + + /** + * @deprecated use {@link elementFonts} with {@link NotationElement.EffectMarker} + */ + public set markerFont(value: Font) { + this.elementFonts.set(NotationElement.EffectMarker, value); + } + + /** + * Ununsed, see deprecation note. + * @defaultValue `italic 12px Georgia, serif` + * @since 0.9.6 + * @deprecated use {@link elementFonts} with the respective + * @json_ignore + */ + public effectFont: Font = RenderingResources._effectFont; + + /** + * The font to use for displaying the bar numbers above the music sheet. + * @defaultValue `11px Arial, sans-serif` + * @since 0.9.6 + * @deprecated use {@link elementFonts} with {@link NotationElement.BarNumber} + */ + public get barNumberFont(): Font { + return this.elementFonts.get(NotationElement.BarNumber)!; + } + + /** + * @deprecated use {@link elementFonts} with {@link NotationElement.BarNumber} + */ + public set barNumberFont(value: Font) { + this.elementFonts.set(NotationElement.BarNumber, value); + } + + // NOTE: the main staff fonts are still own properties. + + /** + * The fonts used by individual elements. Check `defaultFonts` for the elements which have custom fonts. + * Removing fonts from this map can lead to unexpected side effects and errors. Only update it with new values. + * @json_immutable + */ + public readonly elementFonts: Map = new Map(); /** * The font to use for displaying the numbered music notation in the music sheet. @@ -135,13 +310,6 @@ export class RenderingResources { */ public barSeparatorColor: Color = new Color(34, 34, 17, 0xff); - /** - * The font to use for displaying the bar numbers above the music sheet. - * @defaultValue `11px Arial, sans-serif` - * @since 0.9.6 - */ - public barNumberFont: Font = new Font(RenderingResources._sansFont, 11, FontStyle.Plain); - /** * The color to use for displaying the bar numbers above the music sheet. * @defaultValue `rgb(200, 0, 0)` @@ -149,29 +317,6 @@ export class RenderingResources { */ public barNumberColor: Color = new Color(200, 0, 0, 0xff); - /** - * The font to use for displaying finger information in the music sheet. - * @defaultValue `14px Georgia, serif` - * @since 0.9.6 - * @deprecated Since 1.7.0 alphaTab uses the glyphs contained in the SMuFL font - */ - public fingeringFont: Font = new Font(RenderingResources._serifFont, 14, FontStyle.Plain); - - /** - * The font to use for displaying finger information when inline into the music sheet. - * @defaultValue `12px Georgia, serif` - * @since 1.4.0 - * @deprecated Since 1.7.0 alphaTab uses the glyphs contained in the SMuFL font - */ - public inlineFingeringFont: Font = new Font(RenderingResources._serifFont, 12, FontStyle.Plain); - - /** - * The font to use for section marker labels shown above the music sheet. - * @defaultValue `bold 14px Georgia, serif` - * @since 0.9.6 - */ - public markerFont: Font = new Font(RenderingResources._serifFont, 14, FontStyle.Plain, FontWeight.Bold); - /** * The color to use for music notation elements of the primary voice. * @defaultValue `rgb(0, 0, 0)` @@ -193,28 +338,51 @@ export class RenderingResources { */ public scoreInfoColor: Color = new Color(0, 0, 0, 0xff); + public constructor() { + for (const [k, v] of RenderingResources.defaultFonts) { + this.elementFonts.set(k, v.withSize(v.size)); + } + } + /** * @internal * @param element */ public getFontForElement(element: ScoreSubElement): Font { + let notationElement = NotationElement.ScoreWords; switch (element) { case ScoreSubElement.Title: - return this.titleFont; + notationElement = NotationElement.ScoreTitle; + break; case ScoreSubElement.SubTitle: + notationElement = NotationElement.ScoreSubTitle; + break; case ScoreSubElement.Artist: + notationElement = NotationElement.ScoreArtist; + break; case ScoreSubElement.Album: - return this.subTitleFont; + notationElement = NotationElement.ScoreAlbum; + break; case ScoreSubElement.Words: + notationElement = NotationElement.ScoreWords; + break; case ScoreSubElement.Music: + notationElement = NotationElement.ScoreMusic; + break; case ScoreSubElement.WordsAndMusic: - case ScoreSubElement.Transcriber: - return this.wordsFont; + notationElement = NotationElement.ScoreWordsAndMusic; + break; case ScoreSubElement.Copyright: case ScoreSubElement.CopyrightSecondLine: - return this.copyrightFont; + notationElement = NotationElement.ScoreCopyright; + break; + default: + notationElement = NotationElement.ScoreWords; + break; } - return this.wordsFont; + return this.elementFonts.has(notationElement) + ? this.elementFonts.get(notationElement)! + : RenderingResources.defaultFonts.get(NotationElement.ScoreWords)!; } } diff --git a/packages/alphatab/src/ScrollHandlers.ts b/packages/alphatab/src/ScrollHandlers.ts new file mode 100644 index 000000000..bcd81260e --- /dev/null +++ b/packages/alphatab/src/ScrollHandlers.ts @@ -0,0 +1,398 @@ +import type { AlphaTabApiBase } from '@coderline/alphatab/AlphaTabApiBase'; +import type { MidiTickLookupFindBeatResultCursorMode } from '@coderline/alphatab/midi/MidiTickLookup'; +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import { ScrollMode } from '@coderline/alphatab/PlayerSettings'; +import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; +import type { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds'; + +/** + * Classes implementing this interface can handle the scroll logic + * as the playback in alphaTab progresses. + * + * + * @public + */ +export interface IScrollHandler extends Disposable { + /** + * Requests a instant scrolling to the specified beat. + * @param currentBeatBounds The bounds and information about the current beat. + */ + forceScrollTo(currentBeatBounds: BeatBounds): void; + + /** + * Updates whenever the currently beat cursor is updating its start and end location + * from which it starts and animates to. + * @remarks + * This method is tightly coupled to how alphaTab internally handles the beat cursor display. + * alphaTab looks up the current and next beat to which the beat cursor needs to transition + * in a specific amount of time. + * + * In some occations the cursor will transition to the end of the bar instead of the next beat. + * + * @param startBeat the information about the beat where the cursor is starting its animation. + * @param endBeat the information about the beat where the cursor is ending its animation. + * @param cursorMode how the cursor is transitioning (e.g. to end of bar or to the location of the next beat) + * @param actualBeatCursorStartX the exact start position of the beat cursor animation. + * Depending on the exact time of the player, this position might be relatively adjusted. + * @param actualBeatCursorEndX the exact end position of the beat cursor animation. + * Depending on the exact time of the player and cursor mode, + * this might be beyond the expected bounds. + * To ensure a smooth cursor experience (no jumping/flicking back and forth), alphaTab + * optimizes the used end position and animation durations. + * @param actualBeatCursorTransitionDuration The duration of the beat cursor transition in milliseconds. + * Similar to the start and end positions, this duration is adjusted accordingly to ensure + * that the beat cursor remains smoothly at the expected position for the currently played time. + * + */ + onBeatCursorUpdating( + startBeat: BeatBounds, + endBeat: BeatBounds | undefined, + cursorMode: MidiTickLookupFindBeatResultCursorMode, + actualBeatCursorStartX: number, + actualBeatCursorEndX: number, + actualBeatCursorTransitionDuration: number + ): void; +} + +/** + * Some basic scroll handler checking for changed offsets and scroll if changed. + * @internal + */ +export abstract class BasicScrollHandler implements IScrollHandler { + protected api: AlphaTabApiBase; + protected lastScroll = -1; + + public constructor(api: AlphaTabApiBase) { + this.api = api; + } + + [Symbol.dispose]() { + // do nothing + } + + public forceScrollTo(currentBeatBounds: BeatBounds): void { + this._scrollToBeat(currentBeatBounds, true); + this.lastScroll = -1; // force new scroll on next update + } + + private _scrollToBeat(currentBeatBounds: BeatBounds, force: boolean) { + const newLastScroll = this.calculateLastScroll(currentBeatBounds); + // no change, and no instant/force scroll + if (newLastScroll === this.lastScroll && !force) { + return; + } + this.lastScroll = newLastScroll; + + this.doScroll(currentBeatBounds); + } + + protected abstract calculateLastScroll(currentBeatBounds: BeatBounds): number; + protected abstract doScroll(currentBeatBounds: BeatBounds): void; + + public onBeatCursorUpdating( + startBeat: BeatBounds, + _endBeat: BeatBounds | undefined, + _cursorMode: MidiTickLookupFindBeatResultCursorMode, + _actualBeatCursorStartX: number, + _actualBeatCursorEndX: number, + _actualBeatCursorTransitionDuration: number + ): void { + this._scrollToBeat(startBeat, false); + } +} + +/** + * This is the default scroll handler for vertical layouts using {@link ScrollMode.Continuous}. + * Whenever the system changes, we scroll to the new system position vertically. + * @internal + */ +export class VerticalContinuousScrollHandler extends BasicScrollHandler { + protected override calculateLastScroll(currentBeatBounds: BeatBounds): number { + return currentBeatBounds.barBounds.masterBarBounds.realBounds.y; + } + + protected override doScroll(currentBeatBounds: BeatBounds): void { + const ui = this.api.uiFacade; + const settings = this.api.settings; + + const scroll = ui.getScrollContainer(); + const elementOffset = ui.getOffset(scroll, this.api.container); + const y = currentBeatBounds.barBounds.masterBarBounds.realBounds.y + settings.player.scrollOffsetY; + ui.scrollToY(scroll, elementOffset.y + y, this.api.settings.player.scrollSpeed); + } +} + +/** + * This is the default scroll handler for vertical layouts using {@link ScrollMode.OffScreen}. + * Whenever the system changes, we check if the new system bounds are out-of-screen and if yes, we scroll. + * @internal + */ +export class VerticalOffScreenScrollHandler extends BasicScrollHandler { + protected override calculateLastScroll(currentBeatBounds: BeatBounds): number { + // check for system change + return currentBeatBounds.barBounds.masterBarBounds.realBounds.y; + } + + protected override doScroll(currentBeatBounds: BeatBounds): void { + const ui = this.api.uiFacade; + const settings = this.api.settings; + + const scroll = ui.getScrollContainer(); + const elementBottom: number = scroll.scrollTop + ui.getOffset(null, scroll).h; + const barBoundings = currentBeatBounds.barBounds.masterBarBounds; + if ( + barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom || + barBoundings.visualBounds.y < scroll.scrollTop + ) { + const scrollTop: number = barBoundings.realBounds.y + settings.player.scrollOffsetY; + ui.scrollToY(scroll, scrollTop, settings.player.scrollSpeed); + } + } +} + +/** + * This is the default scroll handler for vertical layouts using {@link ScrollMode.Smooth}. + * vertical smooth scrolling aims to place the on-time position + * at scrollOffsetY **at the time when a system starts** + * this means when a system starts, it is at scrollOffsetY, + * then gradually scrolls down the system height reaching the bottom + * when the system completes. + * @internal + */ +export class VerticalSmoothScrollHandler implements IScrollHandler { + private _api: AlphaTabApiBase; + private _lastScroll = -1; + private _scrollContainerResizeUnregister: () => void; + + public constructor(api: AlphaTabApiBase) { + this._api = api; + // we need a resize listener for the overflow calculation + this._scrollContainerResizeUnregister = api.uiFacade.getScrollContainer().resize.on(() => { + const scrollContainer = api.uiFacade.getScrollContainer(); + + const overflowNeeded = api.settings.player.scrollOffsetX; + const viewPortSize = scrollContainer.width; + + // the content needs to shift out of screen (and back into screen with the offset) + // that's why we need the whole width as additional overflow + const overflowNeededAbsolute = viewPortSize + overflowNeeded; + + api.uiFacade.setCanvasOverflow(api.canvasElement, overflowNeededAbsolute, true); + }); + } + + [Symbol.dispose]() { + this._api.uiFacade.setCanvasOverflow(this._api.canvasElement, 0, true); + this._scrollContainerResizeUnregister(); + } + + public forceScrollTo(currentBeatBounds: BeatBounds): void { + const ui = this._api.uiFacade; + const settings = this._api.settings; + + const scroll = ui.getScrollContainer(); + const systemTop: number = + currentBeatBounds.barBounds.masterBarBounds.realBounds.y + settings.player.scrollOffsetY; + + ui.scrollToY(scroll, systemTop, 0); + this._lastScroll = -1; + } + + public onBeatCursorUpdating( + startBeat: BeatBounds, + _endBeat: BeatBounds | undefined, + _cursorMode: MidiTickLookupFindBeatResultCursorMode, + _actualBeatCursorStartX: number, + _actualBeatCursorEndX: number, + actualBeatCursorTransitionDuration: number + ): void { + const ui = this._api.uiFacade; + const settings = this._api.settings; + + const barBoundings = startBeat.barBounds.masterBarBounds; + const systemTop: number = barBoundings.realBounds.y + settings.player.scrollOffsetY; + if (systemTop === this._lastScroll && actualBeatCursorTransitionDuration > 0) { + return; + } + + // jump to start of new system + const scroll = ui.getScrollContainer(); + ui.scrollToY(scroll, systemTop, 0); + + // instant scroll + if (actualBeatCursorTransitionDuration === 0) { + this._lastScroll = -1; + return; + } + + // dynamic scrolling + this._lastScroll = systemTop; + // scroll to bottom over time + const systemBottom = systemTop + barBoundings.realBounds.h; + + // NOTE: this calculation is a bit more expensive, but we only do it once per system + // so we should be good: + // * the more bars we have, the longer the system will play, hence the duration can take a bit longer + // * if we have less bars, we calculate more often, but the calculation will be faster because we sum up less bars. + const systemDuration = this._calculateSystemDuration(barBoundings); + ui.scrollToY(scroll, systemBottom, systemDuration); + } + + private _calculateSystemDuration(barBoundings: MasterBarBounds) { + const systemBars = barBoundings.staffSystemBounds!.bars; + const tickCache = this._api.tickCache!; + + let duration = 0; + + const masterBars = this._api.score!.masterBars; + for (const bar of systemBars) { + const mb = masterBars[bar.index]; + + const mbInfo = tickCache.getMasterBar(mb); + const tempoChanges = tickCache.getMasterBar(mb).tempoChanges; + + let tempo = tempoChanges[0].tempo; + let tick = tempoChanges[0].tick; + for (let i = 1; i < tempoChanges.length; i++) { + const diff = tempoChanges[i].tick - tick; + duration += MidiUtils.ticksToMillis(diff, tempo); + tempo = tempoChanges[i].tempo; + tick = tempoChanges[i].tick; + } + + const toEnd = mbInfo.end - tick; + duration += MidiUtils.ticksToMillis(toEnd, tempo); + } + + return duration; + } +} + +/** + * This is the default scroll handler for horizontal layouts using {@link ScrollMode.Continuous}. + * Whenever the master bar changes, we scroll to the position horizontally. + * @internal + */ +export class HorizontalContinuousScrollHandler extends BasicScrollHandler { + protected override calculateLastScroll(currentBeatBounds: BeatBounds): number { + return currentBeatBounds.barBounds.masterBarBounds.visualBounds.x; + } + + protected override doScroll(currentBeatBounds: BeatBounds): void { + const ui = this.api.uiFacade; + const settings = this.api.settings; + const scroll = ui.getScrollContainer(); + + const barBoundings = currentBeatBounds.barBounds.masterBarBounds; + const scrollLeftContinuous: number = barBoundings.realBounds.x + settings.player.scrollOffsetX; + ui.scrollToX(scroll, scrollLeftContinuous, settings.player.scrollSpeed); + } +} + +/** + * This is the default scroll handler for horizontal layouts using {@link ScrollMode.OffScreen}. + * Whenever the system changes, we check if the new system bounds are out-of-screen and if yes, we scroll. + * @internal + */ +export class HorizontalOffScreenScrollHandler extends BasicScrollHandler { + protected override calculateLastScroll(currentBeatBounds: BeatBounds): number { + return currentBeatBounds.barBounds.masterBarBounds.visualBounds.x; + } + + protected override doScroll(currentBeatBounds: BeatBounds): void { + const ui = this.api.uiFacade; + const settings = this.api.settings; + const scroll = ui.getScrollContainer(); + + const elementRight: number = scroll.scrollLeft + ui.getOffset(null, scroll).w; + const barBoundings = currentBeatBounds.barBounds.masterBarBounds; + if ( + barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight || + barBoundings.visualBounds.x < scroll.scrollLeft + ) { + const scrollLeftOffScreen: number = barBoundings.realBounds.x + settings.player.scrollOffsetX; + ui.scrollToX(scroll, scrollLeftOffScreen, settings.player.scrollSpeed); + } + } +} + +/** + * This is the default scroll handler for horizontal layouts using {@link ScrollMode.Smooth}. + * horiontal smooth scrolling aims to place the on-time position + * at scrollOffsetX from a beat-to-beat perspective. + * This achieves an steady cursor at the same position with rather the music sheet scrolling past it. + * Due to some animation inconsistencies (e.g. CSS animation vs scrolling) there might be a slight + * flickering of the cursor. + * + * To get a fully steady cursor the beat cursor can simply be visually hidden and a cursor can be placed at + * `scrollOffsetX` by the integrator. + * @internal + */ +export class HorizontalSmoothScrollHandler implements IScrollHandler { + private _api: AlphaTabApiBase; + private _lastScroll = -1; + private _scrollContainerResizeUnregister: () => void; + + public constructor(api: AlphaTabApiBase) { + this._api = api; + // we need a resize listener for the overflow calculation + this._scrollContainerResizeUnregister = api.uiFacade.getScrollContainer().resize.on(() => { + const scrollContainer = api.uiFacade.getScrollContainer(); + + const overflowNeeded = api.settings.player.scrollOffsetX; + const viewPortSize = scrollContainer.width; + + // the content needs to shift out of screen (and back into screen with the offset) + // that's why we need the whole width as additional overflow + const overflowNeededAbsolute = viewPortSize + overflowNeeded; + + api.uiFacade.setCanvasOverflow(api.canvasElement, overflowNeededAbsolute, false); + }); + } + + [Symbol.dispose]() { + this._scrollContainerResizeUnregister(); + this._api.uiFacade.setCanvasOverflow(this._api.canvasElement, 0, false); + } + + public forceScrollTo(currentBeatBounds: BeatBounds): void { + const ui = this._api.uiFacade; + const settings = this._api.settings; + + const scroll = ui.getScrollContainer(); + const barStartX: number = currentBeatBounds.onNotesX + settings.player.scrollOffsetY; + + ui.scrollToY(scroll, barStartX, 0); + this._lastScroll = -1; + } + + public onBeatCursorUpdating( + _startBeat: BeatBounds, + _endBeat: BeatBounds | undefined, + _cursorMode: MidiTickLookupFindBeatResultCursorMode, + actualBeatCursorStartX: number, + actualBeatCursorEndX: number, + actualBeatCursorTransitionDuration: number + ): void { + const ui = this._api.uiFacade; + + if (actualBeatCursorEndX === this._lastScroll && actualBeatCursorTransitionDuration > 0) { + return; + } + + // jump to start of new system + const settings = this._api.settings; + const scroll = ui.getScrollContainer(); + ui.scrollToX(scroll, actualBeatCursorStartX + settings.player.scrollOffsetX, 0); + + // instant scroll + if (actualBeatCursorTransitionDuration === 0) { + this._lastScroll = -1; + return; + } + + this._lastScroll = actualBeatCursorEndX; + const scrollX = actualBeatCursorEndX + settings.player.scrollOffsetX; + ui.scrollToX(scroll, scrollX, actualBeatCursorTransitionDuration); + } +} diff --git a/packages/alphatab/src/alphaTab.core.ts b/packages/alphatab/src/alphaTab.core.ts index b447b24ed..12f421550 100644 --- a/packages/alphatab/src/alphaTab.core.ts +++ b/packages/alphatab/src/alphaTab.core.ts @@ -39,7 +39,8 @@ export { Environment, RenderEngineFactory } from '@coderline/alphatab/Environmen export type { IEventEmitter, IEventEmitterOfT } from '@coderline/alphatab/EventEmitter'; export { AlphaTabApi } from '@coderline/alphatab/platform/javascript/AlphaTabApi'; -export { AlphaTabApiBase } from '@coderline/alphatab/AlphaTabApiBase'; +export { AlphaTabApiBase, type PlaybackHighlightChangeEventArgs } from '@coderline/alphatab/AlphaTabApiBase'; +export type { IScrollHandler } from '@coderline/alphatab/ScrollHandlers'; export { WebPlatform } from '@coderline/alphatab/platform/javascript/WebPlatform'; export { VersionInfo as meta } from '@coderline/alphatab/generated/VersionInfo'; diff --git a/packages/alphatab/src/exporter/AlphaTexExporter.ts b/packages/alphatab/src/exporter/AlphaTexExporter.ts index fdcf3ea79..27dd5c55b 100644 --- a/packages/alphatab/src/exporter/AlphaTexExporter.ts +++ b/packages/alphatab/src/exporter/AlphaTexExporter.ts @@ -220,6 +220,9 @@ class AlphaTexPrinter { // outdent from previous items if we had indents switch (m.tag.tag.text) { case 'track': + if (this._staffIndex > 0) { + this._writer.outdent(); + } if (this._trackIndex > 0) { this._writer.outdent(); } diff --git a/packages/alphatab/src/exporter/GpifSoundMapper.ts b/packages/alphatab/src/exporter/GpifSoundMapper.ts new file mode 100644 index 000000000..869283cc1 --- /dev/null +++ b/packages/alphatab/src/exporter/GpifSoundMapper.ts @@ -0,0 +1,630 @@ +import { TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentArticulation'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; +import type { PlaybackInformation } from '@coderline/alphatab/model/PlaybackInformation'; +import type { Track } from '@coderline/alphatab/model/Track'; + +/** + * @internal + */ +class GpifMidiProgramInfo { + public icon: GpifIconIds = GpifIconIds.Piano; + public instrumentSetName: string; + public instrumentSetType: string; + + public constructor(icon: GpifIconIds, instrumentSetName: string, instrumentSetType: string | null = null) { + this.icon = icon; + this.instrumentSetName = instrumentSetName; + if (!instrumentSetType) { + const parts = instrumentSetName.split(' '); + parts[0] = parts[0].substr(0, 1).toLowerCase() + parts[0].substr(1); + this.instrumentSetType = parts.join(''); + } else { + this.instrumentSetType = instrumentSetType; + } + } +} + +// Grabbed via Icon Picker beside track name in GP7 +/** + * @internal + */ +enum GpifIconIds { + // Guitar & Basses + SteelGuitar = 1, + AcousticGuitar = 2, + TwelveStringGuitar = 3, + ElectricGuitar = 4, + Bass = 5, + ClassicalGuitar = 23, + UprightBass = 6, + Ukulele = 7, + Banjo = 8, + Mandolin = 9, + // Orchestral + Piano = 10, + Synth = 12, + Strings = 11, + Brass = 13, + Reed = 14, + Woodwind = 15, + Vocal = 16, + PitchedIdiophone = 17, + Fx = 21, + // Percussions + PercussionKit = 18, + Idiophone = 19, + Membraphone = 20 +} + +/** + * @internal + */ +export class GpifInstrumentSet { + public lineCount: number = 0; + public name: string = ''; + public type: string = ''; + public elements: GpifInstrumentElement[] = []; + + public static create(name: string, type: string, lineCount: number, elements: GpifInstrumentElement[]) { + const insturmentSet = new GpifInstrumentSet(); + insturmentSet.name = name; + insturmentSet.type = type; + insturmentSet.lineCount = lineCount; + insturmentSet.elements = elements; + return insturmentSet; + } +} + +/** + * @internal + */ +export class GpifInstrumentElement { + public name: string; + public type: string; + public soundbankName: string; + public articulations: GpifInstrumentArticulation[]; + + public constructor(name: string, type: string, soundbankName: string, articulations: GpifInstrumentArticulation[]) { + this.name = name; + this.type = type; + this.soundbankName = soundbankName; + this.articulations = articulations; + } +} + +/** + * @internal + */ +export class GpifInstrumentArticulation { + public name: string; + public staffLine: number; + public noteHeads: MusicFontSymbol[]; + public techniqueSymbol: MusicFontSymbol; + public techniqueSymbolPlacement: TechniqueSymbolPlacement; + public inputMidiNumbers: number[]; + public outputMidiNumber: number; + public outputRSESound: string; + + public constructor( + name: string, + staffLine: number, + noteHeads: MusicFontSymbol[], + techniqueSymbol: MusicFontSymbol, + techniqueSymbolPlacement: TechniqueSymbolPlacement, + inputMidiNumbers: number[], + outputMidiNumber: number, + outputRSESound: string + ) { + this.name = name; + this.staffLine = staffLine; + this.noteHeads = noteHeads; + this.techniqueSymbol = techniqueSymbol; + this.techniqueSymbolPlacement = techniqueSymbolPlacement; + this.inputMidiNumbers = inputMidiNumbers; + this.outputMidiNumber = outputMidiNumber; + this.outputRSESound = outputRSESound; + } + + public static template(name: string, inputMidiNumbers: number[], outputRSESound: string) { + return new GpifInstrumentArticulation( + name, + 0, + [], + MusicFontSymbol.None, + TechniqueSymbolPlacement.Outside, + inputMidiNumbers, + 0, + outputRSESound + ); + } +} + +/** + * A helper which provides the RSE Soundbank and MIDI mapping + * details for exporting Guitar Pro files from the alphaTab model. + * @internal + */ +export class GpifSoundMapper { + // NOTE: this code is not generated as the midi insturment list is anyhow fixed and should not never change. + private static _midiProgramInfoLookup: Map = new Map([ + [0, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Acoustic Piano')], + [1, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Acoustic Piano')], + [2, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Piano')], + [3, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Acoustic Piano')], + [4, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Piano')], + [5, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Piano')], + [6, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Harpsichord')], + [7, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Harpsichord')], + [8, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Celesta')], + [9, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Vibraphone')], + [10, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Vibraphone')], + [11, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Vibraphone')], + [12, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Xylophone')], + [13, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Xylophone')], + [14, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Vibraphone')], + [15, new GpifMidiProgramInfo(GpifIconIds.Banjo, 'Banjo')], + [16, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], + [17, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], + [18, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], + [19, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], + [20, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], + [21, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], + [22, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Recorder')], + [23, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], + [24, new GpifMidiProgramInfo(GpifIconIds.ClassicalGuitar, 'Nylon Guitar')], + [25, new GpifMidiProgramInfo(GpifIconIds.SteelGuitar, 'Steel Guitar')], + [26, new GpifMidiProgramInfo(GpifIconIds.SteelGuitar, 'Electric Guitar')], + [27, new GpifMidiProgramInfo(GpifIconIds.ElectricGuitar, 'Electric Guitar')], + [28, new GpifMidiProgramInfo(GpifIconIds.ElectricGuitar, 'Electric Guitar')], + [29, new GpifMidiProgramInfo(GpifIconIds.ElectricGuitar, 'Electric Guitar')], + [30, new GpifMidiProgramInfo(GpifIconIds.SteelGuitar, 'Electric Guitar')], + [31, new GpifMidiProgramInfo(GpifIconIds.SteelGuitar, 'Electric Guitar')], + [32, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Acoustic Bass')], + [33, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Electric Bass')], + [34, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Electric Bass')], + [35, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Acoustic Bass')], + [36, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Electric Bass')], + [37, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Electric Bass')], + [38, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Synth Bass')], + [39, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Synth Bass')], + [40, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], + [41, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Viola')], + [42, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Cello')], + [43, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Contrabass')], + [44, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], + [45, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], + [46, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Harp')], + [47, new GpifMidiProgramInfo(GpifIconIds.Membraphone, 'Timpani')], + [48, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], + [49, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], + [50, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], + [51, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], + [52, new GpifMidiProgramInfo(GpifIconIds.Vocal, 'Voice')], + [53, new GpifMidiProgramInfo(GpifIconIds.Vocal, 'Voice')], + [54, new GpifMidiProgramInfo(GpifIconIds.Vocal, 'Voice')], + [55, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], + [56, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trumpet')], + [57, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trombone')], + [58, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Tuba')], + [59, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trumpet')], + [60, new GpifMidiProgramInfo(GpifIconIds.Brass, 'French Horn')], + [61, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trumpet')], + [62, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trumpet')], + [63, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trumpet')], + [64, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Saxophone')], + [65, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Saxophone')], + [66, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Saxophone')], + [67, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Saxophone')], + [68, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Oboe')], + [69, new GpifMidiProgramInfo(GpifIconIds.Reed, 'English Horn')], + [70, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Bassoon')], + [71, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Clarinet')], + [72, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Piccolo')], + [73, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Flute')], + [74, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Recorder')], + [75, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Flute')], + [76, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Recorder')], + [77, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Flute')], + [78, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Recorder')], + [79, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Flute')], + [80, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], + [81, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], + [82, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], + [83, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], + [84, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], + [85, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], + [86, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], + [87, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], + [88, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], + [89, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], + [90, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], + [91, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], + [92, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], + [93, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], + [94, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], + [95, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], + [96, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Pad Synthesizer')], + [97, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Pad Synthesizer')], + [98, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Pad Synthesizer')], + [99, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Pad Synthesizer')], + [100, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Lead Synthesizer')], + [101, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Lead Synthesizer')], + [102, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Lead Synthesizer')], + [103, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Trumpet')], + [104, new GpifMidiProgramInfo(GpifIconIds.ElectricGuitar, 'Banjo')], + [105, new GpifMidiProgramInfo(GpifIconIds.Banjo, 'Banjo')], + [106, new GpifMidiProgramInfo(GpifIconIds.Ukulele, 'Ukulele')], + [107, new GpifMidiProgramInfo(GpifIconIds.Banjo, 'Banjo')], + [108, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Xylophone')], + [109, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Bassoon')], + [110, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], + [111, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Flute')], + [112, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Xylophone')], + [113, new GpifMidiProgramInfo(GpifIconIds.Idiophone, 'Celesta')], + [114, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Vibraphone')], + [115, new GpifMidiProgramInfo(GpifIconIds.Idiophone, 'Xylophone')], + [116, new GpifMidiProgramInfo(GpifIconIds.Membraphone, 'Xylophone')], + [117, new GpifMidiProgramInfo(GpifIconIds.Membraphone, 'Xylophone')], + [118, new GpifMidiProgramInfo(GpifIconIds.Membraphone, 'Xylophone')], + [119, new GpifMidiProgramInfo(GpifIconIds.Idiophone, 'Celesta')], + [120, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Steel Guitar')], + [121, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], + [122, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], + [123, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], + [124, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], + [125, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], + [126, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], + [127, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Timpani')] + ]); + + // To update the following generated code, use the GpExporterTest.sound-mapper unit test + // which will generate the new code to copy for here. + // We could also use an NPM script for that but for now this is enough. + + // BEGIN generated + private static _drumInstrumentSet = GpifInstrumentSet.create('Drums', 'drumKit', 5, [ + new GpifInstrumentElement('Snare', 'snare', 'Master-Snare', [ + GpifInstrumentArticulation.template('Snare (hit)', [38], 'stick.hit.hit'), + GpifInstrumentArticulation.template('Snare (side stick)', [37], 'stick.hit.sidestick'), + GpifInstrumentArticulation.template('Snare (rim shot)', [91], 'stick.hit.rimshot') + ]), + new GpifInstrumentElement('Charley', 'hiHat', 'Master-Hihat', [ + GpifInstrumentArticulation.template('Hi-Hat (closed)', [42], 'stick.hit.closed'), + GpifInstrumentArticulation.template('Hi-Hat (half)', [92], 'stick.hit.half'), + GpifInstrumentArticulation.template('Hi-Hat (open)', [46], 'stick.hit.open'), + GpifInstrumentArticulation.template('Pedal Hi-Hat (hit)', [44], 'pedal.hit.pedal') + ]), + new GpifInstrumentElement('Acoustic Kick Drum', 'kickDrum', 'AcousticKick-Percu', [ + GpifInstrumentArticulation.template('Kick (hit)', [35], 'pedal.hit.hit') + ]), + new GpifInstrumentElement('Kick Drum', 'kickDrum', 'Master-Kick', [ + GpifInstrumentArticulation.template('Kick (hit)', [36], 'pedal.hit.hit') + ]), + new GpifInstrumentElement('Tom Very High', 'tom', 'Master-Tom05', [ + GpifInstrumentArticulation.template('High Floor Tom (hit)', [50], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Tom High', 'tom', 'Master-Tom04', [ + GpifInstrumentArticulation.template('High Tom (hit)', [48], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Tom Medium', 'tom', 'Master-Tom03', [ + GpifInstrumentArticulation.template('Mid Tom (hit)', [47], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Tom Low', 'tom', 'Master-Tom02', [ + GpifInstrumentArticulation.template('Low Tom (hit)', [45], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Tom Very Low', 'tom', 'Master-Tom01', [ + GpifInstrumentArticulation.template('Very Low Tom (hit)', [43], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Ride', 'ride', 'Master-Ride', [ + GpifInstrumentArticulation.template('Ride (edge)', [93], 'stick.hit.edge'), + GpifInstrumentArticulation.template('Ride (middle)', [51], 'stick.hit.mid'), + GpifInstrumentArticulation.template('Ride (bell)', [53], 'stick.hit.bell'), + GpifInstrumentArticulation.template('Ride (choke)', [94], 'stick.hit.choke') + ]), + new GpifInstrumentElement('Splash', 'splash', 'Master-Splash', [ + GpifInstrumentArticulation.template('Splash (hit)', [55], 'stick.hit.hit'), + GpifInstrumentArticulation.template('Splash (choke)', [95], 'stick.hit.choke') + ]), + new GpifInstrumentElement('China', 'china', 'Master-China', [ + GpifInstrumentArticulation.template('China (hit)', [52], 'stick.hit.hit'), + GpifInstrumentArticulation.template('China (choke)', [96], 'stick.hit.choke') + ]), + new GpifInstrumentElement('Crash High', 'crash', 'Master-Crash02', [ + GpifInstrumentArticulation.template('Crash high (hit)', [49], 'stick.hit.hit'), + GpifInstrumentArticulation.template('Crash high (choke)', [97], 'stick.hit.choke') + ]), + new GpifInstrumentElement('Crash Medium', 'crash', 'Master-Crash01', [ + GpifInstrumentArticulation.template('Crash medium (hit)', [57], 'stick.hit.hit'), + GpifInstrumentArticulation.template('Crash medium (choke)', [98], 'stick.hit.choke') + ]), + new GpifInstrumentElement('Cowbell Low', 'cowbell', 'CowbellBig-Percu', [ + GpifInstrumentArticulation.template('Cowbell low (hit)', [99], 'stick.hit.hit'), + GpifInstrumentArticulation.template('Cowbell low (tip)', [100], 'stick.hit.tip') + ]), + new GpifInstrumentElement('Cowbell Medium', 'cowbell', 'CowbellMid-Percu', [ + GpifInstrumentArticulation.template('Cowbell medium (hit)', [56], 'stick.hit.hit'), + GpifInstrumentArticulation.template('Cowbell medium (tip)', [101], 'stick.hit.tip') + ]), + new GpifInstrumentElement('Cowbell High', 'cowbell', 'CowbellSmall-Percu', [ + GpifInstrumentArticulation.template('Cowbell high (hit)', [102], 'stick.hit.hit'), + GpifInstrumentArticulation.template('Cowbell high (tip)', [103], 'stick.hit.tip') + ]), + new GpifInstrumentElement('Woodblock Low', 'woodblock', 'WoodblockLow-Percu', [ + GpifInstrumentArticulation.template('Woodblock low (hit)', [77], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Woodblock High', 'woodblock', 'WoodblockHigh-Percu', [ + GpifInstrumentArticulation.template('Woodblock high (hit)', [76], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Bongo High', 'bongo', 'BongoHigh-Percu', [ + GpifInstrumentArticulation.template('Bongo High (hit)', [60], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Bongo High (mute)', [104], 'hand.hit.mute'), + GpifInstrumentArticulation.template('Bongo High (slap)', [105], 'hand.hit.slap') + ]), + new GpifInstrumentElement('Bongo Low', 'bongo', 'BongoLow-Percu', [ + GpifInstrumentArticulation.template('Bongo Low (hit)', [61], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Bongo Low (mute)', [106], 'hand.hit.mute'), + GpifInstrumentArticulation.template('Bongo Low (slap)', [107], 'hand.hit.slap') + ]), + new GpifInstrumentElement('Timbale Low', 'timbale', 'TimbaleLow-Percu', [ + GpifInstrumentArticulation.template('Timbale low (hit)', [66], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Timbale High', 'timbale', 'TimbaleHigh-Percu', [ + GpifInstrumentArticulation.template('Timbale high (hit)', [65], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Agogo Low', 'agogo', 'AgogoLow-Percu', [ + GpifInstrumentArticulation.template('Agogo low (hit)', [68], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Agogo High', 'agogo', 'AgogoHigh-Percu', [ + GpifInstrumentArticulation.template('Agogo high (hit)', [67], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Conga Low', 'conga', 'CongaLow-Percu', [ + GpifInstrumentArticulation.template('Conga low (hit)', [64], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Conga low (slap)', [108], 'hand.hit.slap'), + GpifInstrumentArticulation.template('Conga low (mute)', [109], 'hand.hit.mute') + ]), + new GpifInstrumentElement('Conga High', 'conga', 'CongaHigh-Percu', [ + GpifInstrumentArticulation.template('Conga high (hit)', [63], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Conga high (slap)', [110], 'hand.hit.slap'), + GpifInstrumentArticulation.template('Conga high (mute)', [62], 'hand.hit.mute') + ]), + new GpifInstrumentElement('Whistle Low', 'whistle', 'WhistleLow-Percu', [ + GpifInstrumentArticulation.template('Whistle low (hit)', [72], 'blow.hit.hit') + ]), + new GpifInstrumentElement('Whistle High', 'whistle', 'WhistleHigh-Percu', [ + GpifInstrumentArticulation.template('Whistle high (hit)', [71], 'blow.hit.hit') + ]), + new GpifInstrumentElement('Guiro', 'guiro', 'Guiro-Percu', [ + GpifInstrumentArticulation.template('Guiro (hit)', [73], 'stick.hit.hit'), + GpifInstrumentArticulation.template('Guiro (scrap-return)', [74], 'stick.scrape.return') + ]), + new GpifInstrumentElement('Surdo', 'surdo', 'Surdo-Percu', [ + GpifInstrumentArticulation.template('Surdo (hit)', [86], 'brush.hit.hit'), + GpifInstrumentArticulation.template('Surdo (mute)', [87], 'brush.hit.mute') + ]), + new GpifInstrumentElement('Tambourine', 'tambourine', 'Tambourine-Percu', [ + GpifInstrumentArticulation.template('Tambourine (hit)', [54], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Tambourine (return)', [111], 'hand.hit.return'), + GpifInstrumentArticulation.template('Tambourine (roll)', [112], 'hand.hit.roll'), + GpifInstrumentArticulation.template('Tambourine (hand)', [113], 'hand.hit.handhit') + ]), + new GpifInstrumentElement('Cuica', 'cuica', 'Cuica-Percu', [ + GpifInstrumentArticulation.template('Cuica (open)', [79], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Cuica (mute)', [78], 'hand.hit.mute') + ]), + new GpifInstrumentElement('Vibraslap', 'vibraslap', 'Vibraslap-Percu', [ + GpifInstrumentArticulation.template('Vibraslap (hit)', [58], 'hand.hit.hit') + ]), + new GpifInstrumentElement('Triangle', 'triangle', 'Triangle-Percu', [ + GpifInstrumentArticulation.template('Triangle (hit)', [81], 'stick.hit.hit'), + GpifInstrumentArticulation.template('Triangle (mute)', [80], 'stick.hit.mute') + ]), + new GpifInstrumentElement('Grancassa', 'grancassa', 'Grancassa-Percu', [ + GpifInstrumentArticulation.template('Grancassa (hit)', [114], 'mallet.hit.hit') + ]), + new GpifInstrumentElement('Piatti', 'piatti', 'Piatti-Percu', [ + GpifInstrumentArticulation.template('Piatti (hit)', [115], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Piatti (hand)', [116], 'hand.hit.hit') + ]), + new GpifInstrumentElement('Cabasa', 'cabasa', 'Cabasa-Percu', [ + GpifInstrumentArticulation.template('Cabasa (hit)', [69], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Cabasa (return)', [117], 'hand.hit.return') + ]), + new GpifInstrumentElement('Castanets', 'castanets', 'Castanets-Percu', [ + GpifInstrumentArticulation.template('Castanets (hit)', [85], 'hand.hit.hit') + ]), + new GpifInstrumentElement('Claves', 'claves', 'Claves-Percu', [ + GpifInstrumentArticulation.template('Claves (hit)', [75], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Left Maraca', 'maraca', 'Maracas-Percu', [ + GpifInstrumentArticulation.template('Left Maraca (hit)', [70], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Left Maraca (return)', [118], 'hand.hit.return') + ]), + new GpifInstrumentElement('Right Maraca', 'maraca', 'Maracas-Percu', [ + GpifInstrumentArticulation.template('Right Maraca (hit)', [119], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Right Maraca (return)', [120], 'hand.hit.return') + ]), + new GpifInstrumentElement('Shaker', 'shaker', 'ShakerStudio-Percu', [ + GpifInstrumentArticulation.template('Shaker (hit)', [82], 'hand.hit.hit'), + GpifInstrumentArticulation.template('Shaker (return)', [122], 'hand.hit.return') + ]), + new GpifInstrumentElement('Bell Tree', 'bellTree', 'BellTree-Percu', [ + GpifInstrumentArticulation.template('Bell Tree (hit)', [84], 'stick.hit.hit'), + GpifInstrumentArticulation.template('Bell Tree (return)', [123], 'stick.hit.return') + ]), + new GpifInstrumentElement('Jingle Bell', 'jingleBell', 'JingleBell-Percu', [ + GpifInstrumentArticulation.template('Jingle Bell (hit)', [83], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Tinkle Bell', 'jingleBell', 'JingleBell-Percu', [ + GpifInstrumentArticulation.template('Tinkle Bell (hit)', [83], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Golpe', 'unpitched', 'Golpe-Percu', [ + GpifInstrumentArticulation.template('Golpe (thumb)', [124], 'thumb.hit.body'), + GpifInstrumentArticulation.template('Golpe (finger)', [125], 'finger4.hit.body') + ]), + new GpifInstrumentElement('Hand Clap', 'handClap', 'GroupHandClap-Percu', [ + GpifInstrumentArticulation.template('Hand Clap (hit)', [39], 'hand.hit.hit') + ]), + new GpifInstrumentElement('Electric Snare', 'snare', 'ElectricSnare-Percu', [ + GpifInstrumentArticulation.template('Electric Snare (hit)', [40], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Sticks', 'snare', 'Stick-Percu', [ + GpifInstrumentArticulation.template('Snare (side stick)', [31], 'stick.hit.sidestick') + ]), + new GpifInstrumentElement('Very Low Floor Tom', 'tom', 'LowFloorTom-Percu', [ + GpifInstrumentArticulation.template('Low Floor Tom (hit)', [41], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Ride Cymbal 2', 'ride', 'Ride-Percu', [ + GpifInstrumentArticulation.template('Ride (edge)', [59], 'stick.hit.edge'), + GpifInstrumentArticulation.template('Ride (middle)', [126], 'stick.hit.mid'), + GpifInstrumentArticulation.template('Ride (bell)', [127], 'stick.hit.bell'), + GpifInstrumentArticulation.template('Ride (choke)', [29], 'stick.hit.choke') + ]), + new GpifInstrumentElement('Reverse Cymbal', 'crash', 'Reverse-Cymbal', [ + GpifInstrumentArticulation.template('Reverse Cymbal (hit)', [30], 'stick.hit.hit') + ]), + new GpifInstrumentElement('Metronome', 'snare', 'Metronome-Percu', [ + GpifInstrumentArticulation.template('Metronome (hit)', [33], 'stick.hit.sidestick'), + GpifInstrumentArticulation.template('Metronome (bell)', [34], 'stick.hit.hit') + ]) + ]); + // END generated + + private static _elementByArticulation: Map | undefined = undefined; + private static _articulationsById: Map | undefined = undefined; + + private static _initLookups() { + const set = GpifSoundMapper._drumInstrumentSet; + const elementByArticulation = new Map(); + const articulationsById = new Map(); + for (const element of set.elements) { + for (const articulation of element.articulations) { + for (const midi of articulation.inputMidiNumbers) { + const gpId = `${element.name}.${midi}`; + elementByArticulation.set(gpId, element); + articulationsById.set(gpId, articulation); + } + } + } + + GpifSoundMapper._elementByArticulation = elementByArticulation; + GpifSoundMapper._articulationsById = articulationsById; + return elementByArticulation; + } + + public static getIconId(playbackInfo: PlaybackInformation): number { + if (playbackInfo.primaryChannel === 9) { + return GpifIconIds.PercussionKit; + } + if (GpifSoundMapper._midiProgramInfoLookup.has(playbackInfo.program)) { + return GpifSoundMapper._midiProgramInfoLookup.get(playbackInfo.program)!.icon; + } + return GpifIconIds.SteelGuitar; + } + + public static buildInstrumentSet(track: Track): GpifInstrumentSet { + if (track.percussionArticulations.length > 0 || track.isPercussion) { + return GpifSoundMapper._buildPercussionInstrumentSet(track); + } else { + return GpifSoundMapper._buildPitchedInstrumentSet(track); + } + } + + private static readonly _pitchedElement = new GpifInstrumentElement('Pitched', 'pitched', '', [ + new GpifInstrumentArticulation( + '', + 0, + [MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole], + MusicFontSymbol.None, + TechniqueSymbolPlacement.Outside, + [], + 0, + '' + ) + ]); + + private static _buildPitchedInstrumentSet(track: Track): GpifInstrumentSet { + const instrumentSet = new GpifInstrumentSet(); + instrumentSet.lineCount = track.staves[0].standardNotationLineCount; + + const programInfo = GpifSoundMapper._midiProgramInfoLookup.has(track.playbackInfo.program) + ? GpifSoundMapper._midiProgramInfoLookup.get(track.playbackInfo.program)! + : GpifSoundMapper._midiProgramInfoLookup.get(0)!; + + instrumentSet.name = programInfo.instrumentSetName; + instrumentSet.type = programInfo.instrumentSetType; + const element = new GpifInstrumentElement( + GpifSoundMapper._pitchedElement.name, + GpifSoundMapper._pitchedElement.type, + GpifSoundMapper._pitchedElement.soundbankName, + [GpifSoundMapper._pitchedElement.articulations[0]] + ); + instrumentSet.elements.push(element); + return instrumentSet; + } + + private static _buildPercussionInstrumentSet(track: Track): GpifInstrumentSet { + if (!GpifSoundMapper._elementByArticulation) { + GpifSoundMapper._initLookups(); + } + + const instrumentSet = new GpifInstrumentSet(); + instrumentSet.lineCount = track.staves[0].standardNotationLineCount; + instrumentSet.name = 'Drums'; + instrumentSet.type = 'drumKit'; + + const articulations = + track.percussionArticulations.length > 0 + ? track.percussionArticulations + : Array.from(PercussionMapper.instrumentArticulations.values()); + + // NOTE: GP files are very sensitive in terms of articulation and element order. + // notes reference articulations index based within the overall file. + let element: GpifInstrumentElement | undefined = undefined; + for (const articulation of articulations) { + // main info from own articulation + const gpifArticulation = new GpifInstrumentArticulation( + articulation.elementType, + articulation.staffLine, + [articulation.noteHeadDefault, articulation.noteHeadHalf, articulation.noteHeadWhole], + articulation.techniqueSymbol, + articulation.techniqueSymbolPlacement, + [articulation.id], + articulation.outputMidiNumber, + '' + ); + + // additional details we try to lookup from the known templates + const gpId = articulation.uniqueId; + if (GpifSoundMapper._articulationsById!.has(gpId)) { + const knownArticulation = GpifSoundMapper._articulationsById!.get(gpId)!; + gpifArticulation.inputMidiNumbers = knownArticulation.inputMidiNumbers; + gpifArticulation.name = knownArticulation.name; + gpifArticulation.outputRSESound = knownArticulation.outputRSESound; + } + + // check for element change + if (GpifSoundMapper._elementByArticulation!.has(gpId)) { + const knownElement = GpifSoundMapper._elementByArticulation!.get(gpId)!; + if (!element || element.name !== articulation.elementType) { + element = new GpifInstrumentElement( + knownElement.name, + knownElement.type, + knownElement.soundbankName, + [] + ); + instrumentSet.elements.push(element); + } + } else { + if (!element || element.name !== articulation.elementType) { + element = new GpifInstrumentElement(articulation.elementType, articulation.elementType, '', []); + instrumentSet.elements.push(element); + } + } + + element.articulations.push(gpifArticulation); + } + + return instrumentSet; + } +} diff --git a/packages/alphatab/src/exporter/GpifWriter.ts b/packages/alphatab/src/exporter/GpifWriter.ts index 8da0bd5aa..b00ed8737 100644 --- a/packages/alphatab/src/exporter/GpifWriter.ts +++ b/packages/alphatab/src/exporter/GpifWriter.ts @@ -1,3 +1,4 @@ +import { GpifSoundMapper } from '@coderline/alphatab/exporter/GpifSoundMapper'; import { VersionInfo } from '@coderline/alphatab/generated/VersionInfo'; import { GeneralMidi } from '@coderline/alphatab/midi/GeneralMidi'; import { MidiFileGenerator } from '@coderline/alphatab/midi/MidiFileGenerator'; @@ -32,9 +33,7 @@ import type { Note } from '@coderline/alphatab/model/Note'; import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; -import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import type { PlaybackInformation } from '@coderline/alphatab/model/PlaybackInformation'; import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; import type { Score } from '@coderline/alphatab/model/Score'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; @@ -52,59 +51,6 @@ import { Lazy } from '@coderline/alphatab/util/Lazy'; import { XmlDocument } from '@coderline/alphatab/xml/XmlDocument'; import { XmlNode, XmlNodeType } from '@coderline/alphatab/xml/XmlNode'; -// Grabbed via Icon Picker beside track name in GP7 -/** - * @internal - */ -enum GpifIconIds { - // Guitar & Basses - SteelGuitar = 1, - AcousticGuitar = 2, - TwelveStringGuitar = 3, - ElectricGuitar = 4, - Bass = 5, - ClassicalGuitar = 23, - UprightBass = 6, - Ukulele = 7, - Banjo = 8, - Mandolin = 9, - // Orchestral - Piano = 10, - Synth = 12, - Strings = 11, - Brass = 13, - Reed = 14, - Woodwind = 15, - Vocal = 16, - PitchedIdiophone = 17, - Fx = 21, - // Percussions - PercussionKit = 18, - Idiophone = 19, - Membraphone = 20 -} - -/** - * @internal - */ -class GpifMidiProgramInfo { - public icon: GpifIconIds = GpifIconIds.Piano; - public instrumentSetName: string; - public instrumentSetType: string; - - public constructor(icon: GpifIconIds, instrumentSetName: string, instrumentSetType: string | null = null) { - this.icon = icon; - this.instrumentSetName = instrumentSetName; - if (!instrumentSetType) { - const parts = instrumentSetName.split(' '); - parts[0] = parts[0].substr(0, 1).toLowerCase() + parts[0].substr(1); - this.instrumentSetType = parts.join(''); - } else { - this.instrumentSetType = instrumentSetType; - } - } -} - /** * This class can write a score.gpif XML from a given score model. * @internal @@ -116,142 +62,6 @@ export class GpifWriter { private static readonly _sampleRate = 44100; private _rhythmIdLookup: Map = new Map(); - private static _midiProgramInfoLookup: Map = new Map([ - [0, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Acoustic Piano')], - [1, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Acoustic Piano')], - [2, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Piano')], - [3, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Acoustic Piano')], - [4, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Piano')], - [5, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Piano')], - [6, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Harpsichord')], - [7, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Harpsichord')], - [8, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Celesta')], - [9, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Vibraphone')], - [10, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Vibraphone')], - [11, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Vibraphone')], - [12, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Xylophone')], - [13, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Xylophone')], - [14, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Vibraphone')], - [15, new GpifMidiProgramInfo(GpifIconIds.Banjo, 'Banjo')], - [16, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], - [17, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], - [18, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], - [19, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], - [20, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], - [21, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], - [22, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Recorder')], - [23, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Electric Organ')], - [24, new GpifMidiProgramInfo(GpifIconIds.ClassicalGuitar, 'Nylon Guitar')], - [25, new GpifMidiProgramInfo(GpifIconIds.SteelGuitar, 'Steel Guitar')], - [26, new GpifMidiProgramInfo(GpifIconIds.SteelGuitar, 'Electric Guitar')], - [27, new GpifMidiProgramInfo(GpifIconIds.ElectricGuitar, 'Electric Guitar')], - [28, new GpifMidiProgramInfo(GpifIconIds.ElectricGuitar, 'Electric Guitar')], - [29, new GpifMidiProgramInfo(GpifIconIds.ElectricGuitar, 'Electric Guitar')], - [30, new GpifMidiProgramInfo(GpifIconIds.SteelGuitar, 'Electric Guitar')], - [31, new GpifMidiProgramInfo(GpifIconIds.SteelGuitar, 'Electric Guitar')], - [32, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Acoustic Bass')], - [33, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Electric Bass')], - [34, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Electric Bass')], - [35, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Acoustic Bass')], - [36, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Electric Bass')], - [37, new GpifMidiProgramInfo(GpifIconIds.Bass, 'Electric Bass')], - [38, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Synth Bass')], - [39, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Synth Bass')], - [40, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], - [41, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Viola')], - [42, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Cello')], - [43, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Contrabass')], - [44, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], - [45, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], - [46, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Harp')], - [47, new GpifMidiProgramInfo(GpifIconIds.Membraphone, 'Timpani')], - [48, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], - [49, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], - [50, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], - [51, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], - [52, new GpifMidiProgramInfo(GpifIconIds.Vocal, 'Voice')], - [53, new GpifMidiProgramInfo(GpifIconIds.Vocal, 'Voice')], - [54, new GpifMidiProgramInfo(GpifIconIds.Vocal, 'Voice')], - [55, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], - [56, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trumpet')], - [57, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trombone')], - [58, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Tuba')], - [59, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trumpet')], - [60, new GpifMidiProgramInfo(GpifIconIds.Brass, 'French Horn')], - [61, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trumpet')], - [62, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trumpet')], - [63, new GpifMidiProgramInfo(GpifIconIds.Brass, 'Trumpet')], - [64, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Saxophone')], - [65, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Saxophone')], - [66, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Saxophone')], - [67, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Saxophone')], - [68, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Oboe')], - [69, new GpifMidiProgramInfo(GpifIconIds.Reed, 'English Horn')], - [70, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Bassoon')], - [71, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Clarinet')], - [72, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Piccolo')], - [73, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Flute')], - [74, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Recorder')], - [75, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Flute')], - [76, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Recorder')], - [77, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Flute')], - [78, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Recorder')], - [79, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Flute')], - [80, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], - [81, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], - [82, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], - [83, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], - [84, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], - [85, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], - [86, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], - [87, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Lead Synthesizer')], - [88, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], - [89, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], - [90, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], - [91, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], - [92, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], - [93, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], - [94, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], - [95, new GpifMidiProgramInfo(GpifIconIds.Synth, 'Pad Synthesizer')], - [96, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Pad Synthesizer')], - [97, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Pad Synthesizer')], - [98, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Pad Synthesizer')], - [99, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Pad Synthesizer')], - [100, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Lead Synthesizer')], - [101, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Lead Synthesizer')], - [102, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Lead Synthesizer')], - [103, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Trumpet')], - [104, new GpifMidiProgramInfo(GpifIconIds.ElectricGuitar, 'Banjo')], - [105, new GpifMidiProgramInfo(GpifIconIds.Banjo, 'Banjo')], - [106, new GpifMidiProgramInfo(GpifIconIds.Ukulele, 'Ukulele')], - [107, new GpifMidiProgramInfo(GpifIconIds.Banjo, 'Banjo')], - [108, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Xylophone')], - [109, new GpifMidiProgramInfo(GpifIconIds.Reed, 'Bassoon')], - [110, new GpifMidiProgramInfo(GpifIconIds.Strings, 'Violin')], - [111, new GpifMidiProgramInfo(GpifIconIds.Woodwind, 'Flute')], - [112, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Xylophone')], - [113, new GpifMidiProgramInfo(GpifIconIds.Idiophone, 'Celesta')], - [114, new GpifMidiProgramInfo(GpifIconIds.PitchedIdiophone, 'Vibraphone')], - [115, new GpifMidiProgramInfo(GpifIconIds.Idiophone, 'Xylophone')], - [116, new GpifMidiProgramInfo(GpifIconIds.Membraphone, 'Xylophone')], - [117, new GpifMidiProgramInfo(GpifIconIds.Membraphone, 'Xylophone')], - [118, new GpifMidiProgramInfo(GpifIconIds.Membraphone, 'Xylophone')], - [119, new GpifMidiProgramInfo(GpifIconIds.Idiophone, 'Celesta')], - [120, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Steel Guitar')], - [121, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], - [122, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], - [123, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], - [124, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], - [125, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], - [126, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Recorder')], - [127, new GpifMidiProgramInfo(GpifIconIds.Fx, 'Timpani')] - ]); - - private static _drumKitProgramInfo: GpifMidiProgramInfo = new GpifMidiProgramInfo( - GpifIconIds.PercussionKit, - 'Drums', - 'drumKit' - ); public writeXml(score: Score): string { const xmlDocument = new XmlDocument(); @@ -792,16 +602,17 @@ export class GpifWriter { beatNode.addElement('Fadding').innerText = FadeType[beat.fade]; } if (beat.isTremolo) { - switch (beat.tremoloSpeed) { - case Duration.Eighth: + switch (beat.tremoloPicking!.marks) { + case 1: beatNode.addElement('Tremolo').innerText = '1/2'; break; - case Duration.Sixteenth: + case 2: beatNode.addElement('Tremolo').innerText = '1/4'; break; - case Duration.ThirtySecond: + case 3: beatNode.addElement('Tremolo').innerText = '1/8'; break; + // NOTE: guitar pro does not support other tremolos } } if (beat.hasChord) { @@ -1269,7 +1080,7 @@ export class GpifWriter { : 'Default'; trackNode.addElement('UseOneChannelPerString'); - trackNode.addElement('IconId').innerText = GpifWriter._getIconId(track.playbackInfo).toString(); + trackNode.addElement('IconId').innerText = GpifSoundMapper.getIconId(track.playbackInfo).toString(); this._writeInstrumentSetNode(trackNode, track); this._writeTransposeNode(trackNode, track); @@ -1297,16 +1108,6 @@ export class GpifWriter { this._writeSoundsAndAutomations(trackNode, track); } - private static _getIconId(playbackInfo: PlaybackInformation): GpifIconIds { - if (playbackInfo.primaryChannel === 9) { - return GpifWriter._drumKitProgramInfo.icon; - } - if (GpifWriter._midiProgramInfoLookup.has(playbackInfo.program)) { - return GpifWriter._midiProgramInfoLookup.get(playbackInfo.program)!.icon; - } - return GpifIconIds.SteelGuitar; - } - private _writeSoundAndAutomation( soundsNode: XmlNode, automationsNode: XmlNode, @@ -1725,55 +1526,31 @@ export class GpifWriter { } private _writeInstrumentSetNode(trackNode: XmlNode, track: Track) { - const instrumentSet = trackNode.addElement('InstrumentSet'); - - const firstStaff: Staff = track.staves[0]; - - instrumentSet.addElement('LineCount').innerText = firstStaff.standardNotationLineCount.toString(); - - if (track.percussionArticulations.length > 0 || firstStaff.isPercussion) { - const articulations = - track.percussionArticulations.length > 0 - ? track.percussionArticulations - : Array.from(PercussionMapper.instrumentArticulations.values()); - - instrumentSet.addElement('Name').innerText = GpifWriter._drumKitProgramInfo.instrumentSetName; - instrumentSet.addElement('Type').innerText = GpifWriter._drumKitProgramInfo.instrumentSetType; - const currentElementType: string = ''; - let currentElementName: string = ''; - let currentArticulations: XmlNode = new XmlNode(); - const counterPerType = new Map(); - const elements = instrumentSet.addElement('Elements'); - for (const articulation of articulations) { - if (!currentElementType || currentElementType !== articulation.elementType) { - const currentElement = elements.addElement('Element'); - - let name = articulation.elementType; - if (counterPerType.has(name)) { - const counter = counterPerType.get(name)!; - name += ` ${counter}`; - counterPerType.set(name, counter + 1); - } else { - counterPerType.set(name, 1); - } + const instrumentSet = GpifSoundMapper.buildInstrumentSet(track); + const instrumentSetNode = trackNode.addElement('InstrumentSet'); - currentElementName = name; - currentElement.addElement('Name').innerText = name; - currentElement.addElement('Type').innerText = articulation.elementType; + instrumentSetNode.addElement('Name').innerText = instrumentSet.name; + instrumentSetNode.addElement('Type').innerText = instrumentSet.type; + instrumentSetNode.addElement('LineCount').innerText = instrumentSet.lineCount.toString(); - currentArticulations = currentElement.addElement('Articulations'); - } + const elementsNode = instrumentSetNode.addElement('Elements'); + for (const element of instrumentSet.elements) { + const elementNode = elementsNode.addElement('Element'); + elementNode.addElement('Name').innerText = element.name; + elementNode.addElement('Type').innerText = element.type; + elementNode.addElement('SoundbankName').innerText = element.soundbankName; - const articulationNode = currentArticulations.addElement('Articulation'); - articulationNode.addElement('Name').innerText = - `${currentElementName} ${currentArticulations.childNodes.length}`; + const articulationsNode = elementNode.addElement('Articulations'); + for (const articulation of element.articulations) { + const articulationNode = articulationsNode.addElement('Articulation'); + + articulationNode.addElement('Name').innerText = articulation.name; articulationNode.addElement('StaffLine').innerText = articulation.staffLine.toString(); articulationNode.addElement('Noteheads').innerText = [ - this._mapMusicSymbol(articulation.noteHeadDefault), - this._mapMusicSymbol(articulation.noteHeadHalf), - this._mapMusicSymbol(articulation.noteHeadWhole) + this._mapMusicSymbol(articulation.noteHeads[0]), + this._mapMusicSymbol(articulation.noteHeads[1]), + this._mapMusicSymbol(articulation.noteHeads[2]) ].join(' '); - switch (articulation.techniqueSymbolPlacement) { case TechniqueSymbolPlacement.Below: articulationNode.addElement('TechniquePlacement').innerText = 'below'; @@ -1791,36 +1568,12 @@ export class GpifWriter { articulationNode.addElement('TechniqueSymbol').innerText = this._mapMusicSymbol( articulation.techniqueSymbol ); - articulationNode.addElement('InputMidiNumbers').innerText = ''; + articulationNode.addElement('InputMidiNumbers').innerText = articulation.inputMidiNumbers + .map(n => n.toString()) + .join(' '); + articulationNode.addElement('OutputRSESound').innerText = articulation.outputRSESound; articulationNode.addElement('OutputMidiNumber').innerText = articulation.outputMidiNumber.toString(); } - } else { - const programInfo = GpifWriter._midiProgramInfoLookup.has(track.playbackInfo.program) - ? GpifWriter._midiProgramInfoLookup.get(track.playbackInfo.program)! - : GpifWriter._midiProgramInfoLookup.get(0)!; - - instrumentSet.addElement('Name').innerText = programInfo.instrumentSetName; - instrumentSet.addElement('Type').innerText = programInfo.instrumentSetType; - - // Only the simple pitched element for normal instruments - const elements = instrumentSet.addElement('Elements'); - const element = elements.addElement('Element'); - - element.addElement('Pitched').innerText = 'Pitched'; - element.addElement('Type').innerText = 'pitched'; - element.addElement('SoundbankName').innerText = ''; - - const articulations = element.addElement('Articulations'); - const articulation = articulations.addElement('Articulation'); - - articulation.addElement('Name').innerText = ''; - articulation.addElement('StaffLine').innerText = '0'; - articulation.addElement('Noteheads').innerText = 'noteheadBlack noteheadHalf noteheadWhole'; - articulation.addElement('TechniquePlacement').innerText = 'outside'; - articulation.addElement('TechniqueSymbol').innerText = ''; - articulation.addElement('InputMidiNumbers').innerText = ''; - articulation.addElement('OutputRSESound').innerText = ''; - articulation.addElement('OutputMidiNumber').innerText = '0'; } } @@ -1861,6 +1614,10 @@ export class GpifWriter { masterBarNode.addElement('Time').innerText = `${masterBar.timeSignatureNumerator}/${masterBar.timeSignatureDenominator}`; + if (masterBar.actualBeamingRules) { + this._writeBarXProperties(masterBarNode, masterBar); + } + if (masterBar.isFreeTime) { masterBarNode.addElement('FreeTime'); } @@ -1986,6 +1743,39 @@ export class GpifWriter { this._writeFermatas(masterBarNode, masterBar); } + private _writeBarXProperties(masterBarNode: XmlNode, masterBar: MasterBar) { + const properties = masterBarNode.addElement('XProperties'); + + const beamingRules = masterBar.actualBeamingRules; + if (beamingRules) { + // prefer 8th note rule (that's what GP mostly has) + const rule = beamingRules!.findRule(Duration.Eighth); + + // NOTE: it's not clear if guitar pro supports quarter rules + // for that case we better convert this to an "8th" note rule. + let durationProp = rule[0] as number; + let groupSizeFactor = 1; + if (rule[0] === Duration.Quarter) { + durationProp = 8; + groupSizeFactor = 2; + } + + this._writeSimpleXPropertyNode(properties, '1124139010', 'Int', durationProp.toString()); + + const startGroupid = 1124139264; + let i = 0; + while (startGroupid < 1124139295 && i < rule[1].length) { + this._writeSimpleXPropertyNode( + properties, + (startGroupid + i).toString(), + 'Int', + (rule[1][i] * groupSizeFactor).toString() + ); + i++; + } + } + } + private _writeFermatas(parent: XmlNode, masterBar: MasterBar) { const fermataCount = masterBar.fermata?.size ?? 0; if (fermataCount === 0) { diff --git a/packages/alphatab/src/generated/DisplaySettingsJson.ts b/packages/alphatab/src/generated/DisplaySettingsJson.ts index f98964dae..887455617 100644 --- a/packages/alphatab/src/generated/DisplaySettingsJson.ts +++ b/packages/alphatab/src/generated/DisplaySettingsJson.ts @@ -57,6 +57,7 @@ export interface DisplaySettingsJson { * @remarks * AlphaTab has various stave profiles that define which staves will be shown in for the rendered tracks. Its recommended * to keep this on {@link StaveProfile.Default} and rather rely on the options available ob {@link Staff} level + * @deprecated Set the notation visibility by modifying the {@link Staff} properties. */ staveProfile?: StaveProfile | keyof typeof StaveProfile | Lowercase; /** @@ -172,7 +173,7 @@ export interface DisplaySettingsJson { * The top padding applied to first system. * @since 1.4.0 * @category Display - * @defaultValue `5` + * @defaultValue `0` */ firstSystemPaddingTop?: number; /** @@ -186,14 +187,14 @@ export interface DisplaySettingsJson { * The bottom padding applied to systems beside the last one. * @since 1.4.0 * @category Display - * @defaultValue `20` + * @defaultValue `10` */ systemPaddingBottom?: number; /** * The bottom padding applied to the last system. * @since 1.4.0 * @category Display - * @defaultValue `0` + * @defaultValue `5` */ lastSystemPaddingBottom?: number; /** @@ -218,17 +219,31 @@ export interface DisplaySettingsJson { */ accoladeBarPaddingRight?: number; /** - * The bottom padding applied to main notation staves (standard, tabs, numbered, slash). + * The top padding applied to the first main notation staff (standard, tabs, numbered, slash). + * @since 1.8.0 + * @category Display + * @defaultValue `0` + */ + firstNotationStaffPaddingTop?: number; + /** + * The bottom padding applied to last main notation staff (standard, tabs, numbered, slash). + * @since 1.8.0 + * @category Display + * @defaultValue `0` + */ + lastNotationStaffPaddingBottom?: number; + /** + * The top padding applied to main notation staves (standard, tabs, numbered, slash). * @since 1.4.0 * @category Display - * @defaultValue `5` + * @defaultValue `0` */ notationStaffPaddingTop?: number; /** * The bottom padding applied to main notation staves (standard, tabs, numbered, slash). * @since 1.4.0 * @category Display - * @defaultValue `5` + * @defaultValue `0` */ notationStaffPaddingBottom?: number; /** @@ -236,6 +251,8 @@ export interface DisplaySettingsJson { * @since 1.4.0 * @category Display * @defaultValue `0` + * @deprecated Effect staves do not exist anymore, effects are now part of the main notation staves. This value has no effect anymore. + * Use {@link effectBandPaddingBottom} to control the padding after effect bands. */ effectStaffPaddingTop?: number; /** @@ -243,6 +260,8 @@ export interface DisplaySettingsJson { * @since 1.4.0 * @category Display * @defaultValue `0` + * @deprecated Effect staves do not exist anymore, effects are now part of the main notation staves. This value has no effect anymore. + * Use {@link effectBandPaddingBottom} to control the padding after effect bands. */ effectStaffPaddingBottom?: number; /** @@ -266,6 +285,20 @@ export interface DisplaySettingsJson { * @defaultValue `2` */ effectBandPaddingBottom?: number; + /** + * The additional padding to apply between the staves of two separate tracks. + * @since 1.8.0 + * @category Display + * @defaultValue `5` + */ + trackStaffPaddingBetween?: number; + /** + * The additional padding to apply between multiple lyric lines. + * @since 1.8.0 + * @category Display + * @defaultValue `5` + */ + lyricLinesPaddingBetween?: number; /** * The mode used to arrange staves and systems. * @since 1.3.0 @@ -328,6 +361,7 @@ export interface DisplaySettingsJson { * * * Comparing files against each other (top/bottom comparison) * * Aligning the playback of multiple files on one screen assuming the same tempo (e.g. one file per track). + * @deprecated Use the {@link LayoutMode.Parchment} to display a music sheet respecting the systems layout. */ systemsLayoutMode?: SystemsLayoutMode | keyof typeof SystemsLayoutMode | Lowercase; } diff --git a/packages/alphatab/src/generated/DisplaySettingsSerializer.ts b/packages/alphatab/src/generated/DisplaySettingsSerializer.ts index 6433a4a93..c07b064b7 100644 --- a/packages/alphatab/src/generated/DisplaySettingsSerializer.ts +++ b/packages/alphatab/src/generated/DisplaySettingsSerializer.ts @@ -42,6 +42,8 @@ export class DisplaySettingsSerializer { o.set("systemlabelpaddingleft", obj.systemLabelPaddingLeft); o.set("systemlabelpaddingright", obj.systemLabelPaddingRight); o.set("accoladebarpaddingright", obj.accoladeBarPaddingRight); + o.set("firstnotationstaffpaddingtop", obj.firstNotationStaffPaddingTop); + o.set("lastnotationstaffpaddingbottom", obj.lastNotationStaffPaddingBottom); o.set("notationstaffpaddingtop", obj.notationStaffPaddingTop); o.set("notationstaffpaddingbottom", obj.notationStaffPaddingBottom); o.set("effectstaffpaddingtop", obj.effectStaffPaddingTop); @@ -49,6 +51,8 @@ export class DisplaySettingsSerializer { o.set("firststaffpaddingleft", obj.firstStaffPaddingLeft); o.set("staffpaddingleft", obj.staffPaddingLeft); o.set("effectbandpaddingbottom", obj.effectBandPaddingBottom); + o.set("trackstaffpaddingbetween", obj.trackStaffPaddingBetween); + o.set("lyriclinespaddingbetween", obj.lyricLinesPaddingBetween); o.set("systemslayoutmode", obj.systemsLayoutMode as number); return o; } @@ -105,6 +109,12 @@ export class DisplaySettingsSerializer { case "accoladebarpaddingright": obj.accoladeBarPaddingRight = v! as number; return true; + case "firstnotationstaffpaddingtop": + obj.firstNotationStaffPaddingTop = v! as number; + return true; + case "lastnotationstaffpaddingbottom": + obj.lastNotationStaffPaddingBottom = v! as number; + return true; case "notationstaffpaddingtop": obj.notationStaffPaddingTop = v! as number; return true; @@ -126,6 +136,12 @@ export class DisplaySettingsSerializer { case "effectbandpaddingbottom": obj.effectBandPaddingBottom = v! as number; return true; + case "trackstaffpaddingbetween": + obj.trackStaffPaddingBetween = v! as number; + return true; + case "lyriclinespaddingbetween": + obj.lyricLinesPaddingBetween = v! as number; + return true; case "systemslayoutmode": obj.systemsLayoutMode = JsonHelper.parseEnum(v, SystemsLayoutMode)!; return true; diff --git a/packages/alphatab/src/generated/EngravingSettingsCloner.ts b/packages/alphatab/src/generated/EngravingSettingsCloner.ts index e08cc1c1c..08dc51b81 100644 --- a/packages/alphatab/src/generated/EngravingSettingsCloner.ts +++ b/packages/alphatab/src/generated/EngravingSettingsCloner.ts @@ -72,6 +72,7 @@ export class EngravingSettingsCloner { clone.deadSlappedLineWidth = original.deadSlappedLineWidth; clone.leftHandTabTieWidth = original.leftHandTabTieWidth; clone.tabBendDashSize = original.tabBendDashSize; + clone.tabBendStaffPadding = original.tabBendStaffPadding; clone.tabBendPerValueHeight = original.tabBendPerValueHeight; clone.tabBendLabelPadding = original.tabBendLabelPadding; clone.simpleSlideWidth = original.simpleSlideWidth; @@ -91,6 +92,8 @@ export class EngravingSettingsCloner { clone.tuningGlyphStringColumnScale = original.tuningGlyphStringColumnScale; clone.tuningGlyphStringRowPadding = original.tuningGlyphStringRowPadding; clone.directionsScale = original.directionsScale; + clone.multiVoiceDisplacedNoteHeadSpacing = original.multiVoiceDisplacedNoteHeadSpacing; + clone.stemFlagHeight = new Map(original.stemFlagHeight); return clone; } } diff --git a/packages/alphatab/src/generated/EngravingSettingsJson.ts b/packages/alphatab/src/generated/EngravingSettingsJson.ts index 9225f3d55..3c1cdaa49 100644 --- a/packages/alphatab/src/generated/EngravingSettingsJson.ts +++ b/packages/alphatab/src/generated/EngravingSettingsJson.ts @@ -215,7 +215,7 @@ export interface EngravingSettingsJson { */ numberedBarRendererBarSpacing?: number; /** - * The size of the dashed drawn in numbered notation to indicate the durations. + * The padding minimum between the duration dashes. */ numberedDashGlyphPadding?: number; /** @@ -314,6 +314,11 @@ export interface EngravingSettingsJson { * The size of the dashes on bends (e.g. on holds) */ tabBendDashSize?: number; + /** + * The additional padding between the staff and the point + * where bend values are calculated from. + */ + tabBendStaffPadding?: number; /** * The height applied per quarter-note. */ @@ -390,4 +395,13 @@ export interface EngravingSettingsJson { * The relative scale of any directions glyphs drawn like coda or segno. */ directionsScale?: number; + /** + * The spacing between displaced displaced note heads + * in case of multi-voice note head overlaps. + */ + multiVoiceDisplacedNoteHeadSpacing?: number; + /** + * The space needed by flags on the stem-side from top to bottom to place. + */ + stemFlagHeight?: Map, number>; } diff --git a/packages/alphatab/src/generated/EngravingSettingsSerializer.ts b/packages/alphatab/src/generated/EngravingSettingsSerializer.ts index 87f0b4925..19cc823e4 100644 --- a/packages/alphatab/src/generated/EngravingSettingsSerializer.ts +++ b/packages/alphatab/src/generated/EngravingSettingsSerializer.ts @@ -134,6 +134,7 @@ export class EngravingSettingsSerializer { o.set("deadslappedlinewidth", obj.deadSlappedLineWidth); o.set("lefthandtabtiewidth", obj.leftHandTabTieWidth); o.set("tabbenddashsize", obj.tabBendDashSize); + o.set("tabbendstaffpadding", obj.tabBendStaffPadding); o.set("tabbendpervalueheight", obj.tabBendPerValueHeight); o.set("tabbendlabelpadding", obj.tabBendLabelPadding); o.set("simpleslidewidth", obj.simpleSlideWidth); @@ -153,6 +154,14 @@ export class EngravingSettingsSerializer { o.set("tuningglyphstringcolumnscale", obj.tuningGlyphStringColumnScale); o.set("tuningglyphstringrowpadding", obj.tuningGlyphStringRowPadding); o.set("directionsscale", obj.directionsScale); + o.set("multivoicedisplacednoteheadspacing", obj.multiVoiceDisplacedNoteHeadSpacing); + { + const m = new Map(); + o.set("stemflagheight", m); + for (const [k, v] of obj.stemFlagHeight!) { + m.set(k.toString(), v); + } + } return o; } public static setProperty(obj: EngravingSettings, property: string, v: unknown): boolean { @@ -371,6 +380,9 @@ export class EngravingSettingsSerializer { case "tabbenddashsize": obj.tabBendDashSize = v! as number; return true; + case "tabbendstaffpadding": + obj.tabBendStaffPadding = v! as number; + return true; case "tabbendpervalueheight": obj.tabBendPerValueHeight = v! as number; return true; @@ -428,6 +440,15 @@ export class EngravingSettingsSerializer { case "directionsscale": obj.directionsScale = v! as number; return true; + case "multivoicedisplacednoteheadspacing": + obj.multiVoiceDisplacedNoteHeadSpacing = v! as number; + return true; + case "stemflagheight": + obj.stemFlagHeight = new Map(); + JsonHelper.forEach(v, (v, k) => { + obj.stemFlagHeight.set(JsonHelper.parseEnum(k, Duration)!, v as number); + }); + return true; } return false; } diff --git a/packages/alphatab/src/generated/NotationSettingsJson.ts b/packages/alphatab/src/generated/NotationSettingsJson.ts index e6b4cb546..eef659457 100644 --- a/packages/alphatab/src/generated/NotationSettingsJson.ts +++ b/packages/alphatab/src/generated/NotationSettingsJson.ts @@ -130,7 +130,7 @@ export interface NotationSettingsJson { * Controls how high the ryhthm notation is rendered below the tab staff * @since 0.9.6 * @category Notation - * @defaultValue `15` + * @defaultValue `25` * @remarks * This setting can be used in combination with the {@link rhythmMode} setting to control how high the rhythm notation should be rendered below the tab staff. */ diff --git a/packages/alphatab/src/generated/RenderingResourcesJson.ts b/packages/alphatab/src/generated/RenderingResourcesJson.ts index 5b2aee00e..52e6d6ad0 100644 --- a/packages/alphatab/src/generated/RenderingResourcesJson.ts +++ b/packages/alphatab/src/generated/RenderingResourcesJson.ts @@ -5,6 +5,7 @@ // import { EngravingSettingsJson } from "@coderline/alphatab/generated/EngravingSettingsJson"; import { FontJson } from "@coderline/alphatab/model/Font"; +import { NotationElement } from "@coderline/alphatab/NotationSettings"; import { ColorJson } from "@coderline/alphatab/model/Color"; /** * This public class contains central definitions for controlling the visual appearance. @@ -37,53 +38,35 @@ export interface RenderingResourcesJson { */ engravingSettings?: EngravingSettingsJson; /** - * The font to use for displaying the songs copyright information in the header of the music sheet. - * @defaultValue `bold 12px Arial, sans-serif` - * @since 0.9.6 - */ - copyrightFont?: FontJson; - /** - * The font to use for displaying the songs title in the header of the music sheet. - * @defaultValue `32px Georgia, serif` - * @since 0.9.6 - */ - titleFont?: FontJson; - /** - * The font to use for displaying the songs subtitle in the header of the music sheet. - * @defaultValue `20px Georgia, serif` + * Unused, see deprecation note. + * @defaultValue `14px Georgia, serif` * @since 0.9.6 + * @deprecated Since 1.7.0 alphaTab uses the glyphs contained in the SMuFL font + * @json_ignore */ - subTitleFont?: FontJson; + fingeringFont?: FontJson; /** - * The font to use for displaying the lyrics information in the header of the music sheet. - * @defaultValue `15px Arial, sans-serif` - * @since 0.9.6 + * Unused, see deprecation note. + * @defaultValue `12px Georgia, serif` + * @since 1.4.0 + * @deprecated Since 1.7.0 alphaTab uses the glyphs contained in the SMuFL font + * @json_ignore */ - wordsFont?: FontJson; + inlineFingeringFont?: FontJson; /** - * The font to use for displaying certain effect related elements in the music sheet. + * Ununsed, see deprecation note. * @defaultValue `italic 12px Georgia, serif` * @since 0.9.6 + * @deprecated use {@link elementFonts} with the respective + * @json_ignore */ effectFont?: FontJson; /** - * The font to use for displaying beat time information in the music sheet. - * @defaultValue `12px Georgia, serif` - * @since 1.4.0 + * The fonts used by individual elements. Check `defaultFonts` for the elements which have custom fonts. + * Removing fonts from this map can lead to unexpected side effects and errors. Only update it with new values. + * @json_immutable */ - timerFont?: FontJson; - /** - * The font to use for displaying the directions texts. - * @defaultValue `14px Georgia, serif` - * @since 1.4.0 - */ - directionsFont?: FontJson; - /** - * The font to use for displaying the fretboard numbers in chord diagrams. - * @defaultValue `11px Arial, sans-serif` - * @since 0.9.6 - */ - fretboardNumberFont?: FontJson; + elementFonts?: Map, FontJson>; /** * The font to use for displaying the numbered music notation in the music sheet. * @defaultValue `14px Arial, sans-serif` @@ -120,38 +103,12 @@ export interface RenderingResourcesJson { * @since 0.9.6 */ barSeparatorColor?: ColorJson; - /** - * The font to use for displaying the bar numbers above the music sheet. - * @defaultValue `11px Arial, sans-serif` - * @since 0.9.6 - */ - barNumberFont?: FontJson; /** * The color to use for displaying the bar numbers above the music sheet. * @defaultValue `rgb(200, 0, 0)` * @since 0.9.6 */ barNumberColor?: ColorJson; - /** - * The font to use for displaying finger information in the music sheet. - * @defaultValue `14px Georgia, serif` - * @since 0.9.6 - * @deprecated Since 1.7.0 alphaTab uses the glyphs contained in the SMuFL font - */ - fingeringFont?: FontJson; - /** - * The font to use for displaying finger information when inline into the music sheet. - * @defaultValue `12px Georgia, serif` - * @since 1.4.0 - * @deprecated Since 1.7.0 alphaTab uses the glyphs contained in the SMuFL font - */ - inlineFingeringFont?: FontJson; - /** - * The font to use for section marker labels shown above the music sheet. - * @defaultValue `bold 14px Georgia, serif` - * @since 0.9.6 - */ - markerFont?: FontJson; /** * The color to use for music notation elements of the primary voice. * @defaultValue `rgb(0, 0, 0)` diff --git a/packages/alphatab/src/generated/RenderingResourcesSerializer.ts b/packages/alphatab/src/generated/RenderingResourcesSerializer.ts index 68564fe5f..b43047b54 100644 --- a/packages/alphatab/src/generated/RenderingResourcesSerializer.ts +++ b/packages/alphatab/src/generated/RenderingResourcesSerializer.ts @@ -8,6 +8,7 @@ import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; import { EngravingSettingsSerializer } from "@coderline/alphatab/generated/EngravingSettingsSerializer"; import { Font } from "@coderline/alphatab/model/Font"; import { Color } from "@coderline/alphatab/model/Color"; +import { NotationElement } from "@coderline/alphatab/NotationSettings"; /** * @internal */ @@ -25,25 +26,20 @@ export class RenderingResourcesSerializer { const o = new Map(); o.set("smuflfontfamilyname", obj.smuflFontFamilyName); o.set("engravingsettings", EngravingSettingsSerializer.toJson(obj.engravingSettings)); - o.set("copyrightfont", Font.toJson(obj.copyrightFont)!); - o.set("titlefont", Font.toJson(obj.titleFont)!); - o.set("subtitlefont", Font.toJson(obj.subTitleFont)!); - o.set("wordsfont", Font.toJson(obj.wordsFont)!); - o.set("effectfont", Font.toJson(obj.effectFont)!); - o.set("timerfont", Font.toJson(obj.timerFont)!); - o.set("directionsfont", Font.toJson(obj.directionsFont)!); - o.set("fretboardnumberfont", Font.toJson(obj.fretboardNumberFont)!); + { + const m = new Map(); + o.set("elementfonts", m); + for (const [k, v] of obj.elementFonts!) { + m.set(k.toString(), Font.toJson(v)!); + } + } o.set("numberednotationfont", Font.toJson(obj.numberedNotationFont)!); o.set("numberednotationgracefont", Font.toJson(obj.numberedNotationGraceFont)!); o.set("tablaturefont", Font.toJson(obj.tablatureFont)!); o.set("gracefont", Font.toJson(obj.graceFont)!); o.set("stafflinecolor", Color.toJson(obj.staffLineColor)!); o.set("barseparatorcolor", Color.toJson(obj.barSeparatorColor)!); - o.set("barnumberfont", Font.toJson(obj.barNumberFont)!); o.set("barnumbercolor", Color.toJson(obj.barNumberColor)!); - o.set("fingeringfont", Font.toJson(obj.fingeringFont)!); - o.set("inlinefingeringfont", Font.toJson(obj.inlineFingeringFont)!); - o.set("markerfont", Font.toJson(obj.markerFont)!); o.set("mainglyphcolor", Color.toJson(obj.mainGlyphColor)!); o.set("secondaryglyphcolor", Color.toJson(obj.secondaryGlyphColor)!); o.set("scoreinfocolor", Color.toJson(obj.scoreInfoColor)!); @@ -54,29 +50,10 @@ export class RenderingResourcesSerializer { case "smuflfontfamilyname": obj.smuflFontFamilyName = v as string | undefined; return true; - case "copyrightfont": - obj.copyrightFont = Font.fromJson(v)!; - return true; - case "titlefont": - obj.titleFont = Font.fromJson(v)!; - return true; - case "subtitlefont": - obj.subTitleFont = Font.fromJson(v)!; - return true; - case "wordsfont": - obj.wordsFont = Font.fromJson(v)!; - return true; - case "effectfont": - obj.effectFont = Font.fromJson(v)!; - return true; - case "timerfont": - obj.timerFont = Font.fromJson(v)!; - return true; - case "directionsfont": - obj.directionsFont = Font.fromJson(v)!; - return true; - case "fretboardnumberfont": - obj.fretboardNumberFont = Font.fromJson(v)!; + case "elementfonts": + JsonHelper.forEach(v, (v, k) => { + obj.elementFonts.set(JsonHelper.parseEnum(k, NotationElement)!, Font.fromJson(v)!); + }); return true; case "numberednotationfont": obj.numberedNotationFont = Font.fromJson(v)!; @@ -96,21 +73,9 @@ export class RenderingResourcesSerializer { case "barseparatorcolor": obj.barSeparatorColor = Color.fromJson(v)!; return true; - case "barnumberfont": - obj.barNumberFont = Font.fromJson(v)!; - return true; case "barnumbercolor": obj.barNumberColor = Color.fromJson(v)!; return true; - case "fingeringfont": - obj.fingeringFont = Font.fromJson(v)!; - return true; - case "inlinefingeringfont": - obj.inlineFingeringFont = Font.fromJson(v)!; - return true; - case "markerfont": - obj.markerFont = Font.fromJson(v)!; - return true; case "mainglyphcolor": obj.mainGlyphColor = Color.fromJson(v)!; return true; diff --git a/packages/alphatab/src/generated/model/BarSerializer.ts b/packages/alphatab/src/generated/model/BarSerializer.ts index d1e4cbd5d..fc757c6df 100644 --- a/packages/alphatab/src/generated/model/BarSerializer.ts +++ b/packages/alphatab/src/generated/model/BarSerializer.ts @@ -16,6 +16,7 @@ import { SustainPedalMarker } from "@coderline/alphatab/model/Bar"; import { BarLineStyle } from "@coderline/alphatab/model/Bar"; import { KeySignature } from "@coderline/alphatab/model/KeySignature"; import { KeySignatureType } from "@coderline/alphatab/model/KeySignatureType"; +import { BarNumberDisplay } from "@coderline/alphatab/model/RenderStylesheet"; import { BarStyle } from "@coderline/alphatab/model/Bar"; /** * @internal @@ -44,6 +45,7 @@ export class BarSerializer { o.set("barlineright", obj.barLineRight as number); o.set("keysignature", obj.keySignature as number); o.set("keysignaturetype", obj.keySignatureType as number); + o.set("barnumberdisplay", obj.barNumberDisplay as number | undefined); if (obj.style) { o.set("style", BarStyleSerializer.toJson(obj.style)); } @@ -97,6 +99,9 @@ export class BarSerializer { case "keysignaturetype": obj.keySignatureType = JsonHelper.parseEnum(v, KeySignatureType)!; return true; + case "barnumberdisplay": + obj.barNumberDisplay = JsonHelper.parseEnum(v, BarNumberDisplay); + return true; case "style": if (v) { obj.style = new BarStyle(); diff --git a/packages/alphatab/src/generated/model/BeamingRulesSerializer.ts b/packages/alphatab/src/generated/model/BeamingRulesSerializer.ts new file mode 100644 index 000000000..b0f7c779b --- /dev/null +++ b/packages/alphatab/src/generated/model/BeamingRulesSerializer.ts @@ -0,0 +1,44 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { BeamingRules } from "@coderline/alphatab/model/MasterBar"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { Duration } from "@coderline/alphatab/model/Duration"; +/** + * @internal + */ +export class BeamingRulesSerializer { + public static fromJson(obj: BeamingRules, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => BeamingRulesSerializer.setProperty(obj, k, v)); + } + public static toJson(obj: BeamingRules | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + { + const m = new Map(); + o.set("groups", m); + for (const [k, v] of obj.groups!) { + m.set(k.toString(), v); + } + } + return o; + } + public static setProperty(obj: BeamingRules, property: string, v: unknown): boolean { + switch (property) { + case "groups": + obj.groups = new Map(); + JsonHelper.forEach(v, (v, k) => { + obj.groups.set(JsonHelper.parseEnum(k, Duration)!, v as number[]); + }); + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/generated/model/BeatCloner.ts b/packages/alphatab/src/generated/model/BeatCloner.ts index 9b72626ef..ec16d076c 100644 --- a/packages/alphatab/src/generated/model/BeatCloner.ts +++ b/packages/alphatab/src/generated/model/BeatCloner.ts @@ -7,6 +7,7 @@ import { Beat } from "@coderline/alphatab/model/Beat"; import { NoteCloner } from "@coderline/alphatab/generated/model/NoteCloner"; import { AutomationCloner } from "@coderline/alphatab/generated/model/AutomationCloner"; import { BendPointCloner } from "@coderline/alphatab/generated/model/BendPointCloner"; +import { TremoloPickingEffectCloner } from "@coderline/alphatab/generated/model/TremoloPickingEffectCloner"; /** * @internal */ @@ -54,7 +55,7 @@ export class BeatCloner { clone.chordId = original.chordId; clone.graceType = original.graceType; clone.pickStroke = original.pickStroke; - clone.tremoloSpeed = original.tremoloSpeed; + clone.tremoloPicking = original.tremoloPicking ? TremoloPickingEffectCloner.clone(original.tremoloPicking) : undefined; clone.crescendo = original.crescendo; clone.displayStart = original.displayStart; clone.playbackStart = original.playbackStart; diff --git a/packages/alphatab/src/generated/model/BeatSerializer.ts b/packages/alphatab/src/generated/model/BeatSerializer.ts index ad318d32a..bcc414c25 100644 --- a/packages/alphatab/src/generated/model/BeatSerializer.ts +++ b/packages/alphatab/src/generated/model/BeatSerializer.ts @@ -8,6 +8,7 @@ import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; import { NoteSerializer } from "@coderline/alphatab/generated/model/NoteSerializer"; import { AutomationSerializer } from "@coderline/alphatab/generated/model/AutomationSerializer"; import { BendPointSerializer } from "@coderline/alphatab/generated/model/BendPointSerializer"; +import { TremoloPickingEffectSerializer } from "@coderline/alphatab/generated/model/TremoloPickingEffectSerializer"; import { BeatStyleSerializer } from "@coderline/alphatab/generated/model/BeatStyleSerializer"; import { Note } from "@coderline/alphatab/model/Note"; import { BendStyle } from "@coderline/alphatab/model/BendStyle"; @@ -21,6 +22,7 @@ import { BendPoint } from "@coderline/alphatab/model/BendPoint"; import { VibratoType } from "@coderline/alphatab/model/VibratoType"; import { GraceType } from "@coderline/alphatab/model/GraceType"; import { PickStroke } from "@coderline/alphatab/model/PickStroke"; +import { TremoloPickingEffect } from "@coderline/alphatab/model/TremoloPickingEffect"; import { CrescendoType } from "@coderline/alphatab/model/CrescendoType"; import { GolpeType } from "@coderline/alphatab/model/GolpeType"; import { DynamicValue } from "@coderline/alphatab/model/DynamicValue"; @@ -75,7 +77,9 @@ export class BeatSerializer { o.set("chordid", obj.chordId); o.set("gracetype", obj.graceType as number); o.set("pickstroke", obj.pickStroke as number); - o.set("tremolospeed", obj.tremoloSpeed as number | null); + if (obj.tremoloPicking) { + o.set("tremolopicking", TremoloPickingEffectSerializer.toJson(obj.tremoloPicking)); + } o.set("crescendo", obj.crescendo as number); o.set("displaystart", obj.displayStart); o.set("playbackstart", obj.playbackStart); @@ -201,8 +205,14 @@ export class BeatSerializer { case "pickstroke": obj.pickStroke = JsonHelper.parseEnum(v, PickStroke)!; return true; - case "tremolospeed": - obj.tremoloSpeed = JsonHelper.parseEnum(v, Duration) ?? null; + case "tremolopicking": + if (v) { + obj.tremoloPicking = new TremoloPickingEffect(); + TremoloPickingEffectSerializer.fromJson(obj.tremoloPicking, v); + } + else { + obj.tremoloPicking = undefined; + } return true; case "crescendo": obj.crescendo = JsonHelper.parseEnum(v, CrescendoType)!; diff --git a/packages/alphatab/src/generated/model/InstrumentArticulationSerializer.ts b/packages/alphatab/src/generated/model/InstrumentArticulationSerializer.ts index 2e91ec8b8..ba1fd5228 100644 --- a/packages/alphatab/src/generated/model/InstrumentArticulationSerializer.ts +++ b/packages/alphatab/src/generated/model/InstrumentArticulationSerializer.ts @@ -22,6 +22,7 @@ export class InstrumentArticulationSerializer { return null; } const o = new Map(); + o.set("id", obj.id); o.set("elementtype", obj.elementType); o.set("staffline", obj.staffLine); o.set("noteheaddefault", obj.noteHeadDefault as number); @@ -34,6 +35,9 @@ export class InstrumentArticulationSerializer { } public static setProperty(obj: InstrumentArticulation, property: string, v: unknown): boolean { switch (property) { + case "id": + obj.id = v! as number; + return true; case "elementtype": obj.elementType = v! as string; return true; diff --git a/packages/alphatab/src/generated/model/MasterBarSerializer.ts b/packages/alphatab/src/generated/model/MasterBarSerializer.ts index e81d3c234..4c9d2b9d5 100644 --- a/packages/alphatab/src/generated/model/MasterBarSerializer.ts +++ b/packages/alphatab/src/generated/model/MasterBarSerializer.ts @@ -5,9 +5,11 @@ // import { MasterBar } from "@coderline/alphatab/model/MasterBar"; import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { BeamingRulesSerializer } from "@coderline/alphatab/generated/model/BeamingRulesSerializer"; import { SectionSerializer } from "@coderline/alphatab/generated/model/SectionSerializer"; import { AutomationSerializer } from "@coderline/alphatab/generated/model/AutomationSerializer"; import { FermataSerializer } from "@coderline/alphatab/generated/model/FermataSerializer"; +import { BeamingRules } from "@coderline/alphatab/model/MasterBar"; import { TripletFeel } from "@coderline/alphatab/model/TripletFeel"; import { Section } from "@coderline/alphatab/model/Section"; import { Automation } from "@coderline/alphatab/model/Automation"; @@ -35,6 +37,9 @@ export class MasterBarSerializer { o.set("timesignaturenumerator", obj.timeSignatureNumerator); o.set("timesignaturedenominator", obj.timeSignatureDenominator); o.set("timesignaturecommon", obj.timeSignatureCommon); + if (obj.beamingRules) { + o.set("beamingrules", BeamingRulesSerializer.toJson(obj.beamingRules)); + } o.set("isfreetime", obj.isFreeTime); o.set("tripletfeel", obj.tripletFeel as number); if (obj.section) { @@ -87,6 +92,15 @@ export class MasterBarSerializer { case "timesignaturecommon": obj.timeSignatureCommon = v! as boolean; return true; + case "beamingrules": + if (v) { + obj.beamingRules = new BeamingRules(); + BeamingRulesSerializer.fromJson(obj.beamingRules, v); + } + else { + obj.beamingRules = undefined; + } + return true; case "isfreetime": obj.isFreeTime = v! as boolean; return true; diff --git a/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts b/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts index de6319e2b..10ac9a9ac 100644 --- a/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts +++ b/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts @@ -9,6 +9,7 @@ import { BracketExtendMode } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNamePolicy } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNameMode } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNameOrientation } from "@coderline/alphatab/model/RenderStylesheet"; +import { BarNumberDisplay } from "@coderline/alphatab/model/RenderStylesheet"; /** * @internal */ @@ -43,6 +44,7 @@ export class RenderStylesheetSerializer { m.set(k.toString(), v); } } + o.set("globaldisplaychorddiagramsinscore", obj.globalDisplayChordDiagramsInScore); o.set("singletracktracknamepolicy", obj.singleTrackTrackNamePolicy as number); o.set("multitracktracknamepolicy", obj.multiTrackTrackNamePolicy as number); o.set("firstsystemtracknamemode", obj.firstSystemTrackNameMode as number); @@ -57,6 +59,11 @@ export class RenderStylesheetSerializer { a.push(v); } } + o.set("extendbarlines", obj.extendBarLines); + o.set("hideemptystaves", obj.hideEmptyStaves); + o.set("hideemptystavesinfirstsystem", obj.hideEmptyStavesInFirstSystem); + o.set("showsinglestaffbrackets", obj.showSingleStaffBrackets); + o.set("barnumberdisplay", obj.barNumberDisplay as number); return o; } public static setProperty(obj: RenderStylesheet, property: string, v: unknown): boolean { @@ -88,6 +95,9 @@ export class RenderStylesheetSerializer { obj.perTrackChordDiagramsOnTop!.set(Number.parseInt(k), v as boolean); }); return true; + case "globaldisplaychorddiagramsinscore": + obj.globalDisplayChordDiagramsInScore = v! as boolean; + return true; case "singletracktracknamepolicy": obj.singleTrackTrackNamePolicy = JsonHelper.parseEnum(v, TrackNamePolicy)!; return true; @@ -112,6 +122,21 @@ export class RenderStylesheetSerializer { case "pertrackmultibarrest": obj.perTrackMultiBarRest = new Set(v as number[]); return true; + case "extendbarlines": + obj.extendBarLines = v! as boolean; + return true; + case "hideemptystaves": + obj.hideEmptyStaves = v! as boolean; + return true; + case "hideemptystavesinfirstsystem": + obj.hideEmptyStavesInFirstSystem = v! as boolean; + return true; + case "showsinglestaffbrackets": + obj.showSingleStaffBrackets = v! as boolean; + return true; + case "barnumberdisplay": + obj.barNumberDisplay = JsonHelper.parseEnum(v, BarNumberDisplay)!; + return true; } return false; } diff --git a/packages/alphatab/src/generated/model/TremoloPickingEffectCloner.ts b/packages/alphatab/src/generated/model/TremoloPickingEffectCloner.ts new file mode 100644 index 000000000..29b9e0101 --- /dev/null +++ b/packages/alphatab/src/generated/model/TremoloPickingEffectCloner.ts @@ -0,0 +1,17 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { TremoloPickingEffect } from "@coderline/alphatab/model/TremoloPickingEffect"; +/** + * @internal + */ +export class TremoloPickingEffectCloner { + public static clone(original: TremoloPickingEffect): TremoloPickingEffect { + const clone = new TremoloPickingEffect(); + clone.marks = original.marks; + clone.style = original.style; + return clone; + } +} diff --git a/packages/alphatab/src/generated/model/TremoloPickingEffectSerializer.ts b/packages/alphatab/src/generated/model/TremoloPickingEffectSerializer.ts new file mode 100644 index 000000000..7150e3f7c --- /dev/null +++ b/packages/alphatab/src/generated/model/TremoloPickingEffectSerializer.ts @@ -0,0 +1,39 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { TremoloPickingEffect } from "@coderline/alphatab/model/TremoloPickingEffect"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { TremoloPickingStyle } from "@coderline/alphatab/model/TremoloPickingEffect"; +/** + * @internal + */ +export class TremoloPickingEffectSerializer { + public static fromJson(obj: TremoloPickingEffect, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => TremoloPickingEffectSerializer.setProperty(obj, k, v)); + } + public static toJson(obj: TremoloPickingEffect | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("marks", obj.marks); + o.set("style", obj.style as number); + return o; + } + public static setProperty(obj: TremoloPickingEffect, property: string, v: unknown): boolean { + switch (property) { + case "marks": + obj.marks = v! as number; + return true; + case "style": + obj.style = JsonHelper.parseEnum(v, TremoloPickingStyle)!; + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/importer/AlphaTexImporter.ts b/packages/alphatab/src/importer/AlphaTexImporter.ts index dca3988ee..7db2ff544 100644 --- a/packages/alphatab/src/importer/AlphaTexImporter.ts +++ b/packages/alphatab/src/importer/AlphaTexImporter.ts @@ -14,7 +14,7 @@ import { type AlphaTexScoreNode, type AlphaTexTextNode } from '@coderline/alphatab/importer/alphaTex/AlphaTexAst'; -import { AlphaTexParseMode, AlphaTexParser } from '@coderline/alphatab/importer/alphaTex/AlphaTexParser'; +import { type AlphaTexParseMode, AlphaTexParser } from '@coderline/alphatab/importer/alphaTex/AlphaTexParser'; import { AlphaTexAccidentalMode, type AlphaTexDiagnostic, @@ -23,7 +23,8 @@ import { AlphaTexDiagnosticsSeverity, type IAlphaTexImporter, type IAlphaTexImporterState, - AlphaTexStaffNoteKind + AlphaTexStaffNoteKind, + AlphaTexVoiceMode } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; import { ApplyNodeResult, @@ -147,11 +148,11 @@ class AlphaTexImportState implements IAlphaTexImporterState { public ignoredInitialStaff = false; public ignoredInitialTrack = false; public currentDuration = Duration.Quarter; - public articulationValueToIndex = new Map(); + public articulationUniqueIdToIndex = new Map(); public hasAnyProperData = false; - public readonly percussionArticulationNames = new Map(); + public readonly percussionArticulationNames = new Map(); public readonly slurs = new Map(); public readonly lyrics = new Map(); @@ -166,6 +167,7 @@ class AlphaTexImportState implements IAlphaTexImporterState { public currentDynamics = DynamicValue.F; public accidentalMode = AlphaTexAccidentalMode.Explicit; + public voiceMode = AlphaTexVoiceMode.StaffWise; public currentTupletNumerator = -1; public currentTupletDenominator = -1; public scoreNode: AlphaTexScoreNode | undefined; @@ -268,10 +270,6 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter // as long we have some nodes, we can already start semantically // validating and using them - if (scoreNode.bars.length === 0) { - throw new UnsupportedFormatError('No alphaTex data found'); - } - this._bars(scoreNode); if (this.semanticDiagnostics.hasErrors) { @@ -323,7 +321,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter const staff = this._state.currentTrack.staves[0]; staff.displayTranspositionPitch = 0; staff.stringTuning = Tuning.getDefaultTuningFor(6)!; - this._state.articulationValueToIndex.clear(); + this._state.articulationUniqueIdToIndex.clear(); this._beginStaff(staff); @@ -349,18 +347,38 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter private _bars(node: AlphaTexScoreNode) { if (node.bars.length > 0) { + let previousBarCompleted = false; for (const b of node.bars) { - this._bar(b); + this._bar(b, previousBarCompleted); + + switch (this.state.voiceMode) { + case AlphaTexVoiceMode.StaffWise: + // if voices are staff-wise, we definitly have a new bar here + this._state.barIndex++; + previousBarCompleted = true; + break; + case AlphaTexVoiceMode.BarWise: + // if voices are bar-wise, the next bar might be another voice in the same bar + // (barIndex increment is handled inside _barMeta) + // if we have an explicit bar end, we can increase already + if (b.pipe) { + this._state.barIndex++; + this._state.voiceIndex = 0; + this._state.ignoredInitialVoice = false; + previousBarCompleted = true; + } + break; + } } } else { - this._newBar(this._state.currentStaff!); + this._getBar(this._state.currentStaff!); this._detectTuningForStaff(this._state.currentStaff!); this._handleTransposition(this._state.currentStaff!); } } - private _bar(node: AlphaTexBarNode) { - const bar = this._barMeta(node); + private _bar(node: AlphaTexBarNode, previousBarCompleted: boolean) { + const bar = this._barMeta(node, previousBarCompleted); this._detectTuningForStaff(this._state.currentStaff!); this._handleTransposition(this._state.currentStaff!); @@ -525,6 +543,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter let isDead: boolean = false; let isTie: boolean = false; let numericValue: number = -1; + let articulationValue: string = ''; let octave: number = -1; let tone: number = -1; let accidentalMode = NoteAccidentalMode.Default; @@ -572,13 +591,19 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter const percussionArticulationNames = this._state.percussionArticulationNames; if (staffNoteKind === undefined && percussionArticulationNames.size === 0) { for (const [defaultName, defaultValue] of PercussionMapper.instrumentArticulationNames) { - percussionArticulationNames.set(defaultName.toLowerCase(), defaultValue); - percussionArticulationNames.set(ModelUtils.toArticulationId(defaultName), defaultValue); + const articulation = PercussionMapper.getInstrumentArticulationByUniqueId(defaultValue); + if (articulation) { + percussionArticulationNames.set(defaultName.toLowerCase(), defaultValue); + percussionArticulationNames.set( + ModelUtils.toArticulationId(defaultName), + defaultValue + ); + } } } if (percussionArticulationNames.has(articulationName)) { - numericValue = percussionArticulationNames.get(articulationName)!; + articulationValue = percussionArticulationNames.get(articulationName)!; } else { this.addSemanticDiagnostic({ code: AlphaTexDiagnosticCode.AT209, @@ -588,7 +613,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter end: noteValue.end }); // avoid double error - numericValue = Array.from(PercussionMapper.instrumentArticulationNames.values())[0]; + articulationValue = Array.from(PercussionMapper.instrumentArticulationNames.values())[0]; return; } } @@ -663,11 +688,19 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter break; case AlphaTexStaffNoteKind.Articulation: let articulationIndex: number = 0; - if (this._state.articulationValueToIndex.has(numericValue)) { - articulationIndex = this._state.articulationValueToIndex.get(numericValue)!; + + if (articulationValue.length === 0 && numericValue > 0) { + const byId = PercussionMapper.getArticulationById(numericValue); + if (byId) { + articulationValue = byId.uniqueId; + } + } + + if (this._state.articulationUniqueIdToIndex.has(articulationValue)) { + articulationIndex = this._state.articulationUniqueIdToIndex.get(articulationValue)!; } else { articulationIndex = this._state.currentTrack!.percussionArticulations.length; - const articulation = PercussionMapper.getArticulationByInputMidiNumber(numericValue); + const articulation = PercussionMapper.getInstrumentArticulationByUniqueId(articulationValue); if (articulation === null) { this.addSemanticDiagnostic({ code: AlphaTexDiagnosticCode.AT209, @@ -680,7 +713,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter } this._state.currentTrack!.percussionArticulations.push(articulation!); - this._state.articulationValueToIndex.set(numericValue, articulationIndex); + this._state.articulationUniqueIdToIndex.set(articulationValue, articulationIndex); } note.percussionArticulation = articulationIndex; break; @@ -841,7 +874,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter } } - private _barMeta(node: AlphaTexBarNode): Bar { + private _barMeta(node: AlphaTexBarNode, previousBarCompleted: boolean): Bar { // it might be a bit an edge case but a valid one: // one might repeat multiple structural metadata // in one bar starting multiple tracks/staves/voices which are @@ -859,6 +892,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter let previousStaff = this._state.currentStaff!; let hadNewTrack = false; let hadNewStaff = false; + let hadNewVoice = false; let applyInitialBarMetaToPreviousStaff = false; const resetInitialBarMeta = () => { @@ -871,11 +905,18 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter previousStaff = this._state.currentStaff!; hadNewTrack = false; hadNewStaff = false; + hadNewVoice = false; applyInitialBarMetaToPreviousStaff = false; }; const bar: Lazy = new Lazy(() => { - const b = this._newBar(this._state.currentStaff!); + // had a \voice in this bar -> barIndex and voice were updated already + // if not, we start a new bar here + if (!hadNewVoice && !previousBarCompleted) { + this._state.barIndex++; + } + + const b = this._getBar(this._state.currentStaff!); if (initialBarMeta) { for (const initial of initialBarMeta) { this._handler.applyBarMetaData(this, b, initial); @@ -919,6 +960,9 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter // new bar needed on new structural level bar.reset(); break; + case ApplyStructuralMetaDataResult.AppliedNewVoice: + hadNewVoice = true; + break; } if (initialBarMeta) { @@ -989,15 +1033,14 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter return bar.value; } - private _newBar(staff: Staff): Bar { + private _getBar(staff: Staff): Bar { // existing bar? -> e.g. in multi-voice setups where we fill empty voices later if (this._state.barIndex < staff.bars.length) { const bar = staff.bars[this._state.barIndex]; - this._state.barIndex++; return bar; } - const voiceCount = staff.bars.length === 0 ? 1 : staff.bars[0].voices.length; + const voiceCount = staff.bars.length === 0 ? this._state.voiceIndex + 1 : staff.bars[0].voices.length; // need new bar const newBar: Bar = new Bar(); @@ -1008,7 +1051,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter newBar.keySignature = newBar.previousBar!.keySignature; newBar.keySignatureType = newBar.previousBar!.keySignatureType; } - this._state.barIndex++; + this._state.barIndex = newBar.index; if (newBar.index > 0) { newBar.clef = newBar.previousBar!.clef; @@ -1073,27 +1116,67 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter } public startNewVoice() { - if ( - this._state.voiceIndex === 0 && - (this._state.currentStaff!.bars.length === 0 || - (this._state.currentStaff!.bars.length === 1 && - this._state.currentStaff!.bars[0].isEmpty && - !this._state.ignoredInitialVoice)) - ) { - // voice marker on the begining of the first voice without any bar yet? - // -> ignore + // only if we're on the first voice we might skip the initial \voice meta + let shouldIgnoreInitialVoice = this._state.voiceIndex === 0 && !this._state.ignoredInitialVoice; + + // this logic is expanded for readability + if (shouldIgnoreInitialVoice) { + // if we have no bars created yet, we stay on the initial voice + if (this._state.currentStaff!.bars.length === 0) { + shouldIgnoreInitialVoice = true; + } else { + switch (this._state.voiceMode) { + case AlphaTexVoiceMode.StaffWise: + // on staffwise voices, we can only ignore the "initial" voice if the + // first bar we have is completely empty + shouldIgnoreInitialVoice = + this._state.currentStaff!.bars.length === 1 && this._state.currentStaff!.bars[0].isEmpty; + break; + case AlphaTexVoiceMode.BarWise: + // on barwise voices, we ignore the bar count but check only the first voice of the current bar + // to find out if it is the initial empty one + if (this._state.barIndex < this._state.currentStaff!.bars.length) { + // bar exists -> check if empty + const bar = this._state.currentStaff!.bars[this._state.barIndex]; + shouldIgnoreInitialVoice = bar.voices[0].isEmpty; + } else { + // bar doesn't exist yet + shouldIgnoreInitialVoice = true; + } + break; + } + } + } + + if (shouldIgnoreInitialVoice) { this._state.ignoredInitialVoice = true; return; } - // create directly a new empty voice for all bars + + switch (this._state.voiceMode) { + case AlphaTexVoiceMode.StaffWise: + // start using the new voice (see newBar for details on matching) + this._state.voiceIndex++; + this._state.barIndex = 0; + this._state.currentTupletDenominator = -1; + this._state.currentTupletNumerator = -1; + + break; + case AlphaTexVoiceMode.BarWise: + this._state.voiceIndex++; + + this._state.currentTupletDenominator = -1; + this._state.currentTupletNumerator = -1; + break; + } + + // create all missing voices for (const b of this._state.currentStaff!.bars) { - const v = new Voice(); - b.addVoice(v); + while (b.voices.length <= this._state.voiceIndex) { + b.addVoice(new Voice()); + } } - // start using the new voice (see newBar for details on matching) - this._state.voiceIndex++; - this._state.barIndex = 0; - this._state.currentTupletDenominator = -1; - this._state.currentTupletNumerator = -1; + + // create voices } } diff --git a/packages/alphatab/src/importer/BinaryStylesheet.ts b/packages/alphatab/src/importer/BinaryStylesheet.ts index 6ed901323..3da315306 100644 --- a/packages/alphatab/src/importer/BinaryStylesheet.ts +++ b/packages/alphatab/src/importer/BinaryStylesheet.ts @@ -6,6 +6,7 @@ import { BendPoint } from '@coderline/alphatab/model/BendPoint'; import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; import { Color } from '@coderline/alphatab/model/Color'; import { + BarNumberDisplay, type BracketExtendMode, TrackNameMode, TrackNameOrientation, @@ -150,6 +151,9 @@ export class BinaryStylesheet { case 'Global/DrawChords': score.stylesheet.globalDisplayChordDiagramsOnTop = value as boolean; break; + case 'System/drawChordInScore': + score.stylesheet.globalDisplayChordDiagramsInScore = value as boolean; + break; case 'System/showTrackNameSingle': if (!(value as boolean)) { score.stylesheet.singleTrackTrackNamePolicy = TrackNamePolicy.Hidden; @@ -218,6 +222,10 @@ export class BinaryStylesheet { score.stylesheet.otherSystemsTrackNameOrientation = TrackNameOrientation.Vertical; } break; + case 'System/ExtendedBarLines': + score.stylesheet.extendBarLines = value as boolean; + break; + case 'Header/Title': ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Title).template = value as string; break; @@ -340,6 +348,20 @@ export class BinaryStylesheet { ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.CopyrightSecondLine).isVisible = value as boolean; break; + + case 'System/barIndexDrawType': + switch (value as number) { + case 0: + score.stylesheet.barNumberDisplay = BarNumberDisplay.AllBars; + break; + case 1: + score.stylesheet.barNumberDisplay = BarNumberDisplay.FirstOfSystem; + break; + case 2: + score.stylesheet.barNumberDisplay = BarNumberDisplay.Hide; + break; + } + break; } } } @@ -456,6 +478,11 @@ export class BinaryStylesheet { score.stylesheet.globalDisplayChordDiagramsOnTop, DataType.Boolean ); + binaryStylesheet.addValue( + 'System/drawChordInScore', + score.stylesheet.globalDisplayChordDiagramsInScore, + DataType.Boolean + ); switch (score.stylesheet.singleTrackTrackNamePolicy) { case TrackNamePolicy.Hidden: @@ -517,6 +544,8 @@ export class BinaryStylesheet { break; } + binaryStylesheet.addValue('System/ExtendedBarLines', score.stylesheet.extendBarLines, DataType.Boolean); + const scoreStyle = score.style; if (scoreStyle) { for (const [k, v] of scoreStyle.headerAndFooter) { @@ -555,6 +584,18 @@ export class BinaryStylesheet { } } + switch (score.stylesheet.barNumberDisplay) { + case BarNumberDisplay.AllBars: + binaryStylesheet.addValue('System/barIndexDrawType', 0, DataType.Integer); + break; + case BarNumberDisplay.FirstOfSystem: + binaryStylesheet.addValue('System/barIndexDrawType', 1, DataType.Integer); + break; + case BarNumberDisplay.Hide: + binaryStylesheet.addValue('System/barIndexDrawType', 2, DataType.Integer); + break; + } + const writer = ByteBuffer.withCapacity(128); binaryStylesheet.writeTo(writer); return writer.toArray(); diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index 4f4423c08..dbe90ebf8 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -37,16 +37,17 @@ import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { Voice } from '@coderline/alphatab/model/Voice'; -import { Logger } from '@coderline/alphatab/Logger'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import type { IWriteable } from '@coderline/alphatab/io/IWriteable'; -import { FadeType } from '@coderline/alphatab/model/FadeType'; -import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; +import { Logger } from '@coderline/alphatab/Logger'; +import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; import { Direction } from '@coderline/alphatab/model/Direction'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import { FadeType } from '@coderline/alphatab/model/FadeType'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; +import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; +import { TremoloPickingEffect } from '@coderline/alphatab/model/TremoloPickingEffect'; import { WahPedal } from '@coderline/alphatab/model/WahPedal'; -import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal @@ -63,7 +64,10 @@ export class Gp3To5Importer extends ScoreImporter { private _playbackInfos: PlaybackInformation[] = []; private _doubleBars: Set = new Set(); private _clefsPerTrack: Map = new Map(); - private _keySignatures: Map = new Map(); + private _keySignatures: Map = new Map< + number, + [KeySignature, KeySignatureType] + >(); private _beatTextChunksByTrack: Map = new Map(); private _directionLookup: Map = new Map(); @@ -430,6 +434,11 @@ export class Gp3To5Importer extends ScoreImporter { } } + /** + * Guitar Pro 3-6 changes to a bass clef if any string tuning is below B2; + */ + private static readonly _bassClefTuningThreshold = ModelUtils.parseTuning('B2')!.realValue; + public readTrack(): void { const newTrack: Track = new Track(); newTrack.ensureStaveCount(1); @@ -523,10 +532,10 @@ export class Gp3To5Importer extends ScoreImporter { // `12` for all tunings which have bass clefs const clefMode = IOHelper.readInt32LE(this.data); - if (clefMode === 12) { - this._clefsPerTrack.set(index, Clef.F4); + if (clefMode === 12 || tuning[tuning.length - 1] < Gp3To5Importer._bassClefTuningThreshold) { + this._clefsPerTrack.set(newTrack.index, Clef.F4); } else { - this._clefsPerTrack.set(index, Clef.G2); + this._clefsPerTrack.set(newTrack.index, Clef.G2); } // Unknown, no UI setting seem to affect this @@ -566,10 +575,10 @@ export class Gp3To5Importer extends ScoreImporter { GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); } } else { - if (GeneralMidi.isBass(newTrack.playbackInfo.program)) { - this._clefsPerTrack.set(index, Clef.F4); + if (tuning[tuning.length - 1] < Gp3To5Importer._bassClefTuningThreshold) { + this._clefsPerTrack.set(newTrack.index, Clef.F4); } else { - this._clefsPerTrack.set(index, Clef.G2); + this._clefsPerTrack.set(newTrack.index, Clef.G2); } } } @@ -1356,18 +1365,9 @@ export class Gp3To5Importer extends ScoreImporter { } public readTremoloPicking(beat: Beat): void { - const speed: number = this.data.readByte(); - switch (speed) { - case 1: - beat.tremoloSpeed = Duration.Eighth; - break; - case 2: - beat.tremoloSpeed = Duration.Sixteenth; - break; - case 3: - beat.tremoloSpeed = Duration.ThirtySecond; - break; - } + const effect = new TremoloPickingEffect(); + beat.tremoloPicking = effect; + effect.marks = this.data.readByte(); } public readSlide(note: Note): void { diff --git a/packages/alphatab/src/importer/GpifParser.ts b/packages/alphatab/src/importer/GpifParser.ts index 5621d9efa..703399ce4 100644 --- a/packages/alphatab/src/importer/GpifParser.ts +++ b/packages/alphatab/src/importer/GpifParser.ts @@ -18,7 +18,7 @@ import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; import { KeySignature } from '@coderline/alphatab/model/KeySignature'; import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; import { Lyrics } from '@coderline/alphatab/model/Lyrics'; -import { MasterBar } from '@coderline/alphatab/model/MasterBar'; +import { BeamingRules, MasterBar } from '@coderline/alphatab/model/MasterBar'; import { Note } from '@coderline/alphatab/model/Note'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; @@ -35,26 +35,27 @@ import { Voice } from '@coderline/alphatab/model/Voice'; import type { Settings } from '@coderline/alphatab/Settings'; import { XmlDocument } from '@coderline/alphatab/xml/XmlDocument'; -import { type XmlNode, XmlNodeType } from '@coderline/alphatab/xml/XmlNode'; -import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; -import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; -import { InstrumentArticulation, TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentArticulation'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { BeatCloner } from '@coderline/alphatab/generated/model/BeatCloner'; import { NoteCloner } from '@coderline/alphatab/generated/model/NoteCloner'; import { Logger } from '@coderline/alphatab/Logger'; -import { GolpeType } from '@coderline/alphatab/model/GolpeType'; -import { FadeType } from '@coderline/alphatab/model/FadeType'; -import { WahPedal } from '@coderline/alphatab/model/WahPedal'; +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import { BackingTrack } from '@coderline/alphatab/model/BackingTrack'; import { BarreShape } from '@coderline/alphatab/model/BarreShape'; -import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; -import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; import { Direction } from '@coderline/alphatab/model/Direction'; +import { FadeType } from '@coderline/alphatab/model/FadeType'; +import { GolpeType } from '@coderline/alphatab/model/GolpeType'; +import { InstrumentArticulation, TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentArticulation'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { BackingTrack } from '@coderline/alphatab/model/BackingTrack'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; +import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; +import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; +import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; import { Tuning } from '@coderline/alphatab/model/Tuning'; +import { TremoloPickingEffect } from '@coderline/alphatab/model/TremoloPickingEffect'; +import { WahPedal } from '@coderline/alphatab/model/WahPedal'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import { type XmlNode, XmlNodeType } from '@coderline/alphatab/xml/XmlNode'; /** * This structure represents a duration within a gpif @@ -704,7 +705,7 @@ export class GpifParser { } break; case 'Elements': - this._parseElements(track, c); + this._parseElements(track, c, false); break; } } @@ -721,7 +722,7 @@ export class GpifParser { } break; case 'Elements': - this._parseElements(track, c); + this._parseElements(track, c, true); break; case 'LineCount': const lineCount = GpifParser._parseIntSafe(c.innerText, 5); @@ -732,41 +733,45 @@ export class GpifParser { } } } - private _parseElements(track: Track, node: XmlNode) { + private _parseElements(track: Track, node: XmlNode, isInstrumentSet: boolean) { for (const c of node.childElements()) { switch (c.localName) { case 'Element': - this._parseElement(track, c); + this._parseElement(track, c, isInstrumentSet); break; } } } - private _parseElement(track: Track, node: XmlNode) { - const type = node.findChildElement('Type')?.innerText ?? ''; + private _parseElement(track: Track, node: XmlNode, isInstrumentSet: boolean) { + const name = node.findChildElement('Name')?.innerText ?? ''; + for (const c of node.childElements()) { switch (c.localName) { case 'Name': case 'Articulations': - this._parseArticulations(track, c, type); + this._parseArticulations(track, c, isInstrumentSet, name); break; } } } - private _parseArticulations(track: Track, node: XmlNode, elementType: string) { + private _parseArticulations(track: Track, node: XmlNode, isInstrumentSet: boolean, elementName: string) { for (const c of node.childElements()) { switch (c.localName) { case 'Articulation': - this._parseArticulation(track, c, elementType); + this._parseArticulation(track, c, isInstrumentSet, elementName); break; } } } - private _parseArticulation(track: Track, node: XmlNode, elementType: string) { + private _parseArticulation(track: Track, node: XmlNode, isInstrumentSet: boolean, elementName: string) { const articulation = new InstrumentArticulation(); articulation.outputMidiNumber = -1; - articulation.elementType = elementType; + // NOTE: in the past we used the type here, but it is not unique enough. e.g. there are multiple kinds of "ride" ('Ride' vs 'Ride Cymbal 2') + // we have to use the name as element identifier + // using a wrong type leads to wrong "NotationPatch" updates + articulation.elementType = elementName; let name = ''; for (const c of node.childElements()) { const txt = c.innerText; @@ -774,38 +779,28 @@ export class GpifParser { case 'Name': name = c.innerText; break; + case 'InputMidiNumbers': + articulation.id = GpifParser._parseIntSafe(txt.split(' ')[0], 0); + break; case 'OutputMidiNumber': articulation.outputMidiNumber = GpifParser._parseIntSafe(txt, 0); break; case 'TechniqueSymbol': - articulation.techniqueSymbol = this._parseTechniqueSymbol(txt); + articulation.techniqueSymbol = GpifParser.parseTechniqueSymbol(txt); break; case 'TechniquePlacement': - switch (txt) { - case 'outside': - articulation.techniqueSymbolPlacement = TechniqueSymbolPlacement.Outside; - break; - case 'inside': - articulation.techniqueSymbolPlacement = TechniqueSymbolPlacement.Inside; - break; - case 'above': - articulation.techniqueSymbolPlacement = TechniqueSymbolPlacement.Above; - break; - case 'below': - articulation.techniqueSymbolPlacement = TechniqueSymbolPlacement.Below; - break; - } + articulation.techniqueSymbolPlacement = GpifParser.parseTechniqueSymbolPlacement(txt); break; case 'Noteheads': const noteHeadsTxt = GpifParser._splitSafe(txt); if (noteHeadsTxt.length >= 1) { - articulation.noteHeadDefault = this._parseNoteHead(noteHeadsTxt[0]); + articulation.noteHeadDefault = GpifParser.parseNoteHead(noteHeadsTxt[0]); } if (noteHeadsTxt.length >= 2) { - articulation.noteHeadHalf = this._parseNoteHead(noteHeadsTxt[1]); + articulation.noteHeadHalf = GpifParser.parseNoteHead(noteHeadsTxt[1]); } if (noteHeadsTxt.length >= 3) { - articulation.noteHeadWhole = this._parseNoteHead(noteHeadsTxt[2]); + articulation.noteHeadWhole = GpifParser.parseNoteHead(noteHeadsTxt[2]); } if (articulation.noteHeadHalf === MusicFontSymbol.None) { @@ -823,17 +818,20 @@ export class GpifParser { } } - if (articulation.outputMidiNumber !== -1) { + const fullName = `${elementName}.${name}`; + if (isInstrumentSet) { track.percussionArticulations.push(articulation); - if (name.length > 0) { - this._articulationByName.set(name, articulation); - } - } else if (name.length > 0 && this._articulationByName.has(name)) { - this._articulationByName.get(name)!.staffLine = articulation.staffLine; + this._articulationByName.set(fullName, articulation); + } else if (this._articulationByName.has(fullName)) { + // notation patch + this._articulationByName.get(fullName)!.staffLine = articulation.staffLine; } } - private _parseTechniqueSymbol(txt: string): MusicFontSymbol { + /** + * @internal + */ + public static parseTechniqueSymbol(txt: string): MusicFontSymbol { switch (txt) { case 'pictEdgeOfCymbal': return MusicFontSymbol.PictEdgeOfCymbal; @@ -852,7 +850,28 @@ export class GpifParser { } } - private _parseNoteHead(txt: string): MusicFontSymbol { + /** + * @internal + */ + public static parseTechniqueSymbolPlacement(txt: string): TechniqueSymbolPlacement { + switch (txt) { + case 'outside': + return TechniqueSymbolPlacement.Outside; + case 'inside': + return TechniqueSymbolPlacement.Inside; + case 'above': + return TechniqueSymbolPlacement.Above; + case 'below': + return TechniqueSymbolPlacement.Below; + default: + return TechniqueSymbolPlacement.Outside; + } + } + + /** + * @internal + */ + public static parseNoteHead(txt: string): MusicFontSymbol { switch (txt) { case 'noteheadDoubleWholeSquare': return MusicFontSymbol.NoteheadDoubleWholeSquare; @@ -1693,15 +1712,17 @@ export class GpifParser { } break; case 'Tremolo': + const tremolo = new TremoloPickingEffect(); + beat.tremoloPicking = tremolo; switch (c.innerText) { case '1/2': - beat.tremoloSpeed = Duration.Eighth; + tremolo.marks = 1; break; case '1/4': - beat.tremoloSpeed = Duration.Sixteenth; + tremolo.marks = 2; break; case '1/8': - beat.tremoloSpeed = Duration.ThirtySecond; + tremolo.marks = 3; break; } break; @@ -1962,6 +1983,9 @@ export class GpifParser { } private _parseMasterBarXProperties(masterBar: MasterBar, node: XmlNode) { + let beamingRuleDuration: number = Number.NaN; + let beamingRuleGroups: number[] | undefined = undefined; + for (const c of node.childElements()) { switch (c.localName) { case 'XProperty': @@ -1973,10 +1997,40 @@ export class GpifParser { 1 ); break; + case '1124139010': + beamingRuleDuration = GpifParser._parseIntSafe( + c.findChildElement('Int')?.innerText, + Number.NaN + ); + break; + default: + const idNumeric = GpifParser._parseIntSafe(id, 0); + if (idNumeric >= 1124139264 && idNumeric <= 1124139295) { + const groupIndex = idNumeric - 1124139264; + const groupSize = GpifParser._parseIntSafe( + c.findChildElement('Int')?.innerText, + Number.NaN + ); + + if (beamingRuleGroups === undefined) { + beamingRuleGroups = []; + } + while (beamingRuleGroups.length < groupIndex + 1) { + beamingRuleGroups.push(0); + } + beamingRuleGroups[groupIndex] = groupSize; + } + break; } break; } } + + if (!Number.isNaN(beamingRuleDuration) && beamingRuleGroups) { + const rules = new BeamingRules(); + rules.groups.set(beamingRuleDuration as Duration, beamingRuleGroups); + masterBar.beamingRules = rules; + } } private _parseBeatProperties(node: XmlNode, beat: Beat): void { diff --git a/packages/alphatab/src/importer/MusicXmlImporter.ts b/packages/alphatab/src/importer/MusicXmlImporter.ts index 83add730d..549da5689 100644 --- a/packages/alphatab/src/importer/MusicXmlImporter.ts +++ b/packages/alphatab/src/importer/MusicXmlImporter.ts @@ -29,13 +29,16 @@ import { Note, NoteStyle } from '@coderline/alphatab/model/Note'; import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; +import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; +import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; import { Score } from '@coderline/alphatab/model/Score'; import { Section } from '@coderline/alphatab/model/Section'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { Staff } from '@coderline/alphatab/model/Staff'; import { Track } from '@coderline/alphatab/model/Track'; +import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { Voice } from '@coderline/alphatab/model/Voice'; @@ -126,7 +129,8 @@ class TrackInfo { return line; } - private static _defaultNoteArticulation: InstrumentArticulation = new InstrumentArticulation( + private static _defaultNoteArticulation: InstrumentArticulation = InstrumentArticulation.create( + 0, 'Default', 0, 0, @@ -168,7 +172,8 @@ class TrackInfo { const staffLine = musicXmlStaffSteps - stepDifference; - const newArticulation = new InstrumentArticulation( + const newArticulation = InstrumentArticulation.create( + articulation.id, articulation.elementType, staffLine, articulation.outputMidiNumber, @@ -194,6 +199,9 @@ export class MusicXmlImporter extends ScoreImporter { private _indexToTrackInfo: Map = new Map(); private _staffToContext: Map = new Map(); + private _currentBarNumberDisplayPart?: BarNumberDisplay; + private _currentBarNumberDisplayBar?: BarNumberDisplay; + private _divisionsPerQuarterNote: number = 1; private _currentDynamics = DynamicValue.F; @@ -693,6 +701,11 @@ export class MusicXmlImporter extends ScoreImporter { // case 'elevation': Ignored } } + + articulation.id = PercussionMapper.tryMatchKnownArticulation(articulation); + if (articulation.id < 0) { + articulation.id = 0; + } } private static _interpolatePercent(value: number) { @@ -859,23 +872,34 @@ export class MusicXmlImporter extends ScoreImporter { break; } } + + this._currentBarNumberDisplayPart = undefined; } private _parsePartwiseMeasure(element: XmlNode, track: Track, index: number) { const masterBar = this._getOrCreateMasterBar(element, index); - this._parsePartMeasure(element, masterBar, track); + const implicit = element.attributes.get('implicit') === 'yes'; + this._parsePartMeasure(element, masterBar, track, implicit, true); + this._currentBarNumberDisplayBar = undefined; } private _parseTimewiseMeasure(element: XmlNode, index: number) { const masterBar = this._getOrCreateMasterBar(element, index); + const implicit = element.attributes.get('implicit') === 'yes'; for (const c of element.childElements()) { switch (c.localName) { case 'part': - this._parseTimewisePart(c, masterBar); + this._parseTimewisePart(c, masterBar, implicit); + this._currentBarNumberDisplayPart = undefined; + break; + case 'print': + this._parsePrint(c, masterBar, undefined, true); break; } } + + this._currentBarNumberDisplayBar = undefined; } private _getOrCreateMasterBar(element: XmlNode, index: number) { @@ -897,14 +921,14 @@ export class MusicXmlImporter extends ScoreImporter { return masterBar; } - private _parseTimewisePart(element: XmlNode, masterBar: MasterBar) { + private _parseTimewisePart(element: XmlNode, masterBar: MasterBar, implicit: boolean) { const id = element.attributes.get('id'); if (!id || !this._idToTrackInfo.has(id)) { return; } const track = this._idToTrackInfo.get(id)!.track; - this._parsePartMeasure(element, masterBar, track); + this._parsePartMeasure(element, masterBar, track, implicit, false); } // current measure state @@ -920,7 +944,13 @@ export class MusicXmlImporter extends ScoreImporter { */ private _lastBeat: Beat | null = null; - private _parsePartMeasure(element: XmlNode, masterBar: MasterBar, track: Track) { + private _parsePartMeasure( + element: XmlNode, + masterBar: MasterBar, + track: Track, + implicit: boolean, + isPartwise: boolean + ) { this._musicalPosition = 0; this._lastBeat = null; @@ -950,7 +980,7 @@ export class MusicXmlImporter extends ScoreImporter { break; // case 'figured-bass': Not supported case 'print': - this._parsePrint(c, masterBar, track); + this._parsePrint(c, masterBar, track, true); break; case 'sound': this._parseSound(c, masterBar, track); @@ -974,17 +1004,52 @@ export class MusicXmlImporter extends ScoreImporter { // initial empty staff and voice (if no other elements created something already) const staff = this._getOrCreateStaff(track, 0); - this._getOrCreateBar(staff, masterBar); + const bar = this._getOrCreateBar(staff, masterBar); + + if (implicit) { + bar.barNumberDisplay = BarNumberDisplay.Hide; + } else if (isPartwise) { + bar.barNumberDisplay = this._currentBarNumberDisplayBar ?? this._currentBarNumberDisplayPart; + } else { + bar.barNumberDisplay = this._currentBarNumberDisplayPart ?? this._currentBarNumberDisplayBar; + } // clear measure attribute this._keyAllStaves = null; } - private _parsePrint(element: XmlNode, masterBar: MasterBar, track: Track) { - if (element.getAttribute('new-system', 'no') === 'yes') { - track.addLineBreaks(masterBar.index); - } else if (element.getAttribute('new-page', 'no') === 'yes') { - track.addLineBreaks(masterBar.index); + private _parsePrint(element: XmlNode, masterBar: MasterBar, track: Track | undefined, isMeasurePrint: boolean) { + if (track !== undefined) { + if (element.getAttribute('new-system', 'no') === 'yes') { + track.addLineBreaks(masterBar.index); + } else if (element.getAttribute('new-page', 'no') === 'yes') { + track.addLineBreaks(masterBar.index); + } + } + + let newDisplay: BarNumberDisplay | undefined = undefined; + for (const c of element.childElements()) { + switch (c.localName) { + case 'measure-numbering': + switch (c.innerText) { + case 'none': + newDisplay = BarNumberDisplay.Hide; + break; + case 'measure': + newDisplay = BarNumberDisplay.AllBars; + break; + case 'system': + newDisplay = BarNumberDisplay.FirstOfSystem; + break; + } + break; + } + } + + if (isMeasurePrint) { + this._currentBarNumberDisplayBar = newDisplay; + } else { + this._currentBarNumberDisplayPart = newDisplay; } } @@ -1362,6 +1427,15 @@ export class MusicXmlImporter extends ScoreImporter { chord.name += degreeParenthesis ? `(${degree})` : degree; } + if (element.getAttribute('print-frame', 'no') === 'yes') { + chord.showDiagram = true; + this._score.stylesheet.globalDisplayChordDiagramsInScore = true; + } + + if (element.getAttribute('print-object', 'yes') === 'yes') { + chord.showDiagram = true; + } + if (this._nextBeatChord === null) { this._nextBeatChord = chord; } @@ -2158,7 +2232,7 @@ export class MusicXmlImporter extends ScoreImporter { if (unit !== null && perMinute > 0) { const tempoAutomation: Automation = new Automation(); tempoAutomation.type = AutomationType.Tempo; - tempoAutomation.value = (perMinute * (unit / 4)) | 0; + tempoAutomation.value = perMinute * (unit / 4); tempoAutomation.ratioPosition = ratioPosition; if (!this._hasSameTempo(masterBar, tempoAutomation)) { @@ -2703,8 +2777,10 @@ export class MusicXmlImporter extends ScoreImporter { } private static readonly _b4Value = 71; - private _estimateBeamDirection(note: Note): BeamDirection { - return note.calculateRealValue(false, false) < MusicXmlImporter._b4Value ? BeamDirection.Down : BeamDirection.Up; + private _estimateBeamDirection(note: Note): BeamDirection { + return note.calculateRealValue(false, false) < MusicXmlImporter._b4Value + ? BeamDirection.Down + : BeamDirection.Up; } private _parseNoteHead(element: XmlNode, note: Note, beatDuration: Duration, beamDirection: BeamDirection) { @@ -3297,7 +3373,7 @@ export class MusicXmlImporter extends ScoreImporter { } } - private _parseArpeggiate(element: XmlNode, beat: Beat) { + private _parseArpeggiate(element: XmlNode, beat: Beat) { const direction = element.getAttribute('direction', 'down'); switch (direction) { case 'down': @@ -3568,17 +3644,17 @@ export class MusicXmlImporter extends ScoreImporter { break; // case 'schleifer': Not supported case 'tremolo': - switch (c.innerText) { - case '1': - note.beat.tremoloSpeed = Duration.Eighth; - break; - case '2': - note.beat.tremoloSpeed = Duration.Sixteenth; - break; - case '3': - note.beat.tremoloSpeed = Duration.ThirtySecond; - break; + const tremolo = new TremoloPickingEffect(); + note.beat.tremoloPicking = tremolo; + tremolo.marks = Number.parseInt(c.innerText, 10); + + if ( + (c.getAttribute('type', '') === 'unmeasured' && tremolo.marks === 0) || + c.getAttribute('smufl', '') === 'buzzRoll' + ) { + tremolo.style = TremoloPickingStyle.BuzzRoll; } + break; // case 'haydn': Not supported // case 'other-element': Not supported @@ -3607,7 +3683,7 @@ export class MusicXmlImporter extends ScoreImporter { } } - private _parseTied(element: XmlNode, note: Note, staff: Staff): void { + private _parseTied(element: XmlNode, note: Note, staff: Staff): void { const type = element.getAttribute('type'); const number = element.getAttribute('number', ''); diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts index e02f88c37..598cad9ef 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts @@ -1,4 +1,4 @@ -import type { AlphaTexAccidentalMode } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; +import type { AlphaTexAccidentalMode, AlphaTexVoiceMode } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; import type { BarLineStyle } from '@coderline/alphatab/model/Bar'; import type { BarreShape } from '@coderline/alphatab/model/BarreShape'; import type { BendStyle } from '@coderline/alphatab/model/BendStyle'; @@ -14,12 +14,14 @@ import type { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidenta import type { Ottavia } from '@coderline/alphatab/model/Ottavia'; import type { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; import type { + BarNumberDisplay, BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@coderline/alphatab/model/RenderStylesheet'; import type { SimileMark } from '@coderline/alphatab/model/SimileMark'; +import type { TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import type { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; import type { WhammyType } from '@coderline/alphatab/model/WhammyType'; import type { TextAlign } from '@coderline/alphatab/platform/ICanvas'; @@ -74,6 +76,13 @@ export class AlphaTex1EnumMappings { public static readonly alphaTexAccidentalModeReversed = AlphaTex1EnumMappings._reverse( AlphaTex1EnumMappings.alphaTexAccidentalMode ); + public static readonly alphaTexVoiceMode = new Map([ + ['staffwise', 0], + ['barwise', 1] + ]); + public static readonly alphaTexVoiceModeReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.alphaTexVoiceMode + ); public static readonly noteAccidentalMode = new Map([ ['default', 0], ['forcenone', 1], @@ -390,6 +399,21 @@ export class AlphaTex1EnumMappings { ['dadoublecoda', 18] ]); public static readonly directionReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.direction); + public static readonly tremoloPickingStyle = new Map([ + ['default', 0], + ['buzzroll', 1] + ]); + public static readonly tremoloPickingStyleReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.tremoloPickingStyle + ); + public static readonly barNumberDisplay = new Map([ + ['allbars', 0], + ['firstofsystem', 1], + ['hide', 2] + ]); + public static readonly barNumberDisplayReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.barNumberDisplay + ); public static readonly keySignaturesMinorReversed = new Map([ [-7, 'abminor'], [-6, 'ebminor'], diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts index 5ccf35525..8e84fe7ed 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts @@ -188,7 +188,13 @@ export class AlphaTex1LanguageDefinitions { ['firstsystemtracknamemode', [[[[10, 17], 0, ['fullname', 'shortname']]]]], ['othersystemstracknamemode', [[[[10, 17], 0, ['fullname', 'shortname']]]]], ['firstsystemtracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]], - ['othersystemstracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]] + ['othersystemstracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]], + ['extendbarlines', null], + ['chorddiagramsinscore', [[[[10], 1, ['true', 'false']]]]], + ['hideemptystaves', null], + ['hideemptystavesinfirstsystem', null], + ['showsinglestaffbrackets', null], + ['defaultbarnumberdisplay', [[[[10, 17], 0, ['allbars', 'firstofsystem', 'hide']]]]] ]); public static readonly staffMetaDataSignatures = AlphaTex1LanguageDefinitions._signatures([ ['tuning', [[[[10, 17], 0, ['piano', 'none', 'voice']]], [[[10, 17], 5]]]], @@ -472,7 +478,18 @@ export class AlphaTex1LanguageDefinitions { ['spd', [[[[16], 2]]]], ['sph', [[[[16], 2]]]], ['spu', [[[[16], 2]]]], - ['db', null] + ['db', null], + ['voicemode', [[[[10, 17], 0, ['staffwise', 'barwise']]]]], + ['barnumberdisplay', [[[[10, 17], 0, ['allbars', 'firstofsystem', 'hide']]]]], + [ + 'beaming', + [ + [ + [[16], 0], + [[16], 5] + ] + ] + ] ]); public static readonly metaDataProperties = AlphaTex1LanguageDefinitions._metaProps([ [ @@ -525,6 +542,12 @@ export class AlphaTex1LanguageDefinitions { ['othersystemstracknamemode', null], ['firstsystemtracknameorientation', null], ['othersystemstracknameorientation', null], + ['extendbarlines', null], + ['chorddiagramsinscore', null], + ['hideemptystaves', null], + ['hideemptystavesinfirstsystem', null], + ['showsinglestaffbrackets', null], + ['defaultbarnumberdisplay', null], [ 'tuning', [ @@ -580,7 +603,10 @@ export class AlphaTex1LanguageDefinitions { ['spd', null], ['sph', null], ['spu', null], - ['db', null] + ['db', null], + ['voicemode', null], + ['barnumberdisplay', null], + ['beaming', null] ]); public static readonly metaDataSignatures = [ AlphaTex1LanguageDefinitions.scoreMetaDataSignatures, @@ -747,7 +773,15 @@ export class AlphaTex1LanguageDefinitions { ], ['volume', [[[[16], 0]]]], ['balance', [[[[16], 0]]]], - ['tp', [[[[16], 0, ['8', '16', '32']]]]], + [ + 'tp', + [ + [ + [[16], 0], + [[10, 17], 1, ['default', 'buzzroll']] + ] + ] + ], [ 'barre', [ diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index 7673c7abf..555793e11 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -62,7 +62,7 @@ import { GraceType } from '@coderline/alphatab/model/GraceType'; import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; import { Lyrics } from '@coderline/alphatab/model/Lyrics'; -import type { MasterBar } from '@coderline/alphatab/model/MasterBar'; +import { BeamingRules, type MasterBar } from '@coderline/alphatab/model/MasterBar'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import type { Note } from '@coderline/alphatab/model/Note'; import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; @@ -70,7 +70,7 @@ import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import type { RenderStylesheet } from '@coderline/alphatab/model/RenderStylesheet'; +import { BarNumberDisplay, type RenderStylesheet } from '@coderline/alphatab/model/RenderStylesheet'; import { HeaderFooterStyle, Score, ScoreStyle, ScoreSubElement } from '@coderline/alphatab/model/Score'; import { Section } from '@coderline/alphatab/model/Section'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; @@ -78,11 +78,12 @@ import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { Staff } from '@coderline/alphatab/model/Staff'; import { Track } from '@coderline/alphatab/model/Track'; +import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; import { Tuning } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { WahPedal } from '@coderline/alphatab/model/WahPedal'; -import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; /** @@ -91,6 +92,8 @@ import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler { public static readonly instance = new AlphaTex1LanguageHandler(); + private static readonly _timeSignatureDenominators = new Set([1, 2, 4, 8, 16, 32, 64, 128]); + public applyScoreMetaData( importer: IAlphaTexImporter, score: Score, @@ -166,6 +169,9 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler case 'showdynamics': score.stylesheet.hideDynamics = false; return ApplyNodeResult.Applied; + case 'extendbarlines': + score.stylesheet.extendBarLines = true; + return ApplyNodeResult.Applied; case 'bracketextendmode': const bracketExtendMode = AlphaTex1LanguageHandler._parseEnumValue( importer, @@ -256,6 +262,33 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } score.stylesheet.otherSystemsTrackNameOrientation = otherSystemsTrackNameOrientation!; return ApplyNodeResult.Applied; + case 'chorddiagramsinscore': + score.stylesheet.globalDisplayChordDiagramsInScore = metaData.arguments + ? AlphaTex1LanguageHandler._booleanLikeValue(metaData.arguments!.arguments, 0) + : true; + return ApplyNodeResult.Applied; + case 'hideemptystaves': + score.stylesheet.hideEmptyStaves = true; + return ApplyNodeResult.Applied; + case 'hideemptystavesinfirstsystem': + score.stylesheet.hideEmptyStavesInFirstSystem = true; + return ApplyNodeResult.Applied; + case 'showsinglestaffbrackets': + score.stylesheet.showSingleStaffBrackets = true; + return ApplyNodeResult.Applied; + case 'defaultbarnumberdisplay': + const barNumberDisplay = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.arguments!, + 'bar number display', + AlphaTex1EnumMappings.barNumberDisplay + ); + if (barNumberDisplay === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + score.stylesheet.barNumberDisplay = barNumberDisplay!; + return ApplyNodeResult.Applied; + default: return ApplyNodeResult.NotAppliedUnrecognizedMarker; } @@ -275,7 +308,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler const types = lookup.get(tag); if (!types) { - if (args) { + if (args && args.arguments.length > 0) { importer.addSemanticDiagnostic({ code: AlphaTexDiagnosticCode.AT300, message: `Expected no arguments, but found some.`, @@ -442,11 +475,12 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler if (metaData.arguments!.arguments.length === 2) { const number = (metaData.arguments!.arguments[1] as AlphaTexNumberLiteral).value; - if (PercussionMapper.instrumentArticulations.has(number)) { - percussionArticulationNames.set(articulationName.toLowerCase(), number); + const articulation = PercussionMapper.getArticulationById(number); + if (articulation) { + percussionArticulationNames.set(articulationName.toLowerCase(), articulation.uniqueId); return ApplyNodeResult.Applied; } else { - const articulations = Array.from(PercussionMapper.instrumentArticulations.keys()) + const articulations = Array.from(PercussionMapper.instrumentArticulationIds()) .map(n => `${n}`) .join(','); importer.addSemanticDiagnostic({ @@ -565,12 +599,39 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler case 'ts': switch (metaData.arguments!.arguments[0].nodeType) { case AlphaTexNodeType.Number: - bar.masterBar.timeSignatureNumerator = ( - metaData.arguments!.arguments[0] as AlphaTexNumberLiteral - ).value; - bar.masterBar.timeSignatureDenominator = ( - metaData.arguments!.arguments[1] as AlphaTexNumberLiteral - ).value; + bar.masterBar.timeSignatureNumerator = + (metaData.arguments!.arguments[0] as AlphaTexNumberLiteral).value | 0; + + if (bar.masterBar.timeSignatureNumerator < 1 || bar.masterBar.timeSignatureNumerator > 32) { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT211, + message: `Value is out of valid range. Allowed range: 1-32, Actual Value: ${bar.masterBar.timeSignatureNumerator}`, + start: metaData.arguments!.arguments[0].start, + end: metaData.arguments!.arguments[0].end, + severity: AlphaTexDiagnosticsSeverity.Error + }); + } + + bar.masterBar.timeSignatureDenominator = + (metaData.arguments!.arguments[1] as AlphaTexNumberLiteral).value | 0; + + if ( + !AlphaTex1LanguageHandler._timeSignatureDenominators.has( + bar.masterBar.timeSignatureDenominator + ) + ) { + const valueList = Array.from(AlphaTex1LanguageHandler._timeSignatureDenominators).join( + ', ' + ); + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT211, + message: `Value is out of valid range. Allowed range: ${valueList}, Actual Value: ${bar.masterBar.timeSignatureDenominator}`, + start: metaData.arguments!.arguments[0].start, + end: metaData.arguments!.arguments[0].end, + severity: AlphaTexDiagnosticsSeverity.Error + }); + } + break; case AlphaTexNodeType.Ident: case AlphaTexNodeType.String: @@ -592,6 +653,8 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler break; } return ApplyNodeResult.Applied; + case 'beaming': + return this._parseBeamingRule(importer, metaData, bar.masterBar); case 'ks': const keySignature = AlphaTex1LanguageHandler._parseEnumValue( importer, @@ -758,6 +821,8 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler return ApplyNodeResult.Applied; case 'accidentals': return AlphaTex1LanguageHandler._handleAccidentalMode(importer, metaData.arguments!); + case 'voicemode': + return AlphaTex1LanguageHandler._handleVoiceMode(importer, metaData.arguments!); case 'jump': const direction = AlphaTex1LanguageHandler._parseEnumValue( importer, @@ -833,11 +898,74 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler bar.masterBar.isDoubleBar = true; bar.barLineRight = BarLineStyle.LightLight; return ApplyNodeResult.Applied; + case 'barnumberdisplay': + const barNumberDisplay = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.arguments!, + 'bar number display', + AlphaTex1EnumMappings.barNumberDisplay + ); + if (barNumberDisplay === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + bar.barNumberDisplay = barNumberDisplay!; + return ApplyNodeResult.Applied; default: return ApplyNodeResult.NotAppliedUnrecognizedMarker; } } + private _parseBeamingRule(importer: IAlphaTexImporter, metaData: AlphaTexMetaDataNode, masterBar: MasterBar) { + let duration = Duration.Eighth; + const groupSizes: number[] = []; + + const durationValue = (metaData.arguments!.arguments[0] as AlphaTexNumberLiteral).value; + switch (durationValue) { + case 4: + duration = Duration.QuadrupleWhole; + break; + case 8: + duration = Duration.Eighth; + break; + case 16: + duration = Duration.Sixteenth; + break; + case 32: + duration = Duration.ThirtySecond; + break; + default: + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Value is out of valid range. Allowed range: 4,8,16 or 32, Actual Value: ${durationValue}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: metaData.arguments!.arguments[0].start, + end: metaData.arguments!.arguments[0].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + + for (let i = 1; i < metaData.arguments!.arguments.length; i++) { + const groupSize = (metaData.arguments!.arguments[i] as AlphaTexNumberLiteral).value; + if (groupSize < 1) { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Value is out of valid range. Allowed range: >0, Actual Value: ${durationValue}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: metaData.arguments!.arguments[i].start, + end: metaData.arguments!.arguments[i].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + groupSizes.push((metaData.arguments!.arguments[i] as AlphaTexNumberLiteral).value); + } + + if (!masterBar.beamingRules) { + masterBar.beamingRules = new BeamingRules(); + } + masterBar.beamingRules!.groups.set(duration, groupSizes); + return ApplyNodeResult.Applied; + } + private static _handleAccidentalMode(importer: IAlphaTexImporter, args: AlphaTexArgumentList): ApplyNodeResult { const accidentalMode = AlphaTex1LanguageHandler._parseEnumValue( importer, @@ -852,6 +980,20 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler return ApplyNodeResult.Applied; } + private static _handleVoiceMode(importer: IAlphaTexImporter, args: AlphaTexArgumentList): ApplyNodeResult { + const voiceMode = AlphaTex1LanguageHandler._parseEnumValue( + importer, + args, + 'voice mode', + AlphaTex1EnumMappings.alphaTexVoiceMode + ); + if (voiceMode === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + importer.state.voiceMode = voiceMode!; + return ApplyNodeResult.Applied; + } + private static _getChordId(currentStaff: Staff, chordName: string): string { return chordName.toLowerCase() + currentStaff.index + currentStaff.track.index; } @@ -1700,28 +1842,52 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler beat.automations.push(balanceAutomation); return ApplyNodeResult.Applied; case 'tp': - beat.tremoloSpeed = Duration.Eighth; + const tremolo = new TremoloPickingEffect(); + beat.tremoloPicking = tremolo; if (p.arguments && p.arguments.arguments.length > 0) { - const tremoloSpeedValue = (p.arguments!.arguments[0] as AlphaTexNumberLiteral).value; - switch (tremoloSpeedValue) { - case 8: - beat.tremoloSpeed = Duration.Eighth; - break; - case 16: - beat.tremoloSpeed = Duration.Sixteenth; - break; - case 32: - beat.tremoloSpeed = Duration.ThirtySecond; - break; - default: - importer.addSemanticDiagnostic({ - code: AlphaTexDiagnosticCode.AT209, - message: `Unexpected tremolo speed value '${tremoloSpeedValue}, expected: 8, 16 or 32`, - severity: AlphaTexDiagnosticsSeverity.Error, - start: p.arguments!.arguments[0].start, - end: p.arguments!.arguments[0].end - }); + if (p.arguments.arguments.length > 0) { + const tremoloMarks = (p.arguments!.arguments[0] as AlphaTexNumberLiteral).value; + if ( + tremoloMarks >= TremoloPickingEffect.minMarks && + tremoloMarks <= TremoloPickingEffect.maxMarks + ) { + tremolo.marks = tremoloMarks; + } else { + switch (tremoloMarks) { + // backwards compatibility + case 8: + tremolo.marks = 1; + break; + case 16: + tremolo.marks = 2; + break; + case 32: + tremolo.marks = 3; + break; + default: + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected tremolo marks value '${tremoloMarks}, expected: ${TremoloPickingEffect.minMarks}-${TremoloPickingEffect.maxMarks}, or legacy: 8, 16 or 32`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.arguments!.arguments[0].start, + end: p.arguments!.arguments[0].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + } + } + if (p.arguments.arguments.length > 1) { + const tremoloStyle = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.arguments!, + 'tremolo picking style', + AlphaTex1EnumMappings.tremoloPickingStyle, + 1 + ); + if (tremoloStyle === undefined) { return ApplyNodeResult.NotAppliedSemanticError; + } + tremolo.style = tremoloStyle; } } return ApplyNodeResult.Applied; @@ -2465,6 +2631,30 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler ); } + if (stylesheet.extendBarLines) { + nodes.push(Atnf.meta('extendBarLines')); + } + + if (stylesheet.globalDisplayChordDiagramsInScore) { + nodes.push(Atnf.meta('chordDiagramsInScore')); + } + + if (stylesheet.hideEmptyStaves) { + nodes.push(Atnf.meta('hideEmptyStaves')); + } + + if (stylesheet.hideEmptyStavesInFirstSystem) { + nodes.push(Atnf.meta('hideEmptyStavesInFirstSystem')); + } + + if (stylesheet.showSingleStaffBrackets) { + nodes.push(Atnf.meta('showSingleStaffBrackets')); + } + + if (stylesheet.barNumberDisplay !== BarNumberDisplay.AllBars) { + nodes.push(Atnf.identMeta('defaultBarNumberDisplay', BarNumberDisplay[stylesheet.barNumberDisplay])); + } + // Unsupported: // 'globaldisplaychorddiagramsontop', // 'pertrackchorddiagramsontop', @@ -2644,6 +2834,10 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler ]; } + if (bar.barNumberDisplay !== undefined) { + nodes.push(Atnf.identMeta('barNumberDisplay', BarNumberDisplay[bar.barNumberDisplay])); + } + return nodes; } private static _buildStaffMetaDataNodes(nodes: AlphaTexMetaDataNode[], staff: Staff) { @@ -2776,6 +2970,17 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } } + if (masterBar.beamingRules) { + for (const [k, v] of masterBar.beamingRules.groups) { + const args = Atnf.args([Atnf.number(k)], true); + for (const i of v) { + args!.arguments.push(Atnf.number(i)); + } + + nodes.push(Atnf.meta('beaming', args)); + } + } + if ( (masterBar.index > 0 && masterBar.tripletFeel !== masterBar.previousMasterBar?.tripletFeel) || (masterBar.index === 0 && masterBar.tripletFeel !== TripletFeel.NoTripletFeel) @@ -3269,7 +3474,12 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } if (beat.isTremolo) { - Atnf.prop(properties, 'tp', Atnf.numberValue(beat.tremoloSpeed! as number)); + const values: IAlphaTexArgumentValue[] = [Atnf.number(beat.tremoloPicking!.marks)]; + if (beat.tremoloPicking!.style !== TremoloPickingStyle.Default) { + values.push(Atnf.ident(TremoloPickingStyle[beat.tremoloPicking!.style])); + } + + Atnf.prop(properties, 'tp', Atnf.args(values)); } switch (beat.crescendo) { diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts index 19143eb58..31bfcb9b2 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts @@ -47,6 +47,19 @@ export class AlphaTex1MetaDataReader implements IAlphaTexMetaDataReader { AlphaTexNodeType.Number ]); + public hasMetaDataArguments(metaData: AlphaTexMetaDataTagNode): boolean { + const tag = metaData.tag.text.toLowerCase(); + for (const lookup of AlphaTex1LanguageDefinitions.metaDataSignatures) { + if (lookup.has(tag)) { + const types = lookup.get(tag); + return types !== null; + } + } + + // unknown meta -> assume args exist + return true; + } + public readMetaDataArguments( parser: AlphaTexParser, metaData: AlphaTexMetaDataTagNode @@ -83,7 +96,10 @@ export class AlphaTex1MetaDataReader implements IAlphaTexMetaDataReader { if (!AlphaTex1LanguageDefinitions.metaDataProperties.has(tag)) { return undefined; } - const props = AlphaTex1LanguageDefinitions.metaDataProperties.get(tag)!; + const props = AlphaTex1LanguageDefinitions.metaDataProperties.get(tag); + if (!props) { + return this._readPropertyArguments(parser, [], property); + } return this._readPropertyArguments(parser, [props], property); } diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts index 291d6f5a7..5d11afbf1 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts @@ -67,7 +67,7 @@ export class AlphaTexParser { /** * The parsing mode. */ - public mode:AlphaTexParseMode = AlphaTexParseMode.ForModelImport; + public mode: AlphaTexParseMode = AlphaTexParseMode.ForModelImport; public get lexerDiagnostics(): AlphaTexDiagnosticBag { return this.lexer.lexerDiagnostics; @@ -507,6 +507,8 @@ export class AlphaTexParser { return note; } + private static readonly _allowValuesAfterProperties = new Set(['chord']); + private _metaData(metaDataList: AlphaTexMetaDataNode[]) { const tag = this.lexer.peekToken(); if (!tag || tag.nodeType !== AlphaTexNodeType.Tag) { @@ -524,10 +526,11 @@ export class AlphaTexParser { metaDataList.push(metaData); try { + const allowValuesAfterProperties = AlphaTexParser._allowValuesAfterProperties.has(metaData.tag.tag.text); // properties can be before or after the arguments, this is a again a historical // inconsistency on chords const braceCandidate = this.lexer.peekToken(); - if (braceCandidate?.nodeType === AlphaTexNodeType.LBrace) { + if (allowValuesAfterProperties && braceCandidate?.nodeType === AlphaTexNodeType.LBrace) { metaData.propertiesBeforeArguments = true; metaData.properties = this._properties(property => this._metaDataReader.readMetaDataPropertyArguments(this, metaData.tag, property) @@ -554,17 +557,19 @@ export class AlphaTexParser { } } } else { - metaData.arguments = this.argumentList(); - if (!metaData.arguments) { - metaData.arguments = this._metaDataReader.readMetaDataArguments(this, metaData.tag); - if (metaData.arguments && metaData.arguments.arguments.length > 1) { - this.addParserDiagnostic({ - code: AlphaTexDiagnosticCode.AT301, - message: `Metadata arguments should be wrapped into parenthesis.`, - severity: AlphaTexDiagnosticsSeverity.Warning, - start: metaData.arguments?.start ?? metaData.start, - end: metaData.arguments?.end ?? metaData.end - }); + if (this._metaDataReader.hasMetaDataArguments(metaData.tag)) { + metaData.arguments = this.argumentList(); + if (!metaData.arguments) { + metaData.arguments = this._metaDataReader.readMetaDataArguments(this, metaData.tag); + if (metaData.arguments && metaData.arguments.arguments.length > 1) { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT301, + message: `Metadata arguments should be wrapped into parenthesis.`, + severity: AlphaTexDiagnosticsSeverity.Warning, + start: metaData.arguments?.start ?? metaData.start, + end: metaData.arguments?.end ?? metaData.end + }); + } } } diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts index 4013dcd51..7ac9cb275 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts @@ -1,5 +1,5 @@ import type { AlphaTexAstNodeLocation } from '@coderline/alphatab/importer/alphaTex/AlphaTexAst'; -import { AlphaTexParseMode } from '@coderline/alphatab/importer/alphaTex/AlphaTexParser'; +import type { AlphaTexParseMode } from '@coderline/alphatab/importer/alphaTex/AlphaTexParser'; import type { FlatSyncPoint } from '@coderline/alphatab/model/Automation'; import type { SustainPedalMarker } from '@coderline/alphatab/model/Bar'; import type { Beat } from '@coderline/alphatab/model/Beat'; @@ -273,18 +273,27 @@ export enum AlphaTexAccidentalMode { Explicit = 1 } +/** + * @public + */ +export enum AlphaTexVoiceMode { + StaffWise = 0, + BarWise = 1 +} + /** * @public */ export interface IAlphaTexImporterState { score: Score; accidentalMode: AlphaTexAccidentalMode; + voiceMode: AlphaTexVoiceMode; currentDynamics: DynamicValue; currentTupletNumerator: number; currentTupletDenominator: number; readonly syncPoints: FlatSyncPoint[]; readonly slurs: Map; - readonly percussionArticulationNames: Map; + readonly percussionArticulationNames: Map; readonly lyrics: Map; readonly staffHasExplicitDisplayTransposition: Set; readonly staffHasExplicitTuning: Set; diff --git a/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts b/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts index dc86aa63b..9916ce13a 100644 --- a/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts +++ b/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts @@ -1,7 +1,7 @@ import type { + AlphaTexArgumentList, AlphaTexMetaDataTagNode, - AlphaTexPropertyNode, - AlphaTexArgumentList + AlphaTexPropertyNode } from '@coderline/alphatab/importer/alphaTex/AlphaTexAst'; import type { AlphaTexParser } from '@coderline/alphatab/importer/alphaTex/AlphaTexParser'; @@ -9,6 +9,7 @@ import type { AlphaTexParser } from '@coderline/alphatab/importer/alphaTex/Alpha * @internal */ export interface IAlphaTexMetaDataReader { + hasMetaDataArguments(metaData: AlphaTexMetaDataTagNode): boolean; readMetaDataArguments(parser: AlphaTexParser, metaData: AlphaTexMetaDataTagNode): AlphaTexArgumentList | undefined; readMetaDataPropertyArguments( diff --git a/packages/alphatab/src/importer/alphaTex/_barrel.ts b/packages/alphatab/src/importer/alphaTex/_barrel.ts index 2cf4acbbc..9b522270a 100644 --- a/packages/alphatab/src/importer/alphaTex/_barrel.ts +++ b/packages/alphatab/src/importer/alphaTex/_barrel.ts @@ -43,6 +43,7 @@ export { AlphaTexDiagnosticCode, AlphaTexDiagnosticsSeverity, AlphaTexStaffNoteKind, + AlphaTexVoiceMode, ArgumentListParseTypesMode, type IAlphaTexImporter, type IAlphaTexImporterState diff --git a/packages/alphatab/src/midi/AlphaSynthMidiFileHandler.ts b/packages/alphatab/src/midi/AlphaSynthMidiFileHandler.ts index a7e9c84c3..6f705e870 100644 --- a/packages/alphatab/src/midi/AlphaSynthMidiFileHandler.ts +++ b/packages/alphatab/src/midi/AlphaSynthMidiFileHandler.ts @@ -24,6 +24,13 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { private _midiFile: MidiFile; private _smf1Mode: boolean; + /** + * An indicator by how many midi-ticks the song contents are shifted. + * Grace beats at start might require a shift for the first beat to start at 0. + * This information can be used to translate back the player time axis to the music notation. + */ + public tickShift: number = 0; + /** * Initializes a new instance of the {@link AlphaSynthMidiFileHandler} class. * @param midiFile The midi file. @@ -34,7 +41,14 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { this._smf1Mode = smf1Mode; } + public addTickShift(tickShift: number) { + this._midiFile.tickShift = tickShift; + this.tickShift = tickShift; + } + public addTimeSignature(tick: number, timeSignatureNumerator: number, timeSignatureDenominator: number): void { + tick += this.tickShift; + let denominatorIndex: number = 0; let denominator = timeSignatureDenominator; while (true) { @@ -50,12 +64,14 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { } public addRest(track: number, tick: number, channel: number): void { + tick += this.tickShift; if (!this._smf1Mode) { this._midiFile.addEvent(new AlphaTabRestEvent(track, tick, channel)); } } public addNote(track: number, start: number, length: number, key: number, velocity: number, channel: number): void { + start += this.tickShift; this._midiFile.addEvent( new NoteOnEvent( track, @@ -94,16 +110,19 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { controller: ControllerType, value: number ): void { + tick += this.tickShift; this._midiFile.addEvent( new ControlChangeEvent(track, tick, channel, controller, AlphaSynthMidiFileHandler._fixValue(value)) ); } public addProgramChange(track: number, tick: number, channel: number, program: number): void { + tick += this.tickShift; this._midiFile.addEvent(new ProgramChangeEvent(track, tick, channel, program)); } public addTempo(tick: number, tempo: number): void { + tick += this.tickShift; // bpm -> microsecond per quarter note const tempoEvent = new TempoChangeEvent(tick, 0); tempoEvent.beatsPerMinute = tempo; @@ -111,6 +130,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { } public addBend(track: number, tick: number, channel: number, value: number): void { + tick += this.tickShift; if (value >= SynthConstants.MaxPitchWheel) { value = SynthConstants.MaxPitchWheel; } else { @@ -120,6 +140,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { } public addNoteBend(track: number, tick: number, channel: number, key: number, value: number): void { + tick += this.tickShift; if (this._smf1Mode) { this.addBend(track, tick, channel, value); } else { @@ -132,6 +153,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { } public finishTrack(track: number, tick: number): void { + tick += this.tickShift; if (this._midiFile.format === MidiFileFormat.MultiTrack || track === 0) { this._midiFile.addEvent(new EndOfTrackEvent(track, tick)); } diff --git a/packages/alphatab/src/midi/BeatTickLookup.ts b/packages/alphatab/src/midi/BeatTickLookup.ts index 8b7b2f0f5..a33983bb6 100644 --- a/packages/alphatab/src/midi/BeatTickLookup.ts +++ b/packages/alphatab/src/midi/BeatTickLookup.ts @@ -21,6 +21,15 @@ export class BeatTickLookupItem { } } +/** + * Classes implementing this interface can help in checking whether beats are currently being + * displayed so that they can be considered for a tick-search. + * @public + */ +export interface IBeatVisibilityChecker { + isVisible(beat: Beat): boolean; +} + /** * Represents the time period, for which one or multiple {@link Beat}s are played * @public @@ -96,4 +105,18 @@ export class BeatTickLookup { } return null; } + + /** + * Looks for the first visible beat which starts at this lookup so it can be used for cursor placement. + * @param checker The custom checker to see if a beat is visible. + * @returns The first beat which is visible according to the given tracks or null. + */ + getVisibleBeatAtStartWithChecker(checker: IBeatVisibilityChecker): Beat | null { + for (const b of this.highlightedBeats) { + if (b.playbackStart === this.start && checker.isVisible(b.beat)) { + return b.beat; + } + } + return null; + } } diff --git a/packages/alphatab/src/midi/IMidiFileHandler.ts b/packages/alphatab/src/midi/IMidiFileHandler.ts index e14e0bb16..eb75f11e9 100644 --- a/packages/alphatab/src/midi/IMidiFileHandler.ts +++ b/packages/alphatab/src/midi/IMidiFileHandler.ts @@ -85,4 +85,13 @@ export interface IMidiFileHandler { * @param tick The end tick for this track. */ finishTrack(track: number, tick: number): void; + + + /** + * Registers a general shift of the time-axis for the generate midi file. + * @param tickShift The shift in midi ticks by which all midi events beside the initial channel setups are shifted. + * This shift is applied in case grace beats + */ + addTickShift(tickShift: number): void; + } diff --git a/packages/alphatab/src/midi/MidiFile.ts b/packages/alphatab/src/midi/MidiFile.ts index 97a68d9e5..f189d2bc8 100644 --- a/packages/alphatab/src/midi/MidiFile.ts +++ b/packages/alphatab/src/midi/MidiFile.ts @@ -87,6 +87,13 @@ export class MidiFile { * Gets or sets the division per quarter notes. */ public division: number = MidiUtils.QuarterTime; + + /** + * An indicator by how many midi-ticks the song contents are shifted. + * Grace beats at start might require a shift for the first beat to start at 0. + * This information can be used to translate back the player time axis to the music notation. + */ + public tickShift: number = 0; /** * Gets a list of midi events sorted by time. diff --git a/packages/alphatab/src/midi/MidiFileGenerator.ts b/packages/alphatab/src/midi/MidiFileGenerator.ts index 25fe98465..3c1729b82 100644 --- a/packages/alphatab/src/midi/MidiFileGenerator.ts +++ b/packages/alphatab/src/midi/MidiFileGenerator.ts @@ -49,6 +49,13 @@ class MidiNoteDuration { public noteOnly: number = 0; public untilTieOrSlideEnd: number = 0; public letRingEnd: number = 0; + /** + * A factor indicating how much longer/shorter the beat is in its playback respecting + * effects like tuplets, triplet feels, dots, grace beats stealing parts etc. + * + * This factor can be used to relatively adjust durations in effects like trills or tremolos. + */ + public beatDurationFactor: number = 1; } /** @@ -151,6 +158,10 @@ export class MidiFileGenerator { false, (bar, previousMasterBar, currentTick, currentTempo, occurence) => { this._generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence); + if (bar.index === 0 && occurence === 0) { + // tickshift is added after initial track channel details + this._detectTickShift(); + } }, (index, currentTick, currentTempo) => { for (const track of this._score.tracks) { @@ -171,6 +182,24 @@ export class MidiFileGenerator { Logger.debug('Midi', 'Midi generation done'); } + private _detectTickShift() { + let tickShift = 0; + for (const track of this._score.tracks) { + for (const staff of track.staves) { + for (const voice of staff.bars[0].voices) { + if (!voice.isEmpty) { + const beat = voice.beats[0]; + if (beat.playbackStart < tickShift) { + tickShift = beat.playbackStart; + } + } + } + } + } + tickShift = Math.abs(tickShift); + this._handler.addTickShift(tickShift); + } + private _generateTrack(track: Track): void { // channel this._generateChannel(track, track.playbackInfo.primaryChannel, track.playbackInfo); @@ -1137,15 +1166,19 @@ export class MidiFileGenerator { this._handler.addNote(track.index, noteStart, remaining, noteKey, velocity, channel); } - private _getNoteDuration(note: Note, duration: number, tempoOnBeatStart: number): MidiNoteDuration { + private _getNoteDuration(note: Note, beatPlayDuration: number, tempoOnBeatStart: number): MidiNoteDuration { const durationWithEffects: MidiNoteDuration = new MidiNoteDuration(); - durationWithEffects.noteOnly = duration; - durationWithEffects.untilTieOrSlideEnd = duration; - durationWithEffects.letRingEnd = duration; + + const defaultBeatDuration = MidiUtils.toTicks(note.beat.duration); + durationWithEffects.beatDurationFactor = beatPlayDuration / defaultBeatDuration; + + durationWithEffects.noteOnly = beatPlayDuration; + durationWithEffects.untilTieOrSlideEnd = beatPlayDuration; + durationWithEffects.letRingEnd = beatPlayDuration; if (note.isDead) { durationWithEffects.noteOnly = this._applyStaticDuration( MidiFileGenerator._defaultDurationDead, - duration, + beatPlayDuration, tempoOnBeatStart ); durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly; @@ -1155,7 +1188,7 @@ export class MidiFileGenerator { if (note.isPalmMute) { durationWithEffects.noteOnly = this._applyStaticDuration( MidiFileGenerator._defaultDurationPalmMute, - duration, + beatPlayDuration, tempoOnBeatStart ); durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly; @@ -1163,7 +1196,7 @@ export class MidiFileGenerator { return durationWithEffects; } if (note.isStaccato) { - durationWithEffects.noteOnly = (duration / 2) | 0; + durationWithEffects.noteOnly = (beatPlayDuration / 2) | 0; durationWithEffects.untilTieOrSlideEnd = durationWithEffects.noteOnly; durationWithEffects.letRingEnd = durationWithEffects.noteOnly; return durationWithEffects; @@ -1190,7 +1223,8 @@ export class MidiFileGenerator { endNote.beat.playbackDuration, tempoOnBeatStart ); - durationWithEffects.untilTieOrSlideEnd = duration + tieDestinationDuration.untilTieOrSlideEnd; + durationWithEffects.untilTieOrSlideEnd = + beatPlayDuration + tieDestinationDuration.untilTieOrSlideEnd; } } } else if (note.slideOutType === SlideOutType.Legato) { @@ -1233,7 +1267,7 @@ export class MidiFileGenerator { } } if (lastLetRingBeat === note.beat) { - durationWithEffects.letRingEnd = duration; + durationWithEffects.letRingEnd = beatPlayDuration; } else { durationWithEffects.letRingEnd = letRingEnd; } @@ -1313,7 +1347,13 @@ export class MidiFileGenerator { } } - private _generateFadeSteps(track: Track, start: number, duration: number, startVolume: number, endVolume: number): void { + private _generateFadeSteps( + track: Track, + start: number, + duration: number, + startVolume: number, + endVolume: number + ): void { const tickStep: number = 120; // we want to reach the target volume a bit earlier than the end of the note duration = (duration * 0.8) | 0; @@ -1939,7 +1979,10 @@ export class MidiFileGenerator { ): void { const track: Track = note.beat.voice.bar.staff.track; const trillKey: number = note.stringTuning + note.trillFret; + // NOTE: no noteDuration.beatDurationFactor, the trill speed is absolute and not dependent on the + // beat effects let trillLength: number = MidiUtils.toTicks(note.trillSpeed); + let realKey: boolean = true; let tick: number = noteStart; const end: number = noteStart + noteDuration.untilTieOrSlideEnd; @@ -1948,7 +1991,7 @@ export class MidiFileGenerator { if (tick + trillLength >= end) { trillLength = end - tick; } - this._handler.addNote(track.index, tick, trillLength, realKey ? trillKey : noteKey, dynamicValue, channel); + this._handler.addNote(track.index, tick, trillLength, realKey ? noteKey : trillKey, dynamicValue, channel); realKey = !realKey; tick += trillLength; } @@ -1963,9 +2006,18 @@ export class MidiFileGenerator { channel: number ): void { const track: Track = note.beat.voice.bar.staff.track; - let tpLength: number = MidiUtils.toTicks(note.beat.tremoloSpeed!); + const marks = note.beat.tremoloPicking!.marks; + if (marks === 0) { + return; + } + + // the marks represent the duration + let tpLength = + note.beat.tremoloPicking!.getDurationAsTicks(note.beat.duration) * noteDuration.beatDurationFactor; + let tick: number = noteStart; const end: number = noteStart + noteDuration.untilTieOrSlideEnd; + while (tick + 10 < end) { // only the rest on last trill play if (tick + tpLength >= end) { diff --git a/packages/alphatab/src/midi/MidiTickLookup.ts b/packages/alphatab/src/midi/MidiTickLookup.ts index 7e7a2f4c1..c1881d4b9 100644 --- a/packages/alphatab/src/midi/MidiTickLookup.ts +++ b/packages/alphatab/src/midi/MidiTickLookup.ts @@ -1,5 +1,5 @@ import { Logger } from '@coderline/alphatab/Logger'; -import type { BeatTickLookup } from '@coderline/alphatab/midi/BeatTickLookup'; +import type { BeatTickLookup, IBeatVisibilityChecker } from '@coderline/alphatab/midi/BeatTickLookup'; import { MasterBarTickLookup } from '@coderline/alphatab/midi/MasterBarTickLookup'; import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; import type { Beat } from '@coderline/alphatab/model/Beat'; @@ -124,6 +124,19 @@ export class MidiTickLookupFindBeatResult { } } +/** + * @internal + */ +class TrackLookupBeatVisibilityChecker implements IBeatVisibilityChecker { + private _lookup: Set; + public constructor(lookup: Set) { + this._lookup = lookup; + } + public isVisible(beat: Beat): boolean { + return this._lookup.has(beat.voice.bar.staff.track.index); + } +} + /** * This class holds all information about when {@link MasterBar}s and {@link Beat}s are played. * @@ -191,21 +204,35 @@ export class MidiTickLookup { trackLookup: Set, tick: number, currentBeatHint: MidiTickLookupFindBeatResult | null = null + ): MidiTickLookupFindBeatResult | null { + return this.findBeatWithChecker(new TrackLookupBeatVisibilityChecker(trackLookup), tick, currentBeatHint); + } + /** + * Finds the currently played beat given a list of tracks and the current time. + * @param checker The checker to ask whether a beat is visible and should be considered for result. + * @param tick The current time in midi ticks. + * @param currentBeatHint Used for optimized lookup during playback. By passing in a previous result lookup of the next one can be optimized using heuristics. (optional). + * @returns The information about the current beat or null if no beat could be found. + */ + public findBeatWithChecker( + checker: IBeatVisibilityChecker, + tick: number, + currentBeatHint: MidiTickLookupFindBeatResult | null = null ): MidiTickLookupFindBeatResult | null { let result: MidiTickLookupFindBeatResult | null = null; if (currentBeatHint) { - result = this._findBeatFast(trackLookup, currentBeatHint, tick); + result = this._findBeatFast(checker, currentBeatHint, tick); } if (!result) { - result = this._findBeatSlow(trackLookup, currentBeatHint, tick, false); + result = this._findBeatSlow(checker, currentBeatHint, tick, false); } return result; } private _findBeatFast( - trackLookup: Set, + checker: IBeatVisibilityChecker, currentBeatHint: MidiTickLookupFindBeatResult, tick: number ): MidiTickLookupFindBeatResult | null { @@ -214,10 +241,15 @@ export class MidiTickLookup { return currentBeatHint; } // already on the next beat? - if (currentBeatHint.nextBeat && tick >= currentBeatHint.nextBeat.start && tick < currentBeatHint.nextBeat.end) { + if ( + currentBeatHint.nextBeat && + tick >= currentBeatHint.nextBeat.start && + tick < currentBeatHint.nextBeat.end && + (checker === undefined || checker.isVisible(currentBeatHint.nextBeat.beat)) + ) { const next = currentBeatHint.nextBeat!; // fill next in chain - this._fillNextBeat(next, trackLookup); + this._fillNextBeat(next, checker); return next; } @@ -225,7 +257,10 @@ export class MidiTickLookup { return null; } - private _fillNextBeatMultiBarRest(current: MidiTickLookupFindBeatResult, trackLookup: Set) { + private _fillNextBeatMultiBarRest( + current: MidiTickLookupFindBeatResult, + checker: IBeatVisibilityChecker + ) { const group = this.multiBarRestInfo!.get(current.masterBar.masterBar.index)!; // this is a bit sensitive. we assume that the sequence of multi-rest bars and the @@ -242,7 +277,7 @@ export class MidiTickLookup { // one more following -> use start of next if (endMasterBar.nextMasterBar) { current.nextBeat = this._firstBeatInMasterBar( - trackLookup, + checker, endMasterBar.nextMasterBar!, endMasterBar.nextMasterBar!.start, true @@ -284,25 +319,31 @@ export class MidiTickLookup { current.calculateDuration(); } - private _fillNextBeat(current: MidiTickLookupFindBeatResult, trackLookup: Set) { + private _fillNextBeat( + current: MidiTickLookupFindBeatResult, + checker: IBeatVisibilityChecker + ) { // on multibar rests take the duration until the end. if (this._isMultiBarRestResult(current)) { - this._fillNextBeatMultiBarRest(current, trackLookup); + this._fillNextBeatMultiBarRest(current, checker); } else { - this._fillNextBeatDefault(current, trackLookup); + this._fillNextBeatDefault(current, checker); } } - private _fillNextBeatDefault(current: MidiTickLookupFindBeatResult, trackLookup: Set) { + private _fillNextBeatDefault( + current: MidiTickLookupFindBeatResult, + checker: IBeatVisibilityChecker + ) { current.nextBeat = this._findBeatInMasterBar( current.masterBar, current.beatLookup.nextBeat, current.end, - trackLookup, + checker, true ); if (current.nextBeat == null) { - current.nextBeat = this._findBeatSlow(trackLookup, current, current.end, true); + current.nextBeat = this._findBeatSlow(checker, current, current.end, true); } // if we have the next beat take the difference between the times as duration @@ -344,7 +385,7 @@ export class MidiTickLookup { } private _findBeatSlow( - trackLookup: Set, + checker: IBeatVisibilityChecker, currentBeatHint: MidiTickLookupFindBeatResult | null, tick: number, isNextSearch: boolean @@ -376,11 +417,11 @@ export class MidiTickLookup { return null; } - return this._firstBeatInMasterBar(trackLookup, masterBar, tick, isNextSearch); + return this._firstBeatInMasterBar(checker, masterBar, tick, isNextSearch); } private _firstBeatInMasterBar( - trackLookup: Set, + checker: IBeatVisibilityChecker, startMasterBar: MasterBarTickLookup, tick: number, isNextSearch: boolean @@ -389,7 +430,13 @@ export class MidiTickLookup { // scan through beats and find first one which has a beat visible while (masterBar) { if (masterBar.firstBeat) { - const beat = this._findBeatInMasterBar(masterBar, masterBar.firstBeat, tick, trackLookup, isNextSearch); + const beat = this._findBeatInMasterBar( + masterBar, + masterBar.firstBeat, + tick, + checker, + isNextSearch + ); if (beat) { return beat; @@ -414,7 +461,7 @@ export class MidiTickLookup { masterBar: MasterBarTickLookup, currentStartLookup: BeatTickLookup | null, tick: number, - visibleTracks: Set, + checker: IBeatVisibilityChecker, isNextSearch: boolean ): MidiTickLookupFindBeatResult | null { if (!currentStartLookup) { @@ -434,7 +481,7 @@ export class MidiTickLookup { relativeTick < currentStartLookup.end ) { startBeatLookup = currentStartLookup; - startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); + startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker); // found the matching beat lookup but none of the beats are visible // in this case scan further to the next lookup which has any visible beat @@ -443,7 +490,7 @@ export class MidiTickLookup { let currentMasterBar: MasterBarTickLookup | null = masterBar; while (currentMasterBar != null && startBeat == null) { while (currentStartLookup != null) { - startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); + startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker); if (startBeat) { startBeatLookup = currentStartLookup; @@ -463,7 +510,7 @@ export class MidiTickLookup { let currentMasterBar: MasterBarTickLookup | null = masterBar; while (currentMasterBar != null && startBeat == null) { while (currentStartLookup != null) { - startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); + startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker); if (startBeat) { startBeatLookup = currentStartLookup; @@ -492,7 +539,7 @@ export class MidiTickLookup { return null; } - const result = this._createResult(masterBar, startBeatLookup!, startBeat, isNextSearch, visibleTracks); + const result = this._createResult(masterBar, startBeatLookup!, startBeat, isNextSearch, checker); return result; } @@ -502,7 +549,7 @@ export class MidiTickLookup { beatLookup: BeatTickLookup, beat: Beat, isNextSearch: boolean, - visibleTracks: Set + checker: IBeatVisibilityChecker ) { const result = new MidiTickLookupFindBeatResult(masterBar); @@ -513,7 +560,7 @@ export class MidiTickLookup { if (!isNextSearch) { // the next beat filling will adjust this result with the respective durations - this._fillNextBeat(result, visibleTracks); + this._fillNextBeat(result, checker); } // if we do not search for the next beat, we need to still stretch multi-bar-rest // otherwise the fast path will not work correctly @@ -553,6 +600,9 @@ export class MidiTickLookup { } private _findMasterBar(tick: number): MasterBarTickLookup | null { + if (tick <= 0 && this.masterBars.length > 0) { + return this.masterBars[0]; + } const bars: MasterBarTickLookup[] = this.masterBars; let bottom: number = 0; let top: number = bars.length - 1; diff --git a/packages/alphatab/src/midi/_barrel.ts b/packages/alphatab/src/midi/_barrel.ts index bd2108fe9..690ec0331 100644 --- a/packages/alphatab/src/midi/_barrel.ts +++ b/packages/alphatab/src/midi/_barrel.ts @@ -1,41 +1,45 @@ -export { BeatTickLookup, BeatTickLookupItem } from '@coderline/alphatab/midi/BeatTickLookup'; -export { MasterBarTickLookup, MasterBarTickLookupTempoChange } from '@coderline/alphatab/midi/MasterBarTickLookup'; +export { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler'; export { - MidiTickLookup, - MidiTickLookupFindBeatResult, - MidiTickLookupFindBeatResultCursorMode -} from '@coderline/alphatab/midi/MidiTickLookup'; -export { MidiFile, MidiFileFormat, MidiTrack } from '@coderline/alphatab/midi/MidiFile'; + BeatTickLookup, + BeatTickLookupItem, + type IBeatVisibilityChecker +} from '@coderline/alphatab/midi/BeatTickLookup'; export { ControllerType } from '@coderline/alphatab/midi/ControllerType'; export { - MidiEvent, - MidiEventType, - TimeSignatureEvent, - AlphaTabRestEvent, - AlphaTabMetronomeEvent, - NoteEvent, - NoteOnEvent, - NoteOffEvent, - ControlChangeEvent, - ProgramChangeEvent, - TempoChangeEvent, - PitchBendEvent, - NoteBendEvent, - EndOfTrackEvent, - AlphaTabSysExEvent -} from '@coderline/alphatab/midi/MidiEvent'; -export { + AlphaTabSystemExclusiveEvents, DeprecatedMidiEvent, - MetaEventType, - MetaEvent, MetaDataEvent, + MetaEvent, + MetaEventType, MetaNumberEvent, Midi20PerNotePitchBendEvent, - SystemCommonType, SystemCommonEvent, - AlphaTabSystemExclusiveEvents, + SystemCommonType, SystemExclusiveEvent } from '@coderline/alphatab/midi/DeprecatedEvents'; -export { MidiFileGenerator } from '@coderline/alphatab/midi/MidiFileGenerator'; -export { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler'; export type { IMidiFileHandler } from '@coderline/alphatab/midi/IMidiFileHandler'; +export { MasterBarTickLookup, MasterBarTickLookupTempoChange } from '@coderline/alphatab/midi/MasterBarTickLookup'; +export { + AlphaTabMetronomeEvent, + AlphaTabRestEvent, + AlphaTabSysExEvent, + ControlChangeEvent, + EndOfTrackEvent, + MidiEvent, + MidiEventType, + NoteBendEvent, + NoteEvent, + NoteOffEvent, + NoteOnEvent, + PitchBendEvent, + ProgramChangeEvent, + TempoChangeEvent, + TimeSignatureEvent +} from '@coderline/alphatab/midi/MidiEvent'; +export { MidiFile, MidiFileFormat, MidiTrack } from '@coderline/alphatab/midi/MidiFile'; +export { MidiFileGenerator } from '@coderline/alphatab/midi/MidiFileGenerator'; +export { + MidiTickLookup, + MidiTickLookupFindBeatResult, + MidiTickLookupFindBeatResultCursorMode +} from '@coderline/alphatab/midi/MidiTickLookup'; diff --git a/packages/alphatab/src/model/Bar.ts b/packages/alphatab/src/model/Bar.ts index 4da5dc818..32c85e3cd 100644 --- a/packages/alphatab/src/model/Bar.ts +++ b/packages/alphatab/src/model/Bar.ts @@ -8,6 +8,8 @@ import type { Settings } from '@coderline/alphatab/Settings'; import { ElementStyle } from '@coderline/alphatab/model/ElementStyle'; import { KeySignature } from '@coderline/alphatab/model/KeySignature'; import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; +import type { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; +import { Duration } from '@coderline/alphatab/model/Duration'; /** * The different pedal marker types. @@ -396,6 +398,19 @@ export class Bar { */ public keySignatureType: KeySignatureType = KeySignatureType.Major; + /** + * How bar numbers should be displayed. + * If specified, overrides the value from the stylesheet on score level. + */ + public barNumberDisplay?: BarNumberDisplay; + + /** + * The shortest duration contained across beats in this bar. + * @internal + * @json_ignore + */ + public shortestDuration: Duration = Duration.DoubleWhole; + /** * The bar line to draw on the left side of the bar with an "automatic" type resolved to the actual one. * @param isFirstOfSystem Whether the bar is the first one in the system. @@ -469,12 +484,16 @@ export class Bar { this._filledVoices.add(0); this._isEmpty = true; this._isRestOnly = true; + this.shortestDuration = Duration.DoubleWhole; for (let i: number = 0, j: number = this.voices.length; i < j; i++) { const voice: Voice = this.voices[i]; voice.finish(settings, sharedDataBag); if (!voice.isEmpty) { this._isEmpty = false; this._filledVoices.add(i); + if (voice.shortestDuration > this.shortestDuration) { + this.shortestDuration = voice.shortestDuration; + } } if (!voice.isRestOnly) { @@ -490,7 +509,7 @@ export class Bar { if (this.previousBar && this.previousBar.sustainPedals.length > 0) { previousMarker = this.previousBar.sustainPedals[this.previousBar.sustainPedals.length - 1]; - if(previousMarker.pedalType === SustainPedalMarkerType.Up) { + if (previousMarker.pedalType === SustainPedalMarkerType.Up) { previousMarker = null; } } diff --git a/packages/alphatab/src/model/Beat.ts b/packages/alphatab/src/model/Beat.ts index 1e382739c..42fdc428f 100644 --- a/packages/alphatab/src/model/Beat.ts +++ b/packages/alphatab/src/model/Beat.ts @@ -28,6 +28,7 @@ import { WahPedal } from '@coderline/alphatab/model/WahPedal'; import { BarreShape } from '@coderline/alphatab/model/BarreShape'; import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; import { ElementStyle } from '@coderline/alphatab/model/ElementStyle'; +import { TremoloPickingEffect } from '@coderline/alphatab/model/TremoloPickingEffect'; /** * Lists the different modes on how beaming for a beat should be done. @@ -534,14 +535,64 @@ export class Beat { */ public pickStroke: PickStroke = PickStroke.None; + /** + * Whether this beat has a tremolo picking effect. + */ public get isTremolo(): boolean { - return !!this.tremoloSpeed; + return this.tremoloPicking !== undefined; } /** - * Gets or sets the speed of the tremolo effect. + * The tremolo picking effect. + */ + public tremoloPicking?: TremoloPickingEffect; + + /** + * The speed of the tremolo. + * @deprecated Set {@link tremoloPicking} instead. */ - public tremoloSpeed: Duration | null = null; + public get tremoloSpeed(): Duration | null { + const tremolo = this.tremoloPicking; + if (tremolo) { + return tremolo.getDuration(this.duration); + } + return null; + } + + /** + * The speed of the tremolo. + * @deprecated Set {@link tremoloPicking} instead. + */ + public set tremoloSpeed(value: Duration | null) { + if (value === null) { + this.tremoloPicking = undefined; + return; + } + + let effect = this.tremoloPicking; + if (effect === undefined) { + effect = new TremoloPickingEffect(); + this.tremoloPicking = effect; + } + + switch (value) { + case Duration.Eighth: + effect.marks = 1; + break; + case Duration.Sixteenth: + effect.marks = 2; + break; + case Duration.ThirtySecond: + effect.marks = 3; + break; + case Duration.SixtyFourth: + effect.marks = 4; + break; + case Duration.OneHundredTwentyEighth: + effect.marks = 5; + break; + } + } /** * Gets or sets whether a crescendo/decrescendo is applied on this beat. @@ -882,6 +933,13 @@ export class Beat { this.brushDuration = 0; } + const tremolo = this.tremoloPicking; + if (tremolo !== undefined) { + if (tremolo.marks < TremoloPickingEffect.minMarks || tremolo.marks > TremoloPickingEffect.maxMarks) { + this.tremoloPicking = undefined; + } + } + const displayMode: NotationMode = !settings ? NotationMode.GuitarPro : settings.notation.notationMode; let isGradual: boolean = this.text === 'grad' || this.text === 'grad.'; if (isGradual && displayMode === NotationMode.SongBook) { @@ -1037,6 +1095,34 @@ export class Beat { points.splice(1, 1); } } + } else if (points!.length === 3) { + const origin: BendPoint = points[0]; + const middle: BendPoint = points[1]; + const destination: BendPoint = points[2]; + // constant decrease or increase + if ( + (origin.value < middle.value && middle.value < destination.value) || + (origin.value > middle.value && middle.value > destination.value) + ) { + if (origin.value !== 0 && !this.isContinuedWhammy) { + this.whammyBarType = WhammyType.PrediveDive; + } else { + this.whammyBarType = WhammyType.Dive; + } + points.splice(1, 1); + } else if ( + (origin.value > middle.value && middle.value < destination.value) || + (origin.value < middle.value && middle.value > destination.value) + ) { + this.whammyBarType = WhammyType.Dip; + } else if (origin.value === middle.value && middle.value === destination.value) { + if (origin.value !== 0 && !this.isContinuedWhammy) { + this.whammyBarType = WhammyType.Predive; + } else { + this.whammyBarType = WhammyType.Hold; + } + points.splice(1, 1); + } } else if (points!.length === 2) { const origin: BendPoint = points[0]; const destination: BendPoint = points[1]; @@ -1145,7 +1231,6 @@ export class Beat { return this.noteStringLookup.has(noteString); } - // TODO: can be likely eliminated public getNoteWithRealValue(noteRealValue: number): Note | null { if (this.noteValueLookup.has(noteRealValue)) { return this.noteValueLookup.get(noteRealValue)!; diff --git a/packages/alphatab/src/model/Chord.ts b/packages/alphatab/src/model/Chord.ts index 4fc39d51f..801c3c0da 100644 --- a/packages/alphatab/src/model/Chord.ts +++ b/packages/alphatab/src/model/Chord.ts @@ -1,8 +1,5 @@ import type { Staff } from '@coderline/alphatab/model/Staff'; -// TODO: rework model to specify for each finger -// on which frets they are placed. - /** * A chord definition. * @json diff --git a/packages/alphatab/src/model/ElementStyle.ts b/packages/alphatab/src/model/ElementStyle.ts index 50c2c09c4..ef3e44312 100644 --- a/packages/alphatab/src/model/ElementStyle.ts +++ b/packages/alphatab/src/model/ElementStyle.ts @@ -12,6 +12,4 @@ export class ElementStyle { * even if some "higher level" element changes colors. */ public colors: Map = new Map(); - - // TODO: replace NotationSettings.elements by adding a visibility here? } diff --git a/packages/alphatab/src/model/InstrumentArticulation.ts b/packages/alphatab/src/model/InstrumentArticulation.ts index 47d1415aa..5250dacf2 100644 --- a/packages/alphatab/src/model/InstrumentArticulation.ts +++ b/packages/alphatab/src/model/InstrumentArticulation.ts @@ -1,8 +1,6 @@ - import { Duration } from '@coderline/alphatab/model/Duration'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; - /** * This public enum lists all base line modes * @public @@ -33,10 +31,25 @@ export enum TechniqueSymbolPlacement { * @public */ export class InstrumentArticulation { + /** + * An internal ID to identify this articulation for purposes like + * mapping during exports.The exact meaning of the ID is not defined and dependes on the + * importer source. + */ + public id: number = 0; + + /** + * A unique id for this articulation. + */ + public get uniqueId() { + return `${this.elementType}.${this.id}`; + } + /** * Gets or sets the type of the element for which this articulation is for. */ public elementType: string; + /** * The line the note head should be shown for standard notation. * @@ -80,8 +93,10 @@ export class InstrumentArticulation { noteHeadHalf: MusicFontSymbol = MusicFontSymbol.None, noteHeadWhole: MusicFontSymbol = MusicFontSymbol.None, techniqueSymbol: MusicFontSymbol = MusicFontSymbol.None, - techniqueSymbolPlacement: TechniqueSymbolPlacement = TechniqueSymbolPlacement.Inside + techniqueSymbolPlacement: TechniqueSymbolPlacement = TechniqueSymbolPlacement.Inside, + id: number = 0 ) { + this.id = id; this.elementType = elementType; this.outputMidiNumber = outputMidiNumber; this.staffLine = staffLine; @@ -92,6 +107,34 @@ export class InstrumentArticulation { this.techniqueSymbolPlacement = techniqueSymbolPlacement; } + // avoiding breaking change + /** + * @internal + */ + public static create( + id: number = 0, + elementType: string = '', + staffLine: number = 0, + outputMidiNumber: number = 0, + noteHeadDefault: MusicFontSymbol = MusicFontSymbol.None, + noteHeadHalf: MusicFontSymbol = MusicFontSymbol.None, + noteHeadWhole: MusicFontSymbol = MusicFontSymbol.None, + techniqueSymbol: MusicFontSymbol = MusicFontSymbol.None, + techniqueSymbolPlacement: TechniqueSymbolPlacement = TechniqueSymbolPlacement.Inside + ) { + return new InstrumentArticulation( + elementType, + staffLine, + outputMidiNumber, + noteHeadDefault, + noteHeadHalf, + noteHeadWhole, + techniqueSymbol, + techniqueSymbolPlacement, + id + ); + } + public getSymbol(duration: Duration): MusicFontSymbol { switch (duration) { case Duration.Whole: diff --git a/packages/alphatab/src/model/JsonConverter.ts b/packages/alphatab/src/model/JsonConverter.ts index a23e9853c..611961a22 100644 --- a/packages/alphatab/src/model/JsonConverter.ts +++ b/packages/alphatab/src/model/JsonConverter.ts @@ -1,3 +1,8 @@ +import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; +import { ScoreSerializer } from '@coderline/alphatab/generated/model/ScoreSerializer'; +import { SettingsSerializer } from '@coderline/alphatab/generated/SettingsSerializer'; +import { JsonHelper } from '@coderline/alphatab/io/JsonHelper'; +import type { ControllerType } from '@coderline/alphatab/midi/ControllerType'; import { AlphaTabMetronomeEvent, AlphaTabRestEvent, @@ -17,11 +22,6 @@ import { import { MidiFile, MidiTrack } from '@coderline/alphatab/midi/MidiFile'; import { Score } from '@coderline/alphatab/model/Score'; import { Settings } from '@coderline/alphatab/Settings'; -import { ScoreSerializer } from '@coderline/alphatab/generated/model/ScoreSerializer'; -import { SettingsSerializer } from '@coderline/alphatab/generated/SettingsSerializer'; -import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; -import type { ControllerType } from '@coderline/alphatab/midi/ControllerType'; -import { JsonHelper } from '@coderline/alphatab/io/JsonHelper'; /** * This class can convert a full {@link Score} instance to a simple JavaScript object and back for further @@ -144,6 +144,9 @@ export class JsonConverter { JsonHelper.forEach(jsObject, (v, k) => { switch (k) { + case 'tickShift': + midi2.tickShift = v as number; + break; case 'division': midi2.division = v as number; break; @@ -271,6 +274,7 @@ export class JsonConverter { public static midiFileToJsObject(midi: MidiFile): Map { const o = new Map(); o.set('division', midi.division); + o.set('tickShift', midi.tickShift); const tracks: Map[] = []; for (const track of midi.tracks) { diff --git a/packages/alphatab/src/model/MasterBar.ts b/packages/alphatab/src/model/MasterBar.ts index a3470b107..ed1982268 100644 --- a/packages/alphatab/src/model/MasterBar.ts +++ b/packages/alphatab/src/model/MasterBar.ts @@ -1,15 +1,146 @@ import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; import type { Automation } from '@coderline/alphatab/model/Automation'; +import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Beat } from '@coderline/alphatab/model/Beat'; +import type { Direction } from '@coderline/alphatab/model/Direction'; +import { Duration } from '@coderline/alphatab/model/Duration'; import type { Fermata } from '@coderline/alphatab/model/Fermata'; -import type { Bar } from '@coderline/alphatab/model/Bar'; import type { KeySignature } from '@coderline/alphatab/model/KeySignature'; import type { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; import type { RepeatGroup } from '@coderline/alphatab/model/RepeatGroup'; import type { Score } from '@coderline/alphatab/model/Score'; import type { Section } from '@coderline/alphatab/model/Section'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -import type { Direction } from '@coderline/alphatab/model/Direction'; + +/** + * Defines the custom beaming rules which define how beats are beamed together or split apart + * during the automatic beaming when displayed. + * @json + * @json_strict + * @public + * + * @remarks + * The beaming logic works like this: + * + * The time axis of the bar is sliced into even chunks. The chunk-size is defined by the respective group definition. + * Within these chunks groups can then be placed spanning 1 or more chunks. + * + * If beats start within the same "group" they are beamed together. + */ +export class BeamingRules { + private _singleGroupKey?: Duration; + + /** + * The the group for a given "longest duration" within the bar. + * @remarks + * The map key is the duration to which the bar will be sliced into. + * The map value defines the "groups" placed within the sliced. + */ + public groups = new Map(); + + /** + * @internal + * @json_ignore + */ + public uniqueId: string = ''; + + /** + * @internal + * @json_ignore + */ + public timeSignatureNumerator: number = 0; + /** + * @internal + * @json_ignore + */ + public timeSignatureDenominator: number = 0; + + /** + * @internal + */ + public static createSimple( + timeSignatureNumerator: number, + timeSignatureDenominator: number, + duration: Duration, + groups: number[] + ) { + const r = new BeamingRules(); + r.timeSignatureNumerator = timeSignatureNumerator; + r.timeSignatureDenominator = timeSignatureDenominator; + r.groups.set(duration, groups); + r.finish(); + return r; + } + + /** + * @internal + */ + public findRule(shortestDuration: Duration): [Duration, number[]] { + // fast path: one rule -> take it + const singleGroupKey = this._singleGroupKey; + if (singleGroupKey) { + return [singleGroupKey, this.groups.get(singleGroupKey)!]; + } + + if (shortestDuration < Duration.Quarter) { + return [shortestDuration, []]; + } + + // first search shorter + let durationValue = shortestDuration as number; + do { + const duration = durationValue as Duration; + if (this.groups.has(duration)) { + return [duration, this.groups.get(duration)!]; + } + durationValue = durationValue * 2; + } while (durationValue <= (Duration.TwoHundredFiftySixth as number)); + + // then longer + durationValue = (shortestDuration as number) / 2; + do { + const duration = durationValue as Duration; + if (this.groups.has(duration)) { + return [duration, this.groups.get(duration)!]; + } + durationValue = durationValue / 2; + } while (durationValue > (Duration.Half as number)); + + return [shortestDuration, []]; + } + + /** + * @internal + */ + public finish() { + let uniqueId = `${this.timeSignatureNumerator}_${this.timeSignatureDenominator}`; + + for (const [k, v] of this.groups) { + uniqueId += `__${k}`; + + // trim of 0s at the end of the group + let lastZero = v.length; + for (let i = v.length - 1; i >= 0; i--) { + if (v[i] === 0) { + lastZero = i; + } else { + break; + } + } + + if (lastZero < v.length) { + v.splice(lastZero, v.length - lastZero); + } + + uniqueId += `_${v.join('_')}`; + + if (this.groups.size === 1) { + this._singleGroupKey = k; + } + } + this.uniqueId = uniqueId; + } +} /** * The MasterBar stores information about a bar which affects @@ -150,6 +281,18 @@ export class MasterBar { */ public timeSignatureCommon: boolean = false; + /** + * Defines the custom beaming rules which should be applied to this bar and all bars following. + */ + public beamingRules?: BeamingRules; + + /** + * The actual (custom) beaming rules to use for this bar if any were specified. + * @json_ignore + * @internal + */ + public actualBeamingRules?: BeamingRules; + /** * Gets or sets whether the bar indicates a free time playing. */ @@ -180,7 +323,7 @@ export class MasterBar { /** * Gets or sets all tempo automation for this bar. */ - public tempoAutomations: Automation[] = []; + public tempoAutomations: Automation[] = []; /** * The sync points for this master bar to synchronize the alphaTab time axis with the @@ -301,4 +444,34 @@ export class MasterBar { } this.syncPoints!.push(syncPoint); } + + public finish(sharedDataBag: Map) { + let beamingRules = this.beamingRules; + if (beamingRules) { + beamingRules.timeSignatureNumerator = this.timeSignatureNumerator; + beamingRules.timeSignatureDenominator = this.timeSignatureDenominator; + beamingRules.finish(); + } + + if (this.index > 0) { + this.start = this.previousMasterBar!.start + this.previousMasterBar!.calculateDuration(); + + // clear out equal rules to reduce memory consumption. + const previousRules = sharedDataBag.has('beamingRules') + ? (sharedDataBag.get('beamingRules')! as BeamingRules) + : undefined; + + if (previousRules && previousRules.uniqueId === beamingRules?.uniqueId) { + this.beamingRules = undefined; + beamingRules = previousRules; + } else if (!beamingRules) { + beamingRules = previousRules; + } + } + this.actualBeamingRules = beamingRules; + + if (this.beamingRules) { + sharedDataBag.set('beamingRules', beamingRules); + } + } } diff --git a/packages/alphatab/src/model/ModelUtils.ts b/packages/alphatab/src/model/ModelUtils.ts index 4b5291f26..37afff3ec 100644 --- a/packages/alphatab/src/model/ModelUtils.ts +++ b/packages/alphatab/src/model/ModelUtils.ts @@ -1,17 +1,16 @@ +import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; +import { Automation, AutomationType } from '@coderline/alphatab/model/Automation'; +import { Bar } from '@coderline/alphatab/model/Bar'; import { Beat } from '@coderline/alphatab/model/Beat'; -import type { Duration } from '@coderline/alphatab/model/Duration'; -import { HeaderFooterStyle, type Score, ScoreStyle, type ScoreSubElement } from '@coderline/alphatab/model/Score'; -import type { Settings } from '@coderline/alphatab/Settings'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import type { KeySignature } from '@coderline/alphatab/model/KeySignature'; import { MasterBar } from '@coderline/alphatab/model/MasterBar'; +import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; +import { HeaderFooterStyle, type Score, ScoreStyle, type ScoreSubElement } from '@coderline/alphatab/model/Score'; import type { Track } from '@coderline/alphatab/model/Track'; -import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; -import { Bar } from '@coderline/alphatab/model/Bar'; import { Voice } from '@coderline/alphatab/model/Voice'; -import { Automation, AutomationType } from '@coderline/alphatab/model/Automation'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import type { KeySignature } from '@coderline/alphatab/model/KeySignature'; -import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; +import type { Settings } from '@coderline/alphatab/Settings'; +import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; /** * @internal @@ -26,7 +25,6 @@ export class TuningParseResult { } } - /** * @internal */ @@ -45,13 +43,18 @@ export class TuningParseResultTone { * @internal */ export class ModelUtils { + private static readonly _durationIndices = ModelUtils._buildDurationIndices(); + + private static _buildDurationIndices() { + return new Map( + Object.values(Duration) + .filter((k: any) => typeof k === 'number') + .map(d => [d as number as Duration, (d as number) < 0 ? 0 : Math.log2(d as number) | 0]) + ); + } + public static getIndex(duration: Duration): number { - const index: number = 0; - const value: number = duration; - if (value < 0) { - return index; - } - return Math.log2(duration) | 0; + return ModelUtils._durationIndices.get(duration)!; } public static keySignatureIsFlat(ks: number): boolean { @@ -672,22 +675,6 @@ export class ModelUtils { masterBar.previousMasterBar!.nextMasterBar = null; } } - - private static _allMusicFontSymbols: MusicFontSymbol[] = []; - - /** - * Gets a list of all music font symbols used in alphaTab. - */ - public static getAllMusicFontSymbols(): MusicFontSymbol[] { - if (ModelUtils._allMusicFontSymbols.length === 0) { - ModelUtils._allMusicFontSymbols = Object.values(MusicFontSymbol) - .filter((k: any) => typeof k === 'number') - .map(v => v as number as MusicFontSymbol) as MusicFontSymbol[]; - } - - return ModelUtils._allMusicFontSymbols; - } - /** * Lists the display transpositions for some known midi instruments. * It is a common practice to transpose the standard notation for instruments like guitars. @@ -938,7 +925,6 @@ export class ModelUtils { return accidentalToSet; } - /** * @internal */ @@ -946,5 +932,36 @@ export class ModelUtils { return plain.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); } -} + public static minBoundingBox(a: number, b: number) { + if (Number.isNaN(a)) { + return b; + } else if (Number.isNaN(b)) { + return a; + } + return a < b ? a : b; + } + + public static maxBoundingBox(a: number, b: number) { + if (Number.isNaN(a)) { + return b; + } else if (Number.isNaN(b)) { + return a; + } + return a > b ? a : b; + } + public static getSystemLayout(score: Score, systemIndex: number, displayedTracks: Track[]) { + let defaultSystemsLayout: number; + let systemsLayout: number[]; + if (displayedTracks.length === 1) { + defaultSystemsLayout = displayedTracks[0].defaultSystemsLayout; + systemsLayout = displayedTracks[0].systemsLayout; + } else { + // multi track applies + defaultSystemsLayout = score.defaultSystemsLayout; + systemsLayout = score.systemsLayout; + } + + return systemIndex < systemsLayout.length ? systemsLayout[systemIndex] : defaultSystemsLayout; + } +} diff --git a/packages/alphatab/src/model/MusicFontSymbol.ts b/packages/alphatab/src/model/MusicFontSymbol.ts index 34789e15e..b836c651d 100644 --- a/packages/alphatab/src/model/MusicFontSymbol.ts +++ b/packages/alphatab/src/model/MusicFontSymbol.ts @@ -171,9 +171,13 @@ export enum MusicFontSymbol { TextTuplet3LongStem = 0xe202, TextTupletBracketEndLongStem = 0xe203, - Tremolo3 = 0xe222, - Tremolo2 = 0xe221, Tremolo1 = 0xe220, + Tremolo2 = 0xe221, + Tremolo3 = 0xe222, + Tremolo4 = 0xe223, + Tremolo5 = 0xe224, + + BuzzRoll = 0xe22A, Flag8thUp = 0xe240, Flag8thDown = 0xe241, @@ -346,3 +350,38 @@ export enum MusicFontSymbol { FingeringALower = 0xed1b, FingeringCLower = 0xed1c } + +/** + * @internal + */ +export class MusicFontSymbolLookup { + private static _allMusicFontSymbols: MusicFontSymbol[] = []; + private static readonly _blackNoteHeadGlyphs = new Set(); + + private static _initialize() { + const all = MusicFontSymbolLookup._allMusicFontSymbols; + if (all.length === 0) { + for (const v of Object.values(MusicFontSymbol).filter((k: any) => typeof k === 'number')) { + const symbol = v as number as MusicFontSymbol; + all.push(symbol); + const name = MusicFontSymbol[symbol].toLowerCase(); + if (name.endsWith('black')) { + MusicFontSymbolLookup._blackNoteHeadGlyphs.add(symbol); + } + } + } + } + + /** + * Gets a list of all music font symbols used in alphaTab. + */ + public static getAllMusicFontSymbols(): MusicFontSymbol[] { + MusicFontSymbolLookup._initialize(); + return MusicFontSymbolLookup._allMusicFontSymbols; + } + + public static isBlackNoteHead(glph: MusicFontSymbol): boolean { + MusicFontSymbolLookup._initialize(); + return MusicFontSymbolLookup._blackNoteHeadGlyphs.has(glph); + } +} diff --git a/packages/alphatab/src/model/Note.ts b/packages/alphatab/src/model/Note.ts index 2cf0ae3c8..1a55a49a6 100644 --- a/packages/alphatab/src/model/Note.ts +++ b/packages/alphatab/src/model/Note.ts @@ -1,6 +1,6 @@ import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; import type { Beat } from '@coderline/alphatab/model/Beat'; -import type { BendPoint } from '@coderline/alphatab/model/BendPoint'; +import { BendPoint } from '@coderline/alphatab/model/BendPoint'; import { BendStyle } from '@coderline/alphatab/model/BendStyle'; import { BendType } from '@coderline/alphatab/model/BendType'; import { Duration } from '@coderline/alphatab/model/Duration'; @@ -983,6 +983,43 @@ export class Note { } else { Logger.warning('Model', 'Unsupported bend type detected, fallback to custom', null); } + } else if (points!.length === 3) { + const origin: BendPoint = points[0]; + const middle: BendPoint = points[1]; + const destination: BendPoint = points[2]; + // bend higher? + if (destination.value > origin.value) { + if (middle.value > destination.value) { + this.bendType = BendType.BendRelease; + points.splice(1, 0, new BendPoint(middle.offset, middle.value)); + } else if (!this.isContinuedBend && origin.value > 0) { + this.bendType = BendType.PrebendBend; + points.splice(1, 1); + } else { + this.bendType = BendType.Bend; + points.splice(1, 1); + } + } else if (destination.value < origin.value) { + // origin must be > 0 otherwise it's no release, we cannot bend negative + if (this.isContinuedBend) { + this.bendType = BendType.Release; + points.splice(1, 1); + } else { + this.bendType = BendType.PrebendRelease; + points.splice(1, 1); + } + } else { + if (middle.value > origin.value) { + this.bendType = BendType.BendRelease; + points.splice(1, 0, new BendPoint(middle.offset, middle.value)); + } else if (origin.value > 0 && !this.isContinuedBend) { + this.bendType = BendType.Prebend; + points.splice(1, 1); + } else { + this.bendType = BendType.Hold; + points.splice(1, 1); + } + } } else if (points.length === 2) { const origin: BendPoint = points[0]; const destination: BendPoint = points[1]; diff --git a/packages/alphatab/src/model/PercussionMapper.ts b/packages/alphatab/src/model/PercussionMapper.ts index 6654fce0f..cbc6e0942 100644 --- a/packages/alphatab/src/model/PercussionMapper.ts +++ b/packages/alphatab/src/model/PercussionMapper.ts @@ -6,475 +6,341 @@ import type { Note } from '@coderline/alphatab/model/Note'; * @internal */ export class PercussionMapper { - private static _gp6ElementAndVariationToArticulation: number[][] = [ - // known GP6 elements and variations, analyzed from a GPX test file - // with all instruments inside manually aligned with the same names of articulations in GP7 - // [{articulation index}] // [{element number}] => {element name} ({variation[0]}, {variation[1]}, {variation[2]}) - [35, 35, 35], // [0] => Kick (hit, unused, unused) - [38, 91, 37], // [1] => Snare (hit, rim shot, side stick) - [99, 100, 99], // [2] => Cowbell low (hit, tip, unused) - [56, 100, 56], // [3] => Cowbell medium (hit, tip, unused) - [102, 103, 102], // [4] => Cowbell high (hit, tip, unused) - [43, 43, 43], // [5] => Tom very low (hit, unused, unused) - [45, 45, 45], // [6] => Tom low (hit, unused, unused) - [47, 47, 47], // [7] => Tom medium (hit, unused, unused) - [48, 48, 48], // [8] => Tom high (hit, unused, unused) - [50, 50, 50], // [9] => Tom very high (hit, unused, unused) - [42, 92, 46], // [10] => Hihat (closed, half, open) - [44, 44, 44], // [11] => Pedal hihat (hit, unused, unused) - [57, 98, 57], // [12] => Crash medium (hit, choke, unused) - [49, 97, 49], // [13] => Crash high (hit, choke, unused) - [55, 95, 55], // [14] => Splash (hit, choke, unused) - [51, 93, 127], // [15] => Ride (middle, edge, bell) - [52, 96, 52] // [16] => China (hit, choke, unused) - ]; - - public static articulationFromElementVariation(element: number, variation: number): number { - if (element < PercussionMapper._gp6ElementAndVariationToArticulation.length) { - if (variation >= PercussionMapper._gp6ElementAndVariationToArticulation.length) { - variation = 0; - } - return PercussionMapper._gp6ElementAndVariationToArticulation[element][variation]; - } - // unknown combination, should not happen, fallback to some default value (Snare hit) - return 38; - } + // To update the following generated code, use the GpExporterTest.percussion-articulations unit test + // which will generate the new code to copy for here. + // We could also use an NPM script for that but for now this is enough. - /* - * This map was generated using the following steps: - * 1. Make a new GP7 file with a drumkit track - * 2. Add one note for each midi value using the instrument panel - * 3. Load the file in alphaTab and set a breakpoint in the GP7 importer. - * 4. Use the following snipped in the console to generate the map initializer (fix enums manually): - * parser = new DOMParser(); - * xmlDoc = parser.parseFromString(xml, 'text/xml'); - * articulations = xmlDoc.getElementsByTagName('Articulation'); - * existingArticulations = new Map(); - * s = ''; - * for(let i = 0; i < articulations.length; i++) { - * const articulation = articulations[i]; - * let midi = articulation.getElementsByTagName('InputMidiNumbers'); - * if(midi.length === 1) { - * midi = midi[0].textContent; - * const elementType = articulation.parentElement.parentElement.getElementsByTagName('Type')[0].textContent; - * const outputMidiNumber = articulation.getElementsByTagName('OutputMidiNumber')[0].textContent; - * const staffLine = articulation.getElementsByTagName('StaffLine')[0].textContent; - * const techniqueSymbol = articulation.getElementsByTagName('TechniqueSymbol')[0].textContent; - * const techniquePlacement = articulation.getElementsByTagName('TechniquePlacement')[0].textContent; - * const noteHeads = articulation.getElementsByTagName('Noteheads')[0].textContent.split(' ').map(n=>n = 'MusicFontSymbol.' + n); - * if(!existingArticulations.has(midi)) { - * if(techniqueSymbol) { - * s += `['${elementType}', ${midi}, new InstrumentArticulation(${staffLine}, ${outputMidiNumber}, ${noteHeads[0]}, ${noteHeads[1]}, ${noteHeads[2]}, ${techniqueSymbol}, ${techniquePlacement})],\r\n`; - * } - * else { - * s += `['${elementType}', ${midi}, new InstrumentArticulation(${staffLine}, ${outputMidiNumber}, ${noteHeads[0]}, ${noteHeads[1]}, ${noteHeads[2]})],\r\n`; - * } - * existingArticulations.set(midi, true); - * } - * } - * } - * copy(s) - */ - public static instrumentArticulations: Map = new Map([ + // BEGIN generated articulations + public static instrumentArticulations: Map = new Map( [ - 38, - new InstrumentArticulation( - 'snare', + InstrumentArticulation.create( + 38, + 'Snare', 3, 38, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 37, - new InstrumentArticulation( - 'snare', + ), + InstrumentArticulation.create( + 37, + 'Snare', 3, 37, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 91, - new InstrumentArticulation( - 'snare', + ), + InstrumentArticulation.create( + 91, + 'Snare', 3, 38, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite - ) - ], - [ - 42, - new InstrumentArticulation( - 'hiHat', + ), + InstrumentArticulation.create( + 42, + 'Charley', -1, 42, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 92, - new InstrumentArticulation( - 'hiHat', + ), + InstrumentArticulation.create( + 92, + 'Charley', -1, 46, MusicFontSymbol.NoteheadCircleSlash, MusicFontSymbol.NoteheadCircleSlash, MusicFontSymbol.NoteheadCircleSlash - ) - ], - [ - 46, - new InstrumentArticulation( - 'hiHat', + ), + InstrumentArticulation.create( + 46, + 'Charley', -1, 46, MusicFontSymbol.NoteheadCircleX, MusicFontSymbol.NoteheadCircleX, MusicFontSymbol.NoteheadCircleX - ) - ], - [ - 44, - new InstrumentArticulation( - 'hiHat', + ), + InstrumentArticulation.create( + 44, + 'Charley', 9, 44, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 35, - new InstrumentArticulation( - 'kickDrum', + ), + InstrumentArticulation.create( + 35, + 'Acoustic Kick Drum', 8, 35, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 36, - new InstrumentArticulation( - 'kickDrum', + ), + InstrumentArticulation.create( + 36, + 'Kick Drum', 7, 36, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 50, - new InstrumentArticulation( - 'tom', + ), + InstrumentArticulation.create( + 50, + 'Tom Very High', 1, 50, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 48, - new InstrumentArticulation( - 'tom', + ), + InstrumentArticulation.create( + 48, + 'Tom High', 2, 48, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 47, - new InstrumentArticulation( - 'tom', + ), + InstrumentArticulation.create( + 47, + 'Tom Medium', 4, 47, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 45, - new InstrumentArticulation( - 'tom', + ), + InstrumentArticulation.create( + 45, + 'Tom Low', 5, 45, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 43, - new InstrumentArticulation( - 'tom', + ), + InstrumentArticulation.create( + 43, + 'Tom Very Low', 6, 43, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 93, - new InstrumentArticulation( - 'ride', + ), + InstrumentArticulation.create( + 93, + 'Ride', 0, 51, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.PictEdgeOfCymbal, - TechniqueSymbolPlacement.Below - ) - ], - [ - 51, - new InstrumentArticulation( - 'ride', + TechniqueSymbolPlacement.Above + ), + InstrumentArticulation.create( + 51, + 'Ride', 0, 51, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 53, - new InstrumentArticulation( - 'ride', + ), + InstrumentArticulation.create( + 53, + 'Ride', 0, 53, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite - ) - ], - [ - 94, - new InstrumentArticulation( - 'ride', + ), + InstrumentArticulation.create( + 94, + 'Ride', 0, 51, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.ArticStaccatoAbove, - TechniqueSymbolPlacement.Above - ) - ], - [ - 55, - new InstrumentArticulation( - 'splash', + TechniqueSymbolPlacement.Outside + ), + InstrumentArticulation.create( + 55, + 'Splash', -2, 55, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 95, - new InstrumentArticulation( - 'splash', + ), + InstrumentArticulation.create( + 95, + 'Splash', -2, 55, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.ArticStaccatoAbove, - TechniqueSymbolPlacement.Below - ) - ], - [ - 52, - new InstrumentArticulation( - 'china', + TechniqueSymbolPlacement.Outside + ), + InstrumentArticulation.create( + 52, + 'China', -3, 52, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat - ) - ], - [ - 96, - new InstrumentArticulation( - 'china', + ), + InstrumentArticulation.create( + 96, + 'China', -3, 52, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat - ) - ], - [ - 49, - new InstrumentArticulation( - 'crash', + ), + InstrumentArticulation.create( + 49, + 'Crash High', -2, 49, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX - ) - ], - [ - 97, - new InstrumentArticulation( - 'crash', + ), + InstrumentArticulation.create( + 97, + 'Crash High', -2, 49, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.ArticStaccatoAbove, - TechniqueSymbolPlacement.Below - ) - ], - [ - 57, - new InstrumentArticulation( - 'crash', + TechniqueSymbolPlacement.Outside + ), + InstrumentArticulation.create( + 57, + 'Crash Medium', -1, 57, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX - ) - ], - [ - 98, - new InstrumentArticulation( - 'crash', + ), + InstrumentArticulation.create( + 98, + 'Crash Medium', -1, 57, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.ArticStaccatoAbove, - TechniqueSymbolPlacement.Below - ) - ], - [ - 99, - new InstrumentArticulation( - 'cowbell', + TechniqueSymbolPlacement.Outside + ), + InstrumentArticulation.create( + 99, + 'Cowbell Low', 1, 56, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpHalf, MusicFontSymbol.NoteheadTriangleUpWhole - ) - ], - [ - 100, - new InstrumentArticulation( - 'cowbell', + ), + InstrumentArticulation.create( + 100, + 'Cowbell Low', 1, 56, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXHalf, MusicFontSymbol.NoteheadXWhole - ) - ], - [ - 56, - new InstrumentArticulation( - 'cowbell', + ), + InstrumentArticulation.create( + 56, + 'Cowbell Medium', 0, 56, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpHalf, MusicFontSymbol.NoteheadTriangleUpWhole - ) - ], - [ - 101, - new InstrumentArticulation( - 'cowbell', + ), + InstrumentArticulation.create( + 101, + 'Cowbell Medium', 0, 56, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXHalf, MusicFontSymbol.NoteheadXWhole - ) - ], - [ - 102, - new InstrumentArticulation( - 'cowbell', + ), + InstrumentArticulation.create( + 102, + 'Cowbell High', -1, 56, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpHalf, MusicFontSymbol.NoteheadTriangleUpWhole - ) - ], - [ - 103, - new InstrumentArticulation( - 'cowbell', + ), + InstrumentArticulation.create( + 103, + 'Cowbell High', -1, 56, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXHalf, MusicFontSymbol.NoteheadXWhole - ) - ], - [ - 77, - new InstrumentArticulation( - 'woodblock', + ), + InstrumentArticulation.create( + 77, + 'Woodblock Low', -9, 77, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack - ) - ], - [ - 76, - new InstrumentArticulation( - 'woodblock', + ), + InstrumentArticulation.create( + 76, + 'Woodblock High', -10, 76, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack - ) - ], - [ - 60, - new InstrumentArticulation( - 'bongo', + ), + InstrumentArticulation.create( + 60, + 'Bongo High', -4, 60, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 104, - new InstrumentArticulation( - 'bongo', + ), + InstrumentArticulation.create( + 104, + 'Bongo High', -5, 60, MusicFontSymbol.NoteheadBlack, @@ -482,34 +348,28 @@ export class PercussionMapper { MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside - ) - ], - [ - 105, - new InstrumentArticulation( - 'bongo', + ), + InstrumentArticulation.create( + 105, + 'Bongo High', -6, 60, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 61, - new InstrumentArticulation( - 'bongo', + ), + InstrumentArticulation.create( + 61, + 'Bongo Low', -7, 61, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 106, - new InstrumentArticulation( - 'bongo', + ), + InstrumentArticulation.create( + 106, + 'Bongo Low', -8, 61, MusicFontSymbol.NoteheadBlack, @@ -517,89 +377,73 @@ export class PercussionMapper { MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside - ) - ], - [ - 107, - new InstrumentArticulation( - 'bongo', + ), + InstrumentArticulation.create( + 107, + 'Bongo Low', -16, 61, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 66, - new InstrumentArticulation( - 'timbale', + ), + InstrumentArticulation.create( + 66, + 'Timbale Low', 10, 66, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 65, - new InstrumentArticulation( - 'timbale', + ), + InstrumentArticulation.create( + 65, + 'Timbale High', 9, 65, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 68, - new InstrumentArticulation( - 'agogo', + ), + InstrumentArticulation.create( + 68, + 'Agogo Low', 12, 68, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 67, - new InstrumentArticulation( - 'agogo', + ), + InstrumentArticulation.create( + 67, + 'Agogo High', 11, 67, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 64, - new InstrumentArticulation( - 'conga', + ), + InstrumentArticulation.create( + 64, + 'Conga Low', 17, 64, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 108, - new InstrumentArticulation( - 'conga', + ), + InstrumentArticulation.create( + 108, + 'Conga Low', 16, 64, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 109, - new InstrumentArticulation( - 'conga', + ), + InstrumentArticulation.create( + 109, + 'Conga Low', 15, 64, MusicFontSymbol.NoteheadBlack, @@ -607,34 +451,28 @@ export class PercussionMapper { MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside - ) - ], - [ - 63, - new InstrumentArticulation( - 'conga', + ), + InstrumentArticulation.create( + 63, + 'Conga High', 14, 63, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 110, - new InstrumentArticulation( - 'conga', + ), + InstrumentArticulation.create( + 110, + 'Conga High', 13, 63, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 62, - new InstrumentArticulation( - 'conga', + ), + InstrumentArticulation.create( + 62, + 'Conga High', 19, 62, MusicFontSymbol.NoteheadBlack, @@ -642,67 +480,55 @@ export class PercussionMapper { MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside - ) - ], - [ - 72, - new InstrumentArticulation( - 'whistle', + ), + InstrumentArticulation.create( + 72, + 'Whistle Low', -11, 72, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 71, - new InstrumentArticulation( - 'whistle', + ), + InstrumentArticulation.create( + 71, + 'Whistle High', -17, 71, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 73, - new InstrumentArticulation( - 'guiro', + ), + InstrumentArticulation.create( + 73, + 'Guiro', 38, 73, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 74, - new InstrumentArticulation( - 'guiro', + ), + InstrumentArticulation.create( + 74, + 'Guiro', 37, 74, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 86, - new InstrumentArticulation( - 'surdo', + ), + InstrumentArticulation.create( + 86, + 'Surdo', 36, 86, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 87, - new InstrumentArticulation( - 'surdo', + ), + InstrumentArticulation.create( + 87, + 'Surdo', 35, 87, MusicFontSymbol.NoteheadXBlack, @@ -710,104 +536,86 @@ export class PercussionMapper { MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside - ) - ], - [ - 54, - new InstrumentArticulation( - 'tambourine', + ), + InstrumentArticulation.create( + 54, + 'Tambourine', 3, 54, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack - ) - ], - [ - 111, - new InstrumentArticulation( - 'tambourine', + ), + InstrumentArticulation.create( + 111, + 'Tambourine', 2, 54, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.StringsUpBow, - TechniqueSymbolPlacement.Below - ) - ], - [ - 112, - new InstrumentArticulation( - 'tambourine', + TechniqueSymbolPlacement.Above + ), + InstrumentArticulation.create( + 112, + 'Tambourine', 1, 54, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.StringsDownBow, - TechniqueSymbolPlacement.Below - ) - ], - [ - 113, - new InstrumentArticulation( - 'tambourine', + TechniqueSymbolPlacement.Above + ), + InstrumentArticulation.create( + 113, + 'Tambourine', -7, 54, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 79, - new InstrumentArticulation( - 'cuica', + ), + InstrumentArticulation.create( + 79, + 'Cuica', 30, 79, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 78, - new InstrumentArticulation( - 'cuica', + ), + InstrumentArticulation.create( + 78, + 'Cuica', 29, 78, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 58, - new InstrumentArticulation( - 'vibraslap', + ), + InstrumentArticulation.create( + 58, + 'Vibraslap', 28, 58, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 81, - new InstrumentArticulation( - 'triangle', + ), + InstrumentArticulation.create( + 81, + 'Triangle', 27, 81, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 80, - new InstrumentArticulation( - 'triangle', + ), + InstrumentArticulation.create( + 80, + 'Triangle', 26, 80, MusicFontSymbol.NoteheadXBlack, @@ -815,461 +623,448 @@ export class PercussionMapper { MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside - ) - ], - [ - 114, - new InstrumentArticulation( - 'grancassa', + ), + InstrumentArticulation.create( + 114, + 'Grancassa', 25, 43, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 115, - new InstrumentArticulation( - 'piatti', + ), + InstrumentArticulation.create( + 115, + 'Piatti', 18, 49, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 116, - new InstrumentArticulation( - 'piatti', + ), + InstrumentArticulation.create( + 116, + 'Piatti', 24, 49, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 69, - new InstrumentArticulation( - 'cabasa', + ), + InstrumentArticulation.create( + 69, + 'Cabasa', 23, 69, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 117, - new InstrumentArticulation( - 'cabasa', + ), + InstrumentArticulation.create( + 117, + 'Cabasa', 22, 69, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, - TechniqueSymbolPlacement.Below - ) - ], - [ - 85, - new InstrumentArticulation( - 'castanets', + TechniqueSymbolPlacement.Outside + ), + InstrumentArticulation.create( + 85, + 'Castanets', 21, 85, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 75, - new InstrumentArticulation( - 'claves', + ), + InstrumentArticulation.create( + 75, + 'Claves', 20, 75, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 70, - new InstrumentArticulation( - 'maraca', + ), + InstrumentArticulation.create( + 70, + 'Left Maraca', -12, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 118, - new InstrumentArticulation( - 'maraca', + ), + InstrumentArticulation.create( + 118, + 'Left Maraca', -13, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, - TechniqueSymbolPlacement.Below - ) - ], - [ - 119, - new InstrumentArticulation( - 'maraca', + TechniqueSymbolPlacement.Outside + ), + InstrumentArticulation.create( + 119, + 'Right Maraca', -14, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 120, - new InstrumentArticulation( - 'maraca', + ), + InstrumentArticulation.create( + 120, + 'Right Maraca', -15, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, - TechniqueSymbolPlacement.Below - ) - ], - [ - 82, - new InstrumentArticulation( - 'shaker', + TechniqueSymbolPlacement.Outside + ), + InstrumentArticulation.create( + 82, + 'Shaker', -23, - 54, + 82, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 122, - new InstrumentArticulation( - 'shaker', + ), + InstrumentArticulation.create( + 122, + 'Shaker', -24, - 54, + 82, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, - TechniqueSymbolPlacement.Below - ) - ], - [ - 84, - new InstrumentArticulation( - 'bellTree', + TechniqueSymbolPlacement.Outside + ), + InstrumentArticulation.create( + 84, + 'Bell Tree', -18, 53, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 123, - new InstrumentArticulation( - 'bellTree', + ), + InstrumentArticulation.create( + 123, + 'Bell Tree', -19, 53, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, - TechniqueSymbolPlacement.Below - ) - ], - [ - 83, - new InstrumentArticulation( - 'jingleBell', + TechniqueSymbolPlacement.Outside + ), + InstrumentArticulation.create( + 83, + 'Jingle Bell', -20, 53, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 124, - new InstrumentArticulation( - 'unpitched', + ), + InstrumentArticulation.create( + 83, + 'Tinkle Bell', + -20, + 53, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ), + InstrumentArticulation.create( + 124, + 'Golpe', -21, 62, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.GuitarGolpe, - TechniqueSymbolPlacement.Above - ) - ], - [ - 125, - new InstrumentArticulation( - 'unpitched', + TechniqueSymbolPlacement.Below + ), + InstrumentArticulation.create( + 125, + 'Golpe', -22, 62, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.GuitarGolpe, - TechniqueSymbolPlacement.Below - ) - ], - [ - 39, - new InstrumentArticulation( - 'handClap', + TechniqueSymbolPlacement.Above + ), + InstrumentArticulation.create( + 39, + 'Hand Clap', 3, 39, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 40, - new InstrumentArticulation( - 'snare', + ), + InstrumentArticulation.create( + 40, + 'Electric Snare', 3, 40, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 31, - new InstrumentArticulation( - 'snare', + ), + InstrumentArticulation.create( + 31, + 'Sticks', 3, 40, MusicFontSymbol.NoteheadSlashedBlack2, MusicFontSymbol.NoteheadSlashedBlack2, MusicFontSymbol.NoteheadSlashedBlack2 - ) - ], - [ - 41, - new InstrumentArticulation( - 'tom', + ), + InstrumentArticulation.create( + 41, + 'Very Low Floor Tom', 5, 41, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole - ) - ], - [ - 59, - new InstrumentArticulation( - 'ride', + ), + InstrumentArticulation.create( + 59, + 'Ride Cymbal 2', 2, 59, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.PictEdgeOfCymbal, - TechniqueSymbolPlacement.Below - ) - ], - [ - 126, - new InstrumentArticulation( - 'ride', + TechniqueSymbolPlacement.Above + ), + InstrumentArticulation.create( + 126, + 'Ride Cymbal 2', 2, 59, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 127, - new InstrumentArticulation( - 'ride', + ), + InstrumentArticulation.create( + 127, + 'Ride Cymbal 2', 2, 59, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite - ) - ], - [ - 29, - new InstrumentArticulation( - 'ride', + ), + InstrumentArticulation.create( + 29, + 'Ride Cymbal 2', 2, 59, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.ArticStaccatoAbove, - TechniqueSymbolPlacement.Above - ) - ], - [ - 30, - new InstrumentArticulation( - 'crash', + TechniqueSymbolPlacement.Outside + ), + InstrumentArticulation.create( + 30, + 'Reverse Cymbal', -3, 49, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 33, - new InstrumentArticulation( - 'snare', + ), + InstrumentArticulation.create( + 33, + 'Metronome', 3, 37, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack - ) - ], - [ - 34, - new InstrumentArticulation( - 'snare', + ), + InstrumentArticulation.create( + 34, + 'Metronome', 3, 38, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadBlack ) - ] - ]); + ].map(articulation => [articulation.uniqueId, articulation]) + ); - // these are manually defined names/identifiers for the articulation list above. - // they are currently only used in the AlphaTex importer when using default articulations - // but they are kept here close to the source of the default aritculation list to maintain them together. - public static instrumentArticulationNames = new Map([ - ['Ride (choke)', 29], - ['Cymbal (hit)', 30], - ['Snare (side stick)', 31], - ['Snare (side stick) 2', 33], - ['Snare (hit)', 34], - ['Kick (hit)', 35], - ['Kick (hit) 2', 36], - ['Snare (side stick) 3', 37], - ['Snare (hit) 2', 38], - ['Hand Clap (hit)', 39], - ['Snare (hit) 3', 40], - ['Low Floor Tom (hit)', 41], - ['Hi-Hat (closed)', 42], - ['Very Low Tom (hit)', 43], - ['Pedal Hi-Hat (hit)', 44], - ['Low Tom (hit)', 45], - ['Hi-Hat (open)', 46], - ['Mid Tom (hit)', 47], - ['High Tom (hit)', 48], - ['Crash high (hit)', 49], - ['High Floor Tom (hit)', 50], - ['Ride (middle)', 51], - ['China (hit)', 52], - ['Ride (bell)', 53], - ['Tambourine (hit)', 54], - ['Splash (hit)', 55], - ['Cowbell medium (hit)', 56], - ['Crash medium (hit)', 57], - ['Vibraslap (hit)', 58], - ['Ride (edge)', 59], - ['Hand (hit)', 60], - ['Hand (hit)', 61], - ['Bongo high (hit)', 60], - ['Bongo low (hit)', 61], - ['Conga high (mute)', 62], - ['Conga high (hit)', 63], - ['Conga low (hit)', 64], - ['Timbale high (hit)', 65], - ['Timbale low (hit)', 66], - ['Agogo high (hit)', 67], - ['Agogo tow (hit)', 68], - ['Cabasa (hit)', 69], - ['Left Maraca (hit)', 70], - ['Whistle high (hit)', 71], - ['Whistle low (hit)', 72], - ['Guiro (hit)', 73], - ['Guiro (scrap-return)', 74], - ['Claves (hit)', 75], - ['Woodblock high (hit)', 76], - ['Woodblock low (hit)', 77], - ['Cuica (mute)', 78], - ['Cuica (open)', 79], - ['Triangle (rnute)', 80], - ['Triangle (hit)', 81], - ['Shaker (hit)', 82], - ['Tinkle Bell (hat)', 83], - ['Jingle Bell (hit)', 83], - ['Bell Tree (hit)', 84], - ['Castanets (hit)', 85], - ['Surdo (hit)', 86], - ['Surdo (mute)', 87], - ['Snare (rim shot)', 91], - ['Hi-Hat (half)', 92], - ['Ride (edge) 2', 93], - ['Ride (choke) 2', 94], - ['Splash (choke)', 95], - ['China (choke)', 96], - ['Crash high (choke)', 97], - ['Crash medium (choke)', 98], - ['Cowbell low (hit)', 99], - ['Cowbell low (tip)', 100], - ['Cowbell medium (tip)', 101], - ['Cowbell high (hit)', 102], - ['Cowbell high (tip)', 103], - ['Hand (mute)', 104], - ['Hand (slap)', 105], - ['Hand (mute) 2', 106], - ['Hand (slap) 2', 107], - ['Conga low (slap)', 108], - ['Conga low (mute)', 109], - ['Conga high (slap)', 110], - ['Tambourine (return)', 111], - ['Tambourine (roll)', 112], - ['Tambourine (hand)', 113], - ['Grancassa (hit)', 114], - ['Piatti (hat)', 115], - ['Piatti (hand)', 116], - ['Cabasa (return)', 117], - ['Left Maraca (return)', 118], - ['Right Maraca (hit)', 119], - ['Right Maraca (return)', 120], - ['Shaker (return)', 122], - ['Bell Tee (return)', 123], - ['Golpe (thumb)', 124], - ['Golpe (finger)', 125], - ['Ride (middle) 2', 126], - ['Ride (bell) 2', 127] + private static _instrumentArticulationNames = new Map([ + ['Snare (hit)', 'Snare.38'], + ['Snare (side stick)', 'Snare.37'], + ['Snare (rim shot)', 'Snare.91'], + ['Hi-Hat (closed)', 'Charley.42'], + ['Hi-Hat (half)', 'Charley.92'], + ['Hi-Hat (open)', 'Charley.46'], + ['Pedal Hi-Hat (hit)', 'Charley.44'], + ['Kick (hit)', 'Acoustic Kick Drum.35'], + ['Kick (hit) 2', 'Kick Drum.36'], + ['High Floor Tom (hit)', 'Tom Very High.50'], + ['High Tom (hit)', 'Tom High.48'], + ['Mid Tom (hit)', 'Tom Medium.47'], + ['Low Tom (hit)', 'Tom Low.45'], + ['Very Low Tom (hit)', 'Tom Very Low.43'], + ['Ride (edge)', 'Ride.93'], + ['Ride (middle)', 'Ride.51'], + ['Ride (bell)', 'Ride.53'], + ['Ride (choke)', 'Ride.94'], + ['Splash (hit)', 'Splash.55'], + ['Splash (choke)', 'Splash.95'], + ['China (hit)', 'China.52'], + ['China (choke)', 'China.96'], + ['Crash high (hit)', 'Crash High.49'], + ['Crash high (choke)', 'Crash High.97'], + ['Crash medium (hit)', 'Crash Medium.57'], + ['Crash medium (choke)', 'Crash Medium.98'], + ['Cowbell low (hit)', 'Cowbell Low.99'], + ['Cowbell low (tip)', 'Cowbell Low.100'], + ['Cowbell medium (hit)', 'Cowbell Medium.56'], + ['Cowbell medium (tip)', 'Cowbell Medium.101'], + ['Cowbell high (hit)', 'Cowbell High.102'], + ['Cowbell high (tip)', 'Cowbell High.103'], + ['Woodblock low (hit)', 'Woodblock Low.77'], + ['Woodblock high (hit)', 'Woodblock High.76'], + ['Bongo High (hit)', 'Bongo High.60'], + ['Bongo High (mute)', 'Bongo High.104'], + ['Bongo High (slap)', 'Bongo High.105'], + ['Bongo Low (hit)', 'Bongo Low.61'], + ['Bongo Low (mute)', 'Bongo Low.106'], + ['Bongo Low (slap)', 'Bongo Low.107'], + ['Timbale low (hit)', 'Timbale Low.66'], + ['Timbale high (hit)', 'Timbale High.65'], + ['Agogo low (hit)', 'Agogo Low.68'], + ['Agogo high (hit)', 'Agogo High.67'], + ['Conga low (hit)', 'Conga Low.64'], + ['Conga low (slap)', 'Conga Low.108'], + ['Conga low (mute)', 'Conga Low.109'], + ['Conga high (hit)', 'Conga High.63'], + ['Conga high (slap)', 'Conga High.110'], + ['Conga high (mute)', 'Conga High.62'], + ['Whistle low (hit)', 'Whistle Low.72'], + ['Whistle high (hit)', 'Whistle High.71'], + ['Guiro (hit)', 'Guiro.73'], + ['Guiro (scrap-return)', 'Guiro.74'], + ['Surdo (hit)', 'Surdo.86'], + ['Surdo (mute)', 'Surdo.87'], + ['Tambourine (hit)', 'Tambourine.54'], + ['Tambourine (return)', 'Tambourine.111'], + ['Tambourine (roll)', 'Tambourine.112'], + ['Tambourine (hand)', 'Tambourine.113'], + ['Cuica (open)', 'Cuica.79'], + ['Cuica (mute)', 'Cuica.78'], + ['Vibraslap (hit)', 'Vibraslap.58'], + ['Triangle (hit)', 'Triangle.81'], + ['Triangle (mute)', 'Triangle.80'], + ['Grancassa (hit)', 'Grancassa.114'], + ['Piatti (hit)', 'Piatti.115'], + ['Piatti (hand)', 'Piatti.116'], + ['Cabasa (hit)', 'Cabasa.69'], + ['Cabasa (return)', 'Cabasa.117'], + ['Castanets (hit)', 'Castanets.85'], + ['Claves (hit)', 'Claves.75'], + ['Left Maraca (hit)', 'Left Maraca.70'], + ['Left Maraca (return)', 'Left Maraca.118'], + ['Right Maraca (hit)', 'Right Maraca.119'], + ['Right Maraca (return)', 'Right Maraca.120'], + ['Shaker (hit)', 'Shaker.82'], + ['Shaker (return)', 'Shaker.122'], + ['Bell Tree (hit)', 'Bell Tree.84'], + ['Bell Tree (return)', 'Bell Tree.123'], + ['Jingle Bell (hit)', 'Jingle Bell.83'], + ['Tinkle Bell (hit)', 'Tinkle Bell.83'], + ['Golpe (thumb)', 'Golpe.124'], + ['Golpe (finger)', 'Golpe.125'], + ['Hand Clap (hit)', 'Hand Clap.39'], + ['Electric Snare (hit)', 'Electric Snare.40'], + ['Snare (side stick) 2', 'Sticks.31'], + ['Low Floor Tom (hit)', 'Very Low Floor Tom.41'], + ['Ride (edge) 2', 'Ride Cymbal 2.59'], + ['Ride (middle) 2', 'Ride Cymbal 2.126'], + ['Ride (bell) 2', 'Ride Cymbal 2.127'], + ['Ride (choke) 2', 'Ride Cymbal 2.29'], + ['Reverse Cymbal (hit)', 'Reverse Cymbal.30'], + ['Metronome (hit)', 'Metronome.33'], + ['Metronome (bell)', 'Metronome.34'] ]); + // END generated articulations + + private static _gp6ElementAndVariationToArticulation: number[][] = [ + // known GP6 elements and variations, analyzed from a GPX test file + // with all instruments inside manually aligned with the same names of articulations in GP7 + // [{articulation index}] // [{element number}] => {element name} ({variation[0]}, {variation[1]}, {variation[2]}) + [35, 35, 35], // [0] => Kick (hit, unused, unused) + [38, 91, 37], // [1] => Snare (hit, rim shot, side stick) + [99, 100, 99], // [2] => Cowbell low (hit, tip, unused) + [56, 100, 56], // [3] => Cowbell medium (hit, tip, unused) + [102, 103, 102], // [4] => Cowbell high (hit, tip, unused) + [43, 43, 43], // [5] => Tom very low (hit, unused, unused) + [45, 45, 45], // [6] => Tom low (hit, unused, unused) + [47, 47, 47], // [7] => Tom medium (hit, unused, unused) + [48, 48, 48], // [8] => Tom high (hit, unused, unused) + [50, 50, 50], // [9] => Tom very high (hit, unused, unused) + [42, 92, 46], // [10] => Hihat (closed, half, open) + [44, 44, 44], // [11] => Pedal hihat (hit, unused, unused) + [57, 98, 57], // [12] => Crash medium (hit, choke, unused) + [49, 97, 49], // [13] => Crash high (hit, choke, unused) + [55, 95, 55], // [14] => Splash (hit, choke, unused) + [51, 93, 127], // [15] => Ride (middle, edge, bell) + [52, 96, 52] // [16] => China (hit, choke, unused) + ]; + + public static articulationFromElementVariation(element: number, variation: number): number { + if (element < PercussionMapper._gp6ElementAndVariationToArticulation.length) { + if (variation >= PercussionMapper._gp6ElementAndVariationToArticulation.length) { + variation = 0; + } + return PercussionMapper._gp6ElementAndVariationToArticulation[element][variation]; + } + // unknown combination, should not happen, fallback to some default value (Snare hit) + return 38; + } public static getArticulationName(n: Note): string { const articulation = PercussionMapper.getArticulation(n); - let input = n.percussionArticulation; - if (articulation) { - input = articulation.outputMidiNumber; - } // no efficient lookup for now, mainly used by exporter - for (const [name, value] of PercussionMapper.instrumentArticulationNames) { - if (value === input) { - return name; + if (articulation) { + const uniqueId = articulation.uniqueId; + for (const [name, value] of PercussionMapper.instrumentArticulationNames) { + if (value === uniqueId) { + return name; + } + } + } else { + const uniqueId = `.${n.percussionArticulation}`; + for (const [name, value] of PercussionMapper.instrumentArticulationNames) { + if (value.endsWith(uniqueId)) { + return name; + } } } @@ -1287,7 +1082,32 @@ export class PercussionMapper { return trackArticulations[articulationIndex]; } - return PercussionMapper.getArticulationByInputMidiNumber(articulationIndex); + return PercussionMapper.getArticulationById(articulationIndex); + } + + private static _instrumentArticulationsById: Map | undefined; + + private static _initArticulationsById(): Map { + let lookup = PercussionMapper._instrumentArticulationsById; + if (!lookup) { + lookup = new Map(); + for (const articulation of PercussionMapper.instrumentArticulations.values()) { + lookup.set(articulation.id, articulation); + } + PercussionMapper._instrumentArticulationsById = lookup; + } + + return lookup; + } + + public static instrumentArticulationIds(): Iterable { + const lookup = PercussionMapper._initArticulationsById(); + return lookup.keys(); + } + + public static getArticulationById(id: number): InstrumentArticulation | null { + const lookup = PercussionMapper._initArticulationsById(); + return lookup.has(id) ? lookup.get(id)! : null; } public static getElementAndVariation(n: Note): number[] { @@ -1300,7 +1120,7 @@ export class PercussionMapper { for (let element = 0; element < PercussionMapper._gp6ElementAndVariationToArticulation.length; element++) { const variations = PercussionMapper._gp6ElementAndVariationToArticulation[element]; for (let variation = 0; variation < variations.length; variation++) { - const gp6Articulation = PercussionMapper.getArticulationByInputMidiNumber(variations[variation]); + const gp6Articulation = PercussionMapper.getArticulationById(variations[variation]); if (gp6Articulation?.outputMidiNumber === articulation.outputMidiNumber) { return [element, variation]; } @@ -1310,10 +1130,69 @@ export class PercussionMapper { return [-1, -1]; } - public static getArticulationByInputMidiNumber(inputMidiNumber: number): InstrumentArticulation | null { - if (PercussionMapper.instrumentArticulations.has(inputMidiNumber)) { - return PercussionMapper.instrumentArticulations.get(inputMidiNumber)!; + public static readonly instrumentArticulationNames = PercussionMapper._mergeNames([ + new Map([ + // these are historical values we supported in the past, but were + // renamed in Guitar Pro. + // mostly typos due to typos or clarifications. + ['Hand (hit)', 'Bongo Low.61'], + ['Tinkle Bell (hat)', 'Tingle Bell.83'], + ['Cymbal (hit)', 'Reverse Cymbal.30'], + ['Snare (side stick) 3', 'Snare.37'], + ['Snare (hit) 2', 'Snare.38'], + ['Snare (hit) 3', 'Snare.40'], + ['Agogo tow (hit)', 'Agogo Low.68'], + ['Triangle (rnute)', 'Triangle.80'], + ['Hand (mute)', 'Bongo High.104'], + ['Hand (slap)', 'Bongo High.105'], + ['Hand (mute) 2', 'Bongo Low.106'], + ['Hand (slap) 2', 'Bongo Low.107'], + ['Piatti (hat)', 'Piatti.115'], + ['Bell Tee (return)', 'Bell Tree.123'] + ]), + PercussionMapper._instrumentArticulationNames + ]); + + private static _mergeNames(maps: Map[]) { + const merged = new Map(maps[0]); + for (let i = 1; i < maps.length; i++) { + for (const [k, v] of maps[i]) { + merged.set(k, v); + } + } + return merged; + } + + private static _articulationsByOutputNumber: Map | undefined; + public static tryMatchKnownArticulation(articulation: InstrumentArticulation): number { + let articulationsByOutputNumber = PercussionMapper._articulationsByOutputNumber; + if (!articulationsByOutputNumber) { + articulationsByOutputNumber = new Map(); + for (const a of PercussionMapper.instrumentArticulations.values()) { + // first one wins + if (!articulationsByOutputNumber.has(a.outputMidiNumber)) { + articulationsByOutputNumber.set(a.outputMidiNumber, a); + } + } + PercussionMapper._articulationsByOutputNumber = articulationsByOutputNumber; } - return null; + + return articulationsByOutputNumber.has(articulation.outputMidiNumber) + ? articulationsByOutputNumber.get(articulation.outputMidiNumber)!.id + : -1; + } + + private static _instrumentArticulationsByUniqueId: Map | undefined; + public static getInstrumentArticulationByUniqueId(uniqueId: string): InstrumentArticulation | undefined { + let lookup = PercussionMapper._instrumentArticulationsByUniqueId; + if (!lookup) { + lookup = new Map(); + for (const articulation of PercussionMapper.instrumentArticulations.values()) { + lookup.set(articulation.uniqueId, articulation); + } + PercussionMapper._instrumentArticulationsByUniqueId = lookup; + } + + return lookup.has(uniqueId) ? lookup.get(uniqueId)! : undefined; } } diff --git a/packages/alphatab/src/model/RenderStylesheet.ts b/packages/alphatab/src/model/RenderStylesheet.ts index ecc1912ae..81e4a4ad5 100644 --- a/packages/alphatab/src/model/RenderStylesheet.ts +++ b/packages/alphatab/src/model/RenderStylesheet.ts @@ -72,6 +72,25 @@ export enum TrackNameOrientation { Vertical = 1 } +/** + * How bar numbers are displayed + * @public + */ +export enum BarNumberDisplay { + /** + * Show bar numbers on all bars. + */ + AllBars = 0, + /** + * Show bar numbers on the first bar of every system. + */ + FirstOfSystem = 1, + /** + * Hide all bar numbers + */ + Hide = 2 +} + /** * This class represents the rendering stylesheet. * It contains settings which control the display of the score when rendered. @@ -115,6 +134,11 @@ export class RenderStylesheet { */ public perTrackChordDiagramsOnTop: Map | null = null; + /** + * Whether to show the chord diagrams in score. + */ + public globalDisplayChordDiagramsInScore: boolean = false; + /** * The policy where to show track names when a single track is rendered. */ @@ -154,4 +178,34 @@ export class RenderStylesheet { * If single track: Whether to render multiple subsequent empty (or rest-only) bars together as multi-bar rest. */ public perTrackMultiBarRest: Set | null = null; + + /** + * Whether barlines should be drawn across staves within the same system. + */ + public extendBarLines: boolean = false; + + /** + * Whether to hide empty staves. + */ + public hideEmptyStaves: boolean = false; + + /** + * Whether to also hide empty staves in the first system. + * @remarks + * Only has an effect when activating {@link hideEmptyStaves}. + */ + public hideEmptyStavesInFirstSystem: boolean = false; + + /** + * Whether to show brackets and braces across single staves. + * @remarks + * This allows a more consistent view for identifying staves when using + * {@link hideEmptyStaves} + */ + public showSingleStaffBrackets: boolean = false; + + /** + * How bar numbers should be displayed. + */ + public barNumberDisplay: BarNumberDisplay = BarNumberDisplay.AllBars; } diff --git a/packages/alphatab/src/model/Score.ts b/packages/alphatab/src/model/Score.ts index 625587d1b..8b4259aee 100644 --- a/packages/alphatab/src/model/Score.ts +++ b/packages/alphatab/src/model/Score.ts @@ -359,9 +359,10 @@ export class Score { if (this.masterBars.length !== 0) { bar.previousMasterBar = this.masterBars[this.masterBars.length - 1]; bar.previousMasterBar.nextMasterBar = bar; - // TODO: this will not work on anacrusis. Correct anacrusis durations are only working + // NOTE: this will not work on anacrusis. Correct anacrusis durations are only working // when there are beats with playback positions already computed which requires full finish - // chicken-egg problem here. temporarily forcing anacrusis length here to 0 + // chicken-egg problem here. temporarily forcing anacrusis length here to 0, + // .finish() will correct these times bar.start = bar.previousMasterBar.start + (bar.previousMasterBar.isAnacrusis ? 0 : bar.previousMasterBar.calculateDuration()); @@ -432,6 +433,11 @@ export class Score { for (let i: number = 0, j: number = this.tracks.length; i < j; i++) { this.tracks[i].finish(settings, sharedDataBag); } + + // fixup masterbar starts to handle anacrusis lengths + for (const mb of this.masterBars) { + mb.finish(sharedDataBag); + } } /** diff --git a/packages/alphatab/src/model/Staff.ts b/packages/alphatab/src/model/Staff.ts index 0de57feef..82cb2e2f9 100644 --- a/packages/alphatab/src/model/Staff.ts +++ b/packages/alphatab/src/model/Staff.ts @@ -1,8 +1,8 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Chord } from '@coderline/alphatab/model/Chord'; import type { Track } from '@coderline/alphatab/model/Track'; -import type { Settings } from '@coderline/alphatab/Settings'; import { Tuning } from '@coderline/alphatab/model/Tuning'; +import type { Settings } from '@coderline/alphatab/Settings'; /** * This class describes a single staff within a track. There are instruments like pianos @@ -128,6 +128,10 @@ export class Staff { this.displayTranspositionPitch = 0; } this.stringTuning.finish(); + if(this.stringTuning.tunings.length === 0){ + this.showTablature = false; + } + for (let i: number = 0, j: number = this.bars.length; i < j; i++) { this.bars[i].finish(settings, sharedDataBag); for(const v of this.bars[i].filledVoices) { diff --git a/packages/alphatab/src/model/TremoloPickingEffect.ts b/packages/alphatab/src/model/TremoloPickingEffect.ts new file mode 100644 index 000000000..42148aada --- /dev/null +++ b/packages/alphatab/src/model/TremoloPickingEffect.ts @@ -0,0 +1,79 @@ +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import { Duration } from '@coderline/alphatab/model/Duration'; + +/** + * The style of tremolo affecting mainly the display of the effect. + * @public + */ +export enum TremoloPickingStyle { + /** + * A classic tremolo expressed by diagonal bars on the stem. + */ + Default = 0, + /** + * A buzz roll tremolo expressed by a 'z' shaped symbol. + */ + BuzzRoll = 1 +} + +/** + * Describes a tremolo picking effect. + * @json + * @json_strict + * @cloneable + * @public + */ +export class TremoloPickingEffect { + /** + * The minimum number of marks for the tremolo picking effect to be valid. + */ + public static readonly minMarks = 0; + + /** + * The max number of marks for the tremolo picking effect to be valid. + */ + public static readonly maxMarks = 5; + + /** + * The number of marks for the tremolo. + * A mark is equal to a single bar shown for a default tremolos. + */ + public marks: number = 0; + + /** + * The style of the tremolo picking. + */ + public style: TremoloPickingStyle = TremoloPickingStyle.Default; + + /** + * @internal + * @deprecated use {@link getDurationAsTicks} to handle tremolo durations shorter than typical durations. + */ + public getDuration(beatDuration: Duration): Duration { + let marks = this.marks; + if (marks < 1) { + marks = 1; + } + const baseDuration = beatDuration as number; + const actualDuration = baseDuration * Math.pow(2, marks); + if (actualDuration <= Duration.TwoHundredFiftySixth) { + return actualDuration as Duration; + } else { + return Duration.TwoHundredFiftySixth; + } + } + + /** + * Gets the duration of a single tremolo note played in a beat of the given duration + * based on the configured marks. + */ + public getDurationAsTicks(beatDuration: Duration): number { + let marks = this.marks; + if (marks < 1) { + marks = 1; + } + const baseDuration = beatDuration as number; + const actualDuration = baseDuration * Math.pow(2, marks); + return MidiUtils.valueToTicks(actualDuration); + } +} diff --git a/packages/alphatab/src/model/TupletGroup.ts b/packages/alphatab/src/model/TupletGroup.ts index 9a708bd0c..ce3acfd8d 100644 --- a/packages/alphatab/src/model/TupletGroup.ts +++ b/packages/alphatab/src/model/TupletGroup.ts @@ -82,11 +82,12 @@ export class TupletGroup { // by checking all potential note durations. // this logic is very likely not 100% correct but for most cases the tuplets // appeared correct. - if (beat.playbackDuration !== this.beats[0].playbackDuration) { + + if (beat.displayDuration !== this.beats[0].displayDuration) { this._isEqualLengthTuplet = false; } this.beats.push(beat); - this.totalDuration += beat.playbackDuration; + this.totalDuration += beat.displayDuration; if (this._isEqualLengthTuplet) { if (this.beats.length === this.beats[0].tupletNumerator) { this.isFull = true; diff --git a/packages/alphatab/src/model/Voice.ts b/packages/alphatab/src/model/Voice.ts index 33adc3c7a..a6631d496 100644 --- a/packages/alphatab/src/model/Voice.ts +++ b/packages/alphatab/src/model/Voice.ts @@ -1,9 +1,10 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Beat } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import { ElementStyle } from '@coderline/alphatab/model/ElementStyle'; +import { GraceGroup } from '@coderline/alphatab/model/GraceGroup'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import type { Settings } from '@coderline/alphatab/Settings'; -import { GraceGroup } from '@coderline/alphatab/model/GraceGroup'; -import { ElementStyle } from '@coderline/alphatab/model/ElementStyle'; /** * Lists all graphical sub elements within a {@link Voice} which can be styled via {@link Voice.style} @@ -94,6 +95,13 @@ export class Voice { return this._isRestOnly; } + /** + * The shortest duration contained across beats in this bar. + * @internal + * @json_ignore + */ + public shortestDuration = Duration.DoubleWhole; + public insertBeat(after: Beat, newBeat: Beat): void { newBeat.nextBeat = after.nextBeat; if (newBeat.nextBeat) { @@ -191,6 +199,7 @@ export class Voice { let currentDisplayTick: number = 0; let currentPlaybackTick: number = 0; + this.shortestDuration = Duration.DoubleWhole; for (let i: number = 0; i < this.beats.length; i++) { const beat: Beat = this.beats[i]; beat.index = i; @@ -200,6 +209,10 @@ export class Voice { // we need to first steal the duration from the right beat // and place the grace beats correctly if (beat.graceType === GraceType.None) { + if (beat.duration > this.shortestDuration) { + this.shortestDuration = beat.duration; + } + if (beat.graceGroup) { const firstGraceBeat = beat.graceGroup!.beats[0]; const lastGraceBeat = beat.graceGroup!.beats[beat.graceGroup!.beats.length - 1]; diff --git a/packages/alphatab/src/model/_barrel.ts b/packages/alphatab/src/model/_barrel.ts index 2cb44098d..c5c0e1a6a 100644 --- a/packages/alphatab/src/model/_barrel.ts +++ b/packages/alphatab/src/model/_barrel.ts @@ -1,9 +1,17 @@ export { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; export { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; export { AutomationType, Automation, SyncPointData, type FlatSyncPoint } from '@coderline/alphatab/model/Automation'; -export { Bar, SustainPedalMarkerType, SustainPedalMarker, BarSubElement, BarStyle, BarLineStyle } from '@coderline/alphatab/model/Bar'; +export { + Bar, + SustainPedalMarkerType, + SustainPedalMarker, + BarSubElement, + BarStyle, + BarLineStyle +} from '@coderline/alphatab/model/Bar'; export { BarreShape } from '@coderline/alphatab/model/BarreShape'; export { Beat, BeatBeamingMode, BeatSubElement, BeatStyle } from '@coderline/alphatab/model/Beat'; +export { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; export { BendPoint } from '@coderline/alphatab/model/BendPoint'; export { BendStyle } from '@coderline/alphatab/model/BendStyle'; export { BendType } from '@coderline/alphatab/model/BendType'; @@ -28,7 +36,7 @@ export { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; export { KeySignature } from '@coderline/alphatab/model/KeySignature'; export { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; export { Lyrics } from '@coderline/alphatab/model/Lyrics'; -export { MasterBar } from '@coderline/alphatab/model/MasterBar'; +export { MasterBar, BeamingRules } from '@coderline/alphatab/model/MasterBar'; export { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; export { Note, NoteSubElement, NoteStyle } from '@coderline/alphatab/model/Note'; export { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; @@ -42,7 +50,8 @@ export { BracketExtendMode, TrackNamePolicy, TrackNameMode, - TrackNameOrientation + TrackNameOrientation, + BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; export { RepeatGroup } from '@coderline/alphatab/model/RepeatGroup'; export { Score, ScoreSubElement, ScoreStyle, HeaderFooterStyle } from '@coderline/alphatab/model/Score'; diff --git a/packages/alphatab/src/platform/IUiFacade.ts b/packages/alphatab/src/platform/IUiFacade.ts index 57394a251..a0772ea55 100644 --- a/packages/alphatab/src/platform/IUiFacade.ts +++ b/packages/alphatab/src/platform/IUiFacade.ts @@ -175,6 +175,12 @@ export interface IUiFacade { */ scrollToX(scrollElement: IContainer, offset: number, speed: number): void; + /** + * Stops any ongoing scrolling of the given element. + * @param scrollElement The element which might be scrolling dynamically. + */ + stopScrolling(scrollElement: IContainer): void; + /** * Attempts a load of the score represented by the given data object. * @param data The data object to decode @@ -192,6 +198,18 @@ export interface IUiFacade { */ loadSoundFont(data: unknown, append: boolean): boolean; + /** + * Updates the overflows needed to ensure the smooth scrolling + * can reach the "end" at the desired position. + * @param canvasElement The canvas element. + * @param offset The offset we need + * @param isVertical Whether we have a vertical or horizontal overflow + * @remarks + * 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; + /** * This events is fired when the {@link canRender} property changes. */ diff --git a/packages/alphatab/src/platform/javascript/AlphaSynthAudioExporterWorkerApi.ts b/packages/alphatab/src/platform/javascript/AlphaSynthAudioExporterWorkerApi.ts index e1b5cc0dc..8ae2ca72c 100644 --- a/packages/alphatab/src/platform/javascript/AlphaSynthAudioExporterWorkerApi.ts +++ b/packages/alphatab/src/platform/javascript/AlphaSynthAudioExporterWorkerApi.ts @@ -2,8 +2,13 @@ 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, BackingTrackSyncPoint } from '@coderline/alphatab/synth/_barrel'; -import type { AudioExportChunk, AudioExportOptions, IAudioExporterWorker } from '@coderline/alphatab/synth/IAudioExporter'; +import type { AlphaSynthWebWorkerApi } from '@coderline/alphatab/platform/javascript/AlphaSynthWebWorkerApi'; +import type { BackingTrackSyncPoint } from '@coderline/alphatab/synth/IAlphaSynth'; +import type { + AudioExportChunk, + AudioExportOptions, + IAudioExporterWorker +} from '@coderline/alphatab/synth/IAudioExporter'; /** * @target web diff --git a/packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts b/packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts index 124c9448e..62038c4df 100644 --- a/packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts +++ b/packages/alphatab/src/platform/javascript/AlphaTabWebWorker.ts @@ -1,12 +1,13 @@ +import { Environment } from '@coderline/alphatab/Environment'; +import { SettingsSerializer } from '@coderline/alphatab/generated/SettingsSerializer'; +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 { RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; import { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import type { Settings } from '@coderline/alphatab/Settings'; -import { Logger } from '@coderline/alphatab/Logger'; -import { Environment } from '@coderline/alphatab/Environment'; -import { SettingsSerializer } from '@coderline/alphatab/generated/SettingsSerializer'; /** * @target web @@ -67,8 +68,8 @@ export class AlphaTabWebWorker { }); this._renderer.error.on(this._error.bind(this)); break; - case 'alphaTab.invalidate': - this._renderer.render(); + case 'alphaTab.render': + this._renderer.render(data.renderHints); break; case 'alphaTab.resizeRender': this._renderer.resizeRender(); @@ -111,9 +112,9 @@ export class AlphaTabWebWorker { SettingsSerializer.fromJson(this._renderer.settings, json); } - private _renderMultiple(score: Score | null, trackIndexes: number[] | null): void { + private _renderMultiple(score: Score | null, trackIndexes: number[] | null, renderHints?: RenderHints): void { try { - this._renderer.renderScore(score, trackIndexes); + this._renderer.renderScore(score, trackIndexes, renderHints); } catch (e) { this._error(e as Error); } diff --git a/packages/alphatab/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts b/packages/alphatab/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts index c7c6224c7..0ef29268e 100644 --- a/packages/alphatab/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts +++ b/packages/alphatab/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts @@ -1,9 +1,14 @@ import type { AlphaTabApiBase } from '@coderline/alphatab/AlphaTabApiBase'; -import { EventEmitter, type IEventEmitterOfT, type IEventEmitter, EventEmitterOfT } from '@coderline/alphatab/EventEmitter'; +import { + EventEmitter, + type IEventEmitterOfT, + type IEventEmitter, + EventEmitterOfT +} 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 { IScoreRenderer } from '@coderline/alphatab/rendering/IScoreRenderer'; +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'; @@ -55,9 +60,10 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { return jsObject; } - public render(): void { + public render(renderHints?: RenderHints): void { this._worker.postMessage({ - cmd: 'alphaTab.render' + cmd: 'alphaTab.render', + renderHints: renderHints }); } @@ -113,13 +119,15 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { } } - public renderScore(score: Score | null, trackIndexes: number[] | null): void { - const jsObject: unknown = score == null ? null : JsonConverter.scoreToJsObject(Environment.prepareForPostMessage(score)); + public renderScore(score: Score | null, trackIndexes: number[] | null, renderHints?: RenderHints): void { + const jsObject: unknown = + score == null ? null : JsonConverter.scoreToJsObject(Environment.prepareForPostMessage(score)); this._worker.postMessage({ cmd: 'alphaTab.renderScore', score: jsObject, trackIndexes: Environment.prepareForPostMessage(trackIndexes), - fontSizes: FontSizes.fontSizeLookupTables + fontSizes: FontSizes.fontSizeLookupTables, + renderHints }); } @@ -128,7 +136,8 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { new EventEmitterOfT(); public readonly partialLayoutFinished: IEventEmitterOfT = new EventEmitterOfT(); - public readonly renderFinished: IEventEmitterOfT = new EventEmitterOfT(); + public readonly renderFinished: IEventEmitterOfT = + new EventEmitterOfT(); public readonly postRenderFinished: IEventEmitter = new EventEmitter(); public readonly error: IEventEmitterOfT = new EventEmitterOfT(); } diff --git a/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts b/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts index 766bed9b5..b499755a3 100644 --- a/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts +++ b/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts @@ -29,7 +29,7 @@ import { WebPlatform } from '@coderline/alphatab/platform/javascript/WebPlatform import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; import { AlphaSynthAudioWorkletOutput } from '@coderline/alphatab/platform/javascript/AlphaSynthAudioWorkletOutput'; import { ScalableHtmlElementContainer } from '@coderline/alphatab/platform/javascript/ScalableHtmlElementContainer'; -import { PlayerOutputMode } from '@coderline/alphatab/PlayerSettings'; +import { PlayerOutputMode, ScrollMode } from '@coderline/alphatab/PlayerSettings'; import type { SettingsJson } from '@coderline/alphatab/generated/SettingsJson'; import { AudioElementBackingTrackSynthOutput } from '@coderline/alphatab/platform/javascript/AudioElementBackingTrackSynthOutput'; import { BackingTrackPlayer } from '@coderline/alphatab/synth/BackingTrackPlayer'; @@ -65,8 +65,14 @@ interface ResultPlaceholder extends HTMLElement { */ interface RegisteredWebFont { hash: number; - element: HTMLStyleElement; - usages: number; + cssSource: string; + elements: Map< + HTMLDocument, + { + element: HTMLStyleElement; + usages: number; + } + >; fontSuffix: string; checker: FontLoadingChecker; } @@ -203,24 +209,22 @@ export class BrowserUiFacade implements IUiFacade { this._contents = ''; const element: HtmlElementContainer = api.container as HtmlElementContainer; if (settings.core.tex) { - this._contents = element.element.innerHTML; - element.element.innerHTML = ''; + this._contents = element.element.textContent; + element.element.innerText = ''; } this._createStyleElements(settings); this._file = settings.core.file; } private _setupFontCheckers(settings: Settings): void { - this._registerFontChecker(settings.display.resources.copyrightFont); - this._registerFontChecker(settings.display.resources.effectFont); + for (const font of settings.display.resources.elementFonts.values()) { + this._registerFontChecker(font); + } + this._registerFontChecker(settings.display.resources.graceFont); - this._registerFontChecker(settings.display.resources.markerFont); this._registerFontChecker(settings.display.resources.tablatureFont); - this._registerFontChecker(settings.display.resources.titleFont); - this._registerFontChecker(settings.display.resources.wordsFont); - this._registerFontChecker(settings.display.resources.barNumberFont); - this._registerFontChecker(settings.display.resources.fretboardNumberFont); - this._registerFontChecker(settings.display.resources.subTitleFont); + this._registerFontChecker(settings.display.resources.numberedNotationFont); + this._registerFontChecker(settings.display.resources.numberedNotationGraceFont); } private _registerFontChecker(font: Font): void { @@ -233,11 +237,20 @@ export class BrowserUiFacade implements IUiFacade { } public destroy(): void { - (this.rootContainer as HtmlElementContainer).element.innerHTML = ''; + const element = (this.rootContainer as HtmlElementContainer).element; + element.innerHTML = ''; const webFont = this._webFont; - webFont.usages--; - if (webFont.usages <= 0) { - webFont.element.remove(); + + const styleElement = webFont.elements.get(element.ownerDocument); + if (styleElement) { + styleElement.usages--; + if (styleElement.usages <= 0) { + styleElement.element.remove(); + webFont.elements.delete(element.ownerDocument); + } + } + + if (webFont.elements.size === 0) { BrowserUiFacade._registeredWebFonts.delete(webFont.hash); } } @@ -252,6 +265,21 @@ export class BrowserUiFacade implements IUiFacade { return new HtmlElementContainer(canvasElement); } + public setCanvasOverflow(canvasElement: IContainer, overflow: number, isVertical: boolean): void { + const html = (canvasElement as HtmlElementContainer).element; + if (overflow === 0) { + html.style.boxSizing = ''; + html.style.paddingRight = ''; + html.style.paddingBottom = ''; + } else if (isVertical) { + html.style.boxSizing = 'content-box'; + html.style.paddingBottom = `${overflow}px`; + } else { + html.style.boxSizing = 'content-box'; + html.style.paddingRight = `${overflow}px`; + } + } + public triggerEvent( container: IContainer, name: string, @@ -375,8 +403,8 @@ export class BrowserUiFacade implements IUiFacade { const registeredWebFonts = BrowserUiFacade._registeredWebFonts; if (registeredWebFonts.has(hash)) { const webFont = registeredWebFonts.get(hash)!; - webFont.usages++; webFont.checker.fontLoaded.on(this._onFontLoaded.bind(this)); + this._createStyleElement(webFont, root); this._webFont = webFont; return; } @@ -411,10 +439,6 @@ export class BrowserUiFacade implements IUiFacade { overflow: visible !important; }`; - const styleElement = root.createElement('style'); - styleElement.id = `alphaTabStyle${fontSuffix}`; - styleElement.innerHTML = css; - root.getElementsByTagName('head').item(0)!.appendChild(styleElement); const checker = new FontLoadingChecker([familyName]); checker.fontLoaded.on(this._onFontLoaded.bind(this)); this._fontCheckers.set(familyName, checker); @@ -424,15 +448,32 @@ export class BrowserUiFacade implements IUiFacade { const webFont: RegisteredWebFont = { hash, - element: styleElement, + elements: new Map(), fontSuffix, - usages: 1, - checker + checker, + cssSource: css }; + this._createStyleElement(webFont, root); + registeredWebFonts.set(hash, webFont); this._webFont = webFont; } + private _createStyleElement(webFont: RegisteredWebFont, root: Document) { + if (webFont.elements.has(root)) { + webFont.elements.get(root)!.usages++; + return; + } + + const styleElement = root.createElement('style'); + styleElement.id = `alphaTabStyle${webFont.fontSuffix}`; + styleElement.innerHTML = webFont.cssSource; + root.getElementsByTagName('head').item(0)!.appendChild(styleElement); + webFont.elements.set(root, { + element: styleElement, + usages: 1 + }); + } private static _cssFormat(format: FontFileFormat) { switch (format) { @@ -637,6 +678,9 @@ export class BrowserUiFacade implements IUiFacade { placeholder.renderedResultId = undefined; placeholder.renderedResult = undefined; + if (!renderResult.reuseViewport) { + placeholder.textContent = ''; + } this._resultIdToElementLookup.set(renderResult.id, placeholder); // remember which bar is contained in which node for faster lookup @@ -778,6 +822,7 @@ export class BrowserUiFacade implements IUiFacade { beatCursor.style.willChange = 'transform'; beatCursorContainer.width = 3; beatCursorContainer.height = 1; + beatCursorContainer.centerAtPosition = true; beatCursorContainer.setBounds(0, 0, 1, 1); // add cursors to UI @@ -877,54 +922,92 @@ export class BrowserUiFacade implements IUiFacade { this._internalScrollToX((element as HtmlElementContainer).element, scrollTargetY, speed); } + public stopScrolling(scrollElement: IContainer): void { + // stop any current animation + const currentAnimation = this._scrollAnimationLookup.get((scrollElement as HtmlElementContainer).element); + if (currentAnimation !== undefined) { + this._activeScrollAnimations.delete(currentAnimation); + } + } + + private get _nativeBrowserSmoothScroll() { + const settings = this._api.settings.player; + return settings.nativeBrowserSmoothScroll && settings.scrollMode !== ScrollMode.Smooth; + } + + private _scrollAnimationId = 0; + private readonly _activeScrollAnimations = new Set(); + private readonly _scrollAnimationLookup = new Map(); + private _internalScrollToY(element: HTMLElement, scrollTargetY: number, speed: number): void { - if (this._api.settings.player.nativeBrowserSmoothScroll) { + if (this._nativeBrowserSmoothScroll) { element.scrollTo({ top: scrollTargetY, behavior: 'smooth' }); } else { - const startY: number = element.scrollTop; - const diff: number = scrollTargetY - startY; + this._internalScrollTo(element, element.scrollTop, scrollTargetY, speed, scroll => { + element.scrollTop = scroll; + }); + } + } - let start: number = 0; - const step = (x: number) => { - if (start === 0) { - start = x; - } - const time: number = x - start; - const percent: number = Math.min(time / speed, 1); - element.scrollTop = (startY + diff * percent) | 0; - if (time < speed) { - window.requestAnimationFrame(step); - } - }; - window.requestAnimationFrame(step); + private _internalScrollTo( + element: HTMLElement, + startScroll: number, + endScroll: number, + scrollDuration: number, + setValue: (scroll: number) => void + ) { + // stop any current animation + const currentAnimation = this._scrollAnimationLookup.get(element); + if (currentAnimation !== undefined) { + this._activeScrollAnimations.delete(currentAnimation); + } + + if (scrollDuration === 0) { + setValue(endScroll); + return; } + + // start new animation + const animationId = this._scrollAnimationId++; + this._scrollAnimationLookup.set(element, animationId); + this._activeScrollAnimations.add(animationId); + + const diff: number = endScroll - startScroll; + + let start: number = 0; + const step = (x: number) => { + if (!this._activeScrollAnimations.has(animationId)) { + return; + } + + if (start === 0) { + start = x; + } + const time = x - start; + const percent = Math.min(time / scrollDuration, 1); + setValue((startScroll + diff * percent) | 0); + if (time < scrollDuration) { + window.requestAnimationFrame(step); + } else { + this._activeScrollAnimations.delete(animationId); + } + }; + window.requestAnimationFrame(step); } private _internalScrollToX(element: HTMLElement, scrollTargetX: number, speed: number): void { - if (this._api.settings.player.nativeBrowserSmoothScroll) { + if (this._nativeBrowserSmoothScroll) { element.scrollTo({ left: scrollTargetX, behavior: 'smooth' }); } else { - const startX: number = element.scrollLeft; - const diff: number = scrollTargetX - startX; - let start: number = 0; - const step = (t: number) => { - if (start === 0) { - start = t; - } - const time: number = t - start; - const percent: number = Math.min(time / speed, 1); - element.scrollLeft = (startX + diff * percent) | 0; - if (time < speed) { - window.requestAnimationFrame(step); - } - }; - window.requestAnimationFrame(step); + this._internalScrollTo(element, element.scrollLeft, scrollTargetX, speed, scroll => { + element.scrollLeft = scroll; + }); } } diff --git a/packages/alphatab/src/platform/javascript/HtmlElementContainer.ts b/packages/alphatab/src/platform/javascript/HtmlElementContainer.ts index 35f645217..8c871cb84 100644 --- a/packages/alphatab/src/platform/javascript/HtmlElementContainer.ts +++ b/packages/alphatab/src/platform/javascript/HtmlElementContainer.ts @@ -196,6 +196,6 @@ export class HtmlElementContainer implements IContainer { } public clear(): void { - this.element.innerHTML = ''; + this.element.innerText = ''; } } diff --git a/packages/alphatab/src/platform/javascript/ScalableHtmlElementContainer.ts b/packages/alphatab/src/platform/javascript/ScalableHtmlElementContainer.ts index d700f18cf..76ce59b64 100644 --- a/packages/alphatab/src/platform/javascript/ScalableHtmlElementContainer.ts +++ b/packages/alphatab/src/platform/javascript/ScalableHtmlElementContainer.ts @@ -19,6 +19,8 @@ export class ScalableHtmlElementContainer extends HtmlElementContainer { private _xscale: number; private _yscale: number; + public centerAtPosition = false; + public constructor(element: HTMLElement, xscale: number, yscale: number) { super(element); this._xscale = xscale; @@ -63,7 +65,11 @@ export class ScalableHtmlElementContainer extends HtmlElementContainer { h = h / this._yscale; } - this.element.style.transform = `translate(${x}px, ${y}px) scale(${w}, ${h})`; + let transform = `translate(${x}px, ${y}px) scale(${w}, ${h})`; + if(this.centerAtPosition) { + transform += ` translateX(-50%)`; + } + this.element.style.transform = transform; this.element.style.transformOrigin = 'top left'; this.lastBounds.x = x; this.lastBounds.y = y; diff --git a/packages/alphatab/src/rendering/BarRendererBase.ts b/packages/alphatab/src/rendering/BarRendererBase.ts index 2e77fdfb6..c3de38ea2 100644 --- a/packages/alphatab/src/rendering/BarRendererBase.ts +++ b/packages/alphatab/src/rendering/BarRendererBase.ts @@ -1,31 +1,32 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Beat } from '@coderline/alphatab/model/Beat'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import { type Voice, VoiceSubElement } from '@coderline/alphatab/model/Voice'; import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; -import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; -import type { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; +import { EffectBandContainer } from '@coderline/alphatab/rendering/EffectBandContainer'; +import { + BeatContainerGlyph, + type BeatContainerGlyphBase +} from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { LeftToRightLayoutingGlyphGroup } from '@coderline/alphatab/rendering/glyphs/LeftToRightLayoutingGlyphGroup'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { VoiceContainerGlyph } from '@coderline/alphatab/rendering/glyphs/VoiceContainerGlyph'; +import { MultiVoiceContainerGlyph } from '@coderline/alphatab/rendering/glyphs/MultiVoiceContainerGlyph'; +import { ContinuationTieGlyph, type ITieGlyph, type TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import { MultiBarRestBeatContainerGlyph } from '@coderline/alphatab/rendering/MultiBarRestBeatContainerGlyph'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; import { BarBounds } from '@coderline/alphatab/rendering/utils/BarBounds'; import { BarHelpers } from '@coderline/alphatab/rendering/utils/BarHelpers'; +import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; import type { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import type { Settings } from '@coderline/alphatab/Settings'; -import type { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import { InternalSystemsLayoutMode } from '@coderline/alphatab/rendering/layout/ScoreLayout'; -import type { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { MultiBarRestBeatContainerGlyph } from '@coderline/alphatab/rendering/MultiBarRestBeatContainerGlyph'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** * Lists the different position modes for {@link BarRendererBase.getNoteY} @@ -86,28 +87,33 @@ export enum NoteXPosition { * @internal */ export class BarRendererBase { - private _preBeatGlyphs: LeftToRightLayoutingGlyphGroup = new LeftToRightLayoutingGlyphGroup(); - private _voiceContainers: Map = new Map(); - private _postBeatGlyphs: LeftToRightLayoutingGlyphGroup = new LeftToRightLayoutingGlyphGroup(); + private _preBeatGlyphs = new LeftToRightLayoutingGlyphGroup(); + protected readonly voiceContainer = new MultiVoiceContainerGlyph(); + private readonly _postBeatGlyphs = new LeftToRightLayoutingGlyphGroup(); + + private _ties: ITieGlyph[] = []; + + private _multiSystemSlurs?: ContinuationTieGlyph[]; - private _ties: Glyph[] = []; + public topEffects: EffectBandContainer; + public bottomEffects: EffectBandContainer; public get nextRenderer(): BarRendererBase | null { if (!this.bar || !this.bar.nextBar) { return null; } - return this.scoreRenderer.layout!.getRendererForBar(this.staff.staffId, this.bar.nextBar); + return this.scoreRenderer.layout!.getRendererForBar(this.staff!.staffId, this.bar.nextBar); } public get previousRenderer(): BarRendererBase | null { if (!this.bar || !this.bar.previousBar) { return null; } - return this.scoreRenderer.layout!.getRendererForBar(this.staff.staffId, this.bar.previousBar); + return this.scoreRenderer.layout!.getRendererForBar(this.staff!.staffId, this.bar.previousBar); } public scoreRenderer: ScoreRenderer; - public staff!: RenderStaff; + public staff?: RenderStaff; public layoutingInfo!: BarLayoutingInfo; public bar: Bar; public additionalMultiRestBars: Bar[] | null = null; @@ -125,9 +131,25 @@ export class BarRendererBase { public computedWidth: number = 0; public height: number = 0; public index: number = 0; - public topOverflow: number = 0; - public bottomOverflow: number = 0; - public helpers!: BarHelpers; + private _contentTopOverflow: number = 0; + private _contentBottomOverflow: number = 0; + + public beatEffectsMinY = Number.NaN; + public beatEffectsMaxY = Number.NaN; + + public get topOverflow() { + return this._contentTopOverflow + this.topEffects.height; + } + + public get bottomOverflow() { + return this._contentBottomOverflow + this.bottomEffects.height; + } + + protected helpers!: BarHelpers; + + public get collisionHelper() { + return this.helpers.collisionHelper; + } /** * Gets or sets whether this renderer is linked to the next one @@ -143,29 +165,41 @@ export class BarRendererBase { public canWrap: boolean = true; public get showMultiBarRest(): boolean { - return false; + return true; } public constructor(renderer: ScoreRenderer, bar: Bar) { this.scoreRenderer = renderer; this.bar = bar; - if (bar) { - this.helpers = new BarHelpers(this); - } + this.helpers = new BarHelpers(this); + this.topEffects = new EffectBandContainer(this, true); + this.bottomEffects = new EffectBandContainer(this, false); } - public registerTies(ties: Glyph[]) { - this._ties.push(...ties); + public registerTie(tie: ITieGlyph) { + this._ties.push(tie); } public get middleYPosition(): number { return 0; } + public registerBeatEffectOverflows(beatEffectsMinY: number, beatEffectsMaxY: number) { + const currentBeatEffectsMinY = this.beatEffectsMinY; + if (Number.isNaN(currentBeatEffectsMinY) || beatEffectsMinY < currentBeatEffectsMinY) { + this.beatEffectsMinY = beatEffectsMinY; + } + + const currentBeatEffectsMaxY = this.beatEffectsMaxY; + if (Number.isNaN(currentBeatEffectsMaxY) || beatEffectsMaxY > currentBeatEffectsMaxY) { + this.beatEffectsMaxY = beatEffectsMaxY; + } + } + public registerOverflowTop(topOverflow: number): boolean { topOverflow = Math.ceil(topOverflow); - if (topOverflow > this.topOverflow) { - this.topOverflow = topOverflow; + if (topOverflow > this._contentTopOverflow) { + this._contentTopOverflow = topOverflow; return true; } return false; @@ -173,8 +207,8 @@ export class BarRendererBase { public registerOverflowBottom(bottomOverflow: number): boolean { bottomOverflow = Math.ceil(bottomOverflow); - if (bottomOverflow > this.bottomOverflow) { - this.bottomOverflow = bottomOverflow; + if (bottomOverflow > this._contentBottomOverflow) { + this._contentBottomOverflow = bottomOverflow; return true; } return false; @@ -183,11 +217,19 @@ export class BarRendererBase { public scaleToWidth(width: number): void { // preBeat and postBeat glyphs do not get resized const containerWidth: number = width - this._preBeatGlyphs.width - this._postBeatGlyphs.width; - for (const container of this._voiceContainers.values()) { - container.scaleToWidth(containerWidth); + this.voiceContainer.scaleToWidth(containerWidth); + + for (const v of this.helpers.beamHelpers) { + for (const h of v) { + h.alignWithBeats(); + } } + this._postBeatGlyphs.x = this._preBeatGlyphs.x + this._preBeatGlyphs.width + containerWidth; this.width = width; + + this.topEffects.alignGlyphs(); + this.bottomEffects.alignGlyphs(); } public get resources(): RenderingResources { @@ -202,46 +244,29 @@ export class BarRendererBase { return this.scoreRenderer.settings; } - /** - * Gets the scale with which the bar should be displayed in case the model - * scale should be respected. - */ - public get barDisplayScale(): number { - return this.staff.system.staves.length > 1 ? this.bar.masterBar.displayScale : this.bar.displayScale; - } + protected wasFirstOfStaff: boolean = false; - /** - * Gets the absolute width in which the bar should be displayed in case the model - * scale should be respected. - */ - public get barDisplayWidth(): number { - return this.staff.system.staves.length > 1 ? this.bar.masterBar.displayWidth : this.bar.displayWidth; + public get isFirstOfStaff(): boolean { + return this.index === 0; } - protected wasFirstOfLine: boolean = false; - - public get isFirstOfLine(): boolean { - return this.index === 0; + public get isLastOfStaff(): boolean { + return this.index === this.staff!.barRenderers.length - 1; } public get isLast(): boolean { return !this.bar || this.bar.index === this.scoreRenderer.layout!.lastBarIndex; } - public registerLayoutingInfo(): void { + public _registerLayoutingInfo(): void { const info: BarLayoutingInfo = this.layoutingInfo; const preSize: number = this._preBeatGlyphs.width; if (info.preBeatSize < preSize) { info.preBeatSize = preSize; } - let postBeatStart = 0; - for (const container of this._voiceContainers.values()) { - container.registerLayoutingInfo(info); - const x: number = container.x + container.width; - if (postBeatStart < x) { - postBeatStart = x; - } - } + const container = this.voiceContainer; + container.registerLayoutingInfo(info); + const postSize: number = this._postBeatGlyphs.width; if (info.postBeatSize < postSize) { info.postBeatSize = postSize; @@ -250,64 +275,90 @@ export class BarRendererBase { private _appliedLayoutingInfo: number = 0; + public afterReverted() { + this.staff = undefined; + this.registerMultiSystemSlurs(undefined); + this.isFinalized = false; + } + + public afterStaffBarReverted() { + this.topEffects.afterStaffBarReverted(); + this.bottomEffects.afterStaffBarReverted(); + this._registerStaffOverflow(); + } + public applyLayoutingInfo(): boolean { if (this._appliedLayoutingInfo >= this.layoutingInfo.version) { return false; } + + this.topEffects.resetEffectBandSizingInfo(); + this.bottomEffects.resetEffectBandSizingInfo(); + this._appliedLayoutingInfo = this.layoutingInfo.version; // if we need additional space in the preBeat group we simply // add a new spacer this._preBeatGlyphs.width = this.layoutingInfo.preBeatSize; + // on beat glyphs we apply the glyph spacing - let voiceEnd: number = this._preBeatGlyphs.x + this._preBeatGlyphs.width; - for (const c of this._voiceContainers.values()) { - c.x = this._preBeatGlyphs.x + this._preBeatGlyphs.width; - c.applyLayoutingInfo(this.layoutingInfo); - const newEnd: number = c.x + c.width; - if (voiceEnd < newEnd) { - voiceEnd = newEnd; - } - } + const container = this.voiceContainer; + container.x = this._preBeatGlyphs.x + this._preBeatGlyphs.width; + container.applyLayoutingInfo(this.layoutingInfo); + // on the post glyphs we add the spacing before all other glyphs - this._postBeatGlyphs.x = Math.floor(voiceEnd); + this._postBeatGlyphs.x = Math.floor(container.x + container.width); this._postBeatGlyphs.width = this.layoutingInfo.postBeatSize; this.width = Math.ceil(this._postBeatGlyphs.x + this._postBeatGlyphs.width); this.computedWidth = this.width; - // For cases like in the horizontal layout we need to set the fixed width early - // to have correct partials splitting. the proper alignment to this scale will happen - // later in the workflow. - const fixedBarWidth = this.barDisplayWidth; - if ( - fixedBarWidth > 0 && - this.scoreRenderer.layout!.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithWidths - ) { - this.width = fixedBarWidth; - this.computedWidth = fixedBarWidth; - } + this.topEffects.sizeAndAlignEffectBands(); + this.bottomEffects.sizeAndAlignEffectBands(); + this._registerStaffOverflow(); return true; } public isFinalized: boolean = false; - public finalizeRenderer(): boolean { - this.isFinalized = true; + public registerMultiSystemSlurs(startedTies: Generator | undefined) { + if (!startedTies) { + this._multiSystemSlurs = undefined; + return; + } + let ties: ContinuationTieGlyph[] | undefined = undefined; + for (const g of startedTies) { + const continuation = new ContinuationTieGlyph(g); + continuation.renderer = this; + continuation.tieDirection = g.tieDirection; + + if (!ties) { + ties = []; + } + ties.push(continuation); + } + + this._multiSystemSlurs = ties; + } + + private _finalizeTies(ties: Iterable, barTop: number, barBottom: number): boolean { let didChangeOverflows = false; - // allow spacing to be used for tie overflows - const barTop = this.y - this.staff.topSpacing; - const barBottom = this.y + this.height + this.staff.bottomSpacing; - for (const tie of this._ties) { + for (const t of ties) { + const tie = t as unknown as Glyph; tie.doLayout(); - if (tie.height > 0) { - const bottomOverflow = tie.y + tie.height - barBottom; + + if (t.checkForOverflow) { + // NOTE: Ties are aligned on staff level, need to subtract the bar position + const tieTop = tie.getBoundingBoxTop(); + const tieBottom = tie.getBoundingBoxBottom(); + + const bottomOverflow = tieBottom - barBottom; if (bottomOverflow > 0) { if (this.registerOverflowBottom(bottomOverflow)) { didChangeOverflows = true; } } - const topOverflow = tie.y - barTop; + const topOverflow = tieTop - barTop; if (topOverflow < 0) { if (this.registerOverflowTop(topOverflow * -1)) { didChangeOverflows = true; @@ -315,22 +366,44 @@ export class BarRendererBase { } } } - return didChangeOverflows; } - /** - * Gets the top padding for the main content of the renderer. - * Can be used to specify where i.E. the score lines of the notation start. - * @returns - */ - public topPadding: number = 0; + public finalizeRenderer(): boolean { + this.isFinalized = true; - /** - * Gets the bottom padding for the main content of the renderer. - * Can be used to specify where i.E. the score lines of the notation end. - */ - public bottomPadding: number = 0; + let didChangeOverflows = false; + // allow spacing to be used for tie overflows + const barTop = this.y; + const barBottom = this.y + this.height; + + if (this._finalizeTies(this._ties, barTop, barBottom)) { + didChangeOverflows = true; + } + + const multiSystemSlurs = this._multiSystemSlurs; + if (multiSystemSlurs && this._finalizeTies(multiSystemSlurs, barTop, barBottom)) { + didChangeOverflows = true; + } + + const topHeightChanged = this.topEffects.finalizeEffects(); + const bottomHeightChanged = this.bottomEffects.finalizeEffects(); + if (topHeightChanged || bottomHeightChanged) { + didChangeOverflows = true; + } + + if (didChangeOverflows) { + this.updateSizes(); + this._registerStaffOverflow(); + } + + return didChangeOverflows; + } + + private _registerStaffOverflow() { + this.staff!.registerOverflowTop(this.topOverflow); + this.staff!.registerOverflowBottom(this.bottomOverflow); + } public doLayout(): void { if (!this.bar) { @@ -338,33 +411,26 @@ export class BarRendererBase { } this.helpers.initialize(); this._ties = []; - this._preBeatGlyphs = new LeftToRightLayoutingGlyphGroup(); this._preBeatGlyphs.renderer = this; - this._voiceContainers.clear(); - this._postBeatGlyphs = new LeftToRightLayoutingGlyphGroup(); + this.voiceContainer.renderer = this; this._postBeatGlyphs.renderer = this; - for (let i: number = 0; i < this.bar.voices.length; i++) { - const voice: Voice = this.bar.voices[i]; - if (this.hasVoiceContainer(voice)) { - const c: VoiceContainerGlyph = new VoiceContainerGlyph(0, 0, voice); - c.renderer = this; - this._voiceContainers.set(this.bar.voices[i].index, c); - } - } + this.topEffects.doLayout(); + this.bottomEffects.doLayout(); + if (this.bar.simileMark === SimileMark.SecondOfDouble) { this.canWrap = false; } + this.createPreBeatGlyphs(); + this.createBeatGlyphs(); + this.createPostBeatGlyphs(); - // multibar rest - if (this.additionalMultiRestBars) { - const container = new MultiBarRestBeatContainerGlyph(this.getVoiceContainer(this.bar.voices[0])!); - this.addBeatGlyph(container); - } else { - this.createBeatGlyphs(); - } + this._registerLayoutingInfo(); + + // registering happened during creation + this.topEffects.sizeAndAlignEffectBands(false); + this.bottomEffects.sizeAndAlignEffectBands(false); - this.createPostBeatGlyphs(); this.updateSizes(); // finish up all helpers @@ -376,8 +442,10 @@ export class BarRendererBase { this.computedWidth = this.width; - const rendererBottom = this.height; + this.calculateOverflows(0, this.height); + } + protected calculateOverflows(_rendererTop: number, rendererBottom: number) { const preBeatGlyphs = this._preBeatGlyphs.glyphs; if (preBeatGlyphs) { for (const g of preBeatGlyphs) { @@ -386,7 +454,7 @@ export class BarRendererBase { this.registerOverflowTop(topY * -1); } - const bottomY = topY + g.height; + const bottomY = g.getBoundingBoxBottom(); if (bottomY > rendererBottom) { this.registerOverflowBottom(bottomY - rendererBottom); } @@ -400,39 +468,53 @@ export class BarRendererBase { this.registerOverflowTop(topY * -1); } - const bottomY = topY + g.height; + const bottomY = g.getBoundingBoxBottom(); if (bottomY > rendererBottom) { this.registerOverflowBottom(bottomY - rendererBottom); } } } - } - protected hasVoiceContainer(voice: Voice): boolean { - if (this.additionalMultiRestBars || voice.index === 0) { - return true; + const v = this.voiceContainer; + const contentMinY = v.getBoundingBoxTop(); + if (contentMinY < 0) { + this.registerOverflowTop(contentMinY * -1); + } + + const contentMaxY = v.getBoundingBoxBottom(); + if (contentMaxY > rendererBottom) { + this.registerOverflowBottom(contentMaxY - rendererBottom); + } + + const beatEffectsMinY = this.beatEffectsMinY; + if (!Number.isNaN(beatEffectsMinY) && beatEffectsMinY < 0) { + this.registerOverflowTop(beatEffectsMinY * -1); + } + + const beatEffectsMaxY = this.beatEffectsMaxY; + if (!Number.isNaN(beatEffectsMaxY) && beatEffectsMaxY > rendererBottom) { + this.registerOverflowBottom(beatEffectsMaxY - rendererBottom); } - return !voice.isEmpty; } protected updateSizes(): void { - this.staff.registerStaffTop(this.topPadding); - this.staff.registerStaffBottom(this.height - this.bottomPadding); - const voiceContainers: Map = this._voiceContainers; - const beatGlyphsStart: number = this.beatGlyphsStart; - let postBeatStart: number = beatGlyphsStart; - for (const c of voiceContainers.values()) { - c.x = beatGlyphsStart; - c.doLayout(); - const x: number = c.x + c.width; - if (postBeatStart < x) { - postBeatStart = x; - } - } - this._postBeatGlyphs.x = Math.floor(postBeatStart); + this.staff!.registerStaffTop(0); + + this.voiceContainer.x = this._preBeatGlyphs.x + this._preBeatGlyphs.width; + this._postBeatGlyphs.x = Math.floor(this.voiceContainer.x + this.voiceContainer.width); + this.width = Math.ceil(this._postBeatGlyphs.x + this._postBeatGlyphs.width); + + const topHeightChanged = this.topEffects.updateEffectBandHeights(); + const bottomHeightChanged = this.bottomEffects.updateEffectBandHeights(); + if (topHeightChanged || bottomHeightChanged) { + this._registerStaffOverflow(); + } + this.height += this.layoutingInfo.height; this.height = Math.ceil(this.height); + + this.staff!.registerStaffBottom(this.height); } protected addPreBeatGlyph(g: Glyph): void { @@ -440,42 +522,49 @@ export class BarRendererBase { this._preBeatGlyphs.addGlyph(g); } - protected addBeatGlyph(g: BeatContainerGlyph): void { + protected addBeatGlyph(g: BeatContainerGlyphBase): void { g.renderer = this; - g.preNotes.renderer = this; - g.onNotes.renderer = this; - g.onNotes.beamingHelper = this.helpers.beamHelperLookup[g.beat.voice.index].get(g.beat.index)!; - this.getVoiceContainer(g.beat.voice)!.addGlyph(g); + this.voiceContainer.addGlyph(g); } - protected getVoiceContainer(voice: Voice): VoiceContainerGlyph | undefined { - return this._voiceContainers.has(voice.index) ? this._voiceContainers.get(voice.index) : undefined; + public getBeatContainer(beat: Beat): BeatContainerGlyphBase | undefined { + return this.voiceContainer.getBeatContainer(beat); } - public getBeatContainer(beat: Beat): BeatContainerGlyph | undefined { - return this.getVoiceContainer(beat.voice)?.beatGlyphs?.[beat.index]; - } + public paint(cx: number, cy: number, canvas: ICanvas): void { + // canvas.color = Color.random(); + // canvas.fillRect(cx + this.x, cy + this.y, this.width, this.height); - public getPreNotesGlyphForBeat(beat: Beat): BeatGlyphBase | undefined { - return this.getBeatContainer(beat)?.preNotes; - } + this.paintContent(cx, cy, canvas); - public getOnNotesGlyphForBeat(beat: Beat): BeatOnNoteGlyphBase | undefined { - return this.getBeatContainer(beat)?.onNotes; + const topEffectBandY = cy + this.y - this.staff!.topOverflow; + this.topEffects.paint(cx + this.x, topEffectBandY, canvas); + + const bottomEffectBandY = cy + this.y + this.height + this.staff!.bottomOverflow - this.bottomEffects.height; + this.bottomEffects.paint(cx + this.x, bottomEffectBandY, canvas); } - public paint(cx: number, cy: number, canvas: ICanvas): void { + protected paintContent(cx: number, cy: number, canvas: ICanvas): void { this.paintBackground(cx, cy, canvas); canvas.color = this.resources.mainGlyphColor; this._preBeatGlyphs.paint(cx + this.x, cy + this.y, canvas); + this.voiceContainer.paint(cx + this.x, cy + this.y, canvas); + canvas.color = this.resources.mainGlyphColor; + this._postBeatGlyphs.paint(cx + this.x, cy + this.y, canvas); + + this._paintMultiSystemSlurs(cx, cy, canvas); + } - for (const c of this._voiceContainers.values()) { - c.paint(cx + this.x, cy + this.y, canvas); + private _paintMultiSystemSlurs(cx: number, cy: number, canvas: ICanvas) { + const multiSystemSlurs = this._multiSystemSlurs; + if (!multiSystemSlurs) { + return; } - canvas.color = this.resources.mainGlyphColor; - this._postBeatGlyphs.paint(cx + this.x, cy + this.y, canvas); + for (const slur of multiSystemSlurs) { + slur.paint(cx, cy, canvas); + } } protected paintBackground(cx: number, cy: number, canvas: ICanvas): void { @@ -486,6 +575,7 @@ export class BarRendererBase { ); // canvas.color = Color.random(); // canvas.fillRect(cx + this.x, cy + this.y, this.width, this.height); + // canvas.strokeRect(cx + this.x, cy + this.y - this.topOverflow, this.width, this.height + this.topOverflow + this.bottomOverflow); } public buildBoundingsLookup(masterBarBounds: MasterBarBounds, cx: number, cy: number): void { @@ -493,9 +583,9 @@ export class BarRendererBase { barBounds.bar = this.bar; barBounds.visualBounds = new Bounds(); barBounds.visualBounds.x = cx + this.x; - barBounds.visualBounds.y = cy + this.y + this.topPadding; + barBounds.visualBounds.y = cy + this.y; barBounds.visualBounds.w = this.width; - barBounds.visualBounds.h = this.height - this.topPadding - this.bottomPadding; + barBounds.visualBounds.h = this.height; barBounds.realBounds = new Bounds(); barBounds.realBounds.x = cx + this.x; @@ -504,15 +594,7 @@ export class BarRendererBase { barBounds.realBounds.h = this.height; masterBarBounds.addBar(barBounds); - for (const [index, c] of this._voiceContainers) { - const isEmptyBar: boolean = this.bar.isEmpty && index === 0; - if (!c.voice.isEmpty || isEmptyBar) { - for (let i: number = 0, j: number = c.beatGlyphs.length; i < j; i++) { - const bc: BeatContainerGlyph = c.beatGlyphs[i]; - bc.buildBoundingsLookup(barBounds, cx + this.x + c.x, cy + this.y + c.y, isEmptyBar); - } - } - } + this.voiceContainer.buildBoundingsLookup(barBounds, cx + this.x, cy + this.y); } protected addPostBeatGlyph(g: Glyph): void { @@ -520,19 +602,29 @@ export class BarRendererBase { } protected createPreBeatGlyphs(): void { - this.wasFirstOfLine = this.isFirstOfLine; + this.wasFirstOfStaff = this.isFirstOfStaff; } protected createBeatGlyphs(): void { - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - this.createVoiceGlyphs(voice); + if (this.additionalMultiRestBars) { + const container = new MultiBarRestBeatContainerGlyph(); + this.addBeatGlyph(container); + } else { + for (const index of this.bar.filledVoices) { + this.createVoiceGlyphs(this.bar.voices[index]); } } + + this.voiceContainer.doLayout(); + + if (this.topEffects.isLinkedToPreviousRenderer || this.bottomEffects.isLinkedToPreviousRenderer) { + this.isLinkedToPrevious = true; + } } - protected createVoiceGlyphs(_v: Voice): void { - // filled in subclasses + protected createVoiceGlyphs(voice: Voice): void { + this.topEffects.createVoiceGlyphs(voice); + this.bottomEffects.createVoiceGlyphs(voice); } protected createPostBeatGlyphs(): void { @@ -540,75 +632,56 @@ export class BarRendererBase { } public get beatGlyphsStart(): number { - return this._preBeatGlyphs.x + this._preBeatGlyphs.width; + return this.voiceContainer.x; } public get postBeatGlyphsStart(): number { return this._postBeatGlyphs.x; } - public getBeatX(beat: Beat, requestedPosition: BeatXPosition = BeatXPosition.PreNotes): number { - const container = this.getBeatContainer(beat); - if (container) { - switch (requestedPosition) { - case BeatXPosition.PreNotes: - return container.voiceContainer.x + container.x; - case BeatXPosition.OnNotes: - return container.voiceContainer.x + container.x + container.onNotes.x; - case BeatXPosition.MiddleNotes: - return container.voiceContainer.x + container.x + container.onTimeX; - case BeatXPosition.Stem: - const offset = container.onNotes.beamingHelper - ? container.onNotes.beamingHelper.getBeatLineX(beat) - : container.onNotes.x + container.onNotes.width / 2; - return container.voiceContainer.x + offset; - case BeatXPosition.PostNotes: - return container.voiceContainer.x + container.x + container.onNotes.x + container.onNotes.width; - case BeatXPosition.EndBeat: - return container.voiceContainer.x + container.x + container.width; - } - } - return 0; + public getBeatX( + beat: Beat, + requestedPosition: BeatXPosition = BeatXPosition.PreNotes, + useSharedSizes: boolean = false + ): number { + return this.beatGlyphsStart + this.voiceContainer.getBeatX(beat, requestedPosition, useSharedSizes); } - public getRatioPositionX(ticks: number): number { + public getRatioPositionX(ratio: number): number { const firstOnNoteX = this.bar.isEmpty ? this.beatGlyphsStart - : this.getBeatX(this.bar.voices[0].beats[0], BeatXPosition.OnNotes); + : this.getBeatX(this.bar.voices[0].beats[0], BeatXPosition.MiddleNotes); const x = firstOnNoteX; const w = this.postBeatGlyphsStart - firstOnNoteX; - return x + w * ticks; + return x + w * ratio; } public getNoteX(note: Note, requestedPosition: NoteXPosition): number { - const container = this.getBeatContainer(note.beat); - if (container) { - return ( - container.voiceContainer.x + - container.x + - container.onNotes.x + - container.onNotes.getNoteX(note, requestedPosition) - ); - } - return 0; + return this.beatGlyphsStart + this.voiceContainer.getNoteX(note, requestedPosition); } public getNoteY(note: Note, requestedPosition: NoteYPosition): number { - const beat = this.getOnNotesGlyphForBeat(note.beat); - if (beat) { - return beat.getNoteY(note, requestedPosition); - } - return Number.NaN; + return this.voiceContainer.y + +this.voiceContainer.getNoteY(note, requestedPosition); + } + + public getRestY(beat: Beat, requestedPosition: NoteYPosition): number { + return this.voiceContainer.y + +this.voiceContainer.getRestY(beat, requestedPosition); } public reLayout(): void { + this.topEffects.reLayout(); + this.bottomEffects.reLayout(); + this.updateSizes(); + // there are some glyphs which are shown only for renderers at the line start, so we simply recreate them // but we only need to recreate them for the renderers that were the first of the line or are now the first of the line - if ((this.wasFirstOfLine && !this.isFirstOfLine) || (!this.wasFirstOfLine && this.isFirstOfLine)) { + if ((this.wasFirstOfStaff && !this.isFirstOfStaff) || (!this.wasFirstOfStaff && this.isFirstOfStaff)) { this.recreatePreBeatGlyphs(); + this._postBeatGlyphs.doLayout(); } - this.updateSizes(); - this.registerLayoutingInfo(); + + this._registerLayoutingInfo(); + this.calculateOverflows(0, this.height); } protected recreatePreBeatGlyphs() { @@ -656,8 +729,4 @@ export class BarRendererBase { public completeBeamingHelper(_helper: BeamingHelper) { // nothing by default } - - public getBeatDirection(beat: Beat): BeamDirection { - return this.helpers.getBeamingHelperForBeat(beat).direction; - } } diff --git a/packages/alphatab/src/rendering/BarRendererFactory.ts b/packages/alphatab/src/rendering/BarRendererFactory.ts index 6b3dedef0..c5d9bf268 100644 --- a/packages/alphatab/src/rendering/BarRendererFactory.ts +++ b/packages/alphatab/src/rendering/BarRendererFactory.ts @@ -2,22 +2,71 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Staff } from '@coderline/alphatab/model/Staff'; import type { Track } from '@coderline/alphatab/model/Track'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; + +/** + * The different modes on how effect bands are applied to bar renderers. + * @internal + */ +export enum EffectBandMode { + /** + * The band is owned by the specific renderer. + * If the owning renderer is not shown, the band will not be shown either. + * The band is shown on top of the main renderer. + */ + OwnedTop = 0, + /** + * The band is owned by the specific renderer. + * If the owning renderer is not shown, the band will not be shown either. + * The band is shown on bottom of the main renderer. + */ + OwnedBottom = 1, + /** + * The band is shared across renderers. + * If the owning renderer is shown, the band is shown on top the main renderer. + * If the renderer is not shown, the band is shown on the top of the next renderer which is visible. + * + * If no visible render follows, they are added to the bottom of the previous visible renderer. + */ + SharedTop = 2, + + /** + * The band is shared across renderers. + * If the owning renderer is shown, the band is shown on bottom of the main renderer. + * If the owning renderer is not shown, the band is shown on the **bottom** of the next renderer which is visible. + * + * If no visible render follows, they are added to the bottom of the previous visible renderer. + */ + SharedBottom = 3 +} + +/** + * @record + * @internal + */ +export interface EffectBandInfo { + mode: EffectBandMode; + effect: EffectInfo; + order?: number; + shouldCreate?: (staff: Staff) => boolean; +} /** * This is the base public class for creating factories providing BarRenderers * @internal */ export abstract class BarRendererFactory { - public isInsideBracket: boolean = true; - public isRelevantForBoundsLookup: boolean = true; public hideOnMultiTrack: boolean = false; public hideOnPercussionTrack: boolean = false; + public effectBands: EffectBandInfo[]; + public abstract get staffId(): string; - public abstract getStaffPaddingTop(staff: RenderStaff): number; - public abstract getStaffPaddingBottom(staff: RenderStaff): number; + + public constructor(effectBands: EffectBandInfo[]) { + this.effectBands = effectBands; + } public canCreate(_track: Track, staff: Staff): boolean { return !this.hideOnPercussionTrack || !staff.isPercussion; diff --git a/packages/alphatab/src/rendering/BeatXPosition.ts b/packages/alphatab/src/rendering/BeatXPosition.ts index 6e0460f98..fac482e65 100644 --- a/packages/alphatab/src/rendering/BeatXPosition.ts +++ b/packages/alphatab/src/rendering/BeatXPosition.ts @@ -12,7 +12,7 @@ export enum BeatXPosition { */ OnNotes = 1, /** - * Gets the middle-notes position which is located after in the middle the note heads. + * Gets the middle-notes position which is located after in the exact center of the note heads. */ MiddleNotes = 2, /** diff --git a/packages/alphatab/src/rendering/EffectBand.ts b/packages/alphatab/src/rendering/EffectBand.ts index 853c3c655..e408d8268 100644 --- a/packages/alphatab/src/rendering/EffectBand.ts +++ b/packages/alphatab/src/rendering/EffectBand.ts @@ -1,13 +1,13 @@ import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import type { Voice } from '@coderline/alphatab/model/Voice'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { EffectBandContainer } from '@coderline/alphatab/rendering/EffectBandContainer'; import type { EffectBandSlot } from '@coderline/alphatab/rendering/EffectBandSlot'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; -import type { EffectBarRenderer } from '@coderline/alphatab/rendering/EffectBarRenderer'; -import type { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import type { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import type { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** @@ -16,6 +16,7 @@ import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementS export class EffectBand extends Glyph { private _uniqueEffectGlyphs: EffectGlyph[][] = []; private _effectGlyphs: Map[] = []; + private _container: EffectBandContainer; public isEmpty: boolean = true; public previousBand: EffectBand | null = null; @@ -25,13 +26,26 @@ export class EffectBand extends Glyph { public override height: number = 0; public originalHeight: number = 0; public voice: Voice; - public info: EffectBarRendererInfo; + public info: EffectInfo; public slot: EffectBandSlot | null = null; - public constructor(voice: Voice, info: EffectBarRendererInfo) { + public constructor(voice: Voice, info: EffectInfo, container: EffectBandContainer) { super(0, 0); this.voice = voice; this.info = info; + this._container = container; + } + + public *iterateAllGlyphs() { + for (const v of this._effectGlyphs) { + for (const g of v.values()) { + yield g; + } + } + } + + public finalizeBand() { + this.info.finalizeBand(this); } public override doLayout(): void { @@ -42,16 +56,20 @@ export class EffectBand extends Glyph { } } + public static shouldCreateGlyph(beat: Beat, info: EffectInfo, renderer: BarRendererBase) { + return ( + info.shouldCreateGlyph(renderer.settings, beat) && + (!info.hideOnMultiTrack || renderer.staff!.trackIndex === 0) + ); + } + public createGlyph(beat: Beat): void { if (beat.voice !== this.voice) { return; } // NOTE: the track order will never change. even if the staff behind the renderer changes, the trackIndex will not. // so it's okay to access the staff here while creating the glyphs. - if ( - this.info.shouldCreateGlyph(this.renderer.settings, beat) && - (!this.info.hideOnMultiTrack || this.renderer.staff.trackIndex === 0) - ) { + if (EffectBand.shouldCreateGlyph(beat, this.info, this.renderer)) { this.isEmpty = false; if (!this.firstBeat || beat.isBefore(this.firstBeat)) { this.firstBeat = beat; @@ -118,9 +136,8 @@ export class EffectBand extends Glyph { prevEffect = this._effectGlyphs[b.voice.index].get(prevBeat.index)!; } else if (this.renderer.index > 0) { // load the effect from the previous renderer if possible. - const previousRenderer: EffectBarRenderer = this.renderer - .previousRenderer as EffectBarRenderer; - const previousBand = previousRenderer.getBand(prevBeat.voice, this.info.effectId); + const previousContainer = this._container.previousContainer!; + const previousBand = previousContainer.getBand(prevBeat.voice, this.info.effectId); // it can happen that we have an empty voice and then we don't have an effect band if (previousBand) { const voiceGlyphs: Map = @@ -174,31 +191,30 @@ export class EffectBand extends Glyph { public alignGlyphs(): void { for (let v: number = 0; v < this._effectGlyphs.length; v++) { for (const beatIndex of this._effectGlyphs[v].keys()) { - this._alignGlyph(this.info.sizingMode, this.renderer.bar.voices[v].beats[beatIndex]); + const g = this.renderer.bar.voices[v].beats[beatIndex]; + this._alignGlyph(this.info.sizingMode, g); } } + this.info.onAlignGlyphs(this); } private _alignGlyph(sizing: EffectBarGlyphSizing, beat: Beat): void { const g: EffectGlyph = this._effectGlyphs[beat.voice.index].get(beat.index)!; - const container: BeatContainerGlyph = this.renderer.getBeatContainer(beat)!; - - // container is aligned with the "onTimeX" position of the beat in effect renders + const container = this.renderer.getBeatContainer(beat)!; switch (sizing) { case EffectBarGlyphSizing.SinglePreBeat: - // shift to the start using the biggest pre-beat size of the respective beat const offsetToBegin = this.renderer.layoutingInfo.getPreBeatSize(beat); - g.x = this.renderer.beatGlyphsStart + container.x - offsetToBegin; + g.x = this.renderer.beatGlyphsStart + container.x + container.onTimeX - offsetToBegin; break; case EffectBarGlyphSizing.SingleOnBeat: case EffectBarGlyphSizing.GroupedOnBeat: - g.x = this.renderer.beatGlyphsStart + container.x; + g.x = this.renderer.beatGlyphsStart + container.x + container.onTimeX; break; case EffectBarGlyphSizing.SingleOnBeatToEnd: case EffectBarGlyphSizing.GroupedOnBeatToEnd: - g.x = this.renderer.beatGlyphsStart + container.x; - if (container.beat.isLastOfVoice) { + g.x = this.renderer.beatGlyphsStart + container.x + container.onTimeX; + if (container.isLastOfVoice) { g.width = this.renderer.width - g.x; } else { // shift to the start using the biggest post-beat size of the respective beat diff --git a/packages/alphatab/src/rendering/EffectBandContainer.ts b/packages/alphatab/src/rendering/EffectBandContainer.ts new file mode 100644 index 000000000..b0271b22f --- /dev/null +++ b/packages/alphatab/src/rendering/EffectBandContainer.ts @@ -0,0 +1,185 @@ +import type { Voice } from '@coderline/alphatab/model/Voice'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { EffectBandInfo } from '@coderline/alphatab/rendering/BarRendererFactory'; +import { EffectBand } from '@coderline/alphatab/rendering/EffectBand'; +import { EffectBandSizingInfo } from '@coderline/alphatab/rendering/EffectBandSizingInfo'; +import type { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; + +/** + * Wraps the whole effect band staff for having two times the same container + * holding bands (one for the top effects, one for the bottom effects) + * @internal + */ +export class EffectBandContainer { + private _bands: EffectBand[] = []; + private _bandLookup: Map = new Map(); + private _effectBandSizingInfo: EffectBandSizingInfo | null = null; + private _effectInfosSortOrder: Map = new Map(); + public height: number = 0; + + public infos!: EffectBandInfo[]; + private _renderer: BarRendererBase; + private _isTopContainer: boolean; + + public alignGlyphs() { + for (const effectBand of this._bands) { + effectBand.alignGlyphs(); + } + } + + public get previousContainer(): EffectBandContainer | undefined { + return this._renderer.index === 0 + ? undefined + : this._isTopContainer + ? this._renderer.previousRenderer!.topEffects + : this._renderer.previousRenderer!.bottomEffects; + } + + public get isLinkedToPreviousRenderer() { + return this._bands.some(b => b.isLinkedToPrevious); + } + + public constructor(renderer: BarRendererBase, isTopContainer: boolean) { + this._renderer = renderer; + this._isTopContainer = isTopContainer; + } + + public reLayout() { + this.resetEffectBandSizingInfo(); + this.sizeAndAlignEffectBands(); + } + + public afterStaffBarReverted() { + this.resetEffectBandSizingInfo(); + this.sizeAndAlignEffectBands(); + } + + public createVoiceGlyphs(voice: Voice) { + let i = 0; + const renderer = this._renderer; + const notationSettings = renderer.settings.notation; + for (const info of this.infos) { + if (!notationSettings.isNotationElementVisible(info.effect.notationElement)) { + continue; + } + + let band: EffectBand | undefined = undefined; + this._effectInfosSortOrder.set(info.effect, info.order ?? i); + + for (const b of voice.beats) { + // lazy create band to avoid creating and managing bands for all events + // even if only a few exist + if (!band && EffectBand.shouldCreateGlyph(b, info.effect, renderer)) { + band = new EffectBand(voice, info.effect, this); + band.renderer = this._renderer; + band.doLayout(); + this._bands.push(band); + this._bandLookup.set(`${voice.index}.${info.effect.effectId}`, band); + } + + if (band !== undefined) { + band.createGlyph(b); + } + } + i++; + } + } + + public doLayout() { + this._effectInfosSortOrder.clear(); + + this._bands = []; + this._bandLookup = new Map(); + this.resetEffectBandSizingInfo(); + } + + public resetEffectBandSizingInfo() { + if (this._renderer.index > 0) { + this._effectBandSizingInfo = this.previousContainer!._effectBandSizingInfo; + } else { + // try reusing current one to avoid GC pressure + if (this._effectBandSizingInfo && this._effectBandSizingInfo.owner === this) { + this._effectBandSizingInfo.reset(); + } else { + this._effectBandSizingInfo = new EffectBandSizingInfo(this); + } + } + } + + public finalizeEffects() { + return this._updateEffectBandHeights(true); + } + + public updateEffectBandHeights(): boolean { + return this._updateEffectBandHeights(false); + } + + private _updateEffectBandHeights(finalize: boolean): boolean { + if (!this._effectBandSizingInfo) { + return false; + } + + let y: number = 0; + // TODO. activate padding + // const paddingTop = this._isTopContainer ? 0 : this._renderer.settings.display.effectBandPaddingBottom; + // const paddingBottom = this._isTopContainer ? this._renderer.settings.display.effectBandPaddingBottom : 0; + const paddingTop = 0; + const paddingBottom = this._renderer.settings.display.effectBandPaddingBottom; + + for (const slot of this._effectBandSizingInfo.slots) { + slot.shared.y = y; + for (const band of slot.bands) { + y += paddingTop; + band.y = y; + if (finalize) { + band.finalizeBand(); + } + band.height = slot.shared.height; + } + y += slot.shared.height + paddingBottom; + } + y = Math.ceil(y); + + if (y !== this.height) { + this.height = y; + return true; + } + return false; + } + + public sizeAndAlignEffectBands(register: boolean = true) { + for (const effectBand of this._bands) { + effectBand.resetHeight(); + effectBand.alignGlyphs(); + if (register && !effectBand.isEmpty) { + // find a slot that ended before the start of the band + this._effectBandSizingInfo!.register(effectBand); + } + } + + // if we're registering new slots for the effects, we need to sort the + // slots afterwards to keep the registered order. we don't want the "first occured" effect on top but the "first registered" + if (register) { + this._effectBandSizingInfo!.sortSlots(this._effectInfosSortOrder); + } + } + + public paint(cx: number, cy: number, canvas: ICanvas) { + const resources = this._renderer.resources; + for (const effectBand of this._bands) { + canvas.color = effectBand.voice.index === 0 ? resources.mainGlyphColor : resources.secondaryGlyphColor; + if (!effectBand.isEmpty) { + effectBand.paint(cx, cy, canvas); + } + } + } + + public getBand(voice: Voice, effectId: string): EffectBand | null { + const id: string = `${voice.index}.${effectId}`; + if (this._bandLookup.has(id)) { + return this._bandLookup.get(id)!; + } + return null; + } +} diff --git a/packages/alphatab/src/rendering/EffectBandSizingInfo.ts b/packages/alphatab/src/rendering/EffectBandSizingInfo.ts index 850a1c641..37d37df06 100644 --- a/packages/alphatab/src/rendering/EffectBandSizingInfo.ts +++ b/packages/alphatab/src/rendering/EffectBandSizingInfo.ts @@ -1,35 +1,56 @@ import type { EffectBand } from '@coderline/alphatab/rendering/EffectBand'; +import type { EffectBandContainer } from '@coderline/alphatab/rendering/EffectBandContainer'; import { EffectBandSlot } from '@coderline/alphatab/rendering/EffectBandSlot'; +import type { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; /** * @internal */ export class EffectBandSizingInfo { private _effectSlot: Map; + private _assignedSlots: Map; public slots: EffectBandSlot[]; + public owner: EffectBandContainer; - public constructor() { + public constructor(owner: EffectBandContainer) { this.slots = []; this._effectSlot = new Map(); + this._assignedSlots = new Map(); + this.owner = owner; + } + + public reset() { + this._effectSlot.clear(); + this._assignedSlots.clear(); + this.slots = []; } public getOrCreateSlot(band: EffectBand): EffectBandSlot { + // check if we have already a slot + if (this._assignedSlots.has(band)) { + return this._assignedSlots.get(band)!; + } + // first check preferrable slot depending on type if (this._effectSlot.has(band.info.effectId)) { const slot: EffectBandSlot = this._effectSlot.get(band.info.effectId)!; if (slot.canBeUsed(band)) { + this._assignedSlots.set(band, slot); return slot; } } // find any slot that can be used for (const slot of this.slots) { if (slot.canBeUsed(band)) { + this._assignedSlots.set(band, slot); return slot; } } // create a new slot if required const newSlot: EffectBandSlot = new EffectBandSlot(); this.slots.push(newSlot); + this._assignedSlots.set(band, newSlot); + return newSlot; } @@ -38,4 +59,20 @@ export class EffectBandSizingInfo { freeSlot.update(effectBand); this._effectSlot.set(effectBand.info.effectId, freeSlot); } + + public sortSlots(sortOrder: Map) { + for (const s of this.slots) { + s.bands.sort((a, b) => { + const ai = sortOrder.get(a.info)!; + const bi = sortOrder.get(b.info)!; + return ai - bi; + }); + } + + this.slots.sort((a, b) => { + const ai = sortOrder.get(a.bands[0].info)!; + const bi = sortOrder.get(b.bands[0].info)!; + return ai - bi; + }); + } } diff --git a/packages/alphatab/src/rendering/EffectBandSlot.ts b/packages/alphatab/src/rendering/EffectBandSlot.ts index 8f40a4ea7..5fd62634d 100644 --- a/packages/alphatab/src/rendering/EffectBandSlot.ts +++ b/packages/alphatab/src/rendering/EffectBandSlot.ts @@ -44,12 +44,33 @@ export class EffectBandSlot { } public canBeUsed(band: EffectBand): boolean { - return ( - ((!this.shared.uniqueEffectId && band.info.canShareBand) || - band.info.effectId === this.shared.uniqueEffectId) && - (!this.shared.firstBeat || - this.shared.lastBeat!.isBefore(band.firstBeat!) || - this.shared.lastBeat!.isBefore(this.shared.firstBeat)) - ); + const canShareBand = + (!this.shared.uniqueEffectId && band.info.canShareBand) || + band.info.effectId === this.shared.uniqueEffectId; + if (!canShareBand) { + return false; + } + + // first beat in slot + if (!this.shared.firstBeat) { + return true; + } + + // beat is already added and this is an "extended" band connecting to the previous bar + if(this.shared.lastBeat === band.firstBeat){ + return true; + } + + // newly added band is after current beat + if (this.shared.lastBeat!.isBefore(band.firstBeat!)) { + return true; + } + + // historical case, doesn't make much sense, but let's keep it for now + if (this.shared.lastBeat!.isBefore(this.shared.firstBeat)) { + return true; + } + + return false; } } diff --git a/packages/alphatab/src/rendering/EffectBarRenderer.ts b/packages/alphatab/src/rendering/EffectBarRenderer.ts deleted file mode 100644 index 890b96a78..000000000 --- a/packages/alphatab/src/rendering/EffectBarRenderer.ts +++ /dev/null @@ -1,152 +0,0 @@ -import type { Bar } from '@coderline/alphatab/model/Bar'; -import type { Voice } from '@coderline/alphatab/model/Voice'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; -import { EffectBand } from '@coderline/alphatab/rendering/EffectBand'; -import { EffectBandSizingInfo } from '@coderline/alphatab/rendering/EffectBandSizingInfo'; -import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; -import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; -import type { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; -import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; - -/** - * This renderer is responsible for displaying effects above or below the other staves - * like the vibrato. - * @internal - */ -export class EffectBarRenderer extends BarRendererBase { - private _infos: EffectBarRendererInfo[]; - private _bands: EffectBand[] = []; - private _bandLookup: Map = new Map(); - public sizingInfo: EffectBandSizingInfo | null = null; - - public constructor(renderer: ScoreRenderer, bar: Bar, infos: EffectBarRendererInfo[]) { - super(renderer, bar); - this._infos = infos; - } - - protected override updateSizes(): void { - this.topOverflow = 0; - this.bottomOverflow = 0; - this.topPadding = 0; - this.bottomPadding = 0; - this._updateHeight(); - super.updateSizes(); - } - - public override finalizeRenderer(): boolean { - let didChange = super.finalizeRenderer(); - if (this._updateHeight()) { - didChange = true; - } - return didChange; - } - - private _updateHeight(): boolean { - if (!this.sizingInfo) { - return false; - } - let y: number = 0; - for (const slot of this.sizingInfo.slots) { - slot.shared.y = y; - for (const band of slot.bands) { - band.y = y; - band.height = slot.shared.height; - } - y += slot.shared.height + this.settings.display.effectBandPaddingBottom; - } - y = Math.ceil(y); - if (y !== this.height) { - this.height = y; - return true; - } - return false; - } - - public override applyLayoutingInfo(): boolean { - const result = !super.applyLayoutingInfo(); - // we create empty slots for the same group - if (this.index > 0) { - const previousRenderer: EffectBarRenderer = this.previousRenderer as EffectBarRenderer; - this.sizingInfo = previousRenderer.sizingInfo; - } else { - this.sizingInfo = new EffectBandSizingInfo(); - } - for (const effectBand of this._bands) { - effectBand.resetHeight(); - effectBand.alignGlyphs(); - if (!effectBand.isEmpty) { - // find a slot that ended before the start of the band - this.sizingInfo!.register(effectBand); - } - } - this._updateHeight(); - return result; - } - - public override scaleToWidth(width: number): void { - super.scaleToWidth(width); - for (const effectBand of this._bands) { - effectBand.alignGlyphs(); - } - } - - protected override createBeatGlyphs(): void { - this._bands = []; - this._bandLookup = new Map(); - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - for (const info of this._infos) { - const band: EffectBand = new EffectBand(voice, info); - band.renderer = this; - band.doLayout(); - this._bands.push(band); - this._bandLookup.set(`${voice.index}.${info.effectId}`, band); - } - } - } - super.createBeatGlyphs(); - for (const effectBand of this._bands) { - if (effectBand.isLinkedToPrevious) { - this.isLinkedToPrevious = true; - } - } - } - - protected override createVoiceGlyphs(v: Voice): void { - for (const b of v.beats) { - // we create empty glyphs as alignment references and to get the - // effect bar sized - const container: BeatContainerGlyph = new BeatContainerGlyph(b, this.getVoiceContainer(v)!); - container.preNotes = new BeatGlyphBase(); - container.onNotes = new BeatOnNoteGlyphBase(); - this.addBeatGlyph(container); - for (const effectBand of this._bands) { - effectBand.createGlyph(b); - } - } - } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - this.paintBackground(cx, cy, canvas); - // canvas.color = new Color(255, 0, 0, 100); - // canvas.fillRect(cx + this.x, cy + this.y, this.width, this.height); - - for (const effectBand of this._bands) { - canvas.color = - effectBand.voice.index === 0 ? this.resources.mainGlyphColor : this.resources.secondaryGlyphColor; - if (!effectBand.isEmpty) { - effectBand.paint(cx + this.x, cy + this.y, canvas); - } - } - } - - public getBand(voice: Voice, effectId: string): EffectBand | null { - const id: string = `${voice.index}.${effectId}`; - if (this._bandLookup.has(id)) { - return this._bandLookup.get(id)!; - } - return null; - } -} diff --git a/packages/alphatab/src/rendering/EffectBarRendererFactory.ts b/packages/alphatab/src/rendering/EffectBarRendererFactory.ts deleted file mode 100644 index 80f68d6e5..000000000 --- a/packages/alphatab/src/rendering/EffectBarRendererFactory.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Bar } from '@coderline/alphatab/model/Bar'; -import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; -import { BarRendererFactory } from '@coderline/alphatab/rendering/BarRendererFactory'; -import { EffectBarRenderer } from '@coderline/alphatab/rendering/EffectBarRenderer'; -import type { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; -import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; -import type { Staff } from '@coderline/alphatab/model/Staff'; -import type { Track } from '@coderline/alphatab/model/Track'; - -/** - * @internal - */ -export class EffectBarRendererFactory extends BarRendererFactory { - public infos: EffectBarRendererInfo[]; - private _staffId: string; - public get staffId(): string { - return this._staffId; - } - - public shouldShow: ((track: Track, staff: Staff) => boolean) | null; - - public override getStaffPaddingTop(staff: RenderStaff): number { - return staff.system.layout.renderer.settings.display.effectStaffPaddingTop; - } - - public override getStaffPaddingBottom(staff: RenderStaff): number { - return staff.system.layout.renderer.settings.display.effectStaffPaddingBottom; - } - - public constructor( - staffId: string, - infos: EffectBarRendererInfo[], - shouldShow: ((track: Track, staff: Staff) => boolean) | null = null - ) { - super(); - this.infos = infos; - this._staffId = staffId; - this.isInsideBracket = false; - this.isRelevantForBoundsLookup = false; - this.shouldShow = shouldShow; - } - - public override canCreate(track: Track, staff: Staff): boolean { - const shouldShow = this.shouldShow; - return super.canCreate(track, staff) && (!shouldShow || shouldShow(track, staff)); - } - - public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { - return new EffectBarRenderer( - renderer, - bar, - this.infos.filter(i => renderer.settings.notation.isNotationElementVisible(i.notationElement)) - ); - } -} diff --git a/packages/alphatab/src/rendering/EffectBarRendererInfo.ts b/packages/alphatab/src/rendering/EffectInfo.ts similarity index 82% rename from packages/alphatab/src/rendering/EffectBarRendererInfo.ts rename to packages/alphatab/src/rendering/EffectInfo.ts index d66380bbc..94a830077 100644 --- a/packages/alphatab/src/rendering/EffectBarRendererInfo.ts +++ b/packages/alphatab/src/rendering/EffectInfo.ts @@ -1,16 +1,17 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; +import type { NotationElement } from '@coderline/alphatab/NotationSettings'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { EffectBand } from '@coderline/alphatab/rendering/EffectBand'; import type { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import type { Settings } from '@coderline/alphatab/Settings'; -import type { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * A classes inheriting from this base can provide the * data needed by a EffectBarRenderer to create effect glyphs dynamically. * @internal */ -export abstract class EffectBarRendererInfo { +export abstract class EffectInfo { /** * Gets the unique effect name for this effect. (Used for grouping) */ @@ -69,4 +70,17 @@ export abstract class EffectBarRendererInfo { * @returns true if the glyph can be expanded, false if a new glyph needs to be created. */ public abstract canExpand(from: Beat, to: Beat): boolean; + + /** + * Override this method to finalize an effect band with all glyphs created. + * Allows special layout logic like for whammys where we center-align the glyphs and size the band accordingly. + * @param _band The band which is being finalized. + */ + public finalizeBand(_band: EffectBand): void {} + + /** + * Override this method when glyphs are for this effect is being re-aligned during resizing. + * @param _band The band holding the glyph + */ + public onAlignGlyphs(_band: EffectBand) {} } diff --git a/packages/alphatab/src/rendering/IScoreRenderer.ts b/packages/alphatab/src/rendering/IScoreRenderer.ts index 8e902780e..0a071a9c7 100644 --- a/packages/alphatab/src/rendering/IScoreRenderer.ts +++ b/packages/alphatab/src/rendering/IScoreRenderer.ts @@ -5,6 +5,22 @@ import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/Rend import type { BoundsLookup } from '@coderline/alphatab/rendering/utils/BoundsLookup'; import type { Settings } from '@coderline/alphatab/Settings'; +/** + * Additional hints which should be respected during layout and rendering of the score. + * @public + */ +export interface RenderHints { + /** + * A value indicating whether the currently rendered viewport can be reused when rendering the score. + * @remarks + * Set this property to true in cases of live-editing where the rendered score changes from an object perspective, + * but remains the similar from a content perspective. This way the visual update will appear more smooth than a full clearing. + * + * internally it might still be decided to clear the viewport. + */ + reuseViewport?: boolean; +} + /** * Represents the public interface of the component that can render scores. * @public @@ -31,9 +47,10 @@ export interface IScoreRenderer { /** * Initiates a re-rendering of the current setup. + * @param renderHints Additional hints to respect during layouting and rendering. * @since 0.9.6 */ - render(): void; + render(renderHints?: RenderHints): void; /** * Initiates a resize-optimized re-rendering of the score using the current settings. @@ -53,9 +70,10 @@ export interface IScoreRenderer { * Initiates the rendering of the specified tracks of the given score. * @param score The score defining the tracks. * @param trackIndexes The indexes of the tracks to draw. + * @param renderHints Additional hints to respect during layouting and rendering. * @since 0.9.6 */ - renderScore(score: Score | null, trackIndexes: number[] | null): void; + renderScore(score: Score | null, trackIndexes: number[] | null, renderHints?: RenderHints): void; /** * Requests the rendering of a chunk which was layed out before. diff --git a/packages/alphatab/src/rendering/LineBarRenderer.ts b/packages/alphatab/src/rendering/LineBarRenderer.ts index 94bcc4f6f..5c3d11ebf 100644 --- a/packages/alphatab/src/rendering/LineBarRenderer.ts +++ b/packages/alphatab/src/rendering/LineBarRenderer.ts @@ -1,23 +1,25 @@ -import { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; -import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; -import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; -import { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { NotationMode } from '@coderline/alphatab/NotationSettings'; -import { FlagGlyph } from '@coderline/alphatab/rendering/glyphs/FlagGlyph'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { BarLineGlyph } from '@coderline/alphatab/rendering/glyphs/BarLineGlyph'; -import { RepeatCountGlyph } from '@coderline/alphatab/rendering/glyphs/RepeatCountGlyph'; -import { BarNumberGlyph } from '@coderline/alphatab/rendering/glyphs/BarNumberGlyph'; -import { type Beat, BeatBeamingMode, type BeatSubElement } from '@coderline/alphatab/model/Beat'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import type { BarSubElement } from '@coderline/alphatab/model/Bar'; +import { type Beat, BeatBeamingMode, type BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; -import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import type { Note } from '@coderline/alphatab/model/Note'; +import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; +import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; +import { NotationElement, NotationMode } from '@coderline/alphatab/NotationSettings'; +import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; +import { BarRendererBase, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import { BarLineGlyph } from '@coderline/alphatab/rendering/glyphs/BarLineGlyph'; +import { BarNumberGlyph } from '@coderline/alphatab/rendering/glyphs/BarNumberGlyph'; +import { FlagGlyph } from '@coderline/alphatab/rendering/glyphs/FlagGlyph'; +import { RepeatCountGlyph } from '@coderline/alphatab/rendering/glyphs/RepeatCountGlyph'; +import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import { BeamingHelper, BeamingHelperDrawInfo } from '@coderline/alphatab/rendering/utils/BeamingHelper'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** * This is a base class for any bar renderer which renders music notation on a staff @@ -30,7 +32,7 @@ import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; export abstract class LineBarRenderer extends BarRendererBase { protected firstLineY: number = 0; private _startSpacing = false; - protected tupletSize: number = 0; + public tupletSize: number = 0; public get lineOffset(): number { return this.lineSpacing; @@ -45,17 +47,15 @@ export abstract class LineBarRenderer extends BarRendererBase { public abstract get drawnLineCount(): number; protected get topGlyphOverflow() { - return this.smuflMetrics.oneStaffSpace; + return 0; } protected get bottomGlyphOverflow() { - return this.smuflMetrics.oneStaffSpace; + return 0; } protected initLineBasedSizes() { - this.topPadding = this.topGlyphOverflow; - this.bottomPadding = this.bottomGlyphOverflow; - this.height = this.lineOffset * (this.heightLineCount - 1) + this.topPadding + this.bottomPadding; + this.height = this.lineOffset * (this.heightLineCount - 1); } protected override updateSizes(): void { @@ -72,10 +72,10 @@ export abstract class LineBarRenderer extends BarRendererBase { protected updateFirstLineY() { const fullLineHeight = this.lineOffset * (this.heightLineCount - 1); - const actualLineHeight = (this.drawnLineCount - 1) * this.lineOffset; + const actualLineHeight = this.drawnLineCount === 0 ? 0 : (this.drawnLineCount - 1) * this.lineOffset; const lineYOffset = this.smuflMetrics.staffLineThickness / 2; - this.firstLineY = ((this.topPadding + (fullLineHeight - actualLineHeight) / 2) | 0) - lineYOffset; + this.firstLineY = (((fullLineHeight - actualLineHeight) / 2) | 0) - lineYOffset; } public override doLayout(): void { @@ -93,6 +93,16 @@ export abstract class LineBarRenderer extends BarRendererBase { return this.lineOffset * line; } + protected abstract get flagsSubElement(): BeatSubElement; + protected abstract get beamsSubElement(): BeatSubElement; + protected abstract get tupletSubElement(): BeatSubElement; + + protected override paintContent(cx: number, cy: number, canvas: ICanvas): void { + super.paintContent(cx, cy, canvas); + this.paintBeams(cx, cy, canvas, this.flagsSubElement, this.beamsSubElement); + this.paintTuplets(cx, cy, canvas, this.tupletSubElement); + } + protected override paintBackground(cx: number, cy: number, canvas: ICanvas): void { super.paintBackground(cx, cy, canvas); // canvas.color = Color.random(100); @@ -130,7 +140,7 @@ export abstract class LineBarRenderer extends BarRendererBase { // during system fitting it can happen that we have fraction widths // but to have lines until the full end-pixel we round up. - // this way we avoid holes + // this way we avoid holes, const lineWidth = this.width; // we want the lines to be exactly virtually aligned with the respective Y-position @@ -163,14 +173,15 @@ export abstract class LineBarRenderer extends BarRendererBase { // override in subclasses } - protected createStartSpacing(): void { + protected createStartSpacing(): boolean { if (this._startSpacing) { - return; + return false; } const padding = this.index === 0 ? this.settings.display.firstStaffPaddingLeft : this.settings.display.staffPaddingLeft; this.addPreBeatGlyph(new SpacingGlyph(0, 0, padding)); this._startSpacing = true; + return true; } protected paintTuplets( @@ -180,29 +191,31 @@ export abstract class LineBarRenderer extends BarRendererBase { beatElement: BeatSubElement, bracketsAsArcs: boolean = false ): void { - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - const container = this.getVoiceContainer(voice)!; - for (const tupletGroup of container.tupletGroups) { - this._paintTupletHelper( - cx + this.beatGlyphsStart, - cy, - canvas, - tupletGroup, - beatElement, - bracketsAsArcs - ); + for (const v of this.voiceContainer.voiceDrawOrder!) { + if (this.voiceContainer.tupletGroups.has(v)) { + const voice = this.voiceContainer.tupletGroups.get(v)!; + for (const tupletGroup of voice) { + this._paintTupletHelper(cx, cy, canvas, tupletGroup, beatElement, bracketsAsArcs); } } } } - protected abstract getBeamDirection(helper: BeamingHelper): BeamDirection; + protected abstract getBeamDirection(_helper: BeamingHelper): BeamDirection; + + public getBeatDirection(beat: Beat): BeamDirection { + const helper = this.helpers.getBeamingHelperForBeat(beat); + return helper ? this.getBeamDirection(helper) : BeamDirection.Up; + } + protected getTupletBeamDirection(helper: BeamingHelper): BeamDirection { return this.getBeamDirection(helper); } - protected abstract calculateBeamYWithDirection(h: BeamingHelper, x: number, direction: BeamDirection): number; + protected calculateBeamYWithDirection(h: BeamingHelper, x: number, direction: BeamDirection): number { + this.ensureBeamDrawingInfo(h, direction); + return h.drawingInfos.get(direction)!.calcY(x); + } private _paintTupletHelper( cx: number, @@ -267,6 +280,7 @@ export abstract class LineBarRenderer extends BarRendererBase { // check if we need to paint simple footer const offset: number = this.tupletOffset; const size: number = this.tupletSize; + const shift = offset + size * 0.5; using _ = ElementStyleHelper.beat(canvas, beatElement, h.beats[0]); @@ -275,23 +289,23 @@ export abstract class LineBarRenderer extends BarRendererBase { if (h.beats.length === 1 || !h.isFull) { for (const beat of h.beats) { - const beamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(beat.index)!; + const beamingHelper = this.helpers.getBeamingHelperForBeat(beat); if (!beamingHelper) { continue; } const direction: BeamDirection = this.getTupletBeamDirection(beamingHelper); - const tupletX: number = beamingHelper.getBeatLineX(beat); + const tupletX: number = this.getBeatX(beat, BeatXPosition.Stem); let tupletY: number = this.calculateBeamYWithDirection(beamingHelper, tupletX, direction); if (direction === BeamDirection.Down) { - tupletY += offset + size; + tupletY += shift; } else { - tupletY -= offset + size; + tupletY -= shift; } - canvas.fillMusicFontSymbols(cx + this.x + tupletX, cy + this.y + tupletY, 1, s, true); + canvas.fillMusicFontSymbols(cx + this.x + tupletX, cy + this.y + tupletY + size * 0.5, 1, s, true); } } else { const firstBeat: Beat = h.beats[0]; @@ -324,13 +338,13 @@ export abstract class LineBarRenderer extends BarRendererBase { // // Calculate the overall area of the tuplet bracket - const startX: number = this.getBeatX(firstBeat, BeatXPosition.OnNotes) - this.beatGlyphsStart; - const endX: number = this.getBeatX(lastBeat, BeatXPosition.PostNotes) - this.beatGlyphsStart; + const startX: number = this.getBeatX(firstBeat, BeatXPosition.OnNotes); + const endX: number = this.getBeatX(lastBeat, BeatXPosition.PostNotes); // // calculate the y positions for our bracket - const firstNonRestBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(firstNonRestBeat.index)!; - const lastNonRestBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(lastNonRestBeat.index)!; + const firstNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(firstNonRestBeat)!; + const lastNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(lastNonRestBeat)!; const direction = this.getTupletBeamDirection(firstNonRestBeamingHelper); let startY: number = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction); let endY: number = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction); @@ -340,7 +354,6 @@ export abstract class LineBarRenderer extends BarRendererBase { } // align line centered in available space - const shift = offset + size * 0.5; if (direction === BeamDirection.Down) { startY += shift; endY += shift; @@ -423,9 +436,9 @@ export abstract class LineBarRenderer extends BarRendererBase { flagsElement: BeatSubElement, beamsElement: BeatSubElement ): void { - for (const v of this.helpers.beamHelpers) { - for (const h of v) { - this._paintBeamHelper(cx + this.beatGlyphsStart, cy, canvas, h, flagsElement, beamsElement); + for (const v of this.voiceContainer.voiceDrawOrder!) { + for (const h of this.helpers.beamHelpers[v]) { + this.paintBeamHelper(cx, cy, canvas, h, flagsElement, beamsElement); } } } @@ -434,7 +447,33 @@ export abstract class LineBarRenderer extends BarRendererBase { return h.beats.length === 1; } - private _paintBeamHelper( + public hasFlag(beat: Beat) { + if (beat.isRest) { + return false; + } + + const helper = this.helpers.getBeamingHelperForBeat(beat); + if (helper) { + return helper.hasFlag(this.drawBeamHelperAsFlags(helper), beat); + } + + return BeamingHelper.beatHasFlag(beat); + } + + public hasStem(beat: Beat) { + if (beat.isRest) { + return false; + } + + const helper = this.helpers.getBeamingHelperForBeat(beat); + if (helper) { + return helper.hasStem(this.drawBeamHelperAsFlags(helper), beat); + } + + return BeamingHelper.beatHasStem(beat); + } + + protected paintBeamHelper( cx: number, cy: number, canvas: ICanvas, @@ -443,7 +482,7 @@ export abstract class LineBarRenderer extends BarRendererBase { beamsElement: BeatSubElement ): void { canvas.color = h.voice!.index === 0 ? this.resources.mainGlyphColor : this.resources.secondaryGlyphColor; - if (!h.isRestBeamHelper) { + if (this.shouldPaintBeamingHelper(h)) { if (this.drawBeamHelperAsFlags(h)) { this.paintFlag(cx, cy, canvas, h, flagsElement); } else { @@ -452,9 +491,14 @@ export abstract class LineBarRenderer extends BarRendererBase { } } + protected shouldPaintBeamingHelper(h: BeamingHelper) { + return !h.isRestBeamHelper; + } + protected abstract getFlagTopY(beat: Beat, direction: BeamDirection): number; protected abstract getFlagBottomY(beat: Beat, direction: BeamDirection): number; - protected shouldPaintFlag(beat: Beat, h: BeamingHelper): boolean { + + protected shouldPaintFlag(beat: Beat): boolean { // no flags for bend grace beats if (beat.graceType === GraceType.BendGrace) { return false; @@ -464,11 +508,6 @@ export abstract class LineBarRenderer extends BarRendererBase { return false; } - // we don't have an X-position: cannot paint a flag - if (!h.hasBeatLineX(beat)) { - return false; - } - // no flags for any grace notes on songbook mode if (beat.graceType !== GraceType.None && this.settings.notation.notationMode === NotationMode.SongBook) { return false; @@ -488,7 +527,7 @@ export abstract class LineBarRenderer extends BarRendererBase { protected paintFlag(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper, flagsElement: BeatSubElement): void { for (const beat of h.beats) { - if (!this.shouldPaintFlag(beat, h)) { + if (!this.shouldPaintFlag(beat)) { continue; } @@ -496,7 +535,7 @@ export abstract class LineBarRenderer extends BarRendererBase { // // draw line // - const beatLineX: number = h.getBeatLineX(beat); + const beatLineX: number = this.getBeatX(beat, BeatXPosition.Stem); const direction: BeamDirection = this.getBeamDirection(h); const topY: number = cy + this.y + this.getFlagTopY(beat, direction); const bottomY: number = cy + this.y + this.getFlagBottomY(beat, direction); @@ -507,7 +546,7 @@ export abstract class LineBarRenderer extends BarRendererBase { flagY = topY; } - if (!h.hasLine(true, beat)) { + if (!h.hasStem(true, beat)) { continue; } @@ -541,7 +580,7 @@ export abstract class LineBarRenderer extends BarRendererBase { cx + this.x + beatLineX + flagWidth / 2, (topY + bottomY - this.smuflMetrics.glyphHeights.get(MusicFontSymbol.GraceNoteSlashStemDown)!) / 2, - NoteHeadGlyph.GraceScale, + EngravingSettings.GraceScale, MusicFontSymbol.GraceNoteSlashStemDown, true ); @@ -551,7 +590,7 @@ export abstract class LineBarRenderer extends BarRendererBase { cx + this.x + beatLineX + flagWidth / 2, (topY + bottomY + this.smuflMetrics.glyphHeights.get(MusicFontSymbol.GraceNoteSlashStemUp)!) / 2, - NoteHeadGlyph.GraceScale, + EngravingSettings.GraceScale, MusicFontSymbol.GraceNoteSlashStemUp, true ); @@ -569,44 +608,50 @@ export abstract class LineBarRenderer extends BarRendererBase { canvas: ICanvas ): void; - protected getFlagStemSize(duration: Duration, forceMinStem: boolean = false): number { - let size: number = 0; - - switch (duration) { - case Duration.QuadrupleWhole: - case Duration.Half: - case Duration.Quarter: - case Duration.Eighth: - case Duration.Sixteenth: - case Duration.ThirtySecond: - case Duration.SixtyFourth: - case Duration.OneHundredTwentyEighth: - case Duration.TwoHundredFiftySixth: - size = this.smuflMetrics.standardStemLength + this.smuflMetrics.stemFlagOffsets.get(duration)!; - break; - default: - size = forceMinStem ? this.smuflMetrics.standardStemLength : 0; - break; - } - return size; - } - protected override recreatePreBeatGlyphs(): void { this._startSpacing = false; super.recreatePreBeatGlyphs(); } - protected abstract getBarLineStart(beat: Beat, direction: BeamDirection): number; - public calculateBeamY(h: BeamingHelper, x: number): number { return this.calculateBeamYWithDirection(h, x, this.getBeamDirection(h)); } protected override createPreBeatGlyphs(): void { super.createPreBeatGlyphs(); - this.addPreBeatGlyph(new BarLineGlyph(false)); + this.addPreBeatGlyph(new BarLineGlyph(false, this.bar.staff.track.score.stylesheet.extendBarLines)); this.createLinePreBeatGlyphs(); - this.addPreBeatGlyph(new BarNumberGlyph(0, this.getLineHeight(-0.25), this.bar.index + 1)); + let hasSpaceAfterStartGlyphs = false; + if (this.index === 0) { + hasSpaceAfterStartGlyphs = this.createStartSpacing(); + } + + if (this.shouldCreateBarNumber()) { + this.addPreBeatGlyph(new BarNumberGlyph(0, this.getLineHeight(-0.5), this.bar.index + 1)); + } else if (!hasSpaceAfterStartGlyphs) { + this.addPreBeatGlyph(new SpacingGlyph(0, 0, this.smuflMetrics.oneStaffSpace)); + } + } + + public shouldCreateBarNumber(): boolean { + let display = BarNumberDisplay.AllBars; + if (!this.settings.notation.isNotationElementVisible(NotationElement.BarNumber)) { + display = BarNumberDisplay.Hide; + } else if (this.bar.barNumberDisplay !== undefined) { + display = this.bar.barNumberDisplay!; + } else { + display = this.bar.staff.track.score.stylesheet.barNumberDisplay; + } + + switch (display) { + case BarNumberDisplay.AllBars: + return true; + case BarNumberDisplay.FirstOfSystem: + return this.isFirstOfStaff; + case BarNumberDisplay.Hide: + return false; + } + return true; } protected abstract createLinePreBeatGlyphs(): void; @@ -615,10 +660,14 @@ export abstract class LineBarRenderer extends BarRendererBase { super.createPostBeatGlyphs(); const lastBar = this.lastBar; - this.addPostBeatGlyph(new BarLineGlyph(true)); + this.addPostBeatGlyph(new BarLineGlyph(true, this.bar.staff.track.score.stylesheet.extendBarLines)); - if (lastBar.masterBar.isRepeatEnd && lastBar.masterBar.repeatCount > 2) { - this.addPostBeatGlyph(new RepeatCountGlyph(0, this.getLineHeight(-0.25), this.bar.masterBar.repeatCount)); + if ( + lastBar.masterBar.isRepeatEnd && + lastBar.masterBar.repeatCount > 2 && + this.settings.notation.isNotationElementVisible(NotationElement.RepeatCount) + ) { + this.addPostBeatGlyph(new RepeatCountGlyph(0, this.getLineHeight(-0.5), this.bar.masterBar.repeatCount)); } } @@ -630,9 +679,9 @@ export abstract class LineBarRenderer extends BarRendererBase { protected paintBar(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper, beamsElement: BeatSubElement): void { const direction: BeamDirection = this.getBeamDirection(h); const isGrace: boolean = h.graceType !== GraceType.None; - const scaleMod: number = isGrace ? NoteHeadGlyph.GraceScale : 1; - let barSpacing: number = (this.smuflMetrics.beamSpacing + this.smuflMetrics.beamThickness) * scaleMod; - let barSize: number = this.smuflMetrics.beamThickness * scaleMod; + const scaleMod: number = isGrace ? EngravingSettings.GraceScale : 1; + let barSpacing: number = (this.beamSpacing + this.beamThickness) * scaleMod; + let barSize: number = this.beamThickness * scaleMod; if (direction === BeamDirection.Down) { barSpacing = -barSpacing; barSize = -barSize; @@ -640,31 +689,42 @@ export abstract class LineBarRenderer extends BarRendererBase { for (let i: number = 0, j: number = h.beats.length; i < j; i++) { const beat: Beat = h.beats[i]; - if (!h.hasBeatLineX(beat) || beat.deadSlapped) { + if (beat.deadSlapped) { continue; } - const beatLineX: number = h.getBeatLineX(beat); - const y1: number = cy + this.y + this.getBarLineStart(beat, direction); + const stemX: number = this.getBeatX(beat, BeatXPosition.Stem); + let y1: number = cy + this.y; + if (direction === BeamDirection.Up) { + y1 += this.getFlagBottomY(beat, direction); + } else { + y1 += this.getFlagTopY(beat, direction); + } - // ensure we are pixel aligned on the end of the stem to avoid anti-aliasing artifacts - // when combining stems and beams on sub-pixel level - const y2: number = (cy + this.y + this.calculateBeamY(h, beatLineX)) | 0; + const y2: number = cy + this.y + this.calculateBeamY(h, stemX); + // improve subpixel related artifacts on stem/beam overlaps + let stemY1: number; + let stemY2: number; if (y1 < y2) { - this.paintBeamingStem(beat, cy + this.y, cx + this.x + beatLineX, y1, y2, canvas); + stemY1 = y1; + stemY2 = y2; } else { - this.paintBeamingStem(beat, cy + this.y, cx + this.x + beatLineX, y2, y1, canvas); + stemY1 = y2; + stemY2 = y1; } + this.paintBeamingStem(beat, cy + this.y, cx + this.x + stemX, stemY1, stemY2, canvas); + using _ = ElementStyleHelper.beat(canvas, beamsElement, beat); const brokenBarOffset: number = this.smuflMetrics.brokenBeamWidth * scaleMod; const barCount: number = ModelUtils.getIndex(beat.duration) - 2; const barStart: number = cy + this.y; + const stemThickness = this.smuflMetrics.stemThickness; for (let barIndex: number = 0; barIndex < barCount; barIndex++) { - let barStartX: number = 0; + let barStartX: number = Math.floor(stemX + stemThickness); let barEndX: number = 0; let barStartY: number = 0; let barEndY: number = 0; @@ -683,8 +743,7 @@ export abstract class LineBarRenderer extends BarRendererBase { beat.beamingMode === BeatBeamingMode.ForceSplitOnSecondaryToNext ) { // start part - barStartX = beatLineX; - barEndX = barStartX + brokenBarOffset; + barEndX = Math.ceil(barStartX + brokenBarOffset); barStartY = barY + this.calculateBeamY(h, barStartX); barEndY = barY + this.calculateBeamY(h, barEndX); LineBarRenderer.paintSingleBar( @@ -697,8 +756,8 @@ export abstract class LineBarRenderer extends BarRendererBase { ); // end part - barEndX = h.getBeatLineX(h.beats[i + 1]); - barStartX = barEndX - brokenBarOffset; + barEndX = Math.floor(this.getBeatX(h.beats[i + 1], BeatXPosition.Stem)); + barStartX = Math.floor(barEndX - brokenBarOffset); barStartY = barY + this.calculateBeamY(h, barStartX); barEndY = barY + this.calculateBeamY(h, barEndX); LineBarRenderer.paintSingleBar( @@ -712,24 +771,15 @@ export abstract class LineBarRenderer extends BarRendererBase { } else { if (isFullBarJoin) { // full bar? - barStartX = beatLineX; - barEndX = h.getBeatLineX(h.beats[i + 1]); + barEndX = Math.ceil(this.getBeatX(h.beats[i + 1], BeatXPosition.Stem)); } else if (i === 0 || !BeamingHelper.isFullBarJoin(h.beats[i - 1], beat, barIndex)) { - barStartX = beatLineX; - barEndX = barStartX + brokenBarOffset; + barEndX = Math.ceil(barStartX + brokenBarOffset); } else { continue; } barStartY = barY + this.calculateBeamY(h, barStartX); barEndY = barY + this.calculateBeamY(h, barEndX); - // ensure we are pixel aligned on the end of the stem to avoid anti-aliasing artifacts - // when combining stems and beams on sub-pixel level - if (barIndex === 0) { - barStartY = barStartY | 0; - barEndY = barEndY | 0; - } - LineBarRenderer.paintSingleBar( canvas, cx + this.x + barStartX, @@ -740,9 +790,8 @@ export abstract class LineBarRenderer extends BarRendererBase { ); } } else if (i > 0 && !BeamingHelper.isFullBarJoin(beat, h.beats[i - 1], barIndex)) { - barStartX = beatLineX - brokenBarOffset; - barEndX = beatLineX; - barEndX = beatLineX; + barEndX = Math.ceil(stemX); + barStartX = Math.floor(stemX - brokenBarOffset); barStartY = barY + this.calculateBeamY(h, barStartX); barEndY = barY + this.calculateBeamY(h, barEndX); LineBarRenderer.paintSingleBar( @@ -757,9 +806,23 @@ export abstract class LineBarRenderer extends BarRendererBase { } } + // const firstStartX = this.getBeatX(h.beats[0], BeatXPosition.Stem); + // const firstStartY = this.calculateBeamY(h, firstStartX); + // const lastEndX = this.getBeatX(h.beats[h.beats.length - 1], BeatXPosition.Stem); + // const lastEndY = this.calculateBeamY(h, lastEndX); + // canvas.lineWidth = 0.5; + // const c = canvas.color; + // canvas.color = new Color(255, 0, 0); + // canvas.moveTo(cx + this.x + firstStartX, cy + this.y + firstStartY); + // canvas.lineTo(cx + this.x + lastEndX, cy + this.y + lastEndY); + // canvas.stroke(); + // canvas.lineWidth = 1; + // canvas.color = c; + if (h.graceType === GraceType.BeforeBeat) { - const beatLineX: number = h.getBeatLineX(h.beats[0]); - const flagWidth = this.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * NoteHeadGlyph.GraceScale; + const beatLineX: number = this.getBeatX(h.beats[0], BeatXPosition.Stem); + const flagWidth = + this.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * EngravingSettings.GraceScale; let slashY: number = (cy + this.y + this.calculateBeamY(h, beatLineX)) | 0; slashY += barSize + barSpacing; @@ -768,7 +831,7 @@ export abstract class LineBarRenderer extends BarRendererBase { canvas, cx + this.x + beatLineX + flagWidth / 2, slashY, - NoteHeadGlyph.GraceScale, + EngravingSettings.GraceScale, MusicFontSymbol.GraceNoteSlashStemDown, true ); @@ -777,7 +840,7 @@ export abstract class LineBarRenderer extends BarRendererBase { canvas, cx + this.x + beatLineX + flagWidth / 2, slashY, - NoteHeadGlyph.GraceScale, + EngravingSettings.GraceScale, MusicFontSymbol.GraceNoteSlashStemUp, true ); @@ -801,4 +864,336 @@ export abstract class LineBarRenderer extends BarRendererBase { canvas.closePath(); canvas.fill(); } + + protected calculateBeamingOverflows(rendererTop: number, rendererBottom: number) { + let maxNoteY = 0; + let minNoteY = 0; + + for (const v of this.helpers.beamHelpers) { + for (const h of v) { + if (!this.shouldPaintBeamingHelper(h)) { + // no visible helper + } + // notes with stems (and potential flags) + else if (h.beats.length === 1 && h.beats[0].duration >= Duration.Half) { + const tupletDirection = this.getTupletBeamDirection(h); + const direction = this.getBeamDirection(h); + const flagOverflow = this.smuflMetrics.stemFlagOffsets.get(h.beats[0].duration)!; + if (direction === BeamDirection.Up) { + let topY = this.getFlagTopY(h.beats[0], direction) - flagOverflow; + if (h.hasTuplet && tupletDirection === direction) { + topY -= this.tupletSize + this.tupletOffset; + } + if (topY < maxNoteY) { + maxNoteY = topY; + } + + if (h.hasTuplet && tupletDirection !== direction) { + let bottomY = this.getFlagBottomY(h.beats[0], tupletDirection); + bottomY += this.tupletSize + this.tupletOffset; + + if (bottomY > minNoteY) { + minNoteY = bottomY; + } + } + + // bottom handled via beat container bBox + } else { + let bottomY = this.getFlagBottomY(h.beats[0], direction) + flagOverflow; + if (h.hasTuplet && tupletDirection === direction) { + bottomY += this.tupletSize + this.tupletOffset; + } + if (bottomY > minNoteY) { + minNoteY = bottomY; + } + + if (h.hasTuplet && tupletDirection !== direction) { + let topY = this.getFlagTopY(h.beats[0], tupletDirection); + topY -= this.tupletSize + this.tupletOffset; + + if (topY < maxNoteY) { + maxNoteY = topY; + } + } + + // top handled via beat container bBox + } + } + // beamed notes and notes without stems + // (see paintTuplets in case of doubts how we handle tuplets on non beamed notes) + else { + const direction = this.getBeamDirection(h); + this.ensureBeamDrawingInfo(h, direction); + const drawingInfo = h.drawingInfos.get(direction)!; + const tupletDirection = this.getTupletBeamDirection(h); + + if (direction === BeamDirection.Up) { + let topY = Math.min(drawingInfo.startY, drawingInfo.endY); + if (h.hasTuplet && tupletDirection === direction) { + topY -= this.tupletSize + this.tupletOffset; + } + + if (topY < maxNoteY) { + maxNoteY = topY; + } + + let bottomY: number = this.voiceContainer.getLowestNoteY( + h.beatOfLowestNote, + NoteYPosition.Bottom + ); + if (h.hasTuplet && tupletDirection !== direction) { + bottomY += this.tupletSize + this.tupletOffset; + } + + if (bottomY > minNoteY) { + minNoteY = bottomY; + } + } else { + let bottomY = Math.max(drawingInfo.startY, drawingInfo.endY); + + if (h.hasTuplet && tupletDirection === direction) { + bottomY += this.tupletSize + this.tupletOffset; + } + + if (bottomY > minNoteY) { + minNoteY = bottomY; + } + + let topY: number = this.voiceContainer.getHighestNoteY(h.beatOfHighestNote, NoteYPosition.Top); + if (h.hasTuplet && tupletDirection !== direction) { + topY -= this.tupletSize + this.tupletOffset; + } + + if (topY < maxNoteY) { + maxNoteY = topY; + } + } + } + } + } + + if (maxNoteY < rendererTop) { + this.registerOverflowTop(Math.abs(maxNoteY)); + } + + if (minNoteY > rendererBottom) { + this.registerOverflowBottom(Math.abs(minNoteY) - rendererBottom); + } + } + + protected initializeBeamDrawingInfo(h: BeamingHelper, direction: BeamDirection) { + const drawingInfo = new BeamingHelperDrawInfo(); + + const firstBeat = h.beats[0]; + const lastBeat = h.beats[h.beats.length - 1]; + + // 1. put direct diagonal line. + drawingInfo.startBeat = firstBeat; + drawingInfo.startX = this.getBeatX(firstBeat, BeatXPosition.Stem); + drawingInfo.startY = + direction === BeamDirection.Up + ? this.getFlagTopY(firstBeat, direction) + : this.getFlagBottomY(firstBeat, direction); + + drawingInfo.endBeat = lastBeat; + drawingInfo.endX = this.getBeatX(lastBeat, BeatXPosition.Stem); + drawingInfo.endY = + direction === BeamDirection.Up + ? this.getFlagTopY(lastBeat, direction) + : this.getFlagBottomY(lastBeat, direction); + + // 2. ensure max slope + // we use the min/max notes to place the beam along their real position + // we only want a maximum of 10 offset for their gradient + const maxSlope: number = this.smuflMetrics.oneStaffSpace; + if ( + direction === BeamDirection.Down && + drawingInfo.startY > drawingInfo.endY && + drawingInfo.startY - drawingInfo.endY > maxSlope + ) { + drawingInfo.endY = drawingInfo.startY - maxSlope; + } + if ( + direction === BeamDirection.Down && + drawingInfo.endY > drawingInfo.startY && + drawingInfo.endY - drawingInfo.startY > maxSlope + ) { + drawingInfo.startY = drawingInfo.endY - maxSlope; + } + if ( + direction === BeamDirection.Up && + drawingInfo.startY < drawingInfo.endY && + drawingInfo.endY - drawingInfo.startY > maxSlope + ) { + drawingInfo.endY = drawingInfo.startY + maxSlope; + } + if ( + direction === BeamDirection.Up && + drawingInfo.endY < drawingInfo.startY && + drawingInfo.startY - drawingInfo.endY > maxSlope + ) { + drawingInfo.startY = drawingInfo.endY + maxSlope; + } + + return drawingInfo; + } + + protected get beamSpacing() { + return this.smuflMetrics.beamSpacing; + } + protected get beamThickness() { + return this.smuflMetrics.beamThickness; + } + + protected ensureBeamDrawingInfo(h: BeamingHelper, direction: BeamDirection): void { + if (h.drawingInfos.has(direction)) { + return; + } + + // the beaming logic works like this: + // 1. we take the first and last note, add the stem, and put a diagnal line between them. + // 2. the height of the diagonal line must not exceed a max height, + // - if this is the case, the line on the more distant note just gets longer + // 3. any middle elements (notes or rests) shift this diagonal line up/down to avoid overlaps + + const drawingInfo = this.initializeBeamDrawingInfo(h, direction); + h.drawingInfos.set(direction, drawingInfo); + + const barCount: number = ModelUtils.getIndex(h.shortestDuration) - 2; + + // 3. adjust beam drawing order + // we can only draw up to 2 beams towards the noteheads, then we have to grow to the other side + // here we shift accordingly + const barDrawingShift = this.applyBarShift(h, direction, drawingInfo, barCount); + + // 4. let middle elements shift up/down + if (h.beats.length > 1) { + // check if highest note shifts bar up or down + if (direction === BeamDirection.Up) { + const yNeededForHighestNote = barDrawingShift + this.getFlagTopY(h.beatOfHighestNote, direction); + const yGivenByCurrentValues = drawingInfo.calcY(this.getBeatX(h.beatOfHighestNote, BeatXPosition.Stem)); + + const diff = yGivenByCurrentValues - yNeededForHighestNote; + if (diff > 0) { + drawingInfo.startY -= diff; + drawingInfo.endY -= diff; + } + } else { + const yNeededForLowestNote = barDrawingShift + this.getFlagBottomY(h.beatOfLowestNote, direction); + const yGivenByCurrentValues = drawingInfo.calcY(this.getBeatX(h.beatOfLowestNote, BeatXPosition.Stem)); + + const diff = yNeededForLowestNote - yGivenByCurrentValues; + if (diff > 0) { + drawingInfo.startY += diff; + drawingInfo.endY += diff; + } + } + + // check if rest shifts bar up or down + let barSpacing = 0; + if (h.restBeats.length > 0) { + // space needed for the bars, rests need to be below them + const scaleMod: number = h.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; + barSpacing = barCount * (this.beamSpacing + this.beamThickness) * scaleMod; + } + + for (const b of h.restBeats) { + // rest beats which are "under" the beam + if (b.isRest && b.index < h.beats[h.beats.length - 1].index) { + if (direction === BeamDirection.Up) { + const yNeededForRest = this.getBeatContainer(b)!.getBoundingBoxTop() - barSpacing; + const yGivenByCurrentValues = drawingInfo.calcY(this.getBeatX(b, BeatXPosition.Stem)); + + const diff = yGivenByCurrentValues - yNeededForRest; + if (diff > 0) { + drawingInfo.startY -= diff; + drawingInfo.endY -= diff; + } + } else if (direction === BeamDirection.Down) { + const yNeededForRest = this.getBeatContainer(b)!.getBoundingBoxBottom() + barSpacing; + const yGivenByCurrentValues = drawingInfo.calcY(this.getBeatX(b, BeatXPosition.Stem)); + + const diff = yNeededForRest - yGivenByCurrentValues; + if (diff > 0) { + drawingInfo.startY += diff; + drawingInfo.endY += diff; + } + } + } + } + + // check if slash shifts bar up or down + if (h.slashBeats.length > 0) { + for (const b of h.slashBeats) { + const yGivenByCurrentValues = drawingInfo.calcY(this.getBeatX(b, BeatXPosition.Stem)); + const yNeededForSlash = + direction === BeamDirection.Up + ? this.getFlagTopY(b, direction) + : this.getFlagBottomY(b, direction); + + const diff = yNeededForSlash - yGivenByCurrentValues; + if (diff > 0) { + drawingInfo.startY += diff; + drawingInfo.endY += diff; + } + } + } + } + + // avoid subpixel induced problems by rounding + if (direction === BeamDirection.Up) { + drawingInfo.startY = Math.round(drawingInfo.startY); + drawingInfo.endY = Math.round(drawingInfo.endY); + } else { + drawingInfo.startY = Math.round(drawingInfo.startY); + drawingInfo.endY = Math.round(drawingInfo.endY); + } + } + protected applyBarShift( + h: BeamingHelper, + direction: BeamDirection, + drawingInfo: BeamingHelperDrawInfo, + barCount: number + ) { + let barDrawingShift = 0; + const isRest = h.isRestBeamHelper; + const scale = h.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; + + if (barCount > 2 && !isRest) { + const beamSpacing = this.beamSpacing * scale; + const beamThickness = this.beamThickness * scale; + const totalBarsHeight = barCount * beamThickness + (barCount - 1) * beamSpacing; + + if (direction === BeamDirection.Up) { + const bottomBarY = drawingInfo.startY + 2 * beamThickness + beamSpacing; + const barTopY = bottomBarY - totalBarsHeight; + const diff = drawingInfo.startY - barTopY; + if (diff > 0) { + barDrawingShift = diff * -1; + drawingInfo.startY -= diff; + drawingInfo.endY -= diff; + } + } else { + const topBarY = drawingInfo.startY - 2 * beamThickness + beamSpacing; + const barBottomY = topBarY + totalBarsHeight; + const diff = barBottomY - drawingInfo.startY; + if (diff > 0) { + barDrawingShift = diff; + drawingInfo.startY += diff; + drawingInfo.endY += diff; + } + } + } + return barDrawingShift; + } + + protected getMinLineOfBeat(_beat: Beat): number { + return 0; + } + + protected getMaxLineOfBeat(_beat: Beat): number { + return 0; + } + + public abstract getNoteLine(note: Note): number; } diff --git a/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts b/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts index c45f1a4eb..b657c47db 100644 --- a/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts @@ -1,34 +1,158 @@ -import { Beat } from '@coderline/alphatab/model/Beat'; -import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; -import type { VoiceContainerGlyph } from '@coderline/alphatab/rendering/glyphs/VoiceContainerGlyph'; -import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import type { GraceGroup } from '@coderline/alphatab/model/GraceGroup'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import type { Note } from '@coderline/alphatab/model/Note'; +import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import { BeatContainerGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { MultiBarRestGlyph } from '@coderline/alphatab/rendering/glyphs/MultiBarRestGlyph'; +import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; +import type { BarBounds } from '@coderline/alphatab/rendering/utils/BarBounds'; /** * @internal */ -export class MultiBarRestBeatContainerGlyph extends BeatContainerGlyph { - public constructor(voiceContainer: VoiceContainerGlyph) { - super(MultiBarRestBeatContainerGlyph._getOrCreatePlaceholderBeat(voiceContainer), voiceContainer); - this.preNotes = new BeatGlyphBase(); - this.onNotes = new BeatOnNoteGlyphBase(); +export class MultiBarRestBeatContainerGlyph extends BeatContainerGlyphBase { + private _glyph?: MultiBarRestGlyph; + + public constructor() { + super(0, 0); + } + + public override get absoluteDisplayStart(): number { + return this.renderer.bar.masterBar.start; + } + public override get beatId(): number { + return -1; + } + + public override get onTimeX(): number { + return 0; + } + public override get graceType(): GraceType { + return GraceType.None; + } + public override get graceIndex(): number { + return 0; + } + public override get graceGroup(): GraceGroup | null { + return null; + } + public override get voiceIndex(): number { + return 0; + } + public override get isFirstOfTupletGroup(): boolean { + return false; + } + public override get tupletGroup(): TupletGroup | null { + return null; + } + public override get isLastOfVoice(): boolean { + return true; + } + + public override get displayDuration(): number { + return 0; + } + + public override getRestY(requestedPosition: NoteYPosition): number { + const g = this._glyph; + if (g) { + switch (requestedPosition) { + case NoteYPosition.Top: + return g.y; + case NoteYPosition.TopWithStem: + return g.y - this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); + case NoteYPosition.Center: + case NoteYPosition.StemUp: + case NoteYPosition.StemDown: + return g.y + g.height / 2; + case NoteYPosition.Bottom: + return g.y + g.height; + case NoteYPosition.BottomWithStem: + return g.y + g.height + this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); + } + } + return 0; + } + + public override getNoteY(_note: Note, requestedPosition: NoteYPosition): number { + return this.getRestY(requestedPosition); } + public override getHighestNoteY(position: NoteYPosition): number { + return this.getRestY(position); + } + + public override getLowestNoteY(position: NoteYPosition): number { + return this.getRestY(position); + } + + public override getNoteX(_note: Note, requestedPosition: NoteXPosition): number { + const g = this._glyph; + if (g) { + switch (requestedPosition) { + case NoteXPosition.Left: + return g.x; + case NoteXPosition.Center: + return g.x + g.width / 2; + case NoteXPosition.Right: + return g.x + g.width; + } + } + return 0; + } + + public override getBeatX(requestedPosition: BeatXPosition, _useSharedSizes: boolean): number { + const g = this._glyph; + if (g) { + switch (requestedPosition) { + case BeatXPosition.PreNotes: + return g.x; + case BeatXPosition.OnNotes: + case BeatXPosition.MiddleNotes: + case BeatXPosition.Stem: + case BeatXPosition.PostNotes: + return g.x + g.width; + case BeatXPosition.EndBeat: + return this.width; + } + } + return 0; + } + public override registerLayoutingInfo(layoutings: BarLayoutingInfo): void { + const width = this._glyph?.width ?? 0; + layoutings.addBeatSpring(this, 0, width); + } + + public override applyLayoutingInfo(_info: BarLayoutingInfo): void {} + + public override buildBoundingsLookup(_barBounds: BarBounds, _cx: number, _cy: number): void {} + public override doLayout(): void { if (this.renderer.showMultiBarRest) { - this.onNotes.addNormal(new MultiBarRestGlyph()); + this._glyph = new MultiBarRestGlyph(); + this._glyph.renderer = this.renderer; + this._glyph.doLayout(); + this.width = this._glyph.width; } + } - super.doLayout(); + public override doMultiVoiceLayout(): void { + // nothing to do } - private static _getOrCreatePlaceholderBeat(voiceContainer: VoiceContainerGlyph): Beat { - if (voiceContainer.voice.beats.length > 1) { - return voiceContainer.voice.beats[0]; - } - const placeholder = new Beat(); - placeholder.voice = voiceContainer.voice; - return placeholder; + public override getBoundingBoxTop(): number { + return this._glyph?.getBoundingBoxTop() ?? Number.NaN; + } + + public override getBoundingBoxBottom(): number { + return this._glyph?.getBoundingBoxBottom() ?? Number.NaN; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + this._glyph?.paint(cx + this.x, cy + this.y, canvas); } } diff --git a/packages/alphatab/src/rendering/NumberedBarRenderer.ts b/packages/alphatab/src/rendering/NumberedBarRenderer.ts index 366cf72ee..1e6a29a6c 100644 --- a/packages/alphatab/src/rendering/NumberedBarRenderer.ts +++ b/packages/alphatab/src/rendering/NumberedBarRenderer.ts @@ -1,27 +1,29 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; import { type Bar, BarSubElement } from '@coderline/alphatab/model/Bar'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; import type { Voice } from '@coderline/alphatab/model/Voice'; -import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; -import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; -import { NumberedBeatContainerGlyph } from '@coderline/alphatab/rendering/NumberedBeatContainerGlyph'; -import { NumberedBeatGlyph, NumberedBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedBeatGlyph'; -import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTimeSignatureGlyph'; -import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; -import { NumberedKeySignatureGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedKeySignatureGlyph'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import { BarLineGlyph } from '@coderline/alphatab/rendering/glyphs/BarLineGlyph'; import { BarNumberGlyph } from '@coderline/alphatab/rendering/glyphs/BarNumberGlyph'; +import { + NumberedDashBeatContainerGlyph, + NumberedNoteBeatContainerGlyphBase +} from '@coderline/alphatab/rendering/glyphs/NumberedDashBeatContainerGlyph'; +import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTimeSignatureGlyph'; +import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; +import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; +import { NumberedBeatContainerGlyph } from '@coderline/alphatab/rendering/NumberedBeatContainerGlyph'; +import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import type { BeamingHelper, BeamingHelperDrawInfo } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { BarLineGlyph } from '@coderline/alphatab/rendering/glyphs/BarLineGlyph'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; /** * This BarRenderer renders a bar using (Jianpu) Numbered Music Notation @@ -34,26 +36,11 @@ export class NumberedBarRenderer extends LineBarRenderer { private _isOnlyNumbered: boolean; public shortestDuration = Duration.QuadrupleWhole; - public lowestOctave: number | null = null; - public highestOctave: number | null = null; + get dotSpacing(): number { return this.smuflMetrics.glyphHeights.get(MusicFontSymbol.AugmentationDot)! * 2; } - public registerOctave(octave: number) { - if (this.lowestOctave === null) { - this.lowestOctave = octave; - this.highestOctave = octave; - } else { - if (octave < this.lowestOctave!) { - this.lowestOctave = octave; - } - if (octave > this.highestOctave!) { - this.highestOctave = octave; - } - } - } - public override get repeatsBarSubElement(): BarSubElement { return BarSubElement.NumberedRepeats; } @@ -91,55 +78,20 @@ export class NumberedBarRenderer extends LineBarRenderer { return 0; } - public override paint(cx: number, cy: number, canvas: ICanvas): void { - super.paint(cx, cy, canvas); - this.paintBeams(cx, cy, canvas, BeatSubElement.NumberedDuration, BeatSubElement.NumberedDuration); - this.paintTuplets(cx, cy, canvas, BeatSubElement.NumberedTuplet, true); + protected override get flagsSubElement(): BeatSubElement { + return BeatSubElement.NumberedDuration; } - public override doLayout(): void { - super.doLayout(); - let hasTuplets: boolean = false; - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - const c = this.getVoiceContainer(voice)!; - if (c.tupletGroups.length > 0) { - hasTuplets = true; - break; - } - } - } - if (hasTuplets) { - this.registerOverflowTop(this.tupletSize); - } - - if (!this.bar.isEmpty) { - const barCount: number = ModelUtils.getIndex(this.shortestDuration) - 2; - const dotSpacing = this.dotSpacing; - if (barCount > 0) { - const barSpacing: number = this.smuflMetrics.numberedBarRendererBarSpacing; - const barSize: number = this.smuflMetrics.numberedBarRendererBarSize; - const barOverflow = (barCount - 1) * barSpacing + barSize; - - let dotOverflow = 0; - const lowestOctave = this.lowestOctave; - if (lowestOctave !== null) { - dotOverflow = - Math.abs(lowestOctave) * dotSpacing + - this.smuflMetrics.glyphHeights.get(MusicFontSymbol.AugmentationDot)!; - } + protected override get beamsSubElement(): BeatSubElement { + return BeatSubElement.NumberedDuration; + } - this.registerOverflowBottom(barOverflow + dotOverflow); - } + protected override get tupletSubElement(): BeatSubElement { + return BeatSubElement.NumberedTuplet; + } - const highestOctave = this.highestOctave; - if (highestOctave !== null) { - const dotOverflow = - Math.abs(highestOctave) * dotSpacing + - this.smuflMetrics.glyphHeights.get(MusicFontSymbol.AugmentationDot)!; - this.registerOverflowTop(dotOverflow); - } - } + protected override shouldPaintBeamingHelper(_h: BeamingHelper): boolean { + return true; } protected override paintFlag( @@ -159,128 +111,162 @@ export class NumberedBarRenderer extends LineBarRenderer { h: BeamingHelper, flagsElement: BeatSubElement ): void { - if (h.beats.length === 0) { + if (h.beats.length === 0 || h.graceType !== GraceType.None) { return; } - const res = this.resources; for (let i: number = 0, j: number = h.beats.length; i < j; i++) { const beat: Beat = h.beats[i]; using _ = ElementStyleHelper.beat(canvas, flagsElement, beat); - // - // draw line - // - const barSpacing: number = this.smuflMetrics.numberedBarRendererBarSpacing; - const barSize: number = this.smuflMetrics.numberedBarRendererBarSize; - const barCount: number = ModelUtils.getIndex(beat.duration) - 2; - const barStart: number = cy + this.y; + const direction: BeamDirection = this.getBeamDirection(h); + const isGrace: boolean = h.graceType !== GraceType.None; + const scaleMod: number = isGrace ? EngravingSettings.GraceScale : 1; - const beatLineX: number = this.getBeatX(beat, BeatXPosition.PreNotes) - this.beatGlyphsStart; + let barSpacing: number = (this.beamSpacing + this.beamThickness) * scaleMod; + let barSize = this.beamThickness * scaleMod; + if (direction === BeamDirection.Down) { + barSpacing = -barSpacing; + barSize = -barSize; + } - const beamY = this.calculateBeamY(h, beatLineX); + let barCount: number = ModelUtils.getIndex(beat.duration) - 2; + let beatLineX: number = this.getBeatX(beat, BeatXPosition.PreNotes); + + let barStartX: number = 0; + let barEndX: number = 0; + if (i === h.beats.length - 1) { + barStartX = beatLineX; + barEndX = this.getBeatX(beat, BeatXPosition.PostNotes); + } else { + barStartX = beatLineX; + barEndX = this.getBeatX(h.beats[i + 1], BeatXPosition.PreNotes); + } + const barStart: number = cy + this.y + this.calculateBeamY(h, beatLineX); for (let barIndex: number = 0; barIndex < barCount; barIndex++) { - let barStartX: number = 0; - let barEndX: number = 0; - let barStartY: number = 0; const barY: number = barStart + barIndex * barSpacing; - if (i === h.beats.length - 1) { - barStartX = beatLineX; - barEndX = this.getBeatX(beat, BeatXPosition.PostNotes) - this.beatGlyphsStart; - } else { - barStartX = beatLineX; - barEndX = this.getBeatX(h.beats[i + 1], BeatXPosition.PreNotes) - this.beatGlyphsStart; - } - barStartY = barY + beamY; - canvas.fillRect(cx + this.x + barStartX, barStartY, barEndX - barStartX, barSize); + LineBarRenderer.paintSingleBar( + canvas, + cx + this.x + barStartX, + barY, + cx + this.x + barEndX, + barY, + barSize + ); } - const onNotes = this.getBeatContainer(beat)!.onNotes; - let dotCount = onNotes instanceof NumberedBeatGlyph ? (onNotes as NumberedBeatGlyph).octaveDots : 0; - const dotSpacing = this.dotSpacing; - let dotsY = 0; - let dotsOffset = 0; - if (dotCount > 0) { - dotsY = barStart + this.getLineY(0) - res.numberedNotationFont.size; - dotsOffset = dotSpacing * -1; - } else if (dotCount < 0) { - dotsY = barStart + beamY + barCount * (barSpacing + barSize); - dotsOffset = dotSpacing; + // dashes for additional numbers + const container = this.voiceContainer.getBeatContainer(beat) as NumberedBeatContainerGlyph | undefined; + if (container && container.hasAdditionalNumbers) { + for (const additionalNumber of container.iterateAdditionalNumbers()) { + barCount = additionalNumber.barCount; + beatLineX = + this.beatGlyphsStart + + additionalNumber.x + + additionalNumber.getBeatX(BeatXPosition.PreNotes, false); + for (let barIndex = 0; barIndex < barCount; barIndex++) { + const barY: number = barStart + barIndex * barSpacing; + const additionalBarEndX = + this.beatGlyphsStart + + additionalNumber.x + + additionalNumber.getBeatX(BeatXPosition.PostNotes, false); + LineBarRenderer.paintSingleBar( + canvas, + cx + this.x + beatLineX, + barY, + cx + this.x + additionalBarEndX, + barY, + barSize + ); + } + } } - const dotX: number = this.getBeatX(beat, BeatXPosition.MiddleNotes) - this.beatGlyphsStart; - - dotCount = Math.abs(dotCount); + } + } - for (let d = 0; d < dotCount; d++) { - CanvasHelper.fillMusicFontSymbolSafe(canvas,cx + this.x + dotX, dotsY, 1, MusicFontSymbol.AugmentationDot, true); - dotsY += dotsOffset; - } + protected override calculateOverflows(rendererTop: number, rendererBottom: number): void { + super.calculateOverflows(rendererTop, rendererBottom); + if (this.bar.isEmpty) { + return; } + this.calculateBeamingOverflows(rendererTop, rendererBottom); } - public getNoteLine() { + public getNoteLine(_note: Note) { return 0; } - public override get tupletOffset(): number { - // Shift tuplet above the number by: - // * 1 to get back to the center (calculateBeamYWithDirection places the beam below the number) - // * 1.5 to get back to the top of the number - return super.tupletOffset + this.resources.numberedNotationFont.size * 1.5; - } + private _calculateBarHeight(beat: Beat) { + const barCount: number = ModelUtils.getIndex(beat.duration) - 2; + let barHeight = 0; + if (barCount > 0) { + const smufl = this.smuflMetrics; + barHeight = + smufl.numberedBarRendererBarSpacing + + barCount * (smufl.numberedBarRendererBarSpacing + smufl.numberedBarRendererBarSize); + } - protected override getFlagTopY(_beat: Beat, _direction: BeamDirection): number { - return this.getLineY(0) - this.resources.numberedNotationFont.size; + return barHeight; } - protected override getFlagBottomY(_beat: Beat, _direction: BeamDirection): number { - return this.getLineY(0) - this.resources.numberedNotationFont.size; - } + protected override getFlagTopY(beat: Beat, direction: BeamDirection): number { + const barHeight: number = this._calculateBarHeight(beat); + const container = this.voiceContainer.getBeatContainer(beat); + if (!container) { + if (direction === BeamDirection.Up) { + return this.voiceContainer.getBoundingBoxTop() - barHeight; + } + return this.voiceContainer.getBoundingBoxBottom(); + } - protected override getBeamDirection(_helper: BeamingHelper): BeamDirection { - return BeamDirection.Down; + if (direction === BeamDirection.Up) { + return container.getBoundingBoxTop() - barHeight; + } + return container.getBoundingBoxBottom(); } - protected override getTupletBeamDirection(_helper: BeamingHelper): BeamDirection { - return BeamDirection.Up; - } + protected override getFlagBottomY(beat: Beat, direction: BeamDirection): number { + const barHeight: number = this._calculateBarHeight(beat); - public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { - let y = super.getNoteY(note, requestedPosition); - if (Number.isNaN(y)) { - y = this.getLineY(0); + const container = this.voiceContainer.getBeatContainer(beat); + if (!container) { + if (direction === BeamDirection.Down) { + return this.voiceContainer.getBoundingBoxBottom() + barHeight; + } + return this.getLineY(0); } - return y; + + if (direction === BeamDirection.Down) { + return container.getBoundingBoxBottom() + barHeight; + } + return this.getLineY(0); } - protected override calculateBeamYWithDirection(_h: BeamingHelper, _x: number, _direction: BeamDirection): number { - const res = this.resources.numberedNotationFont; - return this.getLineY(0) + res.size; + protected override getBeamDirection(_helper: BeamingHelper): BeamDirection { + return BeamDirection.Down; } - protected override getBarLineStart(_beat: Beat, _direction: BeamDirection): number { - const noteHeadHeight = this.smuflMetrics.glyphHeights.get(MusicFontSymbol.NoteheadBlack)!; - return this.getLineY(0) - noteHeadHeight / 2; + protected override getTupletBeamDirection(_helper: BeamingHelper): BeamDirection { + return BeamDirection.Up; } protected override createPreBeatGlyphs(): void { - this.wasFirstOfLine = this.isFirstOfLine; + this.wasFirstOfStaff = this.isFirstOfStaff; if (this.index === 0 || (this.bar.masterBar.isRepeatStart && this._isOnlyNumbered)) { - this.addPreBeatGlyph(new BarLineGlyph(false)); + this.addPreBeatGlyph(new BarLineGlyph(false, this.bar.staff.track.score.stylesheet.extendBarLines)); } this.createLinePreBeatGlyphs(); - this.addPreBeatGlyph(new BarNumberGlyph(0, this.getLineHeight(-0.25), this.bar.index + 1)); + const hasSpaceAfterStartGlyphs = this.createStartSpacing(); + if (this.shouldCreateBarNumber()) { + this.addPreBeatGlyph(new BarNumberGlyph(0, this.getLineHeight(-0.5), this.bar.index + 1)); + } else if (!hasSpaceAfterStartGlyphs) { + this.addPreBeatGlyph(new SpacingGlyph(0, 0, this.smuflMetrics.oneStaffSpace)); + } } protected override createLinePreBeatGlyphs(): void { - // Key signature - if (!this.bar.previousBar || this.bar.keySignature !== this.bar.previousBar.keySignature) { - this.createStartSpacing(); - this._createKeySignatureGlyphs(); - } - if ( this._isOnlyNumbered && (!this.bar.previousBar || @@ -298,15 +284,8 @@ export class NumberedBarRenderer extends LineBarRenderer { this._createTimeSignatureGlyphs(); } } - private _createKeySignatureGlyphs() { - this.addPreBeatGlyph( - new NumberedKeySignatureGlyph(0, this.getLineY(0), this.bar.keySignature, this.bar.keySignatureType) - ); - } private _createTimeSignatureGlyphs(): void { - this.addPreBeatGlyph(new SpacingGlyph(0, 0, this.smuflMetrics.oneStaffSpace)); - const masterBar = this.bar.masterBar; const g = new ScoreTimeSignatureGlyph( 0, @@ -329,11 +308,44 @@ export class NumberedBarRenderer extends LineBarRenderer { } protected override createVoiceGlyphs(v: Voice): void { + if (v.index > 0) { + return; + } + + super.createVoiceGlyphs(v); + + const absoluteStart = this.bar.masterBar.start; for (const b of v.beats) { - const container: NumberedBeatContainerGlyph = new NumberedBeatContainerGlyph(b, this.getVoiceContainer(v)!); - container.preNotes = v.index === 0 ? new NumberedBeatPreNotesGlyph() : new BeatGlyphBase(); - container.onNotes = v.index === 0 ? new NumberedBeatGlyph() : new BeatOnNoteGlyphBase(); - this.addBeatGlyph(container); + const mainContainer = new NumberedBeatContainerGlyph(b); + this.addBeatGlyph(mainContainer); + + // create dashes and filler glyphs + // we want a glyph on every quarter tick + + if (b.duration < Duration.Quarter) { + const endTick = b.displayStart + b.displayDuration; + let dashTick = b.displayStart + MidiUtils.QuarterTime; + while (dashTick < endTick) { + const isFullTick = endTick - dashTick >= MidiUtils.QuarterTime; + if (isFullTick) { + const dash = new NumberedDashBeatContainerGlyph(v.index, absoluteStart + dashTick); + this.addBeatGlyph(dash); + mainContainer.addDash(dash); + } + // special case to create second note number, this logic doesn't play well with tuplets + else if (b.duration === Duration.Half && b.dots > 1) { + const remainingTickNumber = new NumberedNoteBeatContainerGlyphBase( + b, + absoluteStart + dashTick, + endTick - dashTick + ); + this.addBeatGlyph(remainingTickNumber); + mainContainer.addNotes(remainingTickNumber); + } + + dashTick += MidiUtils.QuarterTime; + } + } } } @@ -345,4 +357,54 @@ export class NumberedBarRenderer extends LineBarRenderer { _bottomY: number, _canvas: ICanvas ): void {} + + protected override get beamSpacing(): number { + return this.smuflMetrics.numberedBarRendererBarSpacing; + } + + protected override get beamThickness(): number { + return this.smuflMetrics.numberedBarRendererBarSize; + } + + protected override paintBeamHelper( + cx: number, + cy: number, + canvas: ICanvas, + h: BeamingHelper, + flagsElement: BeatSubElement, + beamsElement: BeatSubElement + ): void { + if (h.voice?.index === 0) { + super.paintBeamHelper(cx, cy, canvas, h, flagsElement, beamsElement); + } + } + + protected override applyBarShift( + _h: BeamingHelper, + _direction: BeamDirection, + _drawingInfo: BeamingHelperDrawInfo, + _barCount: number + ): number { + return 0; + } + + protected override calculateBeamYWithDirection(h: BeamingHelper, _x: number, direction: BeamDirection): number { + this.ensureBeamDrawingInfo(h, direction); + const info = h.drawingInfos.get(direction)!; + if (direction === BeamDirection.Up) { + return Math.min(info.startY, info.endY); + } else { + return Math.max(info.startY, info.endY); + } + } + + protected override paintTuplets( + cx: number, + cy: number, + canvas: ICanvas, + beatElement: BeatSubElement, + _bracketsAsArcs: boolean = false + ): void { + super.paintTuplets(cx, cy, canvas, beatElement, true); + } } diff --git a/packages/alphatab/src/rendering/NumberedBarRendererFactory.ts b/packages/alphatab/src/rendering/NumberedBarRendererFactory.ts index ff8c99ffc..50705e033 100644 --- a/packages/alphatab/src/rendering/NumberedBarRendererFactory.ts +++ b/packages/alphatab/src/rendering/NumberedBarRendererFactory.ts @@ -1,11 +1,10 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; +import type { Staff } from '@coderline/alphatab/model/Staff'; +import type { Track } from '@coderline/alphatab/model/Track'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { BarRendererFactory } from '@coderline/alphatab/rendering/BarRendererFactory'; -import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import type { Track } from '@coderline/alphatab/model/Track'; -import type { Staff } from '@coderline/alphatab/model/Staff'; -import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; import { NumberedBarRenderer } from '@coderline/alphatab/rendering/NumberedBarRenderer'; +import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; /** * This Factory produces NumberedBarRenderer instances @@ -16,14 +15,6 @@ export class NumberedBarRendererFactory extends BarRendererFactory { return NumberedBarRenderer.StaffId; } - public override getStaffPaddingTop(staff: RenderStaff): number { - return staff.system.layout.renderer.settings.display.notationStaffPaddingTop; - } - - public override getStaffPaddingBottom(staff: RenderStaff): number { - return staff.system.layout.renderer.settings.display.notationStaffPaddingBottom; - } - public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { return new NumberedBarRenderer(renderer, bar); } diff --git a/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts b/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts index 8cb87cab1..9fb902cb1 100644 --- a/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts @@ -1,13 +1,83 @@ +import type { Beat } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; -import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { NumberedTieGlyph } from '@coderline/alphatab/rendering//glyphs/NumberedTieGlyph'; +import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import { NumberedBeatGlyph, NumberedBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedBeatGlyph'; +import { + type INumberedBeatDashGlyph, + type NumberedDashBeatContainerGlyph, + NumberedNoteBeatContainerGlyphBase +} from '@coderline/alphatab/rendering/glyphs/NumberedDashBeatContainerGlyph'; import { NumberedSlurGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedSlurGlyph'; +import type { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import type { BarBounds } from '@coderline/alphatab/rendering/utils/BarBounds'; /** * @internal */ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { + private _slurs: Map = new Map(); private _effectSlurs: NumberedSlurGlyph[] = []; + private _dashes?: INumberedBeatDashGlyph[]; + + public hasAdditionalNumbers = false; + public *iterateAdditionalNumbers() { + const dashes = this._dashes; + if (!dashes) { + return; + } + for (const d of dashes) { + if (d instanceof NumberedNoteBeatContainerGlyphBase) { + yield d as NumberedNoteBeatContainerGlyphBase; + } + } + } + + public constructor(beat: Beat) { + super(beat); + this.preNotes = new NumberedBeatPreNotesGlyph(); + this.onNotes = new NumberedBeatGlyph(); + } + + public addDash(dash: NumberedDashBeatContainerGlyph) { + let dashes = this._dashes; + if (!dashes) { + dashes = []; + this._dashes = dashes; + } + dashes.push(dash); + } + + public addNotes(dash: NumberedNoteBeatContainerGlyphBase) { + let dashes = this._dashes; + if (!dashes) { + dashes = []; + this._dashes = dashes; + } + dashes.push(dash); + this.hasAdditionalNumbers = true; + } + + public override doLayout(): void { + this._slurs.clear(); + this._effectSlurs = []; + super.doLayout(); + } + + public override buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number) { + super.buildBoundingsLookup(barBounds, cx, cy); + // extend bounds to include dashes + const dashes = this._dashes; + if (dashes) { + const beatBounds = barBounds.beats[barBounds.beats.length - 1]; + const lastDash = dashes[dashes.length - 1]; + const visualEndX = lastDash.x + lastDash.contentWidth; + beatBounds.visualBounds.w = visualEndX - beatBounds.visualBounds.x; + + const realEnd = lastDash.x + lastDash.width; + beatBounds.realBounds.w = realEnd - beatBounds.realBounds.x; + } + } protected override createTies(n: Note): void { // create a tie if any effect requires it @@ -15,17 +85,23 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { return; } - if (n.isTieOrigin && n.tieDestination!.isVisible) { - const tie = new NumberedTieGlyph(n, n.tieDestination!, false); + if (n.isTieOrigin && n.tieDestination!.isVisible && !this._slurs.has('numbered.tie')) { + const tie = new NumberedTieGlyph(`numbered.tie.${n.beat.id}`, n, n.tieDestination!, false); this.addTie(tie); + this._slurs.set(tie.slurEffectId, tie); } if (n.isTieDestination) { - const tie = new NumberedTieGlyph(n.tieOrigin!, n, true); + const tie = new NumberedTieGlyph(`numbered.tie.${n.tieOrigin!.beat.id}`, n.tieOrigin!, n, true); this.addTie(tie); } - if (n.isLeftHandTapped && !n.isHammerPullDestination) { - const tapSlur = new NumberedTieGlyph(n, n, false); + if ( + n.isLeftHandTapped && + !n.isHammerPullDestination && + !this._slurs.has(`numbered.tie.leftHandTap.${n.beat.id}`) + ) { + const tapSlur = new NumberedTieGlyph(`numbered.tie.leftHandTap.${n.beat.id}`, n, n, false); this.addTie(tapSlur); + this._slurs.set(tapSlur.slurEffectId, tapSlur); } // start effect slur on first beat if (n.isEffectSlurOrigin && n.effectSlurDestination) { @@ -36,12 +112,22 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { break; } } + if (!expanded) { - const effectSlur = new NumberedSlurGlyph(n, n.effectSlurDestination, false, false); + const effectSlur = new NumberedSlurGlyph( + `numbered.slur.effect`, + n, + n.effectSlurDestination, + false, + false + ); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); + this._slurs.set(effectSlur.slurEffectId, effectSlur); + this._slurs.set('numbered.slur.effect', effectSlur); } } + // end effect slur on last beat if (n.isEffectSlurDestination && n.effectSlurOrigin) { let expanded: boolean = false; @@ -52,9 +138,11 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { } } if (!expanded) { - const effectSlur = new NumberedSlurGlyph(n.effectSlurOrigin, n, false, true); + const effectSlur = new NumberedSlurGlyph(`numbered.slur.effect`, n.effectSlurOrigin, n, false, true); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); + this._slurs.set(effectSlur.slurEffectId, effectSlur); + this._slurs.set('numbered.slur.effect', effectSlur); } } } diff --git a/packages/alphatab/src/rendering/RenderFinishedEventArgs.ts b/packages/alphatab/src/rendering/RenderFinishedEventArgs.ts index 0853b5dc9..081f8db67 100644 --- a/packages/alphatab/src/rendering/RenderFinishedEventArgs.ts +++ b/packages/alphatab/src/rendering/RenderFinishedEventArgs.ts @@ -10,6 +10,20 @@ export class RenderFinishedEventArgs { * Gets or sets the unique id of this event args. */ public id: string = ModelUtils.newGuid(); + + /** + * A value indicating whether the currently rendered viewport can be reused. + * @remarks + * If set to true, the viewport does NOT need to be cleared as a similar + * content will be rendered. + * If set to false, the viewport and any visual partials should be cleared + * as it could lead to UI disturbances otherwise. + * + * The viewport can be typically used on resize renders or if the user supplied + * a rendering hint that the new score is "similar" to the old one (e.g. in case of live-editing). + */ + public reuseViewport: boolean = true; + /** * Gets or sets the x position of the current rendering result. */ diff --git a/packages/alphatab/src/rendering/ScoreBarRenderer.ts b/packages/alphatab/src/rendering/ScoreBarRenderer.ts index 9ba7e0fd8..c5f09c158 100644 --- a/packages/alphatab/src/rendering/ScoreBarRenderer.ts +++ b/packages/alphatab/src/rendering/ScoreBarRenderer.ts @@ -2,32 +2,27 @@ import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; import { type Bar, BarSubElement } from '@coderline/alphatab/model/Bar'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Clef } from '@coderline/alphatab/model/Clef'; -import { Duration } from '@coderline/alphatab/model/Duration'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { KeySignature } from '@coderline/alphatab/model/KeySignature'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import type { Note } from '@coderline/alphatab/model/Note'; +import { Staff } from '@coderline/alphatab/model/Staff'; import type { Voice } from '@coderline/alphatab/model/Voice'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; import { ClefGlyph } from '@coderline/alphatab/rendering/glyphs/ClefGlyph'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { ScoreBeatGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatGlyph'; -import { ScoreBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatPreNotesGlyph'; +import { KeySignatureGlyph } from '@coderline/alphatab/rendering/glyphs/KeySignatureGlyph'; import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTimeSignatureGlyph'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; +import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import { ScoreBeatContainerGlyph } from '@coderline/alphatab/rendering/ScoreBeatContainerGlyph'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { type BeamingHelper, BeamingHelperDrawInfo } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { KeySignature } from '@coderline/alphatab/model/KeySignature'; -import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; -import { KeySignatureGlyph } from '@coderline/alphatab/rendering/glyphs/KeySignatureGlyph'; +import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { Staff } from '@coderline/alphatab/model/Staff'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; -import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; /** * This BarRenderer renders a bar using standard music notation. @@ -38,11 +33,6 @@ export class ScoreBarRenderer extends LineBarRenderer { private static _sharpKsSteps: number[] = [-1, 2, -2, 1, 4, 0, 3]; private static _flatKsSteps: number[] = [3, 0, 4, 1, 5, 2, 6]; - public simpleWhammyOverflow: number = 0; - - public beatEffectsMinY: number | null = null; - public beatEffectsMaxY: number | null = null; - public accidentalHelper: AccidentalHelper; public constructor(renderer: ScoreRenderer, bar: Bar) { @@ -66,34 +56,18 @@ export class ScoreBarRenderer extends LineBarRenderer { return BarSubElement.StandardNotationStaffLine; } - public override get showMultiBarRest(): boolean { - return true; - } - public override get lineSpacing(): number { return this.smuflMetrics.oneStaffSpace; } public override get heightLineCount(): number { - return 5; + return Math.max(5, this.bar.staff.standardNotationLineCount); } public override get drawnLineCount(): number { return this.bar.staff.standardNotationLineCount; } - public registerBeatEffectOverflows(beatEffectsMinY: number, beatEffectsMaxY: number) { - const currentBeatEffectsMinY = this.beatEffectsMinY; - if (currentBeatEffectsMinY == null || beatEffectsMinY < currentBeatEffectsMinY) { - this.beatEffectsMinY = beatEffectsMinY; - } - - const currentBeatEffectsMaxY = this.beatEffectsMaxY; - if (currentBeatEffectsMaxY == null || beatEffectsMaxY > currentBeatEffectsMaxY) { - this.beatEffectsMaxY = beatEffectsMaxY; - } - } - /** * Gets the relative y position of the given steps relative to first line. * @param steps the amount of steps while 2 steps are one line @@ -113,251 +87,50 @@ export class ScoreBarRenderer extends LineBarRenderer { return super.getLineHeight(steps / 2); } - public override doLayout(): void { - super.doLayout(); - - if (!this.bar.isEmpty && this.accidentalHelper.maxLineBeat) { - const top: number = this.getScoreY(-2); - const bottom: number = this.getScoreY(this.heightLineCount * 2); - const whammyOffset: number = this.simpleWhammyOverflow; - - const beatEffectsMinY = this.beatEffectsMinY; - if (beatEffectsMinY !== null) { - const beatEffectTopOverflow = top - beatEffectsMinY; - if (beatEffectTopOverflow > 0) { - this.registerOverflowTop(beatEffectTopOverflow); - } - } - - const beatEffectsMaxY = this.beatEffectsMaxY; - if (beatEffectsMaxY !== null) { - const beatEffectBottomOverflow = beatEffectsMaxY - bottom; - if (beatEffectBottomOverflow > 0) { - this.registerOverflowBottom(beatEffectBottomOverflow); - } - } - - this.registerOverflowTop(whammyOffset); - - const noteOverflowPadding = this.getScoreHeight(1); - - let maxNoteY = 0; - let minNoteY = 0; - - for (const v of this.helpers.beamHelpers) { - for (const h of v) { - if (h.isRestBeamHelper) { - if (h.minRestLine) { - const topY = this.getScoreY(h.maxRestLine!) - noteOverflowPadding; - if (topY < maxNoteY) { - maxNoteY = topY; - } - } - if (h.maxRestLine) { - const bottomY = this.getScoreY(h.maxRestLine!) + noteOverflowPadding; - if (bottomY < maxNoteY) { - maxNoteY = bottomY; - } - } - } else if (h.beats.length === 1) { - // notes with stems - if (h.beats[0].duration >= Duration.Half) { - if (h.direction === BeamDirection.Up) { - let topY = this.getFlagTopY(h.beats[0], h.direction); - if (h.hasTuplet) { - topY -= this.tupletSize + this.tupletOffset; - } - - if (topY < maxNoteY) { - maxNoteY = topY; - } - - const bottomY = this.getFlagBottomY(h.beats[0], h.direction) + noteOverflowPadding; - if (bottomY > minNoteY) { - minNoteY = bottomY; - } - } else { - let bottomY = this.getFlagBottomY(h.beats[0], h.direction); - if (h.hasTuplet) { - bottomY += this.tupletSize + this.tupletOffset; - } - - if (bottomY > minNoteY) { - minNoteY = bottomY; - } - - const topY = this.getFlagTopY(h.beats[0], h.direction) - noteOverflowPadding; - if (topY < maxNoteY) { - maxNoteY = topY; - } - } - } - // standalone notes without stems - else { - const beatContainer = this.getBeatContainer(h.beats[0]); - if (beatContainer) { - let topY = beatContainer.onNotes.getHighestNoteY() - noteOverflowPadding; - let bottomY = beatContainer.onNotes.getLowestNoteY() + noteOverflowPadding; - if (h.direction === BeamDirection.Up) { - if (h.hasTuplet) { - topY -= this.tupletSize + this.tupletOffset; - } - } else { - if (h.hasTuplet) { - bottomY += this.tupletSize + this.tupletOffset; - } - } - - if (bottomY > minNoteY) { - minNoteY = bottomY; - } - if (topY < maxNoteY) { - maxNoteY = topY; - } - } - } - } - // beamed notes - else { - this._ensureDrawingInfo(h, h.direction); - const drawingInfo = h.drawingInfos.get(h.direction)!; - - if (h.direction === BeamDirection.Up) { - let topY = Math.min(drawingInfo.startY, drawingInfo.endY); - if (h.hasTuplet) { - topY -= this.tupletSize + this.tupletOffset; - } - - if (topY < maxNoteY) { - maxNoteY = topY; - } - - const bottomY: number = - this.getBarLineStart(h.beatOfLowestNote, h.direction) + noteOverflowPadding; - if (bottomY > minNoteY) { - minNoteY = bottomY; - } - } else { - let bottomY = Math.max(drawingInfo.startY, drawingInfo.endY); - - if (h.hasTuplet) { - bottomY += this.tupletSize + this.tupletOffset; - } - - if (bottomY > minNoteY) { - minNoteY = bottomY; - } - - const topY: number = - this.getBarLineStart(h.beatOfHighestNote, h.direction) - noteOverflowPadding; - if (topY < maxNoteY) { - maxNoteY = topY; - } - } - } - } - } - if (maxNoteY < top) { - this.registerOverflowTop(Math.abs(maxNoteY) + whammyOffset); - } - - if (minNoteY > bottom) { - this.registerOverflowBottom(Math.abs(minNoteY) - bottom); - } + protected override calculateOverflows(rendererTop: number, rendererBottom: number): void { + super.calculateOverflows(rendererTop, rendererBottom); + if (this.bar.isEmpty) { + return; } + this.calculateBeamingOverflows(rendererTop, rendererBottom); } - public override paint(cx: number, cy: number, canvas: ICanvas): void { - super.paint(cx, cy, canvas); - this.paintBeams(cx, cy, canvas, BeatSubElement.StandardNotationFlags, BeatSubElement.StandardNotationBeams); - this.paintTuplets(cx, cy, canvas, BeatSubElement.StandardNotationTuplet); + protected override get flagsSubElement(): BeatSubElement { + return BeatSubElement.StandardNotationFlags; } - private _getSlashFlagY() { - const line = (this.heightLineCount - 1) / 2; - const slashY = this.getLineY(line); - return slashY; + protected override get beamsSubElement(): BeatSubElement { + return BeatSubElement.StandardNotationBeams; } - protected override getFlagTopY(beat: Beat, direction: BeamDirection): number { - if (beat.slashed) { - let slashY = this._getSlashFlagY(); - const symbol = SlashNoteHeadGlyph.getSymbol(beat.duration); - const scale = beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; - - if (direction === BeamDirection.Down) { - slashY -= this.smuflMetrics.stemDown.has(symbol) - ? this.smuflMetrics.stemDown.get(symbol)!.topY * scale - : 0; - } else { - slashY -= this.smuflMetrics.stemUp.has(symbol) - ? this.smuflMetrics.stemUp.get(symbol)!.bottomY * scale - : 0; - slashY -= this.smuflMetrics.standardStemLength + scale; - } - - return slashY; - } - - const minNote = this.accidentalHelper.getMinLineNote(beat); - if (minNote) { - return this.getBeatContainer(beat)!.onNotes.getNoteY( - minNote, - direction === BeamDirection.Up ? NoteYPosition.TopWithStem : NoteYPosition.StemDown - ); - } - - let y = this.getScoreY(this.accidentalHelper.getMinLine(beat)); + protected override get tupletSubElement(): BeatSubElement { + return BeatSubElement.StandardNotationTuplet; + } - if (direction === BeamDirection.Up) { - const scale = beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; - y -= this.smuflMetrics.standardStemLength * scale; + protected override getFlagTopY(beat: Beat, direction: BeamDirection): number { + const position = direction === BeamDirection.Up ? NoteYPosition.TopWithStem : NoteYPosition.StemDown; + if (beat.isRest) { + return this.getRestY(beat, position); + } else { + return this.voiceContainer.getHighestNoteY(beat, position); } - - return y; } protected override getFlagBottomY(beat: Beat, direction: BeamDirection): number { - if (beat.slashed) { - let slashY = this._getSlashFlagY(); - const symbol = SlashNoteHeadGlyph.getSymbol(beat.duration); - const scale = beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; - - if (direction === BeamDirection.Down) { - slashY -= this.smuflMetrics.stemDown.has(symbol) - ? this.smuflMetrics.stemDown.get(symbol)!.topY * scale - : 0; - slashY += this.smuflMetrics.standardStemLength + scale; - } else { - slashY -= this.smuflMetrics.stemUp.has(symbol) - ? this.smuflMetrics.stemUp.get(symbol)!.bottomY * scale - : 0; - } - - return slashY; - } - - const maxNote = this.accidentalHelper.getMaxLineNote(beat); - if (maxNote) { - return this.getBeatContainer(beat)!.onNotes.getNoteY( - maxNote, - direction === BeamDirection.Up ? NoteYPosition.StemUp : NoteYPosition.BottomWithStem - ); - } + const position = direction === BeamDirection.Up ? NoteYPosition.StemUp : NoteYPosition.BottomWithStem; - let y = this.getScoreY(this.accidentalHelper.getMaxLine(beat)); - if (direction === BeamDirection.Down) { - const scale = beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; - y += this.smuflMetrics.standardStemLength * scale; + if (beat.isRest) { + return this.getRestY(beat, position); + } else { + return this.voiceContainer.getLowestNoteY(beat, position); } - return y; } protected override getBeamDirection(helper: BeamingHelper): BeamDirection { - return helper.direction; + return this._beamDirections.has(helper) ? this._beamDirections.get(helper)! : BeamDirection.Up; } - public centerStaffStemY(helper: BeamingHelper) { + public centerStaffStemY(direction: BeamDirection) { const isStandardFive = this.bar.staff.standardNotationLineCount === Staff.DefaultStandardNotationLineCount; if (isStandardFive) { // center on the middle line for a standard 5-line staff @@ -365,61 +138,16 @@ export class ScoreBarRenderer extends LineBarRenderer { } // for other staff line counts, we align the stem either on the top or bottom line - if (helper.direction === BeamDirection.Up) { + if (direction === BeamDirection.Up) { return this.getScoreY(this.bar.staff.standardNotationLineCount * 2); } return this.getScoreY(0); } - public getStemBottomY(_beamingHelper: BeamingHelper): number { - throw new Error('Method not implemented.'); - } - public override get middleYPosition(): number { return this.getScoreY(this.bar.staff.standardNotationLineCount - 1); } - public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { - if (note.beat.slashed) { - const line = (this.heightLineCount - 1) / 2; - return this.getLineY(line); - } - - let y = super.getNoteY(note, requestedPosition); - if (Number.isNaN(y)) { - // NOTE: some might request the note position before the glyphs have been created - // e.g. the beaming helper, for these we just need a rough - // estimate on the position - const line = AccidentalHelper.computeLineWithoutAccidentals(this.bar, note); - y = this.getScoreY(line); - const scale = note.beat.graceType === GraceType.None ? 1 : NoteHeadGlyph.GraceScale; - const stemHeight = this.smuflMetrics.standardStemLength * scale; - const noteHeadHeight = - this.smuflMetrics.glyphHeights.get(NoteHeadGlyph.getSymbol(note.beat.duration))! * scale; - switch (requestedPosition) { - case NoteYPosition.TopWithStem: - y -= stemHeight; - break; - case NoteYPosition.Top: - y -= noteHeadHeight / 2; - break; - case NoteYPosition.Center: - break; - case NoteYPosition.Bottom: - y += noteHeadHeight / 2; - break; - case NoteYPosition.BottomWithStem: - y += stemHeight; - break; - case NoteYPosition.StemUp: - break; - case NoteYPosition.StemDown: - break; - } - } - return y; - } - public override applyLayoutingInfo(): boolean { const result = super.applyLayoutingInfo(); if (result && this.bar.isMultiVoice) { @@ -437,240 +165,19 @@ export class ScoreBarRenderer extends LineBarRenderer { return result; } - protected override calculateBeamYWithDirection(h: BeamingHelper, x: number, direction: BeamDirection): number { - if (h.beats.length === 0) { - return direction === BeamDirection.Up - ? this.getFlagTopY(h.beats[0], direction) - : this.getFlagBottomY(h.beats[0], direction); - } - - this._ensureDrawingInfo(h, direction); - return h.drawingInfos.get(direction)!.calcY(x); - } - - private _ensureDrawingInfo(h: BeamingHelper, direction: BeamDirection) { - if (!h.drawingInfos.has(direction)) { - const scale = h.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; - const barCount: number = ModelUtils.getIndex(h.shortestDuration) - 2; - let stemSize = this.smuflMetrics.standardStemLength * scale; - - if (h.tremoloDuration) { - // for 16th and shorter beats we need more space for all tremolos - // for 8th beats we need only more space for 32nd tremolos - - // the logic here is not perfect but there is no SMuFL guideline - // on how tremolos need to extend stems - - const oneBeamSize = (this.smuflMetrics.beamThickness + this.smuflMetrics.beamSpacing) * scale; - if (h.shortestDuration > Duration.Eighth) { - if (h.tremoloDuration === Duration.Eighth) { - stemSize += oneBeamSize; - } else { - stemSize += oneBeamSize * 1.5; - } - } else if (h.tremoloDuration === Duration.ThirtySecond) { - stemSize += oneBeamSize * 1.5; - } - } - - const drawingInfo = new BeamingHelperDrawInfo(); - h.drawingInfos.set(direction, drawingInfo); - - // the beaming logic works like this: - // 1. we take the first and last note, add the stem, and put a diagnal line between them. - // 2. the height of the diagonal line must not exceed a max height, - // - if this is the case, the line on the more distant note just gets longer - // 3. any middle elements (notes or rests) shift this diagonal line up/down to avoid overlaps - - const firstBeat = h.beats[0]; - const lastBeat = h.beats[h.beats.length - 1]; - - const isRest = h.isRestBeamHelper; - - // 1. put direct diagonal line. - drawingInfo.startBeat = firstBeat; - drawingInfo.startX = h.getBeatLineX(firstBeat); - if (isRest) { - drawingInfo.startY = - direction === BeamDirection.Up ? this.getScoreY(h.minRestLine!) : this.getScoreY(h.maxRestLine!); - } else { - drawingInfo.startY = - direction === BeamDirection.Up - ? this.getFlagTopY(firstBeat, direction) - : this.getFlagBottomY(firstBeat, direction); - } - - drawingInfo.endBeat = lastBeat; - drawingInfo.endX = h.getBeatLineX(lastBeat); - if (isRest) { - drawingInfo.endY = - direction === BeamDirection.Up ? this.getScoreY(h.minRestLine!) : this.getScoreY(h.maxRestLine!); - } else { - drawingInfo.endY = - direction === BeamDirection.Up - ? this.getFlagTopY(lastBeat, direction) - : this.getFlagBottomY(lastBeat, direction); - } - - // 2. ensure max slope - // we use the min/max notes to place the beam along their real position - // we only want a maximum of 10 offset for their gradient - const maxSlope: number = this.smuflMetrics.oneStaffSpace; - if ( - direction === BeamDirection.Down && - drawingInfo.startY > drawingInfo.endY && - drawingInfo.startY - drawingInfo.endY > maxSlope - ) { - drawingInfo.endY = drawingInfo.startY - maxSlope; - } - if ( - direction === BeamDirection.Down && - drawingInfo.endY > drawingInfo.startY && - drawingInfo.endY - drawingInfo.startY > maxSlope - ) { - drawingInfo.startY = drawingInfo.endY - maxSlope; - } - if ( - direction === BeamDirection.Up && - drawingInfo.startY < drawingInfo.endY && - drawingInfo.endY - drawingInfo.startY > maxSlope - ) { - drawingInfo.endY = drawingInfo.startY + maxSlope; - } - if ( - direction === BeamDirection.Up && - drawingInfo.endY < drawingInfo.startY && - drawingInfo.startY - drawingInfo.endY > maxSlope - ) { - drawingInfo.startY = drawingInfo.endY + maxSlope; - } - - // 3. let middle elements shift up/down - if (h.beats.length > 1) { - // check if highest note shifts bar up or down - if (direction === BeamDirection.Up) { - const yNeededForHighestNote = - this.getScoreY(this.accidentalHelper.getMinLine(h.beatOfHighestNote)) - stemSize; - const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfHighestNote)); - - const diff = yGivenByCurrentValues - yNeededForHighestNote; - if (diff > 0) { - drawingInfo.startY -= diff; - drawingInfo.endY -= diff; - } - } else { - const yNeededForLowestNote = - this.getScoreY(this.accidentalHelper.getMaxLine(h.beatOfLowestNote)) + stemSize; - const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfLowestNote)); - - const diff = yNeededForLowestNote - yGivenByCurrentValues; - if (diff > 0) { - drawingInfo.startY += diff; - drawingInfo.endY += diff; - } - } - - // check if rest shifts bar up or down - if (h.minRestLine !== null || h.maxRestLine !== null) { - const scaleMod: number = h.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; - let barSpacing: number = - barCount * (this.smuflMetrics.beamSpacing + this.smuflMetrics.beamThickness) * scaleMod; - barSpacing += this.smuflMetrics.beamSpacing; - - if (direction === BeamDirection.Up && h.minRestLine !== null) { - const yNeededForRest = this.getScoreY(h.minRestLine!) - barSpacing; - const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfMinRestLine!)); - - const diff = yGivenByCurrentValues - yNeededForRest; - if (diff > 0) { - drawingInfo.startY -= diff; - drawingInfo.endY -= diff; - } - } else if (direction === BeamDirection.Down && h.maxRestLine !== null) { - const yNeededForRest = this.getScoreY(h.maxRestLine!) + barSpacing; - const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfMaxRestLine!)); - - const diff = yNeededForRest - yGivenByCurrentValues; - if (diff > 0) { - drawingInfo.startY += diff; - drawingInfo.endY += diff; - } - } - } - - // check if slash shifts bar up or down - if (h.slashBeats.length > 0) { - for (const b of h.slashBeats) { - const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(b)); - const yNeededForSlash = - h.direction === BeamDirection.Up - ? this.getFlagTopY(b, h.direction) - : this.getFlagBottomY(b, h.direction); - - const diff = yNeededForSlash - yGivenByCurrentValues; - if (diff > 0) { - drawingInfo.startY += diff; - drawingInfo.endY += diff; - } - } - } - } - - // we can only draw up to 2 beams towards the noteheads, then we have to grow to the other side - // here we shift accordingly - if (barCount > 2 && !isRest) { - const beamSpacing = this.smuflMetrics.beamSpacing * scale; - const beamThickness = this.smuflMetrics.beamThickness * scale; - const totalBarsHeight = barCount * beamThickness + (barCount - 1) * beamSpacing; - - if (direction === BeamDirection.Up) { - const bottomBarY = drawingInfo.startY + 2 * beamThickness + beamSpacing; - const barTopY = bottomBarY - totalBarsHeight; - const diff = drawingInfo.startY - barTopY; - if (diff > 0) { - drawingInfo.startY -= diff; - drawingInfo.endY -= diff; - } - } else { - const topBarY = drawingInfo.startY - 2 * beamThickness + beamSpacing; - const barBottomY = topBarY + totalBarsHeight; - const diff = barBottomY - drawingInfo.startY; - if (diff > 0) { - drawingInfo.startY += diff; - drawingInfo.endY += diff; - } - } - } - } + protected override getMinLineOfBeat(beat: Beat): number { + return this.accidentalHelper.getMinSteps(beat) / 2; } - protected override getBarLineStart(beat: Beat, direction: BeamDirection): number { - if (beat.slashed) { - return direction === BeamDirection.Down - ? this.getFlagTopY(beat, direction) - : this.getFlagBottomY(beat, direction); - } - - if (direction === BeamDirection.Up) { - const maxNote = this.accidentalHelper.getMaxLineNote(beat); - if (maxNote) { - return this.getBeatContainer(beat)!.onNotes.getNoteY(maxNote, NoteYPosition.StemUp); - } - return this.getScoreY(this.accidentalHelper.getMaxLine(beat)); - } - - const minNote = this.accidentalHelper.getMinLineNote(beat); - if (minNote) { - return this.getBeatContainer(beat)!.onNotes.getNoteY(minNote, NoteYPosition.StemDown); - } - return this.getScoreY(this.accidentalHelper.getMinLine(beat)); + protected override getMaxLineOfBeat(beat: Beat): number { + return this.accidentalHelper.getMaxSteps(beat) / 2; } protected override createLinePreBeatGlyphs(): void { // Clef let hasClef = false; if ( - this.isFirstOfLine || + this.isFirstOfStaff || this.bar.clef !== this.bar.previousBar!.clef || this.bar.clefOttava !== this.bar.previousBar!.clefOttava ) { @@ -821,44 +328,89 @@ export class ScoreBarRenderer extends LineBarRenderer { } protected override createVoiceGlyphs(v: Voice): void { + super.createVoiceGlyphs(v); for (const b of v.beats) { - const container: ScoreBeatContainerGlyph = new ScoreBeatContainerGlyph(b, this.getVoiceContainer(v)!); - container.preNotes = new ScoreBeatPreNotesGlyph(); - container.onNotes = new ScoreBeatGlyph(); - this.addBeatGlyph(container); + this.addBeatGlyph(new ScoreBeatContainerGlyph(b)); } } - public getNoteLine(n: Note): number { - return this.accidentalHelper.getNoteLine(n); + public override getNoteLine(note: Note): number { + return this.accidentalHelper.getNoteSteps(note) / 2; + } + + public getNoteSteps(n: Note): number { + return this.accidentalHelper.getNoteSteps(n); } + private readonly _beamDirections = new Map(); + public override completeBeamingHelper(helper: BeamingHelper) { - // for multi-voice bars we need to register the positions - // for multi-voice rest displacement to avoid collisions - if (this.bar.isMultiVoice && helper.highestNoteInHelper && helper.lowestNoteInHelper) { - let highestNotePosition = 0; - let lowestNotePosition = 0; + const direction = this._calculateBeamDirection(helper); + this._beamDirections.set(helper, direction); + } - let offset = 0; - if (helper.hasTuplet) { - offset += this.resources.effectFont.size * 2; - } + private _calculateBeamDirection(helper: BeamingHelper): BeamDirection { + // no proper voice (should not happen usually) + if (!helper.voice) { + return BeamDirection.Up; + } + // we have a preferred direction + if (helper.preferredBeamDirection !== null) { + return helper.preferredBeamDirection!; + } + // on multi-voice setups secondary voices are always down + if (helper.voice.index > 0) { + return this._invertBeamDirection(helper, BeamDirection.Down); + } + // on multi-voice setups primary voices are always up + if (helper.voice.bar.isMultiVoice) { + return this._invertBeamDirection(helper, BeamDirection.Up); + } + // grace notes are always up + if (helper.beats[0].graceType !== GraceType.None) { + return this._invertBeamDirection(helper, BeamDirection.Up); + } - if (helper.direction === BeamDirection.Up) { - highestNotePosition = this.getNoteY(helper.highestNoteInHelper, NoteYPosition.TopWithStem) - offset; - lowestNotePosition = this.getNoteY(helper.lowestNoteInHelper, NoteYPosition.Bottom); - } else { - highestNotePosition = this.getNoteY(helper.highestNoteInHelper, NoteYPosition.Top); - lowestNotePosition = this.getNoteY(helper.lowestNoteInHelper, NoteYPosition.BottomWithStem) + offset; - } + if (helper.beats.length === 1 && helper.beats[0].slashed) { + return this._invertBeamDirection(helper, BeamDirection.Down); + } - for (const beat of helper.beats) { - this.helpers.collisionHelper.reserveBeatSlot(beat, highestNotePosition, lowestNotePosition); - } + // the average line is used for determination + // key lowerequal than middle line -> up + // key higher than middle line -> down + if (helper.highestNoteInHelper && helper.lowestNoteInHelper) { + // NOTE: This is the only place where we need the locations before we have positioned the notes + // TODO: we should first register all note-heads and calculate the accidentals+steps + const highestNotePosition = this._getNoteCenterYBeforeLayouting(helper.highestNoteInHelper); + const lowestNotePosition = this._getNoteCenterYBeforeLayouting(helper.lowestNoteInHelper); + + const avg = (highestNotePosition + lowestNotePosition) / 2; + return this._invertBeamDirection( + helper, + this.middleYPosition < avg ? BeamDirection.Up : BeamDirection.Down + ); } + + return this._invertBeamDirection(helper, BeamDirection.Up); + } + + private _getNoteCenterYBeforeLayouting(note: Note): number { + const steps = AccidentalHelper.computeStepsWithoutAccidentals(this.bar, note); + return this.getScoreY(steps); } + private _invertBeamDirection(helper: BeamingHelper, direction: BeamDirection): BeamDirection { + if (!helper.invertBeamDirection) { + return direction; + } + switch (direction) { + case BeamDirection.Down: + return BeamDirection.Up; + // case BeamDirection.Up: + default: + return BeamDirection.Down; + } + } protected override paintBeamingStem( beat: Beat, _cy: number, diff --git a/packages/alphatab/src/rendering/ScoreBarRendererFactory.ts b/packages/alphatab/src/rendering/ScoreBarRendererFactory.ts index 10d9f39c4..c1634bbcc 100644 --- a/packages/alphatab/src/rendering/ScoreBarRendererFactory.ts +++ b/packages/alphatab/src/rendering/ScoreBarRendererFactory.ts @@ -1,11 +1,10 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; +import type { Staff } from '@coderline/alphatab/model/Staff'; +import type { Track } from '@coderline/alphatab/model/Track'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { BarRendererFactory } from '@coderline/alphatab/rendering/BarRendererFactory'; import { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; -import type { Track } from '@coderline/alphatab/model/Track'; -import type { Staff } from '@coderline/alphatab/model/Staff'; /** * This Factory produces ScoreBarRenderer instances @@ -16,14 +15,6 @@ export class ScoreBarRendererFactory extends BarRendererFactory { return ScoreBarRenderer.StaffId; } - public override getStaffPaddingTop(staff: RenderStaff): number { - return staff.system.layout.renderer.settings.display.notationStaffPaddingTop; - } - - public override getStaffPaddingBottom(staff: RenderStaff): number { - return staff.system.layout.renderer.settings.display.notationStaffPaddingBottom; - } - public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { return new ScoreBarRenderer(renderer, bar); } diff --git a/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts b/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts index 25d2fa09e..2a7d82cc7 100644 --- a/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts @@ -1,14 +1,22 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import type { Beat } from '@coderline/alphatab/model/Beat'; import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import { FlagGlyph } from '@coderline/alphatab/rendering/glyphs/FlagGlyph'; +import { ScoreBeatGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatGlyph'; +import { ScoreBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatPreNotesGlyph'; import { ScoreBendGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBendGlyph'; import { ScoreLegatoGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreLegatoGlyph'; import { ScoreSlideLineGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreSlideLineGlyph'; import { ScoreSlurGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreSlurGlyph'; import { ScoreTieGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTieGlyph'; +import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; +import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** @@ -19,9 +27,54 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { private _effectSlur: ScoreSlurGlyph | null = null; private _effectEndSlur: ScoreSlurGlyph | null = null; + public constructor(beat: Beat) { + super(beat); + this.preNotes = new ScoreBeatPreNotesGlyph(); + this.onNotes = new ScoreBeatGlyph(); + } + + public get prebendNoteHeadOffset() { + return (this.preNotes as ScoreBeatPreNotesGlyph).prebendNoteHeadOffset; + } + + public get accidentalsWidth() { + const preNotes = this.preNotes as ScoreBeatPreNotesGlyph; + if (preNotes && preNotes.accidentals) { + return preNotes.accidentals.width; + } + return 0; + } + + public override doMultiVoiceLayout(): void { + this.preNotes.x = 0; + (this.preNotes as ScoreBeatPreNotesGlyph).doMultiVoiceLayout(); + this.onNotes.x = this.preNotes.x + this.preNotes.width; + (this.onNotes as ScoreBeatGlyph).doMultiVoiceLayout(); + + this._bend?.doMultiVoiceLayout(); + } + public override doLayout(): void { this._effectSlur = null; this._effectEndSlur = null; + + // make space for flag + const sr = this.renderer as ScoreBarRenderer; + const beat = this.beat; + const isGrace = beat.graceType !== GraceType.None; + if (sr.hasFlag(beat)) { + const direction = sr.getBeatDirection(beat); + const scale = isGrace ? EngravingSettings.GraceScale : 1; + const symbol = FlagGlyph.getSymbol(beat.duration, direction, isGrace); + const flagWidth = sr.smuflMetrics.glyphWidths.get(symbol)! * scale; + this._flagStretch = flagWidth; + } else if (isGrace) { + // always use flag size as spacing on grace notes + const graceSpacing = + sr.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * EngravingSettings.GraceScale; + this._flagStretch = graceSpacing; + } + super.doLayout(); if (this._bend) { this._bend.renderer = this.renderer; @@ -30,6 +83,22 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { } } + public override getBoundingBoxTop(): number { + if (this._bend !== null) { + return ModelUtils.minBoundingBox(this._bend.getBoundingBoxTop(), super.getBoundingBoxTop()); + } else { + return super.getBoundingBoxTop(); + } + } + + public override getBoundingBoxBottom(): number { + if (this._bend !== null) { + return ModelUtils.maxBoundingBox(this._bend.getBoundingBoxBottom(), super.getBoundingBoxTop()); + } else { + return super.getBoundingBoxBottom(); + } + } + protected override createTies(n: Note): void { // create a tie if any effect requires it if (!n.isVisible) { @@ -45,48 +114,49 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { n.tieDestination && n.tieDestination.isVisible ) { - // tslint:disable-next-line: no-unnecessary-type-assertion - const tie: ScoreTieGlyph = new ScoreTieGlyph(n, n.tieDestination!, false); + const tie: ScoreTieGlyph = new ScoreTieGlyph(`score.tie.${n.id}`, n, n.tieDestination!, false); this.addTie(tie); } if (n.isTieDestination && !n.tieOrigin!.hasBend && !n.beat.hasWhammyBar) { - const tie: ScoreTieGlyph = new ScoreTieGlyph(n.tieOrigin!, n, true); + const tie: ScoreTieGlyph = new ScoreTieGlyph(`score.tie.${n.tieOrigin!.id}`, n.tieOrigin!, n, true); this.addTie(tie); } - // TODO: depending on the type we have other positioning - // we should place glyphs in the preNotesGlyph or postNotesGlyph if needed if (n.slideInType !== SlideInType.None || n.slideOutType !== SlideOutType.None) { const l: ScoreSlideLineGlyph = new ScoreSlideLineGlyph(n.slideInType, n.slideOutType, n, this); this.addTie(l); } if (n.isSlurOrigin && n.slurDestination && n.slurDestination.isVisible) { - // tslint:disable-next-line: no-unnecessary-type-assertion - const tie: ScoreSlurGlyph = new ScoreSlurGlyph(n, n.slurDestination!, false); + const tie: ScoreSlurGlyph = new ScoreSlurGlyph(`score.slur.${n.id}`, n, n.slurDestination!, false); this.addTie(tie); } if (n.isSlurDestination) { - const tie: ScoreSlurGlyph = new ScoreSlurGlyph(n.slurOrigin!, n, true); + const tie: ScoreSlurGlyph = new ScoreSlurGlyph(`score.slur.${n.slurOrigin!.id}`, n.slurOrigin!, n, true); this.addTie(tie); } // start effect slur on first beat if (!this._effectSlur && n.isEffectSlurOrigin && n.effectSlurDestination) { - const effectSlur = new ScoreSlurGlyph(n, n.effectSlurDestination, false); + const effectSlur = new ScoreSlurGlyph(`score.slur.effect.${n.beat.id}`, n, n.effectSlurDestination, false); this._effectSlur = effectSlur; this.addTie(effectSlur); } // end effect slur on last beat if (!this._effectEndSlur && n.beat.isEffectSlurDestination && n.beat.effectSlurOrigin) { - const direction: BeamDirection = this.onNotes.beamingHelper.direction; - const startNote: Note = + const direction = (this.renderer as LineBarRenderer).getBeatDirection(n.beat); + const startNote = direction === BeamDirection.Up ? n.beat.effectSlurOrigin.minNote! : n.beat.effectSlurOrigin.maxNote!; - const endNote: Note = direction === BeamDirection.Up ? n.beat.minNote! : n.beat.maxNote!; - const effectEndSlur = new ScoreSlurGlyph(startNote, endNote, true); + const endNote = direction === BeamDirection.Up ? n.beat.minNote! : n.beat.maxNote!; + const effectEndSlur = new ScoreSlurGlyph( + `score.slur.effect.${startNote.beat.id}`, + startNote, + endNote, + true + ); this._effectEndSlur = effectEndSlur; this.addTie(effectEndSlur); } if (n.hasBend) { if (!this._bend) { - const bend = new ScoreBendGlyph(n.beat); + const bend = new ScoreBendGlyph(this); this._bend = bend; bend.renderer = this.renderer; this.addTie(bend); @@ -103,7 +173,7 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { while (destination.nextBeat && destination.nextBeat.isLegatoDestination) { destination = destination.nextBeat; } - this.addTie(new ScoreLegatoGlyph(this.beat, destination, false)); + this.addTie(new ScoreLegatoGlyph(`score.legato.${this.beat.id}`, this.beat, destination, false)); } } else if (this.beat.isLegatoDestination) { // only create slur for last destination of "group" @@ -112,8 +182,19 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { while (origin.previousBeat && origin.previousBeat.isLegatoOrigin) { origin = origin.previousBeat; } - this.addTie(new ScoreLegatoGlyph(origin, this.beat, true)); + this.addTie(new ScoreLegatoGlyph(`score.legato.${origin.id}`, origin, this.beat, true)); } } } + + private _flagStretch = 0; + + protected override get postBeatStretch(): number { + return super.postBeatStretch + this._flagStretch; + } + + protected override updateWidth(): void { + super.updateWidth(); + this.width += this._flagStretch; + } } diff --git a/packages/alphatab/src/rendering/ScoreRenderer.ts b/packages/alphatab/src/rendering/ScoreRenderer.ts index 36ed98056..c534c4261 100644 --- a/packages/alphatab/src/rendering/ScoreRenderer.ts +++ b/packages/alphatab/src/rendering/ScoreRenderer.ts @@ -1,10 +1,15 @@ import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { Environment } from '@coderline/alphatab/Environment'; -import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@coderline/alphatab/EventEmitter'; +import { + EventEmitter, + type IEventEmitter, + type IEventEmitterOfT, + EventEmitterOfT +} from '@coderline/alphatab/EventEmitter'; import type { Score } from '@coderline/alphatab/model/Score'; import type { Track } from '@coderline/alphatab/model/Track'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { IScoreRenderer } from '@coderline/alphatab/rendering/IScoreRenderer'; +import type { IScoreRenderer, RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; import type { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; import { BoundsLookup } from '@coderline/alphatab/rendering/utils/BoundsLookup'; @@ -70,7 +75,7 @@ export class ScoreRenderer implements IScoreRenderer { return false; } - public renderScore(score: Score | null, trackIndexes: number[] | null): void { + public renderScore(score: Score | null, trackIndexes: number[] | null, renderHints?: RenderHints): void { try { this.score = score; let tracks: Track[] | null = null; @@ -92,7 +97,7 @@ export class ScoreRenderer implements IScoreRenderer { } this.tracks = tracks; - this.render(); + this.render(renderHints); } catch (e) { (this.error as EventEmitterOfT).trigger(e as Error); } @@ -130,7 +135,7 @@ export class ScoreRenderer implements IScoreRenderer { } } - public render(): void { + public render(renderHints?: RenderHints): void { if (this.width === 0) { Logger.warning('Rendering', 'AlphaTab skipped rendering because of width=0 (element invisible)', null); return; @@ -155,7 +160,7 @@ export class ScoreRenderer implements IScoreRenderer { } (this.preRender as EventEmitterOfT).trigger(false); this._recreateLayout(); - this._layoutAndRender(); + this._layoutAndRender(renderHints); Logger.debug('Rendering', 'Rendering finished'); } } @@ -178,13 +183,13 @@ export class ScoreRenderer implements IScoreRenderer { Logger.debug('Rendering', 'Resize finished'); } - private _layoutAndRender(): void { + private _layoutAndRender(renderHints?: RenderHints): void { Logger.debug( 'Rendering', `Rendering at scale ${this.settings.display.scale} with layout ${this.layout!.name}`, null ); - this.layout!.layoutAndRender(); + this.layout!.layoutAndRender(renderHints); this._renderedTracks = this.tracks; this._onRenderFinished(); (this.postRenderFinished as EventEmitter).trigger(); diff --git a/packages/alphatab/src/rendering/ScoreRendererWrapper.ts b/packages/alphatab/src/rendering/ScoreRendererWrapper.ts index 7961311c5..8445936f8 100644 --- a/packages/alphatab/src/rendering/ScoreRendererWrapper.ts +++ b/packages/alphatab/src/rendering/ScoreRendererWrapper.ts @@ -1,6 +1,11 @@ -import { EventEmitter, EventEmitterOfT, type IEventEmitter, type IEventEmitterOfT } from '@coderline/alphatab/EventEmitter'; +import { + EventEmitter, + EventEmitterOfT, + type IEventEmitter, + type IEventEmitterOfT +} from '@coderline/alphatab/EventEmitter'; import type { Score } from '@coderline/alphatab/model/Score'; -import type { IScoreRenderer } from '@coderline/alphatab/rendering/IScoreRenderer'; +import type { IScoreRenderer, RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; import type { BoundsLookup } from '@coderline/alphatab/rendering/utils/BoundsLookup'; import type { Settings } from '@coderline/alphatab/Settings'; @@ -85,18 +90,18 @@ export class ScoreRendererWrapper implements IScoreRenderer { } } - public render(): void { - this._instance?.render(); + public render(renderHints?: RenderHints): void { + this._instance?.render(renderHints); } public resizeRender(): void { this._instance?.resizeRender(); } - public renderScore(score: Score | null, trackIndexes: number[] | null): void { + public renderScore(score: Score | null, trackIndexes: number[] | null, renderHints?: RenderHints): void { this._score = score; this._trackIndexes = trackIndexes; - this._instance?.renderScore(score, trackIndexes); + this._instance?.renderScore(score, trackIndexes, renderHints); } public renderResult(resultId: string): void { diff --git a/packages/alphatab/src/rendering/SlashBarRenderer.ts b/packages/alphatab/src/rendering/SlashBarRenderer.ts index c41e7cfbb..b6165c1ab 100644 --- a/packages/alphatab/src/rendering/SlashBarRenderer.ts +++ b/packages/alphatab/src/rendering/SlashBarRenderer.ts @@ -3,19 +3,15 @@ import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; import type { Voice } from '@coderline/alphatab/model/Voice'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { LineBarRenderer } from '@coderline/alphatab/rendering//LineBarRenderer'; +import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTimeSignatureGlyph'; +import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; +import { SlashBeatContainerGlyph } from '@coderline/alphatab/rendering/SlashBeatContainerGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import { LineBarRenderer } from '@coderline/alphatab/rendering//LineBarRenderer'; -import { SlashBeatContainerGlyph } from '@coderline/alphatab/rendering/SlashBeatContainerGlyph'; -import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import { SlashBeatGlyph } from '@coderline/alphatab/rendering/glyphs/SlashBeatGlyph'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; -import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; -import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTimeSignatureGlyph'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; /** * This BarRenderer renders a bar using Slash Rhythm notation @@ -66,72 +62,52 @@ export class SlashBarRenderer extends LineBarRenderer { return 0; } - public override paint(cx: number, cy: number, canvas: ICanvas): void { - super.paint(cx, cy, canvas); - this.paintBeams(cx, cy, canvas, BeatSubElement.SlashFlags, BeatSubElement.SlashBeams); - this.paintTuplets(cx, cy, canvas, BeatSubElement.SlashTuplet); + protected override get flagsSubElement(): BeatSubElement { + return BeatSubElement.SlashFlags; + } + + protected override get beamsSubElement(): BeatSubElement { + return BeatSubElement.SlashBeams; + } + + protected override get tupletSubElement(): BeatSubElement { + return BeatSubElement.SlashTuplet; } public override doLayout(): void { super.doLayout(); - let hasTuplets: boolean = false; - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - const c = this.getVoiceContainer(voice)!; - if (c.tupletGroups.length > 0) { - hasTuplets = true; - break; - } - } - } - if (hasTuplets) { + if (this.voiceContainer.tupletGroups.size > 0) { this.registerOverflowTop(this.tupletSize); } } - public getNoteLine() { + public getNoteLine(_note: Note) { return 0; } protected override getFlagTopY(beat: Beat, direction: BeamDirection): number { - const noteHeadHeight = this.smuflMetrics.glyphHeights.get(MusicFontSymbol.NoteheadSlashWhiteHalf)!; - let y = this.getLineY(0) - noteHeadHeight / 2; - if (direction === BeamDirection.Up) { - y -= this.getFlagStemSize(beat.duration, true); + const position = direction === BeamDirection.Up ? NoteYPosition.TopWithStem : NoteYPosition.StemDown; + if (beat.notes.length > 0) { + return this.getNoteY(beat.notes[0], position); + } else { + return this.getRestY(beat, position); } - return y; } protected override getFlagBottomY(beat: Beat, direction: BeamDirection): number { - const noteHeadHeight = this.smuflMetrics.glyphHeights.get(MusicFontSymbol.NoteheadSlashWhiteHalf)!; - let y = this.getLineY(0) - noteHeadHeight / 2; - if (direction === BeamDirection.Down) { - y += this.getFlagStemSize(beat.duration, true); + const position = direction === BeamDirection.Up ? NoteYPosition.StemUp : NoteYPosition.BottomWithStem; + + if (beat.notes.length > 0) { + return this.getNoteY(beat.notes[0], position); + } else { + return this.getRestY(beat, position); } - return y; } protected override getBeamDirection(_helper: BeamingHelper): BeamDirection { return BeamDirection.Up; } - public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { - let y = super.getNoteY(note, requestedPosition); - if (Number.isNaN(y)) { - y = this.getLineY(0); - } - return y; - } - - protected override calculateBeamYWithDirection(_h: BeamingHelper, _x: number, _direction: BeamDirection): number { - return this.getLineY(0) - this.getFlagStemSize(_h.shortestDuration); - } - - protected override getBarLineStart(_beat: Beat, _direction: BeamDirection): number { - const noteHeadHeight = this.smuflMetrics.glyphHeights.get(MusicFontSymbol.NoteheadSlashWhiteHalf)!; - return this.getLineY(0) - noteHeadHeight / 2; - } - protected override createLinePreBeatGlyphs(): void { // Key signature if ( @@ -171,12 +147,26 @@ export class SlashBarRenderer extends LineBarRenderer { } protected override createVoiceGlyphs(v: Voice): void { + if (v.index > 0) { + return; + } + + super.createVoiceGlyphs(v); for (const b of v.beats) { - const container: SlashBeatContainerGlyph = new SlashBeatContainerGlyph(b, this.getVoiceContainer(v)!); - container.preNotes = new BeatGlyphBase(); - container.onNotes = v.index === 0 ? new SlashBeatGlyph() : new BeatOnNoteGlyphBase(); - this.addBeatGlyph(container); + this.addBeatGlyph(new SlashBeatContainerGlyph(b)); + } + } + + protected override calculateOverflows(rendererTop: number, rendererBottom: number): void { + super.calculateOverflows(rendererTop, rendererBottom); + if (this.bar.isEmpty) { + return; } + this.calculateBeamingOverflows(rendererTop, rendererBottom); + } + + protected override shouldPaintBeamingHelper(h: BeamingHelper): boolean { + return super.shouldPaintBeamingHelper(h) && h.voice!.index === 0; } protected override paintBeamingStem( @@ -188,12 +178,19 @@ export class SlashBarRenderer extends LineBarRenderer { canvas: ICanvas ): void { using _ = ElementStyleHelper.beat(canvas, BeatSubElement.SlashStem, beat); - const lineWidth = canvas.lineWidth; - canvas.lineWidth = this.smuflMetrics.stemThickness; - canvas.beginPath(); - canvas.moveTo(x, topY); - canvas.lineTo(x, bottomY); - canvas.stroke(); - canvas.lineWidth = lineWidth; + canvas.fillRect(x, topY, this.smuflMetrics.stemThickness, bottomY - topY); + } + + protected override paintBeamHelper( + cx: number, + cy: number, + canvas: ICanvas, + h: BeamingHelper, + flagsElement: BeatSubElement, + beamsElement: BeatSubElement + ): void { + if (h.voice?.index === 0) { + super.paintBeamHelper(cx, cy, canvas, h, flagsElement, beamsElement); + } } } diff --git a/packages/alphatab/src/rendering/SlashBarRendererFactory.ts b/packages/alphatab/src/rendering/SlashBarRendererFactory.ts index 3d1fd7ba7..24b6ef53d 100644 --- a/packages/alphatab/src/rendering/SlashBarRendererFactory.ts +++ b/packages/alphatab/src/rendering/SlashBarRendererFactory.ts @@ -1,11 +1,10 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; +import type { Staff } from '@coderline/alphatab/model/Staff'; +import type { Track } from '@coderline/alphatab/model/Track'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { BarRendererFactory } from '@coderline/alphatab/rendering/BarRendererFactory'; -import { SlashBarRenderer } from '@coderline/alphatab/rendering/SlashBarRenderer'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import type { Track } from '@coderline/alphatab/model/Track'; -import type { Staff } from '@coderline/alphatab/model/Staff'; -import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; +import { SlashBarRenderer } from '@coderline/alphatab/rendering/SlashBarRenderer'; /** * This Factory produces SlashBarRenderer instances @@ -16,14 +15,6 @@ export class SlashBarRendererFactory extends BarRendererFactory { return SlashBarRenderer.StaffId; } - public override getStaffPaddingTop(staff: RenderStaff): number { - return staff.system.layout.renderer.settings.display.notationStaffPaddingTop; - } - - public override getStaffPaddingBottom(staff: RenderStaff): number { - return staff.system.layout.renderer.settings.display.notationStaffPaddingBottom; - } - public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { return new SlashBarRenderer(renderer, bar); } diff --git a/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts b/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts index b0b580817..669294cc9 100644 --- a/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts @@ -1,6 +1,14 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import type { Beat } from '@coderline/alphatab/model/Beat'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; +import { FlagGlyph } from '@coderline/alphatab/rendering/glyphs/FlagGlyph'; +import { SlashBeatGlyph } from '@coderline/alphatab/rendering/glyphs/SlashBeatGlyph'; import { SlashTieGlyph } from '@coderline/alphatab/rendering/glyphs/SlashTieGlyph'; +import type { SlashBarRenderer } from '@coderline/alphatab/rendering/SlashBarRenderer'; /** * @internal @@ -8,21 +16,58 @@ import { SlashTieGlyph } from '@coderline/alphatab/rendering/glyphs/SlashTieGlyp export class SlashBeatContainerGlyph extends BeatContainerGlyph { private _tiedNoteTie: SlashTieGlyph | null = null; + public constructor(beat:Beat){ + super(beat); + this.preNotes = new BeatGlyphBase(); + this.onNotes = new SlashBeatGlyph(); + } + + public override doLayout(): void { + // make space for flag + const sr = this.renderer as SlashBarRenderer; + const beat = this.beat; + const isGrace = beat.graceType !== GraceType.None; + if (sr.hasFlag(beat)) { + const direction = sr.getBeatDirection(beat); + const scale = isGrace ? EngravingSettings.GraceScale : 1; + const symbol = FlagGlyph.getSymbol(beat.duration, direction, isGrace); + const flagWidth = sr.smuflMetrics.glyphWidths.get(symbol)! * scale; + this._flagStretch = flagWidth; + } else if (isGrace) { + // always use flag size as spacing on grace notes + const graceSpacing = + sr.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * EngravingSettings.GraceScale; + this._flagStretch = graceSpacing; + } + + super.doLayout(); + } + protected override createTies(n: Note): void { // create a tie if any effect requires it if (!n.isVisible) { return; } - if (!this._tiedNoteTie && n.isTieOrigin && n.tieDestination!.isVisible) { - const tie: SlashTieGlyph = new SlashTieGlyph(n, n.tieDestination!, false); + const tie: SlashTieGlyph = new SlashTieGlyph('slash.tie', n, n.tieDestination!, false); this._tiedNoteTie = tie; this.addTie(tie); } if (!this._tiedNoteTie && n.isTieDestination) { - const tie: SlashTieGlyph = new SlashTieGlyph(n.tieOrigin!, n, true); + const tie: SlashTieGlyph = new SlashTieGlyph('slash.tie', n.tieOrigin!, n, true); this._tiedNoteTie = tie; this.addTie(tie); } } + + private _flagStretch = 0; + + protected override get postBeatStretch(): number { + return super.postBeatStretch + this._flagStretch; + } + + protected override updateWidth(): void { + super.updateWidth(); + this.width += this._flagStretch; + } } diff --git a/packages/alphatab/src/rendering/TabBarRenderer.ts b/packages/alphatab/src/rendering/TabBarRenderer.ts index ffd772d65..122975265 100644 --- a/packages/alphatab/src/rendering/TabBarRenderer.ts +++ b/packages/alphatab/src/rendering/TabBarRenderer.ts @@ -1,27 +1,23 @@ -import { type Bar, BarSubElement } from '@coderline/alphatab/model/Bar'; +import { BarSubElement } from '@coderline/alphatab/model/Bar'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; -import { Duration } from '@coderline/alphatab/model/Duration'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import type { Note } from '@coderline/alphatab/model/Note'; import type { Voice } from '@coderline/alphatab/model/Voice'; import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; import { TabBeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatContainerGlyph'; -import { TabBeatGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatGlyph'; -import { TabBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatPreNotesGlyph'; +import type { TabBeatGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatGlyph'; import { TabClefGlyph } from '@coderline/alphatab/rendering/glyphs/TabClefGlyph'; import type { TabNoteChordGlyph } from '@coderline/alphatab/rendering/glyphs/TabNoteChordGlyph'; import { TabTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/TabTimeSignatureGlyph'; -import type { VoiceContainerGlyph } from '@coderline/alphatab/rendering/glyphs/VoiceContainerGlyph'; -import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import type { ReservedLayoutAreaSlot } from '@coderline/alphatab/rendering/utils/BarCollisionHelper'; -import { MultiBarRestBeatContainerGlyph } from '@coderline/alphatab/rendering/MultiBarRestBeatContainerGlyph'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; /** * This BarRenderer renders a bar using guitar tablature notation @@ -58,17 +54,6 @@ export class TabBarRenderer extends LineBarRenderer { return BarSubElement.GuitarTabsStaffLine; } - public constructor(renderer: ScoreRenderer, bar: Bar) { - super(renderer, bar); - - if (!bar.staff.showStandardNotation) { - this.showTimeSignature = true; - this.showRests = true; - this.showTiedNotes = true; - this._showMultiBarRest = true; - } - } - public override get lineSpacing(): number { return this.smuflMetrics.tabLineSpacing; } @@ -89,38 +74,33 @@ export class TabBarRenderer extends LineBarRenderer { return mode; } - /** - * Gets the relative y position of the given steps relative to first line. - * @param line the line of the particular string where 0 is the most top line - * @param correction - * @returns - */ - public getTabY(line: number): number { - return super.getLineY(line); + public override getNoteLine(note: Note): number { + return this.bar.staff.tuning.length - note.string; } - public getTabHeight(line: number): number { - return super.getLineHeight(line); - } + public minString = Number.NaN; + public maxString = Number.NaN; protected override collectSpaces(spaces: Float32Array[][]): void { + if (this.additionalMultiRestBars) { + return; + } + const padding: number = this.smuflMetrics.staffLineThickness; - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - const vc: VoiceContainerGlyph = this.getVoiceContainer(voice)!; - for (const bg of vc.beatGlyphs) { - const notes: TabBeatGlyph = bg.onNotes as TabBeatGlyph; - const noteNumbers: TabNoteChordGlyph | null = notes.noteNumbers; - if (noteNumbers) { - for (const [str, noteNumber] of noteNumbers.notesPerString) { - if (!noteNumber.isEmpty) { - spaces[this.bar.staff.tuning.length - str].push( - new Float32Array([ - vc.x + bg.x + notes.x + noteNumbers!.x - padding, - noteNumbers!.width + padding * 2 - ]) - ); - } + const tuning = this.bar.staff.tuning; + for (const voice of this.voiceContainer.beatGlyphs.values()) { + for (const bg of voice) { + const notes: TabBeatGlyph = (bg as TabBeatContainerGlyph).onNotes as TabBeatGlyph; + const noteNumbers: TabNoteChordGlyph | null = notes.noteNumbers; + if (noteNumbers) { + for (const [str, noteNumber] of noteNumbers.notesPerString) { + if (!noteNumber.isEmpty) { + spaces[tuning.length - str].push( + new Float32Array([ + this.beatGlyphsStart + bg.x + notes.x + noteNumbers!.x - padding, + noteNumbers!.width + padding * 2 + ]) + ); } } } @@ -128,59 +108,42 @@ export class TabBarRenderer extends LineBarRenderer { } } - protected override adjustSizes(): void { - if (this.rhythmMode !== TabRhythmMode.Hidden) { - let shortestTremolo = Duration.Whole; - for (const b of this.helpers.beamHelpers) { - for (const h of b) { - if (h.tremoloDuration && (!shortestTremolo || shortestTremolo < h.tremoloDuration!)) { - shortestTremolo = h.tremoloDuration!; - } - } - } - - switch (shortestTremolo) { - case Duration.Eighth: - this.height += this.smuflMetrics.glyphHeights.get(MusicFontSymbol.Tremolo1)!; - break; - case Duration.Sixteenth: - this.height += this.smuflMetrics.glyphHeights.get(MusicFontSymbol.Tremolo2)!; - break; - case Duration.ThirtySecond: - this.height += this.smuflMetrics.glyphHeights.get(MusicFontSymbol.Tremolo3)!; - break; - } + public override doLayout(): void { + const hasStandardNotation = + this.bar.staff.showStandardNotation && this.scoreRenderer.layout!.profile.has(ScoreBarRenderer.StaffId); - this.height += this.settings.notation.rhythmHeight; - this.bottomPadding += this.settings.notation.rhythmHeight; + if (!hasStandardNotation) { + this.showTimeSignature = true; + this.showRests = true; + this.showTiedNotes = true; + this._showMultiBarRest = true; } - } - public override doLayout(): void { super.doLayout(); + + const hasNoteOnTopString = this.minString === 0; + if (hasNoteOnTopString) { + this.registerOverflowTop(this.lineSpacing / 2); + } + const hasNoteOnBottomString = this.maxString === this.bar.staff.tuning.length - 1; + if (hasNoteOnBottomString) { + this.registerOverflowBottom(this.lineSpacing / 2); + } + if (this.rhythmMode !== TabRhythmMode.Hidden) { - this._hasTuplets = false; - for (const voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - const c: VoiceContainerGlyph = this.getVoiceContainer(voice)!; - if (c.tupletGroups.length > 0) { - this._hasTuplets = true; - break; - } - } - } + this._hasTuplets = this.voiceContainer.tupletGroups.size > 0; if (this._hasTuplets) { - this.registerOverflowBottom(this.tupletSize); + this.registerOverflowBottom(this.settings.notation.rhythmHeight + this.tupletSize); } } } protected override createLinePreBeatGlyphs(): void { // Clef - if (this.isFirstOfLine) { + if (this.isFirstOfStaff) { const center: number = (this.bar.staff.tuning.length - 1) / 2; this.createStartSpacing(); - this.addPreBeatGlyph(new TabClefGlyph(0, this.getTabY(center))); + this.addPreBeatGlyph(new TabClefGlyph(0, this.getLineY(center))); } // Time Signature if ( @@ -208,7 +171,7 @@ export class TabBarRenderer extends LineBarRenderer { this.addPreBeatGlyph( new TabTimeSignatureGlyph( 0, - this.getTabY(lines), + this.getLineY(lines), this.bar.masterBar.timeSignatureNumerator, this.bar.masterBar.timeSignatureDenominator, this.bar.masterBar.timeSignatureCommon, @@ -218,70 +181,80 @@ export class TabBarRenderer extends LineBarRenderer { } protected override createVoiceGlyphs(v: Voice): void { - // multibar rest - if (this.additionalMultiRestBars) { - const container = new MultiBarRestBeatContainerGlyph(this.getVoiceContainer(v)!); - this.addBeatGlyph(container); - } else { - for (const b of v.beats) { - const container: TabBeatContainerGlyph = new TabBeatContainerGlyph(b, this.getVoiceContainer(v)!); - container.preNotes = new TabBeatPreNotesGlyph(); - container.onNotes = new TabBeatGlyph(); - this.addBeatGlyph(container); - } + super.createVoiceGlyphs(v); + + for (const b of v.beats) { + this.addBeatGlyph(new TabBeatContainerGlyph(b)); } } - public override paint(cx: number, cy: number, canvas: ICanvas): void { - super.paint(cx, cy, canvas); - if (this.rhythmMode !== TabRhythmMode.Hidden) { - this.paintBeams(cx, cy, canvas, BeatSubElement.GuitarTabFlags, BeatSubElement.GuitarTabBeams); - this.paintTuplets(cx, cy, canvas, BeatSubElement.GuitarTabTuplet); - } + protected override get flagsSubElement(): BeatSubElement { + return BeatSubElement.GuitarTabFlags; } - public override drawBeamHelperAsFlags(h: BeamingHelper): boolean { - return super.drawBeamHelperAsFlags(h) || this.rhythmMode === TabRhythmMode.ShowWithBeams; + protected override get beamsSubElement(): BeatSubElement { + return BeatSubElement.GuitarTabBeams; } - protected override getFlagTopY(beat: Beat, _direction: BeamDirection): number { - const startGlyph: TabBeatGlyph = this.getOnNotesGlyphForBeat(beat) as TabBeatGlyph; - if (!startGlyph.noteNumbers || beat.duration === Duration.Half) { - return this.height - this.settings.notation.rhythmHeight - this.tupletSize; - } - return ( - startGlyph.noteNumbers.getNoteY(startGlyph.noteNumbers.minStringNote!, NoteYPosition.Bottom) + - this.smuflMetrics.staffLineThickness - ); + protected override get tupletSubElement(): BeatSubElement { + return BeatSubElement.GuitarTabTuplet; } - protected override getFlagBottomY(_beat: Beat, _direction: BeamDirection): number { - return this.getFlagAndBarPos(); + protected override paintBeams( + cx: number, + cy: number, + canvas: ICanvas, + flagsElement: BeatSubElement, + beamsElement: BeatSubElement + ): void { + if (this.rhythmMode !== TabRhythmMode.Hidden) { + super.paintBeams(cx, cy, canvas, flagsElement, beamsElement); + } } - protected override getFlagStemSize(_duration: Duration, _forceMinStem: boolean = false): number { - return 0; // fixed size via getFlagBottomY + protected override paintTuplets( + cx: number, + cy: number, + canvas: ICanvas, + beatElement: BeatSubElement, + bracketsAsArcs: boolean = false + ): void { + if (this.rhythmMode !== TabRhythmMode.Hidden) { + super.paintTuplets(cx, cy, canvas, beatElement, bracketsAsArcs); + } } - protected override getBarLineStart(beat: Beat, direction: BeamDirection): number { - return this.getFlagTopY(beat, direction); + public override drawBeamHelperAsFlags(h: BeamingHelper): boolean { + return super.drawBeamHelperAsFlags(h) || this.rhythmMode === TabRhythmMode.ShowWithBeams; } - protected override getBeamDirection(_helper: BeamingHelper): BeamDirection { - return BeamDirection.Down; + protected override getFlagTopY(beat: Beat, direction: BeamDirection): number { + const maxNote = beat.maxStringNote; + const position = direction === BeamDirection.Up ? NoteYPosition.TopWithStem : NoteYPosition.StemDown; + if (maxNote) { + return this.getNoteY(maxNote, position); + } else { + return this.getRestY(beat, position); + } } - protected getFlagAndBarPos(): number { - return this.height - (this._hasTuplets ? this.tupletSize / 2 : 0); + protected override getFlagBottomY(beat: Beat, direction: BeamDirection): number { + const maxNote = beat.minStringNote; + const position = direction === BeamDirection.Up ? NoteYPosition.StemUp : NoteYPosition.BottomWithStem; + + if (maxNote) { + return this.getNoteY(maxNote, position); + } else { + return this.getRestY(beat, position); + } } - protected override calculateBeamYWithDirection(_h: BeamingHelper, _x: number, _direction: BeamDirection): number { - // currently only used for duplets - return this.getFlagAndBarPos(); + protected override getBeamDirection(_helper: BeamingHelper): BeamDirection { + return BeamDirection.Down; } - protected override shouldPaintFlag(beat: Beat, h: BeamingHelper): boolean { - if (!super.shouldPaintFlag(beat, h)) { + protected override shouldPaintFlag(beat: Beat): boolean { + if (!super.shouldPaintFlag(beat)) { return false; } @@ -289,7 +262,7 @@ export class TabBarRenderer extends LineBarRenderer { return false; } - return this.drawBeamHelperAsFlags(h); + return true; } protected override paintBeamingStem( @@ -314,19 +287,38 @@ export class TabBarRenderer extends LineBarRenderer { holes.sort((a, b) => a.topY - b.topY); } - let y = bottomY; - while (y > topY) { - let lineY = topY; - // draw until next hole (if hole reaches into line) - if (holes.length > 0 && holes[holes.length - 1].bottomY > lineY) { - const bottomHole = holes.pop()!; - lineY = cy + bottomHole.bottomY; - canvas.fillRect(x, lineY, this.smuflMetrics.stemThickness, y - lineY); - y = cy + bottomHole.topY; - } else { - canvas.fillRect(x, lineY, this.smuflMetrics.stemThickness, y - lineY); - break; + // fast path -> single note == full line + if (holes.length === 1) { + canvas.fillRect(x, topY, this.smuflMetrics.stemThickness, bottomY - topY); + return; + } + + const bottomYRelative = bottomY - cy; + // slow path -> multiple notes == lines between notes + const bottomHole = holes[holes.length - 1]; + canvas.fillRect( + x, + cy + bottomHole.bottomY, + this.smuflMetrics.stemThickness, + bottomYRelative - bottomHole.bottomY + ); + + for (let i = holes.length - 1; i > 0; i--) { + const bottomHoleY = holes[i].topY; + const topHoleY = holes[i - 1].bottomY; + if (topHoleY < bottomHoleY) { + canvas.fillRect(x, cy + topHoleY, this.smuflMetrics.stemThickness, bottomHoleY - topHoleY); } } } + + protected override calculateOverflows(rendererTop: number, rendererBottom: number): void { + super.calculateOverflows(rendererTop, rendererBottom); + if (this.bar.isEmpty) { + return; + } + if (this.rhythmMode !== TabRhythmMode.Hidden) { + this.calculateBeamingOverflows(rendererTop, rendererBottom); + } + } } diff --git a/packages/alphatab/src/rendering/TabBarRendererFactory.ts b/packages/alphatab/src/rendering/TabBarRendererFactory.ts index ffca3e357..df2521c38 100644 --- a/packages/alphatab/src/rendering/TabBarRendererFactory.ts +++ b/packages/alphatab/src/rendering/TabBarRendererFactory.ts @@ -2,34 +2,21 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Staff } from '@coderline/alphatab/model/Staff'; import type { Track } from '@coderline/alphatab/model/Track'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; -import { BarRendererFactory } from '@coderline/alphatab/rendering/BarRendererFactory'; +import { BarRendererFactory, type EffectBandInfo } from '@coderline/alphatab/rendering/BarRendererFactory'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; -import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; /** * This Factory produces TabBarRenderer instances * @internal */ export class TabBarRendererFactory extends BarRendererFactory { - public showTimeSignature: boolean | null = null; - public showRests: boolean | null = null; - public showTiedNotes: boolean | null = null; - public get staffId(): string { return TabBarRenderer.StaffId; } - public override getStaffPaddingTop(staff: RenderStaff): number { - return staff.system.layout.renderer.settings.display.notationStaffPaddingTop; - } - - public override getStaffPaddingBottom(staff: RenderStaff): number { - return staff.system.layout.renderer.settings.display.notationStaffPaddingBottom; - } - - public constructor() { - super(); + public constructor(effectBands: EffectBandInfo[]) { + super(effectBands); this.hideOnPercussionTrack = true; } @@ -38,16 +25,6 @@ export class TabBarRendererFactory extends BarRendererFactory { } public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { - const tabBarRenderer: TabBarRenderer = new TabBarRenderer(renderer, bar); - if (this.showRests !== null) { - tabBarRenderer.showRests = this.showRests!; - } - if (this.showTimeSignature !== null) { - tabBarRenderer.showTimeSignature = this.showTimeSignature!; - } - if (this.showTiedNotes !== null) { - tabBarRenderer.showTiedNotes = this.showTiedNotes!; - } - return tabBarRenderer; + return new TabBarRenderer(renderer, bar); } } diff --git a/packages/alphatab/src/rendering/_barrel.ts b/packages/alphatab/src/rendering/_barrel.ts index cfa12d921..933190237 100644 --- a/packages/alphatab/src/rendering/_barrel.ts +++ b/packages/alphatab/src/rendering/_barrel.ts @@ -1,12 +1,12 @@ +export type { IScoreRenderer, RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; export { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; -export type { IScoreRenderer } from '@coderline/alphatab/rendering/IScoreRenderer'; export { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; export { BarBounds } from '@coderline/alphatab/rendering/utils/BarBounds'; +export { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; export { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; export { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; export { BoundsLookup } from '@coderline/alphatab/rendering/utils/BoundsLookup'; export { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds'; export { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; export { StaffSystemBounds } from '@coderline/alphatab/rendering/utils/StaffSystemBounds'; -export { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; diff --git a/packages/alphatab/src/rendering/effects/AlternateEndingsEffectInfo.ts b/packages/alphatab/src/rendering/effects/AlternateEndingsEffectInfo.ts index 00654d2bd..ecab2d4e0 100644 --- a/packages/alphatab/src/rendering/effects/AlternateEndingsEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/AlternateEndingsEffectInfo.ts @@ -3,14 +3,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import { AlternateEndingsGlyph } from '@coderline/alphatab/rendering/glyphs/AlternateEndingsGlyph'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class AlternateEndingsEffectInfo extends EffectBarRendererInfo { +export class AlternateEndingsEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectAlternateEndings; } diff --git a/packages/alphatab/src/rendering/effects/BeatBarreEffectInfo.ts b/packages/alphatab/src/rendering/effects/BeatBarreEffectInfo.ts index f5ce4e9ea..b17984aae 100644 --- a/packages/alphatab/src/rendering/effects/BeatBarreEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/BeatBarreEffectInfo.ts @@ -3,7 +3,7 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@coderline/alphatab/rendering/glyphs/LineRangedGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { BarreShape } from '@coderline/alphatab/model/BarreShape'; @@ -11,7 +11,7 @@ import { BarreShape } from '@coderline/alphatab/model/BarreShape'; /** * @internal */ -export class BeatBarreEffectInfo extends EffectBarRendererInfo { +export class BeatBarreEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectLetRing; } @@ -45,7 +45,7 @@ export class BeatBarreEffectInfo extends EffectBarRendererInfo { barre += `B ${BeatBarreEffectInfo.toRoman(beat.barreFret)}`; - return new LineRangedGlyph(barre, false); + return new LineRangedGlyph(barre, NotationElement.EffectBeatBarre, false); } private static readonly _romanLetters = new Map([ diff --git a/packages/alphatab/src/rendering/effects/BeatTimerEffectInfo.ts b/packages/alphatab/src/rendering/effects/BeatTimerEffectInfo.ts index e5fa21258..ec8b5a8c4 100644 --- a/packages/alphatab/src/rendering/effects/BeatTimerEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/BeatTimerEffectInfo.ts @@ -1,5 +1,5 @@ import { NotationElement } from '@coderline/alphatab/NotationSettings'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { Settings } from '@coderline/alphatab/Settings'; import type { Beat } from '@coderline/alphatab/model/Beat'; @@ -10,7 +10,7 @@ import { BeatTimerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatTimerGl /** * @internal */ -export class BeatTimerEffectInfo extends EffectBarRendererInfo { +export class BeatTimerEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectBeatTimer; } diff --git a/packages/alphatab/src/rendering/effects/CapoEffectInfo.ts b/packages/alphatab/src/rendering/effects/CapoEffectInfo.ts index 1ed9b0c02..65c8eb2a2 100644 --- a/packages/alphatab/src/rendering/effects/CapoEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/CapoEffectInfo.ts @@ -4,14 +4,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class CapoEffectInfo extends EffectBarRendererInfo { +export class CapoEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectCapo; } @@ -37,7 +37,7 @@ export class CapoEffectInfo extends EffectBarRendererInfo { 0, 0, `Capo. fret ${beat.voice.bar.staff.capo}`, - renderer.resources.effectFont, + renderer.resources.elementFonts.get(NotationElement.EffectCapo)!, TextAlign.Left ); } diff --git a/packages/alphatab/src/rendering/effects/ChordsEffectInfo.ts b/packages/alphatab/src/rendering/effects/ChordsEffectInfo.ts index 6d80e437d..f5eafef22 100644 --- a/packages/alphatab/src/rendering/effects/ChordsEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/ChordsEffectInfo.ts @@ -4,14 +4,15 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; +import { ChordDiagramGlyph } from '@coderline/alphatab/rendering/glyphs/ChordDiagramGlyph'; /** * @internal */ -export class ChordsEffectInfo extends EffectBarRendererInfo { +export class ChordsEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectChordNames; } @@ -33,7 +34,16 @@ export class ChordsEffectInfo extends EffectBarRendererInfo { } public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { - return new TextGlyph(0, 0, beat.chord!.name, renderer.resources.effectFont, TextAlign.Center); + const showDiagram = beat.voice.bar.staff.track.score.stylesheet.globalDisplayChordDiagramsInScore; + return showDiagram + ? new ChordDiagramGlyph(0, 0, beat.chord!, NotationElement.EffectChordNames, true) + : new TextGlyph( + 0, + 0, + beat.chord!.name, + renderer.resources.elementFonts.get(NotationElement.EffectChordNames)!, + TextAlign.Center + ); } public canExpand(_from: Beat, _to: Beat): boolean { diff --git a/packages/alphatab/src/rendering/effects/CrescendoEffectInfo.ts b/packages/alphatab/src/rendering/effects/CrescendoEffectInfo.ts index e5157477a..5127cfa5a 100644 --- a/packages/alphatab/src/rendering/effects/CrescendoEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/CrescendoEffectInfo.ts @@ -4,14 +4,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import { CrescendoGlyph } from '@coderline/alphatab/rendering/glyphs/CrescendoGlyph'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class CrescendoEffectInfo extends EffectBarRendererInfo { +export class CrescendoEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectCrescendo; } diff --git a/packages/alphatab/src/rendering/effects/DirectionsEffectInfo.ts b/packages/alphatab/src/rendering/effects/DirectionsEffectInfo.ts index 8fe7374e5..8e9f6e1cb 100644 --- a/packages/alphatab/src/rendering/effects/DirectionsEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/DirectionsEffectInfo.ts @@ -2,7 +2,7 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { DirectionsContainerGlyph } from '@coderline/alphatab/rendering/glyphs/DirectionsContainerGlyph'; @@ -10,7 +10,7 @@ import { DirectionsContainerGlyph } from '@coderline/alphatab/rendering/glyphs/D /** * @internal */ -export class DirectionsEffectInfo extends EffectBarRendererInfo { +export class DirectionsEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectDirections; } diff --git a/packages/alphatab/src/rendering/effects/DynamicsEffectInfo.ts b/packages/alphatab/src/rendering/effects/DynamicsEffectInfo.ts index af6990c86..11b100781 100644 --- a/packages/alphatab/src/rendering/effects/DynamicsEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/DynamicsEffectInfo.ts @@ -3,14 +3,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import { DynamicsGlyph } from '@coderline/alphatab/rendering/glyphs/DynamicsGlyph'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class DynamicsEffectInfo extends EffectBarRendererInfo { +export class DynamicsEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectDynamics; } diff --git a/packages/alphatab/src/rendering/effects/FadeEffectInfo.ts b/packages/alphatab/src/rendering/effects/FadeEffectInfo.ts index d0e78d749..5a73dca74 100644 --- a/packages/alphatab/src/rendering/effects/FadeEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/FadeEffectInfo.ts @@ -2,7 +2,7 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { FadeType } from '@coderline/alphatab/model/FadeType'; @@ -11,7 +11,7 @@ import { FadeGlyph } from '@coderline/alphatab/rendering/glyphs/FadeGlyph'; /** * @internal */ -export class FadeEffectInfo extends EffectBarRendererInfo { +export class FadeEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectFadeIn; } diff --git a/packages/alphatab/src/rendering/effects/FermataEffectInfo.ts b/packages/alphatab/src/rendering/effects/FermataEffectInfo.ts index 8aaecbd39..d4b621b51 100644 --- a/packages/alphatab/src/rendering/effects/FermataEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/FermataEffectInfo.ts @@ -3,14 +3,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { FermataGlyph } from '@coderline/alphatab/rendering/glyphs/FermataGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class FermataEffectInfo extends EffectBarRendererInfo { +export class FermataEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectFermata; } diff --git a/packages/alphatab/src/rendering/effects/FingeringEffectInfo.ts b/packages/alphatab/src/rendering/effects/FingeringEffectInfo.ts index d41248f30..f6c13ee3e 100644 --- a/packages/alphatab/src/rendering/effects/FingeringEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/FingeringEffectInfo.ts @@ -5,7 +5,7 @@ import { FingeringMode, NotationElement } from '@coderline/alphatab/NotationSett import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { FingeringGroupGlyph } from '@coderline/alphatab/rendering/glyphs/FingeringGroupGlyph'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; @@ -13,7 +13,7 @@ import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGl /** * @internal */ -export class FingeringEffectInfo extends EffectBarRendererInfo { +export class FingeringEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectFingering; } diff --git a/packages/alphatab/src/rendering/effects/FreeTimeEffectInfo.ts b/packages/alphatab/src/rendering/effects/FreeTimeEffectInfo.ts index 056c939fe..848b69c03 100644 --- a/packages/alphatab/src/rendering/effects/FreeTimeEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/FreeTimeEffectInfo.ts @@ -4,14 +4,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class FreeTimeEffectInfo extends EffectBarRendererInfo { +export class FreeTimeEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectText; } @@ -39,7 +39,13 @@ export class FreeTimeEffectInfo extends EffectBarRendererInfo { } public createNewGlyph(renderer: BarRendererBase, _beat: Beat): EffectGlyph { - return new TextGlyph(0, 0, 'Free time', renderer.resources.effectFont, TextAlign.Left); + return new TextGlyph( + 0, + 0, + 'Free time', + renderer.resources.elementFonts.get(NotationElement.EffectFreeTime)!, + TextAlign.Left + ); } public canExpand(_from: Beat, _to: Beat): boolean { diff --git a/packages/alphatab/src/rendering/effects/GolpeEffectInfo.ts b/packages/alphatab/src/rendering/effects/GolpeEffectInfo.ts index 671652481..3c86a5394 100644 --- a/packages/alphatab/src/rendering/effects/GolpeEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/GolpeEffectInfo.ts @@ -2,8 +2,8 @@ import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { Settings } from '@coderline/alphatab/Settings'; import type { Beat } from '@coderline/alphatab/model/Beat'; -import type { GolpeType } from '@coderline/alphatab/model/GolpeType'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { GolpeType } from '@coderline/alphatab/model/GolpeType'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { GuitarGolpeGlyph } from '@coderline/alphatab/rendering/glyphs/GuitarGolpeGlyph'; @@ -11,20 +11,22 @@ import { GuitarGolpeGlyph } from '@coderline/alphatab/rendering/glyphs/GuitarGol /** * @internal */ -export class GolpeEffectInfo extends EffectBarRendererInfo { +export class GolpeEffectInfo extends EffectInfo { private _type: GolpeType; - private _shouldCreate?: (settings: Settings, beat: Beat) => boolean; - public constructor(type: GolpeType, shouldCreate?: (settings: Settings, beat: Beat) => boolean) { + public constructor(type: GolpeType) { super(); this._type = type; - this._shouldCreate = shouldCreate; } public get notationElement(): NotationElement { return NotationElement.EffectGolpe; } + public override get effectId(): string { + return `${super.effectId}.${GolpeType[this._type]}`; + } + public get hideOnMultiTrack(): boolean { return false; } @@ -37,9 +39,8 @@ export class GolpeEffectInfo extends EffectBarRendererInfo { return EffectBarGlyphSizing.SingleOnBeat; } - public shouldCreateGlyph(settings: Settings, beat: Beat): boolean { - const shouldCreate = this._shouldCreate; - return beat.golpe === this._type && (!shouldCreate || shouldCreate(settings, beat)); + public shouldCreateGlyph(_settings: Settings, beat: Beat): boolean { + return beat.golpe === this._type; } public createNewGlyph(_renderer: BarRendererBase, _beat: Beat): EffectGlyph { diff --git a/packages/alphatab/src/rendering/effects/HarmonicsEffectInfo.ts b/packages/alphatab/src/rendering/effects/HarmonicsEffectInfo.ts index bcfbf9858..5344b0b79 100644 --- a/packages/alphatab/src/rendering/effects/HarmonicsEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/HarmonicsEffectInfo.ts @@ -70,7 +70,10 @@ export class HarmonicsEffectInfo extends NoteEffectInfoBase { } public createNewGlyph(_renderer: BarRendererBase, _beat: Beat): EffectGlyph { - return new LineRangedGlyph(HarmonicsEffectInfo.harmonicToString(this._harmonicType)); + return new LineRangedGlyph( + HarmonicsEffectInfo.harmonicToString(this._harmonicType), + NotationElement.EffectHarmonics + ); } public static harmonicToString(type: HarmonicType): string { diff --git a/packages/alphatab/src/rendering/effects/LetRingEffectInfo.ts b/packages/alphatab/src/rendering/effects/LetRingEffectInfo.ts index 9f01ea774..ee6c9d16e 100644 --- a/packages/alphatab/src/rendering/effects/LetRingEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/LetRingEffectInfo.ts @@ -3,14 +3,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@coderline/alphatab/rendering/glyphs/LineRangedGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class LetRingEffectInfo extends EffectBarRendererInfo { +export class LetRingEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectLetRing; } @@ -32,7 +32,7 @@ export class LetRingEffectInfo extends EffectBarRendererInfo { } public createNewGlyph(_renderer: BarRendererBase, _beat: Beat): EffectGlyph { - return new LineRangedGlyph('LetRing'); + return new LineRangedGlyph('LetRing', NotationElement.EffectLetRing); } public canExpand(_from: Beat, _to: Beat): boolean { diff --git a/packages/alphatab/src/rendering/effects/LyricsEffectInfo.ts b/packages/alphatab/src/rendering/effects/LyricsEffectInfo.ts index 9e1cfd0ca..30972c7ce 100644 --- a/packages/alphatab/src/rendering/effects/LyricsEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/LyricsEffectInfo.ts @@ -1,17 +1,17 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { LyricsGlyph } from '@coderline/alphatab/rendering/glyphs/LyricsGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; import type { Settings } from '@coderline/alphatab/Settings'; -import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class LyricsEffectInfo extends EffectBarRendererInfo { +export class LyricsEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectLyrics; } @@ -33,7 +33,13 @@ export class LyricsEffectInfo extends EffectBarRendererInfo { } public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { - return new LyricsGlyph(0, 0, beat.lyrics!, renderer.resources.effectFont, TextAlign.Center); + return new LyricsGlyph( + 0, + 0, + beat.lyrics!, + renderer.resources.elementFonts.get(NotationElement.EffectLyrics)!, + TextAlign.Center + ); } public canExpand(_from: Beat, _to: Beat): boolean { diff --git a/packages/alphatab/src/rendering/effects/MarkerEffectInfo.ts b/packages/alphatab/src/rendering/effects/MarkerEffectInfo.ts index 7e0437934..360ee0e4d 100644 --- a/packages/alphatab/src/rendering/effects/MarkerEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/MarkerEffectInfo.ts @@ -4,14 +4,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class MarkerEffectInfo extends EffectBarRendererInfo { +export class MarkerEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectMarker; } @@ -44,7 +44,7 @@ export class MarkerEffectInfo extends EffectBarRendererInfo { !beat.voice.bar.masterBar.section!.marker ? beat.voice.bar.masterBar.section!.text : `[${beat.voice.bar.masterBar.section!.marker}] ${beat.voice.bar.masterBar.section!.text}`, - renderer.resources.markerFont, + renderer.resources.elementFonts.get(NotationElement.EffectMarker)!, TextAlign.Left ); } diff --git a/packages/alphatab/src/rendering/effects/NoteEffectInfoBase.ts b/packages/alphatab/src/rendering/effects/NoteEffectInfoBase.ts index 7af24b2d1..efd85cfa7 100644 --- a/packages/alphatab/src/rendering/effects/NoteEffectInfoBase.ts +++ b/packages/alphatab/src/rendering/effects/NoteEffectInfoBase.ts @@ -1,12 +1,12 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; /** * @internal */ -export abstract class NoteEffectInfoBase extends EffectBarRendererInfo { +export abstract class NoteEffectInfoBase extends EffectInfo { protected lastCreateInfo: Note[] | null = null; public shouldCreateGlyph(_settings: Settings, beat: Beat): boolean { diff --git a/packages/alphatab/src/rendering/effects/NoteOrnamentEffectInfo.ts b/packages/alphatab/src/rendering/effects/NoteOrnamentEffectInfo.ts index f3b93304b..1121a8b7f 100644 --- a/packages/alphatab/src/rendering/effects/NoteOrnamentEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/NoteOrnamentEffectInfo.ts @@ -3,7 +3,7 @@ import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { NoteOrnamentGlyph } from '@coderline/alphatab/rendering/glyphs/NoteOrnamentGlyph'; import type { Settings } from '@coderline/alphatab/Settings'; @@ -11,7 +11,7 @@ import type { Settings } from '@coderline/alphatab/Settings'; /** * @internal */ -export class NoteOrnamentEffectInfo extends EffectBarRendererInfo { +export class NoteOrnamentEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectNoteOrnament; } diff --git a/packages/alphatab/src/rendering/effects/NumberedBarKeySignatureEffectInfo.ts b/packages/alphatab/src/rendering/effects/NumberedBarKeySignatureEffectInfo.ts new file mode 100644 index 000000000..31a60c593 --- /dev/null +++ b/packages/alphatab/src/rendering/effects/NumberedBarKeySignatureEffectInfo.ts @@ -0,0 +1,46 @@ +import type { Beat } from '@coderline/alphatab/model/Beat'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; +import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; +import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; +import { NumberedKeySignatureGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedKeySignatureGlyph'; +import type { Settings } from '@coderline/alphatab/Settings'; + +/** + * @internal + */ +export class NumberedBarKeySignatureEffectInfo extends EffectInfo { + public get notationElement(): NotationElement { + return NotationElement.EffectNumberedNotationKeySignature; + } + + public get hideOnMultiTrack(): boolean { + return false; + } + + public get canShareBand(): boolean { + return false; + } + + public get sizingMode(): EffectBarGlyphSizing { + return EffectBarGlyphSizing.FullBar; + } + + public shouldCreateGlyph(_settings: Settings, beat: Beat): boolean { + const bar = beat.voice.bar; + return ( + beat.index === 0 && + beat.voice.index === 0 && + (!bar.previousBar || bar.keySignature !== bar.previousBar.keySignature) + ); + } + + public createNewGlyph(renderer: BarRendererBase, _beat: Beat): EffectGlyph { + return new NumberedKeySignatureGlyph(0, 0, renderer.bar.keySignature, renderer.bar.keySignatureType); + } + + public canExpand(_from: Beat, _to: Beat): boolean { + return false; + } +} diff --git a/packages/alphatab/src/rendering/effects/OttaviaEffectInfo.ts b/packages/alphatab/src/rendering/effects/OttaviaEffectInfo.ts index 87521bf96..4932f3860 100644 --- a/packages/alphatab/src/rendering/effects/OttaviaEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/OttaviaEffectInfo.ts @@ -4,14 +4,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { OttavaGlyph } from '@coderline/alphatab/rendering/glyphs/OttavaGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class OttaviaEffectInfo extends EffectBarRendererInfo { +export class OttaviaEffectInfo extends EffectInfo { private _aboveStaff: boolean; public override get effectId(): string { diff --git a/packages/alphatab/src/rendering/effects/PalmMuteEffectInfo.ts b/packages/alphatab/src/rendering/effects/PalmMuteEffectInfo.ts index 9b75bb636..306e66c68 100644 --- a/packages/alphatab/src/rendering/effects/PalmMuteEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/PalmMuteEffectInfo.ts @@ -24,6 +24,6 @@ export class PalmMuteEffectInfo extends NoteEffectInfoBase { } public createNewGlyph(_renderer: BarRendererBase, _beat: Beat): EffectGlyph { - return new LineRangedGlyph('P.M.'); + return new LineRangedGlyph('P.M.', NotationElement.EffectPalmMute); } } diff --git a/packages/alphatab/src/rendering/effects/PickSlideEffectInfo.ts b/packages/alphatab/src/rendering/effects/PickSlideEffectInfo.ts index be7738fe1..809d3b7b8 100644 --- a/packages/alphatab/src/rendering/effects/PickSlideEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/PickSlideEffectInfo.ts @@ -25,6 +25,6 @@ export class PickSlideEffectInfo extends NoteEffectInfoBase { } public createNewGlyph(_renderer: BarRendererBase, _beat: Beat): EffectGlyph { - return new LineRangedGlyph('P.S.'); + return new LineRangedGlyph('P.S.', NotationElement.EffectPickSlide); } } diff --git a/packages/alphatab/src/rendering/effects/PickStrokeEffectInfo.ts b/packages/alphatab/src/rendering/effects/PickStrokeEffectInfo.ts index 867c66281..0bcf23525 100644 --- a/packages/alphatab/src/rendering/effects/PickStrokeEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/PickStrokeEffectInfo.ts @@ -4,14 +4,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { PickStrokeGlyph } from '@coderline/alphatab/rendering/glyphs/PickStrokeGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class PickStrokeEffectInfo extends EffectBarRendererInfo { +export class PickStrokeEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectPickStroke; } diff --git a/packages/alphatab/src/rendering/effects/RasgueadoEffectInfo.ts b/packages/alphatab/src/rendering/effects/RasgueadoEffectInfo.ts index 4d40e8d0f..3219ceb52 100644 --- a/packages/alphatab/src/rendering/effects/RasgueadoEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/RasgueadoEffectInfo.ts @@ -3,14 +3,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@coderline/alphatab/rendering/glyphs/LineRangedGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class RasgueadoEffectInfo extends EffectBarRendererInfo { +export class RasgueadoEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectRasgueado; } @@ -32,7 +32,7 @@ export class RasgueadoEffectInfo extends EffectBarRendererInfo { } public createNewGlyph(_renderer: BarRendererBase, _beat: Beat): EffectGlyph { - return new LineRangedGlyph('rasg.'); + return new LineRangedGlyph('rasg.', NotationElement.EffectRasgueado); } public canExpand(_from: Beat, _to: Beat): boolean { diff --git a/packages/alphatab/src/rendering/effects/SimpleDipWhammyBarEffectInfo.ts b/packages/alphatab/src/rendering/effects/SimpleDipWhammyBarEffectInfo.ts new file mode 100644 index 000000000..5244a4813 --- /dev/null +++ b/packages/alphatab/src/rendering/effects/SimpleDipWhammyBarEffectInfo.ts @@ -0,0 +1,50 @@ +import type { Beat } from '@coderline/alphatab/model/Beat'; +import { WhammyType } from '@coderline/alphatab/model/WhammyType'; +import { NotationElement, NotationMode } from '@coderline/alphatab/NotationSettings'; +import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; +import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; +import { TabWhammyBarGlyph } from '@coderline/alphatab/rendering/glyphs/TabWhammyBarGlyph'; +import type { Settings } from '@coderline/alphatab/Settings'; + +/** + * @internal + */ +export class SimpleDipWhammyBarEffectInfo extends EffectInfo { + public get notationElement(): NotationElement { + return NotationElement.EffectWhammyBar; + } + + public override get effectId(): string { + return `${super.effectId}.simpledip`; + } + + public get hideOnMultiTrack(): boolean { + return false; + } + + public get canShareBand(): boolean { + return false; + } + + public get sizingMode(): EffectBarGlyphSizing { + return EffectBarGlyphSizing.SingleOnBeat; + } + + public shouldCreateGlyph(settings: Settings, beat: Beat): boolean { + return ( + settings.notation.notationMode === NotationMode.SongBook && + beat.hasWhammyBar && + beat.whammyBarType === WhammyType.Dip + ); + } + + public createNewGlyph(_renderer: BarRendererBase, beat: Beat): EffectGlyph { + return new TabWhammyBarGlyph(beat); + } + + public canExpand(_from: Beat, _to: Beat): boolean { + return true; + } +} diff --git a/packages/alphatab/src/rendering/effects/SlightBeatVibratoEffectInfo.ts b/packages/alphatab/src/rendering/effects/SlightBeatVibratoEffectInfo.ts index ccb4647b4..8553b7b5b 100644 --- a/packages/alphatab/src/rendering/effects/SlightBeatVibratoEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/SlightBeatVibratoEffectInfo.ts @@ -4,14 +4,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import { BeatVibratoGlyph } from '@coderline/alphatab/rendering/glyphs/BeatVibratoGlyph'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class SlightBeatVibratoEffectInfo extends EffectBarRendererInfo { +export class SlightBeatVibratoEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectSlightBeatVibrato; } diff --git a/packages/alphatab/src/rendering/effects/SustainPedalEffectInfo.ts b/packages/alphatab/src/rendering/effects/SustainPedalEffectInfo.ts index ece37409d..dc024f492 100644 --- a/packages/alphatab/src/rendering/effects/SustainPedalEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/SustainPedalEffectInfo.ts @@ -4,13 +4,13 @@ import type { Settings } from '@coderline/alphatab/Settings'; import type { Beat } from '@coderline/alphatab/model/Beat'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import { SustainPedalGlyph } from '@coderline/alphatab/rendering/glyphs/SustainPedalGlyph'; /** * @internal */ -export class SustainPedalEffectInfo extends EffectBarRendererInfo { +export class SustainPedalEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectSustainPedal; } diff --git a/packages/alphatab/src/rendering/effects/TabWhammyEffectInfo.ts b/packages/alphatab/src/rendering/effects/TabWhammyEffectInfo.ts new file mode 100644 index 000000000..37068bfb8 --- /dev/null +++ b/packages/alphatab/src/rendering/effects/TabWhammyEffectInfo.ts @@ -0,0 +1,82 @@ +import type { Beat } from '@coderline/alphatab/model/Beat'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; +import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { EffectBand } from '@coderline/alphatab/rendering/EffectBand'; +import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; +import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; +import { TabWhammyBarGlyph } from '@coderline/alphatab/rendering/glyphs/TabWhammyBarGlyph'; +import type { Settings } from '@coderline/alphatab/Settings'; + +/** + * @internal + */ +export class TabWhammyEffectInfo extends EffectInfo { + public get notationElement(): NotationElement { + return NotationElement.EffectWhammyBarLine; + } + + public get hideOnMultiTrack(): boolean { + return false; + } + + public get canShareBand(): boolean { + return false; + } + + public get sizingMode(): EffectBarGlyphSizing { + return EffectBarGlyphSizing.GroupedOnBeatToEnd; + } + + public shouldCreateGlyph(_settings: Settings, beat: Beat): boolean { + return beat.hasWhammyBar; + } + + public createNewGlyph(_renderer: BarRendererBase, beat: Beat): EffectGlyph { + return new TabWhammyBarGlyph(beat); + } + + public canExpand(_from: Beat, to: Beat): boolean { + return to.hasWhammyBar; + } + + // this logic below handles the vertical alignment of whammys so that the "0" value is center aligned + // within the staff + // the solution is still a bit hacky though. + public static readonly offsetSharedDataKey: string = 'tab.whammy.offset'; + + public override onAlignGlyphs(band: EffectBand): void { + // re-register the sizes so they are available during finalization later + const info = band.renderer.staff!.getSharedLayoutData<[number, number]>( + TabWhammyEffectInfo.offsetSharedDataKey, + [0, 0] + ); + band.renderer.staff!.setSharedLayoutData(TabWhammyEffectInfo.offsetSharedDataKey, info); + for (const g of band.iterateAllGlyphs()) { + const tb = g as TabWhammyBarGlyph; + if (tb.originalTopOffset > info[0]) { + info[0] = tb.originalTopOffset; + } + if (tb.originalBottomOffset > info[1]) { + info[1] = tb.originalBottomOffset; + } + } + } + + public override finalizeBand(band: EffectBand): void { + const info = band.renderer.staff!.getSharedLayoutData<[number, number]>( + TabWhammyEffectInfo.offsetSharedDataKey, + [0, 0] + ); + const top = info[0]; + const bottom = info[1]; + for (const g of band.iterateAllGlyphs()) { + const tb = g as TabWhammyBarGlyph; + tb.topOffset = top; + tb.bottomOffset = bottom; + tb.height = top + bottom; + } + band.slot!.shared.height = top + bottom; + band.height = top + bottom; + } +} diff --git a/packages/alphatab/src/rendering/effects/TapEffectInfo.ts b/packages/alphatab/src/rendering/effects/TapEffectInfo.ts index 1fec3c404..9a494642d 100644 --- a/packages/alphatab/src/rendering/effects/TapEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/TapEffectInfo.ts @@ -4,7 +4,7 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; @@ -12,7 +12,7 @@ import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class TapEffectInfo extends EffectBarRendererInfo { +export class TapEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectTap; } @@ -36,12 +36,12 @@ export class TapEffectInfo extends EffectBarRendererInfo { public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { const res: RenderingResources = renderer.resources; if (beat.slap) { - return new TextGlyph(0, 0, 'S', res.effectFont, TextAlign.Center); + return new TextGlyph(0, 0, 'S', res.elementFonts.get(NotationElement.EffectTap)!, TextAlign.Center); } if (beat.pop) { - return new TextGlyph(0, 0, 'P', res.effectFont, TextAlign.Center); + return new TextGlyph(0, 0, 'P', res.elementFonts.get(NotationElement.EffectTap)!, TextAlign.Center); } - return new TextGlyph(0, 0, 'T', res.effectFont, TextAlign.Center); + return new TextGlyph(0, 0, 'T', res.elementFonts.get(NotationElement.EffectTap)!, TextAlign.Center); } public canExpand(_from: Beat, _to: Beat): boolean { diff --git a/packages/alphatab/src/rendering/effects/TempoEffectInfo.ts b/packages/alphatab/src/rendering/effects/TempoEffectInfo.ts index 3ba9a059a..fe0092996 100644 --- a/packages/alphatab/src/rendering/effects/TempoEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/TempoEffectInfo.ts @@ -2,7 +2,7 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { BarTempoGlyph } from '@coderline/alphatab/rendering/glyphs/BarTempoGlyph'; @@ -10,7 +10,7 @@ import { BarTempoGlyph } from '@coderline/alphatab/rendering/glyphs/BarTempoGlyp /** * @internal */ -export class TempoEffectInfo extends EffectBarRendererInfo { +export class TempoEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectTempo; } diff --git a/packages/alphatab/src/rendering/effects/TextEffectInfo.ts b/packages/alphatab/src/rendering/effects/TextEffectInfo.ts index e562abbb7..e9efe13f9 100644 --- a/packages/alphatab/src/rendering/effects/TextEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/TextEffectInfo.ts @@ -4,14 +4,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class TextEffectInfo extends EffectBarRendererInfo { +export class TextEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectText; } @@ -33,7 +33,13 @@ export class TextEffectInfo extends EffectBarRendererInfo { } public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { - return new TextGlyph(0, 0, beat.text!, renderer.resources.effectFont, TextAlign.Left); + return new TextGlyph( + 0, + 0, + beat.text!, + renderer.resources.elementFonts.get(NotationElement.EffectText)!, + TextAlign.Left + ); } public canExpand(_from: Beat, _to: Beat): boolean { diff --git a/packages/alphatab/src/rendering/effects/TripletFeelEffectInfo.ts b/packages/alphatab/src/rendering/effects/TripletFeelEffectInfo.ts index 22829cfb2..3966742f7 100644 --- a/packages/alphatab/src/rendering/effects/TripletFeelEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/TripletFeelEffectInfo.ts @@ -4,14 +4,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { TripletFeelGlyph } from '@coderline/alphatab/rendering/glyphs/TripletFeelGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class TripletFeelEffectInfo extends EffectBarRendererInfo { +export class TripletFeelEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectTripletFeel; } diff --git a/packages/alphatab/src/rendering/effects/WahPedalEffectInfo.ts b/packages/alphatab/src/rendering/effects/WahPedalEffectInfo.ts index 0fe6d510b..810385fb6 100644 --- a/packages/alphatab/src/rendering/effects/WahPedalEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/WahPedalEffectInfo.ts @@ -3,7 +3,7 @@ import { WahPedal } from '@coderline/alphatab/model/WahPedal'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { WahPedalGlyph } from '@coderline/alphatab/rendering/glyphs/WahPedalGlyph'; import type { Settings } from '@coderline/alphatab/Settings'; @@ -11,7 +11,7 @@ import type { Settings } from '@coderline/alphatab/Settings'; /** * @internal */ -export class WahPedalEffectInfo extends EffectBarRendererInfo { +export class WahPedalEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectWahPedal; } diff --git a/packages/alphatab/src/rendering/effects/WhammyBarEffectInfo.ts b/packages/alphatab/src/rendering/effects/WhammyBarEffectInfo.ts index 7ab059700..75a10a1e8 100644 --- a/packages/alphatab/src/rendering/effects/WhammyBarEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/WhammyBarEffectInfo.ts @@ -3,14 +3,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@coderline/alphatab/rendering/glyphs/LineRangedGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class WhammyBarEffectInfo extends EffectBarRendererInfo { +export class WhammyBarEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectWhammyBar; } @@ -32,7 +32,7 @@ export class WhammyBarEffectInfo extends EffectBarRendererInfo { } public createNewGlyph(_renderer: BarRendererBase, _beat: Beat): EffectGlyph { - return new LineRangedGlyph('w/bar'); + return new LineRangedGlyph('w/bar', NotationElement.EffectWhammyBar); } public canExpand(_from: Beat, _to: Beat): boolean { diff --git a/packages/alphatab/src/rendering/effects/WideBeatVibratoEffectInfo.ts b/packages/alphatab/src/rendering/effects/WideBeatVibratoEffectInfo.ts index 5112dc1aa..8035c9d17 100644 --- a/packages/alphatab/src/rendering/effects/WideBeatVibratoEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/WideBeatVibratoEffectInfo.ts @@ -4,14 +4,14 @@ import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererB import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; import { BeatVibratoGlyph } from '@coderline/alphatab/rendering/glyphs/BeatVibratoGlyph'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '@coderline/alphatab/rendering/EffectBarRendererInfo'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; import type { Settings } from '@coderline/alphatab/Settings'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal */ -export class WideBeatVibratoEffectInfo extends EffectBarRendererInfo { +export class WideBeatVibratoEffectInfo extends EffectInfo { public get notationElement(): NotationElement { return NotationElement.EffectWideBeatVibrato; } diff --git a/packages/alphatab/src/rendering/glyphs/AccentuationGlyph.ts b/packages/alphatab/src/rendering/glyphs/AccentuationGlyph.ts index 5cc0c1237..46fed5646 100644 --- a/packages/alphatab/src/rendering/glyphs/AccentuationGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/AccentuationGlyph.ts @@ -1,8 +1,9 @@ import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; import type { Note } from '@coderline/alphatab/model/Note'; +import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; +import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** @@ -36,7 +37,7 @@ export class AccentuationGlyph extends EffectGlyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - const dir = this.renderer.getBeatDirection(this._note.beat); + const dir = (this.renderer as LineBarRenderer).getBeatDirection(this._note.beat); const symbol = AccentuationGlyph._getSymbol(this._note.accentuated, dir === BeamDirection.Down); const y = dir === BeamDirection.Up ? cy + this.y : cy + this.y + this.height; diff --git a/packages/alphatab/src/rendering/glyphs/AlternateEndingsGlyph.ts b/packages/alphatab/src/rendering/glyphs/AlternateEndingsGlyph.ts index eb44f38b1..6e0d6c481 100644 --- a/packages/alphatab/src/rendering/glyphs/AlternateEndingsGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/AlternateEndingsGlyph.ts @@ -3,6 +3,7 @@ import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { BarLineStyle } from '@coderline/alphatab/model/Bar'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal @@ -31,7 +32,9 @@ export class AlternateEndingsGlyph extends EffectGlyph { public override doLayout(): void { super.doLayout(); - this.height = this.renderer.resources.wordsFont.size + this.renderer.smuflMetrics.alternateEndingsPadding * 2; + this.height = + this.renderer.resources.elementFonts.get(NotationElement.EffectAlternateEndings)!.size + + this.renderer.smuflMetrics.alternateEndingsPadding * 2; let endingsStrings: string = ''; for (let i: number = 0, j: number = this._endings.length; i < j; i++) { endingsStrings += this._endings[i] + 1; @@ -44,7 +47,7 @@ export class AlternateEndingsGlyph extends EffectGlyph { let width = this._closeLine ? this.width - canvas.lineWidth : this.width; const lineBarRight = this.renderer.bar.getActualBarLineRight(); - if(lineBarRight === BarLineStyle.LightHeavy) { + if (lineBarRight === BarLineStyle.LightHeavy) { width -= this.renderer.smuflMetrics.thickBarlineThickness + this.renderer.smuflMetrics.barlineSeparation; } @@ -75,7 +78,7 @@ export class AlternateEndingsGlyph extends EffectGlyph { const baseline: TextBaseline = canvas.textBaseline; canvas.textBaseline = TextBaseline.Top; const res: RenderingResources = this.renderer.resources; - canvas.font = res.wordsFont; + canvas.font = res.elementFonts.get(NotationElement.EffectAlternateEndings)!; canvas.fillText( this._endingsString, cx + this.x + this.renderer.smuflMetrics.alternateEndingsPadding, diff --git a/packages/alphatab/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts b/packages/alphatab/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts index b86438462..7e6c219bd 100644 --- a/packages/alphatab/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts @@ -1,13 +1,13 @@ -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; /** * @internal */ export class ArticStaccatoAboveGlyph extends MusicFontGlyph { public constructor(x: number, y: number) { - super(x, y, NoteHeadGlyph.GraceScale, MusicFontSymbol.ArticStaccatoAbove); + super(x, y, EngravingSettings.GraceScale, MusicFontSymbol.ArticStaccatoAbove); this.center = true; } diff --git a/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts b/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts index 962b99372..fbee6aae0 100644 --- a/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BarLineGlyph.ts @@ -13,6 +13,12 @@ abstract class BarLineGlyphBase extends Glyph { public override doLayout(): void { this.width = this.renderer.smuflMetrics.thinBarlineThickness; } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + this.paintExtended(cx, cy, canvas, this.height); + } + + public abstract paintExtended(cx: number, cy: number, canvas: ICanvas, newHeight: number): void; } /** @@ -31,8 +37,8 @@ class BarLineLightGlyph extends BarLineGlyphBase { : this.renderer.smuflMetrics.thinBarlineThickness; } - public override paint(cx: number, cy: number, canvas: ICanvas): void { - canvas.fillRect(cx + this.x, cy + this.y, this.renderer.smuflMetrics.thinBarlineThickness, this.height); + public override paintExtended(cx: number, cy: number, canvas: ICanvas, newHeight: number): void { + canvas.fillRect(cx + this.x, cy + this.y, this.renderer.smuflMetrics.thinBarlineThickness, newHeight); } } @@ -40,14 +46,14 @@ class BarLineLightGlyph extends BarLineGlyphBase { * @internal */ class BarLineDottedGlyph extends BarLineGlyphBase { - public override paint(cx: number, cy: number, canvas: ICanvas): void { + public override paintExtended(cx: number, cy: number, canvas: ICanvas, newHeight: number): void { const circleRadius: number = this.renderer.smuflMetrics.thinBarlineThickness / 2; const lineHeight = (this.renderer as LineBarRenderer).getLineHeight(1); let circleY = cy + this.y + lineHeight * 0.5 + circleRadius; - const bottom = cy + this.y + this.height; + const bottom = cy + this.y + newHeight; while (circleY < bottom) { canvas.fillCircle(cx + this.x, circleY, circleRadius); circleY += lineHeight; @@ -59,11 +65,11 @@ class BarLineDottedGlyph extends BarLineGlyphBase { * @internal */ class BarLineDashedGlyph extends BarLineGlyphBase { - public override paint(cx: number, cy: number, canvas: ICanvas): void { + public override paintExtended(cx: number, cy: number, canvas: ICanvas, newHeight: number): void { const dashSize: number = this.renderer.smuflMetrics.dashedBarlineDashLength; const x = cx + this.x - this.width / 2; - const dashes: number = Math.ceil(this.height / 2 / dashSize); - const bottom = cy + this.y + this.height; + const dashes: number = Math.ceil(newHeight / 2 / dashSize); + const bottom = cy + this.y + newHeight; const dashGapLength = this.renderer.smuflMetrics.dashedBarlineGapLength; const lw = canvas.lineWidth; @@ -94,8 +100,8 @@ class BarLineHeavyGlyph extends BarLineGlyphBase { this.width = this.renderer.smuflMetrics.thickBarlineThickness; } - public override paint(cx: number, cy: number, canvas: ICanvas): void { - canvas.fillRect(cx + this.x, cy + this.y, this.width, this.height); + public override paintExtended(cx: number, cy: number, canvas: ICanvas, newHeight: number): void { + canvas.fillRect(cx + this.x, cy + this.y, this.width, newHeight); } } @@ -107,7 +113,7 @@ class BarLineRepeatDotsGlyph extends BarLineGlyphBase { this.width = this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.RepeatDot)!; } - public override paint(cx: number, cy: number, canvas: ICanvas): void { + public override paintExtended(cx: number, cy: number, canvas: ICanvas, _newHeight: number): void { const renderer = this.renderer as LineBarRenderer; const lineOffset = renderer.heightLineCount % 2 === 0 ? 1 : 0.5; @@ -122,8 +128,20 @@ class BarLineRepeatDotsGlyph extends BarLineGlyphBase { const dotOffset = dotTop - dotHeight / 2; - CanvasHelper.fillMusicFontSymbolSafe(canvas,cx + this.x, exactCenter + dotOffset - lineHeight, 1, MusicFontSymbol.RepeatDot); - CanvasHelper.fillMusicFontSymbolSafe(canvas,cx + this.x, exactCenter + dotOffset + lineHeight, 1, MusicFontSymbol.RepeatDot); + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + cx + this.x, + exactCenter + dotOffset - lineHeight, + 1, + MusicFontSymbol.RepeatDot + ); + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + cx + this.x, + exactCenter + dotOffset + lineHeight, + 1, + MusicFontSymbol.RepeatDot + ); } } @@ -131,7 +149,7 @@ class BarLineRepeatDotsGlyph extends BarLineGlyphBase { * @internal */ class BarLineShortGlyph extends BarLineGlyphBase { - public override paint(cx: number, cy: number, canvas: ICanvas): void { + public override paintExtended(cx: number, cy: number, canvas: ICanvas, _newHeight: number): void { const renderer = this.renderer as LineBarRenderer; const lines = renderer.drawnLineCount; const gaps = lines - 1; @@ -152,7 +170,7 @@ class BarLineShortGlyph extends BarLineGlyphBase { * @internal */ class BarLineTickGlyph extends BarLineGlyphBase { - public override paint(cx: number, cy: number, canvas: ICanvas): void { + public override paintExtended(cx: number, cy: number, canvas: ICanvas, _newHeight: number): void { const renderer = this.renderer as LineBarRenderer; const lineHeight = renderer.getLineHeight(1); @@ -167,10 +185,12 @@ class BarLineTickGlyph extends BarLineGlyphBase { */ export class BarLineGlyph extends LeftToRightLayoutingGlyphGroup { private _isRight: boolean; + private _extendToNextStaff: boolean; - public constructor(isRight: boolean) { + public constructor(isRight: boolean, extendToNextStaff: boolean) { super(); this._isRight = isRight; + this._extendToNextStaff = extendToNextStaff; } public override doLayout(): void { @@ -281,20 +301,68 @@ export class BarLineGlyph extends LeftToRightLayoutingGlyphGroup { const lineRenderer = this.renderer as LineBarRenderer; - const lineYOffset = lineRenderer.smuflMetrics.staffLineThickness ; - const top: number = this.y + lineRenderer.topPadding - lineYOffset; - const bottom: number = this.y + this.renderer.height - this.renderer.bottomPadding; - const h: number = (bottom - top); + const lineYOffset = lineRenderer.smuflMetrics.staffLineThickness; + let top: number = this.y; + let bottom: number = this.y; + if ( + lineRenderer.drawnLineCount < 2 || + (!this._isRight && lineRenderer.isFirstOfStaff) || + (this._isRight && lineRenderer.isLastOfStaff) + ) { + top -= lineYOffset; + bottom += lineRenderer.height; + } else { + top += lineRenderer.getLineY(0) - lineYOffset / 2; + bottom += lineRenderer.getLineY(lineRenderer.drawnLineCount - 1) + lineYOffset / 2; + } + + const h: number = bottom - top; + + // round up to have pixel-aligned bar lines, x-shift will be used during rendering + // to avoid shifting again all glyphs + let xShift = 0; + if (this._extendToNextStaff && this._isRight) { + const fullWidth = Math.ceil(this.width); + xShift = fullWidth - this.width; + this.width = fullWidth; + } for (const g of this.glyphs!) { g.y = top; + g.x += xShift; g.height = h; } } public override paint(cx: number, cy: number, canvas: ICanvas): void { + const lines = this.glyphs; + if (!lines) { + return; + } + const renderer = this.renderer as LineBarRenderer; using _ = ElementStyleHelper.bar(canvas, renderer.barLineBarSubElement, this.renderer.bar, true); - super.paint(cx, cy, canvas); + + // extending across systems needs some more dynamic lookup, we do that during drawing + // as during layout things are still moving + let actualLineHeight = this.height; + const thisStaff = renderer.staff!; + const allStaves = thisStaff.system.allStaves; + let isExtended = false; + if (this._extendToNextStaff && thisStaff.index < allStaves.length - 1) { + const nextStaff = allStaves[thisStaff.index + 1]; + const lineTop = thisStaff.y + renderer.y; + const lineBottom = nextStaff.y + nextStaff.topOverflow + renderer.smuflMetrics.staffLineThickness; + actualLineHeight = lineBottom - lineTop; + isExtended = true; + } + + for (const line of lines) { + if (isExtended) { + (line as BarLineGlyphBase).paintExtended(cx, cy, canvas, actualLineHeight); + } else { + (line as BarLineGlyphBase).paint(cx, cy, canvas); + } + } } } diff --git a/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts b/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts index 0aacbe801..c6fb37b41 100644 --- a/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts @@ -1,8 +1,9 @@ -import { type ICanvas, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; -import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import { TextBaseline, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; +import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal @@ -16,13 +17,15 @@ export class BarNumberGlyph extends Glyph { } public override doLayout(): void { - this.renderer.scoreRenderer.canvas!.font = this.renderer.resources.barNumberFont; - this.width = - this.renderer.scoreRenderer.canvas!.measureText(this._number).width; + this.renderer.scoreRenderer.canvas!.font = this.renderer.resources.elementFonts.get(NotationElement.BarNumber)!; + const size = this.renderer.scoreRenderer.canvas!.measureText(this._number); + this.width = size.width; + this.height = size.height; + this.y -= this.height; } public override paint(cx: number, cy: number, canvas: ICanvas): void { - if (!this.renderer.staff.isFirstInSystem) { + if (!this.renderer.staff!.isFirstInSystem) { return; } @@ -35,8 +38,8 @@ export class BarNumberGlyph extends Glyph { const res: RenderingResources = this.renderer.resources; const baseline = canvas.textBaseline; + canvas.font = res.elementFonts.get(NotationElement.BarNumber)!; canvas.textBaseline = TextBaseline.Top; - canvas.font = res.barNumberFont; canvas.fillText(this._number, cx + this.x, cy + this.y); canvas.textBaseline = baseline; } diff --git a/packages/alphatab/src/rendering/glyphs/BarTempoGlyph.ts b/packages/alphatab/src/rendering/glyphs/BarTempoGlyph.ts index 3ae1124ea..33eded0e5 100644 --- a/packages/alphatab/src/rendering/glyphs/BarTempoGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BarTempoGlyph.ts @@ -1,5 +1,6 @@ import type { Automation } from '@coderline/alphatab/model/Automation'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { CanvasHelper, TextBaseline, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; @@ -29,7 +30,7 @@ export class BarTempoGlyph extends EffectGlyph { let x = cx + this.renderer.getRatioPositionX(automation.ratioPosition); const res = this.renderer.resources; - canvas.font = res.markerFont; + canvas.font = res.elementFonts.get(NotationElement.EffectMarker)!; const notePosY = cy + @@ -45,12 +46,17 @@ export class BarTempoGlyph extends EffectGlyph { const size = canvas.measureText(text); canvas.fillText(text, x, notePosY); x += size.width; - } - else { + } else { x -= res.engravingSettings.glyphWidths.get(MusicFontSymbol.MetNoteQuarterUp)! / 2; } - CanvasHelper.fillMusicFontSymbolSafe(canvas,x, notePosY, res.engravingSettings.tempoNoteScale, MusicFontSymbol.MetNoteQuarterUp); + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + x, + notePosY, + res.engravingSettings.tempoNoteScale, + MusicFontSymbol.MetNoteQuarterUp + ); x += this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.MetNoteQuarterUp)! * res.engravingSettings.tempoNoteScale; diff --git a/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts index 1f2730993..c34d1e685 100644 --- a/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts @@ -1,73 +1,189 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; +import type { GraceGroup } from '@coderline/alphatab/model/GraceGroup'; +import type { GraceType } from '@coderline/alphatab/model/GraceType'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import type { Note } from '@coderline/alphatab/model/Note'; +import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import type { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; import type { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import type { VoiceContainerGlyph } from '@coderline/alphatab/rendering/glyphs/VoiceContainerGlyph'; +import type { ITieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; import type { BarBounds } from '@coderline/alphatab/rendering/utils/BarBounds'; +import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { FlagGlyph } from '@coderline/alphatab/rendering/glyphs/FlagGlyph'; /** * @internal */ -export class BeatContainerGlyph extends Glyph { - public voiceContainer: VoiceContainerGlyph; +export abstract class BeatContainerGlyphBase extends Glyph { + public abstract get beatId(): number; + public abstract get absoluteDisplayStart(): number; + public abstract get displayDuration(): number; + public abstract get onTimeX(): number; + public abstract get graceType(): GraceType; + public abstract get graceIndex(): number; + public abstract get graceGroup(): GraceGroup | null; + public abstract get voiceIndex(): number; + public abstract get isFirstOfTupletGroup(): boolean; + public abstract get tupletGroup(): TupletGroup | null; + public abstract get isLastOfVoice(): boolean; + public abstract getNoteY(note: Note, requestedPosition: NoteYPosition): number; + public abstract doMultiVoiceLayout(): void; + public abstract getRestY(requestedPosition: NoteYPosition): number; + public abstract getNoteX(note: Note, requestedPosition: NoteXPosition): number; + public abstract getBeatX(requestedPosition: BeatXPosition, useSharedSizes: boolean): number; + public abstract getLowestNoteY(requestedPosition: NoteYPosition): number; + public abstract getHighestNoteY(requestedPosition: NoteYPosition): number; + public abstract registerLayoutingInfo(layoutings: BarLayoutingInfo): void; + public abstract applyLayoutingInfo(info: BarLayoutingInfo): void; + public abstract buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number): void; + public scaleToWidth(beatWidth: number) { + this.width = beatWidth; + } +} + +/** + * @internal + */ +export class BeatContainerGlyph extends BeatContainerGlyphBase { + private _ties: ITieGlyph[] = []; + private _tieWidth = 0; public beat: Beat; public preNotes!: BeatGlyphBase; public onNotes!: BeatOnNoteGlyphBase; - public ties: Glyph[] = []; - public minWidth: number = 0; + + public override getLowestNoteY(requestedPosition: NoteYPosition): number { + return this.onNotes.getLowestNoteY(requestedPosition); + } + + public override getHighestNoteY(requestedPosition: NoteYPosition): number { + return this.onNotes.getHighestNoteY(requestedPosition); + } + + public override get beatId(): number { + return this.beat.id; + } + + public override get isLastOfVoice(): boolean { + return this.beat.isLastOfVoice; + } + + public override get displayDuration(): number { + return this.beat.displayDuration; + } + + public override get graceIndex(): number { + return this.beat.graceIndex; + } + + public override get graceType(): GraceType { + return this.beat.graceType; + } + + public override get absoluteDisplayStart(): number { + return this.beat.absoluteDisplayStart; + } + + public override get graceGroup(): GraceGroup | null { + return this.beat.graceGroup; + } + + public override get voiceIndex(): number { + return this.beat.voice.index; + } + + public override get isFirstOfTupletGroup(): boolean { + return this.beat.hasTuplet && this.beat.tupletGroup!.beats[0].id === this.beat.id; + } + + public override get tupletGroup(): TupletGroup | null { + return this.beat.tupletGroup; + } public get onTimeX(): number { - return this.onNotes.x + this.onNotes.centerX; + return this.onNotes.x + this.onNotes.onTimeX; } - public constructor(beat: Beat, voiceContainer: VoiceContainerGlyph) { + public constructor(beat: Beat) { super(0, 0); this.beat = beat; - this.ties = []; - this.voiceContainer = voiceContainer; + this._ties = []; + } + + public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { + return this.onNotes.y + this.onNotes.getNoteY(note, requestedPosition); + } + + public override getRestY(requestedPosition: NoteYPosition): number { + return this.onNotes.y + this.onNotes.getRestY(requestedPosition); + } + + public override getNoteX(note: Note, requestedPosition: NoteXPosition): number { + return this.onNotes.x + this.onNotes.getNoteX(note, requestedPosition); + } + + public addTie(tie: ITieGlyph) { + const tg = tie as unknown as Glyph; + tg.renderer = this.renderer; + this._ties.push(tie); + this.renderer.registerTie(tie); } - public addTie(tie: Glyph) { - tie.renderer = this.renderer; - this.ties.push(tie); + public override getBoundingBoxTop(): number { + let top = ModelUtils.minBoundingBox(this.preNotes.getBoundingBoxTop(), this.onNotes.getBoundingBoxTop()); + if (Number.isNaN(top)) { + top = (this.renderer as LineBarRenderer).middleYPosition; + } + return top; + } + + public override getBoundingBoxBottom(): number { + let bottom = ModelUtils.maxBoundingBox( + this.preNotes.getBoundingBoxBottom(), + this.onNotes.getBoundingBoxBottom() + ); + if (Number.isNaN(bottom)) { + bottom = (this.renderer as LineBarRenderer).middleYPosition; + } + return bottom; } protected drawBeamHelperAsFlags(helper: BeamingHelper): boolean { return helper.hasFlag(false, undefined); } + protected get postBeatStretch() { + return this.onNotes.computedWidth + this._tieWidth - this.onNotes.onTimeX; + } + public registerLayoutingInfo(layoutings: BarLayoutingInfo): void { - const preBeatStretch: number = this.preNotes.computedWidth + this.onNotes.centerX; - - let postBeatStretch: number = this.onNotes.computedWidth - this.onNotes.centerX; - // make space for flag - const helper = this.renderer.helpers.getBeamingHelperForBeat(this.beat); - if (this.beat.graceType !== GraceType.None) { - // always use flag size as spacing on grace notes - postBeatStretch += this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * NoteHeadGlyph.GraceScale; - } else if (helper && this.drawBeamHelperAsFlags(helper)) { - postBeatStretch += this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * NoteHeadGlyph.GraceScale; - } - for (const tie of this.ties) { - postBeatStretch += tie.width; + const preBeatStretch: number = this.preNotes.computedWidth + this.onNotes.onTimeX; + + let postBeatStretch: number = this.postBeatStretch; + + for (const tie of this._ties) { + const tg = tie as unknown as Glyph; + postBeatStretch += tg.width; } - layoutings.addBeatSpring(this.beat, preBeatStretch, postBeatStretch); + layoutings.addBeatSpring(this, preBeatStretch, postBeatStretch); + + // store sizes for usages in effects + // we might have empty content in the individual bar renderers, but need to know + // the "shared" maximum widths + layoutings.setBeatSizes(this, { + preBeatSize: this.preNotes.width, + onBeatSize: this.onNotes.width + }); } public applyLayoutingInfo(_info: BarLayoutingInfo): void { - this.onNotes.updateBeamingHelper(); this.updateWidth(); } @@ -80,42 +196,33 @@ export class BeatContainerGlyph extends Glyph { this.onNotes.renderer = this.renderer; this.onNotes.container = this; this.onNotes.doLayout(); - this.onNotes.updateBeamingHelper(); + this.createBeatTies(); + this.updateWidth(); + } + + protected createBeatTies() { let i: number = this.beat.notes.length - 1; while (i >= 0) { this.createTies(this.beat.notes[i--]); } - this.renderer.registerTies(this.ties); - this.updateWidth(); + } + + public override doMultiVoiceLayout(): void { + // do nothing by default, overridden when needed } protected updateWidth(): void { - this.minWidth = this.preNotes.width + this.onNotes.width; - if (!this.beat.isRest) { - if (this.onNotes.beamingHelper.beats.length === 1) { - // make space for flag - if (this.beat.duration >= Duration.Eighth) { - const symbol = FlagGlyph.getSymbol(this.beat.duration, - this.onNotes.beamingHelper.direction, - this.beat.graceType !== GraceType.None - ) - this.minWidth += this.renderer.smuflMetrics.glyphWidths.get(symbol)!; - } - } - } + let width = this.preNotes.width + this.onNotes.width; let tieWidth: number = 0; - for (const tie of this.ties) { - if (tie.width > tieWidth) { - tieWidth = tie.width; + for (const tie of this._ties) { + const tg = tie as unknown as Glyph; + if (tg.width > tieWidth) { + tieWidth = tg.width; } } - this.minWidth += tieWidth; - this.width = this.minWidth; - } - - public scaleToWidth(beatWidth: number): void { - this.onNotes.updateBeamingHelper(); - this.width = beatWidth; + this._tieWidth = tieWidth; + width += tieWidth; + this.width = width; } protected createTies(_n: Note): void { @@ -127,6 +234,16 @@ export class BeatContainerGlyph extends Glyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { + // var c = canvas.color; + // canvas.color = Color.random(); + // canvas.fillRect(cx + this.x, cy + this.y + this.preNotes.getBoundingBoxTop(), this.width, this.renderer.height); + // canvas.fillRect(cx + this.x, cy + this.y + this.onNotes.getBoundingBoxTop(), this.width, this.renderer.height); + // canvas.color = Color.random(); + // const top = this.getBoundingBoxTop(); + // const bottom = this.getBoundingBoxBottom(); + // canvas.fillRect(cx + this.x, cy + this.y + top, this.width, bottom-top); + // canvas.color = c; + // var c = canvas.color; // var ta = canvas.textAlign; // canvas.color = new Color(255, 0, 0); @@ -154,15 +271,20 @@ export class BeatContainerGlyph extends Glyph { // canvas.color = new Color(200, 0, 0, 100); // canvas.strokeRect(cx + this.x + this.preNotes.x, cy + this.y + 10, this.preNotes.width, 10); - // canvas.color = new Color(0, 200, 0, 100); - // canvas.strokeRect(cx + this.x + this.onNotes.x, cy + this.y + 10, this.onNotes.width, 10); + // canvas.color = new Color(0, 200, 0, 100); + // canvas.strokeRect( + // cx + this.x + this.onNotes.x, + // cy + this.y + this.beat.voice.index * 1, + // this.onNotes.width, + // 10 + // ); // canvas.color = new Color(0, 200, 200, 100); // canvas.strokeRect(cx + this.x + this.onNotes.x + this.onNotes.centerX, cy, 1, this.renderer.height); // } // canvas.color = c; - const isEmptyGlyph: boolean = this.preNotes.isEmpty && this.onNotes.isEmpty && this.ties.length === 0; + const isEmptyGlyph: boolean = this.preNotes.isEmpty && this.onNotes.isEmpty && this._ties.length === 0; if (isEmptyGlyph) { return; } @@ -172,17 +294,17 @@ export class BeatContainerGlyph extends Glyph { this.onNotes.paint(cx + this.x, cy + this.y, canvas); // reason: we have possibly multiple staves involved and need to calculate the correct positions. - const staffX: number = cx - this.voiceContainer.x - this.renderer.x; - const staffY: number = cy - this.voiceContainer.y - this.renderer.y; - for (let i: number = 0, j: number = this.ties.length; i < j; i++) { - const t: Glyph = this.ties[i]; + const staffX: number = cx - this.renderer.beatGlyphsStart - this.renderer.x; + const staffY: number = cy - this.renderer.y; + for (let i: number = 0, j: number = this._ties.length; i < j; i++) { + const t = this._ties[i] as unknown as Glyph; t.renderer = this.renderer; t.paint(staffX, staffY, canvas); } canvas.endGroup(); } - public buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number, _isEmptyBar: boolean) { + public buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number) { const beatBoundings: BeatBounds = new BeatBounds(); beatBoundings.beat = this.beat; @@ -199,7 +321,7 @@ export class BeatContainerGlyph extends Glyph { beatBoundings.realBounds.w = this.width; beatBoundings.realBounds.h = barBounds.realBounds.h; - beatBoundings.onNotesX = cx + this.x + this.onNotes.centerX; + beatBoundings.onNotesX = cx + this.x + this.onNotes.x + this.onNotes.onTimeX; } else { beatBoundings.visualBounds = new Bounds(); beatBoundings.visualBounds.x = cx + this.x; @@ -215,7 +337,7 @@ export class BeatContainerGlyph extends Glyph { let visualEndX = 0; if (!this.onNotes.isEmpty) { - visualEndX = cx + this.x + this.onNotes.x + this.onNotes.width; + visualEndX = cx + this.x + this.onNotes.x + this.onNotes.onTimeX + this.postBeatStretch; } else if (!this.preNotes.isEmpty) { visualEndX = cx + this.x + this.preNotes.x + this.preNotes.width; } else { @@ -223,13 +345,6 @@ export class BeatContainerGlyph extends Glyph { } beatBoundings.visualBounds.w = visualEndX - beatBoundings.visualBounds.x; - const helper = this.renderer.helpers.getBeamingHelperForBeat(this.beat); - if ((helper && this.drawBeamHelperAsFlags(helper)) || this.beat.graceType !== GraceType.None) { - beatBoundings.visualBounds.w += - this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.Flag8thUp)! * - (this.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1); - } - beatBoundings.visualBounds.y = barBounds.visualBounds.y; beatBoundings.visualBounds.h = barBounds.visualBounds.h; @@ -239,7 +354,7 @@ export class BeatContainerGlyph extends Glyph { beatBoundings.realBounds.w = this.width; beatBoundings.realBounds.h = barBounds.realBounds.h; - beatBoundings.onNotesX = cx + this.x + this.onNotes.x + this.onNotes.centerX; + beatBoundings.onNotesX = cx + this.x + this.onNotes.x + this.onNotes.onTimeX; } barBounds.addBeat(beatBoundings); @@ -248,4 +363,25 @@ export class BeatContainerGlyph extends Glyph { this.onNotes.buildBoundingsLookup(beatBoundings, cx + this.x, cy + this.y); } } + + public getBeatX(requestedPosition: BeatXPosition, useSharedSizes: boolean = false) { + switch (requestedPosition) { + case BeatXPosition.PreNotes: + return this.preNotes.x; + case BeatXPosition.OnNotes: + return this.onNotes.x; + case BeatXPosition.MiddleNotes: + return this.onNotes.x + this.onNotes.middleX; + case BeatXPosition.Stem: + return this.onNotes.x + this.onNotes.stemX; + case BeatXPosition.PostNotes: + const onNoteSize = useSharedSizes + ? (this.renderer.layoutingInfo.getBeatSizes(this.beat)?.onBeatSize ?? this.onNotes.width) + : this.onNotes.width; + return this.onNotes.x + onNoteSize; + case BeatXPosition.EndBeat: + return this.width; + } + return this.preNotes.x; + } } diff --git a/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts b/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts index 944e6d169..382dfc8d8 100644 --- a/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts +++ b/packages/alphatab/src/rendering/glyphs/BeatOnNoteGlyphBase.ts @@ -1,36 +1,20 @@ -import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import type { Note } from '@coderline/alphatab/model/Note'; +import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; /** * @internal */ -export class BeatOnNoteGlyphBase extends BeatGlyphBase { - public beamingHelper!: BeamingHelper; - public centerX: number = 0; - - public updateBeamingHelper(): void { - // - } - - public buildBoundingsLookup(_beatBounds: BeatBounds, _cx: number, _cy: number) { - // implemented in subclasses - } - - public getNoteX(_note: Note, _requestedPosition: NoteXPosition): number { - return 0; - } - public getNoteY(_note: Note, _requestedPosition: NoteYPosition): number { - return 0; - } - - public getHighestNoteY(): number { - return 0; - } +export abstract class BeatOnNoteGlyphBase extends BeatGlyphBase { + public onTimeX: number = 0; + public middleX: number = 0; + public stemX: number = 0; - public getLowestNoteY(): number { - return 0; - } + public abstract buildBoundingsLookup(_beatBounds: BeatBounds, _cx: number, _cy: number): void; + public abstract getNoteX(note: Note, requestedPosition: NoteXPosition): number; + public abstract getNoteY(note: Note, requestedPosition: NoteYPosition): number; + public abstract getRestY(requestedPosition: NoteYPosition): number; + public abstract getHighestNoteY(requestedPosition: NoteYPosition): number; + public abstract getLowestNoteY(requestedPosition: NoteYPosition): number; } diff --git a/packages/alphatab/src/rendering/glyphs/BeatTimerGlyph.ts b/packages/alphatab/src/rendering/glyphs/BeatTimerGlyph.ts index e021b1b44..8de7e9f8b 100644 --- a/packages/alphatab/src/rendering/glyphs/BeatTimerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BeatTimerGlyph.ts @@ -1,3 +1,4 @@ +import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { type ICanvas, TextBaseline, TextAlign } from '@coderline/alphatab/platform/ICanvas'; import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; @@ -20,7 +21,7 @@ export class BeatTimerGlyph extends EffectGlyph { this._text = `${minutes}:${seconds.toString().padStart(2, '0')}`; const c = this.renderer.scoreRenderer.canvas!; - c.font = this.renderer.resources.timerFont; + c.font = this.renderer.resources.elementFonts.get(NotationElement.EffectBeatTimer)!; const size = c.measureText(this._text); @@ -41,7 +42,7 @@ export class BeatTimerGlyph extends EffectGlyph { const f = canvas.font; const b = canvas.textBaseline; const a = canvas.textAlign; - canvas.font = this.renderer.resources.timerFont; + canvas.font = this.renderer.resources.elementFonts.get(NotationElement.EffectBeatTimer)!; canvas.textBaseline = TextBaseline.Middle; canvas.textAlign = TextAlign.Center; canvas.fillText(this._text, cx + this.x, cy + this.y + this.height / 2); diff --git a/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts b/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts index 8650e3d52..a420b3368 100644 --- a/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts @@ -1,6 +1,7 @@ -import type { Color } from '@coderline/alphatab/model/Color'; +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; import type { Beat } from '@coderline/alphatab/model/Beat'; +import type { Color } from '@coderline/alphatab/model/Color'; import { Duration } from '@coderline/alphatab/model/Duration'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; @@ -8,7 +9,10 @@ import { AccidentalGroupGlyph } from '@coderline/alphatab/rendering/glyphs/Accid import { GhostNoteContainerGlyph } from '@coderline/alphatab/rendering/glyphs/GhostNoteContainerGlyph'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { ScoreNoteChordGlyphBase } from '@coderline/alphatab/rendering/glyphs/ScoreNoteChordGlyphBase'; +import { + ScoreChordNoteHeadInfo, + ScoreNoteChordGlyphBase +} from '@coderline/alphatab/rendering/glyphs/ScoreNoteChordGlyphBase'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; @@ -16,7 +20,6 @@ import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection * @internal */ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { - private _beat: Beat; private _showParenthesis: boolean = false; private _noteValueLookup: Map = new Map(); @@ -24,20 +27,28 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { private _preNoteParenthesis: GhostNoteContainerGlyph | null = null; private _postNoteParenthesis: GhostNoteContainerGlyph | null = null; public isEmpty: boolean = true; + private _groupId: string; public override get scale(): number { - return NoteHeadGlyph.GraceScale; + return EngravingSettings.GraceScale; + } + + public override get hasFlag(): boolean { + return false; + } + + public override get hasStem(): boolean { + return false; } public get direction(): BeamDirection { return BeamDirection.Up; } - public noteHeadOffset: number = 0; - - public constructor(beat: Beat, showParenthesis: boolean = false) { + public constructor(groupId: string, beat: Beat, showParenthesis: boolean = false) { super(); this._beat = beat; + this._groupId = groupId; this._showParenthesis = showParenthesis; if (showParenthesis) { this._preNoteParenthesis = new GhostNoteContainerGlyph(true); @@ -45,6 +56,17 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { } } + protected override getScoreChordNoteHeadInfo(): ScoreChordNoteHeadInfo { + const staff = this._beat.voice.bar.staff; + const key = `score.noteheads.${this._groupId}.${staff.track.index}.${staff.index}.${this._beat.absoluteDisplayStart}`; + let existing = this.renderer.staff!.getSharedLayoutData(key, undefined); + if (!existing) { + existing = new ScoreChordNoteHeadInfo(this.direction); + this.renderer.staff!.setSharedLayoutData(key, existing); + } + return new ScoreChordNoteHeadInfo(this.direction); + } + public containsNoteValue(noteValue: number): boolean { return this._noteValueLookup.has(noteValue); } @@ -65,22 +87,22 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { quarterBend, true ); - const line: number = sr.accidentalHelper.getNoteLineForValue(noteValue, false); - noteHeadGlyph.y = sr.getScoreY(line); + const steps: number = sr.accidentalHelper.getNoteStepsForValue(noteValue, false); + noteHeadGlyph.y = sr.getScoreY(steps); if (this._showParenthesis) { this._preNoteParenthesis!.renderer = this.renderer; this._postNoteParenthesis!.renderer = this.renderer; - this._preNoteParenthesis!.addParenthesisOnLine(line, true); - this._postNoteParenthesis!.addParenthesisOnLine(line, true); + this._preNoteParenthesis!.addParenthesisOnSteps(steps, true); + this._postNoteParenthesis!.addParenthesisOnSteps(steps, true); } if (accidental !== AccidentalType.None) { - const g = new AccidentalGlyph(0, noteHeadGlyph.y, accidental, NoteHeadGlyph.GraceScale); + const g = new AccidentalGlyph(0, noteHeadGlyph.y, accidental, EngravingSettings.GraceScale); g.renderer = this.renderer; this._accidentals.renderer = this.renderer; this._accidentals.addGlyph(g); } this._noteValueLookup.set(noteValue, noteHeadGlyph); - this.add(noteHeadGlyph, line); + this.add(noteHeadGlyph, steps); this.isEmpty = false; } @@ -101,7 +123,6 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { } this.noteStartX = x; super.doLayout(); - this.noteHeadOffset = this.noteStartX + (this.width - this.noteStartX) / 2; if (this._showParenthesis) { this._postNoteParenthesis!.x = this.width + this.renderer.smuflMetrics.bendNoteHeadElementPadding; this._postNoteParenthesis!.renderer = this.renderer; diff --git a/packages/alphatab/src/rendering/glyphs/ChordDiagramContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/ChordDiagramContainerGlyph.ts index 759b05162..ca9fe93bc 100644 --- a/packages/alphatab/src/rendering/glyphs/ChordDiagramContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ChordDiagramContainerGlyph.ts @@ -1,9 +1,10 @@ import type { Chord } from '@coderline/alphatab/model/Chord'; +import { ScoreSubElement } from '@coderline/alphatab/model/Score'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { ChordDiagramGlyph } from '@coderline/alphatab/rendering/glyphs/ChordDiagramGlyph'; import { RowContainerGlyph } from '@coderline/alphatab/rendering/glyphs/RowContainerGlyph'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { ScoreSubElement } from '@coderline/alphatab/model/Score'; /** * @internal @@ -11,7 +12,7 @@ import { ScoreSubElement } from '@coderline/alphatab/model/Score'; export class ChordDiagramContainerGlyph extends RowContainerGlyph { public addChord(chord: Chord): void { if (chord.strings.length > 0) { - const chordDiagram: ChordDiagramGlyph = new ChordDiagramGlyph(0, 0, chord); + const chordDiagram: ChordDiagramGlyph = new ChordDiagramGlyph(0, 0, chord, NotationElement.ChordDiagrams); chordDiagram.renderer = this.renderer; chordDiagram.doLayout(); this.glyphs!.push(chordDiagram); diff --git a/packages/alphatab/src/rendering/glyphs/ChordDiagramGlyph.ts b/packages/alphatab/src/rendering/glyphs/ChordDiagramGlyph.ts index 271499580..d7521e3d8 100644 --- a/packages/alphatab/src/rendering/glyphs/ChordDiagramGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ChordDiagramGlyph.ts @@ -1,8 +1,9 @@ import type { Chord } from '@coderline/alphatab/model/Chord'; -import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; -import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; +import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; +import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; /** * @internal @@ -14,67 +15,96 @@ export class ChordDiagramGlyph extends EffectGlyph { private _textRow: number = 0; private _fretRow: number = 0; private _firstFretSpacing: number = 0; + private _center: boolean; + private _fontElement: NotationElement; - public constructor(x: number, y: number, chord: Chord) { + public constructor(x: number, y: number, chord: Chord, fontElement: NotationElement, center: boolean = false) { super(x, y); this._chord = chord; + this._center = center; + this._fontElement = fontElement; } public override doLayout(): void { super.doLayout(); const res: RenderingResources = this.renderer.resources; - this._textRow = res.effectFont.size * 1.5; - this._fretRow = res.effectFont.size * 1.5; - if (this._chord.firstFret > 1) { - this._firstFretSpacing = this.renderer.smuflMetrics.chordDiagramFretSpacing; - } else { - this._firstFretSpacing = 0; + const font = res.elementFonts.get(this._fontElement)!; + this._textRow = font.size * 1.5; + this._fretRow = font.size * 1.5; + this.height = this._textRow; + this.width = 2 * this.renderer.smuflMetrics.chordDiagramPaddingX; + + if (this.renderer.settings.notation.isNotationElementVisible(NotationElement.ChordDiagramFretboardNumbers)) { + if (this._chord.firstFret > 1) { + this._firstFretSpacing = this.renderer.smuflMetrics.chordDiagramFretSpacing; + } else { + this._firstFretSpacing = 0; + } + this.height += + this._fretRow + + ChordDiagramGlyph._frets * this.renderer.smuflMetrics.chordDiagramFretSpacing + + 2 * this.renderer.smuflMetrics.chordDiagramPaddingY; + this.width += + this._firstFretSpacing + + (this._chord.strings.length - 1) * this.renderer.smuflMetrics.chordDiagramStringSpacing; + } else if (this._chord.showName) { + const canvas = this.renderer.scoreRenderer.canvas!; + canvas.font = font; + this.width += canvas.measureText(this._chord.name).width; } - this.height = - this._textRow + - this._fretRow + - ChordDiagramGlyph._frets * this.renderer.smuflMetrics.chordDiagramFretSpacing + - 2 * this.renderer.smuflMetrics.chordDiagramPaddingY; - this.width = - this._firstFretSpacing + - (this._chord.strings.length - 1) * this.renderer.smuflMetrics.chordDiagramStringSpacing + - 2 * this.renderer.smuflMetrics.chordDiagramPaddingX; } public override paint(cx: number, cy: number, canvas: ICanvas): void { cx += this.x + this.renderer.smuflMetrics.chordDiagramPaddingX + this._firstFretSpacing; cy += this.y; - const stringSpacing: number = this.renderer.smuflMetrics.chordDiagramStringSpacing; - const fretSpacing: number = this.renderer.smuflMetrics.chordDiagramFretSpacing; + if (this._center) { + cx -= this.width / 2; + } + const res: RenderingResources = this.renderer.resources; const lineWidth = res.engravingSettings.chordDiagramLineWidth; const w: number = this.width - 2 * this.renderer.smuflMetrics.chordDiagramPaddingX - this._firstFretSpacing + lineWidth; - const circleHeight = res.engravingSettings.glyphHeights.get(MusicFontSymbol.FretboardFilledCircle)!; - const circleTopOffset = res.engravingSettings.glyphTop.get(MusicFontSymbol.FretboardFilledCircle)!; - const xTopOffset = res.engravingSettings.glyphHeights.get(MusicFontSymbol.FretboardX)! / 2; - const oTopOffset = res.engravingSettings.glyphHeights.get(MusicFontSymbol.FretboardO)! / 2; const align: TextAlign = canvas.textAlign; const baseline: TextBaseline = canvas.textBaseline; - canvas.font = res.effectFont; + const font = res.elementFonts.get(this._fontElement)!; + canvas.font = font; canvas.textAlign = TextAlign.Center; canvas.textBaseline = TextBaseline.Top; if (this._chord.showName) { - canvas.fillText(this._chord.name, cx + w / 2, cy + res.effectFont.size / 2); + canvas.fillText(this._chord.name, cx + w / 2, cy + font.size / 2); } + if (this.renderer.settings.notation.isNotationElementVisible(NotationElement.ChordDiagramFretboardNumbers)) { + this._paintFretboard(cx, cy, canvas, w); + } + + canvas.textAlign = align; + canvas.textBaseline = baseline; + } + private _paintFretboard(cx: number, cy: number, canvas: ICanvas, w: number) { cy += this._textRow; - canvas.font = res.fretboardNumberFont; + + const res: RenderingResources = this.renderer.resources; + const stringSpacing: number = this.renderer.smuflMetrics.chordDiagramStringSpacing; + const fretSpacing: number = this.renderer.smuflMetrics.chordDiagramFretSpacing; + const circleHeight = res.engravingSettings.glyphHeights.get(MusicFontSymbol.FretboardFilledCircle)!; + const circleTopOffset = res.engravingSettings.glyphTop.get(MusicFontSymbol.FretboardFilledCircle)!; + const xTopOffset = res.engravingSettings.glyphHeights.get(MusicFontSymbol.FretboardX)! / 2; + const oTopOffset = res.engravingSettings.glyphHeights.get(MusicFontSymbol.FretboardO)! / 2; + const lineWidth = res.engravingSettings.chordDiagramLineWidth; + + canvas.font = res.elementFonts.get(NotationElement.ChordDiagramFretboardNumbers)!; canvas.textBaseline = TextBaseline.Middle; for (let i: number = 0; i < this._chord.strings.length; i++) { const x: number = cx + i * stringSpacing; const y: number = cy + this._fretRow / 2; let fret: number = this._chord.strings[this._chord.strings.length - i - 1]; if (fret < 0) { - CanvasHelper.fillMusicFontSymbolSafe(canvas,x, y + xTopOffset, 1, MusicFontSymbol.FretboardX, true); + CanvasHelper.fillMusicFontSymbolSafe(canvas, x, y + xTopOffset, 1, MusicFontSymbol.FretboardX, true); } else if (fret === 0) { - CanvasHelper.fillMusicFontSymbolSafe(canvas,x, y + oTopOffset, 1, MusicFontSymbol.FretboardO, true); + CanvasHelper.fillMusicFontSymbolSafe(canvas, x, y + oTopOffset, 1, MusicFontSymbol.FretboardO, true); } else { fret -= this._chord.firstFret - 1; canvas.fillText(fret.toString(), x, y); @@ -126,7 +156,8 @@ export class ChordDiagramGlyph extends EffectGlyph { } const y: number = cy + fret * fretSpacing + fretSpacing / 2 + 0.5; const x: number = cx + (this._chord.strings.length - guitarString - 1) * stringSpacing + lineWidth / 2; - CanvasHelper.fillMusicFontSymbolSafe(canvas, + CanvasHelper.fillMusicFontSymbolSafe( + canvas, x, y + circleTopOffset - circleHeight / 2, 1, @@ -142,8 +173,5 @@ export class ChordDiagramGlyph extends EffectGlyph { const xRight: number = cx + (this._chord.strings.length - strings[0] - 1) * stringSpacing; canvas.fillRect(xLeft, y - circleHeight / 2, xRight - xLeft, circleHeight); } - - canvas.textAlign = align; - canvas.textBaseline = baseline; } } diff --git a/packages/alphatab/src/rendering/glyphs/ClefGlyph.ts b/packages/alphatab/src/rendering/glyphs/ClefGlyph.ts index 920b52622..d0d853ea0 100644 --- a/packages/alphatab/src/rendering/glyphs/ClefGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ClefGlyph.ts @@ -1,10 +1,11 @@ +import { BarSubElement } from '@coderline/alphatab/model/Bar'; import { Clef } from '@coderline/alphatab/model/Clef'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; -import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { BarSubElement } from '@coderline/alphatab/model/Bar'; /** * @internal @@ -12,6 +13,7 @@ import { BarSubElement } from '@coderline/alphatab/model/Bar'; export class ClefGlyph extends MusicFontGlyph { private _clef: Clef; private _clefOttava: Ottavia; + private _ottavaGlyph?: MusicFontGlyph; public constructor(x: number, y: number, clef: Clef, clefOttava: Ottavia) { super(x, y, 1, ClefGlyph._getSymbol(clef, clefOttava)); @@ -19,11 +21,80 @@ export class ClefGlyph extends MusicFontGlyph { this._clefOttava = clefOttava; } + public override getBoundingBoxTop(): number { + let top = super.getBoundingBoxTop(); + + const ottava = this._ottavaGlyph; + if (ottava) { + const ottavaTop = this.y + ottava.getBoundingBoxTop(); + top = ModelUtils.minBoundingBox(top, ottavaTop); + } + + return top; + } + + public override getBoundingBoxBottom(): number { + let bottom = super.getBoundingBoxBottom(); + + const ottava = this._ottavaGlyph; + if (ottava) { + const ottavaBottom = this.y + ottava.getBoundingBoxBottom(); + bottom = ModelUtils.maxBoundingBox(bottom, ottavaBottom); + } + + + return bottom; + } + public override doLayout(): void { this.center = true; super.doLayout(); this.width = this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.GClef)!; this.offsetX = this.width / 2; + + this._ottavaGlyph = undefined; + switch (this._clef) { + case Clef.C3: + case Clef.C4: + switch (this._clefOttava) { + case Ottavia._8vb: + return; + } + break; + case Clef.F4: + case Clef.G2: + return; + } + + let ottavaSymbol: MusicFontSymbol; + let top: boolean = false; + switch (this._clefOttava) { + case Ottavia._15ma: + ottavaSymbol = MusicFontSymbol.Clef15; + top = true; + break; + case Ottavia._8va: + ottavaSymbol = MusicFontSymbol.Clef8; + top = true; + break; + case Ottavia._8vb: + ottavaSymbol = MusicFontSymbol.Clef8; + break; + case Ottavia._15mb: + ottavaSymbol = MusicFontSymbol.Clef15; + break; + default: + return; + } + const ottavaX = this.width / 2; + const ottavaY = top + ? this.renderer.smuflMetrics.glyphTop.get(this.symbol)! + : this.renderer.smuflMetrics.glyphBottom.get(this.symbol)! - + this.renderer.smuflMetrics.glyphHeights.get(ottavaSymbol)!; + this._ottavaGlyph = new MusicFontGlyph(ottavaX, -ottavaY, 1, ottavaSymbol); + this._ottavaGlyph!.center = true; + this._ottavaGlyph!.renderer = this.renderer; + this._ottavaGlyph!.doLayout(); } private static _getSymbol(clef: Clef, clefOttava: Ottavia): MusicFontSymbol { @@ -74,46 +145,9 @@ export class ClefGlyph extends MusicFontGlyph { super.paint(cx, cy, canvas); - switch (this._clef) { - case Clef.C3: - case Clef.C4: - switch (this._clefOttava) { - case Ottavia._8vb: - return; - } - break; - case Clef.F4: - case Clef.G2: - return; + const ottava = this._ottavaGlyph; + if (ottava) { + ottava.paint(cx + this.x, cy + this.y, canvas); } - - let ottavaGlyph: MusicFontSymbol; - let top: boolean = false; - switch (this._clefOttava) { - case Ottavia._15ma: - ottavaGlyph = MusicFontSymbol.Clef15; - top = true; - break; - case Ottavia._8va: - ottavaGlyph = MusicFontSymbol.Clef8; - top = true; - break; - case Ottavia._8vb: - ottavaGlyph = MusicFontSymbol.Clef8; - break; - case Ottavia._15mb: - ottavaGlyph = MusicFontSymbol.Clef15; - break; - default: - return; - } - const ottavaX: number = this.width / 2; - const ottavaY = top - ? this.renderer.smuflMetrics.glyphTop.get(this.symbol)! - : this.renderer.smuflMetrics.glyphBottom.get(this.symbol)! - - this.renderer.smuflMetrics.glyphHeights.get(ottavaGlyph)!; - CanvasHelper.fillMusicFontSymbolSafe(canvas,cx + this.x + ottavaX, cy + this.y - ottavaY, 1, ottavaGlyph, true) - - } } diff --git a/packages/alphatab/src/rendering/glyphs/DeadNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/DeadNoteHeadGlyph.ts index d32083a2b..29e64028c 100644 --- a/packages/alphatab/src/rendering/glyphs/DeadNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/DeadNoteHeadGlyph.ts @@ -1,12 +1,11 @@ -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; /** * @internal */ -export class DeadNoteHeadGlyph extends MusicFontGlyph { +export class DeadNoteHeadGlyph extends NoteHeadGlyphBase { public constructor(x: number, y: number, isGrace: boolean) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, MusicFontSymbol.NoteheadXOrnate); + super(x, y, isGrace, MusicFontSymbol.NoteheadXOrnate); } } diff --git a/packages/alphatab/src/rendering/glyphs/DeadSlappedBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/DeadSlappedBeatGlyph.ts index bd1787f96..1f856c26e 100644 --- a/packages/alphatab/src/rendering/glyphs/DeadSlappedBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/DeadSlappedBeatGlyph.ts @@ -7,31 +7,47 @@ import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRende * @internal */ export class DeadSlappedBeatGlyph extends Glyph { + private _topY = 0; public constructor() { super(0, 0); } + public override getBoundingBoxTop(): number { + return this._topY; + } + + public override getBoundingBoxBottom(): number { + return this._topY + this.height; + } + public override doLayout(): void { this.width = this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.NoteheadSlashWhiteHalf)!; - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { const renderer = this.renderer as LineBarRenderer; const crossHeight = renderer.getLineHeight(renderer.heightLineCount - 1); const staffTop = renderer.getLineY(0); - const staffHeight = renderer.getLineHeight(renderer.drawnLineCount - 1); + const staffHeight = renderer.drawnLineCount > 0 ? renderer.getLineHeight(renderer.drawnLineCount - 1) : 0; + + const topY = staffTop + staffHeight / 2 - crossHeight / 2; + + this.height = crossHeight; + + this._topY = topY; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const crossHeight = this.height; - // center X on staff - const centerY = staffTop + staffHeight / 2 - crossHeight / 2; + const topY = this._topY; const lw = canvas.lineWidth; canvas.lineWidth = this.renderer.smuflMetrics.deadSlappedLineWidth; - canvas.moveTo(cx + this.x, cy + centerY); - canvas.lineTo(cx + this.x + this.width, cy + centerY + crossHeight); + canvas.moveTo(cx + this.x, cy + topY); + canvas.lineTo(cx + this.x + this.width, cy + topY + crossHeight); - canvas.moveTo(cx + this.x, cy + centerY + crossHeight); - canvas.lineTo(cx + this.x + this.width, cy + centerY); + canvas.moveTo(cx + this.x, cy + topY + crossHeight); + canvas.lineTo(cx + this.x + this.width, cy + topY); canvas.stroke(); diff --git a/packages/alphatab/src/rendering/glyphs/DiamondNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/DiamondNoteHeadGlyph.ts index aa1658c4e..18cc45075 100644 --- a/packages/alphatab/src/rendering/glyphs/DiamondNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/DiamondNoteHeadGlyph.ts @@ -1,14 +1,13 @@ import { Duration } from '@coderline/alphatab/model/Duration'; -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; /** * @internal */ -export class DiamondNoteHeadGlyph extends MusicFontGlyph { +export class DiamondNoteHeadGlyph extends NoteHeadGlyphBase { public constructor(x: number, y: number, duration: Duration, isGrace: boolean) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, DiamondNoteHeadGlyph._getSymbol(duration)); + super(x, y, isGrace, DiamondNoteHeadGlyph._getSymbol(duration)); } private static _getSymbol(duration: Duration): MusicFontSymbol { diff --git a/packages/alphatab/src/rendering/glyphs/DirectionsContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/DirectionsContainerGlyph.ts index 4dc967d2a..8fe0c7ec5 100644 --- a/packages/alphatab/src/rendering/glyphs/DirectionsContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/DirectionsContainerGlyph.ts @@ -3,6 +3,7 @@ import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { type ICanvas, TextBaseline, TextAlign } from '@coderline/alphatab/platform/ICanvas'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal @@ -47,7 +48,7 @@ class JumpDirectionGlyph extends Glyph { public override doLayout(): void { const c = this.renderer.scoreRenderer.canvas!; - c.font = this.renderer.resources.directionsFont; + c.font = this.renderer.resources.elementFonts.get(NotationElement.EffectDirections)!; this.height = c.measureText(this._text).height; } @@ -56,7 +57,7 @@ class JumpDirectionGlyph extends Glyph { const baseline = canvas.textBaseline; const align = canvas.textAlign; - canvas.font = this.renderer.resources.directionsFont; + canvas.font = this.renderer.resources.elementFonts.get(NotationElement.EffectDirections)!; canvas.textBaseline = TextBaseline.Middle; canvas.textAlign = TextAlign.Right; diff --git a/packages/alphatab/src/rendering/glyphs/FingeringGroupGlyph.ts b/packages/alphatab/src/rendering/glyphs/FingeringGroupGlyph.ts index ee21f4e1c..31827ca8f 100644 --- a/packages/alphatab/src/rendering/glyphs/FingeringGroupGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/FingeringGroupGlyph.ts @@ -138,14 +138,14 @@ export class FingeringGroupGlyph extends GlyphGroup { private _addFinger(note: Note, symbol: MusicFontSymbol) { const sr = this.renderer as ScoreBarRenderer; - const line: number = sr.getNoteLine(note); + const steps: number = sr.getNoteSteps(note); - if (!this._infos.has(line)) { - const info = new FingeringInfo(line, [symbol]); + if (!this._infos.has(steps)) { + const info = new FingeringInfo(steps, [symbol]); info.color = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.StandardNotationEffects, note); - this._infos.set(line, info); + this._infos.set(steps, info); } else { - const info = this._infos.get(line)!; + const info = this._infos.get(steps)!; info.symbols.push(symbol); } } diff --git a/packages/alphatab/src/rendering/glyphs/FlagGlyph.ts b/packages/alphatab/src/rendering/glyphs/FlagGlyph.ts index 19818548d..c0c454aa0 100644 --- a/packages/alphatab/src/rendering/glyphs/FlagGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/FlagGlyph.ts @@ -1,15 +1,22 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { Duration } from '@coderline/alphatab/model/Duration'; -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; /** * @internal */ export class FlagGlyph extends MusicFontGlyph { public constructor(x: number, y: number, duration: Duration, direction: BeamDirection, isGrace: boolean) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, FlagGlyph.getSymbol(duration, direction, isGrace)); + super(x, y, isGrace ? EngravingSettings.GraceScale : 1, FlagGlyph.getSymbol(duration, direction, isGrace)); + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const c = canvas.color; + super.paint(cx, cy, canvas); + canvas.color = c; } public static getSymbol(duration: Duration, direction: BeamDirection, isGrace: boolean): MusicFontSymbol { diff --git a/packages/alphatab/src/rendering/glyphs/GhostNoteContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/GhostNoteContainerGlyph.ts index e3402ca35..d4703b1c6 100644 --- a/packages/alphatab/src/rendering/glyphs/GhostNoteContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/GhostNoteContainerGlyph.ts @@ -11,12 +11,12 @@ import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementS * @internal */ export class GhostNoteInfo { - public line: number = 0; + public steps: number = 0; public isGhost: boolean; public color: Color | undefined; public constructor(line: number, isGhost: boolean, color: Color | undefined) { - this.line = line; + this.steps = line; this.isGhost = isGhost; this.color = color; } @@ -38,7 +38,7 @@ export class GhostNoteContainerGlyph extends Glyph { public addParenthesis(n: Note): void { const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; - const line: number = sr.getNoteLine(n); + const steps: number = sr.getNoteSteps(n); const hasParenthesis: boolean = n.isGhost || (this._isTiedBend(n) && @@ -46,10 +46,10 @@ export class GhostNoteContainerGlyph extends Glyph { const color = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.Effects, n); - this._add(new GhostNoteInfo(line, hasParenthesis, color)); + this._add(new GhostNoteInfo(steps, hasParenthesis, color)); } - public addParenthesisOnLine(line: number, hasParenthesis: boolean): void { + public addParenthesisOnSteps(line: number, hasParenthesis: boolean): void { const info: GhostNoteInfo = new GhostNoteInfo(line, hasParenthesis, undefined); this._add(info); } @@ -74,7 +74,7 @@ export class GhostNoteContainerGlyph extends Glyph { public override doLayout(): void { const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; this._infos.sort((a, b) => { - return a.line - b.line; + return a.steps - b.steps; }); let previousGlyph: GhostParenthesisGlyph | null = null; const sizePerLine: number = sr.getScoreHeight(1); @@ -87,13 +87,13 @@ export class GhostNoteContainerGlyph extends Glyph { g = new GhostParenthesisGlyph(this._isOpen); g.colorOverride = this._infos[i].color; g.renderer = this.renderer; - g.y = sr.getScoreY(this._infos[i].line) - sizePerLine; + g.y = sr.getScoreY(this._infos[i].steps) - sizePerLine; g.height = sizePerLine * 2; g.doLayout(); this._glyphs.push(g); previousGlyph = g; } else { - const y: number = sr.getScoreY(this._infos[i].line) + sizePerLine; + const y: number = sr.getScoreY(this._infos[i].steps) + sizePerLine; previousGlyph.height = y - previousGlyph.y; } } diff --git a/packages/alphatab/src/rendering/glyphs/Glyph.ts b/packages/alphatab/src/rendering/glyphs/Glyph.ts index a6e810765..0a09aa735 100644 --- a/packages/alphatab/src/rendering/glyphs/Glyph.ts +++ b/packages/alphatab/src/rendering/glyphs/Glyph.ts @@ -22,6 +22,10 @@ export class Glyph { return this.y; } + public getBoundingBoxBottom() { + return this.getBoundingBoxTop() + this.height; + } + public doLayout(): void { // to be implemented in subclass } diff --git a/packages/alphatab/src/rendering/glyphs/GlyphGroup.ts b/packages/alphatab/src/rendering/glyphs/GlyphGroup.ts index 725ab8814..de41bb8ee 100644 --- a/packages/alphatab/src/rendering/glyphs/GlyphGroup.ts +++ b/packages/alphatab/src/rendering/glyphs/GlyphGroup.ts @@ -1,3 +1,4 @@ +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; @@ -14,19 +15,27 @@ export class GlyphGroup extends Glyph { } public override getBoundingBoxTop(): number { - let top = 0; + let top = Number.NaN; const glyphs = this.glyphs; if (glyphs) { for (const g of glyphs) { - const gTop = g.getBoundingBoxTop(); - if (gTop < top) { - top = gTop; - } + top = ModelUtils.minBoundingBox(top, g.getBoundingBoxTop()); } } return top; } + public override getBoundingBoxBottom(): number { + let bottom = Number.NaN; + const glyphs = this.glyphs; + if (glyphs) { + for (const g of glyphs) { + bottom = ModelUtils.maxBoundingBox(bottom, g.getBoundingBoxBottom()); + } + } + return bottom; + } + public override doLayout(): void { if (!this.glyphs || this.glyphs.length === 0) { this.width = 0; diff --git a/packages/alphatab/src/rendering/glyphs/GroupedEffectGlyph.ts b/packages/alphatab/src/rendering/glyphs/GroupedEffectGlyph.ts index bd9dcd392..e6994b7d6 100644 --- a/packages/alphatab/src/rendering/glyphs/GroupedEffectGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/GroupedEffectGlyph.ts @@ -18,14 +18,14 @@ export abstract class GroupedEffectGlyph extends EffectGlyph { } public get isLinkedWithPrevious(): boolean { - return !!this.previousGlyph && this.previousGlyph.renderer.staff.system === this.renderer.staff.system; + return !!this.previousGlyph && this.previousGlyph.renderer.staff?.system === this.renderer.staff!.system; } public get isLinkedWithNext(): boolean { return ( !!this.nextGlyph && this.nextGlyph.renderer.isFinalized && - this.nextGlyph.renderer.staff.system === this.renderer.staff.system + this.nextGlyph.renderer.staff?.system === this.renderer.staff!.system ); } diff --git a/packages/alphatab/src/rendering/glyphs/GuitarGolpeGlyph.ts b/packages/alphatab/src/rendering/glyphs/GuitarGolpeGlyph.ts index 613a2c90d..cc872e133 100644 --- a/packages/alphatab/src/rendering/glyphs/GuitarGolpeGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/GuitarGolpeGlyph.ts @@ -1,5 +1,5 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; /** @@ -7,7 +7,7 @@ import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGl */ export class GuitarGolpeGlyph extends MusicFontGlyph { public constructor(x: number, y: number, center: boolean = false) { - super(x, y, NoteHeadGlyph.GraceScale, MusicFontSymbol.GuitarGolpe); + super(x, y, EngravingSettings.GraceScale, MusicFontSymbol.GuitarGolpe); this.center = center; } diff --git a/packages/alphatab/src/rendering/glyphs/LineRangedGlyph.ts b/packages/alphatab/src/rendering/glyphs/LineRangedGlyph.ts index 696867936..bdd4b9786 100644 --- a/packages/alphatab/src/rendering/glyphs/LineRangedGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/LineRangedGlyph.ts @@ -1,3 +1,4 @@ +import type { NotationElement } from '@coderline/alphatab/NotationSettings'; import { TextBaseline, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { GroupedEffectGlyph } from '@coderline/alphatab/rendering/glyphs/GroupedEffectGlyph'; @@ -10,11 +11,13 @@ export class LineRangedGlyph extends GroupedEffectGlyph { private _label: string; private _dashed: boolean; private _labelWidth = 0; + private _fontElement: NotationElement; - public constructor(label: string, dashed: boolean = true) { + public constructor(label: string, fontElement: NotationElement, dashed: boolean = true) { super(BeatXPosition.OnNotes); this._label = label; this._dashed = dashed; + this._fontElement = fontElement; } public override doLayout(): void { @@ -23,7 +26,7 @@ export class LineRangedGlyph extends GroupedEffectGlyph { this.forceGroupedRendering = true; } super.doLayout(); - this.renderer.scoreRenderer.canvas!.font = this.renderer.resources.effectFont; + this.renderer.scoreRenderer.canvas!.font = this.renderer.resources.elementFonts.get(this._fontElement)!; const size = this.renderer.scoreRenderer.canvas!.measureText(this._label); this.height = size.height; this._labelWidth = size.width; @@ -31,7 +34,7 @@ export class LineRangedGlyph extends GroupedEffectGlyph { protected override paintNonGrouped(cx: number, cy: number, canvas: ICanvas): void { const res: RenderingResources = this.renderer.resources; - canvas.font = res.effectFont; + canvas.font = res.elementFonts.get(this._fontElement)!; const b = canvas.textBaseline; canvas.textBaseline = TextBaseline.Middle; diff --git a/packages/alphatab/src/rendering/glyphs/LyricsGlyph.ts b/packages/alphatab/src/rendering/glyphs/LyricsGlyph.ts index fd6d818ba..61841b4c2 100644 --- a/packages/alphatab/src/rendering/glyphs/LyricsGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/LyricsGlyph.ts @@ -7,6 +7,7 @@ import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; */ export class LyricsGlyph extends EffectGlyph { private _lines: string[]; + private _linePositions: number[] = []; public font: Font; public textAlign: TextAlign; @@ -20,16 +21,29 @@ export class LyricsGlyph extends EffectGlyph { public override doLayout(): void { super.doLayout(); - this.height = this.font.size * this._lines.length; + + const lineSpacing = this.renderer.settings.display.lyricLinesPaddingBetween; + + const canvas = this.renderer.scoreRenderer.canvas!; + canvas.font = this.font; + let y = 0; + for (const line of this._lines) { + this._linePositions.push(y); + const size = canvas.measureText(line.length > 0 ? line : ' '); + y += size.height + lineSpacing; + } + y -= lineSpacing; + + this.height = y; } public override paint(cx: number, cy: number, canvas: ICanvas): void { canvas.font = this.font; - const old: TextAlign = canvas.textAlign; + const old = canvas.textAlign; canvas.textAlign = this.textAlign; for (let i: number = 0; i < this._lines.length; i++) { if (this._lines[i]) { - canvas.fillText(this._lines[i], cx + this.x, cy + this.y + i * this.font.size); + canvas.fillText(this._lines[i], cx + this.x, cy + this.y + this._linePositions[i]); } } canvas.textAlign = old; diff --git a/packages/alphatab/src/rendering/glyphs/MultiBarRestGlyph.ts b/packages/alphatab/src/rendering/glyphs/MultiBarRestGlyph.ts index 1917f5764..cd50b4a08 100644 --- a/packages/alphatab/src/rendering/glyphs/MultiBarRestGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/MultiBarRestGlyph.ts @@ -17,19 +17,30 @@ export class MultiBarRestGlyph extends Glyph { ]; private _numberGlyph: MusicFontSymbol[] = []; + private _numberTop = 0; constructor() { super(0, 0); } public override doLayout(): void { - this.width = MultiBarRestGlyph._restSymbols.reduce((p, c) => p + this.renderer.smuflMetrics.glyphWidths.get(c)!, 0); - this.renderer.registerOverflowTop((this.renderer as LineBarRenderer).getLineHeight(1)); + const smufl = this.renderer.smuflMetrics; + this.width = MultiBarRestGlyph._restSymbols.reduce((p, c) => p + smufl.glyphWidths.get(c)!, 0); + const i: number = this.renderer.additionalMultiRestBars!.length + 1; this._numberGlyph = NumberGlyph.getSymbols(i); + + this._numberTop = (this.renderer as LineBarRenderer).getLineY(-1.5); + + const numberGlyphTop = smufl.glyphTop.get(this._numberGlyph[0])!; + this.renderer.registerOverflowTop(Math.abs(this._numberTop) + numberGlyphTop); + } public override paint(cx: number, cy: number, canvas: ICanvas): void { - canvas.fillMusicFontSymbols(cx + this.x, cy + this.y + this.renderer.height / 2, 1, + canvas.fillMusicFontSymbols( + cx + this.x, + cy + this.y + this.renderer.height / 2, + 1, MultiBarRestGlyph._restSymbols ); diff --git a/packages/alphatab/src/rendering/glyphs/MultiVoiceContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/MultiVoiceContainerGlyph.ts new file mode 100644 index 000000000..ce893a60b --- /dev/null +++ b/packages/alphatab/src/rendering/glyphs/MultiVoiceContainerGlyph.ts @@ -0,0 +1,319 @@ +import { Environment } from '@coderline/alphatab/Environment'; +import type { Beat } from '@coderline/alphatab/model/Beat'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import type { Note } from '@coderline/alphatab/model/Note'; +import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; +import { VoiceSubElement } from '@coderline/alphatab/model/Voice'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import type { BeatContainerGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; +import type { BarBounds } from '@coderline/alphatab/rendering/utils/BarBounds'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; + +/** + * This glyph acts as container for handling + * multiple voice rendering + * @internal + */ +export class MultiVoiceContainerGlyph extends Glyph { + public static readonly KeySizeBeat: string = 'Beat'; + + public voiceDrawOrder?: number[]; + + private readonly _beatGlyphLookup = new Map(); + + public beatGlyphs = new Map(); + public tupletGroups = new Map(); + + public constructor() { + super(0, 0); + } + + public override getBoundingBoxTop(): number { + let y = Number.NaN; + for (const v of this.beatGlyphs.values()) { + for (const b of v) { + y = ModelUtils.minBoundingBox(y, b.getBoundingBoxTop()); + } + } + return y; + } + + public override getBoundingBoxBottom(): number { + let y = Number.NaN; + for (const v of this.beatGlyphs.values()) { + for (const b of v) { + y = ModelUtils.maxBoundingBox(y, b.getBoundingBoxBottom()); + } + } + return y; + } + + public scaleToWidth(width: number): void { + const force: number = this.renderer.layoutingInfo.spaceToForce(width); + this._scaleToForce(force); + } + + private _scaleToForce(force: number): void { + this.width = this.renderer.layoutingInfo.calculateVoiceWidth(force); + const positions = this.renderer.layoutingInfo.buildOnTimePositions(force); + for (const beatGlyphs of this.beatGlyphs.values()) { + for (let i: number = 0, j: number = beatGlyphs.length; i < j; i++) { + const currentBeatGlyph = beatGlyphs[i]; + + switch (currentBeatGlyph.graceType) { + case GraceType.None: + currentBeatGlyph.x = + positions.get(currentBeatGlyph.absoluteDisplayStart)! - currentBeatGlyph.onTimeX; + break; + default: + const graceDisplayStart = currentBeatGlyph.graceGroup!.beats[0].absoluteDisplayStart; + const graceGroupId = currentBeatGlyph.graceGroup!.id; + // placement for proper grace notes which have a following note + if (currentBeatGlyph.graceGroup!.isComplete && positions.has(graceDisplayStart)) { + currentBeatGlyph.x = positions.get(graceDisplayStart)! - currentBeatGlyph.onTimeX; + + const graceSprings = this.renderer.layoutingInfo.allGraceRods.get(graceGroupId)!; + + // get the pre beat stretch of this voice/staff, not the + // shared space. This way we use the potentially empty space (see discussions/1092). + const afterGraceBeat = + currentBeatGlyph.graceGroup!.beats[currentBeatGlyph.graceGroup!.beats.length - 1] + .nextBeat; + const preBeatStretch = afterGraceBeat + ? this.renderer.layoutingInfo.getPreBeatSize(afterGraceBeat) + : 0; + + // move right in front to the note + currentBeatGlyph.x -= preBeatStretch; + // respect the post beat width of the grace note + currentBeatGlyph.x -= graceSprings[currentBeatGlyph.graceIndex].postSpringWidth; + // shift to right position of the particular grace note + + currentBeatGlyph.x += graceSprings[currentBeatGlyph.graceIndex].graceBeatWidth; + // move the whole group again forward for cases where another track has e.g. 3 beats and here we have only 2. + // so we shift the whole group of this voice to stick to the end of the group. + const lastGraceSpring = graceSprings[currentBeatGlyph.graceGroup!.beats.length - 1]; + currentBeatGlyph.x -= lastGraceSpring.graceBeatWidth; + } else { + // placement for improper grace beats where no beat in the same bar follows + const graceSpring = this.renderer.layoutingInfo.incompleteGraceRods.get(graceGroupId)!; + const relativeOffset = + graceSpring[currentBeatGlyph.graceIndex].postSpringWidth - + graceSpring[currentBeatGlyph.graceIndex].preSpringWidth; + + if (i > 0) { + if (currentBeatGlyph.graceIndex === 0) { + // we place the grace beat directly after the previous one + // otherwise this causes flickers on resizing + currentBeatGlyph.x = beatGlyphs[i - 1].x + beatGlyphs[i - 1].width; + } else { + // for the multiple grace glyphs we take the width of the grace rod + // this width setting is aligned with the positioning logic below + currentBeatGlyph.x = + beatGlyphs[i - 1].x + + graceSpring[currentBeatGlyph.graceIndex - 1].postSpringWidth - + graceSpring[currentBeatGlyph.graceIndex - 1].preSpringWidth - + relativeOffset; + } + } else { + currentBeatGlyph.x = -relativeOffset; + } + } + break; + } + + // size always previous glyph after we know the position + // of the next glyph + if (i > 0) { + const beatWidth: number = currentBeatGlyph.x - beatGlyphs[i - 1].x; + beatGlyphs[i - 1].scaleToWidth(beatWidth); + } + // for the last glyph size based on the full width + if (i === j - 1) { + const beatWidth: number = this.width - beatGlyphs[beatGlyphs.length - 1].x; + currentBeatGlyph.scaleToWidth(beatWidth); + } + } + } + } + + public registerLayoutingInfo(info: BarLayoutingInfo): void { + for (const beatGlyphs of this.beatGlyphs.values()) { + for (const b of beatGlyphs) { + b.registerLayoutingInfo(info); + } + } + } + + public applyLayoutingInfo(info: BarLayoutingInfo): void { + for (const beatGlyphs of this.beatGlyphs.values()) { + for (const b of beatGlyphs) { + b.applyLayoutingInfo(info); + } + this._scaleToForce(Math.max(this.renderer.settings.display.stretchForce, info.minStretchForce)); + } + } + + public addGlyph(bg: BeatContainerGlyphBase): void { + let beatGlyphs: BeatContainerGlyphBase[]; + if (this.beatGlyphs.has(bg.voiceIndex)) { + beatGlyphs = this.beatGlyphs.get(bg.voiceIndex)!; + } else { + beatGlyphs = []; + this.beatGlyphs.set(bg.voiceIndex, beatGlyphs); + } + + bg.x = + beatGlyphs.length === 0 ? 0 : beatGlyphs[beatGlyphs.length - 1].x + beatGlyphs[beatGlyphs.length - 1].width; + bg.renderer = this.renderer; + beatGlyphs.push(bg); + + const id = bg.beatId; + if (id >= 0) { + this._beatGlyphLookup.set(id, bg); + } + + const newWidth = bg.x + bg.width; + if (newWidth > this.width) { + this.width = newWidth; + } + if (bg.isFirstOfTupletGroup) { + let tupletGroups: TupletGroup[]; + if (this.tupletGroups.has(bg.voiceIndex)) { + tupletGroups = this.tupletGroups.get(bg.voiceIndex)!; + } else { + tupletGroups = []; + this.tupletGroups.set(bg.voiceIndex, tupletGroups); + } + + tupletGroups.push(bg.tupletGroup!); + } + } + + public getBeatX( + beat: Beat, + requestedPosition: BeatXPosition = BeatXPosition.PreNotes, + useSharedSizes: boolean = false + ): number { + const container = this.getBeatContainer(beat); + if (container) { + return container.x + container.getBeatX(requestedPosition, useSharedSizes); + } + return 0; + } + + public getLowestNoteY(beat: Beat, position: NoteYPosition): number { + const container = this.getBeatContainer(beat); + if (container) { + return container.y + container.getLowestNoteY(position); + } + return 0; + } + + public getHighestNoteY(beat: Beat, position: NoteYPosition): number { + const container = this.getBeatContainer(beat); + if (container) { + return container.y + container.getHighestNoteY(position); + } + return 0; + } + + public getNoteX(note: Note, requestedPosition: NoteXPosition): number { + const container = this.getBeatContainer(note.beat); + if (container) { + return container.x + container.getNoteX(note, requestedPosition); + } + return 0; + } + + public getNoteY(note: Note, requestedPosition: NoteYPosition): number { + const beat = this.getBeatContainer(note.beat); + if (beat) { + return beat.y + beat.getNoteY(note, requestedPosition); + } + return 0; + } + + public getRestY(beat: Beat, requestedPosition: NoteYPosition): number { + const container = this.getBeatContainer(beat); + if (container) { + return container.y + container.getRestY(requestedPosition); + } + return 0; + } + + public getBeatContainer(beat: Beat): BeatContainerGlyphBase | undefined { + if (!this._beatGlyphLookup.has(beat.id)) { + return undefined; + } + return this._beatGlyphLookup.get(beat.id); + } + + public buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number): void { + for (const [index, c] of this.beatGlyphs) { + const voice = this.renderer.bar.voices[index]; + if (index === 0 || !voice.isEmpty) { + for (const bc of c) { + bc.buildBoundingsLookup(barBounds, cx + this.x, cy + this.y); + } + } + } + } + + public override doLayout(): void { + for (const v of this.beatGlyphs.values()) { + let x = 0; + for (const b of v) { + b.x = x; + b.doLayout(); + x += b.width; + } + + if (x > this.width) { + this.width = x; + } + } + + if (this.renderer.bar.isMultiVoice) { + this._doMultiVoiceLayout(); + } + + // draw order is reversed so that the main voice overlaps secondary ones + this.voiceDrawOrder = Array.from(this.beatGlyphs.keys()); + Environment.sortDescending(this.voiceDrawOrder); + } + + private _doMultiVoiceLayout() { + for (const v of this.beatGlyphs.values()) { + let x = 0; + for (const b of v) { + b.x = x; + b.doMultiVoiceLayout(); + x += b.width; + } + + if (x > this.width) { + this.width = x; + } + } + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + // canvas.color = Color.random(); + // canvas.strokeRect(cx + this.x, cy + this.y, this.width, this.renderer.height); + for (const v of this.voiceDrawOrder!) { + const beatGlyphs = this.beatGlyphs.get(v)!; + const voice = this.renderer.bar.voices[v]; + using _ = ElementStyleHelper.voice(canvas, VoiceSubElement.Glyphs, voice, true); + + for (let i: number = 0, j: number = beatGlyphs.length; i < j; i++) { + beatGlyphs[i].paint(cx + this.x, cy + this.y, canvas); + } + } + } +} diff --git a/packages/alphatab/src/rendering/glyphs/MusicFontGlyph.ts b/packages/alphatab/src/rendering/glyphs/MusicFontGlyph.ts index 444664f5f..75b3e7909 100644 --- a/packages/alphatab/src/rendering/glyphs/MusicFontGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/MusicFontGlyph.ts @@ -7,7 +7,7 @@ import type { Color } from '@coderline/alphatab/model/Color'; * @internal */ export class MusicFontGlyph extends EffectGlyph { - protected glyphScale: number = 0; + public glyphScale: number = 0; public symbol: MusicFontSymbol; public center: boolean = false; public colorOverride?: Color; diff --git a/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts index 3d170c398..53735156c 100644 --- a/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NoteHeadGlyph.ts @@ -2,18 +2,15 @@ import { Duration } from '@coderline/alphatab/model/Duration'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; /** * @internal */ -export class NoteHeadGlyph extends MusicFontGlyph { - // TODO: SmuFL - public static readonly GraceScale: number = 0.75; - +export class NoteHeadGlyphBase extends MusicFontGlyph { public centerOnStem = false; - - public constructor(x: number, y: number, duration: Duration, isGrace: boolean) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, NoteHeadGlyph.getSymbol(duration)); + public constructor(x: number, y: number, isGrace: boolean, symbol: MusicFontSymbol) { + super(x, y, isGrace ? EngravingSettings.GraceScale : 1, symbol); } public override paint(cx: number, cy: number, canvas: ICanvas): void { @@ -22,6 +19,15 @@ export class NoteHeadGlyph extends MusicFontGlyph { } super.paint(cx, cy, canvas); } +} + +/** + * @internal + */ +export class NoteHeadGlyph extends NoteHeadGlyphBase { + public constructor(x: number, y: number, duration: Duration, isGrace: boolean) { + super(x, y, isGrace, NoteHeadGlyph.getSymbol(duration)); + } public static getSymbol(duration: Duration): MusicFontSymbol { switch (duration) { diff --git a/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts b/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts index d1e4d3b98..d97617b2b 100644 --- a/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts @@ -1,15 +1,16 @@ import { BendType } from '@coderline/alphatab/model/BendType'; import type { Font } from '@coderline/alphatab/model/Font'; import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; +import { NotationElement, NotationMode } from '@coderline/alphatab/NotationSettings'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; -import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { NotationElement, NotationMode } from '@coderline/alphatab/NotationSettings'; +import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; +import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; +import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; /** * @internal @@ -28,6 +29,18 @@ export class NoteNumberGlyph extends Glyph { this._note = note; } + private get _padding() { + return (this.renderer as TabBarRenderer).lineSpacing * 0.25; + } + + public override getBoundingBoxTop(): number { + return this.y - this.height / 2 - this._padding; + } + + public override getBoundingBoxBottom(): number { + return this.y + this.height / 2; + } + public override doLayout(): void { const n: Note = this._note; let fret: number = n.fret - n.beat.voice.bar.staff.transpositionPitch; @@ -102,22 +115,22 @@ export class NoteNumberGlyph extends Glyph { return; } const textWidth: number = this.noteStringWidth + this._trillNoteStringWidth; - const x: number = (cx + this.x + (this.width - textWidth) / 2); + const x: number = cx + this.x + (this.width - textWidth) / 2; + const y = cy + this.y; - this.paintTrill(x, cy, canvas); + this.paintTrill(x, y, canvas); using _ = ElementStyleHelper.note(canvas, NoteSubElement.GuitarTabFretNumber, this._note); - canvas.fillText(this._noteString!, x, cy + this.y); + canvas.fillText(this._noteString!, x, y); + + // canvas.color = Color.random(); + // canvas.fillRect(cx + this.x, cy + this.y - this.height / 2, this.width, this.height); } paintTrill(x: number, cy: number, canvas: ICanvas) { using _ = ElementStyleHelper.note(canvas, NoteSubElement.GuitarTabFretNumber, this._note); const prevFont: Font = this.renderer.scoreRenderer.canvas!.font; this.renderer.scoreRenderer.canvas!.font = this.renderer.resources.graceFont; - canvas.fillText( - this._trillNoteString!, - x + this.noteStringWidth, - cy + this.y - ); + canvas.fillText(this._trillNoteString!, x + this.noteStringWidth, cy); this.renderer.scoreRenderer.canvas!.font = prevFont; } diff --git a/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts index 95e19da08..786fec086 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts @@ -1,29 +1,28 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; +import { BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; +import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; -import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; -import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; -import { NumberedNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedNoteHeadGlyph'; -import type { NumberedBarRenderer } from '@coderline/alphatab/rendering/NumberedBarRenderer'; -import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper'; -import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; -import { AccidentalGroupGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGroupGlyph'; import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; +import { AccidentalGroupGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGroupGlyph'; import { AugmentationDotGlyph } from '@coderline/alphatab/rendering/glyphs/AugmentationDotGlyph'; -import { NumberedDashGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedDashGlyph'; -import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; +import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; +import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import { NumberedNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedNoteHeadGlyph'; +import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; +import type { NumberedBarRenderer } from '@coderline/alphatab/rendering/NumberedBarRenderer'; +import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper'; +import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; +import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; -import { BeatSubElement } from '@coderline/alphatab/model/Beat'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; +import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; /** * @internal @@ -32,11 +31,16 @@ export class NumberedBeatPreNotesGlyph extends BeatGlyphBase { public isNaturalizeAccidental = false; public accidental: AccidentalType = AccidentalType.None; + public skipLayout = false; + protected override get effectElement() { return BeatSubElement.NumberedEffects; } public override doLayout(): void { + if (this.skipLayout) { + return; + } if (!this.container.beat.isRest && !this.container.beat.isEmpty) { const accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); accidentals.renderer = this.renderer; @@ -85,8 +89,8 @@ export class NumberedBeatPreNotesGlyph extends BeatGlyphBase { sr.getLineY(0), accidentalToSet, note.beat.graceType !== GraceType.None - ? NoteHeadGlyph.GraceScale * NoteHeadGlyph.GraceScale - : NoteHeadGlyph.GraceScale + ? EngravingSettings.GraceScale * EngravingSettings.GraceScale + : EngravingSettings.GraceScale ); g.colorOverride = color; g.renderer = this.renderer; @@ -107,12 +111,9 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { public noteHeads: NumberedNoteHeadGlyph | null = null; public deadSlapped: DeadSlappedBeatGlyph | null = null; - public octaveDots: number = 0; - protected override get effectElement() { return BeatSubElement.NumberedEffects; } - public override getNoteX(_note: Note, requestedPosition: NoteXPosition): number { let g: Glyph | null = null; if (this.noteHeads) { @@ -151,18 +152,22 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { } } - public override getLowestNoteY(): number { - return this._internalGetNoteY(NoteYPosition.Center); + public override getLowestNoteY(requestedPosition: NoteYPosition): number { + return this._internalGetNoteY(requestedPosition); } - public override getHighestNoteY(): number { - return this._internalGetNoteY(NoteYPosition.Center); + public override getHighestNoteY(requestedPosition: NoteYPosition): number { + return this._internalGetNoteY(requestedPosition); } public override getNoteY(_note: Note, requestedPosition: NoteYPosition): number { return this._internalGetNoteY(requestedPosition); } + public override getRestY(requestedPosition: NoteYPosition): number { + return this._internalGetNoteY(requestedPosition); + } + private _internalGetNoteY(requestedPosition: NoteYPosition): number { let g: Glyph | null = null; if (this.noteHeads) { @@ -196,26 +201,6 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { return 0; } - public override updateBeamingHelper(): void { - if (this.beamingHelper) { - let g: Glyph | null = null; - if (this.noteHeads) { - g = this.noteHeads; - } else if (this.deadSlapped) { - g = this.deadSlapped; - } - - if (g) { - this.beamingHelper.registerBeatLineX( - 'numbered', - this.container.beat, - this.container.x + this.x + g.x, - this.container.x + this.x + g.x + g.width - ); - } - } - } - public static readonly majorKeySignatureOneValues: Array = [ // Flats 59, 66, 61, 68, 63, 58, 65, @@ -242,11 +227,13 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { sr.shortestDuration = this.container.beat.duration; } - const glyphY = sr.getLineY(sr.getNoteLine()); + let octaveDots = 0; if (!this.container.beat.isEmpty) { + const glyphY = sr.getLineY(0); let numberWithinOctave = '0'; if (this.container.beat.notes.length > 0) { + const note = this.container.beat.notes[0]; const kst = this.renderer.bar.keySignatureType; const ks = this.renderer.bar.keySignature as number; const ksi = ks + 7; @@ -257,8 +244,6 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { : NumberedBeatGlyph.majorKeySignatureOneValues; const oneNoteValue = oneNoteValues[ksi]; - const note = this.container.beat.notes[0]; - if (note.isDead) { numberWithinOctave = 'X'; } else { @@ -266,13 +251,10 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { const index = noteValue < 0 ? ((noteValue % 12) + 12) % 12 : noteValue % 12; - let dots = noteValue < 0 ? ((Math.abs(noteValue) + 12) / 12) | 0 : (noteValue / 12) | 0; + octaveDots = noteValue < 0 ? ((Math.abs(noteValue) + 12) / 12) | 0 : (noteValue / 12) | 0; if (noteValue < 0) { - dots *= -1; + octaveDots *= -1; } - this.octaveDots = dots; - sr.registerOctave(dots); - const stepList = ModelUtils.keySignatureIsSharp(ks) || ModelUtils.keySignatureIsNatural(ks) ? AccidentalHelper.flatNoteSteps @@ -309,7 +291,8 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { glyphY, numberWithinOctave, isGrace, - this.container.beat + this.container.beat, + octaveDots ); this.noteHeads = noteHeadGlyph; @@ -320,50 +303,23 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { // Note dots if (this.container.beat.dots > 0 && this.container.beat.duration >= Duration.Quarter) { for (let i: number = 0; i < this.container.beat.dots; i++) { - const dot = new AugmentationDotGlyph(0, sr.getLineY(0)); + const dot = new AugmentationDotGlyph(0, glyphY); dot.renderer = this.renderer; this.addEffect(dot); } } - - // - // Dashes - let numberOfQuarterNotes = 0; - switch (this.container.beat.duration) { - case Duration.QuadrupleWhole: - numberOfQuarterNotes = 16; - break; - case Duration.DoubleWhole: - numberOfQuarterNotes = 8; - break; - case Duration.Whole: - numberOfQuarterNotes = 4; - break; - case Duration.Half: - numberOfQuarterNotes = 2; - break; - } - - let numberOfAddedQuarters = numberOfQuarterNotes; - for (let i = 0; i < this.container.beat.dots; i++) { - numberOfAddedQuarters = (numberOfAddedQuarters / 2) | 0; - numberOfQuarterNotes += numberOfAddedQuarters; - } - for (let i = 0; i < numberOfQuarterNotes - 1; i++) { - const dash = new NumberedDashGlyph(0, sr.getLineY(0), this.container.beat); - dash.renderer = this.renderer; - this.addNormal(dash); - } } super.doLayout(); if (this.container.beat.isEmpty) { - this.centerX = this.width / 2; + this.onTimeX = this.width / 2; } else if (this.noteHeads) { - this.centerX = this.noteHeads.x + this.noteHeads.width / 2; + this.onTimeX = this.noteHeads.x + this.noteHeads.width / 2; } else if (this.deadSlapped) { - this.centerX = this.deadSlapped.x + this.deadSlapped.width / 2; + this.onTimeX = this.deadSlapped.x + this.deadSlapped.width / 2; } + this.middleX = this.onTimeX; + this.stemX = this.middleX; } } diff --git a/packages/alphatab/src/rendering/glyphs/NumberedDashBeatContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedDashBeatContainerGlyph.ts new file mode 100644 index 000000000..dc702247b --- /dev/null +++ b/packages/alphatab/src/rendering/glyphs/NumberedDashBeatContainerGlyph.ts @@ -0,0 +1,190 @@ +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import type { Beat } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import type { GraceGroup } from '@coderline/alphatab/model/GraceGroup'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import type { Note } from '@coderline/alphatab/model/Note'; +import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import { BeatContainerGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import type { NumberedBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedBeatGlyph'; +import type { NumberedBarRenderer } from '@coderline/alphatab/rendering/NumberedBarRenderer'; +import { NumberedBeatContainerGlyph } from '@coderline/alphatab/rendering/NumberedBeatContainerGlyph'; +import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; +import type { BarBounds } from '@coderline/alphatab/rendering/utils/BarBounds'; + +/** + * @internal + */ +export interface INumberedBeatDashGlyph { + readonly contentWidth: number; + readonly x: number; + readonly width: number; +} + +/** + * @internal + */ +export class NumberedNoteBeatContainerGlyphBase extends NumberedBeatContainerGlyph implements INumberedBeatDashGlyph { + private _absoluteDisplayStart: number; + private _displayDuration: number; + public constructor(beat: Beat, absoluteDisplayStart: number, displayDuration: number) { + super(beat); + this._absoluteDisplayStart = absoluteDisplayStart; + this._displayDuration = displayDuration; + (this.preNotes as NumberedBeatPreNotesGlyph).skipLayout = true; + + this.barCount = NumberedNoteBeatContainerGlyphBase._ticksToBarCount(displayDuration); + } + + private static _ticksToBarCount(displayDuration: number): number { + // we know that displayDuration < MidiUtils.QuarterTime, otherwise this glyph is not created + if (displayDuration >= MidiUtils.toTicks(Duration.Eighth)) { + return 1; + } else if (displayDuration >= MidiUtils.toTicks(Duration.Sixteenth)) { + return 2; + } else if (displayDuration >= MidiUtils.toTicks(Duration.ThirtySecond)) { + return 3; + } else if (displayDuration >= MidiUtils.toTicks(Duration.SixtyFourth)) { + return 4; + } else if (displayDuration >= MidiUtils.toTicks(Duration.OneHundredTwentyEighth)) { + return 5; + } else if (displayDuration >= MidiUtils.toTicks(Duration.TwoHundredFiftySixth)) { + return 6; + } + return 0; + } + + public readonly barCount: number; + + public override get beatId(): number { + return -1; + } + + public get contentWidth() { + return this.onNotes.width; + } + + public override get absoluteDisplayStart(): number { + return this._absoluteDisplayStart; + } + + public override get displayDuration(): number { + return this._displayDuration; + } + + public override get graceType(): GraceType { + return GraceType.None; + } + public override get graceIndex(): number { + return 0; + } + public override get graceGroup(): GraceGroup | null { + return null; + } + + public override get isFirstOfTupletGroup(): boolean { + return false; + } + public override get tupletGroup(): TupletGroup | null { + return null; + } + public override get isLastOfVoice(): boolean { + return false; + } + + public override buildBoundingsLookup(_barBounds: BarBounds, _cx: number, _cy: number): void {} +} + +/** + * @internal + */ +export class NumberedDashBeatContainerGlyph extends BeatContainerGlyphBase implements INumberedBeatDashGlyph { + private _absoluteDisplayStart: number; + private _voiceIndex: number; + public constructor(voiceIndex: number, absoluteDisplayStart: number) { + super(0, 0); + this._absoluteDisplayStart = absoluteDisplayStart; + this._voiceIndex = voiceIndex; + } + + public override get beatId(): number { + return -1; + } + + public get contentWidth() { + return this.renderer.smuflMetrics.numberedDashGlyphWidth; + } + + public override get absoluteDisplayStart(): number { + return this._absoluteDisplayStart; + } + public override get displayDuration(): number { + return MidiUtils.QuarterTime; + } + + public override get onTimeX(): number { + return this.renderer.smuflMetrics.numberedDashGlyphWidth / 2; + } + + public override get graceType(): GraceType { + return GraceType.None; + } + public override get graceIndex(): number { + return 0; + } + public override get graceGroup(): GraceGroup | null { + return null; + } + public override get voiceIndex(): number { + return this._voiceIndex; + } + + public override get isFirstOfTupletGroup(): boolean { + return false; + } + public override get tupletGroup(): TupletGroup | null { + return null; + } + public override get isLastOfVoice(): boolean { + return false; + } + + public override getLowestNoteY(_requestedPosition: NoteYPosition): number { + return 0; + } + + public override getHighestNoteY(_requestedPosition: NoteYPosition): number { + return 0; + } + + public override getNoteY(_note: Note, _requestedPosition: NoteYPosition): number { + return 0; + } + public override doMultiVoiceLayout(): void {} + public override getRestY(_requestedPosition: NoteYPosition): number { + return 0; + } + public override getNoteX(_note: Note, _requestedPosition: NoteXPosition): number { + return 0; + } + public override getBeatX(_requestedPosition: BeatXPosition, _useSharedSizes: boolean): number { + return 0; + } + public override registerLayoutingInfo(layoutings: BarLayoutingInfo): void { + const width = this.renderer.smuflMetrics.numberedDashGlyphWidth; + layoutings.addBeatSpring(this, width / 2, width / 2); + } + public override applyLayoutingInfo(_info: BarLayoutingInfo): void {} + public override buildBoundingsLookup(_barBounds: BarBounds, _cx: number, _cy: number): void {} + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const renderer = this.renderer as NumberedBarRenderer; + const dashWidth = renderer.smuflMetrics.numberedDashGlyphWidth; + const dashHeight = renderer.smuflMetrics.numberedBarRendererBarSize; + const dashY = Math.ceil(cy + renderer.getLineY(0) - dashHeight); + canvas.fillRect(cx + this.x, dashY, dashWidth, dashHeight); + } +} diff --git a/packages/alphatab/src/rendering/glyphs/NumberedDashGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedDashGlyph.ts deleted file mode 100644 index 8de543555..000000000 --- a/packages/alphatab/src/rendering/glyphs/NumberedDashGlyph.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; - -/** - * @internal - */ -export class NumberedDashGlyph extends Glyph { - private _beat: Beat; - - public constructor(x: number, y: number, beat: Beat) { - super(x, y); - this._beat = beat; - } - - public override doLayout(): void { - this.width = - this.renderer.smuflMetrics.numberedDashGlyphWidth + this.renderer.smuflMetrics.numberedDashGlyphPadding; - this.height = this.renderer.smuflMetrics.numberedBarRendererBarSize; - } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - using _ = ElementStyleHelper.beat(canvas, BeatSubElement.NumberedDuration, this._beat); - const padding = this.renderer.smuflMetrics.numberedDashGlyphPadding; - canvas.fillRect(cx + this.x, Math.ceil(cy + this.y - this.height), this.width - padding, this.height); - } -} diff --git a/packages/alphatab/src/rendering/glyphs/NumberedKeySignatureGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedKeySignatureGlyph.ts index 298670881..92bb162be 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedKeySignatureGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedKeySignatureGlyph.ts @@ -1,22 +1,23 @@ -import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; -import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; +import { BarSubElement } from '@coderline/alphatab/model/Bar'; import { KeySignature } from '@coderline/alphatab/model/KeySignature'; +import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; import { CanvasHelper, type ICanvas, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; -import { BarSubElement } from '@coderline/alphatab/model/Bar'; +import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; +import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** * @internal */ -export class NumberedKeySignatureGlyph extends Glyph { +export class NumberedKeySignatureGlyph extends EffectGlyph { private _keySignature: KeySignature; private _keySignatureType: KeySignatureType; private _text: string = ''; private _accidental: AccidentalType = AccidentalType.None; private _accidentalOffset: number = 0; + private _padding: number = 0; public constructor(x: number, y: number, keySignature: KeySignature, keySignatureType: KeySignatureType) { super(x, y); @@ -161,10 +162,15 @@ export class NumberedKeySignatureGlyph extends Glyph { this._text = text + text2; this._accidental = accidental; const c = this.renderer.scoreRenderer.canvas!; - const res = this.renderer.resources; + const settings = this.renderer.settings; + const res = settings.display.resources; c.font = res.numberedNotationFont; this._accidentalOffset = c.measureText(text).width; - this.width = c.measureText(text + text2).width; + const fullSize = c.measureText(text + text2); + this._padding = + this.renderer.index === 0 ? settings.display.firstStaffPaddingLeft : settings.display.staffPaddingLeft; + this.width = this._padding + fullSize.width; + this.height = fullSize.height; } public override paint(cx: number, cy: number, canvas: ICanvas): void { @@ -172,13 +178,14 @@ export class NumberedKeySignatureGlyph extends Glyph { const res = this.renderer.resources; canvas.font = res.numberedNotationFont; - canvas.textBaseline = TextBaseline.Middle; - canvas.fillText(this._text, cx + this.x, cy + this.y); + canvas.textBaseline = TextBaseline.Alphabetic; + canvas.fillText(this._text, cx + this.x + this._padding, cy + this.y + this.height); if (this._accidental !== AccidentalType.None) { - CanvasHelper.fillMusicFontSymbolSafe(canvas, - cx + this.x + this._accidentalOffset, - cy + this.y, + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + cx + this.x + this._padding + this._accidentalOffset, + cy + this.y + this.height, 1, AccidentalGlyph.getMusicSymbol(this._accidental), false diff --git a/packages/alphatab/src/rendering/glyphs/NumberedNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedNoteHeadGlyph.ts index 65a3efde4..5a9ee0967 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedNoteHeadGlyph.ts @@ -1,8 +1,9 @@ -import { type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; -import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { NoteSubElement } from '@coderline/alphatab/model/Note'; +import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; +import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** * @internal @@ -11,12 +12,36 @@ export class NumberedNoteHeadGlyph extends Glyph { private _isGrace: boolean; private _beat: Beat; private _number: string; + private _octaveDots: number; + private _octaveDotsY: number = 0; + private _octaveDotHeight: number = 0; - public constructor(x: number, y: number, number: string, isGrace: boolean, beat: Beat) { + public constructor(x: number, y: number, number: string, isGrace: boolean, beat: Beat, octaveDots: number) { super(x, y); this._isGrace = isGrace; this._number = number; this._beat = beat; + this._octaveDots = octaveDots; + } + + public override getBoundingBoxTop(): number { + let y = -this.height / 2; + + if (this._octaveDots > 0 && this._octaveDotsY < y) { + y = this._octaveDotsY; + } + return this.y + y; + } + + public override getBoundingBoxBottom(): number { + let y = this.height / 2; + + const dotsBottom = this._octaveDotsY + Math.abs(this._octaveDots) * this._octaveDotHeight * 2; + if (this._octaveDots < 0 && y < dotsBottom) { + y = dotsBottom; + } + + return this.y + y; } public override paint(cx: number, cy: number, canvas: ICanvas): void { @@ -28,9 +53,25 @@ export class NumberedNoteHeadGlyph extends Glyph { const res = this.renderer.resources; canvas.font = this._isGrace ? res.numberedNotationGraceFont : res.numberedNotationFont; + const baseline = canvas.textBaseline; canvas.textBaseline = TextBaseline.Middle; canvas.textAlign = TextAlign.Left; canvas.fillText(this._number.toString(), cx + this.x, cy + this.y); + canvas.textBaseline = baseline; + + const dotCount = Math.abs(this._octaveDots); + let dotsY = this._octaveDotsY + res.engravingSettings.glyphTop.get(MusicFontSymbol.AugmentationDot)!; + for (let d = 0; d < dotCount; d++) { + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + cx + this.x + this.width / 2, + cy + this.y + dotsY, + 1, + MusicFontSymbol.AugmentationDot, + true + ); + dotsY += this._octaveDotHeight * 2; + } } public override doLayout(): void { @@ -38,8 +79,27 @@ export class NumberedNoteHeadGlyph extends Glyph { const font = this._isGrace ? res.numberedNotationGraceFont : res.numberedNotationFont; const c = this.renderer.scoreRenderer.canvas!; c.font = font; - const size = c.measureText(`${this._number}`); + const size = c.measureText(`${this._number}`); this.height = size.height; this.width = size.width; + + const dotCount = this._octaveDots; + + const dotHeight = res.engravingSettings.glyphHeights.get(MusicFontSymbol.AugmentationDot)!; + const allDotsHeight = Math.abs(dotCount) * dotHeight * 2; + if (dotCount > 0) { + this._octaveDotsY = + -(this.height / 2) - + allDotsHeight - + res.engravingSettings.glyphTop.get(MusicFontSymbol.AugmentationDot)!; + } else if (dotCount < 0) { + this._octaveDotsY = + this.height / 2 + + // one for the padding + dotHeight + + // align the dots + res.engravingSettings.glyphTop.get(MusicFontSymbol.AugmentationDot)!; + } + this._octaveDotHeight = dotHeight; } } diff --git a/packages/alphatab/src/rendering/glyphs/NumberedSlurGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedSlurGlyph.ts index 90b892af6..7c08afc64 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedSlurGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedSlurGlyph.ts @@ -1,79 +1,11 @@ -import type { Note } from '@coderline/alphatab/model/Note'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; -import { TabTieGlyph } from '@coderline/alphatab/rendering/glyphs/TabTieGlyph'; +import { TabSlurGlyph } from '@coderline/alphatab/rendering/glyphs/TabSlurGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ -export class NumberedSlurGlyph extends TabTieGlyph { - private _direction: BeamDirection; - private _forSlide: boolean; - - public constructor(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean = false) { - super(startNote, endNote, forEnd); - this._direction = BeamDirection.Up; - this._forSlide = forSlide; - } - - protected override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { - return Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight / 2; - } - - public tryExpand(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean): boolean { - // same type required - if (this._forSlide !== forSlide) { - return false; - } - if (this.forEnd !== forEnd) { - return false; - } - // same start and endbeat - if (this.startNote.beat.id !== startNote.beat.id) { - return false; - } - if (this.endNote.beat.id !== endNote.beat.id) { - return false; - } - // if we can expand, expand in correct direction - switch (this._direction) { - case BeamDirection.Up: - if (startNote.realValue > this.startNote.realValue) { - this.startNote = startNote; - this.startBeat = startNote.beat; - } - if (endNote.realValue > this.endNote.realValue) { - this.endNote = endNote; - this.endBeat = endNote.beat; - } - break; - case BeamDirection.Down: - if (startNote.realValue < this.startNote.realValue) { - this.startNote = startNote; - this.startBeat = startNote.beat; - } - if (endNote.realValue < this.endNote.realValue) { - this.endNote = endNote; - this.endBeat = endNote.beat; - } - break; - } - return true; - } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - const startNoteRenderer: BarRendererBase = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, - this.startBeat!.voice.bar - )!; - const direction: BeamDirection = this.getBeamDirection(this.startBeat!, startNoteRenderer); - const slurId: string = `numbered.slur.${this.startNote.beat.id}.${this.endNote.beat.id}.${direction}`; - const renderer = this.renderer; - const isSlurRendered: boolean = renderer.staff.getSharedLayoutData(slurId, false); - if (!isSlurRendered) { - renderer.staff.setSharedLayoutData(slurId, true); - super.paint(cx, cy, canvas); - } +export class NumberedSlurGlyph extends TabSlurGlyph { + protected override calculateTieDirection(): BeamDirection { + return BeamDirection.Up; } } diff --git a/packages/alphatab/src/rendering/glyphs/NumberedTieGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedTieGlyph.ts index 35646ca96..68345e7c1 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedTieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedTieGlyph.ts @@ -1,26 +1,10 @@ -import type { Beat } from '@coderline/alphatab/model/Beat'; -import type { Note } from '@coderline/alphatab/model/Note'; -import { type BarRendererBase, NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import { NoteTieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ -export class NumberedTieGlyph extends TieGlyph { - protected startNote: Note; - protected endNote: Note; - - public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { - super(!startNote ? null : startNote.beat, !endNote ? null : endNote.beat, forEnd); - this.startNote = startNote; - this.endNote = endNote; - } - - private get _isLeftHandTap() { - return this.startNote === this.endNote; - } - +export class NumberedTieGlyph extends NoteTieGlyph { protected override shouldDrawBendSlur() { return ( this.renderer.settings.notation.extendBendArrowsOnTiedNotes && @@ -29,33 +13,7 @@ export class NumberedTieGlyph extends TieGlyph { ); } - public override doLayout(): void { - super.doLayout(); - } - - protected override getBeamDirection(_beat: Beat, _noteRenderer: BarRendererBase): BeamDirection { + protected override calculateTieDirection(): BeamDirection { return BeamDirection.Up; } - - protected override getStartY(): number { - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Top); - } - - protected override getEndY(): number { - return this.getStartY(); - } - - protected override getStartX(): number { - if (this._isLeftHandTap) { - return this.getEndX() - this.startNoteRenderer!.smuflMetrics.leftHandTabTieWidth; - } - return this.startNoteRenderer!.getNoteX(this.startNote, NoteXPosition.Center); - } - - protected override getEndX(): number { - if (this._isLeftHandTap) { - return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); - } - return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Center); - } } diff --git a/packages/alphatab/src/rendering/glyphs/PercussionNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/PercussionNoteHeadGlyph.ts index f62d0a51c..e0c8f82f3 100644 --- a/packages/alphatab/src/rendering/glyphs/PercussionNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/PercussionNoteHeadGlyph.ts @@ -1,14 +1,16 @@ -import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import type { Duration } from '@coderline/alphatab/model/Duration'; -import { TechniqueSymbolPlacement, type InstrumentArticulation } from '@coderline/alphatab/model/InstrumentArticulation'; +import { + type InstrumentArticulation, + TechniqueSymbolPlacement +} from '@coderline/alphatab/model/InstrumentArticulation'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; /** * @internal */ -export class PercussionNoteHeadGlyph extends MusicFontGlyph { +export class PercussionNoteHeadGlyph extends NoteHeadGlyphBase { private _isGrace: boolean; private _articulation: InstrumentArticulation; @@ -19,7 +21,7 @@ export class PercussionNoteHeadGlyph extends MusicFontGlyph { duration: Duration, isGrace: boolean ) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, articulation.getSymbol(duration)); + super(x, y, isGrace, articulation.getSymbol(duration)); this._isGrace = isGrace; this._articulation = articulation; } @@ -31,13 +33,21 @@ export class PercussionNoteHeadGlyph extends MusicFontGlyph { } const offset: number = this._isGrace ? 1 : 0; - CanvasHelper.fillMusicFontSymbolSafe(canvas,cx + this.x, cy + this.y + offset, this.glyphScale, this.symbol, false); + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + cx + this.x, + cy + this.y + offset, + this.glyphScale, + this.symbol, + false + ); if ( this._articulation.techniqueSymbol !== MusicFontSymbol.None && this._articulation.techniqueSymbolPlacement === TechniqueSymbolPlacement.Inside ) { - CanvasHelper.fillMusicFontSymbolSafe(canvas, + CanvasHelper.fillMusicFontSymbolSafe( + canvas, cx + this.x, cy + this.y + offset, this.glyphScale, diff --git a/packages/alphatab/src/rendering/glyphs/PickStrokeGlyph.ts b/packages/alphatab/src/rendering/glyphs/PickStrokeGlyph.ts index f24c56026..e28295e00 100644 --- a/packages/alphatab/src/rendering/glyphs/PickStrokeGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/PickStrokeGlyph.ts @@ -1,14 +1,14 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; /** * @internal */ export class PickStrokeGlyph extends MusicFontGlyph { public constructor(x: number, y: number, pickStroke: PickStroke) { - super(x, y, NoteHeadGlyph.GraceScale, PickStrokeGlyph._getSymbol(pickStroke)); + super(x, y, EngravingSettings.GraceScale, PickStrokeGlyph._getSymbol(pickStroke)); this.center = true; } diff --git a/packages/alphatab/src/rendering/glyphs/RepeatCountGlyph.ts b/packages/alphatab/src/rendering/glyphs/RepeatCountGlyph.ts index 4a29e0cfd..eff8c43e9 100644 --- a/packages/alphatab/src/rendering/glyphs/RepeatCountGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/RepeatCountGlyph.ts @@ -3,6 +3,7 @@ import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal @@ -17,7 +18,13 @@ export class RepeatCountGlyph extends Glyph { } public override doLayout(): void { - this.width = 0; + this.renderer.scoreRenderer.canvas!.font = this.renderer.resources.elementFonts.get( + NotationElement.RepeatCount + )!; + const size = this.renderer.scoreRenderer.canvas!.measureText(`x${this._count}`); + this.width = 0; // do not account width + this.height = size.height; + this.y -= size.height; } public override paint(cx: number, cy: number, canvas: ICanvas): void { @@ -29,7 +36,7 @@ export class RepeatCountGlyph extends Glyph { const res: RenderingResources = this.renderer.resources; const oldAlign: TextAlign = canvas.textAlign; - canvas.font = res.barNumberFont; + canvas.font = res.elementFonts.get(NotationElement.RepeatCount)!; canvas.textAlign = TextAlign.Right; const s: string = `x${this._count}`; const w: number = canvas.measureText(s).width / 1.5; diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts index 0c9b4d747..f7d2329e1 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts @@ -1,48 +1,49 @@ +import { Logger } from '@coderline/alphatab/Logger'; import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; +import { BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; +import { TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentArticulation'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; +import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; +import { PickStroke } from '@coderline/alphatab/model/PickStroke'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { type NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { AccentuationGlyph } from '@coderline/alphatab/rendering/glyphs/AccentuationGlyph'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; +import { ArticStaccatoAboveGlyph } from '@coderline/alphatab/rendering/glyphs/ArticStaccatoAboveGlyph'; import { AugmentationDotGlyph } from '@coderline/alphatab/rendering/glyphs/AugmentationDotGlyph'; +import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; import { DeadNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/DeadNoteHeadGlyph'; import { DiamondNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/DiamondNoteHeadGlyph'; +import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { GhostNoteContainerGlyph } from '@coderline/alphatab/rendering/glyphs/GhostNoteContainerGlyph'; import { GlyphGroup } from '@coderline/alphatab/rendering/glyphs/GlyphGroup'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { GuitarGolpeGlyph } from '@coderline/alphatab/rendering/glyphs/GuitarGolpeGlyph'; +import { NoteHeadGlyph, type NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { PercussionNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/PercussionNoteHeadGlyph'; +import { PickStrokeGlyph } from '@coderline/alphatab/rendering/glyphs/PickStrokeGlyph'; +import { PictEdgeOfCymbalGlyph } from '@coderline/alphatab/rendering/glyphs/PictEdgeOfCymbalGlyph'; import { ScoreNoteChordGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreNoteChordGlyph'; import { ScoreRestGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreRestGlyph'; import { ScoreWhammyBarGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreWhammyBarGlyph'; +import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; +import { StringNumberContainerGlyph } from '@coderline/alphatab/rendering/glyphs/StringNumberContainerGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; -import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; -import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; -import { PercussionNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/PercussionNoteHeadGlyph'; -import { Logger } from '@coderline/alphatab/Logger'; -import { ArticStaccatoAboveGlyph } from '@coderline/alphatab/rendering/glyphs/ArticStaccatoAboveGlyph'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import { PictEdgeOfCymbalGlyph } from '@coderline/alphatab/rendering/glyphs/PictEdgeOfCymbalGlyph'; -import { PickStrokeGlyph } from '@coderline/alphatab/rendering/glyphs/PickStrokeGlyph'; -import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import { GuitarGolpeGlyph } from '@coderline/alphatab/rendering/glyphs/GuitarGolpeGlyph'; +import type { ScoreBeatContainerGlyph } from '@coderline/alphatab/rendering/ScoreBeatContainerGlyph'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import { StringNumberContainerGlyph } from '@coderline/alphatab/rendering/glyphs/StringNumberContainerGlyph'; -import { BeatSubElement } from '@coderline/alphatab/model/Beat'; +import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import type { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; -import { TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentArticulation'; -import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; -import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; /** * @internal */ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { - private _collisionOffset: number = -1000; + private _collisionOffset: number = Number.NaN; private _skipPaint: boolean = false; + private _whammy?: ScoreWhammyBarGlyph; public noteHeads: ScoreNoteChordGlyph | null = null; public restGlyph: ScoreRestGlyph | null = null; @@ -51,49 +52,106 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { return BeatSubElement.StandardNotationEffects; } - public override getNoteX(note: Note, requestedPosition: NoteXPosition): number { - return this.noteHeads ? this.noteHeads.getNoteX(note, requestedPosition) : 0; - } - public override buildBoundingsLookup(beatBounds: BeatBounds, cx: number, cy: number) { if (this.noteHeads) { this.noteHeads.buildBoundingsLookup(beatBounds, cx + this.x, cy + this.y); } } - public override getLowestNoteY(): number { - return this.noteHeads ? this.noteHeads.getLowestNoteY() : 0; + public override getBoundingBoxTop(): number { + let y = this.y; + if (this.noteHeads) { + y = this.noteHeads.getBoundingBoxTop(); + } else if (this.restGlyph) { + y = this.restGlyph.getBoundingBoxTop(); + } + + if (this._whammy?.hasBoundingBox) { + y = Math.min(y, this._whammy.getBoundingBoxTop()); + } + return y; + } + + public override getBoundingBoxBottom(): number { + let y = this.y + this.height; + if (this.noteHeads) { + y = this.noteHeads.getBoundingBoxBottom(); + } else if (this.restGlyph) { + y = this.restGlyph.getBoundingBoxBottom(); + } + + if (this._whammy?.hasBoundingBox) { + y = Math.max(y, this._whammy.getBoundingBoxBottom()); + } + return y; + } + + public override getLowestNoteY(requestedPosition: NoteYPosition): number { + // NOTE: slash handled automatically + return this.noteHeads ? this.noteHeads.getLowestNoteY(requestedPosition) : 0; } - public override getHighestNoteY(): number { - return this.noteHeads ? this.noteHeads.getHighestNoteY() : 0; + public override getHighestNoteY(requestedPosition: NoteYPosition): number { + // NOTE: slash handled automatically + return this.noteHeads ? this.noteHeads.getHighestNoteY(requestedPosition) : 0; } public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { + // for slashed beats always lookup first note + if (note.beat.slashed) { + note = note.beat.notes[0]; + } return this.noteHeads ? this.noteHeads.getNoteY(note, requestedPosition) : 0; } - public override updateBeamingHelper(): void { - if (this.noteHeads) { - this.noteHeads.updateBeamingHelper(this.container.x + this.x); - } else if (this.restGlyph) { - this.restGlyph.updateBeamingHelper(this.container.x + this.x); - if (this.renderer.bar.isMultiVoice && this._collisionOffset === -1000) { - this._collisionOffset = this.renderer.helpers.collisionHelper.applyRestCollisionOffset( - this.container.beat, - this.restGlyph.y, - (this.renderer as ScoreBarRenderer).getScoreHeight(1) - ); - this.y += this._collisionOffset; - const existingRests = this.renderer.helpers.collisionHelper.restDurationsByDisplayTime; - if ( - existingRests.has(this.container.beat.playbackStart) && - existingRests.get(this.container.beat.playbackStart)!.has(this.container.beat.playbackDuration) && - existingRests.get(this.container.beat.playbackStart)!.get(this.container.beat.playbackDuration) !== - this.container.beat.id - ) { - this._skipPaint = true; - } + public override getNoteX(note: Note, requestedPosition: NoteXPosition): number { + // for slashed beats always lookup first note + if (note.beat.slashed) { + note = note.beat.notes[0]; + } + return this.noteHeads ? this.noteHeads.getNoteX(note, requestedPosition) : 0; + } + + public override getRestY(requestedPosition: NoteYPosition): number { + const g = this.restGlyph; + if (g) { + switch (requestedPosition) { + case NoteYPosition.TopWithStem: + return g.getBoundingBoxTop() - this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); + case NoteYPosition.Top: + return g.getBoundingBoxTop(); + case NoteYPosition.Center: + case NoteYPosition.StemUp: + case NoteYPosition.StemDown: + return g.getBoundingBoxTop() + g.height / 2; + case NoteYPosition.Bottom: + return g.getBoundingBoxBottom(); + case NoteYPosition.BottomWithStem: + return g.getBoundingBoxBottom() + this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); + } + } + return 0; + } + + public applyRestCollisionOffset() { + if (!this.restGlyph) { + return; + } + if (Number.isNaN(this._collisionOffset)) { + this._collisionOffset = this.renderer.collisionHelper.applyRestCollisionOffset( + this.container.beat, + this.restGlyph.y, + (this.renderer as ScoreBarRenderer).getScoreHeight(1) + ); + this.y += this._collisionOffset; + const existingRests = this.renderer.collisionHelper.restDurationsByDisplayTime; + if ( + existingRests.has(this.container.beat.playbackStart) && + existingRests.get(this.container.beat.playbackStart)!.has(this.container.beat.playbackDuration) && + existingRests.get(this.container.beat.playbackStart)!.get(this.container.beat.playbackDuration) !== + this.container.beat.id + ) { + this._skipPaint = true; } } } @@ -104,114 +162,183 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { } } + public doMultiVoiceLayout(): void { + this.applyRestCollisionOffset(); + this.noteHeads?.doMultiVoiceLayout(); + this._whammy?.doMultiVoiceLayout(); + + let w: number = 0; + if (this.glyphs) { + for (const g of this.glyphs) { + g.x = w; + w += g.width; + } + } + this.width = w; + this.computedWidth = w; + + this._updatePositions(); + } + public override doLayout(): void { - // create glyphs - const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; - if (!this.container.beat.isEmpty) { - if (!this.container.beat.isRest) { - // - // Note heads - // - const noteHeads = new ScoreNoteChordGlyph(); - this.noteHeads = noteHeads; - noteHeads.beat = this.container.beat; - noteHeads.beamingHelper = this.beamingHelper; - const ghost: GhostNoteContainerGlyph = new GhostNoteContainerGlyph(false); - ghost.renderer = this.renderer; + this._createGlyphs(); + super.doLayout(); + this._updatePositions(); + } - for (const note of this.container.beat.notes) { - if (note.isVisible && (!note.beat.slashed || note.index === 0)) { - this._createNoteGlyph(note); - ghost.addParenthesis(note); - } - } + private _updatePositions() { + if (this.container.beat.isEmpty) { + this.onTimeX = this.width / 2; + this.middleX = this.onTimeX; + this.stemX = this.middleX; + } else if (this.restGlyph) { + this.onTimeX = this.restGlyph!.x + this.restGlyph!.width / 2; + this.middleX = this.onTimeX; + this.stemX = this.middleX; + } else if (this.noteHeads) { + this.onTimeX = this.noteHeads!.x + this.noteHeads!.onTimeX; + this.middleX = this.noteHeads!.x + this.noteHeads!.width / 2; + this.stemX = this.noteHeads!.x + this.noteHeads!.stemX; + } + } - this.addNormal(noteHeads); - if (!ghost.isEmpty) { - this.addEffect(ghost); - } + private _createGlyphs() { + if (this.container.beat.isEmpty) { + return; + } - // - // Whammy Bar - if (this.container.beat.hasWhammyBar) { - const whammy: ScoreWhammyBarGlyph = new ScoreWhammyBarGlyph(this.container.beat); - whammy.renderer = this.renderer; - whammy.doLayout(); - this.container.ties.push(whammy); + if (!this.container.beat.isRest) { + this._createNoteGlyphs(); + } else { + this._createRestGlyphs(); + } + } + + private _createNoteGlyphs() { + const sr = this.renderer as ScoreBarRenderer; + + // + // Note heads + const noteHeads = new ScoreNoteChordGlyph(); + this.noteHeads = noteHeads; + noteHeads.beat = this.container.beat; + const ghost = new GhostNoteContainerGlyph(false); + ghost.renderer = this.renderer; + + if (this.container.beat.slashed) { + const steps = sr.heightLineCount - 1; + const slash = new SlashNoteHeadGlyph(0, sr.getScoreY(steps), this.container.beat); + slash.colorOverride = ElementStyleHelper.noteColor( + sr.resources, + NoteSubElement.StandardNotationNoteHead, + this.container.beat.notes[0] + ); + this.noteHeads!.addMainNoteGlyph(slash, this.container.beat.notes[0], steps); + } else { + for (const note of this.container.beat.notes) { + if (note.isVisible) { + this._createNoteGlyph(note); + ghost.addParenthesis(note); } - // - // Note dots - // - if (this.container.beat.dots > 0) { - for (let i: number = 0; i < this.container.beat.dots; i++) { - const group: GlyphGroup = new GlyphGroup(0, 0); - group.renderer = this.renderer; - for (const note of this.container.beat.notes) { - const g = this._createBeatDot(sr.getNoteLine(note), group); - g.colorOverride = ElementStyleHelper.noteColor( - sr.resources, - NoteSubElement.StandardNotationEffects, - note - ); - } - this.addEffect(group); - } + } + } + + this.addNormal(noteHeads); + if (!ghost.isEmpty) { + this.addEffect(ghost); + } + + // + // Whammy Bar + if (this.container.beat.hasWhammyBar) { + const whammy: ScoreWhammyBarGlyph = new ScoreWhammyBarGlyph(this.container as ScoreBeatContainerGlyph); + this._whammy = whammy; + whammy.renderer = this.renderer; + whammy.doLayout(); + this.container.addTie(whammy); + } + // + // Note dots + if (this.container.beat.dots > 0) { + for (let i: number = 0; i < this.container.beat.dots; i++) { + 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 + ); } + this.addEffect(group); + } + } + + if (this.renderer.bar.isMultiVoice) { + let highestNotePosition = 0; + let lowestNotePosition = 0; + const direction = sr.getBeatDirection(this.container.beat); + + let offset = 0; + if (this.container.beat.hasTuplet) { + offset += sr.tupletOffset + sr.tupletSize; + } + + if (direction === BeamDirection.Up) { + highestNotePosition = this.getHighestNoteY(NoteYPosition.TopWithStem) - offset; + lowestNotePosition = this.getLowestNoteY(NoteYPosition.Bottom); } else { - let line = Math.ceil((this.renderer.bar.staff.standardNotationLineCount - 1) / 2) * 2; - - // this positioning is quite strange, for most staff line counts - // the whole/rest are aligned as half below the whole rest. - // but for staff line count 1 and 3 they are aligned centered on the same line. - if ( - this.container.beat.duration === Duration.Whole && - this.renderer.bar.staff.standardNotationLineCount !== 1 && - this.renderer.bar.staff.standardNotationLineCount !== 3 - ) { - line -= 2; - } + highestNotePosition = this.getHighestNoteY(NoteYPosition.Top); + lowestNotePosition = this.getLowestNoteY(NoteYPosition.BottomWithStem) + offset; + } - const restGlyph = new ScoreRestGlyph(0, sr.getScoreY(line), this.container.beat.duration); - this.restGlyph = restGlyph; - restGlyph.beat = this.container.beat; - restGlyph.beamingHelper = this.beamingHelper; - this.addNormal(restGlyph); - - if (this.renderer.bar.isMultiVoice) { - if (this.container.beat.voice.index === 0) { - const restSizes = BeamingHelper.computeLineHeightsForRest(this.container.beat.duration); - const restTop = restGlyph.y - sr.getScoreHeight(restSizes[0]); - const restBottom = restGlyph.y + sr.getScoreHeight(restSizes[1]); - this.renderer.helpers.collisionHelper.reserveBeatSlot(this.container.beat, restTop, restBottom); - } else { - this.renderer.helpers.collisionHelper.registerRest(this.container.beat); - } - } + this.renderer.collisionHelper.reserveBeatSlot(this.container.beat, highestNotePosition, lowestNotePosition); + } + } - if (this.beamingHelper) { - this.beamingHelper.applyRest(this.container.beat, line); - } + private _createRestGlyphs() { + const sr = this.renderer as ScoreBarRenderer; - // - // Note dots - // - if (this.container.beat.dots > 0) { - for (let i: number = 0; i < this.container.beat.dots; i++) { - const group: GlyphGroup = new GlyphGroup(0, 0); - group.renderer = this.renderer; - this._createBeatDot(line, group); - this.addEffect(group); - } - } + let steps = Math.ceil((this.renderer.bar.staff.standardNotationLineCount - 1) / 2) * 2; + + // this positioning is quite strange, for most staff line counts + // the whole/rest are aligned as half below the whole rest. + // but for staff line count 1 and 3 they are aligned centered on the same line. + if ( + this.container.beat.duration === Duration.Whole && + this.renderer.bar.staff.standardNotationLineCount !== 1 && + this.renderer.bar.staff.standardNotationLineCount !== 3 + ) { + steps -= 2; + } + + const restGlyph = new ScoreRestGlyph(0, sr.getScoreY(steps), this.container.beat.duration); + this.restGlyph = restGlyph; + restGlyph.beat = this.container.beat; + this.addNormal(restGlyph); + + if (this.renderer.bar.isMultiVoice) { + if (this.container.beat.voice.index === 0) { + const restSizes = BeamingHelper.computeLineHeightsForRest(this.container.beat.duration); + const restTop = restGlyph.y - sr.getScoreHeight(restSizes[0]); + const restBottom = restGlyph.y + sr.getScoreHeight(restSizes[1]); + this.renderer.collisionHelper.reserveBeatSlot(this.container.beat, restTop, restBottom); + } else { + this.renderer.collisionHelper.registerRest(this.container.beat); } } - super.doLayout(); - if (this.container.beat.isEmpty) { - this.centerX = this.width / 2; - } else if (this.restGlyph) { - this.centerX = this.restGlyph!.x + this.restGlyph!.width / 2; - } else if (this.noteHeads) { - this.centerX = this.noteHeads!.x + this.noteHeads!.width / 2; + + // + // Note dots + // + if (this.container.beat.dots > 0) { + for (let i: number = 0; i < this.container.beat.dots; i++) { + const group: GlyphGroup = new GlyphGroup(0, 0); + group.renderer = this.renderer; + this._createBeatDot(steps, group); + this.addEffect(group); + } } } @@ -222,7 +349,7 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { return g; } - private _createNoteHeadGlyph(n: Note): MusicFontGlyph { + private _createNoteHeadGlyph(n: Note): NoteHeadGlyphBase { const isGrace: boolean = this.container.beat.graceType !== GraceType.None; const style = n.style; @@ -247,10 +374,6 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { Logger.warning('Rendering', `No articulation found for percussion instrument ${n.percussionArticulation}`); } - if (n.beat.slashed) { - return new SlashNoteHeadGlyph(0, 0, n.beat.duration, isGrace, n.beat); - } - if (n.isDead) { return new DeadNoteHeadGlyph(0, 0, isGrace); } @@ -277,15 +400,10 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { ); // calculate y position - let line: number; - if (n.beat.slashed) { - line = (sr.heightLineCount - 1); - } else { - line = sr.getNoteLine(n); - } + let steps = sr.getNoteSteps(n); - noteHeadGlyph.y = sr.getScoreY(line); - this.noteHeads!.addMainNoteGlyph(noteHeadGlyph, n, line); + noteHeadGlyph.y = sr.getScoreY(steps); + this.noteHeads!.addMainNoteGlyph(noteHeadGlyph, n, steps); if (!n.beat.slashed && n.harmonicType !== HarmonicType.None && n.harmonicType !== HarmonicType.Natural) { // create harmonic note head. @@ -297,15 +415,15 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { this.container.beat.graceType !== GraceType.None ); harmonicsGlyph.colorOverride = noteHeadGlyph.colorOverride; - line = sr.accidentalHelper.getNoteLineForValue(harmonicFret, false); - harmonicsGlyph.y = sr.getScoreY(line); - this.noteHeads!.addEffectNoteGlyph(harmonicsGlyph, line); + steps = sr.accidentalHelper.getNoteStepsForValue(harmonicFret, false); + harmonicsGlyph.y = sr.getScoreY(steps); + this.noteHeads!.addEffectNoteGlyph(harmonicsGlyph, steps); } const belowBeatEffects = this.noteHeads!.belowBeatEffects; const aboveBeatEffects = this.noteHeads!.aboveBeatEffects; const outsideBeatEffects: Map = - this.beamingHelper.direction === BeamDirection.Up + sr.getBeatDirection(this.container.beat) === BeamDirection.Up ? this.noteHeads!.belowBeatEffects : this.noteHeads!.aboveBeatEffects; diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts index 22e3db7b5..79eef5d88 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts @@ -1,23 +1,23 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; +import { BeatSubElement } from '@coderline/alphatab/model/Beat'; import { BendType } from '@coderline/alphatab/model/BendType'; import { BrushType } from '@coderline/alphatab/model/BrushType'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; +import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { WhammyType } from '@coderline/alphatab/model/WhammyType'; import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; import { AccidentalGroupGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGroupGlyph'; import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBase'; import { BendNoteHeadGroupGlyph } from '@coderline/alphatab/rendering/glyphs/BendNoteHeadGroupGlyph'; +import { FingeringGroupGlyph } from '@coderline/alphatab/rendering/glyphs/FingeringGroupGlyph'; import { GhostNoteContainerGlyph } from '@coderline/alphatab/rendering/glyphs/GhostNoteContainerGlyph'; import { ScoreBrushGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBrushGlyph'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { FingeringGroupGlyph } from '@coderline/alphatab/rendering/glyphs/FingeringGroupGlyph'; -import { BeatSubElement } from '@coderline/alphatab/model/Beat'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { SlideInType } from '@coderline/alphatab/model/SlideInType'; /** * @internal @@ -25,7 +25,7 @@ import { SlideInType } from '@coderline/alphatab/model/SlideInType'; export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { private _prebends: BendNoteHeadGroupGlyph | null = null; public get prebendNoteHeadOffset(): number { - return this._prebends ? this._prebends.x + this._prebends.noteHeadOffset : 0; + return this._prebends ? this._prebends.x + this._prebends.onTimeX : 0; } protected override get effectElement() { @@ -34,162 +34,174 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { public accidentals: AccidentalGroupGlyph | null = null; + public doMultiVoiceLayout() { + this._prebends?.doMultiVoiceLayout(); + } + public override doLayout(): void { if (!this.container.beat.isRest) { - const accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); - accidentals.renderer = this.renderer; - - const fingering: FingeringGroupGlyph = new FingeringGroupGlyph(); - fingering.renderer = this.renderer; - - const ghost: GhostNoteContainerGlyph = new GhostNoteContainerGlyph(true); - ghost.renderer = this.renderer; - - const preBends = new BendNoteHeadGroupGlyph(this.container.beat, true); - this._prebends = preBends; - preBends.renderer = this.renderer; - - let hasSimpleSlideIn = false; + this._createGlyphs(); + } + super.doLayout(); + } - for (const note of this.container.beat.notes) { - const color = ElementStyleHelper.noteColor( - this.renderer.resources, - NoteSubElement.StandardNotationEffects, - note - ); - if (note.isVisible) { - if (note.hasBend) { - switch (note.bendType) { - case BendType.PrebendBend: - case BendType.Prebend: - case BendType.PrebendRelease: - preBends.addGlyph( - note.displayValue - ((note.bendPoints![0].value / 2) | 0), - false, - color - ); - break; - } - } else if (note.beat.hasWhammyBar) { - switch (note.beat.whammyBarType) { - case WhammyType.PrediveDive: - case WhammyType.Predive: - this._prebends.addGlyph( - note.displayValue - ((note.beat.whammyBarPoints![0].value / 2) | 0), - false, - color - ); - break; - } + private _createGlyphs() { + const accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); + accidentals.renderer = this.renderer; + + const fingering: FingeringGroupGlyph = new FingeringGroupGlyph(); + fingering.renderer = this.renderer; + + const ghost: GhostNoteContainerGlyph = new GhostNoteContainerGlyph(true); + ghost.renderer = this.renderer; + + let preBends: BendNoteHeadGroupGlyph | null = null; + + + let hasSimpleSlideIn = false; + + for (const note of this.container.beat.notes) { + const color = ElementStyleHelper.noteColor( + this.renderer.resources, + NoteSubElement.StandardNotationEffects, + note + ); + if (note.isVisible) { + if (note.hasBend) { + switch (note.bendType) { + case BendType.PrebendBend: + case BendType.Prebend: + case BendType.PrebendRelease: + if (!preBends) { + preBends = new BendNoteHeadGroupGlyph('prebend', this.container.beat, true); + preBends.renderer = this.renderer; + } + preBends.addGlyph(note.displayValue - ((note.bendPoints![0].value / 2) | 0), false, color); + break; } - this._createAccidentalGlyph(note, accidentals); - ghost.addParenthesis(note); - fingering.addFingers(note); - - switch (note.slideInType) { - case SlideInType.IntoFromBelow: - case SlideInType.IntoFromAbove: - hasSimpleSlideIn = true; + } else if (note.beat.hasWhammyBar) { + switch (note.beat.whammyBarType) { + case WhammyType.PrediveDive: + case WhammyType.Predive: + if (!preBends) { + preBends = new BendNoteHeadGroupGlyph('prebend', this.container.beat, true); + preBends.renderer = this.renderer; + } + preBends.addGlyph( + note.displayValue - ((note.beat.whammyBarPoints![0].value / 2) | 0), + false, + color + ); break; } } - } - - if (hasSimpleSlideIn) { - this.addNormal( - new SpacingGlyph( - 0, - 0, - this.renderer.smuflMetrics.simpleSlideWidth * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) - ) - ); - } - - if (!preBends.isEmpty) { - this.addEffect(preBends); - this.addNormal( - new SpacingGlyph( - 0, - 0, - this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) - ) - ); - } - if (this.container.beat.brushType !== BrushType.None) { - this.addEffect(new ScoreBrushGlyph(this.container.beat)); - this.addNormal( - new SpacingGlyph( - 0, - 0, - this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) - ) - ); - } - if (!fingering.isEmpty) { - if (!this.isEmpty) { - this.addNormal( - new SpacingGlyph( - 0, - 0, - this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) - ) - ); + this._createAccidentalGlyph(note, accidentals); + ghost.addParenthesis(note); + fingering.addFingers(note); + + switch (note.slideInType) { + case SlideInType.IntoFromBelow: + case SlideInType.IntoFromAbove: + hasSimpleSlideIn = true; + break; } + } + } + + if (hasSimpleSlideIn) { + this.addNormal( + new SpacingGlyph( + 0, + 0, + this.renderer.smuflMetrics.simpleSlideWidth * + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) + ) + ); + } - this.addEffect(fingering); + this._prebends = preBends; + if (preBends) { + this.addEffect(preBends); + this.addNormal( + new SpacingGlyph( + 0, + 0, + this.renderer.smuflMetrics.preNoteEffectPadding * + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) + ) + ); + } + if (this.container.beat.brushType !== BrushType.None) { + this.addEffect(new ScoreBrushGlyph(this.container.beat)); + this.addNormal( + new SpacingGlyph( + 0, + 0, + this.renderer.smuflMetrics.preNoteEffectPadding * + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) + ) + ); + } + if (!fingering.isEmpty) { + if (!this.isEmpty) { this.addNormal( new SpacingGlyph( 0, 0, this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) ) ); } - if (!ghost.isEmpty) { - this.addEffect(ghost); - } - if (!accidentals.isEmpty) { - this.accidentals = accidentals; - if (!this.isEmpty) { - this.addNormal( - new SpacingGlyph( - 0, - 0, - this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) - ) - ); - } + this.addEffect(fingering); + this.addNormal( + new SpacingGlyph( + 0, + 0, + this.renderer.smuflMetrics.preNoteEffectPadding * + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) + ) + ); + } - this.addNormal(accidentals); + if (!ghost.isEmpty) { + this.addEffect(ghost); + } + if (!accidentals.isEmpty) { + this.accidentals = accidentals; + if (!this.isEmpty) { this.addNormal( new SpacingGlyph( 0, 0, this.renderer.smuflMetrics.preNoteEffectPadding * - (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) ) ); } + + this.addNormal(accidentals); + this.addNormal( + new SpacingGlyph( + 0, + 0, + this.renderer.smuflMetrics.preNoteEffectPadding * + (this.container.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1) + ) + ); } - super.doLayout(); } private _createAccidentalGlyph(n: Note, accidentals: AccidentalGroupGlyph): void { const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; let accidental: AccidentalType = sr.accidentalHelper.applyAccidental(n); - let noteLine: number = sr.getNoteLine(n); + let noteSteps: number = sr.getNoteSteps(n); const isGrace: boolean = this.container.beat.graceType !== GraceType.None; const color = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.StandardNotationAccidentals, n); - const graceScale = isGrace ? NoteHeadGlyph.GraceScale : 1; + const graceScale = isGrace ? EngravingSettings.GraceScale : 1; if (accidental !== AccidentalType.None) { - const g = new AccidentalGlyph(0, sr.getScoreY(noteLine), accidental, graceScale); + const g = new AccidentalGlyph(0, sr.getScoreY(noteSteps), accidental, graceScale); g.colorOverride = color; g.renderer = this.renderer; accidentals.addGlyph(g); @@ -197,8 +209,8 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { if (n.harmonicType !== HarmonicType.None && n.harmonicType !== HarmonicType.Natural) { const harmonicFret: number = n.displayValue + n.harmonicPitch; accidental = sr.accidentalHelper.applyAccidentalForValue(n.beat, harmonicFret, isGrace, false); - noteLine = sr.accidentalHelper.getNoteLineForValue(harmonicFret, false); - const g = new AccidentalGlyph(0, sr.getScoreY(noteLine), accidental, graceScale); + noteSteps = sr.accidentalHelper.getNoteStepsForValue(harmonicFret, false); + const g = new AccidentalGlyph(0, sr.getScoreY(noteSteps), accidental, graceScale); g.colorOverride = color; g.renderer = this.renderer; accidentals.addGlyph(g); diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts index 52a9cd431..2fcb6d874 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts @@ -1,34 +1,130 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import type { Beat } from '@coderline/alphatab/model/Beat'; import type { BendPoint } from '@coderline/alphatab/model/BendPoint'; import { BendStyle } from '@coderline/alphatab/model/BendStyle'; import { BendType } from '@coderline/alphatab/model/BendType'; import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { BendNoteHeadGroupGlyph } from '@coderline/alphatab/rendering/glyphs/BendNoteHeadGroupGlyph'; -import type { ScoreBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatPreNotesGlyph'; import { ScoreHelperNotesBaseGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreHelperNotesBaseGlyph'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import { type ITieGlyph, TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; +import type { ScoreBeatContainerGlyph } from '@coderline/alphatab/rendering/ScoreBeatContainerGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; /** * @internal */ -export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { +export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGlyph { private _beat: Beat; private _notes: Note[] = []; private _endNoteGlyph: BendNoteHeadGroupGlyph | null = null; private _middleNoteGlyph: BendNoteHeadGroupGlyph | null = null; + private _container: ScoreBeatContainerGlyph; + + public readonly checkForOverflow = false; // handled separately in ScoreBeatContainerGlyph - public constructor(beat: Beat) { + public constructor(container: ScoreBeatContainerGlyph) { super(0, 0); - this._beat = beat; + this._beat = container.beat; + this._container = container; + } + + public override doLayout(): void { + super.doLayout(); + this.width = 0; + } + + public override getBoundingBoxTop(): number { + return super.getBoundingBoxTop() - this._calculateMaxSlurHeight(BeamDirection.Up); + } + + public override getBoundingBoxBottom(): number { + return super.getBoundingBoxBottom() + this._calculateMaxSlurHeight(BeamDirection.Down); + } + + public doMultiVoiceLayout(): void { + this._middleNoteGlyph?.doMultiVoiceLayout(); + this._endNoteGlyph?.doMultiVoiceLayout(); + } + + private _calculateMaxSlurHeight(expectedDirection: BeamDirection) { + const direction = this.getTieDirection(this._beat, this.renderer as ScoreBarRenderer); + if (direction !== expectedDirection) { + return 0; + } + + let maxSlurHeight = 0; + + // this logic is similar to the actual drawing but more lightweight, + // until we rework how we handle ties this is a good estimate + for (const note of this._notes) { + if (note.isTieOrigin) { + continue; + } + + // no helper notes created in addbends for these: + switch (note.bendType) { + case BendType.Custom: + case BendType.Prebend: + case BendType.Hold: + continue; + } + + // at this point in time the beats have not been timely-positioned yet, + // hence we cannot rely on their actual position, we can only estimate the size here + const parent = this.renderer.getBeatContainer(this._beat)!; + const width: number = parent.width * 2; + + let endY: number = 0; + let endX: number = 0; + switch (note.bendType) { + case BendType.Bend: + case BendType.PrebendBend: + endY = this._endNoteGlyph!.minStepsNote!.glyph.getBoundingBoxTop(); + endX = width; + break; + case BendType.BendRelease: + endY = this._middleNoteGlyph!.minStepsNote!.glyph.getBoundingBoxTop(); + endX = width / 2; + break; + case BendType.Release: + case BendType.PrebendRelease: + endY = this._endNoteGlyph!.maxStepsNote!.glyph.getBoundingBoxTop(); + endX = width; + break; + } + const startY = this.renderer.getNoteY(note, NoteYPosition.Top); + let slurHeight = Math.abs( + TieGlyph.calculateBendSlurTopY( + 0, + startY, + endX, + endY, + direction === BeamDirection.Down, + 1, + this.renderer.smuflMetrics.tieHeight + ) - endY + ); + + if (note.bendStyle === BendStyle.Gradual) { + const res = this.renderer.resources; + const c = this.renderer.scoreRenderer.canvas!; + c.font = res.elementFonts.get(NotationElement.ScoreBendSlur)!; + slurHeight += c.measureText('grad.').height; + } + + if (slurHeight > maxSlurHeight) { + maxSlurHeight = slurHeight; + } + } + return maxSlurHeight; } public addBends(note: Note): void { @@ -50,10 +146,10 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { { let endGlyphs = this._endNoteGlyph; if (!endGlyphs) { - endGlyphs = new BendNoteHeadGroupGlyph(note.beat, false); + endGlyphs = new BendNoteHeadGroupGlyph('postbend', note.beat, false); endGlyphs.renderer = this.renderer; this._endNoteGlyph = endGlyphs; - this.bendNoteHeads.push(endGlyphs); + this.addGlyph(endGlyphs); } const lastBendPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; endGlyphs.addGlyph( @@ -68,10 +164,10 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { if (!note.isTieOrigin) { let endGlyphs = this._endNoteGlyph; if (!endGlyphs) { - endGlyphs = new BendNoteHeadGroupGlyph(note.beat, false); + endGlyphs = new BendNoteHeadGroupGlyph('postbend', note.beat, false); endGlyphs.renderer = this.renderer; this._endNoteGlyph = endGlyphs; - this.bendNoteHeads.push(endGlyphs); + this.addGlyph(endGlyphs); } const lastBendPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; endGlyphs.addGlyph( @@ -86,10 +182,10 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { { let middleGlyphs = this._middleNoteGlyph; if (!middleGlyphs) { - middleGlyphs = new BendNoteHeadGroupGlyph(note.beat, false); + middleGlyphs = new BendNoteHeadGroupGlyph('middlebend', note.beat, false); this._middleNoteGlyph = middleGlyphs; middleGlyphs.renderer = this.renderer; - this.bendNoteHeads.push(middleGlyphs); + this.addGlyph(middleGlyphs); } const middleBendPoint: BendPoint = note.bendPoints![1]; middleGlyphs.addGlyph( @@ -99,10 +195,10 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { ); let endGlyphs = this._endNoteGlyph; if (!endGlyphs) { - endGlyphs = new BendNoteHeadGroupGlyph(note.beat, false); + endGlyphs = new BendNoteHeadGroupGlyph('postbend', note.beat, false); endGlyphs.renderer = this.renderer; this._endNoteGlyph = endGlyphs; - this.bendNoteHeads.push(endGlyphs); + this.addGlyph(endGlyphs); } const lastBendPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; endGlyphs.addGlyph( @@ -118,43 +214,59 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { public override paint(cx: number, cy: number, canvas: ICanvas): void { // Draw note heads const startNoteRenderer: ScoreBarRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, this._beat.voice.bar )! as ScoreBarRenderer; const startX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.MiddleNotes); let endBeatX: number = cx + startNoteRenderer.x; if (this._beat.isLastOfVoice) { - endBeatX += startNoteRenderer.postBeatGlyphsStart; + endBeatX += startNoteRenderer.getBeatX(this._beat!, BeatXPosition.EndBeat); } else { endBeatX += startNoteRenderer.getBeatX(this._beat.nextBeat!, BeatXPosition.PreNotes); } + endBeatX -= this.renderer.smuflMetrics.postNoteEffectPadding; if (this._endNoteGlyph) { - endBeatX -= this._endNoteGlyph.upLineX; + const postBeatSize = this._endNoteGlyph.width - this._endNoteGlyph.onTimeX; + endBeatX -= postBeatSize; } const middleX: number = (startX + endBeatX) / 2; if (this._middleNoteGlyph) { - this._middleNoteGlyph.x = middleX - this._middleNoteGlyph.noteHeadOffset; + this._middleNoteGlyph.x = middleX - this._middleNoteGlyph.onTimeX; this._middleNoteGlyph.y = cy + startNoteRenderer.y; this._middleNoteGlyph.paint(0, 0, canvas); } if (this._endNoteGlyph) { - this._endNoteGlyph.x = endBeatX - this._endNoteGlyph.noteHeadOffset; + this._endNoteGlyph.x = endBeatX - this._endNoteGlyph.onTimeX; this._endNoteGlyph.y = cy + startNoteRenderer.y; this._endNoteGlyph.paint(0, 0, canvas); } this._notes.sort((a, b) => { return b.displayValue - a.displayValue; }); + + // draw slurs + if (this.renderer.settings.notation.isNotationElementVisible(NotationElement.ScoreBendSlur)) { + this._paintSlurs(cx, cy, canvas, startNoteRenderer, startX, middleX, endBeatX); + } + } + + private _paintSlurs( + cx: number, + cy: number, + canvas: ICanvas, + startNoteRenderer: ScoreBarRenderer, + startX: number, + middleX: number, + endBeatX: number + ) { const directionBeat: Beat = this._beat.graceType === GraceType.BendGrace ? this._beat.nextBeat! : this._beat; let direction: BeamDirection = this._notes.length === 1 ? this.getTieDirection(directionBeat, startNoteRenderer) : BeamDirection.Up; - const noteHeadHeight = this.renderer.smuflMetrics.glyphHeights.get(MusicFontSymbol.NoteheadBlack)!; - - // draw slurs + canvas.font = this.renderer.resources.elementFonts.get(NotationElement.ScoreBendSlur)!; for (let i: number = 0; i < this._notes.length; i++) { const note: Note = this._notes[i]; using _ = ElementStyleHelper.note(canvas, NoteSubElement.StandardNotationEffects, note); @@ -162,7 +274,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { direction = BeamDirection.Down; } let startY: number = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(note, NoteYPosition.Top); - let heightOffset: number = noteHeadHeight * NoteHeadGlyph.GraceScale * 0.5; + let heightOffset: number = noteHeadHeight * EngravingSettings.GraceScale * 0.5; if (direction === BeamDirection.Down) { startY += noteHeadHeight; } @@ -172,19 +284,19 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { const endNoteRenderer: ScoreBarRenderer | null = !endNote ? null : (this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, endNote.beat.voice.bar ) as ScoreBarRenderer); // if we have a line break we draw only a line until the end if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { const endX: number = cx + startNoteRenderer.x + startNoteRenderer.width; - const noteValueToDraw: number = note.tieDestination!.realValue; + const noteValueToDraw: number = note.tieDestination!.displayValue; startNoteRenderer.accidentalHelper.applyAccidentalForValue(note.beat, noteValueToDraw, false, true); const endY: number = cy + startNoteRenderer.y + startNoteRenderer.getScoreY( - startNoteRenderer.accidentalHelper.getNoteLineForValue(noteValueToDraw, false) + startNoteRenderer.accidentalHelper.getNoteStepsForValue(noteValueToDraw, false) ); if (note.bendType === BendType.Hold || note.bendType === BendType.Prebend) { TieGlyph.paintTie( @@ -206,7 +318,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { endX, endY, direction === BeamDirection.Down, - 1, slurText ); } @@ -237,7 +348,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { endX, endY, direction === BeamDirection.Down, - 1, slurText ); } @@ -248,19 +358,18 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { case BendType.PrebendRelease: let preX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(note.beat, BeatXPosition.PreNotes); - preX += (startNoteRenderer.getPreNotesGlyphForBeat(note.beat) as ScoreBeatPreNotesGlyph) - .prebendNoteHeadOffset; + preX += this._container.prebendNoteHeadOffset; const preY: number = cy + startNoteRenderer.y + startNoteRenderer.getScoreY( - startNoteRenderer.accidentalHelper.getNoteLineForValue( + startNoteRenderer.accidentalHelper.getNoteStepsForValue( note.displayValue - ((note.bendPoints![0].value / 2) | 0), false ) ) + heightOffset; - this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down, 1); + this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down); break; } } else { @@ -280,7 +389,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { endBeatX, endY, direction === BeamDirection.Down, - 1, slurText ); break; @@ -294,7 +402,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { middleX, middleY, direction === BeamDirection.Down, - 1, slurText ); endValue = this._getBendNoteValue(note, note.bendPoints![note.bendPoints!.length - 1]); @@ -306,14 +413,13 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { endBeatX, endY, direction === BeamDirection.Down, - 1, slurText ); break; case BendType.Release: - if (this.bendNoteHeads.length > 0) { + if (this.glyphs) { endValue = this._getBendNoteValue(note, note.bendPoints![note.bendPoints!.length - 1]); - endY = this.bendNoteHeads[0].getNoteValueY(endValue) + heightOffset; + endY = (this.glyphs[0] as BendNoteHeadGroupGlyph).getNoteValueY(endValue) + heightOffset; this.drawBendSlur( canvas, startX, @@ -321,7 +427,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { endBeatX, endY, direction === BeamDirection.Down, - 1, slurText ); } @@ -331,22 +436,21 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { case BendType.PrebendRelease: let preX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(note.beat, BeatXPosition.PreNotes); - preX += (startNoteRenderer.getPreNotesGlyphForBeat(note.beat) as ScoreBeatPreNotesGlyph) - .prebendNoteHeadOffset; + preX += this._container.prebendNoteHeadOffset; const preY: number = cy + startNoteRenderer.y + startNoteRenderer.getScoreY( - startNoteRenderer.accidentalHelper.getNoteLineForValue( + startNoteRenderer.accidentalHelper.getNoteStepsForValue( note.displayValue - ((note.bendPoints![0].value / 2) | 0), false ) ) + heightOffset; - this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down, 1); - if (this.bendNoteHeads.length > 0) { + this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down); + if (this.glyphs) { endValue = this._getBendNoteValue(note, note.bendPoints![note.bendPoints!.length - 1]); - endY = this.bendNoteHeads[0].getNoteValueY(endValue) + heightOffset; + endY = (this.glyphs[0] as BendNoteHeadGroupGlyph).getNoteValueY(endValue) + heightOffset; this.drawBendSlur( canvas, startX, @@ -354,7 +458,6 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { endBeatX, endY, direction === BeamDirection.Down, - 1, slurText ); } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts index f044a70a8..1baeda37d 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts @@ -1,17 +1,14 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { BendNoteHeadGroupGlyph } from '@coderline/alphatab/rendering/glyphs/BendNoteHeadGroupGlyph'; -import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import { GlyphGroup } from '@coderline/alphatab/rendering/glyphs/GlyphGroup'; +import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; /** * @internal */ -export class ScoreHelperNotesBaseGlyph extends Glyph { - protected bendNoteHeads: BendNoteHeadGroupGlyph[] = []; - +export class ScoreHelperNotesBaseGlyph extends GlyphGroup { protected drawBendSlur( canvas: ICanvas, x1: number, @@ -19,16 +16,18 @@ export class ScoreHelperNotesBaseGlyph extends Glyph { x2: number, y2: number, down: boolean, - scale: number, slurText?: string ): void { - TieGlyph.drawBendSlur(canvas, x1, y1, x2, y2, down, scale, this.renderer.smuflMetrics.tieHeight, slurText); + TieGlyph.drawBendSlur(canvas, x1, y1, x2, y2, down, this.renderer.smuflMetrics.tieHeight, slurText); } public override doLayout(): void { - super.doLayout(); + if (!this.glyphs) { + return; + } + this.width = 0; - for (const noteHeads of this.bendNoteHeads) { + for (const noteHeads of this.glyphs) { noteHeads.doLayout(); this.width += noteHeads.width; } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts index 00c206b87..a80a5ce0e 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreLegatoGlyph.ts @@ -1,30 +1,61 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; -import { type BarRendererBase, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; -import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; +import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; /** * @internal */ export class ScoreLegatoGlyph extends TieGlyph { - public constructor(startBeat: Beat, endBeat: Beat, forEnd: boolean = false) { - super(startBeat, endBeat, forEnd); + protected startBeat: Beat; + protected endBeat: Beat; + protected startBeatRenderer: LineBarRenderer | null = null; + protected endBeatRenderer: LineBarRenderer | null = null; + + public constructor(slurEffectId: string, startBeat: Beat, endBeat: Beat, forEnd: boolean) { + super(slurEffectId, forEnd); + this.startBeat = startBeat; + this.endBeat = endBeat; } public override doLayout(): void { super.doLayout(); } - protected override getBeamDirection(beat: Beat, noteRenderer: BarRendererBase): BeamDirection { - if (beat.isRest) { + protected override lookupStartBeatRenderer(): LineBarRenderer { + if (!this.startBeatRenderer) { + this.startBeatRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff!.staffId, + this.startBeat.voice.bar + )! as LineBarRenderer; + } + return this.startBeatRenderer; + } + + protected override lookupEndBeatRenderer(): LineBarRenderer | null { + if (!this.endBeatRenderer) { + this.endBeatRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff!.staffId, + this.endBeat.voice.bar + ) as LineBarRenderer | null; + } + return this.endBeatRenderer; + } + + protected override shouldDrawBendSlur(): boolean { + return false; + } + + protected override calculateTieDirection(): BeamDirection { + if (this.startBeat.isRest) { return BeamDirection.Up; } // invert direction (if stems go up, ties go down to not cross them) - switch ((noteRenderer as ScoreBarRenderer).getBeatDirection(beat)) { + switch (this.lookupStartBeatRenderer().getBeatDirection(this.startBeat)) { case BeamDirection.Up: return BeamDirection.Down; default: @@ -32,76 +63,105 @@ export class ScoreLegatoGlyph extends TieGlyph { } } - protected override getStartY(): number { + protected override calculateStartX(): number { + const startBeatRenderer = this.lookupStartBeatRenderer(); + return startBeatRenderer.x + startBeatRenderer.getBeatX(this.startBeat!, BeatXPosition.MiddleNotes); + } + + protected override calculateStartY(): number { + const startBeatRenderer = this.lookupStartBeatRenderer(); if (this.startBeat!.isRest) { - // below all lines - return (this.startNoteRenderer as ScoreBarRenderer).getScoreY(9); + switch (this.tieDirection) { + case BeamDirection.Up: + return startBeatRenderer.y + startBeatRenderer.getRestY(this.startBeat, NoteYPosition.Top); + default: + return startBeatRenderer.y + startBeatRenderer.getRestY(this.startBeat, NoteYPosition.Bottom); + } } + switch (this.tieDirection) { case BeamDirection.Up: // below lowest note - return this.startNoteRenderer!.getNoteY(this.startBeat!.maxNote!, NoteYPosition.Top); + return startBeatRenderer.y + startBeatRenderer.getNoteY(this.startBeat!.maxNote!, NoteYPosition.Top); default: - return this.startNoteRenderer!.getNoteY(this.startBeat!.minNote!, NoteYPosition.Bottom); + return startBeatRenderer.y + startBeatRenderer.getNoteY(this.startBeat!.minNote!, NoteYPosition.Bottom); } } - protected override getEndY(): number { - const endNoteScoreRenderer = this.endNoteRenderer as ScoreBarRenderer; - if (this.endBeat!.isRest) { + protected override calculateEndX(): number { + const endBeatRenderer = this.lookupEndBeatRenderer(); + if (!endBeatRenderer) { + return this.calculateStartX() + this.renderer.smuflMetrics.leftHandTabTieWidth; + } + const endBeamDirection = endBeatRenderer.getBeatDirection(this.endBeat); + return ( + endBeatRenderer.x + + endBeatRenderer.getBeatX( + this.endBeat, + this.endBeat.duration > Duration.Whole && endBeamDirection === this.tieDirection + ? BeatXPosition.Stem + : BeatXPosition.MiddleNotes + ) + ); + } + + protected override caclculateEndY(): number { + const endBeatRenderer = this.lookupEndBeatRenderer(); + if (!endBeatRenderer) { + return this.calculateStartY(); + } + + if (this.endBeat.isRest) { switch (this.tieDirection) { case BeamDirection.Up: - return endNoteScoreRenderer.getScoreY(9); + return endBeatRenderer.y + endBeatRenderer.getRestY(this.endBeat, NoteYPosition.Top); default: - return endNoteScoreRenderer.getScoreY(0); + return endBeatRenderer.y + endBeatRenderer.getRestY(this.endBeat, NoteYPosition.Bottom); } } - const startBeamDirection = (this.startNoteRenderer as ScoreBarRenderer).getBeatDirection(this.startBeat!); - const endBeamDirection = endNoteScoreRenderer.getBeatDirection(this.endBeat!); + const startBeamDirection = this.lookupStartBeatRenderer().getBeatDirection(this.startBeat!); + const endBeamDirection = endBeatRenderer.getBeatDirection(this.endBeat!); if (startBeamDirection !== endBeamDirection && this.startBeat!.graceType === GraceType.None) { if (endBeamDirection === this.tieDirection) { switch (this.tieDirection) { case BeamDirection.Up: // stem upper end - return endNoteScoreRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.TopWithStem); + return ( + endBeatRenderer.y + + endBeatRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.TopWithStem) + ); default: // stem lower end - return endNoteScoreRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.BottomWithStem); + return ( + endBeatRenderer.y + + endBeatRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.BottomWithStem) + ); } } switch (this.tieDirection) { case BeamDirection.Up: // stem upper end - return endNoteScoreRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.BottomWithStem); + return ( + endBeatRenderer.y + + endBeatRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.BottomWithStem) + ); default: // stem lower end - return endNoteScoreRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.TopWithStem); + return ( + endBeatRenderer.y + endBeatRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.TopWithStem) + ); } } switch (this.tieDirection) { case BeamDirection.Up: // below lowest note - return endNoteScoreRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.Top); + return endBeatRenderer.y + endBeatRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.Top); default: // above highest note - return endNoteScoreRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.Bottom); + return endBeatRenderer.y + endBeatRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.Bottom); } } - - protected override getStartX(): number { - return this.startNoteRenderer!.getBeatX(this.startBeat!, BeatXPosition.MiddleNotes); - } - - protected override getEndX(): number { - const endBeamDirection = (this.endNoteRenderer as ScoreBarRenderer).getBeatDirection(this.endBeat!); - return this.endNoteRenderer!.getBeatX( - this.endBeat!, - this.endBeat!.duration > Duration.Whole && endBeamDirection === this.tieDirection - ? BeatXPosition.Stem - : BeatXPosition.MiddleNotes - ); - } } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts index 13023bb1b..90de9f894 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts @@ -1,22 +1,25 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; import type { Note } from '@coderline/alphatab/model/Note'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; -import { ScoreNoteChordGlyphBase } from '@coderline/alphatab/rendering/glyphs/ScoreNoteChordGlyphBase'; +import type { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; +import type { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { + ScoreChordNoteHeadInfo, + ScoreNoteChordGlyphBase +} from '@coderline/alphatab/rendering/glyphs/ScoreNoteChordGlyphBase'; +import { TremoloPickingGlyph } from '@coderline/alphatab/rendering/glyphs/TremoloPickingGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; -import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; -import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; -import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; +import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import type { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { TremoloPickingGlyph } from '@coderline/alphatab/rendering/glyphs/TremoloPickingGlyph'; -import { Duration } from '@coderline/alphatab/model/Duration'; +import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; /** * @internal @@ -26,18 +29,42 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { private _notes: Note[] = []; private _deadSlapped: DeadSlappedBeatGlyph | null = null; private _tremoloPicking: TremoloPickingGlyph | null = null; + private _stemLengthExtension = 0; public aboveBeatEffects: Map = new Map(); public belowBeatEffects: Map = new Map(); public beat!: Beat; - public beamingHelper!: BeamingHelper; public get direction(): BeamDirection { - return this.beamingHelper.direction; + return (this.renderer as ScoreBarRenderer).getBeatDirection(this.beat); + } + + public override get hasFlag(): boolean { + return (this.renderer as ScoreBarRenderer).hasFlag(this.beat); + } + + public override get hasStem(): boolean { + return (this.renderer as ScoreBarRenderer).hasStem(this.beat); } public override get scale(): number { - return this.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + return this.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; + } + + protected override getScoreChordNoteHeadInfo(): ScoreChordNoteHeadInfo { + // never share grace beats + if (this.beat.graceType !== GraceType.None) { + return new ScoreChordNoteHeadInfo(this.direction); + } + + const staff = this.beat.voice.bar.staff; + const key = `score.noteheads.${staff.track.index}.${staff.index}.${this.beat.voice.bar.index}.${this.beat.absoluteDisplayStart}`; + let existing = this.renderer.staff!.getSharedLayoutData(key, undefined); + if (!existing) { + existing = new ScoreChordNoteHeadInfo(this.direction); + this.renderer.staff!.setSharedLayoutData(key, existing); + } + return existing; } public getNoteX(note: Note, requestedPosition: NoteXPosition): number { @@ -68,22 +95,32 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { return 0; } + public getLowestNoteY(requestedPosition: NoteYPosition): number { + return this.maxStepsNote ? this._internalGetNoteY(this.maxStepsNote.glyph, requestedPosition) : 0; + } + + public getHighestNoteY(requestedPosition: NoteYPosition): number { + return this.minStepsNote ? this._internalGetNoteY(this.minStepsNote.glyph, requestedPosition) : 0; + } + private _internalGetNoteY(n: MusicFontGlyph, requestedPosition: NoteYPosition): number { let pos = this.y + n.y; - const scale = this.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1; + const sr = this.renderer as ScoreBarRenderer; + const scale = this.beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; switch (requestedPosition) { case NoteYPosition.TopWithStem: // stem start pos -= - (this.renderer.smuflMetrics.stemUp.has(n.symbol) - ? this.renderer.smuflMetrics.stemUp.get(n.symbol)!.bottomY - : 0) * scale; + (sr.smuflMetrics.stemUp.has(n.symbol) ? sr.smuflMetrics.stemUp.get(n.symbol)!.bottomY : 0) * scale; // stem size according to duration - pos -= this.renderer.smuflMetrics.standardStemLength * scale; + pos -= sr.smuflMetrics.getStemLength(this.beat.duration, sr.hasFlag(this.beat)) * scale; + pos -= this._stemLengthExtension; + + let topCenterY = sr.centerStaffStemY(this.direction); + topCenterY -= this._stemLengthExtension; - const topCenterY = (this.renderer as ScoreBarRenderer).centerStaffStemY(this.beamingHelper); return Math.min(topCenterY, pos); case NoteYPosition.Top: @@ -101,58 +138,49 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { : -this.renderer.smuflMetrics.glyphHeights.get(n.symbol)! / 2) * scale; // stem size according to duration - pos += this.renderer.smuflMetrics.standardStemLength * scale; + pos += sr.smuflMetrics.getStemLength(this.beat.duration, sr.hasFlag(this.beat)) * scale; + pos += this._stemLengthExtension; + + let bottomCenterY = sr.centerStaffStemY(this.direction); + bottomCenterY += this._stemLengthExtension; - const bottomCenterY = (this.renderer as ScoreBarRenderer).centerStaffStemY(this.beamingHelper); return Math.max(bottomCenterY, pos); case NoteYPosition.StemUp: pos -= - (this.renderer.smuflMetrics.stemUp.has(n.symbol) - ? this.renderer.smuflMetrics.stemUp.get(n.symbol)!.bottomY - : 0) * scale; + (sr.smuflMetrics.stemUp.has(n.symbol) ? sr.smuflMetrics.stemUp.get(n.symbol)!.bottomY : 0) * scale; break; case NoteYPosition.StemDown: pos -= - (this.renderer.smuflMetrics.stemDown.has(n.symbol) - ? this.renderer.smuflMetrics.stemDown.get(n.symbol)!.topY - : -this.renderer.smuflMetrics.glyphHeights.get(n.symbol)! / 2) * scale; + (sr.smuflMetrics.stemDown.has(n.symbol) + ? sr.smuflMetrics.stemDown.get(n.symbol)!.topY + : -sr.smuflMetrics.glyphHeights.get(n.symbol)! / 2) * scale; break; } return pos; } - public addMainNoteGlyph(noteGlyph: MusicFontGlyph, note: Note, noteLine: number): void { + public addMainNoteGlyph(noteGlyph: NoteHeadGlyphBase, note: Note, noteLine: number): void { super.add(noteGlyph, noteLine); this._noteGlyphLookup.set(note.id, noteGlyph); this._notes.push(note); } - public addEffectNoteGlyph(noteGlyph: MusicFontGlyph, noteLine: number): void { + public addEffectNoteGlyph(noteGlyph: NoteHeadGlyphBase, noteLine: number): void { super.add(noteGlyph, noteLine); } - public updateBeamingHelper(cx: number): void { - if (this.beamingHelper) { - this.beamingHelper.registerBeatLineX( - 'score', - this.beat, - cx + this.x + this.upLineX, - cx + this.x + this.downLineX - ); - } - } - public override doLayout(): void { super.doLayout(); - const scoreRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const scoreRenderer = this.renderer as ScoreBarRenderer; if (this.beat.deadSlapped) { this._deadSlapped = new DeadSlappedBeatGlyph(); this._deadSlapped.renderer = this.renderer; this._deadSlapped.doLayout(); this.width = this._deadSlapped.width; + this.onTimeX = this.width / 2; } let aboveBeatEffectsY = 0; @@ -164,13 +192,14 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { aboveBeatEffectsY = scoreRenderer.getScoreY(scoreRenderer.heightLineCount); } else { if (this.direction === BeamDirection.Up) { - belowBeatEffectsY = this._internalGetNoteY(this.maxNote!.glyph, NoteYPosition.Bottom) + effectSpacing; + belowBeatEffectsY = + this._internalGetNoteY(this.maxStepsNote!.glyph, NoteYPosition.Bottom) + effectSpacing; aboveBeatEffectsY = - this._internalGetNoteY(this.minNote!.glyph, NoteYPosition.TopWithStem) - effectSpacing; + this._internalGetNoteY(this.minStepsNote!.glyph, NoteYPosition.TopWithStem) - effectSpacing; } else { belowBeatEffectsY = - this._internalGetNoteY(this.maxNote!.glyph, NoteYPosition.BottomWithStem) + effectSpacing; - aboveBeatEffectsY = this._internalGetNoteY(this.minNote!.glyph, NoteYPosition.Top) - effectSpacing; + this._internalGetNoteY(this.maxStepsNote!.glyph, NoteYPosition.BottomWithStem) + effectSpacing; + aboveBeatEffectsY = this._internalGetNoteY(this.minStepsNote!.glyph, NoteYPosition.Top) - effectSpacing; } } @@ -215,32 +244,40 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { } if (this.beat.isTremolo && !this.beat.deadSlapped) { - const direction = this.direction; - - let tremoloY = 0; - if (direction === BeamDirection.Up) { - const topY = this._internalGetNoteY(this.minNote!.glyph, NoteYPosition.TopWithStem); - const bottomY = this._internalGetNoteY(this.minNote!.glyph, NoteYPosition.StemUp); - - tremoloY = (topY + bottomY) / 2; - } else { - const topY = this._internalGetNoteY(this.maxNote!.glyph, NoteYPosition.StemDown); - const bottomY = this._internalGetNoteY(this.maxNote!.glyph, NoteYPosition.BottomWithStem); - - tremoloY = (topY + bottomY) / 2; - } + this._tremoloPicking = new TremoloPickingGlyph(0, 0, this.beat.tremoloPicking!); + this._tremoloPicking.renderer = this.renderer; + this._tremoloPicking.doLayout(); - let tremoloX: number = direction === BeamDirection.Up ? this.upLineX : this.downLineX; - const speed: Duration = this.beat.tremoloSpeed!; + this._alignTremoloPickingGlyph(); + } + } - if (this.beat.duration < Duration.Half) { - tremoloX = this.width / 2; - } + private _alignTremoloPickingGlyph() { + const g = this._tremoloPicking!; + const direction = this.direction; + if (direction === BeamDirection.Up) { + g.alignTremoloPickingGlyph( + direction, + this.getHighestNoteY(NoteYPosition.TopWithStem), + this.getHighestNoteY(NoteYPosition.Center), + this.beat.duration + ); + } else { + g.alignTremoloPickingGlyph( + direction, + this.getLowestNoteY(NoteYPosition.BottomWithStem), + this.getLowestNoteY(NoteYPosition.Center), + this.beat.duration + ); + } + this._stemLengthExtension = g.stemExtensionHeight; - this._tremoloPicking = new TremoloPickingGlyph(tremoloX, tremoloY, speed); - this._tremoloPicking.renderer = this.renderer; - this._tremoloPicking.doLayout(); + let tremoloX: number = this.stemX; + if (this.beat.duration < Duration.Half) { + tremoloX = this.width / 2; } + + g.x = tremoloX; } public buildBoundingsLookup(beatBounds: BeatBounds, cx: number, cy: number) { diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts index e8677770f..5ae9a3717 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts @@ -1,156 +1,613 @@ +import type { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { BarSubElement } from '@coderline/alphatab/model/Bar'; +import { MusicFontSymbolLookup } from '@coderline/alphatab/model/MusicFontSymbol'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { ScoreNoteGlyphInfo } from '@coderline/alphatab/rendering/glyphs/ScoreNoteGlyphInfo'; +import type { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import type { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; + +/** + * @internal + * @record + */ +export interface ScoreChordNoteHeadGroupSide { + /** + * A lookup for the notes located at particular steps. + * If we have more than 2 filled voices at the same spot, we might have the additional voices + * placed where the secondary voice already is. + */ + notes: Map; + /** + * The width of this individual side. + */ + width: number; + + /** + * The smallest X-coordinate of all glyphs. Used later to calculate + * the overall shift needed to place notes within the bounds. + */ + minX: number; +} + +/** + * @internal + */ +enum NoteHeadIntersectionKind { + NoIntersection = 0, + EndsTouchingOuter = 1, + EndsTouchingInner = 2, + ExactMatch = 3, + FullIntersection = 4 +} + +/** + * @internal + * @record + */ +export interface ScoreChordNoteHeadGroup { + /** + * All notes on the "correct" side of the stem, + * that's left for upwards stems, and right for downward stems. + */ + correctNotes: ScoreChordNoteHeadGroupSide; + /** + * All displaced notes (the other side of the stem compared to {@link correctNotes}) + */ + displacedNotes?: ScoreChordNoteHeadGroupSide; + + /** + * The direction this group defines. + */ + direction: BeamDirection; + + minStep: number; + maxStep: number; + + /** + * The offset of the stem for this group. + * Offset is relative to the group. + */ + stemX: number; + + /** + * Smallest X-coordinate in this group. + * Offset is relative to the group. + */ + minX: number; + /** + * Largest X-coordinate in this group. + * Offset is relative to the group. + */ + maxX: number; + + /** + * The shift applied for the group to avoid overlaps. + */ + multiVoiceShiftX: number; + + hasFlag: boolean; + hasStem: boolean; +} + +/** + * @internal + */ +export class ScoreChordNoteHeadInfo { + /** + * The direction of the main voice. + */ + public mainVoiceDirection = BeamDirection.Up; + + /** + * All groups respective to their direction. + */ + public readonly groups = new Map(); + public minX = 0; + public maxX = 0; + + private _isFinished = false; + + public constructor(mainVoiceDirection: BeamDirection) { + this.mainVoiceDirection = mainVoiceDirection; + } + + public update() { + let minX = 0; + let maxX = 0; + for (const g of this.groups.values()) { + const gMinX = g.minX + g.multiVoiceShiftX; + const gMaxX = g.maxX + g.multiVoiceShiftX; + if (gMinX < minX) { + minX = gMinX; + } + if (maxX < gMaxX) { + maxX = gMaxX; + } + } + this.minX = minX; + this.maxX = maxX; + } + + finish(smufl: EngravingSettings) { + if (this._isFinished) { + return; + } + this._isFinished = true; + + for (const g of this.groups.values()) { + this._checkForGroupDisplacement(g, smufl); + } + this.update(); + } + + private _checkForGroupDisplacement(noteGroup: ScoreChordNoteHeadGroup, smufl: EngravingSettings) { + // no group displace if we're in the same direction + if (this.mainVoiceDirection === noteGroup.direction) { + return; + } + + const mainGroup = this.groups.get(this.mainVoiceDirection)!; + + // no intersection -> we can align the note heads directly. + const intersection = ScoreChordNoteHeadInfo._checkIntersection(mainGroup, noteGroup); + + const spacing = smufl.multiVoiceDisplacedNoteHeadSpacing; + + switch (intersection) { + case NoteHeadIntersectionKind.NoIntersection: + return; + case NoteHeadIntersectionKind.ExactMatch: + // handling note head + if (!ScoreChordNoteHeadInfo._canShareNoteHead(mainGroup, noteGroup)) { + // align stems back-to-back with additional spacing + if (mainGroup.direction === BeamDirection.Up) { + if (noteGroup.displacedNotes) { + noteGroup.multiVoiceShiftX = noteGroup.stemX + noteGroup.correctNotes.width + spacing; + } else { + noteGroup.multiVoiceShiftX = noteGroup.correctNotes.width + spacing; + } + } else { + mainGroup.multiVoiceShiftX = noteGroup.stemX + spacing; + } + } + break; + case NoteHeadIntersectionKind.EndsTouchingOuter: + if (mainGroup.direction === BeamDirection.Up) { + if (mainGroup.displacedNotes) { + noteGroup.multiVoiceShiftX = mainGroup.stemX; + } else { + const diff = mainGroup.stemX - noteGroup.stemX; + noteGroup.multiVoiceShiftX = diff; + } + } else { + if (mainGroup.displacedNotes) { + noteGroup.multiVoiceShiftX = -noteGroup.stemX; + } else { + const diff = noteGroup.stemX - mainGroup.stemX; + mainGroup.multiVoiceShiftX = diff; + } + } + + break; + case NoteHeadIntersectionKind.EndsTouchingInner: + if (mainGroup.direction === BeamDirection.Up) { + mainGroup.multiVoiceShiftX = mainGroup.stemX; + if (noteGroup.hasFlag) { + mainGroup.multiVoiceShiftX += spacing; + } + } else { + noteGroup.multiVoiceShiftX = noteGroup.stemX; + if (mainGroup.hasFlag) { + noteGroup.multiVoiceShiftX += spacing; + } + } + break; + case NoteHeadIntersectionKind.FullIntersection: + // align note head center to stem + if (!mainGroup.hasStem && !noteGroup.hasStem) { + // we can keep them aligned. + } else if (mainGroup.direction === BeamDirection.Up) { + mainGroup.multiVoiceShiftX = mainGroup.stemX; + if (noteGroup.hasFlag) { + mainGroup.multiVoiceShiftX += spacing; + } else { + mainGroup.multiVoiceShiftX -= spacing; + } + } else { + noteGroup.multiVoiceShiftX = noteGroup.stemX; + if (mainGroup.hasFlag) { + noteGroup.multiVoiceShiftX += spacing; + } else { + noteGroup.multiVoiceShiftX -= spacing; + } + } + + break; + } + } + + private static _canShareNoteHead(mainGroup: ScoreChordNoteHeadGroup, thisGroup: ScoreChordNoteHeadGroup) { + const mainGroupBottom = mainGroup.direction === BeamDirection.Up ? mainGroup.maxStep : mainGroup.minStep; + const thisGroupBottom = thisGroup.direction === BeamDirection.Up ? thisGroup.maxStep : thisGroup.minStep; + + const mainGroupBottomNoteHead = mainGroup.correctNotes.notes.get(mainGroupBottom)!; + if (mainGroupBottomNoteHead.length > 1) { + return false; + } + + const thisGroupBottomNoteHead = thisGroup.correctNotes.notes.get(thisGroupBottom)!; + if (thisGroupBottomNoteHead.length > 1) { + return false; + } + + return ScoreChordNoteHeadInfo._canShareNoteHeadGlyph( + mainGroupBottomNoteHead[0].glyph, + thisGroupBottomNoteHead[0].glyph + ); + } + + private static _canShareNoteHeadGlyph(mainGlyph: NoteHeadGlyphBase, thisGlyph: NoteHeadGlyphBase) { + return ( + mainGlyph.glyphScale === thisGlyph.glyphScale && + mainGlyph.centerOnStem === thisGlyph.centerOnStem && + MusicFontSymbolLookup.isBlackNoteHead(mainGlyph.symbol) && + MusicFontSymbolLookup.isBlackNoteHead(thisGlyph.symbol) + ); + } + + private static _checkIntersection( + mainGroup: ScoreChordNoteHeadGroup, + thisGroup: ScoreChordNoteHeadGroup + ): NoteHeadIntersectionKind { + let bottomGap = 0; + if (mainGroup.direction === BeamDirection.Up) { + const mainGroupBottom = mainGroup.maxStep; + const thisGroupBottom = thisGroup.minStep; + + bottomGap = thisGroupBottom - mainGroupBottom; + } else { + const mainGroupBottom = mainGroup.minStep; + const thisGroupBottom = thisGroup.maxStep; + bottomGap = mainGroupBottom - thisGroupBottom; + } + + if (bottomGap === 0) { + return NoteHeadIntersectionKind.ExactMatch; + } + if (bottomGap === 1) { + return NoteHeadIntersectionKind.EndsTouchingOuter; + } + if (bottomGap === -1) { + return NoteHeadIntersectionKind.EndsTouchingInner; + } + if (bottomGap < 0) { + return NoteHeadIntersectionKind.FullIntersection; + } + return NoteHeadIntersectionKind.NoIntersection; + } +} + +/** + * @internal + * @record + */ +interface ScoreNoteGlyphInfo { + glyph: NoteHeadGlyphBase; + steps: number; +} /** * @internal */ export abstract class ScoreNoteChordGlyphBase extends Glyph { private _infos: ScoreNoteGlyphInfo[] = []; + private _noteHeadInfo?: ScoreChordNoteHeadInfo; + protected noteGroup?: ScoreChordNoteHeadGroup; + + public minStepsNote: ScoreNoteGlyphInfo | null = null; + public maxStepsNote: ScoreNoteGlyphInfo | null = null; + public get stemX(): number { + if (!this.noteGroup) { + return 0; + } + + return this.noteGroup!.stemX + this.noteGroup!.multiVoiceShiftX; + } - public minNote: ScoreNoteGlyphInfo | null = null; - public maxNote: ScoreNoteGlyphInfo | null = null; - public upLineX: number = 0; - public downLineX: number = 0; public noteStartX: number = 0; + public onTimeX = 0; + public constructor() { super(0, 0); } public abstract get direction(): BeamDirection; + public abstract get hasFlag(): boolean; + public abstract get hasStem(): boolean; public abstract get scale(): number; - public getLowestNoteY(): number { - return this.maxNote ? (this.renderer as ScoreBarRenderer).getScoreY(this.maxNote.steps) : 0; + public override getBoundingBoxTop(): number { + return this.minStepsNote ? this.minStepsNote.glyph.getBoundingBoxTop() : this.y; } - public getHighestNoteY(): number { - return this.minNote ? (this.renderer as ScoreBarRenderer).getScoreY(this.minNote.steps) : 0; + public override getBoundingBoxBottom(): number { + return this.maxStepsNote ? this.maxStepsNote.glyph.getBoundingBoxBottom() : this.y + this.height; } - protected add(noteGlyph: MusicFontGlyph, noteLine: number): void { - const info: ScoreNoteGlyphInfo = new ScoreNoteGlyphInfo(noteGlyph, noteLine); + protected add(noteGlyph: NoteHeadGlyphBase, noteSteps: number): void { + const info: ScoreNoteGlyphInfo = { glyph: noteGlyph, steps: noteSteps }; this._infos.push(info); - if (!this.minNote || this.minNote.steps > info.steps) { - this.minNote = info; + if (!this.minStepsNote || this.minStepsNote.steps > info.steps) { + this.minStepsNote = info; } - if (!this.maxNote || this.maxNote.steps < info.steps) { - this.maxNote = info; + if (!this.maxStepsNote || this.maxStepsNote.steps < info.steps) { + this.maxStepsNote = info; } } - public override doLayout(): void { - this._infos.sort((a, b) => { - return b.steps - a.steps; - }); - let stemUpX: number = 0; - let stemDownX: number = 0; - let lastDisplaced: boolean = true; - let lastStep: number = 0; - let anyDisplaced = false; + protected abstract getScoreChordNoteHeadInfo(): ScoreChordNoteHeadInfo; + + private _prepareForLayout(info: ScoreChordNoteHeadInfo): ScoreChordNoteHeadGroup { const direction: BeamDirection = this.direction; - // first get stem position on the right side (displacedX) - // to align all note heads accordingly (they might have different widths) - const smufl = this.renderer.smuflMetrics; + // initialize empty info object + if (!info.groups) { + info.mainVoiceDirection = direction; + } + + // sorting helps avoiding weird alignments + // if the stem is upwards we go bottom-up otherwise top-down + // this ensures we start placing notes on the primary side. + if (direction === BeamDirection.Up) { + this._infos.sort((a, b) => { + return b.steps - a.steps; + }); + } else { + this._infos.sort((a, b) => { + return a.steps - b.steps; + }); + } + + // obtain group we belong to + let group: ScoreChordNoteHeadGroup; + const hasFlag = this.hasFlag; + const hasStem = this.hasStem; + if (info.groups!.has(direction)) { + group = info.groups!.get(direction)!; + if (hasFlag) { + group.hasFlag = hasFlag; + } + if (hasStem) { + group.hasStem = hasStem; + } + } else { + group = { + correctNotes: { + notes: new Map(), + width: 0, + minX: Number.NaN + }, + direction, + stemX: 0, + maxX: Number.NaN, + minX: Number.NaN, + minStep: Number.NaN, + maxStep: Number.NaN, + multiVoiceShiftX: 0, + hasFlag, + hasStem + }; + info.groups.set(direction, group); + } + return group; + } + + public override doLayout(): void { + // generally we try to follow the rules defined in "behind the bars" + // "Double-stemmed writing" but not all rules might be implemented + + // The note head alignment has following base logic and rules: + // 1. we have two note groups: + // * stem up + // * stem down + // 2. we have 4 x-positions for note heads + // * stem up non-displaced (left from stem) + // * stem up displaced (right from stem) + // * stem down non-displaced (right from stem) + // * stem down displaced (left from stem) + // 3. by default the non-displaced notes across note groups align vertically + // 4. every note head is registered on its "step" position of the current group + // 5. if the step of the note or +/- 1 step is reserved in the current group, the note head is displaced, otherwise it is non-displaced + // 6. the group of the first voice beat defines the "primary" stem-direction, all other voice beats are "secondary" stem-directions + // 7. if the current voice matches the primary stem direction and we have overlaps: + // * no shifting of the group is needed + // 8. if the current voice does NOT match the primary stem and we have overlaps we might need shifting of the whole group: + // * if the note heads are on the exact same position and have both the same "black" note head (grace, shape etc. are accounted) + // there is no shift and the same "spot" can be used + // * if there is a +/- 1 overlap a shift of the whole group is applied + + // NOTE: + // * This logic is close to what MuseScore does + // * Dorico has own "note groups" for every voice, even if they are in the same stem-direction. + // Then they displace the groups similarly like with different stem-directions (never merging note heads) + + const info = this.getScoreChordNoteHeadInfo(); + const noteGroup = this._prepareForLayout(info); + this.noteGroup = noteGroup; + this._noteHeadInfo = info; + + this._collectNoteDisplacements(noteGroup); + this._alignNoteHeadsGroup(noteGroup); + + info.update(); + + this._updateSizes(); + } + + private _updateSizes() { + const noteGroup = this.noteGroup!; + + // NOTE: no noteGroup.multiVoiceShiftX for onTimeX. we don't shift the time position on displacement + // otherwise the alignment would automatically be corrected and we get no actual "shift" + + // the center of score notes, (used for aligning the beat to the right on-time position) + // is always the center of the "correct note" position. + this.onTimeX = noteGroup.correctNotes.minX + noteGroup.correctNotes.width / 2; + + this.width = this.noteStartX + noteGroup.multiVoiceShiftX + noteGroup.maxX - noteGroup.minX; + } + + public doMultiVoiceLayout() { + this._noteHeadInfo!.finish(this.renderer.smuflMetrics); + this._updateSizes(); + } + + private _alignNoteHeadsGroup(noteGroup: ScoreChordNoteHeadGroup) { + // align all notes so that they align with the stem positions + if (noteGroup.direction === BeamDirection.Up) { + this._alignNoteHeads(noteGroup, noteGroup.correctNotes, true); + if (noteGroup.displacedNotes) { + this._alignNoteHeads(noteGroup, noteGroup.displacedNotes!, false); + } + } else { + this._alignNoteHeads(noteGroup, noteGroup.correctNotes, false); + if (noteGroup.displacedNotes) { + this._alignNoteHeads(noteGroup, noteGroup.displacedNotes!, true); + } + } + } + private _alignNoteHeads( + noteGroup: ScoreChordNoteHeadGroup, + side: ScoreChordNoteHeadGroupSide, + leftOfStem: boolean + ) { const scale = this.scale; - const displaced = new Map(); - for (let i: number = 0, j: number = this._infos.length; i < j; i++) { - const g = this._infos[i].glyph; - g.renderer = this.renderer; - g.doLayout(); - - if (i > 0 && Math.abs(lastStep - this._infos[i].steps) <= 1) { - if (!lastDisplaced) { - anyDisplaced = true; - lastDisplaced = true; - displaced.set(i, true); + const smufl = this.renderer.smuflMetrics; + + for (const stepInfos of side.notes.values()) { + // NOTE: for now we do not displace "third" voices even further but they overlap + for (const info of stepInfos) { + // align directly + info.glyph.x = noteGroup.stemX; + + // + if (info.glyph.centerOnStem) { + // no offset + } + // shift left/right according to stem position or glyph size + else if (leftOfStem) { + // stem-up is the offset on the right side of the notehead + if (smufl.stemUp.has(info.glyph.symbol)) { + info.glyph.x -= smufl.stemUp.get(info.glyph.symbol)!.x * scale; + } else { + info.glyph.x -= smufl.glyphWidths.get(info.glyph.symbol)! * scale; + } } else { - lastDisplaced = false; - displaced.set(i, false); + // stem-down is the offset on the left side of the notehead + if (smufl.stemDown.has(info.glyph.symbol)) { + info.glyph.x += smufl.stemDown.get(info.glyph.symbol)!.x * scale; + } } - } else { - lastDisplaced = false; - displaced.set(i, false); - } - if (smufl.stemUp.has(g.symbol)) { - const stemInfo = smufl.stemUp.get(g.symbol)!; - const topX = stemInfo.x * scale; - if (topX > stemUpX) { - stemUpX = topX; - } - } else { - const topX = smufl.glyphWidths.get(g.symbol)! * scale; - if (topX > stemUpX) { - stemUpX = topX; + // update side + side.width = Math.max(side.width, info.glyph.width); + if (Number.isNaN(side.minX) || info.glyph.x < side.minX) { + side.minX = info.glyph.x; } - } - if (smufl.stemDown.has(g.symbol)) { - const stemInfo = smufl.stemDown.get(g.symbol)!; - const topX = stemInfo.x * scale; - if (topX > stemDownX) { - const diff = topX - stemDownX; - stemDownX = topX; - stemUpX += diff; // shift right accordingly + // update whole group + if (Number.isNaN(noteGroup.minX) || info.glyph.x < noteGroup.minX) { + noteGroup.minX = info.glyph.x; + } + const maxX = info.glyph.x + info.glyph.width; + if (Number.isNaN(noteGroup.maxX) || maxX > noteGroup.maxX) { + noteGroup.maxX = maxX; } } - - lastStep = this._infos[i].steps; } + } - // align all notes so that they align with the stem positions + private static _hasCollision(side: ScoreChordNoteHeadGroupSide, info: ScoreNoteGlyphInfo) { + return side.notes.has(info.steps) || side.notes.has(info.steps + 1) || side.notes.has(info.steps - 1); + } - const stemPosition = anyDisplaced || direction === BeamDirection.Up ? stemUpX : stemDownX; + private _collectNoteDisplacements(noteGroup: ScoreChordNoteHeadGroup) { + for (const info of this._infos) { + info.glyph.renderer = this.renderer; + info.glyph.doLayout(); - let w: number = 0; - for (let i: number = 0, j: number = this._infos.length; i < j; i++) { - const g = this._infos[i].glyph; - const alignDisplaced: boolean = displaced.get(i)!; + const isGroupCollision = ScoreNoteChordGlyphBase._hasCollision(noteGroup.correctNotes, info); - if (alignDisplaced) { - // displaced: shift note to stem position - g.x = stemPosition; - // TODO: shift left? - } else { - // not displaced: align on left side (where down stem would be for notes) - g.x = stemDownX; - if (smufl.stemDown.has(g.symbol)) { - g.x -= smufl.stemDown.get(g.symbol)!.x * scale; + let noteLookup: ScoreChordNoteHeadGroupSide; + if (isGroupCollision) { + if (!noteGroup.displacedNotes) { + noteGroup.displacedNotes = { notes: new Map(), width: 0, minX: 0 }; } + noteLookup = noteGroup.displacedNotes!; + } else { + noteLookup = noteGroup.correctNotes!; } - g.x += this.noteStartX; - w = Math.max(w, g.x + g.width); + let stepInfos: ScoreNoteGlyphInfo[]; + if (noteLookup.notes.has(info.steps)) { + stepInfos = noteLookup.notes.get(info.steps)!; + } else { + stepInfos = []; + noteLookup.notes.set(info.steps, stepInfos); + } + stepInfos.push(info); + + if (Number.isNaN(noteGroup.minStep) || info.steps < noteGroup.minStep) { + noteGroup.minStep = info.steps; + } - // after size calculation, re-align glyph to stem if needed - if (g instanceof NoteHeadGlyph && (g as NoteHeadGlyph).centerOnStem) { - g.x = stemPosition; + if (Number.isNaN(noteGroup.maxStep) || info.steps > noteGroup.maxStep) { + noteGroup.maxStep = info.steps; } + + this._updateGroupStemXPosition(info, noteGroup); } + } + + private _updateGroupStemXPosition(info: ScoreNoteGlyphInfo, noteGroup: ScoreChordNoteHeadGroup) { + const smufl = this.renderer.smuflMetrics; + const scale = this.scale; + let stemX: number; - if (anyDisplaced) { - this.upLineX = stemPosition; - this.downLineX = stemPosition; + if (noteGroup.direction === BeamDirection.Up || noteGroup.displacedNotes) { + if (smufl.stemUp.has(info.glyph.symbol)) { + const stemInfo = smufl.stemUp.get(info.glyph.symbol)!; + stemX = stemInfo.x * scale; + } else { + stemX = smufl.glyphWidths.get(info.glyph.symbol)! * scale; + } } else { - this.upLineX = stemUpX; - this.downLineX = stemDownX; + if (smufl.stemDown.has(info.glyph.symbol)) { + const stemInfo = smufl.stemDown.get(info.glyph.symbol)!; + stemX = stemInfo.x * scale; + } else { + stemX = 0; + } + } + + stemX += this.noteStartX; + + if (stemX > noteGroup.stemX) { + noteGroup.stemX = stemX; } - this.width = w; } public override paint(cx: number, cy: number, canvas: ICanvas): void { cx += this.x; cy += this.y; + this._paintLedgerLines(cx, cy, canvas); + const noteGroup = this.noteGroup!; + cx += noteGroup.multiVoiceShiftX; + const infos: ScoreNoteGlyphInfo[] = this._infos; for (const g of infos) { g.glyph.renderer = this.renderer; @@ -159,7 +616,7 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { } private _paintLedgerLines(cx: number, cy: number, canvas: ICanvas) { - if (!this.minNote) { + if (!this.minStepsNote) { return; } @@ -169,13 +626,13 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { const scale = this.scale; const lineExtension: number = this.renderer.smuflMetrics.legerLineExtension * scale; - const lineWidth: number = this.width - this.noteStartX + lineExtension * 2; + const lineWidth: number = this.width + lineExtension * 2 - this.noteStartX; const lineSpacing = scoreRenderer.getLineHeight(1); const firstTopLedgerY = scoreRenderer.getLineY(-1); const firstBottomLedgerY = scoreRenderer.getLineY(scoreRenderer.drawnLineCount); - const minNoteLineY = scoreRenderer.getLineY(this.minNote!.steps / 2); - const maxNoteLineY = scoreRenderer.getLineY(this.maxNote!.steps / 2); + const minNoteLineY = scoreRenderer.getLineY(this.minStepsNote!.steps / 2); + const maxNoteLineY = scoreRenderer.getLineY(this.maxStepsNote!.steps / 2); const lineYOffset = (this.renderer.smuflMetrics.legerLineThickness * scale) / 2; diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteGlyphInfo.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteGlyphInfo.ts deleted file mode 100644 index 2aec3ce08..000000000 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteGlyphInfo.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; - -/** - * @internal - */ -export class ScoreNoteGlyphInfo { - public glyph: MusicFontGlyph; - public steps: number = 0; - - public constructor(glyph: MusicFontGlyph, line: number) { - this.glyph = glyph; - this.steps = line; - } -} diff --git a/packages/alphatab/src/rendering/glyphs/ScoreRestGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreRestGlyph.ts index 5555bef0a..4d1f0bab0 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreRestGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreRestGlyph.ts @@ -1,17 +1,14 @@ +import { BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import { BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** * @internal */ export class ScoreRestGlyph extends MusicFontGlyph { - public beamingHelper!: BeamingHelper; - public constructor(x: number, y: number, duration: Duration) { super(x, y, 1, ScoreRestGlyph.getSymbol(duration)); } @@ -45,17 +42,6 @@ export class ScoreRestGlyph extends MusicFontGlyph { } } - public updateBeamingHelper(cx: number): void { - if (this.beamingHelper) { - this.beamingHelper.registerBeatLineX( - 'score', - this.beat!, - cx + this.x + this.width / 2, - cx + this.x + this.width / 2 - ); - } - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { this.internalPaint(cx, cy, canvas, BeatSubElement.StandardNotationRests); } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts index da22d78db..6dffdac1f 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreSlideLineGlyph.ts @@ -9,18 +9,22 @@ import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import type { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { NoteVibratoGlyph } from '@coderline/alphatab/rendering/glyphs/NoteVibratoGlyph'; -import type { ScoreBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatPreNotesGlyph'; +import type { ITieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; +import type { ScoreBeatContainerGlyph } from '@coderline/alphatab/rendering/ScoreBeatContainerGlyph'; /** * @internal */ -export class ScoreSlideLineGlyph extends Glyph { +export class ScoreSlideLineGlyph extends Glyph implements ITieGlyph { private _outType: SlideOutType; private _inType: SlideInType; private _startNote: Note; private _parent: BeatContainerGlyph; + // the slide line cannot overflow anything and there are ties drawn in here + public readonly checkForOverflow = false; + public constructor(inType: SlideInType, outType: SlideOutType, startNote: Note, parent: BeatContainerGlyph) { super(0, 0); this._outType = outType; @@ -68,11 +72,8 @@ export class ScoreSlideLineGlyph extends Glyph { } private _getAccidentalsWidth(renderer: ScoreBarRenderer, beat: Beat): number { - const preNotes: ScoreBeatPreNotesGlyph = renderer.getPreNotesGlyphForBeat(beat) as ScoreBeatPreNotesGlyph; - if (preNotes && preNotes.accidentals) { - return preNotes.accidentals.width; - } - return 0; + const container = renderer.getBeatContainer(beat) as ScoreBeatContainerGlyph; + return container.accidentalsWidth; } private _drawSlideOut(cx: number, cy: number, canvas: ICanvas): void { @@ -96,7 +97,7 @@ export class ScoreSlideLineGlyph extends Glyph { if (this._startNote.slideTarget) { const endNoteRenderer: BarRendererBase | null = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, this._startNote.slideTarget.beat.voice.bar ); if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { diff --git a/packages/alphatab/src/rendering/glyphs/ScoreSlurGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreSlurGlyph.ts index bcb5a2717..8735bd156 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreSlurGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreSlurGlyph.ts @@ -1,98 +1,100 @@ -import type { Note } from '@coderline/alphatab/model/Note'; -import { ScoreLegatoGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreLegatoGlyph'; -import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; -import { NoteYPosition, NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import { ScoreTieGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTieGlyph'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ -export class ScoreSlurGlyph extends ScoreLegatoGlyph { - private _startNote: Note; - private _endNote: Note; - - public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { - super(startNote.beat, endNote.beat, forEnd); - this._startNote = startNote; - this._endNote = endNote; +export class ScoreSlurGlyph extends ScoreTieGlyph { + public override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { + return (Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2; } - protected override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { - return Math.log2(endX - startX + 1) * this.renderer.settings.notation.slurHeight / 2; + protected override calculateStartX(): number { + return ( + this.renderer.x + + (this._isStartCentered() + ? this.renderer!.getBeatX(this.startNote.beat, BeatXPosition.MiddleNotes) + : this.renderer!.getNoteX(this.startNote, NoteXPosition.Right)) + ); } - protected override getStartY(): number { + protected override calculateStartY(): number { if (this._isStartCentered()) { switch (this.tieDirection) { case BeamDirection.Up: - // below lowest note - return this.startNoteRenderer!.getNoteY(this._startNote, NoteYPosition.Top); + return this.renderer.y + this.renderer!.getNoteY(this.startNote, NoteYPosition.Top); default: - return this.startNoteRenderer!.getNoteY(this._startNote, NoteYPosition.Bottom); + return this.renderer.y + this.renderer!.getNoteY(this.startNote, NoteYPosition.Bottom); } } - return this.startNoteRenderer!.getNoteY(this._startNote, NoteYPosition.Center); + return this.renderer.y + this.renderer!.getNoteY(this.startNote, NoteYPosition.Center); + } + + protected override calculateEndX(): number { + const endNoteRenderer = this.lookupEndBeatRenderer(); + if (!endNoteRenderer) { + return this.calculateStartX() + this.renderer.smuflMetrics.leftHandTabTieWidth; + } + + if (this._isEndCentered()) { + if (this._isEndOnStem()) { + return endNoteRenderer.x + endNoteRenderer.getBeatX(this.endNote.beat, BeatXPosition.Stem); + } + return endNoteRenderer.x + endNoteRenderer.getNoteX(this.endNote, NoteXPosition.Center); + } + return endNoteRenderer.x + endNoteRenderer.getBeatX(this.endNote.beat, BeatXPosition.PreNotes); } - protected override getEndY(): number { + protected override caclculateEndY(): number { + const endNoteRenderer = this.lookupEndBeatRenderer(); + if (!endNoteRenderer) { + return this.calculateStartY(); + } + if (this._isEndCentered()) { if (this._isEndOnStem()) { switch (this.tieDirection) { case BeamDirection.Up: - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.TopWithStem); + return endNoteRenderer.y + endNoteRenderer.getNoteY(this.endNote, NoteYPosition.TopWithStem); default: - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.BottomWithStem); + return endNoteRenderer.y + endNoteRenderer.getNoteY(this.endNote, NoteYPosition.BottomWithStem); } } switch (this.tieDirection) { case BeamDirection.Up: - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Top); + return endNoteRenderer.y + endNoteRenderer.getNoteY(this.endNote, NoteYPosition.Top); default: - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Bottom); + return endNoteRenderer.y + endNoteRenderer.getNoteY(this.endNote, NoteYPosition.Bottom); } } - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Center); + return endNoteRenderer.y + endNoteRenderer.getNoteY(this.endNote, NoteYPosition.Center); } private _isStartCentered() { return ( - (this._startNote === this._startNote.beat.maxNote && this.tieDirection === BeamDirection.Up) || - (this._startNote === this._startNote.beat.minNote && this.tieDirection === BeamDirection.Down) + (this.startNote === this.startNote.beat.maxNote && this.tieDirection === BeamDirection.Up) || + (this.startNote === this.startNote.beat.minNote && this.tieDirection === BeamDirection.Down) ); } private _isEndCentered() { return ( - this._startNote.beat.graceType === GraceType.None && - ((this._endNote === this._endNote.beat.maxNote && this.tieDirection === BeamDirection.Up) || - (this._endNote === this._endNote.beat.minNote && this.tieDirection === BeamDirection.Down)) + this.startNote.beat.graceType === GraceType.None && + ((this.endNote === this.endNote.beat.maxNote && this.tieDirection === BeamDirection.Up) || + (this.endNote === this.endNote.beat.minNote && this.tieDirection === BeamDirection.Down)) ); } private _isEndOnStem() { - const endNoteScoreRenderer = this.endNoteRenderer as ScoreBarRenderer; - - const startBeamDirection = (this.startNoteRenderer as ScoreBarRenderer).getBeatDirection(this.startBeat!); - const endBeamDirection = endNoteScoreRenderer.getBeatDirection(this.endBeat!); - - return startBeamDirection !== endBeamDirection && this.startBeat!.graceType === GraceType.None; - } + const startBeamDirection = this.lookupStartBeatRenderer().getBeatDirection(this.startNote.beat); + const endBeatRenderer = this.lookupEndBeatRenderer(); + const endBeamDirection = endBeatRenderer + ? endBeatRenderer.getBeatDirection(this.endNote.beat) + : startBeamDirection; - protected override getStartX(): number { - return this._isStartCentered() - ? this.startNoteRenderer!.getBeatX(this._startNote.beat, BeatXPosition.MiddleNotes) - : this.startNoteRenderer!.getNoteX(this._startNote, NoteXPosition.Right); - } - - protected override getEndX(): number { - if (this._isEndCentered()) { - if (this._isEndOnStem()) { - return this.endNoteRenderer!.getBeatX(this._endNote.beat, BeatXPosition.Stem); - } - return this.endNoteRenderer!.getNoteX(this._endNote, NoteXPosition.Center); - } - return this.endNoteRenderer!.getBeatX(this._endNote.beat, BeatXPosition.PreNotes); + return startBeamDirection !== endBeamDirection && this.startNote.beat!.graceType === GraceType.None; } } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreTieGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreTieGlyph.ts index 933142279..17a9effa2 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreTieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreTieGlyph.ts @@ -1,24 +1,11 @@ -import type { Beat } from '@coderline/alphatab/model/Beat'; -import type { Note } from '@coderline/alphatab/model/Note'; -import { type BarRendererBase, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; -import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import { NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import { NoteTieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; /** * @internal */ -export class ScoreTieGlyph extends TieGlyph { - protected startNote: Note; - protected endNote: Note; - - public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { - super(!startNote ? null : startNote.beat, !endNote ? null : endNote.beat, forEnd); - this.startNote = startNote; - this.endNote = endNote; - } - +export class ScoreTieGlyph extends NoteTieGlyph { protected override shouldDrawBendSlur() { return ( this.renderer.settings.notation.extendBendArrowsOnTiedNotes && @@ -27,58 +14,21 @@ export class ScoreTieGlyph extends TieGlyph { ); } - public override doLayout(): void { - super.doLayout(); - } - - protected override getBeamDirection(beat: Beat, noteRenderer: BarRendererBase): BeamDirection { - // invert direction (if stems go up, ties go down to not cross them) - switch ((noteRenderer as ScoreBarRenderer).getBeatDirection(beat)) { - case BeamDirection.Up: - return BeamDirection.Down; - default: - return BeamDirection.Up; - } - } - - protected override getStartY(): number { - if (this.startBeat!.isRest) { - // below all lines - return (this.startNoteRenderer as ScoreBarRenderer).getScoreY(9); - } - switch (this.tieDirection) { - case BeamDirection.Up: - // below lowest note - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Top); - default: - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Bottom); + protected override calculateStartX(): number { + if (this.isLeftHandTap) { + return this.calculateEndX() - this.renderer.smuflMetrics.leftHandTabTieWidth; } + return this.renderer.x + this.renderer!.getBeatX(this.startNote.beat, BeatXPosition.PostNotes); } - protected override getEndY(): number { - const endNoteScoreRenderer = this.endNoteRenderer as ScoreBarRenderer; - if (this.endBeat!.isRest) { - switch (this.tieDirection) { - case BeamDirection.Up: - return endNoteScoreRenderer.getScoreY(9); - default: - return endNoteScoreRenderer.getScoreY(0); - } + protected override calculateEndX(): number { + const endNoteRenderer = this.lookupEndBeatRenderer(); + if (!endNoteRenderer) { + return this.calculateStartX() + this.renderer.smuflMetrics.leftHandTabTieWidth; } - - switch (this.tieDirection) { - case BeamDirection.Up: - return endNoteScoreRenderer.getNoteY(this.endNote, NoteYPosition.Top); - default: - return endNoteScoreRenderer.getNoteY(this.endNote, NoteYPosition.Bottom); + if (this.isLeftHandTap) { + return endNoteRenderer.x + endNoteRenderer.getNoteX(this.endNote, NoteXPosition.Left); } - } - - protected override getStartX(): number { - return this.startNoteRenderer!.getBeatX(this.startNote.beat, BeatXPosition.PostNotes); - } - - protected override getEndX(): number { - return this.endNoteRenderer!.getBeatX(this.endNote.beat, BeatXPosition.PreNotes); + return endNoteRenderer.x + endNoteRenderer.getBeatX(this.endNote.beat, BeatXPosition.PreNotes); } } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts index 255e489ae..e49a40f9e 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts @@ -1,37 +1,67 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import type { BendPoint } from '@coderline/alphatab/model/BendPoint'; import { BendStyle } from '@coderline/alphatab/model/BendStyle'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; import { WhammyType } from '@coderline/alphatab/model/WhammyType'; import { NotationMode } from '@coderline/alphatab/NotationSettings'; import type { ICanvas, TextAlign } from '@coderline/alphatab/platform/ICanvas'; +import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { BendNoteHeadGroupGlyph } from '@coderline/alphatab/rendering/glyphs/BendNoteHeadGroupGlyph'; -import type { ScoreBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreBeatPreNotesGlyph'; import { ScoreHelperNotesBaseGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreHelperNotesBaseGlyph'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import { type ITieGlyph, TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; +import type { ScoreBeatContainerGlyph } from '@coderline/alphatab/rendering/ScoreBeatContainerGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; /** * @internal */ -export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { +export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements ITieGlyph { + private _container: ScoreBeatContainerGlyph; private _beat: Beat; private _endGlyph: BendNoteHeadGroupGlyph | null = null; - public constructor(beat: Beat) { + public readonly checkForOverflow = false; + + public constructor(container: ScoreBeatContainerGlyph) { super(0, 0); - this._beat = beat; + this._container = container; + this._beat = container.beat; + } + + public get hasBoundingBox(): boolean { + const endGlyph = this._endGlyph; + if (!endGlyph) { + return false; + } + return !!endGlyph.minStepsNote && !!endGlyph.maxStepsNote; + } + + public override getBoundingBoxTop(): number { + if (this._endGlyph?.minStepsNote) { + return this._endGlyph.minStepsNote.glyph.getBoundingBoxTop(); + } + return super.getBoundingBoxTop(); + } + + public override getBoundingBoxBottom(): number { + if (this._endGlyph?.maxStepsNote) { + return this._endGlyph.maxStepsNote.glyph.getBoundingBoxBottom(); + } + return super.getBoundingBoxBottom(); + } + + public doMultiVoiceLayout() { + this._endGlyph?.doMultiVoiceLayout(); } public override doLayout(): void { - const whammyMode: NotationMode = this.renderer.settings.notation.notationMode; + const sr = this.renderer as ScoreBarRenderer; + const whammyMode: NotationMode = sr.settings.notation.notationMode; switch (this._beat.whammyBarType) { case WhammyType.None: case WhammyType.Custom: @@ -40,9 +70,13 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { case WhammyType.Dive: case WhammyType.PrediveDive: { - const endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); + const endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph( + 'postwhammy', + this._beat, + false + ); this._endGlyph = endGlyphs; - endGlyphs.renderer = this.renderer; + endGlyphs.renderer = sr; const lastWhammyPoint: BendPoint = this._beat.whammyBarPoints![this._beat.whammyBarPoints!.length - 1]; for (const note of this._beat.notes) { @@ -55,19 +89,22 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { } } endGlyphs.doLayout(); - this.bendNoteHeads.push(endGlyphs); + this.addGlyph(endGlyphs); } break; case WhammyType.Dip: { if (whammyMode === NotationMode.SongBook) { - const res: RenderingResources = this.renderer.resources; - (this.renderer as ScoreBarRenderer).simpleWhammyOverflow = - res.tablatureFont.size + this.renderer.smuflMetrics.songBookWhammyDipHeight; + // handled separately + return; } else { - const middleGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); - middleGlyphs.renderer = this.renderer; - if (this.renderer.settings.notation.notationMode === NotationMode.GuitarPro) { + const middleGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph( + 'middlewhammy', + this._beat, + false + ); + middleGlyphs.renderer = sr; + if (sr.settings.notation.notationMode === NotationMode.GuitarPro) { const middleBendPoint: BendPoint = this._beat.whammyBarPoints![1]; for (const note of this._beat.notes) { middleGlyphs.addGlyph( @@ -78,12 +115,16 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { } } middleGlyphs.doLayout(); - this.bendNoteHeads.push(middleGlyphs); - const endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); - endGlyphs.renderer = this.renderer; + this.addGlyph(middleGlyphs); + const endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph( + 'postwhammy', + this._beat, + false + ); + endGlyphs.renderer = sr; this._endGlyph = endGlyphs; - if (this.renderer.settings.notation.notationMode === NotationMode.GuitarPro) { + if (sr.settings.notation.notationMode === NotationMode.GuitarPro) { const lastBendPoint: BendPoint = this._beat.whammyBarPoints![this._beat.whammyBarPoints!.length - 1]; for (const note of this._beat.notes) { @@ -95,14 +136,16 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { } } endGlyphs.doLayout(); - this.bendNoteHeads.push(endGlyphs); + this.addGlyph(endGlyphs); } } break; case WhammyType.Predive: break; } + super.doLayout(); + this.width = this.width / 2; } public override paint(cx: number, cy: number, canvas: ICanvas): void { @@ -114,7 +157,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { } const whammyMode: NotationMode = this.renderer.settings.notation.notationMode; const startNoteRenderer: ScoreBarRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, beat.voice.bar )! as ScoreBarRenderer; @@ -141,22 +184,19 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { startY += startNoteRenderer.getNoteY(note, NoteYPosition.Top); } - let endX: number = cx + startNoteRenderer.x; - if (beat.isLastOfVoice) { - endX += startNoteRenderer.postBeatGlyphsStart; - } else { - endX += startNoteRenderer.getBeatX(beat, BeatXPosition.EndBeat); - } + let endX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(beat, BeatXPosition.EndBeat); + endX -= this.renderer.smuflMetrics.postNoteEffectPadding; if (this._endGlyph) { - endX -= this._endGlyph.upLineX; + const postBeatSize = this._endGlyph.width - this._endGlyph.onTimeX; + endX -= postBeatSize; } const slurText: string = beat.whammyStyle === BendStyle.Gradual && i === 0 ? 'grad.' : ''; let endNoteRenderer: ScoreBarRenderer | null = null; if (note.isTieOrigin) { endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, note.tieDestination!.beat.voice.bar ) as ScoreBarRenderer | null; if (endNoteRenderer && endNoteRenderer.staff === startNoteRenderer.staff) { @@ -169,7 +209,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { } } - let heightOffset: number = noteHeadHeight * NoteHeadGlyph.GraceScale * 0.5; + let heightOffset: number = noteHeadHeight * EngravingSettings.GraceScale * 0.5; if (direction === BeamDirection.Up) { heightOffset = -heightOffset; } @@ -180,8 +220,8 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { let endY: number = 0; let bendTie: boolean = false; - if (this.bendNoteHeads.length > 0 && this.bendNoteHeads[0].containsNoteValue(endValue)) { - endY = this.bendNoteHeads[0].getNoteValueY(endValue) + heightOffset; + if (this.glyphs && (this.glyphs![0] as BendNoteHeadGroupGlyph).containsNoteValue(endValue)) { + endY = (this.glyphs[0] as BendNoteHeadGroupGlyph).getNoteValueY(endValue) + heightOffset; bendTie = true; } else if ( endNoteRenderer && @@ -221,13 +261,14 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { break; case WhammyType.Dive: if (i === 0) { - this.bendNoteHeads[0].x = endX - this.bendNoteHeads[0].noteHeadOffset; - const previousY = this.bendNoteHeads[0].y; - this.bendNoteHeads[0].y = cy + startNoteRenderer.y; - this.bendNoteHeads[0].paint(0, 0, canvas); - if (this.bendNoteHeads[0].containsNoteValue(endValue)) { + const g0 = this.glyphs![0] as BendNoteHeadGroupGlyph; + g0.x = endX - g0.onTimeX; + const previousY = this.glyphs![0].y; + g0.y = cy + startNoteRenderer.y; + g0.paint(0, 0, canvas); + if (g0.containsNoteValue(endValue)) { endY -= previousY; - endY += this.bendNoteHeads[0].y; + endY += g0.y; } } if (bendTie) { @@ -238,7 +279,6 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { endX, endY, direction === BeamDirection.Down, - 1, slurText ); } else if (note.isTieOrigin) { @@ -257,36 +297,6 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { break; case WhammyType.Dip: if (whammyMode === NotationMode.SongBook) { - if (i === 0) { - const simpleStartX: number = - cx + - startNoteRenderer.x + - startNoteRenderer.getBeatX(this._beat, BeatXPosition.OnNotes); - const simpleEndX: number = - cx + - startNoteRenderer.x + - startNoteRenderer.getBeatX(this._beat, BeatXPosition.PostNotes); - const middleX: number = (simpleStartX + simpleEndX) / 2; - const text: string = ( - ((this._beat.whammyBarPoints![1].value - this._beat.whammyBarPoints![0].value) / 4) | - 0 - ).toString(); - canvas.font = this.renderer.resources.tablatureFont; - canvas.fillText(text, middleX, cy + this.y); - const simpleStartY: number = cy + this.y + canvas.font.size; - const simpleEndY: number = - simpleStartY + this.renderer.smuflMetrics.songBookWhammyDipHeight; - if (this._beat.whammyBarPoints![1].value > this._beat.whammyBarPoints![0].value) { - canvas.moveTo(simpleStartX, simpleEndY); - canvas.lineTo(middleX, simpleStartY); - canvas.lineTo(simpleEndX, simpleEndY); - } else { - canvas.moveTo(simpleStartX, simpleStartY); - canvas.lineTo(middleX, simpleEndY); - canvas.lineTo(simpleEndX, simpleStartY); - } - canvas.stroke(); - } if (note.isTieOrigin) { TieGlyph.paintTie( canvas, @@ -302,11 +312,12 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { } } else { const middleX: number = (startX + endX) / 2; - this.bendNoteHeads[0].x = middleX - this.bendNoteHeads[0].noteHeadOffset; - this.bendNoteHeads[0].y = cy + startNoteRenderer.y; - this.bendNoteHeads[0].paint(0, 0, canvas); + const g0 = this.glyphs![0] as BendNoteHeadGroupGlyph; + g0.x = middleX - g0.onTimeX; + g0.y = cy + startNoteRenderer.y; + g0.paint(0, 0, canvas); const middleValue: number = this._getBendNoteValue(note, beat.whammyBarPoints![1]); - const middleY: number = this.bendNoteHeads[0].getNoteValueY(middleValue) + heightOffset; + const middleY: number = g0.getNoteValueY(middleValue) + heightOffset; this.drawBendSlur( canvas, startX, @@ -314,13 +325,14 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { middleX, middleY, direction === BeamDirection.Down, - 1, slurText ); - this.bendNoteHeads[1].x = endX - this.bendNoteHeads[1].noteHeadOffset; - this.bendNoteHeads[1].y = cy + startNoteRenderer.y; - this.bendNoteHeads[1].paint(0, 0, canvas); - endY = this.bendNoteHeads[1].getNoteValueY(endValue) + heightOffset; + + const g1 = this.glyphs![1] as BendNoteHeadGroupGlyph; + g1.x = endX - g1.onTimeX; + g1.y = cy + startNoteRenderer.y; + g1.paint(0, 0, canvas); + endY = g1.getNoteValueY(endValue) + heightOffset; this.drawBendSlur( canvas, middleX, @@ -328,7 +340,6 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { endX, endY, direction === BeamDirection.Down, - 1, slurText ); } @@ -337,32 +348,23 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { case WhammyType.Predive: let preX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(note.beat, BeatXPosition.PreNotes); - preX += (startNoteRenderer.getPreNotesGlyphForBeat(note.beat) as ScoreBeatPreNotesGlyph) - .prebendNoteHeadOffset; + preX += this._container.prebendNoteHeadOffset; const preY: number = cy + startNoteRenderer.y + startNoteRenderer.getScoreY( - startNoteRenderer.accidentalHelper.getNoteLineForValue( + startNoteRenderer.accidentalHelper.getNoteStepsForValue( note.displayValue - ((note.beat.whammyBarPoints![0].value / 2) | 0), false ) ) + heightOffset; - this.drawBendSlur( - canvas, - preX, - preY, - startX, - startY, - direction === BeamDirection.Down, - 1, - slurText - ); - if (this.bendNoteHeads.length > 0) { - this.bendNoteHeads[0].x = endX - this.bendNoteHeads[0].noteHeadOffset; - this.bendNoteHeads[0].y = cy + startNoteRenderer.y; - this.bendNoteHeads[0].paint(0, 0, canvas); + this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down, slurText); + if (this.glyphs) { + const g0 = this.glyphs![0] as BendNoteHeadGroupGlyph; + g0.x = endX - g0.onTimeX; + g0.y = cy + startNoteRenderer.y; + g0.paint(0, 0, canvas); this.drawBendSlur( canvas, startX, @@ -370,7 +372,6 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { endX, endY, direction === BeamDirection.Down, - 1, slurText ); } diff --git a/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts index c8ef97f81..60175b124 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts @@ -1,23 +1,31 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; -import { AugmentationDotGlyph } from '@coderline/alphatab/rendering/glyphs/AugmentationDotGlyph'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; +import { AugmentationDotGlyph } from '@coderline/alphatab/rendering/glyphs/AugmentationDotGlyph'; +import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; +import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; +import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; +import { SlashRestGlyph } from '@coderline/alphatab/rendering/glyphs/SlashRestGlyph'; +import { TremoloPickingGlyph } from '@coderline/alphatab/rendering/glyphs/TremoloPickingGlyph'; import type { SlashBarRenderer } from '@coderline/alphatab/rendering/SlashBarRenderer'; -import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; -import { SlashRestGlyph } from '@coderline/alphatab/rendering/glyphs/SlashRestGlyph'; -import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; -import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { BeatSubElement } from '@coderline/alphatab/model/Beat'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { NoteBounds } from '@coderline/alphatab/rendering/utils/NoteBounds'; /** * @internal */ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { + private _tremoloPicking?: TremoloPickingGlyph; + private _stemLengthExtension = 0; + public noteHeads: SlashNoteHeadGlyph | null = null; public deadSlapped: DeadSlappedBeatGlyph | null = null; public restGlyph: SlashRestGlyph | null = null; @@ -64,39 +72,95 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { beatBounds.addNote(noteBounds); } } - - public override getLowestNoteY(): number { - return this.noteHeads ? this.noteHeads.y : 0; + + public override getLowestNoteY(requestedPosition: NoteYPosition): number { + return this._internalGetNoteY(requestedPosition); + } + + public override getHighestNoteY(requestedPosition: NoteYPosition): number { + return this._internalGetNoteY(requestedPosition); + } + + public override getRestY(requestedPosition: NoteYPosition): number { + const g = this.restGlyph; + if (g) { + switch (requestedPosition) { + case NoteYPosition.TopWithStem: + return g.getBoundingBoxTop() - this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); + case NoteYPosition.Top: + return g.getBoundingBoxTop(); + case NoteYPosition.Center: + case NoteYPosition.StemUp: + case NoteYPosition.StemDown: + return g.getBoundingBoxTop() + g.height / 2; + case NoteYPosition.Bottom: + return g.getBoundingBoxBottom(); + case NoteYPosition.BottomWithStem: + return g.getBoundingBoxBottom() + this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); + } + } + return 0; } - public override getHighestNoteY(): number { - return this.noteHeads ? this.noteHeads.y : 0; + public override getNoteY(_note: Note, requestedPosition: NoteYPosition): number { + return this._internalGetNoteY(requestedPosition); } - public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { + public _internalGetNoteY(requestedPosition: NoteYPosition): number { let g: Glyph | null = null; let symbol: MusicFontSymbol = MusicFontSymbol.None; + let hasStem = false; if (this.noteHeads) { g = this.noteHeads; - symbol = SlashNoteHeadGlyph.getSymbol(note.beat.duration); + symbol = SlashNoteHeadGlyph.getSymbol(this.container.beat.duration); + hasStem = true; } else if (this.deadSlapped) { g = this.deadSlapped; } if (g) { let pos = this.y + g.y; + const sr = this.renderer as SlashBarRenderer; + const beat = this.container.beat; + const scale = beat.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; switch (requestedPosition) { - case NoteYPosition.Top: case NoteYPosition.TopWithStem: + if (hasStem) { + // stem start + pos -= + (sr.smuflMetrics.stemUp.has(symbol) ? sr.smuflMetrics.stemUp.get(symbol)!.bottomY : 0) * + scale; + + // stem size according to duration + pos -= sr.smuflMetrics.getStemLength(beat.duration, sr.hasFlag(beat)) * scale; + pos -= this._stemLengthExtension; + } else { + pos -= g.height / 2; + } + return pos; + case NoteYPosition.Top: pos -= g.height / 2; break; case NoteYPosition.Center: break; case NoteYPosition.Bottom: - case NoteYPosition.BottomWithStem: pos += g.height / 2; break; + case NoteYPosition.BottomWithStem: + if (hasStem) { + pos -= + (sr.smuflMetrics.stemDown.has(symbol) + ? sr.smuflMetrics.stemDown.get(symbol)!.topY + : -sr.smuflMetrics.glyphHeights.get(symbol)! / 2) * scale; + + // stem size according to duration + pos += sr.smuflMetrics.getStemLength(beat.duration, sr.hasFlag(beat)) * scale; + pos += this._stemLengthExtension; + } else { + pos += g.height / 2; + } + return pos; case NoteYPosition.StemUp: pos -= this.renderer.smuflMetrics.stemUp.has(symbol) @@ -115,29 +179,11 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { return 0; } - public override updateBeamingHelper(): void { - if (this.noteHeads) { - this.noteHeads.updateBeamingHelper(this.container.x + this.x); - } else if (this.deadSlapped) { - if (this.beamingHelper) { - this.beamingHelper.registerBeatLineX( - 'slash', - this.container.beat, - this.container.x + this.x + this.deadSlapped.x + this.width, - this.container.x + this.x + this.deadSlapped.x - ); - } - } else if (this.restGlyph) { - this.restGlyph.updateBeamingHelper(this.container.x + this.x); - } - } - public override doLayout(): void { // create glyphs const sr = this.renderer as SlashBarRenderer; - const line: number = sr.getNoteLine(); - const glyphY = sr.getLineY(line); + const glyphY = sr.getLineY(0); if (this.container.beat.deadSlapped) { const deadSlapped = new DeadSlappedBeatGlyph(); deadSlapped.renderer = this.renderer; @@ -146,28 +192,23 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { this.addEffect(deadSlapped); } else if (!this.container.beat.isEmpty) { if (!this.container.beat.isRest) { - const isGrace: boolean = this.container.beat.graceType !== GraceType.None; - const noteHeadGlyph = new SlashNoteHeadGlyph( - 0, - glyphY, - this.container.beat.duration, - isGrace, - this.container.beat - ); + const noteHeadGlyph = new SlashNoteHeadGlyph(0, glyphY, this.container.beat); this.noteHeads = noteHeadGlyph; noteHeadGlyph.beat = this.container.beat; - noteHeadGlyph.beamingHelper = this.beamingHelper; this.addNormal(noteHeadGlyph); + + if (this.container.beat.isTremolo) { + this._tremoloPicking = new TremoloPickingGlyph(0, 0, this.container.beat.tremoloPicking!); + this._tremoloPicking.renderer = this.renderer; + this._tremoloPicking.doLayout(); + + this._alignTremoloPickingGlyph(); + } } else { const restGlyph = new SlashRestGlyph(0, glyphY, this.container.beat.duration); this.restGlyph = restGlyph; restGlyph.beat = this.container.beat; - restGlyph.beamingHelper = this.beamingHelper; this.addNormal(restGlyph); - - if (this.beamingHelper) { - this.beamingHelper.applyRest(this.container.beat, 0); - } } } @@ -176,25 +217,56 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { // if (this.container.beat.dots > 0) { for (let i: number = 0; i < this.container.beat.dots; i++) { - this.addEffect( - new AugmentationDotGlyph( - 0, - sr.getLineY(sr.getNoteLine()) - sr.getLineHeight(0.5) - ) - ); + this.addEffect(new AugmentationDotGlyph(0, glyphY - sr.getLineHeight(0.5))); } } super.doLayout(); if (this.container.beat.isEmpty) { - this.centerX = this.width / 2; + this.onTimeX = this.width / 2; + this.stemX = this.onTimeX; } else if (this.restGlyph) { - this.centerX = this.restGlyph.x + this.restGlyph.width / 2; + this.onTimeX = this.restGlyph.x + this.restGlyph.width / 2; + this.stemX = this.onTimeX; } else if (this.noteHeads) { - this.centerX = this.noteHeads.x + this.noteHeads.width / 2; + this.onTimeX = this.noteHeads.x + this.noteHeads.width / 2; + this.stemX = this.noteHeads!.x + this.noteHeads!.stemX; } else if (this.deadSlapped) { - this.centerX = this.deadSlapped.x + this.deadSlapped.width / 2; + this.onTimeX = this.deadSlapped.x + this.deadSlapped.width / 2; + this.stemX = this.onTimeX; + } + this.middleX = this.onTimeX; + + const tremolo = this._tremoloPicking; + if (tremolo) { + tremolo.x = this.container.beat.duration < Duration.Half ? this.width / 2 : this.stemX; + } + } + + private _alignTremoloPickingGlyph() { + const g = this._tremoloPicking!; + g.alignTremoloPickingGlyph( + BeamDirection.Up, + this._internalGetNoteY(NoteYPosition.TopWithStem), + this._internalGetNoteY(NoteYPosition.Center), + this.container.beat.duration + ); + this._stemLengthExtension = g.stemExtensionHeight; + + let tremoloX: number = this.stemX; + if (this.container.beat.duration < Duration.Half) { + tremoloX = this.width / 2; + } + + g.x = tremoloX; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + super.paint(cx, cy, canvas); + const tremolo = this._tremoloPicking; + if (tremolo) { + tremolo.paint(cx + this.x, cy + this.y, canvas); } } } diff --git a/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts index b85d78ddc..c881bb149 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashNoteHeadGlyph.ts @@ -1,28 +1,27 @@ +import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import { NoteSubElement } from '@coderline/alphatab/model/Note'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; +import { NoteHeadGlyphBase } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; +import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { NoteSubElement } from '@coderline/alphatab/model/Note'; -import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; /** * @internal */ -export class SlashNoteHeadGlyph extends MusicFontGlyph { - +export class SlashNoteHeadGlyph extends NoteHeadGlyphBase { public beatEffects: Map = new Map(); - public beamingHelper!: BeamingHelper; public noteHeadElement: NoteSubElement = NoteSubElement.SlashNoteHead; public effectElement: BeatSubElement = BeatSubElement.SlashEffects; - private _symbol: MusicFontSymbol; - public constructor(x: number, y: number, duration: Duration, isGrace: boolean, beat: Beat) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, SlashNoteHeadGlyph.getSymbol(duration)); - this._symbol = SlashNoteHeadGlyph.getSymbol(duration); + public stemX: number = 0; + + public constructor(x: number, y: number, beat: Beat) { + super(x, y, beat.graceType !== GraceType.None, SlashNoteHeadGlyph.getSymbol(beat.duration)); this.beat = beat; } @@ -44,15 +43,40 @@ export class SlashNoteHeadGlyph extends MusicFontGlyph { public override doLayout(): void { super.doLayout(); - const effectSpacing: number = this.renderer.smuflMetrics.onNoteEffectPadding; - let effectY = this.renderer.smuflMetrics.glyphHeights.get(this._symbol)!; + const lr = this.renderer as LineBarRenderer; + const effectSpacing: number = lr.smuflMetrics.onNoteEffectPadding; + let effectY = lr.smuflMetrics.glyphHeights.get(this.symbol)!; + + let minEffectY = Number.NaN; + let maxEffectY = Number.NaN; for (const g of this.beatEffects.values()) { g.y += effectY; g.x += this.width / 2; - g.renderer = this.renderer; - g.doLayout(); + g.renderer = lr; effectY += g.height + effectSpacing; + g.doLayout(); + + if (Number.isNaN(minEffectY) || minEffectY > effectY) { + minEffectY = effectY; + } + if (Number.isNaN(maxEffectY) || maxEffectY < effectY) { + maxEffectY = effectY; + } + } + + if (!Number.isNaN(minEffectY)) { + lr.registerBeatEffectOverflows(minEffectY, maxEffectY); + } + + const direction = lr.getBeatDirection(this.beat!); + const symbol = this.symbol; + if (direction === BeamDirection.Up) { + const stemInfoUp = lr.smuflMetrics.stemUp.has(symbol) ? lr.smuflMetrics.stemUp.get(symbol)!.x : 0; + this.stemX = stemInfoUp; + } else { + const stemInfoDown = lr.smuflMetrics.stemDown.has(symbol) ? lr.smuflMetrics.stemDown.get(symbol)!.x : 0; + this.stemX = stemInfoDown; } } @@ -68,23 +92,4 @@ export class SlashNoteHeadGlyph extends MusicFontGlyph { return MusicFontSymbol.NoteheadSlashHorizontalEnds; } } - - public updateBeamingHelper(cx: number) { - if (this.beamingHelper) { - const symbol = this._symbol; - const stemInfoUp = this.renderer.smuflMetrics.stemUp.has(symbol) - ? this.renderer.smuflMetrics.stemUp.get(symbol)!.x - : 0; - const stemInfoDown = this.renderer.smuflMetrics.stemDown.has(symbol) - ? this.renderer.smuflMetrics.stemDown.get(symbol)!.x - : 0; - - this.beamingHelper.registerBeatLineX( - 'slash', - this.beat!, - cx + this.x + stemInfoUp, - cx + this.x + stemInfoDown - ); - } - } } diff --git a/packages/alphatab/src/rendering/glyphs/SlashRestGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashRestGlyph.ts index 9e246aeaa..eab67f16c 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashRestGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashRestGlyph.ts @@ -6,17 +6,6 @@ import { ScoreRestGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreRestGl * @internal */ export class SlashRestGlyph extends ScoreRestGlyph { - public override updateBeamingHelper(cx: number): void { - if (this.beamingHelper) { - this.beamingHelper.registerBeatLineX( - 'slash', - this.beat!, - cx + this.x + this.width / 2, - cx + this.x + this.width / 2 - ); - } - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { super.internalPaint(cx, cy, canvas, BeatSubElement.SlashRests); } diff --git a/packages/alphatab/src/rendering/glyphs/SlashTieGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashTieGlyph.ts index 2548b7f66..de4f432bb 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashTieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashTieGlyph.ts @@ -1,57 +1,20 @@ -import type { Beat } from '@coderline/alphatab/model/Beat'; -import type { Note } from '@coderline/alphatab/model/Note'; -import { type BarRendererBase, NoteYPosition, NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import { NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { NoteTieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ -export class SlashTieGlyph extends TieGlyph { - protected startNote: Note; - protected endNote: Note; - - public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { - super(startNote.beat, endNote.beat, forEnd); - this.startNote = startNote; - this.endNote = endNote; - } - - private get _isLeftHandTap() { - return this.startNote === this.endNote; - } - - protected override getTieHeight(startX: number, startY: number, endX: number, endY: number): number { - if (this._isLeftHandTap) { - return this.startNoteRenderer!.smuflMetrics.tieHeight; - } - return super.getTieHeight(startX, startY, endX, endY); - } - - protected override getBeamDirection(_beat: Beat, _noteRenderer: BarRendererBase): BeamDirection { +export class SlashTieGlyph extends NoteTieGlyph { + protected override calculateTieDirection(): BeamDirection { return BeamDirection.Down; } - protected static getBeamDirectionForNote(_note: Note): BeamDirection { - return BeamDirection.Down; - } - - protected override getStartY(): number { - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Center); - } - - protected override getEndY(): number { - return this.getStartY(); - } - - protected override getStartX(): number { - if (this._isLeftHandTap) { - return this.getEndX() - this.renderer.smuflMetrics.leftHandTabTieWidth; - } - return this.startNoteRenderer!.getNoteX(this.startNote, NoteXPosition.Right); + protected override getStartNotePosition() { + return NoteXPosition.Right; } - protected override getEndX(): number { - return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); + protected override getEndNotePosition(): NoteXPosition { + return NoteXPosition.Left; } } diff --git a/packages/alphatab/src/rendering/glyphs/SpacingGlyph.ts b/packages/alphatab/src/rendering/glyphs/SpacingGlyph.ts index 24281720f..7b39851c6 100644 --- a/packages/alphatab/src/rendering/glyphs/SpacingGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SpacingGlyph.ts @@ -9,4 +9,12 @@ export class SpacingGlyph extends Glyph { super(x, y); this.width = width; } + + public override getBoundingBoxTop(): number { + return Number.NaN; + } + + public override getBoundingBoxBottom(): number { + return Number.NaN; + } } diff --git a/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts index af74b0e0a..437899ea0 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts @@ -1,7 +1,10 @@ +import type { Beat } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import { TabBeatGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatGlyph'; +import { TabBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/TabBeatPreNotesGlyph'; import { TabBendGlyph } from '@coderline/alphatab/rendering/glyphs/TabBendGlyph'; import { TabSlideLineGlyph } from '@coderline/alphatab/rendering/glyphs/TabSlideLineGlyph'; import { TabSlurGlyph } from '@coderline/alphatab/rendering/glyphs/TabSlurGlyph'; @@ -16,6 +19,12 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { private _bend: TabBendGlyph | null = null; private _effectSlurs: TabSlurGlyph[] = []; + public constructor(beat: Beat) { + super(beat); + this.preNotes = new TabBeatPreNotesGlyph(); + this.onNotes = new TabBeatGlyph(); + } + protected override drawBeamHelperAsFlags(helper: BeamingHelper): boolean { return helper.hasFlag((this.renderer as TabBarRenderer).drawBeamHelperAsFlags(helper), this.beat); } @@ -36,15 +45,15 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { } const renderer: TabBarRenderer = this.renderer as TabBarRenderer; if (n.isTieOrigin && renderer.showTiedNotes && n.tieDestination!.isVisible) { - const tie: TabTieGlyph = new TabTieGlyph(n, n.tieDestination!, false); + const tie: TabTieGlyph = new TabTieGlyph(`tab.tie.${n.id}`, n, n.tieDestination!, false); this.addTie(tie); } if (n.isTieDestination && renderer.showTiedNotes) { - const tie: TabTieGlyph = new TabTieGlyph(n.tieOrigin!, n, true); + const tie: TabTieGlyph = new TabTieGlyph(`tab.tie.${n.tieOrigin!.id}`, n.tieOrigin!, n, true); this.addTie(tie); } if (n.isLeftHandTapped && !n.isHammerPullDestination) { - const tapSlur: TabTieGlyph = new TabTieGlyph(n, n, false); + const tapSlur: TabTieGlyph = new TabTieGlyph(`tab.tie.leftHandTap.${n.id}`, n, n, false); this.addTie(tapSlur); } // start effect slur on first beat @@ -57,7 +66,13 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { } } if (!expanded) { - const effectSlur: TabSlurGlyph = new TabSlurGlyph(n, n.effectSlurDestination, false, false); + const effectSlur: TabSlurGlyph = new TabSlurGlyph( + `tab.slur.effect.${n.id}`, + n, + n.effectSlurDestination, + false, + false + ); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); } @@ -72,7 +87,13 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { } } if (!expanded) { - const effectSlur: TabSlurGlyph = new TabSlurGlyph(n.effectSlurOrigin, n, false, true); + const effectSlur: TabSlurGlyph = new TabSlurGlyph( + `tab.slur.effect.${n.effectSlurOrigin.id}`, + n.effectSlurOrigin, + n, + false, + true + ); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); } diff --git a/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts index 182550d6a..da19582b6 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts @@ -1,19 +1,19 @@ +import { BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; -import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; +import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { AugmentationDotGlyph } from '@coderline/alphatab/rendering/glyphs/AugmentationDotGlyph'; +import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { NoteNumberGlyph } from '@coderline/alphatab/rendering/glyphs/NoteNumberGlyph'; +import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; import { TabNoteChordGlyph } from '@coderline/alphatab/rendering/glyphs/TabNoteChordGlyph'; import { TabRestGlyph } from '@coderline/alphatab/rendering/glyphs/TabRestGlyph'; -import { TabWhammyBarGlyph } from '@coderline/alphatab/rendering/glyphs/TabWhammyBarGlyph'; +import { TremoloPickingGlyph } from '@coderline/alphatab/rendering/glyphs/TremoloPickingGlyph'; import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; -import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; -import { BeatSubElement } from '@coderline/alphatab/model/Beat'; -import { SlashNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/SlashNoteHeadGlyph'; -import { TremoloPickingGlyph } from '@coderline/alphatab/rendering/glyphs/TremoloPickingGlyph'; /** * @internal @@ -28,6 +28,20 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { } public override getNoteX(note: Note, requestedPosition: NoteXPosition): number { + if (this.slash) { + let pos = this.slash.x; + switch (requestedPosition) { + case NoteXPosition.Left: + break; + case NoteXPosition.Center: + pos += this.slash.width / 2; + break; + case NoteXPosition.Right: + pos += this.slash.width; + break; + } + return pos; + } return this.noteNumbers ? this.noteNumbers.getNoteX(note, requestedPosition) : 0; } @@ -35,12 +49,33 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { return this.noteNumbers ? this.noteNumbers.getNoteY(note, requestedPosition) : 0; } - public override getLowestNoteY(): number { - return this.noteNumbers ? this.noteNumbers.getLowestNoteY() : 0; + public override getRestY(requestedPosition: NoteYPosition): number { + const g = this.restGlyph; + if (g) { + switch (requestedPosition) { + case NoteYPosition.TopWithStem: + return g.getBoundingBoxTop() - this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); + case NoteYPosition.Top: + return g.getBoundingBoxTop(); + case NoteYPosition.Center: + case NoteYPosition.StemUp: + case NoteYPosition.StemDown: + return g.getBoundingBoxTop() + g.height / 2; + case NoteYPosition.Bottom: + return g.getBoundingBoxTop(); + case NoteYPosition.BottomWithStem: + return g.getBoundingBoxBottom() + this.renderer.smuflMetrics.getStemLength(Duration.Quarter, true); + } + } + return 0; } - public override getHighestNoteY(): number { - return this.noteNumbers ? this.noteNumbers.getHighestNoteY() : 0; + public override getLowestNoteY(requestedPosition: NoteYPosition): number { + return this.noteNumbers ? this.noteNumbers.getLowestNoteY(requestedPosition) : 0; + } + + public override getHighestNoteY(requestedPosition: NoteYPosition): number { + return this.noteNumbers ? this.noteNumbers.getHighestNoteY(requestedPosition) : 0; } public override buildBoundingsLookup(beatBounds: BeatBounds, cx: number, cy: number) { @@ -52,7 +87,7 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { public override doLayout(): void { const tabRenderer: TabBarRenderer = this.renderer as TabBarRenderer; - const centeredEffectGlyphs:Glyph[]=[]; + const centeredEffectGlyphs: Glyph[] = []; if (!this.container.beat.isRest) { // // Note numbers @@ -64,25 +99,17 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { if (this.container.beat.slashed && !this.container.beat.notes.some(x => x.isTieDestination as boolean)) { const line = Math.floor((this.renderer.bar.staff.tuning.length - 1) / 2); const slashY = tabRenderer.getLineY(line); - const slashNoteHead = new SlashNoteHeadGlyph( - 0, - slashY, - this.container.beat.duration, - isGrace, - this.container.beat - ); + const slashNoteHead = new SlashNoteHeadGlyph(0, slashY, this.container.beat); slashNoteHead.noteHeadElement = NoteSubElement.GuitarTabFretNumber; slashNoteHead.effectElement = BeatSubElement.GuitarTabEffects; this.slash = slashNoteHead; slashNoteHead.beat = this.container.beat; - slashNoteHead.beamingHelper = this.beamingHelper; this.addNormal(slashNoteHead); beatEffects = slashNoteHead.beatEffects; } else { const tabNoteNumbers = new TabNoteChordGlyph(0, 0, isGrace); this.noteNumbers = tabNoteNumbers; tabNoteNumbers.beat = this.container.beat; - tabNoteNumbers.beamingHelper = this.beamingHelper; for (const note of this.container.beat.notes) { if (note.isVisible) { this._createNoteGlyph(note); @@ -92,20 +119,10 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { beatEffects = tabNoteNumbers.beatEffects; } - // - // Whammy Bar - if (this.container.beat.hasWhammyBar) { - const whammy: TabWhammyBarGlyph = new TabWhammyBarGlyph(this.container.beat); - whammy.renderer = this.renderer; - whammy.doLayout(); - this.container.ties.push(whammy); - } - // // Tremolo Picking if (this.container.beat.isTremolo && !beatEffects.has('tremolo')) { - const speed = this.container.beat.tremoloSpeed!; - const glyph = new TremoloPickingGlyph(0, 0, speed); + const glyph = new TremoloPickingGlyph(0, 0, this.container.beat.tremoloPicking!); glyph.offsetY = this.renderer.smuflMetrics.glyphTop.get(glyph.symbol)!; beatEffects.set('tremolo', glyph); centeredEffectGlyphs.push(glyph); @@ -115,23 +132,18 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { // Note dots // if (this.container.beat.dots > 0 && tabRenderer.rhythmMode !== TabRhythmMode.Hidden) { + const y: number = this.getNoteY(this.container.beat.maxNote!, NoteYPosition.BottomWithStem); + for (let i: number = 0; i < this.container.beat.dots; i++) { - this.addEffect( - new AugmentationDotGlyph( - 0, - tabRenderer.lineOffset * tabRenderer.bar.staff.tuning.length + - tabRenderer.settings.notation.rhythmHeight - ) - ); + this.addEffect(new AugmentationDotGlyph(0, y)); } } } else { const line = Math.floor((this.renderer.bar.staff.tuning.length - 1) / 2); - const y: number = tabRenderer.getTabY(line); + const y: number = tabRenderer.getLineY(line); const restGlyph = new TabRestGlyph(0, y, tabRenderer.showRests, this.container.beat.duration); this.restGlyph = restGlyph; restGlyph.beat = this.container.beat; - restGlyph.beamingHelper = this.beamingHelper; this.addNormal(restGlyph); // // Note dots @@ -157,40 +169,42 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { this.width = w; this.computedWidth = w; if (this.container.beat.isEmpty) { - this.centerX = this.width / 2; + this.onTimeX = this.width / 2; } else if (this.restGlyph) { - this.centerX = this.restGlyph!.x + this.restGlyph!.width / 2; + this.onTimeX = this.restGlyph!.x + this.restGlyph!.width / 2; } else if (this.noteNumbers) { - this.centerX = this.noteNumbers!.x + this.noteNumbers!.noteStringWidth / 2; + this.onTimeX = this.noteNumbers!.x + this.noteNumbers!.noteStringWidth / 2; } else if (this.slash) { - this.centerX = this.slash!.x + this.slash!.width / 2; + this.onTimeX = this.slash!.x + this.slash!.width / 2; } + this.middleX = this.onTimeX; + this.stemX = this.middleX; - for(const g of centeredEffectGlyphs) { - g.x = this.centerX; - } - } - - public override updateBeamingHelper(): void { - if (this.noteNumbers) { - this.noteNumbers.updateBeamingHelper(this.container.x + this.x); - } else if (this.restGlyph) { - this.restGlyph.updateBeamingHelper(this.container.x + this.x); - } else if (this.slash) { - this.slash.updateBeamingHelper(this.container.x + this.x); + for (const g of centeredEffectGlyphs) { + g.x = this.onTimeX; } } private _createNoteGlyph(n: Note): void { const tr: TabBarRenderer = this.renderer as TabBarRenderer; const noteNumberGlyph: NoteNumberGlyph = new NoteNumberGlyph(0, 0, n); - const l: number = n.beat.voice.bar.staff.tuning.length - n.string; - noteNumberGlyph.y = tr.getTabY(l); + const l: number = tr.getNoteLine(n); + noteNumberGlyph.y = tr.getLineY(l); noteNumberGlyph.renderer = this.renderer; noteNumberGlyph.doLayout(); this.noteNumbers!.addNoteGlyph(noteNumberGlyph, n); - const topY = noteNumberGlyph.y - noteNumberGlyph.height / 2; - const bottomY = topY + noteNumberGlyph.height; - this.renderer.helpers.collisionHelper.reserveBeatSlot(this.container.beat, topY, bottomY); + const topY = noteNumberGlyph.getBoundingBoxTop(); + const bottomY = noteNumberGlyph.getBoundingBoxBottom(); + + this.renderer.collisionHelper.reserveBeatSlot(this.container.beat, topY, bottomY); + + const minString = tr.minString; + const maxString = tr.maxString; + if (Number.isNaN(minString) || minString < n.string) { + tr.minString = l; + } + if (Number.isNaN(maxString) || maxString > n.string) { + tr.maxString = l; + } } } diff --git a/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts index 34b1ce2e2..67471154d 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBendGlyph.ts @@ -1,24 +1,25 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; +import { BendPoint } from '@coderline/alphatab/model/BendPoint'; import { BendStyle } from '@coderline/alphatab/model/BendStyle'; import { BendType } from '@coderline/alphatab/model/BendType'; import type { Color } from '@coderline/alphatab/model/Color'; +import type { Font } from '@coderline/alphatab/model/Font'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import { type BarRendererBase, NoteYPosition, NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { VibratoType } from '@coderline/alphatab/model/VibratoType'; +import { TextBaseline, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { type BarRendererBase, NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import { NoteVibratoGlyph } from '@coderline/alphatab/rendering/glyphs/NoteVibratoGlyph'; import { TabBendRenderPoint } from '@coderline/alphatab/rendering/glyphs/TabBendRenderPoint'; +import type { ITieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; -import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; -import { BendPoint } from '@coderline/alphatab/model/BendPoint'; -import { VibratoType } from '@coderline/alphatab/model/VibratoType'; -import { NoteVibratoGlyph } from '@coderline/alphatab/rendering/glyphs/NoteVibratoGlyph'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; /** * @internal */ -export class TabBendGlyph extends Glyph { +export class TabBendGlyph extends Glyph implements ITieGlyph { private _notes: Note[] = []; private _renderPoints: Map = new Map(); private _preBendMinValue: number = -1; @@ -29,6 +30,8 @@ export class TabBendGlyph extends Glyph { private _releaseContinuedMinValue: number = -1; private _maxBendValue: number = -1; + public readonly checkForOverflow = false; + public constructor() { super(0, 0); } @@ -126,8 +129,9 @@ export class TabBendGlyph extends Glyph { public override doLayout(): void { super.doLayout(); - const bendHeight: number = this._maxBendValue * this.renderer.smuflMetrics.tabBendPerValueHeight; - this.renderer.registerOverflowTop(bendHeight); + + this._calculateAndRegisterOverflow(); + let value: number = 0; for (const note of this._notes) { const renderPoints: TabBendRenderPoint[] = this._renderPoints.get(note.id)!; @@ -177,6 +181,22 @@ export class TabBendGlyph extends Glyph { }); } + private _calculateAndRegisterOverflow() { + const res = this.renderer.resources; + const smufl = this.renderer.smuflMetrics; + let bendHeight: number = this._maxBendValue * smufl.tabBendPerValueHeight; + + bendHeight += smufl.tabBendStaffPadding; + + // account for space + const canvas = this.renderer.scoreRenderer.canvas!; + canvas.font = res.tablatureFont; + const size = canvas.measureText('full'); + bendHeight += size.height + res.engravingSettings.tabBendLabelPadding; + + this.renderer.registerOverflowTop(bendHeight); + } + private _createRenderingPoints(note: Note): TabBendRenderPoint[] { const renderingPoints: TabBendRenderPoint[] = []; // Guitar Pro Rendering Note: @@ -214,18 +234,16 @@ export class TabBendGlyph extends Glyph { canvas.color = this.renderer.resources.secondaryGlyphColor; } for (const note of this._notes) { - const renderPoints: TabBendRenderPoint[] = this._renderPoints.get(note.id)!; - const startNoteRenderer: BarRendererBase = this.renderer; - let endNote: Note = note; - let isMultiBeatBend: boolean = false; + const startNoteRenderer = this.renderer as TabBarRenderer; + let endNote = note; + let isMultiBeatBend = false; let endNoteRenderer: BarRendererBase | null = null; - let endNoteHasBend: boolean = false; - const slurText: string = note.bendStyle === BendStyle.Gradual ? 'grad.' : ''; + let endNoteHasBend = false; let endBeat: Beat | null = null; while (endNote.isTieOrigin) { - const nextNote: Note = endNote.tieDestination!; + const nextNote = endNote.tieDestination!; endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, nextNote.beat.voice.bar ); if (!endNoteRenderer || startNoteRenderer.staff !== endNoteRenderer.staff) { @@ -245,7 +263,7 @@ export class TabBendGlyph extends Glyph { endBeat = endNote.beat; endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, endBeat.voice.bar ) as TabBarRenderer; if ( @@ -255,133 +273,123 @@ export class TabBendGlyph extends Glyph { ) { endBeat = null; } - let startX: number = 0; - let endX: number = 0; - const topY: number = cy + startNoteRenderer.y; - const tabBendArrowSize = this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.ArrowheadBlackDown)!; - startX = cx + startNoteRenderer.x; + + const smufl = startNoteRenderer.smuflMetrics; + const tabBendArrowSize = smufl.glyphWidths.get(MusicFontSymbol.ArrowheadBlackDown)!; + + const topY: number = cy + startNoteRenderer.y - smufl.tabBendStaffPadding; + + let startX = cx + startNoteRenderer.x; + const renderPoints = this._renderPoints.get(note.id)!; if (renderPoints[0].value > 0 || note.isContinuedBend) { startX += startNoteRenderer.getBeatX(note.beat, BeatXPosition.MiddleNotes); } else { startX += startNoteRenderer.getNoteX(note, NoteXPosition.Right); } + + let endX: number = 0; if (!endBeat || (endBeat.isLastOfVoice && !endNoteHasBend)) { endX = cx + endNoteRenderer!.x + endNoteRenderer!.postBeatGlyphsStart; + endX -= this.renderer.smuflMetrics.postNoteEffectPadding; } else if (endNoteHasBend || !endBeat.nextBeat) { endX = cx + endNoteRenderer!.x + endNoteRenderer!.getBeatX(endBeat, BeatXPosition.MiddleNotes); } else if (note.bendType === BendType.Hold) { endX = cx + endNoteRenderer!.x + endNoteRenderer!.getBeatX(endBeat.nextBeat, BeatXPosition.OnNotes); } else { endX = cx + endNoteRenderer!.x + endNoteRenderer!.getBeatX(endBeat.nextBeat, BeatXPosition.PreNotes); + endX -= this.renderer.smuflMetrics.postNoteEffectPadding; } - if (!isMultiBeatBend) { - endX -= tabBendArrowSize; - } - // we need some pixels for the arrow. otherwise we might draw into the next - // note - const width: number = endX - startX; - // calculate offsets per step - const dX: number = width / BendPoint.MaxPosition; - canvas.beginPath(); - for (let i: number = 0, j: number = renderPoints.length - 1; i < j; i++) { - const firstPt: TabBendRenderPoint = renderPoints[i]; - let secondPt: TabBendRenderPoint = renderPoints[i + 1]; - // draw pre-bend if previous - if (i === 0 && firstPt.value !== 0 && !note.isTieDestination) { - this._paintBend(note, new TabBendRenderPoint(0, 0), firstPt, startX, topY, dX, slurText, canvas); - } - if (note.bendType !== BendType.Prebend) { - if (i === 0) { - startX += this.renderer.smuflMetrics.postNoteEffectPadding; - } - this._paintBend(note, firstPt, secondPt, startX, topY, dX, slurText, canvas); - } else if (note.isTieOrigin && note.tieDestination!.hasBend) { - secondPt = new TabBendRenderPoint(BendPoint.MaxPosition, firstPt.value); - secondPt.lineValue = firstPt.lineValue; - this._paintBend(note, firstPt, secondPt, startX, topY, dX, slurText, canvas); - } + // we need some pixels for the arrow. otherwise we might draw into the next + if (!isMultiBeatBend) { + endX -= tabBendArrowSize / 2; } - if (endNote.vibrato !== VibratoType.None) { - const vibratoStartX = endX - cx + tabBendArrowSize - endNoteRenderer.x; - const vibratoStartY: number = - topY - - cy - - this.renderer.smuflMetrics.tabBendPerValueHeight * renderPoints[renderPoints.length - 1].lineValue; - - const vibrato = new NoteVibratoGlyph(vibratoStartX, vibratoStartY, endNote.vibrato); - vibrato.beat = endNote.beat; - vibrato.renderer = endNoteRenderer; - vibrato.doLayout(); - vibrato.paint(cx + endNoteRenderer.x, cy, canvas); - } + this._paintBendLines(canvas, startX, topY, endX, startNoteRenderer, note, renderPoints); + this._paintBendVibrato( + canvas, + cx, + endX + tabBendArrowSize / 2, + topY - smufl.tabBendPerValueHeight * renderPoints[renderPoints.length - 1].lineValue, + endNoteRenderer, + endNote + ); canvas.color = color; } } - private _paintBend( - note: Note, - firstPt: TabBendRenderPoint, - secondPt: TabBendRenderPoint, + private _paintBendVibrato( + canvas: ICanvas, cx: number, - cy: number, - dX: number, - slurText: string, - canvas: ICanvas - ): void { - const r: TabBarRenderer = this.renderer as TabBarRenderer; - const res: RenderingResources = this.renderer.resources; - const overflowOffset: number = r.lineOffset / 2; - const x1: number = cx + dX * firstPt.offset; - const bendValueHeight: number = this.renderer.smuflMetrics.tabBendPerValueHeight; - let y1: number = cy - bendValueHeight * firstPt.lineValue; - if (firstPt.value === 0) { - if (secondPt.offset === firstPt.offset) { - y1 += r.getNoteY(note.beat.maxStringNote!, NoteYPosition.Top) - overflowOffset / 2; - } else { - y1 += r.getNoteY(note, NoteYPosition.Center); - } - } else { - y1 += overflowOffset; - } - const x2: number = cx + dX * secondPt.offset; - let y2: number = cy - bendValueHeight * secondPt.lineValue; - if (secondPt.lineValue === 0) { - y2 += r.getNoteY(note, NoteYPosition.Center); - } else { - y2 += overflowOffset; + vibratoX: number, + topY: number, + endNoteRenderer: BarRendererBase, + endNote: Note + ) { + if (endNote.isTieDestination && endNote.vibrato !== VibratoType.None && !endNote.hasBend) { + const vibratoEndX = cx + endNoteRenderer.x; + const vibratoStartX = vibratoX - vibratoEndX; + const vibrato = new NoteVibratoGlyph(vibratoStartX, 0, endNote.vibrato); + + vibrato.beat = endNote.beat; + vibrato.renderer = endNoteRenderer; + vibrato.doLayout(); + vibrato.paint(vibratoEndX, topY, canvas); } - // what type of arrow? (up/down) - let arrowOffset: number = 0; - const arrowSize = this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.ArrowheadBlackDown)!; - if (secondPt.value > firstPt.value) { - if (y2 + arrowSize > y1) { - y2 = y1 - arrowSize; + } + + private _paintBendLines( + canvas: ICanvas, + cx: number, + cy: number, + endX: number, + noteRenderer: TabBarRenderer, + note: Note, + renderPoints: TabBendRenderPoint[] + ) { + const l = canvas.lineWidth; + const res = this.renderer.resources; + const bl = canvas.textBaseline; + canvas.textBaseline = TextBaseline.Alphabetic; + canvas.lineWidth = res.engravingSettings.arrowShaftThickness; + + // calculate offsets per step + const width: number = endX - cx; + const dX: number = width / BendPoint.MaxPosition; + for (let i: number = 0, j: number = renderPoints.length - 1; i < j; i++) { + const firstPt: TabBendRenderPoint = renderPoints[i]; + let secondPt: TabBendRenderPoint = renderPoints[i + 1]; + // draw pre-bend if previous + if (i === 0 && firstPt.value !== 0 && !note.isTieDestination) { + this._paintBend(canvas, cx, cy, dX, noteRenderer, note, new TabBendRenderPoint(0, 0), firstPt); } - canvas.beginPath(); - canvas.moveTo(x2, y2); - canvas.lineTo(x2 - arrowSize * 0.5, y2 + arrowSize); - canvas.lineTo(x2 + arrowSize * 0.5, y2 + arrowSize); - canvas.closePath(); - canvas.fill(); - arrowOffset = arrowSize; - } else if (secondPt.value !== firstPt.value) { - if (y2 < y1) { - y2 = y1 + arrowSize; + if (note.bendType !== BendType.Prebend) { + if (i === 0) { + cx += this.renderer.smuflMetrics.postNoteEffectPadding; + } + this._paintBend(canvas, cx, cy, dX, noteRenderer, note, firstPt, secondPt); + } else if (note.isTieOrigin && note.tieDestination!.hasBend) { + secondPt = new TabBendRenderPoint(BendPoint.MaxPosition, firstPt.value); + secondPt.lineValue = firstPt.lineValue; + + this._paintBend(canvas, cx, cy, dX, noteRenderer, note, firstPt, secondPt); } - canvas.beginPath(); - canvas.moveTo(x2, y2); - canvas.lineTo(x2 - arrowSize * 0.5, y2 - arrowSize); - canvas.lineTo(x2 + arrowSize * 0.5, y2 - arrowSize); - canvas.closePath(); - canvas.fill(); - arrowOffset = -arrowSize; } - const l = canvas.lineWidth; - canvas.lineWidth = this.renderer.smuflMetrics.arrowShaftThickness; - canvas.beginPath(); + + canvas.lineWidth = l; + canvas.textBaseline = bl; + } + + private _paintBendLine( + canvas: ICanvas, + x1: number, + y1: number, + x2: number, + y2: number, + firstPt: TabBendRenderPoint, + secondPt: TabBendRenderPoint + ) { if (firstPt.value === secondPt.value) { // draw horizontal dashed line // to really have the line ending at the right position @@ -405,6 +413,10 @@ export class TabBendGlyph extends Glyph { } } else { if (x2 > x1) { + const isUp = secondPt.value > firstPt.value; + // small offset to have line inside arrow head and not showing in tip + const arrowOffset = isUp ? 3 : -3; + // draw bezier line from first to second point canvas.moveTo(x1, y1); canvas.bezierCurveTo((x1 + x2) / 2, y1, x2, y1, x2, y2 + arrowOffset); @@ -415,9 +427,77 @@ export class TabBendGlyph extends Glyph { canvas.stroke(); } } - if (slurText && firstPt.offset < secondPt.offset) { - canvas.font = res.graceFont; + } + + private _paintBend( + canvas: ICanvas, + cx: number, + cy: number, + dX: number, + noteRenderer: TabBarRenderer, + note: Note, + firstPt: TabBendRenderPoint, + secondPt: TabBendRenderPoint + ): void { + const noteNumberAndLinePaddingY = noteRenderer.lineOffset / 2; + const res = noteRenderer.resources; + const smufl = res.engravingSettings; + + const x1 = cx + dX * firstPt.offset; + let y1: number; + if (firstPt.value === 0) { + y1 = cy + smufl.tabBendStaffPadding; + if (secondPt.offset === firstPt.offset) { + y1 += noteRenderer.getNoteY(note.beat.maxStringNote!, NoteYPosition.Top) - noteNumberAndLinePaddingY; + } else { + y1 += noteRenderer.getNoteY(note, NoteYPosition.Center); + } + } else { + y1 = cy - smufl.tabBendPerValueHeight * firstPt.lineValue; + } + + const x2 = cx + dX * secondPt.offset; + let y2: number; + if (secondPt.lineValue === 0) { + y2 = cy + smufl.tabBendStaffPadding + noteRenderer.getNoteY(note.beat.maxStringNote!, NoteYPosition.Center); + } else { + y2 = cy - smufl.tabBendPerValueHeight * secondPt.lineValue; + } + + this._paintBendLine(canvas, x1, y1, x2, y2, firstPt, secondPt); + + const arrowSize = smufl.glyphWidths.get(MusicFontSymbol.ArrowheadBlackDown)!; + if (firstPt.value !== secondPt.value) { + const up = secondPt.value > firstPt.value; + this._paintBendLineArrow(canvas, x2, y2, y1, arrowSize, up); + + this._paintBendLineSlurText(canvas, x1, y1, x2, y2, note, res.graceFont); + this._paintBendLineValueText( + canvas, + y1, + x2, + y2 - smufl.tabBendLabelPadding, + firstPt, + secondPt, + res.tablatureFont + ); + } + } + + private _paintBendLineSlurText( + canvas: ICanvas, + x1: number, + y1: number, + x2: number, + y2: number, + note: Note, + font: Font + ) { + if (note.bendStyle === BendStyle.Gradual) { + const slurText = 'grad.'; const size = canvas.measureText(slurText); + canvas.font = font; + let y: number = 0; let x: number = 0; if (y1 > y2) { @@ -430,41 +510,71 @@ export class TabBendGlyph extends Glyph { } canvas.fillText(slurText, x, y); } - if (secondPt.value !== 0 && firstPt.value !== secondPt.value) { - let dV: number = secondPt.value; - const up: boolean = secondPt.value > firstPt.value; - dV = Math.abs(dV); + } + + private _paintBendLineValueText( + canvas: ICanvas, + y1: number, + x2: number, + y2: number, + firstPt: TabBendRenderPoint, + secondPt: TabBendRenderPoint, + font: Font + ) { + if (secondPt.value !== 0) { + const up = secondPt.value > firstPt.value; + + // // calculate label - let s: string = ''; + + let bendValue = secondPt.value; + let bendValueText = ''; // Full Steps - if (dV === 4) { - s = 'full'; - dV -= 4; - } else if (dV >= 4 || dV <= -4) { - const steps: number = (dV / 4) | 0; - s += steps; + if (bendValue === 4) { + bendValueText = 'full'; + bendValue -= 4; + } else if (bendValue >= 4 || bendValue <= -4) { + const steps = (bendValue / 4) | 0; + bendValueText += steps; // Quaters - dV -= steps * 4; + bendValue -= steps * 4; } - if (dV > 0) { - s += TabBendGlyph.getFractionSign(dV); + if (bendValue > 0) { + bendValueText += TabBendGlyph.getFractionSign(bendValue); } - if (s !== '') { - y2 = cy - bendValueHeight * secondPt.value; - let startY: number = y2; - if (!up) { - startY = y1 + (Math.abs(y2 - y1) * 1) / 3; - } - // draw label - canvas.font = res.tablatureFont; - const size = canvas.measureText(s); - const y: number = startY - size.height / 1.5; - const x: number = x2 - size.width / 2; - canvas.fillText(s, x, y - res.engravingSettings.tabBendLabelPadding); + + if (bendValueText !== '') { + const textY = up ? y2 : y1 + (Math.abs(y2 - y1) * 1) / 3; + canvas.font = font; + const size = canvas.measureText(bendValueText); + const textX = x2 - size.width / 2; + canvas.fillText(bendValueText, textX, textY); } } - - canvas.lineWidth = l; + } + private _paintBendLineArrow(canvas: ICanvas, x2: number, y2: number, y1: number, arrowSize: number, up: boolean) { + if (up) { + // shift arrow up in narrow cases + if (y2 + arrowSize > y1) { + y2 = y1 - arrowSize; + } + canvas.beginPath(); + canvas.moveTo(x2, y2); + canvas.lineTo(x2 - arrowSize * 0.5, y2 + arrowSize); + canvas.lineTo(x2 + arrowSize * 0.5, y2 + arrowSize); + canvas.closePath(); + canvas.fill(); + } else { + if (y2 < y1) { + y2 = y1 + arrowSize; + } + canvas.beginPath(); + canvas.moveTo(x2, y2); + canvas.lineTo(x2 - arrowSize * 0.5, y2 - arrowSize); + canvas.lineTo(x2 + arrowSize * 0.5, y2 - arrowSize); + canvas.closePath(); + canvas.fill(); + } } public static getFractionSign(steps: number): string { diff --git a/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts index 095acf803..1e9b03d90 100644 --- a/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts @@ -1,14 +1,15 @@ import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; import type { Note } from '@coderline/alphatab/model/Note'; import { type ICanvas, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; +import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import type { NoteNumberGlyph } from '@coderline/alphatab/rendering/glyphs/NoteNumberGlyph'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; -import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; -import { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import { TremoloPickingGlyph } from '@coderline/alphatab/rendering/glyphs/TremoloPickingGlyph'; import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds'; -import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; +import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; /** * @internal @@ -19,7 +20,6 @@ export class TabNoteChordGlyph extends Glyph { private _isGrace: boolean; public beat!: Beat; - public beamingHelper!: BeamingHelper; public maxStringNote: Note | null = null; public minStringNote: Note | null = null; public beatEffects: Map = new Map(); @@ -57,12 +57,12 @@ export class TabNoteChordGlyph extends Glyph { return 0; } - public getLowestNoteY(): number { - return this.maxStringNote ? this.getNoteY(this.maxStringNote, NoteYPosition.Center) : 0; + public getLowestNoteY(requestedPosition: NoteYPosition): number { + return this.maxStringNote ? this.getNoteY(this.maxStringNote, requestedPosition) : 0; } - public getHighestNoteY(): number { - return this.minStringNote ? this.getNoteY(this.minStringNote, NoteYPosition.Center) : 0; + public getHighestNoteY(requestedPosition: NoteYPosition): number { + return this.minStringNote ? this.getNoteY(this.minStringNote, requestedPosition) : 0; } public getNoteY(note: Note, requestedPosition: NoteYPosition): number { @@ -72,18 +72,27 @@ export class TabNoteChordGlyph extends Glyph { switch (requestedPosition) { case NoteYPosition.Top: - case NoteYPosition.TopWithStem: pos -= n.height / 2; break; + case NoteYPosition.StemUp: + pos = this.y + n.getBoundingBoxTop(); + break; case NoteYPosition.Center: break; case NoteYPosition.Bottom: - case NoteYPosition.BottomWithStem: pos += n.height / 2; break; - - case NoteYPosition.StemUp: case NoteYPosition.StemDown: + pos = this.y + n.getBoundingBoxBottom(); + break; + case NoteYPosition.TopWithStem: + pos = -this.renderer.settings.notation.rhythmHeight; + pos -= this.calculateTremoloHeightForStem(); + break; + + case NoteYPosition.BottomWithStem: + pos = this.renderer.height + this.renderer.settings.notation.rhythmHeight; + pos += this.calculateTremoloHeightForStem(); break; } @@ -92,6 +101,19 @@ export class TabNoteChordGlyph extends Glyph { return 0; } + public calculateTremoloHeightForStem() { + const beat = this.beat; + if (!beat.isTremolo) { + return 0; + } + if (beat.duration <= Duration.Quarter) { + return 0; + } + const symbol = TremoloPickingGlyph._getSymbol(beat.tremoloPicking!); + const smufl = this.renderer.smuflMetrics; + return smufl.glyphHeights.has(symbol) ? smufl.glyphHeights.get(symbol)! : 0; + } + public override doLayout(): void { let w: number = 0; @@ -116,15 +138,30 @@ export class TabNoteChordGlyph extends Glyph { } this.noteStringWidth = noteStringWidth; const tabHeight: number = this.renderer.resources.tablatureFont.size; + + let minEffectY = Number.NaN; + let maxEffectY = Number.NaN; + let effectY: number = this.getNoteY(this.minStringNote!, NoteYPosition.Center) + tabHeight / 2; - // TODO: take care of actual glyph height const effectSpacing: number = this.renderer.smuflMetrics.onNoteEffectPadding; for (const g of this.beatEffects.values()) { g.y += effectY; g.x += this.width / 2; g.renderer = this.renderer; - effectY += g.height + effectSpacing; + g.doLayout(); + effectY += g.height + effectSpacing; + + if (Number.isNaN(minEffectY) || minEffectY > effectY) { + minEffectY = effectY; + } + if (Number.isNaN(maxEffectY) || maxEffectY < effectY) { + maxEffectY = effectY; + } + } + + if (!Number.isNaN(minEffectY)) { + this.renderer.registerBeatEffectOverflows(minEffectY, maxEffectY); } } @@ -169,15 +206,4 @@ export class TabNoteChordGlyph extends Glyph { } } } - - public updateBeamingHelper(cx: number): void { - if (this.beamingHelper && this.beamingHelper.isPositionFrom('tab', this.beat)) { - this.beamingHelper.registerBeatLineX( - 'tab', - this.beat, - cx + this.x + this.width / 2, - cx + this.x + this.width / 2 - ); - } - } } diff --git a/packages/alphatab/src/rendering/glyphs/TabRestGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabRestGlyph.ts index db47b0ab7..ffc1710e6 100644 --- a/packages/alphatab/src/rendering/glyphs/TabRestGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabRestGlyph.ts @@ -3,7 +3,6 @@ import type { Duration } from '@coderline/alphatab/model/Duration'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import { ScoreRestGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreRestGlyph'; -import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** @@ -11,7 +10,6 @@ import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementS */ export class TabRestGlyph extends MusicFontGlyph { private _isVisibleRest: boolean; - public beamingHelper!: BeamingHelper; public constructor(x: number, y: number, isVisibleRest: boolean, duration: Duration) { super(x, y, 1, ScoreRestGlyph.getSymbol(duration)); @@ -22,17 +20,6 @@ export class TabRestGlyph extends MusicFontGlyph { super.doLayout(); } - public updateBeamingHelper(cx: number): void { - if (this.beamingHelper && this.beamingHelper.isPositionFrom('tab', this.beat!)) { - this.beamingHelper.registerBeatLineX( - 'tab', - this.beat!, - cx + this.x + this.width / 2, - cx + this.x + this.width / 2 - ); - } - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { if (this._isVisibleRest) { using _ = ElementStyleHelper.beat(canvas, BeatSubElement.GuitarTabRests, this.beat!); diff --git a/packages/alphatab/src/rendering/glyphs/TabSlideLineGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabSlideLineGlyph.ts index 9127f791b..53c4011f0 100644 --- a/packages/alphatab/src/rendering/glyphs/TabSlideLineGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabSlideLineGlyph.ts @@ -8,17 +8,21 @@ import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import type { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import { NoteVibratoGlyph } from '@coderline/alphatab/rendering/glyphs/NoteVibratoGlyph'; +import type { ITieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; /** * @internal */ -export class TabSlideLineGlyph extends Glyph { +export class TabSlideLineGlyph extends Glyph implements ITieGlyph { private _inType: SlideInType; private _outType: SlideOutType; private _startNote: Note; private _parent: BeatContainerGlyph; + // the slide line cannot overflow anything and there are ties drawn in here + public readonly checkForOverflow = false; + public constructor(inType: SlideInType, outType: SlideOutType, startNote: Note, parent: BeatContainerGlyph) { super(0, 0); this._inType = inType; @@ -39,7 +43,7 @@ export class TabSlideLineGlyph extends Glyph { private _paintSlideIn(cx: number, cy: number, canvas: ICanvas): void { const startNoteRenderer: TabBarRenderer = this.renderer as TabBarRenderer; const sizeX: number = this.renderer.smuflMetrics.simpleSlideWidth; - const sizeY: number = this.renderer.smuflMetrics.simpleSlideHeight; + const sizeY: number = this.renderer.smuflMetrics.simpleSlideHeight; let startX: number = 0; let startY: number = 0; let endX: number = 0; @@ -52,7 +56,11 @@ export class TabSlideLineGlyph extends Glyph { startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Left) - offsetX; - endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) - sizeY; + endY = + cy + + startNoteRenderer.y + + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) - + sizeY; startX = endX - sizeX; startY = cy + @@ -66,7 +74,11 @@ export class TabSlideLineGlyph extends Glyph { startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Left) - offsetX; - endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) + sizeY; + endY = + cy + + startNoteRenderer.y + + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) + + sizeY; startX = endX - sizeX; startY = cy + @@ -83,14 +95,14 @@ export class TabSlideLineGlyph extends Glyph { private _paintSlideOut(cx: number, cy: number, canvas: ICanvas): void { const startNoteRenderer: TabBarRenderer = this.renderer as TabBarRenderer; const sizeX: number = this.renderer.smuflMetrics.simpleSlideWidth; - const sizeY: number = this.renderer.smuflMetrics.simpleSlideHeight; + const sizeY: number = this.renderer.smuflMetrics.simpleSlideHeight; let startX: number = 0; let startY: number = 0; let endX: number = 0; let endY: number = 0; let waves: boolean = false; - const offsetX = this.renderer.smuflMetrics.postNoteEffectPadding; + const offsetX = this.renderer.smuflMetrics.postNoteEffectPadding; switch (this._outType) { case SlideOutType.Shift: @@ -104,7 +116,7 @@ export class TabSlideLineGlyph extends Glyph { if (this._startNote.slideTarget) { const endNoteRenderer: BarRendererBase | null = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, this._startNote.slideTarget.beat.voice.bar ); if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { @@ -140,7 +152,11 @@ export class TabSlideLineGlyph extends Glyph { startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + offsetX; - startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) + sizeY; + startY = + cy + + startNoteRenderer.y + + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) + + sizeY; endX = startX + sizeX; endY = cy + @@ -154,7 +170,11 @@ export class TabSlideLineGlyph extends Glyph { startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + offsetX; - startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) - sizeY; + startY = + cy + + startNoteRenderer.y + + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) - + sizeY; endX = startX + sizeX; endY = cy + diff --git a/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts index 01f8576b5..97ba66030 100644 --- a/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts @@ -1,25 +1,20 @@ import type { Note } from '@coderline/alphatab/model/Note'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { TabTieGlyph } from '@coderline/alphatab/rendering/glyphs/TabTieGlyph'; -import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ export class TabSlurGlyph extends TabTieGlyph { - private _direction: BeamDirection; private _forSlide: boolean; - public constructor(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean = false) { - super(startNote, endNote, forEnd); - this._direction = TabTieGlyph.getBeamDirectionForNote(startNote); + public constructor(slurEffectId: string, startNote: Note, endNote: Note, forSlide: boolean, forEnd:boolean) { + super(slurEffectId, startNote, endNote, forEnd); this._forSlide = forSlide; } - protected override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { - return Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight / 2; + public override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { + return (Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2; } public tryExpand(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean): boolean { @@ -27,9 +22,6 @@ export class TabSlurGlyph extends TabTieGlyph { if (this._forSlide !== forSlide) { return false; } - if (this.forEnd !== forEnd) { - return false; - } // same start and endbeat if (this.startNote.beat.id !== startNote.beat.id) { return false; @@ -37,48 +29,33 @@ export class TabSlurGlyph extends TabTieGlyph { if (this.endNote.beat.id !== endNote.beat.id) { return false; } + const isForEnd = this.renderer === this.lookupEndBeatRenderer(); + if (isForEnd !== forEnd) { + return false; + } // same draw direction - if (this._direction !== TabTieGlyph.getBeamDirectionForNote(startNote)) { + if (this.tieDirection !== TabTieGlyph.getBeamDirectionForNote(startNote)) { return false; } // if we can expand, expand in correct direction - switch (this._direction) { + switch (this.tieDirection) { case BeamDirection.Up: if (startNote.realValue > this.startNote.realValue) { this.startNote = startNote; - this.startBeat = startNote.beat; } if (endNote.realValue > this.endNote.realValue) { this.endNote = endNote; - this.endBeat = endNote.beat; } break; case BeamDirection.Down: if (startNote.realValue < this.startNote.realValue) { this.startNote = startNote; - this.startBeat = startNote.beat; } if (endNote.realValue < this.endNote.realValue) { this.endNote = endNote; - this.endBeat = endNote.beat; } break; } return true; } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - const startNoteRenderer: BarRendererBase = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, - this.startBeat!.voice.bar - )!; - const direction: BeamDirection = this.getBeamDirection(this.startBeat!, startNoteRenderer); - const slurId: string = `tab.slur.${this.startNote.beat.id}.${this.endNote.beat.id}.${direction}`; - const renderer: TabBarRenderer = this.renderer as TabBarRenderer; - const isSlurRendered: boolean = renderer.staff.getSharedLayoutData(slurId, false); - if (!isSlurRendered) { - renderer.staff.setSharedLayoutData(slurId, true); - super.paint(cx, cy, canvas); - } - } } diff --git a/packages/alphatab/src/rendering/glyphs/TabTieGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabTieGlyph.ts index def857990..4e5963185 100644 --- a/packages/alphatab/src/rendering/glyphs/TabTieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabTieGlyph.ts @@ -1,35 +1,13 @@ -import type { Beat } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; -import { type BarRendererBase, NoteYPosition, NoteXPosition } from '@coderline/alphatab/rendering/BarRendererBase'; -import { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import { NoteTieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ -export class TabTieGlyph extends TieGlyph { - protected startNote: Note; - protected endNote: Note; - - public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { - super(startNote.beat, endNote.beat, forEnd); - this.startNote = startNote; - this.endNote = endNote; - } - - private get _isLeftHandTap() { - return this.startNote === this.endNote; - } - - protected override getTieHeight(startX: number, startY: number, endX: number, endY: number): number { - if (this._isLeftHandTap) { - return this.startNoteRenderer!.smuflMetrics.tieHeight; - } - return super.getTieHeight(startX, startY, endX, endY); - } - - protected override getBeamDirection(_beat: Beat, _noteRenderer: BarRendererBase): BeamDirection { - if (this._isLeftHandTap) { +export class TabTieGlyph extends NoteTieGlyph { + protected override calculateTieDirection(): BeamDirection { + if (this.isLeftHandTap) { return BeamDirection.Up; } return TabTieGlyph.getBeamDirectionForNote(this.startNote); @@ -38,33 +16,4 @@ export class TabTieGlyph extends TieGlyph { protected static getBeamDirectionForNote(note: Note): BeamDirection { return note.string > 3 ? BeamDirection.Up : BeamDirection.Down; } - - protected override getStartY(): number { - if (this._isLeftHandTap) { - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Center); - } - - if (this.tieDirection === BeamDirection.Up) { - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Top); - } - return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Bottom); - } - - protected override getEndY(): number { - return this.getStartY(); - } - - protected override getStartX(): number { - if (this._isLeftHandTap) { - return this.getEndX() - this.renderer.smuflMetrics.leftHandTabTieWidth; - } - return this.startNoteRenderer!.getNoteX(this.startNote, NoteXPosition.Center); - } - - protected override getEndX(): number { - if (this._isLeftHandTap) { - return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); - } - return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Center); - } } diff --git a/packages/alphatab/src/rendering/glyphs/TabTimeSignatureGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabTimeSignatureGlyph.ts index 10013c6df..27c888365 100644 --- a/packages/alphatab/src/rendering/glyphs/TabTimeSignatureGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabTimeSignatureGlyph.ts @@ -1,7 +1,7 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { BarSubElement } from '@coderline/alphatab/model/Bar'; import { TimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/TimeSignatureGlyph'; import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; -import { NoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NoteHeadGlyph'; -import { BarSubElement } from '@coderline/alphatab/model/Bar'; /** * @internal @@ -19,7 +19,7 @@ export class TabTimeSignatureGlyph extends TimeSignatureGlyph { protected get numberScale(): number { const renderer: TabBarRenderer = this.renderer as TabBarRenderer; if (renderer.bar.staff.tuning.length <= 4) { - return NoteHeadGlyph.GraceScale; + return EngravingSettings.GraceScale; } return 1; } diff --git a/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts index eef43510e..845d43169 100644 --- a/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts @@ -2,26 +2,28 @@ import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import { BendPoint } from '@coderline/alphatab/model/BendPoint'; import { BendStyle } from '@coderline/alphatab/model/BendStyle'; import { WhammyType } from '@coderline/alphatab/model/WhammyType'; -import { NotationMode, NotationElement } from '@coderline/alphatab/NotationSettings'; +import { NotationElement, NotationMode } from '@coderline/alphatab/NotationSettings'; import { type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; -import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { TabBendGlyph } from '@coderline/alphatab/rendering/glyphs/TabBendGlyph'; -import type { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; +import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** * @internal */ -// TODO: make part of effect bar renderers -export class TabWhammyBarGlyph extends Glyph { - private static readonly _topOffsetSharedDataKey: string = 'tab.whammy.topoffset'; - private static readonly _bottomOffsetSharedDataKey: string = 'tab.whammy.bottomffset'; +export class TabWhammyBarGlyph extends EffectGlyph { private _beat: Beat; private _renderPoints: BendPoint[]; private _isSimpleDip: boolean = false; + public originalTopOffset = 0; + public originalBottomOffset = 0; + public topOffset = 0; + public bottomOffset = 0; + public constructor(beat: Beat) { super(0, 0); this._beat = beat; @@ -58,58 +60,55 @@ export class TabWhammyBarGlyph extends Glyph { public override doLayout(): void { super.doLayout(); + if (this._beat.whammyBarType === WhammyType.Custom) { + return; + } + this._isSimpleDip = this.renderer.settings.notation.notationMode === NotationMode.SongBook && this._beat.whammyBarType === WhammyType.Dip; - // - // Get the min and max values for all combined whammys - let minValue: BendPoint | null = null; - let maxValue: BendPoint | null = null; - let beat: Beat | null = this._beat; - while (beat && beat.hasWhammyBar) { - if (!minValue || minValue.value > beat.minWhammyPoint!.value) { - minValue = beat.minWhammyPoint; - } - if (!maxValue || maxValue.value < beat.maxWhammyPoint!.value) { - maxValue = beat.maxWhammyPoint; - } - beat = beat.nextBeat; - } - let topOffset: number = maxValue!.value > 0 ? Math.abs(this._getOffset(maxValue!.value)) : 0; + + const minValue: BendPoint | null = this._beat.minWhammyPoint; + const maxValue: BendPoint | null = this._beat.maxWhammyPoint; + + let topY: number = maxValue!.value > 0 ? -this._getOffset(maxValue!.value) : 0; + let bottomY: number = minValue!.value < 0 ? -this._getOffset(minValue!.value) : 0; + + const c = this.renderer.scoreRenderer.canvas!; + c.font = this.renderer.resources.tablatureFont; + const labelMeasure = c.measureText('-1'); + + const labelSize = labelMeasure.height + this.renderer.smuflMetrics.tabWhammyTextPadding; + if ( - topOffset > 0 || + topY !== 0 || this._beat.whammyBarPoints![0].value !== 0 || this.renderer.settings.notation.isNotationElementVisible(NotationElement.ZerosOnDiveWhammys) ) { - topOffset += this.renderer.resources.tablatureFont.size + this.renderer.smuflMetrics.tabWhammyTextPadding; + topY -= labelSize; } - const bottomOffset: number = minValue!.value < 0 ? Math.abs(this._getOffset(minValue!.value)) : 0; - - const currentTopOffset: number = this.renderer.staff.getSharedLayoutData( - TabWhammyBarGlyph._topOffsetSharedDataKey, - -1 - ); - let maxTopOffset = currentTopOffset; - if (topOffset > currentTopOffset) { - this.renderer.staff.setSharedLayoutData(TabWhammyBarGlyph._topOffsetSharedDataKey, topOffset); - maxTopOffset = topOffset; + if (bottomY !== 0) { + if (this._isSimpleDip) { + topY -= labelSize; + } else { + const bottomYWithLabel = bottomY - labelSize; + if (bottomYWithLabel < topY) { + topY = bottomYWithLabel; + } + } } - const currentBottomOffset: number = this.renderer.staff.getSharedLayoutData( - TabWhammyBarGlyph._bottomOffsetSharedDataKey, - -1 - ); - let maxBottomOffset = currentBottomOffset; - - if (bottomOffset > currentBottomOffset) { - this.renderer.staff.setSharedLayoutData(TabWhammyBarGlyph._bottomOffsetSharedDataKey, bottomOffset); - maxBottomOffset = currentBottomOffset; - } + topY = Math.abs(topY); + bottomY = Math.abs(bottomY); - this.height = topOffset + bottomOffset; + this.topOffset = topY; + this.bottomOffset = bottomY; + this.originalTopOffset = topY; + this.originalBottomOffset = bottomY; - this.renderer.registerOverflowTop(maxTopOffset + maxBottomOffset); + this.height = topY + bottomY; + this.width = 0; } private _getOffset(value: number): number { @@ -130,13 +129,13 @@ export class TabWhammyBarGlyph extends Glyph { const startNoteRenderer: BarRendererBase = this.renderer; let endBeat: Beat | null = this._beat.nextBeat; - let endNoteRenderer: TabBarRenderer | null = null; + let endNoteRenderer: LineBarRenderer | null = null; let endXPositionType: BeatXPosition = BeatXPosition.PreNotes; if (endBeat) { endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, + this.renderer.staff!.staffId, endBeat.voice.bar - ) as TabBarRenderer | null; + ) as LineBarRenderer | null; if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { endBeat = null; endNoteRenderer = null; @@ -155,29 +154,35 @@ export class TabWhammyBarGlyph extends Glyph { let startX: number = 0; let endX: number = 0; if (this._isSimpleDip) { - startX = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.OnNotes); - endX = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.PostNotes); + startX = cx + startNoteRenderer.getBeatX(this._beat, BeatXPosition.OnNotes, true); + endX = cx + startNoteRenderer.getBeatX(this._beat, BeatXPosition.PostNotes, true); } else { - startX = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.MiddleNotes); - endX = !endNoteRenderer - ? cx + startNoteRenderer.x + startNoteRenderer.postBeatGlyphsStart - : cx + endNoteRenderer.x + endNoteRenderer.getBeatX(endBeat!, endXPositionType); + startX = cx + startNoteRenderer.getBeatX(this._beat, BeatXPosition.MiddleNotes, true); + if (endNoteRenderer) { + endX = + cx - + startNoteRenderer.x + + endNoteRenderer.x + + endNoteRenderer.getBeatX(endBeat!, endXPositionType, true); + } else { + endX = + cx + + startNoteRenderer.getBeatX(this._beat!, BeatXPosition.EndBeat) - + startNoteRenderer.smuflMetrics.postNoteEffectPadding; + } } const oldAlign = canvas.textAlign; const oldBaseLine = canvas.textBaseline; canvas.textAlign = TextAlign.Center; canvas.textBaseline = TextBaseline.Alphabetic; + canvas.font = this.renderer.resources.tablatureFont; if (this._renderPoints.length >= 2) { const dx: number = (endX - startX) / BendPoint.MaxPosition; canvas.beginPath(); - const sharedTopOffset = this.renderer.staff.getSharedLayoutData( - TabWhammyBarGlyph._topOffsetSharedDataKey, - 0 - ); - const zeroY: number = cy + sharedTopOffset; + const zeroY: number = cy + this.topOffset; let slurText: string = this._beat.whammyStyle === BendStyle.Gradual ? 'grad.' : ''; for (let i: number = 0, j: number = this._renderPoints.length - 1; i < j; i++) { const firstPt: BendPoint = this._renderPoints[i]; diff --git a/packages/alphatab/src/rendering/glyphs/TieGlyph.ts b/packages/alphatab/src/rendering/glyphs/TieGlyph.ts index 1f248a9d0..5b96249a0 100644 --- a/packages/alphatab/src/rendering/glyphs/TieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TieGlyph.ts @@ -1,28 +1,34 @@ -import type { Beat } from '@coderline/alphatab/model/Beat'; +import type { Note } from '@coderline/alphatab/model/Note'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import { type BarRendererBase, NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; /** * @internal */ -export class TieGlyph extends Glyph { - protected startBeat: Beat | null; - protected endBeat: Beat | null; - protected yOffset: number = 0; - protected forEnd: boolean; +export interface ITieGlyph { + /** + * Whether the tie is relevant for checking on bar renderer overflows. + * If set, the tie bounds will be requested and the overflow is applied. + */ + readonly checkForOverflow: boolean; +} - protected startNoteRenderer: BarRendererBase | null = null; - protected endNoteRenderer: BarRendererBase | null = null; - protected tieDirection: BeamDirection = BeamDirection.Up; +/** + * @internal + */ +export abstract class TieGlyph extends Glyph implements ITieGlyph { + public tieDirection: BeamDirection = BeamDirection.Up; + public readonly slurEffectId: string; + protected isForEnd: boolean; - public constructor(startBeat: Beat | null, endBeat: Beat | null, forEnd: boolean) { + public constructor(slurEffectId: string, forEnd: boolean) { super(0, 0); - this.startBeat = startBeat; - this.endBeat = endBeat; - this.forEnd = forEnd; + this.slurEffectId = slurEffectId; + this.isForEnd = forEnd; } private _startX: number = 0; @@ -30,154 +36,193 @@ export class TieGlyph extends Glyph { private _endX: number = 0; private _endY: number = 0; private _tieHeight: number = 0; - private _shouldDraw: boolean = false; + private _boundingBox?: Bounds; + private _shouldPaint: boolean = false; + + public get checkForOverflow() { + return this._shouldPaint && this._boundingBox !== undefined; + } + + public override getBoundingBoxTop(): number { + if (this._boundingBox) { + return this._boundingBox!.y; + } + return this._startY; + } + + public override getBoundingBoxBottom(): number { + if (this._boundingBox) { + return this._boundingBox.y + this._boundingBox.h; + } + return this._startY; + } public override doLayout(): void { this.width = 0; - // TODO fix nullability of start/end beat, - if (!this.endBeat) { - this._shouldDraw = false; - return; - } - const startNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, - this.startBeat!.voice.bar - ); - this.startNoteRenderer = startNoteRenderer; - const endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staffId, - this.endBeat.voice.bar - ); - this.endNoteRenderer = endNoteRenderer; + const startNoteRenderer = this.lookupStartBeatRenderer(); + const endNoteRenderer = this.lookupEndBeatRenderer(); this._startX = 0; this._endX = 0; this._startY = 0; this._endY = 0; this.height = 0; - this._shouldDraw = false; // if we are on the tie start, we check if we // either can draw till the end note, or we just can draw till the bar end - this.tieDirection = !startNoteRenderer - ? this.getBeamDirection(this.endBeat, endNoteRenderer!) - : this.getBeamDirection(this.startBeat!, startNoteRenderer); - if (!this.forEnd && startNoteRenderer) { - // line break or bar break + this.tieDirection = this.calculateTieDirection(); + + const forEnd = this.isForEnd; + this._shouldPaint = false; + + if (!forEnd) { if (startNoteRenderer !== endNoteRenderer) { - this._startX = startNoteRenderer.x + this.getStartX(); - this._startY = startNoteRenderer.y + this.getStartY() + this.yOffset; - // line break: to bar end + this._startX = this.calculateStartX(); + this._startY = this.calculateStartY(); if (!endNoteRenderer || startNoteRenderer.staff !== endNoteRenderer.staff) { - this._endX = startNoteRenderer.x + startNoteRenderer.width; + const lastRendererInStaff = + startNoteRenderer.staff!.barRenderers[startNoteRenderer.staff!.barRenderers.length - 1]; + + this._endX = lastRendererInStaff.x + lastRendererInStaff.width; this._endY = this._startY; + + startNoteRenderer.scoreRenderer.layout!.slurRegistry.startMultiSystemSlur(this); } else { - this._endX = endNoteRenderer.x + this.getEndX(); - this._endY = endNoteRenderer.y + this.getEndY() + this.yOffset; + this._endX = this.calculateEndX(); + this._endY = this.caclculateEndY(); } } else { - this._startX = startNoteRenderer.x + this.getStartX(); - this._endX = endNoteRenderer.x + this.getEndX(); - this._startY = startNoteRenderer.y + this.getStartY() + this.yOffset; - this._endY = endNoteRenderer.y + this.getEndY() + this.yOffset; + this._shouldPaint = true; + this._startX = this.calculateStartX(); + this._endX = this.calculateEndX(); + this._startY = this.calculateStartY(); + this._endY = this.caclculateEndY(); } - this._shouldDraw = true; - } else if (!startNoteRenderer || startNoteRenderer.staff !== endNoteRenderer!.staff) { - this._startX = endNoteRenderer!.x; - this._endX = endNoteRenderer!.x + this.getEndX(); - this._startY = endNoteRenderer!.y + this.getEndY() + this.yOffset; - this._endY = this._startY; - this._shouldDraw = true; - } - - if (this._shouldDraw) { - this.y = Math.min(this._startY, this._endY); - if (this.shouldDrawBendSlur()) { - this._tieHeight = 0; // TODO: Bend slur height to be considered? + this._shouldPaint = true; + } else if (startNoteRenderer.staff !== endNoteRenderer!.staff) { + const firstRendererInStaff = startNoteRenderer.staff!.barRenderers[0]; + this._startX = firstRendererInStaff!.x; + + this._endX = this.calculateEndX(); + + const startGlyph = startNoteRenderer.scoreRenderer.layout!.slurRegistry.completeMultiSystemSlur(this); + if (startGlyph) { + this._startY = startGlyph.calculateMultiSystemSlurY(endNoteRenderer!); } else { - this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY); - - const tieBoundingBox = TieGlyph.calculateActualTieHeight( - 1, - this._startX, - this._startY, - this._endX, - this._endY, - this.tieDirection === BeamDirection.Down, - this._tieHeight, - this.renderer.smuflMetrics.tieMidpointThickness - ); - - this.height = tieBoundingBox.h; - - if (this.tieDirection === BeamDirection.Up) { - // the tie might go above `this.y` due to its shape - // here we calculate how much this is so we can consider the - // respective overflow - const overlap = this.y - tieBoundingBox.y; - if (overlap > 0) { - this.y -= overlap; - } - } + this._startY = this.caclculateEndY(); } + + this._endY = this.caclculateEndY(); + + this._shouldPaint = startNoteRenderer.staff !== endNoteRenderer!.staff; } - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { - if (this._shouldDraw) { - if (this.shouldDrawBendSlur()) { - TieGlyph.drawBendSlur( - canvas, - cx + this._startX, - cy + this._startY, - cx + this._endX, - cy + this._endY, - this.tieDirection === BeamDirection.Down, - 1, - this.renderer.smuflMetrics.tieHeight - ); - } else { - TieGlyph.paintTie( - canvas, - 1, - cx + this._startX, - cy + this._startY, - cx + this._endX, - cy + this._endY, - this.tieDirection === BeamDirection.Down, - this._tieHeight, - this.renderer.smuflMetrics.tieMidpointThickness - ); + this._boundingBox = undefined; + this.y = Math.min(this._startY, this._endY); + let tieBoundingBox: Bounds; + if (this.shouldDrawBendSlur()) { + this._tieHeight = 0; + tieBoundingBox = TieGlyph.calculateBendSlurHeight( + this._startX, + this._startY, + this._endX, + this._endY, + this.tieDirection === BeamDirection.Down, + this.renderer.smuflMetrics.tieHeight + ); + } else { + this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY); + + tieBoundingBox = TieGlyph.calculateActualTieHeight( + 1, + this._startX, + this._startY, + this._endX, + this._endY, + this.tieDirection === BeamDirection.Down, + this._tieHeight, + this.renderer.smuflMetrics.tieMidpointThickness + ); + } + + this._boundingBox = tieBoundingBox; + + this.height = tieBoundingBox.h; + + if (this.tieDirection === BeamDirection.Up) { + // the tie might go above `this.y` due to its shape + // here we calculate how much this is so we can consider the + // respective overflow + const overlap = this.y - tieBoundingBox.y; + if (overlap > 0) { + this.y -= overlap; } } } - protected shouldDrawBendSlur() { - return false; + public override paint(cx: number, cy: number, canvas: ICanvas): void { + if (!this._shouldPaint) { + return; + } + + if (this.shouldDrawBendSlur()) { + TieGlyph.drawBendSlur( + canvas, + cx + this._startX, + cy + this._startY, + cx + this._endX, + cy + this._endY, + this.tieDirection === BeamDirection.Down, + this.renderer.smuflMetrics.tieHeight + ); + } else { + TieGlyph.paintTie( + canvas, + 1, + cx + this._startX, + cy + this._startY, + cx + this._endX, + cy + this._endY, + this.tieDirection === BeamDirection.Down, + this._tieHeight, + this.renderer.smuflMetrics.tieMidpointThickness + ); + } } - protected getTieHeight(_startX: number, _startY: number, _endX: number, _endY: number): number { + protected abstract shouldDrawBendSlur(): boolean; + + public getTieHeight(_startX: number, _startY: number, _endX: number, _endY: number): number { return this.renderer.smuflMetrics.tieHeight; } - protected getBeamDirection(_beat: Beat, _noteRenderer: BarRendererBase): BeamDirection { - return BeamDirection.Down; - } + protected abstract calculateTieDirection(): BeamDirection; - protected getStartY(): number { - return 0; - } + protected abstract lookupStartBeatRenderer(): LineBarRenderer; + protected abstract lookupEndBeatRenderer(): LineBarRenderer | null; - protected getEndY(): number { - return 0; - } + protected abstract calculateStartY(): number; - protected getStartX(): number { - return 0; + protected abstract caclculateEndY(): number; + + protected abstract calculateStartX(): number; + + protected abstract calculateEndX(): number; + + public calculateMultiSystemSlurY(renderer: BarRendererBase) { + const startRenderer = this.lookupStartBeatRenderer(); + const startY = this.calculateStartY(); + const relY = startY - startRenderer.y; + return renderer.y + relY; } - protected getEndX(): number { - return 0; + public shouldCreateMultiSystemSlur(renderer: BarRendererBase) { + const endStaff = this.lookupEndBeatRenderer()?.staff; + if (!endStaff) { + return true; + } + + return renderer.staff!.system.index < endStaff.system.index; } public static calculateActualTieHeight( @@ -191,23 +236,37 @@ export class TieGlyph extends Glyph { size: number ): Bounds { const cp = TieGlyph._computeBezierControlPoints(scale, x1, y1, x2, y2, down, offset, size); + if (cp.length === 0) { + return new Bounds(x1, y1, x2 - x1, y2 - y1); + } - x1 = cp[0]; - y1 = cp[1]; - const cpx = cp[2]; - const cpy = cp[3]; - x2 = cp[6]; - y2 = cp[7]; - - const tx = (x1 - cpx) / (x1 - 2 * cpx + x2); - const ex = TieGlyph._calculateExtrema(x1, y1, cpx, cpy, x2, y2, tx); - const xMin = ex.length > 0 ? Math.min(x1, x2, ex[0]) : Math.min(x1, x2); - const xMax = ex.length > 0 ? Math.max(x1, x2, ex[0]) : Math.max(x1, x2); - - const ty = (y1 - cpy) / (y1 - 2 * cpy + y2); - const ey = TieGlyph._calculateExtrema(x1, y1, cpx, cpy, x2, y2, ty); - const yMin = ey.length > 0 ? Math.min(y1, y2, ey[1]) : Math.min(y1, y2); - const yMax = ey.length > 0 ? Math.max(y1, y2, ey[1]) : Math.max(y1, y2); + // For a musical tie/slur, the extrema occur predictably near the midpoint + // Evaluate at midpoint (t=0.5) and check endpoints + const p0x = cp[0]; + const p0y = cp[1]; + const c1x = cp[2]; + const c1y = cp[3]; + const c2x = cp[4]; + const c2y = cp[5]; + const p1x = cp[6]; + const p1y = cp[7]; + + // Evaluate at t=0.5 for midpoint + const midX = 0.125 * p0x + 0.375 * c1x + 0.375 * c2x + 0.125 * p1x; + const midY = 0.125 * p0y + 0.375 * c1y + 0.375 * c2y + 0.125 * p1y; + + // Bounds are simply min/max of start, end, and midpoint + const xMin = Math.min(p0x, p1x, midX); + const xMax = Math.max(p0x, p1x, midX); + let yMin = Math.min(p0y, p1y, midY); + let yMax = Math.max(p0y, p1y, midY); + + // Account for thickness of the tie/slur + if (down) { + yMax += size; + } else { + yMin -= size; + } const b = new Bounds(); b.x = xMin; @@ -217,28 +276,6 @@ export class TieGlyph extends Glyph { return b; } - private static _calculateExtrema( - x1: number, - y1: number, - cpx: number, - cpy: number, - x2: number, - y2: number, - t: number - ): number[] { - if (t <= 0 || 1 <= t) { - return []; - } - - const c1x = x1 + (cpx - x1) * t; - const c1y = y1 + (cpy - y1) * t; - - const c2x = cpx + (x2 - cpx) * t; - const c2y = cpy + (y2 - cpy) * t; - - return [c1x + (c2x - c1x) * t, c1y + (c2y - c1y) * t]; - } - private static _computeBezierControlPoints( scale: number, x1: number, @@ -269,7 +306,7 @@ export class TieGlyph extends Glyph { offset *= scale; size *= scale; - if(down) { + if (down) { offset *= -1; size *= -1; } @@ -359,6 +396,70 @@ export class TieGlyph extends Glyph { // canvas.color = c; } + public static calculateBendSlurTopY( + x1: number, + y1: number, + x2: number, + y2: number, + down: boolean, + scale: number, + bendSlurHeight: number + ) { + let normalVectorX: number = y2 - y1; + let normalVectorY: number = x2 - x1; + const length: number = Math.sqrt(normalVectorX * normalVectorX + normalVectorY * normalVectorY); + if (down) { + normalVectorX *= -1; + } else { + normalVectorY *= -1; + } + // make to unit vector + normalVectorX /= length; + normalVectorY /= length; + + let offset: number = bendSlurHeight * scale; + if (x2 - x1 < 20) { + offset /= 2; + } + const centerY: number = (y2 + y1) / 2; + const cp1Y: number = centerY + offset * normalVectorY; + + return cp1Y; + } + + public static calculateBendSlurHeight( + x1: number, + y1: number, + x2: number, + y2: number, + down: boolean, + bendSlurHeight: number + ): Bounds { + let normalVectorX: number = y2 - y1; + let normalVectorY: number = x2 - x1; + const length: number = Math.sqrt(normalVectorX * normalVectorX + normalVectorY * normalVectorY); + if (down) { + normalVectorX *= -1; + } else { + normalVectorY *= -1; + } + // make to unit vector + normalVectorX /= length; + normalVectorY /= length; + // center of connection + const centerY: number = (y2 + y1) / 2; + let offset: number = bendSlurHeight; + if (x2 - x1 < 20) { + offset /= 2; + } + const cp1Y: number = centerY + offset * normalVectorY; + + const minY = Math.min(y1, y2, cp1Y); + const maxY = Math.max(y1, y2, cp1Y); + + return new Bounds(x1, Math.min(y1, y2, cp1Y), x2 - x1, maxY - minY); + } + public static drawBendSlur( canvas: ICanvas, x1: number, @@ -366,7 +467,6 @@ export class TieGlyph extends Glyph { x2: number, y2: number, down: boolean, - scale: number, bendSlurHeight: number, slurText?: string ): void { @@ -382,10 +482,9 @@ export class TieGlyph extends Glyph { normalVectorX /= length; normalVectorY /= length; // center of connection - // TODO: should be 1/3 const centerX: number = (x2 + x1) / 2; const centerY: number = (y2 + y1) / 2; - let offset: number = bendSlurHeight * scale; + let offset: number = bendSlurHeight; if (x2 - x1 < 20) { offset /= 2; } @@ -403,3 +502,169 @@ export class TieGlyph extends Glyph { } } } + +/** + * A common tie implementation using note details for positioning + * @internal + */ +export abstract class NoteTieGlyph extends TieGlyph { + protected startNote: Note; + protected endNote: Note; + protected startNoteRenderer: LineBarRenderer | null = null; + protected endNoteRenderer: LineBarRenderer | null = null; + + public constructor(slurEffectId: string, startNote: Note, endNote: Note, forEnd: boolean) { + super(slurEffectId, forEnd); + this.startNote = startNote; + this.endNote = endNote; + } + + protected get isLeftHandTap() { + return this.startNote === this.endNote; + } + + public override getTieHeight(startX: number, startY: number, endX: number, endY: number): number { + if (this.isLeftHandTap) { + return this.renderer!.smuflMetrics.tieHeight; + } + return super.getTieHeight(startX, startY, endX, endY); + } + + protected override calculateTieDirection(): BeamDirection { + // invert direction (if stems go up, ties go down to not cross them) + switch (this.lookupStartBeatRenderer().getBeatDirection(this.startNote.beat)) { + case BeamDirection.Up: + return BeamDirection.Down; + default: + return BeamDirection.Up; + } + } + + protected override calculateStartX(): number { + const startNoteRenderer = this.lookupStartBeatRenderer(); + if (this.isLeftHandTap) { + return this.calculateEndX() - startNoteRenderer.smuflMetrics.leftHandTabTieWidth; + } + return startNoteRenderer.x + startNoteRenderer!.getNoteX(this.startNote, this.getStartNotePosition()); + } + + protected getStartNotePosition() { + return NoteXPosition.Center; + } + + protected override calculateStartY(): number { + const startNoteRenderer = this.lookupStartBeatRenderer(); + if (this.isLeftHandTap) { + return startNoteRenderer.y + startNoteRenderer.getNoteY(this.startNote, NoteYPosition.Center); + } + + switch (this.tieDirection) { + case BeamDirection.Up: + return startNoteRenderer.y + startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Top); + default: + return startNoteRenderer.y + startNoteRenderer.getNoteY(this.startNote, NoteYPosition.Bottom); + } + } + + protected override calculateEndX(): number { + const endNoteRenderer = this.lookupEndBeatRenderer(); + if (!endNoteRenderer) { + return this.calculateStartY() + this.renderer.smuflMetrics.leftHandTabTieWidth; + } + if (this.isLeftHandTap) { + return endNoteRenderer!.x + endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); + } + return endNoteRenderer.x + endNoteRenderer.getNoteX(this.endNote, NoteXPosition.Center); + } + + protected getEndNotePosition() { + return NoteXPosition.Center; + } + + protected override caclculateEndY(): number { + const endNoteRenderer = this.lookupEndBeatRenderer(); + if (!endNoteRenderer) { + return this.calculateStartY(); + } + + if (this.isLeftHandTap) { + return endNoteRenderer.y + endNoteRenderer!.getNoteY(this.endNote, NoteYPosition.Center); + } + + switch (this.tieDirection) { + case BeamDirection.Up: + return endNoteRenderer.y + endNoteRenderer!.getNoteY(this.endNote, NoteYPosition.Top); + default: + return endNoteRenderer.y + endNoteRenderer!.getNoteY(this.endNote, NoteYPosition.Bottom); + } + } + + protected override lookupEndBeatRenderer() { + if (!this.endNoteRenderer) { + this.endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff!.staffId, + this.endNote.beat.voice.bar + ) as LineBarRenderer | null; + } + return this.endNoteRenderer; + } + + protected override lookupStartBeatRenderer() { + if (!this.startNoteRenderer) { + this.startNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff!.staffId, + this.startNote.beat.voice.bar + )! as LineBarRenderer; + } + return this.startNoteRenderer; + } + + protected override shouldDrawBendSlur(): boolean { + return false; + } +} + +/** + * A tie glyph for continued multi-system ties/slurs + * @internal + */ +export class ContinuationTieGlyph extends TieGlyph { + private _startTie: TieGlyph; + + public constructor(startTie: TieGlyph) { + super(startTie.slurEffectId, false); + this._startTie = startTie; + } + + protected override lookupStartBeatRenderer() { + return this.renderer as LineBarRenderer; + } + + protected override lookupEndBeatRenderer() { + return this.renderer as LineBarRenderer; + } + + protected override shouldDrawBendSlur(): boolean { + return false; + } + + protected override calculateTieDirection(): BeamDirection { + return this._startTie.tieDirection; + } + + protected override calculateStartY(): number { + return this._startTie.calculateMultiSystemSlurY(this.renderer); + } + protected override caclculateEndY(): number { + return this.calculateStartY(); + } + + protected override calculateStartX(): number { + return this.renderer.staff!.barRenderers[0].x; + } + + protected override calculateEndX(): number { + const last = this.renderer.staff!.barRenderers[this.renderer.staff!.barRenderers.length - 1]; + return last.x + last.width; + } +} diff --git a/packages/alphatab/src/rendering/glyphs/TimeSignatureGlyph.ts b/packages/alphatab/src/rendering/glyphs/TimeSignatureGlyph.ts index 6c86697a1..eb6a8ed6a 100644 --- a/packages/alphatab/src/rendering/glyphs/TimeSignatureGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TimeSignatureGlyph.ts @@ -50,7 +50,6 @@ export abstract class TimeSignatureGlyph extends GlyphGroup { this.addGlyph(common); super.doLayout(); } else { - // TODO: ensure we align them exactly so they meet in the staff center (use glyphTop and glyphBottom accordingly) const numerator: NumberGlyph = new NumberGlyph( 0, 0, diff --git a/packages/alphatab/src/rendering/glyphs/TremoloPickingGlyph.ts b/packages/alphatab/src/rendering/glyphs/TremoloPickingGlyph.ts index 483ccdd9e..59a4beebd 100644 --- a/packages/alphatab/src/rendering/glyphs/TremoloPickingGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TremoloPickingGlyph.ts @@ -1,25 +1,101 @@ -import { Duration } from '@coderline/alphatab/model/Duration'; -import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; +import type { Duration } from '@coderline/alphatab/model/Duration'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { type TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; +import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; +import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; /** * @internal */ export class TremoloPickingGlyph extends MusicFontGlyph { - public constructor(x: number, y: number, duration: Duration) { - super(x, y, 1, TremoloPickingGlyph._getSymbol(duration)); + public constructor(x: number, y: number, effect: TremoloPickingEffect) { + super(x, y, 1, TremoloPickingGlyph._getSymbol(effect)); } - private static _getSymbol(duration: Duration): MusicFontSymbol { - switch (duration) { - case Duration.ThirtySecond: - return MusicFontSymbol.Tremolo3; - case Duration.Sixteenth: - return MusicFontSymbol.Tremolo2; - case Duration.Eighth: - return MusicFontSymbol.Tremolo1; - default: - return MusicFontSymbol.None; + public static _getSymbol(effect: TremoloPickingEffect): MusicFontSymbol { + if (effect.style === TremoloPickingStyle.BuzzRoll) { + return MusicFontSymbol.BuzzRoll; + } else { + switch (effect.marks) { + case 1: + return MusicFontSymbol.Tremolo1; + case 2: + return MusicFontSymbol.Tremolo2; + case 3: + return MusicFontSymbol.Tremolo3; + case 4: + return MusicFontSymbol.Tremolo4; + case 5: + return MusicFontSymbol.Tremolo5; + default: + return MusicFontSymbol.None; + } } } + + public stemExtensionHeight = 0; + + public alignTremoloPickingGlyph(direction: BeamDirection, flagEnd: number, firstNoteY: number, duration: Duration) { + const lr = this.renderer as LineBarRenderer; + const smufl = lr.smuflMetrics; + let tremoloY = 0; + const tremoloOverlap = smufl.glyphHeights.get(MusicFontSymbol.Tremolo1)! / 2; + const tremoloCenterOffset = this.height / 2; + + // whether the center or top bar should be aligned with a staff line + const forceAlignWithStaffLine = this.symbol === MusicFontSymbol.Tremolo1; + const lineSpacing = lr.lineSpacing; + const spacing = forceAlignWithStaffLine ? lineSpacing : lineSpacing / 2; + + if (direction === BeamDirection.Up) { + // start at note + let flagBottom = flagEnd; + + // to bottom of stem + flagBottom += smufl.stemFlagHeight.get(duration)!; + flagBottom -= smufl.stemFlagOffsets.get(duration)!; + + // align with closest step line + tremoloY = spacing * Math.ceil(flagBottom / spacing); + + // ensure at least 1 staff space distance between note and tremolo bottom bar + const tremoloBottomY = tremoloY + tremoloCenterOffset; + const minSpacingY = firstNoteY - lineSpacing; + if (minSpacingY < tremoloBottomY) { + tremoloY = minSpacingY - tremoloCenterOffset; + } + + // reserve the additional space needed in the stem height + flagBottom += tremoloOverlap; + const tremoloTop = tremoloY - tremoloCenterOffset; + if (flagBottom > tremoloTop) { + this.stemExtensionHeight = flagBottom - tremoloTop; + } else { + this.stemExtensionHeight = 0; + } + } else { + // same logic as above but inverted + let flagTop = flagEnd; + flagTop -= smufl.stemFlagHeight.get(duration)!; + flagTop += smufl.stemFlagOffsets.get(duration)!; + tremoloY = spacing * Math.floor(flagTop / spacing); + + const tremoloTopY = tremoloY - tremoloCenterOffset; + const minSpacingY = firstNoteY + lineSpacing; + if (minSpacingY > tremoloTopY) { + tremoloY = minSpacingY + tremoloCenterOffset; + } + + flagTop -= tremoloOverlap; + const tremoloBottom = tremoloY + tremoloCenterOffset; + if (flagTop < tremoloBottom) { + this.stemExtensionHeight = tremoloBottom - flagTop; + } else { + this.stemExtensionHeight = 0; + } + } + + this.y = tremoloY; + } } diff --git a/packages/alphatab/src/rendering/glyphs/TripletFeelGlyph.ts b/packages/alphatab/src/rendering/glyphs/TripletFeelGlyph.ts index dd9500f1f..3b522da82 100644 --- a/packages/alphatab/src/rendering/glyphs/TripletFeelGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TripletFeelGlyph.ts @@ -2,6 +2,7 @@ import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; import { CanvasHelper, TextAlign, TextBaseline, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal @@ -86,7 +87,7 @@ export class TripletFeelGlyph extends EffectGlyph { const b = canvas.textBaseline; canvas.textBaseline = TextBaseline.Bottom; - canvas.font = this.renderer.resources.effectFont; + canvas.font = this.renderer.resources.elementFonts.get(NotationElement.EffectTripletFeel)!; canvas.fillText('(', cx, textY); cx += canvas.measureText('( ').width; diff --git a/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts b/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts index 99dd91840..25fc6c388 100644 --- a/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts @@ -5,6 +5,7 @@ import { GlyphGroup } from '@coderline/alphatab/rendering/glyphs/GlyphGroup'; import { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import type { Color } from '@coderline/alphatab/model/Color'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; /** * @internal @@ -48,7 +49,13 @@ export class TuningGlyph extends GlyphGroup { // Track name if (this._trackLabel.length > 0) { - const trackName = new TextGlyph(0, this.height, this._trackLabel, res.effectFont, TextAlign.Left); + const trackName = new TextGlyph( + 0, + this.height, + this._trackLabel, + res.elementFonts.get(NotationElement.GuitarTuning)!, + TextAlign.Left + ); trackName.renderer = this.renderer; trackName.doLayout(); this.height += trackName.height; @@ -57,7 +64,13 @@ export class TuningGlyph extends GlyphGroup { // Name if (tuning.name.length > 0) { - const tuningName = new TextGlyph(0, this.height, tuning.name, res.effectFont, TextAlign.Left); + const tuningName = new TextGlyph( + 0, + this.height, + tuning.name, + res.elementFonts.get(NotationElement.GuitarTuning)!, + TextAlign.Left + ); tuningName.renderer = this.renderer; tuningName.doLayout(); this.height += tuningName.height; @@ -67,7 +80,7 @@ export class TuningGlyph extends GlyphGroup { const circleScale = this.renderer.smuflMetrics.tuningGlyphCircleNumberScale; const circleHeight = this.renderer.smuflMetrics.glyphHeights.get(MusicFontSymbol.GuitarString0)! * circleScale; - this.renderer.scoreRenderer.canvas!.font = res.effectFont; + this.renderer.scoreRenderer.canvas!.font = res.elementFonts.get(NotationElement.GuitarTuning)!; const stringColumnWidth = (circleHeight + this.renderer.scoreRenderer.canvas!.measureText(' = Gb').width) * res.engravingSettings.tuningGlyphStringColumnScale; @@ -93,7 +106,7 @@ export class TuningGlyph extends GlyphGroup { currentX + circleHeight, currentY + circleHeight / 2, str, - res.effectFont, + res.elementFonts.get(NotationElement.GuitarTuning)!, TextAlign.Left, TextBaseline.Middle ) diff --git a/packages/alphatab/src/rendering/glyphs/VoiceContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/VoiceContainerGlyph.ts deleted file mode 100644 index abd36af41..000000000 --- a/packages/alphatab/src/rendering/glyphs/VoiceContainerGlyph.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { GraceType } from '@coderline/alphatab/model/GraceType'; -import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; -import { type Voice, VoiceSubElement } from '@coderline/alphatab/model/Voice'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; -import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { GlyphGroup } from '@coderline/alphatab/rendering/glyphs/GlyphGroup'; -import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; - -/** - * This glyph acts as container for handling - * multiple voice rendering - * @internal - */ -export class VoiceContainerGlyph extends GlyphGroup { - public static readonly KeySizeBeat: string = 'Beat'; - - public beatGlyphs: BeatContainerGlyph[]; - public voice: Voice; - public tupletGroups: TupletGroup[]; - - public constructor(x: number, y: number, voice: Voice) { - super(x, y); - this.voice = voice; - this.beatGlyphs = []; - this.tupletGroups = []; - } - - public scaleToWidth(width: number): void { - const force: number = this.renderer.layoutingInfo.spaceToForce(width); - this._scaleToForce(force); - } - - private _scaleToForce(force: number): void { - this.width = this.renderer.layoutingInfo.calculateVoiceWidth(force); - const positions: Map = this.renderer.layoutingInfo.buildOnTimePositions(force); - const beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; - - for (let i: number = 0, j: number = beatGlyphs.length; i < j; i++) { - const currentBeatGlyph: BeatContainerGlyph = beatGlyphs[i]; - - switch (currentBeatGlyph.beat.graceType) { - case GraceType.None: - currentBeatGlyph.x = - positions.get(currentBeatGlyph.beat.absoluteDisplayStart)! - currentBeatGlyph.onTimeX; - break; - default: - const graceDisplayStart = currentBeatGlyph.beat.graceGroup!.beats[0].absoluteDisplayStart; - const graceGroupId = currentBeatGlyph.beat.graceGroup!.id; - // placement for proper grace notes which have a following note - if (currentBeatGlyph.beat.graceGroup!.isComplete && positions.has(graceDisplayStart)) { - currentBeatGlyph.x = positions.get(graceDisplayStart)! - currentBeatGlyph.onTimeX; - - const graceSprings = this.renderer.layoutingInfo.allGraceRods.get(graceGroupId)!; - - // get the pre beat stretch of this voice/staff, not the - // shared space. This way we use the potentially empty space (see discussions/1092). - const afterGraceBeat = - currentBeatGlyph.beat.graceGroup!.beats[currentBeatGlyph.beat.graceGroup!.beats.length - 1] - .nextBeat; - const preBeatStretch = afterGraceBeat - ? this.renderer.layoutingInfo.getPreBeatSize(afterGraceBeat) - : 0; - - // move right in front to the note - currentBeatGlyph.x -= preBeatStretch; - // respect the post beat width of the grace note - currentBeatGlyph.x -= graceSprings[currentBeatGlyph.beat.graceIndex].postSpringWidth; - // shift to right position of the particular grace note - - currentBeatGlyph.x += graceSprings[currentBeatGlyph.beat.graceIndex].graceBeatWidth; - // move the whole group again forward for cases where another track has e.g. 3 beats and here we have only 2. - // so we shift the whole group of this voice to stick to the end of the group. - const lastGraceSpring = graceSprings[currentBeatGlyph.beat.graceGroup!.beats.length - 1]; - currentBeatGlyph.x -= lastGraceSpring.graceBeatWidth; - } else { - // placement for improper grace beats where no beat in the same bar follows - const graceSpring = this.renderer.layoutingInfo.incompleteGraceRods.get(graceGroupId)!; - const relativeOffset = - graceSpring[currentBeatGlyph.beat.graceIndex].postSpringWidth - - graceSpring[currentBeatGlyph.beat.graceIndex].preSpringWidth; - - if (i > 0) { - if (currentBeatGlyph.beat.graceIndex === 0) { - // we place the grace beat directly after the previous one - // otherwise this causes flickers on resizing - currentBeatGlyph.x = beatGlyphs[i - 1].x + beatGlyphs[i - 1].width; - } else { - // for the multiple grace glyphs we take the width of the grace rod - // this width setting is aligned with the positioning logic below - currentBeatGlyph.x = - beatGlyphs[i - 1].x + - graceSpring[currentBeatGlyph.beat.graceIndex - 1].postSpringWidth - - graceSpring[currentBeatGlyph.beat.graceIndex - 1].preSpringWidth - - relativeOffset; - } - } else { - currentBeatGlyph.x = -relativeOffset; - } - } - break; - } - - // size always previous glyph after we know the position - // of the next glyph - if (i > 0) { - const beatWidth: number = currentBeatGlyph.x - beatGlyphs[i - 1].x; - beatGlyphs[i - 1].scaleToWidth(beatWidth); - } - // for the last glyph size based on the full width - if (i === j - 1) { - const beatWidth: number = this.width - beatGlyphs[beatGlyphs.length - 1].x; - currentBeatGlyph.scaleToWidth(beatWidth); - } - } - } - - public registerLayoutingInfo(info: BarLayoutingInfo): void { - const beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; - for (const b of beatGlyphs) { - b.registerLayoutingInfo(info); - } - } - - public applyLayoutingInfo(info: BarLayoutingInfo): void { - const beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; - for (const b of beatGlyphs) { - b.applyLayoutingInfo(info); - } - this._scaleToForce(Math.max(this.renderer.settings.display.stretchForce, info.minStretchForce)); - } - - public override addGlyph(g: Glyph): void { - const bg: BeatContainerGlyph = g as BeatContainerGlyph; - g.x = - this.beatGlyphs.length === 0 - ? 0 - : this.beatGlyphs[this.beatGlyphs.length - 1].x + this.beatGlyphs[this.beatGlyphs.length - 1].width; - g.renderer = this.renderer; - g.doLayout(); - this.beatGlyphs.push(bg); - this.width = g.x + g.width; - if (bg.beat.hasTuplet && bg.beat.tupletGroup!.beats[0].id === bg.beat.id) { - this.tupletGroups.push(bg.beat.tupletGroup!); - } - } - - public override doLayout(): void {} - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - // canvas.color = Color.random(); - // canvas.strokeRect(cx + this.x, cy + this.y, this.width, this.renderer.height); - using _ = ElementStyleHelper.voice(canvas, VoiceSubElement.Glyphs, this.voice, true); - - for (let i: number = 0, j: number = this.beatGlyphs.length; i < j; i++) { - this.beatGlyphs[i].paint(cx + this.x, cy + this.y, canvas); - } - } -} diff --git a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts index b167c6ac2..5962fab06 100644 --- a/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts +++ b/packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts @@ -1,11 +1,12 @@ +import { Logger } from '@coderline/alphatab/Logger'; import type { MasterBar } from '@coderline/alphatab/model/MasterBar'; import type { Score } from '@coderline/alphatab/model/Score'; import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; -import { InternalSystemsLayoutMode, ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; +import type { RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; +import { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; +import type { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; -import { Logger } from '@coderline/alphatab/Logger'; -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; /** * @internal @@ -14,6 +15,7 @@ export class HorizontalScreenLayoutPartialInfo { public x: number = 0; public width: number = 0; public masterBars: MasterBar[] = []; + public results: MasterBarsRenderers[] = []; } /** @@ -43,16 +45,7 @@ export class HorizontalScreenLayout extends ScoreLayout { // not supported } - protected doLayoutAndRender(): void { - switch (this.renderer.settings.display.systemsLayoutMode) { - case SystemsLayoutMode.Automatic: - this.systemsLayoutMode = InternalSystemsLayoutMode.Automatic; - break; - case SystemsLayoutMode.UseModelLayout: - this.systemsLayoutMode = InternalSystemsLayoutMode.FromModelWithWidths; - break; - } - + protected doLayoutAndRender(renderHints: RenderHints | undefined): void { const score: Score = this.renderer.score!; let startIndex: number = this.renderer.settings.display.startBar; @@ -67,14 +60,13 @@ export class HorizontalScreenLayout extends ScoreLayout { endBarIndex = startIndex + endBarIndex - 1; // map count to array index endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex)); - this._system = this.createEmptyStaffSystem(); + this._system = this.createEmptyStaffSystem(0); this._system.isLast = true; this._system.x = this.pagePadding![0]; this._system.y = this.pagePadding![1]; const countPerPartial: number = this.renderer.settings.display.barCountPerPartial; const partials: HorizontalScreenLayoutPartialInfo[] = []; let currentPartial: HorizontalScreenLayoutPartialInfo = new HorizontalScreenLayoutPartialInfo(); - let renderX = 0; while (currentBarIndex <= endBarIndex) { const multiBarRestInfo = this.multiBarRestInfo; const additionalMultiBarsRestBarIndices: number[] | null = @@ -88,53 +80,27 @@ export class HorizontalScreenLayout extends ScoreLayout { additionalMultiBarsRestBarIndices ); - // if we detect that the new renderer is linked to the previous - // renderer, we need to put it into the previous partial - if (currentPartial.masterBars.length === 0 && result.isLinkedToPrevious && partials.length > 0) { - const previousPartial: HorizontalScreenLayoutPartialInfo = partials[partials.length - 1]; - previousPartial.masterBars.push(score.masterBars[currentBarIndex]); - previousPartial.width += result.width; - renderX += result.width; - currentPartial.x += renderX; - } else { - currentPartial.masterBars.push(score.masterBars[currentBarIndex]); - currentPartial.width += result.width; - // no targetPartial here because previous partials already handled this code - if (currentPartial.masterBars.length >= countPerPartial) { - if (partials.length === 0) { - // respect accolade and on first partial - currentPartial.width += this._system.accoladeWidth + this.pagePadding![0]; - } - renderX += currentPartial.width; - partials.push(currentPartial); - Logger.debug( - this.name, - `Finished partial from bar ${currentPartial.masterBars[0].index} to ${currentPartial.masterBars[currentPartial.masterBars.length - 1].index}`, - null - ); - currentPartial = new HorizontalScreenLayoutPartialInfo(); - currentPartial.x = renderX; - } + // complete partial if its full and we are not linked + if (currentPartial.masterBars.length >= countPerPartial && !result.isLinkedToPrevious) { + currentPartial = this._completePartial(partials, currentPartial); } + this._scaleBars(result); + + currentPartial.results.push(result); + currentPartial.masterBars.push(score.masterBars[currentBarIndex]); + currentPartial.width += result.width; currentBarIndex++; } + // don't miss the last partial if not empty if (currentPartial.masterBars.length > 0) { - if (partials.length === 0) { - currentPartial.width += this._system.accoladeWidth + this.pagePadding![0]; - } - partials.push(currentPartial); - Logger.debug( - this.name, - `Finished partial from bar ${currentPartial.masterBars[0].index} to ${currentPartial.masterBars[currentPartial.masterBars.length - 1].index}`, - null - ); + this._completePartial(partials, currentPartial); } this._finalizeStaffSystem(); this.height = Math.floor(this._system.y + this._system.height); - this.width = (this._system.x + this._system.width + this.pagePadding![2]); + this.width = this._system.x + this._system.width + this.pagePadding![2]; currentBarIndex = 0; let x = 0; @@ -142,6 +108,7 @@ export class HorizontalScreenLayout extends ScoreLayout { const partial: HorizontalScreenLayoutPartialInfo = partials[i]; const e = new RenderFinishedEventArgs(); + e.reuseViewport = renderHints?.reuseViewport ?? false; e.x = x; e.y = 0; e.totalWidth = this.width; @@ -190,8 +157,70 @@ export class HorizontalScreenLayout extends ScoreLayout { this.height *= this.renderer.settings.display.scale; } + private _scaleBars(result: MasterBarsRenderers) { + result.width = 0; + this._system!.width -= result.width; + for (const r of result.renderers) { + const barDisplayWidth = + r.staff!.system.staves.length > 1 ? r.bar.masterBar.displayWidth : r.bar.displayWidth; + if (barDisplayWidth > 0) { + r.scaleToWidth(barDisplayWidth); + } + const w = r.x + r.width; + if (w > result.width) { + result.width = w; + } + } + this._system!.width += result.width; + } + + private _completePartial( + partials: HorizontalScreenLayoutPartialInfo[], + currentPartial: HorizontalScreenLayoutPartialInfo + ) { + if (partials.length === 0) { + // respect accolade and on first partial + currentPartial.width += this._system!.accoladeWidth + this.pagePadding![0]; + } + + partials.push(currentPartial); + Logger.debug( + this.name, + `Finished partial from bar ${currentPartial.masterBars[0].index} to ${currentPartial.masterBars[currentPartial.masterBars.length - 1].index}`, + null + ); + + // start new partial + const newPartial = new HorizontalScreenLayoutPartialInfo(); + newPartial.x = currentPartial.x + currentPartial.width; + return newPartial; + } + private _finalizeStaffSystem() { - this._system!.scaleToWidth(this._system!.width); + this._alignRenderers(); this._system!.finalizeSystem(); } + + private _alignRenderers(): void { + this.width = 0; + const system = this._system!; + for (const s of system.allStaves) { + s.resetSharedLayoutData(); + + let w = 0; + for (const renderer of s.barRenderers) { + renderer.x = w; + renderer.y = s.topPadding + s.topOverflow; + // note: this will ensure aspects like beaming helpers + // and overflows are prepared for finalization + renderer.scaleToWidth(renderer.width); + w += renderer.width; + } + + if (w > this.width) { + system.width = w; + } + } + system.width += system.accoladeWidth; + } } diff --git a/packages/alphatab/src/rendering/layout/PageViewLayout.ts b/packages/alphatab/src/rendering/layout/PageViewLayout.ts index fe26e9672..250e00a4c 100644 --- a/packages/alphatab/src/rendering/layout/PageViewLayout.ts +++ b/packages/alphatab/src/rendering/layout/PageViewLayout.ts @@ -1,422 +1,27 @@ -import { type ICanvas, TextAlign } from '@coderline/alphatab/platform/ICanvas'; -import type { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; -import { InternalSystemsLayoutMode, ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; -import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; -import type { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; -import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; -import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; -import { Logger } from '@coderline/alphatab/Logger'; import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; -import { ScoreSubElement } from '@coderline/alphatab/model/Score'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import { VerticalLayoutBase } from '@coderline/alphatab/rendering/layout/VerticalLayoutBase'; /** * This layout arranges the bars into a fixed width and dynamic height region. * @internal */ -export class PageViewLayout extends ScoreLayout { - private _systems: StaffSystem[] = []; - private _allMasterBarRenderers: MasterBarsRenderers[] = []; - private _barsFromPreviousSystem: MasterBarsRenderers[] = []; - +export class PageViewLayout extends VerticalLayoutBase { public get name(): string { return 'PageView'; } - protected doLayoutAndRender(): void { - switch (this.renderer.settings.display.systemsLayoutMode) { - case SystemsLayoutMode.Automatic: - this.systemsLayoutMode = InternalSystemsLayoutMode.Automatic; - break; - case SystemsLayoutMode.UseModelLayout: - this.systemsLayoutMode = InternalSystemsLayoutMode.FromModelWithScale; - break; - } - - let y: number = this.pagePadding![1]; - this.width = this.renderer.width; - this._allMasterBarRenderers = []; - // - // 1. Score Info - y = this._layoutAndRenderScoreInfo(y, -1); - // - // 2. Tunings - y = this._layoutAndRenderTunings(y, -1); - // - // 3. Chord Diagrms - y = this._layoutAndRenderChordDiagrams(y, -1); - // - // 4. One result per StaffSystem - y = this._layoutAndRenderScore(y); - - y = this.layoutAndRenderBottomScoreInfo(y); - - y = this.layoutAndRenderAnnotation(y); - - this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; - } - - public get supportsResize(): boolean { - return true; - } - - public get firstBarX(): number { - let x = this.pagePadding![0]; - if (this._systems.length > 0) { - x += this._systems[0].accoladeWidth; - } - return x; - } - - public doResize(): void { - let y: number = this.pagePadding![1]; - this.width = this.renderer.width; - const oldHeight: number = this.height; - // - // 1. Score Info - y = this._layoutAndRenderScoreInfo(y, oldHeight); - // - // 2. Tunings - y = this._layoutAndRenderTunings(y, oldHeight); - // - // 3. Chord Digrams - y = this._layoutAndRenderChordDiagrams(y, oldHeight); - // - // 4. One result per StaffSystem - y = this._resizeAndRenderScore(y, oldHeight); - - y = this.layoutAndRenderBottomScoreInfo(y); - - y = this.layoutAndRenderAnnotation(y); - - this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; - } - - private _layoutAndRenderTunings(y: number, totalHeight: number = -1): number { - if (!this.tuningGlyph) { - return y; - } - - const res: RenderingResources = this.renderer.settings.display.resources; - this.tuningGlyph.x = this.pagePadding![0]; - this.tuningGlyph.width = this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; - this.tuningGlyph.doLayout(); - - const tuningHeight = Math.round(this.tuningGlyph.height); - - const e = new RenderFinishedEventArgs(); - e.x = 0; - e.y = y; - e.width = this.scaledWidth; - e.height = tuningHeight; - e.totalWidth = this.scaledWidth; - e.totalHeight = totalHeight < 0 ? y + e.height : totalHeight; - - this.registerPartial(e, (canvas: ICanvas) => { - canvas.color = res.scoreInfoColor; - canvas.textAlign = TextAlign.Center; - this.tuningGlyph!.paint(0, 0, canvas); - }); - - return y + tuningHeight; - } - - private _layoutAndRenderChordDiagrams(y: number, totalHeight: number = -1): number { - if (!this.chordDiagrams) { - return y; - } - const res: RenderingResources = this.renderer.settings.display.resources; - this.chordDiagrams.x = this.pagePadding![0]; - this.chordDiagrams.width = this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; - this.chordDiagrams.doLayout(); - - const diagramHeight = Math.round(this.chordDiagrams.height); - - const e = new RenderFinishedEventArgs(); - e.x = 0; - e.y = y; - e.width = this.scaledWidth; - e.height = diagramHeight; - e.totalWidth = this.scaledWidth; - e.totalHeight = totalHeight < 0 ? y + diagramHeight : totalHeight; - - this.registerPartial(e, (canvas: ICanvas) => { - canvas.color = res.scoreInfoColor; - canvas.textAlign = TextAlign.Center; - this.chordDiagrams!.paint(0, 0, canvas); - }); - - return y + diagramHeight; - } - - private _layoutAndRenderScoreInfo(y: number, totalHeight: number = -1): number { - Logger.debug(this.name, 'Layouting score info'); - - const e = new RenderFinishedEventArgs(); - e.x = 0; - e.y = y; - - let infoHeight = 0; - - const res: RenderingResources = this.renderer.settings.display.resources; - - const scoreInfoGlyphs: TextGlyph[] = []; - - for (const [scoreElement, _notationElement] of ScoreLayout.headerElements.value) { - if (this.headerGlyphs.has(scoreElement)) { - const glyph: TextGlyph = this.headerGlyphs.get(scoreElement)!; - glyph.y = infoHeight; - this.alignScoreInfoGlyph(glyph); - - let lineHeight = glyph.font.size; - - // words and music on same line if not aligned on same side - if (scoreElement === ScoreSubElement.Words) { - if (this.headerGlyphs.has(ScoreSubElement.Music)) { - const musicGlyph = this.headerGlyphs.get(ScoreSubElement.Music)!; - if (musicGlyph.textAlign !== glyph.textAlign) { - lineHeight = 0; - } - } - } - - infoHeight += lineHeight; - - scoreInfoGlyphs.push(glyph); - } - } - - if (scoreInfoGlyphs.length > 0) { - infoHeight = Math.floor(infoHeight + 17); - e.width = this.scaledWidth; - e.height = infoHeight; - e.totalWidth = this.scaledWidth; - e.totalHeight = totalHeight < 0 ? y + e.height : totalHeight; - this.registerPartial(e, (canvas: ICanvas) => { - canvas.color = res.scoreInfoColor; - canvas.textAlign = TextAlign.Center; - for (const g of scoreInfoGlyphs) { - g.paint(0, 0, canvas); - } - }); - } - - return y + infoHeight; - } - - private _resizeAndRenderScore(y: number, oldHeight: number): number { - // if we have a fixed number of bars per row, we only need to refit them. - const barsPerRowActive = - this.renderer.settings.display.barsPerRow > 0 || - this.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithScale; - - if (barsPerRowActive) { - for (let i: number = 0; i < this._systems.length; i++) { - const system: StaffSystem = this._systems[i]; - this._fitSystem(system); - y += this._paintSystem(system, oldHeight); - } - } else { - this._systems = []; - let currentIndex: number = 0; - const maxWidth: number = this._maxWidth; - let system: StaffSystem = this.createEmptyStaffSystem(); - system.index = this._systems.length; - system.x = this.pagePadding![0]; - system.y = y; - while (currentIndex < this._allMasterBarRenderers.length) { - // if the current renderer still has space in the current system add it - // also force adding in case the system is empty - let renderers: MasterBarsRenderers | null = this._allMasterBarRenderers[currentIndex]; - if (system.width + renderers!.width <= maxWidth || system.masterBarsRenderers.length === 0) { - system.addMasterBarRenderers(this.renderer.tracks!, renderers!); - // move to next system - currentIndex++; - } else { - // if we cannot wrap on the current bar, we remove the last bar - // (this might even remove multiple ones until we reach a bar that can wrap); - while (renderers && !renderers.canWrap && system.masterBarsRenderers.length > 1) { - renderers = system.revertLastBar(); - currentIndex--; - } - // in case we do not have space, we create a new system - system.isFull = true; - system.isLast = this.lastBarIndex === system.lastBarIndex; - this._systems.push(system); - this._fitSystem(system); - y += this._paintSystem(system, oldHeight); - // note: we do not increase currentIndex here to have it added to the next system - system = this.createEmptyStaffSystem(); - system.index = this._systems.length; - system.x = this.pagePadding![0]; - system.y = y; - } - } - system.isLast = this.lastBarIndex === system.lastBarIndex; - // don't forget to finish the last system - this._fitSystem(system); - y += this._paintSystem(system, oldHeight); - } - return y; - } - - private _layoutAndRenderScore(y: number): number { - const startIndex: number = this.firstBarIndex; - let currentBarIndex: number = startIndex; - const endBarIndex: number = this.lastBarIndex; - - this._systems = []; - while (currentBarIndex <= endBarIndex) { - // create system and align set proper coordinates - const system: StaffSystem = this._createStaffSystem(currentBarIndex, endBarIndex); - this._systems.push(system); - system.x = this.pagePadding![0]; - system.y = y; - currentBarIndex = system.lastBarIndex + 1; - // finalize system (sizing etc). - this._fitSystem(system); - Logger.debug( - this.name, - `Rendering partial from bar ${system.firstBarIndex} to ${system.lastBarIndex}`, - null - ); - y += this._paintSystem(system, y); - } - return y; - } - - private _paintSystem(system: StaffSystem, totalHeight: number): number { - // paint into canvas - const height: number = Math.floor(system.height); - - const args: RenderFinishedEventArgs = new RenderFinishedEventArgs(); - args.x = 0; - args.y = system.y; - args.totalWidth = this.scaledWidth; - args.totalHeight = totalHeight; - args.width = this.scaledWidth; - args.height = height; - args.firstMasterBarIndex = system.firstBarIndex; - args.lastMasterBarIndex = system.lastBarIndex; - - system.buildBoundingsLookup(0, 0); - this.registerPartial(args, canvas => { - this.renderer.canvas!.color = this.renderer.settings.display.resources.mainGlyphColor; - this.renderer.canvas!.textAlign = TextAlign.Left; - // NOTE: we use this negation trick to make the system paint itself to 0/0 coordinates - // since we use partial drawing - system.paint(0, -(args.y / this.renderer.settings.display.scale), canvas); - }); - - // calculate coordinates for next system - return height; - } - - /** - * Realignes the bars in this line according to the available space - */ - private _fitSystem(system: StaffSystem): void { - if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) { - system.scaleToWidth(this._maxWidth); - } else { - system.scaleToWidth(system.width); - } - system.finalizeSystem(); - } - - private _getBarsPerSystem(rowIndex: number) { + protected override getBarsPerSystem(systemIndex: number) { let barsPerRow: number = this.renderer.settings.display.barsPerRow; - if (this.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithScale) { - let defaultSystemsLayout: number; - let systemsLayout: number[]; - if (this.renderer.tracks!.length > 1) { - // multi track applies - defaultSystemsLayout = this.renderer.score!.defaultSystemsLayout; - systemsLayout = this.renderer.score!.systemsLayout; - } else { - defaultSystemsLayout = this.renderer.tracks![0].defaultSystemsLayout; - systemsLayout = this.renderer.tracks![0].systemsLayout; - } - - barsPerRow = rowIndex < systemsLayout.length ? systemsLayout[rowIndex] : defaultSystemsLayout; + if (this.renderer.settings.display.systemsLayoutMode === SystemsLayoutMode.UseModelLayout) { + barsPerRow = ModelUtils.getSystemLayout(this.renderer.score!, systemIndex, this.renderer.tracks!); } return barsPerRow; } - private _createStaffSystem(currentBarIndex: number, endIndex: number): StaffSystem { - const system: StaffSystem = this.createEmptyStaffSystem(); - system.index = this._systems.length; - const barsPerRow: number = this._getBarsPerSystem(system.index); - const maxWidth: number = this._maxWidth; - const end: number = endIndex + 1; - - let barIndex = currentBarIndex; - while (barIndex < end) { - if (this._barsFromPreviousSystem.length > 0) { - for (const renderer of this._barsFromPreviousSystem) { - system.addMasterBarRenderers(this.renderer.tracks!, renderer); - barIndex = renderer.lastMasterBarIndex; - } - } else { - const multiBarRestInfo = this.multiBarRestInfo; - const additionalMultiBarsRestBarIndices: number[] | null = - multiBarRestInfo !== null && multiBarRestInfo.has(barIndex) - ? multiBarRestInfo.get(barIndex)! - : null; - - const renderers = system.addBars(this.renderer.tracks!, barIndex, additionalMultiBarsRestBarIndices); - this._allMasterBarRenderers.push(renderers); - barIndex = renderers.lastMasterBarIndex; - } - this._barsFromPreviousSystem = []; - let systemIsFull: boolean = false; - // can bar placed in this line? - if (barsPerRow === -1 && system.width >= maxWidth && system.masterBarsRenderers.length !== 0) { - systemIsFull = true; - } else if (system.masterBarsRenderers.length === barsPerRow + 1) { - systemIsFull = true; - } - if (systemIsFull) { - let reverted = system.revertLastBar(); - if (reverted) { - this._barsFromPreviousSystem.push(reverted); - while (reverted && !reverted.canWrap && system.masterBarsRenderers.length > 1) { - reverted = system.revertLastBar(); - if (reverted) { - this._barsFromPreviousSystem.push(reverted); - } - } - } - system.isFull = true; - system.isLast = false; - this._barsFromPreviousSystem.reverse(); - return system; - } - // do we need a line break after this bar - let anyTrackNeedsLineBreak = false; - let allTracksNeedLineBreak = true; - for (const track of this.renderer.tracks!) { - if (track.lineBreaks && track.lineBreaks!.has(barIndex + 1)) { - anyTrackNeedsLineBreak = true; - } else { - allTracksNeedLineBreak = false; - } - } - - if (anyTrackNeedsLineBreak && allTracksNeedLineBreak) { - system.isFull = true; - system.isLast = false; - return system; - } - system.x = 0; - barIndex++; - } - system.isLast = endIndex === system.lastBarIndex; - return system; - } - - private get _maxWidth(): number { - return this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; + protected override get shouldApplyBarScale(): boolean { + return this.renderer.settings.display.systemsLayoutMode === SystemsLayoutMode.UseModelLayout; } } diff --git a/packages/alphatab/src/rendering/layout/ParchmentLayout.ts b/packages/alphatab/src/rendering/layout/ParchmentLayout.ts new file mode 100644 index 000000000..41153067a --- /dev/null +++ b/packages/alphatab/src/rendering/layout/ParchmentLayout.ts @@ -0,0 +1,21 @@ +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import { VerticalLayoutBase } from '@coderline/alphatab/rendering/layout/VerticalLayoutBase'; + +/** + * This layout arranges the bars into a fixed width and dynamic height region + * respecting the systems layout specified in the data model. + * @internal + */ +export class ParchmentLayout extends VerticalLayoutBase { + public get name(): string { + return 'Parchment'; + } + + protected override getBarsPerSystem(systemIndex: number) { + return ModelUtils.getSystemLayout(this.renderer.score!, systemIndex, this.renderer.tracks!); + } + + protected override get shouldApplyBarScale(): boolean { + return true; + } +} diff --git a/packages/alphatab/src/rendering/layout/ScoreLayout.ts b/packages/alphatab/src/rendering/layout/ScoreLayout.ts index 7a7c3b502..48b41ac17 100644 --- a/packages/alphatab/src/rendering/layout/ScoreLayout.ts +++ b/packages/alphatab/src/rendering/layout/ScoreLayout.ts @@ -1,26 +1,29 @@ import { Environment } from '@coderline/alphatab/Environment'; +import type { EventEmitterOfT } from '@coderline/alphatab/EventEmitter'; +import { Logger } from '@coderline/alphatab/Logger'; import type { Bar } from '@coderline/alphatab/model/Bar'; import { Font, FontStyle, FontWeight } from '@coderline/alphatab/model/Font'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { type Score, ScoreStyle, ScoreSubElement } from '@coderline/alphatab/model/Score'; import type { Staff } from '@coderline/alphatab/model/Staff'; import { type Track, TrackSubElement } from '@coderline/alphatab/model/Track'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; +import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { BarRendererFactory } from '@coderline/alphatab/rendering/BarRendererFactory'; +import { type EffectBandInfo, EffectBandMode } from '@coderline/alphatab/rendering/BarRendererFactory'; import { ChordDiagramContainerGlyph } from '@coderline/alphatab/rendering/glyphs/ChordDiagramContainerGlyph'; import { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; +import { TuningContainerGlyph } from '@coderline/alphatab/rendering/glyphs/TuningContainerGlyph'; +import { TuningGlyph } from '@coderline/alphatab/rendering/glyphs/TuningGlyph'; +import type { RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; +import { SlurRegistry } from '@coderline/alphatab/rendering/layout/SlurRegistry'; import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; import { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; -import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; -import { Logger } from '@coderline/alphatab/Logger'; -import type { EventEmitterOfT } from '@coderline/alphatab/EventEmitter'; -import { NotationElement } from '@coderline/alphatab/NotationSettings'; -import { TuningContainerGlyph } from '@coderline/alphatab/rendering/glyphs/TuningContainerGlyph'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; +import type { BeamingRuleLookup } from '@coderline/alphatab/rendering/utils/BeamingRuleLookup'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { TuningGlyph } from '@coderline/alphatab/rendering/glyphs/TuningGlyph'; import type { Settings } from '@coderline/alphatab/Settings'; import { Lazy } from '@coderline/alphatab/util/Lazy'; @@ -36,27 +39,6 @@ class LazyPartial { } } -/** - * Lists the different modes in which the staves and systems are arranged. - * @internal - */ -export enum InternalSystemsLayoutMode { - /** - * Use the automatic alignment system provided by alphaTab (default) - */ - Automatic = 0, - - /** - * Use the relative scaling information stored in the score model. - */ - FromModelWithScale = 1, - - /** - * Use the absolute size information stored in the score model. - */ - FromModelWithWidths = 2 -} - /** * This is the base class for creating new layouting engines for the score renderer. * @internal @@ -66,6 +48,8 @@ export abstract class ScoreLayout { protected pagePadding: number[] | null = null; + public profile: Set = new Set(); + public abstract get name(): string; public renderer: ScoreRenderer; @@ -83,8 +67,6 @@ export abstract class ScoreLayout { protected chordDiagrams: ChordDiagramContainerGlyph | null = null; protected tuningGlyph: TuningContainerGlyph | null = null; - public systemsLayoutMode: InternalSystemsLayoutMode = InternalSystemsLayoutMode.Automatic; - public constructor(renderer: ScoreRenderer) { this.renderer = renderer; } @@ -92,14 +74,23 @@ export abstract class ScoreLayout { public abstract get firstBarX(): number; public abstract get supportsResize(): boolean; + public slurRegistry = new SlurRegistry(); + public beamingRuleLookups = new Map(); + public resize(): void { this._lazyPartials.clear(); + this.slurRegistry.clear(); this.doResize(); } public abstract doResize(): void; - public layoutAndRender(): void { + public layoutAndRender(renderHints?: RenderHints): void { this._lazyPartials.clear(); + this.slurRegistry.clear(); + this.beamingRuleLookups.clear(); + this._barRendererLookup.clear(); + + this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile)!; const score: Score = this.renderer.score!; @@ -122,7 +113,7 @@ export abstract class ScoreLayout { } this._createScoreInfoGlyphs(); - this.doLayoutAndRender(); + this.doLayoutAndRender(renderHints); } private _lazyPartials: Map = new Map(); @@ -166,7 +157,7 @@ export abstract class ScoreLayout { } } - protected abstract doLayoutAndRender(): void; + protected abstract doLayoutAndRender(renderHints: RenderHints | undefined): void; protected static readonly headerElements: Lazy> = new Lazy( () => @@ -352,10 +343,9 @@ export abstract class ScoreLayout { } } - if(this.chordDiagrams.isEmpty) { + if (this.chordDiagrams.isEmpty) { this.chordDiagrams = null; } - } else { this.chordDiagrams = null; } @@ -365,22 +355,64 @@ export abstract class ScoreLayout { public lastBarIndex: number = 0; - protected createEmptyStaffSystem(): StaffSystem { + protected createEmptyStaffSystem(index: number): StaffSystem { const system: StaffSystem = new StaffSystem(this); + system.index = index; + const allFactories = Environment.defaultRenderers; + + const renderStaves: RenderStaff[] = []; for (let trackIndex: number = 0; trackIndex < this.renderer.tracks!.length; trackIndex++) { const track: Track = this.renderer.tracks![trackIndex]; + for (let staffIndex: number = 0; staffIndex < track.staves.length; staffIndex++) { - const staff: Staff = track.staves[staffIndex]; - const profile: BarRendererFactory[] = Environment.staveProfiles.get( - this.renderer.settings.display.staveProfile - )!; - for (const factory of profile) { - if (factory.canCreate(track, staff)) { - system.addStaff(track, new RenderStaff(trackIndex, staff, factory)); + const staff = track.staves[staffIndex]; + + let sharedTopEffects: EffectBandInfo[] = []; + let sharedBottomEffects: EffectBandInfo[] = []; + + let previousStaff: RenderStaff | undefined = undefined; + + for (const factory of allFactories) { + if (this.profile.has(factory.staffId) && factory.canCreate(track, staff)) { + const renderStaff = new RenderStaff(system, trackIndex, staff, factory); + // insert shared effect bands at front + renderStaff.topEffectInfos.splice(0, 0, ...sharedTopEffects); + renderStaff.bottomEffectInfos.push(...sharedBottomEffects); + previousStaff = renderStaff; + // just remember staff, adding to system comes later when we have all effects collected + renderStaves.push(renderStaff); + sharedTopEffects = []; + sharedBottomEffects = []; + } else { + for (const e of factory.effectBands) { + switch (e.mode) { + case EffectBandMode.SharedTop: + sharedTopEffects.push(e); + break; + case EffectBandMode.SharedBottom: + sharedBottomEffects.push(e); + break; + } + } + } + } + + // don't forget any left-over shared effects. + if (previousStaff) { + if (sharedTopEffects.length > 0) { + previousStaff.bottomEffectInfos.push(...sharedTopEffects); + } + if (sharedBottomEffects.length > 0) { + previousStaff.bottomEffectInfos.push(...sharedBottomEffects); } } } } + + for (const staff of renderStaves) { + system.addStaff(staff); + } + return system; } @@ -396,18 +428,6 @@ export abstract class ScoreLayout { } } - public unregisterBarRenderer(key: string, renderer: BarRendererBase): void { - if (this._barRendererLookup.has(key)) { - const lookup: Map = this._barRendererLookup.get(key)!; - lookup.delete(renderer.bar.id); - if (renderer.additionalMultiRestBars) { - for (const b of renderer.additionalMultiRestBars) { - lookup.delete(b.id); - } - } - } - } - public getRendererForBar(key: string, bar: Bar): BarRendererBase | null { const barRendererId: number = bar.id; if (this._barRendererLookup.has(key) && this._barRendererLookup.get(key)!.has(barRendererId)) { @@ -485,7 +505,11 @@ export abstract class ScoreLayout { const msg: string = 'rendered by alphaTab'; const resources: RenderingResources = this.renderer.settings.display.resources; const size: number = 12; - const font = Font.withFamilyList(resources.copyrightFont.families, size, FontStyle.Plain, FontWeight.Bold); + const fontFamilies = resources.elementFonts.has(NotationElement.ScoreCopyright) + ? resources.elementFonts.get(NotationElement.ScoreCopyright)!.families + : resources.tablatureFont.families; + + const font = Font.withFamilyList(fontFamilies, size, FontStyle.Plain, FontWeight.Bold); const fakeBarRenderer = new BarRendererBase(this.renderer, this.renderer.tracks![0].staves[0].bars[0]); const glyph = new TextGlyph(0, 0, msg, font, TextAlign.Center, undefined, resources.mainGlyphColor); diff --git a/packages/alphatab/src/rendering/layout/SlurRegistry.ts b/packages/alphatab/src/rendering/layout/SlurRegistry.ts new file mode 100644 index 000000000..9a1a0d0e3 --- /dev/null +++ b/packages/alphatab/src/rendering/layout/SlurRegistry.ts @@ -0,0 +1,87 @@ +import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; +import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; + +/** + * @internal + * @record + */ +interface SlurRegistration { + startGlyph: TieGlyph; + endGlyph?: TieGlyph; +} + +/** + * Holds the slur information specific for an individual staff + * @internal + * @record + */ +interface SlurInfoContainer { + /** + * A set of started slurs and ties. + */ + startedSlurs: Map; +} + +/** + * This registry keeps track of which slurs and ties were started and needs completion. + * Slurs might span multiple systems, and in such cases we need to create additional + * slur/ties in the intermediate and end system. + * + * @internal + * + */ +export class SlurRegistry { + private _staffLookup = new Map(); + + public clear() { + this._staffLookup.clear(); + } + + public startMultiSystemSlur(startGlyph: TieGlyph) { + const staffId = SlurRegistry._staffId(startGlyph.renderer.staff!); + let container: SlurInfoContainer; + if (!this._staffLookup.has(staffId)) { + container = { + startedSlurs: new Map() + }; + this._staffLookup.set(staffId, container); + } else { + container = this._staffLookup.get(staffId)!; + } + + container.startedSlurs.set(startGlyph.slurEffectId, { startGlyph }); + } + + private static _staffId(staff: RenderStaff): string { + return `${staff.modelStaff.index}.${staff.modelStaff.track.index}.${staff.staffId}`; + } + + public completeMultiSystemSlur(endGlyph: TieGlyph) { + const staffId = SlurRegistry._staffId(endGlyph.renderer.staff!); + if (!this._staffLookup.has(staffId)) { + return undefined; + } + const container = this._staffLookup.get(staffId)!; + if (container.startedSlurs.has(endGlyph.slurEffectId)) { + const info = container.startedSlurs.get(endGlyph.slurEffectId)!; + info.endGlyph = endGlyph; + return info.startGlyph; + } + return undefined; + } + + public *getAllContinuations(renderer: BarRendererBase): Generator { + const staffId = SlurRegistry._staffId(renderer.staff!); + if (!this._staffLookup.has(staffId) || renderer.index > 0) { + return; + } + + const container = this._staffLookup.get(staffId)!; + for (const g of container.startedSlurs.values()) { + if (g.startGlyph.shouldCreateMultiSystemSlur(renderer)) { + yield g.startGlyph; + } + } + } +} diff --git a/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts new file mode 100644 index 000000000..d1b88c302 --- /dev/null +++ b/packages/alphatab/src/rendering/layout/VerticalLayoutBase.ts @@ -0,0 +1,457 @@ +import { Logger } from '@coderline/alphatab/Logger'; +import { ScoreSubElement } from '@coderline/alphatab/model/Score'; +import { type ICanvas, TextAlign } from '@coderline/alphatab/platform/ICanvas'; +import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; +import type { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; +import type { RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer'; +import { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; +import { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; +import type { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; +import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; + +/** + * Base layout for page and parchment style layouts where we have an endless + * vertical page with fitted systems. + * @internal + */ +export abstract class VerticalLayoutBase extends ScoreLayout { + private _systems: StaffSystem[] = []; + private _allMasterBarRenderers: MasterBarsRenderers[] = []; + private _barsFromPreviousSystem: MasterBarsRenderers[] = []; + + private _reuseViewPort: boolean = false; + + protected doLayoutAndRender(renderHints: RenderHints | undefined): void { + let y: number = this.pagePadding![1]; + this.width = this.renderer.width; + this._allMasterBarRenderers = []; + this._reuseViewPort = renderHints?.reuseViewport ?? false; + + // + // 1. Score Info + y = this._layoutAndRenderScoreInfo(y, -1); + // + // 2. Tunings + y = this._layoutAndRenderTunings(y, -1); + // + // 3. Chord Diagrms + y = this._layoutAndRenderChordDiagrams(y, -1); + // + // 4. One result per StaffSystem + y = this._layoutAndRenderScore(y); + + y = this.layoutAndRenderBottomScoreInfo(y); + + y = this.layoutAndRenderAnnotation(y); + + this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; + } + + protected override registerPartial(args: RenderFinishedEventArgs, callback: (canvas: ICanvas) => void): void { + args.reuseViewport = this._reuseViewPort; + super.registerPartial(args, callback); + } + + public get supportsResize(): boolean { + return true; + } + + public get firstBarX(): number { + let x = this.pagePadding![0]; + if (this._systems.length > 0) { + x += this._systems[0].accoladeWidth; + } + return x; + } + + public doResize(): void { + let y: number = this.pagePadding![1]; + this.width = this.renderer.width; + const oldHeight: number = this.height; + this._reuseViewPort = true; + + // + // 1. Score Info + y = this._layoutAndRenderScoreInfo(y, oldHeight); + // + // 2. Tunings + y = this._layoutAndRenderTunings(y, oldHeight); + // + // 3. Chord Digrams + y = this._layoutAndRenderChordDiagrams(y, oldHeight); + // + // 4. One result per StaffSystem + y = this._resizeAndRenderScore(y, oldHeight); + + y = this.layoutAndRenderBottomScoreInfo(y); + + y = this.layoutAndRenderAnnotation(y); + + this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; + } + + private _layoutAndRenderTunings(y: number, totalHeight: number = -1): number { + if (!this.tuningGlyph) { + return y; + } + + const res: RenderingResources = this.renderer.settings.display.resources; + this.tuningGlyph.x = this.pagePadding![0]; + this.tuningGlyph.width = this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; + this.tuningGlyph.doLayout(); + + const tuningHeight = Math.round(this.tuningGlyph.height); + + const e = new RenderFinishedEventArgs(); + e.x = 0; + e.y = y; + e.width = this.scaledWidth; + e.height = tuningHeight; + e.totalWidth = this.scaledWidth; + e.totalHeight = totalHeight < 0 ? y + e.height : totalHeight; + + this.registerPartial(e, (canvas: ICanvas) => { + canvas.color = res.scoreInfoColor; + canvas.textAlign = TextAlign.Center; + this.tuningGlyph!.paint(0, 0, canvas); + }); + + return y + tuningHeight; + } + + private _layoutAndRenderChordDiagrams(y: number, totalHeight: number = -1): number { + if (!this.chordDiagrams) { + return y; + } + const res: RenderingResources = this.renderer.settings.display.resources; + this.chordDiagrams.x = this.pagePadding![0]; + this.chordDiagrams.width = this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; + this.chordDiagrams.doLayout(); + + const diagramHeight = Math.round(this.chordDiagrams.height); + + const e = new RenderFinishedEventArgs(); + e.x = 0; + e.y = y; + e.width = this.scaledWidth; + e.height = diagramHeight; + e.totalWidth = this.scaledWidth; + e.totalHeight = totalHeight < 0 ? y + diagramHeight : totalHeight; + + this.registerPartial(e, (canvas: ICanvas) => { + canvas.color = res.scoreInfoColor; + canvas.textAlign = TextAlign.Center; + this.chordDiagrams!.paint(0, 0, canvas); + }); + + return y + diagramHeight; + } + + private _layoutAndRenderScoreInfo(y: number, totalHeight: number = -1): number { + Logger.debug(this.name, 'Layouting score info'); + + const e = new RenderFinishedEventArgs(); + e.x = 0; + e.y = y; + + let infoHeight = 0; + + const res: RenderingResources = this.renderer.settings.display.resources; + + const scoreInfoGlyphs: TextGlyph[] = []; + + for (const [scoreElement, _notationElement] of ScoreLayout.headerElements.value) { + if (this.headerGlyphs.has(scoreElement)) { + const glyph: TextGlyph = this.headerGlyphs.get(scoreElement)!; + glyph.y = infoHeight; + this.alignScoreInfoGlyph(glyph); + + let lineHeight = glyph.font.size; + + // words and music on same line if not aligned on same side + if (scoreElement === ScoreSubElement.Words) { + if (this.headerGlyphs.has(ScoreSubElement.Music)) { + const musicGlyph = this.headerGlyphs.get(ScoreSubElement.Music)!; + if (musicGlyph.textAlign !== glyph.textAlign) { + lineHeight = 0; + } + } + } + + infoHeight += lineHeight; + + scoreInfoGlyphs.push(glyph); + } + } + + if (scoreInfoGlyphs.length > 0) { + infoHeight = Math.floor(infoHeight + 17); + e.width = this.scaledWidth; + e.height = infoHeight; + e.totalWidth = this.scaledWidth; + e.totalHeight = totalHeight < 0 ? y + e.height : totalHeight; + this.registerPartial(e, (canvas: ICanvas) => { + canvas.color = res.scoreInfoColor; + canvas.textAlign = TextAlign.Center; + for (const g of scoreInfoGlyphs) { + g.paint(0, 0, canvas); + } + }); + } + + return y + infoHeight; + } + + private _resizeAndRenderScore(y: number, oldHeight: number): number { + // if we have a fixed number of bars per row, we only need to refit them. + const barsPerRowActive = this.getBarsPerSystem(0) > 0; + if (barsPerRowActive) { + for (let i: number = 0; i < this._systems.length; i++) { + const system: StaffSystem = this._systems[i]; + system.width = system.computedWidth; + this._fitSystem(system); + y += this._paintSystem(system, oldHeight); + } + } else { + // clear out staves during re-layout, this info is outdated during + // re-layout of the bars + for (const r of this._allMasterBarRenderers) { + for (const b of r.renderers) { + b.afterReverted(); + } + } + + this._systems = []; + let currentIndex: number = 0; + const maxWidth: number = this._maxWidth; + let system: StaffSystem = this.createEmptyStaffSystem(this._systems.length); + system.x = this.pagePadding![0]; + system.y = y; + while (currentIndex < this._allMasterBarRenderers.length) { + // if the current renderer still has space in the current system add it + // also force adding in case the system is empty + let renderers: MasterBarsRenderers | null = this._allMasterBarRenderers[currentIndex]; + + if (system.width + renderers!.width <= maxWidth || system.masterBarsRenderers.length === 0) { + system.addMasterBarRenderers(this.renderer.tracks!, renderers!); + // move to next bar + currentIndex++; + + if (this._needsLineBreak(currentIndex)) { + system.isFull = true; + } + } else { + // if we cannot wrap on the current bar, we remove the last bar + // (this might even remove multiple ones until we reach a bar that can wrap); + while (renderers && !renderers.canWrap && system.masterBarsRenderers.length > 1) { + renderers = system.revertLastBar(); + currentIndex--; + } + // in case we do not have space, we create a new system + system.isFull = true; + } + + if (system.isFull) { + system.isLast = this.lastBarIndex === system.lastBarIndex; + this._systems.push(system); + this._fitSystem(system); + y += this._paintSystem(system, oldHeight); + // note: we do not increase currentIndex here to have it added to the next system + system = this.createEmptyStaffSystem(this._systems.length); + system.x = this.pagePadding![0]; + system.y = y; + } + } + system.isLast = this.lastBarIndex === system.lastBarIndex; + // don't forget to finish the last system + this._fitSystem(system); + y += this._paintSystem(system, oldHeight); + } + return y; + } + + private _layoutAndRenderScore(y: number): number { + const startIndex: number = this.firstBarIndex; + let currentBarIndex: number = startIndex; + const endBarIndex: number = this.lastBarIndex; + + this._systems = []; + while (currentBarIndex <= endBarIndex) { + // create system and align set proper coordinates + const system: StaffSystem = this._createStaffSystem(currentBarIndex, endBarIndex); + this._systems.push(system); + system.x = this.pagePadding![0]; + system.y = y; + currentBarIndex = system.lastBarIndex + 1; + // finalize system (sizing etc). + this._fitSystem(system); + Logger.debug( + this.name, + `Rendering partial from bar ${system.firstBarIndex} to ${system.lastBarIndex}`, + null + ); + y += this._paintSystem(system, y); + } + return y; + } + + private _paintSystem(system: StaffSystem, totalHeight: number): number { + // paint into canvas + const height: number = Math.floor(system.height); + + const args: RenderFinishedEventArgs = new RenderFinishedEventArgs(); + args.x = 0; + args.y = system.y; + args.totalWidth = this.scaledWidth; + args.totalHeight = totalHeight; + args.width = this.scaledWidth; + args.height = height; + args.firstMasterBarIndex = system.firstBarIndex; + args.lastMasterBarIndex = system.lastBarIndex; + + system.buildBoundingsLookup(0, 0); + this.registerPartial(args, canvas => { + this.renderer.canvas!.color = this.renderer.settings.display.resources.mainGlyphColor; + this.renderer.canvas!.textAlign = TextAlign.Left; + // NOTE: we use this negation trick to make the system paint itself to 0/0 coordinates + // since we use partial drawing + system.paint(0, -(args.y / this.renderer.settings.display.scale), canvas); + }); + + // calculate coordinates for next system + return height; + } + + /** + * Realignes the bars in this line according to the available space + */ + private _fitSystem(system: StaffSystem): void { + if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) { + this._scaleToWidth(system, this._maxWidth); + } else { + this._scaleToWidth(system, system.width); + } + system.finalizeSystem(); + } + + protected abstract get shouldApplyBarScale(): boolean; + + private _scaleToWidth(system: StaffSystem, width: number): void { + const staffWidth = width - system.accoladeWidth; + const shouldApplyBarScale = this.shouldApplyBarScale; + + const totalScale = system.totalBarDisplayScale; + + // NOTE: it currently delivers best results if we evenly distribute the available space across bars + // scaling bars relatively to their computed width, rather causes distortions whenever bars have + // pre-beat glyphs. + + // most precise scaling would come if we use the contents (voiceContainerGlyph) width as a calculation + // factor. but this would make the calculation additionally complex with not much gain. + + const difference: number = width - system.computedWidth; + const spacePerBar: number = difference / system.masterBarsRenderers.length; + + for (const s of system.allStaves) { + s.resetSharedLayoutData(); + + // scale the bars by keeping their respective ratio size + let w = 0; + for (const renderer of s.barRenderers) { + renderer.x = w; + renderer.y = s.topPadding + s.topOverflow; + + let actualBarWidth: number; + if (shouldApplyBarScale) { + const barDisplayScale = system.getBarDisplayScale(renderer); + actualBarWidth = (barDisplayScale * staffWidth) / totalScale; + } else { + actualBarWidth = renderer.computedWidth + spacePerBar; + } + + renderer.scaleToWidth(actualBarWidth); + w += renderer.width; + } + } + system.width = width; + } + + protected abstract getBarsPerSystem(systemIndex: number): number; + + private _createStaffSystem(currentBarIndex: number, endIndex: number): StaffSystem { + const system: StaffSystem = this.createEmptyStaffSystem(this._systems.length); + const barsPerRow: number = this.getBarsPerSystem(system.index); + const maxWidth: number = this._maxWidth; + const end: number = endIndex + 1; + + let barIndex = currentBarIndex; + while (barIndex < end) { + if (this._barsFromPreviousSystem.length > 0) { + for (const renderer of this._barsFromPreviousSystem) { + system.addMasterBarRenderers(this.renderer.tracks!, renderer); + barIndex = renderer.lastMasterBarIndex; + } + } else { + const multiBarRestInfo = this.multiBarRestInfo; + const additionalMultiBarsRestBarIndices: number[] | null = + multiBarRestInfo !== null && multiBarRestInfo.has(barIndex) + ? multiBarRestInfo.get(barIndex)! + : null; + + const renderers = system.addBars(this.renderer.tracks!, barIndex, additionalMultiBarsRestBarIndices); + this._allMasterBarRenderers.push(renderers); + barIndex = renderers.lastMasterBarIndex; + } + this._barsFromPreviousSystem = []; + let systemIsFull: boolean = false; + // can bar placed in this line? + if (barsPerRow === -1 && system.width >= maxWidth && system.masterBarsRenderers.length !== 0) { + systemIsFull = true; + } else if (system.masterBarsRenderers.length === barsPerRow + 1) { + systemIsFull = true; + } + if (systemIsFull) { + let reverted = system.revertLastBar(); + if (reverted) { + this._barsFromPreviousSystem.push(reverted); + while (reverted && !reverted.canWrap && system.masterBarsRenderers.length > 1) { + reverted = system.revertLastBar(); + if (reverted) { + this._barsFromPreviousSystem.push(reverted); + } + } + } + system.isFull = true; + system.isLast = false; + this._barsFromPreviousSystem.reverse(); + return system; + } + if (this._needsLineBreak(barIndex)) { + system.isFull = true; + system.isLast = false; + return system; + } + system.x = 0; + barIndex++; + } + system.isLast = endIndex === system.lastBarIndex; + return system; + } + + private _needsLineBreak(barIndex: number) { + let anyTrackNeedsLineBreak = false; + let allTracksNeedLineBreak = true; + for (const track of this.renderer.tracks!) { + if (track.lineBreaks && track.lineBreaks!.has(barIndex + 1)) { + anyTrackNeedsLineBreak = true; + } else { + allTracksNeedLineBreak = false; + } + } + return anyTrackNeedsLineBreak && allTracksNeedLineBreak; + } + + private get _maxWidth(): number { + return this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; + } +} diff --git a/packages/alphatab/src/rendering/staves/BarLayoutingInfo.ts b/packages/alphatab/src/rendering/staves/BarLayoutingInfo.ts index e47b9d9db..2e3e89a19 100644 --- a/packages/alphatab/src/rendering/staves/BarLayoutingInfo.ts +++ b/packages/alphatab/src/rendering/staves/BarLayoutingInfo.ts @@ -1,10 +1,20 @@ import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; import type { Beat } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; -import { Spring } from '@coderline/alphatab/rendering/staves/Spring'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; +import type { BeatContainerGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import { Spring } from '@coderline/alphatab/rendering/staves/Spring'; + +/** + * @internal + * @record + */ +interface BarLayoutingInfoBeatSizes { + preBeatSize: number; + onBeatSize: number; +} /** * This public class stores size information about a stave. @@ -21,6 +31,7 @@ export class BarLayoutingInfo { private _onTimePositionsForce: number = 0; private _onTimePositions: Map = new Map(); private _incompleteGraceRodsWidth: number = 0; + private _beatSizes: Map = new Map(); // the smallest duration we have between two springs to ensure we have positive spring constants private _minDuration: number = BarLayoutingInfo._defaultMinDuration; @@ -41,6 +52,30 @@ export class BarLayoutingInfo { } } + public getBeatSizes(beat: Beat) { + const key = beat.absoluteDisplayStart; + if (this._beatSizes.has(key)) { + return this._beatSizes.get(key); + } + return undefined; + } + + public setBeatSizes(beat: BeatContainerGlyphBase, sizes: BarLayoutingInfoBeatSizes) { + const key = beat.absoluteDisplayStart; + if (this._beatSizes.has(key)) { + const current = this._beatSizes.get(key)!; + if (current.onBeatSize < sizes.onBeatSize) { + current.onBeatSize = sizes.onBeatSize; + } + + if (current.preBeatSize < sizes.preBeatSize) { + current.preBeatSize = sizes.preBeatSize; + } + } else { + this._beatSizes.set(key, sizes); + } + } + public getPreBeatSize(beat: Beat) { if (beat.graceType !== GraceType.None) { const groupId = beat.graceGroup!.id; @@ -138,7 +173,7 @@ export class BarLayoutingInfo { return spring; } - public addBeatSpring(beat: Beat, preBeatSize: number, postBeatSize: number): void { + public addBeatSpring(beat: BeatContainerGlyphBase, preBeatSize: number, postBeatSize: number): void { const start: number = beat.absoluteDisplayStart; if (beat.graceType !== GraceType.None) { // For grace beats we just remember the the sizes required for them diff --git a/packages/alphatab/src/rendering/staves/RenderStaff.ts b/packages/alphatab/src/rendering/staves/RenderStaff.ts index e85d7b0c3..5a0aa6be1 100644 --- a/packages/alphatab/src/rendering/staves/RenderStaff.ts +++ b/packages/alphatab/src/rendering/staves/RenderStaff.ts @@ -2,11 +2,14 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Staff } from '@coderline/alphatab/model/Staff'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; -import type { BarRendererFactory } from '@coderline/alphatab/rendering/BarRendererFactory'; +import { + type BarRendererFactory, + type EffectBandInfo, + EffectBandMode +} from '@coderline/alphatab/rendering/BarRendererFactory'; import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; import type { StaffSystem } from '@coderline/alphatab/rendering/staves/StaffSystem'; import type { StaffTrackGroup } from '@coderline/alphatab/rendering/staves/StaffTrackGroup'; -import { InternalSystemsLayoutMode } from '@coderline/alphatab/rendering/layout/ScoreLayout'; /** * A Staff represents a single line within a StaffSystem. @@ -26,7 +29,15 @@ export class RenderStaff { public index: number = 0; public staffIndex: number = 0; - public isFirstInSystem: boolean = false; + public isVisible = false; + private _emptyBarCount = 0; + + public get isFirstInSystem() { + return this.system.firstVisibleStaff === this; + } + + public topEffectInfos: EffectBandInfo[] = []; + public bottomEffectInfos: EffectBandInfo[] = []; /** * This is the index of the track being rendered. This is not the index of the track within the model, @@ -46,30 +57,50 @@ export class RenderStaff { * Staff contents actually start. Used for grouping * using a accolade */ - public staveTop: number = 0; + public staffTop: number = 0; - public topSpacing: number = 0; - public bottomSpacing: number = 0; + public topPadding: number = 0; + public bottomPadding: number = 0; /** * This is the visual offset from top where the * Staff contents actually ends. Used for grouping * using a accolade */ - public staveBottom: number = 0; + public staffBottom: number = 0; public get contentTop() { - return this.y + this.staveTop + this.topSpacing + this.topOverflow; + return this.y + this.staffTop + this.topPadding + this.topOverflow; } public get contentBottom() { - return this.y + this.topSpacing + this.topOverflow + this.staveBottom; + return this.y + this.topPadding + this.topOverflow + this.staffBottom; } - public constructor(trackIndex: number, staff: Staff, factory: BarRendererFactory) { + public constructor(system: StaffSystem, trackIndex: number, staff: Staff, factory: BarRendererFactory) { this._factory = factory; this.trackIndex = trackIndex; this.modelStaff = staff; + this.system = system; + for (const b of factory.effectBands) { + if (b.shouldCreate && !b.shouldCreate!(staff)) { + continue; + } + + switch (b.mode) { + case EffectBandMode.OwnedTop: + case EffectBandMode.SharedTop: + this.topEffectInfos.push(b); + break; + + case EffectBandMode.OwnedBottom: + case EffectBandMode.SharedBottom: + this.bottomEffectInfos.push(b); + break; + } + } + + this._updateVisibility(); } public getSharedLayoutData(key: string, def: T): T { @@ -83,20 +114,16 @@ export class RenderStaff { this._sharedLayoutData.set(key, def); } - public get isInsideBracket(): boolean { - return this._factory.isInsideBracket; - } - - public get isRelevantForBoundsLookup(): boolean { - return this._factory.isRelevantForBoundsLookup; - } - public registerStaffTop(offset: number): void { - this.staveTop = offset; + if (offset > this.staffTop) { + this.staffTop = offset; + } } public registerStaffBottom(offset: number): void { - this.staveBottom = offset; + if (offset > this.staffBottom) { + this.staffBottom = offset; + } } public addBarRenderer(renderer: BarRendererBase): void { @@ -105,118 +132,78 @@ export class RenderStaff { renderer.reLayout(); this.barRenderers.push(renderer); this.system.layout.registerBarRenderer(this.staffId, renderer); + if (renderer.bar.isEmpty || renderer.bar.isRestOnly) { + this._emptyBarCount++; + } + this._updateVisibility(); + } + + private _updateVisibility() { + const stylesheet = this.modelStaff.track.score.stylesheet; + const canHideEmptyStaves = + stylesheet.hideEmptyStaves && (stylesheet.hideEmptyStavesInFirstSystem || this.system.index > 0); + if (canHideEmptyStaves) { + this.isVisible = this._emptyBarCount < this.barRenderers.length; + } else { + this.isVisible = true; + } } public addBar(bar: Bar, layoutingInfo: BarLayoutingInfo, additionalMultiBarsRestBars: Bar[] | null): void { const renderer = this._factory.create(this.system.layout.renderer, bar); + + renderer.topEffects.infos = this.topEffectInfos; + renderer.bottomEffects.infos = this.bottomEffectInfos; + renderer.additionalMultiRestBars = additionalMultiBarsRestBars; renderer.staff = this; renderer.index = this.barRenderers.length; renderer.layoutingInfo = layoutingInfo; renderer.doLayout(); - renderer.registerLayoutingInfo(); - - // For cases like in the horizontal layout we need to set the fixed width early - // to have correct partials splitting - const barDisplayWidth = renderer.barDisplayWidth; - if ( - barDisplayWidth > 0 && - this.system.layout.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithWidths - ) { - renderer.width = barDisplayWidth; - } this.barRenderers.push(renderer); - if (bar) { - this.system.layout.registerBarRenderer(this.staffId, renderer); + this.system.layout.registerBarRenderer(this.staffId, renderer); + if (bar.isEmpty || bar.isRestOnly) { + this._emptyBarCount++; } + this._updateVisibility(); } public revertLastBar(): BarRendererBase { + this.resetSharedLayoutData(); + const lastBar: BarRendererBase = this.barRenderers[this.barRenderers.length - 1]; this.barRenderers.splice(this.barRenderers.length - 1, 1); - this.system.layout.unregisterBarRenderer(this.staffId, lastBar); + this.topOverflow = 0; + this.bottomOverflow = 0; for (const r of this.barRenderers) { - r.applyLayoutingInfo(); + r.afterStaffBarReverted(); } + + if (lastBar.bar.isEmpty || lastBar.bar.isRestOnly) { + this._emptyBarCount--; + } + this._updateVisibility(); + return lastBar; } - public scaleToWidth(width: number): void { - this._sharedLayoutData = new Map(); - const topOverflow: number = this.topOverflow; - let x = 0; - - switch (this.system.layout.systemsLayoutMode) { - case InternalSystemsLayoutMode.Automatic: - // Note: here we could do some "intelligent" distribution of - // the space over the bar renderers, for now we evenly apply the space to all bars - const difference: number = width - this.system.computedWidth; - const spacePerBar: number = difference / this.barRenderers.length; - for (const renderer of this.barRenderers) { - renderer.x = x; - renderer.y = this.topSpacing + topOverflow; - - const actualBarWidth = renderer.computedWidth + spacePerBar; - renderer.scaleToWidth(actualBarWidth); - x += renderer.width; - } - break; - case InternalSystemsLayoutMode.FromModelWithScale: - // each bar holds a percentual size where the sum of all scales make the width. - // hence we can calculate the width accordingly by calculating how big each column needs to be percentual. - - width -= this.system.accoladeWidth; - const totalScale = this.system.totalBarDisplayScale; - - for (const renderer of this.barRenderers) { - renderer.x = x; - renderer.y = this.topSpacing + topOverflow; - - const actualBarWidth = (renderer.barDisplayScale * width) / totalScale; - renderer.scaleToWidth(actualBarWidth); - - x += renderer.width; - } - - break; - case InternalSystemsLayoutMode.FromModelWithWidths: - for (const renderer of this.barRenderers) { - renderer.x = x; - renderer.y = this.topSpacing + topOverflow; - const displayWidth = renderer.barDisplayWidth; - if (displayWidth > 0) { - renderer.scaleToWidth(displayWidth); - } else { - renderer.scaleToWidth(renderer.computedWidth); - } - - x += renderer.width; - } - break; - } + public resetSharedLayoutData() { + this._sharedLayoutData.clear(); } - public get topOverflow(): number { - let m: number = 0; - for (let i: number = 0, j: number = this.barRenderers.length; i < j; i++) { - const r: BarRendererBase = this.barRenderers[i]; - if (r.topOverflow > m) { - m = r.topOverflow; - } + public topOverflow = 0; + public registerOverflowTop(overflow: number) { + if (overflow > this.topOverflow) { + this.topOverflow = overflow; } - return m; } - public get bottomOverflow(): number { - let m: number = 0; - for (let i: number = 0, j: number = this.barRenderers.length; i < j; i++) { - const r: BarRendererBase = this.barRenderers[i]; - if (r.bottomOverflow > m) { - m = r.bottomOverflow; - } + public bottomOverflow = 0; + public registerOverflowBottom(overflow: number) { + if (overflow > this.bottomOverflow) { + this.bottomOverflow = overflow; } - return m; } /** @@ -225,19 +212,25 @@ export class RenderStaff { * and we can do an early placement of the render staffs. */ public calculateHeightForAccolade() { - this.topSpacing = this._factory.getStaffPaddingTop(this); - this.bottomSpacing = this._factory.getStaffPaddingBottom(this); + this._applyStaffPaddings(); this.height = this.barRenderers.length > 0 ? this.barRenderers[0].height : 0; if (this.height > 0) { - this.height += Math.ceil(this.topSpacing + this.topOverflow + this.bottomOverflow + this.bottomSpacing); + this.height += Math.ceil(this.topPadding + this.topOverflow + this.bottomOverflow + this.bottomPadding); } } + private _applyStaffPaddings() { + const isFirst = this.index === 0; + const isLast = this.index === this.system.staves.length - 1; + const settings = this.system.layout.renderer.settings.display; + this.topPadding = isFirst ? settings.firstNotationStaffPaddingTop : settings.notationStaffPaddingTop; + this.bottomPadding = isLast ? settings.lastNotationStaffPaddingBottom : settings.notationStaffPaddingBottom; + } + public finalizeStaff(): void { - this.topSpacing = this._factory.getStaffPaddingTop(this); - this.bottomSpacing = this._factory.getStaffPaddingBottom(this); + this._applyStaffPaddings(); this.height = 0; @@ -245,33 +238,35 @@ export class RenderStaff { // changes in the overflows let needsSecondPass = false; let topOverflow: number = this.topOverflow; - for (let i: number = 0; i < this.barRenderers.length; i++) { - this.barRenderers[i].y = this.topSpacing + topOverflow; - this.height = Math.max(this.height, this.barRenderers[i].height); - if (this.barRenderers[i].finalizeRenderer()) { + for (const renderer of this.barRenderers) { + renderer.registerMultiSystemSlurs(this.system.layout!.slurRegistry.getAllContinuations(renderer)); + if (renderer.finalizeRenderer()) { needsSecondPass = true; } + this.height = Math.max(this.height, renderer.height); } // 2nd pass: move renderers to correct position respecting the new overflows if (needsSecondPass) { topOverflow = this.topOverflow; // shift all the renderers to the new position to match required spacing - for (let i: number = 0; i < this.barRenderers.length; i++) { - this.barRenderers[i].y = this.topSpacing + topOverflow; + for (const renderer of this.barRenderers) { + renderer.y = this.topPadding + topOverflow; } // finalize again (to align ties) - for (let i: number = 0; i < this.barRenderers.length; i++) { - this.barRenderers[i].finalizeRenderer(); + for (const renderer of this.barRenderers) { + renderer.finalizeRenderer(); } } if (this.height > 0) { - this.height += this.topSpacing + topOverflow + this.bottomOverflow + this.bottomSpacing; + this.height += this.topPadding + topOverflow + this.bottomOverflow + this.bottomPadding; } this.height = Math.ceil(this.height); + + this._updateVisibility(); } public paint(cx: number, cy: number, canvas: ICanvas, startIndex: number, count: number): void { diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index 0853037b3..71af4a29e 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -1,40 +1,83 @@ +import type { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Font } from '@coderline/alphatab/model/Font'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { + BracketExtendMode, + TrackNameMode, + TrackNameOrientation, + TrackNamePolicy +} from '@coderline/alphatab/model/RenderStylesheet'; import { type Track, TrackSubElement } from '@coderline/alphatab/model/Track'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; +import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; import type { ScoreLayout } from '@coderline/alphatab/rendering/layout/ScoreLayout'; import { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; import { MasterBarsRenderers } from '@coderline/alphatab/rendering/staves/MasterBarsRenderers'; import type { RenderStaff } from '@coderline/alphatab/rendering/staves/RenderStaff'; import { StaffTrackGroup } from '@coderline/alphatab/rendering/staves/StaffTrackGroup'; import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; import { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds'; import { StaffSystemBounds } from '@coderline/alphatab/rendering/utils/StaffSystemBounds'; -import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; -import { NotationElement } from '@coderline/alphatab/NotationSettings'; -import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@coderline/alphatab/model/RenderStylesheet'; -import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; -import type { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; /** * @internal */ export abstract class SystemBracket { - public firstStaffInBracket: RenderStaff | null = null; - public lastStaffInBracket: RenderStaff | null = null; + private _system: StaffSystem; + public firstStaffInBracket?: RenderStaff; + public lastStaffInBracket?: RenderStaff; + public firstVisibleStaffInBracket?: RenderStaff; + public lastVisibleStaffInBracket?: RenderStaff; public drawAsBrace: boolean = false; public braceScale: number = 1; public width: number = 0; public index: number = 0; + public canPaint = false; + + public constructor(system: StaffSystem) { + this._system = system; + } + public abstract includesStaff(s: RenderStaff): boolean; + public updateCanPaint() { + let firstVisibleStaff: RenderStaff | undefined = undefined; + let lastVisibleStaff: RenderStaff | undefined = undefined; + for (let i = this.firstStaffInBracket!.index; i <= this.lastStaffInBracket!.index; i++) { + const staff = this._system.allStaves[i]; + if (staff.isVisible) { + if (!firstVisibleStaff) { + firstVisibleStaff = staff; + } + lastVisibleStaff = staff; + } + } + this.firstVisibleStaffInBracket = firstVisibleStaff; + this.lastVisibleStaffInBracket = lastVisibleStaff; + + if (!firstVisibleStaff || !lastVisibleStaff) { + this.canPaint = false; + return; + } + + // single staff brackets? + const singleStaffBrackets = this._system.layout.renderer.score!.stylesheet.showSingleStaffBrackets; + if (!singleStaffBrackets && firstVisibleStaff === lastVisibleStaff) { + this.canPaint = false; + return; + } + + this.canPaint = true; + } + public finalizeBracket(smuflMetrics: EngravingSettings) { - // systems with just a single staff do not have a bracket - if (this.firstStaffInBracket === this.lastStaffInBracket) { + if (!this.canPaint) { this.width = 0; return; } @@ -49,12 +92,13 @@ export abstract class SystemBracket { } else { this.width = smuflMetrics.bracketThickness; } - if (!this.drawAsBrace || !this.firstStaffInBracket || !this.lastStaffInBracket) { + + if (!this.drawAsBrace) { return; } - const firstStart: number = this.firstStaffInBracket.contentTop; - const lastEnd: number = this.lastStaffInBracket.contentBottom; + const firstStart: number = this.firstVisibleStaffInBracket!.contentTop; + const lastEnd: number = this.lastVisibleStaffInBracket!.contentBottom; const requiredHeight = lastEnd - firstStart; const requiredScaleForBracket = requiredHeight / bravuraBraceHeightAtMusicFontSize; @@ -69,8 +113,8 @@ export abstract class SystemBracket { class SingleTrackSystemBracket extends SystemBracket { protected track: Track; - public constructor(track: Track) { - super(); + public constructor(system: StaffSystem, track: Track) { + super(system); this.track = track; this.drawAsBrace = SingleTrackSystemBracket.isTrackDrawAsBrace(track); } @@ -110,13 +154,12 @@ class SimilarInstrumentSystemBracket extends SingleTrackSystemBracket { * @internal */ export class StaffSystem { - private _allStaves: RenderStaff[] = []; - private _firstStaffInBrackets: RenderStaff | null = null; - private _lastStaffInBrackets: RenderStaff | null = null; - private _accoladeSpacingCalculated: boolean = false; private _brackets: SystemBracket[] = []; + private _staffToBracket = new Map(); + private _contentHeight = 0; + private _hasSystemSeparator = false; public x: number = 0; @@ -136,10 +179,28 @@ export class StaffSystem { public isFull: boolean = false; /** - * The width that the content bars actually need + * The current width of the system to which the content is scaled. + * Includes accolade (tracknames, brackets etc) and the content. + * + * Used to determine the final size needed for rendering. */ public width: number = 0; + + /** + * The minimum/default width to which the system was sized + * when performing the layout. This is the size of the system if no + * fitting/resizing is performed. + * + * Includes accolade (tracknames, brackets etc) and the content. + * + * Used to perform a resizing/refitting of the system. + */ public computedWidth: number = 0; + + /** + * This is the simple sum of all display scales of the bars in this system. + * This value is mainly used in the parchment style layout for correct scaling of the bars. + */ public totalBarDisplayScale: number = 0; public isLast: boolean = false; @@ -149,6 +210,8 @@ export class StaffSystem { public topPadding: number; public bottomPadding: number; + public allStaves: RenderStaff[] = []; + public firstVisibleStaff?: RenderStaff; public constructor(layout: ScoreLayout) { this.layout = layout; @@ -171,17 +234,51 @@ export class StaffSystem { this.masterBarsRenderers.push(renderers); renderers.layoutingInfo.preBeatSize = 0; let src: number = 0; - for (let i: number = 0, j: number = this.staves.length; i < j; i++) { - const g: StaffTrackGroup = this.staves[i]; - for (let k: number = 0, l: number = g.staves.length; k < l; k++) { - const s: RenderStaff = g.staves[k]; + + let firstVisibleStaff: RenderStaff | undefined = undefined; + let anyStaffVisible = false; + + for (const g of this.staves) { + let firstVisibleStaffInGroup: RenderStaff | undefined = undefined; + let lastVisibleStaffInGroup: RenderStaff | undefined = undefined; + + for (const s of g.staves) { const renderer: BarRendererBase = renderers.renderers[src++]; s.addBarRenderer(renderer); + + if (s.isVisible) { + anyStaffVisible = true; + if (!firstVisibleStaffInGroup) { + firstVisibleStaffInGroup = s; + } + if (!firstVisibleStaff) { + firstVisibleStaff = s; + } + + lastVisibleStaffInGroup = s; + } + } + + g.firstVisibleStaff = firstVisibleStaffInGroup; + g.lastVisibleStaff = lastVisibleStaffInGroup; + if (!firstVisibleStaff) { + firstVisibleStaff = firstVisibleStaffInGroup; } } + + if (!anyStaffVisible) { + const group = this.staves[0]; + const firstStaff = group.staves[0]; + firstStaff.isVisible = true; + group.firstVisibleStaff = firstStaff; + group.lastVisibleStaff = firstStaff; + firstVisibleStaff = firstStaff; + } + + this.firstVisibleStaff = firstVisibleStaff; this._calculateAccoladeSpacing(tracks); - this._updateWidthFromLastBar(); + this._applyLayoutAndUpdateWidth(); return renderers; } @@ -196,9 +293,14 @@ export class StaffSystem { result.masterBar = tracks[0].score.masterBars[barIndex]; this.masterBarsRenderers.push(result); + let firstVisibleStaff: RenderStaff | undefined = undefined; + let anyStaffVisible = false; // add renderers const barLayoutingInfo: BarLayoutingInfo = result.layoutingInfo; for (const g of this.staves) { + let firstVisibleStaffInGroup: RenderStaff | undefined = undefined; + let lastVisibleStaffInGroup: RenderStaff | undefined = undefined; + for (const s of g.staves) { const bar: Bar = g.track.staves[s.modelStaff.index].bars[barIndex]; @@ -208,6 +310,15 @@ export class StaffSystem { : additionalMultiBarRestIndexes.map(b => g.track.staves[s.modelStaff.index].bars[b]); s.addBar(bar, barLayoutingInfo, additionalMultiBarsRestBars); + + if (s.isVisible) { + anyStaffVisible = true; + if (!firstVisibleStaffInGroup) { + firstVisibleStaffInGroup = s; + } + lastVisibleStaffInGroup = s; + } + const renderer: BarRendererBase = s.barRenderers[s.barRenderers.length - 1]; result.renderers.push(renderer); if (renderer.isLinkedToPrevious) { @@ -217,34 +328,75 @@ export class StaffSystem { result.canWrap = false; } } + g.firstVisibleStaff = firstVisibleStaffInGroup; + g.lastVisibleStaff = lastVisibleStaffInGroup; + if (!firstVisibleStaff) { + firstVisibleStaff = firstVisibleStaffInGroup; + } } + + if (!anyStaffVisible) { + const group = this.staves[0]; + const firstStaff = group.staves[0]; + firstStaff.isVisible = true; + group.firstVisibleStaff = firstStaff; + group.lastVisibleStaff = firstStaff; + firstVisibleStaff = firstStaff; + } + + this.firstVisibleStaff = firstVisibleStaff; + this._calculateAccoladeSpacing(tracks); barLayoutingInfo.finish(); // ensure same widths of new renderer - result.width = this._updateWidthFromLastBar(); + result.width = this._applyLayoutAndUpdateWidth(); return result; } + public getBarDisplayScale(renderer: BarRendererBase) { + return this.staves.length > 1 ? renderer.bar.masterBar.displayScale : renderer.bar.displayScale; + } + public revertLastBar(): MasterBarsRenderers | null { if (this.masterBarsRenderers.length > 1) { const toRemove: MasterBarsRenderers = this.masterBarsRenderers[this.masterBarsRenderers.length - 1]; this.masterBarsRenderers.splice(this.masterBarsRenderers.length - 1, 1); let width: number = 0; - let barDisplayScale: number = 0; - for (let i: number = 0, j: number = this._allStaves.length; i < j; i++) { - const s: RenderStaff = this._allStaves[i]; - const lastBar: BarRendererBase = s.revertLastBar(); - const computedWidth = lastBar.computedWidth; - if (computedWidth > width) { - width = computedWidth; + let barDisplayScale = 0; + + let firstVisibleStaff: RenderStaff | undefined = undefined; + for (const g of this.staves) { + let firstVisibleStaffInGroup: RenderStaff | undefined = undefined; + let lastVisibleStaffInGroup: RenderStaff | undefined = undefined; + + for (const s of g.staves) { + const lastBar: BarRendererBase = s.revertLastBar(); + const computedWidth = lastBar.computedWidth; + if (computedWidth > width) { + width = computedWidth; + } + lastBar.afterReverted(); + + barDisplayScale = this.getBarDisplayScale(lastBar); + + if (s.isVisible) { + if (!firstVisibleStaffInGroup) { + firstVisibleStaffInGroup = s; + } + lastVisibleStaffInGroup = s; + } } - const newBarDisplayScale = lastBar.barDisplayScale; - if (newBarDisplayScale > barDisplayScale) { - barDisplayScale = newBarDisplayScale; + + g.firstVisibleStaff = firstVisibleStaffInGroup; + g.lastVisibleStaff = lastVisibleStaffInGroup; + if (!firstVisibleStaff) { + firstVisibleStaff = firstVisibleStaffInGroup; } } + this.firstVisibleStaff = firstVisibleStaff; + this.width -= width; this.computedWidth -= width; this.totalBarDisplayScale -= barDisplayScale; @@ -253,26 +405,24 @@ export class StaffSystem { return null; } - private _updateWidthFromLastBar(): number { + private _applyLayoutAndUpdateWidth(): number { let realWidth: number = 0; - let barDisplayScale: number = 0; - for (let i: number = 0, j: number = this._allStaves.length; i < j; i++) { - const s: RenderStaff = this._allStaves[i]; + let barDisplayScale = 0; + for (const s of this.allStaves) { const last = s.barRenderers[s.barRenderers.length - 1]; last.applyLayoutingInfo(); + + barDisplayScale = this.getBarDisplayScale(last); + if (last.computedWidth > realWidth) { realWidth = last.computedWidth; } - - const newBarDisplayScale = last.barDisplayScale; - if (newBarDisplayScale > barDisplayScale) { - barDisplayScale = newBarDisplayScale; - } } + + this.totalBarDisplayScale += barDisplayScale; this.width += realWidth; this.computedWidth += realWidth; - this.totalBarDisplayScale += barDisplayScale; return realWidth; } @@ -319,7 +469,7 @@ export class StaffSystem { let hasAnyTrackName = false; if (shouldRender) { const canvas: ICanvas = this.layout.renderer.canvas!; - const res: Font = settings.display.resources.effectFont; + const res: Font = settings.display.resources.elementFonts.get(NotationElement.TrackNames)!; canvas.font = res; for (const t of tracks) { let trackNameText = ''; @@ -346,8 +496,8 @@ export class StaffSystem { } } + this.accoladeWidth += settings.display.systemLabelPaddingLeft; if (hasAnyTrackName) { - this.accoladeWidth += settings.display.systemLabelPaddingLeft; this.accoladeWidth += settings.display.systemLabelPaddingRight; } } @@ -367,7 +517,7 @@ export class StaffSystem { // - requires a feature to draw glyphs with a max-width or a horizontal stretch scale let currentY: number = 0; - for (const staff of this._allStaves) { + for (const staff of this.allStaves) { staff.y = currentY; staff.calculateHeightForAccolade(); currentY += staff.height; @@ -375,6 +525,7 @@ export class StaffSystem { let braceWidth = 0; for (const b of this._brackets) { + b.updateCanPaint(); b.finalizeBracket(settings.display.resources.engravingSettings); braceWidth = Math.max(braceWidth, b.width); } @@ -383,6 +534,11 @@ export class StaffSystem { this.width += this.accoladeWidth; this.computedWidth += this.accoladeWidth; + } else { + for (const b of this._brackets) { + b.updateCanPaint(); + b.finalizeBracket(settings.display.resources.engravingSettings); + } } } @@ -396,7 +552,8 @@ export class StaffSystem { return null; } - public addStaff(track: Track, staff: RenderStaff): void { + public addStaff(staff: RenderStaff): void { + const track = staff.modelStaff.track; let group: StaffTrackGroup | null = this._getStaffTrackGroup(track); if (!group) { group = new StaffTrackGroup(this, track); @@ -404,66 +561,42 @@ export class StaffSystem { } staff.staffTrackGroup = group; staff.system = this; - staff.index = this._allStaves.length; - this._allStaves.push(staff); + staff.index = this.allStaves.length; + this.allStaves.push(staff); group.addStaff(staff); - if (staff.isInsideBracket) { - if (!this._firstStaffInBrackets) { - this._firstStaffInBrackets = staff; - staff.isFirstInSystem = true; - } - if (!group.firstStaffInBracket) { - group.firstStaffInBracket = staff; - } - this._lastStaffInBrackets = staff; - group.lastStaffInBracket = staff; - let bracket = this._brackets.find(b => b.includesStaff(staff)); - if (!bracket) { - switch (track.score.stylesheet.bracketExtendMode) { - case BracketExtendMode.NoBrackets: - break; - case BracketExtendMode.GroupStaves: - // when grouping staves, we create one bracket for the whole track across all staves - bracket = new SingleTrackSystemBracket(track); - bracket.index = this._brackets.length; - this._brackets.push(bracket); - break; - case BracketExtendMode.GroupSimilarInstruments: - bracket = new SimilarInstrumentSystemBracket(track); - bracket.index = this._brackets.length; - this._brackets.push(bracket); - break; - } + let bracket = this._brackets.find(b => b.includesStaff(staff)); + if (!bracket) { + switch (track.score.stylesheet.bracketExtendMode) { + case BracketExtendMode.NoBrackets: + break; + case BracketExtendMode.GroupStaves: + // when grouping staves, we create one bracket for the whole track across all staves + bracket = new SingleTrackSystemBracket(this, track); + bracket.index = this._brackets.length; + this._brackets.push(bracket); + break; + case BracketExtendMode.GroupSimilarInstruments: + bracket = new SimilarInstrumentSystemBracket(this, track); + bracket.index = this._brackets.length; + this._brackets.push(bracket); + break; } + } - if (bracket) { - if (!bracket.firstStaffInBracket) { - bracket.firstStaffInBracket = staff; - } - bracket.lastStaffInBracket = staff; - // NOTE: one StaffTrackGroup can currently never have multiple brackets so we can safely keep the last known here - group.bracket = bracket; + if (bracket) { + if (!bracket.firstStaffInBracket) { + bracket.firstStaffInBracket = staff; } + bracket.lastStaffInBracket = staff; + // NOTE: one StaffTrackGroup can currently never have multiple brackets so we can safely keep the last known here + group.bracket = bracket; + this._staffToBracket.set(staff, bracket); } } public get height(): number { - return this._allStaves.length === 0 - ? 0 - : Math.ceil( - this._allStaves[this._allStaves.length - 1].y + - this._allStaves[this._allStaves.length - 1].height + - this.topPadding + - this.bottomPadding - ); - } - - public scaleToWidth(width: number): void { - for (let i: number = 0, j: number = this._allStaves.length; i < j; i++) { - this._allStaves[i].scaleToWidth(width); - } - this.width = width; + return Math.ceil(this._contentHeight + this.topPadding + this.bottomPadding); } public paint(cx: number, cy: number, canvas: ICanvas): void { @@ -478,7 +611,7 @@ export class StaffSystem { using _ = ElementStyleHelper.track( canvas, TrackSubElement.SystemSeparator, - this._allStaves[0].modelStaff.track + this.allStaves[0].modelStaff.track ); // NOTE: the divider is currently not "nicely" centered between the systems as this would lead to cropping @@ -506,9 +639,12 @@ export class StaffSystem { } public paintPartial(cx: number, cy: number, canvas: ICanvas, startIndex: number, count: number): void { - for (let i: number = 0, j: number = this._allStaves.length; i < j; i++) { - this._allStaves[i].paint(cx, cy, canvas, startIndex, count); + for (const s of this.allStaves) { + if (s.isVisible) { + s.paint(cx, cy, canvas, startIndex, count); + } } + const res: RenderingResources = this.layout.renderer.settings.display.resources; if (this.staves.length > 0 && startIndex === 0) { @@ -523,7 +659,7 @@ export class StaffSystem { const hasTrackName = this.layout.renderer.settings.notation.isNotationElementVisible( NotationElement.TrackNames ); - canvas.font = res.effectFont; + canvas.font = res.elementFonts.get(NotationElement.TrackNames)!; if (hasTrackName) { const stylesheet = this.layout.renderer.score!.stylesheet; @@ -557,9 +693,9 @@ export class StaffSystem { const oldBaseLine = canvas.textBaseline; const oldTextAlign = canvas.textAlign; for (const g of this.staves) { - if (g.firstStaffInBracket && g.lastStaffInBracket) { - const firstStart: number = cy + g.firstStaffInBracket.contentTop; - const lastEnd: number = cy + g.lastStaffInBracket.contentBottom; + if (g.firstVisibleStaff) { + const firstStart: number = cy + g.firstVisibleStaff.contentTop; + const lastEnd: number = cy + g.lastVisibleStaff!.contentBottom; let trackNameText = ''; switch (trackNameMode) { @@ -581,7 +717,7 @@ export class StaffSystem { const textEndX = // start at beginning of first renderer cx + - g.firstStaffInBracket.x - + g.staves[0].x - // left side of the bracket settings.display.accoladeBarPaddingRight - (g.bracket?.width ?? 0) - @@ -619,84 +755,89 @@ export class StaffSystem { } } - if (this._allStaves.length > 0) { + const needsSystemBarLine = !this.layout.renderer.score!.stylesheet.extendBarLines; + if (this.allStaves.length > 0 && needsSystemBarLine) { let previousStaffInBracket: RenderStaff | null = null; - for (const s of this._allStaves) { - if (s.isInsideBracket) { - if (previousStaffInBracket !== null) { - const previousBottom = previousStaffInBracket.contentBottom; - const thisTop = s.contentTop; + for (const s of this.allStaves) { + if (!s.isVisible) { + continue; + } - const accoladeX: number = cx + previousStaffInBracket.x; + if (previousStaffInBracket !== null) { + const previousBottom = previousStaffInBracket.contentBottom; + const thisTop = s.contentTop; - const firstLineBarRenderer = previousStaffInBracket.barRenderers[0] as LineBarRenderer; + const accoladeX: number = cx + previousStaffInBracket.x; - using _ = ElementStyleHelper.bar( - canvas, - firstLineBarRenderer.staffLineBarSubElement, - firstLineBarRenderer.bar - ); - const h = Math.ceil(thisTop - previousBottom); - canvas.fillRect( - accoladeX, - cy + previousBottom, - res.engravingSettings.thinBarlineThickness, - h - ); - } + const firstLineBarRenderer = previousStaffInBracket.barRenderers[0] as LineBarRenderer; - previousStaffInBracket = s; + using _ = ElementStyleHelper.bar( + canvas, + firstLineBarRenderer.staffLineBarSubElement, + firstLineBarRenderer.bar + ); + const h = Math.ceil(thisTop - previousBottom); + canvas.fillRect(accoladeX, cy + previousBottom, res.engravingSettings.thinBarlineThickness, h); } + + previousStaffInBracket = s; } } // // Draw brackets - for (const bracket of this._brackets!) { - if (bracket.firstStaffInBracket && bracket.lastStaffInBracket) { - const barStartX: number = cx + bracket.firstStaffInBracket.x; - const barSize: number = bracket.width; - const barOffset: number = settings.display.accoladeBarPaddingRight; - const firstStart: number = cy + bracket.firstStaffInBracket.contentTop; - const lastEnd: number = cy + bracket.lastStaffInBracket.contentBottom; - let accoladeStart: number = firstStart; - let accoladeEnd: number = lastEnd; - - if (bracket.drawAsBrace) { - CanvasHelper.fillMusicFontSymbolSafe( - canvas, - barStartX - barOffset - barSize, - accoladeEnd, - bracket.braceScale, - MusicFontSymbol.Brace - ); - } else if (bracket.firstStaffInBracket !== bracket.lastStaffInBracket) { - const barOverflow = barSize / 2; - accoladeStart -= barOverflow; - accoladeEnd += barOverflow * 2; - canvas.fillRect( - barStartX - barOffset - barSize, - accoladeStart, - barSize, - Math.ceil(accoladeEnd - accoladeStart) - ); + this._paintBrackets(cx, cy, canvas); + } + } - const spikeX: number = barStartX - barOffset - barSize; - CanvasHelper.fillMusicFontSymbolSafe( - canvas, - spikeX, - accoladeStart, - 1, - MusicFontSymbol.BracketTop - ); - CanvasHelper.fillMusicFontSymbolSafe( - canvas, - spikeX, - Math.floor(accoladeEnd), - 1, - MusicFontSymbol.BracketBottom - ); - } + private _paintBrackets(cx: number, cy: number, canvas: ICanvas) { + const settings = this.layout.renderer.settings; + + for (const bracket of this._brackets!) { + if (bracket.canPaint) { + const barStartX: number = cx + bracket.firstVisibleStaffInBracket!.x; + const barSize: number = bracket.width; + const barOffset: number = settings.display.accoladeBarPaddingRight; + const firstStart: number = cy + bracket.firstVisibleStaffInBracket!.contentTop; + const lastEnd: number = cy + bracket.lastVisibleStaffInBracket!.contentBottom; + let accoladeStart: number = firstStart; + let accoladeEnd: number = lastEnd; + + if (bracket.drawAsBrace) { + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + barStartX - barOffset - barSize, + accoladeEnd, + bracket.braceScale, + MusicFontSymbol.Brace + ); + } else if (bracket.firstVisibleStaffInBracket !== bracket.lastVisibleStaffInBracket) { + // brackets typically overflow by 1/4 staff-space + const smuflMetrics = settings.display.resources.engravingSettings; + + const bracketOverflow = smuflMetrics.oneStaffSpace * 0.25; + accoladeStart -= bracketOverflow; + accoladeEnd += bracketOverflow; + + // we shift the bar slightly inward so that the spike will hide the edge + // if we're precise we might see a slight light line on subpixel level + const barShift = 3; + canvas.fillRect( + barStartX - barOffset - barSize, + accoladeStart - barShift, + barSize, + Math.ceil(accoladeEnd - accoladeStart + barShift * 2) + ); + + const spikeX: number = barStartX - barOffset - barSize; + CanvasHelper.fillMusicFontSymbolSafe(canvas, spikeX, accoladeStart, 1, MusicFontSymbol.BracketTop); + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + spikeX, + Math.floor(accoladeEnd), + 1, + MusicFontSymbol.BracketBottom + ); } } } @@ -722,12 +863,15 @@ export class StaffSystem { this._hasSystemSeparator = true; } - let currentY: number = 0; - for (const staff of this._allStaves) { - staff.x = this.accoladeWidth; - staff.y = currentY; - staff.finalizeStaff(); - currentY += staff.height; + const anyStaffVisible = this._finalizeTrackGroups(); + + // for now we always force one staff to be visible. + // making also whole systems invisible needs separate attention (also on player cursor handling) + if (!anyStaffVisible) { + const group = this.staves[0]; + const firstStaff = group.staves[0]; + firstStaff.isVisible = true; + this._finalizeTrackGroups(true); } for (const b of this._brackets!) { @@ -735,41 +879,111 @@ export class StaffSystem { } } + private _finalizeTrackGroups(onlyFirstGroup: boolean = false) { + let currentY: number = 0; + const settings = this.layout.renderer.settings; + const smufl = settings.display.resources.engravingSettings; + const topBracketSpikeHeight = smufl.glyphHeights.get(MusicFontSymbol.BracketTop)!; + const bottomBracketSpikeHeight = smufl.glyphHeights.get(MusicFontSymbol.BracketBottom)!; + + let previousStaff: RenderStaff | undefined = undefined; + + let endSpikeOverflow = 0; + let anyStaffVisible = false; + for (const group of this.staves) { + let firstVisibleStaffInGroup: RenderStaff | undefined = undefined; + let lastVisibleStaffInGroup: RenderStaff | undefined = undefined; + for (const staff of group.staves) { + // check if we need "in-between padding" + if (previousStaff !== undefined && previousStaff!.trackIndex !== staff.trackIndex) { + currentY += settings.display.trackStaffPaddingBetween; + } + + const bracket = this._staffToBracket.has(staff) ? this._staffToBracket.get(staff) : undefined; + const hasBracket = bracket && !bracket.drawAsBrace && bracket.canPaint; + if (hasBracket && bracket!.firstStaffInBracket === staff) { + const spikeOverflow = topBracketSpikeHeight - staff.topOverflow; + if (spikeOverflow > 0) { + currentY += spikeOverflow; + } + } + + staff.x = this.accoladeWidth; + staff.y = currentY; + if (!onlyFirstGroup) { + staff.finalizeStaff(); + } + + if (staff.isVisible) { + currentY += staff.height; + + anyStaffVisible = true; + previousStaff = staff; + + if (!firstVisibleStaffInGroup) { + firstVisibleStaffInGroup = staff; + } + lastVisibleStaffInGroup = staff; + } + + endSpikeOverflow = 0; + if (hasBracket && bracket!.lastStaffInBracket === staff) { + const spikeOverflow = bottomBracketSpikeHeight - staff.bottomOverflow; + if (spikeOverflow > 0) { + if (staff.isVisible) { + currentY += spikeOverflow; + } else { + endSpikeOverflow = spikeOverflow; + } + } + } + } + + group.firstVisibleStaff = firstVisibleStaffInGroup; + group.lastVisibleStaff = lastVisibleStaffInGroup; + + if (!this.firstVisibleStaff) { + this.firstVisibleStaff = firstVisibleStaffInGroup; + } + + if (onlyFirstGroup) { + break; + } + } + + // ensure we add overflow if last bracket is hidden + if (endSpikeOverflow) { + currentY += endSpikeOverflow; + } + + this._contentHeight = currentY; + + return anyStaffVisible; + } + public buildBoundingsLookup(cx: number, cy: number): void { if (this.layout.renderer.boundsLookup!.isFinished) { return; } - const _firstStaffInBrackets = this._firstStaffInBrackets; - const _lastStaffInBrackets = this._lastStaffInBrackets; - if (!_firstStaffInBrackets || !_lastStaffInBrackets) { - return; - } + const firstStaff = this.allStaves[0]; + const lastStaff = this.allStaves[this.allStaves.length - 1]; + cy += this.topPadding; - const lastStaff: RenderStaff = this._allStaves[this._allStaves.length - 1]; - const visualTop: number = cy + this.y + _firstStaffInBrackets.y; - const visualBottom: number = cy + this.y + _lastStaffInBrackets.y + _lastStaffInBrackets.height; - const realTop: number = cy + this.y + this._allStaves[0].y; - const realBottom: number = cy + this.y + lastStaff.y + lastStaff.height; - const lineTop: number = - cy + - this.y + - _firstStaffInBrackets.y + - _firstStaffInBrackets.topSpacing + - _firstStaffInBrackets.topOverflow + - (_firstStaffInBrackets.barRenderers.length > 0 ? _firstStaffInBrackets.barRenderers[0].topPadding : 0); - const lineBottom: number = - cy + - this.y + - lastStaff.y + - lastStaff.height - - lastStaff.bottomSpacing - - lastStaff.bottomOverflow - - (lastStaff.barRenderers.length > 0 ? lastStaff.barRenderers[0].bottomPadding : 0); - const visualHeight: number = visualBottom - visualTop; - const lineHeight: number = lineBottom - lineTop; - const realHeight: number = realBottom - realTop; - const x: number = this.x + _firstStaffInBrackets.x; + const visualTop: number = cy + this.y + firstStaff.y; + const visualBottom: number = cy + this.y + lastStaff.y + lastStaff.height; + const visualHeight = visualBottom - visualTop; + + const realTop: number = cy + this.y; + const realBottom: number = cy + this.y + this.height; + const realHeight = realBottom - realTop; + + const lineTop = cy + this.y + firstStaff.y + firstStaff.topPadding + firstStaff.topOverflow; + const lineBottom = + cy + this.y + lastStaff.y + lastStaff.height - lastStaff.bottomPadding - lastStaff.bottomOverflow; + const lineHeight = lineBottom - lineTop; + + const x: number = this.x + firstStaff.x; const staffSystemBounds = new StaffSystemBounds(); staffSystemBounds.visualBounds = new Bounds(); staffSystemBounds.visualBounds.x = cx + this.x; @@ -781,16 +995,20 @@ export class StaffSystem { staffSystemBounds.realBounds.y = cy + this.y; staffSystemBounds.realBounds.w = this.width; staffSystemBounds.realBounds.h = this.height; + this.layout.renderer.boundsLookup!.addStaffSystem(staffSystemBounds); const masterBarBoundsLookup: Map = new Map(); for (let i: number = 0; i < this.staves.length; i++) { - for (const staff of this.staves[i].stavesRelevantForBoundsLookup) { + for (const staff of this.staves[i].staves) { + if (!staff.isVisible) { + continue; + } for (const renderer of staff.barRenderers) { let masterBarBounds: MasterBarBounds; if (!masterBarBoundsLookup.has(renderer.bar.masterBar.index)) { masterBarBounds = new MasterBarBounds(); masterBarBounds.index = renderer.bar.masterBar.index; - masterBarBounds.isFirstOfLine = renderer.isFirstOfLine; + masterBarBounds.isFirstOfLine = renderer.isFirstOfStaff; masterBarBounds.realBounds = new Bounds(); masterBarBounds.realBounds.x = x + renderer.x; masterBarBounds.realBounds.y = realTop; @@ -820,11 +1038,11 @@ export class StaffSystem { } public getBarX(index: number): number { - if (!this._firstStaffInBrackets || this.layout.renderer.tracks!.length === 0) { + if (this.allStaves.length === 0 || this.layout.renderer.tracks!.length === 0) { return 0; } const bar: Bar = this.layout.renderer.tracks![0].staves[0].bars[index]; - const renderer: BarRendererBase = this.layout.getRendererForBar(this._firstStaffInBrackets.staffId, bar)!; + const renderer: BarRendererBase = this.layout.getRendererForBar(this.allStaves[0].staffId, bar)!; return renderer.x; } } diff --git a/packages/alphatab/src/rendering/staves/StaffTrackGroup.ts b/packages/alphatab/src/rendering/staves/StaffTrackGroup.ts index 7b6f2b4b3..0827b231b 100644 --- a/packages/alphatab/src/rendering/staves/StaffTrackGroup.ts +++ b/packages/alphatab/src/rendering/staves/StaffTrackGroup.ts @@ -12,9 +12,8 @@ export class StaffTrackGroup { public track: Track; public staffSystem: StaffSystem; public staves: RenderStaff[] = []; - public stavesRelevantForBoundsLookup: RenderStaff[] = []; - public firstStaffInBracket: RenderStaff | null = null; - public lastStaffInBracket: RenderStaff | null = null; + public firstVisibleStaff?: RenderStaff; + public lastVisibleStaff?: RenderStaff; public bracket: SystemBracket | null = null; public constructor(staffSystem: StaffSystem, track: Track) { @@ -24,8 +23,5 @@ export class StaffTrackGroup { public addStaff(staff: RenderStaff): void { this.staves.push(staff); - if (staff.isRelevantForBoundsLookup) { - this.stavesRelevantForBoundsLookup.push(staff); - } } } diff --git a/packages/alphatab/src/rendering/utils/AccidentalHelper.ts b/packages/alphatab/src/rendering/utils/AccidentalHelper.ts index d8caaecbc..29de7718e 100644 --- a/packages/alphatab/src/rendering/utils/AccidentalHelper.ts +++ b/packages/alphatab/src/rendering/utils/AccidentalHelper.ts @@ -1,23 +1,23 @@ import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Beat } from '@coderline/alphatab/model/Beat'; +import type { Clef } from '@coderline/alphatab/model/Clef'; +import type { KeySignature } from '@coderline/alphatab/model/KeySignature'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import type { Note } from '@coderline/alphatab/model/Note'; import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; -import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; -import type { Clef } from '@coderline/alphatab/model/Clef'; -import type { KeySignature } from '@coderline/alphatab/model/KeySignature'; +import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer'; /** * @internal */ -class BeatLines { - public maxLine: number = -1000; - public maxLineNote: Note|null=null; - public minLine: number = -1000; - public minLineNote: Note|null=null; +class BeatSteps { + public maxSteps: number = -1000; + public maxStepsNote: Note | null = null; + public minSteps: number = -1000; + public minStepsNote: Note | null = null; } /** @@ -29,7 +29,6 @@ export class AccidentalHelper { private _bar: Bar; private _barRenderer: LineBarRenderer; - /** * We always have 7 steps per octave. * (by a step the offsets inbetween score lines is meant, @@ -55,47 +54,40 @@ export class AccidentalHelper { public static readonly flatNoteSteps: number[] = [0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6]; private _registeredAccidentals: Map = new Map(); - private _appliedScoreLines: Map = new Map(); - private _appliedScoreLinesByValue: Map = new Map(); + private _appliedScoreSteps: Map = new Map(); + private _appliedScoreStepsByValue: Map = new Map(); private _notesByValue: Map = new Map(); - private _beatLines: Map = new Map(); + private _beatSteps: Map = new Map(); /** * The beat on which the highest note of this helper was added. * Used together with beaming helper to calculate overflow. */ - public maxLineBeat: Beat | null = null; + public maxStepsBeat: Beat | null = null; /** * The beat on which the lowest note of this helper was added. * Used together with beaming helper to calculate overflow. */ - public minLineBeat: Beat | null = null; + public minStepsBeat: Beat | null = null; /** - * The line of the highest note added to this helper. + * The steps of the highest note added to this helper. */ - public maxLine: number = -1000; + public maxSteps: number = -1000; /** - * The line of the lowest note added to this helper. + * The steps of the lowest note added to this helper. */ - public minLine: number = -1000; + public minSteps: number = -1000; public constructor(barRenderer: LineBarRenderer) { this._barRenderer = barRenderer; this._bar = barRenderer.bar; } - public static getPercussionLine(bar: Bar, noteValue: number): number { - if (noteValue < bar.staff.track.percussionArticulations.length) { - return bar.staff.track.percussionArticulations[noteValue]!.staffLine; - } - return PercussionMapper.getArticulationByInputMidiNumber(noteValue)?.staffLine ?? 0; + public static getPercussionSteps(note: Note): number { + return PercussionMapper.getArticulation(note)?.staffLine ?? 0; } public static getNoteValue(note: Note) { - if (note.isPercussion) { - return note.percussionArticulation; - } - let noteValue: number = note.displayValue; // adjust note height according to accidentals enforced @@ -147,19 +139,18 @@ export class AccidentalHelper { return this._getAccidental(noteValue, quarterBend, relatedBeat, isHelperNote, null); } - public static computeLineWithoutAccidentals(bar: Bar, note: Note) { - let line: number = 0; + public static computeStepsWithoutAccidentals(bar: Bar, note: Note) { + let steps = 0; const noteValue = AccidentalHelper.getNoteValue(note); if (note.isPercussion) { - line = AccidentalHelper.getPercussionLine(bar, noteValue); + steps = AccidentalHelper.getPercussionSteps(note); } else { - line = AccidentalHelper.calculateNoteSteps(bar.keySignature, bar.clef, noteValue); + steps = AccidentalHelper.calculateNoteSteps(bar.keySignature, bar.clef, noteValue); } - return line; + return steps; } - private _getAccidental( noteValue: number, quarterBend: boolean, @@ -171,9 +162,9 @@ export class AccidentalHelper { let accidentalToSet = AccidentalType.None; - const isPercussion = note != null ? note.isPercussion : this._bar.staff.isPercussion; + const isPercussion = note != null ? note.isPercussion : false; if (isPercussion) { - steps = AccidentalHelper.getPercussionLine(this._bar, noteValue); + steps = AccidentalHelper.getPercussionSteps(note!); } else { const accidentalMode = note ? note.accidentalMode : NoteAccidentalMode.Default; steps = AccidentalHelper.calculateNoteSteps(this._bar.keySignature, this._bar.clef, noteValue); @@ -195,21 +186,21 @@ export class AccidentalHelper { case AccidentalType.NaturalQuarterNoteUp: case AccidentalType.SharpQuarterNoteUp: case AccidentalType.FlatQuarterNoteUp: - // quarter notes are always set and not compared with lines + // quarter notes are always set and not compared with steps break; default: // Issue #472: Tied notes across bars do not show the accidentals but also // do not register them. // https://ultimatemusictheory.com/tied-notes-with-accidentals/ if (note && note.isTieDestination && note.beat.index === 0) { - // candidate for skip, check further if start note is on the same line + // candidate for skip, check further if start note is on the same steps const tieOriginBarRenderer = this._barRenderer.scoreRenderer.layout?.getRendererForBar( - this._barRenderer.staff.staffId, + this._barRenderer.staff!.staffId, note.tieOrigin!.beat.voice.bar ) as ScoreBarRenderer | null; if (tieOriginBarRenderer && tieOriginBarRenderer.staff === this._barRenderer.staff) { - const tieOriginLine = tieOriginBarRenderer.accidentalHelper.getNoteLine(note.tieOrigin!); - if (tieOriginLine === steps) { + const tieOriginSteps = tieOriginBarRenderer.accidentalHelper.getNoteSteps(note.tieOrigin!); + if (tieOriginSteps === steps) { skipAccidental = true; } } @@ -228,60 +219,60 @@ export class AccidentalHelper { } if (note) { - this._appliedScoreLines.set(note.id, steps); + this._appliedScoreSteps.set(note.id, steps); this._notesByValue.set(noteValue, note); } else { - this._appliedScoreLinesByValue.set(noteValue, steps); + this._appliedScoreStepsByValue.set(noteValue, steps); } - if (this.minLine === -1000 || this.minLine < steps) { - this.minLine = steps; - this.minLineBeat = relatedBeat; + if (this.minSteps === -1000 || this.minSteps < steps) { + this.minSteps = steps; + this.minStepsBeat = relatedBeat; } - if (this.maxLine === -1000 || this.maxLine > steps) { - this.maxLine = steps; - this.maxLineBeat = relatedBeat; + if (this.maxSteps === -1000 || this.maxSteps > steps) { + this.maxSteps = steps; + this.maxStepsBeat = relatedBeat; } if (!isHelperNote) { - this._registerLine(relatedBeat, steps, note); + this._registerSteps(relatedBeat, steps, note); } return accidentalToSet; } - private _registerLine(relatedBeat: Beat, line: number, note:Note|null) { - let lines: BeatLines; - if (this._beatLines.has(relatedBeat.id)) { - lines = this._beatLines.get(relatedBeat.id)!; + private _registerSteps(relatedBeat: Beat, steps: number, note: Note | null) { + let beatSteps: BeatSteps; + if (this._beatSteps.has(relatedBeat.id)) { + beatSteps = this._beatSteps.get(relatedBeat.id)!; } else { - lines = new BeatLines(); - this._beatLines.set(relatedBeat.id, lines); + beatSteps = new BeatSteps(); + this._beatSteps.set(relatedBeat.id, beatSteps); } - if (lines.minLine === -1000 || line < lines.minLine) { - lines.minLine = line; - lines.minLineNote = note; + if (beatSteps.minSteps === -1000 || steps < beatSteps.minSteps) { + beatSteps.minSteps = steps; + beatSteps.minStepsNote = note; } - if (lines.minLine === -1000 || line > lines.maxLine) { - lines.maxLine = line; - lines.maxLineNote = note; + if (beatSteps.minSteps === -1000 || steps > beatSteps.maxSteps) { + beatSteps.maxSteps = steps; + beatSteps.maxStepsNote = note; } } - public getMaxLine(b: Beat): number { - return this._beatLines.has(b.id) ? this._beatLines.get(b.id)!.maxLine : 0; + public getMaxSteps(b: Beat): number { + return this._beatSteps.has(b.id) ? this._beatSteps.get(b.id)!.maxSteps : 0; } - public getMaxLineNote(b: Beat): Note|null { - return this._beatLines.has(b.id) ? this._beatLines.get(b.id)!.maxLineNote : null; + public getMaxStepsNote(b: Beat): Note | null { + return this._beatSteps.has(b.id) ? this._beatSteps.get(b.id)!.maxStepsNote : null; } - public getMinLine(b: Beat): number { - return this._beatLines.has(b.id) ? this._beatLines.get(b.id)!.minLine : 0; + public getMinSteps(b: Beat): number { + return this._beatSteps.has(b.id) ? this._beatSteps.get(b.id)!.minSteps : 0; } - - public getMinLineNote(b: Beat): Note|null { - return this._beatLines.has(b.id) ? this._beatLines.get(b.id)!.minLineNote : null; + + public getMinStepsNote(b: Beat): Note | null { + return this._beatSteps.has(b.id) ? this._beatSteps.get(b.id)!.minStepsNote : null; } public static calculateNoteSteps(keySignature: KeySignature, clef: Clef, noteValue: number): number { @@ -306,16 +297,16 @@ export class AccidentalHelper { return steps; } - public getNoteLine(n: Note): number { - return this._appliedScoreLines.get(n.id)!; + public getNoteSteps(n: Note): number { + return this._appliedScoreSteps.get(n.id)!; } - public getNoteLineForValue(rawValue: number, searchForNote: boolean = false): number { - if (this._appliedScoreLinesByValue.has(rawValue)) { - return this._appliedScoreLinesByValue.get(rawValue)!; + public getNoteStepsForValue(rawValue: number, searchForNote: boolean = false): number { + if (this._appliedScoreStepsByValue.has(rawValue)) { + return this._appliedScoreStepsByValue.get(rawValue)!; } if (searchForNote && this._notesByValue.has(rawValue)) { - return this.getNoteLine(this._notesByValue.get(rawValue)!); + return this.getNoteSteps(this._notesByValue.get(rawValue)!); } return 0; } diff --git a/packages/alphatab/src/rendering/utils/BarHelpers.ts b/packages/alphatab/src/rendering/utils/BarHelpers.ts index 922bc4258..02fa5f9e5 100644 --- a/packages/alphatab/src/rendering/utils/BarHelpers.ts +++ b/packages/alphatab/src/rendering/utils/BarHelpers.ts @@ -5,14 +5,18 @@ import { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; import { BarCollisionHelper } from '@coderline/alphatab/rendering/utils/BarCollisionHelper'; import type { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import { BeamingRules, type MasterBar } from '@coderline/alphatab/model/MasterBar'; +import { BeamingRuleLookup } from '@coderline/alphatab/rendering/utils/BeamingRuleLookup'; +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; /** * @internal */ export class BarHelpers { private _renderer: BarRendererBase; + private _beamHelperLookup = new Map(); public beamHelpers: BeamingHelper[][] = []; - public beamHelperLookup: Map[] = []; public collisionHelper: BarCollisionHelper; public preferredBeamDirection: BeamDirection | null = null; @@ -25,12 +29,26 @@ export class BarHelpers { const barRenderer = this._renderer; const bar = this._renderer.bar; + const masterBar = bar.masterBar; + const beamingRules = masterBar.actualBeamingRules ?? BarHelpers._findOrBuildDefaultBeamingRules(masterBar); + const rule = beamingRules.findRule(bar.shortestDuration); + // NOTE: moste rules have only one group definition, so its better to reuse the unique id + // than compute a potentially shorter id here. + const key = `beaming_${beamingRules.uniqueId}_${rule[0]}`; + + let beamingRuleLookup = this._renderer.scoreRenderer.layout!.beamingRuleLookups.has(key) + ? this._renderer.scoreRenderer.layout!.beamingRuleLookups.get(key)! + : undefined; + if (!beamingRuleLookup) { + beamingRuleLookup = BeamingRuleLookup.build(masterBar, rule[0], rule[1]); + this._renderer.scoreRenderer.layout!.beamingRuleLookups.set(key, beamingRuleLookup); + } + let currentBeamHelper: BeamingHelper | null = null; let currentGraceBeamHelper: BeamingHelper | null = null; for (let i: number = 0, j: number = bar.voices.length; i < j; i++) { const v: Voice = bar.voices[i]; this.beamHelpers.push([]); - this.beamHelperLookup.push(new Map()); for (let k: number = 0, l: number = v.beats.length; k < l; k++) { const b: Beat = v.beats[k]; let helperForBeat: BeamingHelper | null; @@ -38,6 +56,9 @@ export class BarHelpers { helperForBeat = currentGraceBeamHelper; } else { helperForBeat = currentBeamHelper; + if (currentGraceBeamHelper) { + currentGraceBeamHelper.finish(); + } currentGraceBeamHelper = null; } // if a new beaming helper was started, we close our tuplet grouping as well @@ -47,7 +68,7 @@ export class BarHelpers { helperForBeat.finish(); } // if not possible, create the next beaming helper - helperForBeat = new BeamingHelper(bar.staff, barRenderer); + helperForBeat = new BeamingHelper(bar.staff, barRenderer, beamingRuleLookup); helperForBeat.preferredBeamDirection = this.preferredBeamDirection; helperForBeat.checkBeat(b); if (b.graceType !== GraceType.None) { @@ -57,7 +78,7 @@ export class BarHelpers { } this.beamHelpers[v.index].push(helperForBeat); } - this.beamHelperLookup[v.index].set(b.index, helperForBeat!); + this._beamHelperLookup.set(b.id, helperForBeat!); } if (currentBeamHelper) { currentBeamHelper.finish(); @@ -70,7 +91,96 @@ export class BarHelpers { } } - public getBeamingHelperForBeat(beat: Beat): BeamingHelper { - return this.beamHelperLookup[beat.voice.index].get(beat.index)!; + private static _defaultBeamingRules: Map | undefined; + private static _findOrBuildDefaultBeamingRules(masterBar: MasterBar): BeamingRules { + let defaultBeamingRules = BarHelpers._defaultBeamingRules; + if (!defaultBeamingRules) { + defaultBeamingRules = new Map( + [ + BeamingRules.createSimple(2, 16, Duration.Sixteenth, [1, 1]), + BeamingRules.createSimple(1, 8, Duration.Eighth, [1]), + BeamingRules.createSimple(1, 4, Duration.Quarter, [1]), + + BeamingRules.createSimple(3, 16, Duration.Sixteenth, [3]), + + BeamingRules.createSimple(4, 16, Duration.Sixteenth, [2, 2]), + BeamingRules.createSimple(2, 8, Duration.Eighth, [1, 1]), + + BeamingRules.createSimple(5, 16, Duration.Sixteenth, [3, 2]), + + BeamingRules.createSimple(6, 16, Duration.Sixteenth, [3, 3]), + BeamingRules.createSimple(3, 8, Duration.Eighth, [3]), + + BeamingRules.createSimple(4, 8, Duration.Eighth, [2, 2]), + BeamingRules.createSimple(2, 4, Duration.Quarter, [1, 1]), + + BeamingRules.createSimple(9, 16, Duration.Sixteenth, [3, 3, 3]), + + BeamingRules.createSimple(5, 8, Duration.Eighth, [3, 2]), + + BeamingRules.createSimple(12, 16, Duration.Sixteenth, [3, 3, 3, 3]), + BeamingRules.createSimple(6, 8, Duration.Eighth, [3, 3, 3]), + BeamingRules.createSimple(3, 4, Duration.Quarter, [1, 1, 1]), + + BeamingRules.createSimple(7, 8, Duration.Eighth, [4, 3]), + + BeamingRules.createSimple(8, 8, Duration.Eighth, [3, 3, 2]), + BeamingRules.createSimple(4, 4, Duration.Quarter, [1, 1, 1, 1]), + + BeamingRules.createSimple(9, 8, Duration.Eighth, [3, 3, 3]), + + BeamingRules.createSimple(10, 8, Duration.Eighth, [4, 3, 3]), + BeamingRules.createSimple(5, 4, Duration.Quarter, [1, 1, 1, 1, 1]), + + BeamingRules.createSimple(12, 8, Duration.Eighth, [3, 3, 3, 3]), + BeamingRules.createSimple(6, 4, Duration.Quarter, [1, 1, 1, 1, 1, 1]), + + BeamingRules.createSimple(15, 8, Duration.Eighth, [3, 3, 3, 3, 3, 3]), + + BeamingRules.createSimple(8, 4, Duration.Quarter, [1, 1, 1, 1, 1, 1, 1, 1]), + BeamingRules.createSimple(18, 8, Duration.Eighth, [3, 3, 3, 3, 3, 3]) + ].map(r => [`${r.timeSignatureNumerator}_${r.timeSignatureDenominator}`, r] as [string, BeamingRules]) + ); + + BarHelpers._defaultBeamingRules = defaultBeamingRules; + } + + const key = `${masterBar.timeSignatureNumerator}_${masterBar.timeSignatureDenominator}`; + if (defaultBeamingRules.has(key)) { + return defaultBeamingRules.get(key)!; + } + + // NOTE: this is the old alphaTab logic how we used to beamed bars. + // we either group in quarters, or in 3x8ths depending on the key signature + + let divisionLength: number = MidiUtils.QuarterTime; + switch (masterBar.timeSignatureDenominator) { + case 8: + if (masterBar.timeSignatureNumerator % 3 === 0) { + divisionLength += (MidiUtils.QuarterTime / 2) | 0; + } + break; + } + + const numberOfDivisions = Math.ceil(masterBar.calculateDuration(false) / divisionLength); + const notesPerDivision = (divisionLength / MidiUtils.QuarterTime) * 2; + + const fallback = new BeamingRules(); + const groups: number[] = []; + + for (let i = 0; i < numberOfDivisions; i++) { + groups.push(notesPerDivision); + } + + fallback.groups.set(Duration.Eighth, groups); + fallback.timeSignatureNumerator = masterBar.timeSignatureNumerator; + fallback.timeSignatureDenominator = masterBar.timeSignatureDenominator; + fallback.finish(); + defaultBeamingRules.set(key, fallback); + return fallback; + } + + public getBeamingHelperForBeat(beat: Beat): BeamingHelper | undefined { + return this._beamHelperLookup.has(beat.id) ? this._beamHelperLookup.get(beat.id)! : undefined; } } diff --git a/packages/alphatab/src/rendering/utils/BeamingHelper.ts b/packages/alphatab/src/rendering/utils/BeamingHelper.ts index 2ab5d97fa..17933a77a 100644 --- a/packages/alphatab/src/rendering/utils/BeamingHelper.ts +++ b/packages/alphatab/src/rendering/utils/BeamingHelper.ts @@ -3,23 +3,15 @@ import { type Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import type { Note } from '@coderline/alphatab/model/Note'; import type { Staff } from '@coderline/alphatab/model/Staff'; import type { Voice } from '@coderline/alphatab/model/Voice'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper'; -import { NoteYPosition, type BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; - -/** - * @internal - */ -class BeatLinePositions { - public staffId: string = ''; - public up: number = 0; - public down: number = 0; -} +import type { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import type { BeamingRuleLookup } from '@coderline/alphatab/rendering/utils/BeamingRuleLookup'; /** * @internal @@ -55,15 +47,11 @@ export class BeamingHelperDrawInfo { */ export class BeamingHelper { private _staff: Staff; - private _beatLineXPositions: Map = new Map(); private _renderer: BarRendererBase; - private _firstNonRestBeat: Beat | null = null; - private _lastNonRestBeat: Beat | null = null; - + private _beamingRuleLookup: BeamingRuleLookup; public voice: Voice | null = null; public beats: Beat[] = []; public shortestDuration: Duration = Duration.QuadrupleWhole; - public tremoloDuration?: Duration; /** * an indicator whether any beat has a tuplet on it. @@ -71,6 +59,7 @@ export class BeamingHelper { public hasTuplet: boolean = false; public slashBeats: Beat[] = []; + public restBeats: Beat[] = []; public lowestNoteInHelper: Note | null = null; private _lowestNoteCompareValueInHelper: number = -1; @@ -82,125 +71,53 @@ export class BeamingHelper { public preferredBeamDirection: BeamDirection | null = null; public graceType: GraceType = GraceType.None; - public minRestLine: number | null = null; - public beatOfMinRestLine: Beat | null = null; - - public maxRestLine: number | null = null; - public beatOfMaxRestLine: Beat | null = null; - public get isRestBeamHelper(): boolean { return this.beats.length === 1 && this.beats[0].isRest; } - public hasLine(forceFlagOnSingleBeat: boolean, beat?: Beat): boolean { + public hasStem(forceFlagOnSingleBeat: boolean, beat?: Beat): boolean { return ( - (forceFlagOnSingleBeat && this._beatHasLine(beat!)) || - (!forceFlagOnSingleBeat && this.beats.length === 1 && this._beatHasLine(beat!)) + (forceFlagOnSingleBeat && BeamingHelper.beatHasStem(beat!)) || + (!forceFlagOnSingleBeat && BeamingHelper.beatHasStem(beat!)) ); } - private _beatHasLine(beat: Beat): boolean { + public static beatHasStem(beat: Beat): boolean { return beat!.duration > Duration.Whole; } public hasFlag(forceFlagOnSingleBeat: boolean, beat?: Beat): boolean { return ( - (forceFlagOnSingleBeat && this._beatHasFlag(beat!)) || - (!forceFlagOnSingleBeat && this.beats.length === 1 && this._beatHasFlag(this.beats[0])) + (forceFlagOnSingleBeat && BeamingHelper.beatHasFlag(beat!)) || + (!forceFlagOnSingleBeat && this.beats.length === 1 && BeamingHelper.beatHasFlag(this.beats[0])) ); } - private _beatHasFlag(beat: Beat) { + public static beatHasFlag(beat: Beat) { return ( !beat.deadSlapped && !beat.isRest && (beat.duration > Duration.Quarter || beat.graceType !== GraceType.None) ); } - public constructor(staff: Staff, renderer: BarRendererBase) { + public constructor(staff: Staff, renderer: BarRendererBase, beamingRuleLookup: BeamingRuleLookup) { this._staff = staff; this._renderer = renderer; this.beats = []; + this._beamingRuleLookup = beamingRuleLookup; } - public getBeatLineX(beat: Beat, direction?: BeamDirection): number { - direction = direction ?? this.direction; - - if (this.hasBeatLineX(beat)) { - if (direction === BeamDirection.Up) { - return this._beatLineXPositions.get(beat.index)!.up; - } - return this._beatLineXPositions.get(beat.index)!.down; - } - return 0; - } - - public hasBeatLineX(beat: Beat): boolean { - return this._beatLineXPositions.has(beat.index); - } - - public registerBeatLineX(staffId: string, beat: Beat, up: number, down: number): void { - const positions: BeatLinePositions = this._getOrCreateBeatPositions(beat); - positions.staffId = staffId; - positions.up = up; - positions.down = down; + public alignWithBeats() { for (const v of this.drawingInfos.values()) { - if (v.startBeat === beat) { - v.startX = this.getBeatLineX(beat); - } else if (v.endBeat === beat) { - v.endX = this.getBeatLineX(beat); - } - } - } - - private _getOrCreateBeatPositions(beat: Beat): BeatLinePositions { - if (!this._beatLineXPositions.has(beat.index)) { - this._beatLineXPositions.set(beat.index, new BeatLinePositions()); + v.startX = this._renderer.getBeatX(v.startBeat!, BeatXPosition.Stem); + v.endX = this._renderer.getBeatX(v.endBeat!, BeatXPosition.Stem); + this.drawingInfos.clear(); } - return this._beatLineXPositions.get(beat.index)!; } - public direction: BeamDirection = BeamDirection.Up; public finish(): void { - this.direction = this._calculateDirection(); this._renderer.completeBeamingHelper(this); } - private _calculateDirection(): BeamDirection { - // no proper voice (should not happen usually) - if (!this.voice) { - return BeamDirection.Up; - } - // we have a preferred direction - if (this.preferredBeamDirection !== null) { - return this.preferredBeamDirection!; - } - // on multi-voice setups secondary voices are always down - if (this.voice.index > 0) { - return this._invert(BeamDirection.Down); - } - // on multi-voice setups primary voices are always up - if (this.voice.bar.isMultiVoice) { - return this._invert(BeamDirection.Up); - } - // grace notes are always up - if (this.beats[0].graceType !== GraceType.None) { - return this._invert(BeamDirection.Up); - } - - // the average line is used for determination - // key lowerequal than middle line -> up - // key higher than middle line -> down - if (this.highestNoteInHelper && this.lowestNoteInHelper) { - const highestNotePosition = this._renderer.getNoteY(this.highestNoteInHelper, NoteYPosition.Center); - const lowestNotePosition = this._renderer.getNoteY(this.lowestNoteInHelper, NoteYPosition.Center); - - const avg = (highestNotePosition + lowestNotePosition) / 2; - return this._invert(this._renderer.middleYPosition < avg ? BeamDirection.Up : BeamDirection.Down); - } - - return this._invert(BeamDirection.Up); - } - public static computeLineHeightsForRest(duration: Duration): number[] { switch (duration) { case Duration.QuadrupleWhole: @@ -229,53 +146,6 @@ export class BeamingHelper { return [0, 0]; } - /** - * Registers a rest beat within the accidental helper so the rest - * symbol is considered properly during beaming. - * @param beat The rest beat. - * @param line The line on which the rest symbol is placed - */ - public applyRest(beat: Beat, line: number): void { - // do not accept rests after the last beat which has notes - if ( - (this._lastNonRestBeat && beat.index >= this._lastNonRestBeat.index) || - (this._firstNonRestBeat && beat.index <= this._firstNonRestBeat.index) - ) { - return; - } - - // correct the line of the glyph to a note which would - // be placed at the upper / lower end of the glyph. - let aboveRest = line; - let belowRest = line; - const offsets = BeamingHelper.computeLineHeightsForRest(beat.duration); - aboveRest -= offsets[0]; - belowRest += offsets[1]; - const minRestLine = this.minRestLine; - const maxRestLine = this.maxRestLine; - if (minRestLine === null || minRestLine > aboveRest) { - this.minRestLine = aboveRest; - this.beatOfMinRestLine = beat; - } - if (maxRestLine === null || maxRestLine < belowRest) { - this.maxRestLine = belowRest; - this.beatOfMaxRestLine = beat; - } - } - - private _invert(direction: BeamDirection): BeamDirection { - if (!this.invertBeamDirection) { - return direction; - } - switch (direction) { - case BeamDirection.Down: - return BeamDirection.Up; - // case BeamDirection.Up: - default: - return BeamDirection.Down; - } - } - public checkBeat(beat: Beat): boolean { if (beat.invertBeamDirection) { this.invertBeamDirection = true; @@ -292,7 +162,7 @@ export class BeamingHelper { switch (this.beats[this.beats.length - 1].beamingMode) { case BeatBeamingMode.Auto: case BeatBeamingMode.ForceSplitOnSecondaryToNext: - add = BeamingHelper._canJoin(this.beats[this.beats.length - 1], beat); + add = this._canJoin(this.beats[this.beats.length - 1], beat); break; case BeatBeamingMode.ForceSplitToNext: add = false; @@ -312,12 +182,6 @@ export class BeamingHelper { this.hasTuplet = true; } - if (beat.isTremolo) { - if (!this.tremoloDuration || this.tremoloDuration < beat.tremoloSpeed!) { - this.tremoloDuration = beat.tremoloSpeed!; - } - } - if (beat.graceType !== GraceType.None) { this.graceType = beat.graceType; } @@ -332,13 +196,12 @@ export class BeamingHelper { if (this.shortestDuration < beat.duration) { this.shortestDuration = beat.duration; } - if (!this._firstNonRestBeat) { - this._firstNonRestBeat = beat; - } - this._lastNonRestBeat = beat; } else if (this.beats.length === 0) { this.beats.push(beat); + } else { + this.restBeats.push(beat); } + if (beat.slashed) { this.slashBeats.push(beat); } @@ -358,10 +221,7 @@ export class BeamingHelper { // For percussion we use the line as value to compare whether it is // higher or lower. if (this.voice && note.isPercussion) { - lowestValueForNote = -AccidentalHelper.getPercussionLine( - this.voice.bar, - AccidentalHelper.getNoteValue(note) - ); + lowestValueForNote = -AccidentalHelper.getPercussionSteps(note); highestValueForNote = lowestValueForNote; } else { lowestValueForNote = AccidentalHelper.getNoteValue(note); @@ -381,8 +241,7 @@ export class BeamingHelper { } } - // TODO: Check if this beaming is really correct, I'm not sure if we are connecting beats correctly - private static _canJoin(b1: Beat, b2: Beat): boolean { + private _canJoin(b1: Beat, b2: Beat): boolean { // is this a voice we can join with? if ( !b1 || @@ -404,6 +263,7 @@ export class BeamingHelper { if (m1 !== m2) { return false; } + // get times of those voices and check if the times // are in the same division const start1: number = b1.playbackStart; @@ -422,19 +282,11 @@ export class BeamingHelper { return true; } } - // TODO: create more rules for automatic beaming - let divisionLength: number = MidiUtils.QuarterTime; - switch (m1.masterBar.timeSignatureDenominator) { - case 8: - if (m1.masterBar.timeSignatureNumerator % 3 === 0) { - divisionLength += (MidiUtils.QuarterTime / 2) | 0; - } - break; - } - // check if they are on the same division - const division1: number = ((divisionLength + start1) / divisionLength) | 0 | 0; - const division2: number = ((divisionLength + start2) / divisionLength) | 0 | 0; - return division1 === division2; + + // check if they are on the same group as per rule definitions + const groupId1 = this._beamingRuleLookup.calculateGroupIndex(start1); + const groupId2 = this._beamingRuleLookup.calculateGroupIndex(start2); + return groupId1 === groupId2; } private static _canJoinDuration(d: Duration): boolean { @@ -449,7 +301,6 @@ export class BeamingHelper { } public static isFullBarJoin(a: Beat, b: Beat, barIndex: number): boolean { - // TODO: this getindex call seems expensive since we call this method very often. return ModelUtils.getIndex(a.duration) - 2 - barIndex > 0 && ModelUtils.getIndex(b.duration) - 2 - barIndex > 0; } @@ -461,21 +312,5 @@ export class BeamingHelper { return this.highestNoteInHelper!.beat; } - /** - * Returns whether the the position of the given beat, was registered by the staff of the given ID - * @param staffId - * @param beat - * @returns - */ - public isPositionFrom(staffId: string, beat: Beat): boolean { - if (!this._beatLineXPositions.has(beat.index)) { - return true; - } - return ( - this._beatLineXPositions.get(beat.index)!.staffId === staffId || - !this._beatLineXPositions.get(beat.index)!.staffId - ); - } - public drawingInfos: Map = new Map(); } diff --git a/packages/alphatab/src/rendering/utils/BeamingRuleLookup.ts b/packages/alphatab/src/rendering/utils/BeamingRuleLookup.ts new file mode 100644 index 000000000..59e6d1ffe --- /dev/null +++ b/packages/alphatab/src/rendering/utils/BeamingRuleLookup.ts @@ -0,0 +1,68 @@ +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import type { Duration } from '@coderline/alphatab/model/Duration'; +import type { MasterBar } from '@coderline/alphatab/model/MasterBar'; + +/** + * @internal + */ +export class BeamingRuleLookup { + private _division: number = 0; + private _slots: number[] = []; + private _barDuration: number; + + public constructor(barDuration: number, division: number, slots: number[]) { + this._division = division; + this._slots = slots; + this._barDuration = barDuration; + } + + public calculateGroupIndex(beatStartTime: number) { + // no slots -> all have their own group based (use the start time as index) + if (this._slots.length === 0) { + return beatStartTime; + } + + // rollover within the bar. + beatStartTime = beatStartTime % this._barDuration; + + const slotIndex = Math.floor(beatStartTime / this._division); + return this._slots[slotIndex]; + } + + public static build(masterBar: MasterBar, ruleDuration: Duration, ruleGroups: number[]): BeamingRuleLookup { + const totalDuration = masterBar.calculateDuration(false); + const division = MidiUtils.toTicks(ruleDuration); + const slotCount = totalDuration / division; + + // should only happen in case of improper data. + if (slotCount < 0 || ruleGroups.length === 0) { + return new BeamingRuleLookup(0, 0, []); + } + + let groupIndex = 0; + let remainingSlots = ruleGroups[groupIndex]; + + const slots: number[] = []; + + for (let i = 0; i < slotCount; i++) { + if (groupIndex < ruleGroups.length) { + slots.push(groupIndex); + remainingSlots--; + + if (remainingSlots <= 0) { + groupIndex++; + if (groupIndex < ruleGroups.length) { + remainingSlots = ruleGroups[groupIndex]; + } + } + } else { + // no groups defined for the remaining slots: all slots are treated + // as unjoined + slots.push(groupIndex); + groupIndex++; + } + } + + return new BeamingRuleLookup(totalDuration, division, slots); + } +} diff --git a/packages/alphatab/src/rendering/utils/BeatBounds.ts b/packages/alphatab/src/rendering/utils/BeatBounds.ts index bdff5425a..6e0caa37b 100644 --- a/packages/alphatab/src/rendering/utils/BeatBounds.ts +++ b/packages/alphatab/src/rendering/utils/BeatBounds.ts @@ -63,7 +63,7 @@ export class BeatBounds { if (!notes) { return null; } - // TODO: can be likely optimized + // perf: can be likely optimized // a beat is mostly vertically aligned, we could sort the note bounds by Y // and then do a binary search on the Y-axis. for (const note of notes) { diff --git a/packages/alphatab/src/rendering/utils/Bounds.ts b/packages/alphatab/src/rendering/utils/Bounds.ts index 21917edd7..7bf5c2737 100644 --- a/packages/alphatab/src/rendering/utils/Bounds.ts +++ b/packages/alphatab/src/rendering/utils/Bounds.ts @@ -6,22 +6,22 @@ export class Bounds { /** * Gets or sets the X-position of the rectangle within the music notation. */ - public x: number = 0; + public x: number; /** * Gets or sets the Y-position of the rectangle within the music notation. */ - public y: number = 0; + public y: number; /** * Gets or sets the width of the rectangle. */ - public w: number = 0; + public w: number; /** * Gets or sets the height of the rectangle. */ - public h: number = 0; + public h: number; public scaleWith(scale: number) { this.x *= scale; @@ -29,4 +29,11 @@ export class Bounds { this.w *= scale; this.h *= scale; } + + public constructor(x: number = 0, y: number = 0, w: number = 0, h: number = 0) { + this.x = x; + this.y = y; + this.h = h; + this.w = w; + } } diff --git a/packages/alphatab/src/rendering/utils/BoundsLookup.ts b/packages/alphatab/src/rendering/utils/BoundsLookup.ts index eea224bf7..83788b4e9 100644 --- a/packages/alphatab/src/rendering/utils/BoundsLookup.ts +++ b/packages/alphatab/src/rendering/utils/BoundsLookup.ts @@ -31,6 +31,7 @@ export class BoundsLookup { mb.visualBounds = this._boundsToJson(masterBar.visualBounds); mb.realBounds = this._boundsToJson(masterBar.realBounds); mb.index = masterBar.index; + mb.isFirstOfLine = masterBar.isFirstOfLine; mb.bars = []; for (const bar of masterBar.bars) { const b: BarBounds = {} as any; @@ -88,7 +89,7 @@ export class BoundsLookup { mb.lineAlignedBounds = BoundsLookup._boundsFromJson(masterBar.lineAlignedBounds); mb.visualBounds = BoundsLookup._boundsFromJson(masterBar.visualBounds); mb.realBounds = BoundsLookup._boundsFromJson(masterBar.realBounds); - sg.addBar(mb); + lookup.addMasterBar(mb); for (const bar of masterBar.bars) { const b: BarBounds = new BarBounds(); b.visualBounds = BoundsLookup._boundsFromJson(bar.visualBounds); diff --git a/packages/alphatab/src/rendering/utils/MasterBarBounds.ts b/packages/alphatab/src/rendering/utils/MasterBarBounds.ts index 38a28c076..b40af1c5e 100644 --- a/packages/alphatab/src/rendering/utils/MasterBarBounds.ts +++ b/packages/alphatab/src/rendering/utils/MasterBarBounds.ts @@ -10,7 +10,7 @@ import type { StaffSystemBounds } from '@coderline/alphatab/rendering/utils/Staf */ export class MasterBarBounds { /** - * Gets or sets the index of this bounds relative within the parent lookup. + * The MasterBar index within the data model represented by these bounds. */ public index: number = 0; diff --git a/packages/alphatab/src/synth/AlphaSynthWrapper.ts b/packages/alphatab/src/synth/AlphaSynthWrapper.ts index 98305a231..e52066f31 100644 --- a/packages/alphatab/src/synth/AlphaSynthWrapper.ts +++ b/packages/alphatab/src/synth/AlphaSynthWrapper.ts @@ -1,4 +1,9 @@ -import { EventEmitter, EventEmitterOfT, type IEventEmitter, type IEventEmitterOfT } from '@coderline/alphatab/EventEmitter'; +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'; @@ -7,7 +12,7 @@ import type { Score } from '@coderline/alphatab/model/Score'; import type { BackingTrackSyncPoint, IAlphaSynth } from '@coderline/alphatab/synth/IAlphaSynth'; import type { ISynthOutput } from '@coderline/alphatab/synth/ISynthOutput'; import type { MidiEventsPlayedEventArgs } from '@coderline/alphatab/synth/MidiEventsPlayedEventArgs'; -import type { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange'; +import { 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'; @@ -23,7 +28,7 @@ import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; * This wrapper is used when re-exposing the underlying player via {@link AlphaTabApiBase} to integrators. * Even with dynamic switching between synthesizer, backing tracks etc. aspects like volume, playbackspeed, * event listeners etc. should not be lost. - * + * * @internal */ export class AlphaSynthWrapper implements IAlphaSynth { @@ -38,6 +43,8 @@ export class AlphaSynthWrapper implements IAlphaSynth { private _instance?: IAlphaSynth; private _instanceEventUnregister?: (() => void)[]; + public midiTickShift = 0; + public constructor() { this.ready = new EventEmitter(() => this.isReady); this.readyForPlayback = new EventEmitter(() => this.isReadyForPlayback); @@ -86,7 +93,9 @@ export class AlphaSynthWrapper implements IAlphaSynth { ); newUnregister.push( value.midiLoaded.on(e => { - (this.midiLoaded as EventEmitterOfT).trigger(e); + (this.midiLoaded as EventEmitterOfT).trigger( + this._shiftPositionChangedEventArgsToApi(e)! + ); }) ); newUnregister.push( @@ -99,7 +108,9 @@ export class AlphaSynthWrapper implements IAlphaSynth { ); newUnregister.push( value.positionChanged.on(e => { - (this.positionChanged as EventEmitterOfT).trigger(e); + (this.positionChanged as EventEmitterOfT).trigger( + this._shiftPositionChangedEventArgsToApi(e)! + ); }) ); newUnregister.push( @@ -109,7 +120,9 @@ export class AlphaSynthWrapper implements IAlphaSynth { ); newUnregister.push( value.playbackRangeChanged.on(e => - (this.playbackRangeChanged as EventEmitterOfT).trigger(e) + (this.playbackRangeChanged as EventEmitterOfT).trigger( + this._shiftPlaybackRangeChangedEventArgsToApi(e) + ) ) ); @@ -204,21 +217,21 @@ export class AlphaSynthWrapper implements IAlphaSynth { } public get loadedMidiInfo(): PositionChangedEventArgs | undefined { - return this._instance ? this._instance.loadedMidiInfo : undefined; + return this._instance ? this._shiftPositionChangedEventArgsToApi(this._instance.loadedMidiInfo) : undefined; } public get currentPosition(): PositionChangedEventArgs { return this._instance - ? this._instance.currentPosition + ? this._shiftPositionChangedEventArgsToApi(this._instance.currentPosition)! : new PositionChangedEventArgs(0, 0, 0, 0, false, 120, 120); } public get tickPosition(): number { - return this._instance ? this._instance.tickPosition : 0; + return this._instance ? this._shiftTickToApi(this._instance.tickPosition) : 0; } public set tickPosition(value: number) { if (this._instance) { - this._instance.tickPosition = value; + this._instance.tickPosition = this._shiftTickToPlayer(value); } } @@ -233,12 +246,12 @@ export class AlphaSynthWrapper implements IAlphaSynth { } public get playbackRange(): PlaybackRange | null { - return this._instance ? this._instance.playbackRange : null; + return this._instance ? this._shiftPlaybackRangeToApi(this._instance.playbackRange) : null; } public set playbackRange(value: PlaybackRange | null) { if (this._instance) { - this._instance!.playbackRange = value; + this._instance!.playbackRange = this._shiftPlaybackRangeToPlayer(value); } } @@ -388,4 +401,78 @@ export class AlphaSynthWrapper implements IAlphaSynth { public readonly midiEventsPlayed: IEventEmitterOfT = new EventEmitterOfT(); public readonly playbackRangeChanged: IEventEmitterOfT; + + private _shiftPlaybackRangeChangedEventArgsToApi(e: PlaybackRangeChangedEventArgs): PlaybackRangeChangedEventArgs { + if (e.playbackRange == null) { + return e; + } + + const tickShift = this.midiTickShift; + if (tickShift > 0) { + return new PlaybackRangeChangedEventArgs(this._shiftPlaybackRangeToApi(e.playbackRange)); + } else { + return e; + } + } + + private _shiftPlaybackRangeToApi(e: PlaybackRange | null): PlaybackRange | null { + if (e == null) { + return e; + } + + const tickShift = this.midiTickShift; + if (tickShift > 0) { + const range = new PlaybackRange(); + range.startTick = e.startTick - tickShift; + range.endTick = e.endTick - tickShift; + return range; + } else { + return e; + } + } + + private _shiftPlaybackRangeToPlayer(e: PlaybackRange | null): PlaybackRange | null { + if (e == null) { + return e; + } + + const tickShift = this.midiTickShift; + if (tickShift > 0) { + const range = new PlaybackRange(); + range.startTick = e.startTick + tickShift; + range.endTick = e.endTick + tickShift; + return range; + } else { + return e; + } + } + + private _shiftPositionChangedEventArgsToApi( + e: PositionChangedEventArgs | undefined + ): PositionChangedEventArgs | undefined { + if (!e) { + return e; + } + + const tickShift = this.midiTickShift; + return tickShift > 0 + ? new PositionChangedEventArgs( + e.currentTime, + e.endTime, + e.currentTick - tickShift, + e.endTick - tickShift, + e.isSeek, + e.originalTempo, + e.modifiedTempo + ) + : e; + } + + private _shiftTickToApi(tickPosition: number): number { + return tickPosition - this.midiTickShift; + } + + private _shiftTickToPlayer(tickPosition: number): number { + return tickPosition + this.midiTickShift; + } } diff --git a/packages/alphatab/src/synth/MidiFileSequencer.ts b/packages/alphatab/src/synth/MidiFileSequencer.ts index fd8833d2e..1feac5b3e 100644 --- a/packages/alphatab/src/synth/MidiFileSequencer.ts +++ b/packages/alphatab/src/synth/MidiFileSequencer.ts @@ -236,7 +236,7 @@ export class MidiFileSequencer { let metronomeCount: number = 0; let metronomeLengthInTicks: number = 0; let metronomeLengthInMillis: number = 0; - let metronomeTick: number = 0; + let metronomeTick: number = midiFile.tickShift; // shift metronome to content let metronomeTime: number = 0.0; let previousTick: number = 0; @@ -268,7 +268,7 @@ export class MidiFileSequencer { if (mEvent.type === MidiEventType.TempoChange) { const meta: TempoChangeEvent = mEvent as TempoChangeEvent; - bpm = meta.beatsPerMinute; + bpm = MidiFileSequencer._sanitizeBpm(meta.beatsPerMinute); state.tempoChanges.push(new MidiFileSequencerTempoChange(bpm, absTick, absTime)); metronomeLengthInMillis = metronomeLengthInTicks * (60000.0 / (bpm * midiFile.division)); } else if (mEvent.type === MidiEventType.TimeSignature) { @@ -405,7 +405,7 @@ export class MidiFileSequencer { previousTick = 0; } else { const previousSyncPoint = syncPoints[i - 1]; - previousModifiedTempo = previousSyncPoint.syncBpm; + previousModifiedTempo = MidiFileSequencer._sanitizeBpm(previousSyncPoint.syncBpm); previousMillisecondOffset = previousSyncPoint.syncTime; previousTick = previousSyncPoint.synthTick; } @@ -436,7 +436,7 @@ export class MidiFileSequencer { syncPoint.syncBpm = previousModifiedTempo; } - bpm = state.tempoChanges[tempoChangeIndex].bpm; + bpm = MidiFileSequencer._sanitizeBpm(state.tempoChanges[tempoChangeIndex].bpm); tempoChangeIndex++; } @@ -462,11 +462,16 @@ export class MidiFileSequencer { this._updateCurrentTempo(state, timePosition); const lastTempoChange = state.tempoChanges[state.tempoChangeIndex]; const timeDiff = timePosition - lastTempoChange.time; - const ticks = (timeDiff / (60000.0 / (lastTempoChange.bpm * state.division))) | 0; + const ticks = + (timeDiff / (60000.0 / (MidiFileSequencer._sanitizeBpm(lastTempoChange.bpm) * state.division))) | 0; // we add 1 for possible rounding errors.(floating point issuses) return lastTempoChange.ticks + ticks + 1; } + private static _sanitizeBpm(bpm: number) { + return Math.max(bpm, 1); // prevent <0 bpms. Doesn't make sense and can cause endless loops + } + public currentUpdateCurrentTempo(timePosition: number) { this._updateCurrentTempo(this._mainState, timePosition * this.playbackSpeed); } diff --git a/packages/alphatab/test-data/audio/small-tempo.xml b/packages/alphatab/test-data/audio/small-tempo.xml new file mode 100644 index 000000000..0b0ef609f --- /dev/null +++ b/packages/alphatab/test-data/audio/small-tempo.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + Drums + + Drum Set + + + 10 + 1 + + + + + + + 4 + + 0 + + + + percussion + + + + + + quarter + 0.111 + + + + + + + 16 + 1 + + + + + + 16 + 1 + + + + + + 16 + 1 + + + + \ No newline at end of file diff --git a/packages/alphatab/test-data/exporter/articulations.gp b/packages/alphatab/test-data/exporter/articulations.gp new file mode 100644 index 000000000..3cc06afda Binary files /dev/null and b/packages/alphatab/test-data/exporter/articulations.gp differ diff --git a/packages/alphatab/test-data/exporter/articulations.source b/packages/alphatab/test-data/exporter/articulations.source new file mode 100644 index 000000000..467e29e65 --- /dev/null +++ b/packages/alphatab/test-data/exporter/articulations.source @@ -0,0 +1,198 @@ +// BEGIN generated articulations +public static instrumentArticulations: Map = new Map( + [ + InstrumentArticulation.create(38, "Snare", 3, 38, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(37, "Snare", 3, 37, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(91, "Snare", 3, 38, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite), + InstrumentArticulation.create(42, "Charley", -1, 42, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(92, "Charley", -1, 46, MusicFontSymbol.NoteheadCircleSlash, MusicFontSymbol.NoteheadCircleSlash, MusicFontSymbol.NoteheadCircleSlash), + InstrumentArticulation.create(46, "Charley", -1, 46, MusicFontSymbol.NoteheadCircleX, MusicFontSymbol.NoteheadCircleX, MusicFontSymbol.NoteheadCircleX), + InstrumentArticulation.create(44, "Charley", 9, 44, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(35, "Acoustic Kick Drum", 8, 35, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(36, "Kick Drum", 7, 36, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(50, "Tom Very High", 1, 50, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(48, "Tom High", 2, 48, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(47, "Tom Medium", 4, 47, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(45, "Tom Low", 5, 45, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(43, "Tom Very Low", 6, 43, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(93, "Ride", 0, 51, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.PictEdgeOfCymbal, TechniqueSymbolPlacement.Above), + InstrumentArticulation.create(51, "Ride", 0, 51, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(53, "Ride", 0, 53, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite), + InstrumentArticulation.create(94, "Ride", 0, 51, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.ArticStaccatoAbove, TechniqueSymbolPlacement.Outside), + InstrumentArticulation.create(55, "Splash", -2, 55, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(95, "Splash", -2, 55, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.ArticStaccatoAbove, TechniqueSymbolPlacement.Outside), + InstrumentArticulation.create(52, "China", -3, 52, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat), + InstrumentArticulation.create(96, "China", -3, 52, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat), + InstrumentArticulation.create(49, "Crash High", -2, 49, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX), + InstrumentArticulation.create(97, "Crash High", -2, 49, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.ArticStaccatoAbove, TechniqueSymbolPlacement.Outside), + InstrumentArticulation.create(57, "Crash Medium", -1, 57, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX), + InstrumentArticulation.create(98, "Crash Medium", -1, 57, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.ArticStaccatoAbove, TechniqueSymbolPlacement.Outside), + InstrumentArticulation.create(99, "Cowbell Low", 1, 56, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpHalf, MusicFontSymbol.NoteheadTriangleUpWhole), + InstrumentArticulation.create(100, "Cowbell Low", 1, 56, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXHalf, MusicFontSymbol.NoteheadXWhole), + InstrumentArticulation.create(56, "Cowbell Medium", 0, 56, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpHalf, MusicFontSymbol.NoteheadTriangleUpWhole), + InstrumentArticulation.create(101, "Cowbell Medium", 0, 56, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXHalf, MusicFontSymbol.NoteheadXWhole), + InstrumentArticulation.create(102, "Cowbell High", -1, 56, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpHalf, MusicFontSymbol.NoteheadTriangleUpWhole), + InstrumentArticulation.create(103, "Cowbell High", -1, 56, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXHalf, MusicFontSymbol.NoteheadXWhole), + InstrumentArticulation.create(77, "Woodblock Low", -9, 77, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack), + InstrumentArticulation.create(76, "Woodblock High", -10, 76, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack), + InstrumentArticulation.create(60, "Bongo High", -4, 60, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(104, "Bongo High", -5, 60, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside), + InstrumentArticulation.create(105, "Bongo High", -6, 60, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(61, "Bongo Low", -7, 61, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(106, "Bongo Low", -8, 61, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside), + InstrumentArticulation.create(107, "Bongo Low", -16, 61, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(66, "Timbale Low", 10, 66, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(65, "Timbale High", 9, 65, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(68, "Agogo Low", 12, 68, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(67, "Agogo High", 11, 67, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(64, "Conga Low", 17, 64, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(108, "Conga Low", 16, 64, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(109, "Conga Low", 15, 64, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside), + InstrumentArticulation.create(63, "Conga High", 14, 63, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(110, "Conga High", 13, 63, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(62, "Conga High", 19, 62, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside), + InstrumentArticulation.create(72, "Whistle Low", -11, 72, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(71, "Whistle High", -17, 71, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(73, "Guiro", 38, 73, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(74, "Guiro", 37, 74, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(86, "Surdo", 36, 86, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(87, "Surdo", 35, 87, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside), + InstrumentArticulation.create(54, "Tambourine", 3, 54, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack), + InstrumentArticulation.create(111, "Tambourine", 2, 54, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.StringsUpBow, TechniqueSymbolPlacement.Above), + InstrumentArticulation.create(112, "Tambourine", 1, 54, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.StringsDownBow, TechniqueSymbolPlacement.Above), + InstrumentArticulation.create(113, "Tambourine", -7, 54, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(79, "Cuica", 30, 79, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(78, "Cuica", 29, 78, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(58, "Vibraslap", 28, 58, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(81, "Triangle", 27, 81, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(80, "Triangle", 26, 80, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadParenthesis, TechniqueSymbolPlacement.Inside), + InstrumentArticulation.create(114, "Grancassa", 25, 43, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(115, "Piatti", 18, 49, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(116, "Piatti", 24, 49, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(69, "Cabasa", 23, 69, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(117, "Cabasa", 22, 69, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, TechniqueSymbolPlacement.Outside), + InstrumentArticulation.create(85, "Castanets", 21, 85, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(75, "Claves", 20, 75, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(70, "Left Maraca", -12, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(118, "Left Maraca", -13, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, TechniqueSymbolPlacement.Outside), + InstrumentArticulation.create(119, "Right Maraca", -14, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(120, "Right Maraca", -15, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, TechniqueSymbolPlacement.Outside), + InstrumentArticulation.create(82, "Shaker", -23, 82, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(122, "Shaker", -24, 82, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, TechniqueSymbolPlacement.Outside), + InstrumentArticulation.create(84, "Bell Tree", -18, 53, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(123, "Bell Tree", -19, 53, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, TechniqueSymbolPlacement.Outside), + InstrumentArticulation.create(83, "Jingle Bell", -20, 53, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(83, "Tinkle Bell", -20, 53, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(124, "Golpe", -21, 62, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.GuitarGolpe, TechniqueSymbolPlacement.Below), + InstrumentArticulation.create(125, "Golpe", -22, 62, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.GuitarGolpe, TechniqueSymbolPlacement.Above), + InstrumentArticulation.create(39, "Hand Clap", 3, 39, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(40, "Electric Snare", 3, 40, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(31, "Sticks", 3, 40, MusicFontSymbol.NoteheadSlashedBlack2, MusicFontSymbol.NoteheadSlashedBlack2, MusicFontSymbol.NoteheadSlashedBlack2), + InstrumentArticulation.create(41, "Very Low Floor Tom", 5, 41, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole), + InstrumentArticulation.create(59, "Ride Cymbal 2", 2, 59, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.PictEdgeOfCymbal, TechniqueSymbolPlacement.Above), + InstrumentArticulation.create(126, "Ride Cymbal 2", 2, 59, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(127, "Ride Cymbal 2", 2, 59, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite), + InstrumentArticulation.create(29, "Ride Cymbal 2", 2, 59, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.ArticStaccatoAbove, TechniqueSymbolPlacement.Outside), + InstrumentArticulation.create(30, "Reverse Cymbal", -3, 49, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(33, "Metronome", 3, 37, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack), + InstrumentArticulation.create(34, "Metronome", 3, 38, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadBlack), + ].map(articulation => [articulation.uniqueId, articulation])); + +private static _instrumentArticulationNames = new Map([ + ["Snare (hit)", "Snare.38"], + ["Snare (side stick)", "Snare.37"], + ["Snare (rim shot)", "Snare.91"], + ["Hi-Hat (closed)", "Charley.42"], + ["Hi-Hat (half)", "Charley.92"], + ["Hi-Hat (open)", "Charley.46"], + ["Pedal Hi-Hat (hit)", "Charley.44"], + ["Kick (hit)", "Acoustic Kick Drum.35"], + ["Kick (hit) 2", "Kick Drum.36"], + ["High Floor Tom (hit)", "Tom Very High.50"], + ["High Tom (hit)", "Tom High.48"], + ["Mid Tom (hit)", "Tom Medium.47"], + ["Low Tom (hit)", "Tom Low.45"], + ["Very Low Tom (hit)", "Tom Very Low.43"], + ["Ride (edge)", "Ride.93"], + ["Ride (middle)", "Ride.51"], + ["Ride (bell)", "Ride.53"], + ["Ride (choke)", "Ride.94"], + ["Splash (hit)", "Splash.55"], + ["Splash (choke)", "Splash.95"], + ["China (hit)", "China.52"], + ["China (choke)", "China.96"], + ["Crash high (hit)", "Crash High.49"], + ["Crash high (choke)", "Crash High.97"], + ["Crash medium (hit)", "Crash Medium.57"], + ["Crash medium (choke)", "Crash Medium.98"], + ["Cowbell low (hit)", "Cowbell Low.99"], + ["Cowbell low (tip)", "Cowbell Low.100"], + ["Cowbell medium (hit)", "Cowbell Medium.56"], + ["Cowbell medium (tip)", "Cowbell Medium.101"], + ["Cowbell high (hit)", "Cowbell High.102"], + ["Cowbell high (tip)", "Cowbell High.103"], + ["Woodblock low (hit)", "Woodblock Low.77"], + ["Woodblock high (hit)", "Woodblock High.76"], + ["Bongo High (hit)", "Bongo High.60"], + ["Bongo High (mute)", "Bongo High.104"], + ["Bongo High (slap)", "Bongo High.105"], + ["Bongo Low (hit)", "Bongo Low.61"], + ["Bongo Low (mute)", "Bongo Low.106"], + ["Bongo Low (slap)", "Bongo Low.107"], + ["Timbale low (hit)", "Timbale Low.66"], + ["Timbale high (hit)", "Timbale High.65"], + ["Agogo low (hit)", "Agogo Low.68"], + ["Agogo high (hit)", "Agogo High.67"], + ["Conga low (hit)", "Conga Low.64"], + ["Conga low (slap)", "Conga Low.108"], + ["Conga low (mute)", "Conga Low.109"], + ["Conga high (hit)", "Conga High.63"], + ["Conga high (slap)", "Conga High.110"], + ["Conga high (mute)", "Conga High.62"], + ["Whistle low (hit)", "Whistle Low.72"], + ["Whistle high (hit)", "Whistle High.71"], + ["Guiro (hit)", "Guiro.73"], + ["Guiro (scrap-return)", "Guiro.74"], + ["Surdo (hit)", "Surdo.86"], + ["Surdo (mute)", "Surdo.87"], + ["Tambourine (hit)", "Tambourine.54"], + ["Tambourine (return)", "Tambourine.111"], + ["Tambourine (roll)", "Tambourine.112"], + ["Tambourine (hand)", "Tambourine.113"], + ["Cuica (open)", "Cuica.79"], + ["Cuica (mute)", "Cuica.78"], + ["Vibraslap (hit)", "Vibraslap.58"], + ["Triangle (hit)", "Triangle.81"], + ["Triangle (mute)", "Triangle.80"], + ["Grancassa (hit)", "Grancassa.114"], + ["Piatti (hit)", "Piatti.115"], + ["Piatti (hand)", "Piatti.116"], + ["Cabasa (hit)", "Cabasa.69"], + ["Cabasa (return)", "Cabasa.117"], + ["Castanets (hit)", "Castanets.85"], + ["Claves (hit)", "Claves.75"], + ["Left Maraca (hit)", "Left Maraca.70"], + ["Left Maraca (return)", "Left Maraca.118"], + ["Right Maraca (hit)", "Right Maraca.119"], + ["Right Maraca (return)", "Right Maraca.120"], + ["Shaker (hit)", "Shaker.82"], + ["Shaker (return)", "Shaker.122"], + ["Bell Tree (hit)", "Bell Tree.84"], + ["Bell Tree (return)", "Bell Tree.123"], + ["Jingle Bell (hit)", "Jingle Bell.83"], + ["Tinkle Bell (hit)", "Tinkle Bell.83"], + ["Golpe (thumb)", "Golpe.124"], + ["Golpe (finger)", "Golpe.125"], + ["Hand Clap (hit)", "Hand Clap.39"], + ["Electric Snare (hit)", "Electric Snare.40"], + ["Snare (side stick) 2", "Sticks.31"], + ["Low Floor Tom (hit)", "Very Low Floor Tom.41"], + ["Ride (edge) 2", "Ride Cymbal 2.59"], + ["Ride (middle) 2", "Ride Cymbal 2.126"], + ["Ride (bell) 2", "Ride Cymbal 2.127"], + ["Ride (choke) 2", "Ride Cymbal 2.29"], + ["Reverse Cymbal (hit)", "Reverse Cymbal.30"], + ["Metronome (hit)", "Metronome.33"], + ["Metronome (bell)", "Metronome.34"], +]); +// END generated articulations \ No newline at end of file diff --git a/packages/alphatab/test-data/exporter/notation-legend-formatted.atex b/packages/alphatab/test-data/exporter/notation-legend-formatted.atex index 4e60b2d09..2dea18f19 100644 --- a/packages/alphatab/test-data/exporter/notation-legend-formatted.atex +++ b/packages/alphatab/test-data/exporter/notation-legend-formatted.atex @@ -242,6 +242,8 @@ 12.2{x}.16{sd gr onbeat beam Up} 12.1.1{sd beam Down} | + // Masterbar 37 Metadata + \beaming (8 2 2 2 2) // Bar 37 / Voice 1 contents 5.1{x}.16{sd gr onbeat beam Up} 5.2{x}.16{sd gr onbeat beam Up} @@ -269,6 +271,7 @@ | // Masterbar 41 Metadata \ts (1 4) + \beaming (8 2 2 2 2) // Bar 41 / Voice 1 contents 5.3{sl}.8{beam Down} 7.3.8{beam Down} @@ -299,6 +302,7 @@ | // Masterbar 49 Metadata \ts (4 4) + \beaming (8 2 2 2 2) // Bar 49 / Voice 1 contents 5.4{h}.2{beam Up} 7.4.2{beam Up} @@ -370,9 +374,9 @@ 7.5{pm}.8{beam Up} | // Bar 58 / Voice 1 contents - 7.5.4{tp 32 beam Up} - 8.5.4{tp 32 beam Up} - 9.5{acc #}.2{tp 32 beam Up} + 7.5.4{tp 3 beam Up} + 8.5.4{tp 3 beam Up} + 9.5{acc #}.2{tp 3 beam Up} | // Bar 59 / Voice 1 contents 7.3{tr (9 16)}.1{beam Down} @@ -527,6 +531,7 @@ | // Masterbar 83 Metadata \ts (5 8) + \beaming (8 2 2 2 2) // Bar 83 / Voice 1 contents 7.5.8{beam Up} 7.5.8{beam Up} @@ -535,6 +540,7 @@ | // Masterbar 84 Metadata \ts (4 4) + \beaming (8 2 2 2 2) \tempo 60 // Bar 84 / Voice 1 contents 7.5.8{beam Up} @@ -795,6 +801,7 @@ | // Masterbar 117 Metadata \ts (6 8) + \beaming (8 3 3) // Bar 117 / Voice 1 contents 0.6{lr}.8{beam Up} (0.3{lr} 0.6{lr t}).8{beam Up} @@ -839,6 +846,7 @@ | // Masterbar 124 Metadata \ts (4 4) + \beaming (8 2 2 2 2) // Bar 124 / Voice 1 contents 12.3{lf 2}.4{sd beam Down} 14.3{lf 4}.8{sd beam Down} diff --git a/packages/alphatab/test-data/exporter/soundmapper.source b/packages/alphatab/test-data/exporter/soundmapper.source new file mode 100644 index 000000000..04b68dbe9 --- /dev/null +++ b/packages/alphatab/test-data/exporter/soundmapper.source @@ -0,0 +1,207 @@ +// BEGIN generated +private static _drumInstrumentSet = GpifInstrumentSet.create("Drums", "drumKit", 5, [ + new GpifInstrumentElement("Snare", "snare", "Master-Snare", [ + GpifInstrumentArticulation.template("Snare (hit)", [38], "stick.hit.hit"), + GpifInstrumentArticulation.template("Snare (side stick)", [37], "stick.hit.sidestick"), + GpifInstrumentArticulation.template("Snare (rim shot)", [91], "stick.hit.rimshot"), + ]), + new GpifInstrumentElement("Charley", "hiHat", "Master-Hihat", [ + GpifInstrumentArticulation.template("Hi-Hat (closed)", [42], "stick.hit.closed"), + GpifInstrumentArticulation.template("Hi-Hat (half)", [92], "stick.hit.half"), + GpifInstrumentArticulation.template("Hi-Hat (open)", [46], "stick.hit.open"), + GpifInstrumentArticulation.template("Pedal Hi-Hat (hit)", [44], "pedal.hit.pedal"), + ]), + new GpifInstrumentElement("Acoustic Kick Drum", "kickDrum", "AcousticKick-Percu", [ + GpifInstrumentArticulation.template("Kick (hit)", [35], "pedal.hit.hit"), + ]), + new GpifInstrumentElement("Kick Drum", "kickDrum", "Master-Kick", [ + GpifInstrumentArticulation.template("Kick (hit)", [36], "pedal.hit.hit"), + ]), + new GpifInstrumentElement("Tom Very High", "tom", "Master-Tom05", [ + GpifInstrumentArticulation.template("High Floor Tom (hit)", [50], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Tom High", "tom", "Master-Tom04", [ + GpifInstrumentArticulation.template("High Tom (hit)", [48], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Tom Medium", "tom", "Master-Tom03", [ + GpifInstrumentArticulation.template("Mid Tom (hit)", [47], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Tom Low", "tom", "Master-Tom02", [ + GpifInstrumentArticulation.template("Low Tom (hit)", [45], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Tom Very Low", "tom", "Master-Tom01", [ + GpifInstrumentArticulation.template("Very Low Tom (hit)", [43], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Ride", "ride", "Master-Ride", [ + GpifInstrumentArticulation.template("Ride (edge)", [93], "stick.hit.edge"), + GpifInstrumentArticulation.template("Ride (middle)", [51], "stick.hit.mid"), + GpifInstrumentArticulation.template("Ride (bell)", [53], "stick.hit.bell"), + GpifInstrumentArticulation.template("Ride (choke)", [94], "stick.hit.choke"), + ]), + new GpifInstrumentElement("Splash", "splash", "Master-Splash", [ + GpifInstrumentArticulation.template("Splash (hit)", [55], "stick.hit.hit"), + GpifInstrumentArticulation.template("Splash (choke)", [95], "stick.hit.choke"), + ]), + new GpifInstrumentElement("China", "china", "Master-China", [ + GpifInstrumentArticulation.template("China (hit)", [52], "stick.hit.hit"), + GpifInstrumentArticulation.template("China (choke)", [96], "stick.hit.choke"), + ]), + new GpifInstrumentElement("Crash High", "crash", "Master-Crash02", [ + GpifInstrumentArticulation.template("Crash high (hit)", [49], "stick.hit.hit"), + GpifInstrumentArticulation.template("Crash high (choke)", [97], "stick.hit.choke"), + ]), + new GpifInstrumentElement("Crash Medium", "crash", "Master-Crash01", [ + GpifInstrumentArticulation.template("Crash medium (hit)", [57], "stick.hit.hit"), + GpifInstrumentArticulation.template("Crash medium (choke)", [98], "stick.hit.choke"), + ]), + new GpifInstrumentElement("Cowbell Low", "cowbell", "CowbellBig-Percu", [ + GpifInstrumentArticulation.template("Cowbell low (hit)", [99], "stick.hit.hit"), + GpifInstrumentArticulation.template("Cowbell low (tip)", [100], "stick.hit.tip"), + ]), + new GpifInstrumentElement("Cowbell Medium", "cowbell", "CowbellMid-Percu", [ + GpifInstrumentArticulation.template("Cowbell medium (hit)", [56], "stick.hit.hit"), + GpifInstrumentArticulation.template("Cowbell medium (tip)", [101], "stick.hit.tip"), + ]), + new GpifInstrumentElement("Cowbell High", "cowbell", "CowbellSmall-Percu", [ + GpifInstrumentArticulation.template("Cowbell high (hit)", [102], "stick.hit.hit"), + GpifInstrumentArticulation.template("Cowbell high (tip)", [103], "stick.hit.tip"), + ]), + new GpifInstrumentElement("Woodblock Low", "woodblock", "WoodblockLow-Percu", [ + GpifInstrumentArticulation.template("Woodblock low (hit)", [77], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Woodblock High", "woodblock", "WoodblockHigh-Percu", [ + GpifInstrumentArticulation.template("Woodblock high (hit)", [76], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Bongo High", "bongo", "BongoHigh-Percu", [ + GpifInstrumentArticulation.template("Bongo High (hit)", [60], "hand.hit.hit"), + GpifInstrumentArticulation.template("Bongo High (mute)", [104], "hand.hit.mute"), + GpifInstrumentArticulation.template("Bongo High (slap)", [105], "hand.hit.slap"), + ]), + new GpifInstrumentElement("Bongo Low", "bongo", "BongoLow-Percu", [ + GpifInstrumentArticulation.template("Bongo Low (hit)", [61], "hand.hit.hit"), + GpifInstrumentArticulation.template("Bongo Low (mute)", [106], "hand.hit.mute"), + GpifInstrumentArticulation.template("Bongo Low (slap)", [107], "hand.hit.slap"), + ]), + new GpifInstrumentElement("Timbale Low", "timbale", "TimbaleLow-Percu", [ + GpifInstrumentArticulation.template("Timbale low (hit)", [66], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Timbale High", "timbale", "TimbaleHigh-Percu", [ + GpifInstrumentArticulation.template("Timbale high (hit)", [65], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Agogo Low", "agogo", "AgogoLow-Percu", [ + GpifInstrumentArticulation.template("Agogo low (hit)", [68], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Agogo High", "agogo", "AgogoHigh-Percu", [ + GpifInstrumentArticulation.template("Agogo high (hit)", [67], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Conga Low", "conga", "CongaLow-Percu", [ + GpifInstrumentArticulation.template("Conga low (hit)", [64], "hand.hit.hit"), + GpifInstrumentArticulation.template("Conga low (slap)", [108], "hand.hit.slap"), + GpifInstrumentArticulation.template("Conga low (mute)", [109], "hand.hit.mute"), + ]), + new GpifInstrumentElement("Conga High", "conga", "CongaHigh-Percu", [ + GpifInstrumentArticulation.template("Conga high (hit)", [63], "hand.hit.hit"), + GpifInstrumentArticulation.template("Conga high (slap)", [110], "hand.hit.slap"), + GpifInstrumentArticulation.template("Conga high (mute)", [62], "hand.hit.mute"), + ]), + new GpifInstrumentElement("Whistle Low", "whistle", "WhistleLow-Percu", [ + GpifInstrumentArticulation.template("Whistle low (hit)", [72], "blow.hit.hit"), + ]), + new GpifInstrumentElement("Whistle High", "whistle", "WhistleHigh-Percu", [ + GpifInstrumentArticulation.template("Whistle high (hit)", [71], "blow.hit.hit"), + ]), + new GpifInstrumentElement("Guiro", "guiro", "Guiro-Percu", [ + GpifInstrumentArticulation.template("Guiro (hit)", [73], "stick.hit.hit"), + GpifInstrumentArticulation.template("Guiro (scrap-return)", [74], "stick.scrape.return"), + ]), + new GpifInstrumentElement("Surdo", "surdo", "Surdo-Percu", [ + GpifInstrumentArticulation.template("Surdo (hit)", [86], "brush.hit.hit"), + GpifInstrumentArticulation.template("Surdo (mute)", [87], "brush.hit.mute"), + ]), + new GpifInstrumentElement("Tambourine", "tambourine", "Tambourine-Percu", [ + GpifInstrumentArticulation.template("Tambourine (hit)", [54], "hand.hit.hit"), + GpifInstrumentArticulation.template("Tambourine (return)", [111], "hand.hit.return"), + GpifInstrumentArticulation.template("Tambourine (roll)", [112], "hand.hit.roll"), + GpifInstrumentArticulation.template("Tambourine (hand)", [113], "hand.hit.handhit"), + ]), + new GpifInstrumentElement("Cuica", "cuica", "Cuica-Percu", [ + GpifInstrumentArticulation.template("Cuica (open)", [79], "hand.hit.hit"), + GpifInstrumentArticulation.template("Cuica (mute)", [78], "hand.hit.mute"), + ]), + new GpifInstrumentElement("Vibraslap", "vibraslap", "Vibraslap-Percu", [ + GpifInstrumentArticulation.template("Vibraslap (hit)", [58], "hand.hit.hit"), + ]), + new GpifInstrumentElement("Triangle", "triangle", "Triangle-Percu", [ + GpifInstrumentArticulation.template("Triangle (hit)", [81], "stick.hit.hit"), + GpifInstrumentArticulation.template("Triangle (mute)", [80], "stick.hit.mute"), + ]), + new GpifInstrumentElement("Grancassa", "grancassa", "Grancassa-Percu", [ + GpifInstrumentArticulation.template("Grancassa (hit)", [114], "mallet.hit.hit"), + ]), + new GpifInstrumentElement("Piatti", "piatti", "Piatti-Percu", [ + GpifInstrumentArticulation.template("Piatti (hit)", [115], "hand.hit.hit"), + GpifInstrumentArticulation.template("Piatti (hand)", [116], "hand.hit.hit"), + ]), + new GpifInstrumentElement("Cabasa", "cabasa", "Cabasa-Percu", [ + GpifInstrumentArticulation.template("Cabasa (hit)", [69], "hand.hit.hit"), + GpifInstrumentArticulation.template("Cabasa (return)", [117], "hand.hit.return"), + ]), + new GpifInstrumentElement("Castanets", "castanets", "Castanets-Percu", [ + GpifInstrumentArticulation.template("Castanets (hit)", [85], "hand.hit.hit"), + ]), + new GpifInstrumentElement("Claves", "claves", "Claves-Percu", [ + GpifInstrumentArticulation.template("Claves (hit)", [75], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Left Maraca", "maraca", "Maracas-Percu", [ + GpifInstrumentArticulation.template("Left Maraca (hit)", [70], "hand.hit.hit"), + GpifInstrumentArticulation.template("Left Maraca (return)", [118], "hand.hit.return"), + ]), + new GpifInstrumentElement("Right Maraca", "maraca", "Maracas-Percu", [ + GpifInstrumentArticulation.template("Right Maraca (hit)", [119], "hand.hit.hit"), + GpifInstrumentArticulation.template("Right Maraca (return)", [120], "hand.hit.return"), + ]), + new GpifInstrumentElement("Shaker", "shaker", "ShakerStudio-Percu", [ + GpifInstrumentArticulation.template("Shaker (hit)", [82], "hand.hit.hit"), + GpifInstrumentArticulation.template("Shaker (return)", [122], "hand.hit.return"), + ]), + new GpifInstrumentElement("Bell Tree", "bellTree", "BellTree-Percu", [ + GpifInstrumentArticulation.template("Bell Tree (hit)", [84], "stick.hit.hit"), + GpifInstrumentArticulation.template("Bell Tree (return)", [123], "stick.hit.return"), + ]), + new GpifInstrumentElement("Jingle Bell", "jingleBell", "JingleBell-Percu", [ + GpifInstrumentArticulation.template("Jingle Bell (hit)", [83], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Tinkle Bell", "jingleBell", "JingleBell-Percu", [ + GpifInstrumentArticulation.template("Tinkle Bell (hit)", [83], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Golpe", "unpitched", "Golpe-Percu", [ + GpifInstrumentArticulation.template("Golpe (thumb)", [124], "thumb.hit.body"), + GpifInstrumentArticulation.template("Golpe (finger)", [125], "finger4.hit.body"), + ]), + new GpifInstrumentElement("Hand Clap", "handClap", "GroupHandClap-Percu", [ + GpifInstrumentArticulation.template("Hand Clap (hit)", [39], "hand.hit.hit"), + ]), + new GpifInstrumentElement("Electric Snare", "snare", "ElectricSnare-Percu", [ + GpifInstrumentArticulation.template("Electric Snare (hit)", [40], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Sticks", "snare", "Stick-Percu", [ + GpifInstrumentArticulation.template("Snare (side stick)", [31], "stick.hit.sidestick"), + ]), + new GpifInstrumentElement("Very Low Floor Tom", "tom", "LowFloorTom-Percu", [ + GpifInstrumentArticulation.template("Low Floor Tom (hit)", [41], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Ride Cymbal 2", "ride", "Ride-Percu", [ + GpifInstrumentArticulation.template("Ride (edge)", [59], "stick.hit.edge"), + GpifInstrumentArticulation.template("Ride (middle)", [126], "stick.hit.mid"), + GpifInstrumentArticulation.template("Ride (bell)", [127], "stick.hit.bell"), + GpifInstrumentArticulation.template("Ride (choke)", [29], "stick.hit.choke"), + ]), + new GpifInstrumentElement("Reverse Cymbal", "crash", "Reverse-Cymbal", [ + GpifInstrumentArticulation.template("Reverse Cymbal (hit)", [30], "stick.hit.hit"), + ]), + new GpifInstrumentElement("Metronome", "snare", "Metronome-Percu", [ + GpifInstrumentArticulation.template("Metronome (hit)", [33], "stick.hit.sidestick"), + GpifInstrumentArticulation.template("Metronome (bell)", [34], "stick.hit.hit"), + ]), +]); +// END generated \ No newline at end of file diff --git a/packages/alphatab/test-data/guitarpro5/bass-tuning.gp5 b/packages/alphatab/test-data/guitarpro5/bass-tuning.gp5 new file mode 100644 index 000000000..700c1c137 Binary files /dev/null and b/packages/alphatab/test-data/guitarpro5/bass-tuning.gp5 differ diff --git a/packages/alphatab/test-data/guitarpro8/barnumbers-all.gp b/packages/alphatab/test-data/guitarpro8/barnumbers-all.gp new file mode 100644 index 000000000..fffd5e3c2 Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/barnumbers-all.gp differ diff --git a/packages/alphatab/test-data/guitarpro8/barnumbers-first.gp b/packages/alphatab/test-data/guitarpro8/barnumbers-first.gp new file mode 100644 index 000000000..8b41f2f08 Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/barnumbers-first.gp differ diff --git a/packages/alphatab/test-data/guitarpro8/barnumbers-hide.gp b/packages/alphatab/test-data/guitarpro8/barnumbers-hide.gp new file mode 100644 index 000000000..f279f584a Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/barnumbers-hide.gp differ diff --git a/packages/alphatab/test-data/guitarpro8/custom-beaming.gp b/packages/alphatab/test-data/guitarpro8/custom-beaming.gp new file mode 100644 index 000000000..3d3ac1d8e Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/custom-beaming.gp differ diff --git a/packages/alphatab/test-data/guitarpro8/extended-barlines.gp b/packages/alphatab/test-data/guitarpro8/extended-barlines.gp new file mode 100644 index 000000000..81bc49e1f Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/extended-barlines.gp differ diff --git a/packages/alphatab/test-data/guitarpro8/show-diagrams-in-score.gp b/packages/alphatab/test-data/guitarpro8/show-diagrams-in-score.gp new file mode 100644 index 000000000..ed2e0c232 Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/show-diagrams-in-score.gp differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-a.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-a.png index c442452ab..73cab5c2a 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-a.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-a.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-ab.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-ab.png index 86a617ca4..9d6aa2fcf 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-ab.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-ab.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-b.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-b.png index 68f9954bf..b6102fd8d 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-b.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-b.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-bb.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-bb.png index b58222b05..86d63f1ae 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-bb.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-bb.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-c.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-c.png index e0805bb65..ce395d829 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-c.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-c.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-cb.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-cb.png index 7d63a0b9e..3249afb2a 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-cb.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-cb.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-csharp.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-csharp.png index 8be65ece8..a8102f4b8 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-csharp.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-csharp.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-d.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-d.png index 2432e81e1..de9a3e19e 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-d.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-d.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-db.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-db.png index 5f3288be4..86b58fc52 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-db.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-db.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-e.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-e.png index f12af10b1..4b4399d2d 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-e.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-e.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-eb.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-eb.png index 4512fdffc..e2416bba5 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-eb.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-eb.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-f.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-f.png index 2d3464175..cd1ba9aed 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-f.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-f.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-fsharp.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-fsharp.png index 799157c71..284e8b738 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-fsharp.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-fsharp.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-g.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-g.png index 4619946d7..84790605a 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-g.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-g.png differ diff --git a/packages/alphatab/test-data/guitarpro8/transposition-tonality-gb.png b/packages/alphatab/test-data/guitarpro8/transposition-tonality-gb.png index a2b60f015..e6525fee3 100644 Binary files a/packages/alphatab/test-data/guitarpro8/transposition-tonality-gb.png and b/packages/alphatab/test-data/guitarpro8/transposition-tonality-gb.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png b/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png index 05e57e8fa..f2fed3e79 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png and b/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Binchois.png b/packages/alphatab/test-data/musicxml-samples/Binchois.png index e4c84a6b6..0f280a882 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Binchois.png and b/packages/alphatab/test-data/musicxml-samples/Binchois.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png b/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png index e536b59ac..c8f6f933e 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png and b/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/BrookeWestSample.png b/packages/alphatab/test-data/musicxml-samples/BrookeWestSample.png index bc3ea1fb0..c4275b71f 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/BrookeWestSample.png and b/packages/alphatab/test-data/musicxml-samples/BrookeWestSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Chant.png b/packages/alphatab/test-data/musicxml-samples/Chant.png index 5db016908..f87c5a216 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Chant.png and b/packages/alphatab/test-data/musicxml-samples/Chant.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png b/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png index d9416b08f..e4f33cc95 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png and b/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png b/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png index d80153abe..f22306ee3 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png and b/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Echigo.png b/packages/alphatab/test-data/musicxml-samples/Echigo.png index e83d47a3a..5e18fb18c 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Echigo.png and b/packages/alphatab/test-data/musicxml-samples/Echigo.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png b/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png index 0ec272d4c..78cef85d2 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png and b/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MahlFaGe4Sample.png b/packages/alphatab/test-data/musicxml-samples/MahlFaGe4Sample.png index 0f0cc5366..52d16a8db 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MahlFaGe4Sample.png and b/packages/alphatab/test-data/musicxml-samples/MahlFaGe4Sample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MozaChloSample.png b/packages/alphatab/test-data/musicxml-samples/MozaChloSample.png index 2721eaeba..8e2c25d69 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MozaChloSample.png and b/packages/alphatab/test-data/musicxml-samples/MozaChloSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MozaVeilSample.png b/packages/alphatab/test-data/musicxml-samples/MozaVeilSample.png index 678eab888..6a2f58a66 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MozaVeilSample.png and b/packages/alphatab/test-data/musicxml-samples/MozaVeilSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MozartPianoSonata.png b/packages/alphatab/test-data/musicxml-samples/MozartPianoSonata.png index 956d234d6..05731c5b6 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MozartPianoSonata.png and b/packages/alphatab/test-data/musicxml-samples/MozartPianoSonata.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MozartTrio.png b/packages/alphatab/test-data/musicxml-samples/MozartTrio.png index ea412cce9..25df1ffab 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MozartTrio.png and b/packages/alphatab/test-data/musicxml-samples/MozartTrio.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Saltarello.png b/packages/alphatab/test-data/musicxml-samples/Saltarello.png index 1f6f775a8..2e0c78e19 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Saltarello.png and b/packages/alphatab/test-data/musicxml-samples/Saltarello.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png b/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png index 026983c35..8e03eff88 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png and b/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Telemann.png b/packages/alphatab/test-data/musicxml-samples/Telemann.png index e7c9da4af..03080dce7 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Telemann.png and b/packages/alphatab/test-data/musicxml-samples/Telemann.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/01a-Pitches-Pitches.png b/packages/alphatab/test-data/musicxml-testsuite/01a-Pitches-Pitches.png index b363618d4..14a9691a8 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/01a-Pitches-Pitches.png and b/packages/alphatab/test-data/musicxml-testsuite/01a-Pitches-Pitches.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/01b-Pitches-Intervals.png b/packages/alphatab/test-data/musicxml-testsuite/01b-Pitches-Intervals.png index cf70c8506..67c910786 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/01b-Pitches-Intervals.png and b/packages/alphatab/test-data/musicxml-testsuite/01b-Pitches-Intervals.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.png b/packages/alphatab/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.png index 1adeeacba..6b5920f81 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.png and b/packages/alphatab/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/01d-Pitches-Microtones.png b/packages/alphatab/test-data/musicxml-testsuite/01d-Pitches-Microtones.png index 5844ac594..b72cb717a 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/01d-Pitches-Microtones.png and b/packages/alphatab/test-data/musicxml-testsuite/01d-Pitches-Microtones.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.png b/packages/alphatab/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.png index 5a8c81bad..3a3303683 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.png and b/packages/alphatab/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png b/packages/alphatab/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png index 3923ad562..16a69b6b2 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png and b/packages/alphatab/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/02a-Rests-Durations.png b/packages/alphatab/test-data/musicxml-testsuite/02a-Rests-Durations.png index a05865332..d2e7bde08 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/02a-Rests-Durations.png and b/packages/alphatab/test-data/musicxml-testsuite/02a-Rests-Durations.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/02b-Rests-PitchedRests.png b/packages/alphatab/test-data/musicxml-testsuite/02b-Rests-PitchedRests.png index ded3de812..517927c22 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/02b-Rests-PitchedRests.png and b/packages/alphatab/test-data/musicxml-testsuite/02b-Rests-PitchedRests.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.png b/packages/alphatab/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.png index 49def675b..bd43ba84a 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.png and b/packages/alphatab/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.png b/packages/alphatab/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.png index 31ec76da2..db959cdfc 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.png and b/packages/alphatab/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/02e-Rests-NoType.png b/packages/alphatab/test-data/musicxml-testsuite/02e-Rests-NoType.png index 0e4a57a0b..3afc8635d 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/02e-Rests-NoType.png and b/packages/alphatab/test-data/musicxml-testsuite/02e-Rests-NoType.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/03a-Rhythm-Durations.png b/packages/alphatab/test-data/musicxml-testsuite/03a-Rhythm-Durations.png index 951f5bd7e..55d0af4bf 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/03a-Rhythm-Durations.png and b/packages/alphatab/test-data/musicxml-testsuite/03a-Rhythm-Durations.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/03b-Rhythm-Backup.png b/packages/alphatab/test-data/musicxml-testsuite/03b-Rhythm-Backup.png index 2ad282a6a..4f2c6946e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/03b-Rhythm-Backup.png and b/packages/alphatab/test-data/musicxml-testsuite/03b-Rhythm-Backup.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.png b/packages/alphatab/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.png index b4b578399..89d612723 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.png and b/packages/alphatab/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.png b/packages/alphatab/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.png index 7c30f25ce..61d10ac7b 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.png and b/packages/alphatab/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.png b/packages/alphatab/test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.png index 133f86137..8408e0440 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.png and b/packages/alphatab/test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/03f-Rhythm-Forward.png b/packages/alphatab/test-data/musicxml-testsuite/03f-Rhythm-Forward.png index e18c045c8..fc2530121 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/03f-Rhythm-Forward.png and b/packages/alphatab/test-data/musicxml-testsuite/03f-Rhythm-Forward.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/11a-TimeSignatures.png b/packages/alphatab/test-data/musicxml-testsuite/11a-TimeSignatures.png index 032c00c17..618ecfb1f 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/11a-TimeSignatures.png and b/packages/alphatab/test-data/musicxml-testsuite/11a-TimeSignatures.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.png b/packages/alphatab/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.png index 95f9b512a..a24eb5115 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.png and b/packages/alphatab/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.png b/packages/alphatab/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.png index a3bd0ff62..974e10d2c 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.png and b/packages/alphatab/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.png b/packages/alphatab/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.png index 02e392348..be8694844 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.png and b/packages/alphatab/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.png b/packages/alphatab/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.png index 37ca552a0..b926aae1d 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.png and b/packages/alphatab/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.png b/packages/alphatab/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.png index 9288f9832..600b66cfe 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.png and b/packages/alphatab/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/11g-TimeSignatures-SingleNumber.png b/packages/alphatab/test-data/musicxml-testsuite/11g-TimeSignatures-SingleNumber.png index 32434f16d..c89b80763 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/11g-TimeSignatures-SingleNumber.png and b/packages/alphatab/test-data/musicxml-testsuite/11g-TimeSignatures-SingleNumber.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/11h-TimeSignatures-SenzaMisura.png b/packages/alphatab/test-data/musicxml-testsuite/11h-TimeSignatures-SenzaMisura.png index 22126f1d3..3a695a90e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/11h-TimeSignatures-SenzaMisura.png and b/packages/alphatab/test-data/musicxml-testsuite/11h-TimeSignatures-SenzaMisura.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/12a-Clefs.png b/packages/alphatab/test-data/musicxml-testsuite/12a-Clefs.png index 7c021a4cf..a12075ae3 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/12a-Clefs.png and b/packages/alphatab/test-data/musicxml-testsuite/12a-Clefs.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.png b/packages/alphatab/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.png index 03b0f2b27..1e7fca084 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.png and b/packages/alphatab/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/13a-KeySignatures.png b/packages/alphatab/test-data/musicxml-testsuite/13a-KeySignatures.png index 123bd9f4f..a02e913b2 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/13a-KeySignatures.png and b/packages/alphatab/test-data/musicxml-testsuite/13a-KeySignatures.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.png b/packages/alphatab/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.png index f8e349e08..ed8af15b6 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.png and b/packages/alphatab/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.png b/packages/alphatab/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.png index 2e6d1afb9..8f325d69e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.png and b/packages/alphatab/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.png b/packages/alphatab/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.png index 83dc36ad5..c43b108d8 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.png and b/packages/alphatab/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/13e-KeySignatures-Cancel.png b/packages/alphatab/test-data/musicxml-testsuite/13e-KeySignatures-Cancel.png index e8f07d270..10de53be0 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/13e-KeySignatures-Cancel.png and b/packages/alphatab/test-data/musicxml-testsuite/13e-KeySignatures-Cancel.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/13f-KeySignatures-Visible.png b/packages/alphatab/test-data/musicxml-testsuite/13f-KeySignatures-Visible.png index 0adb04c6c..3f219fced 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/13f-KeySignatures-Visible.png and b/packages/alphatab/test-data/musicxml-testsuite/13f-KeySignatures-Visible.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.png b/packages/alphatab/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.png index addd7bf7b..d4f514aee 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.png and b/packages/alphatab/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/21a-Chord-Basic.png b/packages/alphatab/test-data/musicxml-testsuite/21a-Chord-Basic.png index 514527bad..836badc45 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/21a-Chord-Basic.png and b/packages/alphatab/test-data/musicxml-testsuite/21a-Chord-Basic.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/21b-Chords-TwoNotes.png b/packages/alphatab/test-data/musicxml-testsuite/21b-Chords-TwoNotes.png index 8668f824f..df36ffb02 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/21b-Chords-TwoNotes.png and b/packages/alphatab/test-data/musicxml-testsuite/21b-Chords-TwoNotes.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.png b/packages/alphatab/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.png index 1e2bb8395..2b16af82a 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.png and b/packages/alphatab/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.png b/packages/alphatab/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.png index 957e1f038..7188c59ff 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.png and b/packages/alphatab/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.png b/packages/alphatab/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.png index 9714bba91..2b9fc42cd 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.png and b/packages/alphatab/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.png b/packages/alphatab/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.png index 36550587c..9d2820959 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.png and b/packages/alphatab/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png b/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png index cf7142c4f..0dd679c73 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png and b/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/21h-Chord-Accidentals.png b/packages/alphatab/test-data/musicxml-testsuite/21h-Chord-Accidentals.png index 6f8a8f7f3..d33d4e484 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/21h-Chord-Accidentals.png and b/packages/alphatab/test-data/musicxml-testsuite/21h-Chord-Accidentals.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/22a-Noteheads.png b/packages/alphatab/test-data/musicxml-testsuite/22a-Noteheads.png index f8628572c..42ccae389 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/22a-Noteheads.png and b/packages/alphatab/test-data/musicxml-testsuite/22a-Noteheads.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/22b-Staff-Notestyles.png b/packages/alphatab/test-data/musicxml-testsuite/22b-Staff-Notestyles.png index 28c70aff8..5eaced5a7 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/22b-Staff-Notestyles.png and b/packages/alphatab/test-data/musicxml-testsuite/22b-Staff-Notestyles.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/22c-Noteheads-Chords.png b/packages/alphatab/test-data/musicxml-testsuite/22c-Noteheads-Chords.png index bed3d0034..5d0ba7ff2 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/22c-Noteheads-Chords.png and b/packages/alphatab/test-data/musicxml-testsuite/22c-Noteheads-Chords.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.png b/packages/alphatab/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.png index 5b090b465..c7e30369f 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.png and b/packages/alphatab/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/23a-Tuplets.png b/packages/alphatab/test-data/musicxml-testsuite/23a-Tuplets.png index 5281a15b7..106175ae5 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/23a-Tuplets.png and b/packages/alphatab/test-data/musicxml-testsuite/23a-Tuplets.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/23b-Tuplets-Styles.png b/packages/alphatab/test-data/musicxml-testsuite/23b-Tuplets-Styles.png index b478067b0..d664de55e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/23b-Tuplets-Styles.png and b/packages/alphatab/test-data/musicxml-testsuite/23b-Tuplets-Styles.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.png b/packages/alphatab/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.png index 3248b78d9..bc5cf3611 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.png and b/packages/alphatab/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/23d-Tuplets-Nested.png b/packages/alphatab/test-data/musicxml-testsuite/23d-Tuplets-Nested.png index 8f548b380..b831ad7f2 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/23d-Tuplets-Nested.png and b/packages/alphatab/test-data/musicxml-testsuite/23d-Tuplets-Nested.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.png b/packages/alphatab/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.png index 94713099f..5cc27b897 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.png and b/packages/alphatab/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.png b/packages/alphatab/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.png index 2ea845d1f..47f1c835a 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.png and b/packages/alphatab/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/24a-GraceNotes.png b/packages/alphatab/test-data/musicxml-testsuite/24a-GraceNotes.png index 5087d6b5a..aa74e64d8 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/24a-GraceNotes.png and b/packages/alphatab/test-data/musicxml-testsuite/24a-GraceNotes.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/24b-ChordAsGraceNote.png b/packages/alphatab/test-data/musicxml-testsuite/24b-ChordAsGraceNote.png index 9b21be6c4..ec5129df0 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/24b-ChordAsGraceNote.png and b/packages/alphatab/test-data/musicxml-testsuite/24b-ChordAsGraceNote.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.png b/packages/alphatab/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.png index 4cbe22be1..8f395d247 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.png and b/packages/alphatab/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/24d-AfterGrace.png b/packages/alphatab/test-data/musicxml-testsuite/24d-AfterGrace.png index 5502252de..84609a5ad 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/24d-AfterGrace.png and b/packages/alphatab/test-data/musicxml-testsuite/24d-AfterGrace.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.png b/packages/alphatab/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.png index 433bf2891..b91699bc2 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.png and b/packages/alphatab/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/24f-GraceNote-Slur.png b/packages/alphatab/test-data/musicxml-testsuite/24f-GraceNote-Slur.png index 717d5e02c..1a7630dd9 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/24f-GraceNote-Slur.png and b/packages/alphatab/test-data/musicxml-testsuite/24f-GraceNote-Slur.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/24g-GraceNote-Dynamics.png b/packages/alphatab/test-data/musicxml-testsuite/24g-GraceNote-Dynamics.png index 0eabec062..e579ed5d5 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/24g-GraceNote-Dynamics.png and b/packages/alphatab/test-data/musicxml-testsuite/24g-GraceNote-Dynamics.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.png b/packages/alphatab/test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.png index 86db40df5..0a94dc0b9 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.png and b/packages/alphatab/test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/31a-Directions.png b/packages/alphatab/test-data/musicxml-testsuite/31a-Directions.png index fd0f39fc2..1bcc43e50 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/31a-Directions.png and b/packages/alphatab/test-data/musicxml-testsuite/31a-Directions.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/31b-Directions-Order.png b/packages/alphatab/test-data/musicxml-testsuite/31b-Directions-Order.png index 7caa5f8a9..b0e4259f4 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/31b-Directions-Order.png and b/packages/alphatab/test-data/musicxml-testsuite/31b-Directions-Order.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/31c-MetronomeMarks.png b/packages/alphatab/test-data/musicxml-testsuite/31c-MetronomeMarks.png index a46ef4196..8e3d4f127 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/31c-MetronomeMarks.png and b/packages/alphatab/test-data/musicxml-testsuite/31c-MetronomeMarks.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/31d-Directions-Compounds.png b/packages/alphatab/test-data/musicxml-testsuite/31d-Directions-Compounds.png index 4b95edf7e..ef49db479 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/31d-Directions-Compounds.png and b/packages/alphatab/test-data/musicxml-testsuite/31d-Directions-Compounds.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/32a-Notations.png b/packages/alphatab/test-data/musicxml-testsuite/32a-Notations.png index 010dcf56f..844219ead 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/32a-Notations.png and b/packages/alphatab/test-data/musicxml-testsuite/32a-Notations.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/32b-Articulations-Texts.png b/packages/alphatab/test-data/musicxml-testsuite/32b-Articulations-Texts.png index a079ad114..a71377b7e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/32b-Articulations-Texts.png and b/packages/alphatab/test-data/musicxml-testsuite/32b-Articulations-Texts.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/32c-MultipleNotationChildren.png b/packages/alphatab/test-data/musicxml-testsuite/32c-MultipleNotationChildren.png index 6faedb1e2..ab927037d 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/32c-MultipleNotationChildren.png and b/packages/alphatab/test-data/musicxml-testsuite/32c-MultipleNotationChildren.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/32d-Arpeggio.png b/packages/alphatab/test-data/musicxml-testsuite/32d-Arpeggio.png index ceb8e9e6a..0fcb2cb12 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/32d-Arpeggio.png and b/packages/alphatab/test-data/musicxml-testsuite/32d-Arpeggio.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33a-Spanners.png b/packages/alphatab/test-data/musicxml-testsuite/33a-Spanners.png index cdd4635bb..52b778cc0 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33a-Spanners.png and b/packages/alphatab/test-data/musicxml-testsuite/33a-Spanners.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33b-Spanners-Tie.png b/packages/alphatab/test-data/musicxml-testsuite/33b-Spanners-Tie.png index 4830d8cbe..8962f5a1b 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33b-Spanners-Tie.png and b/packages/alphatab/test-data/musicxml-testsuite/33b-Spanners-Tie.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33c-Spanners-Slurs.png b/packages/alphatab/test-data/musicxml-testsuite/33c-Spanners-Slurs.png index ae317bb71..e79a157d3 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33c-Spanners-Slurs.png and b/packages/alphatab/test-data/musicxml-testsuite/33c-Spanners-Slurs.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.png b/packages/alphatab/test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.png index 00d902e61..5fa9e3a92 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.png and b/packages/alphatab/test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.png b/packages/alphatab/test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.png index 209f2e923..22497714f 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.png and b/packages/alphatab/test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.png b/packages/alphatab/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.png index f495de65b..5a8839501 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.png and b/packages/alphatab/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.png b/packages/alphatab/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.png index 00d9f085e..b5c660870 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.png and b/packages/alphatab/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.png b/packages/alphatab/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.png index b4f9e6de8..159e93b00 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.png and b/packages/alphatab/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33h-Spanners-Glissando.png b/packages/alphatab/test-data/musicxml-testsuite/33h-Spanners-Glissando.png index 837ebb940..7f4c838bb 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33h-Spanners-Glissando.png and b/packages/alphatab/test-data/musicxml-testsuite/33h-Spanners-Glissando.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33i-Ties-NotEnded.png b/packages/alphatab/test-data/musicxml-testsuite/33i-Ties-NotEnded.png index e01c6cdf4..7217ad6bd 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33i-Ties-NotEnded.png and b/packages/alphatab/test-data/musicxml-testsuite/33i-Ties-NotEnded.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/33j-Beams-Tremolos.png b/packages/alphatab/test-data/musicxml-testsuite/33j-Beams-Tremolos.png index d34870561..69f95779c 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/33j-Beams-Tremolos.png and b/packages/alphatab/test-data/musicxml-testsuite/33j-Beams-Tremolos.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/34a-Print-Object-Spanners.png b/packages/alphatab/test-data/musicxml-testsuite/34a-Print-Object-Spanners.png index 89c4d9744..77f508616 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/34a-Print-Object-Spanners.png and b/packages/alphatab/test-data/musicxml-testsuite/34a-Print-Object-Spanners.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/34b-Colors.png b/packages/alphatab/test-data/musicxml-testsuite/34b-Colors.png index 37fd78059..b351de729 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/34b-Colors.png and b/packages/alphatab/test-data/musicxml-testsuite/34b-Colors.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/34c-Font-Size.png b/packages/alphatab/test-data/musicxml-testsuite/34c-Font-Size.png index 2d094ffe6..0ccdef398 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/34c-Font-Size.png and b/packages/alphatab/test-data/musicxml-testsuite/34c-Font-Size.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41a-MultiParts-Partorder.png b/packages/alphatab/test-data/musicxml-testsuite/41a-MultiParts-Partorder.png index 21d5fd069..6571c5076 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41a-MultiParts-Partorder.png and b/packages/alphatab/test-data/musicxml-testsuite/41a-MultiParts-Partorder.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.png b/packages/alphatab/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.png index 14c007c8a..fcab0e37e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.png and b/packages/alphatab/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41c-StaffGroups.png b/packages/alphatab/test-data/musicxml-testsuite/41c-StaffGroups.png index 1d75dc51d..bb22d7fcd 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41c-StaffGroups.png and b/packages/alphatab/test-data/musicxml-testsuite/41c-StaffGroups.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41d-StaffGroups-Nested.png b/packages/alphatab/test-data/musicxml-testsuite/41d-StaffGroups-Nested.png index eb14bc1fb..2dc06d360 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41d-StaffGroups-Nested.png and b/packages/alphatab/test-data/musicxml-testsuite/41d-StaffGroups-Nested.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.png b/packages/alphatab/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.png index a80c4b85f..176497971 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.png and b/packages/alphatab/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.png b/packages/alphatab/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.png index be070c128..7e16f7b2b 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.png and b/packages/alphatab/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.png b/packages/alphatab/test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.png index 7ad2a6197..65e2f7336 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.png and b/packages/alphatab/test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41h-TooManyParts.png b/packages/alphatab/test-data/musicxml-testsuite/41h-TooManyParts.png index 5582709a9..124044ac9 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41h-TooManyParts.png and b/packages/alphatab/test-data/musicxml-testsuite/41h-TooManyParts.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.png b/packages/alphatab/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.png index 144417dbc..492fdfd03 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.png and b/packages/alphatab/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.png b/packages/alphatab/test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.png index 92fac2924..682530666 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.png and b/packages/alphatab/test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41k-PartName-Print.png b/packages/alphatab/test-data/musicxml-testsuite/41k-PartName-Print.png index c61d3c2b1..56fcdbdaa 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41k-PartName-Print.png and b/packages/alphatab/test-data/musicxml-testsuite/41k-PartName-Print.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.png b/packages/alphatab/test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.png index 0f773c40d..246d52c2b 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.png and b/packages/alphatab/test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.png b/packages/alphatab/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.png index 8b7fe1099..c7c77389f 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.png and b/packages/alphatab/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.png b/packages/alphatab/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.png index eeb577b9c..a1c930f91 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.png and b/packages/alphatab/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/43a-PianoStaff.png b/packages/alphatab/test-data/musicxml-testsuite/43a-PianoStaff.png index a13fabae1..55734931a 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/43a-PianoStaff.png and b/packages/alphatab/test-data/musicxml-testsuite/43a-PianoStaff.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.png b/packages/alphatab/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.png index f8b09a8ae..1404d3b64 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.png and b/packages/alphatab/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.png b/packages/alphatab/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.png index 315bc0699..adae8441e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.png and b/packages/alphatab/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.png b/packages/alphatab/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.png index 8d2d9bc56..d47e7d5c1 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.png and b/packages/alphatab/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.png b/packages/alphatab/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.png index 885f6f0f8..02611b7b5 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.png and b/packages/alphatab/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.png b/packages/alphatab/test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.png index 06d085304..6f21220a2 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.png and b/packages/alphatab/test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.png b/packages/alphatab/test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.png index 58aa79ff0..52cdf1584 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.png and b/packages/alphatab/test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/45a-SimpleRepeat.png b/packages/alphatab/test-data/musicxml-testsuite/45a-SimpleRepeat.png index db387e78f..d930e24b2 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/45a-SimpleRepeat.png and b/packages/alphatab/test-data/musicxml-testsuite/45a-SimpleRepeat.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.png b/packages/alphatab/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.png index 2bb6b1b9c..25f256c4e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.png and b/packages/alphatab/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.png b/packages/alphatab/test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.png index 99e42393e..fe851e8ff 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.png and b/packages/alphatab/test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.png b/packages/alphatab/test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.png index 42915e15f..b1745ac09 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.png and b/packages/alphatab/test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/45e-Repeats-Combination.png b/packages/alphatab/test-data/musicxml-testsuite/45e-Repeats-Combination.png index f42a630e7..5a77fdb96 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/45e-Repeats-Combination.png and b/packages/alphatab/test-data/musicxml-testsuite/45e-Repeats-Combination.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.png b/packages/alphatab/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.png index 4835c5adc..156599fba 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.png and b/packages/alphatab/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/45g-Repeats-NotEnded.png b/packages/alphatab/test-data/musicxml-testsuite/45g-Repeats-NotEnded.png index 67c508d8b..7c54e4df7 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/45g-Repeats-NotEnded.png and b/packages/alphatab/test-data/musicxml-testsuite/45g-Repeats-NotEnded.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/45h-Repeats-Partial.png b/packages/alphatab/test-data/musicxml-testsuite/45h-Repeats-Partial.png index 2affdd23d..b62ac1557 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/45h-Repeats-Partial.png and b/packages/alphatab/test-data/musicxml-testsuite/45h-Repeats-Partial.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/45i-Repeats-Nested.png b/packages/alphatab/test-data/musicxml-testsuite/45i-Repeats-Nested.png index 4eaa08856..665ad2a2f 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/45i-Repeats-Nested.png and b/packages/alphatab/test-data/musicxml-testsuite/45i-Repeats-Nested.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/46a-Barlines.png b/packages/alphatab/test-data/musicxml-testsuite/46a-Barlines.png index dc57b5139..7d6a264f2 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/46a-Barlines.png and b/packages/alphatab/test-data/musicxml-testsuite/46a-Barlines.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/46b-MidmeasureBarline.png b/packages/alphatab/test-data/musicxml-testsuite/46b-MidmeasureBarline.png index be6baa06d..43ba1be3e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/46b-MidmeasureBarline.png and b/packages/alphatab/test-data/musicxml-testsuite/46b-MidmeasureBarline.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/46c-Midmeasure-Clef.png b/packages/alphatab/test-data/musicxml-testsuite/46c-Midmeasure-Clef.png index f491c7088..dc9ce78f2 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/46c-Midmeasure-Clef.png and b/packages/alphatab/test-data/musicxml-testsuite/46c-Midmeasure-Clef.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.png b/packages/alphatab/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.png index 7368f7627..f69622f3e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.png and b/packages/alphatab/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.png b/packages/alphatab/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.png index f0bd179f2..596f67492 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.png and b/packages/alphatab/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/46f-IncompleteMeasures.png b/packages/alphatab/test-data/musicxml-testsuite/46f-IncompleteMeasures.png index 09d9f8349..61980ff8a 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/46f-IncompleteMeasures.png and b/packages/alphatab/test-data/musicxml-testsuite/46f-IncompleteMeasures.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.png b/packages/alphatab/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.png index e477f965c..08e19a3b3 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.png and b/packages/alphatab/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/51b-Header-Quotes.png b/packages/alphatab/test-data/musicxml-testsuite/51b-Header-Quotes.png index 7dca6d05c..5900bce6e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/51b-Header-Quotes.png and b/packages/alphatab/test-data/musicxml-testsuite/51b-Header-Quotes.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/51c-MultipleRights.png b/packages/alphatab/test-data/musicxml-testsuite/51c-MultipleRights.png index 262d65b4c..462b846a5 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/51c-MultipleRights.png and b/packages/alphatab/test-data/musicxml-testsuite/51c-MultipleRights.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/51d-EmptyTitle.png b/packages/alphatab/test-data/musicxml-testsuite/51d-EmptyTitle.png index 77f334f82..20b9bfdff 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/51d-EmptyTitle.png and b/packages/alphatab/test-data/musicxml-testsuite/51d-EmptyTitle.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/52a-PageLayout.png b/packages/alphatab/test-data/musicxml-testsuite/52a-PageLayout.png index d1ac67bae..008a52a86 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/52a-PageLayout.png and b/packages/alphatab/test-data/musicxml-testsuite/52a-PageLayout.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/52b-Breaks.png b/packages/alphatab/test-data/musicxml-testsuite/52b-Breaks.png index f28737b74..718db35e5 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/52b-Breaks.png and b/packages/alphatab/test-data/musicxml-testsuite/52b-Breaks.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61a-Lyrics.png b/packages/alphatab/test-data/musicxml-testsuite/61a-Lyrics.png index baa7423d9..06929b92f 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61a-Lyrics.png and b/packages/alphatab/test-data/musicxml-testsuite/61a-Lyrics.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61b-MultipleLyrics.png b/packages/alphatab/test-data/musicxml-testsuite/61b-MultipleLyrics.png index 6f4e31127..1df1f587a 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61b-MultipleLyrics.png and b/packages/alphatab/test-data/musicxml-testsuite/61b-MultipleLyrics.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.png b/packages/alphatab/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.png index e1012f08a..141b3854b 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.png and b/packages/alphatab/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61d-Lyrics-Melisma.png b/packages/alphatab/test-data/musicxml-testsuite/61d-Lyrics-Melisma.png index 26422081b..04387f1d7 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61d-Lyrics-Melisma.png and b/packages/alphatab/test-data/musicxml-testsuite/61d-Lyrics-Melisma.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61e-Lyrics-Chords.png b/packages/alphatab/test-data/musicxml-testsuite/61e-Lyrics-Chords.png index 605ac405f..c29c5f986 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61e-Lyrics-Chords.png and b/packages/alphatab/test-data/musicxml-testsuite/61e-Lyrics-Chords.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.png b/packages/alphatab/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.png index af10cfc66..e759ea2b1 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.png and b/packages/alphatab/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.png b/packages/alphatab/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.png index d640b31d3..02d30df63 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.png and b/packages/alphatab/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.png b/packages/alphatab/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.png index 02ccd4ee4..e7487ec90 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.png and b/packages/alphatab/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61i-Lyrics-Chords.png b/packages/alphatab/test-data/musicxml-testsuite/61i-Lyrics-Chords.png index 32ff82dd6..0329cba0e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61i-Lyrics-Chords.png and b/packages/alphatab/test-data/musicxml-testsuite/61i-Lyrics-Chords.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61j-Lyrics-Elisions.png b/packages/alphatab/test-data/musicxml-testsuite/61j-Lyrics-Elisions.png index bfd7baf83..af978c8fa 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61j-Lyrics-Elisions.png and b/packages/alphatab/test-data/musicxml-testsuite/61j-Lyrics-Elisions.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.png b/packages/alphatab/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.png index f2a04f234..b4b22b841 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.png and b/packages/alphatab/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/71a-Chordnames.png b/packages/alphatab/test-data/musicxml-testsuite/71a-Chordnames.png index 4978401d1..718e45edc 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/71a-Chordnames.png and b/packages/alphatab/test-data/musicxml-testsuite/71a-Chordnames.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/71c-ChordsFrets.png b/packages/alphatab/test-data/musicxml-testsuite/71c-ChordsFrets.png index 0a23d88a9..ed0f43372 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/71c-ChordsFrets.png and b/packages/alphatab/test-data/musicxml-testsuite/71c-ChordsFrets.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.png b/packages/alphatab/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.png index 05f7b50c2..1838f22b7 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.png and b/packages/alphatab/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/71e-TabStaves.png b/packages/alphatab/test-data/musicxml-testsuite/71e-TabStaves.png index c0b2aca9e..dc8cd4760 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/71e-TabStaves.png and b/packages/alphatab/test-data/musicxml-testsuite/71e-TabStaves.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/71f-AllChordTypes.png b/packages/alphatab/test-data/musicxml-testsuite/71f-AllChordTypes.png index d48a325c5..2f0595d96 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/71f-AllChordTypes.png and b/packages/alphatab/test-data/musicxml-testsuite/71f-AllChordTypes.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/71g-MultipleChordnames.png b/packages/alphatab/test-data/musicxml-testsuite/71g-MultipleChordnames.png index fd14f2380..ceaa94f50 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/71g-MultipleChordnames.png and b/packages/alphatab/test-data/musicxml-testsuite/71g-MultipleChordnames.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/72a-TransposingInstruments.png b/packages/alphatab/test-data/musicxml-testsuite/72a-TransposingInstruments.png index 005c2d512..6719a0ff3 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/72a-TransposingInstruments.png and b/packages/alphatab/test-data/musicxml-testsuite/72a-TransposingInstruments.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png b/packages/alphatab/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png index aee77d520..4300fb626 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png and b/packages/alphatab/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/72c-TransposingInstruments-Change.png b/packages/alphatab/test-data/musicxml-testsuite/72c-TransposingInstruments-Change.png index 74bfb56e0..5022744dd 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/72c-TransposingInstruments-Change.png and b/packages/alphatab/test-data/musicxml-testsuite/72c-TransposingInstruments-Change.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/73a-Percussion.png b/packages/alphatab/test-data/musicxml-testsuite/73a-Percussion.png index a2243226d..b485009b6 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/73a-Percussion.png and b/packages/alphatab/test-data/musicxml-testsuite/73a-Percussion.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/74a-FiguredBass.png b/packages/alphatab/test-data/musicxml-testsuite/74a-FiguredBass.png index 69e716ba5..a5e18d234 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/74a-FiguredBass.png and b/packages/alphatab/test-data/musicxml-testsuite/74a-FiguredBass.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/75a-AccordionRegistrations.png b/packages/alphatab/test-data/musicxml-testsuite/75a-AccordionRegistrations.png index a275cea50..bdb71a216 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/75a-AccordionRegistrations.png and b/packages/alphatab/test-data/musicxml-testsuite/75a-AccordionRegistrations.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png b/packages/alphatab/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png index 7333d4a79..6f01f088e 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png and b/packages/alphatab/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.png b/packages/alphatab/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.png index 93f742df9..3ca1abf65 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.png and b/packages/alphatab/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.png b/packages/alphatab/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.png index 02ccd4ee4..e7487ec90 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.png and b/packages/alphatab/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.png differ diff --git a/packages/alphatab/test-data/musicxml3/chord-diagram.png b/packages/alphatab/test-data/musicxml3/chord-diagram.png index 83bca5d5b..23edfb56b 100644 Binary files a/packages/alphatab/test-data/musicxml3/chord-diagram.png and b/packages/alphatab/test-data/musicxml3/chord-diagram.png differ diff --git a/packages/alphatab/test-data/musicxml3/compressed.png b/packages/alphatab/test-data/musicxml3/compressed.png index 6a92ec004..f5b1328c9 100644 Binary files a/packages/alphatab/test-data/musicxml3/compressed.png and b/packages/alphatab/test-data/musicxml3/compressed.png differ diff --git a/packages/alphatab/test-data/musicxml3/first-bar-tempo.png b/packages/alphatab/test-data/musicxml3/first-bar-tempo.png index dde2bba6f..6a4ec528a 100644 Binary files a/packages/alphatab/test-data/musicxml3/first-bar-tempo.png and b/packages/alphatab/test-data/musicxml3/first-bar-tempo.png differ diff --git a/packages/alphatab/test-data/musicxml3/full-bar-rest.png b/packages/alphatab/test-data/musicxml3/full-bar-rest.png index e12c5e748..3b582bb42 100644 Binary files a/packages/alphatab/test-data/musicxml3/full-bar-rest.png and b/packages/alphatab/test-data/musicxml3/full-bar-rest.png differ diff --git a/packages/alphatab/test-data/musicxml3/tie-destination.png b/packages/alphatab/test-data/musicxml3/tie-destination.png index 67c1f0827..0a7d6c204 100644 Binary files a/packages/alphatab/test-data/musicxml3/tie-destination.png and b/packages/alphatab/test-data/musicxml3/tie-destination.png differ diff --git a/packages/alphatab/test-data/musicxml3/track-volume-balance.png b/packages/alphatab/test-data/musicxml3/track-volume-balance.png index dbfdb69e2..359f54906 100644 Binary files a/packages/alphatab/test-data/musicxml3/track-volume-balance.png and b/packages/alphatab/test-data/musicxml3/track-volume-balance.png differ diff --git a/packages/alphatab/test-data/musicxml4/bends.png b/packages/alphatab/test-data/musicxml4/bends.png index fa839742e..ac7371833 100644 Binary files a/packages/alphatab/test-data/musicxml4/bends.png and b/packages/alphatab/test-data/musicxml4/bends.png differ diff --git a/packages/alphatab/test-data/musicxml4/buzzroll.xml b/packages/alphatab/test-data/musicxml4/buzzroll.xml new file mode 100644 index 000000000..bacae7d3f --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/buzzroll.xml @@ -0,0 +1,106 @@ + + + + Title + + + + + Words + Music + Artist + Copyright + + Tab + Notices + + + + + Track 1 + T1 + + + I1 + + + 1 + 1 + 1 + 0.5 + 0.25 + + + + + + + 960 + + + G + 2 + + + + + C + 4 + + 960 + 1 + quarter + + + 0 + + + + + + C + 4 + + 960 + 1 + quarter + + + 1 + + + + + + C + 4 + + 960 + 1 + quarter + + + 0 + + + + + + C + 4 + + 960 + 1 + quarter + + + 1 + + + + + + \ No newline at end of file diff --git a/packages/alphatab/test-data/musicxml4/measure-numbering-none.xml b/packages/alphatab/test-data/musicxml4/measure-numbering-none.xml new file mode 100644 index 000000000..8005df275 --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/measure-numbering-none.xml @@ -0,0 +1,86 @@ + + + + Title + + + + + Words + Music + Artist + Copyright + + Tab + Notices + + + + + Track 1 + T1 + + + I1 + + + 1 + 1 + 0.5 + 0.25 + + + + Track 2 + T2 + + + I2 + + + 2 + 2 + 0.25 + 0.5 + + + + Track 3 + T3 + + + I3 + + + 3 + 3 + 0.25 + 0.25 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/alphatab/test-data/musicxml4/partwise-measure-numbering-measure.xml b/packages/alphatab/test-data/musicxml4/partwise-measure-numbering-measure.xml new file mode 100644 index 000000000..1e1b18b29 --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/partwise-measure-numbering-measure.xml @@ -0,0 +1,70 @@ + + + + Title + + + + + Words + Music + Artist + Copyright + + Tab + Notices + + + + + Track 1 + T1 + + + I1 + + + 1 + 1 + 0.5 + 0.25 + + + + Track 2 + T2 + + + I2 + + + 2 + 2 + 0.25 + 0.5 + + + + + + + + + measure + + + + + + + + + + + + + measure + + + + \ No newline at end of file diff --git a/packages/alphatab/test-data/musicxml4/partwise-measure-numbering-none.xml b/packages/alphatab/test-data/musicxml4/partwise-measure-numbering-none.xml new file mode 100644 index 000000000..37073bc03 --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/partwise-measure-numbering-none.xml @@ -0,0 +1,70 @@ + + + + Title + + + + + Words + Music + Artist + Copyright + + Tab + Notices + + + + + Track 1 + T1 + + + I1 + + + 1 + 1 + 0.5 + 0.25 + + + + Track 2 + T2 + + + I2 + + + 2 + 2 + 0.25 + 0.5 + + + + + + + + + none + + + + + + + + + + + + + none + + + + \ No newline at end of file diff --git a/packages/alphatab/test-data/musicxml4/partwise-measure-numbering-system.xml b/packages/alphatab/test-data/musicxml4/partwise-measure-numbering-system.xml new file mode 100644 index 000000000..d38330b84 --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/partwise-measure-numbering-system.xml @@ -0,0 +1,70 @@ + + + + Title + + + + + Words + Music + Artist + Copyright + + Tab + Notices + + + + + Track 1 + T1 + + + I1 + + + 1 + 1 + 0.5 + 0.25 + + + + Track 2 + T2 + + + I2 + + + 2 + 2 + 0.25 + 0.5 + + + + + + + + + system + + + + + + + + + + + + + system + + + + \ No newline at end of file diff --git a/packages/alphatab/test-data/musicxml4/timewise-measure-numbering-measure.xml b/packages/alphatab/test-data/musicxml4/timewise-measure-numbering-measure.xml new file mode 100644 index 000000000..65c26411d --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/timewise-measure-numbering-measure.xml @@ -0,0 +1,63 @@ + + + + Title + + + + + Words + Music + Artist + Copyright + + Tab + Notices + + + + + Track 1 + T1 + + + I1 + + + 1 + 1 + 0.5 + 0.25 + + + + Track 2 + T2 + + + I2 + + + 2 + 2 + 0.25 + 0.5 + + + + + + + + + + measure + + + + + + + + + \ No newline at end of file diff --git a/packages/alphatab/test-data/musicxml4/timewise-measure-numbering-none.xml b/packages/alphatab/test-data/musicxml4/timewise-measure-numbering-none.xml new file mode 100644 index 000000000..65381f8a4 --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/timewise-measure-numbering-none.xml @@ -0,0 +1,63 @@ + + + + Title + + + + + Words + Music + Artist + Copyright + + Tab + Notices + + + + + Track 1 + T1 + + + I1 + + + 1 + 1 + 0.5 + 0.25 + + + + Track 2 + T2 + + + I2 + + + 2 + 2 + 0.25 + 0.5 + + + + + + + + + + none + + + + + + + + + \ No newline at end of file diff --git a/packages/alphatab/test-data/musicxml4/timewise-measure-numbering-system.xml b/packages/alphatab/test-data/musicxml4/timewise-measure-numbering-system.xml new file mode 100644 index 000000000..8eca9e439 --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/timewise-measure-numbering-system.xml @@ -0,0 +1,63 @@ + + + + Title + + + + + Words + Music + Artist + Copyright + + Tab + Notices + + + + + Track 1 + T1 + + + I1 + + + 1 + 1 + 0.5 + 0.25 + + + + Track 2 + T2 + + + I2 + + + 2 + 2 + 0.25 + 0.5 + + + + + + + + + + system + + + + + + + + + \ No newline at end of file diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png index 7dc88f97e..d954bad7a 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png index 8129f63e3..b30dd8f43 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png index d38fb2427..aa003278c 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png index 958834176..d3d5ba19b 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png index 5f262497b..b3d74478c 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png index d7b1f266c..52d672911 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png index 2611acb7f..b30dd8f43 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png index a64965d37..229ddeb42 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png index 1bb825cb1..ca7495334 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png index 5f262497b..b3d74478c 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png index b036ad443..ed849802e 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/accentuations.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/accentuations.png index 51e00d396..50982bf6d 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/accentuations.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/accentuations.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/barre.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/barre.png index 6a061cf36..bda15e1a7 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/barre.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/barre.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-slash.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-slash.png index 185d42ad8..bffe3d1dd 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-slash.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-slash.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-tempo-change.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-tempo-change.png index 1e382b9fe..e385c1855 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-tempo-change.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/beat-tempo-change.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png index 2591bdd74..0075f08f0 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png index f4d84d7c3..8ee181679 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/bends.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/bends.png index 5941d4221..1eb149445 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/bends.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/bends.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/brush.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/brush.png index e1ff013ba..a1369016e 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/brush.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/brush.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/chords-duplicates.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/chords-duplicates.png index 04b54fe54..561df3ac6 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/chords-duplicates.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/chords-duplicates.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/chords.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/chords.png index 802a67588..c2ddb1693 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/chords.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/chords.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/dead-slap.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/dead-slap.png index 789dfe139..4bdc32a6c 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/dead-slap.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/dead-slap.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/directions-simple.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/directions-simple.png index 359e35f24..1a06eab56 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/directions-simple.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/directions-simple.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/directions-symbols.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/directions-symbols.png index e1377f5e7..9e942b3d1 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/directions-symbols.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/directions-symbols.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/dynamics.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/dynamics.png index 626dc99e0..b8a5c38ba 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/dynamics.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/dynamics.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/fade-in.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/fade-in.png index 3e82abee3..fea6e207f 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/fade-in.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/fade-in.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/fade.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/fade.png index bfa1b1722..0c3e27c21 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/fade.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/fade.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/fingering-new.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/fingering-new.png index bfd9c3c68..80a16bef1 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/fingering-new.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/fingering-new.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/fingering.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/fingering.png index 9f525ee40..30c71bffb 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/fingering.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/fingering.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/free-time.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/free-time.png index ffdacc33e..e5abe0171 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/free-time.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/free-time.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/golpe-tab.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/golpe-tab.png index 52457e9a5..1b55d4467 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/golpe-tab.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/golpe-tab.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/golpe.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/golpe.png index 6503a1566..021e6d961 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/golpe.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/golpe.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/inscore-chord-diagrams.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/inscore-chord-diagrams.png new file mode 100644 index 000000000..c07a31dca Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/inscore-chord-diagrams.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/legato.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/legato.png index b6a704780..2e2af5813 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/legato.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/legato.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/let-ring.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/let-ring.png index 92187af68..1478d0738 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/let-ring.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/let-ring.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-multi-line-spacing.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-multi-line-spacing.png new file mode 100644 index 000000000..c479015fa Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-multi-line-spacing.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-multi-line.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-multi-line.png new file mode 100644 index 000000000..25cf7c251 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-multi-line.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-single-line.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-single-line.png new file mode 100644 index 000000000..a03f57cbc Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-single-line.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/markers.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/markers.png index 5905ba9cf..20b4fc51d 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/markers.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/markers.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/ornaments.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/ornaments.png index fd49e6e49..f97bb128f 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/ornaments.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/ornaments.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/palm-mute.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/palm-mute.png index 83d83b419..f8f3f6add 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/palm-mute.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/palm-mute.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/pick-stroke.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/pick-stroke.png index 5ecd93342..a225223f5 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/pick-stroke.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/pick-stroke.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/rasgueado.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/rasgueado.png index 1e533455d..6f43748a4 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/rasgueado.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/rasgueado.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides-line-break.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides-line-break.png index 7133817e3..3dad7f512 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides-line-break.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides-line-break.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides.png index 72f20b891..9cdf27e2c 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/string-numbers.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/string-numbers.png index 6b15fdef3..8dff522bd 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/string-numbers.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/string-numbers.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-1200.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-1200.png index 1bb4010e2..045603b39 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-1200.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-1200.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-600.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-600.png index fedf0c43d..a70d1f008 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-600.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-600.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-850.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-850.png index 412fc48e7..b99f11517 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-850.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/sustain-850.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tap.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tap.png index b6064bb5b..967418929 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tap.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tap.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tempo-text.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tempo-text.png index 94b635213..5612d41a3 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tempo-text.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tempo-text.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tempo.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tempo.png index c3a7caba3..1ef85f6dd 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tempo.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tempo.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/text.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/text.png index 6d9c16dc4..8507beb7d 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/text.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/text.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/timer.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/timer.png index fb38cc5cd..feaefa6e7 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/timer.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/timer.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-bar.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-bar.png index eb281a8a0..0f17d8faa 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-bar.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-bar.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-flags-bottom.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-flags-bottom.png new file mode 100644 index 000000000..9eba6dd37 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-flags-bottom.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-flags-mixed.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-flags-mixed.png new file mode 100644 index 000000000..2161e681b Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-flags-mixed.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-flags.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-flags.png new file mode 100644 index 000000000..12a921622 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-flags.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-picking.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-picking.png index 66eef4ad2..6a502019c 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-picking.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-picking.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-beams.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-beams.png new file mode 100644 index 000000000..82b310789 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-beams.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-flags.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-flags.png new file mode 100644 index 000000000..971aab93c Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-flags.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-beams.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-beams.png new file mode 100644 index 000000000..79ecd39d9 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-beams.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-flags.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-flags.png new file mode 100644 index 000000000..86f8e7746 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-flags.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-beams.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-beams.png new file mode 100644 index 000000000..d866dc4dd Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-beams.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-flags.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-flags.png new file mode 100644 index 000000000..9f20f3cc9 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-flags.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-beams.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-beams.png new file mode 100644 index 000000000..7a3f2259a Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-beams.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-flags.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-flags.png new file mode 100644 index 000000000..2d478d018 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-flags.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/trill.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/trill.png index a1ad236ab..3fa25f22a 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/trill.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/trill.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/triplet-feel.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/triplet-feel.png index 5b372a122..86be917dc 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/triplet-feel.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/triplet-feel.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets-advanced.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets-advanced.png index c4eaf4adc..0efde3fcc 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets-advanced.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets-advanced.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets.png index 58974a8f8..cb388f859 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tuplets.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/vibrato.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/vibrato.png index 77a3a1f47..ba9e9d670 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/vibrato.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/vibrato.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/alternate-endings.png b/packages/alphatab/test-data/visual-tests/general/alternate-endings.png index 2fdc6ad8e..60eff996a 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/alternate-endings.png and b/packages/alphatab/test-data/visual-tests/general/alternate-endings.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/colors-disabled.png b/packages/alphatab/test-data/visual-tests/general/colors-disabled.png index e30c3b38d..3ac69a4f9 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/colors-disabled.png and b/packages/alphatab/test-data/visual-tests/general/colors-disabled.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/colors.png b/packages/alphatab/test-data/visual-tests/general/colors.png index 4f54dea24..50fe1ab8f 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/colors.png and b/packages/alphatab/test-data/visual-tests/general/colors.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/font-fallback.png b/packages/alphatab/test-data/visual-tests/general/font-fallback.png index e1f8c5f5a..397d799d5 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/font-fallback.png and b/packages/alphatab/test-data/visual-tests/general/font-fallback.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/repeats.png b/packages/alphatab/test-data/visual-tests/general/repeats.png index c1ce42cc7..5acfcd73a 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/repeats.png and b/packages/alphatab/test-data/visual-tests/general/repeats.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/song-details.png b/packages/alphatab/test-data/visual-tests/general/song-details.png index 92c4886b1..adda44fd1 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/song-details.png and b/packages/alphatab/test-data/visual-tests/general/song-details.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/tuning.png b/packages/alphatab/test-data/visual-tests/general/tuning.png index eee60925d..aff4f080a 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/tuning.png and b/packages/alphatab/test-data/visual-tests/general/tuning.png differ diff --git a/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm-with-beams.png b/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm-with-beams.png index 6711ebcde..cec2bae48 100644 Binary files a/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm-with-beams.png and b/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm-with-beams.png differ diff --git a/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm.png b/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm.png index 87e288b15..fa37ac2fc 100644 Binary files a/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm.png and b/packages/alphatab/test-data/visual-tests/guitar-tabs/rhythm.png differ diff --git a/packages/alphatab/test-data/visual-tests/guitar-tabs/string-variations.png b/packages/alphatab/test-data/visual-tests/guitar-tabs/string-variations.png index 81c29d1f8..60a6b6cf3 100644 Binary files a/packages/alphatab/test-data/visual-tests/guitar-tabs/string-variations.png and b/packages/alphatab/test-data/visual-tests/guitar-tabs/string-variations.png differ diff --git a/packages/alphatab/test-data/visual-tests/issues/bottom-effect-band.png b/packages/alphatab/test-data/visual-tests/issues/bottom-effect-band.png new file mode 100644 index 000000000..4f784b16c Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/issues/bottom-effect-band.png differ diff --git a/packages/alphatab/test-data/visual-tests/issues/let-ring-empty-voice.png b/packages/alphatab/test-data/visual-tests/issues/let-ring-empty-voice.png index b4882deea..f9a8adc89 100644 Binary files a/packages/alphatab/test-data/visual-tests/issues/let-ring-empty-voice.png and b/packages/alphatab/test-data/visual-tests/issues/let-ring-empty-voice.png differ diff --git a/packages/alphatab/test-data/visual-tests/issues/no-label-padding-left-no-padding.png b/packages/alphatab/test-data/visual-tests/issues/no-label-padding-left-no-padding.png new file mode 100644 index 000000000..470a0a7ab Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/issues/no-label-padding-left-no-padding.png differ diff --git a/packages/alphatab/test-data/visual-tests/issues/no-label-padding-left-with-label.png b/packages/alphatab/test-data/visual-tests/issues/no-label-padding-left-with-label.png new file mode 100644 index 000000000..eb375c924 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/issues/no-label-padding-left-with-label.png differ diff --git a/packages/alphatab/test-data/visual-tests/issues/no-label-padding-left-without-label.png b/packages/alphatab/test-data/visual-tests/issues/no-label-padding-left-without-label.png new file mode 100644 index 000000000..973c99c42 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/issues/no-label-padding-left-without-label.png differ diff --git a/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-380.png b/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-380.png new file mode 100644 index 000000000..720fc6f68 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-380.png differ diff --git a/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-400.png b/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-400.png new file mode 100644 index 000000000..2b83525dc Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-400.png differ diff --git a/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-500.png b/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-500.png new file mode 100644 index 000000000..074687891 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-500.png differ diff --git a/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-600.png b/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-600.png new file mode 100644 index 000000000..ed3765c14 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/issues/whammy-resize-wrap-600.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-bar-override-all.png b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-bar-override-all.png new file mode 100644 index 000000000..cf361eaff Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-bar-override-all.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-bar-override-first.png b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-bar-override-first.png new file mode 100644 index 000000000..9ed05856f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-bar-override-first.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-bar-override-hide.png b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-bar-override-hide.png new file mode 100644 index 000000000..ef76abb22 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-bar-override-hide.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-stylesheet-all.png b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-stylesheet-all.png new file mode 100644 index 000000000..2b56614e9 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-stylesheet-all.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-stylesheet-first.png b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-stylesheet-first.png new file mode 100644 index 000000000..3ef37bd65 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-stylesheet-first.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-stylesheet-hide.png b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-stylesheet-hide.png new file mode 100644 index 000000000..4489daebb Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/barnumberdisplay-stylesheet-hide.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/brackets-braces-none.png b/packages/alphatab/test-data/visual-tests/layout/brackets-braces-none.png index 5a89dbf31..ecbd87a60 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/brackets-braces-none.png and b/packages/alphatab/test-data/visual-tests/layout/brackets-braces-none.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/brackets-braces-similar.png b/packages/alphatab/test-data/visual-tests/layout/brackets-braces-similar.png index 7fe04e26a..11a80853d 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/brackets-braces-similar.png and b/packages/alphatab/test-data/visual-tests/layout/brackets-braces-similar.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/brackets-braces-staves.png b/packages/alphatab/test-data/visual-tests/layout/brackets-braces-staves.png index 1ed634ee4..658d5ba81 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/brackets-braces-staves.png and b/packages/alphatab/test-data/visual-tests/layout/brackets-braces-staves.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/extended-barlines.png b/packages/alphatab/test-data/visual-tests/layout/extended-barlines.png new file mode 100644 index 000000000..28f63760a Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/extended-barlines.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/extended-barlines.xml b/packages/alphatab/test-data/visual-tests/layout/extended-barlines.xml new file mode 100644 index 000000000..64bc58404 --- /dev/null +++ b/packages/alphatab/test-data/visual-tests/layout/extended-barlines.xml @@ -0,0 +1,265 @@ + + + + + + Two properly nested part groups: + One group (with a bracket) goes from staff 2 to 4, and another group + (with a brace) goes from staff 3 to 4. + + + + + + + + + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + dashed + + + dotted + + + + + dashed + + + heavy-heavy + + + + + heavy-heavy + + + heavy-heavy + + + + + dashed + + + regular + + + + + dashed + + + dashed + + + + + regular + + + + + + dashed + + + + + dotted + + + + + heavy + + + + + heavy-heavy + + + + + heavy-light + + + + + light-heavy + + + + + light-light + + + + + none + + + + + regular + + + + + short + + + + + tick + + + + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + dashed + + + dotted + + + + + dashed + + + heavy-heavy + + + + + heavy-heavy + + + heavy-heavy + + + + + dashed + + + regular + + + + + dashed + + + dashed + + + + + regular + + + + + + dashed + + + + + dotted + + + + + heavy + + + + + heavy-heavy + + + + + heavy-light + + + + + light-heavy + + + + + light-light + + + + + none + + + + + regular + + + + + short + + + + + tick + + + + + + + diff --git a/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves-in-first.png b/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves-in-first.png new file mode 100644 index 000000000..602e12303 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves-in-first.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves.png b/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves.png new file mode 100644 index 000000000..abe0f156a Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/hide-empty-staves.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/horizontal-layout-5to8.png b/packages/alphatab/test-data/visual-tests/layout/horizontal-layout-5to8.png index 4767b53f1..64d6528c2 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/horizontal-layout-5to8.png and b/packages/alphatab/test-data/visual-tests/layout/horizontal-layout-5to8.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/horizontal-layout.png b/packages/alphatab/test-data/visual-tests/layout/horizontal-layout.png index afadfd28a..7237389d2 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/horizontal-layout.png and b/packages/alphatab/test-data/visual-tests/layout/horizontal-layout.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-0-1300.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-0-1300.png new file mode 100644 index 000000000..0b8712152 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-0-1300.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-1-600.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-1-600.png new file mode 100644 index 000000000..aafe16457 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-1-600.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-2-300.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-2-300.png new file mode 100644 index 000000000..ba8acf74f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-2-300.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-3-700.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-3-700.png new file mode 100644 index 000000000..169e4ecc7 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-down-3-700.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-0-600.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-0-600.png new file mode 100644 index 000000000..a3f77c935 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-0-600.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-1-1300.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-1-1300.png new file mode 100644 index 000000000..b07c2fa24 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-1-1300.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-2-700.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-2-700.png new file mode 100644 index 000000000..7580008c8 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-2-700.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-3-300.png b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-3-300.png new file mode 100644 index 000000000..ba8acf74f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/multi-system-slur-scale-up-3-300.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-track.png b/packages/alphatab/test-data/visual-tests/layout/multi-track.png index ff1319524..7ffc82ba3 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/multi-track.png and b/packages/alphatab/test-data/visual-tests/layout/multi-track.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-voice.gp b/packages/alphatab/test-data/visual-tests/layout/multi-voice.gp index 7fd76b1c7..723149b48 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/multi-voice.gp and b/packages/alphatab/test-data/visual-tests/layout/multi-voice.gp differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-voice.png b/packages/alphatab/test-data/visual-tests/layout/multi-voice.png index 4a9fb84c4..3aaeb2d84 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/multi-voice.png and b/packages/alphatab/test-data/visual-tests/layout/multi-voice.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multibar-rest-all-tracks.png b/packages/alphatab/test-data/visual-tests/layout/multibar-rest-all-tracks.png index cd18e53c2..1f43c818a 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/multibar-rest-all-tracks.png and b/packages/alphatab/test-data/visual-tests/layout/multibar-rest-all-tracks.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multibar-rest-multi-track.png b/packages/alphatab/test-data/visual-tests/layout/multibar-rest-multi-track.png index bab7a60f4..ed5eae90c 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/multibar-rest-multi-track.png and b/packages/alphatab/test-data/visual-tests/layout/multibar-rest-multi-track.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multibar-rest-single-track.png b/packages/alphatab/test-data/visual-tests/layout/multibar-rest-single-track.png index 4df9445ff..e593e2369 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/multibar-rest-single-track.png and b/packages/alphatab/test-data/visual-tests/layout/multibar-rest-single-track.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/page-layout-5barsperrow.png b/packages/alphatab/test-data/visual-tests/layout/page-layout-5barsperrow.png index d8c3bb507..9637543c5 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/page-layout-5barsperrow.png and b/packages/alphatab/test-data/visual-tests/layout/page-layout-5barsperrow.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/page-layout-5to8.png b/packages/alphatab/test-data/visual-tests/layout/page-layout-5to8.png index c391ebba1..73bd1ca80 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/page-layout-5to8.png and b/packages/alphatab/test-data/visual-tests/layout/page-layout-5to8.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/page-layout-justify-last-row.png b/packages/alphatab/test-data/visual-tests/layout/page-layout-justify-last-row.png index 8cc7c2a08..877aee4be 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/page-layout-justify-last-row.png and b/packages/alphatab/test-data/visual-tests/layout/page-layout-justify-last-row.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/page-layout.png b/packages/alphatab/test-data/visual-tests/layout/page-layout.png index be35ceb34..960895914 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/page-layout.png and b/packages/alphatab/test-data/visual-tests/layout/page-layout.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-hide.png b/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-hide.png new file mode 100644 index 000000000..71681590e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-hide.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-show.png b/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-show.png new file mode 100644 index 000000000..2d14ecc48 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/layout/single-staff-brackets-show.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/system-divider.gp b/packages/alphatab/test-data/visual-tests/layout/system-divider.gp index 7ad66d594..bdd6848c8 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/system-divider.gp and b/packages/alphatab/test-data/visual-tests/layout/system-divider.gp differ diff --git a/packages/alphatab/test-data/visual-tests/layout/system-divider.png b/packages/alphatab/test-data/visual-tests/layout/system-divider.png index bdfa17e1f..aca5bc071 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/system-divider.png and b/packages/alphatab/test-data/visual-tests/layout/system-divider.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/system-layout-tex.png b/packages/alphatab/test-data/visual-tests/layout/system-layout-tex.png index 4d8f52e29..e6ce159e0 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/system-layout-tex.png and b/packages/alphatab/test-data/visual-tests/layout/system-layout-tex.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/track-names-all-systems-multi.png b/packages/alphatab/test-data/visual-tests/layout/track-names-all-systems-multi.png index c9cc13bea..ab41b1fa0 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/track-names-all-systems-multi.png and b/packages/alphatab/test-data/visual-tests/layout/track-names-all-systems-multi.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/track-names-first-system.png b/packages/alphatab/test-data/visual-tests/layout/track-names-first-system.png index 93cfb0fd6..741d2c7a6 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/track-names-first-system.png and b/packages/alphatab/test-data/visual-tests/layout/track-names-first-system.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-all.png b/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-all.png index beaa671f5..d7636b6d0 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-all.png and b/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-all.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-horizontal.png b/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-horizontal.png index 4c1205c75..5fb36e636 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-horizontal.png and b/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-horizontal.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-short-name.png b/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-short-name.png index e770163c7..5eade17a4 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-short-name.png and b/packages/alphatab/test-data/visual-tests/layout/track-names-full-name-short-name.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Chord-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Chord-Automatic-Stem.png new file mode 100644 index 000000000..bcfcafa47 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Chord-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Single-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Single-Automatic-Stem.png new file mode 100644 index 000000000..75a913c05 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Chord-v2_Eighth_Single-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Single-v2_Eighth_Single-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Single-v2_Eighth_Single-Automatic-Stem.png new file mode 100644 index 000000000..effa4ac1e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_16th_Single-v2_Eighth_Single-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Automatic_Stem.png new file mode 100644 index 000000000..fa42aca75 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Reversed_Stem.png new file mode 100644 index 000000000..d2f101100 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Same_Stem.png new file mode 100644 index 000000000..3c6c9a4bb Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Automatic_Stem.png new file mode 100644 index 000000000..3253d8bb6 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Reversed_Stem.png new file mode 100644 index 000000000..62c633406 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Same_Stem.png new file mode 100644 index 000000000..316683da2 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-V2_8th_Flag_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Automatic_Stem.png new file mode 100644 index 000000000..0f00ac13d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Reversed_Stem.png new file mode 100644 index 000000000..b27c76911 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Same_Stem.png new file mode 100644 index 000000000..93c35c8f7 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Automatic_Stem.png new file mode 100644 index 000000000..fc01b2656 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Reversed_Stem.png new file mode 100644 index 000000000..2eae959b1 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Same_Stem.png new file mode 100644 index 000000000..7fe5fa57e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Half_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Automatic_Stem.png new file mode 100644 index 000000000..acdd9406c Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Reversed_Stem.png new file mode 100644 index 000000000..6ac3747b2 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Same_Stem.png new file mode 100644 index 000000000..d306a10b2 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Automatic_Stem.png new file mode 100644 index 000000000..75a86f26a Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Reversed_Stem.png new file mode 100644 index 000000000..a3de52243 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Same_Stem.png new file mode 100644 index 000000000..09ce2df6d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Chord-v2_Quarter_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Automatic_Stem.png new file mode 100644 index 000000000..63c458fdb Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Reversed_Stem.png new file mode 100644 index 000000000..f8cc5c46e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Same_Stem.png new file mode 100644 index 000000000..581dd2379 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_8th_Flag_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Automatic_Stem.png new file mode 100644 index 000000000..7b928e662 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Reversed_Stem.png new file mode 100644 index 000000000..50ad5fce7 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Same_Stem.png new file mode 100644 index 000000000..8491110ed Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Half_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Automatic_Stem.png new file mode 100644 index 000000000..5b29e2771 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Reversed_Stem.png new file mode 100644 index 000000000..4b261d4be Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Same_Stem.png new file mode 100644 index 000000000..98701b35f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_8th_Flag_Single-v2_Quarter_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Chord-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Chord-Automatic-Stem.png new file mode 100644 index 000000000..b1349f8b8 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Chord-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Single-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Single-Automatic-Stem.png new file mode 100644 index 000000000..e498ae674 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Chord-v2_Eighth_Single-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Single-v2_Eighth_Single-Automatic-Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Single-v2_Eighth_Single-Automatic-Stem.png new file mode 100644 index 000000000..1abc7b258 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Eighth_Single-v2_Eighth_Single-Automatic-Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Automatic_Stem.png new file mode 100644 index 000000000..3a6c888ec Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Reversed_Stem.png new file mode 100644 index 000000000..98912d6ef Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Same_Stem.png new file mode 100644 index 000000000..785b5e40d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Automatic_Stem.png new file mode 100644 index 000000000..c5d7d5b70 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Reversed_Stem.png new file mode 100644 index 000000000..1807ea516 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Same_Stem.png new file mode 100644 index 000000000..d9e2e6a6a Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Chord-v2_Full_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Automatic_Stem.png new file mode 100644 index 000000000..3f578fbee Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Reversed_Stem.png new file mode 100644 index 000000000..9901300f4 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Same_Stem.png new file mode 100644 index 000000000..a1b8bcaa3 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Full_Single-v2_Full_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Automatic_Stem.png new file mode 100644 index 000000000..4a0dcc810 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Reversed_Stem.png new file mode 100644 index 000000000..f3e086a08 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Same_Stem.png new file mode 100644 index 000000000..807b5daf1 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Automatic_Stem.png new file mode 100644 index 000000000..673bf2fc1 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Reversed_Stem.png new file mode 100644 index 000000000..67c2d0dfa Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Same_Stem.png new file mode 100644 index 000000000..c1bac4d28 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Chord-v2_Full_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Automatic_Stem.png new file mode 100644 index 000000000..2bb855fa8 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Reversed_Stem.png new file mode 100644 index 000000000..73353d097 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Same_Stem.png new file mode 100644 index 000000000..a931e4bbf Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Half_Single-v2_Full_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Automatic_Stem.png new file mode 100644 index 000000000..2bf7cb384 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Reversed_Stem.png new file mode 100644 index 000000000..c937ab6eb Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Same_Stem.png new file mode 100644 index 000000000..c937ab6eb Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-Half_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Automatic_Stem.png new file mode 100644 index 000000000..59bff58d9 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Reversed_Stem.png new file mode 100644 index 000000000..e3ca8435d Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Same_Stem.png new file mode 100644 index 000000000..5b1f46c6e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Half_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Automatic_Stem.png new file mode 100644 index 000000000..813da1bc9 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Reversed_Stem.png new file mode 100644 index 000000000..794eb5cad Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Same_Stem.png new file mode 100644 index 000000000..d6759670e Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Chord-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Automatic_Stem.png new file mode 100644 index 000000000..ac11f0068 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Reversed_Stem.png new file mode 100644 index 000000000..7545b9561 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Same_Stem.png new file mode 100644 index 000000000..15ff3315f Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Chord-v2_Quarter_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Automatic_Stem.png new file mode 100644 index 000000000..e0d87a4b8 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Reversed_Stem.png new file mode 100644 index 000000000..28f1b3108 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Same_Stem.png new file mode 100644 index 000000000..0d8a2bc44 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Half_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Automatic_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Automatic_Stem.png new file mode 100644 index 000000000..13f85800c Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Automatic_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Reversed_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Reversed_Stem.png new file mode 100644 index 000000000..3ea76c3ed Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Reversed_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Same_Stem.png b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Same_Stem.png new file mode 100644 index 000000000..88e42cb89 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/multivoice/v1_Quarter_Single-v2_Quarter_Single-Same_Stem.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/accidentals-advanced.png b/packages/alphatab/test-data/visual-tests/music-notation/accidentals-advanced.png index 8c4f365b7..d1691765a 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/accidentals-advanced.png and b/packages/alphatab/test-data/visual-tests/music-notation/accidentals-advanced.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/accidentals.png b/packages/alphatab/test-data/visual-tests/music-notation/accidentals.png index a61263032..eb037b196 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/accidentals.png and b/packages/alphatab/test-data/visual-tests/music-notation/accidentals.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/barlines.png b/packages/alphatab/test-data/visual-tests/music-notation/barlines.png index 78d51b866..0c31066f5 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/barlines.png and b/packages/alphatab/test-data/visual-tests/music-notation/barlines.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/beams-advanced.png b/packages/alphatab/test-data/visual-tests/music-notation/beams-advanced.png index 5bde85014..36b516ba6 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/beams-advanced.png and b/packages/alphatab/test-data/visual-tests/music-notation/beams-advanced.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/brushes-ukulele.png b/packages/alphatab/test-data/visual-tests/music-notation/brushes-ukulele.png index a52ab1b33..a8b46ac7a 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/brushes-ukulele.png and b/packages/alphatab/test-data/visual-tests/music-notation/brushes-ukulele.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/brushes.png b/packages/alphatab/test-data/visual-tests/music-notation/brushes.png index 6c830fe65..a9b3ff3ae 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/brushes.png and b/packages/alphatab/test-data/visual-tests/music-notation/brushes.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/clefs.png b/packages/alphatab/test-data/visual-tests/music-notation/clefs.png index ef4281db5..71185bf83 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/clefs.png and b/packages/alphatab/test-data/visual-tests/music-notation/clefs.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/forced-accidentals.png b/packages/alphatab/test-data/visual-tests/music-notation/forced-accidentals.png index 8540a65c2..ee577e793 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/forced-accidentals.png and b/packages/alphatab/test-data/visual-tests/music-notation/forced-accidentals.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-c3.png b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-c3.png index ef22635b8..e2e89e644 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-c3.png and b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-c3.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-c4.png b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-c4.png index ef0ab51c4..be7b4b128 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-c4.png and b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-c4.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-f4.png b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-f4.png index f3e1ecdba..ce391b3d0 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-f4.png and b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-f4.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-g2.png b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-g2.png index 83c9c6ec9..e55d69aca 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-g2.png and b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-g2.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-mixed.png b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-mixed.png index 8ff9d99fb..7afb9f885 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-mixed.png and b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures-mixed.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures.png b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures.png index 83c9c6ec9..e55d69aca 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/key-signatures.png and b/packages/alphatab/test-data/visual-tests/music-notation/key-signatures.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/notes-rests-beams.png b/packages/alphatab/test-data/visual-tests/music-notation/notes-rests-beams.png index 9bd9c8bcf..cb15da25c 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/notes-rests-beams.png and b/packages/alphatab/test-data/visual-tests/music-notation/notes-rests-beams.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/rest-collisions.png b/packages/alphatab/test-data/visual-tests/music-notation/rest-collisions.png index c5cb03064..7e80b9baa 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/rest-collisions.png and b/packages/alphatab/test-data/visual-tests/music-notation/rest-collisions.png differ diff --git a/packages/alphatab/test-data/visual-tests/music-notation/time-signatures.png b/packages/alphatab/test-data/visual-tests/music-notation/time-signatures.png index 9b7ace1c9..14af184af 100644 Binary files a/packages/alphatab/test-data/visual-tests/music-notation/time-signatures.png and b/packages/alphatab/test-data/visual-tests/music-notation/time-signatures.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-off.png index 777b4c759..b657ec8fd 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-on.png index e0f8cd8e2..0eb81e049 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/effects-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/effects-off.png index 3ae5f6354..b1f2056bd 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/effects-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/effects-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/effects-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/effects-on.png index 0ac26bcc5..7ea8810da 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/effects-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/effects-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-off.png index 6f03f72ed..b8f0ca5b7 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-on.png index 307776269..1f5037778 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png index 1e50f8e3c..cc9dea729 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png index 0b5b3f017..7a207363e 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-album.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-album.png index 32c4f075d..124b47821 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-album.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-album.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-all.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-all.png index 9906d0699..0913f7076 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-all.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-all.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-artist.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-artist.png index 0b1a2f3b5..5d64706b9 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-artist.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-artist.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-copyright.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-copyright.png index d44c9c9db..ed2811063 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-copyright.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-copyright.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-music.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-music.png index e169bf53e..1af0df6be 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-music.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-music.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-subtitle.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-subtitle.png index ffcd28536..b770d035a 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-subtitle.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-subtitle.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-title.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-title.png index 6f134a551..a74705346 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-title.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-title.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words-and-music.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words-and-music.png index a9a3bb16e..36d7afc2f 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words-and-music.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words-and-music.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words.png index 049e3e716..b7afc1c3f 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png index ddc7c063a..5f8af6dec 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png index eda73d822..fe2ed346b 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/track-names-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/track-names-off.png index 41fc6441b..ef4d6d984 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/track-names-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/track-names-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/track-names-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/track-names-on.png index 94e6b1213..53cb474a9 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/track-names-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/track-names-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png index da3c0d04c..894fbc5ba 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png index 30d6d537c..b3e5acf3b 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/accentuations-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/accentuations-default.png index 6f465c074..e2ce3dffb 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/accentuations-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/accentuations-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/accentuations-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/accentuations-songbook.png index 7c3c9296d..caa3434ef 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/accentuations-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/accentuations-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/arpeggio-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/arpeggio-default.png index 47585bcfc..ebad91f0d 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/arpeggio-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/arpeggio-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/arpeggio-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/arpeggio-songbook.png index 47585bcfc..ebad91f0d 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/arpeggio-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/arpeggio-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png index e8d8caf74..15d1a628d 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png index 5ee5a964f..3b72eb225 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/chords-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/chords-default.png index ee9af1a69..b7c12d398 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/chords-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/chords-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/chords-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/chords-songbook.png index ee9af1a69..b7c12d398 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/chords-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/chords-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/crescendo-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/crescendo-default.png index 762051325..15bfb3ae7 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/crescendo-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/crescendo-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/crescendo-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/crescendo-songbook.png index 762051325..15bfb3ae7 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/crescendo-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/crescendo-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/dead-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/dead-default.png index 722d89cc9..8d6e11a02 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/dead-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/dead-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/dead-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/dead-songbook.png index 722d89cc9..8d6e11a02 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/dead-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/dead-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/dynamics-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/dynamics-default.png index 3e6afe636..4cec11582 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/dynamics-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/dynamics-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/dynamics-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/dynamics-songbook.png index 3e6afe636..4cec11582 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/dynamics-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/dynamics-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/fingering-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/fingering-default.png index 74fa7eba9..1fcf16d18 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/fingering-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/fingering-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/fingering-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/fingering-songbook.png index e09a4d544..5795b8636 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/fingering-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/fingering-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png index 6b69be15c..d8b421047 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png index 574736a4d..0f2fb5288 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png index 6c1ec7763..fb4c9f547 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png index d9ca84aaa..cfe1f47cf 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/grace-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/grace-default.png index 261ad756d..02a526402 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/grace-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/grace-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/grace-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/grace-songbook.png index 758edc948..2001b4833 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/grace-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/grace-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/hammer-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/hammer-default.png index 4ada40afe..4dd89f7bb 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/hammer-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/hammer-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/hammer-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/hammer-songbook.png index 4ada40afe..4dd89f7bb 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/hammer-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/hammer-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/harmonics-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/harmonics-default.png index 809735788..c129660f8 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/harmonics-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/harmonics-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/harmonics-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/harmonics-songbook.png index 809735788..c129660f8 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/harmonics-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/harmonics-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/let-ring-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/let-ring-default.png index 881432eb8..d5987aa41 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/let-ring-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/let-ring-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/let-ring-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/let-ring-songbook.png index e90690543..942e82a58 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/let-ring-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/let-ring-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/mixed-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/mixed-default.png index 8d3fde475..ca1644730 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/mixed-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/mixed-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/mixed-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/mixed-songbook.png index fe6a9c1c9..80504bcc6 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/mixed-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/mixed-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/multi-grace-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/multi-grace-default.png index d57162840..48d14d9eb 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/multi-grace-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/multi-grace-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/multi-grace-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/multi-grace-songbook.png index 9ac2c729c..60317e205 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/multi-grace-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/multi-grace-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/multi-voice-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/multi-voice-default.png index 824f97fc0..7516a6217 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/multi-voice-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/multi-voice-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/multi-voice-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/multi-voice-songbook.png index 824f97fc0..7516a6217 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/multi-voice-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/multi-voice-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/ottavia-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/ottavia-default.png index 24b011a6b..22ff566de 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/ottavia-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/ottavia-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/ottavia-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/ottavia-songbook.png index 24b011a6b..22ff566de 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/ottavia-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/ottavia-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/pick-stroke-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/pick-stroke-default.png index 059facf6c..22c1fe9cf 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/pick-stroke-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/pick-stroke-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/pick-stroke-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/pick-stroke-songbook.png index 059facf6c..22c1fe9cf 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/pick-stroke-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/pick-stroke-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1300.png b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1300.png index 3638b0a9a..702be06e1 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1300.png and b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1300.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png index e184f18d1..e790d4194 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png and b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-500.png b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-500.png index 25dcb6508..09d34fed6 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-500.png and b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-500.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-800.png b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-800.png index 8c2d43a52..b8cee8afb 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-800.png and b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-800.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/slash-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/slash-default.png index c061f1b34..55f1768cf 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/slash-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/slash-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/slash-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/slash-songbook.png index c061f1b34..55f1768cf 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/slash-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/slash-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/slides-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/slides-default.png index a114500f8..77366babd 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/slides-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/slides-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/slides-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/slides-songbook.png index a114500f8..77366babd 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/slides-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/slides-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png b/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png index bd54430a0..11b2de995 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png and b/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/staccatissimo-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/staccatissimo-default.png index bddccdcca..90a1d2d11 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/staccatissimo-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/staccatissimo-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/staccatissimo-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/staccatissimo-songbook.png index bddccdcca..90a1d2d11 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/staccatissimo-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/staccatissimo-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/sweep-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/sweep-default.png index 8b11c8f5f..f773d0015 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/sweep-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/sweep-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/sweep-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/sweep-songbook.png index 8b11c8f5f..f773d0015 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/sweep-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/sweep-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/tap-riff-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/tap-riff-default.png index 360875025..80e73d4d4 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/tap-riff-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/tap-riff-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/tap-riff-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/tap-riff-songbook.png index 360875025..80e73d4d4 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/tap-riff-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/tap-riff-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/tempo-change-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/tempo-change-default.png index de40c405e..d3b7e09f7 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/tempo-change-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/tempo-change-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/tempo-change-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/tempo-change-songbook.png index fc013cd98..7dc88934c 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/tempo-change-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/tempo-change-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/text-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/text-default.png index 0a54e84fc..a7d44f6b3 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/text-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/text-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/text-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/text-songbook.png index 0a54e84fc..a7d44f6b3 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/text-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/text-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/tied-note-accidentals-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/tied-note-accidentals-default.png index c91420def..8797e3235 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/tied-note-accidentals-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/tied-note-accidentals-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png index cae70df9c..b646a0e7f 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/trill-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/trill-default.png index 09e31c8ec..4e59d6b19 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/trill-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/trill-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/trill-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/trill-songbook.png index 09e31c8ec..4e59d6b19 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/trill-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/trill-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/triplet-feel-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/triplet-feel-default.png index 04397e96e..0ca246444 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/triplet-feel-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/triplet-feel-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/triplet-feel-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/triplet-feel-songbook.png index 04397e96e..0ca246444 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/triplet-feel-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/triplet-feel-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/vibrato-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/vibrato-default.png index d10ebb56f..32c6f8b0a 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/vibrato-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/vibrato-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/vibrato-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/vibrato-songbook.png index d10ebb56f..32c6f8b0a 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/vibrato-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/vibrato-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/wah-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/wah-default.png index 7ee1d6fae..a4df43be3 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/wah-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/wah-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/wah-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/wah-songbook.png index 7ee1d6fae..a4df43be3 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/wah-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/wah-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/whammy-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/whammy-default.png index e6dc10536..453fd7b73 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/whammy-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/whammy-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/whammy-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/whammy-songbook.png index 3058f6fa2..18078017a 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/whammy-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/whammy-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-notes/beaming-mode.png b/packages/alphatab/test-data/visual-tests/special-notes/beaming-mode.png index dfe31e93d..6832b9f23 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-notes/beaming-mode.png and b/packages/alphatab/test-data/visual-tests/special-notes/beaming-mode.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-notes/dead-notes.png b/packages/alphatab/test-data/visual-tests/special-notes/dead-notes.png index 9ab897580..3b2f3221a 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-notes/dead-notes.png and b/packages/alphatab/test-data/visual-tests/special-notes/dead-notes.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-notes/ghost-notes.png b/packages/alphatab/test-data/visual-tests/special-notes/ghost-notes.png index 0c7a371bc..7a8b39669 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-notes/ghost-notes.png and b/packages/alphatab/test-data/visual-tests/special-notes/ghost-notes.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-1300-2.png b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-1300-2.png index 85b67ef45..5e4f2535c 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-1300-2.png and b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-1300-2.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-1300.png b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-1300.png index 85b67ef45..4afdc155a 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-1300.png and b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-1300.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-800.png b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-800.png index b96112a69..bc788e0ca 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-800.png and b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced-800.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced.png b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced.png index 85b67ef45..4afdc155a 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced.png and b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-advanced.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-alignment.png b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-alignment.png index 5eb1a832c..0a07b147e 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-alignment.png and b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes-alignment.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes.png b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes.png index 9d3aa2b80..294d43660 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-notes/grace-notes.png and b/packages/alphatab/test-data/visual-tests/special-notes/grace-notes.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-notes/tied-notes.png b/packages/alphatab/test-data/visual-tests/special-notes/tied-notes.png index cf9088b49..ad7d85254 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-notes/tied-notes.png and b/packages/alphatab/test-data/visual-tests/special-notes/tied-notes.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/drum-tabs.png b/packages/alphatab/test-data/visual-tests/special-tracks/drum-tabs.png index bfabc48ef..7676a2f96 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-tracks/drum-tabs.png and b/packages/alphatab/test-data/visual-tests/special-tracks/drum-tabs.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/grand-staff.png b/packages/alphatab/test-data/visual-tests/special-tracks/grand-staff.png index a6f6eaea6..6c46a5cee 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-tracks/grand-staff.png and b/packages/alphatab/test-data/visual-tests/special-tracks/grand-staff.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/numbered-durations.png b/packages/alphatab/test-data/visual-tests/special-tracks/numbered-durations.png new file mode 100644 index 000000000..9b535c0e7 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/special-tracks/numbered-durations.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/numbered-tuplets.png b/packages/alphatab/test-data/visual-tests/special-tracks/numbered-tuplets.png new file mode 100644 index 000000000..7dd3f91d8 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/special-tracks/numbered-tuplets.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/numbered.png b/packages/alphatab/test-data/visual-tests/special-tracks/numbered.png index cd9772abe..dd428f249 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-tracks/numbered.png and b/packages/alphatab/test-data/visual-tests/special-tracks/numbered.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/percussion.png b/packages/alphatab/test-data/visual-tests/special-tracks/percussion.png index 39dc1569f..443bf9f6b 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-tracks/percussion.png and b/packages/alphatab/test-data/visual-tests/special-tracks/percussion.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/slash.png b/packages/alphatab/test-data/visual-tests/special-tracks/slash.png index 3b6497b11..3785d177a 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-tracks/slash.png and b/packages/alphatab/test-data/visual-tests/special-tracks/slash.png differ diff --git a/packages/alphatab/test-data/visual-tests/systems-layout/bars-adjusted-automatic.png b/packages/alphatab/test-data/visual-tests/systems-layout/bars-adjusted-automatic.png index f64d8def7..b82cea073 100644 Binary files a/packages/alphatab/test-data/visual-tests/systems-layout/bars-adjusted-automatic.png and b/packages/alphatab/test-data/visual-tests/systems-layout/bars-adjusted-automatic.png differ diff --git a/packages/alphatab/test-data/visual-tests/systems-layout/bars-adjusted-model.png b/packages/alphatab/test-data/visual-tests/systems-layout/bars-adjusted-model.png index 866d41c21..4f9a97228 100644 Binary files a/packages/alphatab/test-data/visual-tests/systems-layout/bars-adjusted-model.png and b/packages/alphatab/test-data/visual-tests/systems-layout/bars-adjusted-model.png differ diff --git a/packages/alphatab/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-single-track.png b/packages/alphatab/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-single-track.png index 7296fab45..d5eae200e 100644 Binary files a/packages/alphatab/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-single-track.png and b/packages/alphatab/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-single-track.png differ diff --git a/packages/alphatab/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-two-tracks.png b/packages/alphatab/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-two-tracks.png index c820431fe..e631c084e 100644 Binary files a/packages/alphatab/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-two-tracks.png and b/packages/alphatab/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-two-tracks.png differ diff --git a/packages/alphatab/test-data/visual-tests/systems-layout/multi-track-single-track.png b/packages/alphatab/test-data/visual-tests/systems-layout/multi-track-single-track.png index 3e113d441..a869f8c17 100644 Binary files a/packages/alphatab/test-data/visual-tests/systems-layout/multi-track-single-track.png and b/packages/alphatab/test-data/visual-tests/systems-layout/multi-track-single-track.png differ diff --git a/packages/alphatab/test-data/visual-tests/systems-layout/multi-track-two-tracks.png b/packages/alphatab/test-data/visual-tests/systems-layout/multi-track-two-tracks.png index da767a284..735486de5 100644 Binary files a/packages/alphatab/test-data/visual-tests/systems-layout/multi-track-two-tracks.png and b/packages/alphatab/test-data/visual-tests/systems-layout/multi-track-two-tracks.png differ diff --git a/packages/alphatab/test-data/visual-tests/systems-layout/resized.png b/packages/alphatab/test-data/visual-tests/systems-layout/resized.png index 43e843d40..0098285fb 100644 Binary files a/packages/alphatab/test-data/visual-tests/systems-layout/resized.png and b/packages/alphatab/test-data/visual-tests/systems-layout/resized.png differ diff --git a/packages/alphatab/test/PrettyFormat.ts b/packages/alphatab/test/PrettyFormat.ts index e907a552b..46eb805cb 100644 --- a/packages/alphatab/test/PrettyFormat.ts +++ b/packages/alphatab/test/PrettyFormat.ts @@ -1038,9 +1038,10 @@ export class SnapshotFile { const name = lines[i].substring(9, endOfName); - if (lines[i].endsWith('`;')) { + if (lines[i].trimEnd().endsWith('`;')) { const startOfValue = lines[i].indexOf('`', endOfName + 2) + 1; - this.snapshots.set(name, lines[i].substring(startOfValue, lines[i].length - 2)); + const endOfValue = lines[i].indexOf('`;', startOfValue + 1); + this.snapshots.set(name, lines[i].substring(startOfValue, endOfValue)); i++; continue; } diff --git a/packages/alphatab/test/TestPlatform.ts b/packages/alphatab/test/TestPlatform.ts index faafafd05..f887fb796 100644 --- a/packages/alphatab/test/TestPlatform.ts +++ b/packages/alphatab/test/TestPlatform.ts @@ -46,7 +46,7 @@ export class TestPlatform { * @partial */ public static async listDirectory(path: string): Promise { - return await fs.promises.readdir(path); + return (await fs.promises.readdir(path, { withFileTypes: true })).filter(t => t.isFile()).map(t => t.name); } /** @@ -125,7 +125,7 @@ export class TestPlatform { public static mapAsUnknownIterable(map: unknown): Iterable<[unknown, unknown]> { return (map as Map).entries(); } - + /** * @target web * @partial @@ -142,4 +142,10 @@ export class TestPlatform { const withConstructor = val as object; return (typeof withConstructor.constructor === 'function' && withConstructor.constructor.name) || 'Object'; } + + /** + * @target web + * @partial + */ + public static currentTestName: string = ''; } diff --git a/packages/alphatab/test/audio/AlphaSynth.test.ts b/packages/alphatab/test/audio/AlphaSynth.test.ts index 36e3a9ef2..a75abe4de 100644 --- a/packages/alphatab/test/audio/AlphaSynth.test.ts +++ b/packages/alphatab/test/audio/AlphaSynth.test.ts @@ -2,7 +2,12 @@ import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler'; import { ControllerType } from '@coderline/alphatab/midi/ControllerType'; -import { type ControlChangeEvent, MidiEventType } from '@coderline/alphatab/midi/MidiEvent'; +import { + type ControlChangeEvent, + type MidiEvent, + MidiEventType, + TempoChangeEvent +} from '@coderline/alphatab/midi/MidiEvent'; import { MidiFile } from '@coderline/alphatab/midi/MidiFile'; import { MidiFileGenerator } from '@coderline/alphatab/midi/MidiFileGenerator'; import type { Score } from '@coderline/alphatab/model/Score'; @@ -12,9 +17,9 @@ import { AudioExportOptions } from '@coderline/alphatab/synth/IAudioExporter'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; import { TinySoundFont } from '@coderline/alphatab/synth/synthesis/TinySoundFont'; import { VorbisFile } from '@coderline/alphatab/synth/vorbis/VorbisFile'; +import { assert, expect } from 'chai'; import { TestOutput } from 'test/audio/TestOutput'; import { TestPlatform } from 'test/TestPlatform'; -import { expect } from 'chai'; describe('AlphaSynthTests', () => { it('pcm-generation', async () => { @@ -332,4 +337,62 @@ describe('AlphaSynthTests', () => { expect(synth.channelGetPresetBank(2)).to.equal(4000); expect(synth.channelGetPresetBank(3)).to.equal(4000); }); + + async function testPlaythrough(midi: MidiFile) { + const testOutput = new TestOutput(false); + const synth = new AlphaSynth(testOutput, 500); + const soundFont = await TestPlatform.loadFile('test-data/audio/default.sf2'); + synth.loadSoundFont(soundFont, false); + synth.loadMidiFile(midi); + synth.play(); + let finished = false; + synth.finished.on(() => { + finished = true; + }); + + const start = Date.now(); + + while (!finished) { + const now = Date.now(); + if (now - start > 2000) { + assert.fail(`play did not complete after ${2000}ms`); + } + testOutput.next(); + } + } + + it('small-tempos', async () => { + const score = ScoreLoader.loadScoreFromBytes(await TestPlatform.loadFile('test-data/audio/small-tempo.xml')); + + expect(score.masterBars[0].tempoAutomations[0].value).to.equal(0.111); + + const midi = new MidiFile(); + const handler = new AlphaSynthMidiFileHandler(midi); + const generator = new MidiFileGenerator(score, null, handler); + generator.generate(); + + const tempoChange: MidiEvent[] = midi.events.filter(e => e instanceof TempoChangeEvent); + expect(tempoChange.length).to.equal(1); + expect((tempoChange[0] as TempoChangeEvent).beatsPerMinute).to.equal(0.111); + + await testPlaythrough(midi); + }); + + it('zero-tempo', async () => { + const score = ScoreLoader.loadScoreFromBytes(await TestPlatform.loadFile('test-data/audio/small-tempo.xml')); + + expect(score.masterBars[0].tempoAutomations[0].value).to.equal(0.111); + score.masterBars[0].tempoAutomations[0].value = 0; + + const midi = new MidiFile(); + const handler = new AlphaSynthMidiFileHandler(midi); + const generator = new MidiFileGenerator(score, null, handler); + generator.generate(); + + const tempoChange: MidiEvent[] = midi.events.filter(e => e instanceof TempoChangeEvent); + expect(tempoChange.length).to.equal(1); + expect((tempoChange[0] as TempoChangeEvent).beatsPerMinute).to.equal(0); + + await testPlaythrough(midi); + }); }); diff --git a/packages/alphatab/test/audio/FlatMidiEventGenerator.ts b/packages/alphatab/test/audio/FlatMidiEventGenerator.ts index d3c4eda8b..728bf0eca 100644 --- a/packages/alphatab/test/audio/FlatMidiEventGenerator.ts +++ b/packages/alphatab/test/audio/FlatMidiEventGenerator.ts @@ -5,10 +5,11 @@ import type { IMidiFileHandler } from '@coderline/alphatab/midi/IMidiFileHandler * @internal */ export class FlatMidiEventGenerator implements IMidiFileHandler { - public midiEvents: FlatMidiEvent[]; + public midiEvents: FlatMidiEvent[] = []; + public tickShift = 0; - public constructor() { - this.midiEvents = []; + public addTickShift(tickShift: number): void { + this.tickShift = tickShift; } public addTimeSignature(tick: number, timeSignatureNumerator: number, timeSignatureDenominator: number): void { diff --git a/packages/alphatab/test/audio/MidiFileGenerator.test.ts b/packages/alphatab/test/audio/MidiFileGenerator.test.ts index e1770883a..3e24ad80d 100644 --- a/packages/alphatab/test/audio/MidiFileGenerator.test.ts +++ b/packages/alphatab/test/audio/MidiFileGenerator.test.ts @@ -5,7 +5,12 @@ import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; import { Logger } from '@coderline/alphatab/Logger'; import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler'; import { ControllerType } from '@coderline/alphatab/midi/ControllerType'; -import { type MidiEvent, MidiEventType, NoteOnEvent, type TimeSignatureEvent } from '@coderline/alphatab/midi/MidiEvent'; +import { + type MidiEvent, + MidiEventType, + NoteOnEvent, + type TimeSignatureEvent +} from '@coderline/alphatab/midi/MidiEvent'; import { MidiFile } from '@coderline/alphatab/midi/MidiFile'; import { MidiFileGenerator } from '@coderline/alphatab/midi/MidiFileGenerator'; import type { MidiTickLookup } from '@coderline/alphatab/midi/MidiTickLookup'; @@ -18,11 +23,18 @@ import { GraceType } from '@coderline/alphatab/model/GraceType'; import type { Note } from '@coderline/alphatab/model/Note'; import type { PlaybackInformation } from '@coderline/alphatab/model/PlaybackInformation'; import type { Score } from '@coderline/alphatab/model/Score'; +import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; +import { Tuning } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { Settings } from '@coderline/alphatab/Settings'; +import { AlphaSynth } from '@coderline/alphatab/synth/AlphaSynth'; +import { AlphaSynthWrapper } from '@coderline/alphatab/synth/AlphaSynthWrapper'; +import { PlaybackRange } from '@coderline/alphatab/synth/PlaybackRange'; +import type { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs'; +import { expect } from 'chai'; import { FlatControlChangeEvent, - type FlatMidiEvent, + FlatMidiEvent, FlatMidiEventGenerator, FlatNoteBendEvent, FlatNoteEvent, @@ -32,8 +44,8 @@ import { FlatTimeSignatureEvent, FlatTrackEndEvent } from 'test/audio/FlatMidiEventGenerator'; +import { TestOutput } from 'test/audio/TestOutput'; import { TestPlatform } from 'test/TestPlatform'; -import { expect } from 'chai'; describe('MidiFileGeneratorTest', () => { const parseTex: (tex: string) => Score = (tex: string): Score => { @@ -341,6 +353,7 @@ describe('MidiFileGeneratorTest', () => { new FlatNoteBendEvent(11 * 40, 0, info.secondaryChannel, note.realValue, 9131), new FlatNoteBendEvent(12 * 40, 0, info.secondaryChannel, note.realValue, 9216), // full bend new FlatNoteBendEvent(12 * 40, 0, info.secondaryChannel, note.realValue, 9216), // full bend + new FlatNoteBendEvent(12 * 40, 0, info.secondaryChannel, note.realValue, 9216), // full bend new FlatNoteBendEvent(13 * 40, 0, info.secondaryChannel, note.realValue, 9131), new FlatNoteBendEvent(14 * 40, 0, info.secondaryChannel, note.realValue, 9045), new FlatNoteBendEvent(15 * 40, 0, info.secondaryChannel, note.realValue, 8960), @@ -1241,7 +1254,7 @@ describe('MidiFileGeneratorTest', () => { const score: Score = parseTex(tex); expect(score.tempo).to.be.equal(60); - expect(score.masterBars[0].tempoAutomations).to.have.length(1); + expect(score.masterBars[0].tempoAutomations.length).to.equal(1); expect(score.masterBars[0].tempoAutomations[0]!.value).to.be.equal(60); const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); @@ -1841,4 +1854,240 @@ describe('MidiFileGeneratorTest', () => { expect(generator.transpositionPitches.has(5)).to.be.true; expect(generator.transpositionPitches.get(5)!).to.equal(12); }); + + it('tickshift-flat', () => { + const score = parseTex(` + C4 {gr bb} C4 C4 C4 C4 + `); + + const handler = new FlatMidiEventGenerator(); + const generator = new MidiFileGenerator(score, null, handler); + generator.generate(); + + expect(handler.tickShift).to.equal(120); + const firstNote = handler.midiEvents.find(e => e instanceof FlatNoteEvent) as FlatNoteEvent; + expect(firstNote.tick).to.equal(-120); + }); + + it('tickshift-synth', () => { + const score = parseTex(` + C4 {gr bb} C4 C4 C4 C4 + `); + + const file = new MidiFile(); + const handler = new AlphaSynthMidiFileHandler(file); + const generator = new MidiFileGenerator(score, null, handler); + generator.generate(); + + expect(handler.tickShift).to.equal(120); + const firstNote = file.events.find(e => e instanceof NoteOnEvent) as NoteOnEvent; + expect(firstNote.tick).to.equal(0); + }); + + it('synthwrapper-mapping', () => { + const wrapper = new AlphaSynthWrapper(); + const output = new TestOutput(); + const synth = new AlphaSynth(output, 100); + wrapper.instance = synth; + + const score = parseTex(` + C4 {gr bb} C4 C4 C4 C4 + `); + + const file = new MidiFile(); + const handler = new AlphaSynthMidiFileHandler(file); + const generator = new MidiFileGenerator(score, null, handler); + generator.generate(); + + wrapper.midiTickShift = handler.tickShift; + wrapper.loadMidiFile(file); + + // the synth is always a bit off internally to handle + // some inclusive/exclusive ranges easier + const tickImprecision = 1; + + // check API -> Player mappings + wrapper.tickPosition = -120; + expect(synth.tickPosition).to.equal(tickImprecision); + + wrapper.tickPosition = 0; + expect(synth.tickPosition).to.equal(120 + tickImprecision); + + const range = new PlaybackRange(); + range.startTick = 960; + range.endTick = 1920; + wrapper.playbackRange = range; + expect(synth.playbackRange!.startTick).to.equal(range.startTick + handler.tickShift); + expect(synth.playbackRange!.endTick).to.equal(range.endTick + handler.tickShift); + + // check API <- Player mappings + wrapper.stop(); + expect(wrapper.tickPosition).to.equal(range.startTick + tickImprecision); + expect(wrapper.loadedMidiInfo!.endTick).to.equal(3840); + expect(wrapper.playbackRange!.startTick).to.equal(range.startTick); + expect(wrapper.playbackRange!.endTick).to.equal(range.endTick); + + wrapper.playbackRange = null; + let lastArgs: PositionChangedEventArgs | null = null; + wrapper.positionChanged.on(e => { + lastArgs = e; + }); + wrapper.tickPosition = 0; + expect(lastArgs!.currentTick).to.equal(tickImprecision); + expect(synth.tickPosition).to.equal(handler.tickShift + tickImprecision); + }); + + describe('effect-note-durations', () => { + function test(tex: string, applyEffect: (beat: Beat) => void) { + const score = ScoreLoader.loadAlphaTex(tex); + let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; + while (beat) { + applyEffect(beat); + beat = beat.nextBeat; + } + + const settings = new Settings(); + settings.player.playTripletFeel = true; + + score.finish(settings); + + const flat = new FlatMidiEventGenerator(); + const generator: MidiFileGenerator = new MidiFileGenerator(score, settings, flat); + generator.generate(); + + const noteEvents = flat.midiEvents + .filter(e => e instanceof FlatNoteEvent) + .map( + e => + `Note: ${Tuning.getTextForTuning((e as FlatNoteEvent).key, true)} ${(e as FlatNoteEvent).length}` + ); + + expect(noteEvents).toMatchSnapshot(); + } + + function addTrill(b: Beat) { + if (b.graceType !== GraceType.None) { + return; + } + b.notes[0].trillValue = b.notes[0].realValue + 12; + b.notes[0].trillSpeed = Duration.ThirtySecond; + } + + function addTremolo(b: Beat, marks: number) { + if (b.graceType !== GraceType.None) { + return; + } + b.tremoloPicking = new TremoloPickingEffect(); + b.tremoloPicking.marks = marks; + b.tremoloPicking.style = TremoloPickingStyle.Default; + } + + // as reference to check snapshots + describe('plain', () => { + const tex = ` + :8 + 5.3 + 7.3 + 9.3 + 10.3 + `; + + it('tripletfeel', () => test(`\\tf triplet8th ${tex}`, _b => {})); + it('tuplet', () => + test(tex, b => { + b.tupletNumerator = 3; + b.tupletDenominator = 2; + })); + it('dot', () => + test(tex, b => { + b.dots = 1; + })); + + it('trill', () => test(tex, b => addTrill(b))); + it('tremolo-2', () => test(tex, b => addTremolo(b, 2))); + it('tremolo-3', () => test(tex, b => addTremolo(b, 3))); + }); + + describe('tuplet', () => { + const tex = ` + :8 + 5.3 {tu 3} + 7.3 {tu 3} + 9.3 {tu 3} + `; + + it('trill', () => test(tex, b => addTrill(b))); + it('tremolo-2', () => test(tex, b => addTremolo(b, 2))); + it('tremolo-3', () => test(tex, b => addTremolo(b, 3))); + }); + + describe('dots', () => { + const tex = ` + :8 + 5.3 {d} + 7.3 {d} + 9.3 {d} + `; + + it('trill', () => test(tex, b => addTrill(b))); + it('tremolo-2', () => test(tex, b => addTremolo(b, 2))); + it('tremolo-3', () => test(tex, b => addTremolo(b, 3))); + }); + + describe('triplet-feel', () => { + const tex = ` + \\tf triplet8th + :8 + 5.3 + 7.3 + 9.3 + 10.3 + `; + + it('trill', () => test(tex, b => addTrill(b))); + it('tremolo-2', () => test(tex, b => addTremolo(b, 2))); + it('tremolo-3', () => test(tex, b => addTremolo(b, 3))); + }); + + describe('grace-notes-on-beat', () => { + const tex = ` + :8 + 3.5 {gr ob} + 5.3 + 5.5 {gr ob} + 7.3 + 7.5 {gr ob} + 9.3 + 9.5 {gr ob} + 10.3 + `; + + it('trill', () => test(tex, b => addTrill(b))); + it('tremolo-2', () => test(tex, b => addTremolo(b, 2))); + it('tremolo-3', () => test(tex, b => addTremolo(b, 3))); + }); + + describe('grace-notes-before-beat', () => { + const tex = ` + :8 + 5.3 {gr bb} + 5.3 + 7.3 {gr bb} + 7.3 + 9.3 {gr bb} + 9.3 + 10.3 {gr bb} + 10.3 + `; + + it('trill', () => test(tex, b => addTrill(b))); + it('tremolo-2', () => test(tex, b => addTremolo(b, 2))); + it('tremolo-3', () => test(tex, b => addTremolo(b, 3))); + }); + + // NOTE: there might be more affected effects which we assume are "good enough" with the + // current behavior. combining these effects are rather unlikely like: + // * brush-strokes combined with trills or tremolos + // * rasgueados combined with trills or tremolos + }); }); diff --git a/packages/alphatab/test/audio/TestOutput.ts b/packages/alphatab/test/audio/TestOutput.ts index b227c78f0..cddc79b58 100644 --- a/packages/alphatab/test/audio/TestOutput.ts +++ b/packages/alphatab/test/audio/TestOutput.ts @@ -1,5 +1,10 @@ import type { ISynthOutput, ISynthOutputDevice } from '@coderline/alphatab/synth/ISynthOutput'; -import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@coderline/alphatab/EventEmitter'; +import { + EventEmitter, + type IEventEmitter, + type IEventEmitterOfT, + EventEmitterOfT +} from '@coderline/alphatab/EventEmitter'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; /** @@ -8,11 +13,16 @@ import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; export class TestOutput implements ISynthOutput { public samples: Float32Array[] = []; public sampleCount: number = 0; + private _storeSamples: boolean; public get sampleRate(): number { return 44100; } + public constructor(storeSamples: boolean = true) { + this._storeSamples = storeSamples; + } + public open(_bufferTimeInMilliseconds: number): void { this.samples = []; (this.ready as EventEmitter).trigger(); @@ -35,7 +45,9 @@ export class TestOutput implements ISynthOutput { } public addSamples(f: Float32Array): void { - this.samples.push(f); + if (this._storeSamples) { + this.samples.push(f); + } this.sampleCount += f.length; (this.samplesPlayed as EventEmitterOfT).trigger(f.length / SynthConstants.AudioChannels); } diff --git a/packages/alphatab/test/audio/__snapshots__/MidiFileGenerator.test.ts.snap b/packages/alphatab/test/audio/__snapshots__/MidiFileGenerator.test.ts.snap new file mode 100644 index 000000000..70aec95d3 --- /dev/null +++ b/packages/alphatab/test/audio/__snapshots__/MidiFileGenerator.test.ts.snap @@ -0,0 +1,492 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`MidiFileGeneratorTest effect-note-durations dots tremolo-2 1`] = ` +Array [ + "Note: C4 180", + "Note: C4 180", + "Note: C4 180", + "Note: C4 180", + "Note: D4 180", + "Note: D4 180", + "Note: D4 180", + "Note: D4 180", + "Note: E4 180", + "Note: E4 180", + "Note: E4 180", + "Note: E4 180", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations dots tremolo-3 1`] = ` +Array [ + "Note: C4 90", + "Note: C4 90", + "Note: C4 90", + "Note: C4 90", + "Note: C4 90", + "Note: C4 90", + "Note: C4 90", + "Note: C4 90", + "Note: D4 90", + "Note: D4 90", + "Note: D4 90", + "Note: D4 90", + "Note: D4 90", + "Note: D4 90", + "Note: D4 90", + "Note: D4 90", + "Note: E4 90", + "Note: E4 90", + "Note: E4 90", + "Note: E4 90", + "Note: E4 90", + "Note: E4 90", + "Note: E4 90", + "Note: E4 90", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations dots trill 1`] = ` +Array [ + "Note: C4 120", + "Note: C5 120", + "Note: C4 120", + "Note: C5 120", + "Note: C4 120", + "Note: C5 120", + "Note: D4 120", + "Note: D5 120", + "Note: D4 120", + "Note: D5 120", + "Note: D4 120", + "Note: D5 120", + "Note: E4 120", + "Note: E5 120", + "Note: E4 120", + "Note: E5 120", + "Note: E4 120", + "Note: E5 120", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations grace-notes-before-beat tremolo-2 1`] = ` +Array [ + "Note: C4 120", + "Note: C4 90", + "Note: C4 90", + "Note: C4 90", + "Note: C4 90", + "Note: D4 120", + "Note: D4 90", + "Note: D4 90", + "Note: D4 90", + "Note: D4 90", + "Note: E4 120", + "Note: E4 90", + "Note: E4 90", + "Note: E4 90", + "Note: E4 90", + "Note: F4 120", + "Note: F4 120", + "Note: F4 120", + "Note: F4 120", + "Note: F4 120", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations grace-notes-before-beat tremolo-3 1`] = ` +Array [ + "Note: C4 120", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: D4 120", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: E4 120", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: F4 120", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations grace-notes-before-beat trill 1`] = ` +Array [ + "Note: C4 120", + "Note: C4 120", + "Note: C5 120", + "Note: C4 120", + "Note: D4 120", + "Note: D4 120", + "Note: D5 120", + "Note: D4 120", + "Note: E4 120", + "Note: E4 120", + "Note: E5 120", + "Note: E4 120", + "Note: F4 120", + "Note: F4 120", + "Note: F5 120", + "Note: F4 120", + "Note: F5 120", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations grace-notes-on-beat tremolo-2 1`] = ` +Array [ + "Note: C3 120", + "Note: C4 90", + "Note: C4 90", + "Note: C4 90", + "Note: C4 90", + "Note: D3 120", + "Note: D4 90", + "Note: D4 90", + "Note: D4 90", + "Note: D4 90", + "Note: E3 120", + "Note: E4 90", + "Note: E4 90", + "Note: E4 90", + "Note: E4 90", + "Note: Gb3 120", + "Note: F4 90", + "Note: F4 90", + "Note: F4 90", + "Note: F4 90", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations grace-notes-on-beat tremolo-3 1`] = ` +Array [ + "Note: C3 120", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: C4 45", + "Note: D3 120", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: D4 45", + "Note: E3 120", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: E4 45", + "Note: Gb3 120", + "Note: F4 45", + "Note: F4 45", + "Note: F4 45", + "Note: F4 45", + "Note: F4 45", + "Note: F4 45", + "Note: F4 45", + "Note: F4 45", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations grace-notes-on-beat trill 1`] = ` +Array [ + "Note: C3 120", + "Note: C4 120", + "Note: C5 120", + "Note: C4 120", + "Note: D3 120", + "Note: D4 120", + "Note: D5 120", + "Note: D4 120", + "Note: E3 120", + "Note: E4 120", + "Note: E5 120", + "Note: E4 120", + "Note: Gb3 120", + "Note: F4 120", + "Note: F5 120", + "Note: F4 120", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations plain dot 1`] = ` +Array [ + "Note: C4 720", + "Note: D4 720", + "Note: E4 720", + "Note: F4 720", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations plain tremolo-2 1`] = ` +Array [ + "Note: C4 120", + "Note: C4 120", + "Note: C4 120", + "Note: C4 120", + "Note: D4 120", + "Note: D4 120", + "Note: D4 120", + "Note: D4 120", + "Note: E4 120", + "Note: E4 120", + "Note: E4 120", + "Note: E4 120", + "Note: F4 120", + "Note: F4 120", + "Note: F4 120", + "Note: F4 120", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations plain tremolo-3 1`] = ` +Array [ + "Note: C4 60", + "Note: C4 60", + "Note: C4 60", + "Note: C4 60", + "Note: C4 60", + "Note: C4 60", + "Note: C4 60", + "Note: C4 60", + "Note: D4 60", + "Note: D4 60", + "Note: D4 60", + "Note: D4 60", + "Note: D4 60", + "Note: D4 60", + "Note: D4 60", + "Note: D4 60", + "Note: E4 60", + "Note: E4 60", + "Note: E4 60", + "Note: E4 60", + "Note: E4 60", + "Note: E4 60", + "Note: E4 60", + "Note: E4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", + "Note: F4 60", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations plain trill 1`] = ` +Array [ + "Note: C4 120", + "Note: C5 120", + "Note: C4 120", + "Note: C5 120", + "Note: D4 120", + "Note: D5 120", + "Note: D4 120", + "Note: D5 120", + "Note: E4 120", + "Note: E5 120", + "Note: E4 120", + "Note: E5 120", + "Note: F4 120", + "Note: F5 120", + "Note: F4 120", + "Note: F5 120", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations plain tripletfeel 1`] = ` +Array [ + "Note: C4 640", + "Note: D4 320", + "Note: E4 640", + "Note: F4 320", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations plain tuplet 1`] = ` +Array [ + "Note: C4 320", + "Note: D4 320", + "Note: E4 320", + "Note: F4 320", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations triplet-feel tremolo-2 1`] = ` +Array [ + "Note: C4 160", + "Note: C4 160", + "Note: C4 160", + "Note: C4 160", + "Note: D4 80", + "Note: D4 80", + "Note: D4 80", + "Note: D4 80", + "Note: E4 160", + "Note: E4 160", + "Note: E4 160", + "Note: E4 160", + "Note: F4 80", + "Note: F4 80", + "Note: F4 80", + "Note: F4 80", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations triplet-feel tremolo-3 1`] = ` +Array [ + "Note: C4 80", + "Note: C4 80", + "Note: C4 80", + "Note: C4 80", + "Note: C4 80", + "Note: C4 80", + "Note: C4 80", + "Note: C4 80", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: E4 80", + "Note: E4 80", + "Note: E4 80", + "Note: E4 80", + "Note: E4 80", + "Note: E4 80", + "Note: E4 80", + "Note: E4 80", + "Note: F4 40", + "Note: F4 40", + "Note: F4 40", + "Note: F4 40", + "Note: F4 40", + "Note: F4 40", + "Note: F4 40", + "Note: F4 40", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations triplet-feel trill 1`] = ` +Array [ + "Note: C4 120", + "Note: C5 120", + "Note: C4 120", + "Note: C5 120", + "Note: C4 120", + "Note: C5 40", + "Note: D4 120", + "Note: D5 120", + "Note: D4 80", + "Note: E4 120", + "Note: E5 120", + "Note: E4 120", + "Note: E5 120", + "Note: E4 120", + "Note: E5 40", + "Note: F4 120", + "Note: F5 120", + "Note: F4 80", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations tuplet tremolo-2 1`] = ` +Array [ + "Note: C4 80", + "Note: C4 80", + "Note: C4 80", + "Note: C4 80", + "Note: D4 80", + "Note: D4 80", + "Note: D4 80", + "Note: D4 80", + "Note: E4 80", + "Note: E4 80", + "Note: E4 80", + "Note: E4 80", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations tuplet tremolo-3 1`] = ` +Array [ + "Note: C4 40", + "Note: C4 40", + "Note: C4 40", + "Note: C4 40", + "Note: C4 40", + "Note: C4 40", + "Note: C4 40", + "Note: C4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: D4 40", + "Note: E4 40", + "Note: E4 40", + "Note: E4 40", + "Note: E4 40", + "Note: E4 40", + "Note: E4 40", + "Note: E4 40", + "Note: E4 40", +] +`; + +exports[`MidiFileGeneratorTest effect-note-durations tuplet trill 1`] = ` +Array [ + "Note: C4 120", + "Note: C5 120", + "Note: C4 80", + "Note: D4 120", + "Note: D5 120", + "Note: D4 80", + "Note: E4 120", + "Note: E5 120", + "Note: E4 80", +] +`; diff --git a/packages/alphatab/test/exporter/AlphaTexExporterOld.test.ts b/packages/alphatab/test/exporter/AlphaTexExporterOld.test.ts deleted file mode 100644 index 665af2335..000000000 --- a/packages/alphatab/test/exporter/AlphaTexExporterOld.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; -import type { Score } from '@coderline/alphatab/model/Score'; -import { Settings } from '@coderline/alphatab/Settings'; -import { AlphaTexExporterOld } from 'test/exporter/AlphaTexExporterOld'; -import { AlphaTexError, AlphaTexImporterOld } from 'test/importer/AlphaTexImporterOld'; -import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; -import { TestPlatform } from 'test/TestPlatform'; -import { assert } from 'chai'; - -describe('AlphaTexExporterOldTest', () => { - async function loadScore(name: string): Promise { - const data = await TestPlatform.loadFile(`test-data/${name}`); - try { - return ScoreLoader.loadScoreFromBytes(data); - } catch { - return null; - } - } - - function parseAlphaTex(tex: string): Score { - const readerBase = new AlphaTexImporterOld(); - readerBase.initFromString(tex, new Settings()); - return readerBase.readScore(); - } - - function exportAlphaTex(score: Score, settings: Settings | null = null): string { - return new AlphaTexExporterOld().exportToString(score, settings); - } - - async function testRoundTripEqual(name: string, ignoreKeys: string[] | null = null): Promise { - const fileName = name.substring(name.lastIndexOf('/') + 1); - - let exported: string = ''; - try { - const expected = await loadScore(name); - if (!expected) { - return; - } - - ComparisonHelpers.alphaTexExportRoundtripPrepare(expected); - - exported = exportAlphaTex(expected); - const actual = parseAlphaTex(exported); - - ComparisonHelpers.alphaTexExportRoundtripEqual(fileName, actual, expected, ignoreKeys); - } catch (e) { - let errorLine = ''; - - const error = e as Error; - if (error.cause instanceof AlphaTexError) { - const alphaTexError = error.cause as AlphaTexError; - - const lines = exported.split('\n'); - errorLine = `Error Line: ${lines[alphaTexError.line - 1]}\n`; - } - - assert.fail(`<${fileName}>${e}\n${errorLine}${error.stack}\n Tex:\n${exported}`); - } - } - - async function testRoundTripFolderEqual(name: string): Promise { - const files: string[] = await TestPlatform.listDirectory(`test-data/${name}`); - for (const file of files.filter(f => !f.endsWith('.png'))) { - await testRoundTripEqual(`${name}/${file}`, null); - } - } - - // Note: we just test all our importer and visual tests to cover all features - - it('importer', async () => { - await testRoundTripFolderEqual('guitarpro7'); - }); - - it('visual-effects-and-annotations', async () => { - await testRoundTripFolderEqual('visual-tests/effects-and-annotations'); - }); - - it('visual-general', async () => { - await testRoundTripFolderEqual('visual-tests/general'); - }); - - it('visual-guitar-tabs', async () => { - await testRoundTripFolderEqual('visual-tests/guitar-tabs'); - }); - - it('visual-layout', async () => { - await testRoundTripFolderEqual('visual-tests/layout'); - }); - - it('visual-music-notation', async () => { - await testRoundTripFolderEqual('visual-tests/music-notation'); - }); - - it('visual-notation-legend', async () => { - await testRoundTripFolderEqual('visual-tests/notation-legend'); - }); - - it('visual-special-notes', async () => { - await testRoundTripFolderEqual('visual-tests/special-notes'); - }); - - it('visual-special-tracks', async () => { - await testRoundTripFolderEqual('visual-tests/special-tracks'); - }); - - it('gp5-to-alphaTex', async () => { - await testRoundTripEqual(`conversion/full-song.gp5`); - }); - - it('gp6-to-alphaTex', async () => { - await testRoundTripEqual(`conversion/full-song.gpx`); - }); - - it('gp7-to-alphaTex', async () => { - await testRoundTripEqual(`conversion/full-song.gp`); - }); -}); diff --git a/packages/alphatab/test/exporter/AlphaTexExporterOld.ts b/packages/alphatab/test/exporter/AlphaTexExporterOld.ts deleted file mode 100644 index c8babb884..000000000 --- a/packages/alphatab/test/exporter/AlphaTexExporterOld.ts +++ /dev/null @@ -1,1366 +0,0 @@ -/* - * This file contains a copy of the "old" alphaTex importer - * it was never released but battle tested during implementation - */ -import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; -import { Environment } from '@coderline/alphatab/Environment'; -import { ScoreExporter } from '@coderline/alphatab/exporter/ScoreExporter'; -import { IOHelper } from '@coderline/alphatab/io/IOHelper'; -import { GeneralMidi } from '@coderline/alphatab/midi/GeneralMidi'; -import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; -import { AutomationType } from '@coderline/alphatab/model/Automation'; -import { type Bar, BarLineStyle, SustainPedalMarkerType } from '@coderline/alphatab/model/Bar'; -import { BarreShape } from '@coderline/alphatab/model/BarreShape'; -import { type Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat'; -import { BendStyle } from '@coderline/alphatab/model/BendStyle'; -import { BendType } from '@coderline/alphatab/model/BendType'; -import { BrushType } from '@coderline/alphatab/model/BrushType'; -import type { Chord } from '@coderline/alphatab/model/Chord'; -import { Clef } from '@coderline/alphatab/model/Clef'; -import { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; -import { Direction } from '@coderline/alphatab/model/Direction'; -import { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; -import { FadeType } from '@coderline/alphatab/model/FadeType'; -import { FermataType } from '@coderline/alphatab/model/Fermata'; -import { Fingers } from '@coderline/alphatab/model/Fingers'; -import { GolpeType } from '@coderline/alphatab/model/GolpeType'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; -import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; -import { KeySignature } from '@coderline/alphatab/model/KeySignature'; -import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; -import type { MasterBar } from '@coderline/alphatab/model/MasterBar'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import type { Note } from '@coderline/alphatab/model/Note'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; -import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; -import { Ottavia } from '@coderline/alphatab/model/Ottavia'; -import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; -import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; -import { - BracketExtendMode, - type RenderStylesheet, - TrackNameMode, - TrackNameOrientation, - TrackNamePolicy -} from '@coderline/alphatab/model/RenderStylesheet'; -import { Score } from '@coderline/alphatab/model/Score'; -import { SimileMark } from '@coderline/alphatab/model/SimileMark'; -import { SlideInType } from '@coderline/alphatab/model/SlideInType'; -import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; -import { Staff } from '@coderline/alphatab/model/Staff'; -import { Track } from '@coderline/alphatab/model/Track'; -import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; -import { VibratoType } from '@coderline/alphatab/model/VibratoType'; -import type { Voice } from '@coderline/alphatab/model/Voice'; -import { WahPedal } from '@coderline/alphatab/model/WahPedal'; -import { WhammyType } from '@coderline/alphatab/model/WhammyType'; -import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; -import { Settings } from '@coderline/alphatab/Settings'; - -/** - * @internal - */ -class WriterGroup { - start: string = ''; - end: string = ''; - comment: string = ''; - hasContent: boolean = false; -} - -/** - * A small helper to write formatted alphaTex code to a string buffer. - * @internal - */ -class AlphaTexWriterOld { - public tex: string = ''; - public isStartOfLine: boolean = true; - public indentString: string = ''; - public currentIndent: number = 0; - - public comments: boolean = false; - - private _groups: WriterGroup[] = []; - private _singleLineComment: string = ''; - - public beginGroup(groupStart: string, groupEnd: string, comment: string = '') { - const group = new WriterGroup(); - group.start = groupStart; - group.end = groupEnd; - group.comment = comment; - this._groups.push(group); - } - - public writeSingleLineComment(text: string, onlyIfContent: boolean = false) { - if (this.comments && text) { - if (onlyIfContent) { - this._singleLineComment = `// ${text}`; - } else { - this.writeLine(`// ${text}`); - } - } - } - - public dropSingleLineComment() { - this._singleLineComment = ''; - } - - public writeInlineComment(text: string) { - if (this.comments && text) { - this.write(`/* ${text} */`); - } - } - - public endGroup() { - const topGroup = this._groups.pop()!; - if (topGroup.hasContent) { - this.write(topGroup.end); - } - } - - public indent() { - if (this.indentString.length > 0) { - this.currentIndent++; - } - } - - public outdent() { - if (this.indentString.length > 0) { - this.currentIndent--; - } - } - - private _preWrite() { - if (this._singleLineComment) { - const comment = this._singleLineComment; - this._singleLineComment = ''; - this.writeLine(comment); - } - - // indent if needed - if (this.isStartOfLine && this.indentString.length > 0) { - for (let i = 0; i < this.currentIndent; i++) { - this.tex += this.indentString; - } - } - this.isStartOfLine = false; - - if (this._singleLineComment) { - this.tex += this._singleLineComment; - } - - // start group - if (this._groups.length > 0) { - const groups = this._groups[this._groups.length - 1]; - if (!groups.hasContent) { - groups.hasContent = true; - this.tex += groups.start; - if (this.comments) { - this.writeInlineComment(groups.comment); - } - } - } - } - - public write(text: string) { - this._preWrite(); - this.tex += text; - this.isStartOfLine = false; - } - - public writeGroupItem(text: any) { - if (this._groups.length === 0) { - throw new AlphaTabError( - AlphaTabErrorType.General, - 'Wrong usage of writeGroupItem, this is an internal error.' - ); - } - - const hasContent = this._groups[this._groups.length - 1].hasContent; - this._preWrite(); - - if (hasContent) { - this.tex += ' '; - } - this.tex += text; - this.isStartOfLine = false; - } - - public writeString(text: string) { - this._preWrite(); - this.tex += Environment.quoteJsonString(text); - this.tex += ' '; - } - - public writeStringMeta(tag: string, value: string, writeIfEmpty: boolean = false) { - if (value.length === 0 && !writeIfEmpty) { - return; - } - - this._preWrite(); - this.tex += `\\${tag} `; - this.tex += Environment.quoteJsonString(value); - this.writeLine(); - } - - public writeMeta(tag: string, value?: string) { - this._preWrite(); - this.tex += `\\${tag} `; - if (value) { - this.tex += value; - } - this.writeLine(); - } - - public writeLine(text?: string) { - this._preWrite(); - if (text !== undefined) { - this.tex += text; - } - - // if not formatted, only add a space at the end - if (this.indentString.length > 0) { - this.tex += '\n'; - } else if (!this.tex.endsWith(' ')) { - this.tex += ' '; - } - this.isStartOfLine = true; - } -} - -/** - * This ScoreExporter can write alphaTex strings. - * @internal - */ -export class AlphaTexExporterOld extends ScoreExporter { - // used to lookup some default values. - private static readonly _defaultScore = new Score(); - private static readonly _defaultTrack = new Track(); - - public get name(): string { - return 'alphaTex (old)'; - } - - public exportToString(score: Score, settings: Settings | null = null) { - this.settings = settings ?? new Settings(); - return this.scoreToAlphaTexString(score); - } - - public writeScore(score: Score) { - const raw = IOHelper.stringToBytes(this.scoreToAlphaTexString(score)); - this.data.write(raw, 0, raw.length); - } - - public scoreToAlphaTexString(score: Score): string { - const writer = new AlphaTexWriterOld(); - writer.comments = this.settings.exporter.comments; - writer.indentString = this.settings.exporter.indent > 0 ? ' '.repeat(this.settings.exporter.indent) : ''; - this._writeScoreTo(writer, score); - return writer.tex; - } - - private _writeScoreTo(writer: AlphaTexWriterOld, score: Score) { - writer.writeSingleLineComment('Score Metadata'); - writer.writeStringMeta('album', score.album); - writer.writeStringMeta('artist', score.artist); - writer.writeStringMeta('copyright', score.copyright); - writer.writeStringMeta('instructions', score.instructions); - writer.writeStringMeta('music', score.music); - writer.writeStringMeta('notices', score.notices); - writer.writeStringMeta('subtitle', score.subTitle); - writer.writeStringMeta('title', score.title); - writer.writeStringMeta('words', score.words); - writer.writeStringMeta('tab', score.tab); - writer.write(`\\tempo ${score.tempo} `); - if (score.tempoLabel) { - writer.writeString(score.tempoLabel); - } - writer.writeLine(); - - if (score.defaultSystemsLayout !== AlphaTexExporterOld._defaultScore.defaultSystemsLayout) { - writer.writeMeta('defaultSystemsLayout', `${score.defaultSystemsLayout}`); - } - if (score.systemsLayout.length > 0) { - writer.writeMeta('systemsLayout', score.systemsLayout.join(' ')); - } - - this._writeStyleSheetTo(writer, score.stylesheet); - writer.writeLine('.'); - - // Unsupported: - // - style - - for (const track of score.tracks) { - writer.writeLine(); - this._writeTrackTo(writer, track); - } - - const flatSyncPoints = score.exportFlatSyncPoints(); - if (flatSyncPoints.length > 0) { - writer.writeLine('.'); - for (const p of flatSyncPoints) { - if (p.barPosition > 0) { - writer.writeMeta('sync', `${p.barIndex} ${p.barOccurence} ${p.millisecondOffset}`); - } else { - writer.writeMeta('sync', `${p.barIndex} ${p.barOccurence} ${p.millisecondOffset} ${p.barPosition}`); - } - } - } - } - - private _writeStyleSheetTo(writer: AlphaTexWriterOld, stylesheet: RenderStylesheet) { - writer.writeSingleLineComment('Score Stylesheet'); - if (stylesheet.hideDynamics) { - writer.writeMeta('hideDynamics'); - } - if (stylesheet.bracketExtendMode !== AlphaTexExporterOld._defaultScore.stylesheet.bracketExtendMode) { - writer.writeMeta('bracketExtendMode', BracketExtendMode[stylesheet.bracketExtendMode]); - } - if (stylesheet.useSystemSignSeparator) { - writer.writeMeta('useSystemSignSeparator'); - } - if (stylesheet.multiTrackMultiBarRest) { - writer.writeMeta('multiBarRest'); - } - if ( - stylesheet.singleTrackTrackNamePolicy !== - AlphaTexExporterOld._defaultScore.stylesheet.singleTrackTrackNamePolicy - ) { - writer.writeMeta('singleTrackTrackNamePolicy', TrackNamePolicy[stylesheet.singleTrackTrackNamePolicy]); - } - if ( - stylesheet.multiTrackTrackNamePolicy !== AlphaTexExporterOld._defaultScore.stylesheet.multiTrackTrackNamePolicy - ) { - writer.writeMeta('multiTrackTrackNamePolicy', TrackNamePolicy[stylesheet.multiTrackTrackNamePolicy]); - } - if (stylesheet.firstSystemTrackNameMode !== AlphaTexExporterOld._defaultScore.stylesheet.firstSystemTrackNameMode) { - writer.writeMeta('firstSystemTrackNameMode', TrackNameMode[stylesheet.firstSystemTrackNameMode]); - } - if ( - stylesheet.otherSystemsTrackNameMode !== AlphaTexExporterOld._defaultScore.stylesheet.otherSystemsTrackNameMode - ) { - writer.writeMeta('otherSystemsTrackNameMode', TrackNameMode[stylesheet.otherSystemsTrackNameMode]); - } - if ( - stylesheet.firstSystemTrackNameOrientation !== - AlphaTexExporterOld._defaultScore.stylesheet.firstSystemTrackNameOrientation - ) { - writer.writeMeta( - 'firstSystemTrackNameOrientation', - TrackNameOrientation[stylesheet.firstSystemTrackNameOrientation] - ); - } - if ( - stylesheet.otherSystemsTrackNameOrientation !== - AlphaTexExporterOld._defaultScore.stylesheet.otherSystemsTrackNameOrientation - ) { - writer.writeMeta( - 'otherSystemsTrackNameOrientation', - TrackNameOrientation[stylesheet.otherSystemsTrackNameOrientation] - ); - } - - // Unsupported: - // 'globaldisplaychorddiagramsontop', - // 'pertrackchorddiagramsontop', - // 'globaldisplaytuning', - // 'globaldisplaytuning', - // 'pertrackdisplaytuning', - // 'pertrackchorddiagramsontop', - // 'pertrackmultibarrest', - } - - private _writeTrackTo(writer: AlphaTexWriterOld, track: Track) { - writer.write('\\track '); - writer.writeString(track.name); - if (track.shortName.length > 0) { - writer.writeString(track.shortName); - } - - writer.writeLine(' {'); - writer.indent(); - - writer.writeSingleLineComment('Track Properties'); - - if (track.color.rgba !== AlphaTexExporterOld._defaultTrack.color.rgba) { - writer.write(` color `); - writer.writeString(track.color.rgba); - writer.writeLine(); - } - if (track.defaultSystemsLayout !== AlphaTexExporterOld._defaultTrack.defaultSystemsLayout) { - writer.write(` defaultSystemsLayout ${track.defaultSystemsLayout}`); - writer.writeLine(); - } - if (track.systemsLayout.length > 0) { - writer.write(` systemsLayout ${track.systemsLayout.join(' ')}`); - writer.writeLine(); - } - - writer.writeLine(` volume ${track.playbackInfo.volume}`); - writer.writeLine(` balance ${track.playbackInfo.balance}`); - - if (track.playbackInfo.isMute) { - writer.writeLine(` mute`); - } - if (track.playbackInfo.isSolo) { - writer.writeLine(` solo`); - } - - if ( - track.score.stylesheet.perTrackMultiBarRest && - track.score.stylesheet.perTrackMultiBarRest!.has(track.index) - ) { - writer.writeLine(` multibarrest`); - } - - writer.writeLine( - ` instrument ${track.isPercussion ? 'percussion' : GeneralMidi.getName(track.playbackInfo.program)}` - ); - if (track.playbackInfo.bank > 0) { - writer.writeLine(` bank ${track.playbackInfo.bank}`); - } - - writer.outdent(); - writer.writeLine('}'); - - writer.indent(); - - for (const staff of track.staves) { - this._writeStaffTo(writer, staff); - } - - // Unsupported: - // - custom percussionArticulations - // - style - - writer.outdent(); - } - - private _writeStaffTo(writer: AlphaTexWriterOld, staff: Staff) { - writer.write('\\staff '); - - writer.beginGroup('{', '}', 'Staff Properties'); - if (staff.showStandardNotation) { - if (staff.standardNotationLineCount !== Staff.DefaultStandardNotationLineCount) { - writer.writeGroupItem(`score ${staff.standardNotationLineCount}`); - } else { - writer.writeGroupItem('score'); - } - } - if (staff.showTablature) { - writer.writeGroupItem('tabs'); - } - if (staff.showSlash) { - writer.writeGroupItem('slash'); - } - if (staff.showNumbered) { - writer.writeGroupItem('numbered'); - } - writer.endGroup(); - writer.writeLine(); - - writer.indent(); - - const voiceCount = Math.max(...staff.filledVoices) + 1; - for (let v = 0; v < voiceCount; v++) { - if (voiceCount > 1) { - writer.write('\\voice '); - writer.writeInlineComment(`Voice ${v + 1}`); - writer.writeLine(); - - writer.indent(); - } - - for (const bar of staff.bars) { - this._writeBarTo(writer, bar, v); - } - - if (voiceCount > 1) { - writer.outdent(); - } - } - - // Unsupported: - // - style - - writer.outdent(); - } - - private _writeBarTo(writer: AlphaTexWriterOld, bar: Bar, voiceIndex: number) { - if (bar.index > 0) { - writer.writeLine('|'); - } - - if (voiceIndex === 0) { - let anyWritten = false; - - // Staff meta on first bar - if (bar.index === 0) { - const l = writer.tex.length; - this._writeStaffMetaTo(writer, bar.staff); - anyWritten = writer.tex.length > l; - } - - // Master Bar meta on first track - if (bar.staff.index === 0 && bar.staff.track.index === 0) { - const l = writer.tex.length; - this._writeMasterBarMetaTo(writer, bar.masterBar); - anyWritten = writer.tex.length > l; - } - - if (anyWritten) { - writer.writeLine(); - } - } - - writer.writeSingleLineComment(`Bar ${bar.index + 1}`); - writer.indent(); - this._writeBarMetaTo(writer, bar); - - // Unsupported: - // - style - - if (!bar.isEmpty) { - this._writeVoiceTo(writer, bar.voices[voiceIndex]); - } else { - writer.writeSingleLineComment(`empty bar`); - } - - writer.outdent(); - } - - private _writeStaffMetaTo(writer: AlphaTexWriterOld, staff: Staff) { - writer.writeSingleLineComment(`Staff ${staff.index + 1} Metadata`); - - if (staff.capo !== 0) { - writer.writeMeta('capo', `${staff.capo}`); - } - if (staff.isPercussion) { - writer.writeMeta('articulation', 'defaults'); - } else if (staff.isStringed) { - writer.write('\\tuning'); - for (const t of staff.stringTuning.tunings) { - writer.write(` ${Tuning.getTextForTuning(t, true)}`); - } - - if ( - staff.track.score.stylesheet.perTrackDisplayTuning && - staff.track.score.stylesheet.perTrackDisplayTuning!.has(staff.track.index) - ) { - writer.write(' hide'); - } - - if (staff.stringTuning.name.length > 0) { - writer.write(' '); - writer.writeString(staff.stringTuning.name); - } - - writer.writeLine(); - } - - if (staff.transpositionPitch !== 0) { - writer.writeMeta('transpose', `${-staff.transpositionPitch}`); - } - - const defaultTransposition = ModelUtils.displayTranspositionPitches.has(staff.track.playbackInfo.program) - ? ModelUtils.displayTranspositionPitches.get(staff.track.playbackInfo.program)! - : 0; - if (staff.displayTranspositionPitch !== defaultTransposition) { - writer.writeMeta('displaytranspose', `${-staff.displayTranspositionPitch}`); - } - - writer.writeMeta('accidentals', 'auto'); - - if (staff.chords != null) { - for (const [_, chord] of staff.chords!) { - this._writeChordTo(writer, chord); - } - } - } - - private _writeChordTo(writer: AlphaTexWriterOld, c: Chord) { - writer.write('\\chord {'); - if (c.firstFret > 0) { - writer.write(`firstfret ${c.firstFret} `); - } - writer.write(`showdiagram ${c.showDiagram ? 'true' : 'false'} `); - writer.write(`showfingering ${c.showFingering ? 'true' : 'false'} `); - writer.write(`showname ${c.showName ? 'true' : 'false'} `); - if (c.barreFrets.length > 0) { - const barre = c.barreFrets.map(f => `${f}`).join(' '); - writer.write(`barre ${barre} `); - } - writer.write('} '); - - writer.writeString(c.name); - - for (let i = 0; i < c.staff.tuning.length; i++) { - const fret = i < c.strings.length ? `${c.strings[i]} ` : `x `; - writer.write(fret); - } - writer.writeLine(); - } - - private _writeMasterBarMetaTo(writer: AlphaTexWriterOld, masterBar: MasterBar) { - writer.writeSingleLineComment(`Masterbar ${masterBar.index + 1} Metadata`, true); - - if (masterBar.alternateEndings !== 0) { - writer.write('\\ae ('); - writer.write( - ModelUtils.getAlternateEndingsList(masterBar.alternateEndings) - .map(i => i + 1) - .join(' ') - ); - writer.writeLine(')'); - } - - if (masterBar.isRepeatStart) { - writer.writeMeta('ro'); - } - - if (masterBar.isRepeatEnd) { - writer.writeMeta('rc', `${masterBar.repeatCount}`); - } - - if ( - masterBar.index === 0 || - masterBar.timeSignatureCommon !== masterBar.previousMasterBar?.timeSignatureCommon || - masterBar.timeSignatureNumerator !== masterBar.previousMasterBar.timeSignatureNumerator || - masterBar.timeSignatureDenominator !== masterBar.previousMasterBar.timeSignatureDenominator - ) { - if (masterBar.timeSignatureCommon) { - writer.writeStringMeta('ts ', 'common'); - } else { - writer.writeLine(`\\ts ${masterBar.timeSignatureNumerator} ${masterBar.timeSignatureDenominator}`); - } - } - - if ( - (masterBar.index > 0 && masterBar.tripletFeel !== masterBar.previousMasterBar?.tripletFeel) || - (masterBar.index === 0 && masterBar.tripletFeel !== TripletFeel.NoTripletFeel) - ) { - writer.writeMeta('tf', TripletFeel[masterBar.tripletFeel]); - } - - if (masterBar.isFreeTime) { - writer.writeMeta('ft'); - } - - if (masterBar.section != null) { - writer.write('\\section '); - writer.writeString(masterBar.section.marker); - writer.writeString(masterBar.section.text); - writer.writeLine(); - } - - if (masterBar.isAnacrusis) { - writer.writeMeta('ac'); - } - - if (masterBar.displayScale !== 1) { - writer.writeMeta('scale', masterBar.displayScale.toFixed(3)); - } - - if (masterBar.displayWidth > 0) { - writer.writeMeta('width', `${masterBar.displayWidth}`); - } - - if (masterBar.directions) { - for (const d of masterBar.directions!) { - let jumpValue: string = Direction[d]; - if (jumpValue.startsWith('Target')) { - jumpValue = jumpValue.substring('Target'.length); - } else if (jumpValue.startsWith('Jump')) { - jumpValue = jumpValue.substring('Jump'.length); - } - writer.writeMeta('jump', jumpValue); - } - } - - for (const a of masterBar.tempoAutomations) { - writer.write(`\\tempo ( ${a.value} `); - if (a.text) { - writer.writeString(a.text); - } - writer.write(`${a.ratioPosition} `); - if (!a.isVisible) { - writer.write('hide '); - } - writer.writeLine(`)`); - } - - writer.dropSingleLineComment(); - } - - private _writeBarMetaTo(writer: AlphaTexWriterOld, bar: Bar) { - writer.writeSingleLineComment(`Bar ${bar.index + 1} Metadata`, true); - const l = writer.tex.length; - - if (bar.index === 0 || bar.clef !== bar.previousBar?.clef) { - writer.writeMeta('clef', Clef[bar.clef]); - } - - if ((bar.index === 0 && bar.clefOttava !== Ottavia.Regular) || bar.clefOttava !== bar.previousBar?.clefOttava) { - let ottava = Ottavia[bar.clefOttava]; - if (ottava.startsWith('_')) { - ottava = ottava.substring(1); - } - writer.writeMeta('ottava', ottava); - } - - if ((bar.index === 0 && bar.simileMark !== SimileMark.None) || bar.simileMark !== bar.previousBar?.simileMark) { - writer.writeMeta('simile', SimileMark[bar.simileMark]); - } - - if (bar.displayScale !== 1) { - writer.writeMeta('scale', bar.displayScale.toFixed(3)); - } - - if (bar.displayWidth > 0) { - writer.writeMeta('width', `${bar.displayWidth}`); - } - - // sustainPedals are on beat level - for (const sp of bar.sustainPedals) { - switch (sp.pedalType) { - case SustainPedalMarkerType.Down: - writer.writeMeta('spd', `${sp.ratioPosition}`); - break; - case SustainPedalMarkerType.Hold: - writer.writeMeta('sph', `${sp.ratioPosition}`); - break; - case SustainPedalMarkerType.Up: - writer.writeMeta('spu', `${sp.ratioPosition}`); - break; - } - } - - if (bar.barLineLeft !== BarLineStyle.Automatic) { - writer.writeMeta('barlineleft', BarLineStyle[bar.barLineLeft]); - } - - if (bar.barLineRight !== BarLineStyle.Automatic) { - writer.writeMeta('barlineright', BarLineStyle[bar.barLineRight]); - } - - if ( - bar.index === 0 || - bar.keySignature !== bar.previousBar!.keySignature || - bar.keySignatureType !== bar.previousBar!.keySignatureType - ) { - let ks = ''; - if (bar.keySignatureType === KeySignatureType.Minor) { - switch (bar.keySignature) { - case KeySignature.Cb: - ks = 'abminor'; - break; - case KeySignature.Gb: - ks = 'ebminor'; - break; - case KeySignature.Db: - ks = 'bbminor'; - break; - case KeySignature.Ab: - ks = 'fminor'; - break; - case KeySignature.Eb: - ks = 'cminor'; - break; - case KeySignature.Bb: - ks = 'gminor'; - break; - case KeySignature.F: - ks = 'dminor'; - break; - case KeySignature.C: - ks = 'aminor'; - break; - case KeySignature.G: - ks = 'eminor'; - break; - case KeySignature.D: - ks = 'bminor'; - break; - case KeySignature.A: - ks = 'f#minor'; - break; - case KeySignature.E: - ks = 'c#minor'; - break; - case KeySignature.B: - ks = 'g#minor'; - break; - case KeySignature.FSharp: - ks = 'd#minor'; - break; - case KeySignature.CSharp: - ks = 'a#minor'; - break; - default: - // fallback to major - ks = KeySignature[bar.keySignature]; - break; - } - } else { - switch (bar.keySignature) { - case KeySignature.FSharp: - ks = 'f#'; - break; - case KeySignature.CSharp: - ks = 'c#'; - break; - default: - ks = KeySignature[bar.keySignature]; - break; - } - } - writer.writeStringMeta('ks', ks); - } - - if (writer.tex.length > l) { - writer.writeLine(); - } - - writer.dropSingleLineComment(); - } - - private _writeVoiceTo(writer: AlphaTexWriterOld, voice: Voice) { - if (voice.isEmpty) { - writer.writeSingleLineComment(`empty voice`); - return; - } - - writer.writeSingleLineComment(`Bar ${voice.bar.index + 1} / Voice ${voice.index + 1} contents`); - - // Unsupported: - // - style - - for (const beat of voice.beats) { - this._writeBeatTo(writer, beat); - } - } - - private _writeBeatTo(writer: AlphaTexWriterOld, beat: Beat) { - // Notes - if (beat.isRest) { - writer.write('r'); - } else if (beat.notes.length === 0) { - writer.write('()'); - } else { - if (beat.notes.length > 1) { - writer.write('('); - } - - for (const note of beat.notes) { - if (note.index > 0) { - writer.write(' '); - } - this._writeNoteTo(writer, note); - } - - if (beat.notes.length > 1) { - writer.write(')'); - } - } - - writer.write(`.${beat.duration as number}`); - - // Unsupported: - // - style - - this._writeBeatEffectsTo(writer, beat); - - writer.writeLine(); - } - - private _writeBeatEffectsTo(writer: AlphaTexWriterOld, beat: Beat) { - writer.beginGroup('{', '}'); - - switch (beat.fade) { - case FadeType.FadeIn: - writer.writeGroupItem('f'); - break; - case FadeType.FadeOut: - writer.writeGroupItem('fo'); - break; - case FadeType.VolumeSwell: - writer.writeGroupItem('vs'); - break; - } - - if (beat.vibrato === VibratoType.Slight) { - writer.writeGroupItem('v'); - } else if (beat.vibrato === VibratoType.Wide) { - writer.writeGroupItem('vw'); - } - - if (beat.slap) { - writer.writeGroupItem('s'); - } - - if (beat.pop) { - writer.writeGroupItem('p'); - } - - if (beat.tap) { - writer.writeGroupItem('tt'); - } - - if (beat.dots >= 2) { - writer.writeGroupItem('dd'); - } else if (beat.dots > 0) { - writer.writeGroupItem('d'); - } - - if (beat.pickStroke === PickStroke.Up) { - writer.writeGroupItem('su'); - } else if (beat.pickStroke === PickStroke.Down) { - writer.writeGroupItem('sd'); - } - - if (beat.hasTuplet) { - writer.writeGroupItem(`tu ${beat.tupletNumerator} ${beat.tupletDenominator}`); - } - - if (beat.hasWhammyBar) { - writer.writeGroupItem('tbe'); - switch (beat.whammyBarType) { - case WhammyType.Custom: - writer.writeGroupItem('custom'); - break; - case WhammyType.Dive: - writer.writeGroupItem('dive'); - break; - case WhammyType.Dip: - writer.writeGroupItem('dip'); - break; - case WhammyType.Hold: - writer.writeGroupItem('hold'); - break; - case WhammyType.Predive: - writer.writeGroupItem('predive'); - break; - case WhammyType.PrediveDive: - writer.writeGroupItem('predivedive'); - break; - } - - switch (beat.whammyStyle) { - case BendStyle.Default: - break; - case BendStyle.Gradual: - writer.writeGroupItem('gradual'); - break; - case BendStyle.Fast: - writer.writeGroupItem('fast'); - break; - } - - writer.beginGroup('(', ')'); - - for (const p of beat.whammyBarPoints!) { - writer.writeGroupItem(` ${p.offset} ${p.value}`); - } - - writer.endGroup(); - } - - switch (beat.brushType) { - case BrushType.BrushUp: - writer.writeGroupItem(`bu ${beat.brushDuration}`); - break; - case BrushType.BrushDown: - writer.writeGroupItem(`bd ${beat.brushDuration}`); - break; - case BrushType.ArpeggioUp: - writer.writeGroupItem(`au ${beat.brushDuration}`); - break; - case BrushType.ArpeggioDown: - writer.writeGroupItem(`ad ${beat.brushDuration}`); - break; - } - - if (beat.chord != null) { - writer.writeGroupItem('ch '); - writer.writeString(beat.chord.name); - } - - if (beat.ottava !== Ottavia.Regular) { - let ottava = Ottavia[beat.ottava]; - if (ottava.startsWith('_')) { - ottava = ottava.substring(1); - } - - writer.writeGroupItem(`ot ${ottava}`); - } - - if (beat.hasRasgueado) { - writer.writeGroupItem(`rasg ${Rasgueado[beat.rasgueado]}`); - } - - if (beat.text != null) { - writer.writeGroupItem('txt '); - writer.writeString(beat.text); - } - - if (beat.lyrics != null && beat.lyrics!.length > 0) { - if (beat.lyrics.length > 1) { - for (let i = 0; i < beat.lyrics.length; i++) { - writer.writeGroupItem(`lyrics ${i} `); - writer.writeString(beat.lyrics[i]); - } - } else { - writer.writeGroupItem('lyrics '); - writer.writeString(beat.lyrics[0]); - } - } - - switch (beat.graceType) { - case GraceType.OnBeat: - writer.writeGroupItem('gr ob'); - break; - case GraceType.BeforeBeat: - writer.writeGroupItem('gr'); - break; - case GraceType.BendGrace: - writer.writeGroupItem('gr b'); - break; - } - - if (beat.isTremolo) { - writer.writeGroupItem(`tp ${beat.tremoloSpeed as number}`); - } - - switch (beat.crescendo) { - case CrescendoType.Crescendo: - writer.writeGroupItem('cre'); - break; - case CrescendoType.Decrescendo: - writer.writeGroupItem('dec'); - break; - } - - if ((beat.voice.bar.index === 0 && beat.index === 0) || beat.dynamics !== beat.previousBeat?.dynamics) { - writer.writeGroupItem(`dy ${DynamicValue[beat.dynamics].toLowerCase()}`); - } - - const fermata = beat.fermata; - if (fermata != null) { - writer.writeGroupItem(`fermata ${FermataType[fermata.type]} ${fermata.length}`); - } - - if (beat.isLegatoOrigin) { - writer.writeGroupItem('legatoorigin'); - } - - for (const automation of beat.automations) { - switch (automation.type) { - case AutomationType.Tempo: - writer.writeGroupItem(`tempo ${automation.value}`); - if (automation.text.length > 0) { - writer.write(' '); - writer.writeString(automation.text); - } - break; - case AutomationType.Volume: - writer.writeGroupItem(`volume ${automation.value}`); - break; - case AutomationType.Instrument: - if (!beat.voice.bar.staff.isPercussion) { - writer.writeGroupItem(`instrument ${GeneralMidi.getName(automation.value)}`); - } - break; - case AutomationType.Balance: - writer.writeGroupItem(`balance ${automation.value}`); - break; - } - } - - switch (beat.wahPedal) { - case WahPedal.Open: - writer.writeGroupItem(`waho`); - break; - case WahPedal.Closed: - writer.writeGroupItem(`wahc`); - break; - } - - if (beat.isBarre) { - writer.writeGroupItem(`barre ${beat.barreFret} ${BarreShape[beat.barreShape]}`); - } - - if (beat.slashed) { - writer.writeGroupItem(`slashed`); - } - - if (beat.deadSlapped) { - writer.writeGroupItem(`ds`); - } - - switch (beat.golpe) { - case GolpeType.Thumb: - writer.writeGroupItem(`glpt`); - break; - case GolpeType.Finger: - writer.writeGroupItem(`glpf`); - break; - } - - if (beat.invertBeamDirection) { - writer.writeGroupItem('beam invert'); - } else if (beat.preferredBeamDirection !== null) { - writer.writeGroupItem(`beam ${BeamDirection[beat.preferredBeamDirection!]}`); - } - - if (beat.beamingMode !== BeatBeamingMode.Auto) { - switch (beat.beamingMode) { - case BeatBeamingMode.ForceSplitToNext: - writer.writeGroupItem(`beam split`); - break; - case BeatBeamingMode.ForceMergeWithNext: - writer.writeGroupItem(`beam merge`); - break; - case BeatBeamingMode.ForceSplitOnSecondaryToNext: - writer.writeGroupItem(`beam splitsecondary`); - break; - } - } - - if (beat.showTimer) { - writer.writeGroupItem(`timer`); - } - - writer.endGroup(); - } - - private _writeNoteTo(writer: AlphaTexWriterOld, note: Note) { - if (note.index > 0) { - writer.write(' '); - } - - if (note.isPercussion) { - writer.writeString(PercussionMapper.getArticulationName(note)); - } else if (note.isPiano) { - writer.write(Tuning.getTextForTuning(note.realValueWithoutHarmonic, true)); - } else if (note.isStringed) { - writer.write(`${note.fret}`); - const stringNumber = note.beat.voice.bar.staff.tuning.length - note.string + 1; - writer.write(`.${stringNumber}`); - } else { - throw new Error('What kind of note'); - } - - // Unsupported: - // - style - - this._writeNoteEffectsTo(writer, note); - } - - private _writeNoteEffectsTo(writer: AlphaTexWriterOld, note: Note) { - writer.beginGroup('{', '}'); - - if (note.hasBend) { - writer.writeGroupItem(`be ${BendType[note.bendType]}`); - - if (note.bendStyle !== BendStyle.Default) { - writer.writeGroupItem(`${BendStyle[note.bendStyle]} `); - } - - writer.beginGroup('(', ')'); - - for (const p of note.bendPoints!) { - writer.writeGroupItem(`${p.offset} ${p.value}`); - } - - writer.endGroup(); - } - - switch (note.harmonicType) { - case HarmonicType.Natural: - writer.writeGroupItem('nh'); - break; - case HarmonicType.Artificial: - writer.writeGroupItem(`ah ${note.harmonicValue}`); - break; - case HarmonicType.Pinch: - writer.writeGroupItem(`ph ${note.harmonicValue}`); - break; - case HarmonicType.Tap: - writer.writeGroupItem(`th ${note.harmonicValue}`); - break; - case HarmonicType.Semi: - writer.writeGroupItem(`sh ${note.harmonicValue}`); - break; - case HarmonicType.Feedback: - writer.writeGroupItem(`fh ${note.harmonicValue}`); - break; - } - - if (note.showStringNumber) { - writer.writeGroupItem(`string`); - } - - if (note.isTrill) { - writer.writeGroupItem(`tr ${note.trillFret} ${note.trillSpeed as number}`); - } - - switch (note.vibrato) { - case VibratoType.Slight: - writer.writeGroupItem('v'); - break; - case VibratoType.Wide: - writer.writeGroupItem('vw'); - break; - } - - switch (note.slideInType) { - case SlideInType.IntoFromBelow: - writer.writeGroupItem('sib'); - break; - case SlideInType.IntoFromAbove: - writer.writeGroupItem('sia'); - break; - } - - switch (note.slideOutType) { - case SlideOutType.Shift: - writer.writeGroupItem('ss'); - break; - case SlideOutType.Legato: - writer.writeGroupItem('sl'); - break; - case SlideOutType.OutUp: - writer.writeGroupItem('sou'); - break; - case SlideOutType.OutDown: - writer.writeGroupItem('sod'); - break; - case SlideOutType.PickSlideDown: - writer.writeGroupItem('psd'); - break; - case SlideOutType.PickSlideUp: - writer.writeGroupItem('psu'); - break; - } - - if (note.isHammerPullOrigin) { - writer.writeGroupItem('h'); - } - - if (note.isLeftHandTapped) { - writer.writeGroupItem('lht'); - } - - if (note.isGhost) { - writer.writeGroupItem('g'); - } - - switch (note.accentuated) { - case AccentuationType.Normal: - writer.writeGroupItem('ac'); - break; - case AccentuationType.Heavy: - writer.writeGroupItem('hac'); - break; - case AccentuationType.Tenuto: - writer.writeGroupItem('ten'); - break; - } - - if (note.isPalmMute) { - writer.writeGroupItem('pm'); - } - - if (note.isStaccato) { - writer.writeGroupItem('st'); - } - - if (note.isLetRing) { - writer.writeGroupItem('lr'); - } - - if (note.isDead) { - writer.writeGroupItem('x'); - } - - if (note.isTieDestination) { - writer.writeGroupItem('t'); - } - - switch (note.leftHandFinger) { - case Fingers.Thumb: - writer.writeGroupItem('lf 1'); - break; - case Fingers.IndexFinger: - writer.writeGroupItem('lf 2'); - break; - case Fingers.MiddleFinger: - writer.writeGroupItem('lf 3'); - break; - case Fingers.AnnularFinger: - writer.writeGroupItem('lf 4'); - break; - case Fingers.LittleFinger: - writer.writeGroupItem('lf 5'); - break; - } - - switch (note.rightHandFinger) { - case Fingers.Thumb: - writer.writeGroupItem('rf 1'); - break; - case Fingers.IndexFinger: - writer.writeGroupItem('rf 2'); - break; - case Fingers.MiddleFinger: - writer.writeGroupItem('rf 3'); - break; - case Fingers.AnnularFinger: - writer.writeGroupItem('rf 4'); - break; - case Fingers.LittleFinger: - writer.writeGroupItem('rf 5'); - break; - } - - if (!note.isVisible) { - writer.writeGroupItem('hide'); - } - - if (note.isSlurOrigin) { - const slurId = `s${note.id}`; - writer.writeGroupItem(`slur ${slurId}`); - } - - if (note.isSlurDestination) { - const slurId = `s${note.slurOrigin!.id}`; - writer.writeGroupItem(`slur ${slurId}`); - } - - if (note.isTrill) { - writer.writeGroupItem(`tr ${note.trillFret} ${note.trillSpeed as number}`); - } - - if (note.accidentalMode !== NoteAccidentalMode.Default) { - writer.writeGroupItem(`acc ${NoteAccidentalMode[note.accidentalMode]}`); - } - - switch (note.ornament) { - case NoteOrnament.InvertedTurn: - writer.writeGroupItem(`iturn`); - break; - case NoteOrnament.Turn: - writer.writeGroupItem(`turn`); - break; - case NoteOrnament.UpperMordent: - writer.writeGroupItem(`umordent`); - break; - case NoteOrnament.LowerMordent: - writer.writeGroupItem(`lmordent`); - break; - } - - writer.endGroup(); - } -} diff --git a/packages/alphatab/test/exporter/Gp7Exporter.test.ts b/packages/alphatab/test/exporter/Gp7Exporter.test.ts index 21cd7af24..a68cf0334 100644 --- a/packages/alphatab/test/exporter/Gp7Exporter.test.ts +++ b/packages/alphatab/test/exporter/Gp7Exporter.test.ts @@ -1,13 +1,24 @@ import { Gp7Exporter } from '@coderline/alphatab/exporter/Gp7Exporter'; +import { + GpifInstrumentArticulation, + GpifInstrumentElement, + GpifInstrumentSet +} from '@coderline/alphatab/exporter/GpifSoundMapper'; import { Gp7To8Importer } from '@coderline/alphatab/importer/Gp7To8Importer'; +import { GpifParser } from '@coderline/alphatab/importer/GpifParser'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; +import { IOHelper } from '@coderline/alphatab/io/IOHelper'; +import { TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentArticulation'; import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Score } from '@coderline/alphatab/model/Score'; import { Settings } from '@coderline/alphatab/Settings'; +import { XmlDocument } from '@coderline/alphatab/xml/XmlDocument'; +import { ZipReader } from '@coderline/alphatab/zip/ZipReader'; +import { assert, expect } from 'chai'; import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; import { TestPlatform } from 'test/TestPlatform'; -import { expect } from 'chai'; describe('Gp7ExporterTest', () => { async function loadScore(name: string): Promise { @@ -78,7 +89,7 @@ describe('Gp7ExporterTest', () => { }); it('visual-layout', async () => { - await testRoundTripFolderEqual('visual-tests/layout'); + await testRoundTripFolderEqual('visual-tests/layout', ['extended-barlines.xml']); }); it('visual-music-notation', async () => { @@ -159,7 +170,7 @@ describe('Gp7ExporterTest', () => { ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '', ['accidentalmode']); - expect(actual.tracks[0].percussionArticulations).to.have.length(2); + expect(actual.tracks[0].percussionArticulations.length).to.equal(2); expect(actual.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(0); expect(actual.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); expect(actual.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(0); @@ -171,9 +182,324 @@ describe('Gp7ExporterTest', () => { }); it('gp8', async () => { - await testRoundTripFolderEqual('guitarpro8', undefined, [ - 'bendpoints', - 'bendtype', - ]); + await testRoundTripFolderEqual('guitarpro8', undefined, ['bendpoints', 'bendtype']); + }); + + /** + * This test generates the articulations code needed for the PercussionMapper. + * To update the code there, run this test and copy the source code from the written file. + * The test will fail and write a ".new" file if the code changed. + */ + it('percussion-articulations', async () => { + const settings = new Settings(); + const zip = new ZipReader( + ByteBuffer.fromBuffer(await TestPlatform.loadFile('test-data/exporter/articulations.gp')) + ).read(); + const gpifData = zip.find(e => e.fileName === 'score.gpif')!.data; + + const xml = new XmlDocument(); + xml.parse(IOHelper.toString(gpifData, settings.importer.encoding)); + + const instrumentSet = readFullInstrumentSet(xml); + + let instrumentArticulationsLookup = + 'public static instrumentArticulations: Map = new Map(\n'; + instrumentArticulationsLookup += ' [\n'; + + let instrumentArticulationNames = 'private static _instrumentArticulationNames = new Map([\n'; + + const nameCounter = new Map(); + + for (const element of instrumentSet.elements) { + for (const a of element.articulations) { + instrumentArticulationsLookup += ` InstrumentArticulation.create(`; + instrumentArticulationsLookup += `${a.inputMidiNumbers[0]}, `; + instrumentArticulationsLookup += `${JSON.stringify(element.name)}, `; + instrumentArticulationsLookup += `${a.staffLine}, `; + instrumentArticulationsLookup += `${a.outputMidiNumber}, `; + instrumentArticulationsLookup += `MusicFontSymbol.${MusicFontSymbol[a.noteHeads[0]]}, `; + instrumentArticulationsLookup += `MusicFontSymbol.${MusicFontSymbol[a.noteHeads[1]]}, `; + instrumentArticulationsLookup += `MusicFontSymbol.${MusicFontSymbol[a.noteHeads[2]]}`; + if (a.techniqueSymbol !== MusicFontSymbol.None) { + instrumentArticulationsLookup += `, MusicFontSymbol.${MusicFontSymbol[a.techniqueSymbol]}, `; + instrumentArticulationsLookup += `TechniqueSymbolPlacement.${TechniqueSymbolPlacement[a.techniqueSymbolPlacement]}`; + } + instrumentArticulationsLookup += `),\n`; + + let name = a.name; + if (nameCounter.has(name)) { + const newCount = nameCounter.get(name)! + 1; + name += ` ${newCount}`; + nameCounter.set(name, newCount); + } else { + nameCounter.set(name, 1); + } + + const uniqueId = `${element.name}.${a.inputMidiNumbers[0]}`; + instrumentArticulationNames += ` [${JSON.stringify(name)}, ${`${JSON.stringify(uniqueId)}`}],\n`; + } + } + + instrumentArticulationsLookup += ' ].map(articulation => [articulation.uniqueId, articulation])'; + instrumentArticulationsLookup += ');'; + instrumentArticulationNames += ']);'; + + const sourceCode = [ + '// BEGIN generated articulations', + instrumentArticulationsLookup, + '', + instrumentArticulationNames, + '// END generated articulations' + ].join('\n'); + + const expected = await TestPlatform.loadFileAsString('test-data/exporter/articulations.source'); + if (expected !== sourceCode) { + await TestPlatform.saveFileAsString('test-data/exporter/articulations.source.new', sourceCode); + assert.fail('Articulations have changed, update the PercussionMapper and update the snapshot file'); + } + }); + + // NOTE: this function could be useful in future if we want to use a real .gp file as "template" + function readFullInstrumentSet(xml: XmlDocument) { + const instrumentSetNode = xml + .findChildElement('GPIF')! + .findChildElement('Tracks')! + .findChildElement('Track')! + .findChildElement('InstrumentSet')!; + + const instrumentSet = new GpifInstrumentSet(); + + instrumentSet.name = instrumentSetNode.findChildElement('Name')!.innerText; + instrumentSet.type = instrumentSetNode.findChildElement('Type')!.innerText; + instrumentSet.lineCount = Number.parseInt(instrumentSetNode.findChildElement('LineCount')!.innerText, 10); + + for (const elementNode of instrumentSetNode.findChildElement('Elements')!.childElements()) { + if (elementNode.localName !== 'Element') { + continue; + } + + const element = new GpifInstrumentElement( + elementNode.findChildElement('Name')!.innerText, + elementNode.findChildElement('Type')!.innerText, + elementNode.findChildElement('SoundbankName')!.innerText, + [] + ); + + for (const articulationNode of elementNode.findChildElement('Articulations')!.childElements()) { + if (articulationNode.localName !== 'Articulation') { + continue; + } + + const articulation = new GpifInstrumentArticulation( + articulationNode.findChildElement('Name')!.innerText, + Number.parseInt(articulationNode.findChildElement('StaffLine')!.innerText, 10), + articulationNode + .findChildElement('Noteheads')! + .innerText.split(' ') + .map(t => GpifParser.parseNoteHead(t)), + GpifParser.parseTechniqueSymbol(articulationNode.findChildElement('TechniqueSymbol')!.innerText), + GpifParser.parseTechniqueSymbolPlacement( + articulationNode.findChildElement('TechniquePlacement')!.innerText + ), + articulationNode + .findChildElement('InputMidiNumbers')! + .innerText.split(' ') + .map(t => Number.parseInt(t, 10)), + Number.parseInt(articulationNode.findChildElement('OutputMidiNumber')!.innerText, 10), + articulationNode.findChildElement('OutputRSESound')!.innerText + ); + + element.articulations.push(articulation); + } + + instrumentSet.elements.push(element); + } + + // we also have to apply the instrument patches + // this is a bit duplicate from what we already do in the GpifParser but test-focused + const notationPatchNode = xml + .findChildElement('GPIF')! + .findChildElement('Tracks')! + .findChildElement('Track')! + .findChildElement('NotationPatch'); + + if (notationPatchNode) { + for (const c of notationPatchNode.childElements()) { + switch (c.localName) { + case 'LineCount': + instrumentSet.lineCount = Number.parseInt(c.innerText, 10); + break; + case 'Elements': + for (const e of c.childElements()) { + switch (e.localName) { + case 'Element': + const elementToPatch = instrumentSet.elements.find( + x => x.name === e.findChildElement('Name')!.innerText + ); + + for (const a of e.findChildElement('Articulations')!.childElements()) { + const name = a.findChildElement('Name')!.innerText; + const articulationToPatch = elementToPatch!.articulations.find( + p => p.name === name + )!; + + for (const ac of a.childElements()) { + switch (ac.localName) { + case 'StaffLine': + articulationToPatch.staffLine = Number.parseInt(ac.innerText, 10); + break; + } + } + } + + break; + } + } + break; + } + } + } + + return instrumentSet; + } + + /** + * This test generates the RSE mapping information for the exporter. + * To update the code there, run this test and copy the source code from the written file. + * The test will fail and write a ".new" file if the code changed. + */ + it('sound-mapper', async () => { + const settings = new Settings(); + const zip = new ZipReader( + ByteBuffer.fromBuffer(await TestPlatform.loadFile('test-data/exporter/articulations.gp')) + ).read(); + const gpifData = zip.find(e => e.fileName === 'score.gpif')!.data; + + const xml = new XmlDocument(); + xml.parse(IOHelper.toString(gpifData, settings.importer.encoding)); + + let instrumentSetCode = 'private static _drumInstrumentSet = GpifInstrumentSet.create('; + + const instrumentSet = readFullInstrumentSet(xml); + + instrumentSetCode += `${JSON.stringify(instrumentSet.name)}, `; + instrumentSetCode += `${JSON.stringify(instrumentSet.type)}, `; + instrumentSetCode += `${instrumentSet.lineCount.toString()}, [\n`; + + for (const element of instrumentSet.elements) { + instrumentSetCode += ` new GpifInstrumentElement(`; + instrumentSetCode += `${JSON.stringify(element.name)}, `; + instrumentSetCode += `${JSON.stringify(element.type)}, `; + instrumentSetCode += `${JSON.stringify(element.soundbankName)}, `; + instrumentSetCode += `[\n`; + + for (const articulation of element.articulations) { + instrumentSetCode += ' GpifInstrumentArticulation.template('; + instrumentSetCode += `${JSON.stringify(articulation.name)}, `; + instrumentSetCode += `[${articulation.inputMidiNumbers.map(n => n.toString()).join(', ')}], `; + instrumentSetCode += `${JSON.stringify(articulation.outputRSESound)}`; + instrumentSetCode += '),\n'; + } + + instrumentSetCode += ` ]),\n`; + } + + instrumentSetCode += `]);`; + + const sourceCode = ['// BEGIN generated', instrumentSetCode, '// END generated'].join('\n'); + + const expected = await TestPlatform.loadFileAsString('test-data/exporter/soundmapper.source'); + if (expected !== sourceCode) { + await TestPlatform.saveFileAsString('test-data/exporter/soundmapper.source.new', sourceCode); + assert.fail('RSE instrument set has, update the GpifSoundMapper and update the snapshot file'); + } + }); + + function getInstrumentSet(gp: Uint8Array) { + const zip = new ZipReader(ByteBuffer.fromBuffer(gp)); + const gpifData = zip.read().find(e => e.fileName === 'score.gpif')!.data; + const xml = new XmlDocument(); + xml.parse(IOHelper.toString(gpifData, '')); + return readFullInstrumentSet(xml); + } + + it('drumkit-roundtrip', async () => { + const inputData = await TestPlatform.loadFile('test-data/exporter/articulations.gp'); + const loaded = ScoreLoader.loadScoreFromBytes(inputData); + + const exported = new Gp7Exporter().export(loaded); + + const expectedInstrumentSet = getInstrumentSet(inputData); + const actualInstrumentSet = getInstrumentSet(exported); + + // order IS important for the elements and articulations. the InstrumentArticulation is index based. + expect(actualInstrumentSet.name).to.equal(expectedInstrumentSet.name); + expect(actualInstrumentSet.type).to.equal(expectedInstrumentSet.type); + expect(actualInstrumentSet.lineCount).to.equal(expectedInstrumentSet.lineCount); + + const expectedElements = Array.from(expectedInstrumentSet.elements); + const actualElements = Array.from(actualInstrumentSet.elements); + + for (let i = 0; i < expectedElements.length; i++) { + const expectedElement = expectedElements[i]; + expect(actualElements.length).to.be.greaterThan( + i, + `Element ${i} (${expectedElement.name}) missing in actual file` + ); + const actualElement = actualElements[i]; + + expect(actualElement.name).to.equal(expectedElement.name); + expect(actualElement.type).to.equal(expectedElement.type); + expect(actualElement.soundbankName).to.equal(expectedElement.soundbankName); + + for (let j = 0; j < expectedElement.articulations.length; j++) { + const expectedArticulation = expectedElement.articulations[j]; + expect(actualElement.articulations.length).to.be.greaterThan( + j, + `Articulation ${i} missing in actual file` + ); + + const actualArticulation = actualElement.articulations[j]; + + expect(actualArticulation.name).to.equal(expectedArticulation.name); + expect(actualArticulation.staffLine).to.equal( + expectedArticulation.staffLine, + `Wrong staffline for articulation ${actualArticulation.name}` + ); + expect(actualArticulation.noteHeads.map(s => MusicFontSymbol[s]).join(' ')).to.equal( + expectedArticulation.noteHeads.map(s => MusicFontSymbol[s]).join(' '), + `Wrong noteHeads for articulation ${actualArticulation.name}` + ); + expect(MusicFontSymbol[actualArticulation.techniqueSymbol]).to.equal( + MusicFontSymbol[expectedArticulation.techniqueSymbol], + `Wrong techniqueSymbol for articulation ${actualArticulation.name}` + ); + expect(TechniqueSymbolPlacement[actualArticulation.techniqueSymbolPlacement]).to.equal( + TechniqueSymbolPlacement[expectedArticulation.techniqueSymbolPlacement], + `Wrong techniqueSymbolPlacement for articulation ${actualArticulation.name}` + ); + expect(actualArticulation.inputMidiNumbers.map(i => i.toString()).join(',')).to.equal( + expectedArticulation.inputMidiNumbers.map(i => i.toString()).join(','), + `Wrong inputMidiNumbers for articulation ${actualArticulation.name}` + ); + expect(actualArticulation.outputMidiNumber).to.equal( + expectedArticulation.outputMidiNumber, + `Wrong outputMidiNumber for articulation ${actualArticulation.name}` + ); + expect(actualArticulation.outputRSESound).to.equal( + expectedArticulation.outputRSESound, + `Wrong outputRSESound for articulation ${actualArticulation.name}` + ); + } + + expect(actualElement.articulations.length).to.equal( + expectedElement.articulations.length, + `articulation length mismatch on element ${expectedElement.name}` + ); + } + + expect(actualInstrumentSet.elements.length).to.equal(expectedInstrumentSet.elements.length); + + // await TestPlatform.saveFile('test-data/exporter/articulations.exported.gp', exported); }); }); diff --git a/packages/alphatab/test/global-hooks.ts b/packages/alphatab/test/global-hooks.ts index 45f24d046..1aaddd00f 100644 --- a/packages/alphatab/test/global-hooks.ts +++ b/packages/alphatab/test/global-hooks.ts @@ -1,6 +1,7 @@ /** @target web */ import * as chai from 'chai'; import { afterAll, beforeEachTest, initializeJestSnapshot } from './mocha.jest-snapshot'; +import { TestPlatform } from 'test/TestPlatform'; export const mochaHooks = { async beforeAll() { @@ -10,6 +11,7 @@ export const mochaHooks = { beforeEach: function (done) { beforeEachTest(this.currentTest!); + TestPlatform.currentTestName = this.currentTest!.title; done(); }, diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts index 1dbb79a23..1b8308f87 100644 --- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts @@ -27,6 +27,7 @@ import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; import { + BarNumberDisplay, BracketExtendMode, TrackNameMode, TrackNameOrientation, @@ -43,7 +44,6 @@ import { Tuning } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { WhammyType } from '@coderline/alphatab/model/WhammyType'; import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; -import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; import { HarmonicsEffectInfo } from '@coderline/alphatab/rendering/effects/HarmonicsEffectInfo'; import { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { Settings } from '@coderline/alphatab/Settings'; @@ -51,6 +51,9 @@ import { StaveProfile } from '@coderline/alphatab/StaveProfile'; import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; import { assert, expect } from 'chai'; +import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; +import { TremoloPickingEffectSerializer } from '@coderline/alphatab/generated/model/TremoloPickingEffectSerializer'; +import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; describe('AlphaTexImporterTest', () => { /** @@ -235,7 +238,7 @@ describe('AlphaTexImporterTest', () => { expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(3); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].isTremolo).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].tremoloSpeed).to.equal(Duration.Sixteenth); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].tremoloPicking!.marks).to.equal(2); }); it('brushes-arpeggio', () => { @@ -1389,9 +1392,9 @@ describe('AlphaTexImporterTest', () => { expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(0); expect(score.tracks[0].percussionArticulations[0].outputMidiNumber).to.equal(49); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); - expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(40); + expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(37); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(2); - expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(37); + expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(40); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(3); expect(score.tracks[0].percussionArticulations[3].outputMidiNumber).to.equal(38); testExportRoundtrip(score); @@ -1409,9 +1412,9 @@ describe('AlphaTexImporterTest', () => { expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(0); expect(score.tracks[0].percussionArticulations[0].outputMidiNumber).to.equal(49); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); - expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(40); + expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(37); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(2); - expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(37); + expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(40); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(3); expect(score.tracks[0].percussionArticulations[3].outputMidiNumber).to.equal(38); testExportRoundtrip(score); @@ -1421,7 +1424,7 @@ describe('AlphaTexImporterTest', () => { const score = parseTex(` . \\tempo 120 1.1.4 1.1 1.1{tempo 60} 1.1 | 1.1.4{tempo 100} 1.1 1.1{tempo 120} 1.1 `); - expect(score.masterBars[0].tempoAutomations).to.have.length(2); + expect(score.masterBars[0].tempoAutomations.length).to.equal(2); expect(score.masterBars[0].tempoAutomations[0].value).to.equal(120); expect(score.masterBars[0].tempoAutomations[0].ratioPosition).to.equal(0); expect(score.masterBars[0].tempoAutomations[1].value).to.equal(60); @@ -1572,7 +1575,7 @@ describe('AlphaTexImporterTest', () => { c3 d3 e3 f3 | c3 d3 e3 f3 `); - expect(score.masterBars).to.have.length(2); + expect(score.masterBars.length).to.equal(2); expect(score.tracks[0].staves[0].bars.length).to.equal(2); expect(score.tracks[0].staves[0].bars[0].voices.length).to.equal(2); @@ -1588,11 +1591,11 @@ describe('AlphaTexImporterTest', () => { c3 d3 e3 f3 | c3 d3 e3 f3 `); - expect(score.masterBars).to.have.length(2); + expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves[0].bars).to.have.length(2); - expect(score.tracks[0].staves[0].bars[0].voices).to.have.length(2); - expect(score.tracks[0].staves[0].bars[1].voices).to.have.length(2); + expect(score.tracks[0].staves[0].bars.length).to.equal(2); + expect(score.tracks[0].staves[0].bars[0].voices.length).to.equal(2); + expect(score.tracks[0].staves[0].bars[1].voices.length).to.equal(2); testExportRoundtrip(score); }); @@ -1603,11 +1606,11 @@ describe('AlphaTexImporterTest', () => { c3 d3 e3 f3 | c3 d3 e3 f3 `); - expect(score.masterBars).to.have.length(2); + expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves[0].bars).to.have.length(2); - expect(score.tracks[0].staves[0].bars[0].voices).to.have.length(2); - expect(score.tracks[0].staves[0].bars[1].voices).to.have.length(2); + expect(score.tracks[0].staves[0].bars.length).to.equal(2); + expect(score.tracks[0].staves[0].bars[0].voices.length).to.equal(2); + expect(score.tracks[0].staves[0].bars[1].voices.length).to.equal(2); testExportRoundtrip(score); }); @@ -1676,13 +1679,13 @@ describe('AlphaTexImporterTest', () => { 3.3.4{ tb dive gradual (0 -12.5) } | `); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarType).to.equal(WhammyType.Dive); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints).to.have.length(2); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints!.length).to.equal(2); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![0].value).to.equal(0); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![1].value).to.equal(-12.5); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarType).to.equal(WhammyType.Dive); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyStyle).to.equal(BendStyle.Gradual); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints).to.have.length(2); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints!.length).to.equal(2); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![0].value).to.equal(0); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![1].value).to.equal(-12.5); testExportRoundtrip(score); @@ -1721,7 +1724,7 @@ describe('AlphaTexImporterTest', () => { G4 G4 G4 { instrument brightacousticpiano } `); expect(score.tracks[0].playbackInfo.program).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations).to.have.length(1); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].type).to.equal( AutomationType.Instrument ); @@ -1745,7 +1748,7 @@ describe('AlphaTexImporterTest', () => { `); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendType).to.equal(BendType.Bend); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendStyle).to.equal(BendStyle.Gradual); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints).to.have.length(2); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints!.length).to.equal(2); testExportRoundtrip(score); }); @@ -1833,7 +1836,7 @@ describe('AlphaTexImporterTest', () => { expect(score.tempo).to.equal(100); expect(score.tempoLabel).to.equal('T1'); - expect(score.masterBars[1].tempoAutomations).to.have.length(1); + expect(score.masterBars[1].tempoAutomations.length).to.equal(1); expect(score.masterBars[1].tempoAutomations[0].value).to.equal(80); expect(score.masterBars[1].tempoAutomations[0].text).to.equal('T2'); testExportRoundtrip(score); @@ -1863,7 +1866,7 @@ describe('AlphaTexImporterTest', () => { `); expect(score.defaultSystemsLayout).to.equal(5); - expect(score.systemsLayout).to.have.length(3); + expect(score.systemsLayout.length).to.equal(3); expect(score.systemsLayout[0]).to.equal(3); expect(score.systemsLayout[1]).to.equal(2); expect(score.systemsLayout[2]).to.equal(3); @@ -1904,7 +1907,7 @@ describe('AlphaTexImporterTest', () => { expect(score.tracks[0].color.rgba).to.equal('#FF0000'); expect(score.tracks[0].defaultSystemsLayout).to.equal(6); - expect(score.tracks[0].systemsLayout).to.have.length(3); + expect(score.tracks[0].systemsLayout.length).to.equal(3); expect(score.tracks[0].systemsLayout[0]).to.equal(3); expect(score.tracks[0].systemsLayout[1]).to.equal(2); expect(score.tracks[0].systemsLayout[0]).to.equal(3); @@ -2217,13 +2220,13 @@ describe('AlphaTexImporterTest', () => { expect(score.tracks[0].playbackInfo.volume).to.equal(7); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations).to.have.length(1); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].type).to.equal( AutomationType.Volume ); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].value).to.equal(8); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations).to.have.length(1); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].type).to.equal( AutomationType.Volume ); @@ -2241,13 +2244,13 @@ describe('AlphaTexImporterTest', () => { expect(score.tracks[0].playbackInfo.balance).to.equal(7); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations).to.have.length(1); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].type).to.equal( AutomationType.Balance ); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].value).to.equal(8); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations).to.have.length(1); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].type).to.equal( AutomationType.Balance ); @@ -2271,7 +2274,7 @@ describe('AlphaTexImporterTest', () => { `); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].deadSlapped).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes).to.have.length(0); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).to.equal(0); testExportRoundtrip(score); }); @@ -2349,7 +2352,7 @@ describe('AlphaTexImporterTest', () => { it('articulation', () => importErrorTest('\\articulation "Test" 0')); it('duration tuplet', () => importErrorTest('. :4 {tu 0}')); it('beat tuplet', () => importErrorTest('. C4 {tu 0}')); - it('tremolo speed', () => importErrorTest('. C4 {tp 0}')); + it('tremolo speed', () => importErrorTest('. C4 {tp 10}')); it('trill', () => importErrorTest('. 3.3 {tr 4 0}')); it('textalign', () => importErrorTest('\\title "Test" "" invalid')); }); @@ -2458,4 +2461,211 @@ describe('AlphaTexImporterTest', () => { // - with instrument already specified }); + + it('extend-bar-lines', () => { + const score = ScoreLoader.loadAlphaTex(` + \\extendBarLines + \\track "Piano1" + \\staff {score} + \\instrument piano + C4 D4 E4 F4 + \\staff {score} + \\clef f4 C3 D3 E3 F3 + \\track "Piano2" + \\staff {score} + \\instrument piano + C4 D4 E4 F4 + \\track "Flute 1" + \\staff { score } + \\instrument flute + C4 D4 E4 F4 + \\track "Flute 2" + \\staff { score } + \\instrument flute + \\clef f4 C3 D3 E3 F3 + \\track "Guitar 1" + \\staff { score tabs } + 0.3.4 2.3.4 5.3.4 7.3.4 + `); + + expect(score.stylesheet.extendBarLines).to.be.true; + + testExportRoundtrip(score); + }); + + describe('voice-mode', () => { + it('default', () => { + expect( + parseTex(` + \\voice + C4 | C5 + \\voice + C3 | C4 + `) + ).toMatchSnapshot(); + }); + it('staffWise', () => { + expect( + parseTex(` + \\voiceMode staffWise + \\voice + C4 | C5 + \\voice + C3 | C4 + `) + ).toMatchSnapshot(); + }); + it('barWise', () => { + expect( + parseTex(` + \\voiceMode barWise + // Bar 1 + \\voice C4 + \\voice C3 + | + // Bar 2 + \\voice C5 + \\voice C4 + `) + ).toMatchSnapshot(); + }); + }); + + it('inline-chord-diagrams', () => { + let score = parseTex(` + \\chordDiagramsInScore + \\chord ("E" 0 0 1 2 2 0) + (0.1 0.2 1.3 2.4 2.5 0.6){ch "E"} + `); + expect(score.stylesheet.globalDisplayChordDiagramsInScore).to.be.true; + + score = parseTex(` + \\chordDiagramsInScore true + \\chord ("E" 0 0 1 2 2 0) + (0.1 0.2 1.3 2.4 2.5 0.6){ch "E"} + `); + expect(score.stylesheet.globalDisplayChordDiagramsInScore).to.be.true; + + score = parseTex(` + \\chordDiagramsInScore false + \\chord ("E" 0 0 1 2 2 0) + (0.1 0.2 1.3 2.4 2.5 0.6){ch "E"} + `); + expect(score.stylesheet.globalDisplayChordDiagramsInScore).to.be.false; + }); + + it('empty-staff-options', () => { + let score = parseTex(` + \\hideEmptyStaves + C4 + `); + expect(score.stylesheet.hideEmptyStaves).to.be.true; + expect(score.stylesheet.hideEmptyStavesInFirstSystem).to.be.false; + expect(score.stylesheet.showSingleStaffBrackets).to.be.false; + + score = parseTex(` + \\hideEmptyStaves + \\hideEmptyStavesInFirstSystem + `); + expect(score.stylesheet.hideEmptyStaves).to.be.true; + expect(score.stylesheet.hideEmptyStavesInFirstSystem).to.be.true; + + score = parseTex(` + \\hideEmptyStavesInFirstSystem + C4 + `); + expect(score.stylesheet.hideEmptyStaves).to.be.false; + expect(score.stylesheet.hideEmptyStavesInFirstSystem).to.be.true; + + score = parseTex(` + \\showSingleStaffBrackets + C4 + `); + expect(score.stylesheet.showSingleStaffBrackets).to.be.true; + }); + + describe('tremolos', () => { + function test(tex: string) { + const score = parseTex(tex); + const beat = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; + const serialized = TremoloPickingEffectSerializer.toJson(beat.tremoloPicking!); + expect(serialized).toMatchSnapshot(); + testExportRoundtrip(score); + } + + // simple + it('tremolo1', () => test(`C4 {tp 1}`)); + it('tremolo2', () => test(`C4 {tp 2}`)); + it('tremolo3', () => test(`C4 {tp 3}`)); + it('tremolo4', () => test(`C4 {tp 4}`)); + it('tremolo5', () => test(`C4 {tp 5}`)); + + // backwards compatibility + it('tremolo8', () => test(`C4 {tp 8}`)); + it('tremolo16', () => test(`C4 {tp 16}`)); + it('tremolo32', () => test(`C4 {tp 32}`)); + + // with default style + it('tremolo-default1', () => test(`C4 {tp (1 default)}`)); + it('tremolo-default2', () => test(`C4 {tp (2 default)}`)); + it('tremolo-default3', () => test(`C4 {tp (3 default)}`)); + it('tremolo-default4', () => test(`C4 {tp (4 default)}`)); + it('tremolo-default5', () => test(`C4 {tp (5 default)}`)); + + // buzzroll + it('buzzroll-default1', () => test(`C4 {tp (1 buzzRoll)}`)); + it('buzzroll-default2', () => test(`C4 {tp (2 buzzRoll)}`)); + it('buzzroll-default3', () => test(`C4 {tp (3 buzzRoll)}`)); + it('buzzroll-default4', () => test(`C4 {tp (4 buzzRoll)}`)); + it('buzzroll-default5', () => test(`C4 {tp (5 buzzRoll)}`)); + }); + + describe('defaultBarNumberDisplay', () => { + function test(tex: string, mode: BarNumberDisplay) { + const score = parseTex(tex); + expect(score.stylesheet.barNumberDisplay).to.equal(mode); + + testExportRoundtrip(score); + } + + it('all', () => test('\\defaultBarNumberDisplay allBars C4', BarNumberDisplay.AllBars)); + it('first', () => test('\\defaultBarNumberDisplay firstOfSystem C4', BarNumberDisplay.FirstOfSystem)); + it('hide', () => test('\\defaultBarNumberDisplay hide C4', BarNumberDisplay.Hide)); + }); + + describe('barNumberDisplay', () => { + function test(tex: string, mode: BarNumberDisplay | undefined) { + const score = parseTex(tex); + expect(score.tracks[0].staves[0].bars[0].barNumberDisplay).to.be.undefined; + expect(score.tracks[0].staves[0].bars[1].barNumberDisplay).to.equal(mode); + + testExportRoundtrip(score); + } + + it('unsert', () => test('\\defaultBarNumberDisplay hide C4 | C4 ', undefined)); + it('all', () => + test('\\defaultBarNumberDisplay hide C4 | \\barNumberDisplay allBars C4 ', BarNumberDisplay.AllBars)); + it('first', () => + test( + '\\defaultBarNumberDisplay hide C4 | \\barNumberDisplay firstOfSystem C4 ', + BarNumberDisplay.FirstOfSystem + )); + it('hide', () => + test('\\defaultBarNumberDisplay allBars C4 | \\barNumberDisplay hide C4 ', BarNumberDisplay.Hide)); + }); + + it('custom-beaming', () => { + const score = parseTex(` + \\ts (4 4) + \\beaming (8 2 2 2 2) + C4.8 * 8 | + C4.8 * 8 | + \\ts (4 4) + \\beaming (8 4 4) + C4.8 * 8 + C4.8 * 8 + `); + expect(score).toMatchSnapshot(); + testExportRoundtrip(score); + }); }); diff --git a/packages/alphatab/test/importer/AlphaTexImporterOld.test.ts b/packages/alphatab/test/importer/AlphaTexImporterOld.test.ts deleted file mode 100644 index d9d7ec042..000000000 --- a/packages/alphatab/test/importer/AlphaTexImporterOld.test.ts +++ /dev/null @@ -1,2401 +0,0 @@ -import { UnsupportedFormatError } from '@coderline/alphatab/importer/UnsupportedFormatError'; -import { AutomationType } from '@coderline/alphatab/model/Automation'; -import { BarreShape } from '@coderline/alphatab/model/BarreShape'; -import { type Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat'; -import { BendStyle } from '@coderline/alphatab/model/BendStyle'; -import { BendType } from '@coderline/alphatab/model/BendType'; -import { BrushType } from '@coderline/alphatab/model/BrushType'; -import { Clef } from '@coderline/alphatab/model/Clef'; -import { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; -import { Direction } from '@coderline/alphatab/model/Direction'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; -import { FadeType } from '@coderline/alphatab/model/FadeType'; -import { FermataType } from '@coderline/alphatab/model/Fermata'; -import { Fingers } from '@coderline/alphatab/model/Fingers'; -import { GolpeType } from '@coderline/alphatab/model/GolpeType'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; -import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; -import { KeySignature } from '@coderline/alphatab/model/KeySignature'; -import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; -import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; -import { Ottavia } from '@coderline/alphatab/model/Ottavia'; -import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; -import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@coderline/alphatab/model/RenderStylesheet'; -import { type Score, ScoreSubElement } from '@coderline/alphatab/model/Score'; -import { SimileMark } from '@coderline/alphatab/model/SimileMark'; -import { SlideInType } from '@coderline/alphatab/model/SlideInType'; -import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; -import type { Staff } from '@coderline/alphatab/model/Staff'; -import type { Track } from '@coderline/alphatab/model/Track'; -import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; -import { VibratoType } from '@coderline/alphatab/model/VibratoType'; -import { WhammyType } from '@coderline/alphatab/model/WhammyType'; -import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; -import { HarmonicsEffectInfo } from '@coderline/alphatab/rendering/effects/HarmonicsEffectInfo'; -import { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { Settings } from '@coderline/alphatab/Settings'; -import { StaveProfile } from '@coderline/alphatab/StaveProfile'; -import { AlphaTexExporterOld } from 'test/exporter/AlphaTexExporterOld'; -import { - AlphaTexError, - AlphaTexImporterOld, - AlphaTexLexerOld, - AlphaTexSymbols -} from 'test/importer/AlphaTexImporterOld'; -import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; -import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; -import { assert, expect } from 'chai'; - -describe('AlphaTexImporterOldTest', () => { - function parseTex(tex: string): Score { - const importer: AlphaTexImporterOld = new AlphaTexImporterOld(); - importer.initFromString(tex, new Settings()); - return importer.readScore(); - } - - // as we often add tests here for new alphaTex features, this helper - // directly allows testing the exporter via a roundtrip comparison - function testExportRoundtrip(expected: Score) { - ComparisonHelpers.alphaTexExportRoundtripPrepare(expected); - - const exported = new AlphaTexExporterOld().exportToString(expected); - const actual = parseTex(exported); - - ComparisonHelpers.alphaTexExportRoundtripEqual('export-roundtrip', actual, expected); - } - - it('ensure-metadata-parsing-issue73', () => { - const tex = `\\title Test - \\words test - \\music alphaTab - \\copyright test - \\tempo 200 - \\instrument 30 - \\capo 2 - \\tuning G3 D2 G2 B2 D3 A4 - . - 0.5.2 1.5.4 3.4.4 | 5.3.8 5.3.8 5.3.8 5.3.8 r.2`; - - const score: Score = parseTex(tex); - expect(score.title).to.equal('Test'); - expect(score.words).to.equal('test'); - expect(score.music).to.equal('alphaTab'); - expect(score.copyright).to.equal('test'); - expect(score.tempo).to.equal(200); - expect(score.tracks.length).to.equal(1); - expect(score.tracks[0].playbackInfo.program).to.equal(30); - expect(score.tracks[0].staves[0].capo).to.equal(2); - expect(score.tracks[0].staves[0].tuning.join(',')).to.equal('55,38,43,47,50,69'); - expect(score.masterBars.length).to.equal(2); - - // bars[0] - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].duration).to.equal(Duration.Half); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].string).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].duration).to.equal(Duration.Quarter); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].fret).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].string).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].duration).to.equal(Duration.Quarter); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].string).to.equal(3); - - // bars[1] - expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].notes.length).to.equal(0); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].duration).to.equal(Duration.Half); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].isRest).to.equal(true); - }); - - it('tuning', () => { - const tex = `\\tuning E4 B3 G3 D3 A2 E2 - . - 0.5.1`; - - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].tuning.join(',')).to.equal(Tuning.getDefaultTuningFor(6)!.tunings.join(',')); - }); - - it('dead-notes1-issue79', () => { - const tex: string = ':4 x.3'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isDead).to.equal(true); - }); - - it('dead-notes2-issue79', () => { - const tex: string = ':4 3.3{x}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isDead).to.equal(true); - }); - - it('trill-issue79', () => { - const tex: string = ':4 3.3{tr 5 16}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isTrill).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].trillSpeed).to.equal(Duration.Sixteenth); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].trillFret).to.equal(5); - }); - - it('tremolo-issue79', () => { - const tex: string = ':4 3.3{tr 5 16}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isTrill).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].trillSpeed).to.equal(Duration.Sixteenth); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].trillFret).to.equal(5); - }); - - it('tremolo-picking-issue79', () => { - const tex: string = ':4 3.3{tp 16}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].isTremolo).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].tremoloSpeed).to.equal(Duration.Sixteenth); - }); - - it('brushes-arpeggio', () => { - const tex: string = ` - (1.1 2.2 3.3 4.4).4{bd 60} (1.1 2.2 3.3 4.4).8{bu 60} (1.1 2.2 3.3 4.4).2{ad 60} (1.1 2.2 3.3 4.4).16{au 60} r | - (1.1 2.2 3.3 4.4).4{bd 120} (1.1 2.2 3.3 4.4).8{bu 120} (1.1 2.2 3.3 4.4).2{ad 120} (1.1 2.2 3.3 4.4).16{au 120} r | - (1.1 2.2 3.3 4.4).4{bd} (1.1 2.2 3.3 4.4).8{bu} (1.1 2.2 3.3 4.4).2{ad} (1.1 2.2 3.3 4.4).16{au} r - `; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].brushType).to.equal(BrushType.BrushDown); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].playbackDuration).to.equal(960); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].brushType).to.equal(BrushType.BrushUp); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].playbackDuration).to.equal(480); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].brushType).to.equal(BrushType.ArpeggioDown); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].playbackDuration).to.equal(1920); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].brushType).to.equal(BrushType.ArpeggioUp); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].playbackDuration).to.equal(240); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].isRest).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].brushType).to.equal(BrushType.None); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].playbackDuration).to.equal(240); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].brushDuration).to.equal(0); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].brushType).to.equal(BrushType.BrushDown); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].brushDuration).to.equal(120); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].brushType).to.equal(BrushType.BrushUp); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].brushDuration).to.equal(120); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].brushType).to.equal(BrushType.ArpeggioDown); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].brushDuration).to.equal(120); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].brushType).to.equal(BrushType.ArpeggioUp); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].brushDuration).to.equal(120); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].isRest).to.equal(true); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].brushType).to.equal(BrushType.None); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].brushDuration).to.equal(0); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].brushType).to.equal(BrushType.BrushDown); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].brushType).to.equal(BrushType.BrushUp); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].brushDuration).to.equal(30); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].brushType).to.equal(BrushType.ArpeggioDown); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].brushDuration).to.equal(480); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[3].brushType).to.equal(BrushType.ArpeggioUp); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[3].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[4].isRest).to.equal(true); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[4].brushType).to.equal(BrushType.None); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[4].brushDuration).to.equal(0); - testExportRoundtrip(score); - }); - - it('hamonics-issue79', () => { - const tex: string = ':8 3.3{nh} 3.3{ah} 3.3{th} 3.3{ph} 3.3{sh}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].harmonicType).to.equal( - HarmonicType.Natural - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].harmonicType).to.equal( - HarmonicType.Artificial - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].harmonicType).to.equal(HarmonicType.Tap); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].harmonicType).to.equal(HarmonicType.Pinch); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].harmonicType).to.equal(HarmonicType.Semi); - }); - - it('hamonics-rendering-text-issue79', async () => { - const tex: string = ':8 3.3{nh} 3.3{ah} 3.3{th} 3.3{ph} 3.3{sh}'; - const score: Score = parseTex(tex); - - await VisualTestHelper.prepareAlphaSkia(); - const settings: Settings = new Settings(); - settings.core.engine = 'svg'; - settings.core.enableLazyLoading = false; - settings.display.staveProfile = StaveProfile.ScoreTab; - const renderer: ScoreRenderer = new ScoreRenderer(settings); - renderer.width = 970; - let svg: string = ''; - renderer.partialRenderFinished.on(r => { - svg += r.renderResult; - }); - renderer.renderScore(score, [0]); - const regexTemplate: string = ']+>\\s*{0}\\s*'; - expect( - new RegExp(regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Natural))).exec( - svg - ) - ).to.be.ok; - expect( - new RegExp( - regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Artificial)) - ).exec(svg) - ).to.be.ok; - expect( - new RegExp(regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Tap))).exec(svg) - ).to.be.ok; - expect( - new RegExp(regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Pinch))).exec(svg) - ).to.be.ok; - expect( - new RegExp(regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Semi))).exec(svg) - ).to.be.ok; - }); - - it('grace-issue79', () => { - const tex: string = ':8 3.3{gr} 3.3{gr ob}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].graceType).to.equal(GraceType.BeforeBeat); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].graceType).to.equal(GraceType.OnBeat); - testExportRoundtrip(score); - }); - - it('left-hand-finger-single-note', () => { - const tex: string = ':8 3.3{lf 1} 3.3{lf 2} 3.3{lf 3} 3.3{lf 4} 3.3{lf 5}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].leftHandFinger).to.equal(Fingers.Thumb); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].leftHandFinger).to.equal( - Fingers.IndexFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].leftHandFinger).to.equal( - Fingers.MiddleFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].leftHandFinger).to.equal( - Fingers.AnnularFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].leftHandFinger).to.equal( - Fingers.LittleFinger - ); - testExportRoundtrip(score); - }); - - it('right-hand-finger-single-note', () => { - const tex: string = ':8 3.3{rf 1} 3.3{rf 2} 3.3{rf 3} 3.3{rf 4} 3.3{rf 5}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].rightHandFinger).to.equal(Fingers.Thumb); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].rightHandFinger).to.equal( - Fingers.IndexFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].rightHandFinger).to.equal( - Fingers.MiddleFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].rightHandFinger).to.equal( - Fingers.AnnularFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].rightHandFinger).to.equal( - Fingers.LittleFinger - ); - testExportRoundtrip(score); - }); - - it('left-hand-finger-chord', () => { - const tex: string = ':8 (3.1{lf 1} 3.2{lf 2} 3.3{lf 3} 3.4{lf 4} 3.5{lf 5})'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].leftHandFinger).to.equal(Fingers.Thumb); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[1].leftHandFinger).to.equal( - Fingers.IndexFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[2].leftHandFinger).to.equal( - Fingers.MiddleFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[3].leftHandFinger).to.equal( - Fingers.AnnularFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[4].leftHandFinger).to.equal( - Fingers.LittleFinger - ); - testExportRoundtrip(score); - }); - - it('right-hand-finger-chord', () => { - const tex: string = ':8 (3.1{rf 1} 3.2{rf 2} 3.3{rf 3} 3.4{rf 4} 3.5{rf 5})'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].rightHandFinger).to.equal(Fingers.Thumb); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[1].rightHandFinger).to.equal( - Fingers.IndexFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[2].rightHandFinger).to.equal( - Fingers.MiddleFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[3].rightHandFinger).to.equal( - Fingers.AnnularFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[4].rightHandFinger).to.equal( - Fingers.LittleFinger - ); - testExportRoundtrip(score); - }); - - it('unstringed', () => { - const tex: string = '\\tuning piano . c4 c#4 d4 d#4 | c4 db4 d4 eb4'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(4); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].realValue).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].realValue).to.equal(61); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].realValue).to.equal(62); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].realValue).to.equal(63); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].realValue).to.equal(60); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].realValue).to.equal(61); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].realValue).to.equal(62); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].realValue).to.equal(63); - testExportRoundtrip(score); - }); - - it('multi-staff-default-settings', () => { - const tex: string = '1.1 | 1.1 | \\staff 2.1 | 2.1'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(2); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[0].staves[1].showTablature).to.be.equal(true); // default settings used - - expect(score.tracks[0].staves[1].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[1].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-staff-default-settings-braces', () => { - const tex: string = '1.1 | 1.1 | \\staff{} 2.1 | 2.1'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(2); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[0].staves[1].showTablature).to.be.equal(true); // default settings used - - expect(score.tracks[0].staves[1].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[1].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('single-staff-with-setting', () => { - const tex: string = '\\staff{score} 1.1 | 1.1'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(1); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(false); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('single-staff-with-slash', () => { - const tex: string = '\\staff{slash} 1.1 | 1.1'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(1); - expect(score.tracks[0].staves[0].showSlash).to.be.equal(true); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(false); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(false); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('single-staff-with-score-and-slash', () => { - const tex: string = '\\staff{score slash} 1.1 | 1.1'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(1); - expect(score.tracks[0].staves[0].showSlash).to.be.equal(true); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(false); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-staff-with-settings', () => { - const tex = `\\staff{score} 1.1 | 1.1 | - \\staff{tabs} \\capo 2 2.1 | 2.1 | - \\staff{score tabs} \\tuning A1 D2 A2 D3 G3 B3 E4 3.1 | 3.1`; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(3); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(false); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[0].staves[1].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[1].showStandardNotation).to.be.equal(false); - expect(score.tracks[0].staves[1].bars.length).to.equal(2); - expect(score.tracks[0].staves[1].capo).to.equal(2); - expect(score.tracks[0].staves[2].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[2].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[2].bars.length).to.equal(2); - expect(score.tracks[0].staves[2].tuning.length).to.equal(7); - testExportRoundtrip(score); - }); - - it('multi-track', () => { - const tex: string = '\\track "First" 1.1 | 1.1 | \\track "Second" 2.2 | 2.2'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(2); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(1); - expect(score.tracks[0].name).to.equal('First'); - expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(0); - expect(score.tracks[0].playbackInfo.secondaryChannel).to.equal(1); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[1].staves.length).to.equal(1); - expect(score.tracks[1].name).to.equal('Second'); - expect(score.tracks[1].playbackInfo.primaryChannel).to.equal(2); - expect(score.tracks[1].playbackInfo.secondaryChannel).to.equal(3); - expect(score.tracks[1].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[1].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[1].staves[0].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-track-names', () => { - const tex: string = - '\\track 1.1 | 1.1 | \\track "Only Long Name" 2.2 | 2.2 | \\track "Very Long Name" "shrt" 3.3 | 3.3 '; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(3); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(1); - expect(score.tracks[0].name).to.equal(''); - expect(score.tracks[0].shortName).to.equal(''); - expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(0); - expect(score.tracks[0].playbackInfo.secondaryChannel).to.equal(1); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[1].staves.length).to.equal(1); - expect(score.tracks[1].name).to.equal('Only Long Name'); - expect(score.tracks[1].shortName).to.equal('Only Long '); - expect(score.tracks[1].playbackInfo.primaryChannel).to.equal(2); - expect(score.tracks[1].playbackInfo.secondaryChannel).to.equal(3); - expect(score.tracks[1].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[1].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[1].staves[0].bars.length).to.equal(2); - expect(score.tracks[2].staves.length).to.equal(1); - expect(score.tracks[2].name).to.equal('Very Long Name'); - expect(score.tracks[2].shortName).to.equal('shrt'); - expect(score.tracks[2].playbackInfo.primaryChannel).to.equal(4); - expect(score.tracks[2].playbackInfo.secondaryChannel).to.equal(5); - expect(score.tracks[2].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[2].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[2].staves[0].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-track-multi-staff', () => { - const tex = `\\track "Piano" - \\staff{score} \\tuning piano \\instrument acousticgrandpiano - c4 d4 e4 f4 | - - \\staff{score} \\tuning piano \\clef F4 - c2 c2 c2 c2 | - - \\track "Guitar" - \\staff{tabs} - 1.2 3.2 0.1 1.1 | - - \\track "Second Guitar" - 1.2 3.2 0.1 1.1 - `; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(3); - expect(score.masterBars.length).to.equal(1); - { - const track1: Track = score.tracks[0]; - expect(track1.name).to.equal('Piano'); - expect(track1.staves.length).to.equal(2); - expect(track1.playbackInfo.program).to.equal(0); - expect(track1.playbackInfo.primaryChannel).to.equal(0); - expect(track1.playbackInfo.secondaryChannel).to.equal(1); - { - const staff1: Staff = track1.staves[0]; - expect(staff1.showTablature).to.be.equal(false); - expect(staff1.showStandardNotation).to.be.equal(true); - expect(staff1.tuning.length).to.equal(0); - expect(staff1.bars.length).to.equal(1); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - { - const staff2: Staff = track1.staves[1]; - expect(staff2.showTablature).to.be.equal(false); - expect(staff2.showStandardNotation).to.be.equal(true); - expect(staff2.tuning.length).to.equal(0); - expect(staff2.bars.length).to.equal(1); - expect(staff2.bars[0].clef).to.equal(Clef.F4); - } - } - { - const track2: Track = score.tracks[1]; - expect(track2.name).to.equal('Guitar'); - expect(track2.staves.length).to.equal(1); - expect(track2.playbackInfo.program).to.equal(25); - expect(track2.playbackInfo.primaryChannel).to.equal(2); - expect(track2.playbackInfo.secondaryChannel).to.equal(3); - { - const staff1: Staff = track2.staves[0]; - expect(staff1.showTablature).to.be.equal(true); - expect(staff1.showStandardNotation).to.be.equal(false); - expect(staff1.tuning.length).to.equal(6); - expect(staff1.bars.length).to.equal(1); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - } - { - const track3: Track = score.tracks[2]; - expect(track3.name).to.equal('Second Guitar'); - expect(track3.staves.length).to.equal(1); - expect(track3.playbackInfo.program).to.equal(25); - expect(track3.playbackInfo.primaryChannel).to.equal(4); - expect(track3.playbackInfo.secondaryChannel).to.equal(5); - { - const staff1: Staff = track3.staves[0]; - expect(staff1.showTablature).to.be.equal(true); - expect(staff1.showStandardNotation).to.be.equal(true); - expect(staff1.tuning.length).to.equal(6); - expect(staff1.bars.length).to.equal(1); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - } - testExportRoundtrip(score); - }); - - it('multi-track-multi-staff-inconsistent-bars', () => { - const tex: string = ` - \\track "Piano" - \\staff{score} \\tuning piano \\instrument acousticgrandpiano - c4 d4 e4 f4 | - - \\staff{score} \\tuning piano \\clef F4 - c2 c2 c2 c2 | c2 c2 c2 c2 | c2 c2 c2 c2 | - - \\track "Guitar" - \\staff{tabs} - 1.2 3.2 0.1 1.1 | 1.2 3.2 0.1 1.1 | - - \\track "Second Guitar" - 1.2 3.2 0.1 1.1 - `; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(3); - expect(score.masterBars.length).to.equal(3); - { - const track1: Track = score.tracks[0]; - expect(track1.name).to.equal('Piano'); - expect(track1.staves.length).to.equal(2); - expect(track1.playbackInfo.program).to.equal(0); - expect(track1.playbackInfo.primaryChannel).to.equal(0); - expect(track1.playbackInfo.secondaryChannel).to.equal(1); - { - const staff1: Staff = track1.staves[0]; - expect(staff1.showTablature).to.be.equal(false); - expect(staff1.showStandardNotation).to.be.equal(true); - expect(staff1.tuning.length).to.equal(0); - expect(staff1.bars.length).to.equal(3); - expect(staff1.bars[0].isEmpty).to.be.equal(false); - expect(staff1.bars[1].isEmpty).to.be.equal(true); - expect(staff1.bars[2].isEmpty).to.be.equal(true); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - { - const staff2: Staff = track1.staves[1]; - expect(staff2.showTablature).to.be.equal(false); - expect(staff2.showStandardNotation).to.be.equal(true); - expect(staff2.tuning.length).to.equal(0); - expect(staff2.bars.length).to.equal(3); - expect(staff2.bars[0].isEmpty).to.be.equal(false); - expect(staff2.bars[1].isEmpty).to.be.equal(false); - expect(staff2.bars[2].isEmpty).to.be.equal(false); - expect(staff2.bars[0].clef).to.equal(Clef.F4); - } - } - { - const track2: Track = score.tracks[1]; - expect(track2.name).to.equal('Guitar'); - expect(track2.staves.length).to.equal(1); - expect(track2.playbackInfo.program).to.equal(25); - expect(track2.playbackInfo.primaryChannel).to.equal(2); - expect(track2.playbackInfo.secondaryChannel).to.equal(3); - { - const staff1: Staff = track2.staves[0]; - expect(staff1.showTablature).to.be.equal(true); - expect(staff1.showStandardNotation).to.be.equal(false); - expect(staff1.tuning.length).to.equal(6); - expect(staff1.bars.length).to.equal(3); - expect(staff1.bars[0].isEmpty).to.be.equal(false); - expect(staff1.bars[1].isEmpty).to.be.equal(false); - expect(staff1.bars[2].isEmpty).to.be.equal(true); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - } - { - const track3: Track = score.tracks[2]; - expect(track3.name).to.equal('Second Guitar'); - expect(track3.staves.length).to.equal(1); - expect(track3.playbackInfo.program).to.equal(25); - expect(track3.playbackInfo.primaryChannel).to.equal(4); - expect(track3.playbackInfo.secondaryChannel).to.equal(5); - { - const staff1: Staff = track3.staves[0]; - expect(staff1.showTablature).to.be.equal(true); - expect(staff1.showStandardNotation).to.be.equal(true); - expect(staff1.tuning.length).to.equal(6); - expect(staff1.bars.length).to.equal(3); - expect(staff1.bars[0].isEmpty).to.be.equal(false); - expect(staff1.bars[1].isEmpty).to.be.equal(true); - expect(staff1.bars[2].isEmpty).to.be.equal(true); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - } - testExportRoundtrip(score); - }); - - it('slides', () => { - const tex: string = '3.3{sl} 4.3 | 3.3{ss} 4.3 | 3.3{sib} 3.3{sia} 3.3{sou} 3.3{sod} | 3.3{psd} 3.3{psu}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(4); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].slideOutType).to.equal( - SlideOutType.Legato - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].slideTarget!.id).to.equal( - score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].id - ); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].slideOutType).to.equal(SlideOutType.Shift); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].slideTarget!.id).to.equal( - score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].id - ); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].notes[0].slideInType).to.equal( - SlideInType.IntoFromBelow - ); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].notes[0].slideInType).to.equal( - SlideInType.IntoFromAbove - ); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].notes[0].slideOutType).to.equal(SlideOutType.OutUp); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[3].notes[0].slideOutType).to.equal( - SlideOutType.OutDown - ); - expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].notes[0].slideOutType).to.equal( - SlideOutType.PickSlideDown - ); - expect(score.tracks[0].staves[0].bars[3].voices[0].beats[1].notes[0].slideOutType).to.equal( - SlideOutType.PickSlideUp - ); - testExportRoundtrip(score); - }); - - it('section', () => { - const tex: string = '\\section Intro 1.1 | 1.1 | \\section "Chorus 01" 1.1 | \\section S Solo'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(4); - expect(score.masterBars[0].isSectionStart).to.be.equal(true); - expect(score.masterBars[0].section!.text).to.equal('Intro'); - expect(score.masterBars[0].section!.marker).to.equal(''); - expect(score.masterBars[1].isSectionStart).to.be.equal(false); - expect(score.masterBars[2].isSectionStart).to.be.equal(true); - expect(score.masterBars[2].section!.text).to.equal('Chorus 01'); - expect(score.masterBars[2].section!.marker).to.equal(''); - expect(score.masterBars[3].isSectionStart).to.be.equal(true); - expect(score.masterBars[3].section!.text).to.equal('Solo'); - expect(score.masterBars[3].section!.marker).to.equal('S'); - testExportRoundtrip(score); - }); - - it('key-signature', () => { - const tex: string = `:1 3.3 | \\ks C 3.3 | \\ks Cmajor 3.3 | \\ks Aminor 3.3 | - \\ks F 3.3 | \\ks bbmajor 3.3 | \\ks CMINOR 3.3 | \\ks aB 3.3 | \\ks db 3.3 | \\ks Ebminor 3.3 | - \\ks g 3.3 | \\ks Dmajor 3.3 | \\ks f#minor 3.3 | \\ks E 3.3 | \\ks Bmajor 3.3 | \\ks d#minor 3.3`; - const score: Score = parseTex(tex); - - const bars = score.tracks[0].staves[0].bars; - const expected: [KeySignature, KeySignatureType][] = [ - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Minor], - [KeySignature.F, KeySignatureType.Major], - [KeySignature.Bb, KeySignatureType.Major], - [KeySignature.Eb, KeySignatureType.Minor], - [KeySignature.Ab, KeySignatureType.Major], - [KeySignature.Db, KeySignatureType.Major], - [KeySignature.Gb, KeySignatureType.Minor], - [KeySignature.G, KeySignatureType.Major], - [KeySignature.D, KeySignatureType.Major], - [KeySignature.A, KeySignatureType.Minor], - [KeySignature.E, KeySignatureType.Major], - [KeySignature.B, KeySignatureType.Major], - [KeySignature.FSharp, KeySignatureType.Minor] - ]; - - for (let i = 0; i < expected.length; i++) { - expect(bars[i].keySignature).to.equal(expected[i][0]); - expect(bars[i].keySignatureType).to.equal(expected[i][1]); - } - testExportRoundtrip(score); - }); - - it('key-signature-multi-staff', () => { - const tex: string = ` - \\track T1 - \\staff - :1 3.3 | \\ks C 3.3 | \\ks Cmajor 3.3 | \\ks Aminor 3.3 | - \\ks F 3.3 | \\ks bbmajor 3.3 | \\ks CMINOR 3.3 | \\ks aB 3.3 | \\ks db 3.3 | \\ks Ebminor 3.3 | - \\ks g 3.3 | \\ks Dmajor 3.3 | \\ks f#minor 3.3 | \\ks E 3.3 | \\ks Bmajor 3.3 | \\ks d#minor 3.3 - \\staff - \\ks d#minor :1 3.3 | \\ks Bmajor 3.3 | \\ks E 3.3 | - \\ks f#minor 3.3 | \\ks Dmajor 3.3 | \\ks g 3.3 | \\ks Ebminor 3.3 | \\ks db 3.3 | \\ks aB 3.3 | - \\ks CMINOR 3.3 | \\ks bbmajor 3.3 | \\ks F 3.3 | \\ks Aminor 3.3 | \\ks Cmajor 3.3 | \\ks C 3.3 | \\ks C 3.3 - `; - const score: Score = parseTex(tex); - - let bars = score.tracks[0].staves[0].bars; - const expected: [KeySignature, KeySignatureType][] = [ - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Minor], - [KeySignature.F, KeySignatureType.Major], - [KeySignature.Bb, KeySignatureType.Major], - [KeySignature.Eb, KeySignatureType.Minor], - [KeySignature.Ab, KeySignatureType.Major], - [KeySignature.Db, KeySignatureType.Major], - [KeySignature.Gb, KeySignatureType.Minor], - [KeySignature.G, KeySignatureType.Major], - [KeySignature.D, KeySignatureType.Major], - [KeySignature.A, KeySignatureType.Minor], - [KeySignature.E, KeySignatureType.Major], - [KeySignature.B, KeySignatureType.Major], - [KeySignature.FSharp, KeySignatureType.Minor] - ]; - - for (let i = 0; i < expected.length; i++) { - expect(bars[i].keySignature).to.equal(expected[i][0], `Wrong keySignature at index ${i}`); - expect(bars[i].keySignatureType).to.equal(expected[i][1], `Wrong keySignature type at index ${i}`); - } - - bars = score.tracks[0].staves[1].bars; - expected.reverse(); - for (let i = 0; i < expected.length; i++) { - expect(bars[i].keySignature).to.equal(expected[i][0], `at ${i}`); - expect(bars[i].keySignatureType).to.equal(expected[i][1], `at ${i}`); - } - testExportRoundtrip(score); - }); - - it('pop-slap-tap', () => { - const tex: string = '3.3{p} 3.3{s} 3.3{tt} r'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].pop).to.be.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].slap).to.be.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].tap).to.be.equal(true); - testExportRoundtrip(score); - }); - - it('triplet-feel-numeric', () => { - const tex: string = '\\tf 0 | \\tf 1 | \\tf 2 | \\tf 3 | \\tf 4 | \\tf 5 | \\tf 6'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - expect(score.masterBars[1].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[2].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[3].tripletFeel).to.equal(TripletFeel.Dotted16th); - expect(score.masterBars[4].tripletFeel).to.equal(TripletFeel.Dotted8th); - expect(score.masterBars[5].tripletFeel).to.equal(TripletFeel.Scottish16th); - expect(score.masterBars[6].tripletFeel).to.equal(TripletFeel.Scottish8th); - testExportRoundtrip(score); - }); - - it('triplet-feel-long-names', () => { - const tex: string = - '\\tf none | \\tf triplet-16th | \\tf triplet-8th | \\tf dotted-16th | \\tf dotted-8th | \\tf scottish-16th | \\tf scottish-8th'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - expect(score.masterBars[1].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[2].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[3].tripletFeel).to.equal(TripletFeel.Dotted16th); - expect(score.masterBars[4].tripletFeel).to.equal(TripletFeel.Dotted8th); - expect(score.masterBars[5].tripletFeel).to.equal(TripletFeel.Scottish16th); - expect(score.masterBars[6].tripletFeel).to.equal(TripletFeel.Scottish8th); - testExportRoundtrip(score); - }); - - it('triplet-feel-short-names', () => { - const tex: string = '\\tf no | \\tf t16 | \\tf t8 | \\tf d16 | \\tf d8 | \\tf s16 | \\tf s8'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - expect(score.masterBars[1].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[2].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[3].tripletFeel).to.equal(TripletFeel.Dotted16th); - expect(score.masterBars[4].tripletFeel).to.equal(TripletFeel.Dotted8th); - expect(score.masterBars[5].tripletFeel).to.equal(TripletFeel.Scottish16th); - expect(score.masterBars[6].tripletFeel).to.equal(TripletFeel.Scottish8th); - testExportRoundtrip(score); - }); - - it('triplet-feel-multi-bar', () => { - const tex: string = '\\tf t16 C4 | C4 | C4 | \\tf t8 C4 | C4 | C4 | \\tf no | C4 | C4 '; - const score: Score = parseTex(tex); - expect(score.masterBars[0].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[1].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[2].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[3].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[4].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[5].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[6].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - expect(score.masterBars[7].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - expect(score.masterBars[8].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - testExportRoundtrip(score); - }); - - it('tuplet-repeat', () => { - const tex: string = ':8 5.3{tu 3}*3'; - const score: Score = parseTex(tex); - const durations: Duration[] = [Duration.Eighth, Duration.Eighth, Duration.Eighth]; - const tuplets = [3, 3, 3]; - let i: number = 0; - let b: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; - while (b) { - expect(b.duration).to.equal(durations[i], `Duration on beat ${i} was wrong`); - if (tuplets[i] === 1) { - expect(b.hasTuplet).to.be.equal(false); - } else { - expect(b.tupletNumerator).to.equal(tuplets[i], `Tuplet on beat ${i} was wrong`); - } - b = b.nextBeat; - i++; - } - expect(i).to.equal(durations.length); - testExportRoundtrip(score); - }); - - it('tuplet-custom', () => { - const tex: string = ':8 5.3{tu 5 2}*5'; - const score: Score = parseTex(tex); - const tupletNumerators = [5, 5, 5, 5, 5]; - const tupletDenominators = [2, 2, 2, 2, 2]; - - let i: number = 0; - let b: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; - while (b) { - expect(b.tupletNumerator).to.equal(tupletNumerators[i], `Tuplet on beat ${i} was wrong`); - expect(b.tupletDenominator).to.equal(tupletDenominators[i], `Tuplet on beat ${i} was wrong`); - b = b.nextBeat; - i++; - } - testExportRoundtrip(score); - }); - - it('simple-anacrusis', () => { - const tex: string = '\\ac 3.3 3.3 | 1.1 2.1 3.1 4.1'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isAnacrusis).to.be.equal(true); - expect(score.masterBars[0].calculateDuration()).to.equal(1920); - expect(score.masterBars[1].calculateDuration()).to.equal(3840); - testExportRoundtrip(score); - }); - - it('multi-bar-anacrusis', () => { - const tex: string = '\\ac 3.3 3.3 | \\ac 3.3 3.3 | 1.1 2.1 3.1 4.1'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isAnacrusis).to.be.equal(true); - expect(score.masterBars[1].isAnacrusis).to.be.equal(true); - expect(score.masterBars[0].calculateDuration()).to.equal(1920); - expect(score.masterBars[1].calculateDuration()).to.equal(1920); - expect(score.masterBars[2].calculateDuration()).to.equal(3840); - testExportRoundtrip(score); - }); - - it('random-anacrusis', () => { - const tex: string = '\\ac 3.3 3.3 | 1.1 2.1 3.1 4.1 | \\ac 3.3 3.3 | 1.1 2.1 3.1 4.1'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isAnacrusis).to.be.equal(true); - expect(score.masterBars[1].isAnacrusis).to.be.equal(false); - expect(score.masterBars[2].isAnacrusis).to.be.equal(true); - expect(score.masterBars[3].isAnacrusis).to.be.equal(false); - expect(score.masterBars[0].calculateDuration()).to.equal(1920); - expect(score.masterBars[1].calculateDuration()).to.equal(3840); - expect(score.masterBars[2].calculateDuration()).to.equal(1920); - expect(score.masterBars[3].calculateDuration()).to.equal(3840); - testExportRoundtrip(score); - }); - - it('repeat', () => { - const tex: string = - '\\ro 1.3 2.3 3.3 4.3 | 5.3 6.3 7.3 8.3 | \\rc 2 1.3 2.3 3.3 4.3 | \\ro \\rc 3 1.3 2.3 3.3 4.3 |'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isRepeatStart).to.be.equal(true); - expect(score.masterBars[1].isRepeatStart).to.be.equal(false); - expect(score.masterBars[2].isRepeatStart).to.be.equal(false); - expect(score.masterBars[3].isRepeatStart).to.be.equal(true); - expect(score.masterBars[0].repeatCount).to.equal(0); - expect(score.masterBars[1].repeatCount).to.equal(0); - expect(score.masterBars[2].repeatCount).to.equal(2); - expect(score.masterBars[3].repeatCount).to.equal(3); - testExportRoundtrip(score); - }); - - it('alternate-endings', () => { - const tex: string = '\\ro 4.3*4 | \\ae (1 2 3) 6.3*4 | \\ae 4 \\rc 4 6.3 6.3 6.3 5.3 |'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isRepeatStart).to.be.equal(true); - expect(score.masterBars[1].isRepeatStart).to.be.equal(false); - expect(score.masterBars[2].isRepeatStart).to.be.equal(false); - expect(score.masterBars[0].repeatCount).to.equal(0); - expect(score.masterBars[1].repeatCount).to.equal(0); - expect(score.masterBars[2].repeatCount).to.equal(4); - expect(score.masterBars[0].alternateEndings).to.equal(0b0000); - expect(score.masterBars[1].alternateEndings).to.equal(0b0111); - expect(score.masterBars[2].alternateEndings).to.equal(0b1000); - testExportRoundtrip(score); - }); - - it('random-alternate-endings', () => { - const tex: string = ` - \\ro \\ae 1 1.1.1 | \\ae 2 2.1 | \\ae 3 3.1 | - 4.3.4*4 | - \\ae 1 1.1.1 | \\ae 2 2.1 | \\ae 3 3.1 | - 4.3.4*4 | - \\ae (1 3) 1.1.1 | \\ae 2 \\rc 3 2.1 | - `; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isRepeatStart).to.be.equal(true); - for (let i = 1; i <= 9; i++) { - expect(score.masterBars[i].isRepeatStart).to.be.equal(false); - } - for (let i = 0; i <= 8; i++) { - expect(score.masterBars[i].repeatCount).to.equal(0); - } - expect(score.masterBars[9].repeatCount).to.equal(3); - expect(score.masterBars[0].alternateEndings).to.equal(0b001); - expect(score.masterBars[1].alternateEndings).to.equal(0b010); - expect(score.masterBars[2].alternateEndings).to.equal(0b100); - expect(score.masterBars[3].alternateEndings).to.equal(0b000); - expect(score.masterBars[4].alternateEndings).to.equal(0b001); - expect(score.masterBars[5].alternateEndings).to.equal(0b010); - expect(score.masterBars[6].alternateEndings).to.equal(0b100); - expect(score.masterBars[7].alternateEndings).to.equal(0b000); - expect(score.masterBars[8].alternateEndings).to.equal(0b101); - expect(score.masterBars[9].alternateEndings).to.equal(0b010); - testExportRoundtrip(score); - }); - - it('default-transposition-on-instruments', () => { - const tex: string = ` - \\track "Piano with Grand Staff" "pno." - \\staff{score} \\tuning piano \\instrument acousticgrandpiano - c4 d4 e4 f4 | - \\staff{score} \\tuning piano \\clef F4 - c2 c2 c2 c2 | - \\track Guitar - \\staff{tabs} \\instrument acousticguitarsteel \\capo 5 - 1.2 3.2 0.1 1.1 - `; - const score: Score = parseTex(tex); - - expect(score.tracks[0].staves[0].transpositionPitch).to.equal(0); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(0); - expect(score.tracks[0].staves[1].transpositionPitch).to.equal(0); - expect(score.tracks[0].staves[1].displayTranspositionPitch).to.equal(0); - expect(score.tracks[1].staves[0].transpositionPitch).to.equal(0); - expect(score.tracks[1].staves[0].displayTranspositionPitch).to.equal(-12); - testExportRoundtrip(score); - }); - - it('dynamics', () => { - const tex: string = '1.1.8{dy ppp} 1.1{dy pp} 1.1{dy p} 1.1{dy mp} 1.1{dy mf} 1.1{dy f} 1.1{dy ff} 1.1{dy fff}'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].dynamics).to.equal(DynamicValue.PP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].dynamics).to.equal(DynamicValue.P); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].dynamics).to.equal(DynamicValue.MP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].dynamics).to.equal(DynamicValue.MF); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[5].dynamics).to.equal(DynamicValue.F); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[6].dynamics).to.equal(DynamicValue.FF); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[7].dynamics).to.equal(DynamicValue.FFF); - testExportRoundtrip(score); - }); - - it('dynamics-auto', () => { - const tex: string = '1.1.4{dy ppp} 1.1 1.1{dy mp} 1.1'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].dynamics).to.equal(DynamicValue.MP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].dynamics).to.equal(DynamicValue.MP); - testExportRoundtrip(score); - }); - - it('dynamics-auto-reset-on-track', () => { - const tex: string = '1.1.4{dy ppp} 1.1 \\track "Second" 1.1.4'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[1].staves[0].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.F); - testExportRoundtrip(score); - }); - - it('dynamics-auto-reset-on-staff', () => { - const tex: string = '1.1.4{dy ppp} 1.1 \\staff 1.1.4'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[1].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.F); - testExportRoundtrip(score); - }); - - it('crescendo', () => { - const tex: string = '1.1.4{dec} 1.1{dec} 1.1{cre} 1.1{cre}'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].crescendo).to.equal(CrescendoType.Decrescendo); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].crescendo).to.equal(CrescendoType.Decrescendo); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].crescendo).to.equal(CrescendoType.Crescendo); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].crescendo).to.equal(CrescendoType.Crescendo); - testExportRoundtrip(score); - }); - - it('left-hand-tapping', () => { - const tex: string = ':4 1.1{lht} 1.1 1.1{lht} 1.1'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isLeftHandTapped).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].isLeftHandTapped).to.equal(false); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].isLeftHandTapped).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].isLeftHandTapped).to.equal(false); - testExportRoundtrip(score); - }); - - it('expect-invalid-format-xml', () => { - expect(() => parseTex('')).to.throw(UnsupportedFormatError); - }); - - it('expect-invalid-format-other-text', () => { - expect(() => parseTex('This is not an alphaTex file')).to.throw(UnsupportedFormatError); - }); - - it('auto-detect-tuning-from-instrument', () => { - let score = parseTex('\\instrument acousticguitarsteel . 3.3'); - expect(score.tracks[0].staves[0].tuning.length).to.equal(6); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(-12); - - score = parseTex('\\instrument acousticbass . 3.3'); - expect(score.tracks[0].staves[0].tuning.length).to.equal(4); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(-12); - - score = parseTex('\\instrument violin . 3.3'); - expect(score.tracks[0].staves[0].tuning.length).to.equal(4); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(0); - - score = parseTex('\\instrument acousticpiano . 3.3'); - expect(score.tracks[0].staves[0].tuning.length).to.equal(0); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(0); - }); - - it('multibyte-encoding', () => { - const multiByteChars = '爱你ÖÄÜ🎸🎵🎶'; - const score = parseTex(`\\title "${multiByteChars}" - . - \\track "🎸" - \\lyrics "Test Lyrics 🤘" - (1.2 1.1).4 x.2.8 0.1 1.1 | 1.2 3.2 0.1 1.1`); - - expect(score.title).to.equal(multiByteChars); - expect(score.tracks[0].name).to.equal('🎸'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].lyrics![0]).to.equal('🤘'); - testExportRoundtrip(score); - }); - - it('does-not-hang-on-backslash', () => { - expect(() => parseTex('\\title Test . 3.3 \\')).to.throw(UnsupportedFormatError); - }); - - it('disallows-unclosed-string', () => { - expect(() => parseTex('\\title "Test . 3.3')).to.throw(UnsupportedFormatError); - }); - - function runSectionNoteSymbolTest(noteSymbol: string) { - const score = parseTex(`1.3.4 * 4 | \\section Verse ${noteSymbol}.1 | 2.3.4*4`); - - expect(score.masterBars.length).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(4); - expect(score.masterBars[1].section!.text).to.equal('Verse'); - expect(score.masterBars[1].section!.marker).to.equal(''); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).to.equal(1); - } - - it('does-not-interpret-note-symbols-on-section', () => { - runSectionNoteSymbolTest('r'); - runSectionNoteSymbolTest('-'); - runSectionNoteSymbolTest('x'); - }); - - it('loads-score-twice-without-hickups', () => { - const tex = `\\title Test - \\words test - \\music alphaTab - \\copyright test - \\tempo 200 - \\instrument 30 - \\capo 2 - \\tuning G3 D2 G2 B2 D3 A4 - . - 0.5.2 1.5.4 3.4.4 | 5.3.8 5.3.8 5.3.8 5.3.8 r.2`; - const importer: AlphaTexImporterOld = new AlphaTexImporterOld(); - for (const _i of [1, 2]) { - importer.initFromString(tex, new Settings()); - const score = importer.readScore(); - expect(score.title).to.equal('Test'); - expect(score.words).to.equal('test'); - expect(score.music).to.equal('alphaTab'); - expect(score.copyright).to.equal('test'); - expect(score.tempo).to.equal(200); - expect(score.tracks.length).to.equal(1); - expect(score.tracks[0].playbackInfo.program).to.equal(30); - expect(score.tracks[0].staves[0].capo).to.equal(2); - expect(score.tracks[0].staves[0].tuning.join(',')).to.equal('55,38,43,47,50,69'); - expect(score.masterBars.length).to.equal(2); - - // bars[0] - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].duration).to.equal(Duration.Half); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].string).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].duration).to.equal(Duration.Quarter); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].fret).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].string).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].duration).to.equal(Duration.Quarter); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].string).to.equal(3); - - // bars[1] - expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].notes.length).to.equal(0); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].duration).to.equal(Duration.Half); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].isRest).to.equal(true); - } - }); - - it('error-shows-symbol-data', () => { - const tex = '3.3.ABC'; - expect(() => parseTex(tex)).to.throw(UnsupportedFormatError); - try { - parseTex(tex); - } catch (e) { - if (!(e instanceof UnsupportedFormatError)) { - assert.fail('Did not throw correct error'); - return; - } - if (!(e.cause instanceof AlphaTexError)) { - assert.fail('Did not contain an AlphaTexError'); - return; - } - const i = e.cause as AlphaTexError; - expect(i.expected).to.equal(AlphaTexSymbols.Number); - expect(i.message?.includes('Number')).to.be.true; - expect(i.symbol).to.equal(AlphaTexSymbols.String); - expect(i.message?.includes('String')).to.be.true; - expect(i.symbolData).to.equal('ABC'); - expect(i.message?.includes('ABC')).to.be.true; - } - }); - - it('tempo-as-float', () => { - const score = parseTex('\\tempo 112.5 .'); - expect(score.tempo).to.equal(112.5); - testExportRoundtrip(score); - }); - - it('tempo-as-float-in-bar', () => { - const score = parseTex('\\tempo 112 . 3.3.1 | \\tempo 333.3 3.3'); - expect(score.tempo).to.equal(112); - expect(score.tracks[0].staves[0].bars[1].masterBar.tempoAutomations[0]?.value).to.equal(333.3); - testExportRoundtrip(score); - }); - - it('percussion-numbers', () => { - const score = parseTex(` - \\instrument "percussion" - . - 30 31 33 34 - `); - expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(9); - expect(score.tracks[0].staves[0].isPercussion).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(0); - expect(score.tracks[0].percussionArticulations[0].outputMidiNumber).to.equal(49); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); - expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(40); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(2); - expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(37); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(3); - expect(score.tracks[0].percussionArticulations[3].outputMidiNumber).to.equal(38); - testExportRoundtrip(score); - }); - - it('percussion-custom-articulation', () => { - const score = parseTex(` - \\instrument "percussion" - \\articulation A 30 - \\articulation B 31 - \\articulation C 33 - \\articulation D 34 - . - A B C D - `); - expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(9); - expect(score.tracks[0].staves[0].isPercussion).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(0); - expect(score.tracks[0].percussionArticulations[0].outputMidiNumber).to.equal(49); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); - expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(40); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(2); - expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(37); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(3); - expect(score.tracks[0].percussionArticulations[3].outputMidiNumber).to.equal(38); - testExportRoundtrip(score); - }); - - it('percussion-default-articulations', () => { - const score = parseTex(` - \\instrument "percussion" - \\articulation defaults - . - "Cymbal (hit)" "Snare (side stick)" "Snare (side stick) 2" "Snare (hit)" - `); - expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(9); - expect(score.tracks[0].staves[0].isPercussion).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(0); - expect(score.tracks[0].percussionArticulations[0].outputMidiNumber).to.equal(49); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); - expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(40); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(2); - expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(37); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(3); - expect(score.tracks[0].percussionArticulations[3].outputMidiNumber).to.equal(38); - testExportRoundtrip(score); - }); - - it('percussion-default-articulations-short', () => { - const score = parseTex(` - \\instrument "percussion" - \\articulation defaults - . - CymbalHit SnareSideStick SnareSideStick2 SnareHit - `); - expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(9); - expect(score.tracks[0].staves[0].isPercussion).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(0); - expect(score.tracks[0].percussionArticulations[0].outputMidiNumber).to.equal(49); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); - expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(40); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(2); - expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(37); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(3); - expect(score.tracks[0].percussionArticulations[3].outputMidiNumber).to.equal(38); - testExportRoundtrip(score); - }); - - it('beat-tempo-change', () => { - const score = parseTex(` - . \\tempo 120 1.1.4 1.1 1.1{tempo 60} 1.1 | 1.1.4{tempo 100} 1.1 1.1{tempo 120} 1.1 - `); - expect(score.masterBars[0].tempoAutomations).to.have.length(2); - expect(score.masterBars[0].tempoAutomations[0].value).to.equal(120); - expect(score.masterBars[0].tempoAutomations[0].ratioPosition).to.equal(0); - expect(score.masterBars[0].tempoAutomations[1].value).to.equal(60); - expect(score.masterBars[0].tempoAutomations[1].ratioPosition).to.equal(0.5); - testExportRoundtrip(score); - }); - - it('note-accidentals', () => { - let tex = '. \n'; - const expectedAccidentalModes: NoteAccidentalMode[] = []; - for (const [k, v] of ModelUtils.accidentalModeMapping) { - if (k) { - tex += `3.3 { acc ${k} } \n`; - expectedAccidentalModes.push(v); - } - } - - const score = parseTex(tex); - - const actualAccidentalModes: NoteAccidentalMode[] = []; - - let b: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; - while (b != null) { - actualAccidentalModes.push(b.notes[0].accidentalMode); - b = b.nextBeat; - } - - expect(actualAccidentalModes.join(',')).to.equal(expectedAccidentalModes.join(',')); - testExportRoundtrip(score); - }); - - it('accidental-mode', () => { - // song level - let score = parseTex('\\accidentals auto . F##4'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.Default - ); - - // track level - score = parseTex('\\track "T1" F##4 | \\track "T2" \\accidentals auto F##4'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.ForceDoubleSharp - ); - expect(score.tracks[1].staves[0].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.Default - ); - - // staff level - score = parseTex('\\track "T1" \\staff F##4 \\staff \\accidentals auto F##4'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.ForceDoubleSharp - ); - expect(score.tracks[0].staves[1].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.Default - ); - - // bar level - score = parseTex('F##4 | \\accidentals auto F##4'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.ForceDoubleSharp - ); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.Default - ); - testExportRoundtrip(score); - }); - - it('dead-slap', () => { - const score = parseTex('r { ds }'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].isRest).to.be.false; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].deadSlapped).to.be.true; - testExportRoundtrip(score); - }); - - it('golpe', () => { - const score = parseTex('3.3 { glpf } 3.3 { glpt }'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].golpe).to.equal(GolpeType.Finger); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].golpe).to.equal(GolpeType.Thumb); - testExportRoundtrip(score); - }); - - it('fade', () => { - const score = parseTex('3.3 { f } 3.3 { fo } 3.3 { vs } '); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].fade).to.equal(FadeType.FadeIn); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].fade).to.equal(FadeType.FadeOut); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].fade).to.equal(FadeType.VolumeSwell); - testExportRoundtrip(score); - }); - - it('barre', () => { - const score = parseTex('3.3 { barre 5 } 3.3 { barre 14 half }'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].barreFret).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].barreShape).to.equal(BarreShape.Full); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].barreFret).to.equal(14); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].barreShape).to.equal(BarreShape.Half); - testExportRoundtrip(score); - testExportRoundtrip(score); - }); - - it('ornaments', () => { - const score = parseTex('3.3 { turn } 3.3 { iturn } 3.3 { umordent } 3.3 { lmordent }'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].ornament).to.equal(NoteOrnament.Turn); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].ornament).to.equal( - NoteOrnament.InvertedTurn - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].ornament).to.equal( - NoteOrnament.UpperMordent - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].ornament).to.equal( - NoteOrnament.LowerMordent - ); - testExportRoundtrip(score); - }); - - it('rasgueado', () => { - const score = parseTex('3.3 { rasg mi } 3.3 { rasg pmptriplet } 3.3 { rasg amianapaest }'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].rasgueado).to.equal(Rasgueado.Mi); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].hasRasgueado).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].rasgueado).to.equal(Rasgueado.PmpTriplet); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].hasRasgueado).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].rasgueado).to.equal(Rasgueado.AmiAnapaest); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].hasRasgueado).to.be.true; - testExportRoundtrip(score); - }); - - it('directions', () => { - const score = parseTex('. \\jump Segno | | \\jump DaCapoAlCoda \\jump Coda \\jump SegnoSegno '); - expect(score.masterBars[0].directions).to.be.ok; - expect(score.masterBars[0].directions).to.contain(Direction.TargetSegno); - - expect(score.masterBars[1].directions).to.not.be.ok; - - expect(score.masterBars[2].directions).to.be.ok; - expect(score.masterBars[2].directions).to.contain(Direction.JumpDaCapoAlCoda); - expect(score.masterBars[2].directions).to.contain(Direction.TargetCoda); - expect(score.masterBars[2].directions).to.contain(Direction.TargetSegnoSegno); - testExportRoundtrip(score); - }); - - it('multi-voice-full', () => { - const score = parseTex(` - \\track "Piano" - \\staff{score} \\tuning piano \\instrument acousticgrandpiano - \\voice - c4 d4 e4 f4 | c4 d4 e4 f4 - \\voice - c3 d3 e3 f3 | c3 d3 e3 f3 - `); - - expect(score.masterBars).to.have.length(2); - - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[1].voices.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-voice-simple-all-voices', () => { - const score = parseTex(` - \\voice - c4 d4 e4 f4 | c4 d4 e4 f4 - \\voice - c3 d3 e3 f3 | c3 d3 e3 f3 - `); - - expect(score.masterBars).to.have.length(2); - - expect(score.tracks[0].staves[0].bars).to.have.length(2); - expect(score.tracks[0].staves[0].bars[0].voices).to.have.length(2); - expect(score.tracks[0].staves[0].bars[1].voices).to.have.length(2); - testExportRoundtrip(score); - }); - - it('multi-voice-simple-skip-initial', () => { - const score = parseTex(` - c4 d4 e4 f4 | c4 d4 e4 f4 - \\voice - c3 d3 e3 f3 | c3 d3 e3 f3 - `); - - expect(score.masterBars).to.have.length(2); - - expect(score.tracks[0].staves[0].bars).to.have.length(2); - expect(score.tracks[0].staves[0].bars[0].voices).to.have.length(2); - expect(score.tracks[0].staves[0].bars[1].voices).to.have.length(2); - testExportRoundtrip(score); - }); - - it('standard-notation-line-count', () => { - const score = parseTex(` - \\staff { score 3 } - `); - expect(score.tracks[0].staves[0].standardNotationLineCount).to.equal(3); - testExportRoundtrip(score); - }); - - it('song-metadata', () => { - const score = parseTex(` - \\title "Title\\tTitle" - \\instructions "Line1\nLine2" - . - `); - expect(score.title).to.equal('Title\tTitle'); - expect(score.instructions).to.equal('Line1\nLine2'); - testExportRoundtrip(score); - }); - - it('tempo-label', () => { - const score = parseTex(` - \\tempo 80 "Label" - . - `); - expect(score.tempo).to.equal(80); - expect(score.tempoLabel).to.equal('Label'); - testExportRoundtrip(score); - }); - - it('transpose', () => { - const score = parseTex(` - \\staff - \\displaytranspose 12 - \\transpose 6 - . - `); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(-12); - expect(score.tracks[0].staves[0].transpositionPitch).to.equal(-6); - testExportRoundtrip(score); - }); - - it('beat-vibrato', () => { - const score = parseTex(` - 3.3.4{v} 3.3.4{vw} - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].vibrato).to.equal(VibratoType.Slight); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].vibrato).to.equal(VibratoType.Wide); - testExportRoundtrip(score); - }); - - it('note-vibrato', () => { - const score = parseTex(` - 3.3{v}.4 3.3{vw}.4 - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].vibrato).to.equal(VibratoType.Slight); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].vibrato).to.equal(VibratoType.Wide); - testExportRoundtrip(score); - }); - - it('whammy', () => { - const score = parseTex(` - 3.3.4{ tb dive (0 -12.5) } | - 3.3.4{ tb dive gradual (0 -12.5) } | - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarType).to.equal(WhammyType.Dive); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints).to.have.length(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![0].value).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![1].value).to.equal(-12.5); - - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarType).to.equal(WhammyType.Dive); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyStyle).to.equal(BendStyle.Gradual); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints).to.have.length(2); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![0].value).to.equal(0); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![1].value).to.equal(-12.5); - testExportRoundtrip(score); - }); - - it('beat-ottava', () => { - const score = parseTex(` - 3.3.4{ ot 15ma } 3.3.4{ ot 8vb } - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].ottava).to.equal(Ottavia._15ma); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].ottava).to.equal(Ottavia._8vb); - testExportRoundtrip(score); - }); - - it('beat-text', () => { - const score = parseTex(` - 3.3.4{ txt "Hello World" } 3.3.4{ txt Hello } - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].text).to.equal('Hello World'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].text).to.equal('Hello'); - testExportRoundtrip(score); - }); - - it('legato-origin', () => { - const score = parseTex(` - 3.3.4{ legatoOrigin } 4.3.4 - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].isLegatoOrigin).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].isLegatoDestination).to.be.true; - testExportRoundtrip(score); - }); - - it('instrument-change', () => { - const score = parseTex(` - \\instrument acousticgrandpiano - G4 G4 G4 { instrument brightacousticpiano } - `); - expect(score.tracks[0].playbackInfo.program).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations).to.have.length(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].type).to.equal( - AutomationType.Instrument - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].value).to.equal(1); - testExportRoundtrip(score); - }); - - it('beat-fermata', () => { - const score = parseTex(` - G4 G4 G4 { fermata medium 4 } - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].fermata).to.be.ok; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].fermata!.type).to.equal(FermataType.Medium); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].fermata!.length).to.equal(4); - testExportRoundtrip(score); - }); - - it('bend-type', () => { - const score = parseTex(` - 3.3{ b bend gradual (0 4)} - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendType).to.equal(BendType.Bend); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendStyle).to.equal(BendStyle.Gradual); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints).to.have.length(2); - testExportRoundtrip(score); - }); - - it('harmonic-values', () => { - const score = parseTex(` - 2.3{nh} 2.3{ah} 2.3{ah 7} 2.3{th} 2.3{th 7} 2.3{ph} 2.3{ph 7} 2.3{sh} 2.3{sh 7} 2.3{fh} 2.3{fh 7} - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].harmonicType).to.equal( - HarmonicType.Natural - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].harmonicValue).to.equal(2.4); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].harmonicType).to.equal( - HarmonicType.Artificial - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].harmonicValue).to.equal(0); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].harmonicType).to.equal( - HarmonicType.Artificial - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].harmonicValue).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].harmonicType).to.equal(HarmonicType.Tap); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].harmonicValue).to.equal(0); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].harmonicType).to.equal(HarmonicType.Tap); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].harmonicValue).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[5].notes[0].harmonicType).to.equal(HarmonicType.Pinch); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[5].notes[0].harmonicValue).to.equal(0); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[6].notes[0].harmonicType).to.equal(HarmonicType.Pinch); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[6].notes[0].harmonicValue).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[7].notes[0].harmonicType).to.equal(HarmonicType.Semi); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[7].notes[0].harmonicValue).to.equal(0); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[8].notes[0].harmonicType).to.equal(HarmonicType.Semi); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[8].notes[0].harmonicValue).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[9].notes[0].harmonicType).to.equal( - HarmonicType.Feedback - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[9].notes[0].harmonicValue).to.equal(0); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[10].notes[0].harmonicType).to.equal( - HarmonicType.Feedback - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[10].notes[0].harmonicValue).to.equal(7); - testExportRoundtrip(score); - }); - - it('time-signature-commons', () => { - const score = parseTex(` - \\ts common - `); - expect(score.masterBars[0].timeSignatureNumerator).to.equal(4); - expect(score.masterBars[0].timeSignatureDenominator).to.equal(4); - expect(score.masterBars[0].timeSignatureCommon).to.be.true; - testExportRoundtrip(score); - }); - - it('clef-ottava', () => { - const score = parseTex(` - \\ottava 15ma - `); - expect(score.tracks[0].staves[0].bars[0].clefOttava).to.equal(Ottavia._15ma); - testExportRoundtrip(score); - }); - - it('simile-mark', () => { - const score = parseTex(` - \\simile simple - `); - expect(score.tracks[0].staves[0].bars[0].simileMark).to.equal(SimileMark.Simple); - testExportRoundtrip(score); - }); - - it('tempo-automation-text', () => { - const score = parseTex(` - \\tempo 100 T1 - . - 3.3.4 * 4 | \\tempo 80 T2 4.3.4*4 - `); - expect(score.tempo).to.equal(100); - expect(score.tempoLabel).to.equal('T1'); - - expect(score.masterBars[1].tempoAutomations).to.have.length(1); - expect(score.masterBars[1].tempoAutomations[0].value).to.equal(80); - expect(score.masterBars[1].tempoAutomations[0].text).to.equal('T2'); - testExportRoundtrip(score); - }); - - it('double-bar', () => { - const tex: string = '3.3 3.3 3.3 3.3 | \\db 1.1 2.1 3.1 4.1'; - const score: Score = parseTex(tex); - expect(score.masterBars[1].isDoubleBar).to.be.equal(true); - testExportRoundtrip(score); - }); - - it('score-options', () => { - const score = parseTex(` - \\defaultSystemsLayout 5 - \\systemsLayout 3 2 3 - \\hideDynamics - \\bracketExtendMode nobrackets - \\useSystemSignSeparator - \\singleTrackTrackNamePolicy allsystems - \\multiTrackTrackNamePolicy Hidden - \\firstSystemTrackNameMode fullname - \\otherSystemsTrackNameMode fullname - \\firstSystemTrackNameOrientation horizontal - \\otherSystemsTrackNameOrientation horizontal - . - `); - - expect(score.defaultSystemsLayout).to.equal(5); - expect(score.systemsLayout).to.have.length(3); - expect(score.systemsLayout[0]).to.equal(3); - expect(score.systemsLayout[1]).to.equal(2); - expect(score.systemsLayout[2]).to.equal(3); - expect(score.stylesheet.hideDynamics).to.be.true; - expect(score.stylesheet.bracketExtendMode).to.equal(BracketExtendMode.NoBrackets); - expect(score.stylesheet.useSystemSignSeparator).to.be.true; - expect(score.stylesheet.singleTrackTrackNamePolicy).to.equal(TrackNamePolicy.AllSystems); - expect(score.stylesheet.multiTrackTrackNamePolicy).to.equal(TrackNamePolicy.Hidden); - expect(score.stylesheet.firstSystemTrackNameMode).to.equal(TrackNameMode.FullName); - expect(score.stylesheet.otherSystemsTrackNameMode).to.equal(TrackNameMode.FullName); - expect(score.stylesheet.firstSystemTrackNameOrientation).to.equal(TrackNameOrientation.Horizontal); - expect(score.stylesheet.otherSystemsTrackNameOrientation).to.equal(TrackNameOrientation.Horizontal); - testExportRoundtrip(score); - }); - - it('bar-sizing', () => { - const score = parseTex(` - 3.3.4 | \\scale 0.5 3.3.4 | \\width 300 3.3.4 - `); - - expect(score.masterBars[1].displayScale).to.equal(0.5); - expect(score.masterBars[2].displayWidth).to.equal(300); - testExportRoundtrip(score); - }); - - it('track-properties', () => { - const score = parseTex(` - \\track "First" { - color "#FF0000" - defaultSystemsLayout 6 - systemsLayout 3 2 3 - volume 7 - balance 3 - mute - solo - } - `); - - expect(score.tracks[0].color.rgba).to.equal('#FF0000'); - expect(score.tracks[0].defaultSystemsLayout).to.equal(6); - expect(score.tracks[0].systemsLayout).to.have.length(3); - expect(score.tracks[0].systemsLayout[0]).to.equal(3); - expect(score.tracks[0].systemsLayout[1]).to.equal(2); - expect(score.tracks[0].systemsLayout[0]).to.equal(3); - expect(score.tracks[0].playbackInfo.volume).to.equal(7); - expect(score.tracks[0].playbackInfo.balance).to.equal(3); - expect(score.tracks[0].playbackInfo.isMute).to.be.true; - expect(score.tracks[0].playbackInfo.isSolo).to.be.true; - testExportRoundtrip(score); - }); - - it('beat-beam', () => { - const score = parseTex(` - :8 3.3{ beam invert } 3.3 | - 3.3{ beam up } 3.3 | - 3.3{ beam down } 3.3 | - 3.3{ beam auto } 3.3 | - 3.3{ beam split } 3.3 | - 3.3{ beam merge } 3.3 | - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].invertBeamDirection).to.be.true; - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].preferredBeamDirection).to.equal(BeamDirection.Up); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].preferredBeamDirection).to.equal( - BeamDirection.Down - ); - expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].beamingMode).to.equal(BeatBeamingMode.Auto); - expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].beamingMode).to.equal( - BeatBeamingMode.ForceSplitToNext - ); - expect(score.tracks[0].staves[0].bars[5].voices[0].beats[0].beamingMode).to.equal( - BeatBeamingMode.ForceMergeWithNext - ); - testExportRoundtrip(score); - }); - - it('note-show-string', () => { - const score = parseTex(` - :8 3.3{ string } - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].showStringNumber).to.be.true; - testExportRoundtrip(score); - }); - - it('note-hide', () => { - const score = parseTex(` - :8 3.3{ hide } - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isVisible).to.be.false; - }); - - it('note-slur', () => { - const score = parseTex(` - :8 (3.3{ slur s1 } 3.4 3.5) (10.3 {slur s1} 17.4 15.5) - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isSlurOrigin).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].isSlurDestination).to.be.true; - testExportRoundtrip(score); - }); - - it('hide-tuning', () => { - const score = parseTex(` - \\track "Track 1" - \\track "Track 2" - \\staff {tabs} - \\tuning A1 D2 A2 D3 G3 B3 E4 hide - 4.1 3.1 2.1 1.1`); - - expect(score.tracks[1].staves[0].stringTuning.tunings[0]).to.equal(33); - expect(score.stylesheet.perTrackDisplayTuning).to.be.ok; - expect(score.stylesheet.perTrackDisplayTuning!.has(1)).to.be.true; - expect(score.stylesheet.perTrackDisplayTuning!.get(1)).to.be.false; - testExportRoundtrip(score); - }); - - it('clefs', () => { - const score = parseTex(` - \\clef C4 \\ottava 15ma C4 | C4 | - \\clef 0 | \\clef 48 | \\clef 60 | \\clef 65 | \\clef 43 | - \\clef Neutral | \\clef C3 | \\clef C4 | \\clef F4 | \\clef G2 | - \\clef "Neutral" | \\clef "C3" | \\clef "C4" | \\clef "F4" | \\clef "G2" | - \\clef n | \\clef alto | \\clef tenor | \\clef bass | \\clef treble | - \\clef "n" | \\clef "alto" | \\clef "tenor" | \\clef "bass" | \\clef "treble" - `); - let barIndex = 0; - expect(score.tracks[0].staves[0].bars[barIndex].clef).to.equal(Clef.C4); - expect(score.tracks[0].staves[0].bars[barIndex++].clefOttava).to.equal(Ottavia._15ma); - expect(score.tracks[0].staves[0].bars[barIndex].clef).to.equal(Clef.C4); - expect(score.tracks[0].staves[0].bars[barIndex++].clefOttava).to.equal(Ottavia._15ma); - - for (let i = 0; i < 5; i++) { - expect(score.tracks[0].staves[0].bars[barIndex++].clef).to.equal( - Clef.Neutral, - `Invalid clef at index ${barIndex - 1}` - ); - expect(score.tracks[0].staves[0].bars[barIndex++].clef).to.equal( - Clef.C3, - `Invalid clef at index ${barIndex - 1}` - ); - expect(score.tracks[0].staves[0].bars[barIndex++].clef).to.equal( - Clef.C4, - `Invalid clef at index ${barIndex - 1}` - ); - expect(score.tracks[0].staves[0].bars[barIndex++].clef).to.equal( - Clef.F4, - `Invalid clef at index ${barIndex - 1}` - ); - expect(score.tracks[0].staves[0].bars[barIndex++].clef).to.equal( - Clef.G2, - `Invalid clef at index ${barIndex - 1}` - ); - } - - testExportRoundtrip(score); - }); - - it('multibar-rest', () => { - const score = parseTex(` - \\multiBarRest - . - \\track A { multiBarRest } - 3.3 - \\track B - 3.3 - - `); - expect(score.stylesheet.multiTrackMultiBarRest).to.be.true; - expect(score.stylesheet.perTrackMultiBarRest).to.be.ok; - expect(score.stylesheet.perTrackMultiBarRest!.has(0)).to.be.true; - expect(score.stylesheet.perTrackMultiBarRest!.has(1)).to.be.false; - testExportRoundtrip(score); - }); - - it('header-footer', async () => { - const score = parseTex(` - \\title "Title" "Title: %TITLE%" left - \\subtitle "Subtitle" "Subtitle: %SUBTITLE%" center - \\artist "Artist" "Artist: %ARTIST%" right - \\album "Album" "Album: %ALBUM%" left - \\words "Words" "Words: %WORDS%" center - \\music "Music" "Music: %MUSIC%" right - \\wordsAndMusic "Words & Music: %MUSIC%" left - \\tab "Tab" "Transcriber: %TABBER%" center - \\copyright "Copyright" "Copyright: %COPYRIGHT%" right - \\copyright2 "Copyright2" right - . - `); - - expect(score.style).to.be.ok; - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Title)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Title)!.template).to.equal('Title: %TITLE%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Title)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Title)!.textAlign).to.equal(TextAlign.Left); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.SubTitle)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.SubTitle)!.template).to.equal('Subtitle: %SUBTITLE%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.SubTitle)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.SubTitle)!.textAlign).to.equal(TextAlign.Center); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Artist)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Artist)!.template).to.equal('Artist: %ARTIST%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Artist)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Artist)!.textAlign).to.equal(TextAlign.Right); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Album)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Album)!.template).to.equal('Album: %ALBUM%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Album)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Album)!.textAlign).to.equal(TextAlign.Left); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Words)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Words)!.template).to.equal('Words: %WORDS%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Words)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Words)!.textAlign).to.equal(TextAlign.Center); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Music)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Music)!.template).to.equal('Music: %MUSIC%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Music)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Music)!.textAlign).to.equal(TextAlign.Right); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.WordsAndMusic)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.WordsAndMusic)!.template).to.equal( - 'Words & Music: %MUSIC%' - ); - expect(score.style!.headerAndFooter.get(ScoreSubElement.WordsAndMusic)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.WordsAndMusic)!.textAlign).to.equal(TextAlign.Left); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Transcriber)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Transcriber)!.template).to.equal( - 'Transcriber: %TABBER%' - ); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Transcriber)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Transcriber)!.textAlign).to.equal(TextAlign.Center); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Copyright)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Copyright)!.template).to.equal( - 'Copyright: %COPYRIGHT%' - ); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Copyright)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Copyright)!.textAlign).to.equal(TextAlign.Right); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.CopyrightSecondLine)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.template).to.equal('Copyright2'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.textAlign).to.equal( - TextAlign.Right - ); - testExportRoundtrip(score); - }); - - it('barlines', () => { - const score = parseTex(` - \\instrument piano - . - \\track "T1" - \\staff - \\barlineleft dashed - \\barlineright dotted - | - \\barlineleft heavyheavy - \\barlineright heavyheavy - - \\staff - \\barlineleft lightlight - \\barlineright lightheavy - | - \\barlineleft heavylight - \\barlineright dashed - `); - expect(score).toMatchSnapshot(); - }); - - it('sync', () => { - const score = parseTex(` - \\tempo 90 - . - 3.4.4*4 | 3.4.4*4 | - \\ro 3.4.4*4 | 3.4.4*4 | \\rc 2 3.4.4*4 | - 3.4.4*4 | 3.4.4*4 - . - \\sync 0 0 0 - \\sync 0 0 1000 0.5 - \\sync 1 0 2000 - \\sync 3 0 3000 - \\sync 3 1 4000 - \\sync 6 1 5000 - `); - - // simplify snapshot - const tracks = score.tracks; - score.tracks = []; - - expect(score).toMatchSnapshot(); - - score.tracks = tracks; - testExportRoundtrip(score); - }); - - it('sync-expect-dot', () => { - const score = parseTex(` - \\title "Prelude in D Minor" - \\artist "J.S. Bach (1685-1750)" - \\copyright "Public Domain" - \\tempo 80 - . - \\ts 3 4 - 0.4.16 (3.2 -.4) (1.1 -.4) (5.1 -.4) 1.1 3.2 1.1 3.2 2.3.8 (3.2 3.4) | - (3.2 0.4).16 (3.2 -.4) (1.1 -.4) (5.1 -.4) 1.1 3.2 1.1 3.2 2.3.8 (3.2 3.4) | - (3.2 0.4).16 (3.2 -.4) (3.1 -.4) (6.1 -.4) 3.1 3.2 3.1 3.2 3.3.8 (3.2 0.3) | - (3.2 0.4).16 (3.2 -.4) (3.1 -.4) (6.1 -.4) 3.1 3.2 3.1 3.2 3.3.8 (3.2 0.3) | - . - \\sync 0 0 0 - \\sync 0 0 1500 0.666 - \\sync 1 0 4075 0.666 - \\sync 2 0 6475 0.333 - \\sync 3 0 10223 1 - `); - - // simplify snapshot - const tracks = score.tracks; - score.tracks = []; - - expect(score).toMatchSnapshot(); - - score.tracks = tracks; - testExportRoundtrip(score); - }); - - it('tuning-name', () => { - const score = parseTex(` - \\tuning E4 B3 G3 D3 A2 E2 "Default" - `); - - expect(score.tracks[0].staves[0].stringTuning.tunings.join(',')).to.equal( - Tuning.getDefaultTuningFor(6)!.tunings.join(',') - ); - expect(score.tracks[0].staves[0].stringTuning.name).to.equal('Default'); - testExportRoundtrip(score); - }); - - it('volume-change', () => { - const score = parseTex(` - \\track "T1" { - volume 7 - } - G4 G4 { volume 8 } G4 { volume 9 } - `); - - expect(score.tracks[0].playbackInfo.volume).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations).to.have.length(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].type).to.equal( - AutomationType.Volume - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].value).to.equal(8); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations).to.have.length(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].type).to.equal( - AutomationType.Volume - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].value).to.equal(9); - testExportRoundtrip(score); - }); - - it('balance-change', () => { - const score = parseTex(` - \\track "T1" { - balance 7 - } - G4 G4 { balance 8 } G4 { balance 9 } - `); - - expect(score.tracks[0].playbackInfo.balance).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations).to.have.length(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].type).to.equal( - AutomationType.Balance - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].value).to.equal(8); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations).to.have.length(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].type).to.equal( - AutomationType.Balance - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].value).to.equal(9); - testExportRoundtrip(score); - }); - - it('beat-barre', () => { - const score = parseTex(` - 3.3.4 { barre 5 half } - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].barreFret).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].barreShape).to.equal(BarreShape.Half); - testExportRoundtrip(score); - }); - - it('beat-dead-slapped', () => { - const score = parseTex(` - ().16 {ds} - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].deadSlapped).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes).to.have.length(0); - testExportRoundtrip(score); - }); - - function parseNumberOrNameTest(tex: string, allowFloats: boolean, expectedSymbols: string[]) { - const lexer = new AlphaTexLexerOld(tex); - lexer.init(allowFloats); - - const actualSymbols: string[] = []; - do { - actualSymbols.push(`${AlphaTexSymbols[lexer.sy]}(${lexer.syData})`); - lexer.sy = lexer.newSy(allowFloats); - } while (lexer.sy !== AlphaTexSymbols.Eof); - - expect(actualSymbols.join(',')).to.equal(expectedSymbols.join(',')); - } - - it('parses-numbers-and-names', () => { - parseNumberOrNameTest('1', false, ['Number(1)']); - parseNumberOrNameTest('1', true, ['Number(1)']); - - parseNumberOrNameTest('1.1', false, ['Number(1)', 'Dot(.)', 'Number(1)']); - parseNumberOrNameTest('1.1', true, ['Number(1.1)']); - - parseNumberOrNameTest('1.1.4', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'Dot(.)', 'Number(4)']); - parseNumberOrNameTest('1.1.4', true, ['Number(1.1)', 'Dot(.)', 'Number(4)']); - - parseNumberOrNameTest('1.1 .4', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'Dot(.)', 'Number(4)']); - parseNumberOrNameTest('1.1 .4', true, ['Number(1.1)', 'Dot(.)', 'Number(4)']); - - parseNumberOrNameTest('1 .1.4', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'Dot(.)', 'Number(4)']); - parseNumberOrNameTest('1 .1.4', true, ['Number(1)', 'Dot(.)', 'Number(1.4)']); - - parseNumberOrNameTest('-1', false, ['Number(-1)']); - parseNumberOrNameTest('-1', true, ['Number(-1)']); - - parseNumberOrNameTest('-1.1', false, ['Number(-1)', 'Dot(.)', 'Number(1)']); - parseNumberOrNameTest('-1.1', true, ['Number(-1.1)']); - - parseNumberOrNameTest('-1.-1', false, ['Number(-1)', 'Dot(.)', 'Number(-1)']); - parseNumberOrNameTest('-1.-1', true, ['Number(-1)', 'Dot(.)', 'Number(-1)']); - - parseNumberOrNameTest('-.1', false, ['String(-)', 'Dot(.)', 'Number(1)']); - parseNumberOrNameTest('-.1', true, ['String(-)', 'Dot(.)', 'Number(1)']); - - parseNumberOrNameTest('1.1(', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'LParensis(()']); - parseNumberOrNameTest('1.1(', true, ['Number(1.1)', 'LParensis(()']); - - parseNumberOrNameTest('1.1{', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'LBrace({)']); - parseNumberOrNameTest('1.1{', true, ['Number(1.1)', 'LBrace({)']); - - parseNumberOrNameTest('1.1|', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'Pipe(|)']); - parseNumberOrNameTest('1.1|', true, ['Number(1.1)', 'Pipe(|)']); - - parseNumberOrNameTest('1.1a', false, ['Number(1)', 'Dot(.)', 'String(1a)']); - parseNumberOrNameTest('1.1a', true, ['String(1.1a)']); // ['Number(1.1a)', 'Dot(.)', 'String(1a)'] would be better but its good enough what we have - - parseNumberOrNameTest('1a.1', false, ['String(1a)', 'Dot(.)', 'Number(1)']); - parseNumberOrNameTest('1a.1', true, ['String(1a)', 'Dot(.)', 'Number(1)']); - - parseNumberOrNameTest('1.1\\test', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'MetaCommand(test)']); - parseNumberOrNameTest('1.1\\test', true, ['Number(1.1)', 'MetaCommand(test)']); - }); - - it('unicode-escape', () => { - const score = parseTex(` - \\title "\\uD83D\\uDE38" - . - `); - - expect(score.title).to.equal('😸'); - }); - - it('utf16', () => { - const score = parseTex(`\\title "🤘🏻" .`); - - expect(score.title).to.equal('🤘🏻'); - }); - - it('beat-lyrics', () => { - const score = parseTex(` - . - 3.3.3 - 3.3.3 {lyrics "A"} - 3.3.3 {lyrics 0 "B C D"} - 3.3.3 {lyrics 0 "E" lyrics 1 "F" lyrics 2 "G"} - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].lyrics).to.not.be.ok; - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].lyrics).to.be.ok; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].lyrics!.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].lyrics![0]).to.equal('A'); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].lyrics).to.be.ok; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].lyrics!.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].lyrics![0]).to.equal('B C D'); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].lyrics).to.be.ok; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].lyrics!.length).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].lyrics![0]).to.equal('E'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].lyrics![1]).to.equal('F'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].lyrics![2]).to.equal('G'); - - testExportRoundtrip(score); - }); - - it('bank', () => { - const score = parseTex(` - \\track "Piano" { instrument electricpiano1} - c4 d4 e4 f4 - - \\track "Piano" { instrument electricpiano1 bank 2 } - c4 d4 e4 f4 - `); - - expect(score.tracks[0].playbackInfo.program).to.equal(4); - expect(score.tracks[0].playbackInfo.bank).to.equal(0); - - expect(score.tracks[1].playbackInfo.program).to.equal(4); - expect(score.tracks[1].playbackInfo.bank).to.equal(2); - - testExportRoundtrip(score); - }); - - it('hide-tempo', () => { - const score = parseTex(` - . - \\tempo (120 "Moderate" 0) - c4 d4 e4 f4 | - \\tempo (120 "Moderate" 0 hide) - c4 d4 e4 f4 - `); - - expect(score.masterBars[0].tempoAutomations[0].isVisible).to.be.true; - expect(score.masterBars[1].tempoAutomations[0].isVisible).to.be.false; - - testExportRoundtrip(score); - }); -}); diff --git a/packages/alphatab/test/importer/AlphaTexImporterOld.ts b/packages/alphatab/test/importer/AlphaTexImporterOld.ts deleted file mode 100644 index 5ae07a607..000000000 --- a/packages/alphatab/test/importer/AlphaTexImporterOld.ts +++ /dev/null @@ -1,3733 +0,0 @@ -import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; -import { BeatCloner } from '@coderline/alphatab/generated/model/BeatCloner'; -import { AlphaTexAccidentalMode } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; -import { ScoreImporter } from '@coderline/alphatab/importer/ScoreImporter'; -import { UnsupportedFormatError } from '@coderline/alphatab/importer/UnsupportedFormatError'; -import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; -import { IOHelper } from '@coderline/alphatab/io/IOHelper'; -import { Logger } from '@coderline/alphatab/Logger'; -import { GeneralMidi } from '@coderline/alphatab/midi/GeneralMidi'; -import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; -import { Automation, AutomationType, type FlatSyncPoint } from '@coderline/alphatab/model/Automation'; -import { Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@coderline/alphatab/model/Bar'; -import { BarreShape } from '@coderline/alphatab/model/BarreShape'; -import { Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat'; -import { BendPoint } from '@coderline/alphatab/model/BendPoint'; -import { BendStyle } from '@coderline/alphatab/model/BendStyle'; -import { BendType } from '@coderline/alphatab/model/BendType'; -import { BrushType } from '@coderline/alphatab/model/BrushType'; -import { Chord } from '@coderline/alphatab/model/Chord'; -import { Clef } from '@coderline/alphatab/model/Clef'; -import { Color } from '@coderline/alphatab/model/Color'; -import { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; -import { Direction } from '@coderline/alphatab/model/Direction'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; -import { FadeType } from '@coderline/alphatab/model/FadeType'; -import { Fermata, FermataType } from '@coderline/alphatab/model/Fermata'; -import { Fingers } from '@coderline/alphatab/model/Fingers'; -import { GolpeType } from '@coderline/alphatab/model/GolpeType'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; -import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; -import { KeySignature } from '@coderline/alphatab/model/KeySignature'; -import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; -import { Lyrics } from '@coderline/alphatab/model/Lyrics'; -import { MasterBar } from '@coderline/alphatab/model/MasterBar'; -import { ModelUtils, type TuningParseResult } from '@coderline/alphatab/model/ModelUtils'; -import { Note } from '@coderline/alphatab/model/Note'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; -import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; -import { Ottavia } from '@coderline/alphatab/model/Ottavia'; -import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; -import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; -import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@coderline/alphatab/model/RenderStylesheet'; -import { Score, ScoreSubElement } from '@coderline/alphatab/model/Score'; -import { Section } from '@coderline/alphatab/model/Section'; -import { SimileMark } from '@coderline/alphatab/model/SimileMark'; -import { SlideInType } from '@coderline/alphatab/model/SlideInType'; -import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; -import type { Staff } from '@coderline/alphatab/model/Staff'; -import { Track } from '@coderline/alphatab/model/Track'; -import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; -import { VibratoType } from '@coderline/alphatab/model/VibratoType'; -import { Voice } from '@coderline/alphatab/model/Voice'; -import { WahPedal } from '@coderline/alphatab/model/WahPedal'; -import { WhammyType } from '@coderline/alphatab/model/WhammyType'; -import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import type { Settings } from '@coderline/alphatab/Settings'; -import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; - -/** - * A list of terminals recognized by the alphaTex-parser - * @public - */ -export enum AlphaTexSymbols { - No = 0, - Eof = 1, - Number = 2, - DoubleDot = 3, - Dot = 4, - String = 5, - Tuning = 6, - LParensis = 7, - RParensis = 8, - LBrace = 9, - RBrace = 10, - Pipe = 11, - MetaCommand = 12, - Multiply = 13, - LowerThan = 14 -} - -/** - * @internal - */ -enum StaffMetaResult { - KnownStaffMeta = 0, - UnknownStaffMeta = 1, - EndOfMetaDetected = 2 -} - -/** - * @internal - */ -export class AlphaTexError extends AlphaTabError { - public position: number; - public line: number; - public col: number; - public nonTerm: string; - public expected: AlphaTexSymbols; - public symbol: AlphaTexSymbols; - public symbolData: unknown; - - public constructor( - message: string | null, - position: number, - line: number, - col: number, - nonTerm: string | null, - expected: AlphaTexSymbols | null, - symbol: AlphaTexSymbols | null, - symbolData: unknown = null - ) { - super(AlphaTabErrorType.AlphaTex, message); - this.position = position; - this.line = line; - this.col = col; - this.nonTerm = nonTerm ?? ''; - this.expected = expected ?? AlphaTexSymbols.No; - this.symbol = symbol ?? AlphaTexSymbols.No; - this.symbolData = symbolData; - Object.setPrototypeOf(this, AlphaTexError.prototype); - } - - public static symbolError( - position: number, - line: number, - col: number, - nonTerm: string, - expected: AlphaTexSymbols, - symbol: AlphaTexSymbols, - symbolData: unknown = null - ): AlphaTexError { - let message = `MalFormed AlphaTex: @${position} (line ${line}, col ${col}): Error on block ${nonTerm}`; - if (expected !== symbol) { - message += `, expected a ${AlphaTexSymbols[expected]} found a ${AlphaTexSymbols[symbol]}`; - if (symbolData !== null) { - message += `: '${symbolData}'`; - } - } else { - message += `, invalid value: '${symbolData}'`; - } - return new AlphaTexError(message, position, line, col, nonTerm, expected, symbol, symbolData); - } - - public static errorMessage(message: string, position: number, line: number, col: number): AlphaTexError { - message = `MalFormed AlphaTex: @${position} (line ${line}, col ${col}): ${message}`; - return new AlphaTexError(message, position, line, col, null, null, null, null); - } -} - -/** - * @internal - */ -export class AlphaTexLexerOld { - private static readonly _eof: number = 0; - - private _position: number = 0; - private _line: number = 1; - private _col: number = 0; - - private _codepoints: number[]; - private _codepoint: number = AlphaTexLexerOld._eof; - - public sy: AlphaTexSymbols = AlphaTexSymbols.No; - public syData: unknown = ''; - - public lastValidSpot: number[] = [0, 1, 0]; - - public allowTuning: boolean = false; - public logErrors: boolean = true; - - public constructor(input: string) { - this._codepoints = [...IOHelper.iterateCodepoints(input)]; - } - - public init(allowFloats: boolean = false) { - this._position = 0; - this._line = 1; - this._col = 0; - this._saveValidSpot(); - - this._codepoint = this._nextCodepoint(); - this.sy = this.newSy(allowFloats); - } - - /** - * Saves the current position, line, and column. - * All parsed data until this point is assumed to be valid. - */ - private _saveValidSpot(): void { - this.lastValidSpot = [this._position, this._line, this._col]; - } - - /** - * Reads, saves, and returns the next character of the source stream. - */ - private _nextCodepoint(): number { - if (this._position < this._codepoints.length) { - this._codepoint = this._codepoints[this._position++]; - // line/col countingF - if (this._codepoint === 0x0a /* \n */) { - this._line++; - this._col = 0; - } else { - this._col++; - } - } else { - this._codepoint = AlphaTexLexerOld._eof; - } - return this._codepoint; - } - - /** - * Reads, saves, and returns the next terminal symbol. - */ - public newSy(allowFloats: boolean = false): AlphaTexSymbols { - // When a new symbol is read, the previous one is assumed to be valid. - // The valid spot is also moved forward when reading past whitespace or comments. - this._saveValidSpot(); - this.sy = AlphaTexSymbols.No; - while (this.sy === AlphaTexSymbols.No) { - this.syData = null; - - if (this._codepoint === AlphaTexLexerOld._eof) { - this.sy = AlphaTexSymbols.Eof; - } else if (AlphaTexLexerOld._isWhiteSpace(this._codepoint)) { - // skip whitespaces - this._codepoint = this._nextCodepoint(); - this._saveValidSpot(); - } else if (this._codepoint === 0x2f /* / */) { - this._codepoint = this._nextCodepoint(); - if (this._codepoint === 0x2f /* / */) { - // single line comment - while ( - this._codepoint !== 0x0d /* \r */ && - this._codepoint !== 0x0a /* \n */ && - this._codepoint !== AlphaTexLexerOld._eof - ) { - this._codepoint = this._nextCodepoint(); - } - } else if (this._codepoint === 0x2a /* * */) { - // multiline comment - while (this._codepoint !== AlphaTexLexerOld._eof) { - if (this._codepoint === 0x2a /* * */) { - this._codepoint = this._nextCodepoint(); - if (this._codepoint === 0x2f /* / */) { - this._codepoint = this._nextCodepoint(); - break; - } - } else { - this._codepoint = this._nextCodepoint(); - } - } - } else { - this._errorMessage(`Unexpected character ${String.fromCodePoint(this._codepoint)}`); - } - this._saveValidSpot(); - } else if (this._codepoint === 0x22 /* " */ || this._codepoint === 0x27 /* ' */) { - const startChar: number = this._codepoint; - this._codepoint = this._nextCodepoint(); - let s: string = ''; - this.sy = AlphaTexSymbols.String; - - let previousCodepoint: number = -1; - - while (this._codepoint !== startChar && this._codepoint !== AlphaTexLexerOld._eof) { - // escape sequences - let codepoint = -1; - - if (this._codepoint === 0x5c /* \ */) { - this._codepoint = this._nextCodepoint(); - if (this._codepoint === 0x5c /* \\ */) { - codepoint = 0x5c; - } else if (this._codepoint === startChar /* \ */) { - codepoint = startChar; - } else if (this._codepoint === 0x52 /* \R */ || this._codepoint === 0x72 /* \r */) { - codepoint = 0x0d; - } else if (this._codepoint === 0x4e /* \N */ || this._codepoint === 0x6e /* \n */) { - codepoint = 0x0a; - } else if (this._codepoint === 0x54 /* \T */ || this._codepoint === 0x74 /* \t */) { - codepoint = 0x09; - } else if (this._codepoint === 0x75 /* \u */) { - // \uXXXX - let hex = ''; - - for (let i = 0; i < 4; i++) { - this._codepoint = this._nextCodepoint(); - if (this._codepoint === AlphaTexLexerOld._eof) { - this._errorMessage('Unexpected end of escape sequence'); - } - hex += String.fromCodePoint(this._codepoint); - } - - codepoint = Number.parseInt(hex, 16); - if (Number.isNaN(codepoint)) { - this._errorMessage(`Invalid unicode value ${hex}`); - } - } else { - this._errorMessage('Unsupported escape sequence'); - } - } else { - codepoint = this._codepoint; - } - - // unicode handling - - // https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-string-type - if (IOHelper.isLeadingSurrogate(previousCodepoint) && IOHelper.isTrailingSurrogate(codepoint)) { - codepoint = (previousCodepoint - 0xd800) * 0x400 + (codepoint - 0xdc00) + 0x10000; - s += String.fromCodePoint(codepoint); - } else if (IOHelper.isLeadingSurrogate(codepoint)) { - // only remember for next character to form a surrogate pair - } else { - // standalone leading surrogate from previous char - if (IOHelper.isLeadingSurrogate(previousCodepoint)) { - s += String.fromCodePoint(previousCodepoint); - } - - if (codepoint > 0) { - s += String.fromCodePoint(codepoint); - } - } - - previousCodepoint = codepoint; - this._codepoint = this._nextCodepoint(); - } - if (this._codepoint === AlphaTexLexerOld._eof) { - this._errorMessage('String opened but never closed'); - } - this.syData = s; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x2d /* - */) { - this._readNumberOrName(allowFloats); - } else if (this._codepoint === 0x2e /* . */) { - this.sy = AlphaTexSymbols.Dot; - this.syData = '.'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x3a /* : */) { - this.sy = AlphaTexSymbols.DoubleDot; - this.syData = ':'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x28 /* ( */) { - this.sy = AlphaTexSymbols.LParensis; - this._codepoint = this._nextCodepoint(); - this.syData = '('; - } else if (this._codepoint === 0x5c /* \ */) { - this._codepoint = this._nextCodepoint(); - this.sy = AlphaTexSymbols.MetaCommand; - // allow double backslash (easier to test when copying from escaped Strings) - if (this._codepoint === 0x5c /* \ */) { - this._codepoint = this._nextCodepoint(); - } - - this.syData = this._readName(); - } else if (this._codepoint === 0x29 /* ) */) { - this.sy = AlphaTexSymbols.RParensis; - this.syData = ')'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x7b /* { */) { - this.sy = AlphaTexSymbols.LBrace; - this.syData = '{'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x7d /* } */) { - this.sy = AlphaTexSymbols.RBrace; - this.syData = '}'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x7c /* | */) { - this.sy = AlphaTexSymbols.Pipe; - this.syData = '|'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x2a /* * */) { - this.sy = AlphaTexSymbols.Multiply; - this.syData = '*'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x3c /* < */) { - this.sy = AlphaTexSymbols.LowerThan; - this.syData = '<'; - this._codepoint = this._nextCodepoint(); - } else if (AlphaTexLexerOld._isDigit(this._codepoint)) { - this._readNumberOrName(allowFloats); - } else if (AlphaTexLexerOld._isNameLetter(this._codepoint)) { - const name: string = this._readName(); - const tuning: TuningParseResult | null = this.allowTuning ? ModelUtils.parseTuning(name) : null; - if (tuning) { - this.sy = AlphaTexSymbols.Tuning; - this.syData = tuning; - } else { - this.sy = AlphaTexSymbols.String; - this.syData = name; - } - } else { - this._errorMessage(`Unexpected character ${String.fromCodePoint(this._codepoint)}`); - } - } - return this.sy; - } - - private _errorMessage(message: string): void { - const e: AlphaTexError = AlphaTexError.errorMessage( - message, - this.lastValidSpot[0], - this.lastValidSpot[1], - this.lastValidSpot[2] - ); - if (this.logErrors) { - Logger.error('AlphaTex', e.message!); - } - throw e; - } - - private _readNumberOrName(allowFloat: boolean) { - let str: string = ''; - - // assume number at start - this.sy = AlphaTexSymbols.Number; - - // negative start or dash - if (this._codepoint === 0x2d) { - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - - // need a number afterwards otherwise we have a string(-) - if (!AlphaTexLexerOld._isDigit(this._codepoint)) { - this.sy = AlphaTexSymbols.String; - } - } - - let keepReading = true; - - let hasDot = false; - do { - switch (this.sy) { - case AlphaTexSymbols.Number: - // adding digits to the number - if (AlphaTexLexerOld._isDigit(this._codepoint)) { - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - keepReading = true; - } - // adding a dot to the number (expecting digit after dot) - else if ( - allowFloat && - !hasDot && - this._codepoint === 0x2e /* . */ && - AlphaTexLexerOld._isDigit(this._codepoints[this._position]) - ) { - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - keepReading = true; - hasDot = true; - } - // letter in number -> fallback to name reading - else if (AlphaTexLexerOld._isNameLetter(this._codepoint)) { - this.sy = AlphaTexSymbols.String; - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - keepReading = true; - } - // general unknown character -> end reading - else { - keepReading = false; - } - break; - case AlphaTexSymbols.String: - if (AlphaTexLexerOld._isNameLetter(this._codepoint)) { - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - keepReading = true; - } else { - keepReading = false; - } - break; - default: - keepReading = false; // should never happen - break; - } - } while (keepReading); - - if (str.length === 0) { - this._errorMessage('number was empty'); - } - - if (this.sy === AlphaTexSymbols.String) { - this.syData = str; - } else { - this.syData = allowFloat ? Number.parseFloat(str) : Number.parseInt(str, 10); - } - return; - } - - /** - * Reads a string from the stream. - * @returns the read string. - */ - private _readName(): string { - let str: string = ''; - do { - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - } while ( - AlphaTexLexerOld._isNameLetter(this._codepoint) || - AlphaTexLexerOld._isDigit(this._codepoint) || - this._codepoint === 0x2d /*-*/ - ); - return str; - } - - /** - * Checks if the given character is a valid letter for a name. - * (no control characters, whitespaces, numbers or dots) - */ - private static _isNameLetter(ch: number): boolean { - return ( - !AlphaTexLexerOld._isTerminal(ch) && // no control characters, whitespaces, numbers or dots - ((0x21 <= ch && ch <= 0x2f) || (0x3a <= ch && ch <= 0x7e) || 0x80 <= ch) // Unicode Symbols - ); - } - - private static _isTerminal(ch: number): boolean { - return ( - ch === 0x2e /* . */ || - ch === 0x7b /* { */ || - ch === 0x7d /* } */ || - ch === 0x5b /* [ */ || - ch === 0x5d /* ] */ || - ch === 0x28 /* ( */ || - ch === 0x29 /* ) */ || - ch === 0x7c /* | */ || - ch === 0x27 /* ' */ || - ch === 0x22 /* " */ || - ch === 0x2a /* * */ || - ch === 0x5c /* \ */ - ); - } - - private static _isWhiteSpace(ch: number): boolean { - return ( - ch === 0x09 /* \t */ || - ch === 0x0a /* \n */ || - ch === 0x0b /* \v */ || - ch === 0x0d /* \r */ || - ch === 0x20 /* space */ - ); - } - - private static _isDigit(ch: number): boolean { - return ch >= 0x30 && ch <= 0x39 /* 0-9 */; - } -} - -/** - * This importer can parse alphaTex markup into a score structure. - * @internal - */ -export class AlphaTexImporterOld extends ScoreImporter { - private _trackChannel: number = 0; - private _score!: Score; - private _currentTrack!: Track; - - private _currentStaff!: Staff; - private _barIndex: number = 0; - private _voiceIndex: number = 0; - private _initialTempo = Automation.buildTempoAutomation(false, 0, 120, 0); - - // Last known position that had valid syntax/symbols - private _currentDuration: Duration = Duration.QuadrupleWhole; - private _currentDynamics: DynamicValue = DynamicValue.PPP; - private _currentTuplet: number = 0; - private _lyrics!: Map; - private _ignoredInitialVoice = false; - - private _staffHasExplicitDisplayTransposition: boolean = false; - private _staffDisplayTranspositionApplied: boolean = false; - private _staffHasExplicitTuning: boolean = false; - private _staffTuningApplied: boolean = false; - private _percussionArticulationNames = new Map(); - private _sustainPedalToBeat = new Map(); - - private _slurs: Map = new Map(); - - private _articulationValueToIndex = new Map(); - - private _lexer!: AlphaTexLexerOld; - - private _accidentalMode: AlphaTexAccidentalMode = AlphaTexAccidentalMode.Explicit; - private _flatSyncPoints: FlatSyncPoint[] = []; - - public logErrors: boolean = true; - - public get name(): string { - return 'AlphaTex'; - } - - public initFromString(tex: string, settings: Settings) { - this.data = ByteBuffer.empty(); - this._lexer = new AlphaTexLexerOld(tex); - this.settings = settings; - // when beginning reading a new score we reset the IDs. - Score.resetIds(); - } - - private get _sy() { - return this._lexer.sy; - } - - private get _syData() { - return this._lexer.syData; - } - - private set _sy(value: AlphaTexSymbols) { - this._lexer.sy = value; - } - - private _newSy(allowFloat: boolean = false) { - return this._lexer.newSy(allowFloat); - } - - public readScore(): Score { - try { - if (this.data.length > 0) { - this._lexer = new AlphaTexLexerOld( - IOHelper.toString(this.data.readAll(), this.settings.importer.encoding) - ); - } - this._lexer.logErrors = this.logErrors; - - this._lexer.allowTuning = true; - this._lyrics = new Map(); - this._sustainPedalToBeat = new Map(); - - this._lexer.init(); - - this._createDefaultScore(); - this._currentDuration = Duration.Quarter; - this._currentDynamics = DynamicValue.F; - this._currentTuplet = 1; - if (this._sy === AlphaTexSymbols.LowerThan) { - // potential XML, stop parsing (alphaTex never starts with <) - throw new UnsupportedFormatError("Unknown start sign '<' (meant to import as XML?)"); - } - - if (this._sy !== AlphaTexSymbols.Eof) { - const anyMetaRead = this._metaData(); - const anyBarsRead = this._bars(); - if (!anyMetaRead && !anyBarsRead) { - throw new UnsupportedFormatError('No alphaTex data found'); - } - - if (this._sy === AlphaTexSymbols.Dot) { - this._sy = this._newSy(); - this._syncPoints(); - } - } - - ModelUtils.consolidate(this._score); - this._score.finish(this.settings); - ModelUtils.trimEmptyBarsAtEnd(this._score); - this._score.rebuildRepeatGroups(); - this._score.applyFlatSyncPoints(this._flatSyncPoints); - for (const [track, lyrics] of this._lyrics) { - this._score.tracks[track].applyLyrics(lyrics); - } - for (const [sustainPedal, beat] of this._sustainPedalToBeat) { - const duration = beat.voice.bar.masterBar.calculateDuration(); - sustainPedal.ratioPosition = beat.playbackStart / duration; - } - return this._score; - } catch (e) { - if (e instanceof AlphaTexError) { - throw new UnsupportedFormatError(e.message, e); - } - throw e; - } - } - - private _syncPoints() { - while (this._sy !== AlphaTexSymbols.Eof) { - this._syncPoint(); - } - } - - private _syncPoint() { - // \sync BarIndex Occurence MillisecondOffset - // \sync BarIndex Occurence MillisecondOffset RatioPosition - - if (this._sy !== AlphaTexSymbols.MetaCommand || (this._syData as string) !== 'sync') { - this._error('syncPoint', AlphaTexSymbols.MetaCommand, true); - } - - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('syncPointBarIndex', AlphaTexSymbols.Number, true); - } - const barIndex = this._syData as number; - - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('syncPointBarOccurence', AlphaTexSymbols.Number, true); - } - const barOccurence = this._syData as number; - - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('syncPointBarMillis', AlphaTexSymbols.Number, true); - } - const millisecondOffset = this._syData as number; - - this._sy = this._newSy(true); - let barPosition = 0; - if (this._sy === AlphaTexSymbols.Number) { - barPosition = this._syData as number; - this._sy = this._newSy(); - } - - this._flatSyncPoints.push({ - barIndex, - barOccurence, - barPosition, - millisecondOffset - }); - } - - private _error(nonterm: string, expected: AlphaTexSymbols, wrongSymbol: boolean = true): void { - let receivedSymbol: AlphaTexSymbols; - let showSyData = false; - if (wrongSymbol) { - receivedSymbol = this._sy; - if ( - // These are the only symbols that can have associated _syData set - receivedSymbol === AlphaTexSymbols.String || - receivedSymbol === AlphaTexSymbols.Number || - receivedSymbol === AlphaTexSymbols.MetaCommand // || - // Tuning does not have a toString() yet, therefore excluded. - // receivedSymbol === AlphaTexSymbols.Tuning - ) { - showSyData = true; - } - } else { - receivedSymbol = expected; - showSyData = true; - } - const e = AlphaTexError.symbolError( - this._lexer.lastValidSpot[0], - this._lexer.lastValidSpot[1], - this._lexer.lastValidSpot[2], - nonterm, - expected, - receivedSymbol, - showSyData ? this._syData : null - ); - if (this.logErrors) { - Logger.error(this.name, e.message!); - } - throw e; - } - - private _errorMessage(message: string): void { - const e: AlphaTexError = AlphaTexError.errorMessage( - message, - this._lexer.lastValidSpot[0], - this._lexer.lastValidSpot[1], - this._lexer.lastValidSpot[2] - ); - if (this.logErrors) { - Logger.error(this.name, e.message!); - } - throw e; - } - - /** - * Initializes the song with some required default values. - * @returns - */ - private _createDefaultScore(): void { - this._score = new Score(); - this._newTrack(); - } - - private _newTrack(): void { - this._currentTrack = new Track(); - this._currentTrack.ensureStaveCount(1); - this._currentTrack.playbackInfo.program = 25; - this._currentTrack.playbackInfo.primaryChannel = this._trackChannel++; - this._currentTrack.playbackInfo.secondaryChannel = this._trackChannel++; - const staff = this._currentTrack.staves[0]; - staff.displayTranspositionPitch = 0; - staff.stringTuning = Tuning.getDefaultTuningFor(6)!; - this._articulationValueToIndex.clear(); - - this._beginStaff(staff); - - this._score.addTrack(this._currentTrack); - this._lyrics.set(this._currentTrack.index, []); - this._currentDynamics = DynamicValue.F; - } - - /** - * Converts a clef string into the clef value. - * @param str the string to convert - * @returns the clef value - */ - private _parseClefFromString(str: string): Clef { - switch (str.toLowerCase()) { - case 'g2': - case 'treble': - return Clef.G2; - case 'f4': - case 'bass': - return Clef.F4; - case 'c3': - case 'alto': - return Clef.C3; - case 'c4': - case 'tenor': - return Clef.C4; - case 'n': - case 'neutral': - return Clef.Neutral; - default: - return Clef.G2; - // error("clef-value", AlphaTexSymbols.String, false); - } - } - - /** - * Converts a clef tuning into the clef value. - * @param i the tuning value to convert - * @returns the clef value - */ - private _parseClefFromInt(i: number): Clef { - switch (i) { - case 0: - return Clef.Neutral; - case 43: - return Clef.G2; - case 65: - return Clef.F4; - case 48: - return Clef.C3; - case 60: - return Clef.C4; - default: - return Clef.G2; - } - } - - private _parseTripletFeelFromString(str: string): TripletFeel { - switch (str.toLowerCase()) { - case 'no': - case 'none': - case 'notripletfeel': - return TripletFeel.NoTripletFeel; - case 't16': - case 'triplet-16th': - case 'triplet16th': - return TripletFeel.Triplet16th; - case 't8': - case 'triplet-8th': - case 'triplet8th': - return TripletFeel.Triplet8th; - case 'd16': - case 'dotted-16th': - case 'dotted16th': - return TripletFeel.Dotted16th; - case 'd8': - case 'dotted-8th': - case 'dotted8th': - return TripletFeel.Dotted8th; - case 's16': - case 'scottish-16th': - case 'scottish16th': - return TripletFeel.Scottish16th; - case 's8': - case 'scottish-8th': - case 'scottish8th': - return TripletFeel.Scottish8th; - default: - return TripletFeel.NoTripletFeel; - } - } - - private _parseTripletFeelFromInt(i: number): TripletFeel { - switch (i) { - case 0: - return TripletFeel.NoTripletFeel; - case 1: - return TripletFeel.Triplet16th; - case 2: - return TripletFeel.Triplet8th; - case 3: - return TripletFeel.Dotted16th; - case 4: - return TripletFeel.Dotted8th; - case 5: - return TripletFeel.Scottish16th; - case 6: - return TripletFeel.Scottish8th; - default: - return TripletFeel.NoTripletFeel; - } - } - - /** - * Converts a keysignature string into the assocciated value. - * @param str the string to convert - * @returns the assocciated keysignature value - */ - private _parseKeySignature(str: string): KeySignature { - switch (str.toLowerCase()) { - case 'cb': - case 'cbmajor': - case 'abminor': - return KeySignature.Cb; - case 'gb': - case 'gbmajor': - case 'ebminor': - return KeySignature.Gb; - case 'db': - case 'dbmajor': - case 'bbminor': - return KeySignature.Db; - case 'ab': - case 'abmajor': - case 'fminor': - return KeySignature.Ab; - case 'eb': - case 'ebmajor': - case 'cminor': - return KeySignature.Eb; - case 'bb': - case 'bbmajor': - case 'gminor': - return KeySignature.Bb; - case 'f': - case 'fmajor': - case 'dminor': - return KeySignature.F; - case 'c': - case 'cmajor': - case 'aminor': - return KeySignature.C; - case 'g': - case 'gmajor': - case 'eminor': - return KeySignature.G; - case 'd': - case 'dmajor': - case 'bminor': - return KeySignature.D; - case 'a': - case 'amajor': - case 'f#minor': - return KeySignature.A; - case 'e': - case 'emajor': - case 'c#minor': - return KeySignature.E; - case 'b': - case 'bmajor': - case 'g#minor': - return KeySignature.B; - case 'f#': - case 'f#major': - case 'd#minor': - return KeySignature.FSharp; - case 'c#': - case 'c#major': - case 'a#minor': - return KeySignature.CSharp; - default: - return KeySignature.C; - // error("keysignature-value", AlphaTexSymbols.String, false); return 0 - } - } - - private _parseKeySignatureType(str: string): KeySignatureType { - if (str.toLowerCase().endsWith('minor')) { - return KeySignatureType.Minor; - } - return KeySignatureType.Major; - } - - private _metaData(): boolean { - let anyTopLevelMeta = false; - let anyOtherMeta = false; - let continueReading: boolean = true; - while (this._sy === AlphaTexSymbols.MetaCommand && continueReading) { - const metadataTag: string = (this._syData as string).toLowerCase(); - switch (metadataTag) { - case 'title': - case 'subtitle': - case 'artist': - case 'album': - case 'words': - case 'music': - case 'copyright': - case 'instructions': - case 'notices': - case 'tab': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - // Known issue: Strings that happen to be parsed as valid Tunings or positive Numbers will not pass this. - // Need to use quotes in that case, or rewrite parsing logic. - this._error(metadataTag, AlphaTexSymbols.String, true); - } - - const metadataValue: string = this._syData as string; - this._sy = this._newSy(); - anyTopLevelMeta = true; - - let element: ScoreSubElement = ScoreSubElement.ChordDiagramList; - switch (metadataTag) { - case 'title': - this._score.title = metadataValue; - element = ScoreSubElement.Title; - break; - case 'subtitle': - this._score.subTitle = metadataValue; - element = ScoreSubElement.SubTitle; - break; - case 'artist': - this._score.artist = metadataValue; - element = ScoreSubElement.Artist; - break; - case 'album': - this._score.album = metadataValue; - element = ScoreSubElement.Album; - break; - case 'words': - this._score.words = metadataValue; - element = ScoreSubElement.Words; - break; - case 'music': - this._score.music = metadataValue; - element = ScoreSubElement.Music; - break; - case 'copyright': - this._score.copyright = metadataValue; - element = ScoreSubElement.Copyright; - break; - case 'instructions': - this._score.instructions = metadataValue; - break; - case 'notices': - this._score.notices = metadataValue; - break; - case 'tab': - this._score.tab = metadataValue; - element = ScoreSubElement.Transcriber; - break; - } - - if (element !== ScoreSubElement.ChordDiagramList) { - this.headerFooterStyle(element); - } - - break; - case 'copyright2': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error(metadataTag, AlphaTexSymbols.String, true); - } - - this.headerFooterStyle(ScoreSubElement.CopyrightSecondLine); - anyTopLevelMeta = true; - break; - case 'wordsandmusic': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error(metadataTag, AlphaTexSymbols.String, true); - } - - this.headerFooterStyle(ScoreSubElement.WordsAndMusic); - anyTopLevelMeta = true; - break; - case 'tempo': - this._sy = this._newSy(true); - if (this._sy === AlphaTexSymbols.Number) { - this._initialTempo.value = this._syData as number; - } else { - this._error('tempo', AlphaTexSymbols.Number, true); - } - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.String) { - this._initialTempo.text = this._syData as string; - this._sy = this._newSy(); - } - anyTopLevelMeta = true; - break; - case 'defaultsystemslayout': - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - this._score.defaultSystemsLayout = this._syData as number; - this._sy = this._newSy(); - anyTopLevelMeta = true; - } else { - this._error('default-systems-layout', AlphaTexSymbols.Number, true); - } - break; - case 'systemslayout': - this._sy = this._newSy(); - anyTopLevelMeta = true; - while (this._sy === AlphaTexSymbols.Number) { - this._score.systemsLayout.push(this._syData as number); - this._sy = this._newSy(); - } - break; - case 'hidedynamics': - this._score.stylesheet.hideDynamics = true; - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'showdynamics': - this._score.stylesheet.hideDynamics = false; - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'bracketextendmode': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('bracketExtendMode', AlphaTexSymbols.String, true); - } - this._score.stylesheet.bracketExtendMode = this._parseBracketExtendMode(this._syData as string); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'usesystemsignseparator': - this._score.stylesheet.useSystemSignSeparator = true; - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'multibarrest': - this._score.stylesheet.multiTrackMultiBarRest = true; - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'singletracktracknamepolicy': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('singleTrackTrackNamePolicy', AlphaTexSymbols.String, true); - } - this._score.stylesheet.singleTrackTrackNamePolicy = this._parseTrackNamePolicy( - this._syData as string - ); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'multitracktracknamepolicy': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('multiTrackTrackNamePolicy', AlphaTexSymbols.String, true); - } - this._score.stylesheet.multiTrackTrackNamePolicy = this._parseTrackNamePolicy( - this._syData as string - ); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'firstsystemtracknamemode': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('firstSystemTrackNameMode', AlphaTexSymbols.String, true); - } - this._score.stylesheet.firstSystemTrackNameMode = this._parseTrackNameMode(this._syData as string); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'othersystemstracknamemode': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('otherSystemsTrackNameMode', AlphaTexSymbols.String, true); - } - this._score.stylesheet.otherSystemsTrackNameMode = this._parseTrackNameMode(this._syData as string); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'firstsystemtracknameorientation': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('firstSystemTrackNameOrientation', AlphaTexSymbols.String, true); - } - this._score.stylesheet.firstSystemTrackNameOrientation = this._parseTrackNameOrientation( - this._syData as string - ); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'othersystemstracknameorientation': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('otherSystemsTrackNameOrientation', AlphaTexSymbols.String, true); - } - this._score.stylesheet.otherSystemsTrackNameOrientation = this._parseTrackNameOrientation( - this._syData as string - ); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - default: - switch (this._handleStaffMeta()) { - case StaffMetaResult.KnownStaffMeta: - anyOtherMeta = true; - break; - case StaffMetaResult.UnknownStaffMeta: - if (anyTopLevelMeta || anyOtherMeta) { - // invalid meta encountered - this._error('metaDataTags', AlphaTexSymbols.String, false); - } else { - // fall forward to bar meta if unknown score meta was found - continueReading = false; - } - break; - case StaffMetaResult.EndOfMetaDetected: - continueReading = false; - break; - } - break; - } - } - if (anyTopLevelMeta) { - if (this._sy !== AlphaTexSymbols.Dot) { - this._error('song', AlphaTexSymbols.Dot, true); - } - this._sy = this._newSy(); - } else if (this._sy === AlphaTexSymbols.Dot) { - this._sy = this._newSy(); - anyTopLevelMeta = true; // just to indicate that there is an indication of proper alphaTex - } - - return anyTopLevelMeta || anyOtherMeta; - } - headerFooterStyle(element: ScoreSubElement) { - const style = ModelUtils.getOrCreateHeaderFooterStyle(this._score, element); - if (style.isVisible === undefined) { - style.isVisible = true; - } - - if (this._sy === AlphaTexSymbols.String) { - const value = this._syData as string; - if (value) { - style.template = value; - } else { - style.isVisible = false; - } - this._sy = this._newSy(); - } - - if (this._sy === AlphaTexSymbols.String) { - switch ((this._syData as string).toLowerCase()) { - case 'left': - style.textAlign = TextAlign.Left; - break; - case 'center': - style.textAlign = TextAlign.Center; - break; - case 'right': - style.textAlign = TextAlign.Right; - break; - } - this._sy = this._newSy(); - } - } - - private _parseTrackNamePolicy(v: string): TrackNamePolicy { - switch (v.toLowerCase()) { - case 'hidden': - return TrackNamePolicy.Hidden; - case 'allsystems': - return TrackNamePolicy.AllSystems; - // case 'firstsystem': - default: - return TrackNamePolicy.FirstSystem; - } - } - - private _parseTrackNameMode(v: string): TrackNameMode { - switch (v.toLowerCase()) { - case 'fullname': - return TrackNameMode.FullName; - // case 'shortname': - default: - return TrackNameMode.ShortName; - } - } - - private _parseTrackNameOrientation(v: string): TrackNameOrientation { - switch (v.toLowerCase()) { - case 'horizontal': - return TrackNameOrientation.Horizontal; - //case 'vertical': - default: - return TrackNameOrientation.Vertical; - } - } - - private _handleStaffMeta(): StaffMetaResult { - switch ((this._syData as string).toLowerCase()) { - case 'capo': - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - this._currentStaff.capo = this._syData as number; - } else { - this._error('capo', AlphaTexSymbols.Number, true); - } - this._sy = this._newSy(); - return StaffMetaResult.KnownStaffMeta; - case 'tuning': - this._sy = this._newSy(); - const strings: number = this._currentStaff.tuning.length; - this._staffHasExplicitTuning = true; - this._staffTuningApplied = false; - this._currentStaff.stringTuning.reset(); - switch (this._sy) { - case AlphaTexSymbols.String: - const text: string = (this._syData as string).toLowerCase(); - if (text === 'piano' || text === 'none' || text === 'voice') { - this._makeCurrentStaffPitched(); - } else { - this._error('tuning', AlphaTexSymbols.Tuning, true); - } - this._sy = this._newSy(); - break; - case AlphaTexSymbols.Tuning: - const tuning: number[] = []; - do { - const t: TuningParseResult = this._syData as TuningParseResult; - tuning.push(t.realValue); - this._sy = this._newSy(); - } while (this._sy === AlphaTexSymbols.Tuning); - this._currentStaff.stringTuning.tunings = tuning; - break; - default: - this._error('tuning', AlphaTexSymbols.Tuning, true); - break; - } - - if (this._sy === AlphaTexSymbols.String) { - if ((this._syData as string).toLowerCase() === 'hide') { - if (!this._score.stylesheet.perTrackDisplayTuning) { - this._score.stylesheet.perTrackDisplayTuning = new Map(); - } - this._score.stylesheet.perTrackDisplayTuning!.set(this._currentTrack.index, false); - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.String) { - this._currentStaff.stringTuning.name = this._syData as string; - this._sy = this._newSy(); - } - } else { - this._currentStaff.stringTuning.name = this._syData as string; - this._sy = this._newSy(); - } - } - - if (strings !== this._currentStaff.tuning.length && (this._currentStaff.chords?.size ?? 0) > 0) { - this._errorMessage('Tuning must be defined before any chord'); - } - return StaffMetaResult.KnownStaffMeta; - case 'instrument': - this._staffTuningApplied = false; - this._readTrackInstrument(); - - return StaffMetaResult.KnownStaffMeta; - case 'bank': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bank', AlphaTexSymbols.Number, true); - } - - this._currentTrack.playbackInfo.bank = this._syData as number; - this._sy = this._newSy(); - return StaffMetaResult.KnownStaffMeta; - case 'lyrics': - this._sy = this._newSy(); - const lyrics: Lyrics = new Lyrics(); - lyrics.startBar = 0; - lyrics.text = ''; - if (this._sy === AlphaTexSymbols.Number) { - lyrics.startBar = this._syData as number; - this._sy = this._newSy(); - } - if (this._sy === AlphaTexSymbols.String) { - lyrics.text = this._syData as string; - this._sy = this._newSy(); - } else { - this._error('lyrics', AlphaTexSymbols.String, true); - } - this._lyrics.get(this._currentTrack.index)!.push(lyrics); - return StaffMetaResult.KnownStaffMeta; - case 'chord': - this._sy = this._newSy(); - const chord: Chord = new Chord(); - this._chordProperties(chord); - if (this._sy === AlphaTexSymbols.String) { - chord.name = this._syData as string; - this._sy = this._newSy(); - } else { - this._error('chord-name', AlphaTexSymbols.String, true); - } - for (let i: number = 0; i < this._currentStaff.tuning.length; i++) { - if (this._sy === AlphaTexSymbols.Number) { - chord.strings.push(this._syData as number); - } else if (this._sy === AlphaTexSymbols.String && (this._syData as string).toLowerCase() === 'x') { - chord.strings.push(-1); - } - this._sy = this._newSy(); - } - this._currentStaff.addChord(this._getChordId(this._currentStaff, chord.name), chord); - return StaffMetaResult.KnownStaffMeta; - case 'articulation': - this._sy = this._newSy(); - - let name = ''; - if (this._sy === AlphaTexSymbols.String) { - name = this._syData as string; - this._sy = this._newSy(); - } else { - this._error('articulation-name', AlphaTexSymbols.String, true); - } - - if (name === 'defaults') { - for (const [defaultName, defaultValue] of PercussionMapper.instrumentArticulationNames) { - this._percussionArticulationNames.set(defaultName.toLowerCase(), defaultValue); - this._percussionArticulationNames.set( - AlphaTexImporterOld._toArticulationId(defaultName), - defaultValue - ); - } - return StaffMetaResult.KnownStaffMeta; - } - - let number = 0; - if (this._sy === AlphaTexSymbols.Number) { - number = this._syData as number; - this._sy = this._newSy(); - } else { - this._error('articulation-number', AlphaTexSymbols.Number, true); - } - - if (!PercussionMapper.instrumentArticulations.has(number)) { - this._errorMessage( - `Unknown articulation ${number}. Refer to https://www.alphatab.net/docs/alphatex/percussion for available ids` - ); - } - - this._percussionArticulationNames.set(name.toLowerCase(), number); - return StaffMetaResult.KnownStaffMeta; - case 'accidentals': - this._handleAccidentalMode(); - return StaffMetaResult.KnownStaffMeta; - case 'displaytranspose': - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - this._currentStaff.displayTranspositionPitch = (this._syData as number) * -1; - this._staffHasExplicitDisplayTransposition = true; - } else { - this._error('displaytranspose', AlphaTexSymbols.Number, true); - } - this._sy = this._newSy(); - return StaffMetaResult.KnownStaffMeta; - case 'transpose': - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - this._currentStaff.transpositionPitch = (this._syData as number) * -1; - } else { - this._error('transpose', AlphaTexSymbols.Number, true); - } - this._sy = this._newSy(); - return StaffMetaResult.KnownStaffMeta; - case 'track': - case 'staff': - // on empty staves we need to proceeed when starting directly a new track or staff - return StaffMetaResult.EndOfMetaDetected; - case 'voice': - this._sy = this._newSy(); - if (this._handleNewVoice()) { - return StaffMetaResult.EndOfMetaDetected; - } - return StaffMetaResult.KnownStaffMeta; - default: - return StaffMetaResult.UnknownStaffMeta; - } - } - private _readTrackInstrument() { - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.Number) { - const instrument: number = this._syData as number; - if (instrument >= 0 && instrument <= 127) { - this._currentTrack.playbackInfo.program = this._syData as number; - } else { - this._error('instrument', AlphaTexSymbols.Number, false); - } - } else if (this._sy === AlphaTexSymbols.String) { - const instrumentName: string = (this._syData as string).toLowerCase(); - if (instrumentName === 'percussion') { - for (const staff of this._currentTrack.staves) { - this._applyPercussionStaff(staff); - } - this._currentTrack.playbackInfo.program = 0; - this._currentTrack.playbackInfo.primaryChannel = SynthConstants.PercussionChannel; - this._currentTrack.playbackInfo.secondaryChannel = SynthConstants.PercussionChannel; - } else { - this._currentTrack.playbackInfo.program = GeneralMidi.getValue(instrumentName); - } - } else { - this._error('instrument', AlphaTexSymbols.Number, true); - } - this._sy = this._newSy(); - } - - private _handleAccidentalMode() { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('accidental-mode', AlphaTexSymbols.String, true); - } - - switch (this._syData as string) { - case 'auto': - this._accidentalMode = AlphaTexAccidentalMode.Auto; - break; - case 'explicit': - this._accidentalMode = AlphaTexAccidentalMode.Explicit; - break; - } - - this._sy = this._newSy(); - } - - private _makeCurrentStaffPitched() { - // clear tuning - this._currentStaff.stringTuning.reset(); - if (!this._staffHasExplicitDisplayTransposition) { - this._currentStaff.displayTranspositionPitch = 0; - } - } - - /** - * Encodes a given string to a shorthand text form without spaces or special characters - */ - private static _toArticulationId(plain: string): string { - return plain.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); - } - - private _applyPercussionStaff(staff: Staff) { - staff.isPercussion = true; - staff.showTablature = false; - staff.track.playbackInfo.program = 0; - } - - private _chordProperties(chord: Chord): void { - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.String) { - switch ((this._syData as string).toLowerCase()) { - case 'firstfret': - this._sy = this._newSy(); - switch (this._sy) { - case AlphaTexSymbols.Number: - chord.firstFret = this._syData as number; - break; - default: - this._error('chord-firstfret', AlphaTexSymbols.Number, true); - break; - } - this._sy = this._newSy(); - break; - case 'showdiagram': - this._sy = this._newSy(); - switch (this._sy) { - case AlphaTexSymbols.String: - chord.showDiagram = (this._syData as string).toLowerCase() !== 'false'; - break; - case AlphaTexSymbols.Number: - chord.showDiagram = (this._syData as number) !== 0; - break; - default: - this._error('chord-showdiagram', AlphaTexSymbols.String, true); - break; - } - this._sy = this._newSy(); - break; - case 'showfingering': - this._sy = this._newSy(); - switch (this._sy) { - case AlphaTexSymbols.String: - chord.showDiagram = (this._syData as string).toLowerCase() !== 'false'; - break; - case AlphaTexSymbols.Number: - chord.showFingering = (this._syData as number) !== 0; - break; - default: - this._error('chord-showfingering', AlphaTexSymbols.String, true); - break; - } - this._sy = this._newSy(); - break; - case 'showname': - this._sy = this._newSy(); - switch (this._sy) { - case AlphaTexSymbols.String: - chord.showName = (this._syData as string).toLowerCase() !== 'false'; - break; - case AlphaTexSymbols.Number: - chord.showName = (this._syData as number) !== 0; - break; - default: - this._error('chord-showname', AlphaTexSymbols.String, true); - break; - } - this._sy = this._newSy(); - break; - case 'barre': - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.Number) { - chord.barreFrets.push(this._syData as number); - this._sy = this._newSy(); - } - break; - default: - this._error('chord-properties', AlphaTexSymbols.String, false); - break; - } - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('chord-properties', AlphaTexSymbols.RBrace, true); - } - this._sy = this._newSy(); - } - - private _bars(): boolean { - const anyData = this._bar(); - while (this._sy !== AlphaTexSymbols.Eof) { - // read pipe from last bar - if (this._sy === AlphaTexSymbols.Pipe) { - this._sy = this._newSy(); - this._bar(); - } else if (this._sy === AlphaTexSymbols.MetaCommand) { - this._bar(); - } else { - break; - } - } - return anyData; - } - - private _trackStaffMeta(): boolean { - if (this._sy !== AlphaTexSymbols.MetaCommand) { - return false; - } - if ((this._syData as string).toLowerCase() === 'track') { - this._staffHasExplicitDisplayTransposition = false; - this._staffHasExplicitTuning = false; - this._staffTuningApplied = false; - this._staffDisplayTranspositionApplied = false; - this._ignoredInitialVoice = false; - - this._sy = this._newSy(); - // new track starting? - if no masterbars it's the \track of the initial track. - if (this._score.masterBars.length > 0) { - this._newTrack(); - } - // name - if (this._sy === AlphaTexSymbols.String) { - this._currentTrack.name = this._syData as string; - this._sy = this._newSy(); - } - // short name - if (this._sy === AlphaTexSymbols.String) { - this._currentTrack.shortName = this._syData as string; - this._sy = this._newSy(); - } - - this._trackProperties(); - } - if (this._sy === AlphaTexSymbols.MetaCommand && (this._syData as string).toLowerCase() === 'staff') { - this._staffHasExplicitDisplayTransposition = false; - this._staffHasExplicitTuning = false; - this._staffTuningApplied = false; - this._staffDisplayTranspositionApplied = false; - this._ignoredInitialVoice = false; - - this._sy = this._newSy(); - if (this._currentTrack.staves[0].bars.length > 0) { - const previousWasPercussion = this._currentStaff.isPercussion; - - this._currentTrack.ensureStaveCount(this._currentTrack.staves.length + 1); - this._beginStaff(this._currentTrack.staves[this._currentTrack.staves.length - 1]); - - if (previousWasPercussion) { - this._applyPercussionStaff(this._currentStaff); - } - - this._currentDynamics = DynamicValue.F; - } - this._staffProperties(); - } - - if (this._sy === AlphaTexSymbols.MetaCommand && (this._syData as string).toLowerCase() === 'voice') { - this._sy = this._newSy(); - - this._handleNewVoice(); - } - - return true; - } - - private _handleNewVoice(): boolean { - if ( - this._voiceIndex === 0 && - (this._currentStaff.bars.length === 0 || - (this._currentStaff.bars.length === 1 && - this._currentStaff.bars[0].isEmpty && - !this._ignoredInitialVoice)) - ) { - // voice marker on the begining of the first voice without any bar yet? - // -> ignore - this._ignoredInitialVoice = true; - return false; - } - // create directly a new empty voice for all bars - for (const b of this._currentStaff.bars) { - const v = new Voice(); - b.addVoice(v); - } - // start using the new voice (see newBar for details on matching) - this._voiceIndex++; - this._barIndex = 0; - return true; - } - - private _beginStaff(staff: Staff) { - this._currentStaff = staff; - this._slurs.clear(); - this._barIndex = 0; - this._voiceIndex = 0; - } - - private _trackProperties(): void { - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.String) { - switch ((this._syData as string).toLowerCase()) { - case 'color': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('track-color', AlphaTexSymbols.String, true); - } - this._currentTrack.color = Color.fromJson(this._syData as string)!; - this._sy = this._newSy(); - - break; - case 'defaultsystemslayout': - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - this._currentTrack.defaultSystemsLayout = this._syData as number; - this._sy = this._newSy(); - } else { - this._error('default-systems-layout', AlphaTexSymbols.Number, true); - } - break; - case 'systemslayout': - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.Number) { - this._currentTrack.systemsLayout.push(this._syData as number); - this._sy = this._newSy(); - } - break; - case 'volume': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('track-volume', AlphaTexSymbols.Number, true); - } - this._currentTrack.playbackInfo.volume = ModelUtils.clamp(this._syData as number, 0, 16); - this._sy = this._newSy(); - break; - case 'balance': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('track-balance', AlphaTexSymbols.Number, true); - } - this._currentTrack.playbackInfo.balance = ModelUtils.clamp(this._syData as number, 0, 16); - this._sy = this._newSy(); - break; - case 'mute': - this._sy = this._newSy(); - this._currentTrack.playbackInfo.isMute = true; - break; - case 'solo': - this._sy = this._newSy(); - this._currentTrack.playbackInfo.isSolo = true; - break; - case 'multibarrest': - this._sy = this._newSy(); - if (!this._score.stylesheet.perTrackMultiBarRest) { - this._score.stylesheet.perTrackMultiBarRest = new Set(); - } - this._score.stylesheet.perTrackMultiBarRest!.add(this._currentTrack.index); - break; - case 'instrument': - this._readTrackInstrument(); - break; - case 'bank': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bank', AlphaTexSymbols.Number, true); - } - - this._currentTrack.playbackInfo.bank = this._syData as number; - this._sy = this._newSy(); - break; - default: - this._error('track-properties', AlphaTexSymbols.String, false); - break; - } - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('track-properties', AlphaTexSymbols.RBrace, true); - } - this._sy = this._newSy(); - } - - private _staffProperties(): void { - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - let showStandardNotation: boolean = false; - let showTabs: boolean = false; - let showSlash: boolean = false; - let showNumbered: boolean = false; - while (this._sy === AlphaTexSymbols.String) { - switch ((this._syData as string).toLowerCase()) { - case 'score': - showStandardNotation = true; - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.Number) { - this._currentStaff.standardNotationLineCount = this._syData as number; - this._sy = this._newSy(); - } - - break; - case 'tabs': - showTabs = true; - this._sy = this._newSy(); - break; - case 'slash': - showSlash = true; - this._sy = this._newSy(); - break; - case 'numbered': - showNumbered = true; - this._sy = this._newSy(); - break; - default: - this._error('staff-properties', AlphaTexSymbols.String, false); - break; - } - } - if (showStandardNotation || showTabs || showSlash || showNumbered) { - this._currentStaff.showStandardNotation = showStandardNotation; - this._currentStaff.showTablature = showTabs; - this._currentStaff.showSlash = showSlash; - this._currentStaff.showNumbered = showNumbered; - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('staff-properties', AlphaTexSymbols.RBrace, true); - } - this._sy = this._newSy(); - } - - private _bar(): boolean { - const anyStaffMeta = this._trackStaffMeta(); - - const bar: Bar = this._newBar(this._currentStaff); - if (this._currentStaff.bars.length > this._score.masterBars.length) { - const master: MasterBar = new MasterBar(); - this._score.addMasterBar(master); - if (master.index > 0) { - master.timeSignatureDenominator = master.previousMasterBar!.timeSignatureDenominator; - master.timeSignatureNumerator = master.previousMasterBar!.timeSignatureNumerator; - master.tripletFeel = master.previousMasterBar!.tripletFeel; - } else { - master.tempoAutomations.push(this._initialTempo); - } - } - const anyBarMeta = this._barMeta(bar); - - // detect tuning for staff - const program = this._currentTrack.playbackInfo.program; - if (!this._staffTuningApplied && !this._staffHasExplicitTuning) { - // reset to defaults - this._currentStaff.stringTuning.reset() - - if (program === 15) { - // dulcimer E4 B3 G3 D3 A2 E2 - this._currentStaff.stringTuning.tunings = Tuning.getDefaultTuningFor(6)!.tunings; - } else if (program >= 24 && program <= 31) { - // guitar E4 B3 G3 D3 A2 E2 - this._currentStaff.stringTuning.tunings = Tuning.getDefaultTuningFor(6)!.tunings; - } else if (program >= 32 && program <= 39) { - // bass G2 D2 A1 E1 - this._currentStaff.stringTuning.tunings = [43, 38, 33, 28]; - } else if ( - program === 40 || - program === 44 || - program === 45 || - program === 48 || - program === 49 || - program === 50 || - program === 51 - ) { - // violin E3 A3 D3 G2 - this._currentStaff.stringTuning.tunings = [52, 57, 50, 43]; - } else if (program === 41) { - // viola A3 D3 G2 C2 - this._currentStaff.stringTuning.tunings = [57, 50, 43, 36]; - } else if (program === 42) { - // cello A2 D2 G1 C1 - this._currentStaff.stringTuning.tunings = [45, 38, 31, 24]; - } else if (program === 43) { - // contrabass - // G2 D2 A1 E1 - this._currentStaff.stringTuning.tunings = [43, 38, 33, 28]; - } else if (program === 105) { - // banjo - // D3 B2 G2 D2 G3 - this._currentStaff.stringTuning.tunings = [50, 47, 43, 38, 55]; - } else if (program === 106) { - // shamisen - // A3 E3 A2 - this._currentStaff.stringTuning.tunings = [57, 52, 45]; - } else if (program === 107) { - // koto - // E3 A2 D2 G1 - this._currentStaff.stringTuning.tunings = [52, 45, 38, 31]; - } else if (program === 110) { - // Fiddle - // E4 A3 D3 G2 - this._currentStaff.stringTuning.tunings = [64, 57, 50, 43]; - } - - this._staffTuningApplied = true; - } - - // display transposition - if (!this._staffDisplayTranspositionApplied && !this._staffHasExplicitDisplayTransposition) { - if (ModelUtils.displayTranspositionPitches.has(program)) { - // guitar E4 B3 G3 D3 A2 E2 - this._currentStaff.displayTranspositionPitch = ModelUtils.displayTranspositionPitches.get(program)!; - } else { - this._currentStaff.displayTranspositionPitch = 0; - } - this._staffDisplayTranspositionApplied = true; - } - - let anyBeatData = false; - const voice: Voice = bar.voices[this._voiceIndex]; - - // if we have a setup like \track \staff \track \staff (without any notes/beats defined) - // we are at a track meta at this point and we don't read any beats - const emptyStaffWithNewStart = - this._sy === AlphaTexSymbols.MetaCommand && - ((this._syData as string).toLowerCase() === 'track' || (this._syData as string).toLowerCase() === 'staff'); - - if (!emptyStaffWithNewStart) { - while (this._sy !== AlphaTexSymbols.Pipe && this._sy !== AlphaTexSymbols.Eof) { - if (!this._beat(voice)) { - break; - } - anyBeatData = true; - } - } - - if (voice.beats.length === 0) { - const emptyBeat: Beat = new Beat(); - emptyBeat.isEmpty = true; - voice.addBeat(emptyBeat); - } - - return anyStaffMeta || anyBarMeta || anyBeatData; - } - - private _newBar(staff: Staff): Bar { - // existing bar? -> e.g. in multi-voice setups where we fill empty voices later - if (this._barIndex < staff.bars.length) { - const bar = staff.bars[this._barIndex]; - this._barIndex++; - return bar; - } - - const voiceCount = staff.bars.length === 0 ? 1 : staff.bars[0].voices.length; - - // need new bar - const newBar: Bar = new Bar(); - staff.addBar(newBar); - if (newBar.previousBar) { - newBar.clef = newBar.previousBar.clef; - newBar.clefOttava = newBar.previousBar.clefOttava; - newBar.keySignature = newBar.previousBar!.keySignature; - newBar.keySignatureType = newBar.previousBar!.keySignatureType; - } - this._barIndex++; - - if (newBar.index > 0) { - newBar.clef = newBar.previousBar!.clef; - } - - for (let i = 0; i < voiceCount; i++) { - const voice: Voice = new Voice(); - newBar.addVoice(voice); - } - - return newBar; - } - - private _beat(voice: Voice): boolean { - // duration specifier? - this._beatDuration(); - - const beat: Beat = new Beat(); - voice.addBeat(beat); - - this._lexer.allowTuning = !this._currentStaff.isPercussion; - - // notes - if (this._sy === AlphaTexSymbols.LParensis) { - this._sy = this._newSy(); - this._note(beat); - while (this._sy !== AlphaTexSymbols.RParensis && this._sy !== AlphaTexSymbols.Eof) { - this._lexer.allowTuning = !this._currentStaff.isPercussion; - if (!this._note(beat)) { - break; - } - } - if (this._sy !== AlphaTexSymbols.RParensis) { - this._error('note-list', AlphaTexSymbols.RParensis, true); - } - this._sy = this._newSy(); - } else if (this._sy === AlphaTexSymbols.String && (this._syData as string).toLowerCase() === 'r') { - // rest voice -> no notes - this._sy = this._newSy(); - } else { - if (!this._note(beat)) { - voice.beats.splice(voice.beats.length - 1, 1); - return false; - } - } - // new duration - if (this._sy === AlphaTexSymbols.Dot) { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('duration', AlphaTexSymbols.Number, true); - } - this._currentDuration = this._parseDuration(this._syData as number); - this._sy = this._newSy(); - } - beat.duration = this._currentDuration; - beat.dynamics = this._currentDynamics; - if (this._currentTuplet !== 1 && !beat.hasTuplet) { - AlphaTexImporterOld._applyTuplet(beat, this._currentTuplet); - } - // beat multiplier (repeat beat n times) - let beatRepeat: number = 1; - if (this._sy === AlphaTexSymbols.Multiply) { - this._sy = this._newSy(); - // multiplier count - if (this._sy !== AlphaTexSymbols.Number) { - this._error('multiplier', AlphaTexSymbols.Number, true); - } else { - beatRepeat = this._syData as number; - } - this._sy = this._newSy(); - } - this._beatEffects(beat); - for (let i: number = 0; i < beatRepeat - 1; i++) { - voice.addBeat(BeatCloner.clone(beat)); - } - return true; - } - - private _beatDuration(): void { - if (this._sy !== AlphaTexSymbols.DoubleDot) { - return; - } - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('duration', AlphaTexSymbols.Number, true); - } - this._currentDuration = this._parseDuration(this._syData as number); - this._currentTuplet = 1; - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.String) { - const effect: string = (this._syData as string).toLowerCase(); - switch (effect) { - case 'tu': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('duration-tuplet', AlphaTexSymbols.Number, true); - } - this._currentTuplet = this._syData as number; - this._sy = this._newSy(); - break; - default: - this._error('beat-duration', AlphaTexSymbols.String, false); - break; - } - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('beat-duration', AlphaTexSymbols.RBrace, true); - } - this._sy = this._newSy(); - } - - private _beatEffects(beat: Beat): void { - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.String) { - if (!this._applyBeatEffect(beat)) { - this._error('beat-effects', AlphaTexSymbols.String, false); - } - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('beat-effects', AlphaTexSymbols.RBrace, true); - } - this._sy = this._newSy(); - } - - /** - * Tries to apply a beat effect to the given beat. - * @returns true if a effect could be applied, otherwise false - */ - private _applyBeatEffect(beat: Beat): boolean { - const syData: string = (this._syData as string).toLowerCase(); - if (syData === 'f') { - beat.fade = FadeType.FadeIn; - } else if (syData === 'fo') { - beat.fade = FadeType.FadeOut; - } else if (syData === 'vs') { - beat.fade = FadeType.VolumeSwell; - } else if (syData === 'v') { - beat.vibrato = VibratoType.Slight; - } else if (syData === 'vw') { - beat.vibrato = VibratoType.Wide; - } else if (syData === 's') { - beat.slap = true; - } else if (syData === 'p') { - beat.pop = true; - } else if (syData === 'tt') { - beat.tap = true; - } else if (syData === 'txt') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('beat-text', AlphaTexSymbols.String, true); - return false; - } - beat.text = this._syData as string; - } else if (syData === 'lyrics') { - this._sy = this._newSy(); - - let lyricsLine = 0; - if (this._sy === AlphaTexSymbols.Number) { - lyricsLine = this._syData as number; - this._sy = this._newSy(); - } - - if (this._sy !== AlphaTexSymbols.String) { - this._error('lyrics', AlphaTexSymbols.String, true); - return false; - } - - if (!beat.lyrics) { - beat.lyrics = []; - } - - while (beat.lyrics!.length <= lyricsLine) { - beat.lyrics.push(''); - } - - beat.lyrics[lyricsLine] = this._syData as string; - } else if (syData === 'dd') { - beat.dots = 2; - } else if (syData === 'd') { - beat.dots = 1; - } else if (syData === 'su') { - beat.pickStroke = PickStroke.Up; - } else if (syData === 'sd') { - beat.pickStroke = PickStroke.Down; - } else if (syData === 'tu') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tuplet', AlphaTexSymbols.Number, true); - return false; - } - - const numerator = this._syData as number; - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.Number) { - const denominator = this._syData as number; - this._sy = this._newSy(); - beat.tupletNumerator = numerator; - beat.tupletDenominator = denominator; - } else { - AlphaTexImporterOld._applyTuplet(beat, numerator); - } - - return true; - } else if (syData === 'tb' || syData === 'tbe') { - this._sy = this._newSy(); - - const exact: boolean = syData === 'tbe'; - - // Type - if (this._sy === AlphaTexSymbols.String) { - beat.whammyBarType = this._parseWhammyType(this._syData as string); - this._sy = this._newSy(); - } - - // Style - if (this._sy === AlphaTexSymbols.String) { - beat.whammyStyle = this._parseBendStyle(this._syData as string); - this._sy = this._newSy(); - } - - // read points - if (this._sy !== AlphaTexSymbols.LParensis) { - this._error('tremolobar-effect', AlphaTexSymbols.LParensis, true); - } - this._sy = this._newSy(true); - while (this._sy !== AlphaTexSymbols.RParensis && this._sy !== AlphaTexSymbols.Eof) { - let offset: number = 0; - let value: number = 0; - if (exact) { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tremolobar-effect', AlphaTexSymbols.Number, true); - } - offset = this._syData as number; - this._sy = this._newSy(true); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tremolobar-effect', AlphaTexSymbols.Number, true); - } - value = this._syData as number; - } else { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tremolobar-effect', AlphaTexSymbols.Number, true); - } - offset = 0; - value = this._syData as number; - } - beat.addWhammyBarPoint(new BendPoint(offset, value)); - this._sy = this._newSy(true); - } - if (beat.whammyBarPoints != null) { - while (beat.whammyBarPoints.length > 60) { - beat.removeWhammyBarPoint(beat.whammyBarPoints.length - 1); - } - // set positions - if (!exact) { - const count: number = beat.whammyBarPoints.length; - const step: number = (BendPoint.MaxPosition / (count - 1)) | 0; - let i: number = 0; - while (i < count) { - beat.whammyBarPoints[i].offset = Math.min(BendPoint.MaxPosition, i * step); - i++; - } - } else { - beat.whammyBarPoints.sort((a, b) => a.offset - b.offset); - } - } - if (this._sy !== AlphaTexSymbols.RParensis) { - this._error('tremolobar-effect', AlphaTexSymbols.RParensis, true); - } - } else if (syData === 'bu' || syData === 'bd' || syData === 'au' || syData === 'ad') { - switch (syData) { - case 'bu': - beat.brushType = BrushType.BrushUp; - break; - case 'bd': - beat.brushType = BrushType.BrushDown; - break; - case 'au': - beat.brushType = BrushType.ArpeggioUp; - break; - case 'ad': - beat.brushType = BrushType.ArpeggioDown; - break; - } - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - // explicit duration - beat.brushDuration = this._syData as number; - this._sy = this._newSy(); - return true; - } - // default to calculated duration - beat.updateDurations(); - if (syData === 'bu' || syData === 'bd') { - beat.brushDuration = beat.playbackDuration / 4 / beat.notes.length; - } else if (syData === 'au' || syData === 'ad') { - beat.brushDuration = beat.playbackDuration / beat.notes.length; - } - return true; - } else if (syData === 'ch') { - this._sy = this._newSy(); - const chordName: string = this._syData as string; - const chordId: string = this._getChordId(this._currentStaff, chordName); - if (!this._currentStaff.hasChord(chordId)) { - const chord: Chord = new Chord(); - chord.showDiagram = false; - chord.name = chordName; - this._currentStaff.addChord(chordId, chord); - } - beat.chordId = chordId; - } else if (syData === 'gr') { - this._sy = this._newSy(); - if ((this._syData as string).toLowerCase() === 'ob') { - beat.graceType = GraceType.OnBeat; - this._sy = this._newSy(); - } else if ((this._syData as string).toLowerCase() === 'b') { - beat.graceType = GraceType.BendGrace; - this._sy = this._newSy(); - } else { - beat.graceType = GraceType.BeforeBeat; - } - return true; - } else if (syData === 'dy') { - this._sy = this._newSy(); - const dynamicString = (this._syData as string).toUpperCase() as keyof typeof DynamicValue; - switch (dynamicString) { - case 'PPP': - case 'PP': - case 'P': - case 'MP': - case 'MF': - case 'F': - case 'FF': - case 'FFF': - case 'PPPP': - case 'PPPPP': - case 'PPPPPP': - case 'FFFF': - case 'FFFFF': - case 'FFFFFF': - case 'SF': - case 'SFP': - case 'SFPP': - case 'FP': - case 'RF': - case 'RFZ': - case 'SFZ': - case 'SFFZ': - case 'FZ': - case 'N': - case 'PF': - case 'SFZP': - beat.dynamics = DynamicValue[dynamicString]; - break; - } - this._currentDynamics = beat.dynamics; - } else if (syData === 'cre') { - beat.crescendo = CrescendoType.Crescendo; - } else if (syData === 'dec') { - beat.crescendo = CrescendoType.Decrescendo; - } else if (syData === 'tempo') { - // NOTE: playbackRatio is calculated on score finish when playback positions are known - const tempoAutomation = this._readTempoAutomation(false); - - if (beat.index === 0) { - const existing = beat.voice.bar.masterBar.tempoAutomations.find(a => a.ratioPosition === 0); - if (existing) { - existing.value = tempoAutomation.value; - existing.text = tempoAutomation.text; - beat.automations.push(existing); - return true; - } - } - beat.automations.push(tempoAutomation); - beat.voice.bar.masterBar.tempoAutomations.push(tempoAutomation); - - return true; - } else if (syData === 'volume') { - // NOTE: playbackRatio is calculated on score finish when playback positions are known - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('volume', AlphaTexSymbols.Number, true); - } - const volumeAutomation: Automation = new Automation(); - volumeAutomation.isLinear = true; - volumeAutomation.type = AutomationType.Volume; - volumeAutomation.value = this._syData as number; - this._sy = this._newSy(); - - beat.automations.push(volumeAutomation); - return true; - } else if (syData === 'balance') { - // NOTE: playbackRatio is calculated on score finish when playback positions are known - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('balance', AlphaTexSymbols.Number, true); - } - const balanceAutomation: Automation = new Automation(); - balanceAutomation.isLinear = true; - balanceAutomation.type = AutomationType.Balance; - balanceAutomation.value = ModelUtils.clamp(this._syData as number, 0, 16); - this._sy = this._newSy(); - - beat.automations.push(balanceAutomation); - return true; - } else if (syData === 'tp') { - this._sy = this._newSy(); - beat.tremoloSpeed = Duration.Eighth; - if (this._sy === AlphaTexSymbols.Number) { - switch (this._syData as number) { - case 8: - beat.tremoloSpeed = Duration.Eighth; - break; - case 16: - beat.tremoloSpeed = Duration.Sixteenth; - break; - case 32: - beat.tremoloSpeed = Duration.ThirtySecond; - break; - default: - beat.tremoloSpeed = Duration.Eighth; - break; - } - this._sy = this._newSy(); - } - return true; - } else if (syData === 'spd') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Down; - // exact ratio position will be applied after .finish() when times are known - sustainPedal.ratioPosition = beat.voice.bar.sustainPedals.length; - this._sustainPedalToBeat.set(sustainPedal, beat); - beat.voice.bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - return true; - } else if (syData === 'sph') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Hold; - // exact ratio position will be applied after .finish() when times are known - sustainPedal.ratioPosition = beat.voice.bar.sustainPedals.length; - this._sustainPedalToBeat.set(sustainPedal, beat); - beat.voice.bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - return true; - } else if (syData === 'spu') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Up; - // exact ratio position will be applied after .finish() when times are known - sustainPedal.ratioPosition = beat.voice.bar.sustainPedals.length; - this._sustainPedalToBeat.set(sustainPedal, beat); - beat.voice.bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - return true; - } else if (syData === 'spe') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Up; - sustainPedal.ratioPosition = 1; - beat.voice.bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - return true; - } else if (syData === 'slashed') { - beat.slashed = true; - this._sy = this._newSy(); - return true; - } else if (syData === 'ds') { - beat.deadSlapped = true; - this._sy = this._newSy(); - if (beat.notes.length === 1 && beat.notes[0].isDead) { - beat.removeNote(beat.notes[0]); - } - return true; - } else if (syData === 'glpf') { - this._sy = this._newSy(); - beat.golpe = GolpeType.Finger; - return true; - } else if (syData === 'glpt') { - this._sy = this._newSy(); - beat.golpe = GolpeType.Thumb; - return true; - } else if (syData === 'waho') { - this._sy = this._newSy(); - beat.wahPedal = WahPedal.Open; - return true; - } else if (syData === 'wahc') { - this._sy = this._newSy(); - beat.wahPedal = WahPedal.Closed; - return true; - } else if (syData === 'barre') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('beat-barre', AlphaTexSymbols.Number, true); - } - beat.barreFret = this._syData as number; - beat.barreShape = BarreShape.Full; - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.String) { - switch ((this._syData as string).toLowerCase()) { - case 'full': - beat.barreShape = BarreShape.Full; - this._sy = this._newSy(); - break; - case 'half': - beat.barreShape = BarreShape.Half; - this._sy = this._newSy(); - break; - } - } - - return true; - } else if (syData === 'rasg') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.String) { - this._error('rasgueado', AlphaTexSymbols.String, true); - } - - switch ((this._syData as string).toLowerCase()) { - case 'ii': - beat.rasgueado = Rasgueado.Ii; - break; - case 'mi': - beat.rasgueado = Rasgueado.Mi; - break; - case 'miitriplet': - beat.rasgueado = Rasgueado.MiiTriplet; - break; - case 'miianapaest': - beat.rasgueado = Rasgueado.MiiAnapaest; - break; - case 'pmptriplet': - beat.rasgueado = Rasgueado.PmpTriplet; - break; - case 'pmpanapaest': - beat.rasgueado = Rasgueado.PmpAnapaest; - break; - case 'peitriplet': - beat.rasgueado = Rasgueado.PeiTriplet; - break; - case 'peianapaest': - beat.rasgueado = Rasgueado.PeiAnapaest; - break; - case 'paitriplet': - beat.rasgueado = Rasgueado.PaiTriplet; - break; - case 'paianapaest': - beat.rasgueado = Rasgueado.PaiAnapaest; - break; - case 'amitriplet': - beat.rasgueado = Rasgueado.AmiTriplet; - break; - case 'amianapaest': - beat.rasgueado = Rasgueado.AmiAnapaest; - break; - case 'ppp': - beat.rasgueado = Rasgueado.Ppp; - break; - case 'amii': - beat.rasgueado = Rasgueado.Amii; - break; - case 'amip': - beat.rasgueado = Rasgueado.Amip; - break; - case 'eami': - beat.rasgueado = Rasgueado.Eami; - break; - case 'eamii': - beat.rasgueado = Rasgueado.Eamii; - break; - case 'peami': - beat.rasgueado = Rasgueado.Peami; - break; - } - this._sy = this._newSy(); - - return true; - } else if (syData === 'ot') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.String) { - this._error('beat-ottava', AlphaTexSymbols.String, true); - } - - beat.ottava = this._parseClefOttavaFromString(this._syData as string); - } else if (syData === 'legatoorigin') { - beat.isLegatoOrigin = true; - } else if (syData === 'instrument') { - this._sy = this._newSy(); - - let program = 0; - - if (this._sy === AlphaTexSymbols.Number) { - program = this._syData as number; - } else if (this._sy === AlphaTexSymbols.String) { - program = GeneralMidi.getValue(this._syData as string); - } else { - this._error('instrument-change', AlphaTexSymbols.Number, true); - } - - const automation = new Automation(); - automation.isLinear = false; - automation.type = AutomationType.Instrument; - automation.value = program; - beat.automations.push(automation); - } else if (syData === 'bank') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bank-change', AlphaTexSymbols.Number, true); - } - - const automation = new Automation(); - automation.isLinear = false; - automation.type = AutomationType.Bank; - automation.value = this._syData as number; - beat.automations.push(automation); - } else if (syData === 'fermata') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('fermata', AlphaTexSymbols.Number, true); - } - - const fermata = new Fermata(); - fermata.type = this._parseFermataFromString(this._syData as string); - - this._sy = this._newSy(true); - if (this._sy === AlphaTexSymbols.Number) { - fermata.length = this._syData as number; - this._sy = this._newSy(); - } - - beat.fermata = fermata; - - return true; - } else if (syData === 'beam') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('beam', AlphaTexSymbols.Number, true); - } - - switch ((this._syData as string).toLowerCase()) { - case 'invert': - beat.invertBeamDirection = true; - break; - case 'up': - beat.preferredBeamDirection = BeamDirection.Up; - break; - case 'down': - beat.preferredBeamDirection = BeamDirection.Down; - break; - case 'auto': - beat.beamingMode = BeatBeamingMode.Auto; - break; - case 'split': - beat.beamingMode = BeatBeamingMode.ForceSplitToNext; - break; - case 'merge': - beat.beamingMode = BeatBeamingMode.ForceMergeWithNext; - break; - case 'splitsecondary': - beat.beamingMode = BeatBeamingMode.ForceSplitOnSecondaryToNext; - break; - } - } else if (syData === 'timer') { - beat.showTimer = true; - } else { - // string didn't match any beat effect syntax - return false; - } - // default behaviour when a beat effect above - // does not handle new symbol + return on its own - this._sy = this._newSy(); - return true; - } - - private _parseBracketExtendMode(str: string): BracketExtendMode { - switch (str.toLowerCase()) { - case 'nobrackets': - return BracketExtendMode.NoBrackets; - case 'groupstaves': - return BracketExtendMode.GroupStaves; - case 'groupsimilarinstruments': - return BracketExtendMode.GroupSimilarInstruments; - default: - return BracketExtendMode.GroupStaves; - } - } - - private _parseFermataFromString(str: string): FermataType { - switch (str.toLowerCase()) { - case 'short': - return FermataType.Short; - case 'medium': - return FermataType.Medium; - case 'long': - return FermataType.Long; - default: - return FermataType.Medium; - } - } - - private _parseClefOttavaFromString(str: string): Ottavia { - switch (str.toLowerCase()) { - case '15ma': - return Ottavia._15ma; - case '8va': - return Ottavia._8va; - case 'regular': - return Ottavia.Regular; - case '8vb': - return Ottavia._8vb; - case '15mb': - return Ottavia._15mb; - default: - return Ottavia.Regular; - } - } - - private _getChordId(currentStaff: Staff, chordName: string): string { - return chordName.toLowerCase() + currentStaff.index + currentStaff.track.index; - } - - private static _applyTuplet(beat: Beat, tuplet: number): void { - switch (tuplet) { - case 3: - beat.tupletNumerator = 3; - beat.tupletDenominator = 2; - break; - case 5: - beat.tupletNumerator = 5; - beat.tupletDenominator = 4; - break; - case 6: - beat.tupletNumerator = 6; - beat.tupletDenominator = 4; - break; - case 7: - beat.tupletNumerator = 7; - beat.tupletDenominator = 4; - break; - case 9: - beat.tupletNumerator = 9; - beat.tupletDenominator = 8; - break; - case 10: - beat.tupletNumerator = 10; - beat.tupletDenominator = 8; - break; - case 11: - beat.tupletNumerator = 11; - beat.tupletDenominator = 8; - break; - case 12: - beat.tupletNumerator = 12; - beat.tupletDenominator = 8; - break; - default: - beat.tupletNumerator = 1; - beat.tupletDenominator = 1; - break; - } - } - - private _isNoteText(txt: string): boolean { - return txt === 'x' || txt === '-' || txt === 'r'; - } - - private _note(beat: Beat): boolean { - // fret.string or TuningWithAccidentals - let isDead: boolean = false; - let isTie: boolean = false; - let fret: number = -1; - let octave: number = -1; - let tone: number = -1; - let accidentalMode: NoteAccidentalMode = NoteAccidentalMode.Default; - switch (this._sy) { - case AlphaTexSymbols.Number: - fret = this._syData as number; - if (this._currentStaff.isPercussion && !PercussionMapper.instrumentArticulations.has(fret)) { - this._errorMessage(`Unknown percussion articulation ${fret}`); - } - break; - case AlphaTexSymbols.String: - if (this._currentStaff.isPercussion) { - const articulationName = (this._syData as string).toLowerCase(); - if (this._percussionArticulationNames.has(articulationName)) { - fret = this._percussionArticulationNames.get(articulationName)!; - } else { - this._errorMessage(`Unknown percussion articulation '${this._syData}'`); - } - } else { - isDead = (this._syData as string) === 'x'; - isTie = (this._syData as string) === '-'; - - if (isTie || isDead) { - fret = 0; - } else { - this._error('note-fret', AlphaTexSymbols.Number, true); - } - } - break; - case AlphaTexSymbols.Tuning: - // auto convert staff - if (beat.index === 0 && beat.voice.index === 0 && beat.voice.bar.index === 0) { - this._makeCurrentStaffPitched(); - } - - const tuning: TuningParseResult = this._syData as TuningParseResult; - octave = tuning.octave; - tone = tuning.tone.noteValue; - if (this._accidentalMode === AlphaTexAccidentalMode.Explicit) { - accidentalMode = tuning.tone.accidentalMode; - } - break; - default: - return false; - } - this._sy = this._newSy(); // Fret done - - const isFretted: boolean = - octave === -1 && this._currentStaff.tuning.length > 0 && !this._currentStaff.isPercussion; - let noteString: number = -1; - if (isFretted) { - // Fret [Dot] String - if (this._sy !== AlphaTexSymbols.Dot) { - this._error('note', AlphaTexSymbols.Dot, true); - } - this._sy = this._newSy(); // dot done - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('note-string', AlphaTexSymbols.Number, true); - } - noteString = this._syData as number; - if (noteString < 1 || noteString > this._currentStaff.tuning.length) { - this._error('note-string', AlphaTexSymbols.Number, false); - } - this._sy = this._newSy(); // string done - } - // read effects - const note: Note = new Note(); - if (isFretted) { - note.string = this._currentStaff.tuning.length - (noteString - 1); - note.isDead = isDead; - note.isTieDestination = isTie; - if (!isTie) { - note.fret = fret; - } - } else if (this._currentStaff.isPercussion) { - const articulationValue = fret; - let articulationIndex: number = 0; - if (this._articulationValueToIndex.has(articulationValue)) { - articulationIndex = this._articulationValueToIndex.get(articulationValue)!; - } else { - articulationIndex = this._currentTrack.percussionArticulations.length; - const articulation = PercussionMapper.getArticulationByInputMidiNumber(articulationValue); - if (articulation === null) { - this._errorMessage(`Unknown articulation value ${articulationValue}`); - } - - this._currentTrack.percussionArticulations.push(articulation!); - this._articulationValueToIndex.set(articulationValue, articulationIndex); - } - - note.percussionArticulation = articulationIndex; - } else { - note.octave = octave; - note.tone = tone; - note.accidentalMode = accidentalMode; - note.isTieDestination = isTie; - } - beat.addNote(note); - this._noteEffects(note); - return true; - } - - private _noteEffects(note: Note): void { - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.String) { - const syData = (this._syData as string).toLowerCase(); - if (syData === 'b' || syData === 'be') { - this._sy = this._newSy(); - const exact: boolean = syData === 'be'; - - // Type - if (this._sy === AlphaTexSymbols.String) { - note.bendType = this._parseBendType(this._syData as string); - this._sy = this._newSy(); - } - - // Style - if (this._sy === AlphaTexSymbols.String) { - note.bendStyle = this._parseBendStyle(this._syData as string); - this._sy = this._newSy(); - } - - // read points - if (this._sy !== AlphaTexSymbols.LParensis) { - this._error('bend-effect', AlphaTexSymbols.LParensis, true); - } - - if (exact) { - // float on position - this._sy = this._newSy(true); - } else { - this._sy = this._newSy(); - } - - while (this._sy !== AlphaTexSymbols.RParensis && this._sy !== AlphaTexSymbols.Eof) { - let offset: number = 0; - let value: number = 0; - if (exact) { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bend-effect-value', AlphaTexSymbols.Number, true); - } - offset = this._syData as number; - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bend-effect-value', AlphaTexSymbols.Number, true); - } - value = this._syData as number; - } else { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bend-effect-value', AlphaTexSymbols.Number, true); - } - value = this._syData as number; - } - note.addBendPoint(new BendPoint(offset, value)); - - if (exact) { - // float on position - this._sy = this._newSy(true); - } else { - this._sy = this._newSy(); - } - } - const points = note.bendPoints; - if (points != null) { - while (points.length > 60) { - points.splice(points.length - 1, 1); - } - // set positions - if (exact) { - points.sort((a, b) => { - return a.offset - b.offset; - }); - } else { - const count: number = points.length; - const step: number = (60 / (count - 1)) | 0; - let i: number = 0; - while (i < count) { - points[i].offset = Math.min(60, i * step); - i++; - } - } - } - if (this._sy !== AlphaTexSymbols.RParensis) { - this._error('bend-effect', AlphaTexSymbols.RParensis, true); - } - this._sy = this._newSy(); - } else if (syData === 'nh') { - note.harmonicType = HarmonicType.Natural; - note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(note.fret); - this._sy = this._newSy(); - } else if (syData === 'ah') { - // todo: Artificial Key - note.harmonicType = HarmonicType.Artificial; - note.harmonicValue = this._harmonicValue(note.harmonicValue); - } else if (syData === 'th') { - // todo: store tapped fret in data - note.harmonicType = HarmonicType.Tap; - note.harmonicValue = this._harmonicValue(note.harmonicValue); - } else if (syData === 'ph') { - note.harmonicType = HarmonicType.Pinch; - note.harmonicValue = this._harmonicValue(note.harmonicValue); - } else if (syData === 'sh') { - note.harmonicType = HarmonicType.Semi; - note.harmonicValue = this._harmonicValue(note.harmonicValue); - } else if (syData === 'fh') { - note.harmonicType = HarmonicType.Feedback; - note.harmonicValue = this._harmonicValue(note.harmonicValue); - } else if (syData === 'tr') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('trill-effect', AlphaTexSymbols.Number, true); - } - const fret: number = this._syData as number; - this._sy = this._newSy(); - let duration: Duration = Duration.Sixteenth; - if (this._sy === AlphaTexSymbols.Number) { - switch (this._syData as number) { - case 16: - duration = Duration.Sixteenth; - break; - case 32: - duration = Duration.ThirtySecond; - break; - case 64: - duration = Duration.SixtyFourth; - break; - default: - duration = Duration.Sixteenth; - break; - } - this._sy = this._newSy(); - } - note.trillValue = fret + note.stringTuning; - note.trillSpeed = duration; - } else if (syData === 'v') { - this._sy = this._newSy(); - note.vibrato = VibratoType.Slight; - } else if (syData === 'vw') { - this._sy = this._newSy(); - note.vibrato = VibratoType.Wide; - } else if (syData === 'sl') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.Legato; - } else if (syData === 'ss') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.Shift; - } else if (syData === 'sib') { - this._sy = this._newSy(); - note.slideInType = SlideInType.IntoFromBelow; - } else if (syData === 'sia') { - this._sy = this._newSy(); - note.slideInType = SlideInType.IntoFromAbove; - } else if (syData === 'sou') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.OutUp; - } else if (syData === 'sod') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.OutDown; - } else if (syData === 'psd') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.PickSlideDown; - } else if (syData === 'psu') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.PickSlideUp; - } else if (syData === 'h') { - this._sy = this._newSy(); - note.isHammerPullOrigin = true; - } else if (syData === 'lht') { - this._sy = this._newSy(); - note.isLeftHandTapped = true; - } else if (syData === 'g') { - this._sy = this._newSy(); - note.isGhost = true; - } else if (syData === 'ac') { - this._sy = this._newSy(); - note.accentuated = AccentuationType.Normal; - } else if (syData === 'hac') { - this._sy = this._newSy(); - note.accentuated = AccentuationType.Heavy; - } else if (syData === 'ten') { - this._sy = this._newSy(); - note.accentuated = AccentuationType.Tenuto; - } else if (syData === 'pm') { - this._sy = this._newSy(); - note.isPalmMute = true; - } else if (syData === 'st') { - this._sy = this._newSy(); - note.isStaccato = true; - } else if (syData === 'lr') { - this._sy = this._newSy(); - note.isLetRing = true; - } else if (syData === 'x') { - this._sy = this._newSy(); - note.isDead = true; - } else if (syData === '-' || syData === 't') { - this._sy = this._newSy(); - note.isTieDestination = true; - } else if (syData === 'lf') { - this._sy = this._newSy(); - let finger: Fingers = Fingers.Thumb; - if (this._sy === AlphaTexSymbols.Number) { - finger = this._toFinger(this._syData as number); - this._sy = this._newSy(); - } - note.leftHandFinger = finger; - } else if (syData === 'rf') { - this._sy = this._newSy(); - let finger: Fingers = Fingers.Thumb; - if (this._sy === AlphaTexSymbols.Number) { - finger = this._toFinger(this._syData as number); - this._sy = this._newSy(); - } - note.rightHandFinger = finger; - } else if (syData === 'acc') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.String) { - this._error('note-accidental', AlphaTexSymbols.String, true); - } - - note.accidentalMode = ModelUtils.parseAccidentalMode(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'turn') { - this._sy = this._newSy(); - note.ornament = NoteOrnament.Turn; - } else if (syData === 'iturn') { - this._sy = this._newSy(); - note.ornament = NoteOrnament.InvertedTurn; - } else if (syData === 'umordent') { - this._sy = this._newSy(); - note.ornament = NoteOrnament.UpperMordent; - } else if (syData === 'lmordent') { - this._sy = this._newSy(); - note.ornament = NoteOrnament.LowerMordent; - } else if (syData === 'string') { - this._sy = this._newSy(); - note.showStringNumber = true; - } else if (syData === 'hide') { - this._sy = this._newSy(); - note.isVisible = false; - } else if (syData === 'slur') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('slur', AlphaTexSymbols.String, true); - } - - const slurId = this._syData as string; - if (this._slurs.has(slurId)) { - const slurOrigin = this._slurs.get(slurId)!; - slurOrigin.slurDestination = note; - - note.slurOrigin = slurOrigin; - note.isSlurDestination = true; - } else { - this._slurs.set(slurId, note); - } - - this._sy = this._newSy(); - } else if (this._applyBeatEffect(note.beat)) { - // Success - } else { - this._error(syData, AlphaTexSymbols.String, false); - } - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('note-effect', AlphaTexSymbols.RBrace, false); - } - this._sy = this._newSy(); - } - - private _harmonicValue(harmonicValue: number): number { - this._sy = this._newSy(true); - if (this._sy === AlphaTexSymbols.Number) { - harmonicValue = this._syData as number; - this._sy = this._newSy(true); - } - return harmonicValue; - } - - private _toFinger(num: number): Fingers { - switch (num) { - case 1: - return Fingers.Thumb; - case 2: - return Fingers.IndexFinger; - case 3: - return Fingers.MiddleFinger; - case 4: - return Fingers.AnnularFinger; - case 5: - return Fingers.LittleFinger; - } - return Fingers.Thumb; - } - - private _parseDuration(duration: number): Duration { - switch (duration) { - case -4: - return Duration.QuadrupleWhole; - case -2: - return Duration.DoubleWhole; - case 1: - return Duration.Whole; - case 2: - return Duration.Half; - case 4: - return Duration.Quarter; - case 8: - return Duration.Eighth; - case 16: - return Duration.Sixteenth; - case 32: - return Duration.ThirtySecond; - case 64: - return Duration.SixtyFourth; - case 128: - return Duration.OneHundredTwentyEighth; - case 256: - return Duration.TwoHundredFiftySixth; - default: - return Duration.Quarter; - } - } - - private _parseBendStyle(str: string): BendStyle { - switch (str.toLowerCase()) { - case 'gradual': - return BendStyle.Gradual; - case 'fast': - return BendStyle.Fast; - default: - return BendStyle.Default; - } - } - - private _parseBendType(str: string): BendType { - switch (str.toLowerCase()) { - case 'none': - return BendType.None; - case 'custom': - return BendType.Custom; - case 'bend': - return BendType.Bend; - case 'release': - return BendType.Release; - case 'bendrelease': - return BendType.BendRelease; - case 'hold': - return BendType.Hold; - case 'prebend': - return BendType.Prebend; - case 'prebendbend': - return BendType.PrebendBend; - case 'prebendrelease': - return BendType.PrebendRelease; - default: - return BendType.Custom; - } - } - - private _barMeta(bar: Bar): boolean { - let anyMeta = false; - const master: MasterBar = bar.masterBar; - let endOfMeta = false; - while (!endOfMeta && this._sy === AlphaTexSymbols.MetaCommand) { - anyMeta = true; - const syData: string = (this._syData as string).toLowerCase(); - if (syData === 'ts') { - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.String) { - if ((this._syData as string).toLowerCase() === 'common') { - master.timeSignatureCommon = true; - master.timeSignatureNumerator = 4; - master.timeSignatureDenominator = 4; - this._sy = this._newSy(); - } else { - this._error('timesignature-numerator', AlphaTexSymbols.String, true); - } - } else { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('timesignature-numerator', AlphaTexSymbols.Number, true); - } - master.timeSignatureNumerator = this._syData as number; - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('timesignature-denominator', AlphaTexSymbols.Number, true); - } - master.timeSignatureDenominator = this._syData as number; - this._sy = this._newSy(); - } - } else if (syData === 'ft') { - master.isFreeTime = true; - this._sy = this._newSy(); - } else if (syData === 'ro') { - master.isRepeatStart = true; - this._sy = this._newSy(); - } else if (syData === 'rc') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('repeatclose', AlphaTexSymbols.Number, true); - } - if ((this._syData as number) > 2048) { - this._error('repeatclose', AlphaTexSymbols.Number, false); - } - master.repeatCount = this._syData as number; - this._sy = this._newSy(); - } else if (syData === 'ae') { - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.LParensis) { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('alternateending', AlphaTexSymbols.Number, true); - } - this._applyAlternateEnding(master); - while (this._sy === AlphaTexSymbols.Number) { - this._applyAlternateEnding(master); - } - if (this._sy !== AlphaTexSymbols.RParensis) { - this._error('alternateending-list', AlphaTexSymbols.RParensis, true); - } - this._sy = this._newSy(); - } else { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('alternateending', AlphaTexSymbols.Number, true); - } - this._applyAlternateEnding(master); - } - } else if (syData === 'ks') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('keysignature', AlphaTexSymbols.String, true); - } - bar.keySignature = this._parseKeySignature(this._syData as string); - bar.keySignatureType = this._parseKeySignatureType(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'clef') { - this._sy = this._newSy(); - switch (this._sy) { - case AlphaTexSymbols.String: - bar.clef = this._parseClefFromString(this._syData as string); - break; - case AlphaTexSymbols.Number: - bar.clef = this._parseClefFromInt(this._syData as number); - break; - case AlphaTexSymbols.Tuning: - const parseResult: TuningParseResult = this._syData as TuningParseResult; - bar.clef = this._parseClefFromInt(parseResult.realValue); - break; - default: - this._error('clef', AlphaTexSymbols.String, true); - break; - } - this._sy = this._newSy(); - } else if (syData === 'tempo') { - const tempoAutomation = this._readTempoAutomation(true); - - const existing = master.tempoAutomations.find(a => a.ratioPosition === tempoAutomation.ratioPosition); - if (existing) { - existing.value = tempoAutomation.value; - existing.text = tempoAutomation.text; - existing.isVisible = tempoAutomation.isVisible; - } else { - master.tempoAutomations.push(tempoAutomation); - } - } else if (syData === 'section') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('section', AlphaTexSymbols.String, true); - } - let text: string = this._syData as string; - this._sy = this._newSy(); - let marker: string = ''; - if (this._sy === AlphaTexSymbols.String && !this._isNoteText((this._syData as string).toLowerCase())) { - marker = text; - text = this._syData as string; - this._sy = this._newSy(); - } - const section: Section = new Section(); - section.marker = marker; - section.text = text; - master.section = section; - } else if (syData === 'tf') { - this._lexer.allowTuning = false; - this._sy = this._newSy(); - this._lexer.allowTuning = true; - switch (this._sy) { - case AlphaTexSymbols.String: - master.tripletFeel = this._parseTripletFeelFromString(this._syData as string); - break; - case AlphaTexSymbols.Number: - master.tripletFeel = this._parseTripletFeelFromInt(this._syData as number); - break; - default: - this._error('triplet-feel', AlphaTexSymbols.String, true); - break; - } - this._sy = this._newSy(); - } else if (syData === 'ac') { - master.isAnacrusis = true; - this._sy = this._newSy(); - } else if (syData === 'db') { - master.isDoubleBar = true; - bar.barLineRight = BarLineStyle.LightLight; - this._sy = this._newSy(); - } else if (syData === 'barlineleft') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('barlineleft', AlphaTexSymbols.String, true); - } - - bar.barLineLeft = this._parseBarLineStyle(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'barlineright') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('barlineright', AlphaTexSymbols.String, true); - } - - bar.barLineRight = this._parseBarLineStyle(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'accidentals') { - this._handleAccidentalMode(); - } else if (syData === 'jump') { - this._handleDirections(master); - } else if (syData === 'ottava') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.String) { - this._error('ottava', AlphaTexSymbols.String, true); - } - - bar.clefOttava = this._parseClefOttavaFromString(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'simile') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.String) { - this._error('simile', AlphaTexSymbols.String, true); - } - - bar.simileMark = this._parseSimileMarkFromString(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'scale') { - this._sy = this._newSy(true); - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('scale', AlphaTexSymbols.Number, true); - } - - master.displayScale = this._syData as number; - bar.displayScale = this._syData as number; - this._sy = this._newSy(); - } else if (syData === 'width') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('width', AlphaTexSymbols.Number, true); - } - - master.displayWidth = this._syData as number; - bar.displayWidth = this._syData as number; - this._sy = this._newSy(); - } else if (syData === 'spd') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Down; - - this._sy = this._newSy(true); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('spd', AlphaTexSymbols.Number, true); - } - sustainPedal.ratioPosition = this._syData as number; - bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - } else if (syData === 'spu') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Up; - - this._sy = this._newSy(true); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('spu', AlphaTexSymbols.Number, true); - } - sustainPedal.ratioPosition = this._syData as number; - bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - } else if (syData === 'sph') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Hold; - - this._sy = this._newSy(true); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('sph', AlphaTexSymbols.Number, true); - } - sustainPedal.ratioPosition = this._syData as number; - bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - } else { - if (bar.index === 0) { - switch (this._handleStaffMeta()) { - case StaffMetaResult.KnownStaffMeta: - // ok -> Continue - break; - case StaffMetaResult.UnknownStaffMeta: - this._error('measure-effects', AlphaTexSymbols.String, false); - break; - case StaffMetaResult.EndOfMetaDetected: - endOfMeta = true; - break; - } - } else { - switch (this._handleStaffMeta()) { - case StaffMetaResult.EndOfMetaDetected: - endOfMeta = true; - break; - default: - this._error('measure-effects', AlphaTexSymbols.String, false); - break; - } - } - } - } - - if (master.index === 0 && master.tempoAutomations.length === 0) { - const tempoAutomation: Automation = new Automation(); - tempoAutomation.isLinear = false; - tempoAutomation.type = AutomationType.Tempo; - tempoAutomation.value = this._score.tempo; - tempoAutomation.text = this._score.tempoLabel; - master.tempoAutomations.push(tempoAutomation); - } - return anyMeta; - } - - private _parseBarLineStyle(v: string): BarLineStyle { - switch (v.toLowerCase()) { - case 'automatic': - return BarLineStyle.Automatic; - case 'dashed': - return BarLineStyle.Dashed; - case 'dotted': - return BarLineStyle.Dotted; - case 'heavy': - return BarLineStyle.Heavy; - case 'heavyheavy': - return BarLineStyle.HeavyHeavy; - case 'heavylight': - return BarLineStyle.HeavyLight; - case 'lightheavy': - return BarLineStyle.LightHeavy; - case 'lightlight': - return BarLineStyle.LightLight; - case 'none': - return BarLineStyle.None; - case 'regular': - return BarLineStyle.Regular; - case 'short': - return BarLineStyle.Short; - case 'tick': - return BarLineStyle.Tick; - } - - return BarLineStyle.Automatic; - } - - private _parseSimileMarkFromString(str: string): SimileMark { - switch (str.toLowerCase()) { - case 'none': - return SimileMark.None; - case 'simple': - return SimileMark.Simple; - case 'firstofdouble': - return SimileMark.FirstOfDouble; - case 'secondofdouble': - return SimileMark.SecondOfDouble; - default: - return SimileMark.None; - } - } - - private _handleDirections(master: MasterBar) { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('direction', AlphaTexSymbols.String, true); - } - - switch ((this._syData as string).toLowerCase()) { - case 'fine': - master.addDirection(Direction.TargetFine); - break; - case 'segno': - master.addDirection(Direction.TargetSegno); - break; - case 'segnosegno': - master.addDirection(Direction.TargetSegnoSegno); - break; - case 'coda': - master.addDirection(Direction.TargetCoda); - break; - case 'doublecoda': - master.addDirection(Direction.TargetDoubleCoda); - break; - - case 'dacapo': - master.addDirection(Direction.JumpDaCapo); - break; - case 'dacapoalcoda': - master.addDirection(Direction.JumpDaCapoAlCoda); - break; - case 'dacapoaldoublecoda': - master.addDirection(Direction.JumpDaCapoAlDoubleCoda); - break; - case 'dacapoalfine': - master.addDirection(Direction.JumpDaCapoAlFine); - break; - - case 'dalsegno': - master.addDirection(Direction.JumpDalSegno); - break; - case 'dalsegnoalcoda': - master.addDirection(Direction.JumpDalSegnoAlCoda); - break; - case 'dalsegnoaldoublecoda': - master.addDirection(Direction.JumpDalSegnoAlDoubleCoda); - break; - case 'dalsegnoalfine': - master.addDirection(Direction.JumpDalSegnoAlFine); - break; - - case 'dalsegnosegno': - master.addDirection(Direction.JumpDalSegnoSegno); - break; - case 'dalsegnosegnoalcoda': - master.addDirection(Direction.JumpDalSegnoSegnoAlCoda); - break; - case 'dalsegnosegnoaldoublecoda': - master.addDirection(Direction.JumpDalSegnoSegnoAlDoubleCoda); - break; - case 'dalsegnosegnoalfine': - master.addDirection(Direction.JumpDalSegnoSegnoAlFine); - break; - - case 'dacoda': - master.addDirection(Direction.JumpDaCoda); - break; - case 'dadoublecoda': - master.addDirection(Direction.JumpDaDoubleCoda); - break; - default: - this._errorMessage(`Unexpected direction value: '${this._syData}'`); - return; - } - - this._sy = this._newSy(); - } - - private _readTempoAutomation(withPosition: boolean) { - this._sy = this._newSy(true); - - const tempoAutomation: Automation = new Automation(); - tempoAutomation.isLinear = false; - tempoAutomation.type = AutomationType.Tempo; - - if (this._sy === AlphaTexSymbols.LParensis && withPosition) { - this._sy = this._newSy(true); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tempo', AlphaTexSymbols.Number, true); - } - - tempoAutomation.value = this._syData as number; - this._sy = this._newSy(true); - - if (this._sy === AlphaTexSymbols.String) { - tempoAutomation.text = this._syData as string; - this._sy = this._newSy(true); - } - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tempo', AlphaTexSymbols.Number, true); - } - tempoAutomation.ratioPosition = this._syData as number; - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.String && (this._syData as string) === 'hide') { - tempoAutomation.isVisible = false; - this._sy = this._newSy(); - } - - if (this._sy !== AlphaTexSymbols.RParensis) { - this._error('tempo', AlphaTexSymbols.RParensis, true); - } - this._sy = this._newSy(); - } else if (this._sy === AlphaTexSymbols.Number) { - tempoAutomation.value = this._syData as number; - - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.String && (this._syData as string) !== 'r') { - tempoAutomation.text = this._syData as string; - this._sy = this._newSy(); - } - } else { - this._error('tempo', AlphaTexSymbols.Number, true); - } - - return tempoAutomation; - } - - private _applyAlternateEnding(master: MasterBar): void { - const num = this._syData as number; - if (num < 1) { - // Repeat numberings start from 1 - this._error('alternateending', AlphaTexSymbols.Number, true); - } - // Alternate endings bitflag starts from 0 - master.alternateEndings |= 1 << (num - 1); - this._sy = this._newSy(); - } - - private _parseWhammyType(str: string): WhammyType { - switch (str.toLowerCase()) { - case 'none': - return WhammyType.None; - case 'custom': - return WhammyType.Custom; - case 'dive': - return WhammyType.Dive; - case 'dip': - return WhammyType.Dip; - case 'hold': - return WhammyType.Hold; - case 'predive': - return WhammyType.Predive; - case 'predivedive': - return WhammyType.PrediveDive; - default: - return WhammyType.Custom; - } - } -} diff --git a/packages/alphatab/test/importer/AlphaTexImporterOldNewCompat.test.ts b/packages/alphatab/test/importer/AlphaTexImporterOldNewCompat.test.ts deleted file mode 100644 index 71bad14ff..000000000 --- a/packages/alphatab/test/importer/AlphaTexImporterOldNewCompat.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { AlphaTexErrorWithDiagnostics, AlphaTexImporter } from '@coderline/alphatab/importer/AlphaTexImporter'; -import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; -import { Logger } from '@coderline/alphatab/Logger'; -import type { Score } from '@coderline/alphatab/model/Score'; -import { Settings } from '@coderline/alphatab/Settings'; -import { AlphaTexExporterOld } from 'test/exporter/AlphaTexExporterOld'; -import { AlphaTexError, AlphaTexImporterOld } from 'test/importer/AlphaTexImporterOld'; -import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; -import { TestPlatform } from 'test/TestPlatform'; -import { assert, expect } from 'chai'; - -describe('AlphaTexImporterOldNewCompat', () => { - async function loadScore(name: string): Promise { - const data = await TestPlatform.loadFile(`test-data/${name}`); - try { - return ScoreLoader.loadScoreFromBytes(data); - } catch { - return null; - } - } - - async function readAndCompare( - name: string, - ignoreKeys: string[] | null, - tex: string, - settings: Settings - ): Promise { - const fileName = name.substring(name.lastIndexOf('/') + 1); - const lines = tex.split('\n'); - - let oldScore: Score; - let newScore: Score; - - try { - const oldImporter = new AlphaTexImporterOld(); - oldImporter.initFromString(tex, settings); - oldScore = oldImporter.readScore(); - ComparisonHelpers.alphaTexExportRoundtripPrepare(oldScore); - } catch (e) { - let errorLine = ''; - const error = e as Error; - if (error.cause instanceof AlphaTexError) { - const alphaTexError = error.cause as AlphaTexError; - errorLine = `Error Line: ${lines[alphaTexError.line - 1]}\n`; - } - - assert.fail(`<${fileName}>${e}\n${errorLine}${error.stack}\n Tex:\n${tex}`); - return; - } - - try { - const newImporter = new AlphaTexImporter(); - newImporter.initFromString(tex, settings); - newScore = newImporter.readScore(); - ComparisonHelpers.alphaTexExportRoundtripPrepare(newScore); - } catch (e) { - let errorDetails = ''; - const error = e as Error; - if (error instanceof AlphaTexErrorWithDiagnostics) { - errorDetails = (error as AlphaTexErrorWithDiagnostics).toString(); - } else if (error.cause instanceof AlphaTexErrorWithDiagnostics) { - errorDetails = (error.cause as AlphaTexErrorWithDiagnostics).toString(); - } - assert.fail(`<${fileName}>${e}\n${errorDetails}${error.stack}\n Tex:\n${tex}`); - return; - } - - ComparisonHelpers.alphaTexExportRoundtripEqual(fileName, newScore, oldScore, ignoreKeys); - } - - async function testRoundTripEqual(name: string, ignoreKeys: string[] | null = null): Promise { - const settings = new Settings(); - const expected = await loadScore(name); - if (!expected) { - return; - } - - ComparisonHelpers.alphaTexExportRoundtripPrepare(expected); - - // use exporters to create alphaTex code for comparison test - - const exportedOld = new AlphaTexExporterOld().exportToString(expected, settings); - await readAndCompare(name, ignoreKeys, exportedOld, settings); - - // old importer cannot load new alphaTex - // const exportedNew = new AlphaTexExporter().exportToString(expected, settings); - // await readAndCompare(name, ignoreKeys, exportedNew, settings); - } - - async function testRoundTripFolderEqual(name: string): Promise { - const files: string[] = await TestPlatform.listDirectory(`test-data/${name}`); - for (const file of files.filter(f => !f.endsWith('.png'))) { - await testRoundTripEqual(`${name}/${file}`, null); - } - } - - it('importer', async () => { - await testRoundTripFolderEqual('guitarpro7'); - }); - - it('visual-effects-and-annotations', async () => { - await testRoundTripFolderEqual('visual-tests/effects-and-annotations'); - }); - - it('visual-general', async () => { - await testRoundTripFolderEqual('visual-tests/general'); - }); - - it('visual-guitar-tabs', async () => { - await testRoundTripFolderEqual('visual-tests/guitar-tabs'); - }); - - it('visual-layout', async () => { - await testRoundTripFolderEqual('visual-tests/layout'); - }); - - it('visual-music-notation', async () => { - await testRoundTripFolderEqual('visual-tests/music-notation'); - }); - - it('visual-notation-legend', async () => { - await testRoundTripFolderEqual('visual-tests/notation-legend'); - }); - - it('visual-special-notes', async () => { - await testRoundTripFolderEqual('visual-tests/special-notes'); - }); - - it('visual-special-tracks', async () => { - await testRoundTripFolderEqual('visual-tests/special-tracks'); - }); - - it('performance', async () => { - const newTex = await TestPlatform.loadFileAsString('test-data/exporter/notation-legend-formatted.atex'); - const settings = new Settings(); - const oldTex = new AlphaTexExporterOld().exportToString(ScoreLoader.loadAlphaTex(newTex, settings)); - - const newTimes: number[] = [] ; - const oldTimes: number[] = []; - - function run(i: number, check: boolean, log: boolean) { - const oldImporter = new AlphaTexImporterOld(); - oldImporter.initFromString(oldTex, settings); - - const oldStart = performance.now(); - oldImporter.readScore(); - const oldEnd = performance.now(); - - const newImporter = new AlphaTexImporter(); - newImporter.initFromString(oldTex, settings); - - /*@target web*/ - if (gc) { - gc(); - } - const newStart = performance.now(); - newImporter.readScore(); - const newEnd = performance.now(); - - if (check) { - const oldTime = oldEnd - oldStart; - const newTime = newEnd - newStart; - if (log) { - Logger.info('Test-AlphaTexImporterOldNewCompat-performance', 'Old', i, oldTime); - Logger.info('Test-AlphaTexImporterOldNewCompat-performance', 'New', i, newTime); - Logger.info('Test-AlphaTexImporterOldNewCompat-performance', 'Diff', i, newTime - oldTime); - } - newTimes.push(newTime); - oldTimes.push(oldTime); - } - } - - // warmup - for (let i = 0; i < 10; i++) { - run(i, false, false); - } - - const testCount = 100; - for (let i = 0; i < testCount; i++) { - run(i, true, false); - } - - const meanNew = newTimes[(newTimes.length / 2) | 0]; - expect(meanNew).to.be.lessThan(25); - const meanOld = oldTimes[(oldTimes.length / 2) | 0]; - Logger.info('Test-AlphaTexImporterOldNewCompat-performance', 'Mean Ratio', meanNew / meanOld); - }); - - // it('profile', async () => { - // const session = new inspector.Session(); - // session.connect(); - - // const newTex = await TestPlatform.loadFileAsString('test-data/exporter/notation-legend-formatted.atex'); - // const settings = new Settings(); - // const oldTex = new AlphaTexExporterOld().exportToString(ScoreLoader.loadAlphaTex(newTex, settings)); - - // await new Promise(resolve => { - // session.post('Profiler.enable', () => - // session.post('Profiler.start', () => { - // resolve(); - // }) - // ); - // }); - - // for (let i = 0; i < 10; i++) { - // const newImporter = new AlphaTexImporter(); - // newImporter.initFromString(oldTex, settings); - // newImporter.readScore(); - // } - - // await new Promise((resolve, reject) => { - // session.post('Profiler.stop', async (sessionErr, data) => { - // if (sessionErr) { - // reject(sessionErr); - // return; - // } - - // try { - // await TestPlatform.saveFileAsString( - // `${new Date().toISOString().replaceAll(/[^0-9]/g, '')}.cpuprofile`, - // JSON.stringify(data.profile) - // ); - // resolve(); - // } catch (e) { - // reject(e); - // } - // }); - // }); - // }).timeout(60000); -}); diff --git a/packages/alphatab/test/importer/AlphaTexParameter.test.ts b/packages/alphatab/test/importer/AlphaTexParameter.test.ts index f61474854..d20fb79a1 100644 --- a/packages/alphatab/test/importer/AlphaTexParameter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexParameter.test.ts @@ -141,7 +141,7 @@ describe('AlphaTexParameterTests', () => { describe('metadata', () => { describe('empty signature', () => { - it('empty', () => importTest(`\\ac() C4`)); + it('empty', () => importTest(`\\track() C4`)); }); describe('single overload', () => { diff --git a/packages/alphatab/test/importer/AlphaTexParser.test.ts b/packages/alphatab/test/importer/AlphaTexParser.test.ts index fbfa134cf..c94d939e1 100644 --- a/packages/alphatab/test/importer/AlphaTexParser.test.ts +++ b/packages/alphatab/test/importer/AlphaTexParser.test.ts @@ -22,7 +22,8 @@ describe('AlphaTexParserTest', () => { it('known semantic', () => parserTest('\\title "Title" "Template" left . ')); it('known multiple', () => parserTest('\\title "Title" \\subtitle "Sub" . ')); it('known property', () => parserTest('\\track "Name" {color "red"} . ')); - it('known property before value', () => parserTest('\\track {color "red"} "Name" . ')); + it('known property before value', () => parserTest('\\chord {showFingering} "Name" . ')); + it('notelist after props', () => parserTest('\\staff { tabs } (3.3 3.3).2')); it('unknown valuelist', () => parserTest('\\notExisting ("Value") . ')); it('unknown multiple', () => parserTest('\\notExisting ("Value") \\notExisting ("") . ')); it('valuelist propertylist empty', () => parserTest('\\notExisting ("Value") {} . ')); @@ -160,6 +161,7 @@ describe('AlphaTexParserTest', () => { describe('ambiguous', () => { it('tempo and stringed note', () => parserTest('\\tempo 120 3.3 3.4')); + it('voice followed by note list', () => parserTest('\\voice (C4 C5)')); }); describe('intermediate', () => { diff --git a/packages/alphatab/test/importer/Gp3Importer.test.ts b/packages/alphatab/test/importer/Gp3Importer.test.ts index b920454a3..61e84c68e 100644 --- a/packages/alphatab/test/importer/Gp3Importer.test.ts +++ b/packages/alphatab/test/importer/Gp3Importer.test.ts @@ -98,7 +98,6 @@ describe('Gp3ImporterTest', () => { }); it('vibrato', async () => { - // TODO: Check why this vibrato is not recognized const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/vibrato.gp3'); const score: Score = reader.readScore(); GpImporterTestHelper.checkVibrato(score, false); diff --git a/packages/alphatab/test/importer/Gp5Importer.test.ts b/packages/alphatab/test/importer/Gp5Importer.test.ts index af12fee60..e56f85ebb 100644 --- a/packages/alphatab/test/importer/Gp5Importer.test.ts +++ b/packages/alphatab/test/importer/Gp5Importer.test.ts @@ -8,6 +8,7 @@ import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { GpImporterTestHelper } from 'test/importer/GpImporterTestHelper'; import { expect } from 'chai'; +import { Clef } from '@coderline/alphatab/model/Clef'; describe('Gp5ImporterTest', () => { it('score-info', async () => { @@ -544,4 +545,12 @@ describe('Gp5ImporterTest', () => { expect(score.tracks[1].playbackInfo.program).to.equal(25); expect(score.tracks[1].playbackInfo.bank).to.equal(77); }); + + it('tuning-bass-clef', async () => { + const score = (await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/bass-tuning.gp5')).readScore(); + expect(score.tracks[0].staves[0].bars[0].clef).to.equal(Clef.F4); + expect(score.tracks[1].staves[0].bars[0].clef).to.equal(Clef.F4); + expect(score.tracks[2].staves[0].bars[0].clef).to.equal(Clef.F4); + expect(score.tracks[3].staves[0].bars[0].clef).to.equal(Clef.F4); + }); }); diff --git a/packages/alphatab/test/importer/Gp8Importer.test.ts b/packages/alphatab/test/importer/Gp8Importer.test.ts index 0bb99db37..9fe94ed4e 100644 --- a/packages/alphatab/test/importer/Gp8Importer.test.ts +++ b/packages/alphatab/test/importer/Gp8Importer.test.ts @@ -3,15 +3,22 @@ import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; import { AutomationType } from '@coderline/alphatab/model/Automation'; import { BeatBeamingMode } from '@coderline/alphatab/model/Beat'; import { Direction } from '@coderline/alphatab/model/Direction'; -import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@coderline/alphatab/model/RenderStylesheet'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import { + BarNumberDisplay, + BracketExtendMode, + TrackNameMode, + TrackNameOrientation, + TrackNamePolicy +} from '@coderline/alphatab/model/RenderStylesheet'; import { ScoreSubElement } from '@coderline/alphatab/model/Score'; import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { Settings } from '@coderline/alphatab/Settings'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; +import { expect } from 'chai'; import { GpImporterTestHelper } from 'test/importer/GpImporterTestHelper'; import { TestPlatform } from 'test/TestPlatform'; -import { expect } from 'chai'; describe('Gp8ImporterTest', () => { async function prepareImporterWithFile(name: string): Promise { @@ -46,13 +53,13 @@ describe('Gp8ImporterTest', () => { it('beat-tempo-change', async () => { const score = (await prepareImporterWithFile('guitarpro8/beat-tempo-change.gp')).readScore(); - expect(score.masterBars[0].tempoAutomations).to.have.length(2); + expect(score.masterBars[0].tempoAutomations.length).to.equal(2); expect(score.masterBars[0].tempoAutomations[0].value).to.have.equal(120); expect(score.masterBars[0].tempoAutomations[0].ratioPosition).to.equal(0); expect(score.masterBars[0].tempoAutomations[1].value).to.equal(60); expect(score.masterBars[0].tempoAutomations[1].ratioPosition).to.equal(0.5); - expect(score.masterBars[1].tempoAutomations).to.have.length(2); + expect(score.masterBars[1].tempoAutomations.length).to.equal(2); expect(score.masterBars[1].tempoAutomations[0].value).to.equal(100); expect(score.masterBars[1].tempoAutomations[0].ratioPosition).to.equal(0); expect(score.masterBars[1].tempoAutomations[1].value).to.equal(120); @@ -132,6 +139,14 @@ describe('Gp8ImporterTest', () => { expect(show.stylesheet.globalDisplayChordDiagramsOnTop).to.be.true; }); + it('show-chord-diagrams-in-score', async () => { + const hide = (await prepareImporterWithFile('guitarpro8/show-diagrams-in-score.gp')).readScore(); + expect(hide.stylesheet.globalDisplayChordDiagramsInScore).to.be.true; + + const show = (await prepareImporterWithFile('guitarpro8/directions.gp')).readScore(); + expect(show.stylesheet.globalDisplayChordDiagramsInScore).to.be.false; + }); + it('beaming-mode', async () => { const score = (await prepareImporterWithFile('guitarpro8/beaming-mode.gp')).readScore(); @@ -417,11 +432,76 @@ describe('Gp8ImporterTest', () => { expect(score.tracks[0].playbackInfo.bank).to.equal(0); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].automations.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].getAutomation(AutomationType.Instrument)?.value).to.equal(25); + expect( + score.tracks[0].staves[0].bars[0].voices[0].beats[0].getAutomation(AutomationType.Instrument)?.value + ).to.equal(25); // expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].getAutomation(AutomationType.Bank)?.value).to.equal(0); skipped expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].automations.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].getAutomation(AutomationType.Instrument)?.value).to.equal(25); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].getAutomation(AutomationType.Bank)?.value).to.equal(256); + expect( + score.tracks[0].staves[0].bars[1].voices[0].beats[0].getAutomation(AutomationType.Instrument)?.value + ).to.equal(25); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].getAutomation(AutomationType.Bank)?.value).to.equal( + 256 + ); + }); + + it('extend-bar-lines', async () => { + const score = (await prepareImporterWithFile('guitarpro8/extended-barlines.gp')).readScore(); + + expect(score.stylesheet.extendBarLines).to.be.true; + }); + + describe('barnumbers', () => { + it('all', async () => { + const score = (await prepareImporterWithFile('guitarpro8/barnumbers-all.gp')).readScore(); + expect(score.stylesheet.barNumberDisplay).to.equal(BarNumberDisplay.AllBars); + }); + it('hide', async () => { + const score = (await prepareImporterWithFile('guitarpro8/barnumbers-hide.gp')).readScore(); + expect(score.stylesheet.barNumberDisplay).to.equal(BarNumberDisplay.Hide); + }); + it('first', async () => { + const score = (await prepareImporterWithFile('guitarpro8/barnumbers-first.gp')).readScore(); + expect(score.stylesheet.barNumberDisplay).to.equal(BarNumberDisplay.FirstOfSystem); + }); + }); + + it('custom-beaming', async () => { + const score = (await prepareImporterWithFile('guitarpro8/custom-beaming.gp')).readScore(); + + // NOTE: no need to verify all details, we'll have a visual test for that. + + expect(score.masterBars[0].beamingRules).to.be.ok; + expect(score.masterBars[0].beamingRules!.groups.has(Duration.Eighth)).to.be.true; + expect(score.masterBars[0].beamingRules!.groups.get(Duration.Eighth)!.join(',')).to.be.equal('2,2,2,2'); + // equal to previous + expect(score.masterBars[1].beamingRules === undefined, 'expected beamingRules of bar 1 to be undefined').to.be + .true; + expect( + score.masterBars[1].actualBeamingRules === score.masterBars[0].beamingRules, + 'actualBeamingRules of bar 1 incorrect' + ).to.be.true; + expect(score.masterBars[2].beamingRules === undefined, 'expected beamingRules of bar 2 to be undefined').to.be + .true; + expect( + score.masterBars[2].actualBeamingRules === score.masterBars[0].beamingRules, + 'actualBeamingRules of bar 1 incorrect' + ).to.be.true; + expect(score.masterBars[3].beamingRules === undefined, 'expected beamingRules of bar 3 to be undefined').to.be + .true; + expect( + score.masterBars[3].actualBeamingRules === score.masterBars[0].beamingRules, + 'actualBeamingRules of bar 1 incorrect' + ).to.be.true; + expect(score.masterBars[4].beamingRules === undefined, 'expected beamingRules of bar 4 to be undefined').to.be + .true; + expect( + score.masterBars[4].actualBeamingRules === score.masterBars[0].beamingRules, + 'actualBeamingRules of bar 1 incorrect' + ).to.be.true; + + 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'); }); }); diff --git a/packages/alphatab/test/importer/GpImporterTestHelper.ts b/packages/alphatab/test/importer/GpImporterTestHelper.ts index b5b7e0fcc..269796297 100644 --- a/packages/alphatab/test/importer/GpImporterTestHelper.ts +++ b/packages/alphatab/test/importer/GpImporterTestHelper.ts @@ -167,17 +167,14 @@ export class GpImporterTestHelper { } public static checkBend(score: Score): void { - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints!.length).to.equal(3); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints!.length).to.equal(2); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints![0].offset).to.equal(0); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints![0].value).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints![1].offset).to.equal(15); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints![1].offset).to.equal(60); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints![1].value).to.equal(4); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints![2].offset).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints![2].value).to.equal(4); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].bendPoints!.length).to.equal(7); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].bendPoints![0].offset).to.equal(0); @@ -282,13 +279,13 @@ export class GpImporterTestHelper { expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].trillSpeed).to.equal(Duration.Sixteenth); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].isTremolo).to.be.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].tremoloSpeed).to.equal(Duration.ThirtySecond); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].tremoloPicking!.marks).to.equal(3); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].isTremolo).to.be.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].tremoloSpeed).to.equal(Duration.Sixteenth); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].tremoloPicking!.marks).to.equal(2); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].isTremolo).to.be.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].tremoloSpeed).to.equal(Duration.Eighth); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].tremoloPicking!.marks).to.equal(1); } public static checkOtherEffects(score: Score, skipInstrumentCheck: boolean = false): void { @@ -305,7 +302,7 @@ export class GpImporterTestHelper { expect(score.tracks[0].staves[0].bars[3].voices[0].beats[1].text).to.equal('Text'); expect(score.masterBars[4].isDoubleBar).to.be.equal(true); - expect(score.masterBars[4].tempoAutomations).to.have.length(1); + expect(score.masterBars[4].tempoAutomations.length).to.equal(1); expect(score.masterBars[4].tempoAutomations[0]!.value).to.equal(120.0); if (!skipInstrumentCheck) { expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].getAutomation(AutomationType.Instrument)).to.be diff --git a/packages/alphatab/test/importer/GpxImporter.test.ts b/packages/alphatab/test/importer/GpxImporter.test.ts index 8922001c6..d4c4be0c9 100644 --- a/packages/alphatab/test/importer/GpxImporter.test.ts +++ b/packages/alphatab/test/importer/GpxImporter.test.ts @@ -132,7 +132,7 @@ describe('GpxImporterTest', () => { ); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].bendPoints![1].value).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints!.length).to.equal(3); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints!.length).to.equal(4); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![0].offset).to.be.closeTo( 0, @@ -153,10 +153,16 @@ describe('GpxImporterTest', () => { expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![1].value).to.equal(12); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![2].offset).to.be.closeTo( + 30, + 0.001 + ); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![2].value).to.equal(12); + + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![3].offset).to.be.closeTo( 60, 0.001 ); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![2].value).to.equal(6); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![3].value).to.equal(6); }); it('tremolo', async () => { diff --git a/packages/alphatab/test/importer/MusicXmlImporter.test.ts b/packages/alphatab/test/importer/MusicXmlImporter.test.ts index 454ae4ef4..d5fb108f7 100644 --- a/packages/alphatab/test/importer/MusicXmlImporter.test.ts +++ b/packages/alphatab/test/importer/MusicXmlImporter.test.ts @@ -1,8 +1,9 @@ -import { MusicXmlImporterTestHelper } from 'test/importer/MusicXmlImporterTestHelper'; -import type { Score } from '@coderline/alphatab/model/Score'; import { BendType } from '@coderline/alphatab/model/BendType'; -import { expect } from 'chai'; import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; +import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; +import type { Score } from '@coderline/alphatab/model/Score'; +import { expect } from 'chai'; +import { MusicXmlImporterTestHelper } from 'test/importer/MusicXmlImporterTestHelper'; describe('MusicXmlImporterTests', () => { it('track-volume', async () => { @@ -45,9 +46,9 @@ describe('MusicXmlImporterTests', () => { ); expect(score.tempo).to.be.equal(60); - expect(score.masterBars[0].tempoAutomations).to.have.length(1); + expect(score.masterBars[0].tempoAutomations.length).to.equal(1); expect(score.masterBars[0].tempoAutomations[0]?.value).to.be.equal(60); - expect(score.masterBars[1].tempoAutomations).to.have.length(1); + expect(score.masterBars[1].tempoAutomations.length).to.equal(1); expect(score.masterBars[1].tempoAutomations[0].value).to.be.equal(60); }); it('tie-destination', async () => { @@ -268,4 +269,55 @@ describe('MusicXmlImporterTests', () => { expect(score.tracks[1].playbackInfo.program).to.equal(1); expect(score.tracks[1].playbackInfo.bank).to.equal(77); }); + + it('buzzroll', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/buzzroll.xml'); + expect(score).toMatchSnapshot(); + }); + + describe('barnumberdisplay', async () => { + async function testPartwise(filename: string, display: BarNumberDisplay) { + const score = await MusicXmlImporterTestHelper.loadFile(`test-data/musicxml4/${filename}`); + expect(score.tracks[0].staves[0].bars[1].barNumberDisplay).to.equal(display); + expect(score.tracks[1].staves[0].bars[2].barNumberDisplay).to.equal(display); + } + + async function testTimewise(filename: string, display: BarNumberDisplay) { + const score = await MusicXmlImporterTestHelper.loadFile(`test-data/musicxml4/${filename}`); + expect(score.tracks[0].staves[0].bars[1].barNumberDisplay).to.equal(display); + expect(score.tracks[1].staves[0].bars[1].barNumberDisplay).to.equal(display); + } + + it('partwise-none', async () => + await testPartwise('partwise-measure-numbering-none.xml', BarNumberDisplay.Hide)); + it('partwise-measure', async () => + await testPartwise('partwise-measure-numbering-measure.xml', BarNumberDisplay.AllBars)); + it('partwise-system', async () => + await testPartwise('partwise-measure-numbering-system.xml', BarNumberDisplay.FirstOfSystem)); + it('partwise-implicit', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/partwise-anacrusis.xml'); + expect(score.tracks[0].staves[0].bars[0].barNumberDisplay).to.equal(BarNumberDisplay.Hide); + expect(score.tracks[0].staves[0].bars[1].barNumberDisplay).to.be.undefined; + expect(score.tracks[0].staves[0].bars[3].barNumberDisplay).to.equal(BarNumberDisplay.Hide); + expect(score.tracks[1].staves[0].bars[0].barNumberDisplay).to.equal(BarNumberDisplay.Hide); + expect(score.tracks[1].staves[0].bars[1].barNumberDisplay).to.be.undefined; + expect(score.tracks[1].staves[0].bars[3].barNumberDisplay).to.equal(BarNumberDisplay.Hide); + }); + + it('timewise-none', async () => + await testTimewise('timewise-measure-numbering-none.xml', BarNumberDisplay.Hide)); + it('timewise-measure', async () => + await testTimewise('timewise-measure-numbering-measure.xml', BarNumberDisplay.AllBars)); + it('timewise-system', async () => + await testTimewise('timewise-measure-numbering-system.xml', BarNumberDisplay.FirstOfSystem)); + it('timewise-implicit', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/timewise-anacrusis.xml'); + expect(score.tracks[0].staves[0].bars[0].barNumberDisplay).to.equal(BarNumberDisplay.Hide); + expect(score.tracks[0].staves[0].bars[1].barNumberDisplay).to.be.undefined; + expect(score.tracks[0].staves[0].bars[3].barNumberDisplay).to.equal(BarNumberDisplay.Hide); + expect(score.tracks[1].staves[0].bars[0].barNumberDisplay).to.equal(BarNumberDisplay.Hide); + expect(score.tracks[1].staves[0].bars[1].barNumberDisplay).to.be.undefined; + expect(score.tracks[1].staves[0].bars[3].barNumberDisplay).to.equal(BarNumberDisplay.Hide); + }); + }); }); diff --git a/packages/alphatab/test/importer/MusicXmlImporterTestHelper.ts b/packages/alphatab/test/importer/MusicXmlImporterTestHelper.ts index 6326e6d25..055823d79 100644 --- a/packages/alphatab/test/importer/MusicXmlImporterTestHelper.ts +++ b/packages/alphatab/test/importer/MusicXmlImporterTestHelper.ts @@ -1,8 +1,10 @@ import { MusicXmlImporter } from '@coderline/alphatab/importer/MusicXmlImporter'; import { UnsupportedFormatError } from '@coderline/alphatab/importer/UnsupportedFormatError'; import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; +import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { Bar } from '@coderline/alphatab/model/Bar'; import { Beat } from '@coderline/alphatab/model/Beat'; +import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; import { MasterBar } from '@coderline/alphatab/model/MasterBar'; import { Note } from '@coderline/alphatab/model/Note'; import type { Score } from '@coderline/alphatab/model/Score'; @@ -10,12 +12,10 @@ import { Staff } from '@coderline/alphatab/model/Staff'; import { Track } from '@coderline/alphatab/model/Track'; import { Voice } from '@coderline/alphatab/model/Voice'; import { Settings } from '@coderline/alphatab/Settings'; -import { TestPlatform } from 'test/TestPlatform'; -import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; -import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; import { assert } from 'chai'; +import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; +import { TestPlatform } from 'test/TestPlatform'; import { VisualTestHelper, VisualTestOptions, VisualTestRun } from 'test/visualTests/VisualTestHelper'; -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; /** * @internal @@ -68,7 +68,7 @@ export class MusicXmlImporterTestHelper { if (render) { settings.display.justifyLastSystem = score.masterBars.length > 4; if (score.tracks.some(t => t.systemsLayout.length > 0)) { - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; } prepare?.(settings); diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap index e6b631bce..b70ee1495 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap @@ -313,7 +313,7 @@ Map { "beats" => Array [ Map { "__kind" => "Beat", - "id" => 0, + "id" => 1, "isempty" => true, "automations" => Array [ Map { @@ -330,6 +330,19 @@ Map { }, ], }, + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, ], }, ], @@ -758,7 +771,7 @@ Map { "beats" => Array [ Map { "__kind" => "Beat", - "id" => 0, + "id" => 1, "isempty" => true, "automations" => Array [ Map { @@ -775,6 +788,19 @@ Map { }, ], }, + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, ], }, ], @@ -1205,7 +1231,7 @@ Map { "beats" => Array [ Map { "__kind" => "Beat", - "id" => 0, + "id" => 1, "isempty" => true, "automations" => Array [ Map { @@ -1222,6 +1248,19 @@ Map { }, ], }, + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, ], }, ], @@ -1367,7 +1406,7 @@ Map { "beats" => Array [ Map { "__kind" => "Beat", - "id" => 0, + "id" => 1, "isempty" => true, "automations" => Array [ Map { @@ -1384,6 +1423,19 @@ Map { }, ], }, + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, ], }, ], @@ -1527,6 +1579,7 @@ Map { ], }, ], + "showtablature" => false, }, Map { "__kind" => "Staff", @@ -1729,6 +1782,7 @@ Map { ], }, ], + "showtablature" => false, }, Map { "__kind" => "Staff", @@ -2006,6 +2060,7 @@ Map { ], }, ], + "showtablature" => false, }, Map { "__kind" => "Staff", @@ -2208,6 +2263,7 @@ Map { ], }, ], + "showtablature" => false, }, Map { "__kind" => "Staff", @@ -2219,6 +2275,19 @@ Map { Map { "__kind" => "Voice", "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 4, "beats" => Array [ Map { "__kind" => "Beat", @@ -2237,11 +2306,24 @@ Map { "voices" => Array [ Map { "__kind" => "Voice", - "id" => 4, + "id" => 5, "beats" => Array [ Map { "__kind" => "Beat", - "id" => 4, + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, "isempty" => true, "displayduration" => 960, "playbackduration" => 960, @@ -2256,11 +2338,24 @@ Map { "voices" => Array [ Map { "__kind" => "Voice", - "id" => 5, + "id" => 7, "beats" => Array [ Map { "__kind" => "Beat", - "id" => 5, + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, "isempty" => true, "displayduration" => 960, "playbackduration" => 960, @@ -2410,6 +2505,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -2634,6 +2730,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -2858,6 +2955,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -3157,6 +3255,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -3381,6 +3480,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -3404,7 +3504,7 @@ Map { "beats" => Array [ Map { "__kind" => "Beat", - "id" => 2, + "id" => 4, "isempty" => true, "automations" => Array [ Map { @@ -3421,6 +3521,19 @@ Map { }, ], }, + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, ], }, Map { @@ -3429,11 +3542,24 @@ Map { "voices" => Array [ Map { "__kind" => "Voice", - "id" => 4, + "id" => 5, "beats" => Array [ Map { "__kind" => "Beat", - "id" => 4, + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, "isempty" => true, "displayduration" => 960, "playbackduration" => 960, @@ -3448,11 +3574,24 @@ Map { "voices" => Array [ Map { "__kind" => "Voice", - "id" => 5, + "id" => 7, "beats" => Array [ Map { "__kind" => "Beat", - "id" => 5, + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, "isempty" => true, "displayduration" => 960, "playbackduration" => 960, @@ -3605,6 +3744,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -3926,6 +4066,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -4250,6 +4391,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -4394,7 +4536,7 @@ Map { "beats" => Array [ Map { "__kind" => "Beat", - "id" => 0, + "id" => 1, "isempty" => true, "automations" => Array [ Map { @@ -4411,6 +4553,19 @@ Map { }, ], }, + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, ], }, ], @@ -4595,6 +4750,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -4800,6 +4956,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -4893,6 +5050,7 @@ Map { "barlineright" => 4, }, ], + "showtablature" => false, }, Map { "__kind" => "Staff", @@ -4940,6 +5098,7 @@ Map { "barlineright" => 1, }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -4952,67 +5111,715 @@ Map { } `; -exports[`AlphaTexImporterTest errors at209 accidentalmode: lexer-diagnostics 1`] = `Array []`; - -exports[`AlphaTexImporterTest errors at209 accidentalmode: parser-diagnostics 1`] = `Array []`; - -exports[`AlphaTexImporterTest errors at209 accidentalmode: semantic-diagnostics 1`] = ` -Array [ - Map { - "code" => 209, - "severity" => 2, - "message" => "Unexpected accidental mode value 'invalid', expected: auto,explicit", - "start" => Map { - "col" => 21, - "line" => 1, - "offset" => 20, +exports[`AlphaTexImporterTest custom-beaming 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "beamingrules" => Map { + "groups" => Map { + "8" => Array [ + 2, + 2, + 2, + 2, + ], + }, + }, + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], }, - }, -] -`; - -exports[`AlphaTexImporterTest errors at209 articulation: lexer-diagnostics 1`] = `Array []`; - -exports[`AlphaTexImporterTest errors at209 articulation: parser-diagnostics 1`] = `Array []`; - -exports[`AlphaTexImporterTest errors at209 articulation: semantic-diagnostics 1`] = ` -Array [ - Map { - "code" => 209, - "severity" => 2, - "message" => "Unexpected articulation value '0', expected: 38,37,91,42,92,46,44,35,36,50,48,47,45,43,93,51,53,94,55,95,52,96,49,97,57,98,99,100,56,101,102,103,77,76,60,104,105,61,106,107,66,65,68,67,64,108,109,63,110,62,72,71,73,74,86,87,54,111,112,113,79,78,58,81,80,114,115,116,69,117,85,75,70,118,119,120,82,122,84,123,83,124,125,39,40,31,41,59,126,127,29,30,33,34", - "start" => Map { - "col" => 23, - "line" => 1, - "offset" => 22, + Map { + "__kind" => "MasterBar", + "start" => 3840, }, - }, -] -`; - -exports[`AlphaTexImporterTest errors at209 bar optional: lexer-diagnostics 1`] = `Array []`; - -exports[`AlphaTexImporterTest errors at209 bar optional: parser-diagnostics 1`] = `Array []`; - -exports[`AlphaTexImporterTest errors at209 bar optional: semantic-diagnostics 1`] = ` -Array [ - Map { - "code" => 209, - "severity" => 2, - "message" => "Unexpected additional value 'Number', expected: required(String|Ident),optional(String|Ident)", - "start" => Map { - "col" => 18, - "line" => 1, - "offset" => 17, + Map { + "__kind" => "MasterBar", + "beamingrules" => Map { + "groups" => Map { + "8" => Array [ + 4, + 4, + ], + }, + }, + "start" => 7680, }, - }, -] -`; - -exports[`AlphaTexImporterTest errors at209 bar required: lexer-diagnostics 1`] = `Array []`; - -exports[`AlphaTexImporterTest errors at209 bar required: parser-diagnostics 1`] = `Array []`; - + ], + "tracks" => Array [ + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 0, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 0, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 480, + "playbackstart" => 480, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 2, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 2, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 3, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 3, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 1440, + "playbackstart" => 1440, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 4, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 4, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 5, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 5, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 2400, + "playbackstart" => 2400, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 6, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 6, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 2880, + "playbackstart" => 2880, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 7, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 7, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 3360, + "playbackstart" => 3360, + "displayduration" => 480, + "playbackduration" => 480, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 8, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 9, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 9, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 480, + "playbackstart" => 480, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 10, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 10, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 11, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 11, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 1440, + "playbackstart" => 1440, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 12, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 12, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 13, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 13, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 2400, + "playbackstart" => 2400, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 14, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 14, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 2880, + "playbackstart" => 2880, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 15, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 15, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 3360, + "playbackstart" => 3360, + "displayduration" => 480, + "playbackduration" => 480, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 16, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 16, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 17, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 17, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 480, + "playbackstart" => 480, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 18, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 18, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 19, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 19, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 1440, + "playbackstart" => 1440, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 20, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 20, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 21, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 21, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 2400, + "playbackstart" => 2400, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 22, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 22, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 2880, + "playbackstart" => 2880, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 23, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 23, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 3360, + "playbackstart" => 3360, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 24, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 24, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 3840, + "playbackstart" => 3840, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 25, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 25, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 4320, + "playbackstart" => 4320, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 26, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 26, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 4800, + "playbackstart" => 4800, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 27, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 27, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 5280, + "playbackstart" => 5280, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 28, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 28, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 5760, + "playbackstart" => 5760, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 29, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 29, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 6240, + "playbackstart" => 6240, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 30, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 30, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 6720, + "playbackstart" => 6720, + "displayduration" => 480, + "playbackduration" => 480, + }, + Map { + "__kind" => "Beat", + "id" => 31, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 31, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 7200, + "playbackstart" => 7200, + "displayduration" => 480, + "playbackduration" => 480, + }, + ], + }, + ], + }, + ], + "showtablature" => false, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest errors at209 accidentalmode: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 accidentalmode: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 accidentalmode: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected accidental mode value 'invalid', expected: auto,explicit", + "start" => Map { + "col" => 21, + "line" => 1, + "offset" => 20, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 articulation: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 articulation: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 articulation: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected articulation value '0', expected: 38,37,91,42,92,46,44,35,36,50,48,47,45,43,93,51,53,94,55,95,52,96,49,97,57,98,99,100,56,101,102,103,77,76,60,104,105,61,106,107,66,65,68,67,64,108,109,63,110,62,72,71,73,74,86,87,54,111,112,113,79,78,58,81,80,114,115,116,69,117,85,75,70,118,119,120,82,122,84,123,83,124,125,39,40,31,41,59,126,127,29,30,33,34", + "start" => Map { + "col" => 23, + "line" => 1, + "offset" => 22, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 bar optional: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bar optional: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bar optional: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected additional value 'Number', expected: required(String|Ident),optional(String|Ident)", + "start" => Map { + "col" => 18, + "line" => 1, + "offset" => 17, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 bar required: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bar required: parser-diagnostics 1`] = `Array []`; + exports[`AlphaTexImporterTest errors at209 bar required: semantic-diagnostics 1`] = ` Array [ Map { @@ -5447,7 +6254,7 @@ Array [ Map { "code" => 209, "severity" => 2, - "message" => "Unexpected percussion articulation value 'invalid', expected: oneOf(ride (choke),ridechoke,cymbal (hit),cymbalhit,snare (side stick),snaresidestick,snare (side stick) 2,snaresidestick2,snare (hit),snarehit,kick (hit),kickhit,kick (hit) 2,kickhit2,snare (side stick) 3,snaresidestick3,snare (hit) 2,snarehit2,hand clap (hit),handclaphit,snare (hit) 3,snarehit3,low floor tom (hit),lowfloortomhit,hi-hat (closed),hihatclosed,very low tom (hit),verylowtomhit,pedal hi-hat (hit),pedalhihathit,low tom (hit),lowtomhit,hi-hat (open),hihatopen,mid tom (hit),midtomhit,high tom (hit),hightomhit,crash high (hit),crashhighhit,high floor tom (hit),highfloortomhit,ride (middle),ridemiddle,china (hit),chinahit,ride (bell),ridebell,tambourine (hit),tambourinehit,splash (hit),splashhit,cowbell medium (hit),cowbellmediumhit,crash medium (hit),crashmediumhit,vibraslap (hit),vibraslaphit,ride (edge),rideedge,hand (hit),handhit,bongo high (hit),bongohighhit,bongo low (hit),bongolowhit,conga high (mute),congahighmute,conga high (hit),congahighhit,conga low (hit),congalowhit,timbale high (hit),timbalehighhit,timbale low (hit),timbalelowhit,agogo high (hit),agogohighhit,agogo tow (hit),agogotowhit,cabasa (hit),cabasahit,left maraca (hit),leftmaracahit,whistle high (hit),whistlehighhit,whistle low (hit),whistlelowhit,guiro (hit),guirohit,guiro (scrap-return),guiroscrapreturn,claves (hit),claveshit,woodblock high (hit),woodblockhighhit,woodblock low (hit),woodblocklowhit,cuica (mute),cuicamute,cuica (open),cuicaopen,triangle (rnute),trianglernute,triangle (hit),trianglehit,shaker (hit),shakerhit,tinkle bell (hat),tinklebellhat,jingle bell (hit),jinglebellhit,bell tree (hit),belltreehit,castanets (hit),castanetshit,surdo (hit),surdohit,surdo (mute),surdomute,snare (rim shot),snarerimshot,hi-hat (half),hihathalf,ride (edge) 2,rideedge2,ride (choke) 2,ridechoke2,splash (choke),splashchoke,china (choke),chinachoke,crash high (choke),crashhighchoke,crash medium (choke),crashmediumchoke,cowbell low (hit),cowbelllowhit,cowbell low (tip),cowbelllowtip,cowbell medium (tip),cowbellmediumtip,cowbell high (hit),cowbellhighhit,cowbell high (tip),cowbellhightip,hand (mute),handmute,hand (slap),handslap,hand (mute) 2,handmute2,hand (slap) 2,handslap2,conga low (slap),congalowslap,conga low (mute),congalowmute,conga high (slap),congahighslap,tambourine (return),tambourinereturn,tambourine (roll),tambourineroll,tambourine (hand),tambourinehand,grancassa (hit),grancassahit,piatti (hat),piattihat,piatti (hand),piattihand,cabasa (return),cabasareturn,left maraca (return),leftmaracareturn,right maraca (hit),rightmaracahit,right maraca (return),rightmaracareturn,shaker (return),shakerreturn,bell tee (return),bellteereturn,golpe (thumb),golpethumb,golpe (finger),golpefinger,ride (middle) 2,ridemiddle2,ride (bell) 2,ridebell2).", + "message" => "Unexpected percussion articulation value 'invalid', expected: oneOf(hand (hit),handhit,cymbal (hit),cymbalhit,snare (side stick) 3,snaresidestick3,snare (hit) 2,snarehit2,agogo tow (hit),agogotowhit,triangle (rnute),trianglernute,hand (mute),handmute,hand (slap),handslap,hand (mute) 2,handmute2,hand (slap) 2,handslap2,piatti (hat),piattihat,bell tee (return),bellteereturn,snare (hit),snarehit,snare (side stick),snaresidestick,snare (rim shot),snarerimshot,hi-hat (closed),hihatclosed,hi-hat (half),hihathalf,hi-hat (open),hihatopen,pedal hi-hat (hit),pedalhihathit,kick (hit),kickhit,kick (hit) 2,kickhit2,high floor tom (hit),highfloortomhit,high tom (hit),hightomhit,mid tom (hit),midtomhit,low tom (hit),lowtomhit,very low tom (hit),verylowtomhit,ride (edge),rideedge,ride (middle),ridemiddle,ride (bell),ridebell,ride (choke),ridechoke,splash (hit),splashhit,splash (choke),splashchoke,china (hit),chinahit,china (choke),chinachoke,crash high (hit),crashhighhit,crash high (choke),crashhighchoke,crash medium (hit),crashmediumhit,crash medium (choke),crashmediumchoke,cowbell low (hit),cowbelllowhit,cowbell low (tip),cowbelllowtip,cowbell medium (hit),cowbellmediumhit,cowbell medium (tip),cowbellmediumtip,cowbell high (hit),cowbellhighhit,cowbell high (tip),cowbellhightip,woodblock low (hit),woodblocklowhit,woodblock high (hit),woodblockhighhit,bongo high (hit),bongohighhit,bongo high (mute),bongohighmute,bongo high (slap),bongohighslap,bongo low (hit),bongolowhit,bongo low (mute),bongolowmute,bongo low (slap),bongolowslap,timbale low (hit),timbalelowhit,timbale high (hit),timbalehighhit,agogo low (hit),agogolowhit,agogo high (hit),agogohighhit,conga low (hit),congalowhit,conga low (slap),congalowslap,conga low (mute),congalowmute,conga high (hit),congahighhit,conga high (slap),congahighslap,conga high (mute),congahighmute,whistle low (hit),whistlelowhit,whistle high (hit),whistlehighhit,guiro (hit),guirohit,guiro (scrap-return),guiroscrapreturn,surdo (hit),surdohit,surdo (mute),surdomute,tambourine (hit),tambourinehit,tambourine (return),tambourinereturn,tambourine (roll),tambourineroll,tambourine (hand),tambourinehand,cuica (open),cuicaopen,cuica (mute),cuicamute,vibraslap (hit),vibraslaphit,triangle (hit),trianglehit,triangle (mute),trianglemute,grancassa (hit),grancassahit,piatti (hit),piattihit,piatti (hand),piattihand,cabasa (hit),cabasahit,cabasa (return),cabasareturn,castanets (hit),castanetshit,claves (hit),claveshit,left maraca (hit),leftmaracahit,left maraca (return),leftmaracareturn,right maraca (hit),rightmaracahit,right maraca (return),rightmaracareturn,shaker (hit),shakerhit,shaker (return),shakerreturn,bell tree (hit),belltreehit,bell tree (return),belltreereturn,jingle bell (hit),jinglebellhit,tinkle bell (hit),tinklebellhit,golpe (thumb),golpethumb,golpe (finger),golpefinger,hand clap (hit),handclaphit,electric snare (hit),electricsnarehit,snare (side stick) 2,snaresidestick2,low floor tom (hit),lowfloortomhit,ride (edge) 2,rideedge2,ride (middle) 2,ridemiddle2,ride (bell) 2,ridebell2,ride (choke) 2,ridechoke2,reverse cymbal (hit),reversecymbalhit,metronome (hit),metronomehit,metronome (bell),metronomebell).", "start" => Map { "col" => 25, "line" => 1, @@ -5466,11 +6273,11 @@ Array [ Map { "code" => 209, "severity" => 2, - "message" => "Unexpected tremolo speed value '0, expected: 8, 16 or 32", + "message" => "Unexpected tremolo marks value '10, expected: 0-5, or legacy: 8, 16 or 32", "start" => Map { - "col" => 11, + "col" => 12, "line" => 1, - "offset" => 10, + "offset" => 11, }, }, ] @@ -6143,191 +6950,749 @@ Map { }, Map { "__kind" => "MasterBar", - "syncpoints" => Array [ + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 2000, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "isrepeatstart" => true, + "start" => 7680, + }, + Map { + "__kind" => "MasterBar", + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 3000, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 1, + "millisecondoffset" => 4000, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 11520, + }, + Map { + "__kind" => "MasterBar", + "repeatcount" => 2, + "start" => 15360, + }, + Map { + "__kind" => "MasterBar", + "start" => 19200, + }, + Map { + "__kind" => "MasterBar", + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 1, + "millisecondoffset" => 5000, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 23040, + }, + ], +} +`; + +exports[`AlphaTexImporterTest sync-expect-dot 1`] = ` +Map { + "__kind" => "Score", + "artist" => "J.S. Bach (1685-1750)", + "copyright" => "Public Domain", + "title" => "Prelude in D Minor", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 80, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 0, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 1500, + }, + "ratioposition" => 0.666, + "text" => "", + "isvisible" => true, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 4075, + }, + "ratioposition" => 0.666, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 2880, + }, + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 6475, + }, + "ratioposition" => 0.333, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 5760, + }, + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 10223, + }, + "ratioposition" => 1, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 8640, + }, + ], +} +`; + +exports[`AlphaTexImporterTest tremolos buzzroll-default1 1`] = ` +Map { + "marks" => 1, + "style" => 1, +} +`; + +exports[`AlphaTexImporterTest tremolos buzzroll-default2 1`] = ` +Map { + "marks" => 2, + "style" => 1, +} +`; + +exports[`AlphaTexImporterTest tremolos buzzroll-default3 1`] = ` +Map { + "marks" => 3, + "style" => 1, +} +`; + +exports[`AlphaTexImporterTest tremolos buzzroll-default4 1`] = ` +Map { + "marks" => 4, + "style" => 1, +} +`; + +exports[`AlphaTexImporterTest tremolos buzzroll-default5 1`] = ` +Map { + "marks" => 5, + "style" => 1, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo-default1 1`] = ` +Map { + "marks" => 1, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo-default2 1`] = ` +Map { + "marks" => 2, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo-default3 1`] = ` +Map { + "marks" => 3, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo-default4 1`] = ` +Map { + "marks" => 4, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo-default5 1`] = ` +Map { + "marks" => 5, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo1 1`] = ` +Map { + "marks" => 1, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo2 1`] = ` +Map { + "marks" => 2, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo3 1`] = ` +Map { + "marks" => 3, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo4 1`] = ` +Map { + "marks" => 4, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo5 1`] = ` +Map { + "marks" => 5, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo8 1`] = ` +Map { + "marks" => 1, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo16 1`] = ` +Map { + "marks" => 2, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo32 1`] = ` +Map { + "marks" => 3, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest voice-mode barWise 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ Map { "islinear" => false, - "type" => 4, - "value" => 0, - "syncpointvalue" => Map { - "baroccurence" => 0, - "millisecondoffset" => 2000, - }, + "type" => 0, + "value" => 120, "ratioposition" => 0, "text" => "", - "isvisible" => true, + "isvisible" => false, }, ], - "start" => 3840, }, Map { "__kind" => "MasterBar", - "isrepeatstart" => true, - "start" => 7680, + "start" => 3840, }, + ], + "tracks" => Array [ Map { - "__kind" => "MasterBar", - "syncpoints" => Array [ + "__kind" => "Track", + "staves" => Array [ Map { - "islinear" => false, - "type" => 4, - "value" => 0, - "syncpointvalue" => Map { - "baroccurence" => 0, - "millisecondoffset" => 3000, - }, - "ratioposition" => 0, - "text" => "", - "isvisible" => true, + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 0, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 0, + "octave" => 5, + "tone" => 0, + }, + ], + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 4, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 2, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 3, + "octave" => 5, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "showtablature" => false, }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest voice-mode default 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ Map { "islinear" => false, - "type" => 4, - "value" => 0, - "syncpointvalue" => Map { - "baroccurence" => 1, - "millisecondoffset" => 4000, - }, + "type" => 0, + "value" => 120, "ratioposition" => 0, "text" => "", - "isvisible" => true, + "isvisible" => false, }, ], - "start" => 11520, - }, - Map { - "__kind" => "MasterBar", - "repeatcount" => 2, - "start" => 15360, }, Map { "__kind" => "MasterBar", - "start" => 19200, + "start" => 3840, }, + ], + "tracks" => Array [ Map { - "__kind" => "MasterBar", - "syncpoints" => Array [ + "__kind" => "Track", + "staves" => Array [ Map { - "islinear" => false, - "type" => 4, - "value" => 0, - "syncpointvalue" => Map { - "baroccurence" => 1, - "millisecondoffset" => 5000, - }, - "ratioposition" => 0, - "text" => "", - "isvisible" => true, + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 0, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 0, + "octave" => 5, + "tone" => 0, + }, + ], + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 2, + "octave" => 4, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 3, + "octave" => 5, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "showtablature" => false, }, ], - "start" => 23040, + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, }, ], } `; -exports[`AlphaTexImporterTest sync-expect-dot 1`] = ` +exports[`AlphaTexImporterTest voice-mode staffWise 1`] = ` Map { "__kind" => "Score", - "artist" => "J.S. Bach (1685-1750)", - "copyright" => "Public Domain", - "title" => "Prelude in D Minor", "masterbars" => Array [ Map { "__kind" => "MasterBar", - "timesignaturenumerator" => 3, "tempoautomations" => Array [ Map { "islinear" => false, "type" => 0, - "value" => 80, - "ratioposition" => 0, - "text" => "", - "isvisible" => true, - }, - ], - "syncpoints" => Array [ - Map { - "islinear" => false, - "type" => 4, - "value" => 0, - "syncpointvalue" => Map { - "baroccurence" => 0, - "millisecondoffset" => 0, - }, + "value" => 120, "ratioposition" => 0, "text" => "", - "isvisible" => true, - }, - Map { - "islinear" => false, - "type" => 4, - "value" => 0, - "syncpointvalue" => Map { - "baroccurence" => 0, - "millisecondoffset" => 1500, - }, - "ratioposition" => 0.666, - "text" => "", - "isvisible" => true, - }, - ], - }, - Map { - "__kind" => "MasterBar", - "timesignaturenumerator" => 3, - "syncpoints" => Array [ - Map { - "islinear" => false, - "type" => 4, - "value" => 0, - "syncpointvalue" => Map { - "baroccurence" => 0, - "millisecondoffset" => 4075, - }, - "ratioposition" => 0.666, - "text" => "", - "isvisible" => true, + "isvisible" => false, }, ], - "start" => 2880, }, Map { "__kind" => "MasterBar", - "timesignaturenumerator" => 3, - "syncpoints" => Array [ - Map { - "islinear" => false, - "type" => 4, - "value" => 0, - "syncpointvalue" => Map { - "baroccurence" => 0, - "millisecondoffset" => 6475, - }, - "ratioposition" => 0.333, - "text" => "", - "isvisible" => true, - }, - ], - "start" => 5760, + "start" => 3840, }, + ], + "tracks" => Array [ Map { - "__kind" => "MasterBar", - "timesignaturenumerator" => 3, - "syncpoints" => Array [ + "__kind" => "Track", + "staves" => Array [ Map { - "islinear" => false, - "type" => 4, - "value" => 0, - "syncpointvalue" => Map { - "baroccurence" => 0, - "millisecondoffset" => 10223, - }, - "ratioposition" => 1, - "text" => "", - "isvisible" => true, + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 0, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 0, + "octave" => 5, + "tone" => 0, + }, + ], + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 2, + "octave" => 4, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 3, + "octave" => 5, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "showtablature" => false, }, ], - "start" => 8640, + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, }, ], } diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporterOld.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporterOld.test.ts.snap index 64ad3244a..eab636b11 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporterOld.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporterOld.test.ts.snap @@ -82,6 +82,7 @@ Map { "barlineright" => 4, }, ], + "showtablature" => false, }, Map { "__kind" => "Staff", @@ -129,6 +130,7 @@ Map { "barlineright" => 1, }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap index fbeda6a89..58e202661 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap @@ -1,28 +1,31 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`AlphaTexParameterTests handler-validation metadata empty signature empty 1`] = ` -Score (1,1) -> (1,9) { +Score (1,1) -> (1,12) { bars: Array [ - Bar (1,1) -> (1,9) { + Bar (1,1) -> (1,12) { metaData: Array [ - Meta (1,1) -> (1,6) { - tag: Tag "ac" (1,1) -> (1,4) { + Meta (1,1) -> (1,9) { + tag: Tag "track" (1,1) -> (1,7) { prefix: Backslash (1,1) -> (1,2), - tag: Ident "ac" (1,2) -> (1,4), + tag: Ident "track" (1,2) -> (1,7), }, - arguments: Arguments (1,4) -> (1,6) { - openParenthesis: LParen (1,4) -> (1,5), + arguments: Arguments (1,7) -> (1,9) { + openParenthesis: LParen (1,7) -> (1,8), arguments: Array [], - closeParenthesis: RParen (1,5) -> (1,6), + closeParenthesis: RParen (1,8) -> (1,9), + signatureCandidateIndices: Array [ + 0, + ], }, }, ], beats: Array [ - Beat (1,7) -> (1,9) { - notes: NoteList (1,7) -> (1,9) { + Beat (1,10) -> (1,12) { + notes: NoteList (1,10) -> (1,12) { notes: Array [ - Note (1,7) -> (1,9) { - noteValue: Ident "C4" (1,7) -> (1,9), + Note (1,10) -> (1,12) { + noteValue: Ident "C4" (1,10) -> (1,12), }, ], }, diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap index f381b08fa..101ce7eb2 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap @@ -64,6 +64,43 @@ exports[`AlphaTexParserTest ambiguous tempo, temponame and stringed note: lexer- exports[`AlphaTexParserTest ambiguous tempo, temponame and stringed note: parser-diagnostics 1`] = `Array []`; +exports[`AlphaTexParserTest ambiguous voice followed by note list 1`] = ` +Score (1,1) -> (1,15) { + bars: Array [ + Bar (1,1) -> (1,15) { + metaData: Array [ + Meta (1,1) -> (1,7) { + tag: Tag "voice" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "voice" (1,2) -> (1,7), + }, + }, + ], + beats: Array [ + Beat (1,8) -> (1,15) { + notes: NoteList (1,8) -> (1,15) { + openParenthesis: LParen (1,8) -> (1,9), + notes: Array [ + Note (1,9) -> (1,11) { + noteValue: Ident "C4" (1,9) -> (1,11), + }, + Note (1,12) -> (1,14) { + noteValue: Ident "C5" (1,12) -> (1,14), + }, + ], + closeParenthesis: RParen (1,14) -> (1,15), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest ambiguous voice followed by note list: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest ambiguous voice followed by note list: parser-diagnostics 1`] = `Array []`; + exports[`AlphaTexParserTest comments bar meta singleline 1`] = ` Score (1,1) -> (3,9) { bars: Array [ @@ -6370,18 +6407,18 @@ Score (1,1) -> (1,30) { `; exports[`AlphaTexParserTest valid-score-metadata known property before value 1`] = ` -Score (1,1) -> (1,30) { +Score (1,1) -> (1,32) { bars: Array [ - Bar (1,1) -> (1,30) { + Bar (1,1) -> (1,32) { metaData: Array [ - Meta (1,1) -> (1,27) { - tag: Tag "track" (1,1) -> (1,7) { + Meta (1,1) -> (1,29) { + tag: Tag "chord" (1,1) -> (1,7) { prefix: Backslash (1,1) -> (1,2), - tag: Ident "track" (1,2) -> (1,7), + tag: Ident "chord" (1,2) -> (1,7), }, - arguments: Arguments (1,22) -> (1,27) { + arguments: Arguments (1,24) -> (1,29) { arguments: Array [ - String "Name" (1,22) -> (1,27) { + String "Name" (1,24) -> (1,29) { parameterIndices: Map { 0 => 0, }, @@ -6392,27 +6429,14 @@ Score (1,1) -> (1,30) { ], validated: true, }, - properties: Props (1,8) -> (1,21) { + properties: Props (1,8) -> (1,23) { openBrace: LBrace (1,8) -> (1,9), properties: Array [ - Prop (1,9) -> (1,19) { - property: Ident "color" (1,9) -> (1,14), - properties: Arguments (1,15) -> (1,19) { - arguments: Array [ - String "red" (1,15) -> (1,19) { - parameterIndices: Map { - 0 => 0, - }, - }, - ], - signatureCandidateIndices: Array [ - 0, - ], - validated: true, - }, + Prop (1,9) -> (1,22) { + property: Ident "showFingering" (1,9) -> (1,22), }, ], - closeBrace: RBrace (1,20) -> (1,21), + closeBrace: RBrace (1,22) -> (1,23), }, }, ], @@ -6501,6 +6525,58 @@ exports[`AlphaTexParserTest valid-score-metadata known valuelist: lexer-diagnost exports[`AlphaTexParserTest valid-score-metadata known valuelist: parser-diagnostics 1`] = `Array []`; +exports[`AlphaTexParserTest valid-score-metadata notelist after props 1`] = ` +Score (1,1) -> (1,28) { + bars: Array [ + Bar (1,1) -> (1,28) { + metaData: Array [ + Meta (1,1) -> (1,16) { + tag: Tag "staff" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "staff" (1,2) -> (1,7), + }, + properties: Props (1,8) -> (1,16) { + openBrace: LBrace (1,8) -> (1,9), + properties: Array [ + Prop (1,10) -> (1,14) { + property: Ident "tabs" (1,10) -> (1,14), + }, + ], + closeBrace: RBrace (1,15) -> (1,16), + }, + }, + ], + beats: Array [ + Beat (1,17) -> (1,28) { + notes: NoteList (1,17) -> (1,26) { + openParenthesis: LParen (1,17) -> (1,18), + notes: Array [ + Note (1,18) -> (1,21) { + noteValue: Number "3" (1,18) -> (1,19), + noteStringDot: Dot (1,19) -> (1,20), + noteString: Number "3" (1,20) -> (1,21), + }, + Note (1,22) -> (1,25) { + noteValue: Number "3" (1,22) -> (1,23), + noteStringDot: Dot (1,23) -> (1,24), + noteString: Number "3" (1,24) -> (1,25), + }, + ], + closeParenthesis: RParen (1,25) -> (1,26), + }, + durationDot: Dot (1,26) -> (1,27), + durationValue: Number "2" (1,27) -> (1,28), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-score-metadata notelist after props: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata notelist after props: parser-diagnostics 1`] = `Array []`; + exports[`AlphaTexParserTest valid-score-metadata unknown multiple 1`] = ` Score (1,1) -> (1,43) { bars: Array [ diff --git a/packages/alphatab/test/importer/__snapshots__/Gp8Importer.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/Gp8Importer.test.ts.snap index a2048297d..0fed28a05 100644 --- a/packages/alphatab/test/importer/__snapshots__/Gp8Importer.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/Gp8Importer.test.ts.snap @@ -9,6 +9,16 @@ Map { "masterbars" => Array [ Map { "__kind" => "MasterBar", + "beamingrules" => Map { + "groups" => Map { + "8" => Array [ + 2, + 2, + 2, + 2, + ], + }, + }, "tempoautomations" => Array [ Map { "islinear" => false, @@ -695,6 +705,14 @@ Map { Map { "__kind" => "MasterBar", "timesignaturenumerator" => 6, + "beamingrules" => Map { + "groups" => Map { + "8" => Array [ + 6, + 6, + ], + }, + }, "tempoautomations" => Array [ Map { "islinear" => false, @@ -723,6 +741,16 @@ Map { }, Map { "__kind" => "MasterBar", + "beamingrules" => Map { + "groups" => Map { + "8" => Array [ + 2, + 2, + 2, + 2, + ], + }, + }, "tempoautomations" => Array [ Map { "islinear" => false, @@ -1044,10 +1072,28 @@ Map { Map { "__kind" => "MasterBar", "timesignaturenumerator" => 6, + "beamingrules" => Map { + "groups" => Map { + "8" => Array [ + 6, + 6, + ], + }, + }, "start" => 689280, }, Map { "__kind" => "MasterBar", + "beamingrules" => Map { + "groups" => Map { + "8" => Array [ + 2, + 2, + 2, + 2, + ], + }, + }, "start" => 695040, }, Map { diff --git a/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap index 5b28d6966..b55cd2ecf 100644 --- a/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap @@ -63,6 +63,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -547,6 +548,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -560,6 +562,160 @@ Map { } `; +exports[`MusicXmlImporterTests buzzroll 1`] = ` +Map { + "__kind" => "Score", + "artist" => "Artist", + "copyright" => "Copyright", + "music" => "Music", + "notices" => "Notices", + "title" => "Title", + "words" => "Words", + "tab" => "Tab", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + ], + "tracks" => Array [ + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 0, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 0, + "octave" => 5, + "tone" => 0, + }, + ], + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "tremolopicking" => Map { + "marks" => 0, + "style" => 1, + }, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 5, + "tone" => 0, + }, + ], + "tremolopicking" => Map { + "marks" => 1, + "style" => 0, + }, + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + Map { + "__kind" => "Beat", + "id" => 2, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 2, + "octave" => 5, + "tone" => 0, + }, + ], + "tremolopicking" => Map { + "marks" => 0, + "style" => 1, + }, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + Map { + "__kind" => "Beat", + "id" => 3, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 3, + "octave" => 5, + "tone" => 0, + }, + ], + "tremolopicking" => Map { + "marks" => 1, + "style" => 1, + }, + "displaystart" => 2880, + "playbackstart" => 2880, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + ], + }, + ], + }, + ], + "showtablature" => false, + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "secondarychannel" => 1, + }, + "name" => "Track 1", + "shortname" => "T1", + }, + ], + "stylesheet" => Map { + "hidedynamics" => true, + }, +} +`; + exports[`MusicXmlImporterTests partwise-anacrusis 1`] = ` Map { "__kind" => "Score", @@ -637,6 +793,7 @@ Map { ], }, ], + "barnumberdisplay" => 2, }, Map { "__kind" => "Bar", @@ -694,6 +851,7 @@ Map { ], }, ], + "barnumberdisplay" => 2, }, Map { "__kind" => "Bar", @@ -715,6 +873,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -758,6 +917,7 @@ Map { ], }, ], + "barnumberdisplay" => 2, }, Map { "__kind" => "Bar", @@ -815,6 +975,7 @@ Map { ], }, ], + "barnumberdisplay" => 2, }, Map { "__kind" => "Bar", @@ -836,6 +997,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -962,6 +1124,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -1045,6 +1208,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -1130,6 +1294,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -1347,6 +1512,7 @@ Map { ], }, ], + "showtablature" => false, }, Map { "__kind" => "Staff", @@ -1494,6 +1660,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -1729,6 +1896,7 @@ Map { "keysignature" => 4, }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -1895,6 +2063,7 @@ Map { "keysignature" => 4, }, ], + "showtablature" => false, }, Map { "__kind" => "Staff", @@ -2357,6 +2526,7 @@ Map { "keysignature" => 4, }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -2553,6 +2723,7 @@ Map { ], }, ], + "showtablature" => false, }, Map { "__kind" => "Staff", @@ -2770,6 +2941,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -2863,6 +3035,7 @@ Map { ], }, ], + "barnumberdisplay" => 2, }, Map { "__kind" => "Bar", @@ -2920,6 +3093,7 @@ Map { ], }, ], + "barnumberdisplay" => 2, }, Map { "__kind" => "Bar", @@ -2941,6 +3115,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -2984,6 +3159,7 @@ Map { ], }, ], + "barnumberdisplay" => 2, }, Map { "__kind" => "Bar", @@ -3041,6 +3217,7 @@ Map { ], }, ], + "barnumberdisplay" => 2, }, Map { "__kind" => "Bar", @@ -3062,6 +3239,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -3188,6 +3366,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -3271,6 +3450,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { @@ -3356,6 +3536,7 @@ Map { ], }, ], + "showtablature" => false, }, ], "playbackinfo" => Map { diff --git a/packages/alphatab/test/model/JsonConverter.test.ts b/packages/alphatab/test/model/JsonConverter.test.ts index dcaedd756..f68a942f0 100644 --- a/packages/alphatab/test/model/JsonConverter.test.ts +++ b/packages/alphatab/test/model/JsonConverter.test.ts @@ -1,17 +1,16 @@ -import { LayoutMode } from '@coderline/alphatab/LayoutMode'; -import { LogLevel } from '@coderline/alphatab/LogLevel'; -import { StaveProfile } from '@coderline/alphatab/StaveProfile'; -import { Settings } from '@coderline/alphatab/Settings'; import { SettingsSerializer } from '@coderline/alphatab/generated/SettingsSerializer'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; +import { LayoutMode } from '@coderline/alphatab/LayoutMode'; +import { LogLevel } from '@coderline/alphatab/LogLevel'; import { Color } from '@coderline/alphatab/model/Color'; import { Font, FontStyle } from '@coderline/alphatab/model/Font'; import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; import type { Score } from '@coderline/alphatab/model/Score'; -import { NotationElement, TabRhythmMode, NotationMode, FingeringMode } from '@coderline/alphatab/NotationSettings'; +import { FingeringMode, NotationElement, NotationMode, TabRhythmMode } from '@coderline/alphatab/NotationSettings'; +import { Settings } from '@coderline/alphatab/Settings'; +import { assert, expect } from 'chai'; import { TestPlatform } from 'test/TestPlatform'; import { ComparisonHelpers } from './ComparisonHelpers'; -import { assert, expect } from 'chai'; describe('JsonConverterTest', () => { async function loadScore(name: string): Promise { @@ -110,9 +109,11 @@ describe('JsonConverterTest', () => { expected.display.scale = 10; expected.display.stretchForce = 2; - expected.display.staveProfile = StaveProfile.ScoreTab; expected.display.barCountPerPartial = 14; - expected.display.resources.copyrightFont = new Font('copy', 15, FontStyle.Plain); + expected.display.resources.elementFonts.set( + NotationElement.ScoreCopyright, + new Font('copy', 15, FontStyle.Plain) + ); expected.display.resources.staffLineColor = new Color(255, 0, 0, 100); expected.display.padding = [1, 2, 3, 4]; @@ -165,7 +166,7 @@ describe('JsonConverterTest', () => { raw.set('notationRhythmMode', 'sHoWWITHbArs'); // immutable - raw.set('displayResourcesCopyrightFont', 'italic 18px Roboto'); + raw.set('displayResourcesMainGlyphColor', '#FF0000'); SettingsSerializer.fromJson(settings, raw); @@ -174,9 +175,9 @@ describe('JsonConverterTest', () => { expect(settings.display.layoutMode).to.equal(LayoutMode.Horizontal); expect(settings.display.scale).to.equal(5); expect(settings.notation.rhythmMode).to.equal(TabRhythmMode.ShowWithBars); - expect(settings.display.resources.copyrightFont.families[0]).to.equal('Roboto'); - expect(settings.display.resources.copyrightFont.size).to.equal(18); - expect(settings.display.resources.copyrightFont.style).to.equal(FontStyle.Italic); + expect(settings.display.resources.mainGlyphColor.r).to.equal(255); + expect(settings.display.resources.mainGlyphColor.g).to.equal(0); + expect(settings.display.resources.mainGlyphColor.b).to.equal(0); }); /*@target web*/ @@ -196,7 +197,7 @@ describe('JsonConverterTest', () => { // json_partial_names notationRhythmMode: 'sHoWWITHbArs', // immutable - displayResourcesCopyrightFont: 'italic 18px Roboto' + displayResourcesMainGlyphColor: '#FF0000' }; SettingsSerializer.fromJson(settings, raw); @@ -206,8 +207,8 @@ describe('JsonConverterTest', () => { expect(settings.display.layoutMode).to.equal(LayoutMode.Horizontal); expect(settings.display.scale).to.equal(5); expect(settings.notation.rhythmMode).to.equal(TabRhythmMode.ShowWithBars); - expect(settings.display.resources.copyrightFont.families[0]).to.equal('Roboto'); - expect(settings.display.resources.copyrightFont.size).to.equal(18); - expect(settings.display.resources.copyrightFont.style).to.equal(FontStyle.Italic); + expect(settings.display.resources.mainGlyphColor.r).to.equal(255); + expect(settings.display.resources.mainGlyphColor.g).to.equal(0); + expect(settings.display.resources.mainGlyphColor.b).to.equal(0); }); }); diff --git a/packages/alphatab/test/visualTests/TestUiFacade.ts b/packages/alphatab/test/visualTests/TestUiFacade.ts index 7d9950220..6e9efc966 100644 --- a/packages/alphatab/test/visualTests/TestUiFacade.ts +++ b/packages/alphatab/test/visualTests/TestUiFacade.ts @@ -1,5 +1,10 @@ import type { AlphaTabApiBase } from '@coderline/alphatab/AlphaTabApiBase'; -import { EventEmitter, EventEmitterOfT, type IEventEmitter, type IEventEmitterOfT } from '@coderline/alphatab/EventEmitter'; +import { + EventEmitter, + EventEmitterOfT, + type IEventEmitter, + type IEventEmitterOfT +} from '@coderline/alphatab/EventEmitter'; import { Settings } from '@coderline/alphatab/Settings'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; import { Score } from '@coderline/alphatab/model/Score'; @@ -128,6 +133,10 @@ export class TestUiFacade implements IUiFacade { this.resizeThrottle = 10; } + public stopScrolling(_scrollElement: IContainer): void {} + + public setCanvasOverflow(_canvasElement: IContainer, _overflow: number, _isVertical: boolean): void {} + public initialize(api: AlphaTabApiBase, raw: unknown): void { this._api = api; let settings: Settings; diff --git a/packages/alphatab/test/visualTests/VisualTestHelper.ts b/packages/alphatab/test/visualTests/VisualTestHelper.ts index c3dcc14be..d840d46d7 100644 --- a/packages/alphatab/test/visualTests/VisualTestHelper.ts +++ b/packages/alphatab/test/visualTests/VisualTestHelper.ts @@ -1,18 +1,18 @@ +import * as alphaSkiaModule from '@coderline/alphaskia'; +import { AlphaSkiaCanvas, AlphaSkiaImage } from '@coderline/alphaskia'; +import { AlphaTabApiBase } from '@coderline/alphatab/AlphaTabApiBase'; +import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; +import { Environment } from '@coderline/alphatab/Environment'; +import { AlphaTexImporter } from '@coderline/alphatab/importer/AlphaTexImporter'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; +import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; +import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; import type { Score } from '@coderline/alphatab/model/Score'; +import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; import { Settings } from '@coderline/alphatab/Settings'; import { TestPlatform } from 'test/TestPlatform'; -import { Environment } from '@coderline/alphatab/Environment'; -import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs'; -import { AlphaTexImporter } from '@coderline/alphatab/importer/AlphaTexImporter'; -import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; import { PixelMatch, PixelMatchOptions } from 'test/visualTests/PixelMatch'; -import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; -import { AlphaTabApiBase } from '@coderline/alphatab/AlphaTabApiBase'; import { TestUiFacade } from './TestUiFacade'; -import * as alphaSkiaModule from '@coderline/alphaskia'; -import { AlphaSkiaCanvas, AlphaSkiaImage } from '@coderline/alphaskia'; -import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; /** * @internal @@ -32,22 +32,18 @@ export class VisualTestRun { export class VisualTestOptions { public score: Score; public runs: VisualTestRun[]; - public settings?: Settings; + public settings: Settings; public tracks?: number[]; public tolerancePercent?: number; public prepareFullImage?: (run: VisualTestRun, api: AlphaTabApiBase, fullImage: AlphaSkiaCanvas) => void; - public constructor(score: Score, runs: VisualTestRun[], settings?: Settings) { + public constructor(score: Score, runs: VisualTestRun[], settings: Settings | undefined) { this.score = score; this.runs = runs; - this.settings = settings; + this.settings = settings ?? new Settings(); } public static async file(inputFile: string, runs: VisualTestRun[], settings?: Settings) { - if (!settings) { - settings = new Settings(); - } - const inputFileData = await TestPlatform.loadFile(`test-data/visual-tests/${inputFile}`); const score: Score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); @@ -91,8 +87,17 @@ export class VisualTestHelper { await VisualTestHelper.runVisualTestFull(o); } - public static runVisualTestTex(tex: string, referenceFileName: string, settings?: Settings): Promise { - return VisualTestHelper.runVisualTestFull(VisualTestOptions.tex(tex, referenceFileName, settings)); + public static runVisualTestTex( + tex: string, + referenceFileName: string, + settings?: Settings, + configure?: (o: VisualTestOptions) => void + ): Promise { + const o = VisualTestOptions.tex(tex, referenceFileName, settings); + if (configure) { + configure(o); + } + return VisualTestHelper.runVisualTestFull(o); } public static async runVisualTestFull(options: VisualTestOptions): Promise { @@ -246,20 +251,18 @@ export class VisualTestHelper { Environment.highDpiFactor = 1; // test data is in scale 1 settings.core.enableLazyLoading = false; - settings.display.resources.copyrightFont.families = ['Noto Sans', 'Noto Music', 'Noto Color Emoji']; - settings.display.resources.titleFont.families = ['Noto Serif', 'Noto Music', 'Noto Color Emoji']; - settings.display.resources.subTitleFont.families = ['Noto Serif', 'Noto Music', 'Noto Color Emoji']; - settings.display.resources.wordsFont.families = ['Noto Serif', 'Noto Music', 'Noto Color Emoji']; - settings.display.resources.effectFont.families = ['Noto Serif', 'Noto Music', 'Noto Color Emoji']; - settings.display.resources.timerFont.families = ['Noto Serif', 'Noto Music', 'Noto Color Emoji']; - settings.display.resources.fretboardNumberFont.families = ['Noto Sans', 'Noto Music', 'Noto Color Emoji']; settings.display.resources.tablatureFont.families = ['Noto Sans', 'Noto Music', 'Noto Color Emoji']; settings.display.resources.graceFont.families = ['Noto Sans', 'Noto Music', 'Noto Color Emoji']; - settings.display.resources.barNumberFont.families = ['Noto Sans', 'Noto Music', 'Noto Color Emoji']; - settings.display.resources.markerFont.families = ['Noto Serif', 'Noto Music', 'Noto Color Emoji']; - settings.display.resources.directionsFont.families = ['Noto Serif', 'Noto Music', 'Noto Color Emoji']; settings.display.resources.numberedNotationFont.families = ['Noto Sans', 'Noto Music', 'Noto Color Emoji']; settings.display.resources.numberedNotationGraceFont.families = ['Noto Sans', 'Noto Music', 'Noto Color Emoji']; + + for (const f of settings.display.resources.elementFonts.values()) { + if (f.families.includes('sans-serif')) { + f.families = ['Noto Sans', 'Noto Music', 'Noto Color Emoji']; + } else { + f.families = ['Noto Serif', 'Noto Music', 'Noto Color Emoji']; + } + } } public static async compareVisualResult( @@ -320,7 +323,7 @@ export class VisualTestHelper { let errorMessage = ''; const oldActual = actual; - const tolerancePercent = options.tolerancePercent ?? 1; + const tolerancePercent = options.tolerancePercent ?? 0; if (expected) { const sizeMismatch = expected.width !== actual.width || expected.height !== actual.height; diff --git a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts index 1fd58f3b7..91452df36 100644 --- a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -1,10 +1,10 @@ -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; +import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { BeatBarreEffectInfo } from '@coderline/alphatab/rendering/effects/BeatBarreEffectInfo'; import { Settings } from '@coderline/alphatab/Settings'; +import { expect } from 'chai'; import { TestPlatform } from 'test/TestPlatform'; import { VisualTestHelper, VisualTestOptions, VisualTestRun } from 'test/visualTests/VisualTestHelper'; -import { expect } from 'chai'; describe('EffectsAndAnnotationsTests', () => { it('markers', async () => { @@ -250,7 +250,7 @@ describe('EffectsAndAnnotationsTests', () => { it('rasgueado', async () => { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('effects-and-annotations/rasgueado.gp', settings); }); @@ -305,4 +305,261 @@ describe('EffectsAndAnnotationsTests', () => { 'test-data/visual-tests/effects-and-annotations/legato.png' ); }); + + it('inscore-chord-diagrams', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\chordDiagramsInScore true + \\chord ("E" 0 0 1 2 2 0) + \\chord ("C" 0 1 0 2 3 x) + + (0.1 0.2 1.3 2.4 2.5 0.6){ch "E"} r r r | + (0.1 1.2 0.3 2.4 3.5){ch "C"} r r r | + `, + 'test-data/visual-tests/effects-and-annotations/inscore-chord-diagrams.png' + ); + }); + + describe('tremolo-extended', async () => { + function flagsTex(noteString: number) { + return ['8', '16', '32'] + .map( + tp => + ` + // 1 bar + 3.${noteString}.32 {beam split tp ${tp}} + 3.${noteString}.16 {beam split tp ${tp}} + 3.${noteString}.8 {beam split tp ${tp}} | + + 5.${noteString}.32 {beam split tp ${tp}} + 5.${noteString}.16 {beam split tp ${tp}} + 5.${noteString}.8 {beam split tp ${tp}} | + + -21.${noteString}.32 {beam split tp ${tp}} + -21.${noteString}.16 {beam split tp ${tp}} + -21.${noteString}.8 {beam split tp ${tp}} | + + -19.${noteString}.32 {beam split tp ${tp}} + -19.${noteString}.16 {beam split tp ${tp}} + -19.${noteString}.8 {beam split tp ${tp}} | + + 15.${noteString}.32 {beam split tp ${tp}} + 15.${noteString}.16 {beam split tp ${tp}} + 15.${noteString}.8 {beam split tp ${tp}} | + + 17.${noteString}.32 {beam split tp ${tp}} + 17.${noteString}.16 {beam split tp ${tp}} + 17.${noteString}.8 {beam split tp ${tp}} | + + 39.${noteString}.32 {beam split tp ${tp}} + 39.${noteString}.16 {beam split tp ${tp}} + 39.${noteString}.8 {beam split tp ${tp}} | + + 41.${noteString}.32 {beam split tp ${tp}} + 41.${noteString}.16 {beam split tp ${tp}} + 41.${noteString}.8 {beam split tp ${tp}} | + ` + ) + .join('\n'); + } + + it('flags-mixed', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track {defaultsystemslayout 8} + \\staff {score tabs numbered slash} + ${flagsTex(4)} + `, + 'test-data/visual-tests/effects-and-annotations/tremolo-flags-mixed.png', + undefined, + o => { + o.settings.display.layoutMode = LayoutMode.Parchment; + } + ); + }); + + it('flags-tab', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track {defaultsystemslayout 8} + \\staff {tabs} + ${flagsTex(4)} + `, + 'test-data/visual-tests/effects-and-annotations/tremolo-flags.png', + undefined, + o => { + o.settings.display.layoutMode = LayoutMode.Parchment; + } + ); + }); + + it('flags-tab-bottom', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track {defaultsystemslayout 8} + \\staff {tabs} + ${flagsTex(6)} + `, + 'test-data/visual-tests/effects-and-annotations/tremolo-flags-bottom.png', + undefined, + o => { + o.settings.display.layoutMode = LayoutMode.Parchment; + } + ); + }); + + async function test(tex: string, referenceFileName: string, configure?: (o: VisualTestOptions) => void) { + await VisualTestHelper.runVisualTestTex( + tex, + `test-data/visual-tests/effects-and-annotations/${referenceFileName}.png`, + undefined, + configure + ); + } + + describe('standard', () => { + it('default-flags', async () => + await test( + ` + \\staff {score slash} + C4.8 {tp 1} | C4.32 {tp 1} | + C4.8 {tp 2} | C4.32 {tp 2} | + C4.8 {tp 3} | C4.32 {tp 3} | + C4.8 {tp 4} | C4.32 {tp 4} | + C4.8 {tp 5} | C4.32 {tp 5} + `, + 'tremolo-standard-default-flags' + )); + + it('default-beams', async () => + await test( + ` + \\staff {score slash} + C4.8 {tp 1} C4.8 {tp 1} | C4.32 {tp 1} C4.32 {tp 1} | + C4.8 {tp 2} C4.8 {tp 2} | C4.32 {tp 2} C4.32 {tp 2} | + C4.8 {tp 3} C4.8 {tp 3} | C4.32 {tp 3} C4.32 {tp 3} | + C4.8 {tp 4} C4.8 {tp 4} | C4.32 {tp 4} C4.32 {tp 4} | + C4.8 {tp 5} C4.8 {tp 5} | C4.32 {tp 5} C4.32 {tp 5} + `, + 'tremolo-standard-default-beams' + )); + + it('buzzroll-flags', async () => + await test( + ` + \\staff {score slash} + C4.8 {tp (1 buzzRoll)} | C4.32 {tp (1 buzzRoll)} | + C4.8 {tp (2 buzzRoll)} | C4.32 {tp (2 buzzRoll)} | + C4.8 {tp (3 buzzRoll)} | C4.32 {tp (3 buzzRoll)} | + C4.8 {tp (4 buzzRoll)} | C4.32 {tp (4 buzzRoll)} | + C4.8 {tp (5 buzzRoll)} | C4.32 {tp (5 buzzRoll)} + `, + 'tremolo-standard-buzzroll-flags' + )); + + it('buzzroll-beams', async () => + await test( + ` + \\staff {score slash} + C4.8 {tp (1 buzzRoll)} C4.8 {tp (1 buzzRoll)} | C4.32 {tp (1 buzzRoll)} C4.32 {tp (1 buzzRoll)} | + C4.8 {tp (2 buzzRoll)} C4.8 {tp (2 buzzRoll)} | C4.32 {tp (2 buzzRoll)} C4.32 {tp (2 buzzRoll)} | + C4.8 {tp (3 buzzRoll)} C4.8 {tp (3 buzzRoll)} | C4.32 {tp (3 buzzRoll)} C4.32 {tp (3 buzzRoll)} | + C4.8 {tp (4 buzzRoll)} C4.8 {tp (4 buzzRoll)} | C4.32 {tp (4 buzzRoll)} C4.32 {tp (4 buzzRoll)} | + C4.8 {tp (5 buzzRoll)} C4.8 {tp (5 buzzRoll)} | C4.32 {tp (5 buzzRoll)} C4.32 {tp (5 buzzRoll)} + `, + 'tremolo-standard-buzzroll-beams' + )); + }); + describe('tabs', () => { + it('default-flags', async () => + await test( + ` + \\staff {tabs} + 3.6.8 {tp 1} | 3.6.32 {tp 1} | + 3.6.8 {tp 2} | 3.6.32 {tp 2} | + 3.6.8 {tp 3} | 3.6.32 {tp 3} | + 3.6.8 {tp 4} | 3.6.32 {tp 4} | + 3.6.8 {tp 5} | 3.6.32 {tp 5} + `, + 'tremolo-tabs-default-flags' + )); + + it('default-beams', async () => + await test( + ` + \\staff {tabs} + 3.6.8 {tp 1} 3.6.8 {tp 1} | 3.6.32 {tp 1} 3.6.32 {tp 1} | + 3.6.8 {tp 2} 3.6.8 {tp 2} | 3.6.32 {tp 2} 3.6.32 {tp 2} | + 3.6.8 {tp 3} 3.6.8 {tp 3} | 3.6.32 {tp 3} 3.6.32 {tp 3} | + 3.6.8 {tp 4} 3.6.8 {tp 4} | 3.6.32 {tp 4} 3.6.32 {tp 4} | + 3.6.8 {tp 5} 3.6.8 {tp 5} | 3.6.32 {tp 5} 3.6.32 {tp 5} + `, + 'tremolo-tabs-default-beams' + )); + + it('buzzroll-flags', async () => + await test( + ` + \\staff {tabs} + 3.6.8 {tp (1 buzzRoll)} | 3.6.32 {tp (1 buzzRoll)} | + 3.6.8 {tp (2 buzzRoll)} | 3.6.32 {tp (2 buzzRoll)} | + 3.6.8 {tp (3 buzzRoll)} | 3.6.32 {tp (3 buzzRoll)} | + 3.6.8 {tp (4 buzzRoll)} | 3.6.32 {tp (4 buzzRoll)} | + 3.6.8 {tp (5 buzzRoll)} | 3.6.32 {tp (5 buzzRoll)} + `, + 'tremolo-tabs-buzzroll-flags' + )); + + it('buzzroll-beams', async () => + await test( + ` + \\staff {tabs} + 3.6.8 {tp (1 buzzRoll)} 3.6.8 {tp (1 buzzRoll)} | 3.6.32 {tp (1 buzzRoll)} 3.6.32 {tp (1 buzzRoll)} | + 3.6.8 {tp (2 buzzRoll)} 3.6.8 {tp (2 buzzRoll)} | 3.6.32 {tp (2 buzzRoll)} 3.6.32 {tp (2 buzzRoll)} | + 3.6.8 {tp (3 buzzRoll)} 3.6.8 {tp (3 buzzRoll)} | 3.6.32 {tp (3 buzzRoll)} 3.6.32 {tp (3 buzzRoll)} | + 3.6.8 {tp (4 buzzRoll)} 3.6.8 {tp (4 buzzRoll)} | 3.6.32 {tp (4 buzzRoll)} 3.6.32 {tp (4 buzzRoll)} | + 3.6.8 {tp (5 buzzRoll)} 3.6.8 {tp (5 buzzRoll)} | 3.6.32 {tp (5 buzzRoll)} 3.6.32 {tp (5 buzzRoll)} + `, + 'tremolo-tabs-buzzroll-beams' + )); + }); + }); + + describe('lyrics', () => { + it('single-line', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\lyrics "Do Re Mi Fa" + C4 C4 C4 C4 + `, + `test-data/visual-tests/effects-and-annotations/lyrics-single-line.png` + ); + }); + + it('multi-line', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\lyrics "Do Re Mi Fa" + \\lyrics "Do Mi " + C4 C4 C4 C4 + `, + `test-data/visual-tests/effects-and-annotations/lyrics-multi-line.png` + ); + }); + + it('multi-line-spacing', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\lyrics "Do Re Mi Fa" + \\lyrics "Do Mi " + C4 C4 C4 C4 + `, + `test-data/visual-tests/effects-and-annotations/lyrics-multi-line-spacing.png`, + undefined, + o => { + o.settings.display.lyricLinesPaddingBetween = 20; + } + ); + }); + }); }); diff --git a/packages/alphatab/test/visualTests/features/Layout.test.ts b/packages/alphatab/test/visualTests/features/Layout.test.ts index 8b98912d2..262fa3573 100644 --- a/packages/alphatab/test/visualTests/features/Layout.test.ts +++ b/packages/alphatab/test/visualTests/features/Layout.test.ts @@ -1,4 +1,3 @@ -import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { Settings } from '@coderline/alphatab/Settings'; import { VisualTestHelper, VisualTestOptions, VisualTestRun } from 'test/visualTests/VisualTestHelper'; @@ -62,7 +61,7 @@ describe('LayoutTests', () => { it('brackets-braces-none', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/brackets-braces-none.gp', settings, o => { o.tracks = [0, 1, 2, 3, 4, 5, 6, 7, 8]; }); @@ -70,7 +69,7 @@ describe('LayoutTests', () => { it('brackets-braces-similar', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/brackets-braces-similar.gp', settings, o => { o.tracks = [0, 1, 2, 3, 4, 5, 6, 7, 8]; }); @@ -78,7 +77,7 @@ describe('LayoutTests', () => { it('brackets-braces-staves', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/brackets-braces-staves.gp', settings, o => { o.tracks = [0, 1, 2, 3, 4, 5, 6, 7, 8]; }); @@ -86,7 +85,7 @@ describe('LayoutTests', () => { it('brackets-braces-system-divider', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/system-divider.gp', settings, o => { o.tracks = [0, 1]; }); @@ -94,31 +93,31 @@ describe('LayoutTests', () => { it('track-names-full-name-all', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/track-names-full-name-all.gp', settings); }); it('track-names-full-name-short-name', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/track-names-full-name-short-name.gp', settings); }); it('track-names-full-name-horizontal', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/track-names-full-name-horizontal.gp', settings); }); it('track-names-first-system', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/track-names-first-system.gp', settings); }); it('track-names-all-systems-multi', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTest('layout/track-names-all-systems-multi.gp', settings, o => { o.tracks = [0, 1]; }); @@ -126,7 +125,7 @@ describe('LayoutTests', () => { it('system-layout-tex', async () => { const settings: Settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTestTex( ` \\track { defaultSystemsLayout 3 } @@ -159,4 +158,260 @@ describe('LayoutTests', () => { o.runs[0].referenceFileName = 'test-data/visual-tests/layout/multibar-rest-all-tracks.png'; }); }); + + it('extended-barlines', async () => { + await VisualTestHelper.runVisualTest('layout/extended-barlines.xml', undefined, o => { + o.score.stylesheet.extendBarLines = true; + o.tracks = [0, 1]; + }); + }); + + it('multi-system-slur-scale-down', async () => { + await VisualTestHelper.runVisualTestTex( + ` + C4 {slur S1} + | r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r + A4 {slur S1} + `, + '', + undefined, + o => { + o.score.stylesheet.extendBarLines = true; + o.runs = [ + new VisualTestRun(1300, 'test-data/visual-tests/layout/multi-system-slur-scale-down-0-1300.png'), + new VisualTestRun(600, 'test-data/visual-tests/layout/multi-system-slur-scale-down-1-600.png'), + new VisualTestRun(300, 'test-data/visual-tests/layout/multi-system-slur-scale-down-2-300.png'), + new VisualTestRun(300, 'test-data/visual-tests/layout/multi-system-slur-scale-down-3-700.png') + ]; + } + ); + }); + + it('multi-system-slur-scale-up', async () => { + await VisualTestHelper.runVisualTestTex( + ` + C4 {slur S1} + | r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r + A4 {slur S1} + `, + '', + undefined, + o => { + o.score.stylesheet.extendBarLines = true; + o.runs = [ + new VisualTestRun(600, 'test-data/visual-tests/layout/multi-system-slur-scale-up-0-600.png'), + new VisualTestRun(1300, 'test-data/visual-tests/layout/multi-system-slur-scale-up-1-1300.png'), + new VisualTestRun(700, 'test-data/visual-tests/layout/multi-system-slur-scale-up-2-700.png'), + new VisualTestRun(300, 'test-data/visual-tests/layout/multi-system-slur-scale-up-3-300.png') + ]; + } + ); + }); + + it('hide-empty-staves', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\hideEmptyStaves + \\defaultSystemsLayout 3 + \\multiTrackTrackNamePolicy allSystems + \\track "T1" + C4.4 *4 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + + \\track "T2" + \\clef C3 + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + `, + 'test-data/visual-tests/layout/hide-empty-staves.png', + undefined, + o => { + o.tracks = o.score.tracks.map(t => t.index); + o.settings.display.layoutMode = LayoutMode.Parchment; + } + ); + }); + + it('hide-empty-staves-in-first', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\hideEmptyStaves + \\hideEmptyStavesInFirstSystem + + \\defaultSystemsLayout 3 + \\multiTrackTrackNamePolicy allSystems + \\track "T1" + C4.4 *4 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + + \\track "T2" + \\clef C3 + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + `, + 'test-data/visual-tests/layout/hide-empty-staves-in-first.png', + undefined, + o => { + o.tracks = o.score.tracks.map(t => t.index); + o.settings.display.layoutMode = LayoutMode.Parchment; + } + ); + }); + + it('single-staff-brackets', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\hideEmptyStaves + \\showSingleStaffBrackets + \\defaultSystemsLayout 3 + \\multiTrackTrackNamePolicy allSystems + \\track "T1" + \\staff {score} + C4.4 *4 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + \\staff {score} + \\clef C3 + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + `, + 'test-data/visual-tests/layout/single-staff-brackets-show.png', + undefined, + o => { + o.tracks = o.score.tracks.map(t => t.index); + o.settings.display.layoutMode = LayoutMode.Parchment; + } + ); + + await VisualTestHelper.runVisualTestTex( + ` + \\hideEmptyStaves + \\defaultSystemsLayout 3 + \\multiTrackTrackNamePolicy allSystems + \\track "T1" + \\staff {score} + C4.4 *4 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + \\staff {score} + \\clef C3 + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | + r.1 | C4 | + `, + 'test-data/visual-tests/layout/single-staff-brackets-hide.png', + undefined, + o => { + o.tracks = o.score.tracks.map(t => t.index); + o.settings.display.layoutMode = LayoutMode.Parchment; + } + ); + }); + + describe('barnumberdisplay', () => { + describe('stylesheet', () => { + it('all', async () => + await VisualTestHelper.runVisualTestTex( + ` + \\defaultBarNumberDisplay allBars + C4.1 | C4.1 | C4.1 | + C4.1 | C4.1 | C4.1 + `, + 'test-data/visual-tests/layout/barnumberdisplay-stylesheet-all.png', + undefined, + o => { + o.settings.display.layoutMode = LayoutMode.Parchment; + } + )); + it('first', async () => + await VisualTestHelper.runVisualTestTex( + ` + \\defaultBarNumberDisplay firstOfSystem + C4.1 | C4.1 | C4.1 | + C4.1 | C4.1 | C4.1 + `, + 'test-data/visual-tests/layout/barnumberdisplay-stylesheet-first.png', + undefined, + o => { + o.settings.display.layoutMode = LayoutMode.Parchment; + } + )); + it('hide', async () => + await VisualTestHelper.runVisualTestTex( + ` + \\defaultBarNumberDisplay hide + C4.1 | C4.1 | C4.1 | + C4.1 | C4.1 | C4.1 + `, + 'test-data/visual-tests/layout/barnumberdisplay-stylesheet-hide.png', + undefined, + o => { + o.settings.display.layoutMode = LayoutMode.Parchment; + } + )); + }); + + describe('bar-override', () => { + it('all', async () => + await VisualTestHelper.runVisualTestTex( + ` + \\defaultBarNumberDisplay allBars + C4.1 | \\barNumberDisplay hide C4.1 | C4.1 | + C4.1 | C4.1 | C4.1 + `, + 'test-data/visual-tests/layout/barnumberdisplay-bar-override-all.png', + undefined, + o => { + o.settings.display.layoutMode = LayoutMode.Parchment; + } + )); + it('first', async () => + await VisualTestHelper.runVisualTestTex( + ` + \\defaultBarNumberDisplay firstOfSystem + C4.1 | \\barNumberDisplay allBars C4.1 | C4.1 | + \\barNumberDisplay hide C4.1 | C4.1 | C4.1 + `, + 'test-data/visual-tests/layout/barnumberdisplay-bar-override-first.png', + undefined, + o => { + o.settings.display.layoutMode = LayoutMode.Parchment; + } + )); + it('hide', async () => + await VisualTestHelper.runVisualTestTex( + ` + \\defaultBarNumberDisplay hide + C4.1 | \\barNumberDisplay allBars C4.1 | C4.1 | + \\barNumberDisplay firstOfSystem C4.1 | \\barNumberDisplay firstOfSystem C4.1 | C4.1 + `, + 'test-data/visual-tests/layout/barnumberdisplay-bar-override-hide.png', + undefined, + o => { + o.settings.display.layoutMode = LayoutMode.Parchment; + } + )); + }); + }); }); diff --git a/packages/alphatab/test/visualTests/features/MultiVoice.test.ts b/packages/alphatab/test/visualTests/features/MultiVoice.test.ts new file mode 100644 index 000000000..2c21ece9b --- /dev/null +++ b/packages/alphatab/test/visualTests/features/MultiVoice.test.ts @@ -0,0 +1,726 @@ +import { LayoutMode } from '@coderline/alphatab/LayoutMode'; +import { Settings } from '@coderline/alphatab/Settings'; +import { TestPlatform } from 'test/TestPlatform'; +import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; + +describe('MultiVoiceTests', () => { + describe('displace', async () => { + async function test(tex: string) { + const settings = new Settings(); + settings.display.justifyLastSystem = true; + settings.display.layoutMode = LayoutMode.Parchment; + + const fileName = TestPlatform.currentTestName.replaceAll(':', '_').replaceAll(',', '').replaceAll(' ', '_'); + await VisualTestHelper.runVisualTestTex( + ` + \\track {defaultSystemsLayout 1} + \\staff + \\voiceMode barWise + ${tex} + `, + `test-data/visual-tests/multivoice/${fileName}.png`, + settings, + o => { + o.runs[0].width = 600; + } + ); + } + + // v1 Quarter Single-v2 Quarter Single + it('v1 Quarter Single-v2 Quarter Single-Automatic Stem', async () => + await test( + ` + \\voice + E5*5 + \\voice + C5 D5 E5 F5 G5 + ` + )); + it('v1 Quarter Single-v2 Quarter Single-Reversed Stem', async () => + await test( + ` + \\voice + E5{beam down}*5 + \\voice + C5{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + it('v1 Quarter Single-v2 Quarter Single-Same Stem', async () => + await test( + ` + \\voice + E5{beam up}*5 + \\voice + C5{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + + // v1 Quarter Chord-v2 Quarter Single + it('v1 Quarter Chord-v2 Quarter Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5)*5 + \\voice + C5 D5 E5 F5 G5 + ` + )); + it('v1 Quarter Chord-v2 Quarter Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5){beam down}*5 + \\voice + C5{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Quarter Chord-v2 Quarter Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5){beam up}*5 + \\voice + C5{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Quarter Chord-v2 Quarter Single + it('v1 Quarter Chord-v2 Quarter Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5)*5 + \\voice + (C5 B4) (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 Quarter Chord-v2 Quarter Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5){beam down}*5 + \\voice + (C5 B4){beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 Quarter Chord-v2 Quarter Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5){beam up}*5 + \\voice + (C5 B4){beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + // v1 Quarter Single-v2 Half Single + it('v1 Quarter Single-v2 Half Single-Automatic Stem', async () => + await test( + ` + \\ts (10 4) + \\voice + E5.4 r E5.4 r E5.4 r E5.4 r E5.4 r + \\voice + C5.2 D5 E5 F5 G5 + ` + )); + it('v1 Quarter Single-v2 Half Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.4{beam down} r E5.4{beam down} r E5.4{beam down} r E5.4{beam down} r E5.4{beam down} r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + it('v1 Quarter Single-v2 Half Single-Same Stem', async () => + await test( + ` + \\voice + E5.4 r E5.4 r E5.4 r E5.4 r E5.4 r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + + // v1 Quarter Chord, Half Single + it('v1 Quarter Chord-Half Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r + \\voice + C5.2 D5 E5 F5 G5 + ` + )); + it('v1 Quarter Chord-Half Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Quarter Chord-Half Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Quarter Chord-v2 Half Chord + it('v1 Quarter Chord-v2 Half Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r (E5 F5).4 r + \\voice + (C5 B4).2 (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 Quarter Chord-v2 Half Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).4{beam down} r (E5 F5).4{beam down} r (E5 F5).4{beam down} r (E5 F5).4{beam down} r (E5 F5).4{beam down} r + \\voice + (C5 B4).2{beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 Quarter Chord-v2 Half Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).4{beam up} r (E5 F5).4{beam up} r (E5 F5).4{beam up} r (E5 F5).4{beam up} r (E5 F5).4 r{beam up} + \\voice + (C5 B4).2{beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + // v1 8th Flag Single-v2 8th Flag Single + it('v1 8th Flag Single-v2 8th Flag Single-Automatic Stem', async () => + await test( + ` + \\ts (5 4) + \\voice + E5.8 r E5 r E5 r E5 r E5 r + \\voice + C5 r D5 r E5 r F5 r G5 r + ` + )); + it('v1 8th Flag Single-v2 8th Flag Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.8{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r + \\voice + C5{beam up} r D5{beam up} r E5{beam up} r F5{beam up} r G5 {beam up} r + ` + )); + it('v1 8th Flag Single-v2 8th Flag Single-Same Stem', async () => + await test( + ` + \\voice + E5.8{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r + \\voice + C5{beam up} r D5{beam up} r E5{beam up} r F5{beam up} r G5 {beam up} r + ` + )); + + // v1 8th Flag Chord, V2 8th Flag Single + it('v1 8th Flag Chord-V2 8th Flag Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + C5 r D5 r E5 r F5 r G5 r + ` + )); + it('v1 8th Flag Chord-V2 8th Flag Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + C5{beam up} r D5{beam up} r E5{beam up} r F5{beam up} r G5{beam up} r + ` + )); + it('v1 8th Flag Chord-V2 8th Flag Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r + \\voice + C5{beam up} r D5{beam up} r E5{beam up} r F5{beam up} r G5{beam up} r + ` + )); + + // v1 8th Flag Chord, V2 8th Flag Chord + it('v1 8th Flag Chord-V2 8th Flag Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + (C5 B4) r (D5 C5) r (E5 D5) r (F5 E5) r (G5 F5) r + ` + )); + it('v1 8th Flag Chord-V2 8th Flag Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + (C5 B4){beam up} r (D5 C5){beam up} r (E5 D5){beam up} r (F5 E5){beam up} r (G5 F5){beam up} r + ` + )); + it('v1 8th Flag Chord-V2 8th Flag Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r + \\voice + (C5 B4){beam up} r (D5 C5){beam up} r (E5 D5){beam up} r (F5 E5){beam up} r (G5 F5){beam up} r + ` + )); + + // v1 8th Flag Single-v2 Quarter Single + it('v1 8th Flag Single-v2 Quarter Single-Automatic Stem', async () => + await test( + ` + \\voice + E5.8 r E5 r E5 r E5 r E5 r + \\voice + C5.4 D5 E5 F5 G5 + ` + )); + it('v1 8th Flag Single-v2 Quarter Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.8{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r + \\voice + C5.4{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + it('v1 8th Flag Single-v2 Quarter Single-Same Stem', async () => + await test( + ` + \\voice + E5.8{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r + \\voice + C5.4{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + + // v1 8th Flag Chord-v2 Quarter Single + it('v1 8th Flag Chord-v2 Quarter Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + C5.4 D5 E5 F5 G5 + ` + )); + it('v1 8th Flag Chord-v2 Quarter Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + C5.4{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 8th Flag Chord-v2 Quarter Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r + \\voice + C5.4{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 8th Flag Chord-v2 Quarter Chord + it('v1 8th Flag Chord-v2 Quarter Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + (C5 B4).4 (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 8th Flag Chord-v2 Quarter Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + (C5 B4).4{beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 8th Flag Chord-v2 Quarter Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8 {beam up} r (E5 F5) {beam up} r (E5 F5) {beam up} r (E5 F5) {beam up} r (E5 F5) {beam up} r + \\voice + (C5 B4).4 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + // v1 8th Flag Single-v2 Half Single + it('v1 8th Flag Single-v2 Half Single-Automatic Stem', async () => + await test( + ` + \\ts (5 2) + \\voice + E5.8 r r r E5 r r r E5 r r r E5 r r r E5 r r r + \\voice + C5.2 D5 E5 F5 G5 + ` + )); + it('v1 8th Flag Single-v2 Half Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.8{beam down} r r r E5{beam down} r r r E5{beam down} r r r E5{beam down} r r r E5{beam down} r r r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + it('v1 8th Flag Single-v2 Half Single-Same Stem', async () => + await test( + ` + \\voice + E5.8{beam up} r r r E5{beam up} r r r E5{beam up} r r r E5{beam up} r r r E5{beam up} r r r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5 {beam up} + ` + )); + + // v1 8th Flag Chord-v2 Half Single + it('v1 8th Flag Chord-v2 Half Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r r r (E5 F5) r r r (E5 F5) r r r (E5 F5) r r r (E5 F5) r r r + \\voice + C5.2 D5 E5 F5 G5 + ` + )); + it('v1 8th Flag Chord-v2 Half Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 8th Flag Chord-v2 Half Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r + \\voice + C5.2{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 8th Flag Chord-v2 Half Chord + it('v1 8th Flag Chord-v2 Half Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).8 r r r (E5 F5) r r r (E5 F5) r r r (E5 F5) r r r (E5 F5) r r r + \\voice + (C5 B4).2 (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 8th Flag Chord-v2 Half Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r (E5 F5){beam down} r r r + \\voice + (C5 B4).2{beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 8th Flag Chord-v2 Half Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).8{beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r (E5 F5){beam up} r r r + \\voice + (C5 B4).2 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + // v1 Full Single-v2 Full Single + it('v1 Full Single-v2 Full Single-Automatic Stem', async () => + await test( + ` + \\ts (5 1) + \\voice + E5.1 E5 E5 E5 E5 + \\voice + C5.1 D5 E5 F5 G5 + ` + )); + it('v1 Full Single-v2 Full Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.1{beam down} E5{beam down} E5{beam down} E5{beam down} E5{beam down} + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Full Single-v2 Full Single-Same Stem', async () => + await test( + ` + \\voice + E5.1{beam up} E5{beam up} E5{beam up} E5{beam up} E5{beam up} + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Full Chord-v2 Full Single + it('v1 Full Chord-v2 Full Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).1 (E5 F5) (E5 F5) (E5 F5) (E5 F5) + \\voice + C5.1 D5 E5 F5 G5 + ` + )); + it('v1 Full Chord-v2 Full Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).1{beam down} (E5 F5){beam down} (E5 F5){beam down} (E5 F5){beam down} (E5 F5){beam down} + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Full Chord-v2 Full Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).1{beam up} (E5 F5){beam up} (E5 F5){beam up} (E5 F5){beam up} (E5 F5){beam up} + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Full Chord-v2 Full Chord + it('v1 Full Chord-v2 Full Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).1 (E5 F5) (E5 F5) (E5 F5) (E5 F5) + \\voice + (C5 B4).1 (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 Full Chord-v2 Full Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).1{beam down} (E5 F5){beam down} (E5 F5){beam down} (E5 F5){beam down} (E5 F5){beam down} + \\voice + (C5 B4).1 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 Full Chord-v2 Full Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).1{beam up} (E5 F5){beam up} (E5 F5){beam up} (E5 F5){beam up} (E5 F5){beam up} + \\voice + (C5 B4).1 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + //// + + // v1 Half Single-v2 Full Single + it('v1 Half Single-v2 Full Single-Automatic Stem', async () => + await test( + ` + \\ts (5 1) + \\voice + E5.2 r E5 r E5 r E5 r E5 r + \\voice + C5.1 D5 E5 F5 G5 + ` + )); + it('v1 Half Single-v2 Full Single-Reversed Stem', async () => + await test( + ` + \\voice + E5.2{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r E5{beam down} r + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Half Single-v2 Full Single-Same Stem', async () => + await test( + ` + \\voice + E5.2{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r E5{beam up} r + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Half Chord-v2 Full Single + it('v1 Half Chord-v2 Full Single-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).2 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + C5.1 D5 E5 F5 G5 + ` + )); + it('v1 Half Chord-v2 Full Single-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).2{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + it('v1 Half Chord-v2 Full Single-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).2{beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r + \\voice + C5.1{beam up} D5{beam up} E5{beam up} F5{beam up} G5{beam up} + ` + )); + + // v1 Half Chord-v2 Full Chord + it('v1 Half Chord-v2 Full Chord-Automatic Stem', async () => + await test( + ` + \\voice + (E5 F5).2 r (E5 F5) r (E5 F5) r (E5 F5) r (E5 F5) r + \\voice + (C5 B4).1 (D5 C5) (E5 D5) (F5 E5) (G5 F5) + ` + )); + it('v1 Half Chord-v2 Full Chord-Reversed Stem', async () => + await test( + ` + \\voice + (E5 F5).2{beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r (E5 F5){beam down} r + \\voice + (C5 B4).1 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + it('v1 Half Chord-v2 Full Chord-Same Stem', async () => + await test( + ` + \\voice + (E5 F5).2{beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r (E5 F5){beam up} r + \\voice + (C5 B4).1 {beam up} (D5 C5){beam up} (E5 D5){beam up} (F5 E5){beam up} (G5 F5){beam up} + ` + )); + + it('v1 Eighth Single-v2 Eighth Single-Automatic-Stem', async () => await test(` + \\voice + E5.8*10 + \\voice + C5*2 D5*2 E5*2 F5*2 G5*2 + `)); + + it('v1 Eighth Chord-v2 Eighth Single-Automatic-Stem', async () => await test(` + \\voice + (E5 F5).8*10 + \\voice + C5*2 D5*2 E5*2 F5*2 G5*2 + `)); + + it('v1 Eighth Chord-v2 Eighth Chord-Automatic-Stem', async () => await test(` + \\voice + (E5 F5).8*10 + \\voice + (C5 B4)*2 (D5 C5)*2 (E5 D5)*2 (F5 E5)*2 (G5 F5)*2 + `)); + + it('v1 16th Single-v2 Eighth Single-Automatic-Stem', async () => await test(` + \\voice + E5.16*20 + \\voice + C5.8*2 D5*2 E5*2 F5*2 G5*2 + `)); + + it('v1 16th Chord-v2 Eighth Single-Automatic-Stem', async () => await test(` + \\voice + (E5 F5).16*20 + \\voice + C5.8*2 D5*2 E5*2 F5*2 G5*2 + `)); + + it('v1 16th Chord-v2 Eighth Chord-Automatic-Stem', async () => await test(` + \\voice + (E5 F5).16*20 + \\voice + (C5 B4).8*2 (D5 C5)*2 (E5 D5)*2 (F5 E5)*2 (G5 F5)*2 + `)); + + // Known issues: (beat counts refer to the beats which "overlap", not the rests or filler beats) + + // Accepted due to force of same stem instead of different directions for voices: + // * Bar 3 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 6 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 9 Beat 3-5: Displace logic breaks + // * Bar 12 beat 3: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 15 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 15 Beat 3: there is an overlap + // * Bar 15 Beat 4: the half note is not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 18 Beat 3: Displace logic breaks + // * Bar 18 Beat 4: Displace logic breaks + // * Bar 18 Beat 5: Displace logic breaks + // * Bar 21 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 24 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 27 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 27 Beat 3: Displace logic breaks + // * Bar 27 Beat 4: the half note is not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 30 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 33 Beat 2: the note heads should be swapped (lowest goes to the 'correct' side'), stem is also not long enough due to displace + // * Bar 33 Beat 4: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 36 Beat 3: Displace logic breaks + // * Bar 36 Beat 4: Displace logic breaks + // * Bar 36 Beat 5: Displace logic breaks + // * Bar 39 Beat 2: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 42 Beat 2: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 42 Beat 4: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 45 Beat 3: Displace logic breaks + // * Bar 45 Beat 4: Displace logic breaks + // * Bar 45 Beat 5: Displace logic breaks + // * Bar 51 Beat 3: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 54 Beat 3: Displace logic breaks + // * Bar 54 Beat 4: Displace logic breaks + // * Bar 54 Beat 5: Displace logic breaks + // * Bar 60 Beat 4: one note head not visible (check for exact overlaps and try to go back to 'correct' side?) + // * Bar 63 Beat 3: Displace logic breaks + // * Bar 63 Beat 4: Displace logic breaks + // * Bar 63 Beat 5: Displace logic breaks + }); +}); diff --git a/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts b/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts index 3e631b270..448c30d17 100644 --- a/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts +++ b/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts @@ -1,3 +1,4 @@ +import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; describe('SpecialTracksTests', () => { @@ -22,4 +23,102 @@ describe('SpecialTracksTests', () => { it('numbered', async () => { await VisualTestHelper.runVisualTest('special-tracks/numbered.gp'); }); + + it('numbered-tuplets', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track "Num" + \\staff {numbered} + + C2.2 {tu 3}*3 | + C7.2 {tu 3}*3 | + + C2.16 {tu 3}*3 | + C7.16 {tu 3}*3 | + + + \\track "Std & Num" + \\staff {score numbered} + + C2.2 {tu 3}*3 | + C7.2 {tu 3}*3 | + + C2.16 {tu 3}*3 | + C7.16 {tu 3}*3 | + `, + 'test-data/visual-tests/special-tracks/numbered-tuplets.png', + undefined, + o => { + o.tracks = o.score.tracks.map(t => t.index); + } + ); + }); + + it('numbered-durations', async () =>{ + await VisualTestHelper.runVisualTestTex( + ` + \\bracketExtendMode noBrackets + \\track {defaultSystemsLayout 1} + \\staff {numbered} + \\section ("16th notes") + C4.16 * 16 | + + \\section ("8th notes") + C4.8 * 8 | + \\section ("8th notes dotted") + C4.8 {d} * 6 r.16 | + \\section ("8th notes double-dotted") + C4.8 {dd} * 4 r.8 | + + \\section ("Quarter notes") + C4.4 * 4 | + \\section ("Quarter notes dotted") + C4.4 {d} * 2 r.4 | + \\section ("Quarter notes double-dotted") + C4.4 {dd} * 2 r.8 | + + \\section ("Half notes") + C4.2 * 2 | + \\section ("Half notes dotted") + C4.2 {d} r.4 | + \\section ("Half notes double dotted") + C4.2 {dd} r.8 | + + \\section ("Whole notes") + \\ts (8 4) + C4.1 * 2 | + \\section ("Half notes dotted") + C4.1 {d} r.2 | + \\section ("Half notes double dotted") + C4.1 {dd} r.4 | + + \\staff {numbered} + C4.4 * 4 | + + C4.4 * 4 | + C4.4 * 4 | + C4.4 * 4 | + + C4.4 * 4 | + C4.4 * 4 | + C4.4 * 4 | + + C4.4 * 4 | + C4.4 * 4 | + C4.4 * 4 | + + C4.4 * 8 | + C4.4 * 8 | + C4.4 * 8 | + + r + `, + 'test-data/visual-tests/special-tracks/numbered-durations.png', + undefined, + o => { + o.settings.display.layoutMode = LayoutMode.Parchment; + o.tracks = o.score.tracks.map(t => t.index); + } + ); + }) }); diff --git a/packages/alphatab/test/visualTests/features/SystemsLayout.test.ts b/packages/alphatab/test/visualTests/features/SystemsLayout.test.ts index 35f7004a8..d4f810fe3 100644 --- a/packages/alphatab/test/visualTests/features/SystemsLayout.test.ts +++ b/packages/alphatab/test/visualTests/features/SystemsLayout.test.ts @@ -21,7 +21,7 @@ describe('SystemsLayoutTests', () => { it('bars-adjusted-model', async () => { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( @@ -34,7 +34,7 @@ describe('SystemsLayoutTests', () => { it('multi-track-single-track', async () => { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( @@ -47,7 +47,7 @@ describe('SystemsLayoutTests', () => { it('multi-track-two-tracks', async () => { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; const options = await VisualTestOptions.file( 'systems-layout/multi-track-different.gp', @@ -60,7 +60,7 @@ describe('SystemsLayoutTests', () => { it('resized', async () => { const settings = new Settings(); - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + settings.display.layoutMode = LayoutMode.Parchment; await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( @@ -74,7 +74,6 @@ describe('SystemsLayoutTests', () => { it('horizontal-fixed-sizes-single-track', async () => { const settings = new Settings(); settings.display.layoutMode = LayoutMode.Horizontal; - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; const score = ScoreLoader.loadScoreFromBytes( await TestPlatform.loadFile('test-data/visual-tests/systems-layout/multi-track-different.gp') @@ -101,7 +100,6 @@ describe('SystemsLayoutTests', () => { it('horizontal-fixed-sizes-two-tracks', async () => { const settings = new Settings(); settings.display.layoutMode = LayoutMode.Horizontal; - settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; const score = ScoreLoader.loadScoreFromBytes( await TestPlatform.loadFile('test-data/visual-tests/systems-layout/multi-track-different.gp') ); diff --git a/packages/alphatab/test/visualTests/issues/BrokenRenders.test.ts b/packages/alphatab/test/visualTests/issues/BrokenRenders.test.ts index 512aee389..e9ecf815a 100644 --- a/packages/alphatab/test/visualTests/issues/BrokenRenders.test.ts +++ b/packages/alphatab/test/visualTests/issues/BrokenRenders.test.ts @@ -1,5 +1,5 @@ import { TestPlatform } from 'test/TestPlatform'; -import { VisualTestHelper } from '../VisualTestHelper'; +import { VisualTestHelper, VisualTestOptions, VisualTestRun } from '../VisualTestHelper'; import { Settings } from '@coderline/alphatab/Settings'; import { XmlDocument } from '@coderline/alphatab/xml/XmlDocument'; import { expect } from 'chai'; @@ -12,6 +12,41 @@ describe('BrokenRendersTests', () => { await VisualTestHelper.runVisualTest('issues/let-ring-empty-voice.gp'); }); + it('bottom-effect-band', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\lyrics "Do Re Mi Fa So" + C4 {tr 16} C4 C4 C4 | C4 c4`, + 'test-data/visual-tests/issues/bottom-effect-band.png' + ); + }); + + it('whammy-resize-wrap', async () => { + const score = ScoreLoader.loadAlphaTex(` + \\staff {tabs} + 7.3.4 + 8.3 + 10.3.4 + 12.3.4{tbe (dip default 0 0 15 -4 30 0) beam Down} + | + 5.3{nh}.4{tbe (dive default 0 0 30.599999999999998 8) beam Down} + 5.3 + 5.3`); + await VisualTestHelper.runVisualTestFull( + new VisualTestOptions( + score, + [ + new VisualTestRun(600, 'test-data/visual-tests/issues/whammy-resize-wrap-600.png'), + new VisualTestRun(400, 'test-data/visual-tests/issues/whammy-resize-wrap-400.png'), + // 431 + new VisualTestRun(380, 'test-data/visual-tests/issues/whammy-resize-wrap-380.png'), + new VisualTestRun(500, 'test-data/visual-tests/issues/whammy-resize-wrap-500.png') + ], + new Settings() + ) + ); + }); + it('valid-svg', async () => { const settings = new Settings(); settings.core.engine = 'svg'; @@ -53,4 +88,44 @@ describe('BrokenRendersTests', () => { } } }); + + describe('no-label-padding-left', () => { + it('no-padding', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track "T1" + C4 * 4 + `, + 'test-data/visual-tests/issues/no-label-padding-left-no-padding.png' + ); + }); + + it('with-label', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track "T1" + C4 * 4 + `, + 'test-data/visual-tests/issues/no-label-padding-left-with-label.png', + undefined, + o => { + o.settings.display.systemLabelPaddingLeft = 100; + } + ); + }); + + it('without-label', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track + C4 * 4 + `, + 'test-data/visual-tests/issues/no-label-padding-left-without-label.png', + undefined, + o => { + o.settings.display.systemLabelPaddingLeft = 100; + } + ); + }); + }); }); diff --git a/packages/alphatex/package.json b/packages/alphatex/package.json index da0d4bc5c..5c02aed87 100644 --- a/packages/alphatex/package.json +++ b/packages/alphatex/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-alphatex", - "version": "1.7.1", + "version": "1.8.0", "private": true, "scripts": { "lint": "biome lint", @@ -8,9 +8,9 @@ "generate": "tsx scripts/generate.ts" }, "devDependencies": { - "rimraf": "^6.0.1", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3" }, "type": "module" diff --git a/packages/alphatex/scripts/generate-parser.ts b/packages/alphatex/scripts/generate-parser.ts index 5cb688854..d5261e9f7 100644 --- a/packages/alphatex/scripts/generate-parser.ts +++ b/packages/alphatex/scripts/generate-parser.ts @@ -17,8 +17,7 @@ import { type AlphaTexMappedEnumName } from '@coderline/alphatab-alphatex/enum'; -import * as alphaTab from '@coderline/alphatab' - +import * as alphaTab from '@coderline/alphatab'; type LanguageDefinitionsVisitorContext = { foundDefinitions: boolean; @@ -397,7 +396,7 @@ function enumMappingsVisitor( members.push( generateKeySignaturesReversed('keySignaturesMinorReversed', (entry: AlphaTexMappedEnumMappingEntry) => { - return entry?.aliases!.find(a => a.endsWith('minor'))!; + return entry!.aliases!.find(a => a.endsWith('minor'))!; }) ); members.push( diff --git a/packages/alphatex/src/definitions.ts b/packages/alphatex/src/definitions.ts index d2cab63a7..1e721ca30 100644 --- a/packages/alphatex/src/definitions.ts +++ b/packages/alphatex/src/definitions.ts @@ -147,8 +147,17 @@ import { noteVibratoWide } from '@coderline/alphatab-alphatex//properties/note/v import { x } from '@coderline/alphatab-alphatex//properties/note/x'; import { metadata, properties } from '@coderline/alphatab-alphatex/common'; import { db } from '@coderline/alphatab-alphatex/metadata/bar/db'; +import { voiceMode } from '@coderline/alphatab-alphatex/metadata/bar/voiceMode'; +import { defaultBarNumberDisplay } from '@coderline/alphatab-alphatex/metadata/score/defaultbarnumberdisplay'; +import { chordDiagramsInScore } from '@coderline/alphatab-alphatex/metadata/score/chordDiagramsInScore'; +import { extendBarLines } from '@coderline/alphatab-alphatex/metadata/score/extendbarlines'; +import { hideEmptyStaves } from '@coderline/alphatab-alphatex/metadata/score/hideemptystaves'; +import { hideEmptyStavesInFirstSystem } from '@coderline/alphatab-alphatex/metadata/score/hideemptystavesinfirstsystem'; +import { showSingleStaffBrackets } from '@coderline/alphatab-alphatex/metadata/score/showsinglestaffbrackets'; import { instrumentMeta } from '@coderline/alphatab-alphatex/metadata/staff/instrument'; import type { AlphaTexExample, WithDescription, WithSignatures } from '@coderline/alphatab-alphatex/types'; +import { barNumberDisplay } from '@coderline/alphatab-alphatex/metadata/bar/barnumberdisplay'; +import { beaming } from '@coderline/alphatab-alphatex/metadata/bar/beamingRule'; export const structuralMetaData = metadata(track, staff, voice); export const scoreMetaData = metadata( @@ -176,7 +185,13 @@ export const scoreMetaData = metadata( firstSystemTrackNameMode, otherSystemsTrackNameMode, firstSystemTrackNameOrientation, - otherSystemsTrackNameOrientation + otherSystemsTrackNameOrientation, + extendBarLines, + chordDiagramsInScore, + hideEmptyStaves, + hideEmptyStavesInFirstSystem, + showSingleStaffBrackets, + defaultBarNumberDisplay ); export const staffMetaData = metadata( @@ -214,7 +229,10 @@ export const barMetaData = metadata( spd, sph, spu, - db + db, + voiceMode, + barNumberDisplay, + beaming ); export const allMetadata = new Map([ diff --git a/packages/alphatex/src/enum.ts b/packages/alphatex/src/enum.ts index 1ef2afdd7..014c0516e 100644 --- a/packages/alphatex/src/enum.ts +++ b/packages/alphatex/src/enum.ts @@ -9,6 +9,7 @@ export const alphaTexMappedEnumLookup = { GraceType: alphaTab.model.GraceType, FermataType: alphaTab.model.FermataType, AlphaTexAccidentalMode: alphaTab.importer.alphaTex.AlphaTexAccidentalMode, + AlphaTexVoiceMode: alphaTab.importer.alphaTex.AlphaTexVoiceMode, NoteAccidentalMode: alphaTab.model.NoteAccidentalMode, BarreShape: alphaTab.model.BarreShape, Ottavia: alphaTab.model.Ottavia, @@ -26,7 +27,9 @@ export const alphaTexMappedEnumLookup = { TripletFeel: alphaTab.model.TripletFeel, BarLineStyle: alphaTab.model.BarLineStyle, SimileMark: alphaTab.model.SimileMark, - Direction: alphaTab.model.Direction + Direction: alphaTab.model.Direction, + TremoloPickingStyle: alphaTab.model.TremoloPickingStyle, + BarNumberDisplay: alphaTab.model.BarNumberDisplay }; export type AlphaTexMappedEnumName = keyof typeof alphaTexMappedEnumLookup; @@ -106,6 +109,18 @@ export const alphaTexMappedEnumMapping: { Auto: { snippet: 'auto', shortDescription: 'Automatic (Based on Pitch)' }, Explicit: { snippet: 'explicit', shortDescription: 'Explicit (as Written)' } }, + AlphaTexVoiceMode: { + StaffWise: { + snippet: 'staffWise', + shortDescription: 'Staff-Wise voices', + longDescription: 'A new voice resets to bar 1 from where the notation of a new voice can be added.' + }, + BarWise: { + snippet: 'barWise', + shortDescription: 'Bar-Wise voices', + longDescription: 'A new voice adds a new voice to the current bar only.' + } + }, NoteAccidentalMode: { Default: { snippet: 'default', shortDescription: 'Auto-detect the accidentals', aliases: ['d'] }, ForceNone: { snippet: 'forceNone', shortDescription: 'Force no accidentals', aliases: ['-'] }, @@ -435,6 +450,15 @@ export const alphaTexMappedEnumMapping: { JumpDalSegnoSegnoAlFine: { snippet: 'dalSegnoSegnoAlFine', shortDescription: 'DalSegnoSegnoAlFine (Jump)' }, JumpDaCoda: { snippet: 'daCoda', shortDescription: 'DaCoda (Jump)' }, JumpDaDoubleCoda: { snippet: 'daDoubleCoda', shortDescription: 'DaDoubleCoda (Jump)' } + }, + TremoloPickingStyle: { + Default: { snippet: 'default', shortDescription: 'Default tremolo' }, + BuzzRoll: { snippet: 'buzzRoll', shortDescription: 'Buzz roll tremolo' } + }, + BarNumberDisplay: { + AllBars: { snippet: 'allBars', shortDescription: 'All bars' }, + FirstOfSystem: { snippet: 'firstOfSystem', shortDescription: 'First bar of every system' }, + Hide: { snippet: 'hide', shortDescription: 'Hide' } } }; diff --git a/packages/alphatex/src/metadata/bar/barnumberdisplay.ts b/packages/alphatex/src/metadata/bar/barnumberdisplay.ts new file mode 100644 index 000000000..6cd37c999 --- /dev/null +++ b/packages/alphatex/src/metadata/bar/barnumberdisplay.ts @@ -0,0 +1,41 @@ +import * as alphaTab from '@coderline/alphatab'; +import { enumParameter } from '@coderline/alphatab-alphatex/enum'; +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const barNumberDisplay: MetadataTagDefinition = { + tag: '\\barNumberDisplay', + snippet: '\\barNumberDisplay ${1:allBars}$0', + shortDescription: 'Sets the display mode for bar numbers.', + signatures: [ + { + parameters: [ + { + name: 'mode', + shortDescription: 'The mode to use', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter('BarNumberDisplay') + } + ] + } + ], + examples: [ + { + options: { display: { layoutMode: 'Parchment' } }, + tex: ` + \\defaultBarNumberDisplay hide + \\track { defaultsystemslayout 3 } + C4.1 | \\barNumberDisplay allBars C4.1 | C4.1 | + \\barNumberDisplay firstOfSystem C4.1 | \\barNumberDisplay firstOfSystem C4.1 | C4.1 + ` + }, + { + options: { display: { layoutMode: 'Parchment' } }, + tex: ` + \\defaultBarNumberDisplay firstOfSystem + \\track { defaultsystemslayout 3 } + C4.1 | \\barNumberDisplay allBars C4.1 | C4.1 | + \\barNumberDisplay hide C4.1 | C4.1 | C4.1 + ` + } + ] +}; diff --git a/packages/alphatex/src/metadata/bar/beamingRule.ts b/packages/alphatex/src/metadata/bar/beamingRule.ts new file mode 100644 index 000000000..f6024005e --- /dev/null +++ b/packages/alphatex/src/metadata/bar/beamingRule.ts @@ -0,0 +1,60 @@ +import * as alphaTab from '@coderline/alphatab'; +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const beaming: MetadataTagDefinition = { + tag: '\\beaming', + snippet: '\\beaming ($1 $2) $0', + shortDescription: 'Set the time signature', + longDescription: ` + Defines a custom beaming rule defining how beams of certain durations should be beamed. + + To define how beats should be beamed we need 2 parts: + + 1. A duration with which we splitup the bars + 2. A list of group sizes defining how many split-parts should be beamed together. + + The beaming rules go hand-in-hand with the time signature as the rules need to properly + define the groups for the whole beat. + + Let's take a simple example of a 4/4 time signature. If we want to ensure that the beats within the quarter notes are + beamed together we can write variants like this: + + a. \`\\beaming (4 1 1 1 1)\` + b. \`\\beaming (8 2 2 2 2)\` + c. \`\\beaming (16 4 4 4 4)\` + + We slice the bar into 4, 8 or 16 parts. Then we add "groups" to those parts. If two beats start in the same group, they can be beamed together. + Simple as that. + + There are some common guidelines on how beaming "should be done" and alphaTab ships a wide range of defaults. But in case of more specialized time signatures, + you can also customize the beaming as you need by slicing the bar and grouping the beats as needed. + `, + signatures: [ + { + parameters: [ + { + name: 'duration', + shortDescription: 'The note duration defining the smallest group size', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + type: alphaTab.importer.alphaTex.AlphaTexNodeType.Number + }, + { + name: 'groups', + shortDescription: 'For every group the number of notes contained in the group.', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.ValueListWithoutParenthesis, + type: alphaTab.importer.alphaTex.AlphaTexNodeType.Number + } + ] + } + ], + + examples: ` + \\ts (4 4) + \\beaming (8 4 2 2) + C4.8 * 8 | + + \\ts (4 4) + \\beaming (8 4 4) + C4.8 * 8 + ` +}; diff --git a/packages/alphatex/src/metadata/bar/voiceMode.ts b/packages/alphatex/src/metadata/bar/voiceMode.ts new file mode 100644 index 000000000..a83eb2598 --- /dev/null +++ b/packages/alphatex/src/metadata/bar/voiceMode.ts @@ -0,0 +1,75 @@ +import * as alphaTab from '@coderline/alphatab'; +import { enumParameter } from '@coderline/alphatab-alphatex/enum'; +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const voiceMode: MetadataTagDefinition = { + tag: '\\voiceMode', + snippet: '\\voiceMode $1 $0', + shortDescription: 'Changes the mode how voices are treated', + longDescription: ` + Changes the mode how alphaTab should treat voices when adding \`\\voice\`. + + You can either choose to write voice-by-voice where each voice has all bars defined. + You write bar-by-bar where a new voice is only added to the current bar. + `, + signatures: [ + { + parameters: [ + { + name: 'mode', + shortDescription: 'The mode which should be active', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter('AlphaTexVoiceMode') + } + ] + } + ], + examples: [` + \\title "Staff Wise Voices" + \\voiceMode staffWise + // Voice 1 + \\voice + // Bar 1 Voice 1 + C4*4 | + // Bar 2 Voice 1 + C5*4 | + // Bar 3 Voice 1 + C6*4 + // Voice 2 + \\voice + // Bar 1 Voice 2 + C3*4 | + // Bar 2 Voice 2 + C4*4 | + // Bar 3 Voice 2 + C5 * 4 + `, + ` + \\title "Bar Wise Voices" + \\voiceMode barWise + // Bar 1 + // Bar 1 Voice 1 + \\voice + C4*4 + // Bar 1 Voice 2 + \\voice + C3*4 + | + // Bar 2 + // Bar 2 Voice 1 + \\voice + C5*4 + // Bar 2 Voice 2 + \\voice + C4*4 + | + // Bar 3 + // Bar 3 Voice 1 + \\voice + C6*4 + // Bar 3 Voice 2 + \\voice + C5 * 4 + ` + ] +}; diff --git a/packages/alphatex/src/metadata/score/chordDiagramsInScore.ts b/packages/alphatex/src/metadata/score/chordDiagramsInScore.ts new file mode 100644 index 000000000..71039463f --- /dev/null +++ b/packages/alphatex/src/metadata/score/chordDiagramsInScore.ts @@ -0,0 +1,33 @@ +import * as alphaTab from '@coderline/alphatab'; +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const chordDiagramsInScore: MetadataTagDefinition = { + tag: '\\chordDiagramsInScore', + snippet: '\\chordDiagramsInScore', + shortDescription: 'Show inline chord diagrams in score.', + longDescription: `Configures whether chord diagrams are shown inline in the score..`, + signatures: [ + { + parameters: [ + { + name: 'visibility', + shortDescription: 'The visibility of the diagrams', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Optional, + defaultValue: 'true', + type: alphaTab.importer.alphaTex.AlphaTexNodeType.Ident, + values: [ + { name: 'true', snippet: 'true', shortDescription: 'Show the diagrams' }, + { name: 'false', snippet: 'false', shortDescription: 'Hide the diagrams' } + ] + } + ] + } + ], + examples: [ + ` + \\chordDiagramsInScore + \\chord ("E" 0 0 1 2 2 0) + (0.1 0.2 1.3 2.4 2.5 0.6){ch "E"} + ` + ] +}; diff --git a/packages/alphatex/src/metadata/score/defaultbarnumberdisplay.ts b/packages/alphatex/src/metadata/score/defaultbarnumberdisplay.ts new file mode 100644 index 000000000..00826ae76 --- /dev/null +++ b/packages/alphatex/src/metadata/score/defaultbarnumberdisplay.ts @@ -0,0 +1,53 @@ +import * as alphaTab from '@coderline/alphatab'; +import { enumParameter } from '@coderline/alphatab-alphatex/enum'; +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const defaultBarNumberDisplay: MetadataTagDefinition = { + tag: '\\defaultBarNumberDisplay', + snippet: '\\defaultBarNumberDisplay ${1:allBars}$0', + shortDescription: 'Sets the display mode for bar numbers on all bars.', + signatures: [ + { + parameters: [ + { + name: 'mode', + shortDescription: 'The mode to use', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter('BarNumberDisplay') + } + ] + } + ], + examples: [ + { + options: { display: { layoutMode: 'Parchment' } }, + tex: ` + \\defaultBarNumberDisplay allBars + \\title "All Bars" + \\track { defaultsystemslayout 3 } + C4.1 | C4.1 | C4.1 | + C4.1 | C4.1 | C4.1 + ` + }, + { + options: { display: { layoutMode: 'Parchment' } }, + tex: ` + \\defaultBarNumberDisplay firstOfSystem + \\title "First of System" + \\track { defaultsystemslayout 3 } + C4.1 | C4.1 | C4.1 | + C4.1 | C4.1 | C4.1 + ` + }, + { + options: { display: { layoutMode: 'Parchment' } }, + tex: ` + \\defaultBarNumberDisplay hide + \\title "Hide" + \\track { defaultsystemslayout 3 } + C4.1 | C4.1 | C4.1 | + C4.1 | C4.1 | C4.1 + ` + } + ] +}; diff --git a/packages/alphatex/src/metadata/score/extendbarlines.ts b/packages/alphatex/src/metadata/score/extendbarlines.ts new file mode 100644 index 000000000..e0dcee461 --- /dev/null +++ b/packages/alphatex/src/metadata/score/extendbarlines.ts @@ -0,0 +1,36 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const extendBarLines: MetadataTagDefinition = { + tag: '\\extendBarLines', + snippet: '\\extendBarLines', + shortDescription: `Extend the bar lines across staves in the same system.`, + signatures: [ + { + parameters: [] + } + ], + examples: ` + \\extendBarLines + \\track "Piano1" + \\staff {score} + \\instrument piano + C4 D4 E4 F4 + \\staff {score} + \\clef f4 C3 D3 E3 F3 + \\track "Piano2" + \\staff {score} + \\instrument piano + C4 D4 E4 F4 + \\track "Flute 1" + \\staff { score } + \\instrument flute + C4 D4 E4 F4 + \\track "Flute 2" + \\staff { score } + \\instrument flute + \\clef f4 C3 D3 E3 F3 + \\track "Guitar 1" + \\staff { score tabs } + 0.3.4 2.3.4 5.3.4 7.3.4 + ` +}; diff --git a/packages/alphatex/src/metadata/score/hideemptystaves.ts b/packages/alphatex/src/metadata/score/hideemptystaves.ts new file mode 100644 index 000000000..133de18d7 --- /dev/null +++ b/packages/alphatex/src/metadata/score/hideemptystaves.ts @@ -0,0 +1,25 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const hideEmptyStaves: MetadataTagDefinition = { + tag: '\\hideEmptyStaves', + snippet: '\\hideEmptyStaves', + shortDescription: `Hide empty staves.`, + signatures: [ + { + parameters: [] + } + ], + examples: { + options: { display: {systemsLayoutMode: 'UseModelLayout'}}, + tex: ` + \\hideEmptyStaves + \\defaultSystemsLayout 3 + \\track "Track 1" + C4 * 4 | C4 * 4 | C4 * 4 | + C4 * 4 | C4 * 4 | C4 * 4 | + \\track "Track 2" + r | r | r | + r | C4.1 | r | + ` + } +} diff --git a/packages/alphatex/src/metadata/score/hideemptystavesinfirstsystem.ts b/packages/alphatex/src/metadata/score/hideemptystavesinfirstsystem.ts new file mode 100644 index 000000000..2274a306f --- /dev/null +++ b/packages/alphatex/src/metadata/score/hideemptystavesinfirstsystem.ts @@ -0,0 +1,26 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const hideEmptyStavesInFirstSystem: MetadataTagDefinition = { + tag: '\\hideEmptyStavesInFirstSystem', + snippet: '\\hideEmptyStavesInFirstSystem', + shortDescription: `Hide empty staves in first system.`, + signatures: [ + { + parameters: [] + } + ], + examples: { + options: { display: {systemsLayoutMode: 'UseModelLayout'}}, + tex: ` + \\hideEmptyStaves + \\hideEmptyStavesInFirstSystem + \\defaultSystemsLayout 3 + \\track "Track 1" + C4 * 4 | C4 * 4 | C4 * 4 | + r | r | r | + \\track "Track 2" + r | r | r | + r | C4.1 | r | + ` + } +} diff --git a/packages/alphatex/src/metadata/score/showsinglestaffbrackets.ts b/packages/alphatex/src/metadata/score/showsinglestaffbrackets.ts new file mode 100644 index 000000000..40c4d4bc3 --- /dev/null +++ b/packages/alphatex/src/metadata/score/showsinglestaffbrackets.ts @@ -0,0 +1,28 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const showSingleStaffBrackets: MetadataTagDefinition = { + tag: '\\showSingleStaffBrackets', + snippet: '\\showSingleStaffBrackets', + shortDescription: `Show brackets and braces on single staves.`, + signatures: [ + { + parameters: [] + } + ], + examples: { + options: { display: {systemsLayoutMode: 'UseModelLayout'}}, + tex: ` + \\hideEmptyStaves + \\hideEmptyStavesInFirstSystem + \\showSingleStaffBrackets + \\defaultSystemsLayout 3 + \\track "Track 1" + \\staff { score } + C4 * 4 | C4 * 4 | C4 * 4 | + C4 * 4 | C4 * 4 | C4 * 4 | + \\staff { score } + r | r | r | + r | C4.1 | r | + ` + } +} diff --git a/packages/alphatex/src/properties/beat/tp.ts b/packages/alphatex/src/properties/beat/tp.ts index a9084ccbe..cf06c2c02 100644 --- a/packages/alphatex/src/properties/beat/tp.ts +++ b/packages/alphatex/src/properties/beat/tp.ts @@ -1,4 +1,5 @@ import * as alphaTab from '@coderline/alphatab'; +import { enumParameter } from '@coderline/alphatab-alphatex/enum'; import type { PropertyDefinition } from '@coderline/alphatab-alphatex/types'; export const tp: PropertyDefinition = { @@ -10,20 +11,36 @@ export const tp: PropertyDefinition = { { parameters: [ { - name: 'speed', - shortDescription: 'The tremolo picking speed', + name: 'marks', + shortDescription: 'The number of tremolo marks', type: alphaTab.importer.alphaTex.AlphaTexNodeType.Number, parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + valuesOnlyForCompletion: true, values: [ - { name: '8', snippet: '8', shortDescription: '8th Notes' }, - { name: '16', snippet: '16', shortDescription: '16th Notes' }, - { name: '32', snippet: '32', shortDescription: '32nd Notes' } + { name: '1', snippet: '1', shortDescription: '1 tremolo mark (8th notes)' }, + { name: '2', snippet: '2', shortDescription: '2 tremolo mark (16th notes)' }, + { name: '3', snippet: '3', shortDescription: '3 tremolo mark (32nd notes)' }, + { name: '4', snippet: '4', shortDescription: '4 tremolo mark (64th notes)' }, + { name: '5', snippet: '5', shortDescription: '5 tremolo mark (128th notes)' } ] + }, + { + name: 'style', + shortDescription: 'The tremolo style', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Optional, + ...enumParameter('TremoloPickingStyle') } ] } ], - examples: ` - 3.3{tp 8} 3.3{tp 16} 3.3{tp 32} + examples: [ + ` + 3.3{tp 1} 3.3{tp 2} 3.3{tp 3} + `, + ` + \\title "Buzz Rolls" + 3.3{tp (0 buzzRoll)} // no audio + 3.3{tp (1 buzzRoll)} // 8th notes tremolo shown as buzzroll ` + ] }; diff --git a/packages/csharp/package.json b/packages/csharp/package.json index adc804050..fdb3aadfb 100644 --- a/packages/csharp/package.json +++ b/packages/csharp/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-csharp", - "version": "1.7.1", + "version": "1.8.0", "description": "The C# target of alphaTab.", "private": true, "type": "module", @@ -13,6 +13,6 @@ }, "devDependencies": { "@coderline/alphatab-transpiler": "*", - "rimraf": "^6.1.0" + "rimraf": "^6.1.2" } } diff --git a/packages/csharp/src/AlphaTab.Test/Test/Globals.cs b/packages/csharp/src/AlphaTab.Test/Test/Globals.cs index 9a9ded2ba..2c77499f1 100644 --- a/packages/csharp/src/AlphaTab.Test/Test/Globals.cs +++ b/packages/csharp/src/AlphaTab.Test/Test/Globals.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Reflection; -using System.Threading; -using AlphaTab.Collections; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace AlphaTab.Test; @@ -26,6 +24,11 @@ public static Expector Expect(T actual) return new Expector(actual); } + public static Expector Expect(T actual, string message) + { + return new Expector(actual, message); + } + public static Expector Expect(Action actual) { return new Expector(actual); @@ -52,41 +55,50 @@ public static string UseSnapshotValue(string baseName, string hint) internal class NotExpector { - private readonly T _actual; + private readonly T? _actual; public NotExpector Be => this; + private readonly string? _message; - public NotExpector(T actual) + public NotExpector(T? actual, string? message = null) { _actual = actual; + _message = message; } public void Ok() { if (_actual is string s) { - Assert.IsTrue(string.IsNullOrEmpty(s)); + Assert.IsTrue(string.IsNullOrEmpty(s), _message); } else { - Assert.AreEqual(default!, _actual); + Assert.AreEqual(default!, _actual, _message); } } + + public void Undefined() + { + Assert.IsNotNull(_actual, _message); + } } internal class Expector { - private readonly T _actual; + private readonly T? _actual; + private readonly string? _message; - public Expector(T actual) + public Expector(T? actual, string? message = null) { _actual = actual; + _message = message; } public Expector To => this; public NotExpector Not() { - return new NotExpector(_actual); + return new NotExpector(_actual, _message); } public Expector Be => this; @@ -104,28 +116,30 @@ public void Equal(object? expected, string? message = null) expected = (int)d; } - Assert.AreEqual(expected, _actual, message); + Assert.AreEqual(expected, _actual, message ?? _message); } public void LessThan(double expected) { if (_actual is IComparable d) { - Assert.IsTrue(d.CompareTo(expected) < 0, $"Expected Expected[{d}] < Actual[{_actual}]"); + Assert.IsTrue(d.CompareTo(expected) < 0, _message ?? $"Expected Expected[{d}] < Actual[{_actual}]"); } } - public void GreaterThan(double expected) + public void GreaterThan(double expected, string? message = null) { if (_actual is int i) { - Assert.IsTrue(i.CompareTo(expected) > 0, + Assert.IsTrue(i > expected, + _message ?? message ?? $"Expected {expected} to be greater than {_actual}"); } if (_actual is double d) { Assert.IsTrue(d.CompareTo(expected) > 0, + _message ?? message ?? $"Expected {expected} to be greater than {_actual}"); } } @@ -135,7 +149,7 @@ public void CloseTo(double expected, double delta, string? message = null) if (_actual is IConvertible c) { Assert.AreEqual(expected, - c.ToDouble(System.Globalization.CultureInfo.InvariantCulture), delta, message); + c.ToDouble(System.Globalization.CultureInfo.InvariantCulture), delta, message ?? _message); } else { @@ -150,19 +164,24 @@ public void ToBe(object expected) expected = (double)i; } - Assert.AreEqual(expected, _actual); + Assert.AreEqual(expected, _actual, _message); } public void Ok() { - Assert.AreNotEqual(default!, _actual); + Assert.AreNotEqual(default!, _actual, _message); + } + + public void Undefined() + { + Assert.IsNull(_actual, _message); } public void Length(int length) { if (_actual is ICollection collection) { - Assert.AreEqual(length, collection.Count); + Assert.AreEqual(length, collection.Count, _message); } else { @@ -174,7 +193,7 @@ public void Contain(object element) { if (_actual is ICollection collection) { - CollectionAssert.Contains(collection, element); + CollectionAssert.Contains(collection, element, _message); } else { @@ -186,7 +205,7 @@ public void True() { if (_actual is bool b) { - Assert.IsTrue(b); + Assert.IsTrue(b, _message); } else { @@ -198,7 +217,7 @@ public void False() { if (_actual is bool b) { - Assert.IsFalse(b); + Assert.IsFalse(b, _message); } else { @@ -214,7 +233,7 @@ public void Throw(Type expected) try { d(); - Assert.Fail("Did not throw error"); + Assert.Fail(_message ?? "Did not throw error"); } catch (Exception e) { @@ -224,7 +243,7 @@ public void Throw(Type expected) } } - Assert.Fail("Exception type didn't match"); + Assert.Fail(_message ?? "Exception type didn't match"); } else { @@ -266,7 +285,7 @@ public void ToMatchSnapshot(string hint = "") var error = snapshotFile.Match(snapshotName, _actual); if (!string.IsNullOrEmpty(error)) { - Assert.Fail(error); + Assert.Fail((_message ?? "") + error); } } diff --git a/packages/csharp/src/AlphaTab.Test/TestPlatform.cs b/packages/csharp/src/AlphaTab.Test/TestPlatform.cs index b26da441b..c8ca09f64 100644 --- a/packages/csharp/src/AlphaTab.Test/TestPlatform.cs +++ b/packages/csharp/src/AlphaTab.Test/TestPlatform.cs @@ -51,7 +51,7 @@ public static async Task LoadFileAsJson(string path) Converters = { new ArrayTupleConverterFactory() } }; - private class ArrayTupleConverterFactory :JsonConverterFactory + private class ArrayTupleConverterFactory : JsonConverterFactory { public override bool CanConvert(Type typeToConvert) { @@ -80,7 +80,7 @@ public override JsonConverter CreateConverter( typeof(ArrayTupleConverter<,>).MakeGenericType(keyType, valueType), BindingFlags.Instance | BindingFlags.Public, binder: null, - args: new object[]{options}, + args: new object[] { options }, culture: null)!; return converter; @@ -262,4 +262,19 @@ public static string GetConstructorName(object val) _ => val.GetType().Name }; } + + public static string CurrentTestName + { + get + { + var testMethodInfo = TestMethodAccessor.CurrentTest; + if (testMethodInfo == null) + { + return ""; + } + var testName = testMethodInfo.MethodInfo.GetCustomAttribute()! + .DisplayName; + return testName ?? ""; + } + } } diff --git a/packages/csharp/src/AlphaTab.Windows/NAudioSynthOutput.cs b/packages/csharp/src/AlphaTab.Windows/NAudioSynthOutput.cs index 568aee037..01a1b1ff4 100644 --- a/packages/csharp/src/AlphaTab.Windows/NAudioSynthOutput.cs +++ b/packages/csharp/src/AlphaTab.Windows/NAudioSynthOutput.cs @@ -96,7 +96,10 @@ public void Play() /// public void Pause() { - _context!.Pause(); + if (_context!.PlaybackState == PlaybackState.Playing) + { + _context!.Pause(); + } } /// diff --git a/packages/csharp/src/AlphaTab.Windows/Themes/Generic.xaml b/packages/csharp/src/AlphaTab.Windows/Themes/Generic.xaml index 4097ad141..8c0ef4717 100644 --- a/packages/csharp/src/AlphaTab.Windows/Themes/Generic.xaml +++ b/packages/csharp/src/AlphaTab.Windows/Themes/Generic.xaml @@ -11,7 +11,9 @@ + Padding="{TemplateBinding Padding}" + VerticalScrollBarVisibility="Auto" + HorizontalScrollBarVisibility="Auto"> diff --git a/packages/csharp/src/AlphaTab.Windows/WinForms/ControlContainer.cs b/packages/csharp/src/AlphaTab.Windows/WinForms/ControlContainer.cs index fba7e4703..338a4da14 100644 --- a/packages/csharp/src/AlphaTab.Windows/WinForms/ControlContainer.cs +++ b/packages/csharp/src/AlphaTab.Windows/WinForms/ControlContainer.cs @@ -1,4 +1,5 @@ -using System.Drawing; +using System; +using System.Drawing; using System.Windows.Forms; using AlphaTab.Platform; @@ -45,14 +46,20 @@ public ControlContainer(Control control) public double Width { get => Control.ClientSize.Width - Control.Padding.Horizontal; - set => Control.Width = (int)value + Control.Padding.Horizontal; + set => Control.BeginInvoke(() => + { + Control.Width = (int)value + Control.Padding.Horizontal; + }); } public double Height { get => Control.ClientSize.Height - Control.Padding.Vertical; - set => Control.Height = (int)value + Control.Padding.Vertical; + set => Control.BeginInvoke(() => + { + Control.Height = (int)value + Control.Padding.Vertical; + }); } public bool IsVisible => Control.Visible && Control.Width > 0; @@ -62,10 +69,14 @@ public double ScrollLeft get => Control is ScrollableControl scroll ? scroll.AutoScrollPosition.X : 0; set { - if (Control is ScrollableControl scroll) + Control.BeginInvoke(() => { - scroll.AutoScrollPosition = new Point((int)value, scroll.AutoScrollPosition.Y); - } + if (Control is ScrollableControl scroll) + { + scroll.AutoScrollPosition = + new Point((int)value, scroll.AutoScrollPosition.Y); + } + }); } } @@ -74,42 +85,44 @@ public double ScrollTop get => Control is ScrollableControl scroll ? scroll.VerticalScroll.Value : 0; set { - if (Control is ScrollableControl scroll) + Control.BeginInvoke(() => { - scroll.AutoScrollPosition = new Point(scroll.AutoScrollPosition.X, (int)value); - } + if (Control is ScrollableControl scroll) + { + scroll.AutoScrollPosition = + new Point(scroll.AutoScrollPosition.X, (int)value); + } + }); } } public void AppendChild(IContainer child) { - Control.Controls.Add(((ControlContainer)child).Control); + Control.BeginInvoke(() => { Control.Controls.Add(((ControlContainer)child).Control); }); } public void StopAnimation() { - //Control.BeginAnimation(Canvas.LeftProperty, null); } public void TransitionToX(double duration, double x) { - // TODO: Animation - Control.Left = (int)x; - - //Control.BeginAnimation(Canvas.LeftProperty, - // new DoubleAnimation(x, new Duration(TimeSpan.FromMilliseconds(duration)))); + Control.BeginInvoke(() => { Control.Left = (int)x; }); } public void Clear() { - Control.Controls.Clear(); + Control.BeginInvoke(() => { Control.Controls.Clear(); }); } public void SetBounds(double x, double y, double w, double h) { - Control.Left = (int)x; - Control.Top = (int)y; - Control.Width = (int)w; - Control.Height = (int)h; + Control.BeginInvoke(() => + { + Control.Left = (int)x; + Control.Top = (int)y; + Control.Width = (int)w; + Control.Height = (int)h; + }); } public IEventEmitter Resize { get; set; } diff --git a/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs b/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs index 9a5427212..bd4518ffd 100644 --- a/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs +++ b/packages/csharp/src/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs @@ -136,7 +136,6 @@ public override void BeginUpdateRenderResults(RenderFinishedEventArgs? r) { SettingsContainer.BeginInvoke((Action)(renderResult => { - if (renderResult == null || !_resultIdToElementLookup.TryGetValue(renderResult.Id, out var placeholder)) { @@ -149,7 +148,7 @@ public override void BeginUpdateRenderResults(RenderFinishedEventArgs? r) switch (body) { case string _: - // TODO: svg support + // NOTE: no svg support return; case AlphaSkiaImage skiaImage: using (skiaImage) @@ -304,5 +303,24 @@ public override void ScrollToX(IContainer scrollElement, double offset, double s var c = ((ControlContainer)scrollElement).Control; c.AutoScrollOffset = new Point((int)offset, c.AutoScrollOffset.Y); } + + public override void StopScrolling(IContainer scrollElement) + { + // no scrolling animations, hence nothing to do + } + + public override void SetCanvasOverflow(IContainer canvasElement, double overflow, + bool isVertical) + { + var c = ((ControlContainer)canvasElement).Control; + if (!(c is AlphaTabLayoutPanel p)) + { + return; + } + + p.Padding = isVertical + ? new Padding(0, 0, 0, (int)overflow) + : new Padding(0, 0, (int)overflow, 0); + } } } diff --git a/packages/csharp/src/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs b/packages/csharp/src/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs index 668d20777..13af754de 100644 --- a/packages/csharp/src/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs +++ b/packages/csharp/src/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs @@ -46,95 +46,119 @@ public FrameworkElementContainer(FrameworkElement control) public double Width { - get => (float) Control.ActualWidth; - set => Control.Width = value; + get => (float)Control.ActualWidth; + set => Control.Dispatcher.BeginInvoke((Action)(() => { Control.Width = value; })); } public double Height { - get => (float) Control.ActualHeight; - set => Control.Height = value; + get => (float)Control.ActualHeight; + set => Control.Dispatcher.BeginInvoke((Action)(() => { Control.Height = value; })); } public bool IsVisible => Control.IsVisible && Control.ActualWidth > 0; public double ScrollLeft { - get => Control is ScrollViewer scroll ? (float) scroll.HorizontalOffset : 0; + get => Control is ScrollViewer scroll ? (float)scroll.HorizontalOffset : 0; set { - if (Control is ScrollViewer scroll) + Control.Dispatcher.BeginInvoke((Action)(() => { - scroll.ScrollToHorizontalOffset(value); - } + if (Control is ScrollViewer scroll) + { + scroll.ScrollToHorizontalOffset(value); + } + })); } } public double ScrollTop { - get => Control is ScrollViewer scroll ? (float) scroll.VerticalOffset : 0; + get => Control is ScrollViewer scroll ? (float)scroll.VerticalOffset : 0; set { - if (Control is ScrollViewer scroll) + Control.Dispatcher.BeginInvoke((Action)(() => { - scroll.ScrollToVerticalOffset(value); - } + if (Control is ScrollViewer scroll) + { + scroll.ScrollToVerticalOffset(value); + } + })); } } public void AppendChild(IContainer child) { - if (Control is Panel p) - { - p.Children.Add(((FrameworkElementContainer) child).Control); - } - else if (Control is ScrollViewer s && s.Content is ContentControl sc) - { - sc.Content = ((FrameworkElementContainer) child).Control; - } - else if (Control is ScrollViewer ss && ss.Content is Decorator d) - { - d.Child = ((FrameworkElementContainer) child).Control; - } - else if (Control is ScrollViewer sss && sss.Content is Panel pp) - { - pp.Children.Add(((FrameworkElementContainer) child).Control); - } - else if (Control is ContentControl c) + Control.Dispatcher.BeginInvoke((Action)(() => { - c.Content = ((FrameworkElementContainer) child).Control; - } + if (Control is Panel p) + { + p.Children.Add(((FrameworkElementContainer)child).Control); + } + else if (Control is ScrollViewer s && s.Content is ContentControl sc) + { + sc.Content = ((FrameworkElementContainer)child).Control; + } + else if (Control is ScrollViewer ss && ss.Content is Decorator d) + { + d.Child = ((FrameworkElementContainer)child).Control; + } + else if (Control is ScrollViewer sss && sss.Content is Panel pp) + { + pp.Children.Add(((FrameworkElementContainer)child).Control); + } + else if (Control is ContentControl c) + { + c.Content = ((FrameworkElementContainer)child).Control; + } + })); } + private double _targetX = 0; public void StopAnimation() { - Control.BeginAnimation(Canvas.LeftProperty, null); + Control.Dispatcher.BeginInvoke((Action)(() => + { + Control.BeginAnimation(Canvas.LeftProperty, null); + Canvas.SetLeft(Control, _targetX); + })); } public void TransitionToX(double duration, double x) { - Control.BeginAnimation(Canvas.LeftProperty, - new DoubleAnimation(x, new Duration(TimeSpan.FromMilliseconds(duration)))); + _targetX = x; + Control.Dispatcher.BeginInvoke((Action)(() => + { + Control.BeginAnimation(Canvas.LeftProperty, + new DoubleAnimation(x, new Duration(TimeSpan.FromMilliseconds(duration)))); + })); } public void Clear() { - if (Control is Panel p) + Control.Dispatcher.BeginInvoke((Action)(() => { - p.Children.Clear(); - } + if (Control is Panel p) + { + p.Children.Clear(); + } + })); } public void SetBounds(double x, double y, double w, double h) { - Canvas.SetLeft(Control, x); - Canvas.SetTop(Control, y); - Control.Width = w; - Control.Height = h; + Control.Dispatcher.BeginInvoke((Action)(() => + { + Canvas.SetLeft(Control, x); + Canvas.SetTop(Control, y); + Control.Width = w; + Control.Height = h; + })); } public IEventEmitter Resize { get; set; } diff --git a/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs b/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs index 6506dd001..042051e27 100644 --- a/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs +++ b/packages/csharp/src/AlphaTab.Windows/Wpf/WpfUiFacade.cs @@ -6,6 +6,7 @@ using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; +using System.Windows.Media.Animation; using System.Windows.Shapes; using AlphaTab.Synth; using AlphaTab.Platform; @@ -158,7 +159,7 @@ public override void BeginUpdateRenderResults(RenderFinishedEventArgs? r) ImageSource? source = null; if (body is string) { - // TODO: svg support + // NOTE: no svg support return; } @@ -260,20 +261,22 @@ public override Cursors CreateCursors() IsHitTestVisible = false }; - barCursor.SetBinding(Shape.FillProperty, new Binding(nameof(SettingsContainer.BarCursorFill)) - { - Source = SettingsContainer - }); + barCursor.SetBinding(Shape.FillProperty, + new Binding(nameof(SettingsContainer.BarCursorFill)) + { + Source = SettingsContainer + }); var beatCursor = new Rectangle { IsHitTestVisible = false, Width = 3 }; - beatCursor.SetBinding(Shape.FillProperty, new Binding(nameof(SettingsContainer.BeatCursorFill)) - { - Source = SettingsContainer - }); + beatCursor.SetBinding(Shape.FillProperty, + new Binding(nameof(SettingsContainer.BeatCursorFill)) + { + Source = SettingsContainer + }); cursorWrapper.Children.Add(selectionWrapper); cursorWrapper.Children.Add(barCursor); @@ -351,23 +354,88 @@ public override void ScrollToY(IContainer scrollElement, double offset, double s { if (((FrameworkElementContainer)scrollElement).Control is ScrollViewer s) { - s.ScrollToVerticalOffset(offset); + if (speed < 10) + { + s.ScrollToVerticalOffset(offset); + } + else + { + s.BeginAnimation(ScrollViewerExtension.ScrollYProperty, + new DoubleAnimation(offset, + new Duration(TimeSpan.FromMilliseconds(speed)))); + } } - - //scrollElementWpf.BeginAnimation(ScrollViewer.VerticalOffsetProperty, - // new DoubleAnimation(offset, new System.Windows.Duration(TimeSpan.FromMilliseconds(speed)))); } public override void ScrollToX(IContainer scrollElement, double offset, double speed) { if (((FrameworkElementContainer)scrollElement).Control is ScrollViewer s) { - s.ScrollToHorizontalOffset(offset); + if (speed < 10) + { + s.ScrollToHorizontalOffset(offset); + } + else + { + s.BeginAnimation(ScrollViewerExtension.ScrollXProperty, + new DoubleAnimation(offset, + new Duration(TimeSpan.FromMilliseconds(speed)))); + } + } + } + + public override void StopScrolling(IContainer scrollElement) + { + if (((FrameworkElementContainer)scrollElement).Control is ScrollViewer s) + { + s.BeginAnimation(ScrollViewerExtension.ScrollXProperty, null); + s.BeginAnimation(ScrollViewerExtension.ScrollYProperty, null); } + } - //var scrollElementWpf = ((FrameworkElementContainer)scrollElement).Control; - //scrollElementWpf.BeginAnimation(ScrollViewer.HorizontalOffsetProperty, - // new DoubleAnimation(offset, new System.Windows.Duration(TimeSpan.FromMilliseconds(speed)))); + public override void SetCanvasOverflow(IContainer canvasElement, double overflow, + bool isVertical) + { + if (!(((FrameworkElementContainer)canvasElement).Control is Canvas c)) + { + return; + } + + c.Margin = isVertical + ? new Thickness(0, 0, 0, overflow) + : new Thickness(0, 0, overflow, 0); + } + } + + internal class ScrollViewerExtension + { + public static readonly DependencyProperty ScrollXProperty = + DependencyProperty.RegisterAttached( + "ScrollX", typeof(double), typeof(ScrollViewerExtension), + new PropertyMetadata(0.0, OnScrollXChanged)); + + + public static readonly DependencyProperty ScrollYProperty = + DependencyProperty.RegisterAttached( + "ScrollY", typeof(double), typeof(ScrollViewerExtension), + new PropertyMetadata(0.0, OnScrollYChanged)); + + private static void OnScrollXChanged(DependencyObject d, + DependencyPropertyChangedEventArgs e) + { + if (d is ScrollViewer s) + { + s.ScrollToHorizontalOffset((double)e.NewValue); + } + } + + private static void OnScrollYChanged(DependencyObject d, + DependencyPropertyChangedEventArgs e) + { + if (d is ScrollViewer s) + { + s.ScrollToVerticalOffset((double)e.NewValue); + } } } } diff --git a/packages/csharp/src/AlphaTab/Core/ArrayTuple.cs b/packages/csharp/src/AlphaTab/Core/ArrayTuple.cs index 53482883f..1f63a2745 100644 --- a/packages/csharp/src/AlphaTab/Core/ArrayTuple.cs +++ b/packages/csharp/src/AlphaTab/Core/ArrayTuple.cs @@ -3,10 +3,10 @@ /// /// A mixed-type array tuple (like [string, number] in JavaScript). /// -public readonly struct ArrayTuple +public class ArrayTuple { - public T0 V0 { get; } - public T1 V1 { get; } + public T0 V0 { get; set; } + public T1 V1 { get; set;} public ArrayTuple(T0 v0, T1 v1) { diff --git a/packages/csharp/src/AlphaTab/Core/Console.cs b/packages/csharp/src/AlphaTab/Core/Console.cs index 0f4053500..9a81cfd6d 100644 --- a/packages/csharp/src/AlphaTab/Core/Console.cs +++ b/packages/csharp/src/AlphaTab/Core/Console.cs @@ -7,24 +7,24 @@ internal class Console public virtual void Debug(string format, object?[]? details) { var message = details != null ? string.Format(format, details) : format; - Trace.Write(message, "AlphaTab Debug"); + Trace.WriteLine(message, "AlphaTab Debug"); } public virtual void Warn(string format, object?[]? details) { var message = details != null ? string.Format(format, details) : format; - Trace.Write(message, "AlphaTab Warn"); + Trace.WriteLine(message, "AlphaTab Warn"); } public virtual void Info(string format, object?[]? details) { var message = details != null ? string.Format(format, details) : format; - Trace.Write(message, "AlphaTab Info"); + Trace.WriteLine(message, "AlphaTab Info"); } public virtual void Error(string format, object?[]? details) { var message = details != null ? string.Format(format, details) : format; - Trace.Write(message, "AlphaTab Error"); + Trace.WriteLine(message, "AlphaTab Error"); } -} \ No newline at end of file +} diff --git a/packages/csharp/src/AlphaTab/Core/TypeHelper.cs b/packages/csharp/src/AlphaTab/Core/TypeHelper.cs index ef2cea204..95dec6afe 100644 --- a/packages/csharp/src/AlphaTab/Core/TypeHelper.cs +++ b/packages/csharp/src/AlphaTab/Core/TypeHelper.cs @@ -102,6 +102,16 @@ public static IList Splice(this IList data, double start, double delete return new List(items); } + public static IList Splice(this IList data, double start, double deleteCount, + IEnumerable newItems) + { + var items = data.GetRange((int)start, (int)deleteCount); + data.RemoveRange((int)start, (int)deleteCount); + data.InsertRange((int)start, newItems); + + return new List(items); + } + public static IList Slice(this IList data) { return new AlphaTab.Collections.List(data); diff --git a/packages/csharp/src/AlphaTab/Environment.cs b/packages/csharp/src/AlphaTab/Environment.cs index 0c57b357d..0948893bc 100644 --- a/packages/csharp/src/AlphaTab/Environment.cs +++ b/packages/csharp/src/AlphaTab/Environment.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -57,4 +58,9 @@ public static string QuoteJsonString(string value) { return Json.QuoteJsonString(value); } + + internal static void SortDescending(System.Collections.Generic.IList list) + { + list.Sort((a, b) => b - a); + } } diff --git a/packages/csharp/src/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs b/packages/csharp/src/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs index 798e21e79..f79c64142 100644 --- a/packages/csharp/src/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs +++ b/packages/csharp/src/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs @@ -12,7 +12,7 @@ internal abstract class AlphaSynthWorkerApiBase : IAlphaSynth private LogLevel _logLevel; private readonly double _bufferTimeInMilliseconds; - public AlphaSynth Player { get; private set; } + public AlphaSynth? Player { get; private set; } protected AlphaSynthWorkerApiBase(ISynthOutput output, LogLevel logLevel, double bufferTimeInMilliseconds) { @@ -45,10 +45,10 @@ protected void Initialize() DispatchOnUiThread(OnReady); } - public bool IsReady => Player.IsReady; - public bool IsReadyForPlayback => Player.IsReadyForPlayback; + public bool IsReady => Player?.IsReady ?? false; + public bool IsReadyForPlayback => Player?.IsReadyForPlayback ?? false; - public PlayerState State => Player == null ? PlayerState.Paused : Player.State; + public PlayerState State => Player?.State ?? PlayerState.Paused; public LogLevel LogLevel { diff --git a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs index cc1a2729c..0ba2a22eb 100644 --- a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs +++ b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs @@ -101,15 +101,15 @@ private bool CheckAccess() return Thread.CurrentThread == _workerThread; } - public void Render() + public void Render(RenderHints? renderHints = null) { if (CheckAccess()) { - _renderer.Render(); + _renderer.Render(renderHints); } else { - _workerQueue.Add(Render); + _workerQueue.Add(() => Render(renderHints)); } } @@ -154,17 +154,17 @@ public void ResizeRender() } } - public void RenderScore(Score? score, IList? trackIndexes) + public void RenderScore(Score? score, IList? trackIndexes, RenderHints? renderHints = null) { if (CheckAccess()) { - _renderer.RenderScore(score, trackIndexes); + _renderer.RenderScore(score, trackIndexes, renderHints); } else { _workerQueue.Add(() => RenderScore(score, - trackIndexes)); + trackIndexes, renderHints)); } } @@ -212,4 +212,4 @@ protected virtual void OnPostRenderFinished() { ((EventEmitter)PostRenderFinished).Trigger(); } -} \ No newline at end of file +} diff --git a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedUiFacade.cs b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedUiFacade.cs index 1d0b1f832..527ee4638 100644 --- a/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedUiFacade.cs +++ b/packages/csharp/src/AlphaTab/Platform/CSharp/ManagedUiFacade.cs @@ -33,6 +33,10 @@ public virtual void Initialize(AlphaTabApiBase api, TSettings setting TotalResultCount = new ConcurrentQueue(); } + 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); diff --git a/packages/csharp/src/Directory.Build.props b/packages/csharp/src/Directory.Build.props index 1fcbb7c2e..1d300035e 100644 --- a/packages/csharp/src/Directory.Build.props +++ b/packages/csharp/src/Directory.Build.props @@ -2,8 +2,8 @@ portable true - 1.7.1 - 1.7.1.0 + 1.8.0 + 1.8.0.0 $(AssemblyVersion) Danielku15 CoderLine @@ -17,7 +17,7 @@ git music,notation,engraver,renderer true - snupkg + snupkg true - + \ No newline at end of file diff --git a/packages/kotlin/package.json b/packages/kotlin/package.json index 46be75e79..9a3d25b60 100644 --- a/packages/kotlin/package.json +++ b/packages/kotlin/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-kotlin", - "version": "1.7.1", + "version": "1.8.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 aa43db27c..e7710b8ef 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.7.1-SNAPSHOT" +var libVersion = "1.8.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/EnvironmentPartials.kt b/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt index e112c5dcb..d3f534bcc 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/EnvironmentPartials.kt @@ -1,5 +1,6 @@ package alphaTab +import alphaTab.collections.DoubleList import alphaTab.platform.Json import alphaTab.platform.android.AndroidCanvas import alphaTab.platform.android.AndroidEnvironment @@ -52,5 +53,10 @@ internal class EnvironmentPartials { @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() + + } } diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt b/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt index 98edd9470..06466dfbf 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt @@ -129,6 +129,10 @@ public class DoubleList : IDoubleIterable { _items.sort(0, _size) } + internal fun sortDescending() { + _items.sortDescending(0, _size) + } + public fun shift(): Double { val d = _items[0] if (_items.size > 1) { @@ -148,6 +152,19 @@ public class DoubleList : IDoubleIterable { return Iterator(this) } + public fun splice(start: Double, deleteCount: Double) { + val firstAfterDelete = (start + deleteCount).toInt() + val itemsAfterDelete = this.length.toInt() - firstAfterDelete; + if(itemsAfterDelete > 0) { + _items.copyInto( + _items, + start.toInt(), + firstAfterDelete, + itemsAfterDelete + ) + } + this._size -= deleteCount.toInt() + } private class Iterator(private val list: DoubleList) : DoubleIterator() { private var _index = 0 override fun hasNext(): Boolean { diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/collections/List.kt b/packages/kotlin/src/android/src/main/java/alphaTab/collections/List.kt index 4723be953..951ada471 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/collections/List.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/collections/List.kt @@ -175,6 +175,16 @@ public class List : Iterable { _data.addAll(start.toInt(), newElements.toList()) } + public fun splice(start: Double, deleteCount: Double, newElements: Iterable) { + var actualStart = start.toInt() + if (actualStart < 0) { + actualStart += _data.size + } + + _data.removeRange(start.toInt(), (start + deleteCount).toInt()) + _data.addAll(start.toInt(), newElements.toList()) + } + public fun join(separator: String): String { return _data.map { it.toTemplate() }.joinToString(separator) } diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/core/ArrayTuple.kt b/packages/kotlin/src/android/src/main/java/alphaTab/core/ArrayTuple.kt index 5bcff813a..9fb2d0de1 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/core/ArrayTuple.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/core/ArrayTuple.kt @@ -2,33 +2,33 @@ package alphaTab.core // generic variant (would box) interface IArrayTuple { - val v0: T0 - val v1: T1 + var v0: T0 + var v1: T1 operator fun component1(): T0 operator fun component2(): T1 } -data class ArrayTuple(override val v0: T0, override val v1: T1) : IArrayTuple +data class ArrayTuple(override var v0: T0, override var v1: T1) : IArrayTuple // strong typed variants (Kotlin should compile this to non-boxed variants where possible) interface IObjectBooleanArrayTuple : IArrayTuple -data class ObjectBooleanArrayTuple(override val v0: T, override val v1: Boolean) : +data class ObjectBooleanArrayTuple(override var v0: T, override var v1: Boolean) : IObjectBooleanArrayTuple interface IObjectDoubleArrayTuple : IArrayTuple -data class ObjectDoubleArrayTuple(override val v0: T, override val v1: Double) : +data class ObjectDoubleArrayTuple(override var v0: T, override var v1: Double) : IObjectDoubleArrayTuple interface IDoubleObjectArrayTuple : IArrayTuple -data class DoubleObjectArrayTuple(override val v0: Double, override val v1: T) : +data class DoubleObjectArrayTuple(override var v0: Double, override var v1: T) : IDoubleObjectArrayTuple interface IDoubleDoubleArrayTuple : IArrayTuple -data class DoubleDoubleArrayTuple(override val v0: Double = 0.0, override val v1: Double = 0.0) : +data class DoubleDoubleArrayTuple(override var v0: Double = 0.0, override var v1: Double = 0.0) : IDoubleDoubleArrayTuple interface IDoubleBooleanArrayTuple : IArrayTuple -data class DoubleBooleanArrayTuple(override val v0: Double = 0.0, override val v1: Boolean = false) : +data class DoubleBooleanArrayTuple(override var v0: Double = 0.0, override var v1: Boolean = false) : IDoubleBooleanArrayTuple diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidRootViewContainer.kt b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidRootViewContainer.kt index c24168653..a96c9b434 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidRootViewContainer.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/platform/android/AndroidRootViewContainer.kt @@ -3,6 +3,7 @@ package alphaTab.platform.android import alphaTab.* import alphaTab.platform.IContainer import alphaTab.platform.IMouseEventArgs +import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.view.View import android.widget.HorizontalScrollView @@ -15,14 +16,14 @@ import kotlin.contracts.ExperimentalContracts internal class AndroidRootViewContainer : IContainer, View.OnLayoutChangeListener { private val _outerScroll: HorizontalScrollView private val _innerScroll: ScrollView - private val _uiInvoke: ( action: (() -> Unit) ) -> Unit + private val _uiInvoke: (action: (() -> Unit)) -> Unit internal val renderSurface: AlphaTabRenderSurface constructor( outerScroll: HorizontalScrollView, innerScroll: ScrollView, renderSurface: AlphaTabRenderSurface, - uiInvoke: ( action: (() -> Unit) ) -> Unit + uiInvoke: (action: (() -> Unit)) -> Unit ) { _uiInvoke = uiInvoke _innerScroll = innerScroll @@ -101,21 +102,53 @@ internal class AndroidRootViewContainer : IContainer, View.OnLayoutChangeListene } } - fun scrollToX(offset: Double) { + private var _scrollToX: ObjectAnimator? = null + private var _scrollToY: ObjectAnimator? = null + + fun scrollToX(offset: Double, speed: Double) { _uiInvoke { - _outerScroll.smoothScrollTo( - (offset * Environment.highDpiFactor).toInt(), - _outerScroll.scrollY - ) + _scrollToX?.end() + val scrollX = (offset * Environment.highDpiFactor).toInt() + if (speed < 10) { + _outerScroll.scrollX = scrollX + } else { + + val animation = ObjectAnimator.ofInt( + _outerScroll, + "scrollX", + scrollX + ).setDuration(speed.toLong()) + animation.start() + _scrollToX = animation + } + } + } + + fun scrollToY(offset: Double, speed: Double) { + _uiInvoke { + _scrollToY?.end() + val scrollY = (offset * Environment.highDpiFactor).toInt() + if (speed < 10) { + _innerScroll.scrollY = scrollY + } else { + + val animation = ObjectAnimator.ofInt( + _innerScroll, + "scrollY", + scrollY + ).setDuration(speed.toLong()) + animation.start() + _scrollToY = animation + } } } - fun scrollToY(offset: Double) { + fun stopScrolling(){ _uiInvoke { - _innerScroll.smoothScrollTo( - _innerScroll.scrollX, - (offset * Environment.highDpiFactor).toInt() - ) + _scrollToY?.end() + _scrollToY = null + _scrollToX?.end() + _scrollToX = null } } } 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 index 560a3f5eb..b344357da 100644 --- 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 @@ -6,6 +6,7 @@ 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 @@ -95,11 +96,11 @@ internal class AndroidThreadScoreRenderer : IScoreRenderer, Runnable { } } - override fun render() { + override fun render(renderHints: RenderHints?) { if (checkAccess()) { - renderer.render() + renderer.render(renderHints) } else { - _workerQueue.add { render() } + _workerQueue.add { render(renderHints) } } } @@ -119,14 +120,15 @@ internal class AndroidThreadScoreRenderer : IScoreRenderer, Runnable { } } - override fun renderScore(score: Score?, trackIndexes: DoubleList?) { + override fun renderScore(score: Score?, trackIndexes: DoubleList?, renderHints: RenderHints?) { if (checkAccess()) { - renderer.renderScore(score, trackIndexes) + renderer.renderScore(score, trackIndexes, renderHints) } else { _workerQueue.add { renderScore( score, - trackIndexes + trackIndexes, + renderHints ) } } 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 c4c921cf9..479b980fc 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 @@ -186,7 +186,10 @@ internal class AndroidUiFacade : IUiFacade { synthToUse = this.createWorkerPlayer(); } - return AndroidThreadAlphaSynthAudioExporter(synthToUse as AndroidThreadAlphaSynthWorkerPlayer, needNewWorker); + return AndroidThreadAlphaSynthAudioExporter( + synthToUse as AndroidThreadAlphaSynthWorkerPlayer, + needNewWorker + ); } @@ -382,12 +385,32 @@ internal class AndroidUiFacade : IUiFacade { override fun scrollToX(scrollElement: IContainer, offset: Double, speed: Double) { val view = (scrollElement as AndroidRootViewContainer) - view.scrollToX(offset) + view.scrollToX(offset, speed) } override fun scrollToY(scrollElement: IContainer, offset: Double, speed: Double) { val view = (scrollElement as AndroidRootViewContainer) - view.scrollToY(offset) + view.scrollToY(offset, speed) + } + + override fun stopScrolling(scrollElement: IContainer) { + val view = (scrollElement as AndroidRootViewContainer) + view.stopScrolling() + } + + override fun setCanvasOverflow( + canvasElement: IContainer, + overflow: Double, + isVertical: Boolean + ) { + val view = (canvasElement as AndroidViewContainer).view; + if (view is AlphaTabRenderSurface) { + if (isVertical) { + view.setPadding(0, 0, 0, overflow.toInt()) + } else { + view.setPadding(0, 0, overflow.toInt(), 0) + } + } } override fun load( 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 1533343ba..cb8aa34c4 100644 --- a/packages/kotlin/src/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt +++ b/packages/kotlin/src/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt @@ -12,6 +12,8 @@ import alphaTab.core.DoubleDoubleArrayTuple import alphaTab.core.IArrayTuple import alphaTab.core.IDoubleDoubleArrayTuple import alphaTab.core.IRecord +import alphaTab.core.TestGlobals +import alphaTab.core.TestName import alphaTab.core.ecmaScript.Record import alphaTab.core.ecmaScript.Uint8Array import alphaTab.core.toInvariantString @@ -21,11 +23,13 @@ import com.beust.klaxon.JsonValue import com.beust.klaxon.Klaxon import com.beust.klaxon.KlaxonException import kotlinx.coroutines.CompletableDeferred +import org.junit.Assert import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream import java.io.OutputStream import java.io.OutputStreamWriter +import java.lang.reflect.Method import java.nio.file.Paths import kotlin.contracts.ExperimentalContracts import kotlin.reflect.KClass @@ -297,5 +301,35 @@ class TestPlatformPartials { null -> "" else -> o.javaClass.name } + + public fun findTestMethod(): Method { + val walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + var testMethod: Method? = null + walker.forEach { frame -> + if (testMethod == null) { + val method = frame.declaringClass.getDeclaredMethod( + frame.methodName, + *frame.methodType.parameterArray() + ) + + if (method.getAnnotation(org.junit.Test::class.java) != null) { + testMethod = method + } + } + } + + if (testMethod == null) { + Assert.fail("No information about current test available, cannot find test snapshot"); + } + + return testMethod!! + } + + internal val currentTestName: String + get() { + val testMethodInfo = findTestMethod() + val testName = testMethodInfo.getAnnotation(TestName::class.java)!!.name + return testName + } } } diff --git a/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt b/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt index ca022e011..c8827432e 100644 --- a/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt +++ b/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt @@ -24,36 +24,42 @@ class assert { } } -class NotExpector(private val actual: T) { +class NotExpector(private val actual: T, private val message: String? = null) { val be get() = this fun ok() { when (actual) { is Int -> { - Assert.assertEquals(0, actual) + Assert.assertEquals(message, 0, actual) } is Double -> { - Assert.assertEquals(0.0, actual, 0.0) + Assert.assertEquals(message, 0.0, actual, 0.0) } is String -> { - Assert.assertEquals("", actual) + Assert.assertEquals(message, "", actual) } else -> { - Assert.assertNull(actual) + Assert.assertNull(message, actual) } } } + + + fun undefined() { + Assert.assertNotNull(message, actual) + } + } -class Expector(private val actual: T) { +class Expector(private val actual: T, private val message: String? = null) { val to get() = this - fun not() = NotExpector(actual) + fun not() = NotExpector(actual, message) val be get() = this @@ -82,14 +88,14 @@ class Expector(private val actual: T) { } } - Assert.assertEquals(message, expectedTyped, actualToCheck as Any?) + Assert.assertEquals(this.message ?: message, expectedTyped, actualToCheck as Any?) } fun lessThan(expected: Double) { if (actual is Number) { Assert.assertTrue( - "Expected $actual to be less than $expected", + this.message ?: "Expected $actual to be less than $expected", actual.toDouble() < expected ) } else { @@ -98,10 +104,10 @@ class Expector(private val actual: T) { } - fun greaterThan(expected: Double) { + fun greaterThan(expected: Double, message: String? = null) { if (actual is Number) { Assert.assertTrue( - "Expected $actual to be greater than $expected", + this.message ?: (message ?: "Expected $actual to be greater than $expected"), actual.toDouble() > expected ) } else { @@ -111,7 +117,7 @@ class Expector(private val actual: T) { fun closeTo(expected: Double, delta: Double, message: String? = null) { if (actual is Number) { - Assert.assertEquals(message, expected, actual.toDouble(), delta) + Assert.assertEquals(this.message ?: message, expected, actual.toDouble(), delta) } else { Assert.fail("toBeCloseTo can only be used with numeric operands"); } @@ -119,11 +125,11 @@ class Expector(private val actual: T) { fun length(expected: Double) { if (actual is alphaTab.collections.List<*>) { - Assert.assertEquals(expected.toInt(), actual.length.toInt()) + Assert.assertEquals(message, expected.toInt(), actual.length.toInt()) } else if (actual is alphaTab.collections.DoubleList) { - Assert.assertEquals(expected.toInt(), actual.length.toInt()) + Assert.assertEquals(message, expected.toInt(), actual.length.toInt()) } else if (actual is alphaTab.collections.BooleanList) { - Assert.assertEquals(expected.toInt(), actual.length.toInt()) + Assert.assertEquals(message, expected.toInt(), actual.length.toInt()) } else { Assert.fail("length can only be used with collection operands"); } @@ -132,7 +138,7 @@ class Expector(private val actual: T) { fun contain(value: Any) { if (actual is Iterable<*>) { Assert.assertTrue( - "Expected collection ${actual.joinToString(",")} to contain $value", + message ?: "Expected collection ${actual.joinToString(",")} to contain $value", actual.contains(value) ) } else { @@ -144,22 +150,26 @@ class Expector(private val actual: T) { Assert.assertNotNull(actual) when (actual) { is Int -> { - Assert.assertNotEquals(0, actual) + Assert.assertNotEquals(message, 0, actual) } is Double -> { - Assert.assertNotEquals(0.0, actual) + Assert.assertNotEquals(message, 0.0, actual) } is String -> { - Assert.assertNotEquals("", actual) + Assert.assertNotEquals(message, "", actual) } } } + fun undefined() { + Assert.assertNull(message, actual) + } + fun `true`() { if (actual is Boolean) { - Assert.assertTrue(actual); + Assert.assertTrue(message, actual); } else { Assert.fail("toBeTrue can only be used on booleans:"); } @@ -167,7 +177,7 @@ class Expector(private val actual: T) { fun `false`() { if (actual is Boolean) { - Assert.assertFalse(actual); + Assert.assertFalse(message, actual); } else { Assert.fail("toBeFalse can only be used on booleans:"); } @@ -179,46 +189,24 @@ class Expector(private val actual: T) { if (actual is Function0<*>) { try { actual() - Assert.fail("Did not throw error " + expected.qualifiedName); + Assert.fail(message ?: ("Did not throw error " + expected.qualifiedName)); } catch (e: Throwable) { if (expected::class.isInstance(e::class)) { return; } } - Assert.fail("Exception type didn't match"); + Assert.fail(message ?: "Exception type didn't match"); } else { Assert.fail("ToThrowError can only be used with an exception"); } } - private fun findTestMethod(): Method { - val walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) - var testMethod: Method? = null - walker.forEach { frame -> - if (testMethod == null) { - val method = frame.declaringClass.getDeclaredMethod( - frame.methodName, - *frame.methodType.parameterArray() - ) - - if (method.getAnnotation(org.junit.Test::class.java) != null) { - testMethod = method - } - } - } - - if (testMethod == null) { - Assert.fail("No information about current test available, cannot find test snapshot"); - } - - return testMethod!! - } @ExperimentalUnsignedTypes @ExperimentalContracts fun toMatchSnapshot(hint: String = "") { - val testMethodInfo = findTestMethod() + val testMethodInfo = TestPlatformPartials.findTestMethod() val file = testMethodInfo.getAnnotation(SnapshotFile::class.java)?.path if (file.isNullOrEmpty()) { Assert.fail("Missing SnapshotFile annotation with path to .snap file") @@ -241,11 +229,6 @@ class Expector(private val actual: T) { val snapshotName = TestGlobals.useSnapshotValue(parts.joinToString(" "), hint); - - - - - val error = snapshotFile.match(snapshotName, actual) if (!error.isNullOrEmpty()) { Assert.fail(error) @@ -281,8 +264,8 @@ class TestGlobals { return "$fullName ${value.toInt()}"; } - fun expect(actual: T): Expector { - return Expector(actual); + fun expect(actual: T, message:String? = null): Expector { + return Expector(actual, message); } fun expect(actual: () -> Unit): Expector<() -> Unit> { diff --git a/packages/lsp/package.json b/packages/lsp/package.json index a5ebf31bb..8b5a7a816 100644 --- a/packages/lsp/package.json +++ b/packages/lsp/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-language-server", - "version": "1.7.1", + "version": "1.8.0", "description": "A language server for alphaTab providing coding assistance for alphaTex.", "keywords": [ "guitar", @@ -34,22 +34,22 @@ "test": "mocha" }, "dependencies": { - "@coderline/alphatab": "^1.7.1", + "@coderline/alphatab": "^1.8.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@microsoft/api-extractor": "^7.53.1", + "@biomejs/biome": "^2.3.11", + "@microsoft/api-extractor": "^7.55.2", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^24.8.1", + "@types/node": "^25.0.6", "assert": "^2.1.0", - "chai": "^6.2.0", + "chai": "^6.2.2", "mocha": "^11.7.4", - "rimraf": "^6.0.1", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3" }, "bin": "./dist/server.mjs", diff --git a/packages/lsp/src/server/completion.ts b/packages/lsp/src/server/completion.ts index 8c4892074..e93fe1ad9 100644 --- a/packages/lsp/src/server/completion.ts +++ b/packages/lsp/src/server/completion.ts @@ -6,6 +6,7 @@ import { durationChangeProperties, noteProperties, scoreMetaData, + staffMetaData, structuralMetaData } from '@coderline/alphatab-alphatex/definitions'; import type { @@ -37,6 +38,7 @@ interface CompletionItemWithData extends CompletionItem { const topLevelCompletions = [ ...createMetaDataDocCompletions(structuralMetaData), + ...createMetaDataDocCompletions(staffMetaData), ...createMetaDataDocCompletions(scoreMetaData), ...createMetaDataDocCompletions(barMetaData) ]; @@ -62,12 +64,12 @@ export function setupCompletion(connection: Connection, documents: TextDocuments } const endOfBar = - bar.pipe?.start!.offset ?? + bar.pipe?.start?.offset ?? (barIndex === document.ast.bars.length - 1 ? Number.MAX_SAFE_INTEGER : document.ast.bars[barIndex + 1].start!.offset); - const endOfBarMetaData = bar.beats[0]?.start!.offset ?? endOfBar; + const endOfBarMetaData = bar.beats[0]?.start?.offset ?? endOfBar; const metaData = bar ? binaryNodeSearch(bar.metaData, offset, endOfBarMetaData) : undefined; if (metaData) { const metaDataIndex = bar.metaData.indexOf(metaData); @@ -148,16 +150,16 @@ function createBeatCompletions( beat.durationChange.start!.offset < offset && offset <= beat.durationChange!.end!.offset ) { - const endOfDurationChange = beat.notes?.start!.offset ?? endOfBeat; + const endOfDurationChange = beat.notes?.start?.offset ?? endOfBeat; return createDurationChangeCompletions(beat.durationChange, offset, endOfDurationChange); } if (beat.notes && beat.notes.start!.offset < offset && offset <= beat.notes.end!.offset) { const endOfNotes = - beat.notes?.closeParenthesis?.start!.offset ?? - beat.durationDot?.start!.offset ?? - beat.beatEffects?.start!.offset ?? - beat.beatMultiplier?.start!.offset ?? + beat.notes?.closeParenthesis?.start?.offset ?? + beat.durationDot?.start?.offset ?? + beat.beatEffects?.start?.offset ?? + beat.beatMultiplier?.start?.offset ?? endOfBeat; const note = binaryNodeSearch(beat.notes.notes, offset, endOfNotes); if (note) { @@ -199,7 +201,7 @@ function createBeatCompletions( if (beat.beatEffects && beat.beatEffects.start!.offset < offset && beat.beatEffects.end!.offset) { const endOfProperties = - beat.beatEffects?.closeBrace?.start!.offset ?? beat.beatMultiplier?.start!.offset ?? endOfBeat; + beat.beatEffects?.closeBrace?.start?.offset ?? beat.beatMultiplier?.start?.offset ?? endOfBeat; completions.splice( 0, 0, @@ -245,7 +247,7 @@ function createDurationChangeCompletions( durationChange.properties.start!.offset < offset && durationChange.properties.end!.offset ) { - const endOfProperties = durationChange.properties?.closeBrace?.start!.offset ?? endOfDurationChange; + const endOfProperties = durationChange.properties?.closeBrace?.start?.offset ?? endOfDurationChange; completions.push( ...createPropertiesCompletions(durationChange.properties, offset, durationChangeProperties, endOfProperties) ); @@ -262,7 +264,7 @@ function createNoteCompletions( ): CompletionItem[] { const completions: CompletionItem[] = []; if (note.noteEffects && note.noteEffects.start!.offset < offset && note.noteEffects.end!.offset) { - const endOfProperties = note.noteEffects.closeBrace?.start!.offset ?? endOfNote; + const endOfProperties = note.noteEffects.closeBrace?.start?.offset ?? endOfNote; if (beat.notes!.notes.length === 1) { completions.splice( @@ -402,9 +404,9 @@ function createMetaDataCompletions( } const endOfArguments = - metaData.arguments?.closeParenthesis?.start!.offset ?? - metaData.arguments?.end!.offset ?? - metaData.properties?.start!.offset ?? + metaData.arguments?.closeParenthesis?.start?.offset ?? + metaData.arguments?.end?.offset ?? + metaData.properties?.start?.offset ?? endOfMetaData; completions.splice( 0, @@ -413,7 +415,7 @@ function createMetaDataCompletions( ); if (metaDataDocs?.properties) { - const endOfProperties = metaData.properties?.closeBrace?.start!.offset ?? endOfMetaData; + const endOfProperties = metaData.properties?.closeBrace?.start?.offset ?? endOfMetaData; completions.splice( 0, 0, @@ -433,12 +435,12 @@ function createArgumentCompletions( if (actualValues) { const value = binaryNodeSearch(actualValues.arguments, offset, trailingEnd); if (value?.parameterIndices) { - const isNextParameter = offset > value.end!.offset; + const isNextParameter = offset > value.end!.offset; const signatureCandidates = resolveSignature(signatures, actualValues); for (const [k, v] of signatureCandidates) { let parameterIndex = value.parameterIndices.get(k); - if(parameterIndex !== undefined && isNextParameter && parameterIndex < v.parameters.length - 1) { + if (parameterIndex !== undefined && isNextParameter && parameterIndex < v.parameters.length - 1) { parameterIndex++; } diff --git a/packages/lsp/src/server/signatureHelp.ts b/packages/lsp/src/server/signatureHelp.ts index 031600973..b1be2d240 100644 --- a/packages/lsp/src/server/signatureHelp.ts +++ b/packages/lsp/src/server/signatureHelp.ts @@ -59,7 +59,7 @@ function createMetaDataSignatureHelp( return null; } - const endOfValues = metaData.arguments?.end!.offset ?? metaData.properties?.start!.offset ?? metaData.end!.offset; + const endOfValues = metaData.arguments?.end?.offset ?? metaData.properties?.start?.offset ?? metaData.end!.offset; if (metaData.start!.offset <= offset && offset < endOfValues) { return createArgumentsSignatureHelp(`\\${metaData.tag.tag.text}`, metaDataDocs, metaData.arguments, offset); } diff --git a/packages/lsp/src/server/utils.ts b/packages/lsp/src/server/utils.ts index 2a54a24d1..ef89014cd 100644 --- a/packages/lsp/src/server/utils.ts +++ b/packages/lsp/src/server/utils.ts @@ -1,6 +1,6 @@ import * as alphaTab from '@coderline/alphatab'; import type { ParameterDefinition, SignatureDefinition } from '@coderline/alphatab-alphatex/types'; -import { ClientCapabilities } from 'vscode-languageserver'; +import type { ClientCapabilities } from 'vscode-languageserver'; function binaryNodeSearchInner( items: T[], diff --git a/packages/monaco/package.json b/packages/monaco/package.json index edf4e4f82..894392f09 100644 --- a/packages/monaco/package.json +++ b/packages/monaco/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-monaco", - "version": "1.7.1", + "version": "1.8.0", "description": "A Monaco editor integration for alphaTab providing coding assistance for alphaTex.", "keywords": [ "guitar", @@ -31,25 +31,25 @@ "test": "mocha" }, "dependencies": { - "@coderline/alphatab": "^1.7.1", - "@coderline/alphatab-language-server": "^1.7.1", - "monaco-editor": "^0.54.0", + "@coderline/alphatab": "^1.8.0", + "@coderline/alphatab-language-server": "^1.8.0", + "monaco-editor": "^0.55.1", "vscode-languageserver-types": "^3.17.5", "vscode-oniguruma": "^2.0.1", - "vscode-textmate": "^9.2.1" + "vscode-textmate": "^9.3.1" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@microsoft/api-extractor": "^7.53.1", + "@biomejs/biome": "^2.3.11", + "@microsoft/api-extractor": "^7.55.2", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^24.8.1", + "@types/node": "^25.0.6", "assert": "^2.1.0", - "chai": "^6.2.0", + "chai": "^6.2.2", "mocha": "^11.7.4", - "rimraf": "^6.0.1", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3" }, "bin": "./dist/alphaTab.monaco.mjs", diff --git a/packages/monaco/src/lsp.ts b/packages/monaco/src/lsp.ts index 8e3446a2f..e0ba59cd8 100644 --- a/packages/monaco/src/lsp.ts +++ b/packages/monaco/src/lsp.ts @@ -16,6 +16,8 @@ import { CompletionResolveRequest, DidChangeTextDocumentNotification, DidOpenTextDocumentNotification, + type DocumentDiagnosticParams, + DocumentDiagnosticRequest, type HoverParams, HoverRequest, type InitializeParams, @@ -24,7 +26,6 @@ import { type Logger, PositionEncodingKind, type ProtocolConnection, - PublishDiagnosticsNotification, type SignatureHelpParams, SignatureHelpRequest } from 'vscode-languageserver-protocol'; @@ -131,18 +132,32 @@ export async function basicEditorLspIntegration( } }); - setupDocumentHandling(editor, documentUri, connection); - setupDiagnostics(editor, connection); + setupDocumentHandlingAndDiagnostics(editor, documentUri, connection); setupCompletion(documentUri, info.languageId, connection, initResponse); setupHover(documentUri, info.languageId, connection, initResponse); setupSignatureHelp(documentUri, info.languageId, connection, initResponse); } -function setupDocumentHandling( +function setupDocumentHandlingAndDiagnostics( editor: monaco.editor.IStandaloneCodeEditor, documentUri: string, connection: ProtocolConnection ) { + async function updateDiagnostics() { + const params: DocumentDiagnosticParams = { + textDocument: { + uri: documentUri + } + }; + const result = await connection.sendRequest(DocumentDiagnosticRequest.type, params); + if (result.kind === 'unchanged') { + return; + } + + monaco.editor.setModelMarkers(editor.getModel()!, 'lsp', result.items.map(lspToMonacoMarker)); + } + updateDiagnostics(); + editor.onDidChangeModelContent(async e => { await connection.sendNotification(DidChangeTextDocumentNotification.type, { textDocument: { @@ -151,15 +166,8 @@ function setupDocumentHandling( }, contentChanges: e.changes.map(monacoToLspContentChange) }); - }); -} -function setupDiagnostics( - editor: monaco.editor.IStandaloneCodeEditor, - connection: ProtocolConnection -) { - connection.onNotification(PublishDiagnosticsNotification.type, e => { - monaco.editor.setModelMarkers(editor.getModel()!, 'lsp', e.diagnostics.map(lspToMonacoMarker)); + await updateDiagnostics(); }); } diff --git a/packages/playground/alphatex-editor.ts b/packages/playground/alphatex-editor.ts index f128c56b4..ef157ac8f 100644 --- a/packages/playground/alphatex-editor.ts +++ b/packages/playground/alphatex-editor.ts @@ -8,11 +8,10 @@ import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import Split from 'split.js'; import { setupControl } from './control'; - async function setupLspAlphaTexLanguageSupport(editor: monaco.editor.IStandaloneCodeEditor) { await basicEditorLspIntegration( editor, - new Worker(new URL('./alphaTexLanguageServerWrap', import.meta.url), { type: 'module' }), + new Worker(new URL('./alphatexLanguageServerWrap', import.meta.url), { type: 'module' }), { logger: { error(message) { @@ -83,8 +82,8 @@ async function load(url: URL, type: XMLHttpRequest['responseType']): Promise< async function setupEditor(api: alphaTab.AlphaTabApi, element: HTMLElement) { Split(['#editor-wrap', '#alphatab-wrap']); - const initialCode = sessionStorage.getItem('alphatex-editor.code') ?? trimCode(element.innerHTML); - element.innerHTML = ''; + const initialCode = sessionStorage.getItem('alphatex-editor.code') || trimCode(element.textContent); + element.innerText = ''; await setupMonaco(); @@ -94,18 +93,35 @@ async function setupEditor(api: alphaTab.AlphaTabApi, element: HTMLElement) { automaticLayout: true }); + let fromTex = true; + api.settings.exporter.comments = true; + api.settings.exporter.indent = 2; + api.scoreLoaded.on(score => { + if (!fromTex) { + const exporter = new alphaTab.exporter.AlphaTexExporter(); + const tex = exporter.exportToString(score, api.settings); + editor.getModel()!.setValue(tex); + } + }); + function loadTex(tex: string) { const importer = new alphaTab.importer.AlphaTexImporter(); importer.initFromString(tex, api.settings); + importer.logErrors = true; let score: alphaTab.model.Score; try { score = importer.readScore(); + api.updateSettings(); } catch { return; } sessionStorage.setItem('alphatex-editor.code', tex); - api.renderTracks(score.tracks); + fromTex = true; + api.renderTracks(score.tracks, { + reuseViewport: true + }); + fromTex = false; } editor.onDidChangeModelContent(() => { diff --git a/packages/playground/control-template.html b/packages/playground/control-template.html index 67ebedd4b..1bb770814 100644 --- a/packages/playground/control-template.html +++ b/packages/playground/control-template.html @@ -141,14 +141,34 @@ Layout + + diff --git a/packages/playground/control.css b/packages/playground/control.css index b7278eeeb..f659afd78 100644 --- a/packages/playground/control.css +++ b/packages/playground/control.css @@ -22,6 +22,9 @@ @import url('bootstrap/dist/css/bootstrap.min.css'); +@import url('./select-handles.css'); +@import url('./crosshair.css'); + .at-cursor-bar { /* Defines the color of the bar background when a bar is played */ background: rgba(255, 242, 0, 0.25); diff --git a/packages/playground/control.ts b/packages/playground/control.ts index 0ec41754c..42ba9dd76 100644 --- a/packages/playground/control.ts +++ b/packages/playground/control.ts @@ -1,6 +1,7 @@ import * as alphaTab from '@coderline/alphatab'; import * as bootstrap from 'bootstrap'; import Handlebars from 'handlebars'; +import { setupSelectionHandles } from 'select-handles'; const toDomElement = (() => { const parser = document.createElement('div'); @@ -15,12 +16,14 @@ const params = new URL(window.location.href).searchParams; const defaultSettings = { core: { logLevel: (params.get('loglevel') ?? 'info') as alphaTab.json.CoreSettingsJson['logLevel'], + engine: params.get('engine') ?? 'default', file: '/test-data/audio/full-song.gp5', fontDirectory: '/font/bravura/' }, player: { playerMode: alphaTab.PlayerMode.EnabledAutomatic, scrollOffsetX: -10, + scrollOffsetY: -20, soundFont: '/font/sonivox/sonivox.sf2' } } satisfies alphaTab.json.SettingsJson; @@ -251,13 +254,13 @@ export function setupControl(selector: string, customSettings: alphaTab.json.Set console.error('alphaTab error', e); }); - el.ondragover = e => { + document.ondragover = e => { e.stopPropagation(); e.preventDefault(); - e.dataTransfer!.dropEffect = 'link'; + e.dataTransfer!.dropEffect = 'copy'; }; - el.ondrop = e => { + document.ondrop = e => { e.stopPropagation(); e.preventDefault(); const files = e.dataTransfer!.files; @@ -597,15 +600,41 @@ export function setupControl(selector: string, customSettings: alphaTab.json.Set switch ((e.target as HTMLAnchorElement).dataset.layout) { case 'page': settings.display.layoutMode = alphaTab.LayoutMode.Page; - settings.player.scrollMode = alphaTab.ScrollMode.Continuous; break; - case 'horizontal-bar': + case 'horizontal': settings.display.layoutMode = alphaTab.LayoutMode.Horizontal; + break; + case 'parchment': + settings.display.layoutMode = alphaTab.LayoutMode.Parchment; + break; + } + + at.updateSettings(); + at.render(); + }; + } + for (const a of control.querySelectorAll('.at-scroll-options a')) { + a.onclick = e => { + e.preventDefault(); + const settings = at.settings; + switch ((e.target as HTMLAnchorElement).dataset.scroll) { + case 'off': + settings.player.scrollMode = alphaTab.ScrollMode.Off; + break; + case 'continuous': settings.player.scrollMode = alphaTab.ScrollMode.Continuous; + settings.player.scrollOffsetX = -10; + settings.player.scrollOffsetY = -10; break; - case 'horizontal-screen': - settings.display.layoutMode = alphaTab.LayoutMode.Horizontal; + case 'offscreen': settings.player.scrollMode = alphaTab.ScrollMode.OffScreen; + settings.player.scrollOffsetX = -10; + settings.player.scrollOffsetY = -10; + break; + case 'smooth': + settings.player.scrollMode = alphaTab.ScrollMode.Smooth; + settings.player.scrollOffsetX = -50; + settings.player.scrollOffsetY = -100; break; } @@ -618,227 +647,15 @@ export function setupControl(selector: string, customSettings: alphaTab.json.Set new bootstrap.Tooltip(t); } - at.playbackRangeChanged.on(e => { - if (e.playbackRange) { - } - }); - setupSelectionHandles(el, at); // expose api for fiddling in developer tools (window as any).api = at; + (window as any).alphaTab = alphaTab; return at; } -function createSelectionHandles(element: HTMLElement): { startHandle: HTMLElement; endHandle: HTMLElement } { - // create - const handleWrapper = document.createElement('div'); - handleWrapper.classList.add('at-selection-handles'); - element.insertBefore(handleWrapper, element.querySelector('at-surface')); - - const startHandle = document.createElement('div'); - startHandle.classList.add('at-selection-handle', 'at-selection-handle-start'); - handleWrapper.appendChild(startHandle); - - const endHandle = document.createElement('div'); - endHandle.classList.add('at-selection-handle', 'at-selection-handle-end'); - handleWrapper.appendChild(endHandle); - - return { startHandle, endHandle }; -} - -interface HandleDragState { - isDragging: 'start' | 'end' | undefined; -} -function setupHandleDrag( - element: HTMLElement, - handle: HTMLElement, - dragState: HandleDragState, - type: HandleDragState['isDragging'], - onMove: (e: MouseEvent) => void, - onDragEnd: (e: MouseEvent) => void -) { - handle.addEventListener( - 'mousedown', - e => { - e.preventDefault(); - element.classList.add('at-selection-handle-drag'); - handle.classList.add('at-selection-handle-drag'); - dragState.isDragging = type; - }, - false - ); - document.addEventListener( - 'mousemove', - e => { - if (dragState.isDragging !== type) { - return; - } - e.preventDefault(); - onMove(e); - }, - true - ); - document.addEventListener( - 'mouseup', - e => { - if (dragState.isDragging !== type) { - return; - } - e.preventDefault(); - dragState.isDragging = undefined; - element.classList.remove('at-selection-handle-drag'); - handle.classList.remove('at-selection-handle-drag'); - onDragEnd(e); - }, - true - ); -} - -function getRelativePosition(parent: HTMLElement, e: MouseEvent): { relX: number; relY: number } { - const parentPos = parent.getBoundingClientRect(); - const parentLeft: number = parentPos.left + parent.ownerDocument!.defaultView!.pageXOffset; - const parentTop: number = parentPos.top + parent.ownerDocument!.defaultView!.pageYOffset; - - const relX = e.pageX - parentLeft; - const relY = e.pageY - parentTop; - - return { relX, relY }; -} - -function getBeatFromEvent( - element: HTMLElement, - api: alphaTab.AlphaTabApi, - e: MouseEvent -): alphaTab.model.Beat | undefined { - const { relX, relY } = getRelativePosition(element, e); - const beat = api.boundsLookup?.getBeatAtPos(relX, relY); - if (!beat) { - return undefined; - } - - const bounds = api.boundsLookup!.findBeat(beat); - if (!bounds) { - return undefined; - } - - // only snap to beat beat if we are over the whitespace after the beat - const visualBoundsEnd = bounds.visualBounds.x + bounds.visualBounds.w; - const realBoundsEnd = bounds.realBounds.x + bounds.realBounds.w; - if (relX < visualBoundsEnd || relX > realBoundsEnd) { - return undefined; - } - - return beat; -} - -function setupSelectionHandles(element: HTMLElement, api: alphaTab.AlphaTabApi) { - const { startHandle, endHandle } = createSelectionHandles(element); - - // override internal logic for updating selection UI - interface SelectionInfo { - beat: alphaTab.model.Beat; - bounds: alphaTab.rendering.BeatBounds | null; - } - interface AlphaTabApiSelectionInternals { - _selectionStart: SelectionInfo | null; - _selectionEnd: SelectionInfo | null; - _cursorSelectRange(startBeat: SelectionInfo | null, endBeat: SelectionInfo | null): void; - } - - const apiWithInternals = api as unknown as AlphaTabApiSelectionInternals; - - // listen to selection range changes to place handles - const oldMethod = apiWithInternals._cursorSelectRange; - apiWithInternals._cursorSelectRange = function (startBeat, endBeat) { - oldMethod.call(this, startBeat, endBeat); - - // no selection - if (!startBeat || !endBeat || startBeat.beat === endBeat.beat) { - startHandle.classList.remove('active'); - endHandle.classList.remove('active'); - return; - } - - const selectionItems = element.querySelectorAll('.at-selection > div'); - if (selectionItems.length === 0) { - return; - } - - startHandle.classList.add('active'); - startHandle.style.left = `${startBeat.bounds!.realBounds.x}px`; - startHandle.style.top = `${startBeat.bounds!.barBounds.masterBarBounds.visualBounds.y}px`; - startHandle.style.height = `${startBeat.bounds!.barBounds.masterBarBounds.visualBounds.h}px`; - - endHandle.classList.add('active'); - endHandle.style.left = `${endBeat.bounds!.realBounds.x + endBeat.bounds!.realBounds.w}px`; - endHandle.style.top = `${endBeat.bounds!.barBounds.masterBarBounds.visualBounds.y}px`; - endHandle.style.height = `${endBeat.bounds!.barBounds.masterBarBounds.visualBounds.h}px`; - }; - - // setup dragging of handles - const dragState: HandleDragState = { isDragging: undefined }; - - function updatePlaybackRange() { - const realMasterBarStart: number = api.tickCache!.getMasterBarStart( - apiWithInternals._selectionStart!.beat.voice.bar.masterBar - ); - - const realMasterBarEnd: number = api.tickCache!.getMasterBarStart( - apiWithInternals._selectionEnd!.beat.voice.bar.masterBar - ); - - const range = new alphaTab.synth.PlaybackRange(); - range.startTick = realMasterBarStart + apiWithInternals._selectionStart!.beat.playbackStart; - range.endTick = - realMasterBarEnd + - apiWithInternals._selectionEnd!.beat.playbackStart + - apiWithInternals._selectionEnd!.beat.playbackDuration - - 50; - api.playbackRange = range; - } - - setupHandleDrag( - element, - startHandle, - dragState, - 'start', - e => { - const beat = getBeatFromEvent(element, api, e); - if (!beat) { - return; - } - - apiWithInternals._selectionStart = { - beat, - bounds: null - }; - apiWithInternals._cursorSelectRange(apiWithInternals._selectionStart, apiWithInternals._selectionEnd); - }, - updatePlaybackRange - ); - - setupHandleDrag( - element, - endHandle, - dragState, - 'end', - e => { - const beat = getBeatFromEvent(element, api, e); - if (!beat) { - return; - } - apiWithInternals._selectionEnd = { - beat, - bounds: null - }; - apiWithInternals._cursorSelectRange(apiWithInternals._selectionStart, apiWithInternals._selectionEnd); - }, - updatePlaybackRange - ); -} - function percentageToDegrees(percentage: number) { return (percentage / 100) * 360; } @@ -858,3 +675,5 @@ function updateProgress(el: HTMLElement, value: number) { } el.querySelector('.progress-value-number')!.innerText = String(value | 0); } + +import './crosshair'; diff --git a/packages/playground/crosshair.css b/packages/playground/crosshair.css new file mode 100644 index 000000000..43ff3fa5a --- /dev/null +++ b/packages/playground/crosshair.css @@ -0,0 +1,27 @@ +#crosshair { + position: absolute; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 20000; +} + +.crosshair-x,.crosshair-y { + pointer-events: none; + position: absolute; +} + +.crosshair-y { + width: 100vw; + border-bottom: 1px dotted #000; + height: 1px; + left: 0; +} + +.crosshair-x { + height: 100vw; + border-left: 1px dotted #000; + width: 1px; + top: 0; +} + diff --git a/packages/playground/crosshair.ts b/packages/playground/crosshair.ts new file mode 100644 index 000000000..752889edd --- /dev/null +++ b/packages/playground/crosshair.ts @@ -0,0 +1,50 @@ +let showCrossHair = false; +document.addEventListener('keydown', e => { + const shouldShowCrossHair = e.getModifierState('CapsLock'); + if (showCrossHair !== shouldShowCrossHair) { + showCrossHair = shouldShowCrossHair; + if (e.getModifierState('CapsLock')) { + showCrosshair(); + } else { + hideCrosshair(); + } + } +}); + +let crosshairX: HTMLElement | undefined; +let crosshairY: HTMLElement | undefined; +function showCrosshair() { + const element = document.createElement('div'); + element.id = 'crosshair'; + + crosshairX = document.createElement('div'); + crosshairX.classList.add('crosshair-x'); + element.appendChild(crosshairX); + crosshairY = document.createElement('div'); + crosshairY.classList.add('crosshair-y'); + element.appendChild(crosshairY); + + document.body.appendChild(element); + element.classList.add('crosshair'); + + document.addEventListener('mousemove', moveCrosshair, true); +} + +function hideCrosshair() { + const element = document.getElementById('crosshair'); + if (element) { + element.remove(); + } + crosshairX = undefined; + crosshairY = undefined; +} + +function moveCrosshair(e: MouseEvent) { + if (!crosshairX) { + document.removeEventListener('mousemove', moveCrosshair, true); + return; + } + + crosshairX!.style.left = `${e.pageX}px`; + crosshairY!.style.top = `${e.pageY}px`; +} diff --git a/packages/playground/package.json b/packages/playground/package.json index bdf450a86..539144904 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -1,20 +1,20 @@ { "name": "@coderline/alphatab-playground", - "version": "1.7.1", + "version": "1.8.0", "description": "A development playground for alphaTab to test features while developing", "private": true, "type": "module", "dependencies": { "@coderline/alphatab": "*", "@fontsource/noto-sans": "^5.2.10", - "@fontsource/noto-serif": "^5.2.8", + "@fontsource/noto-serif": "^5.2.9", "@fortawesome/fontawesome-free": "^7.1.0", "@popperjs/core": "^2.11.8", "@types/serve-static": "^2.2.0", "bootstrap": "^5.3.8", "handlebars": "^4.7.8", - "monaco-editor": "^0.54.0", - "serve-static": "^2.2.0" + "monaco-editor": "^0.55.1", + "serve-static": "^2.2.1" }, "scripts": { "lint": "biome lint", @@ -26,7 +26,7 @@ "split.js": "^1.6.5", "tslib": "^2.8.1", "typescript": "^5.9.3", - "vite": "^7.2.6", - "vite-tsconfig-paths": "^5.1.4" + "vite": "^7.3.1", + "vite-tsconfig-paths": "^6.0.4" } } diff --git a/packages/playground/select-handles.css b/packages/playground/select-handles.css new file mode 100644 index 000000000..e49aa7ef6 --- /dev/null +++ b/packages/playground/select-handles.css @@ -0,0 +1,35 @@ +.at-selection-handles { + position : absolute; + pointer-events: none; + z-index: 1001; + display: inline; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.at-selection-handle { + position : absolute; + pointer-events: auto; + cursor: ew-resize; + background: #7cb9ff; + width: 4px; + opacity: 0; + transition: opacity 150ms ease-in-out; + display: none; +} + +.at-selection-handle:hover, +.at-selection-handle.at-selection-handle-drag { + opacity: 1; +} + +.at-selection-handle.active { + display: block; +} + +.at-selection-handle-drag * { + cursor: ew-resize !important; +} + diff --git a/packages/playground/select-handles.ts b/packages/playground/select-handles.ts new file mode 100644 index 000000000..80d3aeb29 --- /dev/null +++ b/packages/playground/select-handles.ts @@ -0,0 +1,176 @@ +import type * as alphaTab from '@coderline/alphatab'; + +interface HandleDragState { + isDragging: 'start' | 'end' | undefined; +} + +function createSelectionHandles(element: HTMLElement): { startHandle: HTMLElement; endHandle: HTMLElement } { + const handleWrapper = document.createElement('div'); + handleWrapper.classList.add('at-selection-handles'); + element.insertBefore(handleWrapper, element.querySelector('at-surface')); + + const startHandle = document.createElement('div'); + startHandle.classList.add('at-selection-handle', 'at-selection-handle-start'); + handleWrapper.appendChild(startHandle); + + const endHandle = document.createElement('div'); + endHandle.classList.add('at-selection-handle', 'at-selection-handle-end'); + handleWrapper.appendChild(endHandle); + + return { startHandle, endHandle }; +} + +function setupHandleDrag( + element: HTMLElement, + handle: HTMLElement, + dragState: HandleDragState, + type: HandleDragState['isDragging'], + onMove: (e: MouseEvent) => void, + onDragEnd: (e: MouseEvent) => void +) { + handle.addEventListener( + 'mousedown', + e => { + e.preventDefault(); + element.classList.add('at-selection-handle-drag'); + handle.classList.add('at-selection-handle-drag'); + dragState.isDragging = type; + }, + false + ); + document.addEventListener( + 'mousemove', + e => { + if (dragState.isDragging !== type) { + return; + } + e.preventDefault(); + onMove(e); + }, + true + ); + document.addEventListener( + 'mouseup', + e => { + if (dragState.isDragging !== type) { + return; + } + e.preventDefault(); + dragState.isDragging = undefined; + element.classList.remove('at-selection-handle-drag'); + handle.classList.remove('at-selection-handle-drag'); + onDragEnd(e); + }, + true + ); +} + +function getRelativePosition(parent: HTMLElement, e: MouseEvent): { relX: number; relY: number } { + const parentPos = parent.getBoundingClientRect(); + const parentLeft: number = parentPos.left + parent.ownerDocument!.defaultView!.pageXOffset; + const parentTop: number = parentPos.top + parent.ownerDocument!.defaultView!.pageYOffset; + + const relX = e.pageX - parentLeft; + const relY = e.pageY - parentTop; + + return { relX, relY }; +} + +function getBeatFromEvent( + element: HTMLElement, + api: alphaTab.AlphaTabApi, + e: MouseEvent +): alphaTab.model.Beat | undefined { + const { relX, relY } = getRelativePosition(element, e); + const beat = api.boundsLookup?.getBeatAtPos(relX, relY); + if (!beat) { + return undefined; + } + + const bounds = api.boundsLookup!.findBeat(beat); + if (!bounds) { + return undefined; + } + + // only snap to beat beat if we are over the whitespace after the beat + const visualBoundsEnd = bounds.visualBounds.x + bounds.visualBounds.w; + const realBoundsEnd = bounds.realBounds.x + bounds.realBounds.w; + if (relX < visualBoundsEnd || relX > realBoundsEnd) { + return undefined; + } + + return beat; +} + +export function setupSelectionHandles(element: HTMLElement, api: alphaTab.AlphaTabApi) { + const { startHandle, endHandle } = createSelectionHandles(element); + + // listen to selection range changes to place handles + let currentHighlight: alphaTab.PlaybackHighlightChangeEventArgs | undefined; + api.playbackRangeHighlightChanged.on(e => { + currentHighlight = e; + // no selection + if (!e.startBeat || !e.endBeat) { + startHandle.classList.remove('active'); + endHandle.classList.remove('active'); + return; + } + + startHandle.classList.add('active'); + startHandle.style.left = `${e.startBeatBounds!.realBounds.x}px`; + startHandle.style.top = `${e.startBeatBounds!.barBounds.masterBarBounds.visualBounds.y}px`; + startHandle.style.height = `${e.startBeatBounds!.barBounds.masterBarBounds.visualBounds.h}px`; + + endHandle.classList.add('active'); + endHandle.style.left = `${e.endBeatBounds!.realBounds.x + e.endBeatBounds!.realBounds.w}px`; + endHandle.style.top = `${e.endBeatBounds!.barBounds.masterBarBounds.visualBounds.y}px`; + endHandle.style.height = `${e.endBeatBounds!.barBounds.masterBarBounds.visualBounds.h}px`; + }); + + // setup dragging of handles + const dragState: HandleDragState = { isDragging: undefined }; + + setupHandleDrag( + element, + startHandle, + dragState, + 'start', + e => { + if (!currentHighlight?.startBeat) { + return; + } + + const beat = getBeatFromEvent(element, api, e); + if (!beat) { + return; + } + + api.highlightPlaybackRange(beat, currentHighlight.endBeat!); + }, + () => { + api.applyPlaybackRangeFromHighlight(); + } + ); + + setupHandleDrag( + element, + endHandle, + dragState, + 'end', + e => { + if (!currentHighlight?.startBeat) { + return; + } + + const beat = getBeatFromEvent(element, api, e); + if (!beat) { + return; + } + + api.highlightPlaybackRange(currentHighlight!.startBeat!, beat); + }, + () => { + api.applyPlaybackRangeFromHighlight(); + } + ); +} diff --git a/packages/playground/test-results.html b/packages/playground/test-results.html index 1da390f58..7c78c5e6b 100644 --- a/packages/playground/test-results.html +++ b/packages/playground/test-results.html @@ -116,7 +116,7 @@ -

alphaTab - Visual Test Results

+

alphaTab - Visual Test Results

This page contains any failing visual tests for comparison and acceptance. Run the visualTests via npm run test or using ('.expected')!; const ac = el.querySelector('.actual')!; @@ -64,6 +67,8 @@ function setupComparer(card: HTMLElement, el: HTMLElement, result: TestResult) { acceptButton.innerText = 'Accepted'; } card.classList.add('accepted'); + result.accepted = true; + updateRemaining(); }; xhr.onerror = () => { alert('error accepting test result'); @@ -171,6 +176,7 @@ async function createResultViewer(result: TestResult) { async function displayResults(results: TestResult[]) { const wrapper = document.querySelector('#results-wrapper')!; wrapper.innerHTML = ''; + currentResults = results; for (const result of results) { wrapper.appendChild(await createResultViewer(result)); @@ -179,6 +185,15 @@ async function displayResults(results: TestResult[]) { if (results.length === 0) { wrapper.innerHTML = '

'; } + updateRemaining(); +} + +function updateRemaining() { + if (currentResults.length === 0) { + return; + } + document.querySelector('#remaining')!.innerText = + `(${currentResults.filter(r => !r.accepted).length}/${currentResults.length})`; } function loadResults() { diff --git a/packages/playground/vite.plugin.server.ts b/packages/playground/vite.plugin.server.ts index 656845829..5ff870c7e 100644 --- a/packages/playground/vite.plugin.server.ts +++ b/packages/playground/vite.plugin.server.ts @@ -36,7 +36,7 @@ export default function server(): Plugin { return { name: 'at-test-data-server', configureServer(server) { - const testDataPath = path.join(__dirname, '..', 'alphaTab', 'test-data'); + const testDataPath = path.join(__dirname, '..', 'alphatab', 'test-data'); server.middlewares.use('/font', serveStatic(path.join(__dirname, '..', 'alphatab', 'font'))); server.middlewares.use('/test-data', serveStatic(testDataPath)); diff --git a/packages/tooling/package.json b/packages/tooling/package.json index 665ecf105..61f8cc3f0 100644 --- a/packages/tooling/package.json +++ b/packages/tooling/package.json @@ -1,11 +1,11 @@ { "name": "@coderline/alphatab-tooling", - "version": "1.7.1", + "version": "1.8.0", "type": "module", "description": "Additional build tooling for alphaTab like common build configurations", "private": true, "devDependencies": { - "@microsoft/api-extractor": "^7.53.3", + "@microsoft/api-extractor": "^7.55.2", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.3.0", "rollup-plugin-license": "^3.6.0", diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 6c3578670..0f115d5f3 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-transpiler", - "version": "1.7.1", + "version": "1.8.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 b095b2128..34764cd0e 100644 --- a/packages/transpiler/src/AstPrinterBase.ts +++ b/packages/transpiler/src/AstPrinterBase.ts @@ -152,10 +152,41 @@ export default abstract class AstPrinterBase { d.isStatic && !d.setAccessor && cs.isPrimitiveTypeNode(d.type) && - !!d.initializer && + this._isConstantExpression(d.initializer) && (!d.getAccessor || !d.getAccessor.body) ); } + private _isConstantExpression(expression: cs.Expression | undefined): boolean { + if (expression === undefined) { + return false; + } + + switch (expression.nodeType) { + case cs.SyntaxKind.PrefixUnaryExpression: + return this._isConstantExpression((expression as cs.PrefixUnaryExpression).operand); + case cs.SyntaxKind.PostfixUnaryExpression: + return this._isConstantExpression((expression as cs.PostfixUnaryExpression).operand); + case cs.SyntaxKind.NullLiteral: + case cs.SyntaxKind.TrueLiteral: + case cs.SyntaxKind.FalseLiteral: + case cs.SyntaxKind.StringLiteral: + case cs.SyntaxKind.NumericLiteral: + case cs.SyntaxKind.DefaultExpression: + return true; + case cs.SyntaxKind.BinaryExpression: + return ( + this._isConstantExpression((expression as cs.BinaryExpression).left) && + this._isConstantExpression((expression as cs.BinaryExpression).right) + ); + case cs.SyntaxKind.ParenthesizedExpression: + return this._isConstantExpression((expression as cs.ParenthesizedExpression).expression); + // case cs.SyntaxKind.MemberAccessExpression: + // case cs.SyntaxKind.Identifier: + // maybe detect enums and other constant declarations? + } + + return false; + } protected writePropertyAsField(d: cs.PropertyDeclaration) { if ( diff --git a/packages/transpiler/src/csharp/CSharpAstTransformer.ts b/packages/transpiler/src/csharp/CSharpAstTransformer.ts index e7814ef41..f242c7dff 100644 --- a/packages/transpiler/src/csharp/CSharpAstTransformer.ts +++ b/packages/transpiler/src/csharp/CSharpAstTransformer.ts @@ -2371,6 +2371,23 @@ export default class CSharpAstTransformer { } protected visitReturnStatement(parent: cs.Node, s: ts.ReturnStatement) { + 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 + const yieldExpression = { + expression: null, + parent: yieldExpressionStmt, + tsNode: s, + nodeType: cs.SyntaxKind.YieldExpression + } as cs.YieldExpression; + yieldExpressionStmt.expression = yieldExpression; + return yieldExpressionStmt; + } + const returnStatement = { nodeType: cs.SyntaxKind.ReturnStatement, parent: parent, diff --git a/packages/transpiler/src/csharp/CSharpEmitterContext.ts b/packages/transpiler/src/csharp/CSharpEmitterContext.ts index da1ca9339..49ab75bf8 100644 --- a/packages/transpiler/src/csharp/CSharpEmitterContext.ts +++ b/packages/transpiler/src/csharp/CSharpEmitterContext.ts @@ -722,8 +722,6 @@ export default class CSharpEmitterContext { } } break; - case cs.SyntaxKind.ArrayTupleNode: - return true; case cs.SyntaxKind.UnresolvedTypeNode: this.resolveUnresolvedTypeNode(mapValueType as cs.UnresolvedTypeNode); return this.isCsValueType(mapValueType); @@ -1602,9 +1600,6 @@ export default class CSharpEmitterContext { // actual type at location must be non nullable const declaredTypeNonNull = this.typeChecker.getNonNullableType(declaredType); - if (this.typeChecker.isTupleType(declaredTypeNonNull)) { - return true; - } const contextualType = this.typeChecker.getTypeOfSymbolAtLocation(symbol, expression); if (!contextualType || this.isNullableType(contextualType)) { @@ -1946,7 +1941,7 @@ export default class CSharpEmitterContext { return true; } - return this.isEnum(tsType) || this.typeChecker.isTupleType(tsType); + return this.isEnum(tsType); } public isEnum(tsType: ts.Type) { diff --git a/packages/transpiler/src/kotlin/KotlinAstPrinter.ts b/packages/transpiler/src/kotlin/KotlinAstPrinter.ts index 974039b42..5f77d5643 100644 --- a/packages/transpiler/src/kotlin/KotlinAstPrinter.ts +++ b/packages/transpiler/src/kotlin/KotlinAstPrinter.ts @@ -627,7 +627,8 @@ 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; if (d.initializer && !isLateInit) { @@ -1961,7 +1962,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeExpression(expr.expression); this.write(')'); } else { - this.write('return'); + this.write('return@iterator'); } } diff --git a/packages/transpiler/src/kotlin/KotlinEmitterContext.ts b/packages/transpiler/src/kotlin/KotlinEmitterContext.ts index 8b2070eae..e77a87c9d 100644 --- a/packages/transpiler/src/kotlin/KotlinEmitterContext.ts +++ b/packages/transpiler/src/kotlin/KotlinEmitterContext.ts @@ -69,13 +69,6 @@ export default class KotlinEmitterContext extends CSharpEmitterContext { return undefined; } - protected override isCsValueType(mapValueType: cs.TypeNode | null): boolean { - if (mapValueType?.nodeType === cs.SyntaxKind.ArrayTupleNode) { - return false; - } - return super.isCsValueType(mapValueType); - } - public override getNameFromSymbol(symbol: ts.Symbol): string { const parent = 'parent' in symbol ? (symbol.parent as ts.Symbol) : undefined; diff --git a/packages/vite/package.json b/packages/vite/package.json index 9a119fdf6..a2d84723c 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-vite", - "version": "1.7.1", + "version": "1.8.0", "description": "A plugin for Vite to bundle alphaTab into your webapps.", "keywords": [ "guitar", @@ -43,25 +43,25 @@ }, "dependencies": { "magic-string": "^0.30.21", - "vite": "^7.2.6" + "vite": "^7.3.1" }, "engines": { "node": ">=20.19.0" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@microsoft/api-extractor": "^7.53.3", + "@biomejs/biome": "^2.3.11", + "@microsoft/api-extractor": "^7.55.2", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^24.10.0", + "@types/node": "^25.0.6", "assert": "^2.1.0", - "chai": "^6.2.1", + "chai": "^6.2.2", "mocha": "^11.7.5", - "rimraf": "^6.1.0", + "rimraf": "^6.1.2", "rollup-plugin-node-externals": "^8.1.2", "terser": "^5.44.1", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3" }, "files": [ diff --git a/packages/vite/src/alphaTabVitePlugin.ts b/packages/vite/src/alphaTabVitePlugin.ts index 2ff314230..d4ad3ea1d 100644 --- a/packages/vite/src/alphaTabVitePlugin.ts +++ b/packages/vite/src/alphaTabVitePlugin.ts @@ -1,3 +1,4 @@ +import { detectionGlobalPlugin } from '@coderline/alphatab-vite/detectionGlobalPlugin'; import type { AlphaTabVitePluginOptions } from './AlphaTabVitePluginOptions'; import type { Plugin } from './bridge'; import { copyAssetsPlugin } from './copyAssetsPlugin'; @@ -12,6 +13,7 @@ export function alphaTab(options?: AlphaTabVitePluginOptions) { options ??= {}; + plugins.push(detectionGlobalPlugin()); plugins.push(importMetaUrlPlugin(options)); plugins.push(workerPlugin(options)); plugins.push(copyAssetsPlugin(options)); diff --git a/packages/vite/src/detectionGlobalPlugin.ts b/packages/vite/src/detectionGlobalPlugin.ts new file mode 100644 index 000000000..d2f3f2a2d --- /dev/null +++ b/packages/vite/src/detectionGlobalPlugin.ts @@ -0,0 +1,38 @@ +import MagicString from 'magic-string'; +import type { Plugin, ResolvedConfig } from './bridge'; + +const marker = '__ALPHATAB_VITE__'; + +/** + * @public + */ +export function detectionGlobalPlugin(): Plugin { + let resolvedConfig: ResolvedConfig; + return { + name: 'vite-plugin-alphatab-global', + + configResolved(config) { + resolvedConfig = config as ResolvedConfig; + }, + + shouldTransformCachedModule({ code }) { + return code.includes(marker); + }, + + async transform(code, id) { + if (!code.includes(marker)) { + return; + } + + const s = new MagicString(code); + s.replaceAll(marker, JSON.stringify(true)); + return { + code: s.toString(), + map: + resolvedConfig.command === 'build' && resolvedConfig.build.sourcemap + ? s.generateMap({ hires: 'boundary', source: id }) + : null + }; + } + }; +} diff --git a/packages/vite/test/Vite.test.ts b/packages/vite/test/Vite.test.ts index 745694384..e32c00dac 100644 --- a/packages/vite/test/Vite.test.ts +++ b/packages/vite/test/Vite.test.ts @@ -62,6 +62,8 @@ describe('Vite', () => { expect(text).to.include('assets/alphaTab.worklet-'); // without custom chunking the app will bundle alphatab directly expect(text).to.include(".at-surface"); + // ensure __ALPHATAB_VITE__ got replaced + expect(text).to.not.include("__ALPHATAB_VITE__"); appValidated = true; } else if (file.name.startsWith('alphaTab.worker-')) { expect(text).to.include('initializeWorker()'); diff --git a/packages/vscode/.vscode-test/vscode-win32-x64-archive-1.106.3/resources/app/node_modules.asar b/packages/vscode/.vscode-test/vscode-win32-x64-archive-1.106.3/resources/app/node_modules.asar new file mode 100644 index 000000000..0be269ff7 Binary files /dev/null and b/packages/vscode/.vscode-test/vscode-win32-x64-archive-1.106.3/resources/app/node_modules.asar differ diff --git a/packages/vscode/package.json b/packages/vscode/package.json index e012ccb36..8976cb39d 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -1,6 +1,6 @@ { "name": "alphatab-vscode", - "version": "1.7.1", + "version": "1.8.0", "private": true, "description": "A Visual Studio Code extension for alphaTab providing coding assistance for alphaTex.", "keywords": [ @@ -34,21 +34,21 @@ "test": "vscode-test" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", + "@biomejs/biome": "^2.3.11", "@rollup/plugin-node-resolve": "^16.0.3", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", - "@types/node": "^24.8.1", - "@types/vscode": "^1.106.1", - "@vscode/test-cli": "^0.0.11", + "@types/node": "^25.0.6", + "@types/vscode": "^1.108.1", + "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "assert": "^2.1.0", - "chai": "^6.2.0", + "chai": "^6.2.2", "concurrently": "^9.2.1", "mocha": "^11.7.4", - "rimraf": "^6.0.1", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3", "vscode-languageclient": "^9.0.1" }, diff --git a/packages/webpack/package.json b/packages/webpack/package.json index c9248c876..17ab3d695 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -1,6 +1,6 @@ { "name": "@coderline/alphatab-webpack", - "version": "1.7.1", + "version": "1.8.0", "description": "A plugin for WebPack to bundle alphaTab into your webapps.", "keywords": [ "guitar", @@ -42,23 +42,23 @@ "test": "mocha" }, "dependencies": { - "webpack": "^5.101.3" + "webpack": "^5.104.1" }, "engines": { "node": ">=20.19.0" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", + "@biomejs/biome": "^2.3.11", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", - "@types/node": "^24.10.0", + "@types/node": "^25.0.6", "assert": "^2.1.0", - "chai": "^6.2.1", + "chai": "^6.2.2", "html-webpack-plugin": "^5.6.5", "mocha": "^11.7.5", - "rimraf": "^6.1.0", + "rimraf": "^6.1.2", "tslib": "^2.8.1", - "tsx": "^4.20.6", + "tsx": "^4.21.0", "typescript": "^5.9.3", "webpack-cli": "^6.0.1" }, diff --git a/packages/webpack/src/AlphaTabWebPackPlugin.ts b/packages/webpack/src/AlphaTabWebPackPlugin.ts index ac11d3247..582559d2b 100644 --- a/packages/webpack/src/AlphaTabWebPackPlugin.ts +++ b/packages/webpack/src/AlphaTabWebPackPlugin.ts @@ -14,6 +14,7 @@ import type { webPackWithAlphaTab, webpackTypes } from './Utils'; import { injectWebWorkerDependency } from './AlphaTabWebWorkerDependency'; import { injectWorkletRuntimeModule } from './AlphaTabWorkletStartRuntimeModule'; import { injectWorkletDependency } from './AlphaTabWorkletDependency'; +import { configureDetectionGlobal } from '@coderline/alphatab-webpack/DetectionGlobal'; const WINDOWS_ABS_PATH_REGEXP = /^[a-zA-Z]:[\\/]/; const WINDOWS_PATH_SEPARATOR_REGEXP = /\\/g; @@ -219,6 +220,7 @@ export class AlphaTabWebPackPlugin { cachedContextify ); this._configureAssetCopy(this._webPackWithAlphaTab, pluginName, compiler, compilation); + configureDetectionGlobal(pluginName, normalModuleFactory); }); } diff --git a/packages/webpack/src/AlphaTabWebWorkerDependency.ts b/packages/webpack/src/AlphaTabWebWorkerDependency.ts index b6265f082..175013087 100644 --- a/packages/webpack/src/AlphaTabWebWorkerDependency.ts +++ b/packages/webpack/src/AlphaTabWebWorkerDependency.ts @@ -81,8 +81,8 @@ export function injectWebWorkerDependency(webPackWithAlphaTab: webPackWithAlphaT runtimeRequirements.add(webPackWithAlphaTab.webpack.RuntimeGlobals.getChunkScriptFilename); source.replace( - dep.range[0], - dep.range[1] - 1, + dep.range![0], + dep.range![1] - 1, `/* worker import */ ${workerImportBaseUrl} + ${ webPackWithAlphaTab.webpack.RuntimeGlobals.getChunkScriptFilename }(${JSON.stringify(chunk.id)}), ${webPackWithAlphaTab.webpack.RuntimeGlobals.baseURI}` diff --git a/packages/webpack/src/AlphaTabWorkletDependency.ts b/packages/webpack/src/AlphaTabWorkletDependency.ts index 3019ddeac..2313fc45e 100644 --- a/packages/webpack/src/AlphaTabWorkletDependency.ts +++ b/packages/webpack/src/AlphaTabWorkletDependency.ts @@ -99,8 +99,8 @@ export function injectWorkletDependency(webPackWithAlphaTab: webPackWithAlphaTab runtimeRequirements.add(webPackWithAlphaTab.alphaTab.RuntimeGlobalWorkletGetStartupChunks); source.replace( - dep.range[0], - dep.range[1] - 1, + dep.range![0], + dep.range![1] - 1, webPackWithAlphaTab.webpack.Template.asString([ '(/* worklet bootstrap */ async function(__webpack_worklet__) {', webPackWithAlphaTab.webpack.Template.indent([ diff --git a/packages/webpack/src/DetectionGlobal.ts b/packages/webpack/src/DetectionGlobal.ts new file mode 100644 index 000000000..7c0c7bd44 --- /dev/null +++ b/packages/webpack/src/DetectionGlobal.ts @@ -0,0 +1,17 @@ +import type { Expression } from 'estree'; +import { tapJavaScript } from './Utils'; + +/** + * @internal + */ +export function configureDetectionGlobal(pluginName: string, normalModuleFactory: any) { + const parserPlugin = (parser: any) => { + parser.hooks.evaluateIdentifier.for('__ALPHATAB_WEBPACK__').tap(pluginName, (expr: Expression) => { + const res = parser.evaluate('true'); + res.setRange(expr.range); + return res; + }); + }; + + tapJavaScript(normalModuleFactory, pluginName, parserPlugin); +} diff --git a/packages/webpack/test/WebPack.test.ts b/packages/webpack/test/WebPack.test.ts index 58baf273a..dbea983c0 100644 --- a/packages/webpack/test/WebPack.test.ts +++ b/packages/webpack/test/WebPack.test.ts @@ -30,10 +30,7 @@ describe('WebPack', () => { filename: '[name]-[contenthash:8].js', path: path.resolve('./out') }, - plugins: [ - new AlphaTabWebPackPlugin(), - new HtmlWebpackPlugin() - ], + plugins: [new AlphaTabWebPackPlugin(), new HtmlWebpackPlugin()], optimization: { minimize: false, splitChunks: { @@ -105,7 +102,8 @@ describe('WebPack', () => { expect(text).to.include('class AlphaTabApiBase'); // ensure the library mode is active as needed expect(text).to.include('alphaTabApp = __webpack_exports__'); - + // ensure __ALPHATAB_WEBPACK__ got replaced + expect(text).to.not.include('__ALPHATAB_WEBPACK__'); appValidated = true; } else if (file.name.endsWith('.js')) { if (text.includes('class AlphaTabApiBase')) { diff --git a/tsconfig.base.json b/tsconfig.base.json index 389707c7e..5ee84767c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -94,6 +94,7 @@ "${configDir}/*.ts", "${configDir}/src", "${configDir}/test", + "${configDir}/scripts", ], "exclude": [ // monorepo root