diff --git a/.eclintrc.cjs b/.eclintrc.cjs deleted file mode 100644 index 232c1a631..000000000 --- a/.eclintrc.cjs +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-env node */ -module.exports = { - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - root: true, -}; \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 000000000..ce5c0bc60 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +FORCE_COLOR=1 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report_form.yml b/.github/ISSUE_TEMPLATE/bug_report_form.yml index 0cb1099c8..da3e0d2b2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_form.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_form.yml @@ -43,16 +43,27 @@ body: description: Providing a minimal reproducible example can help a lot. validations: required: false -- type: dropdown - id: version +- type: textarea + id: environment attributes: - label: Found in Version - description: Which version of alphaTab are you using? - options: - - 1.4 (alpha) - - 1.3 - - 1.2 - - Other + label: Version and Environment + description: | + Please provide detailed information about your environment, like alphaTab version used, browser, device, .net runtime, Android version,.. + Since 1.5.0: You can call `alphaTab.Environment.printEnvironmentInfo()` and copy the printed info. Or enable [debug logs](https://www.alphatab.net/docs/reference/settings/core/loglevel) and they are always printed. + Just grab them from the console/debugger/output/logcat output. + placeholder: | + [AlphaTab][VersionInfo] alphaTab 1.5.0 + [AlphaTab][VersionInfo] commit: 43c51b693438c54c0023ba729d7a7aa351e0f1fd + [AlphaTab][VersionInfo] build date: 2025-04-13T12:44:25.644Z + [AlphaTab][VersionInfo] High DPI: 1 + [AlphaTab][VersionInfo] Browser: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 + [AlphaTab][VersionInfo] Platform: BrowserModule + [AlphaTab][VersionInfo] WebPack: false + [AlphaTab][VersionInfo] Vite: false + [AlphaTab][VersionInfo] Window Size: 1529x1152 + [AlphaTab][VersionInfo] Screen Size: 3840x1200 + + render: bash validations: required: true - type: dropdown @@ -72,21 +83,7 @@ body: - Other validations: required: true -- type: textarea - id: environment - attributes: - label: Environment - description: | - examples: - - **OS**: Windows 10 Pro - - **Browser**: Chrome 92.0.4515.159 - value: | - - **OS**: - - **Browser**: - - **.net Version**: - render: markdown - validations: - required: true + - type: textarea id: further attributes: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64e6cf411..9f459ed32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,24 @@ on: workflow_dispatch: jobs: + lint: + name: Linting and TypeCheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + cache: 'npm' + - run: npm install + - run: npm run generate-typescript + - run: npm run lint-ci + if: ${{ always() }} + - run: npm run typecheck + if: ${{ always() }} + build_web: name: Build and Test Web runs-on: ubuntu-latest @@ -18,9 +36,18 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 'lts/*' - - run: npm install + cache: 'npm' + - run: npm ci - run: npm run build - run: npm run test + id: test + - uses: actions/upload-artifact@v4 + if: ${{ failure() && steps.test.conclusion == 'failure' }} + with: + name: test-results-web + path: | + test-data/**/*.new.png + test-data/**/*.diff.png build_csharp: name: Build and Test C# @@ -32,12 +59,21 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 'lts/*' + cache: 'npm' - uses: actions/setup-dotnet@v4 with: dotnet-version: '8' - - run: npm install + - run: npm ci - run: npm run build-csharp - run: npm run test-csharp + id: test + - uses: actions/upload-artifact@v4 + if: ${{ failure() && steps.test.conclusion == 'failure' }} + with: + name: test-results-csharp + path: | + test-data/**/*.new.png + test-data/**/*.diff.png build_kotlin: name: Build and Test Kotlin @@ -58,19 +94,23 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 'lts/*' + cache: 'npm' - uses: actions/setup-java@v4 with: java-version: '19' distribution: 'temurin' - - uses: actions/cache@v4 + - uses: gradle/actions/setup-gradle@v4 with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-cache-v5-${{ github.job }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle-cache-v5-${{ github.job }}- - - run: npm install + cache-read-only: false + - run: npm ci - run: npm run build-kotlin - run: npm run test-kotlin - - run: ./src.kotlin/alphaTab/gradlew --stop \ No newline at end of file + id: test + - uses: actions/upload-artifact@v4 + if: ${{ failure() && steps.test.conclusion == 'failure' }} + with: + name: test-results-kotlin + path: | + test-data/**/*.new.png + test-data/**/*.diff.png + - run: ./src.kotlin/alphaTab/gradlew --stop diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index be6d73895..e2124578b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: cache: 'npm' - run: node ./scripts/update-version.js alpha ${{github.run_number}} - - run: npm install + - run: npm ci - run: npm run build - run: npm pack @@ -66,7 +66,7 @@ jobs: with: dotnet-version: "8" - - run: npm install + - run: npm ci - run: node ./scripts/update-csharp-version.js alpha ${{github.run_number}} - run: npm run build-csharp @@ -98,7 +98,7 @@ jobs: java-version: "19" distribution: "temurin" - - run: npm install + - run: npm ci - run: node ./scripts/update-kotlin-version.js SNAPSHOT - run: npm run build-kotlin diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1acb011c1..decb033b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 'lts/*' - - run: npm install + - run: npm ci - run: node ./scripts/update-version.js ${{github.run_number}} - run: npm run build - run: npm pack @@ -43,7 +43,7 @@ jobs: dotnet-version: '8' env: NUGET_AUTH_TOKEN: ${{secrets.NUGET_API_KEY}} - - run: npm install + - run: npm ci - run: node ./scripts/update-csharp-version.js ${{github.run_number}} - run: npm run build-csharp - run: dotnet nuget push src.csharp/AlphaTab/bin/Release/*.nupkg -k ${{secrets.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json --skip-duplicate @@ -71,7 +71,7 @@ jobs: java-version: "19" distribution: "temurin" - - run: npm install + - run: npm ci - run: node ./scripts/update-kotlin-version.js - run: npm run build-kotlin - run: ./gradlew publishToMavenCentral diff --git a/.mocharc.json b/.mocharc.json index a0be35e23..7956a6f58 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -3,10 +3,14 @@ "ts" ], "node-option": [ + "env-file=.env", "experimental-specifier-resolution=node", "import=tsx/esm", "no-warnings" ], "spec": "test/**/*.test.ts", - "timeout": "10000" + "timeout": "10000", + "require": [ + "test/global-hooks.ts" + ] } \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 894de1a92..000000000 --- a/.prettierrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "printWidth": 120, - "tabWidth": 4, - "useTabs": false, - "semi": true, - "singleQuote": true, - "trailingComma": "none", - "bracketSpacing": true, - "arrowParens": "avoid" -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 126333b04..2c69583a5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,19 @@ { - "mochaExplorer.files": "test/**/*.test.ts", - "mochaExplorer.nodeArgv": [ - "--experimental-specifier-resolution=node", - "--import=tsx" - ], - "mochaExplorer.logpanel": true + "debug.javascript.terminalOptions": { + "resolveSourceMapLocations": [ + "${workspaceFolder}/**", + "!**/node_modules/**", + "**/node_modules/.vite-temp/**" + ] + }, + "typescript.preferences.importModuleSpecifier": "non-relative", + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + } } \ No newline at end of file diff --git a/LICENSE.header b/LICENSE.header index fbdfa5caa..f823470d4 100644 --- a/LICENSE.header +++ b/LICENSE.header @@ -42,4 +42,7 @@ Integrated Libraries: License: BSD-3-Clause Copyright: Copyright (c) 2002-2020 Xiph.org Foundation URL: https://github.com/xiph/vorbis - Purpose: NVorbis adopted some code from libvorbis. \ No newline at end of file + Purpose: NVorbis adopted some code from libvorbis. + +@preserve +@license \ No newline at end of file diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 000000000..3780658a4 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,65 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "files": { + "include": ["src/**", "test/**", "src.compiler/**"], + "ignore": ["dist", ".rollup.cache", "node_modules", "test-data", "src/generated/**"] + }, + "formatter": { + "enabled": true, + "formatWithErrors": true, + "ignore": [], + "attributePosition": "auto", + "indentStyle": "space", + "indentWidth": 4, + "lineWidth": 120, + "lineEnding": "lf" + }, + "linter": { + "enabled": true, + "rules": { + "style": { + "noInferrableTypes": "off", // we want to be explicit (for transpiler) + "noNonNullAssertion": "off", // we use these assertions often. + "noParameterAssign": "off", // useful for default values + "useExponentiationOperator": "off", // not supported in C# and Kotlin, lets stay with Math.pow + "useBlockStatements": { + "level": "error", + "fix": "safe" + } + }, + "suspicious": { + "noExplicitAny": "off" // used in areas where we work with dynamic JSON data + }, + "correctness": { + "noUnusedImports": { + "level": "error", + "fix": "safe" + }, + "noSwitchDeclarations": "off" + }, + "complexity": { + "useOptionalChain": "off", // its currently a bit sensitive with Kotlin as they evaulate things differently + "noStaticOnlyClass": "off" // we are strictly OOP (for transpiler) + } + } + }, + "organizeImports": { + "enabled": true + }, + "javascript": { + "formatter": { + "arrowParentheses": "asNeeded", + "bracketSameLine": true, + "bracketSpacing": true, + "quoteProperties": "asNeeded", + "semicolons": "always", + "trailingCommas": "none", + "quoteStyle": "single" + } + }, + "json": { + "formatter": { + "trailingCommas": "none" + } + } +} diff --git a/font/bravura/Bravura.ttf b/font/bravura/Bravura.ttf deleted file mode 100644 index 533ed1c37..000000000 Binary files a/font/bravura/Bravura.ttf and /dev/null differ diff --git a/font/noto-color-emoji/NotoColorEmoji_WindowsCompatible.ttf b/font/noto-color-emoji/NotoColorEmoji_WindowsCompatible.ttf new file mode 100644 index 000000000..b2346a4e2 Binary files /dev/null and b/font/noto-color-emoji/NotoColorEmoji_WindowsCompatible.ttf differ diff --git a/font/noto-color-emoji/OFL.txt b/font/noto-color-emoji/OFL.txt new file mode 100644 index 000000000..04e112f22 --- /dev/null +++ b/font/noto-color-emoji/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2021 Google Inc. All Rights Reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/font/noto-music/NotoMusic-Regular.otf b/font/noto-music/NotoMusic-Regular.otf new file mode 100644 index 000000000..fb87aa7cc Binary files /dev/null and b/font/noto-music/NotoMusic-Regular.otf differ diff --git a/font/noto-music/OFL.txt b/font/noto-music/OFL.txt new file mode 100644 index 000000000..0373e14e1 --- /dev/null +++ b/font/noto-music/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/font/noto-sans/NotoSans-Bold.otf b/font/noto-sans/NotoSans-Bold.otf new file mode 100644 index 000000000..7325f69a1 Binary files /dev/null and b/font/noto-sans/NotoSans-Bold.otf differ diff --git a/font/noto-sans/NotoSans-Bold.ttf b/font/noto-sans/NotoSans-Bold.ttf deleted file mode 100644 index 506f7d842..000000000 Binary files a/font/noto-sans/NotoSans-Bold.ttf and /dev/null differ diff --git a/font/noto-sans/NotoSans-BoldItalic.otf b/font/noto-sans/NotoSans-BoldItalic.otf new file mode 100644 index 000000000..123f51fc9 Binary files /dev/null and b/font/noto-sans/NotoSans-BoldItalic.otf differ diff --git a/font/noto-sans/NotoSans-BoldItalic.ttf b/font/noto-sans/NotoSans-BoldItalic.ttf deleted file mode 100644 index 0e8fa4bde..000000000 Binary files a/font/noto-sans/NotoSans-BoldItalic.ttf and /dev/null differ diff --git a/font/noto-sans/NotoSans-Italic.otf b/font/noto-sans/NotoSans-Italic.otf new file mode 100644 index 000000000..202f4547c Binary files /dev/null and b/font/noto-sans/NotoSans-Italic.otf differ diff --git a/font/noto-sans/NotoSans-Italic.ttf b/font/noto-sans/NotoSans-Italic.ttf deleted file mode 100644 index d9b9e148c..000000000 Binary files a/font/noto-sans/NotoSans-Italic.ttf and /dev/null differ diff --git a/font/noto-sans/NotoSans-Regular.otf b/font/noto-sans/NotoSans-Regular.otf new file mode 100644 index 000000000..b8f04a591 Binary files /dev/null and b/font/noto-sans/NotoSans-Regular.otf differ diff --git a/font/noto-sans/NotoSans-Regular.ttf b/font/noto-sans/NotoSans-Regular.ttf deleted file mode 100644 index 4bac02f2f..000000000 Binary files a/font/noto-sans/NotoSans-Regular.ttf and /dev/null differ diff --git a/font/noto-serif/NotoSerif-Bold.otf b/font/noto-serif/NotoSerif-Bold.otf new file mode 100644 index 000000000..fef58086b Binary files /dev/null and b/font/noto-serif/NotoSerif-Bold.otf differ diff --git a/font/noto-serif/NotoSerif-Bold.ttf b/font/noto-serif/NotoSerif-Bold.ttf deleted file mode 100644 index aa21d44e4..000000000 Binary files a/font/noto-serif/NotoSerif-Bold.ttf and /dev/null differ diff --git a/font/noto-serif/NotoSerif-BoldItalic.otf b/font/noto-serif/NotoSerif-BoldItalic.otf new file mode 100644 index 000000000..154bebaee Binary files /dev/null and b/font/noto-serif/NotoSerif-BoldItalic.otf differ diff --git a/font/noto-serif/NotoSerif-BoldItalic.ttf b/font/noto-serif/NotoSerif-BoldItalic.ttf deleted file mode 100644 index 5a38d83d4..000000000 Binary files a/font/noto-serif/NotoSerif-BoldItalic.ttf and /dev/null differ diff --git a/font/noto-serif/NotoSerif-Italic.otf b/font/noto-serif/NotoSerif-Italic.otf new file mode 100644 index 000000000..21ea4c9f7 Binary files /dev/null and b/font/noto-serif/NotoSerif-Italic.otf differ diff --git a/font/noto-serif/NotoSerif-Italic.ttf b/font/noto-serif/NotoSerif-Italic.ttf deleted file mode 100644 index 786ecc731..000000000 Binary files a/font/noto-serif/NotoSerif-Italic.ttf and /dev/null differ diff --git a/font/noto-serif/NotoSerif-Regular.otf b/font/noto-serif/NotoSerif-Regular.otf new file mode 100644 index 000000000..0efd27de6 Binary files /dev/null and b/font/noto-serif/NotoSerif-Regular.otf differ diff --git a/font/noto-serif/NotoSerif-Regular.ttf b/font/noto-serif/NotoSerif-Regular.ttf deleted file mode 100644 index dc30297e4..000000000 Binary files a/font/noto-serif/NotoSerif-Regular.ttf and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 93cd8c87d..115bd6865 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,22 @@ { "name": "@coderline/alphatab", - "version": "1.4.4", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@coderline/alphatab", - "version": "1.4.4", + "version": "1.5.0", "license": "MPL-2.0", - "dependencies": { - "@types/express": "^5.0.0", - "@types/node": "^22.13.4", - "@types/opener": "^1.4.3" - }, "devDependencies": { - "@coderline/alphaskia": "^2.3.120", - "@coderline/alphaskia-windows": "^2.3.120", + "@biomejs/biome": "^1.9.4", + "@coderline/alphaskia": "^3.3.135", + "@coderline/alphaskia-linux": "^3.3.135", + "@coderline/alphaskia-windows": "^3.3.135", "@fontsource/noto-sans": "^5.1.1", "@fontsource/noto-serif": "^5.1.1", "@fortawesome/fontawesome-free": "^6.7.2", + "@microsoft/api-extractor": "^7.51.1", "@popperjs/core": "^2.11.8", "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-node-resolve": "^16.0.0", @@ -27,26 +25,29 @@ "@rollup/plugin-typescript": "^12.1.2", "@types/chai": "^5.0.1", "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", "@types/mocha": "^10.0.10", + "@types/node": "^22.13.4", + "@types/opener": "^1.4.3", "@typescript-eslint/eslint-plugin": "^8.24.1", "@typescript-eslint/parser": "^8.24.1", "ace-builds": "^1.38.0", "assert": "^2.1.0", "bootstrap": "^5.3.3", "chai": "^5.2.0", + "chalk": "^5.4.1", "concurrently": "^9.1.2", "cors": "^2.8.5", "eslint": "^9.20.1", - "express": "^4.21.2", + "express": "^5.1.0", "fs-extra": "^11.3.0", "handlebars": "^4.7.8", "html-webpack-plugin": "^5.6.3", + "jest-snapshot": "^29.7.0", "mocha": "^11.1.0", "multer": "^1.4.5-lts.1", "opener": "^1.5.2", - "prettier": "^3.5.2", "rimraf": "^6.0.1", - "rollup": "^4.34.8", "rollup-plugin-copy": "^3.5.0", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-license": "^3.6.0", @@ -54,7 +55,8 @@ "tslib": "^2.8.1", "tsx": "^4.19.3", "typescript": "^5.7.3", - "vite": "^6.1.1", + "vite": "^6.2.0", + "vite-tsconfig-paths": "^5.1.4", "webpack": "^5.98.0", "webpack-cli": "^6.0.1" }, @@ -62,472 +64,638 @@ "node": ">=6.0.0" } }, + "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, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, - "optional": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "dev": true, - "optional": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, - "optional": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@coderline/alphaskia": { - "version": "2.3.120", - "resolved": "https://registry.npmjs.org/@coderline/alphaskia/-/alphaskia-2.3.120.tgz", - "integrity": "sha512-BoVtcpqULVixvENIIrKrm+rIkSTH9vtdJ07XZHtkh6yWBi1HV4Vp2/64YcmvYMCK8leP+uf/4PMhxzgXcanrXQ==", + "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, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "@coderline/alphaskia-linux": "^2.3.0", - "@coderline/alphaskia-macos": "^2.3.0", - "@coderline/alphaskia-windows": "^2.3.0" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@coderline/alphaskia-linux": { - "version": "2.3.120", - "resolved": "https://registry.npmjs.org/@coderline/alphaskia-linux/-/alphaskia-linux-2.3.120.tgz", - "integrity": "sha512-VbIPe7fgfOHOwOVdy+Bm/iCgcQwFdN8xKkhrUfjMe6AJ7n6/vFrnj9pYtHIN57e5TqlU66jcvG9k/JCkozbWJg==", + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", "dev": true, - "optional": true, + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, "engines": { - "node": ">=18.0.0" + "node": ">=6.9.0" } }, - "node_modules/@coderline/alphaskia-macos": { - "version": "2.3.120", - "resolved": "https://registry.npmjs.org/@coderline/alphaskia-macos/-/alphaskia-macos-2.3.120.tgz", - "integrity": "sha512-nLSNfK73pj3gGMsbkC47TNlFlKOZkIgP2b596QM3kmaDD2i9fKGsa+3f/qEPzwNseWH9d+RW8LZ4MPQldOMQfQ==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", "dev": true, - "optional": true, + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { - "node": ">=18.0.0" + "node": ">=6.9.0" } }, - "node_modules/@coderline/alphaskia-windows": { - "version": "2.3.120", - "resolved": "https://registry.npmjs.org/@coderline/alphaskia-windows/-/alphaskia-windows-2.3.120.tgz", - "integrity": "sha512-pRRLlbEzNzTa9ZSMS1hDgMVjnt9Jfwakxe+5lFKPXIiRU3GXzCfUOXF1naoCtxdDTVl3q664AIuf1eV3dP34DQ==", + "node_modules/@babel/helper-compilation-targets/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, - "engines": { - "node": ">=18.0.0" + "dependencies": { + "yallist": "^3.0.2" } }, - "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==", + "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, - "engines": { - "node": ">=14.17.0" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "cpu": [ - "arm" - ], + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "cpu": [ - "x64" - ], + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "cpu": [ - "arm" - ], + "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, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "cpu": [ - "arm64" - ], + "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, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "cpu": [ - "ia32" - ], + "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, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "cpu": [ - "loong64" - ], + "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, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "cpu": [ - "mips64el" - ], + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "cpu": [ - "ppc64" - ], + "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, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "cpu": [ - "riscv64" - ], + "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, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "cpu": [ - "s390x" - ], + "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, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ - "netbsd" + "darwin" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ - "netbsd" + "darwin" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ - "sunos" + "linux" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "optional": true, @@ -535,13 +703,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", "cpu": [ "x64" ], @@ -551,235 +719,710 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@coderline/alphaskia": { + "version": "3.3.135", + "resolved": "https://registry.npmjs.org/@coderline/alphaskia/-/alphaskia-3.3.135.tgz", + "integrity": "sha512-Dkm9OdhY/CvVXips3feBzbW0oB2m8dzCI/oA9ZW3GQWW1rpD0GqfE6qSukDCcEgYf/3fdHMSCpPr5JBCdEofFw==", "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18.0.0" }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "optionalDependencies": { + "@coderline/alphaskia-linux": "^3.0.0", + "@coderline/alphaskia-macos": "^3.0.0", + "@coderline/alphaskia-windows": "^3.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "node_modules/@coderline/alphaskia-linux": { + "version": "3.3.135", + "resolved": "https://registry.npmjs.org/@coderline/alphaskia-linux/-/alphaskia-linux-3.3.135.tgz", + "integrity": "sha512-mWFgEGS+8leKJrLg1KQiQ7oittw1kdE2St1JvA2V2zQeOMpOCsyTcsj2Eid3v7UTNtRsPl1CeUleWWUobznYxQ==", "dev": true, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "node_modules/@coderline/alphaskia-macos": { + "version": "3.0.135", + "resolved": "https://registry.npmjs.org/@coderline/alphaskia-macos/-/alphaskia-macos-3.0.135.tgz", + "integrity": "sha512-/zfkq8sxHPXAifNg5UwzT3dUAvUBa+qTgqHqlTnnqgoPVEX5w9+6AVvrYt+JBxaBrS8PBheP/H+k4SFe/kF5bQ==", "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.4", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, + "optional": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18.0.0" } }, - "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "node_modules/@coderline/alphaskia-windows": { + "version": "3.3.135", + "resolved": "https://registry.npmjs.org/@coderline/alphaskia-windows/-/alphaskia-windows-3.3.135.tgz", + "integrity": "sha512-k3JTzUGmYvz7XrDLsL4HTKeJIGbRQlncCizbYjZR1IDGlliTQSx6ueBX+cRfcw8qRBkstumllYGVxTtfAv4daw==", "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "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, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=14.17.0" } }, - "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@eslint/core": "^0.10.0", - "levn": "^0.4.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@fontsource/noto-sans": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@fontsource/noto-sans/-/noto-sans-5.1.1.tgz", - "integrity": "sha512-WesuII3BzvzVr0JqYIgnEeJPwXvpFAo9tNCMH1AqoLSCdStKJugMaIcVJ/sT+Pw9ytIlUO3ccbqbe+BhhsXm9g==", - "dev": true - }, - "node_modules/@fontsource/noto-serif": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@fontsource/noto-serif/-/noto-serif-5.1.1.tgz", - "integrity": "sha512-XSKR1rMOG+80gEaKYZNzXSLGHBrWuu6+R2vCbgaHY5cGkfD3YhPCb6MVX94qd4ICW0U/KC9p6bP3ezUJ42PJcA==", - "dev": true - }, - "node_modules/@fortawesome/fontawesome-free": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", - "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/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==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.25.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.0.tgz", + "integrity": "sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fontsource/noto-sans": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fontsource/noto-sans/-/noto-sans-5.2.6.tgz", + "integrity": "sha512-46D39T1/QJ16LSbCTaNjjrGS6Y69FMLS9FeHKsrXSQg15Z2MFqmlnNMpURx2l6Sr3x9t9EVdr3CAkHnZyvea+w==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/noto-serif": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fontsource/noto-serif/-/noto-serif-5.2.6.tgz", + "integrity": "sha512-mEGeD/duOwgEJooUsXpdCz3WdBJBfm7LEP2XQz/OSBXxj/GVwisULiOH2B7oL2tLBT6EGKTzZfmJAo6ZzOG0+w==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { @@ -831,21 +1474,267 @@ "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=12" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -861,9 +1750,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -880,9 +1769,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -895,6 +1784,140 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@microsoft/api-extractor": { + "version": "7.52.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.4.tgz", + "integrity": "sha512-mIEcqgx877CFwNrTuCdPnlIGak8FjlayZb8sSBwWXX+i4gxkZRpMsb5BQcFW3v1puuJB3jYMqQ08kyAc4Vldhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.30.5", + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.13.0", + "@rushstack/rig-package": "0.5.3", + "@rushstack/terminal": "0.15.2", + "@rushstack/ts-command-line": "4.23.7", + "lodash": "~4.17.15", + "minimatch": "~3.0.3", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "source-map": "~0.6.1", + "typescript": "5.8.2" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.30.5", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.5.tgz", + "integrity": "sha512-0ic4rcbcDZHz833RaTZWTGu+NpNgrxVNjVaor0ZDUymfDFzjA/Uuk8hYziIUIOEOSTfmIQqyzVwlzxZxPe7tOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.13.0" + } + }, + "node_modules/@microsoft/api-extractor/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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@microsoft/api-extractor/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, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "ajv": "~8.12.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@microsoft/tsdoc-config/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==", + "dev": true, + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -951,10 +1974,11 @@ } }, "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", - "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "version": "28.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz", + "integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==", "dev": true, + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", @@ -977,9 +2001,9 @@ } }, "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", - "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", + "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", "dev": true, "license": "MIT", "dependencies": { @@ -1071,14 +2095,14 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" + "picomatch": "^4.0.2" }, "engines": { "node": ">=14.0.0" @@ -1092,279 +2116,494 @@ } } }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", - "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", - "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", - "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", - "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", - "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", - "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", - "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", - "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", - "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", - "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", - "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", - "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", - "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", - "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", - "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", - "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", - "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", - "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", "cpu": [ "ia32" ], "dev": true, - "optional": true, - "os": [ - "win32" - ] + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rushstack/node-core-library": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.13.0.tgz", + "integrity": "sha512-IGVhy+JgUacAdCGXKUrRhwHMTzqhWwZUI+qEPcdzsb80heOw0QPbhhoVsoiMF7Klp8eYsp7hzpScMXmOa3Uhfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "~8.13.0", + "ajv-draft-04": "~1.0.0", + "ajv-formats": "~3.0.1", + "fs-extra": "~11.3.0", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@rushstack/node-core-library/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": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/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": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/node-core-library/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": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/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": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.3.tgz", + "integrity": "sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==", + "dev": true, + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/terminal": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.2.tgz", + "integrity": "sha512-7Hmc0ysK5077R/IkLS9hYu0QuNafm+TbZbtYVzCMbeOdMjaRboLKrhryjwZSRJGJzu+TV1ON7qZHeqf58XfLpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/node-core-library": "5.13.0", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.23.7", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.7.tgz", + "integrity": "sha512-Gr9cB7DGe6uz5vq2wdr89WbVDKz0UeuFEn5H2CfWDe7JvjFFaiV15gi6mqDBTbHhHCWS7w8mF1h3BnIfUndqdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/terminal": "0.15.2", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "node_modules/@rushstack/ts-command-line/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": { + "sprintf-js": "~1.0.2" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", - "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", - "cpu": [ - "x64" - ], + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "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, - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "node_modules/@types/chai": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.0.1.tgz", - "integrity": "sha512-5T8ajsg3M/FOncpLYW7sdOcD6yf4+722sze/tc4KQV0P8Z2rAr3SAuHCIkYmYpt8VbcQlnz8SxlOlPQYefe4cA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-iu1JLYmGmITRzUgNiLMZD3WCoFzpYtueuyAgHTXqgwSRAMIlFTnZqG6/xenkpUGRJEzSfklUTI4GNSzks/dc0w==", "dev": true, + "license": "MIT", "dependencies": { "@types/deep-eql": "*" } @@ -1373,6 +2612,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -1413,19 +2653,21 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "dev": true, + "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", "@types/serve-static": "*" } }, @@ -1433,6 +2675,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -1459,6 +2702,15 @@ "@types/node": "*" } }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "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", @@ -1468,7 +2720,32 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "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 + }, + "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, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "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, + "dependencies": { + "@types/istanbul-lib-report": "*" + } }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -1479,7 +2756,8 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true }, "node_modules/@types/minimatch": { "version": "5.1.2", @@ -1497,6 +2775,7 @@ "version": "22.13.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -1506,6 +2785,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/@types/opener/-/opener-1.4.3.tgz", "integrity": "sha512-g7TYSmy2RKZkU3QT/9pMISrhVmQtMNaYq6Aojn3Y6pht29Nu9VuijJCYIjofRj7ZaFtKdxh1I8xf3vdW4l86fg==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -1513,12 +2793,14 @@ "node_modules/@types/qs": { "version": "6.9.16", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", - "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==" + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true }, "node_modules/@types/resolve": { "version": "1.20.2", @@ -1530,6 +2812,7 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -1539,23 +2822,46 @@ "version": "1.15.5", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, "dependencies": { "@types/http-errors": "*", "@types/mime": "*", "@types/node": "*" } }, + "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 + }, + "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, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "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 + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz", - "integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", + "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.24.1", - "@typescript-eslint/type-utils": "8.24.1", - "@typescript-eslint/utils": "8.24.1", - "@typescript-eslint/visitor-keys": "8.24.1", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/type-utils": "8.30.1", + "@typescript-eslint/utils": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1571,20 +2877,18 @@ "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", - "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.24.1", - "@typescript-eslint/types": "8.24.1", - "@typescript-eslint/typescript-estree": "8.24.1", - "@typescript-eslint/visitor-keys": "8.24.1", - "debug": "^4.3.4" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1592,21 +2896,14 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz", - "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.24.1", - "@typescript-eslint/visitor-keys": "8.24.1" - }, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1615,16 +2912,15 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz", - "integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.24.1", - "@typescript-eslint/utils": "8.24.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "@typescript-eslint/types": "8.30.1", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1632,39 +2928,33 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", - "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", - "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", + "node_modules/@typescript-eslint/parser": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", + "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.1", - "@typescript-eslint/visitor-keys": "8.24.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1674,43 +2964,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/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, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz", - "integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.24.1", - "@typescript-eslint/types": "8.24.1", - "@typescript-eslint/typescript-estree": "8.24.1" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1718,20 +2984,19 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", - "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", + "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/utils": "8.30.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1739,2571 +3004,2789 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "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==", - "dev": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "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==", - "dev": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "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==", - "dev": true - }, - "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==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "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==", - "dev": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "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==", - "dev": true - }, - "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==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "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==", + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, - "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==", + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" + "@typescript-eslint/types": "8.30.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "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==", + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "balanced-match": "^1.0.0" } }, - "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==", + "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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==", + "node_modules/@typescript-eslint/type-utils/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" + }, "engines": { - "node": ">=18.12.0" + "node": ">=16 || 14 >=14.17" }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/@typescript-eslint/types": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=18.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "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==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, "engines": { - "node": ">=18.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, - "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==", - "dev": true - }, - "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==", - "dev": true + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/@typescript-eslint/typescript-estree/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": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/ace-builds": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.38.0.tgz", - "integrity": "sha512-5W3B5/5Rl/dAsp7Fb6xXOc6bCYln+4qOnpQHh4OpxZSoDsp1KFiu5lA3TJBr8/5DQu6+8rlduCGecMSnKuU2MQ==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "node_modules/@typescript-eslint/utils": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", + "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", "dev": true, - "bin": { - "acorn": "bin/acorn" + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1" }, "engines": { - "node": ">=0.4.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "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==", + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "^8.0.0" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" }, - "peerDependencies": { - "ajv": "^8.0.0" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.30.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/ajv-formats/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==", - "dev": true - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "engines": { - "node": ">=6" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, - "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==", + "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@typescript-eslint/utils/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, - "optional": true, + "license": "ISC", "dependencies": { - "color-convert": "^1.9.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "dev": true, + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "@typescript-eslint/types": "8.30.1", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": ">= 8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "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==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=8.6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://opencollective.com/eslint" } }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "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==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "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==", "dev": true }, - "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==", + "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==", "dev": true }, - "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==", + "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==", + "dev": true + }, + "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==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "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==", "dev": true }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "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==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, - "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==", + "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==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" + "@xtuc/ieee754": "^1.2.0" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "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==", "dev": true, - "engines": { - "node": ">=12" + "dependencies": { + "@xtuc/long": "4.2.2" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "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==", + "dev": true + }, + "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==", "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "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==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "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==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "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==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "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==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "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, "engines": { - "node": ">=8" + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" } }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "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, - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "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, - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "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==", "dev": true }, - "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==", + "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==", "dev": true }, - "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.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==", + "node_modules/accepts/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==", "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "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 - }, - "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "node_modules/accepts/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==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" + "mime-db": "^1.54.0" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">= 0.6" } }, - "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "node_modules/ace-builds": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.40.0.tgz", + "integrity": "sha512-wOCyJfNRsq/yLTR7z2KpwcjaInuUs/mosu/OFLGGUA+g+ApD9OJ1AToHDIp0Xpa2koHJ79bmOya73oWjCNbjlA==", + "dev": true, + "license": "BSD-3-Clause" }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, - "dependencies": { - "streamsearch": "^1.1.0" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=10.16.0" + "node": ">=0.4.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "engines": { - "node": ">= 0.8" + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "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==", + "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==", "dev": true, "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "engines": { - "node": ">=10" + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001680", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", - "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] + "node_modules/ajv-formats/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==", + "dev": true }, - "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "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, - "optional": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "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, "engines": { - "node": ">= 16" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">= 8" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "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, "engines": { - "node": ">=6.0" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "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==", + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "dev": true + }, + "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 + }, + "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, - "dependencies": { - "source-map": "~0.6.0" - }, "engines": { - "node": ">= 10.0" + "node": ">=0.10.0" } }, - "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==", + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "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==", + "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, "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" } }, - "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==", + "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, - "dependencies": { - "isobject": "^3.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true, - "optional": true, - "dependencies": { - "color-name": "1.1.3" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, - "optional": true - }, - "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "dev": true - }, - "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==", - "dev": true + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/commenting": { + "node_modules/babel-preset-current-node-syntax": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/commenting/-/commenting-1.1.0.tgz", - "integrity": "sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA==", - "dev": true - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" + "engines": { + "node": ">=8" } }, - "node_modules/concat-stream/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 - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "dev": true, + "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/concat-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 + "node_modules/body-parser/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "node_modules/concat-stream/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==", + "node_modules/body-parser/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==", "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "node_modules/body-parser/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==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" + "mime-db": "^1.54.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + "node": ">= 0.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==", + "node_modules/body-parser/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 0.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==", + "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 + }, + "node_modules/bootstrap": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", + "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "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==", + "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, "dependencies": { - "has-flag": "^4.0.0" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, - "node_modules/concurrently/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==", + "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 + }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "color-name": "~1.1.4" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": ">=7.0.0" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/concurrently/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==", + "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, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "node_modules/concurrently/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==", + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "dev": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, "engines": { - "node": ">=8" + "node": ">=10.16.0" } }, - "node_modules/concurrently/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==", + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "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": { - "safe-buffer": "5.2.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "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": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true - }, - "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 - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "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, "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" } }, - "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, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, "engines": { - "node": ">= 8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/caniuse-lite": { + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">= 8" + "node": ">=12" } }, - "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==", + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/fb55" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "node": ">= 16" } }, - "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { - "ms": "2.1.2" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">=6.0" + "node": ">= 8.10.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "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==", + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.0" } }, - "node_modules/deep-eql": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz", - "integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==", + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "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==", + "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, + "dependencies": { + "source-map": "~0.6.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 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==", + "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": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "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==", + "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, "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=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==", + "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, + "dependencies": { + "isobject": "^3.0.1" + }, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "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, + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=7.0.0" } }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "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 + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "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==", + "dev": true + }, + "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 + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, - "engines": { - "node": ">=0.3.1" + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/concat-stream/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 + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "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==", + "node_modules/concat-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 + }, + "node_modules/concat-stream/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, "dependencies": { - "utila": "~0.4" + "safe-buffer": "~5.1.0" } }, - "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==", + "node_modules/concurrently": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", "dev": true, + "license": "MIT", "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "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" - } - ] - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "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, "dependencies": { - "domelementtype": "^2.2.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">= 4" + "node": ">=8" }, "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "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, "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "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, "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "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 - }, - "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==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.5.55", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", - "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", - "dev": true - }, - "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 - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, + "has-flag": "^4.0.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "node_modules/concurrently/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==", "dev": true, "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "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, - "bin": { - "envinfo": "dist/cli.js" + "node": ">=10" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/es-define-property": { + "node_modules/content-disposition": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4" + "safe-buffer": "5.2.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.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==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 0.6" } }, - "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "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 }, - "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "node": ">= 0.6" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=6.6.0" } }, - "node_modules/escape-html": { + "node_modules/core-util-is": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, - "optional": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, "engines": { - "node": ">=0.8.0" + "node": ">= 0.10" } }, - "node_modules/eslint": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", - "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "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, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/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, + "dependencies": { + "isexe": "^2.0.0" }, "bin": { - "eslint": "bin/eslint.js" + "node-which": "bin/node-which" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 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, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" }, "funding": { - "url": "https://eslint.org/donate" + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" }, - "peerDependencies": { - "jiti": "*" + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "jiti": { + "supports-color": { "optional": true } } }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "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, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/deep-eql": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz", + "integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=6" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "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": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=0.10.0" } }, - "node_modules/eslint/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==", + "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, "dependencies": { - "color-convert": "^2.0.1" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint/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==", + "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==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, + "license": "MIT", "engines": { - "node": ">=7.0.0" + "node": ">= 0.8" } }, - "node_modules/eslint/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 - }, - "node_modules/eslint/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==", + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.3.1" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "dependencies": { - "is-glob": "^4.0.3" + "path-type": "^4.0.0" }, "engines": { - "node": ">=10.13.0" + "node": ">=8" } }, - "node_modules/eslint/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==", - "dev": true, - "engines": { - "node": ">=8" + "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, + "dependencies": { + "utila": "~0.4" } }, - "node_modules/eslint/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==", + "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, "dependencies": { - "has-flag": "^4.0.0" + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "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" + } + ] + }, + "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, "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "domelementtype": "^2.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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, "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "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==", + "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": { - "estraverse": "^5.2.0" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">=4.0" + "node": ">= 0.4" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "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 + }, + "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==", "dev": true, - "engines": { - "node": ">=4.0" - } + "license": "MIT" }, - "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==", + "node_modules/electron-to-chromium": { + "version": "1.5.55", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", + "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", "dev": true }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "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 + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=10.13.0" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true, - "engines": { - "node": ">=0.8.x" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dev": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "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, + "bin": { + "envinfo": "dist/cli.js" }, "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=4" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "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, - "dependencies": { - "ms": "2.0.0" + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "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, + "engines": { + "node": ">= 0.4" + } }, - "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==", + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", "dev": true }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "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": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=8.6.0" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" } }, - "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 - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "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==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] + "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==", + "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, "engines": { - "node": ">= 4.9.1" + "node": ">=8" } }, - "node_modules/fastq": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", - "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "node_modules/eslint": { + "version": "9.25.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.0.tgz", + "integrity": "sha512-MsBdObhM4cEwkzCiraDv7A6txFXEqtNXOb877TsSp2FCkBNl8JfVQrmiuDqC1IkejT6JLPzYBXx/xAiYhyzgGA==", "dev": true, + "license": "MIT", "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", - "dev": true, + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.25.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, "peerDependencies": { - "picomatch": "^3 || ^4" + "jiti": "*" }, "peerDependenciesMeta": { - "picomatch": { + "jiti": { "optional": true } } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "flat-cache": "^4.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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, "dependencies": { - "to-regex-range": "^5.0.1" + "color-convert": "^2.0.1" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "node_modules/eslint/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, "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/eslint/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, - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "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==", + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "is-glob": "^4.0.3" }, "engines": { - "node": ">=10" + "node": ">=10.13.0" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "bin": { - "flat": "cli.js" + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/flat-cache": { + "node_modules/esprima": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, "engines": { - "node": ">=16" + "node": ">=4" } }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "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==", "dev": true, "dependencies": { - "is-callable": "^1.1.3" + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" } }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=4.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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 + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, "engines": { - "node": ">=14.14" + "node": ">=0.8.x" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "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==", + "node_modules/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "dev": true, + "license": "MIT", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">= 0.8" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "node_modules/express/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==", "dev": true, + "license": "MIT", "engines": { - "node": "*" + "node": ">= 0.6" } }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "node_modules/express/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==", "dev": true, + "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.6" } }, - "node_modules/get-tsconfig": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", - "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", + "node_modules/express/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, + "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "engines": { + "node": ">= 0.6" } }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "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==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8.6.0" } }, - "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==", + "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, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } + "license": "MIT" }, - "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==", + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "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, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 4.9.1" } }, - "node_modules/globby": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", - "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "node_modules/fastq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", "dev": true, "dependencies": { - "@types/glob": "^7.1.1", - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=8" + "reusify": "^1.0.4" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "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, "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bser": "2.1.1" } }, - "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==", - "dev": true - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" + "node": ">=16.0.0" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "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, - "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.8" } }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "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, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "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, - "engines": { - "node": ">= 0.4" + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=16" } }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, "engines": { - "node": ">= 0.4" + "node": ">= 0.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==", + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, - "bin": { - "he": "bin/he" + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "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==", + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "dev": true, "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, - "node_modules/html-minifier-terser/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 12" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/html-webpack-plugin": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", - "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", + "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, - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "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, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" + "engines": { + "node": ">=6.9.0" } }, - "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==", + "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, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, "engines": { - "node": ">= 0.8" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "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": { - "safer-buffer": ">= 2.1.2 < 3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "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, "engines": { - "node": ">= 4" + "node": ">=8.0.0" } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "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": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "node_modules/get-tsconfig": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", "dev": true, "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" + "resolve-pkg-maps": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, "engines": { - "node": ">=0.8.19" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "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, "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "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==", "dev": true }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10.13.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, "engines": { - "node": ">= 0.10" + "node": ">=8" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "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 + }, + "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, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4311,67 +5794,80 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, "dependencies": { - "binary-extensions": "^2.0.0" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" }, "engines": { - "node": ">=8" + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" } }, - "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==", + "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==", "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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==", + "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": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "has-symbols": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4380,689 +5876,846 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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": { - "is-extglob": "^2.1.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "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 + "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, + "bin": { + "he": "bin/he" + } }, - "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==", + "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, "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" }, - "engines": { - "node": ">= 0.4" + "bin": { + "html-minifier-terser": "cli.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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, "engines": { - "node": ">=0.12.0" + "node": ">=12" } }, - "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==", + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, "engines": { - "node": ">=8" + "node": ">= 12" } }, - "node_modules/is-plain-object": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", - "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "node_modules/html-webpack-plugin": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", + "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "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", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], "dependencies": { - "@types/estree": "*" + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" } }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "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==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.11" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8" } }, - "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==", + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "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, "engines": { "node": ">=0.10.0" } }, - "node_modules/jackspeak": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", - "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "node": ">= 4" } }, - "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==", + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-worker/node_modules/has-flag": { + "node_modules/import-lazy": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/jest-worker/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==", + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "optional": true + "engines": { + "node": ">=0.8.19" + } }, - "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==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "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==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "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, + "engines": { + "node": ">=10.13.0" + } }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, "dependencies": { - "universalify": "^2.0.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "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, "dependencies": { - "json-buffer": "3.0.1" + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "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==", + "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, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "hasown": "^2.0.0" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "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==", + "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, "engines": { - "node": ">=6.11.5" + "node": ">=8" } }, - "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==", + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", "dev": true, "dependencies": { - "p-locate": "^5.0.0" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "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, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "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 }, - "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==", + "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, "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "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, - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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==", + "node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@types/estree": "*" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/log-symbols/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==", + "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, - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols/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==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "node_modules/log-symbols/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==", + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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, "engines": { "node": ">=8" } }, - "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==", + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" }, "engines": { "node": ">=8" } }, - "node_modules/loupe": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz", - "integrity": "sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==", + "node_modules/istanbul-lib-instrument/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, - "dependencies": { - "get-func-name": "^2.0.1" + "bin": { + "semver": "bin/semver.js" } }, - "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==", + "node_modules/jackspeak": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", "dev": true, "dependencies": { - "tslib": "^2.0.3" + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "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 - }, - "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">= 0.6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "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, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "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==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "engines": { - "node": ">= 8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { - "node": ">=8.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "node_modules/jest-haste-map/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==", "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=8.6" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, - "bin": { - "mime": "cli.js" + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">= 0.6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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==", + "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, "dependencies": { - "mime-db": "1.52.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { - "node": "*" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "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, "dependencies": { - "minimist": "^1.2.5" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/mocha": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", - "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, - "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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, "dependencies": { - "balanced-match": "^1.0.0" + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/mocha/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==", + "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, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/mocha/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==", + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/mocha/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==", + "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, "dependencies": { - "brace-expansion": "^2.0.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/mocha/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==", + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/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, - "dependencies": { - "@isaacs/cliui": "^8.0.2" + "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "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==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { - "node": ">=10" + "node": ">= 10.13.0" } }, - "node_modules/mocha/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==", - "dev": true - }, - "node_modules/mocha/node_modules/supports-color": { + "node_modules/jest-worker/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==", @@ -5077,1004 +6730,1122 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "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==", + "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, - "engines": { - "node": "*" - } + "license": "MIT" }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "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/multer": { - "version": "1.4.5-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", - "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "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==", "dev": true, "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" + "argparse": "^2.0.1" }, - "engines": { - "node": ">= 6.0.0" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "bin": { - "nanoid": "bin/nanoid.cjs" + "jsesc": "bin/jsesc" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=6" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "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==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "engines": { - "node": ">= 0.6" - } + "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==", + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "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==", + "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, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } }, - "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==", + "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, "engines": { "node": ">=0.10.0" } }, - "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==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "dependencies": { - "boolbase": "^1.0.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "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==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=6.11.5" } }, - "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "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, + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "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, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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, - "engines": { - "node": ">= 0.4" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" + "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, + "dependencies": { + "color-convert": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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==", + "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, "dependencies": { - "ee-first": "1.1.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "node_modules/loupe": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz", + "integrity": "sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==", "dev": true, "dependencies": { - "wrappy": "1" + "get-func-name": "^2.0.1" } }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "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, - "bin": { - "opener": "bin/opener-bin.js" + "dependencies": { + "tslib": "^2.0.3" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "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 + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "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==", + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "tmpl": "1.0.5" } }, - "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==", + "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, - "dependencies": { - "p-limit": "^3.0.2" - }, + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "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==", + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true, "engines": { - "node": ">=6" + "node": ">= 0.6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true - }, - "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==", + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/dword-design" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "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==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" + "engines": { + "node": ">= 8" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "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, "dependencies": { - "callsites": "^3.0.0" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=6" + "node": ">=8.6" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "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, "engines": { - "node": ">= 0.8" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "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==", + "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==", "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" + "engines": { + "node": ">= 0.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==", + "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==", "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, "engines": { - "node": ">=8" + "node": ">= 0.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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "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==", + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "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 + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } }, - "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==", + "node_modules/mocha": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", "dev": true, + "license": "MIT", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" }, - "engines": { - "node": ">=16 || 14 >=14.18" + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "node_modules/mocha/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, "engines": { - "node": ">= 14.16" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "node_modules/mocha/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==", "dev": true, - "engines": { - "node": ">=12" + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/mocha/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, "dependencies": { - "find-up": "^4.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/pkg-dir/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==", + "node_modules/mocha/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, "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/pkg-dir/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==", + "node_modules/mocha/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, "dependencies": { - "p-locate": "^4.1.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/pkg-dir/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==", + "node_modules/mocha/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==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/pkg-dir/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==", + "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, + "engines": { + "node": "*" + } + }, + "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==", + "dev": true + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", "dev": true, + "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">= 6.0.0" } }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "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 + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true + }, + "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, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "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 + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/prettier": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", - "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, + "license": "MIT", "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "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, - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" + "url": "https://github.com/sponsors/ljharb" } }, - "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 - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", "dev": true, "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "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, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dev": true, "dependencies": { - "side-channel": "^1.0.6" + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" }, "engines": { - "node": ">=0.6" + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "dependencies": { - "safe-buffer": "^5.1.0" + "wrappy": "1" } }, - "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==", + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8.0" } }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "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, "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">= 0.8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "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, "dependencies": { - "picomatch": "^2.2.1" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=8.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "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, "engines": { - "node": ">=8.6" + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "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, + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/dword-design" } }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "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, "dependencies": { - "resolve": "^1.20.0" + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">=6" } }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 0.8" } }, - "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==", + "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, "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "engines": { "node": ">=0.10.0" } }, - "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==", + "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, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "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 + }, + "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, "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, - "bin": { - "resolve": "bin/resolve" + "engines": { + "node": ">=16 || 14 >=14.18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=16" } }, - "node_modules/resolve-cwd/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==", + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "engines": { "node": ">=8" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": ">=4" - } - }, - "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==", - "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "node": ">= 14.16" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, "engines": { - "node": "20 || >=22" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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, - "dependencies": { - "balanced-match": "^1.0.0" + "engines": { + "node": ">= 6" } }, - "node_modules/rimraf/node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "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, "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "find-up": "^4.0.0" }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", - "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "node_modules/pkg-dir/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, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": "20 || >=22" + "node": ">=8" } }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "node_modules/pkg-dir/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, "dependencies": { - "brace-expansion": "^2.0.1" + "p-locate": "^4.1.0" }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, - "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==", + "node_modules/pkg-dir/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, "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "p-try": "^2.0.0" }, "engines": { - "node": "20 || >=22" + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/rollup": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", - "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "node_modules/pkg-dir/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, "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" + "p-limit": "^2.2.0" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.8", - "@rollup/rollup-android-arm64": "4.34.8", - "@rollup/rollup-darwin-arm64": "4.34.8", - "@rollup/rollup-darwin-x64": "4.34.8", - "@rollup/rollup-freebsd-arm64": "4.34.8", - "@rollup/rollup-freebsd-x64": "4.34.8", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", - "@rollup/rollup-linux-arm-musleabihf": "4.34.8", - "@rollup/rollup-linux-arm64-gnu": "4.34.8", - "@rollup/rollup-linux-arm64-musl": "4.34.8", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", - "@rollup/rollup-linux-riscv64-gnu": "4.34.8", - "@rollup/rollup-linux-s390x-gnu": "4.34.8", - "@rollup/rollup-linux-x64-gnu": "4.34.8", - "@rollup/rollup-linux-x64-musl": "4.34.8", - "@rollup/rollup-win32-arm64-msvc": "4.34.8", - "@rollup/rollup-win32-ia32-msvc": "4.34.8", - "@rollup/rollup-win32-x64-msvc": "4.34.8", - "fsevents": "~2.3.2" + "node": ">=8" } }, - "node_modules/rollup-plugin-copy": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz", - "integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==", + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "@types/fs-extra": "^8.0.1", - "colorette": "^1.1.0", - "fs-extra": "^8.1.0", - "globby": "10.0.1", - "is-plain-object": "^3.0.0" + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=8.3" + "node": "^10 || ^12 || >=14" } }, - "node_modules/rollup-plugin-copy/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">= 0.8.0" } }, - "node_modules/rollup-plugin-copy/node_modules/jsonfile": { + "node_modules/pretty-error": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" } }, - "node_modules/rollup-plugin-copy/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, "engines": { - "node": ">= 4.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/rollup-plugin-dts": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.1.1.tgz", - "integrity": "sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==", + "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 + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "dependencies": { - "magic-string": "^0.30.10" + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" }, "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/Swatinem" - }, - "optionalDependencies": { - "@babel/code-frame": "^7.24.2" - }, - "peerDependencies": { - "rollup": "^3.29.4 || ^4", - "typescript": "^4.5 || ^5.0" + "node": ">= 0.10" } }, - "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==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "commenting": "~1.1.0", - "fdir": "^6.4.3", - "lodash": "~4.17.21", - "magic-string": "~0.30.0", - "moment": "~2.30.1", - "package-name-regex": "~2.0.6", - "spdx-expression-validate": "~2.0.0", - "spdx-satisfies": "~5.0.1" + "side-channel": "^1.1.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=0.6" }, - "peerDependencies": { - "rollup": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -6089,1294 +7860,1465 @@ "type": "consulting", "url": "https://feross.org/support" } - ], + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "dependencies": { - "queue-microtask": "^1.2.2" + "safe-buffer": "^5.1.0" } }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "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==", "dev": true, - "dependencies": { - "tslib": "^2.1.0" + "license": "MIT", + "engines": { + "node": ">= 0.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==", + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "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 }, - "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" + "picomatch": "^2.2.1" }, "engines": { - "node": ">= 10.13.0" + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "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, "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "resolve": "^1.20.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">= 10.13.0" } }, - "node_modules/schema-utils/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==", + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "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, "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" } }, - "node_modules/schema-utils/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==", - "dev": true + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, "bin": { - "semver": "bin/semver.js" + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "dependencies": { + "resolve-from": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/resolve-cwd/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "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==", "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, - "dependencies": { - "ms": "2.0.0" + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dev": true, + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, "engines": { - "node": ">= 0.8" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/send/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==", - "dev": true - }, - "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==", + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "randombytes": "^2.1.0" + "balanced-match": "^1.0.0" } }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", "dev": true, "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">= 0.8.0" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", "dev": true, - "dependencies": { - "define-data-property": "^1.1.2", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - }, "engines": { - "node": ">= 0.4" + "node": "20 || >=22" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "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==", + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dev": true, "dependencies": { - "kind-of": "^6.0.2" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=8" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/shebang-command": { + "node_modules/rimraf/node_modules/path-scurry": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, "dependencies": { - "shebang-regex": "^3.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=8" - } - }, - "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, - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", - "dev": true, + "node": "20 || >=22" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "node_modules/rollup": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">= 0.4" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "fsevents": "~2.3.2" } }, - "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==", + "node_modules/rollup-plugin-copy": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz", + "integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==", "dev": true, - "engines": { - "node": ">=14" + "dependencies": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=8.3" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/rollup-plugin-copy/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, "engines": { - "node": ">=8" + "node": ">=6 <7 || >=8" } }, - "node_modules/smob": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", - "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==", - "dev": true + "node_modules/rollup-plugin-copy/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } }, - "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==", + "node_modules/rollup-plugin-copy/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 4.0.0" } }, - "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==", + "node_modules/rollup-plugin-dts": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.2.1.tgz", + "integrity": "sha512-sR3CxYUl7i2CHa0O7bA45mCrgADyAQ0tVtGSqi3yvH28M+eg1+g5d7kQ9hLvEz5dorK3XVsH5L2jwHLQf72DzA==", "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "magic-string": "^0.30.17" + }, "engines": { - "node": ">=0.10.0" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/Swatinem" + }, + "optionalDependencies": { + "@babel/code-frame": "^7.26.2" + }, + "peerDependencies": { + "rollup": "^3.29.4 || ^4", + "typescript": "^4.5 || ^5.0" } }, - "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==", + "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, "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "commenting": "~1.1.0", + "fdir": "^6.4.3", + "lodash": "~4.17.21", + "magic-string": "~0.30.0", + "moment": "~2.30.1", + "package-name-regex": "~2.0.6", + "spdx-expression-validate": "~2.0.0", + "spdx-satisfies": "~5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, - "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==", + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, + "license": "MIT", "dependencies": { - "array-find-index": "^1.0.2", - "spdx-expression-parse": "^3.0.0", - "spdx-ranges": "^2.0.0" + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" } }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "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==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "queue-microtask": "^1.2.2" } }, - "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==", + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, "dependencies": { - "spdx-expression-parse": "^3.0.0" + "tslib": "^2.1.0" } }, - "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", - "dev": true - }, - "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 - }, - "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==", + "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==", "dev": true, - "dependencies": { - "spdx-compare": "^1.0.0", - "spdx-expression-parse": "^3.0.0", - "spdx-ranges": "^2.0.0" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, - "engines": { - "node": ">= 0.8" - } + "license": "MIT" }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, "engines": { - "node": ">=10.0.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "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==", + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": ">=8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "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==", + "node_modules/schema-utils/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==", "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "fast-deep-equal": "^3.1.3" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "ajv": "^8.8.2" } }, - "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==", + "node_modules/schema-utils/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==", + "dev": true + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "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==", + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" }, "engines": { - "node": ">=8" + "node": ">= 18" } }, - "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==", + "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==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "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==", "dev": true, - "optional": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">=4" + "node": ">= 0.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==", + "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==", "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "randombytes": "^2.1.0" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "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==", "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, "engines": { - "node": ">=6" + "node": ">= 18" } }, - "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", - "dev": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", - "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "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, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" + "kind-of": "^6.0.2" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "node": ">=8" } }, - "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==", + "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, "dependencies": { - "is-number": "^7.0.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=8.0" + "node": ">=8" } }, - "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==", + "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, "engines": { - "node": ">=0.6" + "node": ">=8" } }, - "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==", + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", "dev": true, - "bin": { - "tree-kill": "cli.js" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, "engines": { - "node": ">=18.12" + "node": ">= 0.4" }, - "peerDependencies": { - "typescript": ">=4.8.4" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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 - }, - "node_modules/tsx": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", - "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=18.0.0" + "node": ">= 0.4" }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, + "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, + "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, - "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "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, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, "engines": { - "node": ">=14.17" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, "engines": { - "node": ">=0.8.0" + "node": ">=8" } }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "node_modules/smob": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", + "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==", + "dev": true }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "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==", "dev": true, "engines": { - "node": ">= 10.0.0" + "node": ">=0.10.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "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==", "dev": true, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "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==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "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, + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "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, "dependencies": { - "punycode": "^2.1.0" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, - "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==", + "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, "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" + "spdx-expression-parse": "^3.0.0" } }, - "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==", + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, - "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==", + "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 }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "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, + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "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, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, "engines": { - "node": ">= 0.4.0" + "node": ">=10" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/vite": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz", - "integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==", + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "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": { + "node": ">=0.6.19" + } + }, + "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, "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.5.2", - "rollup": "^4.30.1" - }, - "bin": { - "vite": "bin/vite.js" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "node": ">=8" + } + }, + "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, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", - "cpu": [ - "ppc64" - ], + "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, - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", - "cpu": [ - "arm" - ], + "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, - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", - "cpu": [ - "arm64" - ], + "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, - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", - "cpu": [ - "x64" - ], + "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, - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", - "cpu": [ - "arm64" - ], + "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, - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", - "cpu": [ - "x64" - ], + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">=6" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", - "cpu": [ - "arm64" - ], + "node_modules/terser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", - "cpu": [ - "x64" - ], + "node_modules/terser-webpack-plugin": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, "engines": { - "node": ">=18" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } } }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", - "cpu": [ - "arm" - ], + "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, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", - "cpu": [ - "arm64" - ], + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, "engines": { - "node": ">=18" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", - "cpu": [ - "ia32" - ], + "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 + }, + "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, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "is-number": "^7.0.0" + }, "engines": { - "node": ">=18" + "node": ">=8.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", - "cpu": [ - "loong64" - ], + "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==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=0.6" } }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", - "cpu": [ - "mips64el" - ], + "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, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "bin": { + "tree-kill": "cli.js" } }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", - "cpu": [ - "ppc64" - ], + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", - "cpu": [ - "riscv64" - ], + "node_modules/tsconfck": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", + "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "bin": { + "tsconfck": "bin/tsconfck.js" + }, "engines": { - "node": ">=18" + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", - "cpu": [ - "s390x" - ], + "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 + }, + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, "engines": { - "node": ">=18" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", - "cpu": [ - "x64" - ], + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "prelude-ls": "^1.2.1" + }, "engines": { - "node": ">=18" + "node": ">= 0.8.0" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", - "cpu": [ - "arm64" - ], + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, "engines": { - "node": ">=18" + "node": ">= 0.6" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", - "cpu": [ - "x64" - ], + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "optional": true, - "os": [ - "netbsd" - ], + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=18" + "node": ">=14.17" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", - "cpu": [ - "arm64" - ], + "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==", "dev": true, "optional": true, - "os": [ - "openbsd" - ], + "bin": { + "uglifyjs": "bin/uglifyjs" + }, "engines": { - "node": ">=18" + "node": ">=0.8.0" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", - "cpu": [ - "x64" - ], + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true, - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=18" + "node": ">= 10.0.0" } }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", - "cpu": [ - "x64" - ], + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, - "optional": true, - "os": [ - "sunos" - ], + "license": "MIT", "engines": { - "node": ">=18" + "node": ">= 0.8" } }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", - "cpu": [ - "arm64" - ], + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, - "optional": true, - "os": [ - "win32" + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], - "engines": { - "node": ">=18" + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", - "cpu": [ - "ia32" - ], + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "punycode": "^2.1.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", - "cpu": [ - "x64" - ], + "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, + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "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 + }, + "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 + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", "dev": true, - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">= 0.8" } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "node_modules/vite": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", + "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", "dev": true, - "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, "bin": { - "esbuild": "bin/esbuild" + "vite": "bin/vite.js" }, "engines": { - "node": ">=18" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "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==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "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, + "dependencies": { + "makeerror": "1.0.12" } }, "node_modules/watchpack": { @@ -7393,10 +9335,11 @@ } }, "node_modules/webpack": { - "version": "5.98.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", - "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", + "version": "5.99.6", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz", + "integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -7637,24 +9580,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/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, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/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 - }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -7671,30 +9596,29 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/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==", + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, - "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" }, "engines": { - "node": ">=7.0.0" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/wrap-ansi/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/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, "node_modules/xtend": { @@ -7715,6 +9639,12 @@ "node": ">=10" } }, + "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 + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 2400a2993..4d6f1515f 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,8 @@ { "name": "@coderline/alphatab", - "version": "1.4.4", + "version": "1.5.0", "description": "alphaTab is a music notation and guitar tablature rendering library", - "keywords": [ - "guitar", - "music-notation", - "music-sheet", - "html5", - "svg", - "guitar-tablature" - ], + "keywords": ["guitar", "music-notation", "music-sheet", "html5", "svg", "guitar-tablature"], "homepage": "https://alphatab.net", "bugs": { "url": "https://github.com/coderline/alphaTab/issues" @@ -26,19 +19,19 @@ "typings": "dist/alphaTab.d.ts", "exports": { ".": { + "types": "./dist/alphaTab.d.ts", "import": "./dist/alphaTab.mjs", - "require": "./dist/alphaTab.js", - "types": "./dist/alphaTab.d.ts" + "require": "./dist/alphaTab.js" }, "./webpack": { + "types": "./dist/alphaTab.webpack.d.ts", "import": "./dist/alphaTab.webpack.mjs", - "require": "./dist/alphaTab.webpack.js", - "types": "./dist/alphaTab.webpack.d.ts" + "require": "./dist/alphaTab.webpack.js" }, "./vite": { + "types": "./dist/alphaTab.vite.d.ts", "import": "./dist/alphaTab.vite.mjs", - "require": "./dist/alphaTab.vite.js", - "types": "./dist/alphaTab.vite.d.ts" + "require": "./dist/alphaTab.vite.js" }, "./soundfont/*": "./dist/soundfont/*", "./font/*": "./dist/font/*" @@ -47,13 +40,20 @@ "node": ">=6.0.0" }, "scripts": { - "clean": "rimraf dist && rimraf .rollup.cache", - "lint": "eslint .", - "start": "node scripts/setup-playground.js && npm run generate-typescript && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript -w", + "clean": "rimraf dist", + "lint": "biome lint", + "lint-fix": "biome lint --write", + "lint-ci": "biome lint --reporter=github", + "format": "biome format --write", + "start": "node scripts/setup-playground.js && npm run generate-typescript && vite", "generate-typescript": "rimraf src/generated && tsx src.compiler/typescript/AlphaTabGenerator.ts", "generate-csharp": "npm run generate-typescript && tsx src.compiler/csharp/CSharpTranspiler.ts --outDir dist/lib.csharp", "generate-kotlin": "npm run generate-typescript && tsx src.compiler/kotlin/KotlinTranspiler.ts --outDir dist/lib.kotlin", - "build": "npm run generate-typescript && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript", + "build": "npm run generate-typescript && npm run build-web-full", + "build-web-full": "npm run build-web && npm run build-vite && npm run build-webpack", + "build-web": "vite build --mode esm && vite build --mode umd", + "build-vite": "vite build --mode vite-cjs && vite build --mode vite-esm", + "build-webpack": "vite build --mode webpack-cjs && vite build --mode webpack-esm", "build-csharp": "npm run generate-csharp && cd src.csharp && dotnet build -c Release", "build-kotlin": "npm run generate-kotlin && node scripts/gradlew.mjs assembleRelease", "test": "mocha", @@ -64,11 +64,14 @@ "prepack": "node scripts/prepack.mjs" }, "devDependencies": { - "@coderline/alphaskia": "^2.3.120", - "@coderline/alphaskia-windows": "^2.3.120", + "@biomejs/biome": "^1.9.4", + "@coderline/alphaskia": "^3.3.135", + "@coderline/alphaskia-windows": "^3.3.135", + "@coderline/alphaskia-linux": "^3.3.135", "@fontsource/noto-sans": "^5.1.1", "@fontsource/noto-serif": "^5.1.1", "@fortawesome/fontawesome-free": "^6.7.2", + "@microsoft/api-extractor": "^7.51.1", "@popperjs/core": "^2.11.8", "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-node-resolve": "^16.0.0", @@ -77,26 +80,29 @@ "@rollup/plugin-typescript": "^12.1.2", "@types/chai": "^5.0.1", "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", "@types/mocha": "^10.0.10", + "@types/node": "^22.13.4", + "@types/opener": "^1.4.3", "@typescript-eslint/eslint-plugin": "^8.24.1", "@typescript-eslint/parser": "^8.24.1", "ace-builds": "^1.38.0", "assert": "^2.1.0", "bootstrap": "^5.3.3", "chai": "^5.2.0", + "chalk": "^5.4.1", "concurrently": "^9.1.2", "cors": "^2.8.5", "eslint": "^9.20.1", - "express": "^4.21.2", + "express": "^5.1.0", "fs-extra": "^11.3.0", "handlebars": "^4.7.8", "html-webpack-plugin": "^5.6.3", + "jest-snapshot": "^29.7.0", "mocha": "^11.1.0", "multer": "^1.4.5-lts.1", "opener": "^1.5.2", - "prettier": "^3.5.2", "rimraf": "^6.0.1", - "rollup": "^4.34.8", "rollup-plugin-copy": "^3.5.0", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-license": "^3.6.0", @@ -104,7 +110,8 @@ "tslib": "^2.8.1", "tsx": "^4.19.3", "typescript": "^5.7.3", - "vite": "^6.1.1", + "vite": "^6.2.0", + "vite-tsconfig-paths": "^5.1.4", "webpack": "^5.98.0", "webpack-cli": "^6.0.1" }, @@ -118,10 +125,5 @@ "/dist/soundfont/*", "LICENSE", "LICENSE.header" - ], - "dependencies": { - "@types/express": "^5.0.0", - "@types/node": "^22.13.4", - "@types/opener": "^1.4.3" - } + ] } diff --git a/playground-template/alphatex-editor.html b/playground-template/alphatex-editor.html index b5149f2b2..4b62bc4e1 100644 --- a/playground-template/alphatex-editor.html +++ b/playground-template/alphatex-editor.html @@ -51,15 +51,21 @@ function setupEditor(api, selector) { const element = document.querySelector(selector); - element.innerHTML = trimCode(element.innerHTML); + + const initialCode = sessionStorage.getItem('alphatex-editor.code') ?? trimCode(element.innerHTML); + element.innerHTML = initialCode; const editor = ace.edit(element, { mode: 'ace/mode/tex' }); editor.session.on('change', () => { + const tex = editor.getSession().getDocument().getAllLines().join('\n'); api.tex(editor.getSession().getDocument().getAllLines().join('\n'), 'all'); + sessionStorage.setItem('alphatex-editor.code', tex) }); } + + const req = new XMLHttpRequest(); req.onload = data => { document.getElementById('placeholder').outerHTML = req.responseText; diff --git a/playground-template/control-template.html b/playground-template/control-template.html index 141ed1f6e..ce1ea39f5 100644 --- a/playground-template/control-template.html +++ b/playground-template/control-template.html @@ -102,6 +102,21 @@
+
+ + +
{ + await at.setOutputDevice(device); + }; + item.innerText = device.label + (device.isDefault ? ' (default)' : ''); + return item; + } + + control.querySelector('.at-output-device').addEventListener('show.bs.dropdown', async () => { + const devices = await at.enumerateOutputDevices(); + if (devices.length === 0) { + return; + } + + const list = control.querySelector('.at-output-device .dropdown-menu'); + list.innerHTML = ''; + for (const d of devices) { + const item = createOutputDeviceItem(d); + list.appendChild(item); + } + }); + control.querySelectorAll('.at-speed-options a').forEach(function (a) { a.onclick = function (e) { e.preventDefault(); diff --git a/rollup.config.cjs.ts b/rollup.config.cjs.ts deleted file mode 100644 index 18d001826..000000000 --- a/rollup.config.cjs.ts +++ /dev/null @@ -1,48 +0,0 @@ -import terser from '@rollup/plugin-terser'; -import { OutputOptions, Plugin, RollupOptions } from 'rollup'; - -const importMetaPlugin = { - name: 'import-meta', - resolveImportMeta() { - return '{}'; // prevent import.meta to be empty in non ES outputs - } -}; - -/** - * Creates the ESM flavor configurations for alphaTab - */ -export default function cjs( - isWatch: boolean, - commonOutput: Partial, - bundlePlugins: Plugin[] -): RollupOptions[] { - const withCjs = !isWatch || !!process.env.ALPHATAB_CJS; - if (!withCjs) { - return []; - } - - return [ - { - input: `src/alphaTab.main.ts`, - output: [ - { - ...commonOutput, - file: 'dist/alphaTab.js', - plugins: [importMetaPlugin], - sourcemap: true - }, - { - ...commonOutput, - file: 'dist/alphaTab.min.js', - plugins: [terser(), importMetaPlugin], - sourcemap: true - } - ], - watch: { - include: ['src/**'], - exclude: 'node_modules/**' - }, - plugins: [...bundlePlugins] - } - ]; -} diff --git a/rollup.config.esm.ts b/rollup.config.esm.ts deleted file mode 100644 index 0b3fff466..000000000 --- a/rollup.config.esm.ts +++ /dev/null @@ -1,121 +0,0 @@ -import terser from '@rollup/plugin-terser'; -import copy from 'rollup-plugin-copy'; -import server from './rollup.plugin.server'; -import MagicString from 'magic-string'; -import { OutputOptions, Plugin, RollupOptions } from 'rollup'; - -/** - * Creates the ESM flavor configurations for alphaTab - */ -export default function esm( - isWatch: boolean, - commonOutput: Partial, - bundlePlugins: Plugin[] -): RollupOptions[] { - return [ - // Core Bundle - { - input: `src/alphaTab.core.ts`, - output: [ - { - file: 'dist/alphaTab.core.mjs', - format: 'es', - sourcemap: true - }, - !isWatch && { - file: 'dist/alphaTab.core.min.mjs', - format: 'es', - plugins: [terser()], - sourcemap: true - } - ] - .filter(e => typeof e == 'object') - .map(o => ({ ...commonOutput, ...o } as OutputOptions)), - external: [], - watch: { - include: ['src/**'], - exclude: 'node_modules/**' - }, - plugins: [ - ...bundlePlugins, - copy({ - targets: [ - { src: 'font/bravura/*', dest: 'dist/font' }, - { src: 'font/sonivox/*', dest: 'dist/soundfont' } - ] - }), - isWatch && - server({ - openPage: '/playground/control.html', - port: 8080 - }) - ] - }, - - // Entry points - ...[ - { input: 'alphaTab.main', output: 'alphaTab' }, - { input: 'alphaTab.worker', output: 'alphaTab.worker' }, - { input: 'alphaTab.worklet', output: 'alphaTab.worklet' } - ].map(x => { - return { - input: `src/${x.input}.ts`, - output: [ - { - file: `dist/${x.output}.mjs`, - format: 'es' as const, - sourcemap: true, - plugins: [ - { - name: 'adjust-script-paths', - renderChunk(code) { - const modifiedCode = new MagicString(code); - - modifiedCode - .replaceAll("alphaTab.core'", "alphaTab.core.mjs'") - .replaceAll("alphaTab.worker'", "alphaTab.worker.mjs'") - .replaceAll("alphaTab.worklet'", "alphaTab.worklet.mjs'"); - - return { - code: modifiedCode.toString(), - map: modifiedCode.generateMap() - }; - } - } as Plugin - ] - }, - { - file: `dist/${x.output}.min.mjs`, - format: 'es' as const, - plugins: [ - { - name: 'adjust-script-paths', - renderChunk(code) { - const modifiedCode = new MagicString(code); - - modifiedCode - .replaceAll("alphaTab.core'", "alphaTab.core.min.mjs'") - .replaceAll("alphaTab.worker'", "alphaTab.worker.min.mjs'") - .replaceAll("alphaTab.worklet'", "alphaTab.worklet.min.mjs'"); - - return { - code: modifiedCode.toString(), - map: modifiedCode.generateMap() - }; - } - } as Plugin, - terser() - ], - sourcemap: true - } - ], - external: ['./alphaTab.core'], - watch: { - include: [`src/${x.input}.ts`], - exclude: 'node_modules/**' - }, - plugins: [...bundlePlugins.filter(p => p.name !== 'resolve-typescript-paths')] - }; - }) - ]; -} diff --git a/rollup.config.ts b/rollup.config.ts deleted file mode 100644 index 2bc897289..000000000 --- a/rollup.config.ts +++ /dev/null @@ -1,101 +0,0 @@ -import resolve from './rollup.plugin.resolve'; -import dts from 'rollup-plugin-dts'; -import license from 'rollup-plugin-license'; -import fs from 'fs'; -import typescript from '@rollup/plugin-typescript'; -import { ModuleFormat, RollupOptions } from 'rollup'; - -import esm from './rollup.config.esm'; -import cjs from './rollup.config.cjs'; -import webpack from './rollup.config.webpack'; -import vite from './rollup.config.vite'; - -function getGitBranch(): string { - const filepath = '.git/HEAD'; - if (!fs.existsSync(filepath)) { - throw new Error('.git/HEAD does not exist'); - } - const buf = fs.readFileSync(filepath); - const match = /ref: refs\/heads\/([^\n]+)/.exec(buf.toString()); - return match ? match[1] : ''; -} - -const commonOutput = { - name: 'alphaTab', - format: 'umd' as ModuleFormat, - globals: { - jQuery: 'jQuery' - } -}; - -const bundlePlugins = [ - typescript({ - tsconfig: './tsconfig.build.json', - outputToFilesystem: true - }), - license({ - banner: { - content: { - file: 'LICENSE.header' - }, - data() { - let buildNumber = process.env.GITHUB_RUN_NUMBER || 0; - let gitBranch = getGitBranch(); - return { - branch: gitBranch, - build: buildNumber - }; - } - } - }), - resolve({ - mappings: { - '@src': 'dist/lib' - } - }) -]; - -const isWatch = !!process.env.ROLLUP_WATCH; - -export default [ - ...esm(isWatch, commonOutput, bundlePlugins), - ...cjs(isWatch, commonOutput, bundlePlugins), - - // - // typescript type declarations - { - input: 'dist/types/alphaTab.main.d.ts', - output: [ - { - file: 'dist/alphaTab.d.ts', - format: 'es' - } - ], - plugins: [ - dts(), - resolve({ - mappings: { - '@src': 'dist/types' - }, - types: true - }) - ] - } satisfies RollupOptions, - - // - // Bundlers - ...webpack(isWatch, bundlePlugins), - ...vite(isWatch, bundlePlugins) -].map(x => { - x.onLog = (level, log, handler) => { - if (log.code === 'CIRCULAR_DEPENDENCY') { - return; // Ignore circular dependency warnings - } - handler(level, log); - }; - - if (x.watch) { - x.watch.clearScreen = false; - } - return x; -}); diff --git a/rollup.config.vite.ts b/rollup.config.vite.ts deleted file mode 100644 index aa54c8983..000000000 --- a/rollup.config.vite.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Plugin, RollupOptions } from 'rollup'; -import dts from 'rollup-plugin-dts'; -import resolve from './rollup.plugin.resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -const importMetaPlugin = { - name: 'import-meta', - resolveImportMeta() { - return '{}'; // prevent import.meta to be empty in non ES outputs - } -}; - -export default function vite(isWatch: boolean, bundlePlugins: Plugin[]): RollupOptions[] { - const outputVite = !isWatch || !!process.env.ALPHATAB_VITE; - if (!outputVite) { - return []; - } - - return [ - { - input: 'dist/types/alphaTab.vite.d.ts', - output: [ - { - file: 'dist/alphaTab.vite.d.ts', - format: 'es' - } - ], - external: ['vite', 'rollup', 'fs', 'url'], - plugins: [ - dts(), - resolve({ - mappings: { - '@src': 'dist/types' - }, - types: true - }) - ] - }, - { - input: 'src/alphaTab.vite.ts', - output: [ - { - name: 'alphaTabVite', - file: 'dist/alphaTab.vite.js', - plugins: [importMetaPlugin], - sourcemap: true, - format: 'cjs' - } - ], - external: ['vite', 'rollup'], - watch: { - include: ['src/alphaTab.vite.ts', 'src/vite/**'], - exclude: 'node_modules/**' - }, - plugins: [...bundlePlugins, commonjs(), nodeResolve()] - }, - { - input: 'src/alphaTab.vite.ts', - output: [ - { - file: 'dist/alphaTab.vite.mjs', - plugins: [], - sourcemap: true, - format: 'es' - } - ], - external: ['vite', 'rollup', 'fs', 'path'], - watch: { - include: ['src/alphaTab.vite.ts', 'src/vite/**'], - exclude: 'node_modules/**' - }, - plugins: [...bundlePlugins, nodeResolve()] - } - ]; -} diff --git a/rollup.config.webpack.ts b/rollup.config.webpack.ts deleted file mode 100644 index 229fc1132..000000000 --- a/rollup.config.webpack.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Plugin, RollupOptions } from 'rollup'; -import dts from 'rollup-plugin-dts'; -import resolve from './rollup.plugin.resolve'; -import commonjs from '@rollup/plugin-commonjs'; - -const importMetaPlugin = { - name: 'import-meta', - resolveImportMeta() { - return '{}'; // prevent import.meta to be empty in non ES outputs - } -}; - -export default function webpack(isWatch: boolean, bundlePlugins: Plugin[]): RollupOptions[] { - const outputWebPack = !isWatch || !!process.env.ALPHATAB_WEBPACK; - if (!outputWebPack) { - return []; - } - - return [ - { - input: 'dist/types/alphaTab.webpack.d.ts', - output: [ - { - file: 'dist/alphaTab.webpack.d.ts', - format: 'es' - } - ], - external: ['webpack'], - plugins: [ - dts(), - resolve({ - mappings: { - '@src': 'dist/types' - }, - types: true - }) - ] - }, - { - input: 'src/alphaTab.webpack.ts', - output: [ - { - name: 'alphaTabWebPack', - file: 'dist/alphaTab.webpack.js', - plugins: [importMetaPlugin], - sourcemap: true, - format: 'cjs' - } - ], - external: [ - 'webpack', - 'fs', - 'path', - 'url' - ], - watch: { - include: ['src/alphaTab.webpack.ts'], - exclude: 'node_modules/**' - }, - plugins: [...bundlePlugins, commonjs()] - }, - { - input: 'src/alphaTab.webpack.ts', - output: [ - { - file: 'dist/alphaTab.webpack.mjs', - plugins: [], - sourcemap: true, - format: 'es' - } - ], - external: [ - 'webpack', - 'fs', - 'path' - ], - watch: { - include: ['src/alphaTab.webpack.ts', 'src/webpack/**'], - exclude: 'node_modules/**' - }, - plugins: [...bundlePlugins] - } - ]; -} diff --git a/rollup.plugin.resolve.ts b/rollup.plugin.resolve.ts deleted file mode 100644 index 51fa740f2..000000000 --- a/rollup.plugin.resolve.ts +++ /dev/null @@ -1,70 +0,0 @@ -import path from 'path'; -import glob from 'glob'; -import fs from 'fs'; -import { Plugin } from 'rollup'; - -export interface ResolvePluginOptions { - mappings: { - [key: string]: string - }, - types?: boolean -} - -export default function resolve(options: ResolvePluginOptions) { - const mappings = options.mappings; - const types = options.types; - - return { - name: 'resolve-typescript-paths', - resolveId: function (importee, importer) { - if (typeof importer === 'undefined' || importee.startsWith('\0')) { - return null; - } - - const extension = types ? '.d.ts' : '.js'; - - if (fs.existsSync(importee)) { - return importee; - } else if (importee.startsWith('**')) { - return importee; - } else { - const importerDir = path.dirname(importer); - let resolved = importee; - - let match = Object.entries(mappings).filter(m => importee.startsWith(m[0])); - if (match && match.length > 0) { - if (match[0][1].endsWith(extension)) { - resolved = path.join(process.cwd(), match[0][1]); - } else { - resolved = path.join(process.cwd(), match[0][1], importee.substring(match[0][0].length)); - } - } else { - resolved = path.join(importerDir, importee); - } - - if (fs.existsSync(path.join(resolved, 'index' + extension))) { - resolved = path.join(resolved, 'index'); - } - - resolved += extension; - if (fs.existsSync(resolved)) { - return resolved; - } - - return null; - } - }, - load(id) { - if (id.startsWith('**')) { - const files = glob.sync(id, { - cwd: process.cwd() - }); - const source = files - .map((file, i) => `export * as _${i} from ${JSON.stringify(path.join(process.cwd(), file))};`) - .join('\r\n'); - return source; - } - return null; - } - } as Plugin; -}; diff --git a/rollup.plugin.server.ts b/rollup.plugin.server.ts deleted file mode 100644 index 22a57fbd1..000000000 --- a/rollup.plugin.server.ts +++ /dev/null @@ -1,117 +0,0 @@ -import express from 'express'; -import opener from 'opener'; -import fs from 'fs'; -import path from 'path'; -import cors from 'cors'; -import url from 'url'; -import { acceptOne } from './scripts/accept-new-reference-files.common' - -const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); - -export interface ServerOptions { - port: number; - openPage: string; -} - -let app: express.Express; -export default function server(options: ServerOptions) { - if (app) { - (app as any).close(); - } - - app = express(); - - app.use(cors()); - app.use(express.json()); - - const exposedFolders = ['dist', 'src', 'font', 'img', 'playground', 'playground-template', 'test-data', 'node_modules']; - - for (const exposedFolder of exposedFolders) { - app.use('/' + exposedFolder, express.static(exposedFolder)); - } - - app.get('/test-results', async (req, res) => { - try { - const response: any = []; - - async function crawl(d: string, name: string) { - // console.log('Crawling ', d); - const dir = await fs.promises.opendir(d); - try { - while (true) { - const entry = await dir.read(); - if (!entry) { - break; - } else if (entry.isDirectory() && entry.name !== '.' && entry.name !== '..') { - await crawl(path.join(d, entry.name), name + '/' + entry.name); - } else if (entry.isFile()) { - if (entry.name.endsWith('.new.png')) { - response.push({ - originalFile: name + '/' + entry.name.replace('.new.png', '.png'), - newFile: name + '/' + entry.name, - diffFile: name + '/' + entry.name.replace('.new.png', '.diff.png') - }); - } - } - } - } finally { - await dir.close(); - } - } - - const testDataPath = path.join(__dirname, 'test-data'); - // console.log('will crawl: ', testDataPath); - await crawl(testDataPath, 'test-data'); - - res.json(response); - } catch (e) { - res.json({ - message: (e as Error).message, - stack: (e as Error).stack - }); - } - }); - - app.post('/accept-test-result', async (req, res) => { - try { - const body = req.body; - // basic validation that nothing bad happens - if(typeof body.originalFile !== 'string'){ - res.sendStatus(400); - return; - } - const newFile = path.normalize(path.resolve(path.join(__dirname, body.newFile))); - const testDataPath = path.normalize(path.resolve(path.join(__dirname, 'test-data'))); - - if(!newFile.startsWith(testDataPath)){ - res.sendStatus(400); - return; - } - - await acceptOne(newFile); - res.json({ - message: 'Accepted' - }); - } catch (e) { - res.json({ - message: (e as Error).message, - stack: (e as Error).stack - }); - } - }); - - app.listen(options.port, () => { - console.log('Server listening on port ' + options.port); - }); - - let first = true; - return { - name: 'server', - generateBundle() { - if (first) { - first = false; - opener(`http://localhost:${options.port}` + options.openPage); - } - } - }; -} diff --git a/scripts/element-style-transformer.ts b/scripts/element-style-transformer.ts new file mode 100644 index 000000000..411ab73f0 --- /dev/null +++ b/scripts/element-style-transformer.ts @@ -0,0 +1,111 @@ +import ts from 'typescript'; + +export function isElementStyleHelper(node: ts.Statement): boolean { + return !!( + ts.isVariableStatement(node) && + node.declarationList.flags & ts.NodeFlags.Using && + node.declarationList.declarations.length === 1 && + node.declarationList.declarations[0].initializer && + node.declarationList.declarations[0].initializer.getText().includes('ElementStyleHelper') + ); +}; + + +export function elementStyleUsingTransformer() { + return (context: ts.TransformationContext) => { + return (source: ts.SourceFile) => { + // a transformer for a more lightweight "using" declaration. the built-in TS using declarations + // allocate a stack of scopes to register and free stuff. this is way too much overhead for our ElementStyleHelper + // which is called on very low level (e.g. on notes) + // here we convert it to a simple try->finally with some trade-off on variable scopes. + const rewriteElementStyleHelper = (block: ts.Block): ts.Block => { + const newStatements: ts.Statement[] = []; + + for (let i = 0; i < block.statements.length; i++) { + const node = block.statements[i]; + + // using s = ElementStyleHelper.track(...); + // -> + // const s = ElementStyleHelper.track(...); + // try { following statements } finally { s?.[Symbol.Dispose](); } + if (isElementStyleHelper(node)) { + const vs = node as ts.VariableStatement; + // lower using to a simple const + newStatements.push( + ts.factory.createVariableStatement( + vs.modifiers, + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + vs.declarationList.declarations[0].name, + undefined, + undefined, + vs.declarationList.declarations[0].initializer + ) + ], + ts.NodeFlags.Const + ) + ) + ); + + // wrap all upcoming statements into a try->finally + // note that this might break variable scopes if not used properly in code + // we do not pull (yet?) any declarations to the outer scope + const tryStatements: ts.Statement[] = []; + + i++; + for (; i < block.statements.length; i++) { + if (isElementStyleHelper(block.statements[i])) { + i--; + break; + } else { + tryStatements.push(visitor(block.statements[i]) as ts.Statement); + } + } + + // s?.[Symbol.dispose]?.(); + const freeResource = ts.factory.createExpressionStatement( + ts.factory.createCallChain( + ts.factory.createElementAccessChain( + ts.factory.createIdentifier( + (vs.declarationList.declarations[0].name as ts.Identifier).text + ), + ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('Symbol'), + ts.factory.createIdentifier('dispose') + ) + ), + ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), + undefined, + undefined + ) + ); + newStatements.push( + ts.factory.createTryStatement( + ts.factory.createBlock(tryStatements), + undefined, + ts.factory.createBlock([freeResource]) + ) + ); + } else { + newStatements.push(visitor(node) as ts.Statement); + } + } + + return ts.factory.createBlock(newStatements, true); + }; + + const visitor = (node: ts.Node) => { + if (ts.isBlock(node)) { + return rewriteElementStyleHelper(node); + } + + return ts.visitEachChild(node, visitor, context); + }; + + return ts.visitEachChild(source, visitor, context); + }; + }; +} + diff --git a/src.compiler/AstPrinterBase.ts b/src.compiler/AstPrinterBase.ts index 615da9ed0..45af13ee9 100644 --- a/src.compiler/AstPrinterBase.ts +++ b/src.compiler/AstPrinterBase.ts @@ -1,8 +1,8 @@ import * as cs from './csharp/CSharpAst'; -import * as ts from 'typescript'; -import * as path from 'path'; -import * as fs from 'fs'; -import CSharpEmitterContext from './csharp/CSharpEmitterContext'; +import ts from 'typescript'; +import path from 'node:path'; +import fs from 'node:fs'; +import type CSharpEmitterContext from './csharp/CSharpEmitterContext'; export default abstract class AstPrinterBase { protected _sourceFile: cs.SourceFile; @@ -96,7 +96,9 @@ export default abstract class AstPrinterBase { protected writeAttributes(d: cs.AttributedElement) { if (d.attributes) { - d.attributes.forEach(a => this.writeAttribute(a)); + for (const a of d.attributes) { + this.writeAttribute(a); + } } } @@ -181,14 +183,15 @@ export default abstract class AstPrinterBase { protected abstract writeNewExpression(expr: cs.NewExpression); protected abstract writeCastExpression(expr: cs.CastExpression); protected abstract writeNonNullExpression(expr: cs.NonNullExpression); + protected abstract writeYieldExpression(expr: cs.YieldExpression); protected writeToDoExpression(expr: cs.ToDoExpression) { this.write('/* TODO */'); } protected writeIdentifier(expr: cs.Identifier | string) { - let name; - if (typeof expr !== "string") { + let name: string; + if (typeof expr !== 'string') { name = this._context.getSymbolName(expr) ?? expr.text; } else { name = expr; @@ -196,7 +199,7 @@ export default abstract class AstPrinterBase { this.write(this.escapeIdentifier(name)); } - protected abstract escapeIdentifier(identifier:string) : string; + protected abstract escapeIdentifier(identifier: string): string; protected writeNullSafeExpression(expr: cs.NullSafeExpression) { this.writeExpression(expr.expression); @@ -210,14 +213,24 @@ export default abstract class AstPrinterBase { this.writeCommaSeparated(expr.typeArguments, t => this.writeType(t)); this.write('>'); } + + if (expr.nullSafe) { + this.write('?.'); + this.write(this._context.toMethodName('invoke')); + } + this.write('('); if (expr.arguments.length > 5) { this.writeLine(); this._indent++; - this.writeCommaSeparated(expr.arguments, a => { - this.writeExpression(a); - this.writeLine(); - }, true); + this.writeCommaSeparated( + expr.arguments, + a => { + this.writeExpression(a); + this.writeLine(); + }, + true + ); this._indent--; this.writeLine(); } else { @@ -265,7 +278,7 @@ export default abstract class AstPrinterBase { protected writeTypeParameterConstraints(typeParameters: cs.TypeParameterDeclaration[] | undefined) { if (typeParameters) { this._indent++; - typeParameters.forEach(p => { + for (const p of typeParameters) { if (p.constraint) { this.writeLine(); this.write('where '); @@ -273,7 +286,7 @@ export default abstract class AstPrinterBase { this.write(' : '); this.writeType(p.constraint, false, false, true); } - }); + } this._indent--; } } @@ -384,6 +397,9 @@ export default abstract class AstPrinterBase { case cs.SyntaxKind.TypeOfExpression: this.writeTypeOfExpression(expr as cs.TypeOfExpression); break; + case cs.SyntaxKind.YieldExpression: + this.writeYieldExpression(expr as cs.YieldExpression); + break; case cs.SyntaxKind.TypeReference: this.writeType(expr as cs.TypeReference); break; @@ -439,6 +455,9 @@ export default abstract class AstPrinterBase { case cs.SyntaxKind.TryStatement: this.writeTryStatement(s as cs.TryStatement); break; + case cs.SyntaxKind.LocalFunction: + this.writeLocalFunction(s as cs.LocalFunctionDeclaration); + break; } } @@ -446,7 +465,9 @@ export default abstract class AstPrinterBase { this.writeLine('try'); this.writeBlock(s.tryBlock); if (s.catchClauses) { - s.catchClauses.forEach(c => this.writeCatchClause(c)); + for (const c of s.catchClauses) { + this.writeCatchClause(c); + } } if (s.finallyBlock) { this.writeLine('finally'); @@ -586,7 +607,9 @@ export default abstract class AstPrinterBase { this.write('new'); this.beginBlock(); - expr.properties.forEach(p => this.writeAnonymousObjectProperty(p)); + for (const p of expr.properties) { + this.writeAnonymousObjectProperty(p); + } this.endBlock(); } @@ -597,4 +620,6 @@ export default abstract class AstPrinterBase { this.writeExpression(expr.value); this.writeLine(','); } + + protected abstract writeLocalFunction(expr: cs.LocalFunctionDeclaration); } diff --git a/src.compiler/BuilderHelpers.ts b/src.compiler/BuilderHelpers.ts index 72ef45c2b..ee198934c 100644 --- a/src.compiler/BuilderHelpers.ts +++ b/src.compiler/BuilderHelpers.ts @@ -1,5 +1,5 @@ -import * as ts from 'typescript'; -import * as path from 'path'; +import ts from 'typescript'; +import path from 'node:path'; const ignoredFiles = [/rollup.*/]; @@ -54,78 +54,6 @@ function findNode(node: ts.Node, kind: ts.SyntaxKind): ts.Node | null { return null; } -export function getTypeWithNullableInfo( - checker: ts.TypeChecker, - node: ts.TypeNode | ts.Type | undefined, - allowUnionAsPrimitive: boolean -) { - if (!node) { - return { - isNullable: false, - isUnionType: false, - type: {} as ts.Type - }; - } - - let isNullable = false; - let isUnionType = false; - let type: ts.Type | null = null; - if ('kind' in node) { - if (ts.isUnionTypeNode(node)) { - for (const t of node.types) { - if (t.kind === ts.SyntaxKind.NullKeyword) { - isNullable = true; - } else if (ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword) { - isNullable = true; - } else if (type !== null) { - if (allowUnionAsPrimitive) { - isUnionType = true; - type = checker.getTypeAtLocation(node); - break; - } else { - throw new Error( - 'Multi union types on JSON settings not supported: ' + - node.getSourceFile().fileName + - ':' + - node.getText() - ); - } - } else { - type = checker.getTypeAtLocation(t); - } - } - } else { - type = checker.getTypeAtLocation(node); - } - } else if ('flags' in node) { - if (node.isUnion()) { - for (const t of node.types) { - if ((t.flags & ts.TypeFlags.Null) !== 0 || (t.flags & ts.TypeFlags.Undefined) !== 0) { - isNullable = true; - } else if (type !== null) { - if (allowUnionAsPrimitive) { - isUnionType = true; - type = node; - break; - } else { - throw new Error('Multi union types on JSON settings not supported: ' + node.symbol.name); - } - } else { - type = t; - } - } - } else { - type = node; - } - } - - return { - isNullable, - isUnionType, - type: type as ts.Type - }; -} - export function unwrapArrayItemType(type: ts.Type, typeChecker: ts.TypeChecker): ts.Type | null { if (type.symbol && type.symbol.name === 'Array') { return (type as ts.TypeReference).typeArguments![0]; @@ -167,7 +95,7 @@ export function isPrimitiveType(type: ts.Type | null) { return true; } - return isEnumType(type); + return false; } export function isNumberType(type: ts.Type | null) { @@ -184,12 +112,18 @@ export function isNumberType(type: ts.Type | null) { export function isEnumType(type: ts.Type) { // if for some reason this returns true... - if (hasFlag(type, ts.TypeFlags.Enum)) return true; + if (hasFlag(type, ts.TypeFlags.Enum)) { + return true; + } // it's not an enum type if it's an enum literal type - if (hasFlag(type, ts.TypeFlags.EnumLiteral) && !type.isUnion()) return false; + if (hasFlag(type, ts.TypeFlags.EnumLiteral)) { + return true; + } // get the symbol and check if its value declaration is an enum declaration const symbol = type.getSymbol(); - if (!symbol) return false; + if (!symbol) { + return false; + } const { valueDeclaration } = symbol; return valueDeclaration && valueDeclaration.kind === ts.SyntaxKind.EnumDeclaration; @@ -199,10 +133,6 @@ export function wrapToNonNull(isNullableType: boolean, expr: ts.Expression, fact return isNullableType ? expr : factory.createNonNullExpression(expr); } -export function isTypedArray(type: ts.Type) { - return !!type.symbol?.members?.has(ts.escapeLeadingUnderscores('slice')); -} - export function hasFlag(type: ts.Type, flag: ts.TypeFlags): boolean { return (type.flags & flag) === flag; } @@ -229,7 +159,9 @@ function markNodeSynthesized(node: ts.Node): ts.Node { export function cloneTypeNode(node: T): T { if (ts.isUnionTypeNode(node)) { return ts.factory.createUnionTypeNode(node.types.map(cloneTypeNode)) as any as T; - } else if ( + } + + if ( node.kind === ts.SyntaxKind.StringKeyword || node.kind === ts.SyntaxKind.NumberKeyword || node.kind === ts.SyntaxKind.BooleanKeyword || @@ -238,77 +170,37 @@ export function cloneTypeNode(node: T): T { node.kind === ts.SyntaxKind.VoidKeyword ) { return ts.factory.createKeywordTypeNode(node.kind) as any as T; - } else if (ts.isLiteralTypeNode(node)) { + } + if (ts.isLiteralTypeNode(node)) { switch (node.literal.kind) { case ts.SyntaxKind.StringLiteral: return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(node.literal.text)) as any as T; default: return ts.factory.createLiteralTypeNode(node.literal) as any as T; } - } else if (ts.isArrayTypeNode(node)) { - return ts.factory.createArrayTypeNode(cloneTypeNode(node.elementType)) as any as T; - } else if (ts.isTypeReferenceNode(node)) { - return ts.factory.createTypeReferenceNode(cloneTypeNode(node.typeName), - node.typeArguments?.map(a => cloneTypeNode(a)) - ) as any as T; - } else if (ts.isIdentifier(node)) { - return ts.factory.createIdentifier(node.text) as any as T; - } else if (ts.isQualifiedName(node)) { - if (typeof node.right === 'string') { - return ts.factory.createQualifiedName(cloneTypeNode(node.left), node.right) as any as T; - } else { - return ts.factory.createQualifiedName(cloneTypeNode(node.left), cloneTypeNode(node.right)) as any as T; - } } - throw new Error(`Unsupported TypeNode: '${ts.SyntaxKind[node.kind]}' extend type node cloning`); -} - - -export function isPrimitiveToJson(type: ts.Type, typeChecker: ts.TypeChecker) { - if (!type) { - return false; + if (ts.isArrayTypeNode(node)) { + return ts.factory.createArrayTypeNode(cloneTypeNode(node.elementType)) as any as T; } - const isArray = isTypedArray(type); - const arrayItemType = unwrapArrayItemType(type, typeChecker); - - if (hasFlag(type, ts.TypeFlags.Unknown)) { - return true; + if (ts.isTypeReferenceNode(node)) { + return ts.factory.createTypeReferenceNode( + cloneTypeNode(node.typeName), + node.typeArguments?.map(a => cloneTypeNode(a)) + ) as any as T; } - if (hasFlag(type, ts.TypeFlags.Number)) { - return true; - } - if (hasFlag(type, ts.TypeFlags.String)) { - return true; - } - if (hasFlag(type, ts.TypeFlags.Boolean)) { - return true; + + if (ts.isIdentifier(node)) { + return ts.factory.createIdentifier(node.text) as any as T; } - if (arrayItemType) { - if (isArray && hasFlag(arrayItemType, ts.TypeFlags.Number)) { - return true; - } - if (isArray && hasFlag(arrayItemType, ts.TypeFlags.String)) { - return true; - } - if (isArray && hasFlag(arrayItemType, ts.TypeFlags.Boolean)) { - return true; - } - } else if (type.symbol) { - switch (type.symbol.name) { - case 'Uint8Array': - case 'Uint16Array': - case 'Uint32Array': - case 'Int8Array': - case 'Int16Array': - case 'Int32Array': - case 'Float32Array': - case 'Float64Array': - return true; + if (ts.isQualifiedName(node)) { + if (typeof node.right === 'string') { + return ts.factory.createQualifiedName(cloneTypeNode(node.left), node.right) as any as T; } + return ts.factory.createQualifiedName(cloneTypeNode(node.left), cloneTypeNode(node.right)) as any as T; } - return false; -} \ No newline at end of file + throw new Error(`Unsupported TypeNode: '${ts.SyntaxKind[node.kind]}' extend type node cloning`); +} diff --git a/src.compiler/TranspilerBase.ts b/src.compiler/TranspilerBase.ts index 280fde8d7..3c6753b11 100644 --- a/src.compiler/TranspilerBase.ts +++ b/src.compiler/TranspilerBase.ts @@ -4,9 +4,7 @@ function createDiagnosticReporter(pretty?: boolean): ts.DiagnosticReporter { const host: ts.FormatDiagnosticsHost = { getCurrentDirectory: () => ts.sys.getCurrentDirectory(), getNewLine: () => ts.sys.newLine, - getCanonicalFileName: ts.sys.useCaseSensitiveFileNames - ? x => x - : x => x.toLowerCase(), + getCanonicalFileName: ts.sys.useCaseSensitiveFileNames ? x => x : x => x.toLowerCase() }; if (!pretty) { @@ -19,7 +17,7 @@ function createDiagnosticReporter(pretty?: boolean): ts.DiagnosticReporter { } interface Emitter { - name: string, + name: string; emit(program: ts.Program, diagnostics: ts.Diagnostic[]): void; } @@ -38,7 +36,13 @@ export default function (emitters: Emitter[], handleErrors: boolean = false) { ts.sys.exit(ts.ExitStatus.InvalidProject_OutputsSkipped); }; - const parsedCommandLine = ts.getParsedCommandLineOfConfigFile(commandLine.options.project!, commandLine.options, parseConfigFileHost, /*extendedConfigCache*/ undefined, commandLine.watchOptions)!; + const parsedCommandLine = ts.getParsedCommandLineOfConfigFile( + commandLine.options.project!, + commandLine.options, + parseConfigFileHost, + /*extendedConfigCache*/ undefined, + commandLine.watchOptions + )!; const pretty = !!ts.sys.writeOutputIsTTY?.(); if (pretty) { reportDiagnostic = createDiagnosticReporter(true); @@ -48,7 +52,7 @@ export default function (emitters: Emitter[], handleErrors: boolean = false) { rootNames: parsedCommandLine.fileNames, options: parsedCommandLine.options, projectReferences: parsedCommandLine.projectReferences, - host: ts.createCompilerHost(parsedCommandLine.options), + host: ts.createCompilerHost(parsedCommandLine.options) }); let allDiagnostics: ts.Diagnostic[] = []; @@ -66,19 +70,23 @@ export default function (emitters: Emitter[], handleErrors: boolean = false) { program.getTypeChecker(); - for(const emitter of emitters) { + for (const emitter of emitters) { console.log(`[${emitter.name}] Emitting...`); emitter.emit(program, allDiagnostics); } if (handleErrors) { - let diagnostics = ts.sortAndDeduplicateDiagnostics(allDiagnostics); + const diagnostics = ts.sortAndDeduplicateDiagnostics(allDiagnostics); let errorCount = 0; let warningCount = 0; - for(const d of diagnostics) { + for (const d of diagnostics) { switch (d.category) { - case ts.DiagnosticCategory.Error: errorCount++; break; - case ts.DiagnosticCategory.Warning: warningCount++; break; + case ts.DiagnosticCategory.Error: + errorCount++; + break; + case ts.DiagnosticCategory.Warning: + warningCount++; + break; } reportDiagnostic(d); } @@ -90,7 +98,12 @@ export default function (emitters: Emitter[], handleErrors: boolean = false) { length: undefined, code: 6194, messageText: `Compilation completed with ${errorCount} errors and ${warningCount} warnings${ts.sys.newLine}`, - category: errorCount > 0 ? ts.DiagnosticCategory.Error : warningCount > 0 ? ts.DiagnosticCategory.Warning : ts.DiagnosticCategory.Message, + category: + errorCount > 0 + ? ts.DiagnosticCategory.Error + : warningCount > 0 + ? ts.DiagnosticCategory.Warning + : ts.DiagnosticCategory.Message }); } @@ -101,7 +114,7 @@ export default function (emitters: Emitter[], handleErrors: boolean = false) { } else { ts.sys.exit(ts.ExitStatus.Success); } - } else{ + } else { console.log('Done transpiling'); } -} \ No newline at end of file +} diff --git a/src.compiler/csharp/CSharpAst.ts b/src.compiler/csharp/CSharpAst.ts index 066e4112c..fc74628bd 100644 --- a/src.compiler/csharp/CSharpAst.ts +++ b/src.compiler/csharp/CSharpAst.ts @@ -1,85 +1,88 @@ -import * as ts from 'typescript'; +import type * as ts from 'typescript'; // Base export enum SyntaxKind { - SourceFile, - UsingDeclaration, - NamespaceDeclaration, - ClassDeclaration, - EnumDeclaration, - InterfaceDeclaration, - TypeParameterDeclaration, - MethodDeclaration, - ConstructorDeclaration, - FieldDeclaration, - PropertyDeclaration, - PropertyAccessorDeclaration, - ParameterDeclaration, - UnresolvedTypeNode, - TypeReference, - FunctionTypeNode, - PrimitiveTypeNode, - EnumMember, - ArrayTypeNode, - MapTypeNode, - - Block, - EmptyStatement, - VariableStatement, - ExpressionStatement, - IfStatement, - DoStatement, - WhileStatement, - ForStatement, - ForEachStatement, - BreakStatement, - ContinueStatement, - ReturnStatement, - SwitchStatement, - ThrowStatement, - TryStatement, - - VariableDeclarationList, - VariableDeclaration, - DeconstructDeclaration, - CaseClause, - DefaultClause, - CatchClause, - - PrefixUnaryExpression, - PostfixUnaryExpression, - NullLiteral, - TrueLiteral, - FalseLiteral, - ThisLiteral, - BaseLiteralExpression, - StringLiteral, - AwaitExpression, - BinaryExpression, - ConditionalExpression, - LambdaExpression, - NumericLiteral, - StringTemplateExpression, - IsExpression, - ParenthesizedExpression, - ArrayCreationExpression, - MemberAccessExpression, - AnonymousObjectCreationExpression, - AnonymousObjectProperty, - ElementAccessExpression, - InvocationExpression, - NewExpression, - CastExpression, - NonNullExpression, - NullSafeExpression, - Identifier, - DefaultExpression, - ToDoExpression, - TypeOfExpression, - - Attribute, - - SpreadExpression + SourceFile = 0, + UsingDeclaration = 1, + NamespaceDeclaration = 2, + ClassDeclaration = 3, + EnumDeclaration = 4, + InterfaceDeclaration = 5, + TypeParameterDeclaration = 6, + MethodDeclaration = 7, + ConstructorDeclaration = 8, + FieldDeclaration = 9, + PropertyDeclaration = 10, + PropertyAccessorDeclaration = 11, + ParameterDeclaration = 12, + UnresolvedTypeNode = 13, + TypeReference = 14, + FunctionTypeNode = 15, + PrimitiveTypeNode = 16, + EnumMember = 17, + ArrayTypeNode = 18, + MapTypeNode = 19, + ArrayTupleNode = 20, + + Block = 21, + EmptyStatement = 22, + VariableStatement = 23, + ExpressionStatement = 24, + IfStatement = 25, + DoStatement = 26, + WhileStatement = 27, + ForStatement = 28, + ForEachStatement = 29, + BreakStatement = 30, + ContinueStatement = 31, + ReturnStatement = 32, + SwitchStatement = 33, + ThrowStatement = 34, + TryStatement = 35, + + VariableDeclarationList = 36, + VariableDeclaration = 37, + DeconstructDeclaration = 38, + CaseClause = 39, + DefaultClause = 40, + CatchClause = 41, + + PrefixUnaryExpression = 42, + PostfixUnaryExpression = 43, + NullLiteral = 44, + TrueLiteral = 45, + FalseLiteral = 46, + ThisLiteral = 47, + BaseLiteralExpression = 48, + StringLiteral = 49, + AwaitExpression = 50, + BinaryExpression = 51, + ConditionalExpression = 52, + LambdaExpression = 53, + NumericLiteral = 54, + StringTemplateExpression = 55, + IsExpression = 56, + ParenthesizedExpression = 57, + ArrayCreationExpression = 58, + MemberAccessExpression = 59, + AnonymousObjectCreationExpression = 60, + AnonymousObjectProperty = 61, + ElementAccessExpression = 62, + InvocationExpression = 63, + NewExpression = 64, + CastExpression = 65, + NonNullExpression = 66, + NullSafeExpression = 67, + Identifier = 68, + DefaultExpression = 69, + ToDoExpression = 70, + TypeOfExpression = 71, + + Attribute = 72, + + SpreadExpression = 73, + LocalFunction = 74, + YieldExpression = 75 } export interface Node { @@ -91,16 +94,19 @@ export interface Node { } export interface SourceFile extends Node { + nodeType: SyntaxKind.SourceFile; fileName: string; usings: UsingDeclaration[]; namespace: NamespaceDeclaration; } export interface UsingDeclaration extends Node { + nodeType: SyntaxKind.UsingDeclaration; namespaceOrTypeName: string; } export interface NamespaceDeclaration extends Node { + nodeType: SyntaxKind.NamespaceDeclaration; namespace: string; declarations: NamespaceMember[]; } @@ -108,11 +114,11 @@ export interface NamespaceDeclaration extends Node { export type NamespaceMember = ClassDeclaration | EnumDeclaration | InterfaceDeclaration; export enum Visibility { - None, - Public, - Protected, - Private, - Internal + None = 0, + Public = 1, + Protected = 2, + Private = 3, + Internal = 4 } export interface DocumentedElement extends Node { @@ -123,6 +129,7 @@ export interface AttributedElement { } export interface Attribute extends Node { + nodeType: SyntaxKind.Attribute; type: TypeNode; arguments?: Expression[]; } @@ -134,6 +141,7 @@ export interface NamedElement { // Declarations export interface TypeParameterDeclaration extends NamedElement, Node { + nodeType: SyntaxKind.TypeParameterDeclaration; constraint?: TypeNode; } @@ -145,6 +153,7 @@ export interface NamedTypeDeclaration extends NamedElement, DocumentedElement, N } export interface ClassDeclaration extends NamedTypeDeclaration { + nodeType: SyntaxKind.ClassDeclaration; baseClass?: TypeNode; interfaces?: TypeNode[]; isAbstract: boolean; @@ -159,14 +168,17 @@ export type ClassMember = | NamedTypeDeclaration; export interface EnumDeclaration extends NamedTypeDeclaration { + nodeType: SyntaxKind.EnumDeclaration; members: EnumMember[]; } export interface EnumMember extends Node, NamedElement, DocumentedElement { + nodeType: SyntaxKind.EnumMember; initializer?: Expression; } export interface InterfaceDeclaration extends NamedTypeDeclaration { + nodeType: SyntaxKind.InterfaceDeclaration; interfaces?: TypeNode[]; members: InterfaceMember[]; } @@ -185,10 +197,12 @@ export interface MethodDeclarationBase extends MemberDeclaration { } export interface MethodDeclaration extends MethodDeclarationBase, AttributedElement { + nodeType: SyntaxKind.MethodDeclaration; isVirtual: boolean; isOverride: boolean; isAbstract: boolean; isTestMethod: boolean; + isGeneratorFunction: boolean; partial: boolean; returnType: TypeNode; parameters: ParameterDeclaration[]; @@ -197,16 +211,19 @@ export interface MethodDeclaration extends MethodDeclarationBase, AttributedElem } export interface ConstructorDeclaration extends MethodDeclarationBase { + nodeType: SyntaxKind.ConstructorDeclaration; baseConstructorArguments?: Expression[]; } export interface FieldDeclaration extends MemberDeclaration { + nodeType: SyntaxKind.FieldDeclaration; isReadonly: boolean; type: TypeNode; initializer?: Expression; } export interface PropertyDeclaration extends MemberDeclaration { + nodeType: SyntaxKind.PropertyDeclaration; isVirtual: boolean; isOverride: boolean; isAbstract: boolean; @@ -218,11 +235,13 @@ export interface PropertyDeclaration extends MemberDeclaration { } export interface PropertyAccessorDeclaration extends Node { + nodeType: SyntaxKind.PropertyAccessorDeclaration; keyword: string; body?: Block | Expression; } export interface ParameterDeclaration extends NamedElement, Node, DocumentedElement { + nodeType: SyntaxKind.ParameterDeclaration; type?: TypeNode; initializer?: Expression; params: boolean; @@ -236,6 +255,7 @@ export interface TypeNode extends Node { } export interface UnresolvedTypeNode extends TypeNode { + nodeType: SyntaxKind.UnresolvedTypeNode; tsType?: ts.Type; tsSymbol?: ts.Symbol; typeArguments?: UnresolvedTypeNode[]; @@ -243,40 +263,49 @@ export interface UnresolvedTypeNode extends TypeNode { export type TypeReferenceType = NamedTypeDeclaration | TypeParameterDeclaration | TypeNode | string; export interface TypeReference extends TypeNode { + nodeType: SyntaxKind.TypeReference; reference: TypeReferenceType; isAsync: boolean; typeArguments?: TypeNode[]; } export interface ArrayTypeNode extends TypeNode { + nodeType: SyntaxKind.ArrayTypeNode; elementType: TypeNode; } export interface MapTypeNode extends TypeNode { - keyType: TypeNode; + nodeType: SyntaxKind.MapTypeNode; + keyType: TypeNode | null; keyIsValueType: boolean; - valueType: TypeNode; + valueType: TypeNode | null; valueIsValueType: boolean; } +export interface ArrayTupleNode extends TypeNode { + nodeType: SyntaxKind.ArrayTupleNode; + types: TypeNode[]; +} + export interface FunctionTypeNode extends TypeNode { + nodeType: SyntaxKind.FunctionTypeNode; parameterTypes: TypeNode[]; returnType: TypeNode; } export enum PrimitiveType { - Bool, - String, - Double, - Int, - Void, - Object, - Dynamic, - Var, - Long + Bool = 0, + String = 1, + Double = 2, + Int = 3, + Void = 4, + Object = 5, + Var = 6, + Long = 7 } export interface PrimitiveTypeNode extends TypeNode { + nodeType: SyntaxKind.PrimitiveTypeNode; type: PrimitiveType; } @@ -285,178 +314,235 @@ export interface PrimitiveTypeNode extends TypeNode { export interface Expression extends Node {} export interface PrefixUnaryExpression extends Node { + nodeType: SyntaxKind.PrefixUnaryExpression; operand: Expression; operator: string; } export interface PostfixUnaryExpression extends Node { + nodeType: SyntaxKind.PostfixUnaryExpression; operand: Expression; operator: string; } -export interface NullLiteral extends Node {} +export interface NullLiteral extends Node { + nodeType: SyntaxKind.NullLiteral; +} -export interface BooleanLiteral extends Node {} +export interface BooleanLiteral extends Node { + nodeType: SyntaxKind.TrueLiteral | SyntaxKind.FalseLiteral; +} -export interface ThisLiteral extends Node {} +export interface ThisLiteral extends Node { + nodeType: SyntaxKind.ThisLiteral; +} export interface BaseLiteralExpression extends Node {} export interface StringLiteral extends Node { + nodeType: SyntaxKind.StringLiteral; text: string; } export interface AwaitExpression extends Node { + nodeType: SyntaxKind.AwaitExpression; expression: Expression; } export interface BinaryExpression extends Node { + nodeType: SyntaxKind.BinaryExpression; left: Expression; operator: string; right: Expression; } export interface ConditionalExpression extends Node { + nodeType: SyntaxKind.ConditionalExpression; condition: Expression; whenTrue: Expression; whenFalse: Expression; } export interface LambdaExpression extends Node { + nodeType: SyntaxKind.LambdaExpression; parameters: ParameterDeclaration[]; body: Block | Expression; returnType: TypeNode; } +export interface LocalFunctionDeclaration extends Node { + nodeType: SyntaxKind.LocalFunction; + name: string; + parameters: ParameterDeclaration[]; + body: Block; + returnType: TypeNode; +} + export interface NumericLiteral extends Node { + nodeType: SyntaxKind.NumericLiteral; value: string; } export interface StringTemplateExpression extends Node { + nodeType: SyntaxKind.StringTemplateExpression; chunks: (StringLiteral | Expression)[]; } export interface IsExpression extends Node { + nodeType: SyntaxKind.IsExpression; expression: Expression; type: TypeNode; newName?: string; } export interface ParenthesizedExpression extends Node { + nodeType: SyntaxKind.ParenthesizedExpression; expression: Expression; } export interface SpreadExpression extends Node { + nodeType: SyntaxKind.SpreadExpression; expression: Expression; } export interface DefaultExpression extends Node { + nodeType: SyntaxKind.DefaultExpression; type?: TypeNode; } export interface ArrayCreationExpression extends Node { + nodeType: SyntaxKind.ArrayCreationExpression; type?: TypeNode; values?: Expression[]; sizeExpression?: Expression; } export interface MemberAccessExpression extends Node { + nodeType: SyntaxKind.MemberAccessExpression; expression: Expression; member: string; nullSafe?: boolean; } export interface AnonymousObjectCreationExpression extends Node { + nodeType: SyntaxKind.AnonymousObjectCreationExpression; properties: AnonymousObjectProperty[]; } export interface AnonymousObjectProperty extends Node { + nodeType: SyntaxKind.AnonymousObjectProperty; name: string; value: Expression; } export interface ElementAccessExpression extends Node { + nodeType: SyntaxKind.ElementAccessExpression; expression: Expression; argumentExpression: Expression; nullSafe: boolean; } export interface InvocationExpression extends Node { + nodeType: SyntaxKind.InvocationExpression; expression: Expression; arguments: Expression[]; typeArguments?: TypeNode[]; + nullSafe?: boolean; } export interface NewExpression extends Node { + nodeType: SyntaxKind.NewExpression; type: TypeNode; arguments: Expression[]; } export interface CastExpression extends Node { + nodeType: SyntaxKind.CastExpression; type: TypeNode; expression: Expression; } export interface NonNullExpression extends Node { + nodeType: SyntaxKind.NonNullExpression; expression: Expression; } export interface TypeOfExpression extends Node { - expression: Expression; + nodeType: SyntaxKind.TypeOfExpression; + expression?: Expression; + type?: TypeNode; } export interface NullSafeExpression extends Node { + nodeType: SyntaxKind.NullSafeExpression; expression: Expression; } export interface Identifier extends Expression { + nodeType: SyntaxKind.Identifier; text: string; } -export interface ToDoExpression extends Node {} +export interface ToDoExpression extends Node { + nodeType: SyntaxKind.ToDoExpression; +} +export interface YieldExpression extends Node { + nodeType: SyntaxKind.YieldExpression; + expression: Expression | null; +} // Statements export interface Statement extends Node {} export interface Block extends Statement { + nodeType: SyntaxKind.Block; statements: Statement[]; } -export interface EmptyStatement extends Statement {} +export interface EmptyStatement extends Statement { + nodeType: SyntaxKind.EmptyStatement; +} export enum VariableStatementKind { - Normal, - Const, - Using, - AwaitUsing + Normal = 0, + Const = 1, + Using = 2, + AwaitUsing = 3 } export interface VariableStatement extends Statement { + nodeType: SyntaxKind.VariableStatement; declarationList: VariableDeclarationList; variableStatementKind: VariableStatementKind; } export interface ExpressionStatement extends Statement { + nodeType: SyntaxKind.ExpressionStatement; expression: Expression; } export interface IfStatement extends Statement { + nodeType: SyntaxKind.IfStatement; expression: Expression; thenStatement: Statement; elseStatement?: Statement; } export interface DoStatement extends Statement { + nodeType: SyntaxKind.DoStatement; expression: Expression; statement: Statement; } export interface WhileStatement extends Statement { + nodeType: SyntaxKind.WhileStatement; expression: Expression; statement: Statement; } export interface VariableDeclarationList extends Node { + nodeType: SyntaxKind.VariableDeclarationList; declarations: VariableDeclaration[]; isConst: boolean; } export interface VariableDeclaration extends Node { + nodeType: SyntaxKind.VariableDeclaration; type: TypeNode; name: string; deconstructNames?: string[]; @@ -464,10 +550,12 @@ export interface VariableDeclaration extends Node { } export interface DeconstructDeclaration extends Node { + nodeType: SyntaxKind.DeconstructDeclaration; names: string[]; } export interface ForStatement extends Statement { + nodeType: SyntaxKind.ForStatement; initializer?: VariableDeclarationList | Expression; condition?: Expression; incrementor?: Expression; @@ -475,44 +563,56 @@ export interface ForStatement extends Statement { } export interface ForEachStatement extends Statement { + nodeType: SyntaxKind.ForEachStatement; initializer: VariableDeclarationList | Expression; expression: Expression; statement: Statement; } -export interface BreakStatement extends Statement {} +export interface BreakStatement extends Statement { + nodeType: SyntaxKind.BreakStatement; +} -export interface ContinueStatement extends Statement {} +export interface ContinueStatement extends Statement { + nodeType: SyntaxKind.ContinueStatement; +} export interface ReturnStatement extends Statement { + nodeType: SyntaxKind.ReturnStatement; expression?: Expression; } export interface SwitchStatement extends Statement { + nodeType: SyntaxKind.SwitchStatement; expression: Expression; caseClauses: (CaseClause | DefaultClause)[]; } export interface CaseClause extends Node { + nodeType: SyntaxKind.CaseClause; expression: Expression; statements: Statement[]; } export interface DefaultClause extends Node { + nodeType: SyntaxKind.DefaultClause; statements: Statement[]; } export interface ThrowStatement extends Statement { + nodeType: SyntaxKind.ThrowStatement; expression?: Expression; } export interface TryStatement extends Statement { + nodeType: SyntaxKind.TryStatement; tryBlock: Block; catchClauses?: CatchClause[]; finallyBlock?: Block; } export interface CatchClause extends Node { + nodeType: SyntaxKind.CatchClause; variableDeclaration: VariableDeclaration; block: Block; } @@ -737,11 +837,15 @@ export function isToDoExpression(node: Node): node is ToDoExpression { export function isTypeOfExpression(node: Node): node is TypeOfExpression { return node.nodeType === SyntaxKind.TypeOfExpression; } - export function isAttribute(node: Node): node is Attribute { return node.nodeType === SyntaxKind.Attribute; } - export function isSpreadExpression(node: Node): node is SpreadExpression { return node.nodeType === SyntaxKind.SpreadExpression; } +export function isLocalFunction(node: Node): node is LocalFunctionDeclaration { + return node.nodeType === SyntaxKind.LocalFunction; +} +export function isYieldExpression(node: Node): node is YieldExpression { + return node.nodeType === SyntaxKind.YieldExpression; +} diff --git a/src.compiler/csharp/CSharpAstPrinter.ts b/src.compiler/csharp/CSharpAstPrinter.ts index 3ef958f8e..a2a25cd4f 100644 --- a/src.compiler/csharp/CSharpAstPrinter.ts +++ b/src.compiler/csharp/CSharpAstPrinter.ts @@ -1,13 +1,8 @@ import * as cs from './CSharpAst'; import * as ts from 'typescript'; -import CSharpEmitterContext from './CSharpEmitterContext'; import AstPrinterBase from '../AstPrinterBase'; export default class CSharpAstPrinter extends AstPrinterBase { - public constructor(sourceFile: cs.SourceFile, context: CSharpEmitterContext) { - super(sourceFile, context); - } - private keywords: Set = new Set([ 'abstract', 'as', @@ -90,7 +85,7 @@ export default class CSharpAstPrinter extends AstPrinterBase { protected override escapeIdentifier(identifier: string): string { if (this.keywords.has(identifier)) { - return '@' + identifier; + return `@${identifier}`; } return identifier; } @@ -186,7 +181,9 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.writeLine(); this.beginBlock(); - d.members.forEach(m => this.writeMember(m)); + for (const m of d.members) { + this.writeMember(m); + } this.endBlock(); } @@ -198,7 +195,9 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.writeLine(); this.beginBlock(); - d.members.forEach(m => this.writeEnumMember(m)); + for (const m of d.members) { + this.writeEnumMember(m); + } this.endBlock(); } @@ -250,12 +249,12 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.beginBlock(); let hasConstuctor = false; - d.members.forEach(m => { + for (const m of d.members) { this.writeMember(m); if (cs.isConstructorDeclaration(m) && !m.isStatic) { hasConstuctor = true; } - }); + } if (d.baseClass && !hasConstuctor) { let baseClass: cs.TypeReferenceType | undefined = d; @@ -264,7 +263,9 @@ export default class CSharpAstPrinter extends AstPrinterBase { if (typeof baseClass === 'string') { constructorDeclaration = undefined; break; - } else if (cs.isClassDeclaration(baseClass)) { + } + + if (cs.isClassDeclaration(baseClass)) { constructorDeclaration = baseClass.members.find(m => cs.isConstructorDeclaration(m) ) as cs.ConstructorDeclaration; @@ -359,6 +360,31 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.writeTypeParameterConstraints(d.typeParameters); this.writeBody(d.body); + this.writeLine(); + + if (this.isGetEnumerator(d)) { + this.write('System.Collections.Generic.IEnumerator<'); + this.writeType((d.returnType as cs.TypeReference).typeArguments![0]); + this.write('> System.Collections.Generic.IEnumerable<'); + this.writeType((d.returnType as cs.TypeReference).typeArguments![0]); + this.writeLine('>.GetEnumerator()'); + this.beginBlock(); + this.writeLine('return GetEnumerator();'); + this.endBlock(); + this.writeLine('System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() '); + this.beginBlock(); + this.writeLine('return GetEnumerator();'); + this.endBlock(); + } + } + + private isGetEnumerator(d: cs.MethodDeclaration) { + return ( + d.name === 'GetEnumerator' && + cs.isTypeReference(d.returnType) && + d.returnType.typeArguments && + d.returnType.typeArguments.length === 1 + ); } protected writeParameterDocumentation(d: cs.MethodDeclaration) { @@ -504,7 +530,6 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.write('struct'); break; case cs.PrimitiveType.Object: - case cs.PrimitiveType.Dynamic: case cs.PrimitiveType.String: case cs.PrimitiveType.Void: this.write('class'); @@ -515,9 +540,6 @@ export default class CSharpAstPrinter extends AstPrinterBase { case cs.PrimitiveType.Bool: this.write('bool'); break; - case cs.PrimitiveType.Dynamic: - this.write('dynamic'); - break; case cs.PrimitiveType.Double: this.write('double'); break; @@ -568,22 +590,33 @@ export default class CSharpAstPrinter extends AstPrinterBase { break; case cs.SyntaxKind.MapTypeNode: const mapType = type as cs.MapTypeNode; - if (!mapType.valueIsValueType) { - if (forNew) { - this.write('AlphaTab.Collections.Map<'); - } else { - this.write('AlphaTab.Collections.IMap<'); - } + if (!mapType.keyType && !mapType.valueType) { + this.write('System.Collections.IDictionary'); } else { - if (forNew) { - this.write('AlphaTab.Collections.ValueTypeMap<'); + if (!mapType.valueIsValueType) { + if (forNew) { + this.write('AlphaTab.Collections.Map<'); + } else { + this.write('AlphaTab.Collections.IMap<'); + } } else { - this.write('AlphaTab.Collections.IValueTypeMap<'); + if (forNew) { + this.write('AlphaTab.Collections.ValueTypeMap<'); + } else { + this.write('AlphaTab.Collections.IValueTypeMap<'); + } } + this.writeType(mapType.keyType!); + this.write(', '); + this.writeType(mapType.valueType!); + this.write('>'); } - this.writeType(mapType.keyType); - this.write(', '); - this.writeType(mapType.valueType); + + break; + case cs.SyntaxKind.ArrayTupleNode: + const arrayTupleType = type as cs.ArrayTupleNode; + this.write('AlphaTab.Core.ArrayTuple<'); + this.writeCommaSeparated(arrayTupleType.types, p => this.writeType(p)); this.write('>'); break; case cs.SyntaxKind.FunctionTypeNode: @@ -616,12 +649,19 @@ export default class CSharpAstPrinter extends AstPrinterBase { let isAsyncVoid = false; const targetType = (type as cs.TypeReference).reference; + let typeArguments = typeReference.typeArguments; if (typeof targetType === 'string') { - this.write(targetType); + if (forNew && targetType === this._context.makeIterableType() && typeArguments) { + this.writeType(typeArguments[0]); + this.write('[]'); + typeArguments = undefined; + } else { + this.write(targetType); + } } else { if (typeReference.isAsync) { this.write('System.Threading.Tasks.Task'); - if (!cs.isPrimitiveTypeNode(targetType) || targetType.type != cs.PrimitiveType.Void) { + if (!cs.isPrimitiveTypeNode(targetType) || targetType.type !== cs.PrimitiveType.Void) { this.write('<'); this.writeType(targetType, forNew); } else { @@ -633,9 +673,9 @@ export default class CSharpAstPrinter extends AstPrinterBase { } if (!isAsyncVoid) { - if (typeReference.typeArguments && typeReference.typeArguments.length > 0) { + if (typeArguments && typeArguments.length > 0) { this.write('<'); - this.writeCommaSeparated(typeReference.typeArguments, p => this.writeType(p)); + this.writeCommaSeparated(typeArguments, p => this.writeType(p)); this.write('>'); } @@ -657,21 +697,24 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.write(this._context.getFullName((type as cs.EnumMember).parent as cs.NamedTypeDeclaration)); break; default: - this.write('TODO: ' + cs.SyntaxKind[type.nodeType]); + this.write(`TODO: ${cs.SyntaxKind[type.nodeType]}`); break; } - if ((type.isNullable) && !forNew && !forTypeConstraint) { + if (type.isNullable && !forNew && !forTypeConstraint) { this.write('?'); } } protected writeTypeOfExpression(expr: cs.TypeOfExpression) { this.write('typeof'); + + this.write('('); if (expr.expression) { - this.write('('); this.writeExpression(expr.expression); - this.write(')'); + } else if (expr.type) { + this.writeType(expr.type); } + this.write(')'); } protected writePrefixUnaryExpression(expr: cs.PrefixUnaryExpression) { @@ -721,8 +764,8 @@ export default class CSharpAstPrinter extends AstPrinterBase { protected writeStringTemplateExpression(expr: cs.StringTemplateExpression) { this.write('string.Format(System.Globalization.CultureInfo.InvariantCulture, @"'); - let exprs: cs.Expression[] = []; - expr.chunks.forEach(c => { + const exprs: cs.Expression[] = []; + for (const c of expr.chunks) { if (cs.isStringLiteral(c)) { const escapedText = c.text.replaceAll('"', '""').replaceAll('{', '{{').replaceAll('}', '}}'); this.write(escapedText); @@ -730,12 +773,19 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.write(`{${exprs.length}}`); exprs.push(c as cs.Expression); } - }); + } this.write('"'); - exprs.forEach(expr => { + for (const expr of exprs) { this.write(', '); - this.writeExpression(expr); - }); + + if (cs.isStringLiteral(expr)) { + this.writeExpression(expr); + } else { + this.write('('); + this.writeExpression(expr); + this.write(').ToTemplate()'); + } + } this.write(')'); } @@ -814,7 +864,7 @@ export default class CSharpAstPrinter extends AstPrinterBase { protected writeNonNullExpression(expr: cs.NonNullExpression) { this.writeExpression(expr.expression); - if (!cs.isNonNullExpression(expr)) { + if (!cs.isNonNullExpression(expr.expression)) { this.write('!'); } } @@ -823,7 +873,7 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.write('catch ('); this.writeType(c.variableDeclaration.type); this.write(' '); - this.write(c.variableDeclaration.name); + this.write(this.escapeIdentifier(c.variableDeclaration.name)); this.writeLine(')'); this.writeBlock(c.block); } @@ -834,13 +884,13 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.writeLine(')'); this.beginBlock(); - s.caseClauses.forEach(c => { + for (const c of s.caseClauses) { if (cs.isDefaultClause(c)) { this.writeDefaultClause(c); } else if (cs.isCaseClause(c)) { this.writeCaseClause(c); } - }); + } this.endBlock(); } @@ -850,14 +900,19 @@ export default class CSharpAstPrinter extends AstPrinterBase { this.writeExpression(c.expression); this.writeLine(':'); this._indent++; - c.statements.forEach(s => this.writeStatement(s)); + + for (const s of c.statements) { + this.writeStatement(s); + } this._indent--; } protected writeDefaultClause(c: cs.DefaultClause) { this.writeLine('default:'); this._indent++; - c.statements.forEach(s => this.writeStatement(s)); + for (const s of c.statements) { + this.writeStatement(s); + } this._indent--; } @@ -944,11 +999,11 @@ export default class CSharpAstPrinter extends AstPrinterBase { if (i > 0) { this.write(', '); } - this.write(v); + this.write(this.escapeIdentifier(v)); }); this.write(')'); } else { - this.write(d.name); + this.write(this.escapeIdentifier(d.name)); } if (d.initializer) { @@ -960,11 +1015,30 @@ export default class CSharpAstPrinter extends AstPrinterBase { protected writeBlock(b: cs.Block) { this.beginBlock(); - b.statements.forEach(s => this.writeStatement(s)); + for (const s of b.statements) { + this.writeStatement(s); + } this.endBlock(); } protected writeUsing(using: cs.UsingDeclaration) { this.writeLine(`using ${using.namespaceOrTypeName};`); } + + protected writeLocalFunction(expr: cs.LocalFunctionDeclaration) { + this.writeType(expr.returnType); + this.write(` ${expr.name}`); + this.writeParameters(expr.parameters); + this.writeBlock(expr.body); + } + + protected writeYieldExpression(expr: cs.YieldExpression) { + this.write('yield '); + if (expr.expression) { + this.write('return '); + this.writeExpression(expr.expression); + } else { + this.write('break'); + } + } } diff --git a/src.compiler/csharp/CSharpAstTransformer.ts b/src.compiler/csharp/CSharpAstTransformer.ts index 6e28865f7..6c9e05f09 100644 --- a/src.compiler/csharp/CSharpAstTransformer.ts +++ b/src.compiler/csharp/CSharpAstTransformer.ts @@ -1,8 +1,8 @@ -import * as ts from 'typescript'; +import ts from 'typescript'; import * as cs from './CSharpAst'; -import * as path from 'path'; -import CSharpEmitterContext from './CSharpEmitterContext'; -import exp from 'constants'; +import path from 'node:path'; +import fs from 'node:fs'; +import type CSharpEmitterContext from './CSharpEmitterContext'; export default class CSharpAstTransformer { protected _typeScriptFile: ts.SourceFile; @@ -11,8 +11,9 @@ export default class CSharpAstTransformer { protected _currentClassElement: ts.ClassElement | null = null; protected _declarationOrAssignmentTypeStack: ts.Type[] = []; - protected _testClassAttribute: string = 'microsoft.visualStudio.testTools.unitTesting.TestClass'; - protected _testMethodAttribute: string = 'microsoft.visualStudio.testTools.unitTesting.TestMethod'; + protected _testClassAttribute: string = 'alphaTab.test.TestClass'; + protected _testMethodAttribute: string = 'alphaTab.test.TestMethod'; + protected _snapshotFileAttribute: string = 'alphaTab.test.SnapshotFile'; public get extension(): string { return '.cs'; @@ -101,7 +102,7 @@ export default class CSharpAstTransformer { break; } - this._typeScriptFile.statements.forEach(s => { + for (const s of this._typeScriptFile.statements) { if (ts.isExportDeclaration(s)) { globalExports.push(s); } else if (ts.isClassDeclaration(s) || ts.isInterfaceDeclaration(s) || ts.isEnumDeclaration(s)) { @@ -150,11 +151,11 @@ export default class CSharpAstTransformer { } else if (!ts.isImportDeclaration(s)) { globalStatements.push(s); } - }); + } // TODO: Introduce setting for main library name. if (path.basename(this._typeScriptFile.fileName).toLowerCase() === 'alphatab.core.ts') { - globalExports.forEach(x => { + for (const x of globalExports) { if (!x.name && x.exportClause) { if (ts.isNamespaceExport(x.exportClause)) { if (!x.moduleSpecifier) { @@ -179,7 +180,7 @@ export default class CSharpAstTransformer { } } } else { - x.exportClause.elements.forEach(e => { + for (const e of x.exportClause.elements) { const symbol = this._context.typeChecker.getTypeAtLocation(e.name)?.symbol; if (symbol) { this._context.registerSymbolAsExported(symbol); @@ -190,18 +191,18 @@ export default class CSharpAstTransformer { ts.DiagnosticCategory.Error ); } - }); + } } } else { this._context.addTsNodeDiagnostics(x, 'Unsupported export', ts.DiagnosticCategory.Error); } - }); + } - globalStatements.forEach(s => { + for (const s of globalStatements) { if (ts.isVariableStatement(s) && s.modifiers?.find(m => m.kind === ts.SyntaxKind.ExportKeyword)) { - s.declarationList.declarations.forEach(d => { + for (const d of s.declarationList.declarations) { if (d.initializer && ts.isObjectLiteralExpression(d.initializer)) { - d.initializer.properties.forEach(p => { + for (const p of d.initializer.properties) { if (ts.isShorthandPropertyAssignment(p)) { const symbol = this._context.typeChecker.getTypeAtLocation(p.name)?.symbol; if (symbol) { @@ -220,44 +221,45 @@ export default class CSharpAstTransformer { ts.DiagnosticCategory.Message ); } - }); + } } else { this._context.addTsNodeDiagnostics(d, 'Unsupported export', ts.DiagnosticCategory.Message); } - }); + } } - }); + } // TODO: register global exports } else { // validate global statements if (!defaultExport || !ts.isClassDeclaration(defaultExport)) { - globalStatements.forEach(s => { + for (const s of globalStatements) { this._context.addTsNodeDiagnostics( s, 'Global statements in modules are only allowed if there is a default class export', ts.DiagnosticCategory.Error ); - }); + } } - additionalNestedExportDeclarations.forEach(s => { + for (const s of additionalNestedExportDeclarations) { this._context.addTsNodeDiagnostics( s, 'Global statements in modules are not yet supported', ts.DiagnosticCategory.Error ); - }); - additionalNestedNonExportsDeclarations.forEach(s => { + } + + for (const s of additionalNestedNonExportsDeclarations) { this._context.addTsNodeDiagnostics( s, 'Global statements in modules are not yet supported', ts.DiagnosticCategory.Error ); - }); + } // TODO: make root namespace configurable from outside. - let folders = path + const folders = path .dirname( path.relative( path.resolve(this._context.compilerOptions.baseUrl!), @@ -270,7 +272,7 @@ export default class CSharpAstTransformer { folders.shift(); } this._csharpFile.namespace.namespace = - this._context.toPascalCase('alphaTab') + folders.map(f => '.' + this._context.toPascalCase(f)).join(''); + this._context.toPascalCase('alphaTab') + folders.map(f => `.${this._context.toPascalCase(f)}`).join(''); if (defaultExport) { this.visit( @@ -280,9 +282,15 @@ export default class CSharpAstTransformer { globalStatements ); } - additionalExportDeclarations.forEach(d => this.visit(d)); - additionalNonExportDeclarations.forEach(d => this.visit(d)); - testClasses.forEach(d => this.visitTestClass(d)); + for (const d of additionalExportDeclarations) { + this.visit(d); + } + for (const d of additionalNonExportDeclarations) { + this.visit(d); + } + for (const d of testClasses) { + this.visitTestClass(d); + } if (this._csharpFile.namespace.declarations.length > 0) { this._context.addSourceFile(this._csharpFile); @@ -368,7 +376,9 @@ export default class CSharpAstTransformer { csEnum.documentation = this.visitDocumentation(node.name); } - node.members.forEach(m => this.visitEnumMember(csEnum, m)); + for (const m of node.members) { + this.visitEnumMember(csEnum, m); + } this._csharpFile.namespace.declarations.push(csEnum); this._context.registerSymbol(csEnum); @@ -398,11 +408,13 @@ export default class CSharpAstTransformer { protected visitInterfaceDeclaration(node: ts.InterfaceDeclaration) { let extendsClauses: ts.ExpressionWithTypeArguments[] = []; - node.heritageClauses?.forEach(c => { - if (c.token === ts.SyntaxKind.ExtendsKeyword) { - extendsClauses = c.types.slice(); + if (node.heritageClauses) { + for (const c of node.heritageClauses) { + if (c.token === ts.SyntaxKind.ExtendsKeyword) { + extendsClauses = c.types.slice(); + } } - }); + } const csInterface: cs.InterfaceDeclaration = { visibility: cs.Visibility.Public, @@ -441,7 +453,9 @@ export default class CSharpAstTransformer { } if (!csInterface.skipEmit) { - node.members.forEach(m => this.visitInterfaceElement(csInterface, m)); + for (const m of node.members) { + this.visitInterfaceElement(csInterface, m); + } } this._csharpFile.namespace.declarations.push(csInterface); @@ -460,7 +474,7 @@ export default class CSharpAstTransformer { }; if (p.constraint) { - let constraintType = ts.isUnionTypeNode(p.constraint) ? p.constraint.types[0] : p.constraint; + const constraintType = ts.isUnionTypeNode(p.constraint) ? p.constraint.types[0] : p.constraint; csTypeParameter.constraint = this.createUnresolvedTypeNode(csTypeParameter, constraintType); } @@ -535,7 +549,7 @@ export default class CSharpAstTransformer { ]; } - ((d.arguments![1] as ts.ArrowFunction).body as ts.Block).statements.forEach(s => { + for (const s of ((d.arguments![1] as ts.ArrowFunction).body as ts.Block).statements) { if (ts.isExpressionStatement(s)) { if (ts.isCallExpression(s.expression)) { if (ts.isIdentifier(s.expression.expression) && s.expression.expression.text === 'it') { @@ -543,14 +557,14 @@ export default class CSharpAstTransformer { } else { this._context.addTsNodeDiagnostics( s, - 'Unsupported test method function call ' + s.expression.expression.getText(), + `Unsupported test method function call ${s.expression.expression.getText()}`, ts.DiagnosticCategory.Error ); } } else { this._context.addTsNodeDiagnostics( s, - 'Unsupported test class member ' + ts.SyntaxKind[s.expression.kind], + `Unsupported test class member ${ts.SyntaxKind[s.expression.kind]}`, ts.DiagnosticCategory.Error ); } @@ -561,11 +575,11 @@ export default class CSharpAstTransformer { } else { this._context.addTsNodeDiagnostics( s, - 'Unsupported test class member ' + ts.SyntaxKind[s.kind], + `Unsupported test class member ${ts.SyntaxKind[s.kind]}`, ts.DiagnosticCategory.Error ); } - }); + } this._csharpFile.namespace.declarations.push(csClass); } @@ -581,6 +595,7 @@ export default class CSharpAstTransformer { isOverride: false, isStatic: false, isVirtual: false, + isGeneratorFunction: false, partial: !!ts.getJSDocTags(d).find(t => t.tagName.text === 'partial'), name: this._context.toPascalCase((d.name as ts.Identifier).text), parameters: [], @@ -595,7 +610,9 @@ export default class CSharpAstTransformer { const type = this._context.typeChecker.getTypeAtLocation(d.name!); csMethod.returnType.parent = csMethod; - d.parameters.forEach(p => csMethod.parameters.push(this.makeParameter(csMethod, p))); + for (const p of d.parameters) { + csMethod.parameters.push(this.makeParameter(csMethod, p)); + } this._declarationOrAssignmentTypeStack.push(type); csMethod.body = this.visitBlock(csMethod, d.body as ts.Block); this._declarationOrAssignmentTypeStack.pop(); @@ -615,6 +632,7 @@ export default class CSharpAstTransformer { isStatic: false, isVirtual: false, isTestMethod: true, + isGeneratorFunction: false, partial: !!ts.getJSDocTags(d).find(t => t.tagName.text === 'partial'), name: this._context.toMethodName((d.arguments[0] as ts.StringLiteral).text), parameters: [], @@ -630,7 +648,7 @@ export default class CSharpAstTransformer { }; if (csMethod.name.match(/^[^a-zA-Z].*/)) { - csMethod.name = 'Test' + csMethod.name; + csMethod.name = `Test${csMethod.name}`; } csMethod.attributes = [ @@ -641,7 +659,14 @@ export default class CSharpAstTransformer { parent: null, nodeType: cs.SyntaxKind.TypeReference, reference: this._context.makeTypeName(this._testMethodAttribute) - } as cs.TypeReference + } as cs.TypeReference, + arguments: [ + { + parent: null, + nodeType: cs.SyntaxKind.StringLiteral, + text: (d.arguments[0] as ts.StringLiteral).text + } as cs.StringLiteral + ] } ]; @@ -666,10 +691,37 @@ export default class CSharpAstTransformer { csMethod.body = this.visitBlock(csMethod, testFunction.body as ts.Block); parent.members.push(csMethod); + + const sourcePath = d.getSourceFile().fileName; + const snapshotFilePath = path.resolve( + sourcePath, + '..', + '__snapshots__', + `${path.basename(sourcePath).toLowerCase()}.snap` + ); + if (fs.existsSync(snapshotFilePath)) { + const relative = path.relative(path.resolve(this._context.compilerOptions.baseUrl!), snapshotFilePath); + csMethod.attributes.push({ + parent: csMethod, + nodeType: cs.SyntaxKind.Attribute, + type: { + parent: null, + nodeType: cs.SyntaxKind.TypeReference, + reference: this._context.makeTypeName(this._snapshotFileAttribute) + } as cs.TypeReference, + arguments: [ + { + parent: null, + nodeType: cs.SyntaxKind.StringLiteral, + text: relative.replaceAll('\\', '/') + } as cs.StringLiteral + ] + }); + } } protected visitTestClassProperty(parent: cs.ClassDeclaration, s: ts.VariableStatement) { - s.declarationList.declarations.forEach(d => { + for (const d of s.declarationList.declarations) { const type = this._context.typeChecker.getTypeAtLocation(d.name); if (this._context.isFunctionType(type) && d.initializer && ts.isArrowFunction(d.initializer)) { const csMethod: cs.MethodDeclaration = { @@ -680,6 +732,7 @@ export default class CSharpAstTransformer { isStatic: false, isVirtual: false, isTestMethod: false, + isGeneratorFunction: false, partial: !!ts.getJSDocTags(d).find(t => t.tagName.text === 'partial'), name: this._context.toPascalCase(d.name.getText()), returnType: {} as cs.TypeNode, @@ -708,7 +761,9 @@ export default class CSharpAstTransformer { csMethod.returnType.parent = csMethod; - d.initializer.parameters.forEach(p => csMethod.parameters.push(this.makeParameter(csMethod, p))); + for (const p of d.initializer.parameters) { + csMethod.parameters.push(this.makeParameter(csMethod, p)); + } this._declarationOrAssignmentTypeStack.push(type); csMethod.body = this.visitBlock(csMethod, d.initializer.body as ts.Block); this._declarationOrAssignmentTypeStack.pop(); @@ -745,7 +800,7 @@ export default class CSharpAstTransformer { parent.members.push(csProperty); this._context.registerSymbol(csProperty); } - }); + } } protected visitClassDeclaration( @@ -757,14 +812,16 @@ export default class CSharpAstTransformer { let extendsClause: ts.ExpressionWithTypeArguments | null = null; let implementsClauses: ts.ExpressionWithTypeArguments[] = []; - node.heritageClauses?.forEach(c => { - if (c.token === ts.SyntaxKind.ExtendsKeyword) { - extendsClause = c.types[0]; - } - if (c.token === ts.SyntaxKind.ImplementsKeyword) { - implementsClauses = c.types.slice(); + if (node.heritageClauses) { + for (const c of node.heritageClauses) { + if (c.token === ts.SyntaxKind.ExtendsKeyword) { + extendsClause = c.types[0]; + } + if (c.token === ts.SyntaxKind.ImplementsKeyword) { + implementsClauses = c.types.slice(); + } } - }); + } const csClass: cs.ClassDeclaration = { visibility: cs.Visibility.Public, @@ -813,7 +870,9 @@ export default class CSharpAstTransformer { } if (!csClass.skipEmit) { - node.members.forEach(m => this.visitClassElement(csClass, m)); + for (const m of node.members) { + this.visitClassElement(csClass, m); + } if (globalStatements && globalStatements.length > 0) { const staticConstructor = { @@ -831,12 +890,12 @@ export default class CSharpAstTransformer { } as cs.Block } as cs.ConstructorDeclaration; - globalStatements.forEach(s => { + for (const s of globalStatements) { const st = this.visitStatement(staticConstructor.body!, s)!; if (st) { (staticConstructor.body as cs.Block).statements.push(st); } - }); + } csClass.members.push(staticConstructor); } @@ -847,7 +906,7 @@ export default class CSharpAstTransformer { } protected visitDocumentation(node: ts.Node): string | undefined { - let symbol = this._context.typeChecker.getSymbolAtLocation(node); + const symbol = this._context.typeChecker.getSymbolAtLocation(node); if (!symbol) { return undefined; } @@ -898,7 +957,7 @@ export default class CSharpAstTransformer { } protected visitClassElement(parent: cs.ClassDeclaration, classElement: ts.ClassElement) { - let isSkipped = this.shouldSkip(classElement, false); + const isSkipped = this.shouldSkip(classElement, false); if (isSkipped) { this._context.processingSkippedElement = true; } @@ -922,7 +981,7 @@ export default class CSharpAstTransformer { } else { this._context.addTsNodeDiagnostics( classElement, - 'Unsupported class element: ' + ts.SyntaxKind[classElement.kind], + `Unsupported class element: ${ts.SyntaxKind[classElement.kind]}`, ts.DiagnosticCategory.Error ); } @@ -942,7 +1001,7 @@ export default class CSharpAstTransformer { } else { this._context.addTsNodeDiagnostics( classElement, - 'Unsupported interface element: ' + ts.SyntaxKind[classElement.kind], + `Unsupported interface element: ${ts.SyntaxKind[classElement.kind]}`, ts.DiagnosticCategory.Error ); } @@ -973,13 +1032,13 @@ export default class CSharpAstTransformer { let isReadonly = false; if (classElement.modifiers) { - classElement.modifiers.forEach(m => { + for (const m of classElement.modifiers) { switch (m.kind) { case ts.SyntaxKind.ReadonlyKeyword: isReadonly = true; break; } - }); + } } csProperty.type.parent = csProperty; @@ -1012,14 +1071,12 @@ export default class CSharpAstTransformer { body: classElement.body ? this.visitBlock(member, classElement.body) : null } as cs.PropertyAccessorDeclaration; - if (this._context.markOverride(classElement)) { - member.isOverride = true; - } + this.applyPropertyOverride(member, classElement); } else { const signature = this._context.typeChecker.getSignatureFromDeclaration(classElement); const returnType = this._context.typeChecker.getReturnTypeOfSignature(signature!); - let newProperty: cs.PropertyDeclaration = { + const newProperty: cs.PropertyDeclaration = { isAbstract: false, isOverride: false, isVirtual: false, @@ -1034,12 +1091,10 @@ export default class CSharpAstTransformer { tsSymbol: this._context.getSymbolForDeclaration(classElement) }; - if (this._context.markOverride(classElement)) { - newProperty.isOverride = true; - } + this.applyPropertyOverride(newProperty, classElement); if (classElement.modifiers) { - classElement.modifiers.forEach(m => { + for (const m of classElement.modifiers) { switch (m.kind) { case ts.SyntaxKind.AbstractKeyword: newProperty.isAbstract = true; @@ -1053,7 +1108,7 @@ export default class CSharpAstTransformer { newProperty.isOverride = false; break; } - }); + } } newProperty.type.parent = newProperty; @@ -1082,65 +1137,129 @@ export default class CSharpAstTransformer { body: classElement.body ? this.visitBlock(member, classElement.body) : null } as cs.PropertyAccessorDeclaration; - if (this._context.markOverride(classElement)) { - member.isOverride = true; - } + this.applyPropertyOverride(member, classElement); return member.setAccessor; - } else { - const signature = this._context.typeChecker.getSignatureFromDeclaration(classElement); - const returnType = this._context.typeChecker.getReturnTypeOfSignature(signature!); + } + const signature = this._context.typeChecker.getSignatureFromDeclaration(classElement); + const returnType = this._context.typeChecker.getReturnTypeOfSignature(signature!); - let newProperty: cs.PropertyDeclaration = { - isAbstract: false, - isOverride: false, - isVirtual: false, - isStatic: false, - name: propertyName, - nodeType: cs.SyntaxKind.PropertyDeclaration, - parent: parent, - visibility: this.mapVisibility(classElement, cs.Visibility.Public), - type: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, returnType), - skipEmit: this.shouldSkip(classElement, false), - tsNode: classElement, - tsSymbol: this._context.getSymbolForDeclaration(classElement) - }; + const newProperty: cs.PropertyDeclaration = { + isAbstract: false, + isOverride: false, + isVirtual: false, + isStatic: false, + name: propertyName, + nodeType: cs.SyntaxKind.PropertyDeclaration, + parent: parent, + visibility: this.mapVisibility(classElement, cs.Visibility.Public), + type: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, returnType), + skipEmit: this.shouldSkip(classElement, false), + tsNode: classElement, + tsSymbol: this._context.getSymbolForDeclaration(classElement) + }; - if (this._context.markOverride(classElement)) { - newProperty.isOverride = true; - } + this.applyPropertyOverride(newProperty, classElement); - if (classElement.modifiers) { - classElement.modifiers.forEach(m => { - switch (m.kind) { - case ts.SyntaxKind.AbstractKeyword: - newProperty.isAbstract = true; - parent.isAbstract = true; - newProperty.isVirtual = false; - newProperty.isOverride = false; - break; - case ts.SyntaxKind.StaticKeyword: - newProperty.isStatic = true; - newProperty.isVirtual = false; - newProperty.isOverride = false; - break; - } - }); + if (classElement.modifiers) { + for (const m of classElement.modifiers) { + switch (m.kind) { + case ts.SyntaxKind.AbstractKeyword: + newProperty.isAbstract = true; + parent.isAbstract = true; + newProperty.isVirtual = false; + newProperty.isOverride = false; + break; + case ts.SyntaxKind.StaticKeyword: + newProperty.isStatic = true; + newProperty.isVirtual = false; + newProperty.isOverride = false; + break; + } } + } - newProperty.type.parent = newProperty; + newProperty.type.parent = newProperty; - newProperty.setAccessor = { - keyword: 'set', - parent: newProperty, - nodeType: cs.SyntaxKind.PropertyAccessorDeclaration, - tsNode: classElement, - body: classElement.body ? this.visitBlock(newProperty, classElement.body) : null - } as cs.PropertyAccessorDeclaration; + newProperty.setAccessor = { + keyword: 'set', + parent: newProperty, + nodeType: cs.SyntaxKind.PropertyAccessorDeclaration, + tsNode: classElement, + body: classElement.body ? this.visitBlock(newProperty, classElement.body) : null + } as cs.PropertyAccessorDeclaration; - parent.members.push(newProperty); + parent.members.push(newProperty); + + return newProperty.setAccessor; + } + + protected applyMethodOverride(csMethod: cs.MethodDeclaration, classElement: ts.MethodDeclaration) { + const overrides = this._context.markOverride(classElement); + if (overrides.length > 0) { + csMethod.isOverride = true; + for (const o of overrides) { + let type: ts.Type | undefined = undefined; + let typeNode: ts.TypeNode | undefined = undefined; + if (ts.isMethodDeclaration(classElement)) { + const signature = this._context.typeChecker.getSignatureFromDeclaration(classElement); + type = signature ? this._context.typeChecker.getReturnTypeOfSignature(signature) : undefined; + } + + if (!type) { + return; + } + + if (ts.isMethodDeclaration(o)) { + typeNode = o.type; + } else if (ts.isMethodSignature(o)) { + typeNode = o.type; + } + + this._context.removeUnresolvedTypeNode(csMethod.returnType as cs.UnresolvedTypeNode); + csMethod.returnType = this.createUnresolvedTypeNode(csMethod, typeNode ?? o, type); + + // NOTE: we could also ensure the correct parameter list here + return; + } + } + } + + private applyPropertyOverride( + csProperty: cs.PropertyDeclaration, + classElement: ts.PropertyDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration + ) { + const overrides = this._context.markOverride(classElement); + if (overrides.length > 0) { + csProperty.isOverride = true; + for (const o of overrides) { + let type: ts.Type | undefined = undefined; + let typeNode: ts.TypeNode | undefined = undefined; + + if (ts.isGetAccessorDeclaration(o)) { + const signature = this._context.typeChecker.getSignatureFromDeclaration(o); + type = signature ? this._context.typeChecker.getReturnTypeOfSignature(signature) : undefined; + typeNode = o.type; + } else if (ts.isSetAccessorDeclaration(o)) { + const signature = this._context.typeChecker.getSignatureFromDeclaration(o); + type = signature ? this._context.typeChecker.getTypeOfSymbol(signature.parameters[0]) : undefined; + typeNode = o.parameters[0].type; + } else if (ts.isPropertyDeclaration(o)) { + type = this._context.typeChecker.getTypeAtLocation(o); + typeNode = o.type; + } else if (ts.isPropertySignature(o)) { + type = this._context.typeChecker.getTypeAtLocation(o); + typeNode = o.type; + } + + if (!type) { + return; + } - return newProperty.setAccessor; + this._context.removeUnresolvedTypeNode(csProperty.type as cs.UnresolvedTypeNode); + csProperty.type = this.createUnresolvedTypeNode(csProperty, typeNode ?? o, type); + return; + } } } @@ -1169,13 +1288,11 @@ export default class CSharpAstTransformer { csProperty.documentation = this.visitDocumentation(classElement.name); } - if (this._context.markOverride(classElement)) { - csProperty.isOverride = true; - } + this.applyPropertyOverride(csProperty, classElement); let isReadonly = false; if (classElement.modifiers) { - classElement.modifiers.forEach(m => { + for (const m of classElement.modifiers) { switch (m.kind) { case ts.SyntaxKind.AbstractKeyword: csProperty.isAbstract = true; @@ -1194,7 +1311,7 @@ export default class CSharpAstTransformer { isReadonly = true; break; } - }); + } } csProperty.type.parent = csProperty; @@ -1252,10 +1369,16 @@ export default class CSharpAstTransformer { isStatic: false, isVirtual: false, isTestMethod: false, + isGeneratorFunction: !!classElement.asteriskToken, partial: !!ts.getJSDocTags(classElement).find(t => t.tagName.text === 'partial'), - name: this._context.toMethodName((classElement.name as ts.Identifier).text), + name: this._context.buildMethodName(classElement.name), parameters: [], - returnType: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, returnType), + returnType: this.createUnresolvedTypeNode( + null, + classElement.type ?? classElement, + returnType, + returnType?.getSymbol() + ), visibility: this.mapVisibility(classElement, cs.Visibility.Public), tsNode: classElement, tsSymbol: this._context.getSymbolForDeclaration(classElement), @@ -1266,12 +1389,10 @@ export default class CSharpAstTransformer { csMethod.documentation = this.visitDocumentation(classElement.name); } - if (this._context.markOverride(classElement)) { - csMethod.isOverride = true; - } + this.applyMethodOverride(csMethod, classElement); if (classElement.modifiers) { - classElement.modifiers.forEach(m => { + for (const m of classElement.modifiers) { switch (m.kind) { case ts.SyntaxKind.AbstractKeyword: csMethod.isAbstract = true; @@ -1289,20 +1410,22 @@ export default class CSharpAstTransformer { csMethod.isAsync = true; break; } - }); + } } csMethod.returnType.parent = csMethod; if (classElement.typeParameters && classElement.typeParameters.length > 0) { csMethod.typeParameters = []; - classElement.typeParameters.forEach(p => { + for (const p of classElement.typeParameters) { const csp = this.visitTypeParameterDeclaration(csMethod, p); csMethod.typeParameters!.push(csp); - }); + } } - classElement.parameters.forEach(p => this.visitMethodParameter(csMethod, p)); + for (const p of classElement.parameters) { + this.visitMethodParameter(csMethod, p); + } if (classElement.body && !csMethod.skipEmit) { csMethod.body = this.visitBlock(csMethod, classElement.body); @@ -1382,6 +1505,9 @@ export default class CSharpAstTransformer { return this.visitThrowStatement(parent, s as ts.ThrowStatement); case ts.SyntaxKind.TryStatement: return this.visitTryStatement(parent, s as ts.TryStatement); + + case ts.SyntaxKind.FunctionDeclaration: + return this.visitFunctionDeclaration(parent, s as ts.FunctionDeclaration); } return {} as cs.ThrowStatement; } @@ -1433,11 +1559,11 @@ export default class CSharpAstTransformer { variableStatement.declarationList = this.visitVariableDeclarationList(variableStatement, s.declarationList); - if ((s.declarationList.flags & ts.NodeFlags.Const) != 0) { + if ((s.declarationList.flags & ts.NodeFlags.Const) !== 0) { variableStatement.variableStatementKind = cs.VariableStatementKind.Const; - } else if ((s.declarationList.flags & ts.NodeFlags.Using) != 0) { + } else if ((s.declarationList.flags & ts.NodeFlags.Using) !== 0) { variableStatement.variableStatementKind = cs.VariableStatementKind.Using; - } else if ((s.declarationList.flags & ts.NodeFlags.AwaitUsing) != 0) { + } else if ((s.declarationList.flags & ts.NodeFlags.AwaitUsing) !== 0) { variableStatement.variableStatementKind = cs.VariableStatementKind.AwaitUsing; } @@ -1450,12 +1576,12 @@ export default class CSharpAstTransformer { parent: parent, tsNode: s, declarations: [], - isConst: (s.flags & ts.NodeFlags.Const) != 0 + isConst: (s.flags & ts.NodeFlags.Const) !== 0 } as cs.VariableDeclarationList; - s.declarations.forEach(d => - variableStatement.declarations.push(this.visitVariableDeclaration(variableStatement, d)) - ); + for (const d of s.declarations) { + variableStatement.declarations.push(this.visitVariableDeclaration(variableStatement, d)); + } return variableStatement; } @@ -1520,30 +1646,6 @@ export default class CSharpAstTransformer { return null; } - // chai property statements like "expected(false).to.be.ok;" - if ( - ts.isPropertyAccessExpression(s.expression) && - ts.isPropertyAccessExpression(s.expression.expression) && - ts.isPropertyAccessExpression(s.expression.expression.expression) && - ts.isIdentifier(s.expression.expression.name) && - s.expression.expression.name.text == 'be' && - ts.isIdentifier(s.expression.expression.expression.name) && - (s.expression.expression.expression.name.text == 'to' || - s.expression.expression.expression.name.text == 'not') - ) { - const access = expressionStatement.expression; - - expressionStatement.expression = { - nodeType: cs.SyntaxKind.InvocationExpression, - parent: expressionStatement, - arguments: [], - expression: access, - tsNode: s.expression, - skipEmit: access.skipEmit - } as cs.InvocationExpression; - access.parent = expressionStatement.expression; - } - return expressionStatement; } @@ -1772,7 +1874,7 @@ export default class CSharpAstTransformer { return null; } - s.caseBlock.clauses.forEach(c => { + for (const c of s.caseBlock.clauses) { if (ts.isDefaultClause(c)) { switchStatement.caseClauses.push(this.visitDefaultClause(switchStatement, c)); } else { @@ -1781,7 +1883,7 @@ export default class CSharpAstTransformer { switchStatement.caseClauses.push(cl); } } - }); + } return switchStatement; } @@ -1794,12 +1896,12 @@ export default class CSharpAstTransformer { statements: [] } as cs.DefaultClause; - s.statements.forEach(c => { + for (const c of s.statements) { const statement = this.visitStatement(defaultClause, c); if (statement) { defaultClause.statements.push(statement); } - }); + } return defaultClause; } @@ -1821,12 +1923,12 @@ export default class CSharpAstTransformer { if (!caseClause.expression) { return null; } - s.statements.forEach(c => { + for (const c of s.statements) { const statement = this.visitStatement(caseClause, c); if (statement) { caseClause.statements.push(statement); } - }); + } return caseClause; } @@ -1888,7 +1990,7 @@ export default class CSharpAstTransformer { protected visitMethodSignature( parent: cs.ClassDeclaration | cs.InterfaceDeclaration, classElement: ts.MethodSignature - ) { + ): cs.MethodDeclaration { const signature = this._context.typeChecker.getSignatureFromDeclaration(classElement); const returnType = this._context.typeChecker.getReturnTypeOfSignature(signature!); @@ -1900,8 +2002,9 @@ export default class CSharpAstTransformer { isStatic: false, isVirtual: false, isTestMethod: false, + isGeneratorFunction: false, partial: !!ts.getJSDocTags(classElement).find(t => t.tagName.text === 'partial'), - name: this._context.toMethodName((classElement.name as ts.Identifier).text), + name: this._context.buildMethodName(classElement.name), parameters: [], returnType: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, returnType), visibility: cs.Visibility.None, @@ -1917,7 +2020,7 @@ export default class CSharpAstTransformer { if (classElement.typeParameters && classElement.typeParameters.length > 0) { csMethod.typeParameters = []; - classElement.typeParameters.forEach(p => { + for (const p of classElement.typeParameters) { const csp = { parent: csMethod, name: p.name.text, @@ -1929,16 +2032,20 @@ export default class CSharpAstTransformer { } csMethod.typeParameters!.push(csp); - }); + } } - classElement.parameters.forEach(p => this.visitMethodParameter(csMethod, p)); + for (const p of classElement.parameters) { + this.visitMethodParameter(csMethod, p); + } if (!csMethod.skipEmit) { parent.members.push(csMethod); } this._context.registerSymbol(csMethod); + + return csMethod; } protected mapVisibility(node: ts.Node, fallback: cs.Visibility): cs.Visibility { if (this._context.isInternal(node)) { @@ -2003,7 +2110,9 @@ export default class CSharpAstTransformer { skipEmit: this.shouldSkip(classElement, false) }; - classElement.parameters.forEach(p => this.visitMethodParameter(csConstructor, p)); + for (const p of classElement.parameters) { + this.visitMethodParameter(csConstructor, p); + } if (classElement.body) { csConstructor.body = this.visitBlock(csConstructor, classElement.body); @@ -2100,20 +2209,21 @@ export default class CSharpAstTransformer { return this.visitStringLiteral(parent, expression as ts.Identifier); case ts.SyntaxKind.SpreadElement: return this.visitSpreadElement(parent, expression as ts.SpreadElement); - - case ts.SyntaxKind.SyntheticReferenceExpression: - case ts.SyntaxKind.CommaListExpression: - case ts.SyntaxKind.ClassExpression: - case ts.SyntaxKind.JSDocTypeExpression: - case ts.SyntaxKind.JsxExpression: - case ts.SyntaxKind.OmittedExpression: - case ts.SyntaxKind.PartiallyEmittedExpression: - case ts.SyntaxKind.ImportKeyword: - case ts.SyntaxKind.DeleteExpression: - case ts.SyntaxKind.VoidExpression: case ts.SyntaxKind.YieldExpression: - case ts.SyntaxKind.SyntheticExpression: - case ts.SyntaxKind.TaggedTemplateExpression: + return this.visitYieldExpression(parent, expression as ts.YieldExpression); + + // case ts.SyntaxKind.SyntheticReferenceExpression: + // case ts.SyntaxKind.CommaListExpression: + // case ts.SyntaxKind.ClassExpression: + // case ts.SyntaxKind.JSDocTypeExpression: + // case ts.SyntaxKind.JsxExpression: + // case ts.SyntaxKind.OmittedExpression: + // case ts.SyntaxKind.PartiallyEmittedExpression: + // case ts.SyntaxKind.ImportKeyword: + // case ts.SyntaxKind.DeleteExpression: + // case ts.SyntaxKind.VoidExpression: + // case ts.SyntaxKind.SyntheticExpression: + // case ts.SyntaxKind.TaggedTemplateExpression: default: this._context.addTsNodeDiagnostics( expression, @@ -2227,15 +2337,14 @@ export default class CSharpAstTransformer { } as cs.Identifier; return identifier; - } else { - const csExpr = { - parent: parent, - tsNode: expression, - nodeType: cs.SyntaxKind.ThisLiteral - } as cs.ThisLiteral; - - return csExpr; } + const csExpr = { + parent: parent, + tsNode: expression, + nodeType: cs.SyntaxKind.ThisLiteral + } as cs.ThisLiteral; + + return csExpr; } protected visitSuperLiteralExpression(parent: cs.Node, expression: ts.SuperExpression) { @@ -2331,7 +2440,29 @@ export default class CSharpAstTransformer { } return csExpr; - } else if (expression.operatorToken.kind === ts.SyntaxKind.AsteriskAsteriskToken) { + } + + if (expression.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) { + const csExpr = { + parent: parent, + tsNode: expression, + nodeType: cs.SyntaxKind.IsExpression, + expression: null!, + type: null! + } as cs.IsExpression; + + csExpr.expression = this.visitExpression(csExpr, expression.left)!; + csExpr.type = this.createUnresolvedTypeNode( + csExpr, + expression.right, + this._context.typeChecker.getTypeAtLocation(expression.right), + this._context.typeChecker.getSymbolAtLocation(expression.right) + ); + + return csExpr; + } + + if (expression.operatorToken.kind === ts.SyntaxKind.AsteriskAsteriskToken) { this._context.addTsNodeDiagnostics( expression, 'Exponentiation expresssions are not yet supported', @@ -2342,7 +2473,9 @@ export default class CSharpAstTransformer { parent: parent, tsNode: expression } as cs.ToDoExpression; - } else if ( + } + + if ( expression.operatorToken.kind === ts.SyntaxKind.BarEqualsToken || expression.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken || expression.operatorToken.kind === ts.SyntaxKind.CaretEqualsToken || @@ -2365,7 +2498,7 @@ export default class CSharpAstTransformer { return null; } - const bitOp = (assignment.right = { + const bitOp = { parent: assignment, nodeType: cs.SyntaxKind.BinaryExpression, tsNode: expression, @@ -2382,7 +2515,9 @@ export default class CSharpAstTransformer { tsNode: expression } as cs.ParenthesizedExpression, operator: '' - } as cs.BinaryExpression); + } as cs.BinaryExpression; + + assignment.right = bitOp; bitOp.right.parent = bitOp; switch (expression.operatorToken.kind) { @@ -2416,7 +2551,7 @@ export default class CSharpAstTransformer { const isRightEnum = rightType.flags & ts.TypeFlags.Enum || rightType.flags & ts.TypeFlags.EnumLiteral; if (!isLeftEnum || !isRightEnum) { - const toInt = ((bitOp.left as cs.ParenthesizedExpression).expression = { + const toInt = { parent: bitOp, nodeType: cs.SyntaxKind.CastExpression, expression: {} as cs.Expression, @@ -2427,7 +2562,8 @@ export default class CSharpAstTransformer { tsNode: expression } as cs.PrimitiveTypeNode, tsNode: expression - } as cs.CastExpression); + } as cs.CastExpression; + (bitOp.left as cs.ParenthesizedExpression).expression = toInt; toInt.expression = this.visitExpression(assignment, expression.left)!; if (!toInt.expression) { return null; @@ -2463,100 +2599,115 @@ export default class CSharpAstTransformer { assignment.right = this.makeDouble(assignment.right); return assignment; - } else { - const binaryExpression = { - parent: parent, - nodeType: cs.SyntaxKind.BinaryExpression, - tsNode: expression, - left: {} as cs.Expression, - right: {} as cs.Expression, - operator: this.mapOperator(expression.operatorToken.kind) - } as cs.BinaryExpression; + } + const binaryExpression = { + parent: parent, + nodeType: cs.SyntaxKind.BinaryExpression, + tsNode: expression, + left: {} as cs.Expression, + right: {} as cs.Expression, + operator: this.mapOperator(expression.operatorToken.kind) + } as cs.BinaryExpression; - binaryExpression.left = this.visitExpression(binaryExpression, expression.left)!; - if (!binaryExpression.left) { - return null; - } + binaryExpression.left = this.visitExpression(binaryExpression, expression.left)!; + if (!binaryExpression.left) { + return null; + } - binaryExpression.right = this.visitExpression(binaryExpression, expression.right)!; - if (!binaryExpression.right) { - return null; - } + binaryExpression.right = this.visitExpression(binaryExpression, expression.right)!; + if (!binaryExpression.right) { + return null; + } - const leftType = this._context.typeChecker.getTypeAtLocation(expression.left); - const rightType = this._context.typeChecker.getTypeAtLocation(expression.right); + const leftType = this._context.typeChecker.getTypeAtLocation(expression.left); + const rightType = this._context.typeChecker.getTypeAtLocation(expression.right); - const isLeftEnum = leftType.flags & ts.TypeFlags.Enum || leftType.flags & ts.TypeFlags.EnumLiteral; - const isRightEnum = rightType.flags & ts.TypeFlags.Enum || rightType.flags & ts.TypeFlags.EnumLiteral; + const isLeftEnum = this._context.isEnum(leftType); + const isRightEnum = this._context.isEnum(rightType); - if (!isLeftEnum || !isRightEnum) { - switch (expression.operatorToken.kind) { - case ts.SyntaxKind.PlusToken: - case ts.SyntaxKind.PlusEqualsToken: - // string and number concatenation - if ( - leftType.flags & ts.TypeFlags.Number && - rightType.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral) - ) { - binaryExpression.left = this.toInvariantString(binaryExpression.left); - } else if ( - rightType.flags & ts.TypeFlags.Number && - leftType.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral) - ) { - binaryExpression.right = this.toInvariantString(binaryExpression.right); + if (!isLeftEnum || !isRightEnum) { + switch (expression.operatorToken.kind) { + case ts.SyntaxKind.PlusToken: + case ts.SyntaxKind.PlusEqualsToken: + // string and number concatenation + if ( + leftType.flags & ts.TypeFlags.Number && + rightType.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral) + ) { + binaryExpression.left = this.toInvariantString(binaryExpression.left); + } else if ( + rightType.flags & ts.TypeFlags.Number && + leftType.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral) + ) { + binaryExpression.right = this.toInvariantString(binaryExpression.right); + } else { + // number arithmetics + if (isLeftEnum) { + binaryExpression.left = this.makeDouble(binaryExpression.left); } - break; - case ts.SyntaxKind.AmpersandToken: - case ts.SyntaxKind.GreaterThanGreaterThanToken: - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - case ts.SyntaxKind.LessThanLessThanToken: - case ts.SyntaxKind.BarToken: - case ts.SyntaxKind.CaretToken: - if (!this.hasBinaryOperationMakeInt(binaryExpression.left)) { - binaryExpression.left = this.makeInt(binaryExpression.left, true); + if (isRightEnum) { + binaryExpression.right = this.makeDouble(binaryExpression.right); } + } + break; + case ts.SyntaxKind.AmpersandToken: + case ts.SyntaxKind.GreaterThanGreaterThanToken: + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + case ts.SyntaxKind.LessThanLessThanToken: + case ts.SyntaxKind.BarToken: + case ts.SyntaxKind.CaretToken: + if (!this.hasBinaryOperationMakeInt(binaryExpression.left)) { + binaryExpression.left = this.makeInt(binaryExpression.left, true); + } - if (!this.hasBinaryOperationMakeInt(binaryExpression.right)) { - let allowLongOnRight = false; - switch (expression.operatorToken.kind) { - case ts.SyntaxKind.AmpersandToken: - case ts.SyntaxKind.BarToken: - case ts.SyntaxKind.CaretToken: - allowLongOnRight = true; - break; - default: - allowLongOnRight = false; - break; - } - - binaryExpression.right = this.makeInt(binaryExpression.right, allowLongOnRight); + if (!this.hasBinaryOperationMakeInt(binaryExpression.right)) { + let allowLongOnRight = false; + switch (expression.operatorToken.kind) { + case ts.SyntaxKind.AmpersandToken: + case ts.SyntaxKind.BarToken: + case ts.SyntaxKind.CaretToken: + allowLongOnRight = true; + break; + default: + allowLongOnRight = false; + break; } - let nextParent = parent; - while (cs.isParenthesizedExpression(nextParent)) { - nextParent = nextParent.parent!; - } + binaryExpression.right = this.makeInt(binaryExpression.right, allowLongOnRight); + } - if ( - nextParent.nodeType !== cs.SyntaxKind.BinaryExpression || - (nextParent as cs.BinaryExpression).operator === '=' - ) { - return this.makeDouble(binaryExpression); - } - break; - case ts.SyntaxKind.SlashToken: - if (expression.left.kind === ts.SyntaxKind.NumericLiteral) { - binaryExpression.left = this.makeDouble(binaryExpression.left); - } - if (expression.right.kind === ts.SyntaxKind.NumericLiteral) { - binaryExpression.right = this.makeDouble(binaryExpression.right); - } - break; - } - } + let nextParent = parent; + while (cs.isParenthesizedExpression(nextParent)) { + nextParent = nextParent.parent!; + } + + if ( + nextParent.nodeType !== cs.SyntaxKind.BinaryExpression || + (nextParent as cs.BinaryExpression).operator === '=' + ) { + return this.makeDouble(binaryExpression); + } + break; + case ts.SyntaxKind.SlashToken: + case ts.SyntaxKind.AsteriskToken: + case ts.SyntaxKind.MinusToken: + if (expression.left.kind === ts.SyntaxKind.NumericLiteral) { + binaryExpression.left = this.makeDouble(binaryExpression.left); + } else if (isLeftEnum) { + binaryExpression.left = this.makeDouble(binaryExpression.left); + } + + if (expression.right.kind === ts.SyntaxKind.NumericLiteral) { + binaryExpression.right = this.makeDouble(binaryExpression.right); + } else if (isRightEnum) { + binaryExpression.right = this.makeDouble(binaryExpression.right); + } - return binaryExpression; + break; + } } + + return binaryExpression; } private hasBinaryOperationMakeInt(left: cs.Expression): boolean { @@ -2739,13 +2890,14 @@ export default class CSharpAstTransformer { arguments: [] } as cs.InvocationExpression; - const access = (call.expression = { + const access = { parent: call, tsNode: expression.tsNode, nodeType: cs.SyntaxKind.MemberAccessExpression, expression: {} as cs.Expression, member: this._context.toMethodName('isTruthy') - } as cs.MemberAccessExpression); + } as cs.MemberAccessExpression; + call.expression = access; access.expression = { parent: access, @@ -2760,65 +2912,101 @@ export default class CSharpAstTransformer { return call; } - protected visitFunctionExpression(parent: cs.Node, expression: ts.FunctionExpression) { - if (cs.isExpressionStatement(parent)) { - this._context.addTsNodeDiagnostics( - expression, - 'Local function declarations are not yet supported', - ts.DiagnosticCategory.Error + protected visitFunctionDeclaration(parent: cs.Node, expression: ts.FunctionDeclaration) { + const localFunction: cs.LocalFunctionDeclaration = { + name: (expression.name as ts.Identifier)?.text, + nodeType: cs.SyntaxKind.LocalFunction, + parent: parent, + tsNode: expression, + body: {} as cs.Block, + parameters: [], + returnType: {} as cs.TypeNode + }; + + for (const p of expression.parameters) { + localFunction.parameters.push(this.makeParameter(localFunction, p)); + } + + const signature = this._context.typeChecker.getSignatureFromDeclaration(expression); + if (!signature) { + this._context.addCsNodeDiagnostics( + localFunction, + 'Could not get signature for function', + ts.DiagnosticCategory.Error ); - return { - nodeType: cs.SyntaxKind.ToDoExpression, - parent: parent, - tsNode: expression - } as cs.ToDoExpression; + localFunction.returnType = { + nodeType: cs.SyntaxKind.PrimitiveTypeNode, + parent: localFunction, + type: cs.PrimitiveType.Void + } as cs.PrimitiveTypeNode; } else { - if (expression.name) { - this._context.addTsNodeDiagnostics( - expression, - 'Local functions with names have no matching kind in C#, name will be omitted', - ts.DiagnosticCategory.Warning - ); - } + const returnType = signature.getReturnType(); + localFunction.returnType = this.createUnresolvedTypeNode( + localFunction, + expression.type ?? expression, + returnType + ); + } - const lambdaExpression = { - nodeType: cs.SyntaxKind.LambdaExpression, - parent: parent, - tsNode: expression, - body: {} as cs.Expression, - parameters: [], - returnType: {} as cs.TypeNode - } as cs.LambdaExpression; - - const signature = this._context.typeChecker.getSignatureFromDeclaration(expression); - if (!signature) { - this._context.addCsNodeDiagnostics( - lambdaExpression, - 'Could not get signature for function', - ts.DiagnosticCategory.Error - ); - lambdaExpression.returnType = { - nodeType: cs.SyntaxKind.PrimitiveTypeNode, - parent: lambdaExpression, - type: cs.PrimitiveType.Void - } as cs.PrimitiveTypeNode; - } else { - const returnType = signature.getReturnType(); - lambdaExpression.returnType = this.createUnresolvedTypeNode( - lambdaExpression, - expression.type ?? expression, - returnType - ); - } + if (expression.body) { + localFunction.body = this.visitBlock(localFunction, expression.body); + } else { + localFunction.body = { + nodeType: cs.SyntaxKind.Block, + statements: [], + parent: localFunction + }; + } - expression.parameters.forEach(p => { - lambdaExpression.parameters.push(this.makeParameter(lambdaExpression, p)); - }); + return localFunction; + } - lambdaExpression.body = this.visitBlock(lambdaExpression, expression.body); + protected visitFunctionExpression(parent: cs.Node, expression: ts.FunctionExpression) { + if (expression.name) { + this._context.addTsNodeDiagnostics( + expression, + 'Local functions with names have no matching kind in C#, name will be omitted', + ts.DiagnosticCategory.Warning + ); + } - return lambdaExpression; + const lambdaExpression = { + nodeType: cs.SyntaxKind.LambdaExpression, + parent: parent, + tsNode: expression, + body: {} as cs.Expression, + parameters: [], + returnType: {} as cs.TypeNode + } as cs.LambdaExpression; + + const signature = this._context.typeChecker.getSignatureFromDeclaration(expression); + if (!signature) { + this._context.addCsNodeDiagnostics( + lambdaExpression, + 'Could not get signature for function', + ts.DiagnosticCategory.Error + ); + lambdaExpression.returnType = { + nodeType: cs.SyntaxKind.PrimitiveTypeNode, + parent: lambdaExpression, + type: cs.PrimitiveType.Void + } as cs.PrimitiveTypeNode; + } else { + const returnType = signature.getReturnType(); + lambdaExpression.returnType = this.createUnresolvedTypeNode( + lambdaExpression, + expression.type ?? expression, + returnType + ); } + + for (const p of expression.parameters) { + lambdaExpression.parameters.push(this.makeParameter(lambdaExpression, p)); + } + + lambdaExpression.body = this.visitBlock(lambdaExpression, expression.body); + + return lambdaExpression; } protected visitArrowExpression(parent: cs.Node, expression: ts.ArrowFunction) { @@ -2831,9 +3019,9 @@ export default class CSharpAstTransformer { returnType: {} as cs.TypeNode } as cs.LambdaExpression; - expression.parameters.forEach(p => { + for (const p of expression.parameters) { lambdaExpression.parameters.push(this.makeParameter(lambdaExpression, p)); - }); + } const signature = this._context.typeChecker.getSignatureFromDeclaration(expression); if (!signature) { @@ -2966,7 +3154,7 @@ export default class CSharpAstTransformer { } as cs.StringLiteral); } - expression.templateSpans.forEach(s => { + for (const s of expression.templateSpans) { const e = this.visitExpression(templateString, s.expression); if (e) { templateString.chunks.push(e); @@ -2977,7 +3165,7 @@ export default class CSharpAstTransformer { tsNode: s, text: s.literal.text } as cs.StringLiteral); - }); + } return templateString; } @@ -3017,148 +3205,88 @@ export default class CSharpAstTransformer { } protected visitArrayLiteralExpression(parent: cs.Node, expression: ts.ArrayLiteralExpression) { - if (this.isMapEntry(expression)) { - return this.createMapEntry(parent, expression); - } else if (this.isMapInitializer(expression)) { + const type = this._context.typeChecker.getTypeAtLocation(expression); + if (this._context.typeChecker.isTupleType(type)) { const csExpr = { parent: parent, tsNode: expression, - nodeType: cs.SyntaxKind.InvocationExpression, - arguments: [], - expression: {} as cs.Expression - } as cs.InvocationExpression; + nodeType: cs.SyntaxKind.NewExpression, + type: null!, + arguments: [] + } as cs.NewExpression; - csExpr.expression = this.makeMemberAccess( - csExpr, - this._context.makeTypeName('alphaTab.core.TypeHelper'), - this._context.toMethodName('mapInitializer') - ); + csExpr.type = this._context.makeArrayTupleType(csExpr, []); - expression.elements.forEach(e => { - const ex = this.visitExpression(csExpr, e); - if (ex) { - csExpr.arguments.push(ex); - } - }); - - // steal generic from inner element - if ( - csExpr.arguments.length > 0 && - cs.isInvocationExpression(csExpr.arguments[0]) && - cs.isTypeReference(csExpr.arguments[0].expression) - ) { - csExpr.typeArguments = [csExpr.arguments[0].expression]; + let tupleType = this._context.typeChecker.getContextualType(expression); + let typeArgs = tupleType + ? this._context.typeChecker.getTypeArguments(tupleType as ts.TypeReference) + : undefined; + if (!typeArgs || typeArgs.length !== expression.elements.length) { + tupleType = type; + typeArgs = this._context.typeChecker.getTypeArguments(tupleType as ts.TypeReference); } - return csExpr; - } else if (this.isSetInitializer(expression)) { - const csExpr = { - parent: parent, - tsNode: expression, - nodeType: cs.SyntaxKind.InvocationExpression, - arguments: [], - expression: {} as cs.Expression - } as cs.InvocationExpression; - - csExpr.expression = this.makeMemberAccess( - csExpr, - this._context.makeTypeName('alphaTab.core.TypeHelper'), - this._context.toMethodName('setInitializer') + (csExpr.type as cs.ArrayTupleNode).types = typeArgs!.map((p, i) => + this.createUnresolvedTypeNode(csExpr.type, expression.elements[i], p) ); - const setCreation = expression.parent as ts.NewExpression; - if (setCreation.typeArguments) { - csExpr.typeArguments = setCreation.typeArguments.map(t => this.createUnresolvedTypeNode(csExpr, t)); - } - - expression.elements.forEach(e => { + for (const e of expression.elements) { const ex = this.visitExpression(csExpr, e); if (ex) { - csExpr.arguments.push(ex); + csExpr.arguments!.push(ex); } - }); - - return csExpr; - } else { - const csExpr = { - parent: parent, - tsNode: expression, - nodeType: cs.SyntaxKind.ArrayCreationExpression, - values: [] - } as cs.ArrayCreationExpression; - - const contextual = this._context.typeChecker.getContextualType(expression); - if (!contextual || !contextual.symbol || contextual.symbol.name !== 'Iterable') { - csExpr.type = this.createUnresolvedTypeNode(csExpr, expression, contextual); } - expression.elements.forEach(e => { - const ex = this.visitExpression(csExpr, e); - if (ex) { - csExpr.values!.push(ex); - } - }); - return csExpr; } - } - protected createMapEntry(parent: cs.Node, expression: ts.ArrayLiteralExpression): cs.Expression { const csExpr = { parent: parent, tsNode: expression, - nodeType: cs.SyntaxKind.InvocationExpression, - arguments: [], - expression: {} as cs.Expression - } as cs.InvocationExpression; + nodeType: cs.SyntaxKind.ArrayCreationExpression, + values: [] + } as cs.ArrayCreationExpression; - csExpr.expression = this.makeMemberAccess( - csExpr, - this._context.makeTypeName('alphaTab.core.TypeHelper'), - this._context.toMethodName('createMapEntry') - ); + const contextual = this._context.typeChecker.getContextualType(expression); + if (!contextual || !contextual.symbol || contextual.symbol.name !== 'Iterable') { + csExpr.type = this.createUnresolvedTypeNode(csExpr, expression, contextual); + } - expression.elements.forEach(e => { + for (const e of expression.elements) { const ex = this.visitExpression(csExpr, e); if (ex) { - csExpr.arguments.push(ex); + csExpr.values!.push(ex); } - }); - - return csExpr; - } - - protected isMapInitializer(expression: ts.ArrayLiteralExpression) { - const isCandidate = expression.parent.kind === ts.SyntaxKind.NewExpression; - if (!isCandidate) { - return false; } - return this._context.typeChecker.getTypeAtLocation(expression.parent).symbol.name === 'Map'; + return csExpr; } + protected createMapEntry(parent: cs.Node, expression: ts.ArrayLiteralExpression): cs.Expression { + const csExpr = { + parent: parent, + tsNode: expression, + nodeType: cs.SyntaxKind.NewExpression, + type: null!, + arguments: [] + } as cs.NewExpression; - protected isMapEntry(expression: ts.ArrayLiteralExpression) { - const isCandidate = - expression.elements.length === 2 && - expression.parent.kind === ts.SyntaxKind.ArrayLiteralExpression && - expression.parent.parent.kind === ts.SyntaxKind.NewExpression; - if (!isCandidate) { - return false; - } + csExpr.type = { + nodeType: cs.SyntaxKind.ArrayTupleNode, + parent: csExpr, + types: [], + isNullable: false + } as cs.ArrayTupleNode; - switch (expression.parent.parent.parent.kind) { - case ts.SyntaxKind.PropertyDeclaration: - return this.getDeclarationOrAssignmentType()!.symbol.name === 'Map'; - case ts.SyntaxKind.BinaryExpression: - if ( - (expression.parent.parent.parent as ts.BinaryExpression).operatorToken.kind === - ts.SyntaxKind.EqualsToken - ) { - return this._context.getType(expression.parent.parent)?.symbol.name === 'Map'; - } - break; + for (const e of expression.elements) { + const ex = this.visitExpression(csExpr, e); + if (ex) { + (csExpr.type as cs.ArrayTupleNode).types.push( + this.createUnresolvedTypeNode(csExpr.type, e, this._context.typeChecker.getTypeAtLocation(e)) + ); + csExpr.arguments.push(ex); + } } - return false; + return csExpr; } protected isSetInitializer(expression: ts.ArrayLiteralExpression) { @@ -3192,9 +3320,11 @@ export default class CSharpAstTransformer { nodeType: cs.SyntaxKind.MemberAccessExpression } as cs.MemberAccessExpression; + let convertToInvocation = false; + if (memberAccess.tsSymbol) { if (this._context.isMethodSymbol(memberAccess.tsSymbol)) { - memberAccess.member = this._context.toMethodName(expression.name.text); + memberAccess.member = this._context.buildMethodName(expression.name); } else if (this._context.isPropertySymbol(memberAccess.tsSymbol)) { memberAccess.member = this._context.toPropertyName(expression.name.text); } @@ -3211,10 +3341,12 @@ export default class CSharpAstTransformer { if (memberAccess.tsSymbol) { const parentSymbol = (memberAccess.tsSymbol as any).parent as ts.Symbol; if (parentSymbol) { - const renamed = this.getSymbolName(parentSymbol!, memberAccess.tsSymbol!, memberAccess); + const renamed = this.getSymbolName(parentSymbol!, memberAccess.tsSymbol!); if (renamed) { memberAccess.member = renamed; } + + convertToInvocation = this.convertPropertyToInvocation(parentSymbol!, memberAccess.tsSymbol!); } } @@ -3227,10 +3359,41 @@ export default class CSharpAstTransformer { return null; } + if (convertToInvocation && !ts.isCallExpression(expression.parent)) { + const invocation: cs.InvocationExpression = { + nodeType: cs.SyntaxKind.InvocationExpression, + expression: memberAccess, + arguments: [], + tsNode: memberAccess.tsNode, + tsSymbol: memberAccess.tsSymbol, + parent: memberAccess.parent, + skipEmit: memberAccess.skipEmit + }; + + memberAccess.parent = invocation; + + return this.wrapToSmartCast(parent, invocation, expression); + } return this.wrapToSmartCast(parent, memberAccess, expression); } - protected getSymbolName(parentSymbol: ts.Symbol, symbol: ts.Symbol, expression: cs.Expression): string | null { + protected convertPropertyToInvocation(parentSymbol: ts.Symbol, symbol: ts.Symbol): boolean { + switch (parentSymbol.name) { + case 'Error': + switch (symbol.name) { + case 'stack': + case 'cause': + return true; + } + break; + // chai assertions + case 'Assertion': + return true; + } + return false; + } + + protected getSymbolName(parentSymbol: ts.Symbol, symbol: ts.Symbol): string | null { switch (parentSymbol.name) { case 'Array': switch (symbol.name) { @@ -3272,7 +3435,7 @@ export default class CSharpAstTransformer { properties: [] } as cs.AnonymousObjectCreationExpression; - expression.properties.forEach(p => { + for (const p of expression.properties) { if (ts.isPropertyAssignment(p)) { const assignment = { parent: objectLiteral, @@ -3333,7 +3496,9 @@ export default class CSharpAstTransformer { lambda.returnType = this.createUnresolvedTypeNode(lambda, p.type ?? p, returnType); } - p.parameters.forEach(param => lambda.parameters.push(this.makeParameter(lambda, param))); + for (const param of p.parameters) { + lambda.parameters.push(this.makeParameter(lambda, param)); + } lambda.body = this.visitBlock(parent, p.body!); assignment.value = lambda; @@ -3352,7 +3517,7 @@ export default class CSharpAstTransformer { ts.DiagnosticCategory.Error ); } - }); + } return objectLiteral; } @@ -3365,13 +3530,14 @@ export default class CSharpAstTransformer { nodeType: cs.SyntaxKind.InvocationExpression, tsNode: expr.tsNode } as cs.InvocationExpression; - const memberAccess = (callExpr.expression = { + const memberAccess = { expression: null!, member: this._context.toPascalCase('toInvariantString'), parent: callExpr, tsNode: expr.tsNode, nodeType: cs.SyntaxKind.MemberAccessExpression - } as cs.MemberAccessExpression); + } as cs.MemberAccessExpression; + callExpr.expression = memberAccess; const par = { parent: memberAccess, @@ -3387,8 +3553,37 @@ export default class CSharpAstTransformer { } protected visitElementAccessExpression(parent: cs.Node, expression: ts.ElementAccessExpression) { - // Enum[value] => value.ToString() - if (this.isEnumToString(expression)) { + // Enum[enumValue] => value.toString() + // Enum[string] => TypeHelper.parseEnum(value, Type) + if (this.isEnumFromOrToString(expression)) { + const elementType = this._context.typeChecker.getTypeAtLocation(expression.argumentExpression); + + if (this._context.isEnum(elementType)) { + const callExpr = { + parent: parent, + arguments: [], + expression: {} as cs.Expression, + nodeType: cs.SyntaxKind.InvocationExpression, + tsNode: expression + } as cs.InvocationExpression; + + const memberAccess = { + expression: {} as cs.Expression, + member: this._context.toPascalCase('toString'), + parent: callExpr, + tsNode: expression, + nodeType: cs.SyntaxKind.MemberAccessExpression + } as cs.MemberAccessExpression; + callExpr.expression = memberAccess; + + memberAccess.expression = this.visitExpression(memberAccess, expression.argumentExpression)!; + if (!memberAccess.expression) { + return null; + } + + return callExpr; + } + const callExpr = { parent: parent, arguments: [], @@ -3397,22 +3592,55 @@ export default class CSharpAstTransformer { tsNode: expression } as cs.InvocationExpression; - const memberAccess = (callExpr.expression = { + callExpr.expression = this.makeMemberAccess( + callExpr, + this._context.makeTypeName('alphaTab.core.TypeHelper'), + this._context.toMethodName('parseEnum') + ); + + const enumType = this._context.typeChecker.getTypeAtLocation(expression.expression); + callExpr.typeArguments = [ + this.createUnresolvedTypeNode(callExpr, expression.argumentExpression, enumType, enumType.symbol) + ]; + + const typeOf: cs.TypeOfExpression = { + nodeType: cs.SyntaxKind.TypeOfExpression, + parent: callExpr + }; + typeOf.type = this.createUnresolvedTypeNode( + typeOf, + expression.argumentExpression, + enumType, + enumType.symbol + ); + + callExpr.arguments = [this.visitExpression(callExpr, expression.argumentExpression)!, typeOf]; + + return callExpr; + } + + const argumentSymbol = this._context.typeChecker.getSymbolAtLocation(expression.argumentExpression); + const elementAccessMethod = argumentSymbol ? this._context.getMethodNameFromSymbol(argumentSymbol) : ''; + if(elementAccessMethod) { + + const memberAccess = { + nodeType: cs.SyntaxKind.MemberAccessExpression, expression: {} as cs.Expression, - member: this._context.toPascalCase('toString'), - parent: callExpr, + member: this._context.toMethodName(elementAccessMethod), + parent: parent, tsNode: expression, - nodeType: cs.SyntaxKind.MemberAccessExpression - } as cs.MemberAccessExpression); - - memberAccess.expression = this.visitExpression(memberAccess, expression.argumentExpression)!; + nullSafe: !!expression.questionDotToken + } as cs.MemberAccessExpression; + + memberAccess.expression = this.visitExpression(memberAccess, expression.expression)!; if (!memberAccess.expression) { return null; } - return callExpr; + return memberAccess; } + const elementAccess = { expression: {} as cs.Expression, argumentExpression: {} as cs.Expression, @@ -3433,12 +3661,18 @@ export default class CSharpAstTransformer { } const symbol = this._context.typeChecker.getSymbolAtLocation(expression.expression); - let type = symbol ? this._context.typeChecker.getTypeOfSymbolAtLocation(symbol!, expression.expression) : null; + let type = symbol + ? this._context.typeChecker.getTypeOfSymbolAtLocation(symbol!, expression.expression) + : this._context.typeChecker.getTypeAtLocation(expression.expression); if (type) { type = this._context.typeChecker.getNonNullableType(type); } + const isArrayTupleAccessor = type && this._context.typeChecker.isTupleType(type); const isArrayAccessor = - !symbol || (type && type.symbol && !!type.symbol.members?.has(ts.escapeLeadingUnderscores('slice'))); + (!isArrayTupleAccessor && !symbol) || + (type && type.symbol && !!type.symbol.members?.has(ts.escapeLeadingUnderscores('slice'))); + + const forceCast = false; if (isArrayAccessor) { const csArg = { expression: {} as cs.Expression, @@ -3459,15 +3693,38 @@ export default class CSharpAstTransformer { } as cs.ParenthesizedExpression; argumentExpression.parent = par; csArg.expression = par; + } else if (isArrayTupleAccessor) { + let index = expression.argumentExpression; + while (ts.isParenthesizedExpression(index)) { + index = index.expression; + } + + // x[0] -> x.V1 + if (ts.isNumericLiteral(index)) { + return { + expression: elementAccess.expression, + member: this._context.toPropertyName(`v${index.text}`), + parent: parent, + tsNode: expression, + nodeType: cs.SyntaxKind.MemberAccessExpression, + nullSafe: !!expression.questionDotToken + } as cs.MemberAccessExpression; + } + // x[expr] -> x[expr] as Type + this._context.addTsNodeDiagnostics( + expression, + 'Dynamic expressions on tuple types are not supported', + ts.DiagnosticCategory.Error + ); } else { elementAccess.argumentExpression = argumentExpression; argumentExpression.parent = elementAccess; } - return this.wrapToSmartCast(parent, elementAccess, expression); + return this.wrapToSmartCast(parent, elementAccess, expression, forceCast); } - protected isEnumToString(expression: ts.ElementAccessExpression): boolean { + protected isEnumFromOrToString(expression: ts.ElementAccessExpression): boolean { const enumType = this._context.typeChecker.getTypeAtLocation(expression.expression); return !!(enumType?.symbol && enumType.symbol.flags & ts.SymbolFlags.RegularEnum); } @@ -3550,7 +3807,8 @@ export default class CSharpAstTransformer { expression: {} as cs.Expression, parent: parent, tsNode: expression, - nodeType: cs.SyntaxKind.InvocationExpression + nodeType: cs.SyntaxKind.InvocationExpression, + nullSafe: !!expression.questionDotToken } as cs.InvocationExpression; // chai @@ -3559,7 +3817,7 @@ export default class CSharpAstTransformer { parent: callExpression, tsNode: expression.expression, nodeType: cs.SyntaxKind.Identifier, - text: 'TestGlobals.' + this._context.toPascalCase('expect') + text: `TestGlobals.${this._context.toPascalCase('expect')}` } as cs.Identifier; } else { callExpression.expression = this.visitExpression(callExpression, expression.expression)!; @@ -3576,18 +3834,18 @@ export default class CSharpAstTransformer { return null; } - expression.arguments.forEach(a => { + for (const a of expression.arguments) { const e = this.visitExpression(callExpression, a); if (e) { callExpression.arguments.push(e); } - }); + } if (expression.typeArguments) { callExpression.typeArguments = []; - expression.typeArguments.forEach(a => - callExpression.typeArguments!.push(this.createUnresolvedTypeNode(callExpression, a)) - ); + for (const a of expression.typeArguments) { + callExpression.typeArguments!.push(this.createUnresolvedTypeNode(callExpression, a)); + } } return this.makeTruthy(callExpression); @@ -3606,7 +3864,7 @@ export default class CSharpAstTransformer { type = this._context.typeChecker.getTypeOfSymbolAtLocation(symbol, expression.expression) ?? null; } - if (type?.symbol?.name == 'PromiseConstructor') { + if (type?.symbol?.name === 'PromiseConstructor') { const invocation = { parent: parent, tsNode: expression, @@ -3627,13 +3885,13 @@ export default class CSharpAstTransformer { } const isVoidPromise = - !expression.typeArguments || expression.typeArguments[0].kind == ts.SyntaxKind.VoidKeyword; + !expression.typeArguments || expression.typeArguments[0].kind === ts.SyntaxKind.VoidKeyword; if (!isVoidPromise) { invocation.typeArguments = []; - expression.typeArguments!.forEach(a => - invocation.typeArguments!.push(this.createUnresolvedTypeNode(invocation, a)) - ); + for (const a of expression.typeArguments!) { + invocation.typeArguments!.push(this.createUnresolvedTypeNode(invocation, a)); + } } else if (e && cs.isLambdaExpression(e)) { e.parameters[0].type = { nodeType: cs.SyntaxKind.FunctionTypeNode, @@ -3662,9 +3920,9 @@ export default class CSharpAstTransformer { if (expression.typeArguments) { csType.typeArguments = []; - expression.typeArguments.forEach(a => - csType.typeArguments!.push(this.createUnresolvedTypeNode(newExpression, a)) - ); + for (const a of expression.typeArguments) { + csType.typeArguments!.push(this.createUnresolvedTypeNode(newExpression, a)); + } } else { const typeAtLocation = this._context.typeChecker.getTypeAtLocation(expression) as ts.TypeReference; if (typeAtLocation.typeArguments && typeAtLocation.typeArguments.length > 0) { @@ -3673,11 +3931,11 @@ export default class CSharpAstTransformer { // we have some inferred type arguments here if (actualTypeArguments && actualTypeArguments.length === typeAtLocation.typeArguments.length) { csType.typeArguments = []; - actualTypeArguments.forEach(a => { + for (const a of actualTypeArguments) { csType.typeArguments!.push( this.createUnresolvedTypeNode(newExpression, expression.expression, a) ); - }); + } } else { csType.typeArguments = []; @@ -3692,12 +3950,12 @@ export default class CSharpAstTransformer { } if (expression.arguments) { - expression.arguments.forEach(a => { + for (const a of expression.arguments) { const e = this.visitExpression(newExpression, a); if (e) { newExpression.arguments.push(e); } - }); + } } if (type && type.symbol && type.symbol.name === 'ArrayConstructor' && newExpression.arguments.length === 1) { @@ -3746,21 +4004,40 @@ export default class CSharpAstTransformer { } return valueAccessExpression; - } else { - const nonNullExpression = { - expression: {} as cs.Expression, - parent: parent, - tsNode: expression, - nodeType: cs.SyntaxKind.NonNullExpression - } as cs.NonNullExpression; + } + const nonNullExpression = { + expression: {} as cs.Expression, + parent: parent, + tsNode: expression, + nodeType: cs.SyntaxKind.NonNullExpression + } as cs.NonNullExpression; - nonNullExpression.expression = this.visitExpression(nonNullExpression, expression.expression)!; - if (!nonNullExpression.expression) { - return null; - } + nonNullExpression.expression = this.visitExpression(nonNullExpression, expression.expression)!; + if (!nonNullExpression.expression) { + return null; + } + + return nonNullExpression; + } - return nonNullExpression; + protected visitYieldExpression(parent: cs.Node, expression: ts.YieldExpression) { + const yieldExpression = { + expression: {} as cs.Expression, + member: 'Value', + parent: parent, + tsNode: expression, + nodeType: cs.SyntaxKind.YieldExpression + } as cs.YieldExpression; + + yieldExpression.expression = expression.expression + ? this.visitExpression(yieldExpression, expression.expression) + : null; + + if (expression.expression && !yieldExpression.expression) { + return null; } + + return yieldExpression; } protected visitIdentifier(parent: cs.Node, expression: ts.Identifier) { @@ -3768,8 +4045,8 @@ export default class CSharpAstTransformer { return { parent: parent, tsNode: expression, - nodeType: cs.SyntaxKind.NullLiteral - } as cs.NullLiteral; + nodeType: cs.SyntaxKind.DefaultExpression + } as cs.DefaultExpression; } const identifier = { @@ -3809,7 +4086,12 @@ export default class CSharpAstTransformer { return expression.text; } - protected wrapToSmartCast(parent: cs.Node, node: cs.Node, expression: ts.Expression): cs.Expression { + protected wrapToSmartCast( + parent: cs.Node, + node: cs.Node, + expression: ts.Expression, + forceCast: boolean = false + ): cs.Expression { if (node.tsSymbol) { if ( (node.tsSymbol.flags & ts.SymbolFlags.Property) === ts.SymbolFlags.Property || @@ -3819,7 +4101,7 @@ export default class CSharpAstTransformer { ts.SymbolFlags.FunctionScopedVariable || (node.tsSymbol.flags & ts.SymbolFlags.BlockScopedVariable) === ts.SymbolFlags.BlockScopedVariable ) { - let smartCastType = this._context.getSmartCastType(expression); + const smartCastType = this._context.getSmartCastType(expression); if (smartCastType && !this._context.isIterable(smartCastType)) { if (smartCastType.flags & ts.TypeFlags.Boolean) { return this.makeTruthy(node, true); @@ -3832,7 +4114,7 @@ export default class CSharpAstTransformer { nodeType: cs.SyntaxKind.ParenthesizedExpression } as cs.ParenthesizedExpression; - const castExpression = (paren.expression = { + const castExpression = { type: this.createUnresolvedTypeNode( null, expression, @@ -3843,12 +4125,13 @@ export default class CSharpAstTransformer { parent: paren, tsNode: expression, nodeType: cs.SyntaxKind.CastExpression - } as cs.CastExpression); + } as cs.CastExpression; + paren.expression = castExpression; castExpression.type.parent = castExpression; castExpression.expression.parent = castExpression; - return paren; + return this.makeTruthy(paren); } const isValueTypeNotNullSmartCast = this._context.isValueTypeNotNullSmartCast(expression); @@ -3861,23 +4144,22 @@ export default class CSharpAstTransformer { expression: node, member: 'Value' } as cs.MemberAccessExpression; - } else { - return { - parent: parent, - nodeType: cs.SyntaxKind.NonNullExpression, - tsNode: expression, - expression: node - } as cs.NonNullExpression; } + return this.makeTruthy({ + parent: parent, + nodeType: cs.SyntaxKind.NonNullExpression, + tsNode: expression, + expression: node + } as cs.NonNullExpression); } if (this._context.isNonNullSmartCast(expression)) { - return { + return this.makeTruthy({ parent: parent, nodeType: cs.SyntaxKind.NonNullExpression, tsNode: expression, expression: node - } as cs.NonNullExpression; + } as cs.NonNullExpression); } } } @@ -3991,6 +4273,8 @@ export default class CSharpAstTransformer { return '>>>='; case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken: return '>>='; + case ts.SyntaxKind.QuestionQuestionEqualsToken: + return '??='; } return ''; } diff --git a/src.compiler/csharp/CSharpEmitter.ts b/src.compiler/csharp/CSharpEmitter.ts index 6edd0406c..29dc177e1 100644 --- a/src.compiler/csharp/CSharpEmitter.ts +++ b/src.compiler/csharp/CSharpEmitter.ts @@ -1,20 +1,17 @@ -import * as ts from 'typescript'; +import type * as ts from 'typescript'; import CSharpAstTransformer from './CSharpAstTransformer'; import CSharpEmitterContext from './CSharpEmitterContext'; import CSharpAstPrinter from './CSharpAstPrinter'; -import { transpileFilter } from '../BuilderHelpers' +import { transpileFilter } from '../BuilderHelpers'; export default function emit(program: ts.Program, diagnostics: ts.Diagnostic[]) { const context = new CSharpEmitterContext(program); console.log('[C#] Transforming to C# AST'); - program.getRootFileNames() - .filter(transpileFilter) - .forEach(file => { + for (const file of program.getRootFileNames().filter(transpileFilter)) { const sourceFile = program.getSourceFile(file)!; const transformer = new CSharpAstTransformer(sourceFile, context); transformer.transform(); - }); - + } console.log('[C#] Resolving types'); context.resolveAllUnresolvedTypeNodes(); @@ -22,12 +19,12 @@ export default function emit(program: ts.Program, diagnostics: ts.Diagnostic[]) if (!context.hasErrors) { console.log(`[C#] Writing Result to ${context.compilerOptions.outDir!} (${context.csharpFiles.length} files)`); - context.csharpFiles.forEach(file => { + for (const file of context.csharpFiles) { const printer = new CSharpAstPrinter(file, context); printer.print(); diagnostics.push(...printer.diagnostics); - }) + } } diagnostics.push(...context.diagnostics); -} \ No newline at end of file +} diff --git a/src.compiler/csharp/CSharpEmitterContext.ts b/src.compiler/csharp/CSharpEmitterContext.ts index 4d903d00c..b0085db85 100644 --- a/src.compiler/csharp/CSharpEmitterContext.ts +++ b/src.compiler/csharp/CSharpEmitterContext.ts @@ -1,11 +1,10 @@ import * as cs from './CSharpAst'; -import * as ts from 'typescript'; -import * as path from 'path'; +import ts from 'typescript'; +import path from 'node:path'; type SymbolKey = string; export default class CSharpEmitterContext { - private _fileLookup: Map = new Map(); private _symbolLookup: Map = new Map(); private _exportedSymbols: Map = new Map(); private _virtualSymbols: Map = new Map(); @@ -58,7 +57,13 @@ export default class CSharpEmitterContext { return (tsSymbol.flags & ts.SymbolFlags.Property) !== 0; } - public isTypeAssignable(targetType: ts.Type, actualType: ts.Type) { + public isTypeAssignable(targetType: ts.Type, contextualTypeNullable: ts.Type, actualType: ts.Type) { + if ( + contextualTypeNullable.flags === ts.TypeFlags.Any || + contextualTypeNullable.flags === ts.TypeFlags.Unknown + ) { + return true; + } if (targetType.flags === ts.TypeFlags.Any || targetType.flags === ts.TypeFlags.Unknown) { return true; } @@ -75,6 +80,13 @@ export default class CSharpEmitterContext { this._unresolvedTypeNodes.push(unresolved); } + public removeUnresolvedTypeNode(unresolved: cs.UnresolvedTypeNode) { + if (this.processingSkippedElement) { + return; + } + this._unresolvedTypeNodes = this._unresolvedTypeNodes.filter(n => n !== unresolved); + } + public getSymbolName(expr: cs.Node): string | undefined { const symbolKey = this.getSymbolKey(expr.tsSymbol); if (expr.tsSymbol) { @@ -87,25 +99,54 @@ export default class CSharpEmitterContext { return this.getFullName(csSymbol as cs.NamedTypeDeclaration, expr); } return csSymbol.name; - } else if ( + } + + if ( expr.tsSymbol.flags & ts.SymbolFlags.Class || expr.tsSymbol.flags & ts.SymbolFlags.Interface || expr.tsSymbol.flags & ts.SymbolFlags.ConstEnum || expr.tsSymbol.flags & ts.SymbolFlags.RegularEnum ) { + switch (expr.tsSymbol.name) { + case 'Error': + return this.makeExceptionType(); + case 'Iterable': + return this.makeIterableType(); + case 'Iterator': + return this.makeIteratorType(); + case 'Generator': + return this.makeGeneratorType(); + case 'Disposable': + return 'Disposable'; + } + return this.buildCoreNamespace(expr.tsSymbol) + this.toCoreTypeName(expr.tsSymbol.name); - } else if (expr.tsSymbol.flags & ts.SymbolFlags.Function) { + } + + if (expr.tsSymbol.flags & ts.SymbolFlags.Function) { if (this.isTestFunction(expr.tsSymbol)) { - return this.toPascalCase('alphaTab.test') + '.Globals.' + this.toPascalCase(expr.tsSymbol.name); + return `${this.toPascalCase('alphaTab.test')}.Globals.${this.toPascalCase(expr.tsSymbol.name)}`; } - return this.toPascalCase('alphaTab.core') + '.Globals.' + this.toPascalCase(expr.tsSymbol.name); - } else if ( + + if (expr.tsSymbol.valueDeclaration && expr.tsNode) { + const sourceFile = expr.tsSymbol.valueDeclaration.getSourceFile(); + if (sourceFile === expr.tsNode.getSourceFile()) { + return expr.tsSymbol.name; + } + } + + return `${this.toPascalCase('alphaTab.core')}.Globals.${this.toPascalCase(expr.tsSymbol.name)}`; + } + + if ( (expr.tsSymbol.flags & ts.SymbolFlags.FunctionScopedVariable && this.isGlobalVariable(expr.tsSymbol)) || (expr.tsSymbol.flags & ts.SymbolFlags.NamespaceModule && this.isKnownModule(expr.tsSymbol)) ) { - return this.toPascalCase('alphaTab.core') + '.Globals.' + this.toPascalCase(expr.tsSymbol.name); - } else if (expr.tsSymbol) { - let externalModule = this.resolveExternalModuleOfType(expr.tsSymbol); + return `${this.toPascalCase('alphaTab.core')}.Globals.${this.toPascalCase(expr.tsSymbol.name)}`; + } + + if (expr.tsSymbol) { + const externalModule = this.resolveExternalModuleOfType(expr.tsSymbol); if (externalModule) { return externalModule + this.toPascalCase(expr.tsSymbol.name); } @@ -148,9 +189,9 @@ export default class CSharpEmitterContext { case cs.SyntaxKind.ClassDeclaration: case cs.SyntaxKind.InterfaceDeclaration: case cs.SyntaxKind.EnumDeclaration: - return this.getFullName(type.parent as cs.NamedTypeDeclaration) + '.' + this.getClassName(type, expr); + return `${this.getFullName(type.parent as cs.NamedTypeDeclaration)}.${this.getClassName(type, expr)}`; case cs.SyntaxKind.NamespaceDeclaration: - return (type.parent as cs.NamespaceDeclaration).namespace + '.' + this.getClassName(type, expr); + return `${(type.parent as cs.NamespaceDeclaration).namespace}.${this.getClassName(type, expr)}`; } return ''; } @@ -160,7 +201,7 @@ export default class CSharpEmitterContext { } public resolveAllUnresolvedTypeNodes() { - for (let node of this._unresolvedTypeNodes) { + for (const node of this._unresolvedTypeNodes) { let resolved = this.resolveType(node); if (!resolved) { resolved = this.resolveType(node); @@ -219,7 +260,6 @@ export default class CSharpEmitterContext { public addSourceFile(csharpFile: cs.SourceFile) { this.csharpFiles.push(csharpFile); - this._fileLookup.set(csharpFile.tsNode as ts.SourceFile, csharpFile); } public resolveType(node: cs.UnresolvedTypeNode): cs.TypeNode | null { @@ -237,7 +277,7 @@ export default class CSharpEmitterContext { } if (node.tsType) { - resolved = this.getTypeFromTsType(node, node.tsType, node.tsSymbol, node.typeArguments); + resolved = this.getTypeFromTsType(node, node.tsType, node.tsSymbol, node.typeArguments, node.tsNode); } if (resolved) { @@ -285,25 +325,63 @@ export default class CSharpEmitterContext { node: cs.Node, tsType: ts.Type, tsSymbol?: ts.Symbol, - typeArguments?: cs.UnresolvedTypeNode[] + typeArguments?: cs.UnresolvedTypeNode[], + typeNode?: ts.Node ): cs.TypeNode | null { let csType: cs.TypeNode | null = this.resolveKnownTypeSymbol(node, tsType, typeArguments); if (csType) { - return csType; + return this.applyNullable(csType, typeNode); + } + + if (typeNode && ts.isTypeOperatorNode(typeNode)) { + if (typeNode.operator === ts.SyntaxKind.KeyOfKeyword) { + return { + nodeType: cs.SyntaxKind.PrimitiveTypeNode, + type: cs.PrimitiveType.String, + isNullable: false + } as cs.PrimitiveTypeNode; + } + return null; } csType = this.resolvePrimitiveType(node, tsType); if (csType) { - return csType; + return this.applyNullable(csType, typeNode); } csType = this.resolveUnionType(node, tsType, typeArguments); if (csType) { - return csType; + return this.applyNullable(csType, typeNode); + } + + csType = this.resolveIntersectionType(node, tsType, typeArguments); + if (csType) { + return this.applyNullable(csType, typeNode); } csType = this.resolveUnknownTypeSymbol(node, tsType, tsSymbol, typeArguments); + return this.applyNullable(csType, typeNode); + } + + private applyNullable(csType: cs.TypeNode | null, typeNode: ts.Node | undefined): cs.TypeNode | null { + if (!csType || !typeNode || ts.isTypeNode(typeNode)) { + return csType; + } + + if (ts.isUnionTypeNode(typeNode)) { + for (const t of typeNode.types) { + if ( + t.kind === ts.SyntaxKind.NullKeyword || + (ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword) || + t.kind === ts.SyntaxKind.UndefinedKeyword || + (ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.UndefinedKeyword) + ) { + csType.isNullable = true; + } + } + } + return csType; } @@ -320,9 +398,23 @@ export default class CSharpEmitterContext { if (!tsSymbol) { return null; } + if (tsType.isTypeParameter()) { + return { + nodeType: cs.SyntaxKind.TypeReference, + parent: node.parent, + tsNode: node.tsNode, + isAsync: false, + reference: tsType.symbol.name + } as cs.TypeReference; + } // some built in type handling - switch (tsSymbol.name) { + let symbolName = tsSymbol.name; + if (symbolName.endsWith('Constructor')) { + symbolName = symbolName.substring(0, symbolName.length - 'Constructor'.length); + } + + switch (symbolName) { case 'Promise': const promiseType = tsType as ts.TypeReference; @@ -337,7 +429,7 @@ export default class CSharpEmitterContext { if ( promiseReturnType != null && cs.isPrimitiveTypeNode(promiseReturnType) && - promiseReturnType.type == cs.PrimitiveType.Void + promiseReturnType.type === cs.PrimitiveType.Void ) { promiseReturnType = null; } @@ -367,7 +459,78 @@ export default class CSharpEmitterContext { mapValueType = this.getTypeFromTsType(node, mapType.typeArguments[1]); } - return this.createMapType(tsSymbol, node, mapKeyType!, mapValueType!); + return this.createMapType(tsSymbol, node, mapKeyType, mapValueType); + case 'Iterable': + const iterableType = tsType as ts.TypeReference; + + let iterableItemType: cs.TypeNode | null = null; + + if (typeArguments) { + iterableItemType = typeArguments[0]; + } else if (iterableType.typeArguments) { + iterableItemType = this.getTypeFromTsType(node, iterableType.typeArguments[0]); + } + + return { + nodeType: cs.SyntaxKind.TypeReference, + parent: node.parent, + tsNode: node.tsNode, + reference: this.makeIterableType(), + typeArguments: [iterableItemType] + } as cs.TypeReference; + + case 'Generator': + const generatorType = tsType as ts.TypeReference; + + let generatorItemType: cs.TypeNode | null = null; + + if (typeArguments) { + generatorItemType = typeArguments[0]; + } else if (generatorType.typeArguments) { + generatorItemType = this.getTypeFromTsType(node, generatorType.typeArguments[0]); + } + + return { + nodeType: cs.SyntaxKind.TypeReference, + parent: node.parent, + tsNode: node.tsNode, + reference: this.makeGeneratorType(), + typeArguments: [generatorItemType] + } as cs.TypeReference; + + case 'Iterator': + const iteratorType = tsType as ts.TypeReference; + + let iteratorItemType: cs.TypeNode | null = null; + + if (typeArguments) { + iteratorItemType = typeArguments[0]; + } else if (iteratorType.typeArguments) { + iteratorItemType = this.getTypeFromTsType(node, iteratorType.typeArguments[0]); + } + + return { + nodeType: cs.SyntaxKind.TypeReference, + parent: node.parent, + tsNode: node.tsNode, + reference: this.makeIteratorType(), + typeArguments: [iteratorItemType] + } as cs.TypeReference; + + case 'Disposable': + return { + nodeType: cs.SyntaxKind.TypeReference, + isAsync: false, + isNullable: false, + reference: 'Disposable' + } as cs.TypeReference; + case 'Error': + return { + nodeType: cs.SyntaxKind.TypeReference, + isAsync: false, + isNullable: false, + reference: this.makeExceptionType() + } as cs.TypeReference; case 'Array': const arrayType = tsType as ts.TypeReference; let arrayElementType: cs.TypeNode | null = null; @@ -388,11 +551,9 @@ export default class CSharpEmitterContext { return this.createArrayListType(tsSymbol, node, arrayElementType); case ts.InternalSymbolName.Type: - let csType: cs.TypeNode | null = null; - - csType = this.resolveFunctionTypeFromTsType(node, tsType); - - return csType; + return this.resolveFunctionTypeFromTsType(node, tsType); + case ts.InternalSymbolName.Function: + return this.resolveFunctionTypeFromTsType(node, tsType); default: let externalModule = this.resolveExternalModuleOfType(tsSymbol); @@ -401,7 +562,7 @@ export default class CSharpEmitterContext { } // remove ArrayBuffer type arguments - switch (tsSymbol.name) { + switch (symbolName) { case 'Int8Array': case 'Uint8Array': case 'Int16Array': @@ -412,6 +573,7 @@ export default class CSharpEmitterContext { case 'Float64Array': case 'DataView': typeArguments = []; + tsSymbol = undefined; break; } @@ -419,7 +581,8 @@ export default class CSharpEmitterContext { nodeType: cs.SyntaxKind.TypeReference, parent: node.parent, tsNode: node.tsNode, - reference: externalModule + tsSymbol.name, + tsSymbol: tsSymbol, + reference: externalModule + symbolName, typeArguments: typeArguments } as cs.TypeReference; } @@ -437,7 +600,9 @@ export default class CSharpEmitterContext { case 'AlphaSkiaTextAlign': case 'AlphaSkiaTextBaseline': case 'AlphaSkiaTypeface': - return this.alphaSkiaModule() + '.'; + case 'AlphaSkiaTextStyle': + case 'AlphaSkiaTextMetrics': + return `${this.alphaSkiaModule()}.`; } return undefined; @@ -459,8 +624,8 @@ export default class CSharpEmitterContext { protected createMapType( symbol: ts.Symbol, node: cs.Node, - mapKeyType: cs.TypeNode, - mapValueType: cs.TypeNode + mapKeyType: cs.TypeNode | null, + mapValueType: cs.TypeNode | null ): cs.TypeNode { return { nodeType: cs.SyntaxKind.MapTypeNode, @@ -473,7 +638,7 @@ export default class CSharpEmitterContext { } as cs.MapTypeNode; } - protected isCsValueType(mapValueType: cs.TypeNode) { + protected isCsValueType(mapValueType: cs.TypeNode | null) { if (mapValueType) { switch (mapValueType.nodeType) { case cs.SyntaxKind.PrimitiveTypeNode: @@ -493,6 +658,8 @@ export default class CSharpEmitterContext { } } break; + case cs.SyntaxKind.ArrayTupleNode: + return true; } } return false; @@ -506,59 +673,36 @@ export default class CSharpEmitterContext { return null; } - let functionTypeNode: ts.FunctionTypeNode | null = null; - for (const declaration of tsType.symbol.declarations) { - if (ts.isFunctionTypeNode(declaration)) { - functionTypeNode = declaration; - break; - } - } - - if (!functionTypeNode) { + const signatures = tsType.getCallSignatures(); + if (signatures.length === 0) { return null; } - const typeNodeToCsType = (typeNode: ts.TypeNode) => { - const nodeTsType = this.typeChecker.getTypeAtLocation(typeNode); - if (!nodeTsType) { - return null; - } - - const nodeType = this.getTypeFromTsType(node, nodeTsType); - if (!nodeType) { - return null; - } - - return nodeType; - }; - - const returnType = typeNodeToCsType(functionTypeNode.type); + const returnType = this.getTypeFromTsType(node, signatures[0].getReturnType()); if (!returnType) { - this.addTsNodeDiagnostics( - functionTypeNode.type, - 'Could not resolve return type', - ts.DiagnosticCategory.Error - ); + this.addCsNodeDiagnostics(node, 'Could not resolve return type', ts.DiagnosticCategory.Error); return null; } const parameterTypes: cs.TypeNode[] = []; - for (const p of functionTypeNode.parameters) { - const symbol = this.typeChecker.getSymbolAtLocation(p.name); - if (!symbol) { - this.addTsNodeDiagnostics(p, 'Could not resolve symbol for parameter', ts.DiagnosticCategory.Error); - return null; - } - - const pTsType = this.typeChecker.getTypeOfSymbolAtLocation(symbol, p); + for (const p of signatures[0].parameters) { + const pTsType = this.typeChecker.getTypeOfSymbol(p); if (!pTsType) { - this.addTsNodeDiagnostics(p, 'Could not resolve type for parameter', ts.DiagnosticCategory.Error); + this.addTsNodeDiagnostics( + p.declarations![0], + 'Could not resolve type for parameter', + ts.DiagnosticCategory.Error + ); return null; } const pType = this.getTypeFromTsType(node, pTsType); if (!pType) { - this.addTsNodeDiagnostics(p, 'Could not map type for parameter', ts.DiagnosticCategory.Error); + this.addTsNodeDiagnostics( + p.declarations![0], + 'Could not map type for parameter', + ts.DiagnosticCategory.Error + ); return null; } @@ -566,18 +710,26 @@ export default class CSharpEmitterContext { } let typeParameters: cs.TypeNode[] | undefined = undefined; - if (functionTypeNode.typeParameters) { + if (signatures[0].typeParameters) { typeParameters = []; - for (const tp of functionTypeNode.typeParameters) { - const tpTsType = this.typeChecker.getTypeAtLocation(tp); + for (const tp of signatures[0].typeParameters) { + const tpTsType = this.typeChecker.getTypeOfSymbol(tp.symbol); if (!tpTsType) { - this.addTsNodeDiagnostics(tp, 'Could not resolve type parameter', ts.DiagnosticCategory.Error); + this.addTsNodeDiagnostics( + tp.symbol.declarations![0], + 'Could not resolve type parameter', + ts.DiagnosticCategory.Error + ); return null; } const tpType = this.getTypeFromTsType(node, tpTsType); if (!tpType) { - this.addTsNodeDiagnostics(tp, 'Could not map type parameter', ts.DiagnosticCategory.Error); + this.addTsNodeDiagnostics( + tp.symbol.declarations![0], + 'Could not map type parameter', + ts.DiagnosticCategory.Error + ); return null; } @@ -614,7 +766,7 @@ export default class CSharpEmitterContext { // external union type alias, refer by name if (!tsType.symbol && tsType.aliasSymbol) { let isNullable = false; - for (let t of tsType.types) { + for (const t of tsType.types) { if ((t.flags & ts.TypeFlags.Null) !== 0) { isNullable = true; } else if ((t.flags & ts.TypeFlags.Undefined) !== 0) { @@ -645,17 +797,25 @@ export default class CSharpEmitterContext { } else if (actualType == null) { actualType = t; } else if (actualType != null && actualType.flags !== t.flags) { - let isEmitted = this.isNodeEmitted(parent); + const isEmitted = this.isNodeEmitted(parent); if (isEmitted && t.symbol.name !== 'PromiseLike') { this.addCsNodeDiagnostics( parent, - 'Union type covering multiple types detected, fallback to dynamic', + 'Union type covering multiple types detected, fallback to object', ts.DiagnosticCategory.Warning ); } fallbackToObject = true; - } else { - actualType = t; + } else if (actualType !== t) { + if (t.symbol?.name === 'PromiseLike') { + this.addCsNodeDiagnostics( + parent, + 'Union type with promise detected, ignoring', + ts.DiagnosticCategory.Warning + ); + } else { + actualType = t; + } } } @@ -673,7 +833,7 @@ export default class CSharpEmitterContext { } let type: cs.TypeNode | null; - if (actualType == tsType) { + if (actualType === tsType) { type = { nodeType: cs.SyntaxKind.TypeReference, parent: parent, @@ -692,10 +852,90 @@ export default class CSharpEmitterContext { } as cs.TypeReference; } + private resolveIntersectionType( + parent: cs.Node, + tsType: ts.Type, + typeArguments?: cs.UnresolvedTypeNode[] + ): cs.TypeNode | null { + if (!tsType.isIntersection()) { + return null; + } + + let isNonNullable = false; + let actualType: ts.Type | null = null; + let fallbackToObject = false; + for (let t of tsType.types) { + if (t.isLiteral()) { + t = this.typeChecker.getBaseTypeOfLiteralType(t); + } + + // NonNullable (e.g. type Y = X & {}) + if ((t.flags & ts.TypeFlags.Object) !== 0 && (t as ts.ObjectType).getProperties().length === 0) { + isNonNullable = true; + } else if (actualType == null) { + actualType = t; + } else if (actualType != null && actualType.flags !== t.flags) { + const isEmitted = this.isNodeEmitted(parent); + if (isEmitted && t.symbol.name !== 'PromiseLike') { + this.addCsNodeDiagnostics( + parent, + 'Intersection type covering multiple types detected, fallback to object', + ts.DiagnosticCategory.Warning + ); + } + fallbackToObject = true; + } else if (actualType !== t) { + if (t.symbol?.name === 'PromiseLike') { + this.addCsNodeDiagnostics( + parent, + 'Intersection type with promise detected, ignoring', + ts.DiagnosticCategory.Warning + ); + } else { + actualType = t; + } + } + } + + if (fallbackToObject) { + return { + nodeType: cs.SyntaxKind.PrimitiveTypeNode, + parent: parent, + type: cs.PrimitiveType.Object, + isNullable: isNonNullable + } as cs.PrimitiveTypeNode; + } + + if (!actualType) { + return null; + } + + let type: cs.TypeNode | null; + if (actualType === tsType) { + type = { + nodeType: cs.SyntaxKind.TypeReference, + parent: parent, + reference: tsType.symbol.name, + isNullable: isNonNullable + } as cs.TypeReference; + } else { + type = this.getTypeFromTsType(parent, actualType, undefined, typeArguments); + } + + return { + nodeType: cs.SyntaxKind.TypeReference, + parent: parent, + reference: type, + isNullable: isNonNullable + } as cs.TypeReference; + } + private isNodeEmitted(node: cs.Node): boolean { if ('skipEmit' in node && (node.skipEmit as boolean)) { return false; - } else if (node.parent) { + } + + if (node.parent) { return this.isNodeEmitted(node.parent); } return true; @@ -734,7 +974,26 @@ export default class CSharpEmitterContext { // raw object without symbol -> dynamic if ((tsType.flags & ts.TypeFlags.Object) !== 0 && !tsType.symbol) { - return handleNullablePrimitive(cs.PrimitiveType.Dynamic); + if (this.typeChecker.isTupleType(tsType)) { + // Note: named tuples here if we start using it + + // Array style tuples: [unknown,unknown] + return this.makeArrayTupleType(parent, this.typeChecker.getTypeArguments(tsType as ts.TypeReference)); + } + + this.addCsNodeDiagnostics( + parent, + `Could not translate type ${this.typeChecker.typeToString(tsType)}, fallback to object`, + ts.DiagnosticCategory.Warning + ); + return handleNullablePrimitive(cs.PrimitiveType.Object); + } + + // undefined -> nullable object + if ((tsType.flags & ts.TypeFlags.Undefined) === ts.TypeFlags.Undefined) { + const undefinedType = handleNullablePrimitive(cs.PrimitiveType.Object); + undefinedType.isNullable = true; + return undefinedType; } // any -> dynamic @@ -784,18 +1043,28 @@ export default class CSharpEmitterContext { } as cs.PrimitiveTypeNode; } - // never -> dynamic - // (actually it would be void for usages on return values, but we rather have it as array element type on empty arrays) + // never -> void if ((tsType.flags & ts.TypeFlags.Never) !== 0) { return { nodeType: cs.SyntaxKind.PrimitiveTypeNode, - type: cs.PrimitiveType.Dynamic + type: cs.PrimitiveType.Void } as cs.PrimitiveTypeNode; } return null; } + public makeArrayTupleType(parent: cs.Node, typeArguments: readonly ts.Type[]): cs.ArrayTupleNode { + const ref = { + parent, + nodeType: cs.SyntaxKind.ArrayTupleNode + } as cs.ArrayTupleNode; + + ref.types = typeArguments.map(x => this.getTypeFromTsType(ref, x)!); + + return ref; + } + private resolveKnownTypeSymbol( node: cs.Node, tsType: ts.Type, @@ -813,7 +1082,7 @@ export default class CSharpEmitterContext { if (typeArguments) { reference.typeArguments = []; - typeArguments.forEach(a => { + for (const a of typeArguments) { const parameterType = this.resolveType(a); if (!parameterType) { this.addTsNodeDiagnostics( @@ -824,12 +1093,12 @@ export default class CSharpEmitterContext { } else { reference.typeArguments!.push(parameterType); } - }); + } } else { const tsTypeArguments = (tsType as ts.TypeReference).typeArguments; if (tsTypeArguments) { reference.typeArguments = []; - tsTypeArguments.forEach(a => { + for (const a of tsTypeArguments) { const parameterType = this.getTypeFromTsType(node, a); if (!parameterType) { this.addTsNodeDiagnostics( @@ -840,12 +1109,14 @@ export default class CSharpEmitterContext { } else { reference.typeArguments!.push(parameterType); } - }); + } } } return reference; - } else if (tsType.isTypeParameter()) { + } + + if (tsType.isTypeParameter()) { return { nodeType: cs.SyntaxKind.TypeReference, parent: node.parent, @@ -857,8 +1128,21 @@ export default class CSharpEmitterContext { return null; } - public makeExceptionType(): cs.TypeReferenceType { - return this.makeTypeName('system.Exception'); + public makeExceptionType(): string { + // global alias + return this.makeTypeName('Error'); + } + + public makeIterableType(): string { + return this.makeTypeName('System.Collections.Generic.IEnumerable'); + } + + public makeIteratorType(): string { + return this.makeTypeName('System.Collections.Generic.IEnumerator'); + } + + public makeGeneratorType(): string { + return this.makeTypeName('System.Collections.Generic.IEnumerator'); } public makeTypeName(tsName: string): string { @@ -877,34 +1161,40 @@ export default class CSharpEmitterContext { return result; } - protected buildCoreNamespace(aliasSymbol: ts.Symbol) { + protected buildCoreNamespace(aliasSymbol?: ts.Symbol) { let suffix = ''; - if (aliasSymbol.name === 'Map') { - return this.toPascalCase('alphaTab.collections') + suffix + '.'; - } + if (aliasSymbol) { + if (aliasSymbol.name === 'Map') { + return `${this.toPascalCase('alphaTab.collections') + suffix}.`; + } - if (aliasSymbol.declarations) { - for (const decl of aliasSymbol.declarations) { - let fileName = path.basename(decl.getSourceFile().fileName).toLowerCase(); - if (fileName.startsWith('lib.') && fileName.endsWith('.d.ts')) { - fileName = fileName.substring(4, fileName.length - 5); - if (fileName.length) { - suffix = fileName.split('.').map(s => { - if (s.match(/es[0-9]{4}/)) { - return '.' + this.toPascalCase('ecmaScript'); - } - if (s.match(/es[0-9]{1}/)) { - return '.' + this.toPascalCase('ecmaScript'); - } - return '.' + this.toPascalCase(s); - })[0]; + if (aliasSymbol.name === 'Error') { + return ''; + } + + if (aliasSymbol.declarations) { + for (const decl of aliasSymbol.declarations) { + let fileName = path.basename(decl.getSourceFile().fileName).toLowerCase(); + if (fileName.startsWith('lib.') && fileName.endsWith('.d.ts')) { + fileName = fileName.substring(4, fileName.length - 5); + if (fileName.length) { + suffix = fileName.split('.').map(s => { + if (s.match(/es[0-9]{4}/)) { + return `.${this.toPascalCase('ecmaScript')}`; + } + if (s.match(/es[0-9]{1}/)) { + return `.${this.toPascalCase('ecmaScript')}`; + } + return `.${this.toPascalCase(s)}`; + })[0]; + } } } } } - return this.toPascalCase('alphaTab.core') + suffix + '.'; + return `${this.toPascalCase('alphaTab.core') + suffix}.`; } protected toCoreTypeName(s: string) { if (s === 'Map') { @@ -954,6 +1244,11 @@ export default class CSharpEmitterContext { } } + public resolveSymbol(symbol: ts.Symbol): (cs.NamedElement & cs.Node) | undefined { + const symbolKey = this.getSymbolKey(symbol); + return this._symbolLookup.get(symbolKey); + } + public isConst(declaration: cs.FieldDeclaration) { const symbolKey = this.getSymbolKey(declaration.tsSymbol!); return this._symbolConst.has(symbolKey); @@ -976,14 +1271,13 @@ export default class CSharpEmitterContext { const declaration = symbol.valueDeclaration ? symbol.valueDeclaration : symbol.declarations && symbol.declarations.length > 0 - ? symbol.declarations[0] - : undefined; + ? symbol.declarations[0] + : undefined; if (declaration) { - return symbol.name + '_' + declaration.getSourceFile().fileName + '_' + declaration.pos; - } else { - return symbol.name; + return `${symbol.name}_${declaration.getSourceFile().fileName}_${declaration.pos}`; } + return symbol.name; } public getSymbolForDeclaration(node: ts.Node): ts.Symbol | undefined { @@ -997,6 +1291,20 @@ export default class CSharpEmitterContext { return symbol; } + public getMethodNameFromSymbol(symbol: ts.Symbol): string { + const parent = 'parent' in symbol ? (symbol.parent as ts.Symbol) : undefined; + + if (symbol.name === 'dispose' && (!parent || parent.name === 'SymbolConstructor')) { + return symbol.name; + } + + if (symbol.name === 'iterator' && (!parent || parent.name === 'SymbolConstructor')) { + return this.toMethodName('getEnumerator'); + } + + return ''; + } + public isUnknownSmartCast(expression: ts.Expression) { const smartCastType = this.getSmartCastType(expression); return ( @@ -1018,7 +1326,9 @@ export default class CSharpEmitterContext { } return false; - } else if (type.symbol != null && type.symbol.name === 'Iterable') { + } + + if (type.symbol != null && type.symbol.name === 'Iterable') { return true; } @@ -1113,14 +1423,14 @@ export default class CSharpEmitterContext { } // declared type must be nullable - let declaredType = this.typeChecker.getTypeAtLocation(declarations[0]); + const declaredType = this.typeChecker.getTypeAtLocation(declarations[0]); if (!this.isNullableType(declaredType)) { return undefined; } // actual type at location must be non nullable - let declaredTypeNonNull = this.typeChecker.getNonNullableType(declaredType); - let contextualType = this.typeChecker.getTypeOfSymbolAtLocation(symbol, expression); + const declaredTypeNonNull = this.typeChecker.getNonNullableType(declaredType); + const contextualType = this.typeChecker.getTypeOfSymbolAtLocation(symbol, expression); if (!contextualType || this.isNullableType(contextualType)) { return undefined; } @@ -1150,7 +1460,7 @@ export default class CSharpEmitterContext { return false; } - let contextualType = this.typeChecker.getContextualType(expression); + const contextualType = this.typeChecker.getTypeAtLocation(expression); if (!contextualType) { return false; } @@ -1170,21 +1480,20 @@ export default class CSharpEmitterContext { symbol = this.typeChecker.getAliasedSymbol(symbol); } - if ( - symbol.flags & ts.SymbolFlags.Interface || - symbol.flags & ts.SymbolFlags.Class || - symbol.flags & ts.SymbolFlags.BlockScopedVariable || - symbol.flags & ts.SymbolFlags.FunctionScopedVariable - ) { + if (symbol.flags & ts.SymbolFlags.Interface || symbol.flags & ts.SymbolFlags.Class) { return false; } - let declaredType = this.typeChecker.getTypeAtLocation(declarations[0]); + const declaredType = this.typeChecker.getTypeAtLocation(declarations[0]); if (!this.isNullableType(declaredType)) { return false; } - return contextualType == this.typeChecker.getNonNullableType(declaredType); + return ( + this.typeChecker.getNonNullableType(declaredType) === this.typeChecker.getNonNullableType(contextualType) && + this.isNullableType(declaredType) && + !this.isNullableType(contextualType) + ); } public getSmartCastType(expression: ts.Expression): ts.Type | null { @@ -1193,6 +1502,16 @@ export default class CSharpEmitterContext { return null; } + // no smartcast on assignments + if ( + ts.isBinaryExpression(expression.parent) && + (expression.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken || + expression.parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken) && + expression.parent.left === expression + ) { + return null; + } + if ( expression.parent.kind === ts.SyntaxKind.NonNullExpression && expression.parent.parent.kind === ts.SyntaxKind.AsExpression @@ -1210,7 +1529,7 @@ export default class CSharpEmitterContext { let symbol = this.typeChecker.getSymbolAtLocation(expression); if (!symbol) { // smartcast to unknown? - let contextualType = this.typeChecker.getContextualType(expression); + const contextualType = this.typeChecker.getContextualType(expression); if ( contextualType && ((contextualType.flags & ts.TypeFlags.Any) !== 0 || (contextualType.flags & ts.TypeFlags.Unknown) !== 0) @@ -1243,7 +1562,7 @@ export default class CSharpEmitterContext { let declaredType = this.typeChecker.getTypeAtLocation(declarations[0]); - let contextualTypeNullable = contextualType; + const contextualTypeNullable = contextualType; contextualType = this.typeChecker.getNonNullableType(contextualType); declaredType = this.typeChecker.getNonNullableType(declaredType); @@ -1290,12 +1609,13 @@ export default class CSharpEmitterContext { // enum literal to same enum type if ( contextualType.flags & ts.TypeFlags.EnumLiteral && - (declaredType.symbol as any)?.parent == contextualType.symbol + (declaredType.symbol as any)?.parent === contextualType.symbol ) { return null; } - return contextualType !== declaredType && !this.isTypeAssignable(contextualType, declaredType) + return contextualType !== declaredType && + !this.isTypeAssignable(contextualType, contextualTypeNullable, declaredType) ? contextualTypeNullable : null; } @@ -1349,28 +1669,18 @@ export default class CSharpEmitterContext { this._virtualSymbols.set(key, true); } - public markOverride(classElement: ts.ClassElement): boolean { + public markOverride(classElement: ts.ClassElement): (ts.ClassElement | ts.TypeElement)[] { let parent: ts.Node = classElement; while (parent.kind !== ts.SyntaxKind.ClassDeclaration) { if (parent.parent) { parent = parent.parent; } else { - return false; + return []; } } - let classDecl = parent as ts.ClassDeclaration; - let classSymbol = this.typeChecker.getSymbolAtLocation(classDecl.name!); - if (!classSymbol) { - return false; - } - - let classType = this.typeChecker.getDeclaredTypeOfSymbol(classSymbol); - if (!classType || !classType.isClass()) { - return false; - } - - const overridden = this.getOverriddenMembers(classType, classElement); + const classDecl = parent as ts.ClassDeclaration; + const overridden = this.getOverriddenMembers(classDecl, classElement); if (overridden.length > 0) { const member = this.typeChecker.getSymbolAtLocation(classElement) ?? @@ -1378,37 +1688,48 @@ export default class CSharpEmitterContext { this._virtualSymbols.set(this.getSymbolKey(member), true); for (const s of overridden) { - const symbolKey = this.getSymbolKey(s); + const overriddenMember = + this.typeChecker.getSymbolAtLocation(s) ?? this.typeChecker.getSymbolAtLocation(s.name!); + const symbolKey = this.getSymbolKey(overriddenMember); this._virtualSymbols.set(symbolKey, true); } } - return overridden.length > 0; + return overridden; } - protected getOverriddenMembers(classType: ts.InterfaceType, classElement: ts.ClassElement): ts.Symbol[] { - const symbols: ts.Symbol[] = []; - this.collectOverriddenMembersByName(symbols, classType, classElement.name!.getText(), false, false); - return symbols; + protected getOverriddenMembers( + classType: ts.ClassDeclaration | ts.InterfaceDeclaration, + classElement: ts.ClassElement + ): (ts.ClassElement | ts.TypeElement)[] { + const overriddenItems: (ts.ClassElement | ts.TypeElement)[] = []; + this.collectOverriddenMembersByName(overriddenItems, classType, classElement.name!.getText(), false, false); + return overriddenItems; } protected collectOverriddenMembersByName( - symbols: ts.Symbol[], - classType: ts.InterfaceType, + overriddenItems: (ts.ClassElement | ts.TypeElement)[], + classType: ts.ClassDeclaration | ts.InterfaceDeclaration, memberName: string, includeOwnMembers: boolean = false, allowInterfaces: boolean = false ) { - const member = classType.symbol?.members?.get(ts.escapeLeadingUnderscores(memberName)); + const member = classType.members.find(m => m.name?.getText() === memberName); if (includeOwnMembers && member) { - symbols.push(member); + overriddenItems.push(member); } - const baseTypes = classType.getBaseTypes(); - if (baseTypes) { - for (const baseType of baseTypes) { - if ((allowInterfaces && baseType.isClassOrInterface()) || baseType.isClass()) { - this.collectOverriddenMembersByName(symbols, baseType, memberName, true, allowInterfaces); + if (classType.heritageClauses) { + for (const implementsClause of classType.heritageClauses) { + for (const typeSyntax of implementsClause.types) { + const declarations = this.typeChecker.getTypeFromTypeNode(typeSyntax)?.symbol.declarations; + if (declarations) { + for (const decl of declarations) { + if (ts.isClassDeclaration(decl) || (allowInterfaces && ts.isInterfaceDeclaration(decl))) { + this.collectOverriddenMembersByName(overriddenItems, decl, memberName, true, true); + } + } + } } } } @@ -1427,6 +1748,8 @@ export default class CSharpEmitterContext { tsType = this.typeChecker.getTypeAtLocation(expression); } + tsType = this.typeChecker.getNonNullableType(tsType); + return this.isValueType(tsType); } @@ -1439,10 +1762,37 @@ export default class CSharpEmitterContext { return true; } + return this.isEnum(tsType) || this.typeChecker.isTupleType(tsType); + } + + public isEnum(tsType: ts.Type) { // enums if (tsType.symbol && tsType.symbol.flags & ts.SymbolFlags.Enum) { return true; } + if (tsType.symbol && tsType.symbol.flags & ts.SymbolFlags.EnumMember) { + return true; + } + if (tsType.flags & ts.TypeFlags.EnumLiteral) { + return true; + } + + // enums disguised as union + if (tsType.isUnion() && (tsType as ts.UnionType).types.length > 0) { + let isEnum = true; + for (const t of (tsType as ts.UnionType).types) { + if ( + !t.symbol || + (!(t.symbol.flags & ts.SymbolFlags.Enum) && !(t.symbol.flags & ts.SymbolFlags.EnumMember)) + ) { + isEnum = false; + break; + } + } + if (isEnum) { + return true; + } + } return false; } @@ -1515,11 +1865,11 @@ export default class CSharpEmitterContext { switch (node.nodeType) { case cs.SyntaxKind.ClassDeclaration: const csClass = node as cs.ClassDeclaration; - csClass.members.forEach(m => { + for (const m of csClass.members) { if (this.makeVirtual(m, visited)) { hasVirtualMember = true; } - }); + } let baseClass = csClass.baseClass; while (baseClass != null) { @@ -1585,14 +1935,16 @@ export default class CSharpEmitterContext { } if (csClass.interfaces) { - csClass.interfaces.forEach(i => this.makePublic(i, visited)); + for (const i of csClass.interfaces) { + this.makePublic(i, visited); + } } - csClass.members.forEach(m => { - if (m.visibility == cs.Visibility.Public || m.visibility == cs.Visibility.Protected) { + for (const m of csClass.members) { + if (m.visibility === cs.Visibility.Public || m.visibility === cs.Visibility.Protected) { this.makePublic(m, visited); } - }); + } break; case cs.SyntaxKind.EnumDeclaration: const csEnum = node as cs.EnumDeclaration; @@ -1603,22 +1955,28 @@ export default class CSharpEmitterContext { csInterface.visibility = cs.Visibility.Public; if (csInterface.interfaces) { - csInterface.interfaces.forEach(i => this.makePublic(i, visited)); + for (const i of csInterface.interfaces) { + this.makePublic(i, visited); + } } - csInterface.members.forEach(m => { + for (const m of csInterface.members) { this.makePublic(m, visited); - }); + } break; case cs.SyntaxKind.ConstructorDeclaration: const csConstructor = node as cs.ConstructorDeclaration; - csConstructor.parameters.forEach(p => this.makePublic(p, visited)); + for (const p of csConstructor.parameters) { + this.makePublic(p, visited); + } break; case cs.SyntaxKind.MethodDeclaration: const csMethod = node as cs.MethodDeclaration; - csMethod.parameters.forEach(p => this.makePublic(p, visited)); + for (const p of csMethod.parameters) { + this.makePublic(p, visited); + } this.makePublic(csMethod.returnType, visited); break; case cs.SyntaxKind.PropertyDeclaration: @@ -1634,7 +1992,9 @@ export default class CSharpEmitterContext { case cs.SyntaxKind.TypeReference: const csTypeRef = node as cs.TypeReference; if (csTypeRef.typeArguments) { - csTypeRef.typeArguments.forEach(r => this.makePublic(r, visited)); + for (const r of csTypeRef.typeArguments) { + this.makePublic(r, visited); + } } if (typeof csTypeRef.reference !== 'string') { this.makePublic(csTypeRef.reference, visited); @@ -1649,8 +2009,12 @@ export default class CSharpEmitterContext { break; case cs.SyntaxKind.MapTypeNode: const mapType = node as cs.MapTypeNode; - this.makePublic(mapType.keyType, visited); - this.makePublic(mapType.valueType, visited); + if (mapType.keyType) { + this.makePublic(mapType.keyType, visited); + } + if (mapType.valueType) { + this.makePublic(mapType.valueType, visited); + } break; } } @@ -1675,6 +2039,36 @@ export default class CSharpEmitterContext { } public getDefaultUsings(): string[] { - return [this.toPascalCase('system'), this.toPascalCase('alphaTab') + '.' + this.toPascalCase('core')]; + return [this.toPascalCase('system'), `${this.toPascalCase('alphaTab')}.${this.toPascalCase('core')}`]; + } + + public buildMethodName(propertyName: ts.PropertyName) { + let methodName: string = ''; + if (ts.isIdentifier(propertyName)) { + methodName = propertyName.text; + } else if (ts.isComputedPropertyName(propertyName)) { + if (ts.isPropertyAccessExpression(propertyName.expression)) { + const symbol = this.getSymbolForDeclaration(propertyName.expression); + if (symbol) { + methodName = this.getMethodNameFromSymbol(symbol); + } + } else if (ts.isStringLiteral(propertyName)) { + methodName = (propertyName as ts.StringLiteral).text; + } else { + methodName = ``; + this.addTsNodeDiagnostics(propertyName, 'Unsupported method name syntax', ts.DiagnosticCategory.Error); + } + } else if (ts.isStringLiteral(propertyName)) { + methodName = propertyName.text; + } else if (ts.isPrivateIdentifier(propertyName)) { + methodName = propertyName.text.substring(1); + } + + if (!methodName) { + methodName = ``; + this.addTsNodeDiagnostics(propertyName, 'Unsupported method name syntax', ts.DiagnosticCategory.Error); + } + + return this.toMethodName(methodName); } } diff --git a/src.compiler/csharp/CSharpTranspiler.ts b/src.compiler/csharp/CSharpTranspiler.ts index 949f6f3f8..5e6b714c6 100644 --- a/src.compiler/csharp/CSharpTranspiler.ts +++ b/src.compiler/csharp/CSharpTranspiler.ts @@ -1,7 +1,12 @@ import emit from './CSharpEmitter'; -import transpiler from '../TranspilerBase' +import transpiler from '../TranspilerBase'; -transpiler([{ - name: 'C#', - emit: emit -}], true); \ No newline at end of file +transpiler( + [ + { + name: 'C#', + emit: emit + } + ], + true +); diff --git a/src.compiler/kotlin/KotlinAstPrinter.ts b/src.compiler/kotlin/KotlinAstPrinter.ts index 0e40b1a26..5b93ffcdd 100644 --- a/src.compiler/kotlin/KotlinAstPrinter.ts +++ b/src.compiler/kotlin/KotlinAstPrinter.ts @@ -1,15 +1,19 @@ import * as cs from '../csharp/CSharpAst'; import * as ts from 'typescript'; -import CSharpEmitterContext from '../csharp/CSharpEmitterContext'; import AstPrinterBase from '../AstPrinterBase'; +import type KotlinEmitterContext from './KotlinEmitterContext'; export default class KotlinAstPrinter extends AstPrinterBase { private _forceInteger: boolean = false; private _returnRunTest: boolean[] = []; + private _thisScope: string[] = []; private _useScopes: number[] = []; - public constructor(sourceFile: cs.SourceFile, context: CSharpEmitterContext) { + protected override _context: KotlinEmitterContext; + + public constructor(sourceFile: cs.SourceFile, context: KotlinEmitterContext) { super(sourceFile, context); + this._context = context; } private keywords: Set = new Set([ @@ -78,7 +82,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { protected override escapeIdentifier(identifier: string): string { if (this.keywords.has(identifier)) { - return '`' + identifier + '`'; + return `\`${identifier}\``; } return identifier; } @@ -188,9 +192,9 @@ export default class KotlinAstPrinter extends AstPrinterBase { if (p.type) { this.write(': '); if (p.params) { - this.writeType((p.type as cs.ArrayTypeNode).elementType, false, false, false, true, false); + this.writeType((p.type as cs.ArrayTypeNode).elementType, false, false, false, true); } else { - this.writeType(p.type, false, false, false, true, false); + this.writeType(p.type, false, false, false, true); } } if (!this.isOverrideMethod(p.parent!)) { @@ -229,7 +233,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { return false; } - let method = parent as cs.MethodDeclaration; + const method = parent as cs.MethodDeclaration; return method.isOverride; } @@ -239,7 +243,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeLine('@kotlin.contracts.ExperimentalContracts'); this.writeLine('@kotlin.ExperimentalUnsignedTypes'); this.writeVisibility(d.visibility); - this.write(`interface `); + this.write('interface '); this.writeIdentifier(d.name); this.writeTypeParameters(d.typeParameters); @@ -251,8 +255,9 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeTypeParameterConstraints(d.typeParameters); this.writeLine(); this.beginBlock(); - - d.members.forEach(m => this.writeMember(m)); + for (const m of d.members) { + this.writeMember(m); + } this.endBlock(); } @@ -261,16 +266,16 @@ export default class KotlinAstPrinter extends AstPrinterBase { this._forceInteger = true; this.writeDocumentation(d); this.writeVisibility(d.visibility); - this.write(`enum class `); + this.write('enum class '); this.writeIdentifier(d.name); - this.write(`(override val value: Int): alphaTab.core.IAlphaTabEnum`); + this.write('(override val value: Int): alphaTab.core.IAlphaTabEnum'); this.writeLine(); this.beginBlock(); let currentEnumValue = 0; for (let i = 0; i < d.members.length; i++) { - let m = d.members[i]; + const m = d.members[i]; this.writeDocumentation(m); this.writeIdentifier(m.name); if (m.initializer) { @@ -295,7 +300,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.write('companion object'); this.beginBlock(); - this.write(`public fun fromValue(v:Double): `); + this.write('public fun fromValue(v:Double): '); this.writeIdentifier(d.name); this.beginBlock(); this.write('return when(v.toInt())'); @@ -333,6 +338,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.write('class '); this.writeIdentifier(d.name); + this.writeTypeParameters(d.typeParameters); if (d.baseClass) { @@ -356,8 +362,8 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.beginBlock(); let hasConstuctor = false; - let statics: cs.ClassMember[] = []; - d.members.forEach(m => { + const statics: cs.ClassMember[] = []; + for (const m of d.members) { if ('isStatic' in m && m.isStatic) { statics.push(m); } else { @@ -366,15 +372,15 @@ export default class KotlinAstPrinter extends AstPrinterBase { hasConstuctor = true; } } - }); + } if (statics.length > 0) { this.write('companion object'); this.beginBlock(); - statics.forEach(s => { + for (const s of statics) { this.writeMember(s); - }); + } this.endBlock(); } @@ -386,7 +392,8 @@ export default class KotlinAstPrinter extends AstPrinterBase { if (typeof baseClass === 'string') { constructorDeclaration = undefined; break; - } else if (cs.isClassDeclaration(baseClass)) { + } + if (cs.isClassDeclaration(baseClass)) { constructorDeclaration = baseClass.members.find(m => cs.isConstructorDeclaration(m) ) as cs.ConstructorDeclaration; @@ -430,14 +437,20 @@ export default class KotlinAstPrinter extends AstPrinterBase { nodeType: cs.SyntaxKind.Identifier, text: p.name, tsNode: defaultConstructor.tsNode - } as cs.Identifier) + }) as cs.Identifier ); this.writeMember(defaultConstructor); } else { this.writeLine('public constructor()'); + if (d.baseClass) { + this.writeLine(': super()'); + } } } else if (!hasConstuctor) { this.writeLine('public constructor()'); + if (d.baseClass) { + this.writeLine(': super()'); + } } this.endBlock(); @@ -463,6 +476,9 @@ export default class KotlinAstPrinter extends AstPrinterBase { if (d.isStatic) { this.writeLine('@kotlin.jvm.JvmStatic'); } + if (d.isTestMethod) { + this.writeLine('@Test'); + } this.writeVisibility(d.visibility); if (d.isAbstract) { @@ -480,7 +496,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.write('suspend '); } - this.write(`fun `); + this.write('fun '); this.writeTypeParameters(d.typeParameters); this.writeIdentifier(d.name); this.writeParameters(d.parameters); @@ -493,10 +509,26 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeTypeParameterConstraints(d.typeParameters); } + if (d.isGeneratorFunction) { + this.write(' = iterator'); + this._thisScope.push((d.parent as cs.NamedTypeDeclaration).name); + } this.writeBody(d.body); + if (d.isGeneratorFunction) { + this._thisScope.pop(); + } + this._returnRunTest.pop(); } + protected writeThisLiteral(expr: cs.ThisLiteral): void { + super.writeThisLiteral(expr); + const scope = this._thisScope.at(-1); + if (scope) { + this.write(`@${scope}`); + } + } + protected writeBody(body: cs.Expression | cs.Block | undefined) { if (body) { if (cs.isBlock(body)) { @@ -517,7 +549,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { if (d.isStatic) { this.write('init '); } else { - this.write(`constructor`); + this.write('constructor'); this.writeParameters(d.parameters); if (d.baseConstructorArguments) { this.writeLine(); @@ -582,6 +614,9 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.write(': '); this.writeType(d.type); + const needsInitializer = + isAutoProperty && d.type.isNullable && d.parent!.nodeType !== cs.SyntaxKind.InterfaceDeclaration; + let initializerWritten = false; if (d.initializer && !isLateInit) { this.write(' = '); @@ -589,22 +624,23 @@ export default class KotlinAstPrinter extends AstPrinterBase { initializerWritten = true; this.writeLine(); } else if (writeAsField) { + if (needsInitializer && !initializerWritten) { + this.write(' = null'); + } this.writeLine(); } if (!writeAsField) { - if(isAutoProperty && d.type.isNullable && !initializerWritten && - d.parent!.nodeType !== cs.SyntaxKind.InterfaceDeclaration - ) { + if (needsInitializer && !initializerWritten) { this.write(' = null'); } this.writeLine(); - if(!isAutoProperty) { + if (!isAutoProperty) { if (d.getAccessor) { this.writePropertyAccessor(d.getAccessor); } - + if (d.setAccessor) { this.writePropertyAccessor(d.setAccessor); } @@ -671,8 +707,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { forNew: boolean = false, asNativeArray: boolean = false, forTypeConstraint: boolean = false, - allowPromise: boolean = true, - optionalAsNullable: boolean = true + allowPromise: boolean = true ) { switch (type.nodeType) { case cs.SyntaxKind.PrimitiveTypeNode: @@ -681,10 +716,9 @@ export default class KotlinAstPrinter extends AstPrinterBase { case cs.PrimitiveType.Bool: case cs.PrimitiveType.Int: case cs.PrimitiveType.Double: - this.write('struct'); + this.write('IAlphaTabEnum'); break; case cs.PrimitiveType.Object: - case cs.PrimitiveType.Dynamic: case cs.PrimitiveType.String: case cs.PrimitiveType.Void: this.write('class'); @@ -695,9 +729,6 @@ export default class KotlinAstPrinter extends AstPrinterBase { case cs.PrimitiveType.Bool: this.write('Boolean'); break; - case cs.PrimitiveType.Dynamic: - this.write('Any'); - break; case cs.PrimitiveType.Double: this.write(this._forceInteger ? 'Int' : 'Double'); break; @@ -730,58 +761,113 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeType(arrayType.elementType); this.write('>'); } else { - var elementTypeName = this.getContainerTypeName(arrayType.elementType); + const elementTypeName = this.getContainerTypeName(arrayType.elementType); if (elementTypeName) { this.write('alphaTab.collections.'); this.write(elementTypeName); this.write('List'); } else { - const isDynamicArray = - cs.isPrimitiveTypeNode(arrayType.elementType) && - arrayType.elementType.type === cs.PrimitiveType.Dynamic; - if (isDynamicArray && !forNew) { - this.write('alphaTab.collections.List<*>'); - } else { - if (forNew) { - this.write('alphaTab.collections.List<'); - } else { - this.write('alphaTab.collections.List<'); - } - this.writeType(arrayType.elementType); - this.write('>'); - } + this.write('alphaTab.collections.List<'); + this.writeType(arrayType.elementType); + this.write('>'); } } break; case cs.SyntaxKind.MapTypeNode: const mapType = type as cs.MapTypeNode; + if (!mapType.keyType && !mapType.valueType) { + this.write('alphaTab.collections.MapBase<*,*>'); + } else { + const keyTypeName = this.getContainerTypeName(mapType.keyType!); + const valueTypeName = this.getContainerTypeName(mapType.valueType!); + + this.write('alphaTab.collections.'); + if (keyTypeName && valueTypeName) { + this.write(keyTypeName); + this.write(valueTypeName); + this.write('Map'); + } else if (keyTypeName) { + this.write(keyTypeName); + this.write('ObjectMap<'); + this.writeType(mapType.valueType!); + this.write('>'); + } else if (valueTypeName) { + this.write('Object'); + this.write(valueTypeName); + this.write('Map<'); + this.writeType(mapType.keyType!); + this.write('>'); + } else { + this.write('Map<'); + this.writeType(mapType.keyType!); + this.write(', '); + this.writeType(mapType.valueType!); + this.write('>'); + } + } + break; + case cs.SyntaxKind.ArrayTupleNode: + const arrayTupleType = type as cs.ArrayTupleNode; - var keyTypeName = this.getContainerTypeName(mapType.keyType); - var valueTypeName = this.getContainerTypeName(mapType.valueType); - - this.write('alphaTab.collections.'); - if (keyTypeName && valueTypeName) { - this.write(keyTypeName); - this.write(valueTypeName); - this.write('Map'); - } else if (keyTypeName) { - this.write(keyTypeName); - this.write('ObjectMap<'); - this.writeType(mapType.valueType); - this.write('>'); - } else if (valueTypeName) { - this.write('Object'); - this.write(valueTypeName); - this.write('Map<'); - this.writeType(mapType.keyType); - this.write('>'); + if (arrayTupleType.types.length > 2) { + if (forNew) { + this.write('alphaTab.core.ArrayTuple'); + } else { + this.write('alphaTab.core.IArrayTuple'); + } + this.write(arrayTupleType.types.length.toString()); + this.write('<'); + this.writeCommaSeparated(arrayTupleType.types, p => this.writeType(p)); } else { - this.write('Map<'); - this.writeType(mapType.keyType); - this.write(', '); - this.writeType(mapType.valueType); - this.write('>'); + let arrayTupleName = ''; + const newTypeArgs: cs.TypeNode[] = []; + for (const arg of arrayTupleType.types) { + let itemType: cs.TypeReferenceType = arg; + while (typeof itemType !== 'string' && cs.isTypeReference(itemType)) { + itemType = itemType.reference; + } + + if (typeof itemType === 'string') { + arrayTupleName += 'Object'; + newTypeArgs.push(arg); + } else if (cs.isPrimitiveTypeNode(itemType)) { + switch (itemType.type) { + case cs.PrimitiveType.Bool: + arrayTupleName += 'Boolean'; + break; + case cs.PrimitiveType.Int: + arrayTupleName += 'Int'; + break; + case cs.PrimitiveType.Double: + arrayTupleName += 'Double'; + break; + default: + arrayTupleName += 'Object'; + newTypeArgs.push(arg); + break; + } + } else { + arrayTupleName += 'Object'; + newTypeArgs.push(arg); + } + } + + if (arrayTupleName === 'ObjectObject') { + arrayTupleName = ''; + } + + if (forNew) { + this.write(`alphaTab.core.${arrayTupleName}ArrayTuple`); + } else { + this.write(`alphaTab.core.I${arrayTupleName}ArrayTuple`); + } + + if (newTypeArgs.length > 0) { + this.write('<'); + this.writeCommaSeparated(newTypeArgs, p => this.writeType(p)); + this.write('>'); + } } break; case cs.SyntaxKind.FunctionTypeNode: @@ -814,8 +900,25 @@ export default class KotlinAstPrinter extends AstPrinterBase { if (typeReference.typeArguments && typeReference.typeArguments.length > 0) { this.write('<'); + this.writeCommaSeparated(typeReference.typeArguments, p => this.writeType(p)); + this.write('>'); + } else if (typeReference.tsSymbol) { + // we have to resolve the number of potential type parameters of the type + const expectedParameters = typeReference.tsSymbol.declarations + ?.map(d => { + if (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) { + return d.typeParameters; + } + }) + .find(x => !!x); + + if (expectedParameters) { + this.write('<'); + this.writeCommaSeparated(Array.from(expectedParameters), () => this.write('*')); + this.write('>'); + } } if (typeReference.isAsync && allowPromise) { @@ -835,10 +938,10 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.write(this._context.getFullName((type as cs.EnumMember).parent as cs.NamedTypeDeclaration)); break; default: - this.write('TODO: ' + cs.SyntaxKind[type.nodeType]); + this.write(`TODO: ${cs.SyntaxKind[type.nodeType]}`); break; } - if ((type.isNullable) && !forNew && !forTypeConstraint) { + if (type.isNullable && !forNew && !forTypeConstraint) { this.write('?'); } } @@ -865,8 +968,13 @@ export default class KotlinAstPrinter extends AstPrinterBase { protected writeTypeOfExpression(expr: cs.TypeOfExpression) { if (expr.expression) { this.writeExpression(expr.expression); - this.write('::class'); } + if (expr.type) { + this.writeType(expr.type); + } + + this.write('::class'); + } protected writePrefixUnaryExpression(expr: cs.PrefixUnaryExpression) { @@ -1027,7 +1135,9 @@ export default class KotlinAstPrinter extends AstPrinterBase { protected isIntResultExpression(expr: cs.Expression): boolean { if (cs.isInvocationExpression(expr)) { return this.isIntResultExpression(expr.expression); - } else if (cs.isBinaryExpression(expr)) { + } + + if (cs.isBinaryExpression(expr)) { switch (expr.operator) { case '<<': case '>>': @@ -1037,11 +1147,11 @@ export default class KotlinAstPrinter extends AstPrinterBase { return true; } return false; - } else if (cs.isParenthesizedExpression(expr)) { + } + if (cs.isParenthesizedExpression(expr)) { return this.isIntResultExpression(expr.expression); - } else { - return false; } + return false; } protected writeNumericLiteral(expr: cs.NumericLiteral) { @@ -1054,7 +1164,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { protected writeStringTemplateExpression(expr: cs.StringTemplateExpression) { this.write('"""'); - expr.chunks.forEach(c => { + for (const c of expr.chunks) { if (cs.isStringLiteral(c)) { const escapedText = c.text; this.write(escapedText); @@ -1063,7 +1173,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeExpression(c); this.write(').toTemplate()}'); } - }); + } this.write('"""'); } @@ -1081,7 +1191,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { elementType = expr.type.reference.elementType; } - let type = elementType ? this.getContainerTypeName(elementType) : null; + const type = elementType ? this.getContainerTypeName(elementType) : null; this.write('alphaTab.collections.'); if (type) { this.write(type); @@ -1198,7 +1308,8 @@ export default class KotlinAstPrinter extends AstPrinterBase { if ( expr.tsSymbol?.valueDeclaration && ts.isMethodDeclaration(expr.tsSymbol.valueDeclaration) && - !ts.isCallExpression(expr.tsNode!.parent) + (!ts.isCallExpression(expr.tsNode!.parent) || + (expr.tsNode!.parent as ts.CallExpression).expression !== expr.tsNode) ) { return true; } @@ -1212,7 +1323,9 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeExpression(expr.argumentExpression); this.write(')'); return; - } else if (this.isNullable(expr.expression)) { + } + + if (this.isNullable(expr.expression)) { this.write('!!'); } this.write('['); @@ -1289,7 +1402,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { let hasDefault = false; - for(let i = 0; i < s.caseClauses.length; i++) { + for (let i = 0; i < s.caseClauses.length; i++) { const c = s.caseClauses[i]; if (cs.isDefaultClause(c)) { hasDefault = true; @@ -1298,17 +1411,19 @@ export default class KotlinAstPrinter extends AstPrinterBase { // avoid "value", "value2", else -> // but write directly else -> let hasNonElseStatement = false; - for(let j = i; j < s.caseClauses.length; j++) { + for (let j = i; j < s.caseClauses.length; j++) { const c2 = s.caseClauses[j]; - if(cs.isDefaultClause(c2)) { + if (cs.isDefaultClause(c2)) { break; - } else if((c2 as cs.CaseClause).statements.length > 0) { + } + + if ((c2 as cs.CaseClause).statements.length > 0) { hasNonElseStatement = true; break; } } - if(hasNonElseStatement) { + if (hasNonElseStatement) { this.writeCaseClause(c as cs.CaseClause); } } @@ -1351,14 +1466,18 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeLine(' -> '); this.beginBlock(); - this.getCaseClauseStatements(c.statements).forEach(s => this.writeStatement(s)); + for (const s of this.getCaseClauseStatements(c.statements)) { + this.writeStatement(s); + } this.endBlock(); } protected writeDefaultClause(c: cs.DefaultClause) { this.writeLine('else -> '); this.beginBlock(); - this.getCaseClauseStatements(c.statements).forEach(s => this.writeStatement(s)); + for (const s of this.getCaseClauseStatements(c.statements)) { + this.writeStatement(s); + } this.endBlock(); } @@ -1399,39 +1518,39 @@ export default class KotlinAstPrinter extends AstPrinterBase { protected writeForStatement(s: cs.ForStatement) { this._forLoopIncrementors.push(s.incrementor); - if (/*!this.tryWriteForRange(s)*/ true) { - this.write('if (true) '); - this.beginBlock(); + // if (/*!this.tryWriteForRange(s)*/ true) { + this.write('if (true) '); + this.beginBlock(); - if (s.initializer) { - if (cs.isVariableDeclarationList(s.initializer)) { - this.writeVariableDeclarationList(s.initializer); - } else { - this.writeExpression(s.initializer as cs.Expression); - } + if (s.initializer) { + if (cs.isVariableDeclarationList(s.initializer)) { + this.writeVariableDeclarationList(s.initializer); + } else { + this.writeExpression(s.initializer as cs.Expression); } - this.writeLine(); + } + this.writeLine(); - this.write('while('); - if (s.condition) { - this.writeExpression(s.condition); - } - this.write(')'); - this.beginBlock(); + this.write('while('); + if (s.condition) { + this.writeExpression(s.condition); + } + this.write(')'); + this.beginBlock(); - if (cs.isBlock(s.statement)) { - for (const stmt of s.statement.statements) { - this.writeStatement(stmt); - } - } else { - this._indent++; - this.writeStatement(s.statement); - this._indent--; + if (cs.isBlock(s.statement)) { + for (const stmt of s.statement.statements) { + this.writeStatement(stmt); } - this.writeForIncrementors(this._forLoopIncrementors.at(-1)); - this.endBlock(); - this.endBlock(); + } else { + this._indent++; + this.writeStatement(s.statement); + this._indent--; } + this.writeForIncrementors(this._forLoopIncrementors.at(-1)); + this.endBlock(); + this.endBlock(); + // } this._forLoopIncrementors.pop(); } @@ -1449,7 +1568,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { } private writeBinaryExpressionsAsStatements(e: cs.BinaryExpression) { - if (e.operator == ',') { + if (e.operator === ',') { if (cs.isBinaryExpression(e.left)) { this.writeBinaryExpressionsAsStatements(e.left); } else { @@ -1592,7 +1711,7 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.writeExpression((s.condition as cs.BinaryExpression).right); } else { this.writeExpression((s.condition as cs.BinaryExpression).left); - this.write(' ' + (s.condition as cs.BinaryExpression).operator); + this.write(` ${(s.condition as cs.BinaryExpression).operator}`); this.write(' it'); } @@ -1702,9 +1821,9 @@ export default class KotlinAstPrinter extends AstPrinterBase { } } protected writeVariableDeclarationList(declarationList: cs.VariableDeclarationList) { - declarationList.declarations.forEach(d => { + for (const d of declarationList.declarations) { this.writeVariableDeclaration(d); - }); + } } protected writeVariableDeclaration(d: cs.VariableDeclaration) { this.write('var '); @@ -1742,7 +1861,9 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.write('if (true) '); } this.beginBlock(); - b.statements.forEach(s => this.writeStatement(s)); + for (const s of b.statements) { + this.writeStatement(s); + } this.endBlock(); } @@ -1774,4 +1895,26 @@ export default class KotlinAstPrinter extends AstPrinterBase { this.write('.spread()'); } } + + protected writeLocalFunction(expr: cs.LocalFunctionDeclaration) { + this.write(`fun ${expr.name}`); + this.writeParameters(expr.parameters); + this.write(': '); + this.writeType(expr.returnType); + this.writeBlock(expr.body); + } + + protected writeYieldExpression(expr: cs.YieldExpression) { + if (expr.expression) { + this.write('yield('); + this.writeExpression(expr.expression); + this.write(')'); + } else { + this.write('return'); + } + } + + protected writeDefaultExpression(_: cs.DefaultExpression): void { + this.write('null'); + } } diff --git a/src.compiler/kotlin/KotlinAstTransformer.ts b/src.compiler/kotlin/KotlinAstTransformer.ts index d50f77053..f844ff971 100644 --- a/src.compiler/kotlin/KotlinAstTransformer.ts +++ b/src.compiler/kotlin/KotlinAstTransformer.ts @@ -1,33 +1,47 @@ -import * as ts from 'typescript'; +import ts from 'typescript'; import * as cs from '../csharp/CSharpAst'; -import * as path from 'path'; -import CSharpEmitterContext from '../csharp/CSharpEmitterContext'; +import path from 'node:path'; +import type CSharpEmitterContext from '../csharp/CSharpEmitterContext'; import CSharpAstTransformer from '../csharp/CSharpAstTransformer'; +import type KotlinEmitterContext from './KotlinEmitterContext'; export default class KotlinAstTransformer extends CSharpAstTransformer { - public constructor(typeScript: ts.SourceFile, context: CSharpEmitterContext) { + protected override _context: KotlinEmitterContext; + public constructor(typeScript: ts.SourceFile, context: KotlinEmitterContext) { super(typeScript, context); this._testClassAttribute = ''; - this._testMethodAttribute = 'Test'; + this._testMethodAttribute = 'TestName'; + this._snapshotFileAttribute = 'SnapshotFile'; + + this._context = context; } public override get extension(): string { return '.kt'; } - public override get targetTag(): string { - return 'kotlin'; - } - private _paramReferences: Map[] = []; private _paramsWithAssignment: Set[] = []; private getMethodLocalParameterName(name: string) { - return 'param' + name; + return `param${name}`; + } + + protected override visitMethodSignature( + parent: cs.ClassDeclaration | cs.InterfaceDeclaration, + classElement: ts.MethodSignature + ) { + const csMethod = super.visitMethodSignature(parent, classElement); + + if (ts.getJSDocTags(classElement).find(t => t.tagName.text === 'async')) { + csMethod.isAsync = true; + } + + return csMethod; } protected override buildFileName(fileName: string, context: CSharpEmitterContext): string { - let parts = this.removeExtension(fileName).split(path.sep); + const parts = this.removeExtension(fileName).split(path.sep); if (parts.length > 0) { switch (parts[0]) { case 'src': @@ -68,9 +82,10 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { protected override visitPrefixUnaryExpression(parent: cs.Node, expression: ts.PrefixUnaryExpression) { const pre = super.visitPrefixUnaryExpression(parent, expression); - const preUnwrapped = pre && cs.isCastExpression(pre) - ? (pre.expression as cs.PrefixUnaryExpression) - : (pre as cs.PrefixUnaryExpression); + const preUnwrapped = + pre && cs.isCastExpression(pre) + ? (pre.expression as cs.PrefixUnaryExpression) + : (pre as cs.PrefixUnaryExpression); if (preUnwrapped) { switch (preUnwrapped.operator) { @@ -144,10 +159,10 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { } private injectParametersAsLocal(block: cs.Block) { - let localParams: cs.VariableStatement[] = []; + const localParams: cs.VariableStatement[] = []; - let currentAssignments = this._paramsWithAssignment[this._paramsWithAssignment.length - 1]; - let currentScope = this._paramReferences[this._paramReferences.length - 1]; + const currentAssignments = this._paramsWithAssignment[this._paramsWithAssignment.length - 1]; + const currentScope = this._paramReferences[this._paramReferences.length - 1]; for (const p of currentAssignments) { const renamedP = this.getMethodLocalParameterName(p); @@ -171,7 +186,7 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { isConst: false } as cs.VariableDeclarationList; - let declaration = { + const declaration = { nodeType: cs.SyntaxKind.VariableDeclaration, parent: variableStatement.declarationList, tsNode: block.tsNode, @@ -229,44 +244,70 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { // and await expressions are normal method calls. // this is a problem when we want to use the raw Promise like asyncFunction().then().catch() // The following code wraps the code to a "alphaTab.core.TypeHelper.suspendToDeferred({ asyncFunction() }).then().catch() + + // similarly in reverse cases, when we have suspend function calling a method which returns a Deferred directly (e.g. on async interface methods) + // then we call .await() if (invocation && cs.isInvocationExpression(invocation)) { const returnType = this._context.typeChecker.getTypeAtLocation(expression); const method = this._context.typeChecker.getSymbolAtLocation(expression.expression); - if (returnType?.symbol?.name === "Promise" - && (method as any)?.parent?.name !== 'Promise' - && !ts.isAwaitExpression(expression.parent)) { - const suspendToDeferred = { - nodeType: cs.SyntaxKind.InvocationExpression, - } as cs.InvocationExpression; - - suspendToDeferred.expression = this.makeMemberAccess( - suspendToDeferred, - this._context.makeTypeName('alphaTab.core.TypeHelper'), - this._context.toMethodName('suspendToDeferred') - ); - - suspendToDeferred.arguments = [ - { - nodeType: cs.SyntaxKind.LambdaExpression, - parameters: [] as cs.ParameterDeclaration[], - body: invocation, - parent: suspendToDeferred, - returnType: { - nodeType: cs.SyntaxKind.PrimitiveTypeNode, - type: cs.PrimitiveType.Void - } as cs.PrimitiveTypeNode - } as cs.LambdaExpression - ]; - - return suspendToDeferred; + if (returnType?.symbol?.name === 'Promise' && (method as any)?.parent?.name !== 'Promise') { + const isSuspend = + method?.valueDeclaration && + ((method.valueDeclaration as ts.MethodDeclaration).modifiers?.some( + m => m.kind === ts.SyntaxKind.AsyncKeyword + ) || + ts.getJSDocTags(method.valueDeclaration!).find(t => t.tagName.text === 'async')); + + if (!ts.isAwaitExpression(expression.parent) && isSuspend) { + const suspendToDeferred = { + nodeType: cs.SyntaxKind.InvocationExpression + } as cs.InvocationExpression; + + suspendToDeferred.expression = this.makeMemberAccess( + suspendToDeferred, + this._context.makeTypeName('alphaTab.core.TypeHelper'), + this._context.toMethodName('suspendToDeferred') + ); + + suspendToDeferred.arguments = [ + { + nodeType: cs.SyntaxKind.LambdaExpression, + parameters: [] as cs.ParameterDeclaration[], + body: invocation, + parent: suspendToDeferred, + returnType: { + nodeType: cs.SyntaxKind.PrimitiveTypeNode, + type: cs.PrimitiveType.Void + } as cs.PrimitiveTypeNode + } as cs.LambdaExpression + ]; + + return suspendToDeferred; + } + + if (ts.isAwaitExpression(expression.parent) && !isSuspend) { + const deferredToSuspend = { + nodeType: cs.SyntaxKind.InvocationExpression + } as cs.InvocationExpression; + + deferredToSuspend.expression = { + expression: invocation, + member: 'await', + parent: parent, + nodeType: cs.SyntaxKind.MemberAccessExpression + } as cs.MemberAccessExpression; + + deferredToSuspend.arguments = []; + + return deferredToSuspend; + } } } return invocation; } - protected override visitConstructorDeclaration( parent: cs.ClassDeclaration, classElement: ts.ConstructorDeclaration @@ -310,7 +351,6 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { return func; } - protected override visitTestClassMethod(parent: cs.ClassDeclaration, d: ts.FunctionDeclaration) { this._paramReferences.push(new Map()); this._paramsWithAssignment.push(new Set()); @@ -327,7 +367,6 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { return method; } - protected override visitMethodDeclaration( parent: cs.ClassDeclaration | cs.InterfaceDeclaration, classElement: ts.MethodDeclaration @@ -347,11 +386,26 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { return method; } - protected override getSymbolName( - parentSymbol: ts.Symbol, - symbol: ts.Symbol, - expression: cs.Expression - ): string | null { + protected visitFunctionDeclaration( + parent: cs.Node, + expression: ts.FunctionDeclaration + ): cs.LocalFunctionDeclaration { + this._paramReferences.push(new Map()); + this._paramsWithAssignment.push(new Set()); + + const fun = super.visitFunctionDeclaration(parent, expression); + + if (fun.body) { + this.injectParametersAsLocal(fun.body); + } + + this._paramReferences.pop(); + this._paramsWithAssignment.pop(); + + return fun; + } + + protected override getSymbolName(parentSymbol: ts.Symbol, symbol: ts.Symbol): string | null { switch (parentSymbol.name) { case 'String': switch (symbol.name) { @@ -417,14 +471,16 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { arguments: [] } as cs.InvocationExpression; - let expr = this.visitExpression(call, expression.expression); + const expr = this.visitExpression(call, expression.expression); if (!expr) { return null; } call.arguments.push(expr); return call; - } else if (this.isCastFromEnumToNumber(expression)) { + } + + if (this.isCastFromEnumToNumber(expression)) { const methodAccess = { nodeType: cs.SyntaxKind.MemberAccessExpression, parent: parent, @@ -433,7 +489,7 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { tsNode: expression } as cs.MemberAccessExpression; - let expr = this.visitExpression(methodAccess, expression.expression); + const expr = this.visitExpression(methodAccess, expression.expression); if (!expr) { return null; } @@ -447,90 +503,26 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { } as cs.InvocationExpression; return call; - } else { - return super.visitAsExpression(parent, expression); } + return super.visitAsExpression(parent, expression); } private isCastFromEnumToNumber(expression: ts.AsExpression) { - let targetType = this._context.typeChecker.getTypeFromTypeNode(expression.type); - let nonNullable = this._context.typeChecker.getNonNullableType(targetType); + const targetType = this._context.typeChecker.getTypeFromTypeNode(expression.type); + const nonNullable = this._context.typeChecker.getNonNullableType(targetType); if (nonNullable.flags === ts.TypeFlags.Number) { - let sourceType = this._context.typeChecker.getNonNullableType(this._context.getType(expression.expression)); + const sourceType = this._context.typeChecker.getNonNullableType( + this._context.getType(expression.expression) + ); return sourceType.flags & ts.TypeFlags.Enum || sourceType.flags & ts.TypeFlags.EnumLiteral; } return false; } private isCastToEnum(expression: ts.AsExpression) { - let targetType = this._context.typeChecker.getTypeFromTypeNode(expression.type); + const targetType = this._context.typeChecker.getTypeFromTypeNode(expression.type); return targetType.flags & ts.TypeFlags.Enum || targetType.flags & ts.TypeFlags.EnumLiteral; } - protected override createMapEntry(parent: cs.Node, expression: ts.ArrayLiteralExpression): cs.Expression { - const csExpr = { - parent: parent, - tsNode: expression, - nodeType: cs.SyntaxKind.InvocationExpression, - arguments: [], - expression: {} as cs.Expression - } as cs.InvocationExpression; - - const type: cs.TypeReference = { - nodeType: cs.SyntaxKind.TypeReference, - parent: csExpr, - reference: '', - isAsync: false, - tsNode: expression - }; - - type.reference = 'MapEntry'; - if (expression.elements.length === 2) { - const keyType = this._context.getType(expression.elements[0]); - let keyTypeContainerName = this.getContainerTypeName(keyType); - - const valueType = this._context.getType(expression.elements[1]); - let valueTypeContainerName = this.getContainerTypeName(valueType); - - if (!keyTypeContainerName) { - type.typeArguments = type.typeArguments ?? []; - type.typeArguments.push({ - nodeType: cs.SyntaxKind.TypeReference, - isAsync: false, - parent: type, - reference: this.createUnresolvedTypeNode(type, expression.elements[0], keyType) - } as cs.TypeReference); - } - - if (!valueTypeContainerName) { - type.typeArguments = type.typeArguments ?? []; - type.typeArguments.push({ - nodeType: cs.SyntaxKind.TypeReference, - isAsync: false, - parent: type, - reference: this.createUnresolvedTypeNode(type, expression.elements[1], valueType) - } as cs.TypeReference); - } - - if (keyTypeContainerName || valueTypeContainerName) { - keyTypeContainerName = keyTypeContainerName || 'Object'; - valueTypeContainerName = valueTypeContainerName || 'Object'; - type.reference = keyTypeContainerName + valueTypeContainerName + type.reference; - } - } - - type.reference = this._context.makeTypeName(`alphaTab.collections.${type.reference}`); - csExpr.expression = type; - - expression.elements.forEach(e => { - const ex = this.visitExpression(csExpr, e); - if (ex) { - csExpr.arguments.push(ex); - } - }); - - return csExpr; - } - override visitTestClass(d: ts.CallExpression): void { this._csharpFile.usings.push({ nodeType: cs.SyntaxKind.UsingDeclaration, @@ -540,23 +532,24 @@ export default class KotlinAstTransformer extends CSharpAstTransformer { super.visitTestClass(d); } - private getContainerTypeName(tsType: ts.Type): string | null { - if (this._context.isNullableType(tsType)) { - return null; - } - if ( - (tsType.flags & ts.TypeFlags.Enum) !== 0 || - (tsType.flags & ts.TypeFlags.EnumLike) !== 0 || - (tsType.flags & ts.TypeFlags.EnumLiteral) !== 0 - ) { - return null; - } - if ((tsType.flags & ts.TypeFlags.Number) !== 0 || (tsType.flags & ts.TypeFlags.NumberLiteral) !== 0) { - return 'Double'; + protected override convertPropertyToInvocation(parentSymbol: ts.Symbol, symbol: ts.Symbol): boolean { + switch (parentSymbol.name) { + // chai assertions + case 'Assertion': + return true; } - if ((tsType.flags & ts.TypeFlags.Boolean) !== 0 || (tsType.flags & ts.TypeFlags.BooleanLiteral) !== 0) { - return 'Boolean'; + return false; + } + + protected applyMethodOverride(csMethod: cs.MethodDeclaration, classElement: ts.MethodDeclaration) { + super.applyMethodOverride(csMethod, classElement); + if (!csMethod.isOverride) { + if ( + ts.isComputedPropertyName(classElement.name) && + classElement.name.getText().includes('Symbol.dispose') + ) { + csMethod.isOverride = true; + } } - return null; } -} \ No newline at end of file +} diff --git a/src.compiler/kotlin/KotlinEmitter.ts b/src.compiler/kotlin/KotlinEmitter.ts index 15e9fe327..4437a4156 100644 --- a/src.compiler/kotlin/KotlinEmitter.ts +++ b/src.compiler/kotlin/KotlinEmitter.ts @@ -1,20 +1,18 @@ -import * as ts from 'typescript'; +import type * as ts from 'typescript'; import KotlinAstPrinter from './KotlinAstPrinter'; import KotlinAstTransformer from './KotlinAstTransformer'; import KotlinEmitterContext from './KotlinEmitterContext'; -import { transpileFilter } from '../BuilderHelpers' +import { transpileFilter } from '../BuilderHelpers'; export default function emit(program: ts.Program, diagnostics: ts.Diagnostic[]) { const context = new KotlinEmitterContext(program); console.log('[Kotlin] Transforming to Kotlin AST'); - program.getRootFileNames() - .filter(transpileFilter) - .forEach(file => { - const sourceFile = program.getSourceFile(file)!; - const transformer = new KotlinAstTransformer(sourceFile, context); - transformer.transform(); - }); + for (const file of program.getRootFileNames().filter(transpileFilter)) { + const sourceFile = program.getSourceFile(file)!; + const transformer = new KotlinAstTransformer(sourceFile, context); + transformer.transform(); + } console.log('[Kotlin] Resolving types'); context.resolveAllUnresolvedTypeNodes(); @@ -22,13 +20,13 @@ export default function emit(program: ts.Program, diagnostics: ts.Diagnostic[]) if (!context.hasErrors) { console.log('[Kotlin] Writing Result'); - context.csharpFiles.forEach(file => { + for (const file of context.csharpFiles) { // console.log(`[Kotlin] ${file.fileName} ${file.namespace.declarations.map(d=>d.name).join(', ')}`) const printer = new KotlinAstPrinter(file, context); printer.print(); diagnostics.push(...printer.diagnostics); - }) + } } diagnostics.push(...context.diagnostics); -} \ No newline at end of file +} diff --git a/src.compiler/kotlin/KotlinEmitterContext.ts b/src.compiler/kotlin/KotlinEmitterContext.ts index 107f7c866..341414a3e 100644 --- a/src.compiler/kotlin/KotlinEmitterContext.ts +++ b/src.compiler/kotlin/KotlinEmitterContext.ts @@ -31,23 +31,21 @@ export default class KotlinEmitterContext extends CSharpEmitterContext { } protected override toCoreTypeName(s: string) { - if(s === 'String') { + if (s === 'String') { return 'CoreString'; } - if(s === 'Map') { - return 'Map<*, *>' + if (s === 'Map') { + return 'Map<*, *>'; } return s; } public override getDefaultUsings(): string[] { - return [ - this.toPascalCase('alphaTab') + '.' + this.toPascalCase('core') - ]; + return [`${this.toPascalCase('alphaTab')}.${this.toPascalCase('core')}`]; } - public override makeExceptionType(): cs.TypeReferenceType { - return this.makeTypeName('kotlin.Throwable') + public override makeExceptionType(): string { + return this.makeTypeName('kotlin.Throwable'); } private isSymbolPartial(tsSymbol: ts.Symbol): boolean { @@ -58,37 +56,49 @@ export default class KotlinEmitterContext extends CSharpEmitterContext { return !!ts.getJSDocTags(tsSymbol.valueDeclaration).find(t => t.tagName.text === 'partial'); } + protected getOverriddenMembers( + classType: ts.ClassDeclaration | ts.InterfaceDeclaration, + classElement: ts.ClassElement + ): (ts.ClassElement | ts.TypeElement)[] { + const overriddenItems: (ts.ClassElement | ts.TypeElement)[] = []; + super.collectOverriddenMembersByName(overriddenItems, classType, classElement.name!.getText(), false, true); + return overriddenItems; + } - protected override getOverriddenMembers(classType: ts.InterfaceType, classElement: ts.ClassElement): ts.Symbol[] { - const symbols: ts.Symbol[] = []; - this.collectOverriddenMembersByName(symbols, classType, classElement.name!.getText(), false, true); - return symbols; + public override isValueTypeNotNullSmartCast(expression: ts.Expression): boolean | undefined { + return undefined; } - protected override collectOverriddenMembersByName(symbols: ts.Symbol[], classType: ts.InterfaceType, memberName: string, includeOwnMembers: boolean = false, allowInterfaces: boolean = false) { - super.collectOverriddenMembersByName(symbols, classType, memberName, includeOwnMembers, allowInterfaces); + protected isCsValueType(mapValueType: cs.TypeNode | null): boolean { + if(mapValueType?.nodeType === cs.SyntaxKind.ArrayTupleNode) { + return false; + } + return super.isCsValueType(mapValueType); + } - if ( - classType.symbol.valueDeclaration && - ts.isClassDeclaration(classType.symbol.valueDeclaration) && - classType.symbol.valueDeclaration.heritageClauses - ) { - const implementsClause = classType.symbol.valueDeclaration.heritageClauses?.find( - c => c.token === ts.SyntaxKind.ImplementsKeyword - ); - - if (implementsClause) { - for (const typeSyntax of implementsClause.types) { - const type = this.typeChecker.getTypeFromTypeNode(typeSyntax); - super.collectOverriddenMembersByName(symbols, type as ts.InterfaceType, memberName, true, allowInterfaces); - } - } + public override getMethodNameFromSymbol(symbol: ts.Symbol): string { + const parent = 'parent' in symbol ? (symbol.parent as ts.Symbol) : undefined; + + if (symbol.name === 'dispose' && (!parent || parent.name === 'SymbolConstructor')) { + return 'close'; } - return false; + if (symbol.name === 'iterator' && (!parent || parent.name === 'SymbolConstructor')) { + return this.toMethodName('iterator'); + } + + return ''; } - public override isValueTypeNotNullSmartCast(expression: ts.Expression): boolean | undefined { - return undefined; + public override makeIterableType(): string { + return this.makeTypeName('kotlin.collections.Iterable'); + } + + public override makeGeneratorType(): string { + return this.makeTypeName('kotlin.collections.Iterator'); + } + + public makeIteratorType(): string { + return this.makeTypeName('kotlin.collections.Iterator'); } } diff --git a/src.compiler/kotlin/KotlinTranspiler.ts b/src.compiler/kotlin/KotlinTranspiler.ts index a3c4c123f..f5c19a2a5 100644 --- a/src.compiler/kotlin/KotlinTranspiler.ts +++ b/src.compiler/kotlin/KotlinTranspiler.ts @@ -1,7 +1,12 @@ import emit from './KotlinEmitter'; -import transpiler from '../TranspilerBase' +import transpiler from '../TranspilerBase'; -transpiler([{ - name: 'Kotlin', - emit: emit -}], true); \ No newline at end of file +transpiler( + [ + { + name: 'Kotlin', + emit: emit + } + ], + true +); diff --git a/src.compiler/typescript/AlphaTabGenerator.ts b/src.compiler/typescript/AlphaTabGenerator.ts index 90c3e25c0..f825b99c2 100644 --- a/src.compiler/typescript/AlphaTabGenerator.ts +++ b/src.compiler/typescript/AlphaTabGenerator.ts @@ -1,31 +1,53 @@ -import * as ts from 'typescript'; +import ts from 'typescript'; import cloneEmit from './CloneEmitter'; import { GENERATED_FILE_HEADER } from './EmitterBase'; import serializerEmit from './SerializerEmitter'; import transpiler from '../TranspilerBase'; -import * as fs from 'fs'; +import fs from 'node:fs'; import jsonDeclarationEmit from './JsonDeclarationEmitter'; +import { execSync } from 'node:child_process'; -transpiler([{ - name: 'Clone', - emit: cloneEmit -}, { - name: 'Serializer', - emit: serializerEmit -}, { - name: 'JSON Declarations', - emit: jsonDeclarationEmit -}], false); +transpiler( + [ + { + name: 'Clone', + emit: cloneEmit + }, + { + name: 'Serializer', + emit: serializerEmit + }, + { + name: 'JSON Declarations', + emit: jsonDeclarationEmit + } + ], + false +); // Write version file const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8')); const { version } = packageJson; const fileHandle = fs.openSync('src/generated/VersionInfo.ts', 'w'); -fs.writeSync(fileHandle, `\ +const commit = execSync('git rev-parse HEAD').toString().trim(); + +fs.writeSync( + fileHandle, + `\ ${GENERATED_FILE_HEADER} + + export class VersionInfo { public static readonly version: string = '${version}'; public static readonly date: string = '${new Date().toISOString()}'; + public static readonly commit: string = '${commit}'; + + public static print(print: (message:string) => void) { + print(\`alphaTab \${VersionInfo.version}\`); + print(\`commit: \${VersionInfo.commit}\`); + print(\`build date: \${VersionInfo.date}\`); + } } -`); +` +); ts.sys.exit(ts.ExitStatus.Success); diff --git a/src.compiler/typescript/CloneEmitter.ts b/src.compiler/typescript/CloneEmitter.ts index 9d2fbc78b..711840762 100644 --- a/src.compiler/typescript/CloneEmitter.ts +++ b/src.compiler/typescript/CloneEmitter.ts @@ -2,17 +2,17 @@ * This file contains an emitter which generates classes to clone * any data models following certain rules. */ -import * as path from 'path'; -import * as ts from 'typescript'; +import path from 'node:path'; +import ts from 'typescript'; import createEmitter from './EmitterBase'; -import { getTypeWithNullableInfo, unwrapArrayItemType } from '../BuilderHelpers'; +import { getTypeWithNullableInfo } from './TypeSchema'; function removeExtension(fileName: string) { return fileName.substring(0, fileName.lastIndexOf('.')); } function toImportPath(fileName: string) { - return '@' + removeExtension(fileName).split('\\').join('/'); + return `@${removeExtension(fileName).split('\\').join('/')}`; } function isClonable(type: ts.Type): boolean { @@ -52,10 +52,10 @@ function isCloneMember(propertyDeclaration: ts.PropertyDeclaration) { function generateClonePropertyStatements( prop: ts.PropertyDeclaration, - typeChecker: ts.TypeChecker, + program: ts.Program, importer: (name: string, module: string) => void ): ts.Statement[] { - const propertyType = getTypeWithNullableInfo(typeChecker, prop.type!, true); + const propertyType = getTypeWithNullableInfo(program, prop.type!, true, !!prop.questionToken, undefined); const statements: ts.Statement[] = []; @@ -72,15 +72,15 @@ function generateClonePropertyStatements( ]; } - const arrayItemType = unwrapArrayItemType(propertyType.type!, typeChecker); + const arrayItemType = propertyType.arrayItemType; if (arrayItemType) { - if (isClonable(arrayItemType)) { + if (arrayItemType.isCloneable) { const collectionAddMethod = ts .getJSDocTags(prop) .filter(t => t.tagName.text === 'clone_add') .map(t => t.comment ?? '')[0] as string; - importer(arrayItemType.symbol!.name + 'Cloner', './' + arrayItemType.symbol!.name + 'Cloner'); + importer(`${arrayItemType.typeAsString}Cloner`, `@src/generated/model/${arrayItemType.typeAsString}Cloner`); const loopItems = [ ...assign(ts.factory.createArrayLiteralExpression(undefined)), @@ -93,54 +93,61 @@ function generateClonePropertyStatements( ts.factory.createNonNullExpression( ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('original'), propertyName) ), - ts.factory.createBlock([ - ts.factory.createExpressionStatement( - collectionAddMethod - ? // clone.addProp(ItemTypeCloner.clone(i)) - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('clone'), - collectionAddMethod - ), - undefined, - [ - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier(arrayItemType.symbol!.name + 'Cloner'), - 'clone' - ), - undefined, - [ts.factory.createIdentifier('i')] - ) - ] - ) - : // clone.prop.push(ItemTypeCloner.clone(i)) - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( + ts.factory.createBlock( + [ + ts.factory.createExpressionStatement( + collectionAddMethod + ? // clone.addProp(ItemTypeCloner.clone(i)) + ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier('clone'), - propertyName + collectionAddMethod ), - 'push' - ), - undefined, - [ - ts.factory.createCallExpression( + undefined, + [ + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier( + `${arrayItemType.typeAsString}Cloner` + ), + 'clone' + ), + undefined, + [ts.factory.createIdentifier('i')] + ) + ] + ) + : // clone.prop.push(ItemTypeCloner.clone(i)) + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier(arrayItemType.symbol!.name + 'Cloner'), - 'clone' + ts.factory.createIdentifier('clone'), + propertyName ), - undefined, - [ts.factory.createIdentifier('i')] - ) - ] - ) - ) - ], true) + 'push' + ), + undefined, + [ + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier( + `${arrayItemType.typeAsString}Cloner` + ), + 'clone' + ), + undefined, + [ts.factory.createIdentifier('i')] + ) + ] + ) + ) + ], + true + ) ) ]; - if (propertyType.isNullable) { + if (propertyType.isNullable || propertyType.isOptional) { // if(original.prop) { // clone.prop = []; // for(const i of original.prop) { clone.addProp(ItemTypeCloner.clone(i)); } @@ -174,7 +181,10 @@ function generateClonePropertyStatements( [] ); - if (propertyType.isNullable) { + const nullOrUndefined = propertyType.isNullable + ? ts.factory.createNull() + : ts.factory.createIdentifier('undefined'); + if (propertyType.isNullable || propertyType.isOptional) { statements.push( ...assign( ts.factory.createConditionalExpression( @@ -185,7 +195,7 @@ function generateClonePropertyStatements( ts.factory.createToken(ts.SyntaxKind.QuestionToken), sliceCall, ts.factory.createToken(ts.SyntaxKind.ColonToken), - ts.factory.createNull() + nullOrUndefined ) ) ); @@ -195,10 +205,14 @@ function generateClonePropertyStatements( } } } else { - if (isClonable(propertyType.type!)) { - importer(propertyType.type.symbol!.name + 'Cloner', './' + propertyType.type.symbol!.name + 'Cloner'); + if (propertyType.isCloneable) { + importer(`${propertyType.typeAsString}Cloner`, `@src/generated/model/${propertyType.typeAsString}Cloner`); // clone.prop = original.prop ? TypeNameCloner.clone(original.prop) : null + // clone.prop = original.prop ? TypeNameCloner.clone(original.prop) : undefined + const nullOrUndefined = propertyType.isNullable + ? ts.factory.createNull() + : ts.factory.createIdentifier('undefined'); statements.push( ...assign( ts.factory.createConditionalExpression( @@ -209,7 +223,7 @@ function generateClonePropertyStatements( ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier(propertyType.type.symbol!.name + 'Cloner'), + ts.factory.createIdentifier(`${propertyType.typeAsString}Cloner`), 'clone' ), undefined, @@ -221,7 +235,7 @@ function generateClonePropertyStatements( ] ), ts.factory.createToken(ts.SyntaxKind.ColonToken), - ts.factory.createNull() + nullOrUndefined ) ) ); @@ -243,13 +257,12 @@ function generateCloneBody( input: ts.ClassDeclaration, importer: (name: string, module: string) => void ): ts.Block { - const typeChecker = program.getTypeChecker(); const propertiesToSerialize = input.members .filter(m => ts.isPropertyDeclaration(m) && isCloneMember(m)) .map(m => m as ts.PropertyDeclaration); const bodyStatements = propertiesToSerialize.reduce((stmts, prop) => { - stmts.push(...generateClonePropertyStatements(prop, typeChecker, importer)); + stmts.push(...generateClonePropertyStatements(prop, program, importer)); return stmts; }, new Array()); @@ -273,7 +286,9 @@ function generateCloneBody( ...bodyStatements, // return json; ts.factory.createReturnStatement(ts.factory.createIdentifier('clone')) - ], true); + ], + true + ); } function createCloneMethod( @@ -333,7 +348,7 @@ export default createEmitter('cloneable', (program, input) => { statements.push( ts.factory.createClassDeclaration( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - input.name!.text + 'Cloner', + `${input.name!.text}Cloner`, undefined, undefined, [createCloneMethod(program, input, importer)] @@ -348,7 +363,11 @@ export default createEmitter('cloneable', (program, input) => { false, undefined, ts.factory.createNamedImports([ - ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(input.name!.text)) + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(input.name!.text) + ) ]) ), ts.factory.createStringLiteral(toImportPath(sourceFileName)) diff --git a/src.compiler/typescript/EmitterBase.ts b/src.compiler/typescript/EmitterBase.ts index 9333b2b96..88a0ffb13 100644 --- a/src.compiler/typescript/EmitterBase.ts +++ b/src.compiler/typescript/EmitterBase.ts @@ -1,6 +1,6 @@ -import * as path from 'path'; -import * as ts from 'typescript'; -import * as fs from 'fs'; +import path from 'node:path'; +import ts from 'typescript'; +import fs from 'node:fs'; export const GENERATED_FILE_HEADER = `\ // @@ -86,16 +86,16 @@ export default function createEmitter( generate: (program: ts.Program, classDeclaration: ts.ClassDeclaration) => ts.SourceFile ) { function scanSourceFile(program: ts.Program, sourceFile: ts.SourceFile) { - sourceFile.statements.forEach(stmt => { + for (const stmt of sourceFile.statements) { if (ts.isClassDeclaration(stmt) && ts.getJSDocTags(stmt).some(t => t.tagName.text === jsDocMarker)) { generateClass(program, stmt, generate); } - }); + } } return function emit(program: ts.Program, _diagnostics: ts.Diagnostic[]) { - program.getRootFileNames().forEach(file => { + for (const file of program.getRootFileNames()) { scanSourceFile(program, program.getSourceFile(file)!); - }); + } }; } diff --git a/src.compiler/typescript/JsonDeclarationEmitter.ts b/src.compiler/typescript/JsonDeclarationEmitter.ts index c457a5bde..6602381b8 100644 --- a/src.compiler/typescript/JsonDeclarationEmitter.ts +++ b/src.compiler/typescript/JsonDeclarationEmitter.ts @@ -2,94 +2,58 @@ * This file contains an emitter which generates classes to clone * any data models following certain rules. */ -import * as path from 'path'; -import * as url from 'url'; + import * as ts from 'typescript'; import createEmitter, { generateFile } from './EmitterBase'; -import { - cloneTypeNode, - getTypeWithNullableInfo, - isEnumType, - isPrimitiveType, - unwrapArrayItemType -} from '../BuilderHelpers'; - -function removeExtension(fileName: string) { - return fileName.substring(0, fileName.lastIndexOf('.')); -} - -const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); -const root = path.resolve(__dirname, '..', '..'); - -function toImportPath(fileName: string) { - return '@' + removeExtension(path.relative(root, fileName)).split('\\').join('/'); -} +import { getTypeWithNullableInfo, type TypeWithNullableInfo } from './TypeSchema'; -function createDefaultJsonTypeNode(checker: ts.TypeChecker, type: ts.Type, isNullable: boolean) { +function createDefaultJsonTypeNode(checker: ts.TypeChecker, type: TypeWithNullableInfo, isNullable: boolean) { if (isNullable) { const notNullable = createDefaultJsonTypeNode(checker, type, false); return ts.factory.createUnionTypeNode([notNullable, ts.factory.createLiteralTypeNode(ts.factory.createNull())]); } - if (isPrimitiveType(type)) { - if ('intrinsicName' in type) { - return ts.factory.createTypeReferenceNode(type.intrinsicName as string); - } else { - return ts.factory.createTypeReferenceNode('TODO'); - } - } else if (checker.isArrayType(type)) { - return ts.factory.createTypeReferenceNode(type.symbol.name); - } else if (type.symbol) { - return ts.factory.createTypeReferenceNode(type.symbol.name); - } else if (type.isStringLiteral()) { - return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(type.value)); - } else if (type.isLiteral()) { - if (typeof type.value === 'string') { - return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(type.value)); - } else { - return ts.factory.createTypeReferenceNode('TODO'); - } - } else if ('intrinsicName' in type) { - return ts.factory.createTypeReferenceNode(type.intrinsicName as string); - } else { - return ts.factory.createTypeReferenceNode('TODO'); - } + return type.createTypeNode(); } function createJsonTypeNode( checker: ts.TypeChecker, - type: ts.TypeNode | ts.Type, + typeInfo: TypeWithNullableInfo, importer: (name: string, module: string) => void ): ts.TypeNode | undefined { - const typeInfo = getTypeWithNullableInfo(checker, type!, true); - - if (isEnumType(typeInfo.type)) { - const sourceFile = typeInfo.type.symbol.valueDeclaration?.getSourceFile(); - if (sourceFile) { - importer(typeInfo.type.symbol.name, toImportPath(sourceFile.fileName)); - } + if (typeInfo.isEnumType) { + importer(typeInfo.typeAsString, typeInfo.modulePath); return ts.factory.createUnionTypeNode([ - ts.factory.createTypeReferenceNode(typeInfo.type.symbol.name), + typeInfo.createTypeNode(), ts.factory.createTypeOperatorNode( ts.SyntaxKind.KeyOfKeyword, - ts.factory.createTypeQueryNode(ts.factory.createIdentifier(typeInfo.type.symbol.name)) - ) + ts.factory.createTypeQueryNode(ts.factory.createIdentifier(typeInfo.typeAsString)) + ), + ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Lowercase'), [ + ts.factory.createTypeOperatorNode( + ts.SyntaxKind.KeyOfKeyword, + ts.factory.createTypeQueryNode(ts.factory.createIdentifier(typeInfo.typeAsString)) + ) + ]) ]); - } else if (checker.isArrayType(typeInfo.type)) { - const arrayItemType = unwrapArrayItemType(typeInfo.type, checker); + } + + if (typeInfo.isArray) { + const arrayItemType = typeInfo.arrayItemType; if (arrayItemType) { const result = createJsonTypeNode(checker, arrayItemType, importer); if (result) { return ts.factory.createArrayTypeNode(result); - } else { - return ts.factory.createArrayTypeNode(createDefaultJsonTypeNode(checker, arrayItemType, false)); } + return ts.factory.createArrayTypeNode(createDefaultJsonTypeNode(checker, arrayItemType, false)); } return undefined; - } else if (!isPrimitiveType(typeInfo.type) && typeInfo.type.isUnion()) { + } + + if (!typeInfo.isPrimitiveType && typeInfo.isUnionType) { const types: ts.TypeNode[] = []; - for (const type of typeInfo.type.types) { + for (const type of typeInfo.unionTypes!) { const mapped = createJsonTypeNode(checker, type, importer); if (mapped) { types.push(mapped); @@ -98,51 +62,39 @@ function createJsonTypeNode( } } return ts.factory.createUnionTypeNode(types); - } else if (typeInfo.type.symbol) { - const sourceFile = typeInfo.type.symbol.valueDeclaration?.getSourceFile(); - if (sourceFile) { - const isOwnType = !sourceFile.fileName.includes('node_modules'); - - if (isOwnType) { - const isGeneratedJsonDeclaration = ts - .getJSDocTags(typeInfo.type.symbol.valueDeclaration!) - .some(t => t.tagName.text === 'json_declaration'); - if (isGeneratedJsonDeclaration) { - importer(typeInfo.type.symbol.name + 'Json', './' + typeInfo.type.symbol.name + 'Json'); - } else { - importer(typeInfo.type.symbol.name + 'Json', toImportPath(sourceFile.fileName)); - } - } + } + if (typeInfo.modulePath) { + const isOwnType = typeInfo.isOwnType; - let args: ts.TypeNode[] | undefined = undefined; - if ('typeArguments' in typeInfo.type) { - args = []; - for (const arg of (typeInfo.type as ts.TypeReference).typeArguments ?? []) { - const mapped = createJsonTypeNode(checker, arg, importer); - - if (mapped) { - args.push(mapped); - } else { - const argTypeInfo = getTypeWithNullableInfo(checker, arg, true); - args.push(createDefaultJsonTypeNode(checker, argTypeInfo.type, argTypeInfo.isNullable)); - } - } + if (isOwnType) { + const isGeneratedJsonDeclaration = typeInfo.jsDocTags?.some(t => t.tagName.text === 'json_declaration'); + if (isGeneratedJsonDeclaration) { + importer(`${typeInfo.typeAsString}Json`, `@src/generated/${typeInfo.typeAsString}Json`); + } else { + importer(`${typeInfo.typeAsString}Json`, typeInfo.modulePath); } + } - let symbolType: ts.TypeNode = ts.factory.createTypeReferenceNode( - typeInfo.type.symbol.name + (isOwnType ? 'Json' : ''), - args - ); + let args: ts.TypeNode[] | undefined = undefined; + if (typeInfo.typeArguments) { + args = []; + for (const arg of typeInfo.typeArguments) { + const mapped = createJsonTypeNode(checker, arg, importer); - if (typeInfo.type.symbol.name === 'Map' && args && args.length === 2) { - // symbolType = ts.factory.createUnionTypeNode([ - // symbolType, - // ts.factory.createTypeReferenceNode('Record' - // ]); + if (mapped) { + args.push(mapped); + } else { + args.push(createDefaultJsonTypeNode(checker, arg, arg.isNullable)); + } } - - return symbolType; } + + const symbolType: ts.TypeNode = ts.factory.createTypeReferenceNode( + typeInfo.typeAsString + (isOwnType ? 'Json' : ''), + args + ); + + return symbolType; } return undefined; } @@ -158,7 +110,7 @@ function cloneJsDoc(node: T, source: ts.Node, additionalTags: text = text .substring(2, text.length - 2) .split('\n') - .map(l => ' ' + l.trimStart()) + .map(l => ` ${l.trimStart()}`) .join('\n') .trimStart(); @@ -180,10 +132,10 @@ function createJsonMember( input: ts.PropertyDeclaration, importer: (name: string, module: string) => void ): ts.TypeElement { - const checker = program.getTypeChecker(); - let type = createJsonTypeNode(checker, input.type!, importer) ?? cloneTypeNode(input.type!); + const typeInfo = getTypeWithNullableInfo(program, input.type!, true, false, undefined); + const type = createJsonTypeNode(program.getTypeChecker(), typeInfo, importer) ?? typeInfo.createTypeNode(); - let prop = ts.factory.createPropertySignature( + const prop = ts.factory.createPropertySignature( undefined, input.name, ts.factory.createToken(ts.SyntaxKind.QuestionToken), @@ -208,7 +160,7 @@ function createJsonMembers( .map(m => createJsonMember(program, m as ts.PropertyDeclaration, importer)); } -let allJsonTypes: string[] = []; +let allJsonTypes: Map = new Map(); const emit = createEmitter('json_declaration', (program, input) => { console.log(`Writing JSON Type Declaration for ${input.name!.text}`); const statements: ts.Statement[] = []; @@ -220,6 +172,10 @@ const emit = createEmitter('json_declaration', (program, input) => { } imported.add(name); + if (name.endsWith('Json')) { + allJsonTypes.set(name, module); + } + statements.push( ts.factory.createImportDeclaration( undefined, @@ -239,7 +195,7 @@ const emit = createEmitter('json_declaration', (program, input) => { cloneJsDoc( ts.factory.createInterfaceDeclaration( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - input.name!.text + 'Json', + `${input.name!.text}Json`, undefined, undefined, createJsonMembers(program, input, importer) @@ -249,7 +205,7 @@ const emit = createEmitter('json_declaration', (program, input) => { ) ); - allJsonTypes.push(input.name!.text + 'Json'); + allJsonTypes.set(`${input.name!.text}Json`, `@src/generated/${input.name!.text}Json`); const sourceFile = ts.factory.createSourceFile( [...statements], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), @@ -259,18 +215,18 @@ const emit = createEmitter('json_declaration', (program, input) => { return sourceFile; }); export default function emitWithIndex(program: ts.Program, _diagnostics: ts.Diagnostic[]) { - allJsonTypes = []; + allJsonTypes = new Map(); emit(program, _diagnostics); - const statements = allJsonTypes.map(type => + const statements = Array.from(allJsonTypes.entries()).map(type => ts.factory.createExportDeclaration( undefined, true, ts.factory.createNamedExports([ - ts.factory.createExportSpecifier(false, undefined, ts.factory.createIdentifier(type)) + ts.factory.createExportSpecifier(false, undefined, ts.factory.createIdentifier(type[0])) ]), - ts.factory.createStringLiteral(`./${type}`) + ts.factory.createStringLiteral(type[1]) ) ); @@ -280,5 +236,5 @@ export default function emitWithIndex(program: ts.Program, _diagnostics: ts.Diag ts.NodeFlags.None ); - generateFile(program, sourceFile, 'json.ts'); + generateFile(program, sourceFile, '_jsonbarrel.ts'); } diff --git a/src.compiler/typescript/Serializer.common.ts b/src.compiler/typescript/Serializer.common.ts index 1c356afca..c5e9a0cec 100644 --- a/src.compiler/typescript/Serializer.common.ts +++ b/src.compiler/typescript/Serializer.common.ts @@ -1,71 +1,9 @@ -import * as ts from 'typescript'; -import * as path from 'path'; +import type { TypeWithNullableInfo } from './TypeSchema'; -export interface JsonProperty { - partialNames: boolean; - property: ts.PropertyDeclaration; - jsonNames: string[]; - asRaw: boolean; - target?: string; - isReadOnly: boolean; -} - -export interface JsonSerializable { - isStrict: boolean; - hasToJsonExtension: boolean; - hasSetPropertyExtension: boolean; - properties: JsonProperty[]; -} - - -export function isImmutable(type: ts.Type | null): boolean { - if (!type || !type.symbol) { - return false; - } - - const declaration = type.symbol.valueDeclaration; - if (declaration) { - return !!ts.getJSDocTags(declaration).find(t => t.tagName.text === 'json_immutable'); - } - - return false; -} - -export function createStringUnknownMapNode(): ts.TypeNode { - return ts.factory.createTypeReferenceNode('Map', [ - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) - ]); -} - -function removeExtension(fileName: string) { - return fileName.substring(0, fileName.lastIndexOf('.')); -} - -export function toImportPath(fileName: string) { - return '@' + removeExtension(fileName).split('\\').join('/'); -} - -export function findModule(type: ts.Type, options: ts.CompilerOptions) { - if (type.symbol && type.symbol.declarations) { - for (const decl of type.symbol.declarations) { - const file = decl.getSourceFile(); - if (file) { - const relative = path.relative(path.join(path.resolve(options.baseUrl!)), path.resolve(file.fileName)); - return toImportPath(relative); - } - } - - return './' + type.symbol.name; - } - - return ''; -} - -export function findSerializerModule(type: ts.Type, options: ts.CompilerOptions) { - let module = findModule(type, options); +export function findSerializerModule(type: TypeWithNullableInfo) { + const module = type.modulePath; const importPath = module.split('/'); importPath.splice(1, 0, 'generated'); - importPath[importPath.length - 1] = type.symbol!.name + 'Serializer'; + importPath[importPath.length - 1] = `${type.typeAsString}Serializer`; return importPath.join('/'); } diff --git a/src.compiler/typescript/Serializer.fromJson.ts b/src.compiler/typescript/Serializer.fromJson.ts index 66e72906f..a80ea241b 100644 --- a/src.compiler/typescript/Serializer.fromJson.ts +++ b/src.compiler/typescript/Serializer.fromJson.ts @@ -1,28 +1,40 @@ import * as ts from 'typescript'; import { createNodeFromSource, setMethodBody } from '../BuilderHelpers'; -import { JsonSerializable } from './Serializer.common'; +import type { TypeSchema } from './TypeSchema'; -function generateFromJsonBody(serializable: JsonSerializable, importer: (name: string, module: string) => void) { +function generateFromJsonBody( + input: ts.ClassDeclaration, + serializable: TypeSchema, + importer: (name: string, module: string) => void +) { importer('JsonHelper', '@src/io/JsonHelper'); - return ts.factory.createBlock([ - createNodeFromSource(`if(!m) { + + const serializerName = `${input.name!.text}Serializer`; + return ts.factory.createBlock( + [ + createNodeFromSource( + `if(!m) { return; - }`, ts.SyntaxKind.IfStatement), - serializable.isStrict - ? createNodeFromSource( - `JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v));`, - ts.SyntaxKind.ExpressionStatement - ) - : createNodeFromSource( - `JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v));`, - ts.SyntaxKind.ExpressionStatement - ) - ], true); + }`, + ts.SyntaxKind.IfStatement + ), + serializable.isStrict + ? createNodeFromSource( + `JsonHelper.forEach(m, (v, k) => ${serializerName}.setProperty(obj, k, v));`, + ts.SyntaxKind.ExpressionStatement + ) + : createNodeFromSource( + `JsonHelper.forEach(m, (v, k) => ${serializerName}.setProperty(obj, k.toLowerCase(), v));`, + ts.SyntaxKind.ExpressionStatement + ) + ], + true + ); } export function createFromJsonMethod( input: ts.ClassDeclaration, - serializable: JsonSerializable, + serializable: TypeSchema, importer: (name: string, module: string) => void ) { const methodDecl = createNodeFromSource( @@ -32,5 +44,5 @@ export function createFromJsonMethod( }`, ts.SyntaxKind.MethodDeclaration ); - return setMethodBody(methodDecl, generateFromJsonBody(serializable, importer)); + return setMethodBody(methodDecl, generateFromJsonBody(input, serializable, importer)); } diff --git a/src.compiler/typescript/Serializer.setProperty.ts b/src.compiler/typescript/Serializer.setProperty.ts index f2cd54214..5663473e7 100644 --- a/src.compiler/typescript/Serializer.setProperty.ts +++ b/src.compiler/typescript/Serializer.setProperty.ts @@ -1,148 +1,103 @@ import * as ts from 'typescript'; -import { cloneTypeNode, createNodeFromSource, isPrimitiveToJson, isSet, setMethodBody } from '../BuilderHelpers'; -import { isPrimitiveType } from '../BuilderHelpers'; -import { hasFlag } from '../BuilderHelpers'; -import { getTypeWithNullableInfo } from '../BuilderHelpers'; -import { isTypedArray } from '../BuilderHelpers'; -import { unwrapArrayItemType } from '../BuilderHelpers'; -import { isMap } from '../BuilderHelpers'; -import { isEnumType } from '../BuilderHelpers'; -import { isNumberType } from '../BuilderHelpers'; -import { wrapToNonNull } from '../BuilderHelpers'; -import { - createStringUnknownMapNode, - findModule, - findSerializerModule, - isImmutable, - JsonProperty, - JsonSerializable -} from './Serializer.common'; - -function isPrimitiveFromJson(type: ts.Type, typeChecker: ts.TypeChecker) { - if (!type) { - return false; - } - - const isArray = isTypedArray(type); - const arrayItemType = unwrapArrayItemType(type, typeChecker); +import { createNodeFromSource, setMethodBody } from '../BuilderHelpers'; +import { findSerializerModule } from './Serializer.common'; +import type { TypeSchema, TypeWithNullableInfo } from './TypeSchema'; + +export function createStringUnknownMapNode(): ts.TypeNode { + return ts.factory.createTypeReferenceNode('Map', [ + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) + ]); +} - if (hasFlag(type, ts.TypeFlags.Unknown)) { - return true; - } - if (hasFlag(type, ts.TypeFlags.Number)) { +function isPrimitiveForSerialization(type: TypeWithNullableInfo) { + if (type.isPrimitiveType) { return true; } - if (hasFlag(type, ts.TypeFlags.String)) { + if (type.arrayItemType?.isPrimitiveType) { return true; } - if (hasFlag(type, ts.TypeFlags.Boolean)) { + if (type.isTypedArray) { return true; } - if (arrayItemType) { - if (isArray && hasFlag(arrayItemType, ts.TypeFlags.Number)) { - return true; - } - if (isArray && hasFlag(arrayItemType, ts.TypeFlags.String)) { - return true; - } - if (isArray && hasFlag(arrayItemType, ts.TypeFlags.Boolean)) { - return true; - } - } else if (type.symbol) { - switch (type.symbol.name) { - case 'Uint8Array': - case 'Uint16Array': - case 'Uint32Array': - case 'Int8Array': - case 'Int16Array': - case 'Int32Array': - case 'Float32Array': - case 'Float64Array': - return true; - } - } - - return null; + return false; } -function generateSetPropertyBody( - program: ts.Program, - serializable: JsonSerializable, - importer: (name: string, module: string) => void -) { +function generateSetPropertyBody(serializable: TypeSchema, importer: (name: string, module: string) => void) { const statements: ts.Statement[] = []; const cases: ts.CaseOrDefaultClause[] = []; - const typeChecker = program.getTypeChecker(); for (const prop of serializable.properties) { const jsonNames = prop.jsonNames.map(j => j.toLowerCase()); const caseValues: string[] = jsonNames.filter(j => j !== ''); - const fieldName = (prop.property.name as ts.Identifier).text; + const fieldName = prop.name; const caseStatements: ts.Statement[] = []; - const type = getTypeWithNullableInfo(typeChecker, prop.property.type, true); - - const assignField = function (expr: ts.Expression): ts.Statement { - return ts.factory.createExpressionStatement( + const assignField = (expr: ts.Expression): ts.Statement => + ts.factory.createExpressionStatement( ts.factory.createAssignment( ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), expr ) ); - }; - if (!prop.asRaw && type.isUnionType) { + if (!prop.asRaw && prop.type.isUnionType) { caseStatements.push( assignField( ts.factory.createAsExpression( - type.isNullable + prop.type.isNullable || prop.type.isOptional ? ts.factory.createIdentifier('v') : ts.factory.createNonNullExpression(ts.factory.createIdentifier('v')), - cloneTypeNode(prop.property.type!) + prop.type.createTypeNode() ) ) ); caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); - } else if (prop.asRaw || isPrimitiveFromJson(type.type!, typeChecker)) { + } else if (prop.asRaw || isPrimitiveForSerialization(prop.type)) { caseStatements.push( assignField( ts.factory.createAsExpression( - type.isNullable + prop.type.isNullable || prop.type.isOptional ? ts.factory.createIdentifier('v') : ts.factory.createNonNullExpression(ts.factory.createIdentifier('v')), - cloneTypeNode(prop.property.type!) + prop.type.createTypeNode() ) ) ); caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); - } else if (isEnumType(type.type)) { - importer(type.type.symbol!.name, findModule(type.type, program.getCompilerOptions())); + } else if (prop.type.isEnumType) { + importer(prop.type.typeAsString, prop.type.modulePath); importer('JsonHelper', '@src/io/JsonHelper'); - if (type.isNullable) { + if (prop.type.isNullable) { + caseStatements.push( + createNodeFromSource( + `obj.${fieldName} = JsonHelper.parseEnum<${prop.type.typeAsString}>(v, ${prop.type.typeAsString}) ?? null;`, + ts.SyntaxKind.ExpressionStatement + ) + ); + } else if (prop.type.isOptional) { caseStatements.push( createNodeFromSource( - `obj.${fieldName} = JsonHelper.parseEnum<${type.type.symbol.name}>(v, ${type.type.symbol.name});`, + `obj.${fieldName} = JsonHelper.parseEnum<${prop.type.typeAsString}>(v, ${prop.type.typeAsString});`, ts.SyntaxKind.ExpressionStatement ) ); } else { caseStatements.push( createNodeFromSource( - `obj.${fieldName} = JsonHelper.parseEnum<${type.type.symbol.name}>(v, ${type.type.symbol.name})!;`, + `obj.${fieldName} = JsonHelper.parseEnum<${prop.type.typeAsString}>(v, ${prop.type.typeAsString})!;`, ts.SyntaxKind.ExpressionStatement ) ); } caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); - } else if (isTypedArray(type.type!)) { - const arrayItemType = unwrapArrayItemType(type.type!, typeChecker)!; + } else if (prop.type.isArray) { + const arrayItemType = prop.type.arrayItemType!; const collectionAddMethod = - (ts - .getJSDocTags(prop.property) - .filter(t => t.tagName.text === 'json_add') - .map(t => t.comment ?? '')[0] as string) ?? `${fieldName}.push`; + (prop.jsDocTags.filter(t => t.tagName.text === 'json_add').map(t => t.comment ?? '')[0] as string) ?? + `${fieldName}.push`; // obj.fieldName = []; // for(const i of value) { @@ -153,9 +108,9 @@ function generateSetPropertyBody( // obj.fieldName.push(Type.FromJson(__li)); // } - let itemSerializer = arrayItemType.symbol.name + 'Serializer'; - importer(itemSerializer, findSerializerModule(arrayItemType, program.getCompilerOptions())); - importer(arrayItemType.symbol.name, findModule(arrayItemType, program.getCompilerOptions())); + const itemSerializer = `${arrayItemType.typeAsString}Serializer`; + importer(itemSerializer, findSerializerModule(arrayItemType)); + importer(arrayItemType.typeAsString, arrayItemType.modulePath); const loopItems = [ createNodeFromSource( @@ -164,7 +119,7 @@ function generateSetPropertyBody( ), createNodeFromSource( `for(const o of (v as (Map | null)[])) { - const i = new ${arrayItemType.symbol.name}(); + const i = new ${arrayItemType.typeAsString}(); ${itemSerializer}.fromJson(i, o); obj.${collectionAddMethod}(i) }`, @@ -172,7 +127,7 @@ function generateSetPropertyBody( ) ]; - if (type.isNullable) { + if (prop.type.isNullable || prop.type.isOptional) { caseStatements.push( ts.factory.createIfStatement( ts.factory.createIdentifier('v'), @@ -183,62 +138,54 @@ function generateSetPropertyBody( caseStatements.push(...loopItems); } caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); - } else if (isMap(type.type)) { - const mapType = type.type as ts.TypeReference; - if (!isPrimitiveType(mapType.typeArguments![0])) { - throw new Error('only Map maps are supported extend if needed!'); - } - + } else if (prop.type.isMap) { let mapKey: ts.Expression; - if (isEnumType(mapType.typeArguments![0])) { - importer( - mapType.typeArguments![0].symbol!.name, - findModule(mapType.typeArguments![0], program.getCompilerOptions()) - ); + if (prop.type.typeArguments![0].isEnumType) { + importer(prop.type.typeArguments![0].typeAsString, prop.type.typeArguments![0].modulePath); importer('JsonHelper', '@src/io/JsonHelper'); mapKey = createNodeFromSource( - `JsonHelper.parseEnum<${mapType.typeArguments![0].symbol!.name}>(k, ${ - mapType.typeArguments![0].symbol!.name + `JsonHelper.parseEnum<${prop.type.typeArguments![0].typeAsString}>(k, ${ + prop.type.typeArguments![0].typeAsString })!`, ts.SyntaxKind.NonNullExpression ); - } else if (isNumberType(mapType.typeArguments![0])) { - mapKey = createNodeFromSource(`parseInt(k)`, ts.SyntaxKind.CallExpression); + } else if (prop.type.typeArguments![0].isNumberType) { + mapKey = createNodeFromSource('Number.parseInt(k)', ts.SyntaxKind.CallExpression); } else { mapKey = ts.factory.createIdentifier('k'); } - let mapValue; + const mapValueTypeInfo = prop.type.typeArguments![1]; + + let mapValue: ts.Expression; let itemSerializer: string = ''; - if (isPrimitiveFromJson(mapType.typeArguments![1], typeChecker)) { + if (isPrimitiveForSerialization(mapValueTypeInfo)) { mapValue = ts.factory.createAsExpression( ts.factory.createIdentifier('v'), - typeChecker.typeToTypeNode(mapType.typeArguments![1] , undefined, undefined)! + mapValueTypeInfo.createTypeNode() ); - } else { - itemSerializer = mapType.typeArguments![1].symbol.name + 'Serializer'; - importer(itemSerializer, findSerializerModule(mapType.typeArguments![1], program.getCompilerOptions())); - importer( - mapType.typeArguments![1]!.symbol.name, - findModule(mapType.typeArguments![1], program.getCompilerOptions()) + } else if (mapValueTypeInfo.isJsonImmutable) { + importer(mapValueTypeInfo.typeAsString, mapValueTypeInfo.modulePath); + mapValue = createNodeFromSource( + `${mapValueTypeInfo.typeAsString}.fromJson(v)`, + ts.SyntaxKind.CallExpression ); + } else { + itemSerializer = `${mapValueTypeInfo.typeAsString}Serializer`; + importer(itemSerializer, findSerializerModule(mapValueTypeInfo)); + importer(mapValueTypeInfo.typeAsString, mapValueTypeInfo.modulePath); mapValue = ts.factory.createIdentifier('i'); } - const collectionAddMethod = - (ts - .getJSDocTags(prop.property) - .filter(t => t.tagName.text === 'json_add') - .map(t => t.comment ?? '')[0] as string); + const collectionAddMethod = prop.jsDocTags + .filter(t => t.tagName.text === 'json_add') + .map(t => t.comment ?? '')[0] as string; caseStatements.push( assignField( ts.factory.createNewExpression( ts.factory.createIdentifier('Map'), - [ - typeChecker.typeToTypeNode(mapType.typeArguments![0], undefined, undefined)!, - typeChecker.typeToTypeNode(mapType.typeArguments![1], undefined, undefined)! - ], + prop.type.typeArguments!.map(t => t.createTypeNode()), [] ) ) @@ -264,7 +211,7 @@ function generateSetPropertyBody( [ itemSerializer.length > 0 && createNodeFromSource( - `const i = new ${mapType.typeArguments![1].symbol.name}();`, + `const i = new ${mapValueTypeInfo.typeAsString}();`, ts.SyntaxKind.VariableStatement ), itemSerializer.length > 0 && @@ -280,15 +227,17 @@ function generateSetPropertyBody( collectionAddMethod ) : ts.factory.createPropertyAccessExpression( - type.isNullable - ? ts.factory.createNonNullExpression(ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('obj'), - ts.factory.createIdentifier(fieldName) - )) - : ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('obj'), - ts.factory.createIdentifier(fieldName) - ), + prop.type.isNullable || prop.type.isOptional + ? ts.factory.createNonNullExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('obj'), + ts.factory.createIdentifier(fieldName) + ) + ) + : ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('obj'), + ts.factory.createIdentifier(fieldName) + ), ts.factory.createIdentifier('set') ), undefined, @@ -305,30 +254,19 @@ function generateSetPropertyBody( ); caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); - } else if (isSet(type.type)) { - const setType = type.type as ts.TypeReference; - if (!isPrimitiveType(setType.typeArguments![0])) { - throw new Error('only Set maps are supported extend if needed!'); - } - - const collectionAddMethod = ts - .getJSDocTags(prop.property) - .filter(t => t.tagName.text === 'json_add') - .map(t => t.comment ?? '')[0] as string; + } else if (prop.type.isSet) { + const collectionAddMethod = prop.jsDocTags + .filter(t => t.tagName.text === 'json_add') + .map(t => t.comment ?? '')[0] as string; - if (isPrimitiveFromJson(setType.typeArguments![0], typeChecker)) { - importer( - setType.typeArguments![0].symbol!.name, - findModule(setType.typeArguments![0], program.getCompilerOptions()) - ); + if (isPrimitiveForSerialization(prop.type.typeArguments![0])) { + const elementTypeName = prop.type.typeArguments![0].typeAsString; - if(collectionAddMethod) { + if (collectionAddMethod) { caseStatements.push( createNodeFromSource( - `for (const i of (v as unknown[])) { - obj.${collectionAddMethod}(i as ${ - setType.typeArguments![0].symbol!.name - }); + `for (const i of (v as ${elementTypeName}[])) { + obj.${collectionAddMethod}(i); }`, ts.SyntaxKind.ForOfStatement ) @@ -337,29 +275,23 @@ function generateSetPropertyBody( caseStatements.push( assignField( createNodeFromSource( - `new Set<${setType.typeArguments![0].symbol!.name}>(v as ${ - setType.typeArguments![0].symbol!.name - }[])!`, + `new Set<${elementTypeName}>(v as ${elementTypeName}[])!`, ts.SyntaxKind.NewExpression ) ) ); } - - } else if (isEnumType(setType.typeArguments![0])) { - importer( - setType.typeArguments![0].symbol!.name, - findModule(setType.typeArguments![0], program.getCompilerOptions()) - ); + } else if (prop.type.typeArguments![0].isEnumType) { + importer(prop.type.typeArguments![0].typeAsString, prop.type.typeArguments![0].modulePath); importer('JsonHelper', '@src/io/JsonHelper'); - if(collectionAddMethod) { + if (collectionAddMethod) { caseStatements.push( createNodeFromSource( `for (const i of (v as number[]) ) { obj.${collectionAddMethod}(JsonHelper.parseEnum<${ - setType.typeArguments![0].symbol!.name - }>(i, ${setType.typeArguments![0].symbol!.name})!); + prop.type.typeArguments![0].typeAsString + }>(i, ${prop.type.typeArguments![0].typeAsString})!); }`, ts.SyntaxKind.ForOfStatement ) @@ -370,45 +302,78 @@ function generateSetPropertyBody( assignField( createNodeFromSource( `new Set<${ - setType.typeArguments![0].symbol!.name + prop.type.typeArguments![0].typeAsString }>( (v! as number[]).map(i => JsonHelper.parseEnum<${ - setType.typeArguments![0].symbol!.name - }>(v, ${setType.typeArguments![0].symbol!.name})!`, + prop.type.typeArguments![0].typeAsString + }>(v, ${prop.type.typeArguments![0].typeAsString})!`, ts.SyntaxKind.NewExpression ) ) ); } + } else { + throw new Error(`Unsupported set type: ${prop.type.typeAsString}`); } caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); - } else if (isImmutable(type.type)) { - let itemSerializer = type.type.symbol.name; - importer(itemSerializer, findModule(type.type, program.getCompilerOptions())); + } else if (prop.type.isJsonImmutable) { + importer(prop.type.typeAsString, prop.type.modulePath); // obj.fieldName = TypeName.fromJson(value)! // return true; caseStatements.push( createNodeFromSource( - `obj.${fieldName} = ${itemSerializer}.fromJson(v)!;`, + `obj.${fieldName} = ${prop.type.typeAsString}.fromJson(v)!;`, ts.SyntaxKind.ExpressionStatement ) ); caseStatements.push( - createNodeFromSource(`return true;`, ts.SyntaxKind.ReturnStatement) + createNodeFromSource('return true;', ts.SyntaxKind.ReturnStatement) ); + } else if (serializable.isStrict) { + const itemSerializer = `${prop.type.typeAsString}Serializer`; + importer(itemSerializer, findSerializerModule(prop.type)); + + if (prop.type.isNullable || prop.type.isOptional) { + importer(prop.type.typeAsString, prop.type.modulePath); + caseStatements.push( + createNodeFromSource( + ` + if (v) { + obj.${fieldName} = new ${prop.type.typeAsString}(); + ${itemSerializer}.fromJson(obj.${fieldName}, v); + } else { + obj.${fieldName} = ${prop.type.isNullable ? 'null' : 'undefined'} + }`, + ts.SyntaxKind.IfStatement + ) + ); + caseStatements.push( + createNodeFromSource('return true;', ts.SyntaxKind.ReturnStatement) + ); + } else { + caseStatements.push( + createNodeFromSource( + `${itemSerializer}.fromJson(obj.${fieldName},v);`, + ts.SyntaxKind.ExpressionStatement + ) + ); + caseStatements.push( + createNodeFromSource('return true;', ts.SyntaxKind.ReturnStatement) + ); + } } else { - // for complex types it is a bit more tricky + // for complex types in non-strict mode it is a bit more tricky // if the property matches exactly, we use fromJson // if the property starts with the field name, we try to set a sub-property const jsonNameArray = ts.factory.createArrayLiteralExpression( jsonNames.map(n => ts.factory.createStringLiteral(n)) ); - let itemSerializer = type.type.symbol.name + 'Serializer'; - importer(itemSerializer, findSerializerModule(type.type, program.getCompilerOptions())); - if (type.isNullable) { - importer(type.type.symbol!.name, findModule(type.type, program.getCompilerOptions())); + const itemSerializer = `${prop.type.typeAsString}Serializer`; + importer(itemSerializer, findSerializerModule(prop.type)); + if (prop.type.isNullable || prop.type.isOptional) { + importer(prop.type.typeAsString, prop.type.modulePath); } // TODO if no partial name support, simply generate cases @@ -425,7 +390,7 @@ function generateSetPropertyBody( ts.factory.createNumericLiteral('0') ), ts.factory.createBlock( - !type.isNullable + !prop.type.isNullable && !prop.type.isOptional ? [ ts.factory.createExpressionStatement( ts.factory.createCallExpression( @@ -456,7 +421,7 @@ function generateSetPropertyBody( [ assignField( ts.factory.createNewExpression( - ts.factory.createIdentifier(type.type.symbol.name), + ts.factory.createIdentifier(prop.type.typeAsString), undefined, [] ) @@ -489,112 +454,105 @@ function generateSetPropertyBody( ts.factory.createReturnStatement(ts.factory.createTrue()) ], true - ), - !prop.partialNames - ? undefined - : ts.factory.createBlock( - [ - // for(const candidate of ["", "core"]) { - // if(candidate.indexOf(property) === 0) { - // if(!this.field) { this.field = new FieldType(); } - // if(this.field.setProperty(property.substring(candidate.length), value)) return true; - // } - // } - ts.factory.createForOfStatement( - undefined, - ts.factory.createVariableDeclarationList( - [ts.factory.createVariableDeclaration('c')], - ts.NodeFlags.Const - ), - jsonNameArray, - ts.factory.createBlock( - [ - ts.factory.createIfStatement( - ts.factory.createBinaryExpression( - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('property'), - 'indexOf' - ), - [], - [ts.factory.createIdentifier('c')] - ), - ts.SyntaxKind.EqualsEqualsEqualsToken, - ts.factory.createNumericLiteral('0') - ), - ts.factory.createBlock( - [ - type.isNullable && - ts.factory.createIfStatement( - ts.factory.createPrefixUnaryExpression( - ts.SyntaxKind.ExclamationToken, - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('obj'), - fieldName - ) - ), - ts.factory.createBlock([ - assignField( - ts.factory.createNewExpression( - ts.factory.createIdentifier( - type.type!.symbol!.name - ), - [], - [] - ) - ) - ]) - ), - ts.factory.createIfStatement( - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier(itemSerializer), - 'setProperty' - ), - [], - [ - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('obj'), - fieldName - ), - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('property'), - 'substring' - ), - [], - [ - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('c'), - 'length' - ) - ] - ), - ts.factory.createIdentifier('v') - ] - ), - ts.factory.createBlock( - [ - ts.factory.createReturnStatement( - ts.factory.createTrue() - ) - ], - true - ) - ) - ].filter(s => !!s) as ts.Statement[], - true - ) - ) - ], - true - ) - ) - ], - true - ) + ) ) ); + + if (prop.partialNames) { + statements.push( + // for(const candidate of ["", "core"]) { + // if(candidate.indexOf(property) === 0) { + // if(!this.field) { this.field = new FieldType(); } + // if(this.field.setProperty(property.substring(candidate.length), value)) return true; + // } + // } + ts.factory.createForOfStatement( + undefined, + ts.factory.createVariableDeclarationList( + [ts.factory.createVariableDeclaration('c')], + ts.NodeFlags.Const + ), + jsonNameArray, + ts.factory.createBlock( + [ + ts.factory.createIfStatement( + ts.factory.createBinaryExpression( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('property'), + 'indexOf' + ), + [], + [ts.factory.createIdentifier('c')] + ), + ts.SyntaxKind.EqualsEqualsEqualsToken, + ts.factory.createNumericLiteral('0') + ), + ts.factory.createBlock( + [ + prop.type.isNullable || + (prop.type.isOptional && + ts.factory.createIfStatement( + ts.factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('obj'), + fieldName + ) + ), + ts.factory.createBlock([ + assignField( + ts.factory.createNewExpression( + ts.factory.createIdentifier(prop.type.typeAsString), + [], + [] + ) + ) + ]) + )), + ts.factory.createIfStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(itemSerializer), + 'setProperty' + ), + [], + [ + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('obj'), + fieldName + ), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('property'), + 'substring' + ), + [], + [ + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('c'), + 'length' + ) + ] + ), + ts.factory.createIdentifier('v') + ] + ), + ts.factory.createBlock( + [ts.factory.createReturnStatement(ts.factory.createTrue())], + true + ) + ) + ].filter(s => !!s) as ts.Statement[], + true + ) + ) + ], + true + ) + ) + ); + } } if (caseStatements.length > 0) { @@ -628,7 +586,7 @@ function generateSetPropertyBody( if (serializable.hasSetPropertyExtension) { statements.push( ts.factory.createReturnStatement( - createNodeFromSource(`obj.setProperty(property, v);`, ts.SyntaxKind.CallExpression) + createNodeFromSource('obj.setProperty(property, v);', ts.SyntaxKind.CallExpression) ) ); } else { @@ -639,9 +597,8 @@ function generateSetPropertyBody( } export function createSetPropertyMethod( - program: ts.Program, input: ts.ClassDeclaration, - serializable: JsonSerializable, + serializable: TypeSchema, importer: (name: string, module: string) => void ) { const methodDecl = createNodeFromSource( @@ -651,5 +608,5 @@ export function createSetPropertyMethod( }`, ts.SyntaxKind.MethodDeclaration ); - return setMethodBody(methodDecl, generateSetPropertyBody(program, serializable, importer)); + return setMethodBody(methodDecl, generateSetPropertyBody(serializable, importer)); } diff --git a/src.compiler/typescript/Serializer.toJson.ts b/src.compiler/typescript/Serializer.toJson.ts index 97dc713eb..8571f37a6 100644 --- a/src.compiler/typescript/Serializer.toJson.ts +++ b/src.compiler/typescript/Serializer.toJson.ts @@ -1,25 +1,9 @@ import * as ts from 'typescript'; -import { - createNodeFromSource, - setMethodBody, - isPrimitiveType, - hasFlag, - getTypeWithNullableInfo, - isTypedArray, - unwrapArrayItemType, - isMap, - isEnumType, - isSet, - isPrimitiveToJson -} from '../BuilderHelpers'; -import { findModule, findSerializerModule, isImmutable, JsonProperty, JsonSerializable } from './Serializer.common'; +import { createNodeFromSource, setMethodBody } from '../BuilderHelpers'; +import { findSerializerModule } from './Serializer.common'; +import type { TypeSchema } from './TypeSchema'; - -function generateToJsonBody( - program: ts.Program, - serializable: JsonSerializable, - importer: (name: string, module: string) => void -) { +function generateToJsonBody(serializable: TypeSchema, importer: (name: string, module: string) => void) { const statements: ts.Statement[] = []; statements.push( @@ -42,8 +26,8 @@ function generateToJsonBody( ) ); - for (let prop of serializable.properties) { - const fieldName = (prop.property.name as ts.Identifier).text; + for (const prop of serializable.properties) { + const fieldName = prop.name; const jsonName = prop.jsonNames.filter(n => n !== '')[0]; if (!jsonName || prop.isReadOnly) { @@ -52,11 +36,7 @@ function generateToJsonBody( let propertyStatements: ts.Statement[] = []; - const typeChecker = program.getTypeChecker(); - const type = getTypeWithNullableInfo(typeChecker, prop.property.type!, prop.asRaw); - const isArray = isTypedArray(type.type!); - - if (prop.asRaw || isPrimitiveToJson(type.type!, typeChecker)) { + if (prop.asRaw || prop.type.isPrimitiveType) { propertyStatements.push( createNodeFromSource( ` @@ -65,8 +45,8 @@ function generateToJsonBody( ts.SyntaxKind.ExpressionStatement ) ); - } else if (isEnumType(type.type!)) { - if (type.isNullable) { + } else if (prop.type.isEnumType) { + if (prop.type.isNullable) { propertyStatements.push( createNodeFromSource( ` @@ -75,6 +55,15 @@ function generateToJsonBody( ts.SyntaxKind.ExpressionStatement ) ); + } else if (prop.type.isOptional) { + propertyStatements.push( + createNodeFromSource( + ` + o.set(${JSON.stringify(jsonName)}, obj.${fieldName} as number|undefined); + `, + ts.SyntaxKind.ExpressionStatement + ) + ); } else { propertyStatements.push( createNodeFromSource( @@ -85,37 +74,52 @@ function generateToJsonBody( ) ); } - } else if (isArray) { - const arrayItemType = unwrapArrayItemType(type.type!, typeChecker)!; - let itemSerializer = arrayItemType.symbol.name + 'Serializer'; - importer(itemSerializer, findSerializerModule(arrayItemType, program.getCompilerOptions())); - if (type.isNullable) { - propertyStatements.push( - createNodeFromSource( - `if(obj.${fieldName} !== null) { + } else if (prop.type.isArray) { + const arrayItemType = prop.type.arrayItemType!; + if (arrayItemType.isOwnType && !arrayItemType.isEnumType) { + const itemSerializer = `${arrayItemType.typeAsString}Serializer`; + importer(itemSerializer, findSerializerModule(arrayItemType)); + if (prop.type.isNullable) { + propertyStatements.push( + createNodeFromSource( + `if(obj.${fieldName} !== null) { o.set(${JSON.stringify(jsonName)}, obj.${fieldName}?.map(i => ${itemSerializer}.toJson(i))); }`, - ts.SyntaxKind.IfStatement - ) - ); + ts.SyntaxKind.IfStatement + ) + ); + } else if (prop.type.isOptional) { + propertyStatements.push( + createNodeFromSource( + `if(obj.${fieldName} !== undefined) { + o.set(${JSON.stringify(jsonName)}, obj.${fieldName}?.map(i => ${itemSerializer}.toJson(i))); + }`, + ts.SyntaxKind.IfStatement + ) + ); + } else { + propertyStatements.push( + createNodeFromSource( + ` + o.set(${JSON.stringify(jsonName)}, obj.${fieldName}.map(i => ${itemSerializer}.toJson(i))); + `, + ts.SyntaxKind.ExpressionStatement + ) + ); + } } else { propertyStatements.push( createNodeFromSource( ` - o.set(${JSON.stringify(jsonName)}, obj.${fieldName}.map(i => ${itemSerializer}.toJson(i))); - `, + o.set(${JSON.stringify(jsonName)}, obj.${fieldName}); + `, ts.SyntaxKind.ExpressionStatement ) ); } - } else if (isMap(type.type)) { - const mapType = type.type as ts.TypeReference; - if (!isPrimitiveType(mapType.typeArguments![0])) { - throw new Error('only Map maps are supported extend if needed!'); - } - + } else if (prop.type.isMap) { let serializeBlock: ts.Block; - if (isPrimitiveToJson(mapType.typeArguments![1], typeChecker)) { + if (prop.type.typeArguments![1].isPrimitiveType) { serializeBlock = createNodeFromSource( `{ const m = new Map(); @@ -126,7 +130,7 @@ function generateToJsonBody( }`, ts.SyntaxKind.Block ); - } else if (isEnumType(mapType.typeArguments![1])) { + } else if (prop.type.typeArguments![1].isEnumType) { serializeBlock = createNodeFromSource( `{ const m = new Map(); @@ -137,9 +141,21 @@ function generateToJsonBody( }`, ts.SyntaxKind.Block ); + } else if (prop.type.typeArguments![1].isJsonImmutable) { + const notNull = !prop.type.typeArguments![1].isNullable ? '!' : ''; + serializeBlock = createNodeFromSource( + `{ + const m = new Map(); + o.set(${JSON.stringify(jsonName)}, m); + for(const [k, v] of obj.${fieldName}!) { + m.set(k.toString(), ${prop.type.typeArguments![1].typeAsString}.toJson(v)${notNull}); + } + }`, + ts.SyntaxKind.Block + ); } else { - const itemSerializer = mapType.typeArguments![1].symbol.name + 'Serializer'; - importer(itemSerializer, findSerializerModule(mapType.typeArguments![1], program.getCompilerOptions())); + const itemSerializer = `${prop.type.typeArguments![1].typeAsString}Serializer`; + importer(itemSerializer, findSerializerModule(prop.type.typeArguments![1])); serializeBlock = createNodeFromSource( `{ @@ -153,13 +169,16 @@ function generateToJsonBody( ); } - if (type.isNullable) { + if (prop.type.isNullable || prop.type.isOptional) { + const nullOrUndefined = prop.type.isNullable + ? ts.factory.createNull() + : ts.factory.createIdentifier('undefined'); propertyStatements.push( ts.factory.createIfStatement( ts.factory.createBinaryExpression( ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), ts.SyntaxKind.ExclamationEqualsEqualsToken, - ts.factory.createNull() + nullOrUndefined ), serializeBlock ) @@ -167,17 +186,12 @@ function generateToJsonBody( } else { propertyStatements.push(serializeBlock); } - } else if (isSet(type.type)) { - const setType = type.type as ts.TypeReference; - if (!isPrimitiveType(setType.typeArguments![0])) { - throw new Error('only Set maps are supported, extend if needed!'); - } - + } else if (prop.type.isSet) { let serializeBlock: ts.Block; - if (isPrimitiveToJson(setType.typeArguments![1], typeChecker)) { + if (prop.type.typeArguments![0].isPrimitiveType) { serializeBlock = createNodeFromSource( `{ - const a:unknown[] = []; + const a:${prop.type.typeArguments![0].typeAsString}[] = []; o.set(${JSON.stringify(jsonName)}, a); for(const v of obj.${fieldName}!) { a.push(v); @@ -185,7 +199,7 @@ function generateToJsonBody( }`, ts.SyntaxKind.Block ); - } else if (isEnumType(setType.typeArguments![0])) { + } else if (prop.type.typeArguments![0].isEnumType) { serializeBlock = createNodeFromSource( `{ const a:number[] = []; @@ -197,16 +211,21 @@ function generateToJsonBody( ts.SyntaxKind.Block ); } else { - throw new Error('only Set maps are supported, extend if needed!'); + throw new Error( + `only Set and Set sets are supported, extend if needed! Found ${prop.type.typeAsString}` + ); } - if (type.isNullable) { + if (prop.type.isNullable || prop.type.isOptional) { + const nullOrUndefined = prop.type.isNullable + ? ts.factory.createNull() + : ts.factory.createIdentifier('undefined'); propertyStatements.push( ts.factory.createIfStatement( ts.factory.createBinaryExpression( ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), ts.SyntaxKind.ExclamationEqualsEqualsToken, - ts.factory.createNull() + nullOrUndefined ), serializeBlock ) @@ -214,20 +233,45 @@ function generateToJsonBody( } else { propertyStatements.push(serializeBlock); } - } else if (isImmutable(type.type)) { - let itemSerializer = type.type.symbol.name; - importer(itemSerializer, findModule(type.type, program.getCompilerOptions())); + } else if (prop.type.isJsonImmutable) { + importer(prop.type.typeAsString, prop.type.modulePath); + const notNull = !prop.type.isNullable ? '!' : ''; propertyStatements.push( createNodeFromSource( ` - o.set(${JSON.stringify(jsonName)}, ${itemSerializer}.toJson(obj.${fieldName})); + o.set(${JSON.stringify(jsonName)}, ${prop.type.typeAsString}.toJson(obj.${fieldName})${notNull}); `, ts.SyntaxKind.ExpressionStatement ) ); + } else if (serializable.isStrict) { + const itemSerializer = `${prop.type.typeAsString}Serializer`; + importer(itemSerializer, findSerializerModule(prop.type)); + + if (prop.type.isNullable || prop.type.isOptional) { + propertyStatements.push( + createNodeFromSource( + ` + if(obj.${fieldName}) { + o.set(${JSON.stringify(jsonName)}, ${itemSerializer}.toJson(obj.${fieldName})); + } + `, + ts.SyntaxKind.IfStatement + ) + ); + } else { + propertyStatements.push( + createNodeFromSource( + ` + o.set(${JSON.stringify(jsonName)}, ${itemSerializer}.toJson(obj.${fieldName})); + `, + ts.SyntaxKind.ExpressionStatement + ) + ); + } } else { - let itemSerializer = type.type.symbol.name + 'Serializer'; - importer(itemSerializer, findSerializerModule(type.type, program.getCompilerOptions())); + const itemSerializer = `${prop.type.typeAsString}Serializer`; + importer(itemSerializer, findSerializerModule(prop.type)); propertyStatements.push( createNodeFromSource( ` @@ -249,7 +293,7 @@ function generateToJsonBody( if (serializable.hasToJsonExtension) { statements.push( - createNodeFromSource(`obj.toJson(o);`, ts.SyntaxKind.ExpressionStatement) + createNodeFromSource('obj.toJson(o);', ts.SyntaxKind.ExpressionStatement) ); } @@ -259,9 +303,8 @@ function generateToJsonBody( } export function createToJsonMethod( - program: ts.Program, input: ts.ClassDeclaration, - serializable: JsonSerializable, + serializable: TypeSchema, importer: (name: string, module: string) => void ) { const methodDecl = createNodeFromSource( @@ -271,5 +314,5 @@ export function createToJsonMethod( }`, ts.SyntaxKind.MethodDeclaration ); - return setMethodBody(methodDecl, generateToJsonBody(program, serializable, importer)); + return setMethodBody(methodDecl, generateToJsonBody(serializable, importer)); } diff --git a/src.compiler/typescript/SerializerEmitter.ts b/src.compiler/typescript/SerializerEmitter.ts index 1a60acd4f..2de44a4ec 100644 --- a/src.compiler/typescript/SerializerEmitter.ts +++ b/src.compiler/typescript/SerializerEmitter.ts @@ -3,14 +3,13 @@ * any data models to and from JSON following certain rules. */ -import * as path from 'path'; -import * as ts from 'typescript'; +import path from 'node:path'; +import ts from 'typescript'; import createEmitter from './EmitterBase'; -import { JsonProperty, JsonSerializable, toImportPath } from './Serializer.common'; import { createSetPropertyMethod } from './Serializer.setProperty'; import { createFromJsonMethod } from './Serializer.fromJson'; import { createToJsonMethod } from './Serializer.toJson'; - +import { buildTypeSchema, toImportPath } from './TypeSchema'; export default createEmitter('json', (program, input) => { console.log(`Writing Serializer for ${input.name!.text}`); @@ -19,50 +18,7 @@ export default createEmitter('json', (program, input) => { path.resolve(input.getSourceFile().fileName) ); - const serializable: JsonSerializable = { - properties: [], - isStrict: !!ts.getJSDocTags(input).find(t => t.tagName.text === 'json_strict'), - hasToJsonExtension: false, - hasSetPropertyExtension: false - }; - - input.members.forEach(member => { - if (ts.isPropertyDeclaration(member)) { - const propertyDeclaration = member as ts.PropertyDeclaration; - if ( - !propertyDeclaration.modifiers!.find( - m => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.PrivateKeyword - ) - ) { - const jsonNames = [(member.name as ts.Identifier).text.toLowerCase()]; - - if (ts.getJSDocTags(member).find(t => t.tagName.text === 'json_on_parent')) { - jsonNames.push(''); - } - - if (!ts.getJSDocTags(member).find(t => t.tagName.text === 'json_ignore')) { - serializable.properties.push({ - property: propertyDeclaration, - jsonNames: jsonNames, - asRaw: !!ts.getJSDocTags(member).find(t => t.tagName.text === 'json_raw'), - partialNames: !!ts.getJSDocTags(member).find(t => t.tagName.text === 'json_partial_names'), - target: ts.getJSDocTags(member).find(t => t.tagName.text === 'target')?.comment as string, - isReadOnly: !!ts.getJSDocTags(member).find(t => t.tagName.text === 'json_read_only') - }); - } - } - } - else if (ts.isMethodDeclaration(member)) { - switch ((member.name as ts.Identifier).text) { - case 'toJson': - serializable.hasToJsonExtension = true; - break; - case 'setProperty': - serializable.hasSetPropertyExtension = true; - break; - } - } - }); + const serializable = buildTypeSchema(program, input); const statements: ts.Statement[] = []; @@ -90,13 +46,13 @@ export default createEmitter('json', (program, input) => { statements.push( ts.factory.createClassDeclaration( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - input.name!.text + 'Serializer', + `${input.name!.text}Serializer`, undefined, undefined, [ createFromJsonMethod(input, serializable, importer), - createToJsonMethod(program, input, serializable, importer), - createSetPropertyMethod(program, input, serializable, importer) + createToJsonMethod(input, serializable, importer), + createSetPropertyMethod(input, serializable, importer) ] ) ); @@ -109,7 +65,11 @@ export default createEmitter('json', (program, input) => { false, undefined, ts.factory.createNamedImports([ - ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(input.name!.text)) + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(input.name!.text) + ) ]) ), ts.factory.createStringLiteral(toImportPath(sourceFileName)) diff --git a/src.compiler/typescript/TypeSchema.ts b/src.compiler/typescript/TypeSchema.ts new file mode 100644 index 000000000..cbef0dd60 --- /dev/null +++ b/src.compiler/typescript/TypeSchema.ts @@ -0,0 +1,417 @@ +import path from 'node:path'; +import ts from 'typescript'; +import url from 'node:url'; +import { isEnumType, isNumberType, isPrimitiveType } from '../BuilderHelpers'; + +export type TypeWithNullableInfo = { + readonly isNullable: boolean; + readonly isOptional: boolean; + readonly isUnionType: boolean; + readonly isPrimitiveType: boolean; + readonly isEnumType: boolean; + readonly isOwnType: boolean; + readonly isTypedArray: boolean; + readonly typeAsString: string; + readonly modulePath: string; + readonly isCloneable: boolean; + readonly isJsonImmutable: boolean; + readonly isNumberType: boolean; + readonly isMap: boolean; + readonly isSet: boolean; + readonly isArray: boolean; + readonly arrayItemType?: TypeWithNullableInfo; + readonly typeArguments?: readonly TypeWithNullableInfo[]; + readonly unionTypes?: readonly TypeWithNullableInfo[]; + readonly jsDocTags?: readonly ts.JSDocTag[]; + createTypeNode(): ts.TypeNode; +}; + +type Writeable = { -readonly [P in keyof T]: T[P] }; + +export function buildTypeSchema(program: ts.Program, input: ts.ClassDeclaration) { + const schema: TypeSchema = { + properties: [], + isStrict: !!ts.getJSDocTags(input).find(t => t.tagName.text === 'json_strict'), + hasToJsonExtension: false, + hasSetPropertyExtension: false + }; + + const handleMember = ( + member: ts.ClassDeclaration['members'][0], + typeArgumentMapping: Map | undefined + ) => { + if (ts.isPropertyDeclaration(member)) { + const propertyDeclaration = member as ts.PropertyDeclaration; + if ( + !propertyDeclaration.modifiers!.find( + m => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.PrivateKeyword + ) + ) { + const jsonNames = [(member.name as ts.Identifier).text.toLowerCase()]; + const jsDoc = ts.getJSDocTags(member); + + if (jsDoc.find(t => t.tagName.text === 'json_on_parent')) { + jsonNames.push(''); + } + + if (!jsDoc.find(t => t.tagName.text === 'json_ignore')) { + const asRaw = !!jsDoc.find(t => t.tagName.text === 'json_raw'); + const isReadonly = !!jsDoc.find(t => t.tagName.text === 'json_read_only'); + schema.properties.push({ + jsonNames: jsonNames, + 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'), + name: (member.name as ts.Identifier).text, + jsDocTags: jsDoc, + type: getTypeWithNullableInfo( + program, + member.type!, + asRaw || isReadonly, + !!member.questionToken, + typeArgumentMapping + ) + }); + } + } + } else if (ts.isMethodDeclaration(member)) { + switch ((member.name as ts.Identifier).text) { + case 'toJson': + schema.hasToJsonExtension = true; + break; + case 'setProperty': + schema.hasSetPropertyExtension = true; + break; + } + } + }; + + let hierarchy: ts.ClassDeclaration | undefined = input; + let typeArgumentMapping: Map | undefined; + const checker = program.getTypeChecker(); + while (hierarchy) { + for (const x of hierarchy.members) { + handleMember(x, typeArgumentMapping); + } + + const extendsClause = hierarchy.heritageClauses?.find(c => c.token === ts.SyntaxKind.ExtendsKeyword); + if (extendsClause?.types.length === 1) { + const baseType = checker.getTypeAtLocation(extendsClause.types[0]); + if (baseType.symbol?.valueDeclaration && ts.isClassDeclaration(baseType.symbol?.valueDeclaration)) { + hierarchy = baseType.symbol?.valueDeclaration; + + if (extendsClause.types[0].typeArguments) { + typeArgumentMapping = new Map(); + + const typeArgs = extendsClause.types[0].typeArguments; + for (let i = 0; i < typeArgs.length; i++) { + typeArgumentMapping.set( + hierarchy.typeParameters![i].name.text, + checker.getTypeAtLocation(typeArgs[i]) + ); + } + } else { + typeArgumentMapping = undefined; + } + } else { + hierarchy = undefined; + } + } else { + hierarchy = undefined; + } + } + + return schema; +} + +export function getTypeWithNullableInfo( + program: ts.Program, + node: ts.TypeNode | ts.Type, + allowUnion: boolean, + isOptionalFromDeclaration: boolean, + typeArgumentMapping: Map | undefined +): TypeWithNullableInfo { + const checker = program.getTypeChecker(); + + let typeInfo: Writeable = { + isNullable: false, + isOptional: isOptionalFromDeclaration, + isUnionType: false, + isPrimitiveType: false, + isEnumType: false, + isTypedArray: false, + isOwnType: false, + typeAsString: '', + modulePath: '', + isJsonImmutable: false, + isNumberType: false, + isMap: false, + isSet: false, + isArray: false, + isCloneable: false, + createTypeNode() { + return undefined!; + } + }; + let mainType: ts.Type | undefined; + + const fillBaseInfoFrom = (tsType: ts.Type) => { + const valueDeclaration = tsType.symbol?.valueDeclaration; + mainType = tsType; + + typeInfo.typeAsString = checker.typeToString(tsType, undefined, undefined); + typeInfo.modulePath = findModule(tsType, program.getCompilerOptions()); + typeInfo.isOwnType = + !!typeInfo.modulePath && !typeInfo.modulePath.includes('node_modules') && !!valueDeclaration; + + if (isEnumType(tsType)) { + typeInfo.isEnumType = true; + } else if (isPrimitiveType(tsType)) { + typeInfo.isNumberType = isNumberType(tsType); + typeInfo.isPrimitiveType = true; + } else if (typeInfo.isOwnType) { + typeInfo.jsDocTags = valueDeclaration ? ts.getJSDocTags(valueDeclaration) : undefined; + if (typeInfo.jsDocTags) { + typeInfo.isJsonImmutable = !!typeInfo.jsDocTags.find(t => t.tagName.text === 'json_immutable'); + typeInfo.isCloneable = !!typeInfo.jsDocTags.find(t => t.tagName.text === 'cloneable'); + } + + if (tsType.flags & ts.ObjectFlags.Reference) { + typeInfo.typeArguments = (tsType as ts.TypeReference).typeArguments?.map(p => + getTypeWithNullableInfo(program, p, allowUnion, false, typeArgumentMapping) + ); + } + } else if (checker.isArrayType(tsType)) { + typeInfo.isArray = true; + typeInfo.arrayItemType = getTypeWithNullableInfo( + program, + (tsType as ts.TypeReference).typeArguments![0], + allowUnion, + false, + typeArgumentMapping + ); + } else if (tsType.symbol) { + typeInfo.typeAsString = tsType.symbol.name; + switch (tsType.symbol.name) { + case 'Uint8Array': + case 'Uint16Array': + case 'Uint32Array': + case 'Int8Array': + case 'Int16Array': + case 'Int32Array': + case 'Float32Array': + case 'Float64Array': + typeInfo.isTypedArray = true; + typeInfo.arrayItemType = { + isNullable: false, + isOptional: false, + isUnionType: false, + isPrimitiveType: true, + isEnumType: false, + isOwnType: false, + isTypedArray: false, + typeAsString: 'number', + modulePath: '', + isCloneable: false, + isJsonImmutable: false, + isNumberType: true, + isMap: false, + isSet: false, + isArray: false, + createTypeNode(): ts.TypeNode { + return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); + } + }; + break; + case 'Map': + typeInfo.isMap = true; + typeInfo.typeArguments = (tsType as ts.TypeReference).typeArguments!.map(p => + getTypeWithNullableInfo(program, p, allowUnion, false, typeArgumentMapping) + ); + break; + case 'Set': + typeInfo.isSet = true; + typeInfo.typeArguments = (tsType as ts.TypeReference).typeArguments!.map(p => + getTypeWithNullableInfo(program, p, allowUnion, false, typeArgumentMapping) + ); + break; + default: + if (tsType.isTypeParameter()) { + if (typeArgumentMapping?.has(typeInfo.typeAsString)) { + typeInfo = getTypeWithNullableInfo( + program, + typeArgumentMapping.get(typeInfo.typeAsString)!, + allowUnion, + false, + typeArgumentMapping + ); + } else { + throw new Error(`Unresolved type parameters ${typeInfo.typeAsString}`); + } + } else if ((tsType as ts.Type).flags & ts.ObjectFlags.Reference) { + typeInfo.typeArguments = (tsType as ts.TypeReference).typeArguments?.map(p => + getTypeWithNullableInfo(program, p, allowUnion, false, typeArgumentMapping) + ); + } + break; + } + } + }; + + if ('kind' in node) { + if (ts.isUnionTypeNode(node)) { + for (const t of node.types) { + if (t.kind === ts.SyntaxKind.NullKeyword) { + typeInfo.isNullable = true; + } else if (ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword) { + typeInfo.isNullable = true; + } else if (t.kind === ts.SyntaxKind.UndefinedKeyword) { + typeInfo.isOptional = true; + } else if (ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.UndefinedKeyword) { + typeInfo.isOptional = true; + } else if (!mainType) { + mainType = checker.getTypeFromTypeNode(t); + } else if (allowUnion) { + if (!typeInfo.unionTypes) { + typeInfo.unionTypes = [ + getTypeWithNullableInfo(program, mainType, false, false, typeArgumentMapping) + ]; + } + + (typeInfo.unionTypes as TypeWithNullableInfo[]).push( + getTypeWithNullableInfo(program, t, false, false, typeArgumentMapping) + ); + } else { + throw new Error( + `Multi union types on not supported at this location: ${node.getSourceFile().fileName}:${node.getText()}` + ); + } + } + + if (!typeInfo.unionTypes && mainType) { + fillBaseInfoFrom(mainType); + } + } else { + fillBaseInfoFrom(checker.getTypeFromTypeNode(node)); + } + } else { + // use typeArgumentMapping + if (isPrimitiveType(node) || isEnumType(node)) { + fillBaseInfoFrom(node); + } else if (node.isUnion()) { + for (const t of node.types) { + if ((t.flags & ts.TypeFlags.Null) !== 0) { + typeInfo.isNullable = true; + } else if ((t.flags & ts.TypeFlags.Undefined) !== 0) { + typeInfo.isOptional = true; + } else if (!mainType) { + fillBaseInfoFrom(t); + } else if (allowUnion) { + typeInfo.unionTypes ??= []; + (typeInfo.unionTypes as TypeWithNullableInfo[]).push( + getTypeWithNullableInfo(program, t, false, false, typeArgumentMapping) + ); + } else { + throw new Error(`Multi union types on not supported at this location: ${typeInfo.typeAsString}`); + } + } + } else { + fillBaseInfoFrom(node); + } + } + + const applyNullableAndUndefined = (type: ts.TypeNode): ts.TypeNode => { + if (typeInfo.isNullable || typeInfo.isOptional) { + const types: ts.TypeNode[] = [type]; + + if (typeInfo.isNullable) { + types.push(ts.factory.createLiteralTypeNode(ts.factory.createNull())); + } + if (typeInfo.isOptional) { + types.push(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword)); + } + + return ts.factory.createUnionTypeNode(types); + } + + return type; + }; + + if (typeInfo.unionTypes) { + typeInfo.createTypeNode = () => { + const types: ts.TypeNode[] = typeInfo.unionTypes!.map(t => t.createTypeNode()); + if (typeInfo.isNullable) { + types.push(ts.factory.createLiteralTypeNode(ts.factory.createNull())); + } + if (typeInfo.isOptional) { + types.push(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword)); + } + return ts.factory.createUnionTypeNode(types); + }; + } else if (typeInfo.isPrimitiveType) { + typeInfo.createTypeNode = () => { + return applyNullableAndUndefined(checker.typeToTypeNode(mainType!, undefined, undefined)!); + }; + } else if (typeInfo.isArray) { + typeInfo.createTypeNode = () => { + return applyNullableAndUndefined(ts.factory.createArrayTypeNode(typeInfo.arrayItemType!.createTypeNode())); + }; + } else { + typeInfo.createTypeNode = () => { + return applyNullableAndUndefined( + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier(typeInfo.typeAsString), + typeInfo.typeArguments?.map(t => t.createTypeNode()) + ) + ); + }; + } + + return typeInfo; +} + +export interface TypeProperty { + partialNames: boolean; + name: string; + jsDocTags: readonly ts.JSDocTag[]; + type: TypeWithNullableInfo; + jsonNames: string[]; + asRaw: boolean; + target?: string; + isReadOnly: boolean; +} + +export interface TypeSchema { + isStrict: boolean; + hasToJsonExtension: boolean; + hasSetPropertyExtension: boolean; + properties: TypeProperty[]; +} + +function removeExtension(fileName: string) { + return fileName.substring(0, fileName.lastIndexOf('.')); +} + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); +const root = path.resolve(__dirname, '..', '..'); + +export function toImportPath(fileName: string) { + return `@${removeExtension(path.relative(root, fileName)).split('\\').join('/')}`; +} + +function findModule(type: ts.Type, options: ts.CompilerOptions) { + if (type.symbol && type.symbol.declarations) { + for (const decl of type.symbol.declarations) { + const file = decl.getSourceFile(); + if (file) { + const relative = path.relative(path.join(path.resolve(options.baseUrl!)), path.resolve(file.fileName)); + return toImportPath(relative); + } + } + + return `./${type.symbol.name}`; + } + + return ''; +} diff --git a/src.csharp/AlphaTab.Test/AlphaTab.Test.csproj b/src.csharp/AlphaTab.Test/AlphaTab.Test.csproj index 4853a734a..4a6e8d14e 100644 --- a/src.csharp/AlphaTab.Test/AlphaTab.Test.csproj +++ b/src.csharp/AlphaTab.Test/AlphaTab.Test.csproj @@ -7,17 +7,17 @@ enable 10 true - $(NoWarn);0659;0168 + $(NoWarn);0659;0168;1717;1998 - - - - - - + + + + + + diff --git a/src.csharp/AlphaTab.Test/GlobalUsings.cs b/src.csharp/AlphaTab.Test/GlobalUsings.cs index 0d9f4576b..7c9d319fd 100644 --- a/src.csharp/AlphaTab.Test/GlobalUsings.cs +++ b/src.csharp/AlphaTab.Test/GlobalUsings.cs @@ -2,3 +2,6 @@ global using AlphaTab.Core; global using AlphaTab.Core.EcmaScript; global using AlphaTab.Test; + +global using Error = System.Exception; +global using Disposable = System.IDisposable; diff --git a/src.csharp/AlphaTab.Test/Model/ComparisonHelpers.cs b/src.csharp/AlphaTab.Test/Model/ComparisonHelpers.cs index c6596ce16..8f0d2819c 100644 --- a/src.csharp/AlphaTab.Test/Model/ComparisonHelpers.cs +++ b/src.csharp/AlphaTab.Test/Model/ComparisonHelpers.cs @@ -1,6 +1,4 @@ using System.Collections.Generic; -using AlphaTab.Collections; -using AlphaTab.Test; namespace AlphaTab.Model; diff --git a/src.csharp/AlphaTab.Test/ScoreSerializerPlugin.cs b/src.csharp/AlphaTab.Test/ScoreSerializerPlugin.cs new file mode 100644 index 000000000..72b8960e1 --- /dev/null +++ b/src.csharp/AlphaTab.Test/ScoreSerializerPlugin.cs @@ -0,0 +1,9 @@ +namespace AlphaTab; + +partial class ScoreSerializerPlugin +{ + private static bool IsPlatformTypeEqual(object? a, object? b) + { + throw new Error("Unexpected value in serialized json" + a?.GetType().FullName); + } +} diff --git a/src.csharp/AlphaTab.Test/SnapshotFileAttribute.cs b/src.csharp/AlphaTab.Test/SnapshotFileAttribute.cs new file mode 100644 index 000000000..99028217d --- /dev/null +++ b/src.csharp/AlphaTab.Test/SnapshotFileAttribute.cs @@ -0,0 +1,15 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace AlphaTab.Test; + +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +sealed class SnapshotFileAttribute : Attribute +{ + public string Path { get; } + + public SnapshotFileAttribute(string path) + { + Path = path; + } +} diff --git a/src.csharp/AlphaTab.Test/Test/Globals.cs b/src.csharp/AlphaTab.Test/Test/Globals.cs index 9e24936db..4586caef6 100644 --- a/src.csharp/AlphaTab.Test/Test/Globals.cs +++ b/src.csharp/AlphaTab.Test/Test/Globals.cs @@ -1,10 +1,14 @@ using System; using System.Collections; +using System.IO; +using System.Reflection; +using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace AlphaTab.Test; #pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. -public static class assert +// ReSharper disable once InconsistentNaming +internal static class assert #pragma warning restore CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. { public static void Fail(string message) @@ -13,7 +17,7 @@ public static void Fail(string message) } } -public static class TestGlobals +internal static class TestGlobals { public static Expector Expect(T actual) { @@ -29,9 +33,16 @@ public static void Fail(object? message) { Assert.Fail(Convert.ToString(message)); } + + internal static AsyncLocal SnapshotAssertionCounter { get; } + static TestGlobals() + { + SnapshotAssertionCounter = new AsyncLocal(); + TestMethodAttribute.GlobalBeforeTest += () => { SnapshotAssertionCounter.Value = 0; }; + } } -public class NotExpector +internal class NotExpector { private readonly T _actual; public NotExpector Be => this; @@ -54,7 +65,7 @@ public void Ok() } } -public class Expector +internal class Expector { private readonly T _actual; @@ -64,7 +75,12 @@ public Expector(T actual) } public Expector To => this; - public NotExpector Not => new(_actual); + + public NotExpector Not() + { + return new NotExpector(_actual); + } + public Expector Be => this; public Expector Have => this; @@ -74,6 +90,7 @@ public void Equal(object? expected, string? message = null) { expected = (double)i; } + if (expected is double d && _actual is int) { expected = (int)d; @@ -82,15 +99,26 @@ public void Equal(object? expected, string? message = null) Assert.AreEqual(expected, _actual, message); } + public void LessThan(double expected) + { + if (_actual is IComparable d) + { + Assert.IsTrue(d.CompareTo(expected) < 0, $"Expected Expected[{d}] < Actual[{_actual}]"); + } + } + public void GreaterThan(double expected) { if (_actual is int i) { - Assert.IsTrue(i.CompareTo(expected) > 0, $"Expected {expected} to be greater than {_actual}"); + Assert.IsTrue(i.CompareTo(expected) > 0, + $"Expected {expected} to be greater than {_actual}"); } + if (_actual is double d) { - Assert.IsTrue(d.CompareTo(expected) > 0, $"Expected {expected} to be greater than {_actual}"); + Assert.IsTrue(d.CompareTo(expected) > 0, + $"Expected {expected} to be greater than {_actual}"); } } @@ -121,9 +149,10 @@ public void Ok() { Assert.AreNotEqual(default!, _actual); } + public void Length(int length) { - if(_actual is ICollection collection) + if (_actual is ICollection collection) { Assert.AreEqual(length, collection.Count); } @@ -135,7 +164,7 @@ public void Length(int length) public void Contain(object element) { - if(_actual is ICollection collection) + if (_actual is ICollection collection) { CollectionAssert.Contains(collection, element); } @@ -170,7 +199,6 @@ public void False() } - public void Throw(Type expected) { if (_actual is Action d) @@ -195,4 +223,41 @@ public void Throw(Type expected) Assert.Fail("ToThrowError can only be used with an exception"); } } -} \ No newline at end of file + + public void ToMatchSnapshot(string hint = "") + { + var testMethodInfo = TestMethodAccessor.CurrentTest; + Assert.IsNotNull(testMethodInfo, + "No information about current test available, cannot find test snapshot"); + + var file = testMethodInfo.MethodInfo.GetCustomAttribute()?.Path; + if (string.IsNullOrEmpty(file)) + { + Assert.Fail("Missing SnapshotFileAttribute with path to .snap file"); + } + + var absoluteSnapFilePath = Path.GetFullPath(Path.Join( + TestPlatform.RepositoryRoot.Value, + file + )); + if (!File.Exists(absoluteSnapFilePath)) + { + Assert.Fail("Could not find snapshot file at " + absoluteSnapFilePath); + } + + var snapshotFile = SnapshotFileRepository.LoadSnapshortFile(absoluteSnapFilePath); + + var testSuiteName = testMethodInfo.MethodInfo.DeclaringType!.Name; + var testName = testMethodInfo.MethodInfo.GetCustomAttribute()!.DisplayName; + + var snapshortName = $"{testSuiteName} {testName} {++TestGlobals.SnapshotAssertionCounter.Value}"; + + var error = snapshotFile.Match(snapshortName, _actual); + if (!string.IsNullOrEmpty(error)) + { + Assert.Fail(error); + } + } +} + + diff --git a/src.csharp/AlphaTab.Test/Test/PrettyFormat.cs b/src.csharp/AlphaTab.Test/Test/PrettyFormat.cs new file mode 100644 index 000000000..491c8dbd9 --- /dev/null +++ b/src.csharp/AlphaTab.Test/Test/PrettyFormat.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace AlphaTab; + +partial class PrettyFormat +{ + public static IEnumerable> MapAsUnknownIterable(object map) + { + if (map is IDictionary mapAsUnknownIterable) + { + foreach (DictionaryEntry v in mapAsUnknownIterable) + { + yield return new ArrayTuple(v.Key, v.Value); + } + } + else + { + throw new ArgumentException("Provided value was no map", nameof(map)); + } + } +} diff --git a/src.csharp/AlphaTab.Test/TestClassAttribute.cs b/src.csharp/AlphaTab.Test/TestClassAttribute.cs new file mode 100644 index 000000000..a56bf1377 --- /dev/null +++ b/src.csharp/AlphaTab.Test/TestClassAttribute.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +// ReSharper disable once CheckNamespace +namespace AlphaTab.Test; + +public class TestClassAttribute : Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute +{ +} + +public class TestMethodAttribute : Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute +{ + public TestMethodAttribute(string displayName) : base(displayName) + { + } + + public override TestResult[] Execute(ITestMethod testMethod) + { + TestMethodAccessor.CurrentTest = testMethod; + GlobalBeforeTest?.Invoke(); + var result = base.Execute(testMethod); + TestMethodAccessor.CurrentTest = null; + return result; + } + + public static event Action? GlobalBeforeTest; +} + +public static class TestMethodAccessor +{ + private static readonly AsyncLocal TestContextLocal = new(); + + public static ITestMethod? CurrentTest + { + get => TestContextLocal.Value; + set => TestContextLocal.Value = value; + } +} diff --git a/src.csharp/AlphaTab.Test/TestPlatform.cs b/src.csharp/AlphaTab.Test/TestPlatform.cs index 2435083ca..db8ae5914 100644 --- a/src.csharp/AlphaTab.Test/TestPlatform.cs +++ b/src.csharp/AlphaTab.Test/TestPlatform.cs @@ -8,7 +8,7 @@ namespace AlphaTab; static partial class TestPlatform { - private static readonly Lazy RepositoryRoot = new(() => + public static readonly Lazy RepositoryRoot = new(() => { var currentDir = new DirectoryInfo(System.Environment.CurrentDirectory); while (currentDir != null) @@ -21,17 +21,28 @@ static partial class TestPlatform currentDir = currentDir.Parent; } - throw new IOException($"Could not find repository root via working dir {System.Environment.CurrentDirectory}"); + throw new IOException( + $"Could not find repository root via working dir {System.Environment.CurrentDirectory}"); }); public static async Task LoadFile(string path) { - await using var fs = new FileStream(Path.Combine(RepositoryRoot.Value, path), FileMode.Open); + await using var fs = + new FileStream(Path.Combine(RepositoryRoot.Value, path), FileMode.Open); await using var ms = new MemoryStream(); await fs.CopyToAsync(ms); return new Uint8Array(ms.ToArray()); } + public static Uint8Array LoadFileSync(string path) + { + using var fs = + new FileStream(Path.Combine(RepositoryRoot.Value, path), FileMode.Open); + using var ms = new MemoryStream(); + fs.CopyTo(ms); + return new Uint8Array(ms.ToArray()); + } + public static async Task SaveFile(string name, Uint8Array data) { var path = Path.Combine(RepositoryRoot.Value, name); @@ -42,7 +53,8 @@ public static async Task SaveFile(string name, Uint8Array data) public static Task> ListDirectory(string path) { - return Task.FromResult((IList) Directory.EnumerateFiles(Path.Combine(RepositoryRoot.Value, path)) + return Task.FromResult((IList)Directory + .EnumerateFiles(Path.Combine(RepositoryRoot.Value, path)) .Select(Path.GetFileName) .ToList()); } @@ -58,6 +70,12 @@ public static Task DeleteFile(string path) { File.Delete(path); } + return Task.CompletedTask; } + + public static T[] EnumValues(Type type) where T : struct, Enum + { + return Enum.GetValues(); + } } diff --git a/src.csharp/AlphaTab.Windows/AlphaTab.Windows.csproj b/src.csharp/AlphaTab.Windows/AlphaTab.Windows.csproj index 85409285a..0ee4f0b72 100644 --- a/src.csharp/AlphaTab.Windows/AlphaTab.Windows.csproj +++ b/src.csharp/AlphaTab.Windows/AlphaTab.Windows.csproj @@ -27,7 +27,7 @@ - + diff --git a/src.csharp/AlphaTab.Windows/NAudioSynthOutput.cs b/src.csharp/AlphaTab.Windows/NAudioSynthOutput.cs index 60318c669..c7d03e940 100644 --- a/src.csharp/AlphaTab.Windows/NAudioSynthOutput.cs +++ b/src.csharp/AlphaTab.Windows/NAudioSynthOutput.cs @@ -1,4 +1,9 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using AlphaTab.Core; using AlphaTab.Synth; using AlphaTab.Synth.Ds; using AlphaTab.Core.EcmaScript; @@ -14,11 +19,13 @@ public class NAudioSynthOutput : WaveProvider32, ISynthOutput, IDisposable { private const int BufferSize = 4096; private const int PreferredSampleRate = 44100; + private const int DirectSoundLatency = 40; - private DirectSoundOut _context; + private DirectSoundOut? _context; private CircularSampleBuffer _circularBuffer; private int _bufferCount; private int _requestedBufferCount; + private ISynthOutputDevice? _device; /// public double SampleRate => PreferredSampleRate; @@ -29,7 +36,6 @@ public class NAudioSynthOutput : WaveProvider32, ISynthOutput, IDisposable public NAudioSynthOutput() : base(PreferredSampleRate, (int)SynthConstants.AudioChannels) { - _context = null!; _circularBuffer = null!; } @@ -42,8 +48,7 @@ public void Activate() /// public void Open(double bufferTimeInMilliseconds) { - var latency = 40; - _context = new DirectSoundOut(latency); + _context = new DirectSoundOut(DirectSoundLatency); _context.Init(this); // NAudio introduces another level of buffering and latency @@ -76,7 +81,7 @@ public void Dispose() /// public void Close() { - _context.Stop(); + _context!.Stop(); _circularBuffer.Clear(); _context.Dispose(); } @@ -85,13 +90,13 @@ public void Close() public void Play() { RequestBuffers(); - _context.Play(); + _context!.Play(); } /// public void Pause() { - _context.Pause(); + _context!.Pause(); } /// @@ -156,5 +161,67 @@ public override int Read(float[] buffer, int offset, int count) /// public IEventEmitter SampleRequest { get; } = new EventEmitter(); + + /// + public Task> EnumerateOutputDevices() + { + var defaultPlayback = DirectSoundOut.DSDEVID_DefaultPlayback; + GetDeviceID(ref defaultPlayback, out Guid realDefault); + + return Task.FromResult( + DirectSoundOut.Devices + .Where(d => d.Guid != Guid.Empty) + .Map(d => (ISynthOutputDevice)new NAudioOutputDevice(d, + realDefault)) + ); + } + + [DllImport("dsound.dll", CharSet = CharSet.Unicode, + CallingConvention = CallingConvention.StdCall, SetLastError = true, + PreserveSig = false)] + private static extern void GetDeviceID(ref Guid pGuidSrc, out Guid pGuidDest); + + + /// + public Task SetOutputDevice(ISynthOutputDevice? device) + { + if (_context != null) + { + _context.Stop(); + _circularBuffer.Clear(); + _context.Dispose(); + } + + _context = new DirectSoundOut( + device == null + ? DirectSoundOut.DSDEVID_DefaultPlayback + : ((NAudioOutputDevice)device).Device.Guid, + DirectSoundLatency); + _device = device; + _context.Init(this); + + return Task.CompletedTask; + } + + /// + public Task GetOutputDevice() + { + return Task.FromResult(_device); + } + } + + internal class NAudioOutputDevice : ISynthOutputDevice + { + public DirectSoundDeviceInfo Device { get; } + + public NAudioOutputDevice(DirectSoundDeviceInfo device, Guid defaultDevice) + { + Device = device; + IsDefault = device.Guid == defaultDevice; + } + + public string DeviceId => Device.Guid.ToString("N"); + public string Label => Device.Description; + public bool IsDefault { get; } } } diff --git a/src.csharp/AlphaTab.Windows/WinForms/AlphaTabLayoutPanel.cs b/src.csharp/AlphaTab.Windows/WinForms/AlphaTabLayoutPanel.cs index faefe961d..4c6d142a2 100644 --- a/src.csharp/AlphaTab.Windows/WinForms/AlphaTabLayoutPanel.cs +++ b/src.csharp/AlphaTab.Windows/WinForms/AlphaTabLayoutPanel.cs @@ -1,6 +1,4 @@ -using System.Drawing; -using System.Windows.Forms; -using System.Windows.Forms.Layout; +using System.Windows.Forms; namespace AlphaTab.WinForms { diff --git a/src.csharp/AlphaTab.Windows/WinForms/ControlContainer.cs b/src.csharp/AlphaTab.Windows/WinForms/ControlContainer.cs index 1b71c0179..fba7e4703 100644 --- a/src.csharp/AlphaTab.Windows/WinForms/ControlContainer.cs +++ b/src.csharp/AlphaTab.Windows/WinForms/ControlContainer.cs @@ -1,7 +1,6 @@ using System.Drawing; using System.Windows.Forms; using AlphaTab.Platform; -using AlphaTab.Rendering.Utils; namespace AlphaTab.WinForms { diff --git a/src.csharp/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs b/src.csharp/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs index 701c7217c..668d20777 100644 --- a/src.csharp/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs +++ b/src.csharp/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs @@ -3,7 +3,6 @@ using System.Windows.Controls; using System.Windows.Media.Animation; using AlphaTab.Platform; -using AlphaTab.Rendering.Utils; namespace AlphaTab.Wpf { diff --git a/src.csharp/AlphaTab/AlphaTab.csproj b/src.csharp/AlphaTab/AlphaTab.csproj index fa00d5079..186aaa632 100644 --- a/src.csharp/AlphaTab/AlphaTab.csproj +++ b/src.csharp/AlphaTab/AlphaTab.csproj @@ -6,8 +6,8 @@ AlphaTab true true - README.md - $(NoWarn);0162;1591;1573;NU5105;0168 + README.md + $(NoWarn);0162;1591;1573;NU5105;0168;1998 $(NoWarn);8600;8601;8602;8603;8604;8605 netstandard2.0 enable @@ -21,18 +21,18 @@ - + - - - + + + - - Platform\Skia\Bravura.ttf - + + Platform\Skia\Bravura.otf + diff --git a/src.csharp/AlphaTab/Collections/List.cs b/src.csharp/AlphaTab/Collections/List.cs index 835a100c4..dcaeb958d 100644 --- a/src.csharp/AlphaTab/Collections/List.cs +++ b/src.csharp/AlphaTab/Collections/List.cs @@ -2,7 +2,7 @@ namespace AlphaTab.Collections; -internal class List : System.Collections.Generic.List, Iterable +internal class List : System.Collections.Generic.List { public List(double size) : this(new T[(int)size]) @@ -16,4 +16,4 @@ public List() public List(IEnumerable collection) : base(collection) { } -} \ No newline at end of file +} diff --git a/src.csharp/AlphaTab/Collections/Map.cs b/src.csharp/AlphaTab/Collections/Map.cs index e3d47887e..f3a04adf4 100644 --- a/src.csharp/AlphaTab/Collections/Map.cs +++ b/src.csharp/AlphaTab/Collections/Map.cs @@ -9,18 +9,16 @@ public interface IMap void Clear(); } public interface IMap : IMap, IEnumerable> - where TValue : class? { IEnumerable Keys(); IEnumerable Values(); bool Has(TKey key); - TValue Get(TKey key); + TValue? Get(TKey key); void Set(TKey key, TValue value); void Delete(TKey key); } public class Map : Dictionary, IMap - where TValue : class? { public double Size => Count; IEnumerable IMap.Keys() @@ -44,6 +42,14 @@ public Map(IEnumerable> entries) this[entry.Key] = entry.Value; } } + + public Map(IEnumerable> entries) + { + foreach (var entry in entries) + { + this[entry.V0] = entry.V1; + } + } public Map(IEnumerable> entries) { foreach (var entry in entries) @@ -57,9 +63,10 @@ public bool Has(TKey key) return ContainsKey(key); } - public TValue Get(TKey key) + public TValue? Get(TKey key) { - return this[key]; + TryGetValue(key, out var v); + return v; } public void Set(TKey key, TValue value) @@ -77,4 +84,4 @@ public void Delete(TKey key) { Remove(key); } -} \ No newline at end of file +} diff --git a/src.csharp/AlphaTab/Collections/ValueTypeMap.cs b/src.csharp/AlphaTab/Collections/ValueTypeMap.cs index 2c1f95ed8..b03bcfe1e 100644 --- a/src.csharp/AlphaTab/Collections/ValueTypeMap.cs +++ b/src.csharp/AlphaTab/Collections/ValueTypeMap.cs @@ -42,6 +42,14 @@ public ValueTypeMap(IEnumerable> entries) } } + public ValueTypeMap(IEnumerable> entries) + { + foreach (var entry in entries) + { + this[entry.V0] = entry.V1; + } + } + public ValueTypeMap(IEnumerable> entries) { foreach (var entry in entries) @@ -80,4 +88,4 @@ IEnumerator> IEnumerable>.GetEnume return ((IEnumerable>) this).Select(kvp => new MapEntry(kvp)).GetEnumerator(); } -} \ No newline at end of file +} diff --git a/src.csharp/AlphaTab/Core/ArrayTuple.cs b/src.csharp/AlphaTab/Core/ArrayTuple.cs new file mode 100644 index 000000000..53482883f --- /dev/null +++ b/src.csharp/AlphaTab/Core/ArrayTuple.cs @@ -0,0 +1,27 @@ +namespace AlphaTab.Core; + +/// +/// A mixed-type array tuple (like [string, number] in JavaScript). +/// +public readonly struct ArrayTuple +{ + public T0 V0 { get; } + public T1 V1 { get; } + + public ArrayTuple(T0 v0, T1 v1) + { + V0 = v0; + V1 = v1; + } + + public void Deconstruct(out T0 v0, out T1 v1) + { + v0 = V0; + v1 = V1; + } + + public override string ToString() + { + return $"[{V0}, {V1}]"; + } +} diff --git a/src.csharp/AlphaTab/Core/Dom/TextDecoder.cs b/src.csharp/AlphaTab/Core/Dom/TextDecoder.cs index bf3df4c8d..b930ba788 100644 --- a/src.csharp/AlphaTab/Core/Dom/TextDecoder.cs +++ b/src.csharp/AlphaTab/Core/Dom/TextDecoder.cs @@ -1,5 +1,4 @@ using System.Text; -using AlphaTab.Core.EcmaScript; namespace AlphaTab.Core.Dom; diff --git a/src.csharp/AlphaTab/Core/Dom/TextEncoder.cs b/src.csharp/AlphaTab/Core/Dom/TextEncoder.cs index 0c47b3827..952b72418 100644 --- a/src.csharp/AlphaTab/Core/Dom/TextEncoder.cs +++ b/src.csharp/AlphaTab/Core/Dom/TextEncoder.cs @@ -1,5 +1,4 @@ using System.Text; -using AlphaTab.Core.EcmaScript; namespace AlphaTab.Core.Dom; diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Array.cs b/src.csharp/AlphaTab/Core/EcmaScript/Array.cs index d55801e6b..547c74d1e 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Array.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Array.cs @@ -15,4 +15,14 @@ public static IList From(IEnumerable x) { return x.ToList(); } -} \ No newline at end of file + + public static IList From(IEnumerator x) + { + var list = new List(); + foreach (var i in x) + { + list.Add(i); + } + return list; + } +} diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Error.cs b/src.csharp/AlphaTab/Core/EcmaScript/Error.cs deleted file mode 100644 index b006329f6..000000000 --- a/src.csharp/AlphaTab/Core/EcmaScript/Error.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace AlphaTab.Core.EcmaScript; - -public class Error : System.Exception -{ - public Error? Cause - { - get - { - return InnerException is Error e ? e : null; - } - } - public Error(string message) : base(message) - { - } - public Error(string message, System.Exception inner) : base(message, inner) - { - } - - public string Stack => StackTrace; -} \ No newline at end of file diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Int32Array.cs b/src.csharp/AlphaTab/Core/EcmaScript/Int32Array.cs index 86dc9406d..19b51bcc7 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Int32Array.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Int32Array.cs @@ -22,6 +22,11 @@ public Int32Array(IList list) _data = new ArraySegment(list.Select(i => (int)i).ToArray()); } + public Int32Array(IList list) + { + _data = new ArraySegment(list.ToArray()); + } + private Int32Array(ArraySegment data) { _data = data; @@ -74,4 +79,4 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } -} \ No newline at end of file +} diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Iterable.cs b/src.csharp/AlphaTab/Core/EcmaScript/Iterable.cs deleted file mode 100644 index 9095c191c..000000000 --- a/src.csharp/AlphaTab/Core/EcmaScript/Iterable.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace AlphaTab.Core.EcmaScript; - -internal interface Iterable : IEnumerable -{ -} \ No newline at end of file diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Number.cs b/src.csharp/AlphaTab/Core/EcmaScript/Number.cs new file mode 100644 index 000000000..93bd9e9f7 --- /dev/null +++ b/src.csharp/AlphaTab/Core/EcmaScript/Number.cs @@ -0,0 +1,64 @@ +using System.Globalization; + +namespace AlphaTab.Core.EcmaScript; + +internal static class Number +{ + public const double POSITIVE_INFINITY = double.PositiveInfinity; + public const double MIN_SAFE_INTEGER = -9007199254740991.0; + public const double MAX_SAFE_INTEGER = 9007199254740991.0; + public const double NaN = double.NaN; + + public static double ParseInt(char c) + { + return ParseInt(c.ToString()); + } + + public static double ParseInt(string s) + { + if (double.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var d)) + { + return (int)d; + } + + return double.NaN; + } + + public static double ParseInt(char c, int radix) + { + return ParseInt(c.ToString(), radix); + } + + public static double ParseInt(string s, int radix) + { + if (radix == 16 && int.TryParse(s, NumberStyles.HexNumber, + CultureInfo.InvariantCulture, + out var d)) + { + return d; + } + + if (radix == 10 && int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, + out d)) + { + return d; + } + + return double.NaN; + } + + public static double ParseFloat(string s) + { + if (double.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var d)) + { + return d; + } + + return double.NaN; + } + + public static bool IsNaN(double d) + { + return double.IsNaN(d); + } +} diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Promise.cs b/src.csharp/AlphaTab/Core/EcmaScript/Promise.cs index ab428fae7..7cffa6a2d 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Promise.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Promise.cs @@ -7,8 +7,9 @@ namespace AlphaTab.Core.EcmaScript; internal static class Promise { [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Task Race(IList tasks) + public static async Task Race(IList tasks) { - return Task.WhenAny(tasks); + var completed = await Task.WhenAny(tasks); + await completed; } } diff --git a/src.csharp/AlphaTab/Core/EcmaScript/RegExp.cs b/src.csharp/AlphaTab/Core/EcmaScript/RegExp.cs index d3683e064..9d6bad918 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/RegExp.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/RegExp.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Text.RegularExpressions; namespace AlphaTab.Core.EcmaScript; @@ -60,6 +61,13 @@ public string Replace(string input, string replacement) : _regex.Replace(input, replacement, 1); } + public string Replace(string input, Func replacer) + { + return _global + ? _regex.Replace(input, match => replacer(match.Value, match.Groups[1].Value)) + : _regex.Replace(input, match => replacer(match.Value, match.Groups[1].Value), 1); + } + public string[] Split(string value) { return _regex.Split(value); diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Set.cs b/src.csharp/AlphaTab/Core/EcmaScript/Set.cs index 4d675c81a..82d2e45d9 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Set.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Set.cs @@ -5,7 +5,11 @@ namespace AlphaTab.Core.EcmaScript; -public class Set : IEnumerable, ICollection +public abstract class Set +{ +} + +public class Set : Set, IEnumerable, ICollection { private readonly HashSet _data; @@ -16,9 +20,9 @@ public Set() public double Size => _data.Count; - public Set(IEnumerable values) + public Set(IEnumerable? values) { - _data = new HashSet(values); + _data = values == null ? new HashSet() : new HashSet(values); } public void Clear() @@ -69,4 +73,4 @@ void ICollection.CopyTo(System.Array array, int index) { _data.CopyTo((T[])array, index); } -} \ No newline at end of file +} diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Uint16Array.cs b/src.csharp/AlphaTab/Core/EcmaScript/Uint16Array.cs index 985330244..7ab2db3b6 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Uint16Array.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Uint16Array.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Uint32Array.cs b/src.csharp/AlphaTab/Core/EcmaScript/Uint32Array.cs index 366a1e6aa..34519a670 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Uint32Array.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Uint32Array.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; namespace AlphaTab.Core.EcmaScript; diff --git a/src.csharp/AlphaTab/Core/Globals.cs b/src.csharp/AlphaTab/Core/Globals.cs index a78a49d7a..0d64df4ec 100644 --- a/src.csharp/AlphaTab/Core/Globals.cs +++ b/src.csharp/AlphaTab/Core/Globals.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Globalization; using System.Threading.Tasks; @@ -6,59 +7,21 @@ namespace AlphaTab.Core; internal static class Globals { - public const double NaN = double.NaN; public static Console Console { get; } = new Console(); - public static double ParseInt(char c) - { - return ParseInt(c.ToString()); - } + public static void SetImmediate(Action action) { action(); } - public static double ParseInt(string s) - { - if (double.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var d)) - { - return (int) d; - } - return double.NaN; - } - public static double ParseInt(char c, int radix) - { - return ParseInt(c.ToString(), radix); - } - public static double ParseInt(string s, int radix) - { - if (radix == 16 && int.TryParse(s, NumberStyles.HexNumber, - CultureInfo.InvariantCulture, - out var d)) - { - return d; - } - return double.NaN; - } - public static double ParseFloat(string s) - { - if (double.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var d)) - { - return d; - } - return double.NaN; - } - public static bool IsNaN(double d) - { - return double.IsNaN(d); - } public static void SetTimeout(Action action, double timeout) { @@ -68,4 +31,15 @@ public static void SetTimeout(Action action, double timeout) action(); }); } -} \ No newline at end of file + + public static readonly PerformanceInstance Performance = new(); + + public class PerformanceInstance + { + public double Now() + { + var seconds = Stopwatch.GetTimestamp() / Stopwatch.Frequency; + return (int)(seconds / 1000); + } + } +} diff --git a/src.csharp/AlphaTab/Core/TypeHelper.cs b/src.csharp/AlphaTab/Core/TypeHelper.cs index b70c1cf7d..352c3cb5a 100644 --- a/src.csharp/AlphaTab/Core/TypeHelper.cs +++ b/src.csharp/AlphaTab/Core/TypeHelper.cs @@ -15,9 +15,14 @@ public static IList CreateList(params T[] values) return new List(values); } + public static T ParseEnum(string s, Type _) where T : struct + { + return Enum.TryParse(s, true, out T value) ? value : default; + } + public static void Add(this IList list, IList newItems) { - if(list is List l) + if (list is List l) { l.AddRange(newItems); } @@ -35,6 +40,16 @@ public static T Find(this IList list, Func predicate) return list.FirstOrDefault(predicate); } + public static bool Includes(this IList list, T item) + { + return list.Contains(item); + } + + public static bool Includes(this System.Collections.IList list, object item) + { + return list.Contains(item); + } + public static bool Some(this IList list, Func predicate) { return list.Any(predicate); @@ -42,17 +57,17 @@ public static bool Some(this IList list, Func predicate) public static IList Splice(this IList data, double start, double deleteCount) { - var items = data.GetRange((int) start, (int) deleteCount); - data.RemoveRange((int) start, (int) deleteCount); + var items = data.GetRange((int)start, (int)deleteCount); + data.RemoveRange((int)start, (int)deleteCount); return new List(items); } public static IList Splice(this IList data, double start, double deleteCount, params T[] newItems) { - var items = data.GetRange((int) start, (int) deleteCount); - data.RemoveRange((int) start, (int) deleteCount); - data.InsertRange((int) start, newItems); + var items = data.GetRange((int)start, (int)deleteCount); + data.RemoveRange((int)start, (int)deleteCount); + data.InsertRange((int)start, newItems); return new List(items); } @@ -81,7 +96,7 @@ public static void Reverse(this IList data) public static IList Slice(this IList data, double start) { - return new List(data.GetRange((int) start, data.Count - (int) start)); + return new List(data.GetRange((int)start, data.Count - (int)start)); } public static IList GetRange(this IList data, int index, int count) @@ -187,10 +202,10 @@ public static IList Sort(this IList data, Func func) switch (data) { case List l: - l.Sort((a, b) => (int) func(a, b)); + l.Sort((a, b) => (int)func(a, b)); break; case T[] array: - System.Array.Sort(array, (a, b) => (int) func(a, b)); + System.Array.Sort(array, (a, b) => (int)func(a, b)); break; default: throw new NotSupportedException("Cannot sort list of type " + @@ -199,6 +214,7 @@ public static IList Sort(this IList data, Func func) return data; } + public static void Sort(this IList data) { switch (data) @@ -216,13 +232,15 @@ public static void Sort(this IList data) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TResult Reduce(this IEnumerable source, Func func, TResult seed) + public static TResult Reduce(this IEnumerable source, + Func func, TResult seed) { return source.Aggregate(seed, func); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IList Map(this IEnumerable source, Func func) + public static IList Map(this IEnumerable source, + Func func) { return source.Select(func).ToList(); } @@ -248,7 +266,7 @@ public static string Join(this IEnumerable source, string separa [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string Substr(this string s, double start, double length) { - return s.Substring((int) start, (int) length); + return s.Substring((int)start, (int)length); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -261,19 +279,19 @@ public static string PadStart(this string s, double length, string pad) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string Substr(this string s, double start) { - return s.Substring((int) start); + return s.Substring((int)start); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CharCodeAt(this string s, double index) { - return s[(int) index]; + return s[(int)index]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string CharAt(this string s, double index) { - return s.Substring((int) index, 1); + return s.Substring((int)index, 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -297,34 +315,14 @@ public static int LocaleCompare(this string a, string b) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IList Split(this string s, string separator) { - return new List(s.Split(new[] {separator}, StringSplitOptions.None)); - } - - public static KeyValuePair CreateMapEntry(int key, TValue value) - { - return new KeyValuePair(key, value); - } - - public static KeyValuePair CreateMapEntry(TKey key, int value) - { - return new KeyValuePair(key, value); - } - - public static KeyValuePair CreateMapEntry(TKey key, TValue value) - { - return new KeyValuePair(key, value); - } - - public static KeyValuePair> CreateMapEntry(TKey key, AlphaTab.Collections.List value) - { - return new KeyValuePair>(key, value); + return new List(s.Split(new[] { separator }, StringSplitOptions.None)); } public static string ToInvariantString(this double num, int radix) { if (radix == 16) { - return ((int) num).ToString("X"); + return ((int)num).ToString("X"); } return num.ToString(CultureInfo.InvariantCulture); @@ -342,7 +340,8 @@ public static string ToInvariantString(this int num) public static string ToInvariantString(this Enum num) { - return ((IConvertible)num).ToInt32(CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture); + return ((IConvertible)num).ToInt32(CultureInfo.InvariantCulture) + .ToString(CultureInfo.InvariantCulture); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -357,6 +356,12 @@ public static string Replace(this string input, RegExp pattern, string replaceme return pattern.Replace(input, replacement); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Replace(this string input, RegExp pattern, Func replacer) + { + return pattern.Replace(input, replacer); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsTruthy(string? s) { @@ -368,6 +373,7 @@ public static bool IsTruthy(object? s) { return s != null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsTruthy(bool? b) { @@ -397,13 +403,13 @@ public static IList Map(this IList source, [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string SubstringIndex(this string s, double startIndex) { - return s.Substring((int) startIndex); + return s.Substring((int)startIndex); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string SubstringIndex(this string s, double startIndex, double endIndex) { - return s.Substring((int) startIndex, (int) (endIndex - startIndex)); + return s.Substring((int)startIndex, (int)(endIndex - startIndex)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -418,6 +424,12 @@ public static string ReplaceAll(this string s, string before, string after) return s.Replace(before, after); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ReplaceAll(this string s, RegExp before, string after) + { + return s.Replace(before, after); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Task Then(this Task s, Action after) { @@ -430,10 +442,9 @@ public static Task Catch(this Task s, Action after) { s.ContinueWith(x => { - if (x.Exception?.InnerExceptions.Count == 1 && - x.Exception.InnerExceptions[0] is Error e) + if (x.Exception?.InnerExceptions.Count == 1) { - after(e); + after(x.Exception.InnerExceptions[0]); } else { @@ -442,6 +453,7 @@ public static Task Catch(this Task s, Action after) }, TaskContinuationOptions.OnlyOnFaulted); return s; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Task Catch(this Task s, Action after) { @@ -449,6 +461,29 @@ public static Task Catch(this Task s, Action after) return s; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object ToTemplate(this T value) + { + return value switch + { + bool b => b.ToTemplate(), + Enum e => e.ToTemplate(), + _ => value + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object ToTemplate(this Enum value) + { + return ((IConvertible)value).ToInt32(CultureInfo.InvariantCulture); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object ToTemplate(this bool value) + { + return value ? "true" : "false"; + } + public static string TypeOf(object? actual) { switch (actual) @@ -483,10 +518,23 @@ public static IEnumerable SetInitializer(params T[] items) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable MapInitializer(params T[] items) + public static string Stack(this Error e) + { + return e.StackTrace; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Error? Cause(this Error e) + { + return e.InnerException; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IList MapInitializer(params T[] items) { return items; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ToFixed(this double value, int decimals) { @@ -513,12 +561,14 @@ public static string Repeat(this string value, double count) { builder.Append(value); } + return builder.ToString(); } public static Task CreatePromise(Action> run) { var taskCompletionSource = new TaskCompletionSource(); + void Resolve() { taskCompletionSource.SetResult(null); @@ -532,10 +582,11 @@ void Reject(object o) taskCompletionSource.SetException(e); break; case string s: - taskCompletionSource.SetException(new PromiseRejectedException(s)); + taskCompletionSource.SetException(new PromiseRejectedError(s)); break; default: - taskCompletionSource.SetException(new PromiseRejectedException("Promise was rejected", o)); + taskCompletionSource.SetException( + new PromiseRejectedError("Promise was rejected", o)); break; } } @@ -544,23 +595,28 @@ void Reject(object o) return taskCompletionSource.Task; } + + + public static IEnumerator GetEnumerator(this IEnumerator enumerator) + { + return enumerator; + } } } -public class PromiseRejectedException : Exception +public class PromiseRejectedError : Error { public object? RejectData { get; } - public PromiseRejectedException() + public PromiseRejectedError() { } - public PromiseRejectedException(string message) : base(message) + public PromiseRejectedError(string message) : base(message) { - } - public PromiseRejectedException(string message, object rejectData) : base(message) + public PromiseRejectedError(string message, object rejectData) : base(message) { RejectData = rejectData; } diff --git a/src.csharp/AlphaTab/Environment.cs b/src.csharp/AlphaTab/Environment.cs index 03e644fd4..61e2a6ae1 100644 --- a/src.csharp/AlphaTab/Environment.cs +++ b/src.csharp/AlphaTab/Environment.cs @@ -1,8 +1,7 @@ using System; -using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using AlphaTab.Core; using AlphaTab.Collections; using AlphaTab.Platform.CSharp; @@ -16,6 +15,14 @@ public static void PlatformInit() } + private static void PrintPlatformInfo(System.Action print) + { + print($".net Runtime: {RuntimeInformation.FrameworkDescription}"); + print($"Process: {RuntimeInformation.ProcessArchitecture}"); + print($"OS Description: {RuntimeInformation.OSDescription}"); + print($"OS Arch: {RuntimeInformation.OSArchitecture}"); + } + public static Action Throttle(Action action, double delay) { CancellationTokenSource? cancellationTokenSource = null; @@ -41,4 +48,6 @@ private static void CreatePlatformSpecificRenderEngines(IMap Player.IsReady; - public bool IsReadyForPlayback => Player.IsReadyForPlayback; + public bool IsReady => _player.IsReady; + public bool IsReadyForPlayback => _player.IsReadyForPlayback; - public PlayerState State => Player == null ? PlayerState.Paused : Player.State; + public PlayerState State => _player == null ? PlayerState.Paused : _player.State; public LogLevel LogLevel { @@ -55,62 +55,62 @@ public LogLevel LogLevel set { _logLevel = value; - DispatchOnWorkerThread(() => { Player.LogLevel = value; }); + DispatchOnWorkerThread(() => { _player.LogLevel = value; }); } } public double MasterVolume { - get => Player.MasterVolume; - set => DispatchOnWorkerThread(() => { Player.MasterVolume = value; }); + get => _player.MasterVolume; + set => DispatchOnWorkerThread(() => { _player.MasterVolume = value; }); } public double CountInVolume { - get => Player.CountInVolume; - set => DispatchOnWorkerThread(() => { Player.CountInVolume = value; }); + get => _player.CountInVolume; + set => DispatchOnWorkerThread(() => { _player.CountInVolume = value; }); } public IList MidiEventsPlayedFilter { - get => Player.MidiEventsPlayedFilter; - set => DispatchOnWorkerThread(() => { Player.MidiEventsPlayedFilter = value; }); + get => _player.MidiEventsPlayedFilter; + set => DispatchOnWorkerThread(() => { _player.MidiEventsPlayedFilter = value; }); } public double MetronomeVolume { - get => Player.MetronomeVolume; - set => DispatchOnWorkerThread(() => { Player.MetronomeVolume = value; }); + get => _player.MetronomeVolume; + set => DispatchOnWorkerThread(() => { _player.MetronomeVolume = value; }); } public double PlaybackSpeed { - get => Player.PlaybackSpeed; - set => DispatchOnWorkerThread(() => { Player.PlaybackSpeed = value; }); + get => _player.PlaybackSpeed; + set => DispatchOnWorkerThread(() => { _player.PlaybackSpeed = value; }); } public double TickPosition { - get => Player.TickPosition; - set => DispatchOnWorkerThread(() => { Player.TickPosition = value; }); + get => _player.TickPosition; + set => DispatchOnWorkerThread(() => { _player.TickPosition = value; }); } public double TimePosition { - get => Player.TimePosition; - set => DispatchOnWorkerThread(() => { Player.TimePosition = value; }); + get => _player.TimePosition; + set => DispatchOnWorkerThread(() => { _player.TimePosition = value; }); } public PlaybackRange? PlaybackRange { - get => Player.PlaybackRange; - set => DispatchOnWorkerThread(() => { Player.PlaybackRange = value; }); + get => _player.PlaybackRange; + set => DispatchOnWorkerThread(() => { _player.PlaybackRange = value; }); } public bool IsLooping { - get => Player.IsLooping; - set => DispatchOnWorkerThread(() => { Player.IsLooping = value; }); + get => _player.IsLooping; + set => DispatchOnWorkerThread(() => { _player.IsLooping = value; }); } public bool Play() @@ -120,73 +120,73 @@ public bool Play() return false; } - DispatchOnWorkerThread(() => { Player.Play(); }); + DispatchOnWorkerThread(() => { _player.Play(); }); return true; } public void Pause() { - DispatchOnWorkerThread(() => { Player.Pause(); }); + DispatchOnWorkerThread(() => { _player.Pause(); }); } public void PlayOneTimeMidiFile(MidiFile midiFile) { - DispatchOnWorkerThread(() => { Player.PlayOneTimeMidiFile(midiFile); }); + DispatchOnWorkerThread(() => { _player.PlayOneTimeMidiFile(midiFile); }); } public void PlayPause() { - DispatchOnWorkerThread(() => { Player.PlayPause(); }); + DispatchOnWorkerThread(() => { _player.PlayPause(); }); } public void Stop() { - DispatchOnWorkerThread(() => { Player.Stop(); }); + DispatchOnWorkerThread(() => { _player.Stop(); }); } public void ResetSoundFonts() { - DispatchOnWorkerThread(() => { Player.ResetSoundFonts(); }); + DispatchOnWorkerThread(() => { _player.ResetSoundFonts(); }); } public void LoadSoundFont(Uint8Array data, bool append) { - DispatchOnWorkerThread(() => { Player.LoadSoundFont(data, append); }); + DispatchOnWorkerThread(() => { _player.LoadSoundFont(data, append); }); } public void LoadMidiFile(MidiFile midi) { - DispatchOnWorkerThread(() => { Player.LoadMidiFile(midi); }); + DispatchOnWorkerThread(() => { _player.LoadMidiFile(midi); }); } public void ApplyTranspositionPitches(IValueTypeMap transpositionPitches) { - DispatchOnWorkerThread(() => { Player.ApplyTranspositionPitches(transpositionPitches); }); + DispatchOnWorkerThread(() => { _player.ApplyTranspositionPitches(transpositionPitches); }); } public void SetChannelMute(double channel, bool mute) { - DispatchOnWorkerThread(() => { Player.SetChannelMute(channel, mute); }); + DispatchOnWorkerThread(() => { _player.SetChannelMute(channel, mute); }); } public void ResetChannelStates() { - DispatchOnWorkerThread(() => { Player.ResetChannelStates(); }); + DispatchOnWorkerThread(() => { _player.ResetChannelStates(); }); } public void SetChannelSolo(double channel, bool solo) { - DispatchOnWorkerThread(() => { Player.SetChannelSolo(channel, solo); }); + DispatchOnWorkerThread(() => { _player.SetChannelSolo(channel, solo); }); } public void SetChannelVolume(double channel, double volume) { - DispatchOnWorkerThread(() => { Player.SetChannelVolume(channel, volume); }); + DispatchOnWorkerThread(() => { _player.SetChannelVolume(channel, volume); }); } public void SetChannelTranspositionPitch(double channel, double semitones) { - DispatchOnWorkerThread(() => { Player.SetChannelTranspositionPitch(channel, semitones); }); + DispatchOnWorkerThread(() => { _player.SetChannelTranspositionPitch(channel, semitones); }); } public IEventEmitter Ready { get; } = new EventEmitter(); @@ -256,4 +256,4 @@ protected virtual void OnPlaybackRangeChanged(PlaybackRangeChangedEventArgs obj) { DispatchOnUiThread(() => ((EventEmitterOfT)PlaybackRangeChanged).Trigger(obj)); } -} \ No newline at end of file +} diff --git a/src.csharp/AlphaTab/Platform/CSharp/GdiCanvas.cs b/src.csharp/AlphaTab/Platform/CSharp/GdiCanvas.cs index 9902856b8..4207a6950 100644 --- a/src.csharp/AlphaTab/Platform/CSharp/GdiCanvas.cs +++ b/src.csharp/AlphaTab/Platform/CSharp/GdiCanvas.cs @@ -68,7 +68,7 @@ static GdiCanvas() var type = typeof(GdiCanvas).GetTypeInfo(); using var bravura = - type.Assembly.GetManifestResourceStream(type.Namespace + ".Bravura.ttf"); + type.Assembly.GetManifestResourceStream(type.Namespace + ".Bravura.otf"); var dataPtr = Marshal.AllocCoTaskMem((int)bravura.Length); try { @@ -462,12 +462,12 @@ public void FillText(string text, double x, double y) _graphics.DrawString(text, _font, _brush, new PointF((float)x, (float)y), _stringFormat); } - public TextMetrics MeasureText(string text) + public MeasuredText MeasureText(string text) { lock (MeasurementGraphics) { var measured = MeasurementGraphics.MeasureString(text, _font, new PointF(0, 0), _stringFormat); - return new TextMetrics(measured.Width, measured.Height); + return new MeasuredText(measured.Width, measured.Height); } } diff --git a/src.csharp/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthWorkerApi.cs b/src.csharp/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthWorkerApi.cs index 28e27e746..4a3155354 100644 --- a/src.csharp/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthWorkerApi.cs +++ b/src.csharp/AlphaTab/Platform/CSharp/ManagedThreadAlphaSynthWorkerApi.cs @@ -9,8 +9,8 @@ internal class ManagedThreadAlphaSynthWorkerApi : AlphaSynthWorkerApiBase { private readonly Action _uiInvoke; private readonly Thread _workerThread; - private BlockingCollection _workerQueue; - private CancellationTokenSource _workerCancellationToken; + private readonly BlockingCollection _workerQueue; + private readonly CancellationTokenSource _workerCancellationToken; private readonly ManualResetEventSlim? _threadStartedEvent; public ManagedThreadAlphaSynthWorkerApi(ISynthOutput output, LogLevel logLevel, Action uiInvoke, double bufferTimeInMilliseconds) @@ -22,8 +22,10 @@ public ManagedThreadAlphaSynthWorkerApi(ISynthOutput output, LogLevel logLevel, _workerQueue = new BlockingCollection(); _workerCancellationToken = new CancellationTokenSource(); - _workerThread = new Thread(DoWork); - _workerThread.IsBackground = true; + _workerThread = new Thread(DoWork) + { + IsBackground = true + }; _workerThread.Start(); _threadStartedEvent.Wait(); @@ -73,4 +75,4 @@ private void DoWork() action(); } } -} \ No newline at end of file +} diff --git a/src.csharp/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs b/src.csharp/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs index 54577a6d5..cc1a2729c 100644 --- a/src.csharp/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs +++ b/src.csharp/AlphaTab/Platform/CSharp/ManagedThreadScoreRenderer.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; -using AlphaTab.Core.EcmaScript; using AlphaTab.Model; using AlphaTab.Rendering; using AlphaTab.Rendering.Utils; diff --git a/src.csharp/AlphaTab/Platform/CSharp/ManagedUiFacade.cs b/src.csharp/AlphaTab/Platform/CSharp/ManagedUiFacade.cs index 0274ab4f7..0536f71c6 100644 --- a/src.csharp/AlphaTab/Platform/CSharp/ManagedUiFacade.cs +++ b/src.csharp/AlphaTab/Platform/CSharp/ManagedUiFacade.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.IO; using AlphaTab.Synth; -using AlphaTab.Core.EcmaScript; using AlphaTab.Importer; using AlphaTab.Model; using AlphaTab.Rendering; diff --git a/src.csharp/AlphaTab/Platform/Skia/AlphaSkiaBridge/AlphaSkiaBridge.cs b/src.csharp/AlphaTab/Platform/Skia/AlphaSkiaBridge/AlphaSkiaBridge.cs index 029d64f99..c7288f02d 100644 --- a/src.csharp/AlphaTab/Platform/Skia/AlphaSkiaBridge/AlphaSkiaBridge.cs +++ b/src.csharp/AlphaTab/Platform/Skia/AlphaSkiaBridge/AlphaSkiaBridge.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; namespace AlphaTab.Platform.Skia.AlphaSkiaBridge; @@ -27,7 +29,9 @@ public class AlphaSkiaImage : IDisposable /// Gets the target . /// public AlphaSkia.AlphaSkiaImage Image { get; } + internal double Width => Image.Width; + internal double Height => Image.Height; internal AlphaSkiaImage(AlphaSkia.AlphaSkiaImage image) @@ -40,34 +44,28 @@ public void Dispose() Image.Dispose(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ArrayBuffer? ReadPixels() { var data = Image.ReadPixels(); - if (data == null) - { - return null; - } - - return new ArrayBuffer(data); + return data == null ? null : new ArrayBuffer(data); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ArrayBuffer? ToPng() { var data = Image.ToPng(); - if (data == null) - { - return null; - } - - return new ArrayBuffer(data); + return data == null ? null : new ArrayBuffer(data); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static AlphaSkiaImage? Decode(ArrayBuffer buffer) { var underlying = AlphaSkia.AlphaSkiaImage.Decode(buffer.Raw); return underlying == null ? null : new AlphaSkiaImage(underlying); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static AlphaSkiaImage? FromPixels(double width, double height, ArrayBuffer pixels) { var underlying = @@ -85,7 +83,9 @@ internal class AlphaSkiaCanvas : IDisposable public uint Color { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _canvas.Color; + [MethodImpl(MethodImplOptions.AggressiveInlining)] set => _canvas.Color = value; } @@ -184,18 +184,22 @@ public void Stroke() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FillText(string text, AlphaSkiaTypeface typeface, double fontSize, double x, + public void FillText(string text, AlphaSkiaTextStyle textStyle, double fontSize, double x, double y, - AlphaSkiaTextAlign textAlign, AlphaSkiaTextBaseline baselineBridge) + AlphaSkiaTextAlign textAlign, AlphaSkiaTextBaseline baseline) { - _canvas.FillText(text, typeface.Typeface, (float)fontSize, (float)x, (float)y, - (AlphaSkia.AlphaSkiaTextAlign)textAlign, (AlphaSkia.AlphaSkiaTextBaseline)baselineBridge); + _canvas.FillText(text, textStyle.TextStyle, (float)fontSize, (float)x, (float)y, + (AlphaSkia.AlphaSkiaTextAlign)textAlign, (AlphaSkia.AlphaSkiaTextBaseline)baseline); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double MeasureText(string text, AlphaSkiaTypeface typeface, double fontSize) + public AlphaSkiaTextMetrics MeasureText(string text, AlphaSkiaTextStyle textStyle, + double fontSize, + AlphaSkiaTextAlign textAlign, AlphaSkiaTextBaseline baseline) { - return _canvas.MeasureText(text, typeface.Typeface, (float)fontSize); + return new AlphaSkiaTextMetrics(_canvas.MeasureText(text, textStyle.TextStyle, + (float)fontSize, + (AlphaSkia.AlphaSkiaTextAlign)textAlign, (AlphaSkia.AlphaSkiaTextBaseline)baseline)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -231,25 +235,46 @@ public void EndRotate() _canvas.EndRotate(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void DrawImage(AlphaSkiaImage image, double x, double y, double width, double height) { _canvas.DrawImage(image.Image, (float)x, (float)y, (float)width, (float)height); } } -internal class AlphaSkiaTypeface : IDisposable +internal sealed class AlphaSkiaTypeface : IDisposable { - internal AlphaSkia.AlphaSkiaTypeface Typeface { get; } + public AlphaSkia.AlphaSkiaTypeface Typeface + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } - internal AlphaSkiaTypeface(AlphaSkia.AlphaSkiaTypeface typeface) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public AlphaSkiaTypeface(AlphaSkia.AlphaSkiaTypeface typeface) { Typeface = typeface; } - public string FamilyName => Typeface.FamilyName; - public bool IsBold => Typeface.IsBold; - public bool IsItalic => Typeface.IsItalic; + public string FamilyName + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Typeface.FamilyName; + } + + public double Weight + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Typeface.Weight; + } + public bool IsItalic + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Typeface.IsItalic; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { Typeface.Dispose(); @@ -265,11 +290,151 @@ public void Dispose() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static AlphaSkiaTypeface? Create(string name, bool bold, bool italic) + public static AlphaSkiaTypeface? Create(string name, double weight, bool italic) { - var underlying = AlphaSkia.AlphaSkiaTypeface.Create(name, bold, italic); + var underlying = AlphaSkia.AlphaSkiaTypeface.Create(name, (ushort)weight, italic); return underlying == null ? null : new AlphaSkiaTypeface(underlying); } } + +internal sealed class AlphaSkiaTextStyle : IDisposable +{ + public AlphaSkia.AlphaSkiaTextStyle TextStyle + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public AlphaSkiaTextStyle(AlphaSkia.AlphaSkiaTextStyle textStyle) + { + TextStyle = textStyle; + } + + public AlphaSkiaTextStyle(IList fontFamilies, double weight, bool isItalic) + : this(new AlphaSkia.AlphaSkiaTextStyle( + fontFamilies.ToArray(), + (ushort)weight, + isItalic + )) + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + TextStyle.Dispose(); + } + + public string[] FontFamilies + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextStyle.FontFamilies; + } + + public double Weight + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextStyle.Weight; + } + + public bool IsItalic + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextStyle.IsItalic; + } +} + +internal sealed class AlphaSkiaTextMetrics : IDisposable +{ + public AlphaSkia.AlphaSkiaTextMetrics TextMetrics + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public AlphaSkiaTextMetrics(AlphaSkia.AlphaSkiaTextMetrics textMetrics) + { + TextMetrics = textMetrics; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + TextMetrics.Dispose(); + } + + public double Width + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.Width; + } + + public double ActualBoundingBoxLeft + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.ActualBoundingBoxLeft; + } + + public double ActualBoundingBoxRight + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.ActualBoundingBoxRight; + } + + public double FontBoundingBoxAscent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.FontBoundingBoxAscent; + } + + public double FontBoundingBoxDescent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.FontBoundingBoxDescent; + } + + public double ActualBoundingBoxAscent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.ActualBoundingBoxAscent; + } + + public double ActualBoundingBoxDescent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.ActualBoundingBoxDescent; + } + + public double EmHeightAscent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.EmHeightAscent; + } + + public double EmHeightDescent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.EmHeightDescent; + } + + public double HangingBaseline + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.HangingBaseline; + } + + public double AlphabeticBaseline + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.AlphabeticBaseline; + } + + public double IdeographicBaseline + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => TextMetrics.IdeographicBaseline; + } +} diff --git a/src.csharp/AlphaTab/Platform/Skia/SkiaCanvas.cs b/src.csharp/AlphaTab/Platform/Skia/SkiaCanvas.cs index e5d9b0c93..805ca7883 100644 --- a/src.csharp/AlphaTab/Platform/Skia/SkiaCanvas.cs +++ b/src.csharp/AlphaTab/Platform/Skia/SkiaCanvas.cs @@ -3,22 +3,22 @@ namespace AlphaTab.Platform.Skia; -partial class SkiaCanvas +internal partial class SkiaCanvas { static SkiaCanvas() { - // attempt to load correct skia native lib var type = typeof(SkiaCanvas).GetTypeInfo(); using var bravura = - type.Assembly.GetManifestResourceStream(type.Namespace + ".Bravura.ttf")!; - var bravuraData = new MemoryStream((int)bravura.Length); + type.Assembly.GetManifestResourceStream(type.Namespace + ".Bravura.otf")!; + using var bravuraData = new MemoryStream((int)bravura.Length); bravura.CopyTo(bravuraData); - MusicFont = AlphaSkiaTypeface.Register(new ArrayBuffer(bravuraData.ToArray()))!; + + Enable(new ArrayBuffer(bravuraData.ToArray()), null); } + // ReSharper disable once UnusedParameter.Global internal static void Enable(ArrayBuffer bravura, object? unused) { - MusicFont?.Dispose(); - MusicFont = AlphaSkiaTypeface.Register(bravura); + InitializeMusicFont(AlphaSkiaTypeface.Register(bravura)!); } } diff --git a/src.csharp/AlphaTab/Platform/Svg/FontSizes.cs b/src.csharp/AlphaTab/Platform/Svg/FontSizes.cs index 18ae51772..fd9ba680b 100644 --- a/src.csharp/AlphaTab/Platform/Svg/FontSizes.cs +++ b/src.csharp/AlphaTab/Platform/Svg/FontSizes.cs @@ -1,7 +1,4 @@ -using AlphaTab.Core; -using AlphaTab.Core.EcmaScript; - -namespace AlphaTab.Platform.Svg; +namespace AlphaTab.Platform.Svg; partial class FontSizes { diff --git a/src.csharp/Directory.Build.props b/src.csharp/Directory.Build.props index 1f792a51f..194bb40e7 100644 --- a/src.csharp/Directory.Build.props +++ b/src.csharp/Directory.Build.props @@ -2,8 +2,8 @@ portable true - 1.4.4 - 1.4.4.0 + 1.5.0 + 1.5.0.0 $(AssemblyVersion) Danielku15 CoderLine diff --git a/src.kotlin/alphaTab/android/build.gradle.kts b/src.kotlin/alphaTab/android/build.gradle.kts index fcd5a5997..13978e2fa 100644 --- a/src.kotlin/alphaTab/android/build.gradle.kts +++ b/src.kotlin/alphaTab/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.4.4-SNAPSHOT" +var libVersion = "1.5.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" @@ -116,7 +116,7 @@ android { androidResources { ignoreAssetsPattern = arrayOf( "eot", - "otf", + "ttf", "svg", "woff", "woff2", diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/EnvironmentPartials.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/EnvironmentPartials.kt index 9fc163ecb..00716e54c 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/EnvironmentPartials.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/EnvironmentPartials.kt @@ -1,6 +1,8 @@ package alphaTab import alphaTab.platform.android.AndroidCanvas +import alphaTab.platform.android.AndroidEnvironment +import android.os.Build import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -19,13 +21,22 @@ internal class EnvironmentPartials { ) engines.set( "default", - engines.get("skia") + engines.get("skia")!! ) } internal fun platformInit() { } + internal fun printPlatformInfo(print: (message: String) -> Unit) { + print("OS Name: ${System.getProperty("os.name")}"); + print("OS Version: ${System.getProperty("os.version")}"); + print("Device Brand: ${Build.MANUFACTURER}"); + print("Device Model: ${Build.MODEL}"); + print("SDK Version: ${Build.VERSION.SDK_INT}"); + print("Screen Size: ${AndroidEnvironment.screenWidth}x${AndroidEnvironment.screenHeight}"); + } + private val throttleScope = CoroutineScope(Dispatchers.Default) internal fun throttle(toThrottle: () -> Unit, delay: Double): () -> Unit { var job: Job? = null diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/BooleanList.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/BooleanList.kt index 6834f9097..0ddd8ba2a 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/BooleanList.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/BooleanList.kt @@ -38,10 +38,12 @@ internal class BooleanList : IBooleanIterable { _data.fill(b) } + @Suppress("NOTHING_TO_INLINE") public inline fun spread(): BooleanArray { return _data; } + @Suppress("NOTHING_TO_INLINE") public inline fun indexOf(v: Boolean): Double { return _data.indexOf(v).toDouble() } diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleBooleanMap.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleBooleanMap.kt index 918fadd23..b7fe13f0c 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleBooleanMap.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleBooleanMap.kt @@ -31,7 +31,7 @@ public open class DoubleBooleanMapEntry { public constructor(key: Double, value: Boolean) { _key = key _value = value - } + } } public class DoubleBooleanMapEntryInternal : DoubleBooleanMapEntry(), IMapEntryInternal { @@ -50,13 +50,13 @@ public class DoubleBooleanMap : MapBase entry.key == k }) >= 0 } - public fun get(key: Double): Boolean { + public fun get(key: Double): Boolean? { val i = findEntryInternal(key, { entry, k -> entry.key == k }) if (i >= 0) { return entries[i].value } - throw KeyNotFoundException() + return null } public fun set(key: Double, value: Boolean) { diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleDoubleMap.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleDoubleMap.kt index 89d17a1c7..4a16bbbd3 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleDoubleMap.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleDoubleMap.kt @@ -31,7 +31,7 @@ public open class DoubleDoubleMapEntry { public constructor(key: Double, value: Double) { _key = key _value = value - } + } } public class DoubleDoubleMapEntryInternal : DoubleDoubleMapEntry(), IMapEntryInternal { @@ -50,13 +50,13 @@ public class DoubleDoubleMap : MapBase entry.key == k }) >= 0 } - public fun get(key: Double): Double { + public fun get(key: Double): Double? { val i = findEntryInternal(key, { entry, k -> entry.key == k }) if (i >= 0) { return entries[i].value } - throw KeyNotFoundException() + return null } public fun set(key: Double, value: Double) { diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleList.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleList.kt index b48f06f06..97ac00f3d 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleList.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleList.kt @@ -30,6 +30,15 @@ public class DoubleList : IDoubleIterable { _size = elements.size } + internal constructor(elements: IDoubleIterable) { + _items = DoubleArray(0) + _size = 0 + for(d in elements) { + push(d) + } + } + + private constructor(elements: DoubleArray, size: Int) { _items = elements _size = size @@ -150,7 +159,7 @@ public class DoubleList : IDoubleIterable { } } - + internal fun reduce(operation: (acc: Double, v: Double) -> Double, initial:Double): Double { var accumulator = initial for (element in _items) accumulator = operation(accumulator, element) diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleObjectMap.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleObjectMap.kt index b9bdbd1e3..1a6867441 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleObjectMap.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/DoubleObjectMap.kt @@ -1,5 +1,8 @@ package alphaTab.collections +import alphaTab.core.DoubleObjectArrayTuple +import alphaTab.core.ObjectBooleanArrayTuple + public open class DoubleObjectMapEntry { private var _key: Double public var key: Double @@ -50,9 +53,9 @@ public class DoubleObjectMapEntryInternal : DoubleObjectMapEntry public class DoubleObjectMap : MapBase, DoubleObjectMapEntryInternal> { public constructor() - public constructor(iterable: Iterable>) { + public constructor(iterable: Iterable>) { for (it in iterable) { - set(it.key, it.value) + set(it.v0, it.v1) } } @@ -61,13 +64,13 @@ public class DoubleObjectMap : { entry, k -> entry.key == k }) >= 0 } - public fun get(key: Double): TValue { + public fun get(key: Double): TValue? { val i = findEntryInternal(key, { entry, k -> entry.key == k }) if (i >= 0) { return entries[i].value } - throw KeyNotFoundException() + return null } public fun set(key: Double, value: TValue) { diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/List.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/List.kt index 881f2480e..febf09755 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/List.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/List.kt @@ -27,7 +27,7 @@ public class List : Iterable { _data = items.toMutableList() } - private constructor(items: MutableList) { + internal constructor(items: MutableList) { _data = items } @@ -63,6 +63,10 @@ public class List : Iterable { return _data.any(predicate) } + public fun includes(value: T): Boolean { + return _data.contains(value) + } + public fun indexOf(value: T): Double { return _data.indexOf(value).toDouble() } @@ -71,11 +75,11 @@ public class List : Iterable { return _data.removeLast() } - public fun unshift(item:T) { + public fun unshift(item: T) { _data.add(0, item) } - public fun sort(comparison: (a: T, b: T) -> Double) : List { + public fun sort(comparison: (a: T, b: T) -> Double): List { _data.sortWith { a, b -> comparison(a, b).toInt() } return this } @@ -120,8 +124,7 @@ public class List : Iterable { public fun splice(start: Double, deleteCount: Double, vararg newElements: T) { var actualStart = start.toInt() - if (actualStart < 0) - { + if (actualStart < 0) { actualStart += _data.size } @@ -132,4 +135,8 @@ public class List : Iterable { public fun join(separator: String): String { return _data.joinToString(separator) } -} \ No newline at end of file +} + +internal inline fun List.toArray(): Array { + return this._data.toTypedArray() +} diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/Map.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/Map.kt index 18cf3f864..ec3572afd 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/Map.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/Map.kt @@ -1,5 +1,7 @@ package alphaTab.collections +import alphaTab.core.ArrayTuple + public open class MapEntry { private var _key: TKey public var key: TKey @@ -51,7 +53,12 @@ public class MapEntryInternal : MapEntry(), public class Map: MapBase, MapEntryInternal> { public constructor() - public constructor(iterable: Iterable>) { + public constructor(iterable: Iterable>) { + for (it in iterable) { + set(it.v0, it.v1) + } + } + public constructor(iterable: Map) { for (it in iterable) { set(it.key, it.value) } @@ -64,13 +71,13 @@ public class Map: } @Suppress("UNCHECKED_CAST") - public fun get(key: TKey): TValue { + public fun get(key: TKey): TValue? { val i = findEntryInternal(key as Any?, { entry, k -> entry.key == (k as TKey) }) if (i >= 0) { return entries[i].value } - throw KeyNotFoundException() + return null } public fun set(key: TKey, value: TValue) { diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/ObjectBooleanMap.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/ObjectBooleanMap.kt index 52960d671..fe7c3ec22 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/ObjectBooleanMap.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/ObjectBooleanMap.kt @@ -1,5 +1,8 @@ package alphaTab.collections +import alphaTab.core.ArrayTuple +import alphaTab.core.ObjectBooleanArrayTuple + public open class ObjectBooleanMapEntry { private var _key: TKey public var key: TKey @@ -51,9 +54,9 @@ public class ObjectBooleanMapEntryInternal : ObjectBooleanMapEntry() public class ObjectBooleanMap : MapBase, ObjectBooleanMapEntryInternal> { public constructor() - public constructor(iterable: Iterable>) { + public constructor(iterable: Iterable>) { for (it in iterable) { - set(it.key, it.value) + set(it.v0, it.v1) } } @@ -64,13 +67,13 @@ public class ObjectBooleanMap : } @Suppress("UNCHECKED_CAST") - public fun get(key: TKey): Boolean { + public fun get(key: TKey): Boolean? { val i = findEntryInternal(key as Any, { entry, k -> entry.key == (k as TKey) }) if (i >= 0) { return entries[i].value } - throw KeyNotFoundException() + return null } public fun set(key: TKey, value: Boolean) { diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/ObjectDoubleMap.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/ObjectDoubleMap.kt index 2f710fc92..d649796f5 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/ObjectDoubleMap.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/collections/ObjectDoubleMap.kt @@ -1,5 +1,7 @@ package alphaTab.collections +import alphaTab.core.ObjectDoubleArrayTuple + public open class ObjectDoubleMapEntry { private var _key: TKey public var key: TKey @@ -50,9 +52,9 @@ public class ObjectDoubleMapEntryInternal : ObjectDoubleMapEntry(), public class ObjectDoubleMap : MapBase, ObjectDoubleMapEntryInternal> { public constructor() - public constructor(iterable: Iterable>) { + public constructor(iterable: Iterable>) { for (it in iterable) { - set(it.key, it.value) + set(it.v0, it.v1) } } @@ -63,13 +65,13 @@ public class ObjectDoubleMap : } @Suppress("UNCHECKED_CAST") - public fun get(key: TKey): Double { + public fun get(key: TKey): Double? { val i = findEntryInternal(key as Any, { entry, k -> entry.key == (k as TKey) }) if (i >= 0) { return entries[i].value } - throw KeyNotFoundException() + return null } public fun set(key: TKey, value: Double) { diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ArrayTuple.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ArrayTuple.kt new file mode 100644 index 000000000..5b6d436b3 --- /dev/null +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ArrayTuple.kt @@ -0,0 +1,34 @@ +package alphaTab.core + +// generic variant (would box) +interface IArrayTuple { + val v0: T0 + val v1: T1 + + operator fun component1(): T0 + operator fun component2(): T1 +} + +data class ArrayTuple(override val v0: T0, override val 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) : + IObjectBooleanArrayTuple + +interface IObjectDoubleArrayTuple : IArrayTuple +data class ObjectDoubleArrayTuple(override val v0: T, override val v1: Double) : + IObjectDoubleArrayTuple + +interface IDoubleObjectArrayTuple : IArrayTuple +data class DoubleObjectArrayTuple(override val v0: Double, override val v1: T) : + IDoubleObjectArrayTuple + +interface IDoubleDoubleArrayTuple : IArrayTuple +data class DoubleDoubleArrayTuple(override val v0: Double, override val v1: Double) : + IDoubleDoubleArrayTuple + +interface IDoubleBooleanArrayTuple : IArrayTuple +data class DoubleBooleanArrayTuple(override val v0: Double, override val v1: Boolean) : + IDoubleBooleanArrayTuple + diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/Globals.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/Globals.kt index 7ebf3177f..edbfe2cec 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/Globals.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/Globals.kt @@ -22,7 +22,7 @@ import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.suspendCoroutine @ExperimentalUnsignedTypes -internal fun UByteArray.decodeToFloatArray(): FloatArray { +internal inline fun UByteArray.decodeToFloatArray(): FloatArray { val fb = ByteBuffer.wrap(this.toByteArray()).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer(); val fa = FloatArray(fb.limit()) fb.get(fa) @@ -30,7 +30,7 @@ internal fun UByteArray.decodeToFloatArray(): FloatArray { } @ExperimentalUnsignedTypes -internal fun UByteArray.decodeToDoubleArray(): DoubleArray { +internal inline fun UByteArray.decodeToDoubleArray(): DoubleArray { val db = ByteBuffer.wrap(this.toByteArray()).order(ByteOrder.LITTLE_ENDIAN).asDoubleBuffer(); val da = DoubleArray(db.limit()) db.get(da) @@ -38,37 +38,45 @@ internal fun UByteArray.decodeToDoubleArray(): DoubleArray { } @ExperimentalUnsignedTypes -internal fun UByteArray.decodeToString(encoding: String): String { +internal inline fun UByteArray.decodeToString(encoding: String): String { return String(this.toByteArray(), 0, this.size, Charset.forName(encoding)) } -internal fun > List.sort() { +internal inline fun > List.sort() { this.sort { a, b -> a.compareTo(b).toDouble() } } -internal fun String.substr(startIndex: Double, length: Double): String { +internal inline fun String.substr(startIndex: Double, length: Double): String { return this.substring(startIndex.toInt(), (startIndex + length).toInt()) } -internal fun String.substr(startIndex: Double): String { +internal inline fun String.substr(startIndex: Double): String { return this.substring(startIndex.toInt()) } -internal fun String.splitBy(separator: String): List { +internal inline fun String.splitBy(separator: String): List { return List(this.split(separator)) } -internal fun String.replace(pattern: RegExp, replacement: String): String { +internal inline fun String.replace(pattern: RegExp, replacement: String): String { return pattern.replace(this, replacement) } -internal fun String.indexOfInDouble(item: String): Double { +internal fun String.replace(pattern: RegExp, replacement: (match:String, group1:String) -> String): String { + return pattern.replace(this, replacement) +} + +internal inline fun String.indexOfInDouble(item: String): Double { return this.indexOf(item).toDouble() } +internal inline fun String.indexOfInDouble(item: String, startIndex: Double): Double { + return this.indexOf(item, startIndex.toInt()).toDouble() +} + internal fun Double.toInvariantString(base: Double): String { return this.toInt().toString(base.toInt()) } @@ -92,68 +100,71 @@ internal fun Double.toInvariantString(): String { return this.toInt().toString(); } -internal fun IAlphaTabEnum.toInvariantString(): String { +internal inline fun IAlphaTabEnum.toInvariantString(): String { return this.toString() } -internal fun Double.toFixed(decimals: Double): String { - return String.format("%.${decimals}f", this); +internal inline fun Double.toFixed(decimals: Double): String { + return String.format("%.${decimals.toInt()}f", this); } -internal fun String.lastIndexOfInDouble(item: String): Double { +internal inline fun String.lastIndexOfInDouble(item: String): Double { return this.lastIndexOf(item).toDouble() } -internal operator fun Double.plus(str: String): String { +internal inline operator fun Double.plus(str: String): String { return this.toInvariantString() + str } -internal fun String.charAt(index: Double): String { +internal inline fun String.charAt(index: Double): String { return this.substring(index.toInt(), index.toInt() + 1) } -internal fun String.charCodeAt(index: Int): Double { +internal inline fun String.charCodeAt(index: Int): Double { return this[index].code.toDouble() } -internal fun String.charCodeAt(index: Double): Double { +internal inline fun String.charCodeAt(index: Double): Double { return this[index.toInt()].code.toDouble() } -internal fun String.split(delimiter: String): List { - @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") +internal inline fun String.split(delimiter: String): List { return List(this.split(delimiters = arrayOf(delimiter), ignoreCase = false, limit = 0)) } -internal fun String.substring(startIndex: Double, endIndex: Double): String { +internal inline fun String.substring(startIndex: Double, endIndex: Double): String { return this.substring(startIndex.toInt(), endIndex.toInt()) } -internal operator fun String.get(index: Double): Char { +internal inline operator fun String.get(index: Double): Char { return this[index.toInt()] } -internal fun String.substring(startIndex: Double): String { +internal inline fun String.substring(startIndex: Double): String { return this.substring(startIndex.toInt()) } -internal fun String.replaceAll(before: String, after: String): String { +internal inline fun String.replaceAll(before: String, after: String): String { return this.replace(before, after) } -internal fun IAlphaTabEnum.toDouble(): Double { +internal inline fun String.replaceAll(search: RegExp, after: String): String { + return this.replace(search, after) +} + +internal inline fun IAlphaTabEnum.toDouble(): Double { return this.value.toDouble() } -internal fun IAlphaTabEnum?.toDouble(): Double? { +internal inline fun IAlphaTabEnum?.toDouble(): Double? { return this?.value.toDouble() } -internal fun IAlphaTabEnum.toInt(): Int { +internal inline fun IAlphaTabEnum.toInt(): Int { return this.value } -internal fun IAlphaTabEnum?.toInt(): Int? { +internal inline fun IAlphaTabEnum?.toInt(): Int? { return this?.value } @@ -175,13 +186,23 @@ internal fun Any?.toDouble(): Double { throw ClassCastException("Cannot cast ${this::class.simpleName} to double") } -internal fun Int?.toDouble(): Double? { +internal fun Any?.toLong(): Long { + if (this is Long) { + return this + } + if (this == null) { + throw ClassCastException("Cannot cast null to long") + } + throw ClassCastException("Cannot cast ${this::class.simpleName} to long") +} + +internal inline fun Int?.toDouble(): Double? { return this?.toDouble() } -internal fun String.toDoubleOrNaN(): Double { +internal inline fun String.toDoubleOrNaN(): Double { try { - val number = NumberFormat.getInstance(Locale.ROOT).parse(this) + val number = NumberFormat.getInstance(Locale.ROOT).parse(this.trim()) if (number != null) { return number.toDouble() } @@ -192,7 +213,7 @@ internal fun String.toDoubleOrNaN(): Double { internal fun String.toIntOrNaN(): Double { try { - val number = NumberFormat.getInstance(Locale.ROOT).parse(this) + val number = NumberFormat.getInstance(Locale.ROOT).parse(this.trim()) if (number != null) { return number.toInt().toDouble() } @@ -203,7 +224,7 @@ internal fun String.toIntOrNaN(): Double { internal fun String.toIntOrNaN(radix: Double): Double { try { - return Integer.parseInt(this, radix.toInt()).toDouble(); + return Integer.parseInt(this.trim(), radix.toInt()).toDouble(); } catch (e: Throwable) { } return Double.NaN @@ -212,44 +233,27 @@ internal fun String.toIntOrNaN(radix: Double): Double { internal class Globals { companion object { - const val NaN: Double = Double.NaN val console = Console() - public fun isNaN(s: Double): Boolean { - return s.isNaN() - } - - public fun parseFloat(s: String): Double { - return s.toDoubleOrNaN() - } - - fun parseInt(s: String): Double { - return s.toIntOrNaN() - } - - fun parseInt(s: String, radix: Double): Double { - return s.toIntOrNaN(radix) - } - - fun parseInt(s: Char): Double { - return parseInt(s.toString()) - } - - fun parseInt(s: Char, radix: Double): Double { - return parseInt(s.toString(), radix) - } - - fun setImmediate(action: () -> Unit) { + inline fun setImmediate(action: () -> Unit) { action() } - fun setTimeout(action: () -> Unit, millis: Double): Deferred { + inline fun setTimeout(noinline action: () -> Unit, millis: Double): Deferred { @Suppress("OPT_IN_USAGE") return GlobalScope.async { delay(millis.toLong()) action() } } + + val performance = Performance() + } +} + +internal class Performance { + fun now(): Double { + return System.currentTimeMillis().toDouble() } } @@ -285,11 +289,11 @@ internal fun Deferred.catch(callback: (alphaTab.core.ecmaScript.Error) -> internal val ArrayBuffer.byteLength: Int get() = this.size -internal fun String.repeat(count:Double): String { +internal fun String.repeat(count: Double): String { return this.repeat(count.toInt()) } -internal fun String.padStart(length:Double, pad:String): String { +internal fun String.padStart(length: Double, pad: String): String { return this.padStart(length.toInt(), pad[0]) } diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/TypeHelper.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/TypeHelper.kt index 2293dbe48..023362ee8 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/TypeHelper.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/TypeHelper.kt @@ -1,5 +1,7 @@ package alphaTab.core +import alphaTab.AlphaTabError +import alphaTab.AlphaTabErrorType import alphaTab.core.ecmaScript.RegExp import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -7,6 +9,9 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract +import kotlin.reflect.KClass + +typealias Disposable = AutoCloseable internal class TypeHelper { companion object { @@ -14,6 +19,24 @@ internal class TypeHelper { return RegExp(pattern, flags) } + @ExperimentalContracts + @ExperimentalUnsignedTypes + public inline fun > parseEnum(value: String, @Suppress("UNUSED_PARAMETER") type: KClass): T { + return parseEnum(value, enumValues()) + } + + @ExperimentalContracts + @ExperimentalUnsignedTypes + public fun > parseEnum(value: String, values: Array): T { + val valueLower = value.lowercase() + for (e in values) { + if (valueLower.equals(e.name, true)) { + return e + } + } + throw AlphaTabError(AlphaTabErrorType.General, "Could not parse enum value '$value'") + } + @ExperimentalContracts public fun isTruthy(s: String?): Boolean { contract { returns(true) implies (s != null) } @@ -30,6 +53,11 @@ internal class TypeHelper { return s != null } + @ExperimentalContracts + public fun isTruthy(s: Double): Boolean { + return !s.isNaN() && s != 0.0 + } + @ExperimentalUnsignedTypes public fun typeOf(s: Any?): String { return when (s) { diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Array.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Array.kt index cd1e9910d..7e3450e91 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Array.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Array.kt @@ -1,11 +1,16 @@ package alphaTab.core.ecmaScript +import alphaTab.collections.IDoubleIterable + @Suppress("NOTHING_TO_INLINE") internal class Array { companion object { public inline fun from(x: Iterable): alphaTab.collections.List { return alphaTab.collections.List(x) } + public inline fun from(x: IDoubleIterable): alphaTab.collections.DoubleList { + return alphaTab.collections.DoubleList(x) + } public inline fun isArray(x:Any?):Boolean { return x is alphaTab.collections.List<*> } diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Iterable.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Iterable.kt deleted file mode 100644 index 1f6312312..000000000 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Iterable.kt +++ /dev/null @@ -1,3 +0,0 @@ -package alphaTab.core.ecmaScript - -internal typealias Iterable = kotlin.collections.Iterable diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Number.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Number.kt new file mode 100644 index 000000000..21381eb66 --- /dev/null +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Number.kt @@ -0,0 +1,40 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package alphaTab.core.ecmaScript + +import alphaTab.core.toDoubleOrNaN +import alphaTab.core.toIntOrNaN + +internal class Number { + + companion object { + const val POSITIVE_INFINITY: Double = Double.POSITIVE_INFINITY + const val MAX_SAFE_INTEGER: Double = 9007199254740991.0 + const val MIN_SAFE_INTEGER: Double = -9007199254740991.0 + const val NaN: Double = Double.NaN + + inline fun isNaN(s: Double): Boolean { + return s.isNaN() + } + + inline fun parseFloat(s: String): Double { + return s.toDoubleOrNaN() + } + + inline fun parseInt(s: String): Double { + return s.toIntOrNaN() + } + + inline fun parseInt(s: String, radix: Double): Double { + return s.toIntOrNaN(radix) + } + + inline fun parseInt(s: Char): Double { + return parseInt(s.toString()) + } + + inline fun parseInt(s: Char, radix: Double): Double { + return parseInt(s.toString(), radix) + } + } +} diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Promise.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Promise.kt index 54a492757..dc4619e93 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Promise.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Promise.kt @@ -1,18 +1,23 @@ package alphaTab.core.ecmaScript +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.selects.select internal class Promise { companion object { - suspend fun race(promises: alphaTab.collections.List>) { - return coroutineScope { - select { - for(p in promises) { - p.onAwait { it } - } - }.also { coroutineContext.cancelChildren() } + fun race(promises: alphaTab.collections.List>): kotlinx.coroutines.Deferred { + @Suppress("OPT_IN_USAGE") + return GlobalScope.async { + coroutineScope { + select { + for (p in promises) { + p.onAwait { it } + } + }.also { coroutineContext.cancelChildren() } + } } } } diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/RegExp.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/RegExp.kt index ff9b4bc25..9189c5f37 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/RegExp.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/RegExp.kt @@ -1,29 +1,35 @@ package alphaTab.core.ecmaScript +import android.os.Build +import java.util.regex.Pattern + private data class RegExpCacheEntry(val pattern: String, val flags: String) + private val RegexpCache = HashMap() internal class RegExp { - private var _regex: Regex + private var _regex: Pattern private var _global: Boolean public constructor(regex: String, flags: String = "") { val cache = RegexpCache.getOrPut(RegExpCacheEntry(regex, flags), { - val options = HashSet() + var options = 0 for (c in flags) { when (c) { 'i' -> { - options.add(RegexOption.IGNORE_CASE) + options = options or Pattern.CASE_INSENSITIVE } + 'g' -> { _global = true } + 'm' -> { - options.add(RegexOption.MULTILINE) + options = options or Pattern.MULTILINE } } } - _regex = Regex(regex, options) + _regex = Pattern.compile(regex, options) this }) @@ -32,13 +38,52 @@ internal class RegExp { } public fun exec(s: String): Boolean { - return _regex.matches(s) + return _regex.matcher(s).matches() } public fun replace(s: String, replacement: String): String { return if (_global) - _regex.replace(s, replacement) + _regex.matcher(s).replaceAll(replacement) else - _regex.replaceFirst(s, replacement) + _regex.matcher(s).replaceFirst(replacement) + } + + fun replace(s: String, replacement: (match: String, group1: String) -> String): String { + val matcher = _regex.matcher(s); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return if (_global) + matcher.replaceAll { + replacement(it.group(), it.group(1)) + } + else + matcher.replaceFirst { + replacement(it.group(), it.group(1)) + } + } else { + // custom implementation for older android versions + if(!matcher.find()){ + return s; + } + + val sb = StringBuilder() + + var appendPos = 0 + if(_global) { + + do { + sb.append(s, appendPos, matcher.start()) + sb.append(replacement(matcher.group(), matcher.group(1)!!)) + appendPos = matcher.end() + } while (matcher.find()) + + sb.append(s, appendPos, s.length) + } else { + sb.append(s, appendPos, matcher.start()) + sb.append(replacement(matcher.group(), matcher.group(1)!!)) + sb.append(s, matcher.end(), s.length) + } + + return sb.toString() + } } } diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Set.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Set.kt index e364a089a..fd6d74aba 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Set.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/core/ecmaScript/Set.kt @@ -1,42 +1,50 @@ package alphaTab.core.ecmaScript +import alphaTab.collections.ObjectBooleanMap + public class Set : Iterable { - private val _set: HashSet + private val _storage: ObjectBooleanMap public constructor() { - _set = HashSet() + _storage = ObjectBooleanMap() } public val size : Double - get() = _set.size.toDouble() + get() = _storage.size.toDouble() public constructor(values: Iterable?) { - _set = values?.toHashSet() ?: HashSet() + _storage = ObjectBooleanMap() + if(values != null){ + for(v in values) { + add(v) + } + } + } public fun add(item: T) { - _set.add(item) + _storage.set(item, true) } public fun has(item: T): Boolean { - return _set.contains(item) + return _storage.has(item) } public fun delete(item: T) { - _set.remove(item) + _storage.delete(item) } public fun forEach(action: (item: T) -> Unit) { - for (i in _set) { + for (i in _storage.keys()) { action(i) } } override fun iterator(): Iterator { - return _set.iterator() + return _storage.keys().iterator() } fun clear() { - _set.clear() + _storage.clear() } } diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AlphaTabRenderSurface.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AlphaTabRenderSurface.kt index 0ceee29bd..18c02362f 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AlphaTabRenderSurface.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AlphaTabRenderSurface.kt @@ -93,7 +93,7 @@ internal class AlphaTabRenderSurface(context: Context, attributeSet: AttributeSe public fun fillPlaceholder(result: RenderFinishedEventArgs) { if (_resultIdToIndex.has(result.id)) { - val index = _resultIdToIndex.get(result.id); + val index = _resultIdToIndex.get(result.id)!! _placeholders[index.toInt()].apply { this.result = result this.state = RenderPlaceholder.STATE_RENDER_DONE diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt index 037acabb0..3a5c8abb8 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidAudioWorker.kt @@ -46,10 +46,14 @@ internal class AndroidAudioWorker( _writeThread = Thread { this@AndroidAudioWorker.writeSamples() } - _writeThread!!.name = "alphaTab Audio Worker"; + _writeThread!!.name = "alphaTab Audio Worker" _writeThread!!.start() } + fun setOutputDevice(device: AudioDeviceInfo?) { + _track.preferredDevice = device + } + private fun writeSamples() { while (!_stopped) { if (_track.playState == AudioTrack.PLAYSTATE_PLAYING) { @@ -82,7 +86,7 @@ internal class AndroidAudioWorker( _track.play() _stopped = false - _updateSchedule = _updateTimer.scheduleAtFixedRate( + _updateSchedule = _updateTimer.scheduleWithFixedDelay( { this@AndroidAudioWorker.onUpdatePlayedSamples() }, 0L, 50L, TimeUnit.MILLISECONDS @@ -103,7 +107,7 @@ internal class AndroidAudioWorker( private var _previousPosition: Int = -1 private val _timestamp = AudioTimestamp() - private val _lastTimestampUpdate: Long = -1L; + private val _lastTimestampUpdate: Long = -1L private fun onUpdatePlayedSamples() { val sinceUpdateInMillis = (System.nanoTime() - _lastTimestampUpdate) / 10e6 diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidCanvas.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidCanvas.kt index 1f75e7784..45604cb45 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidCanvas.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidCanvas.kt @@ -9,7 +9,7 @@ import alphaTab.model.MusicFontSymbol import alphaTab.platform.ICanvas import alphaTab.platform.TextAlign import alphaTab.platform.TextBaseline -import alphaTab.platform.TextMetrics +import alphaTab.platform.MeasuredText import android.content.Context import android.graphics.* import kotlin.contracts.ExperimentalContracts @@ -28,7 +28,7 @@ val CustomTypeFaces = HashMap() internal class AndroidCanvas : ICanvas { companion object { public fun initialize(context: Context) { - MusicFont = Typeface.createFromAsset(context.assets, "Bravura.ttf") + MusicFont = Typeface.createFromAsset(context.assets, "Bravura.otf") } public fun registerCustomFont(name: String, face: Typeface) { @@ -284,9 +284,9 @@ internal class AndroidCanvas : ICanvas { } - override fun measureText(text: String): TextMetrics { + override fun measureText(text: String): MeasuredText { if (text.isEmpty()) { - return TextMetrics(0.0, 0.0) + return MeasuredText(0.0, 0.0) } var width = 0.0 var height = 0.0 @@ -297,7 +297,7 @@ internal class AndroidCanvas : ICanvas { width = bounds.width().toDouble() height = bounds.height().toDouble() }) - return TextMetrics(width, height) + return MeasuredText(width, height) } override fun fillMusicFontSymbol( diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidEnvironment.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidEnvironment.kt index 75456b014..d20bae325 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidEnvironment.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidEnvironment.kt @@ -11,6 +11,9 @@ internal class AndroidEnvironment { companion object { private var _isInitialized: Boolean = false + var screenWidth:Int = 0; + var screenHeight:Int = 0; + @ExperimentalUnsignedTypes @ExperimentalContracts public fun initializeAndroid(context: android.content.Context) { @@ -21,10 +24,13 @@ internal class AndroidEnvironment { Environment.HighDpiFactor = context.resources.displayMetrics.density.toDouble() + screenWidth = context.resources.displayMetrics.widthPixels + screenHeight = context.resources.displayMetrics.heightPixels + AndroidCanvas.initialize(context) var bravuraBytes: ByteArray; - context.assets.open("Bravura.ttf").use { + context.assets.open("Bravura.otf").use { bravuraBytes = it.readBytes() } diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt index 0a6273c56..fb18f5d30 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidSynthOutput.kt @@ -3,14 +3,23 @@ package alphaTab.platform.android import alphaTab.* import alphaTab.EventEmitter import alphaTab.EventEmitterOfT +import alphaTab.collections.List import alphaTab.synth.ISynthOutput import alphaTab.synth.ds.CircularSampleBuffer import kotlin.contracts.ExperimentalContracts import alphaTab.core.ecmaScript.Float32Array +import alphaTab.synth.ISynthOutputDevice +import android.content.Context +import android.media.AudioDeviceInfo +import android.media.AudioManager +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlin.math.min @ExperimentalUnsignedTypes @ExperimentalContracts internal class AndroidSynthOutput( + private val context: Context, private val synthInvoke: (action: (() -> Unit)) -> Unit ) : ISynthOutput { companion object { @@ -18,6 +27,7 @@ internal class AndroidSynthOutput( private const val PreferredSampleRate = 44100 } + private var _device: ISynthOutputDevice? = null private var _bufferCount = 0 private var _requestedBufferCount = 0 @@ -106,7 +116,7 @@ internal class AndroidSynthOutput( fun read(buffer: FloatArray, offset: Int, sampleCount: Int): Int { val read = Float32Array(sampleCount.toDouble()) - val actual = _circularBuffer.read(read, 0.0, Math.min(read.length, _circularBuffer.count)) + val actual = _circularBuffer.read(read, 0.0, min(read.length, _circularBuffer.count)) read.data.copyInto(buffer, offset, 0, sampleCount) requestBuffers() @@ -117,4 +127,35 @@ internal class AndroidSynthOutput( override val ready: IEventEmitter = EventEmitter() override val samplesPlayed: IEventEmitterOfT = EventEmitterOfT() override val sampleRequest: IEventEmitter = EventEmitter() + + override suspend fun enumerateOutputDevices(): List { + val audioService = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager? + ?: return List() + + return List( + audioService.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { + it.isSink + }.map { AndroidOutputDevice(it) } + ) + } + + override suspend fun setOutputDevice(device: ISynthOutputDevice?): Unit { + _audioContext.setOutputDevice((device as AndroidOutputDevice?)?.device) + _device = device + } + + override suspend fun getOutputDevice(): ISynthOutputDevice? { + return _device + } +} + +@ExperimentalUnsignedTypes +@ExperimentalContracts +internal class AndroidOutputDevice(val device: AudioDeviceInfo) : ISynthOutputDevice { + override val deviceId: String + get() = device.id.toString() + override val label: String + get() = device.productName.toString() + override val isDefault: Boolean + get() = false } diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt index 0bda1b011..d4e111689 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidThreadAlphaSynthWorkerPlayer.kt @@ -30,6 +30,9 @@ internal class AndroidThreadAlphaSynthWorkerPlayer : IAlphaSynth, Runnable { private var _logLevel: LogLevel private var _bufferTimeInMilliseconds: Double + override val output: ISynthOutput + get() = _output + constructor( logLevel: LogLevel, output: ISynthOutput, diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidUiFacade.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidUiFacade.kt index 638f7b9c7..db0324b3c 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidUiFacade.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/android/AndroidUiFacade.kt @@ -159,7 +159,7 @@ internal class AndroidUiFacade : IUiFacade { var player: AndroidThreadAlphaSynthWorkerPlayer? = null player = AndroidThreadAlphaSynthWorkerPlayer( api.settings.core.logLevel, - AndroidSynthOutput { + AndroidSynthOutput(_renderWrapper.context) { player!!.addToWorker(it) }, this::beginInvoke, diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaCanvas.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaCanvas.kt index eac0abdb1..67093d34a 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaCanvas.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaCanvas.kt @@ -1,7 +1,7 @@ package alphaTab.platform.skia @ExperimentalUnsignedTypes -class AlphaSkiaCanvas : AutoCloseable { +internal class AlphaSkiaCanvas : AutoCloseable { companion object { fun rgbaToColor(r: Double, g: Double, b: Double, a: Double): UInt { return alphaTab.alphaSkia.AlphaSkiaCanvas.rgbaToColor( @@ -103,7 +103,7 @@ class AlphaSkiaCanvas : AutoCloseable { fun fillText( text: String, - typeFace: AlphaSkiaTypeface, + textStyle: AlphaSkiaTextStyle, fontSize: Double, x: Double, y: Double, @@ -112,21 +112,27 @@ class AlphaSkiaCanvas : AutoCloseable { ) { this.canvas.fillText( text, - typeFace.typeface, + textStyle.textStyle, fontSize.toFloat(), x.toFloat(), y.toFloat(), textAlign.align, - textBaseline.align + textBaseline.baseline ) } fun measureText( text: String, - typeFace: AlphaSkiaTypeface, - fontSize: Double - ): Double { - return this.canvas.measureText(text, typeFace.typeface, fontSize.toFloat()).toDouble() + textStyle: AlphaSkiaTextStyle, + fontSize: Double, + textAlign: AlphaSkiaTextAlign, + textBaseline: AlphaSkiaTextBaseline + + ): AlphaSkiaTextMetrics { + return AlphaSkiaTextMetrics(this.canvas.measureText(text, textStyle.textStyle, fontSize.toFloat(), + textAlign.align, + textBaseline.baseline + )) } fun beginRotate(centerX: Double, centerY: Double, angle: Double) { diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaImage.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaImage.kt index 792b2edf6..d64dd1d5d 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaImage.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaImage.kt @@ -3,7 +3,7 @@ package alphaTab.platform.skia import alphaTab.core.ecmaScript.ArrayBuffer @ExperimentalUnsignedTypes -class AlphaSkiaImage internal constructor(internal val image: alphaTab.alphaSkia.AlphaSkiaImage) : +public class AlphaSkiaImage internal constructor(internal val image: alphaTab.alphaSkia.AlphaSkiaImage) : AutoCloseable { companion object { fun decode(data: ArrayBuffer): AlphaSkiaImage? { diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextAlign.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextAlign.kt index fe75a07ff..29c9661b3 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextAlign.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextAlign.kt @@ -1,6 +1,6 @@ package alphaTab.platform.skia -enum class AlphaSkiaTextAlign(internal val align: alphaTab.alphaSkia.AlphaSkiaTextAlign) { +internal enum class AlphaSkiaTextAlign(internal val align: alphaTab.alphaSkia.AlphaSkiaTextAlign) { Left(alphaTab.alphaSkia.AlphaSkiaTextAlign.LEFT), Center(alphaTab.alphaSkia.AlphaSkiaTextAlign.CENTER), Right(alphaTab.alphaSkia.AlphaSkiaTextAlign.RIGHT) diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextBaseline.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextBaseline.kt index d864b06db..c9dc4ebc4 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextBaseline.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextBaseline.kt @@ -1,6 +1,6 @@ package alphaTab.platform.skia -enum class AlphaSkiaTextBaseline(internal val align: alphaTab.alphaSkia.AlphaSkiaTextBaseline) { +internal enum class AlphaSkiaTextBaseline(internal val baseline: alphaTab.alphaSkia.AlphaSkiaTextBaseline) { Alphabetic(alphaTab.alphaSkia.AlphaSkiaTextBaseline.ALPHABETIC), Top(alphaTab.alphaSkia.AlphaSkiaTextBaseline.TOP), Middle(alphaTab.alphaSkia.AlphaSkiaTextBaseline.MIDDLE), diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextMetrics.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextMetrics.kt new file mode 100644 index 000000000..4da2e1367 --- /dev/null +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextMetrics.kt @@ -0,0 +1,22 @@ +package alphaTab.platform.skia + +internal class AlphaSkiaTextMetrics(val textMetrics: alphaTab.alphaSkia.AlphaSkiaTextMetrics) : + AutoCloseable { + + val width: Double get() = textMetrics.width.toDouble() + val actualBoundingBoxLeft: Double get() = textMetrics.actualBoundingBoxLeft.toDouble() + val actualBoundingBoxRight: Double get() = textMetrics.actualBoundingBoxRight.toDouble() + val fontBoundingBoxAscent: Double get() = textMetrics.fontBoundingBoxAscent.toDouble() + val fontBoundingBoxDescent: Double get() = textMetrics.fontBoundingBoxDescent.toDouble() + val actualBoundingBoxAscent: Double get() = textMetrics.actualBoundingBoxAscent.toDouble() + val actualBoundingBoxDescent: Double get() = textMetrics.actualBoundingBoxDescent.toDouble() + val emHeightAscent: Double get() = textMetrics.emHeightAscent.toDouble() + val emHeightDescent: Double get() = textMetrics.emHeightDescent.toDouble() + val hangingBaseline: Double get() = textMetrics.hangingBaseline.toDouble() + val alphabeticBaseline: Double get() = textMetrics.alphabeticBaseline.toDouble() + val ideographicBaseline: Double get() = textMetrics.ideographicBaseline.toDouble() + + override fun close() { + textMetrics.close() + } +} diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextStyle.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextStyle.kt new file mode 100644 index 000000000..2cf20bbf3 --- /dev/null +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTextStyle.kt @@ -0,0 +1,29 @@ +package alphaTab.platform.skia + +import alphaTab.collections.toArray + +internal class AlphaSkiaTextStyle : AutoCloseable { + + val textStyle: alphaTab.alphaSkia.AlphaSkiaTextStyle + + constructor(textStyle: alphaTab.alphaSkia.AlphaSkiaTextStyle) { + this.textStyle = textStyle + } + + val fontFamilies: alphaTab.collections.List get() = alphaTab.collections.List(*this.textStyle.fontFamilies) + val weight: Double get() = this.textStyle.weight.toDouble() + val isItalic: Boolean get() = this.textStyle.isItalic + + constructor(familyNames: alphaTab.collections.List, weight: Double, isItalic: Boolean) + : this( + alphaTab.alphaSkia.AlphaSkiaTextStyle( + familyNames.toArray(), + weight.toInt(), + isItalic + ) + ) + + override fun close() { + textStyle.close() + } +} diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTypeface.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTypeface.kt index 0b3b3661f..cfd2e9dce 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTypeface.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/AlphaSkiaTypeface.kt @@ -1,6 +1,6 @@ package alphaTab.platform.skia -class AlphaSkiaTypeface private constructor(internal val typeface: alphaTab.alphaSkia.AlphaSkiaTypeface) : +internal class AlphaSkiaTypeface private constructor(internal val typeface: alphaTab.alphaSkia.AlphaSkiaTypeface) : AutoCloseable { companion object { fun create(family: String, isBold: Boolean, isItalic: Boolean): AlphaSkiaTypeface? { @@ -21,8 +21,8 @@ class AlphaSkiaTypeface private constructor(internal val typeface: alphaTab.alph val isItalic: Boolean get() = this.typeface.isItalic - val isBold: Boolean - get() = this.typeface.isBold + val weight: Double + get() = this.typeface.weight.toDouble() override fun close() { this.typeface.close() diff --git a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/SkiaCanvasPartials.kt b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/SkiaCanvasPartials.kt index f8e31f8a9..030dd735e 100644 --- a/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/SkiaCanvasPartials.kt +++ b/src.kotlin/alphaTab/android/src/main/java/alphaTab/platform/skia/SkiaCanvasPartials.kt @@ -2,7 +2,7 @@ package alphaTab.platform.skia import kotlin.contracts.ExperimentalContracts -class SkiaCanvasPartials { +internal class SkiaCanvasPartials { companion object { @Suppress("UNUSED_PARAMETER") @ExperimentalContracts diff --git a/src.kotlin/alphaTab/android/src/test/java/alphaTab/PrettyFormatPartials.kt b/src.kotlin/alphaTab/android/src/test/java/alphaTab/PrettyFormatPartials.kt new file mode 100644 index 000000000..d63cd7440 --- /dev/null +++ b/src.kotlin/alphaTab/android/src/test/java/alphaTab/PrettyFormatPartials.kt @@ -0,0 +1,25 @@ +package alphaTab + +import alphaTab.collections.DoubleBooleanMap +import alphaTab.collections.DoubleDoubleMap +import alphaTab.collections.DoubleObjectMap +import alphaTab.collections.ObjectBooleanMap +import alphaTab.collections.ObjectDoubleMap +import alphaTab.core.ArrayTuple +import alphaTab.core.IArrayTuple + +@kotlin.contracts.ExperimentalContracts +@kotlin.ExperimentalUnsignedTypes +class PrettyFormatPartials { + companion object { + fun mapAsUnknownIterable(map: Any?): Iterable> = when(map) { + is alphaTab.collections.Map<*, *> -> map.map { ArrayTuple(it.key, it.value) } + is DoubleBooleanMap -> map.map { ArrayTuple(it.key, it.value) } + is DoubleDoubleMap -> map.map { ArrayTuple(it.key, it.value) } + is DoubleObjectMap<*> -> map.map { ArrayTuple(it.key, it.value) } + is ObjectBooleanMap<*> -> map.map { ArrayTuple(it.key, it.value) } + is ObjectDoubleMap<*> -> map.map { ArrayTuple(it.key, it.value) } + else -> throw ClassCastException("Invalid map type: " + map?.javaClass?.name) + } + } +} diff --git a/src.kotlin/alphaTab/android/src/test/java/alphaTab/ScoreSerializerPluginPartials.kt b/src.kotlin/alphaTab/android/src/test/java/alphaTab/ScoreSerializerPluginPartials.kt new file mode 100644 index 000000000..ba9d74ccc --- /dev/null +++ b/src.kotlin/alphaTab/android/src/test/java/alphaTab/ScoreSerializerPluginPartials.kt @@ -0,0 +1,18 @@ +package alphaTab + +import alphaTab.collections.BooleanList +import alphaTab.collections.DoubleList + +class ScoreSerializerPluginPartials { + companion object { + fun isPlatformTypeEqual(v: Any?, dv: Any?): Boolean { + if(v is DoubleList && dv is DoubleList) { + return v.length == dv.length + } + else if(v is BooleanList && dv is BooleanList) { + return v.length == dv.length + } + throw Error("Unexpected value in serialized json " + dv?.javaClass?.name) + } + } +} diff --git a/src.kotlin/alphaTab/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt b/src.kotlin/alphaTab/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt index 021536edb..ecee670c0 100644 --- a/src.kotlin/alphaTab/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt +++ b/src.kotlin/alphaTab/android/src/test/java/alphaTab/TestPlatformPartialsImpl.kt @@ -1,12 +1,14 @@ package alphaTab import alphaTab.core.ecmaScript.Uint8Array +import kotlinx.coroutines.CompletableDeferred import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream import java.io.OutputStream import java.nio.file.Paths import kotlin.contracts.ExperimentalContracts +import kotlin.reflect.KClass @ExperimentalUnsignedTypes @ExperimentalContracts @@ -17,7 +19,11 @@ class TestPlatformPartials { filePath.toFile().delete() } - fun loadFile(path: String): Uint8Array { + fun loadFile(path: String): kotlinx.coroutines.Deferred { + return CompletableDeferred(loadFileSync(path)) + } + + fun loadFileSync(path: String): Uint8Array { val fs = openFileRead(path) val ms = ByteArrayOutputStream() fs.use { @@ -32,17 +38,18 @@ class TestPlatformPartials { path = path.parent ?: throw AlphaTabError(AlphaTabErrorType.General, "Could not find project root") } - println(path.toString()) path.toString() } private fun openFileRead(path: String): InputStream { - val filePath = Paths.get(projectRoot, path) + val sub = Paths.get(path) + val filePath = if(sub.isAbsolute) sub else Paths.get(projectRoot, path) return filePath.toFile().inputStream() } private fun openFileWrite(path: String): OutputStream { - val fullPath = Paths.get(projectRoot, path) + val sub = Paths.get(path) + val fullPath = if(sub.isAbsolute) sub else Paths.get(projectRoot, path) Logger.info("Test", "Saving file '$path' to '$fullPath'") fullPath.parent.toFile().mkdirs() return fullPath.toFile().outputStream() @@ -68,5 +75,11 @@ class TestPlatformPartials { ?.filter { it.isFile } ?.map { it.name } ?: emptyList()) } + + internal inline fun > enumValues( + @Suppress("UNUSED_PARAMETER") type: KClass + ): alphaTab.collections.List { + return alphaTab.collections.List(enumValues().toMutableList()) + } } } diff --git a/src.kotlin/alphaTab/android/src/test/java/alphaTab/core/TestGlobals.kt b/src.kotlin/alphaTab/android/src/test/java/alphaTab/core/TestGlobals.kt index c3e859cf4..64cea5754 100644 --- a/src.kotlin/alphaTab/android/src/test/java/alphaTab/core/TestGlobals.kt +++ b/src.kotlin/alphaTab/android/src/test/java/alphaTab/core/TestGlobals.kt @@ -1,9 +1,17 @@ package alphaTab.core +import alphaTab.SnapshotFileRepository +import alphaTab.TestPlatformPartials +import alphaTab.collections.ObjectDoubleMap import org.junit.Assert -import kotlin.math.exp +import java.lang.reflect.Method +import java.nio.file.Paths +import kotlin.contracts.ExperimentalContracts import kotlin.reflect.KClass +annotation class TestName(val name: String) +annotation class SnapshotFile(val path: String) + typealias Test = org.junit.Test class assert { @@ -42,8 +50,7 @@ class NotExpector(private val actual: T) { class Expector(private val actual: T) { val to get() = this - val not - get() = NotExpector(actual) + fun not() = NotExpector(actual) val be get() = this @@ -74,11 +81,21 @@ class Expector(private val actual: T) { Assert.assertEquals(message, expectedTyped, actualToCheck as Any?) } + + fun lessThan(expected: Double) { + if (actual is Number) { + Assert.assertTrue("Expected $actual to be less than $expected", actual.toDouble() < expected) + } else { + Assert.fail("lessThan can only be used with numeric operands"); + } + } + + fun greaterThan(expected: Double) { if (actual is Number) { Assert.assertTrue("Expected $actual to be greater than $expected", actual.toDouble() > expected) } else { - Assert.fail("toBeCloseTo can only be used with numeric operands"); + Assert.fail("greaterThan can only be used with numeric operands"); } } fun closeTo(expected: Double, delta: Double, message: String? = null) { @@ -160,10 +177,72 @@ class Expector(private val actual: T) { 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(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() { + val testMethodInfo = findTestMethod() + val file = testMethodInfo.getAnnotation(SnapshotFile::class.java)?.path + if (file.isNullOrEmpty()) { + Assert.fail("Missing SnapshotFile annotation with path to .snap file") + } + + val absoluteSnapFilePath = Paths.get( + TestPlatformPartials.projectRoot, + file + ).toAbsolutePath(); + if (!absoluteSnapFilePath.toFile().exists()) { + Assert.fail("Could not find snapshot file at $absoluteSnapFilePath") + } + + val snapshotFile = SnapshotFileRepository.loadSnapshortFile(absoluteSnapFilePath.toString()) + + val testSuiteName = testMethodInfo.declaringClass.simpleName + val testName = testMethodInfo.getAnnotation(TestName::class.java)!!.name + + val fullTestName = "$testSuiteName $testName " + + val counter = (TestGlobals.snapshotAssertionCounters.get(fullTestName) ?: 0.0) + 1 + TestGlobals.snapshotAssertionCounters.set(fullTestName, counter) + + val snapshotName = "$fullTestName${counter.toInt()}" + + val error = snapshotFile.match(snapshotName, actual) + if (!error.isNullOrEmpty()) { + Assert.fail(error) + } + } } class TestGlobals { + @ExperimentalUnsignedTypes + @ExperimentalContracts companion object { + val snapshotAssertionCounters: ObjectDoubleMap = ObjectDoubleMap() + fun expect(actual: T): Expector { return Expector(actual); } @@ -175,5 +254,7 @@ class TestGlobals { fun fail(message: Any?) { Assert.fail(message.toString()) } + + } } diff --git a/src.kotlin/alphaTab/gradle/libs.versions.toml b/src.kotlin/alphaTab/gradle/libs.versions.toml index 4ac2b4b52..2aa487df4 100644 --- a/src.kotlin/alphaTab/gradle/libs.versions.toml +++ b/src.kotlin/alphaTab/gradle/libs.versions.toml @@ -1,18 +1,18 @@ [versions] -agp = "8.8.1" -kotlin = "2.1.10" +agp = "8.9.1" +kotlin = "2.1.20" -kotlinx-coroutines = "1.10.1" +kotlinx-coroutines = "1.10.2" dokka = "2.0.0" espressoCore = "3.6.1" appcompat = "1.7.0" -coreKtx = "1.15.0" +coreKtx = "1.16.0" junit = "4.13.2" junitVersion = "1.2.1" -alphaskia = "2.3.120" -mavenPublish = "0.30.1-SNAPSHOT" +alphaskia = "3.3.135" +mavenPublish = "0.31.0" [libraries] diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index d462510ed..05720ea77 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -2,53 +2,100 @@ import { AlphaSynthMidiFileHandler } from '@src/midi/AlphaSynthMidiFileHandler'; import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; import { MidiFile } from '@src/midi/MidiFile'; + +import type { + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + MidiEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + TimeSignatureEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + AlphaTabMetronomeEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + AlphaTabRestEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + NoteOnEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + NoteOffEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + ControlChangeEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + ProgramChangeEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + TempoChangeEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + PitchBendEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + NoteBendEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + EndOfTrackEvent +} from '@src/midi/MidiEvent'; + +import type { + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + MetaEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + MetaDataEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + MetaNumberEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + Midi20PerNotePitchBendEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + SystemCommonEvent, + // biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 + SystemExclusiveEvent +} from '@src/midi/DeprecatedEvents'; + import { - MidiTickLookup, - MidiTickLookupFindBeatResult, + type MidiTickLookup, + type MidiTickLookupFindBeatResult, MidiTickLookupFindBeatResultCursorMode } from '@src/midi/MidiTickLookup'; -import { IAlphaSynth } from '@src/synth/IAlphaSynth'; +import type { IAlphaSynth } from '@src/synth/IAlphaSynth'; import { PlaybackRange } from '@src/synth/PlaybackRange'; import { PlayerState } from '@src/synth/PlayerState'; -import { PlayerStateChangedEventArgs } from '@src/synth/PlayerStateChangedEventArgs'; -import { PositionChangedEventArgs } from '@src/synth/PositionChangedEventArgs'; +import type { PlayerStateChangedEventArgs } from '@src/synth/PlayerStateChangedEventArgs'; +import type { PositionChangedEventArgs } from '@src/synth/PositionChangedEventArgs'; import { Environment } from '@src/Environment'; -import { EventEmitter, IEventEmitter, IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; +import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; -import { Beat } from '@src/model/Beat'; -import { Score } from '@src/model/Score'; -import { Track } from '@src/model/Track'; +import type { Beat } from '@src/model/Beat'; +import type { Score } from '@src/model/Score'; +import type { Track } from '@src/model/Track'; -import { IContainer } from '@src/platform/IContainer'; -import { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; -import { IUiFacade } from '@src/platform/IUiFacade'; +import type { IContainer } from '@src/platform/IContainer'; +import type { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; +import type { IUiFacade } from '@src/platform/IUiFacade'; import { ScrollMode } from '@src/PlayerSettings'; import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; -import { IScoreRenderer } from '@src/rendering/IScoreRenderer'; +import type { IScoreRenderer } from '@src/rendering/IScoreRenderer'; -import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; +import type { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; -import { Bounds } from '@src/rendering/utils/Bounds'; -import { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; -import { MasterBarBounds } from '@src/rendering/utils/MasterBarBounds'; -import { StaffSystemBounds } from '@src/rendering/utils/StaffSystemBounds'; +import type { Bounds } from '@src/rendering/utils/Bounds'; +import type { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; +import type { MasterBarBounds } from '@src/rendering/utils/MasterBarBounds'; +import type { StaffSystemBounds } from '@src/rendering/utils/StaffSystemBounds'; import { ResizeEventArgs } from '@src/ResizeEventArgs'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { ModelUtils } from '@src/model/ModelUtils'; import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; -import { Note } from '@src/model/Note'; -import { MidiEventType } from '@src/midi/MidiEvent'; -import { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; -import { PlaybackRangeChangedEventArgs } from '@src/synth/PlaybackRangeChangedEventArgs'; +import type { Note } from '@src/model/Note'; +import type { MidiEventType } from '@src/midi/MidiEvent'; +import type { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; +import type { PlaybackRangeChangedEventArgs } from '@src/synth/PlaybackRangeChangedEventArgs'; import { ActiveBeatsChangedEventArgs } from '@src/synth/ActiveBeatsChangedEventArgs'; -import { BeatTickLookupItem } from './midi/BeatTickLookup'; +import type { BeatTickLookupItem } from '@src/midi/BeatTickLookup'; +import type { ISynthOutputDevice } from '@src/synth/ISynthOutput'; + +// biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 +import type { CoreSettings } from '@src/CoreSettings'; class SelectionInfo { public beat: Beat; @@ -70,38 +117,146 @@ export class AlphaTabApiBase { private _trackIndexes: number[] | null = null; private _trackIndexLookup: Set | null = null; private _isDestroyed: boolean = false; + private _score: Score | null = null; + private _tracks: Track[] = []; /** * Gets the UI facade to use for interacting with the user interface. */ public readonly uiFacade: IUiFacade; /** - * Gets the UI container that holds the whole alphaTab control. + * The UI container that holds the whole alphaTab control. + * @remarks + * Gets the UI container that represents the element on which alphaTab was initialized. Note that this is not the raw instance, but a UI framework specific wrapper for alphaTab. + * @category Properties - Core + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * const container = api.container; + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * var container = api.Container; + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * val container = api.container; + * ``` */ public readonly container: IContainer; /** - * Gets the score renderer used for rendering the music sheet. This is the low-level API responsible for the actual rendering chain. + * The score renderer used for rendering the music sheet. + * @remarks + * This is the low-level API responsible for the actual rendering engine. + * Gets access to the underling {@link IScoreRenderer} that is used for the rendering. + * + * @category Properties - Core + * @since 0.9.4 */ public readonly renderer: IScoreRenderer; /** - * Gets the score holding all information about the song being rendered. + * The score holding all information about the song being rendered + * @category Properties - Core + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * updateScoreInfo(api.score); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * UpdateScoreInfo(api.Score); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * updateScoreInfo(api.score) + * ``` */ - public score: Score | null = null; + public get score(): Score | null { + return this._score; + } /** - * Gets the settings that are used for rendering the music notation. + * The settings that are used for rendering the music notation. + * @remarks + * Gets access to the underling {@link Settings} object that is currently used by alphaTab. + * + * @category Properties - Core + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * showSettingsModal(api.settings); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * ShowSettingsDialog(api.Settings); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * showSettingsDialog(api.settings) + * ``` */ public settings!: Settings; /** - * Gets a list of the tracks that are currently rendered; + * The list of the tracks that are currently rendered. + * + * @category Properties - Core + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * highlightCurrentTracksInTrackSelector(api.tracks); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * HighlightCurrentTracksInTrackSelector(api.Tracks); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * highlightCurrentTracksInTrackSelector(api.tracks) + * ``` */ - public tracks: Track[] = []; + public get tracks(): Track[] { + return this._tracks; + } /** - * Gets the UI container that will hold all rendered results. + * The UI container that will hold all rendered results. + * @since 0.9.4 + * @category Properties - Core */ public readonly canvasElement: IContainer; @@ -117,6 +272,8 @@ export class AlphaTabApiBase { uiFacade.initialize(this, settings); Logger.logLevel = this.settings.core.logLevel; + Environment.printEnvironmentInfo(false); + this.canvasElement = uiFacade.createCanvasElement(); this.container.appendChild(this.canvasElement); if ( @@ -139,7 +296,7 @@ export class AlphaTabApiBase { } }, uiFacade.resizeThrottle) ); - let initialResizeEventInfo: ResizeEventArgs = new ResizeEventArgs(); + const initialResizeEventInfo: ResizeEventArgs = new ResizeEventArgs(); initialResizeEventInfo.oldWidth = this.renderer.width; initialResizeEventInfo.newWidth = this.container.width | 0; initialResizeEventInfo.settings = this.settings; @@ -149,8 +306,8 @@ export class AlphaTabApiBase { this.onRenderFinished(renderingResult); }); this.renderer.postRenderFinished.on(() => { - let duration: number = Date.now() - this._startTime; - Logger.debug('rendering', 'Rendering completed in ' + duration + 'ms'); + const duration: number = Date.now() - this._startTime; + Logger.debug('rendering', `Rendering completed in ${duration}ms`); this.onPostRenderFinished(); }); this.renderer.preRender.on(_ => { @@ -175,6 +332,33 @@ export class AlphaTabApiBase { /** * Destroys the alphaTab control and restores the initial state of the UI. + * @remarks + * This function destroys the alphaTab control and tries to restore the initial state of the UI. This might be useful if + * our website is quite dynamic and you need to uninitialize alphaTab from an element again. After destroying alphaTab + * it cannot be used anymore. Any further usage leads to unexpected behavior. + * + * @category Methods - Core + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.destroy(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.Destroy(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.destroy() + * ``` */ public destroy(): void { this._isDestroyed = true; @@ -186,7 +370,42 @@ export class AlphaTabApiBase { } /** - * Applies any changes that were done to the settings object and informs the {@link renderer} about any new values to consider. + * Applies any changes that were done to the settings object. + * @remarks + * It also informs the {@link renderer} about any new values to consider. + * By default alphaTab will not trigger any re-rendering or settings update just if the settings object itself was changed. This method must be called + * to trigger an update of the settings in all components. Then a re-rendering can be initiated using the {@link render} method. + * + * @category Methods - Core + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.settings.display.scale = 2.0; + * api.updateSettings(); + * api.render(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * + * api.Settings.Display.Scale = 2.0; + * api.UpdateSettings(); + * api.Render() + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * + * api.settings.display.scale = 2.0 + * api.updateSettings() + * api.render() + * ``` */ public updateSettings(): void { const score = this.score; @@ -210,11 +429,40 @@ export class AlphaTabApiBase { } /** - * Attempts a load of the score represented by the given data object. - * @param scoreData The data container supported by {@link IUiFacade} + * Initiates a load of the score using the given data. + * @returns true if the data object is supported and a load was initiated, otherwise false + * @param scoreData The data container supported by {@link IUiFacade}. The supported types is depending on the platform: + * + * * A `alphaTab.model.Score` instance (all platforms) + * * A `ArrayBuffer` or `Uint8Array` containing one of the supported file formats (all platforms, native byte array or input streams on other platforms) + * * A url from where to download the binary data of one of the supported file formats (browser only) + * * @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. - * @returns true if the data object is supported and a load was initiated, otherwise false + * @category Methods - Player + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.load('/assets/MyFile.gp'); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.Load(System.IO.File.OpenRead("MyFile.gp")); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * contentResolver.openInputStream(uri).use { + * api.load(it) + * } + * ``` */ public load(scoreData: unknown, trackIndexes?: number[]): boolean { try { @@ -238,9 +486,32 @@ 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. + * + * @category Methods - Core + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.RenderScore(generateScore(),[ 2, 3 ]); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.RenderScore(GenerateScore(), new double[] { 2, 3 }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.renderScore(generateScore(), alphaTab.collections.DoubleList(2, 3)); + * ``` */ public renderScore(score: Score, trackIndexes?: number[]): void { - let tracks: Track[] = []; + const tracks: Track[] = []; if (!trackIndexes) { if (score.tracks.length > 0) { tracks.push(score.tracks[0]); @@ -251,11 +522,11 @@ export class AlphaTabApiBase { tracks.push(score.tracks[0]); } } else if (trackIndexes.length === 1 && trackIndexes[0] === -1) { - for (let track of score.tracks) { + for (const track of score.tracks) { tracks.push(track); } } else { - for (let index of trackIndexes) { + for (const index of trackIndexes) { if (index >= 0 && index <= score.tracks.length) { tracks.push(score.tracks[index]); } @@ -268,11 +539,40 @@ export class AlphaTabApiBase { /** * Renders the given list of tracks. * @param tracks The tracks to render. They must all belong to the same score. + * + * @category Methods - Core + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.renderTracks([api.score.tracks[0], api.score.tracks[1]]); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.RenderTracks(new []{ + * api.Score.Tracks[2], + * api.Score.Tracks[3] + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.renderTracks(alphaTab.collections.List( + * api.score.tracks[2], + * api.score.tracks[3] + * } + * ``` */ public renderTracks(tracks: Track[]): void { if (tracks.length > 0) { - let score: Score = tracks[0].score; - for (let track of tracks) { + const score: Score = tracks[0].score; + for (const track of tracks) { if (track.score !== score) { this.onError( new AlphaTabError( @@ -290,10 +590,11 @@ export class AlphaTabApiBase { private internalRenderTracks(score: Score, tracks: Track[]): void { ModelUtils.applyPitchOffsets(this.settings, score); if (score !== this.score) { - this.score = score; - this.tracks = tracks; + this._score = score; + this._tracks = tracks; + this._tickCache = null; this._trackIndexes = []; - for (let track of tracks) { + for (const track of tracks) { this._trackIndexes.push(track.index); } this._trackIndexLookup = new Set(this._trackIndexes); @@ -301,12 +602,20 @@ export class AlphaTabApiBase { this.loadMidiForScore(); this.render(); } else { - this.tracks = tracks; + this._tracks = tracks; + + const startIndex = ModelUtils.computeFirstDisplayedBarIndex(score, this.settings); + const endIndex = ModelUtils.computeLastDisplayedBarIndex(score, this.settings, startIndex); + if (this._tickCache) { + this._tickCache.multiBarRestInfo = ModelUtils.buildMultiBarRestInfo(this.tracks, startIndex, endIndex); + } + this._trackIndexes = []; - for (let track of tracks) { + for (const track of tracks) { this._trackIndexes.push(track.index); } this._trackIndexLookup = new Set(this._trackIndexes); + this.render(); } } @@ -326,7 +635,7 @@ export class AlphaTabApiBase { this.triggerResize(); }); } else { - let resizeEventInfo: ResizeEventArgs = new ResizeEventArgs(); + const resizeEventInfo: ResizeEventArgs = new ResizeEventArgs(); resizeEventInfo.oldWidth = this.renderer.width; resizeEventInfo.newWidth = this.container.width; resizeEventInfo.settings = this.settings; @@ -364,13 +673,36 @@ export class AlphaTabApiBase { * Tells alphaTab to render the given alphaTex. * @param tex The alphaTex code to render. * @param tracks If set, the given tracks will be rendered, otherwise the first track only will be rendered. + * @category Methods - Core + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.tex("\\title 'Test' . 3.3.4"); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.Tex("\\title 'Test' . 3.3.4"); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.tex("\\title 'Test' . 3.3.4"); + * ``` */ public tex(tex: string, tracks?: number[]): void { try { - let parser: AlphaTexImporter = new AlphaTexImporter(); + const parser: AlphaTexImporter = new AlphaTexImporter(); parser.logErrors = true; parser.initFromString(tex, this.settings); - let score: Score = parser.readScore(); + const score: Score = parser.readScore(); this.renderScore(score, tracks); } catch (e) { this.onError(e as Error); @@ -378,10 +710,42 @@ export class AlphaTabApiBase { } /** - * Attempts a load of the score represented by the given data object. - * @param data The data object to decode + * Triggers a load of the soundfont from the given data. + * @remarks + * AlphaTab only supports SoundFont2 and SoundFont3 {@since 1.4.0} encoded soundfonts for loading. To load a soundfont the player must be enabled in advance. + * + * @param data The data object to decode. The supported data types is depending on the platform. + * + * * A `ArrayBuffer` or `Uint8Array` (all platforms, native byte array or input streams on other platforms) + * * A url from where to download the binary data of one of the supported file formats (browser only) + * * @param append Whether to fully replace or append the data from the given soundfont. - * @returns true if the data object is supported and a load was initiated, otherwise false + * @returns `true` if the passed in object is a supported format and loading was initiated, otherwise `false`. + * + * @category Methods - Player + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.loadSoundFont('/assets/MyFile.sf2'); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.LoadSoundFont(System.IO.File.OpenRead("MyFile.sf2")); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * contentResolver.openInputStream(uri).use { + * api.loadSoundFont(it) + * } + * ``` */ public loadSoundFont(data: unknown, append: boolean = false): boolean { if (!this.player) { @@ -391,7 +755,46 @@ export class AlphaTabApiBase { } /** - * Resets all loaded soundfonts as if they were not loaded. + * Unloads all presets from previously loaded SoundFonts. + * @remarks + * This function resets the player internally to not have any SoundFont loaded anymore. This allows you to reduce the memory usage of the page + * if multiple partial SoundFonts are loaded via `loadSoundFont(..., true)`. Depending on the workflow you might also just want to use `loadSoundFont(..., false)` once + * instead of unloading the previous SoundFonts. + * + * @category Methods - Player + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.loadSoundFont('/assets/guitars.sf2', true); + * api.loadSoundFont('/assets/pianos.sf2', true); + * // .. + * api.resetSoundFonts(); + * api.loadSoundFont('/assets/synths.sf2', true); + * ``` + * + * @example + * C# + * ```cs + *var api = new AlphaTabApi(...); + *api.LoadSoundFont(System.IO.File.OpenRead("guitars.sf2"), true); + *api.LoadSoundFont(System.IO.File.OpenRead("pianos.sf2"), true); + *... + *api.ResetSoundFonts(); + *api.LoadSoundFont(System.IO.File.OpenRead("synths.sf2"), true); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.loadSoundFont(readResource("guitars.sf2"), true) + * api.loadSoundFont(readResource("pianos.sf2"), true) + * ... + * api.resetSoundFonts() + * api.loadSoundFont(readResource("synths.sf2"), true) + * ``` */ public resetSoundFonts(): void { if (!this.player) { @@ -401,7 +804,32 @@ export class AlphaTabApiBase { } /** - * Initiates a re-rendering of the current setup. If rendering is not yet possible, it will be deferred until the UI changes to be ready for rendering. + * Initiates a re-rendering of the current setup. + * @remarks + * If rendering is not yet possible, it will be deferred until the UI changes to be ready for rendering. + * + * @category Methods - Core + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.render(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.Render(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.render() + * ``` */ public render(): void { if (!this.renderer) { @@ -419,17 +847,139 @@ export class AlphaTabApiBase { private _tickCache: MidiTickLookup | null = null; /** - * Gets the tick cache related to the current score. + * The tick cache allowing lookup of midi ticks to beats. + * @remarks + * Gets the tick cache allowing lookup of midi ticks to beats. If the player is enabled, a midi file will be generated + * for the loaded {@link Score} for later playback. During this generation this tick cache is filled with the + * exact midi ticks when beats are played. + * + * The {@link MidiTickLookup.findBeat} method allows a lookup of the beat related to a given input midi tick. + * + * @category Properties - Player + * @since 1.2.3 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * const lookupResult = api.tickCache.findBeat(new Set([0, 1]), 100); + * const currentBeat = lookupResult?.currentBeat; + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * var lookupResult = api.TickCache.FindBeat(new AlphaTab.Core.EcmaScript.Set(0, 1), 100); + * var currentBeat = lookupResult?.CurrentBeat; + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * val lookupResult = api.tickCache.findBeat(alphaTab.core.ecmaScript.Set(0, 1), 100); + * val currentBeat = lookupResult?.CurrentBeat; + * ``` */ public get tickCache(): MidiTickLookup | null { return this._tickCache; } + /** + * The tick cache allowing lookup of midi ticks to beats. + * @remarks + * In older versions of alphaTab you can access the `boundsLookup` via {@link IScoreRenderer.boundsLookup} on {@link renderer}. + * + * After the rendering completed alphaTab exposes via this lookup the location of the individual + * notation elements. The lookup provides fast access to the bars and beats at a given location. + * If the {@link CoreSettings.includeNoteBounds} option was activated also the location of the individual notes can be obtained. + * + * The property contains a `BoundsLookup` instance which follows a hierarchical structure that represents + * the tree of rendered elements. + * + * The hierarchy is: `staffSystems > bars(1) > bars(2) > beats > notes` + * + * * `staffSystems` - Represent the bounds of the individual systems ("rows") where staves are contained. + * * `bars(1)` - Represent the bounds of all bars for a particular master bar across all tracks. + * * `bars(2)` - Represent the bounds of an individual bar of a track. The bounds on y-axis span the region of the staff and notes might exceed this bounds. + * * `beats` - Represent the bounds of the individual beats within a track. The bounds on y-axis are equal to the bar bounds. + * * `notes` - Represent the bounds of the individual note heads/numbers within a track. + * + * Each bounds hierarchy have a `visualBounds` and `realBounds`. + * + * * `visualBounds` - Represent the area covering all visually visible elements + * * `realBounds` - Represents the actual bounds of the elements in this beat including whitespace areas. + * * `noteHeadBounds` (only on `notes` level) - Represents the area of the note heads or number based on the staff + * + * You can check out the individual sizes and regions. + * @category Properties - Core + * @since 1.5.0 + */ + public get boundsLookup() { + return this.renderer.boundsLookup; + } + /** * Gets the alphaSynth player used for playback. This is the low-level API to the Midi synthesizer used for playback. */ + /** + * The alphaSynth player used for playback. + * @remarks + * This is the low-level API to the Midi synthesizer used for playback. + * Gets access to the underling {@link IAlphaSynth} that is used for the audio playback. + * @category Properties - Player + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * setupPlayerEvents(api.settings); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * SetupPlayerEvents(api.Player); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * setupPlayerEvents(api.player) + * ``` + */ public player: IAlphaSynth | null = null; + /** + * Whether the player is ready for starting the playback. + * @remarks + * Gets whether the synthesizer is ready for playback. The player is ready for playback when + * all background workers are started, the audio output is initialized, a soundfont is loaded, and a song was loaded into the player as midi file. + * @category Properties - Player + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * if(api.isReadyForPlayback)) api.play(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * if(api.IsReadyForPlayback) api.Play(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * if (api.isReadyForPlayback) api.play() + * ``` + */ public get isReadyForPlayback(): boolean { if (!this.player) { return false; @@ -437,6 +987,33 @@ export class AlphaTabApiBase { return this.player.isReadyForPlayback; } + /** + * The current player state. + * @remarks + * Gets the current player state, meaning whether it is paused or playing. + * @category Properties - Player + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * if(api.playerState != alphaTab.synth.PlayerState.Playing) api.play(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * if(api.PlayerState != PlayerState.Playing) api.Play(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * if (api.playerState != PlayerState.Playing) api.play() + * ``` + */ public get playerState(): PlayerState { if (!this.player) { return PlayerState.Paused; @@ -444,6 +1021,33 @@ export class AlphaTabApiBase { return this.player.state; } + /** + * The current master volume as percentage (0-1). + * @remarks + * Gets or sets the master volume of the overall audio being played. The volume is annotated in percentage where 1.0 would be the normal volume and 0.5 only 50%. + * @category Properties - Player + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.masterVolume = 0.5; + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.MasterVolume = 0.5; + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.masterVolume = 0.5 + * ``` + */ public get masterVolume(): number { if (!this.player) { return 0; @@ -457,6 +1061,34 @@ export class AlphaTabApiBase { } } + /** + * The metronome volume as percentage (0-1). + * @remarks + * Gets or sets the volume of the metronome. By default the metronome is disabled but can be enabled by setting the volume different. + * @category Properties - Player + * @defaultValue `0` + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.metronomeVolume = 0.5; + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.MetronomeVolume = 0.5; + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.metronomeVolume = 0.5 + * ``` + */ public get metronomeVolume(): number { if (!this.player) { return 0; @@ -470,6 +1102,34 @@ export class AlphaTabApiBase { } } + /** + * The volume of the count-in metronome ticks. + * @remarks + * Gets or sets the volume of the metronome during the count-in of the song. By default the count-in is disabled but can be enabled by setting the volume different. + * @category Properties - Player + * @since 1.1.0 + * @defaultValue `0` + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.countInVolume = 0.5; + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.CountInVolume = 0.5; + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.countInVolume = 0.5 + * ``` + */ public get countInVolume(): number { if (!this.player) { return 0; @@ -483,6 +1143,62 @@ export class AlphaTabApiBase { } } + /** + * The midi events which will trigger the `midiEventsPlayed` event + * @remarks + * Gets or sets the midi events which will trigger the `midiEventsPlayed` event. With this filter set you can enable + * that alphaTab will signal any midi events as they are played by the synthesizer. This allows reacing on various low level + * audio playback elements like notes/rests played or metronome ticks. + * + * Refer to the [related guide](https://alphatab.net/docs/guides/handling-midi-events) to learn more about this feature. + * @defaultValue `[]` + * @category Properties - Player + * @since 1.2.0 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.midiEventsPlayedFilter = [alphaTab.midi.MidiEventType.AlphaTabMetronome]; + * api.midiEventsPlayed.on(function(e) { + * for(const midi of e.events) { + * if(midi.isMetronome) { + * console.log('Metronome tick ' + midi.metronomeNumerator); + * } + * } + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.MidiEventsPlayedFilter = new MidiEventType[] { AlphaTab.Midi.MidiEventType.AlphaTabMetronome }; + * api.MidiEventsPlayed.On(e => + * { + * foreach(var midi of e.events) + * { + * if(midi is AlphaTab.Midi.AlphaTabMetronomeEvent metronome) + * { + * Console.WriteLine("Metronome tick " + metronome.MetronomeNumerator); + * } + * } + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...); + * api.midiEventsPlayedFilter = alphaTab.collections.List( alphaTab.midi.MidiEventType.AlphaTabMetronome ) + * api.midiEventsPlayed.on { e -> + * for (midi in e.events) { + * if(midi instanceof alphaTab.midi.AlphaTabMetronomeEvent && midi.isMetronome) { + * println("Metronome tick " + midi.tick); + * } + * } + * } + * ``` + */ public get midiEventsPlayedFilter(): MidiEventType[] { if (!this.player) { return []; @@ -496,6 +1212,31 @@ export class AlphaTabApiBase { } } + /** + * The position within the song in midi ticks. + * @category Properties - Player + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.tickPosition = 4000; + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.TickPosition = 4000; + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.tickPosition = 4000 + * ``` + */ public get tickPosition(): number { if (!this.player) { return 0; @@ -509,6 +1250,31 @@ export class AlphaTabApiBase { } } + /** + * The position within the song in milliseconds + * @category Properties - Player + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.timePosition = 4000; + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.TimePosition = 4000; + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.timePosition = 4000 + * ``` + */ public get timePosition(): number { if (!this.player) { return 0; @@ -522,6 +1288,37 @@ export class AlphaTabApiBase { } } + /** + * The range of the song that should be played. + * @remarks + * Gets or sets the range of the song that should be played. The range is defined in midi ticks or the whole song is played if the range is set to null + * @category Properties - Player + * @defaultValue `null` + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playbackRange = { startTick: 1000, endTick: 50000 }; + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlaybackRange = new PlaybackRange { StartTick = 1000, EndTick = 50000 }; + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playbackRange = PlaybackRange.apply { + * startTick = 1000 + * endTick = 50000 + * } + * ``` + */ public get playbackRange(): PlaybackRange | null { if (!this.player) { return null; @@ -538,6 +1335,34 @@ export class AlphaTabApiBase { } } + /** + * The current playback speed as percentage + * @remarks + * Controls the current playback speed as percentual value. Normal speed is 1.0 (100%) and 0.5 would be 50%. + * @category Properties - Player + * @defaultValue `1` + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playbackSpeed = 0.5; + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlaybackSpeed = 0.5; + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playbackSpeed = 0.5 + * ``` + */ public get playbackSpeed(): number { if (!this.player) { return 0; @@ -551,6 +1376,34 @@ export class AlphaTabApiBase { } } + /** + * Whether the playback should automatically restart after it finished. + * @remarks + * This setting controls whether the playback should automatically restart after it finished to create a playback loop. + * @category Properties - Player + * @defaultValue `false` + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.isLooping = true; + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.IsLooping = true; + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.isLooping = true + * ``` + */ public get isLooping(): boolean { if (!this.player) { return false; @@ -590,8 +1443,8 @@ export class AlphaTabApiBase { this.player.readyForPlayback.on(() => { this.onPlayerReady(); if (this.tracks) { - for (let track of this.tracks) { - let volume: number = track.playbackInfo.volume / 16; + for (const track of this.tracks) { + const volume: number = track.playbackInfo.volume / 16; this.player!.setChannelVolume(track.playbackInfo.primaryChannel, volume); this.player!.setChannelVolume(track.playbackInfo.secondaryChannel, volume); } @@ -618,10 +1471,16 @@ export class AlphaTabApiBase { return; } + const score = this.score!; + Logger.debug('AlphaTab', 'Generating Midi'); - let midiFile: MidiFile = new MidiFile(); - let handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile); - let generator: MidiFileGenerator = new MidiFileGenerator(this.score, this.settings, handler); + const midiFile: MidiFile = new MidiFile(); + const handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile); + const generator: MidiFileGenerator = new MidiFileGenerator(score, this.settings, handler); + + const startIndex = ModelUtils.computeFirstDisplayedBarIndex(score, this.settings); + const endIndex = ModelUtils.computeLastDisplayedBarIndex(score, this.settings, startIndex); + generator.tickLookup.multiBarRestInfo = ModelUtils.buildMultiBarRestInfo(this.tracks, startIndex, endIndex); // we pass the transposition pitches separately to alphaSynth. generator.applyTranspositionPitches = false; @@ -641,12 +1500,42 @@ export class AlphaTabApiBase { * Changes the volume of the given tracks. * @param tracks The tracks for which the volume should be changed. * @param volume The volume to set for all tracks in percent (0-1) + * + * @remarks + * This will result in a volume change of the primary and secondary midi channel that the track uses for playback. + * If the track shares the channels with another track, all related tracks will be changed as they cannot be distinguished. + * @category Methods - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.changeTrackVolume([api.score.tracks[0], api.score.tracks[1]], 1.5); + * api.changeTrackVolume([api.score.tracks[2]], 0.5); + * ``` + * + * @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); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...); + * api.changeTrackVolume(alphaTab.collections.List(api.score.tracks[0], api.score.tracks[1]), 1.5); + * api.changeTrackVolume(alphaTab.collections.List(api.score.tracks[2]), 0.5); + * ``` */ public changeTrackVolume(tracks: Track[], volume: number): void { if (!this.player) { return; } - for (let track of tracks) { + for (const track of tracks) { this.player.setChannelVolume(track.playbackInfo.primaryChannel, volume); this.player.setChannelVolume(track.playbackInfo.secondaryChannel, volume); } @@ -654,15 +1543,42 @@ export class AlphaTabApiBase { /** * Changes the given tracks to be played solo or not. - * If one or more tracks are set to solo, only those tracks are hearable. * @param tracks The list of tracks to play solo or not. * @param solo If set to true, the tracks will be added to the solo list. If false, they are removed. + * + * @remarks + * If any track is set to solo, all other tracks are muted, unless they are also flagged as solo. + * This will result in a solo playback of the primary and secondary midi channel that the track uses for playback. + * If the track shares the channels with another track, all related tracks will be played as they cannot be distinguished. + * @category Methods - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.changeTrackSolo([api.score.tracks[0], api.score.tracks[1]], true); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.ChangeTrackSolo(new Track[] { api.Score.Tracks[0], api.Score.Tracks[1] }, true); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.changeTrackSolo(alphaTab.collections.List(api.score.tracks[0], api.score.tracks[1]), true); + * ``` */ public changeTrackSolo(tracks: Track[], solo: boolean): void { if (!this.player) { return; } - for (let track of tracks) { + for (const track of tracks) { this.player.setChannelSolo(track.playbackInfo.primaryChannel, solo); this.player.setChannelSolo(track.playbackInfo.secondaryChannel, solo); } @@ -672,28 +1588,83 @@ export class AlphaTabApiBase { * Changes the given tracks to be muted or not. * @param tracks The list of track to mute or unmute. * @param mute If set to true, the tracks will be muted. If false they are unmuted. + * + * @remarks + * This will result in a muting of the primary and secondary midi channel that the track uses + * for playback. If the track shares the channels with another track, all tracks will be muted as during playback they cannot be distinguished. + * @category Methods - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.changeTrackMute([api.score.tracks[0], api.score.tracks[1]], true); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.ChangeTrackMute(new Track[] { api.Score.Tracks[0], api.Score.Tracks[1] }, true); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.changeTrackMute(alphaTab.collections.List(api.score.tracks[0], api.score.tracks[1]), true); + * ``` */ public changeTrackMute(tracks: Track[], mute: boolean): void { if (!this.player) { return; } - for (let track of tracks) { + for (const track of tracks) { this.player.setChannelMute(track.playbackInfo.primaryChannel, mute); this.player.setChannelMute(track.playbackInfo.secondaryChannel, mute); } } /** - * Changes the pitch transpose applied to the given tracks. These pitches are additional to the ones - * applied to the song via the settings and allows a more live-update. + * Changes the pitch transpose applied to the given tracks. * @param tracks The list of tracks to change. * @param semitones The number of semitones to apply as pitch offset. + * + * @remarks + * These pitches are additional to the ones applied to the song via the settings and data model and allows a more live-update via a UI. + * @category Methods - Player + * @since 1.4.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.changeTrackTranspositionPitch([api.score.tracks[0], api.score.tracks[1]], 3); + * api.changeTrackTranspositionPitch([api.score.tracks[2]], 2); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.ChangeTrackTranspositionPitch(new Track[] { api.Score.Tracks[0], api.Score.Tracks[1] }, 3); + * api.ChangeTrackTranspositionPitch(new Track[] { api.Score.Tracks[2] }, 3); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...); + * api.changeTrackTranspositionPitch(alphaTab.collections.List(api.score.tracks[0], api.score.tracks[1]), 3); + * api.changeTrackTranspositionPitch(alphaTab.collections.List(api.score.tracks[2]), 2); + * ``` */ public changeTrackTranspositionPitch(tracks: Track[], semitones: number): void { if (!this.player) { return; } - for (let track of tracks) { + for (const track of tracks) { this.player.setChannelTranspositionPitch(track.playbackInfo.primaryChannel, semitones); this.player.setChannelTranspositionPitch(track.playbackInfo.secondaryChannel, semitones); } @@ -702,6 +1673,29 @@ export class AlphaTabApiBase { /** * Starts the playback of the current song. * @returns true if the playback was started, otherwise false. Reasons for not starting can be that the player is not ready or already playing. + * @category Methods - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.play(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.Play(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.play() + * ``` */ public play(): boolean { if (!this.player) { @@ -712,6 +1706,29 @@ export class AlphaTabApiBase { /** * Pauses the playback of the current song. + * @category Methods - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.pause(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.Pause(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.pause(); + * ``` */ public pause(): void { if (!this.player) { @@ -722,6 +1739,31 @@ export class AlphaTabApiBase { /** * Toggles between play/pause depending on the current player state. + * @remarks + * If the player was playing, it will pause. If it is paused, it will initiate a play. + * @category Methods - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playPause(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlayPause(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playPause() + * ``` */ public playPause(): void { if (!this.player) { @@ -732,6 +1774,31 @@ export class AlphaTabApiBase { /** * Stops the playback of the current song, and moves the playback position back to the start. + * @remarks + * If a dedicated playback range is selected, it will move the playback position to the start of this range, not the whole song. + * @category Methods - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.stop(); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.Stop(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.stop() + * ``` */ public stop(): void { if (!this.player) { @@ -741,8 +1808,37 @@ export class AlphaTabApiBase { } /** - * Triggers the play of the given beat. This will stop the any other current ongoing playback. + * Triggers the play of the given beat. * @param beat the single beat to play + * @remarks + * This will stop the any other current ongoing playback. + * This method can be used in applications when individual beats need to be played for lesson or editor style purposes. + * The player will not report any change in state or playback position during the playback of the requested beat. + * It is a playback of audio separate to the main song playback. + * @returns true if the playback was started, otherwise false. Reasons for not starting can be that the player is not ready or already playing. + * @category Methods - Player + * @since 1.1.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playBeat(api.score.tracks[0].staves[0].bars[0].voices[0].beats[0]); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlayBeat(api.Score.Tracks[0].Staves[0].Bars[0].Voices[0].Beats[0]); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playBeat(api.score.tracks[0].staves[0].bars[0].voices[0].beats[0]) + * ``` */ public playBeat(beat: Beat): void { if (!this.player) { @@ -750,9 +1846,9 @@ export class AlphaTabApiBase { } // we generate a new midi file containing only the beat - let midiFile: MidiFile = new MidiFile(); - let handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile); - let generator: MidiFileGenerator = new MidiFileGenerator( + const midiFile: MidiFile = new MidiFile(); + const handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile); + const generator: MidiFileGenerator = new MidiFileGenerator( beat.voice.bar.staff.track.score, this.settings, handler @@ -763,8 +1859,36 @@ export class AlphaTabApiBase { } /** - * Triggers the play of the given note. This will stop the any other current ongoing playback. - * @param beat the single note to play + * Triggers the play of the given note. + * @param note the single note to play + * @remarks + * This will stop the any other current ongoing playback. + * This method can be used in applications when individual notes need to be played for lesson or editor style purposes. + * The player will not report any change in state or playback position during the playback of the requested note. + * It is a playback of audio separate to the main song playback. + * @category Methods - Player + * @since 1.1.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playNote(api.score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlayNote(api.Score.Tracks[0].Staves[0].Bars[0].Voices[0].Beats[0].Notes[0]); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playNote(api.score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]); + * ``` */ public playNote(note: Note): void { if (!this.player) { @@ -772,9 +1896,9 @@ export class AlphaTabApiBase { } // we generate a new midi file containing only the beat - let midiFile: MidiFile = new MidiFile(); - let handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile); - let generator: MidiFileGenerator = new MidiFileGenerator( + const midiFile: MidiFile = new MidiFile(); + const handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile); + const generator: MidiFileGenerator = new MidiFileGenerator( note.beat.voice.bar.staff.track.score, this.settings, handler @@ -811,7 +1935,7 @@ export class AlphaTabApiBase { if (this.settings.player.enableCursor && !this._cursorWrapper) { // // Create cursors - let cursors = this.uiFacade.createCursors(); + const cursors = this.uiFacade.createCursors(); if (cursors) { // store options and created elements for fast access this._cursorWrapper = cursors.cursorWrapper; @@ -847,8 +1971,8 @@ export class AlphaTabApiBase { this.player.stateChanged.on(e => { this._playerState = e.state; if (!e.stopped && e.state === PlayerState.Paused) { - let currentBeat = this._currentBeat; - let tickCache = this._tickCache; + const currentBeat = this._currentBeat; + const tickCache = this._tickCache; if (currentBeat && tickCache) { this.player!.tickPosition = tickCache.getBeatStart(currentBeat.beat); } @@ -863,14 +1987,24 @@ export class AlphaTabApiBase { * @param stop * @param shouldScroll whether we should scroll to the bar (if scrolling is active) */ - private cursorUpdateTick(tick: number, stop: boolean, shouldScroll: boolean = false, forceUpdate:boolean = false): void { - let cache: MidiTickLookup | null = this._tickCache; + private cursorUpdateTick( + tick: number, + stop: boolean, + shouldScroll: boolean = false, + forceUpdate: boolean = false + ): void { + const cache: MidiTickLookup | null = this._tickCache; if (cache) { - let tracks = this._trackIndexLookup; + const tracks = this._trackIndexLookup; if (tracks != null && tracks.size > 0) { - let beat: MidiTickLookupFindBeatResult | null = cache.findBeat(tracks, tick, this._currentBeat); + const beat: MidiTickLookupFindBeatResult | null = cache.findBeat(tracks, tick, this._currentBeat); if (beat) { - this.cursorUpdateBeat(beat, stop, shouldScroll, forceUpdate || this.playerState === PlayerState.Paused); + this.cursorUpdateBeat( + beat, + stop, + shouldScroll, + forceUpdate || this.playerState === PlayerState.Paused + ); } } } @@ -893,23 +2027,23 @@ export class AlphaTabApiBase { if (!beat) { return; } - let cache: BoundsLookup | null = this.renderer.boundsLookup; + const cache: BoundsLookup | null = this.renderer.boundsLookup; if (!cache) { return; } - let previousBeat = this._currentBeat; - let previousCache: BoundsLookup | null = this._previousCursorCache; - let previousState: PlayerState | null = this._previousStateForCursor; + const previousBeat = this._currentBeat; + const previousCache: BoundsLookup | null = this._previousCursorCache; + const previousState: PlayerState | null = this._previousStateForCursor; if ( !forceUpdate && beat === previousBeat?.beat && cache === previousCache && - previousState === this._playerState && + previousState === this._playerState && previousBeat?.start === lookupResult.start ) { return; } - let beatBoundings: BeatBounds | null = cache.findBeat(beat); + const beatBoundings: BeatBounds | null = cache.findBeat(beat); if (!beatBoundings) { return; } @@ -936,7 +2070,9 @@ export class AlphaTabApiBase { } /** - * Initiates a scroll to the cursor + * Initiates a scroll to the cursor. + * @since 1.2.3 + * @category Methods - Player */ public scrollToCursor() { const barBounds = this._currentBarBounds; @@ -945,29 +2081,29 @@ export class AlphaTabApiBase { } } - public internalScrollToCursor(barBoundings: MasterBarBounds) { - let scrollElement: IContainer = this.uiFacade.getScrollContainer(); - let isVertical: boolean = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical; - let mode: ScrollMode = this.settings.player.scrollMode; + 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 - let y: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; + const y: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; if (y !== this._lastScroll) { this._lastScroll = y; switch (mode) { case ScrollMode.Continuous: - let elementOffset: Bounds = this.uiFacade.getOffset(scrollElement, this.container); + const elementOffset: Bounds = this.uiFacade.getOffset(scrollElement, this.container); this.uiFacade.scrollToY(scrollElement, elementOffset.y + y, this.settings.player.scrollSpeed); break; case ScrollMode.OffScreen: - let elementBottom: number = + const elementBottom: number = scrollElement.scrollTop + this.uiFacade.getOffset(null, scrollElement).h; if ( barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom || barBoundings.visualBounds.y < scrollElement.scrollTop ) { - let scrollTop: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; + const scrollTop: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; this.uiFacade.scrollToY(scrollElement, scrollTop, this.settings.player.scrollSpeed); } break; @@ -976,24 +2112,24 @@ export class AlphaTabApiBase { } else { // when scrolling on the x-axis, we preliminary check if the new bar has // moved on the x-axis - let x: number = barBoundings.visualBounds.x; + const x: number = barBoundings.visualBounds.x; if (x !== this._lastScroll) { this._lastScroll = x; switch (mode) { case ScrollMode.Continuous: - let scrollLeftContinuous: number = + 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: - let elementRight: number = + const elementRight: number = scrollElement.scrollLeft + this.uiFacade.getOffset(null, scrollElement).w; if ( barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight || barBoundings.visualBounds.x < scrollElement.scrollLeft ) { - let scrollLeftOffScreen: number = + const scrollLeftOffScreen: number = barBoundings.realBounds.x + this.settings.player.scrollOffsetX; this._lastScroll = barBoundings.visualBounds.x; this.uiFacade.scrollToX( @@ -1022,8 +2158,8 @@ export class AlphaTabApiBase { const barCursor = this._barCursor; const beatCursor = this._beatCursor; - let barBoundings: MasterBarBounds = beatBoundings.barBounds.masterBarBounds; - let barBounds: Bounds = barBoundings.visualBounds; + const barBoundings: MasterBarBounds = beatBoundings.barBounds.masterBarBounds; + const barBounds: Bounds = barBoundings.visualBounds; this._currentBarBounds = barBoundings; @@ -1046,39 +2182,41 @@ export class AlphaTabApiBase { // actively playing? -> animate cursor and highlight items let shouldNotifyBeatChange = false; - if (this._playerState === PlayerState.Playing && !stop) { + const isPlayingUpdate = this._playerState === PlayerState.Playing && !stop; + if (isPlayingUpdate) { if (this.settings.player.enableElementHighlighting) { - for (let highlight of beatsToHighlight) { - let className: string = BeatContainerGlyph.getGroupId(highlight.beat); + for (const highlight of beatsToHighlight) { + const className: string = BeatContainerGlyph.getGroupId(highlight.beat); this.uiFacade.highlightElements(className, beat.voice.bar.index); } } - if (this.settings.player.enableAnimatedBeatCursor) { - let nextBeatX: number = barBoundings.visualBounds.x + barBoundings.visualBounds.w; - // 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. - let nextBeatBoundings: BeatBounds | null = cache.findBeat(nextBeat); - if ( - nextBeatBoundings && - nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds - ) { - nextBeatX = nextBeatBoundings.onNotesX; - } + shouldScroll = !stop; + shouldNotifyBeatChange = true; + } + + if (this.settings.player.enableAnimatedBeatCursor && beatCursor) { + let nextBeatX: number = barBoundings.visualBounds.x + barBoundings.visualBounds.w; + // 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); + if ( + nextBeatBoundings && + nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds + ) { + nextBeatX = nextBeatBoundings.onNotesX; } + } + + if (isPlayingUpdate) { // we need to put the transition to an own animation frame // otherwise the stop animation above is not applied. this.uiFacade.beginInvoke(() => { - if (beatCursor) { - beatCursor.transitionToX(duration / this.playbackSpeed, nextBeatX); - } + beatCursor!.transitionToX(duration / this.playbackSpeed, nextBeatX); }); } - - shouldScroll = !stop; - shouldNotifyBeatChange = true; } if (shouldScroll && !this._beatMouseDown && this.settings.player.scrollMode !== ScrollMode.Off) { @@ -1092,6 +2230,42 @@ export class AlphaTabApiBase { } } + /** + * This event is fired when the played beat changed. + * + * @eventProperty + * @category Events - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playedBeatChanged.on((beat) => { + * updateFretboard(beat); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlayedBeatChanged.On(beat => + * { + * UpdateFretboard(beat); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playedBeatChanged.on { beat -> + * updateFretboard(beat) + * } + * ``` + * + */ public playedBeatChanged: IEventEmitterOfT = new EventEmitterOfT(); private onPlayedBeatChanged(beat: Beat): void { if (this._isDestroyed) { @@ -1101,8 +2275,48 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'playedBeatChanged', beat); } + /** + * This event is fired when the currently active beats across all tracks change. + * + * @remarks + * Unlike the {@link playedBeatChanged} event this event contains the beats of all tracks and voices independent of them being rendered. + * + * @eventProperty + * @category Events - Player + * @since 1.2.3 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.activeBeatsChanged.on(args => { + * updateHighlights(args.activeBeats); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.ActiveBeatsChanged.On(args => + * { + * UpdateHighlights(args.ActiveBeats); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.activeBeatsChanged.on { args -> + * updateHighlights(args.activeBeats) + * } + * ``` + * + */ public activeBeatsChanged: IEventEmitterOfT = new EventEmitterOfT(); + private onActiveBeatsChanged(e: ActiveBeatsChangedEventArgs): void { if (this._isDestroyed) { return; @@ -1116,12 +2330,244 @@ export class AlphaTabApiBase { private _selectionStart: SelectionInfo | null = null; private _selectionEnd: SelectionInfo | null = null; + /** + * This event is fired whenever a the user presses the mouse button on a beat. + * @eventProperty + * @category Events - Player + * @since 0.9.7 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.beatMouseDown.on((beat) => { + * startSelectionOnBeat(beat); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.BeatMouseDown.On(beat => + * { + * StartSelectionOnBeat(args); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.beatMouseDown.on { beat -> + * startSelectionOnBeat(args) + * } + * ``` + */ public beatMouseDown: IEventEmitterOfT = new EventEmitterOfT(); + + /** + * This event is fired whenever the user moves the mouse over a beat after the user already pressed the button on a beat. + * @eventProperty + * @category Events - Player + * @since 0.9.7 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.beatMouseMove.on((beat) => { + * expandSelectionToBeat(beat); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.BeatMouseMove.On(beat => + * { + * ExpandSelectionToBeat(beat); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.beatMouseMove.on { beat -> + * expandSelectionToBeat(beat) + * } + * ``` + */ public beatMouseMove: IEventEmitterOfT = new EventEmitterOfT(); + + /** + * This event is fired whenever the user releases the mouse after a mouse press on a beat. + * @remarks + * This event is fired regardless of whether the mouse was released on a beat. + * The parameter is null if the mouse was released somewhere beside the beat. + * + * @eventProperty + * @category Events - Player + * @since 0.9.7 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.beatMouseUp.on((beat) => { + * hideSelection(beat); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.BeatMouseUp.On(beat => + * { + * HideSelection(beat); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.beatMouseUp.on { beat -> + * hideSelection(beat) + * } + * ``` + */ public beatMouseUp: IEventEmitterOfT = new EventEmitterOfT(); + /** + * This event is fired whenever a the user presses the mouse button on a note head/number. + * @remarks + * This event is fired whenever a the user presses the mouse button on a note. + * It is only fired if {@link CoreSettings.includeNoteBounds} was set to `true` because + * only then this hit detection can be done. A click on a note is considered if the note head or the note number on tabs are clicked as documented in {@link boundsLookup}. + * + * @eventProperty + * @category Events - Player + * @since 1.2.3 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.noteMouseDown.on((note) => { + * api.playNote(note); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.NoteMouseDown.On(note => + * { + * api.PlayNote(note); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.noteMouseDown.on { note -> + * api.playNote(note) + * } + * ``` + * + */ public noteMouseDown: IEventEmitterOfT = new EventEmitterOfT(); + + /** + * This event is fired whenever the user moves the mouse over a note after the user already pressed the button on a note. + * @remarks + * This event is fired whenever the user moves the mouse over a note after the user already pressed the button on a note. + * It is only fired if {@link CoreSettings.includeNoteBounds} was set to `true` because + * only then this hit detection can be done. A click on a note is considered if the note head or the note number on tabs are clicked as documented in {@link boundsLookup} + * + * @eventProperty + * @category Events - Player + * @since 1.2.3 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.noteMouseMove.on((note) => { + * changeNote(note) + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.NoteMouseMove.On(note => + * { + * ChangeNote(note); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.noteMouseMove.on { note -> + * changeNote(note) + * } + * ``` + * + */ public noteMouseMove: IEventEmitterOfT = new EventEmitterOfT(); + + /** + * This event is fired whenever the user releases the mouse after a mouse press on a note. + * @remarks + * This event is fired whenever a the user presses the mouse button on a note. + * This event is fired regardless of whether the mouse was released on a note. + * The parameter is null if the mouse was released somewhere beside the note. + * It is only fired if {@link CoreSettings.includeNoteBounds} was set to `true` because + * only then this hit detection can be done. A click on a note is considered if the note head or the note number on tabs are clicked as documented in the {@link boundsLookup}. + * + * @eventProperty + * @category Events - Player + * @since 1.2.3 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.noteMouseUp.on((note) => { + * api.playNote(note); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.NoteMouseUp.On(note => + * { + * api.PlayNote(note); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.noteMouseUp.on { note -> + * api.playNote(note) + * } + * ``` + * + */ public noteMouseUp: IEventEmitterOfT = new EventEmitterOfT(); private onBeatMouseDown(originalEvent: IMouseEventArgs, beat: Beat): void { @@ -1187,22 +2633,22 @@ export class AlphaTabApiBase { this.settings.player.enableUserInteraction ) { if (this._selectionEnd) { - let startTick: number = + const startTick: number = this._tickCache?.getBeatStart(this._selectionStart!.beat) ?? this._selectionStart!.beat.absolutePlaybackStart; - let endTick: number = + const endTick: number = this._tickCache?.getBeatStart(this._selectionEnd!.beat) ?? this._selectionEnd!.beat.absolutePlaybackStart; if (endTick < startTick) { - let t: SelectionInfo = this._selectionStart!; + 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) - let tickCache: MidiTickLookup = this._tickCache; - let realMasterBarStart: number = tickCache.getMasterBarStart( + const tickCache: MidiTickLookup = this._tickCache; + const realMasterBarStart: number = tickCache.getMasterBarStart( this._selectionStart.beat.voice.bar.masterBar ); // move to selection start @@ -1213,11 +2659,11 @@ export class AlphaTabApiBase { this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart; // set playback range if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) { - let realMasterBarEnd: number = tickCache.getMasterBarStart( + const realMasterBarEnd: number = tickCache.getMasterBarStart( this._selectionEnd.beat.voice.bar.masterBar ); - let range = new PlaybackRange(); + const range = new PlaybackRange(); range.startTick = realMasterBarStart + this._selectionStart.beat.playbackStart; range.endTick = realMasterBarEnd + @@ -1273,9 +2719,9 @@ export class AlphaTabApiBase { if (this.settings.player.enableUserInteraction) { e.preventDefault(); } - let relX: number = e.getX(this.canvasElement); - let relY: number = e.getY(this.canvasElement); - let beat: Beat | null = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null; + const relX: number = e.getX(this.canvasElement); + const relY: number = e.getY(this.canvasElement); + const beat: Beat | null = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null; if (beat) { this.onBeatMouseDown(e, beat); @@ -1291,9 +2737,9 @@ export class AlphaTabApiBase { if (!this._beatMouseDown) { return; } - let relX: number = e.getX(this.canvasElement); - let relY: number = e.getY(this.canvasElement); - let beat: Beat | null = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null; + const relX: number = e.getX(this.canvasElement); + const relY: number = e.getY(this.canvasElement); + const beat: Beat | null = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null; if (beat) { this.onBeatMouseMove(e, beat); @@ -1312,9 +2758,9 @@ export class AlphaTabApiBase { if (this.settings.player.enableUserInteraction) { e.preventDefault(); } - let relX: number = e.getX(this.canvasElement); - let relY: number = e.getY(this.canvasElement); - let beat: Beat | null = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null; + const relX: number = e.getX(this.canvasElement); + const relY: number = e.getY(this.canvasElement); + const beat: Beat | null = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null; this.onBeatMouseUp(e, beat); if (this._noteMouseDown) { @@ -1340,11 +2786,11 @@ export class AlphaTabApiBase { } private cursorSelectRange(startBeat: SelectionInfo | null, endBeat: SelectionInfo | null): void { - let cache: BoundsLookup | null = this.renderer.boundsLookup; + const cache: BoundsLookup | null = this.renderer.boundsLookup; if (!cache) { return; } - let selectionWrapper: IContainer | null = this._selectionWrapper; + const selectionWrapper: IContainer | null = this._selectionWrapper; if (!selectionWrapper) { return; } @@ -1360,14 +2806,14 @@ export class AlphaTabApiBase { if (!endBeat.bounds) { endBeat.bounds = cache.findBeat(endBeat.beat); } - let startTick: number = this._tickCache?.getBeatStart(startBeat.beat) ?? startBeat.beat.absolutePlaybackStart; - let endTick: number = this._tickCache?.getBeatStart(endBeat.beat) ?? endBeat.beat.absolutePlaybackStart; + 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) { - let t: SelectionInfo = startBeat; + const t: SelectionInfo = startBeat; startBeat = endBeat; endBeat = t; } - let startX: number = startBeat.bounds!.realBounds.x; + const startX: number = startBeat.bounds!.realBounds.x; let endX: number = endBeat.bounds!.realBounds.x + endBeat.bounds!.realBounds.w; if (endBeat.beat.index === endBeat.beat.voice.beats.length - 1) { endX = @@ -1382,11 +2828,11 @@ export class AlphaTabApiBase { // from the startbeat to the end of the staff, // then fill all staffs until the end-beat staff // then from staff-start to the end beat (or to end of bar if it's the last beat) - let staffStartX: number = startBeat.bounds!.barBounds.masterBarBounds.staffSystemBounds!.visualBounds.x; - let staffEndX: number = + const staffStartX: number = startBeat.bounds!.barBounds.masterBarBounds.staffSystemBounds!.visualBounds.x; + const staffEndX: number = startBeat.bounds!.barBounds.masterBarBounds.staffSystemBounds!.visualBounds.x + startBeat.bounds!.barBounds.masterBarBounds.staffSystemBounds!.visualBounds.w; - let startSelection: IContainer = this.uiFacade.createSelectionElement()!; + const startSelection: IContainer = this.uiFacade.createSelectionElement()!; startSelection.setBounds( startX, startBeat.bounds!.barBounds.masterBarBounds.visualBounds.y, @@ -1394,11 +2840,11 @@ export class AlphaTabApiBase { startBeat.bounds!.barBounds.masterBarBounds.visualBounds.h ); selectionWrapper.appendChild(startSelection); - let staffStartIndex: number = startBeat.bounds!.barBounds.masterBarBounds.staffSystemBounds!.index + 1; - let staffEndIndex: number = endBeat.bounds!.barBounds.masterBarBounds.staffSystemBounds!.index; + 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++) { - let staffBounds: StaffSystemBounds = cache.staffSystems[staffIndex]; - let middleSelection: IContainer = this.uiFacade.createSelectionElement()!; + const staffBounds: StaffSystemBounds = cache.staffSystems[staffIndex]; + const middleSelection: IContainer = this.uiFacade.createSelectionElement()!; middleSelection.setBounds( staffStartX, staffBounds.visualBounds.y, @@ -1407,7 +2853,7 @@ export class AlphaTabApiBase { ); selectionWrapper.appendChild(middleSelection); } - let endSelection: IContainer = this.uiFacade.createSelectionElement()!; + const endSelection: IContainer = this.uiFacade.createSelectionElement()!; endSelection.setBounds( staffStartX, endBeat.bounds!.barBounds.masterBarBounds.visualBounds.y, @@ -1417,7 +2863,7 @@ export class AlphaTabApiBase { selectionWrapper.appendChild(endSelection); } else { // if the beats are on the same staff, we simply highlight from the startbeat to endbeat - let selection: IContainer = this.uiFacade.createSelectionElement()!; + const selection: IContainer = this.uiFacade.createSelectionElement()!; selection.setBounds( startX, startBeat.bounds!.barBounds.masterBarBounds.visualBounds.y, @@ -1428,6 +2874,46 @@ export class AlphaTabApiBase { } } + /** + * This event is fired whenever a new song is loaded. + * @remarks + * This event is fired whenever a new song is loaded or changing due to {@link renderScore} or {@link renderTracks} calls. + * It is fired after the transposition midi pitches from the settings were applied, but before any midi is generated or rendering is started. + * This allows any modification of the score before further processing. + * + * @eventProperty + * @category Events - Core + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.scoreLoaded.on((score) => { + * updateSongInformationInUi(score); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.ScoreLoaded.On(score => + * { + * UpdateSongInformationInUi(score); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.scoreLoaded.on { score -> + * updateSongInformationInUi(score) + * } + * ``` + * + */ public scoreLoaded: IEventEmitterOfT = new EventEmitterOfT(); private onScoreLoaded(score: Score): void { if (this._isDestroyed) { @@ -1437,6 +2923,52 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'scoreLoaded', score); } + /** + * This event is fired when alphaTab was resized and is about to rerender the music notation. + * @remarks + * This event is fired when alphaTab was resized and is about to rerender the music notation. Before the re-rendering on resize + * the settings will be updated in the related components. This means that any changes to the layout options or other display settings are + * considered. This allows to implement scenarios where maybe the scale or the layout mode dynamically changes along the resizing. + * + * @eventProperty + * @category Events - Core + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.resize.on((args) => { + * args.settings.scale = args.newWidth > 1300 + * ? 1.5 + * : (args.newWidth > 800) ? 1.3 : 1; + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.Resize.On(args => + * { + * args.Settings.Display.Scale = args.NewWidth > 1300 + * ? 1.5 + * : (args.NewWidth > 800) ? 1.3 : 1; + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.resize.on { args -> + * args.settings.display.scale = args.newWidth > 1300 + * ? 1.5 + * : (args.newWidth > 800) ? 1.3 : 1; + * }); + * ``` + * + */ public resize: IEventEmitterOfT = new EventEmitterOfT(); private onResize(e: ResizeEventArgs): void { if (this._isDestroyed) { @@ -1446,6 +2978,44 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'resize', e); } + /** + * This event is fired when the rendering of the whole music sheet is starting. + * @remarks + * All preparations are completed and the layout and render sequence is about to start. + * + * @eventProperty + * @category Events - Core + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.renderStarted.on(() => { + * updateProgressBar("Rendering"); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.RenderStarted.On(resized => + * { + * UpdateProgressBar("Rendering"); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.renderStarted.on { resized -> + * updateProgressBar("Rendering"); + * } + * ``` + * + */ public renderStarted: IEventEmitterOfT = new EventEmitterOfT(); private onRenderStarted(resize: boolean): void { if (this._isDestroyed) { @@ -1455,6 +3025,45 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'renderStarted', resize); } + /** + * This event is fired when the rendering of the whole music sheet is finished. + * @remarks + * This event is fired when the rendering of the whole music sheet is finished from the render engine side. There might be still tasks open for + * the display component to visually display the rendered components when this event is notified (e.g. resizing of DOM elements are done). + * + * @eventProperty + * @category Events - Core + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.renderFinished.on(() => { + * updateProgressBar("Finishing"); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.RenderFinished.On(() => + * { + * UpdateProgressBar("Finishing"); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.renderFinished.on { + * updateProgressBar("Finishing") + * } + * ``` + * + */ public renderFinished: IEventEmitterOfT = new EventEmitterOfT(); private onRenderFinished(renderingResult: RenderFinishedEventArgs): void { if (this._isDestroyed) { @@ -1464,6 +3073,48 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'renderFinished', renderingResult); } + /** + * This event is fired when the rendering of the whole music sheet is finished, and all handlers of `renderFinished` ran. + * @remarks + * If {@link CoreSettings.enableLazyLoading} is enabled not all partial images of the music sheet might be rendered. + * In this case the `renderFinished` event rather represents that the whole music sheet has been layouted and arranged + * and every partial image can be requested for rendering. If you neeed more fine-grained access + * to the actual layouting and rendering progress, you need to look at the low-level apis {@link IScoreRenderer.partialLayoutFinished} and + * {@link IScoreRenderer.partialRenderFinished} accessible via {@link renderer}. + * + * @eventProperty + * @category Events - Core + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.postRenderFinished.on(() => { + * hideLoadingIndicator(); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PostRenderFinished.On(() => + * { + * HideLoadingIndicator(); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.postRenderFinished.on { + * hideLoadingIndicator(); + * } + * ``` + * + */ public postRenderFinished: IEventEmitter = new EventEmitter(); private onPostRenderFinished(): void { if (this._isDestroyed) { @@ -1473,7 +3124,51 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'postRenderFinished', null); } + /** + * This event is fired when an error within alphatab occurred. + * + * @remarks + * This event is fired when an error within alphatab occurred. Use this event as global error handler to show errors + * to end-users. Due to the asynchronous nature of alphaTab, no call to the API will directly throw an error if it fails. + * Instead a signal to this error handlers will be sent. + * + * @eventProperty + * @category Events - Core + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.error.on((error) { + * displayError(error); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.Error.On((error) => + * { + * DisplayError(error); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.error.on { error -> + * displayError(error) + * } + * ``` + * + */ public error: IEventEmitterOfT = new EventEmitterOfT(); + /** + * @internal + */ public onError(error: Error): void { if (this._isDestroyed) { return; @@ -1483,6 +3178,44 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'error', error); } + /** + * This event is fired when all required data for playback is loaded and ready. + * @remarks + * This event is fired when all required data for playback is loaded and ready. The player is ready for playback when + * all background workers are started, the audio output is initialized, a soundfont is loaded, and a song was loaded into the player as midi file. + * + * @eventProperty + * @category Events - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playerReady.on(() => { + * enablePlayerControls(); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlayerReady.On(() => + * { + * EnablePlayerControls() + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playerReady.on { + * enablePlayerControls() + * } + * ``` + */ public playerReady: IEventEmitter = new EventEmitter(); private onPlayerReady(): void { if (this._isDestroyed) { @@ -1492,6 +3225,56 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'playerReady', null); } + /** + * This event is fired when the playback of the whole song finished. + * @remarks + * This event is finished regardless on whether looping is enabled or not. + * + * @eventProperty + * @category Events - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playerFinished.on((args) => { + * // speed trainer + * api.playbackSpeed = Math.min(1.0, api.playbackSpeed + 0.1); + * }); + * api.isLooping = true; + * api.playbackSpeed = 0.5; + * api.play() + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlayerFinished.On(() => + * { + * // speed trainer + * api.PlaybackSpeed = Math.Min(1.0, api.PlaybackSpeed + 0.1); + * }); + * api.IsLooping = true; + * api.PlaybackSpeed = 0.5; + * api.Play(); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playerFinished.on { + * // speed trainer + * api.playbackSpeed = min(1.0, api.playbackSpeed + 0.1); + * } + * api.isLooping = true + * api.playbackSpeed = 0.5 + * api.play() + * ``` + * + */ public playerFinished: IEventEmitter = new EventEmitter(); private onPlayerFinished(): void { if (this._isDestroyed) { @@ -1501,6 +3284,42 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'playerFinished', null); } + /** + * This event is fired when the SoundFont needed for playback was loaded. + * + * @eventProperty + * @category Events - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.soundFontLoaded.on(() => { + * hideSoundFontLoadingIndicator(); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.SoundFontLoaded.On(() => + * { + * HideSoundFontLoadingIndicator(); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...); + * api.soundFontLoaded.on { + * hideSoundFontLoadingIndicator(); + * } + * ``` + * + */ public soundFontLoaded: IEventEmitter = new EventEmitter(); private onSoundFontLoaded(): void { if (this._isDestroyed) { @@ -1510,6 +3329,52 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'soundFontLoaded', null); } + /** + * This event is fired when a Midi file is being loaded. + * + * @remarks + * This event is fired when a Midi file for the song was generated and is being loaded + * by the synthesizer. This event can be used to inspect or modify the midi events + * which will be played for the song. This can be used to generate other visual representations + * of the song. + * + * > [!NOTE] + * > The generated midi file will NOT contain any metronome and count-in related events. The metronome and + * > count-in ticks are handled within the synthesizer. + * + * @eventProperty + * @category Events - Player + * @since 1.2.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.midiLoad.on(file => { + * initializePianoPractice(file); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.MidiLoad.On(file => + * { + * InitializePianoPractice(file); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.midiLoad.on { file -> + * initializePianoPractice(file) + * } + * ``` + * + */ public midiLoad: IEventEmitterOfT = new EventEmitterOfT(); private onMidiLoad(e: MidiFile): void { if (this._isDestroyed) { @@ -1519,6 +3384,45 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'midiLoad', e); } + /** + * This event is fired when the Midi file needed for playback was loaded. + * + * @eventProperty + * @category Events - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.midiLoaded.on(e => { + * hideGeneratingAudioIndicator(); + * updateSongDuration(e.endTime); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.MidiLoaded.On(e => + * { + * HideGeneratingAudioIndicator(); + * UpdateSongDuration(e.EndTime); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.midiLoaded.on { e -> + * hideGeneratingAudioIndicator() + * updateSongDuration(e.endTime) + * } + * ``` + * + */ public midiLoaded: IEventEmitterOfT = new EventEmitterOfT(); private onMidiLoaded(e: PositionChangedEventArgs): void { if (this._isDestroyed) { @@ -1528,6 +3432,42 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'midiFileLoaded', e); } + /** + * This event is fired when the playback state changed. + * + * @eventProperty + * @category Events - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playerStateChanged.on((args) => { + * updatePlayerControls(args.state, args.stopped); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlayerStateChanged.On(args => + * { + * UpdatePlayerControls(args); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playerStateChanged.on { args -> + * updatePlayerControls(args) + * } + * ``` + * + */ public playerStateChanged: IEventEmitterOfT = new EventEmitterOfT(); private onPlayerStateChanged(e: PlayerStateChangedEventArgs): void { @@ -1538,6 +3478,42 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'playerStateChanged', e); } + /** + * This event is fired when the current playback position of the song changed. + * + * @eventProperty + * @category Events - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playerPositionChanged.on((args) => { + * updatePlayerPosition(args); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlayerPositionChanged.On(args => + * { + * UpdatePlayerPosition(args); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playerPositionChanged.on { args -> + * updatePlayerPosition(args) + * } + * ``` + * + */ public playerPositionChanged: IEventEmitterOfT = new EventEmitterOfT(); private onPlayerPositionChanged(e: PositionChangedEventArgs): void { @@ -1550,6 +3526,85 @@ export class AlphaTabApiBase { } } + /** + * This event is fired when the synthesizer played certain midi events. + * + * @remarks + * This event is fired when the synthesizer played certain midi events. This allows reacing on various low level + * audio playback elements like notes/rests played or metronome ticks. + * + * Refer to the [related guide](https://www.alphatab.net/docs/guides/handling-midi-events) to learn more about this feature. + * + * Also note that the provided data models changed significantly in {@version 1.3.0}. We try to provide backwards compatibility + * until some extend but highly encourage changing to the new models in case of problems. + * + * @eventProperty + * @category Events - Player + * @since 1.2.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.midiEventsPlayedFilter = [alphaTab.midi.MidiEventType.AlphaTabMetronome]; + * api.midiEventsPlayed.on(function(e) { + * for(const midi of e.events) { + * if(midi.isMetronome) { + * console.log('Metronome tick ' + midi.tick); + * } + * } + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.MidiEventsPlayedFilter = new MidiEventType[] { AlphaTab.Midi.MidiEventType.AlphaTabMetronome }; + * api.MidiEventsPlayed.On(e => + * { + * foreach(var midi of e.events) + * { + * if(midi is AlphaTab.Midi.AlphaTabMetronomeEvent sysex && sysex.IsMetronome) + * { + * Console.WriteLine("Metronome tick " + midi.Tick); + * } + * } + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...); + * api.midiEventsPlayedFilter = alphaTab.collections.List( alphaTab.midi.MidiEventType.AlphaTabMetronome ) + * api.midiEventsPlayed.on { e -> + * for (midi in e.events) { + * if(midi instanceof alphaTab.midi.AlphaTabMetronomeEvent && midi.isMetronome) { + * println("Metronome tick " + midi.tick); + * } + * } + * } + * ``` + * @see {@link MidiEvent} + * @see {@link TimeSignatureEvent} + * @see {@link AlphaTabMetronomeEvent} + * @see {@link AlphaTabRestEvent} + * @see {@link NoteOnEvent} + * @see {@link NoteOffEvent} + * @see {@link ControlChangeEvent} + * @see {@link ProgramChangeEvent} + * @see {@link TempoChangeEvent} + * @see {@link PitchBendEvent} + * @see {@link NoteBendEvent} + * @see {@link EndOfTrackEvent} + * @see {@link MetaEvent} + * @see {@link MetaDataEvent} + * @see {@link MetaNumberEvent} + * @see {@link Midi20PerNotePitchBendEvent} + * @see {@link SystemCommonEvent} + * @see {@link SystemExclusiveEvent} + */ public midiEventsPlayed: IEventEmitterOfT = new EventEmitterOfT(); private onMidiEventsPlayed(e: MidiEventsPlayedEventArgs): void { @@ -1560,6 +3615,58 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'midiEventsPlayed', e); } + /** + * This event is fired when the playback range changed. + * + * @eventProperty + * @category Events - Player + * @since 1.2.3 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.playbackRangeChanged.on((args) => { + * if (args.playbackRange) { + * highlightRangeInProgressBar(args.playbackRange.startTick, args.playbackRange.endTick); + * } else { + * clearHighlightInProgressBar(); + * } + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * api.PlaybackRangeChanged.On(args => + * { + * if (args.PlaybackRange != null) + * { + * HighlightRangeInProgressBar(args.PlaybackRange.StartTick, args.PlaybackRange.EndTick); + * } + * else + * { + * ClearHighlightInProgressBar(); + * } + * }); + * ``` + * + * @example + * Android + * ```kotlin + * val api = AlphaTabApi(...) + * api.playbackRangeChanged.on { args -> + * val playbackRange = args.playbackRange + * if (playbackRange != null) { + * highlightRangeInProgressBar(playbackRange.startTick, playbackRange.endTick) + * } else { + * clearHighlightInProgressBar() + * } + * } + * ``` + * + */ public playbackRangeChanged: IEventEmitterOfT = new EventEmitterOfT(); private onPlaybackRangeChanged(e: PlaybackRangeChangedEventArgs): void { @@ -1581,4 +3688,162 @@ export class AlphaTabApiBase { (this.settingsUpdated as EventEmitter).trigger(); this.uiFacade.triggerEvent(this.container, 'settingsUpdated', null); } + + /** + * Loads and lists the available output devices which can be used by the player. + * @returns the list of available devices or an empty list if there are no permissions, or the player is not enabled. + * + * @remarks + * Will request permissions if needed. + * + * The values provided, can be passed into {@link setOutputDevice} to change dynamically the output device on which + * the sound is played. + * + * In the web version this functionality relies on experimental APIs and might not yet be available in all browsers. https://caniuse.com/mdn-api_audiocontext_sinkid + * @category Methods - Player + * @since 1.5.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * const devices = await api.enumerateOutputDevices(); + * + * buildDeviceSelector(devices, async selectedDevice => { + * await api.setOutputDevice(selectedDevice); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * var devices = await api.EnumerateOutputDevices(); + * + * BuildDeviceSelector(devices, async selectedDevice => { + * await api.SetOutputDevice(selectedDevice); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * fun init() = kotlinx.coroutines.runBlocking { + * val api = AlphaTabApi(...) + * val devices = api.enumerateOutputDevices().await() + * + * buildDeviceSelector(devices, fun (selectedDevice) { + * suspend { + * await api.setOutputDevice(selectedDevice) + * } + * }); + * } + * ``` + */ + public async enumerateOutputDevices(): Promise { + if (this.player) { + return await this.player.output.enumerateOutputDevices(); + } + + return [] as ISynthOutputDevice[]; + } + + /** + * Changes the output device which should be used for playing the audio (player must be enabled). + * @param device The output device to use, or null to switch to the default device. + * + * @remarks + * Use {@link enumerateOutputDevices} to load the list of available devices. + * + * In the web version this functionality relies on experimental APIs and might not yet be available in all browsers. https://caniuse.com/mdn-api_audiocontext_sinkid + * @category Methods - Player + * @since 1.5.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * const devices = await api.enumerateOutputDevices(); + * + * buildDeviceSelector(devices, async selectedDevice => { + * await api.setOutputDevice(selectedDevice); + * }); + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * var devices = await api.EnumerateOutputDevices(); + * + * BuildDeviceSelector(devices, async selectedDevice => { + * await api.SetOutputDevice(selectedDevice); + * }); + * ``` + * + * @example + * Android + * ```kotlin + * fun init() = kotlinx.coroutines.runBlocking { + * val api = AlphaTabApi(...) + * val devices = api.enumerateOutputDevices().await() + * + * buildDeviceSelector(devices, fun (selectedDevice) { + * suspend { + * await api.setOutputDevice(selectedDevice) + * } + * }); + * } + * ``` + */ + public async setOutputDevice(device: ISynthOutputDevice | null): Promise { + if (this.player) { + await this.player.output.setOutputDevice(device); + } + } + + /** + * The currently configured output device if changed via {@link setOutputDevice}. + * @returns The custom configured output device which was set via {@link setOutputDevice} or `null` + * if the default outputDevice is used. + * The output device might change dynamically if devices are connected/disconnected (e.g. bluetooth headset). + * + * @remarks + * Assumes {@link setOutputDevice} has been used. + * In the web version this functionality relies on experimental APIs and might not yet be available in all browsers. https://caniuse.com/mdn-api_audiocontext_sinkid + * + * @category Methods - Player + * @since 1.5.0 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * updateOutputDeviceUI(await api.getOutputDevice()) + * ``` + * + * @example + * C# + * ```cs + * var api = new AlphaTabApi(...); + * UpdateOutputDeviceUI(await api.GetOutputDevice()) + * ``` + * + * @example + * Android + * ```kotlin + * fun init() = kotlinx.coroutines.runBlocking { + * val api = AlphaTabApi(...) + * updateOutputDeviceUI(api.getOutputDevice().await()) + * } + * ``` + * + */ + public async getOutputDevice(): Promise { + if (this.player) { + return await this.player.output.getOutputDevice(); + } + + return null; + } } diff --git a/src/AlphaTabError.ts b/src/AlphaTabError.ts index bf7d6ad7a..3a3891b0f 100644 --- a/src/AlphaTabError.ts +++ b/src/AlphaTabError.ts @@ -1,14 +1,14 @@ export enum AlphaTabErrorType { - General, - Format, - AlphaTex + General = 0, + Format = 1, + AlphaTex = 2 } export class AlphaTabError extends Error { public type: AlphaTabErrorType; - - public constructor(type: AlphaTabErrorType, message: string | null = "", inner?: Error) { - super(message ?? "", { cause: inner }); + + public constructor(type: AlphaTabErrorType, message: string | null = '', inner?: Error) { + super(message ?? '', { cause: inner }); this.type = type; Object.setPrototypeOf(this, AlphaTabError.prototype); } diff --git a/src/CoreSettings.ts b/src/CoreSettings.ts index cb59d45d1..dc02100e1 100644 --- a/src/CoreSettings.ts +++ b/src/CoreSettings.ts @@ -1,70 +1,162 @@ import { Environment } from '@src/Environment'; import { LogLevel } from '@src/LogLevel'; +// biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 +import type { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; /** + * All main settings of alphaTab controlling rather general aspects of its behavior. * @json * @json_declaration */ export class CoreSettings { /** - * Gets or sets the script file url that will be used to spawn the workers. + * The full URL to the alphaTab JavaScript file. + * @remarks + * AlphaTab needs to know the full URL to the script file it is contained in to launch the web workers. AlphaTab will do its best to auto-detect + * this path but in case it fails, this setting can be used to explicitly define it. Altenatively also a global variable `ALPHATAB_ROOT` can + * be defined before initializing. Please be aware that bundling alphaTab together with other scripts might cause errors + * in case those scripts are not suitable for web workers. e.g. if there is a script bundled together with alphaTab that accesses the DOM, + * this will cause an error when alphaTab starts this script as worker. + * @defaultValue Absolute url to JavaScript file containing alphaTab. (auto detected) + * @category Core - JavaScript Specific * @target web + * @since 0.9.6 */ public scriptFile: string | null = null; /** - * Gets or sets the url to the fonts that will be used to generate the alphaTab font style. + * The full URL to the alphaTab font directory. + * @remarks + * AlphaTab will generate some dynamic CSS that is needed for displaying the music symbols correctly. For this it needs to know + * where the Web Font files of [Bravura](https://github.com/steinbergmedia/bravura) are. Normally alphaTab expects + * them to be in a `font` subfolder beside the script file. If this is not the case, this setting must be used to configure the path. + * Alternatively also a global variable `ALPHATAB_FONT` can be set on the page before initializing alphaTab. + * @defaultValue `"${AlphaTabScriptFolder}/font/"` + * @category Core - JavaScript Specific * @target web + * @since 0.9.6 */ public fontDirectory: string | null = null; /** - * Gets or sets the file to load directly after initializing alphaTab. + * The full URL to the input file to be loaded. + * @remarks + * AlphaTab can automatically load and render a file after initialization. This eliminates the need of manually calling + * one of the load methods which are available. alphaTab will automatically initiate an `XMLHttpRequest` after initialization + * to load and display the provided url of this setting. Note that this setting is only interpreted once on initialization. + * @defaultValue `null` + * @category Core - JavaScript Specific * @target web + * @since 0.9.6 */ public file: string | null = null; /** - * Gets or sets whether the UI element contains alphaTex code that should be - * used to initialize alphaTab. + * Whether the contents of the DOM element should be loaded as alphaTex. * @target web + * @remarks + * This setting allows you to fill alphaTex code into the DOM element and make alphaTab automatically + * load it when initializing. Note that this setting is only interpreted once on initialization. + * @defaultValue `false` + * @category Core - JavaScript Specific + * @since 0.9.6 + * @example + * JavaScript + * ```html + *
\title "Simple alphaTex init" . 3.3*4
+ * + * ``` */ public tex: boolean = false; /** - * Gets or sets the initial tracks that should be loaded for the score. - * @target web + * The tracks to display for the initally loaded file. * @json_raw + * @remarks + * This setting can be used in combinition with the {@link file} or {@link tex} option. It controls which of the tracks + * of the initially loaded file should be displayed. + * @defaultValue `null` + * @category Core - JavaScript Specific + * @target web + * @since 0.9.6 */ public tracks: number | number[] | 'all' | null = null; /** - * Gets or sets whether lazy loading for displayed elements is enabled. + * Enables lazy loading of the rendered music sheet chunks. + * @remarks + * AlphaTab renders the music sheet in smaller sub-chunks to have fast UI feedback. Not all of those sub-chunks are immediately + * appended to the DOM due to performance reasons. AlphaTab tries to detect which elements are visible on the screen, and only + * appends those elements to the DOM. This reduces the load of the browser heavily but is not working for all layouts and use cases. + * This setting set to false, ensures that all rendered items are instantly appended to the DOM. + * The lazy rendering of partial might not be available on all platforms. + * @defaultValue `true` + * @category Core + * @since 0.9.6 */ public enableLazyLoading: boolean = true; /** * The engine which should be used to render the the tablature. - * - * - **default**- Platform specific default engine - * - **html5**- HTML5 Canvas - * - **svg**- SVG + * @remarks + * AlphaTab can use various render engines to draw the music notation. The available render engines is specific to the platform. Please refer to the table below to find out which engines are available on which platform. + * - `default`- Platform specific default engine + * - `html5`- Uses HTML5 canvas elements to render the music notation (browser only) + * - `svg`- Outputs SVG strings (all platforms, default for web) + * - `skia` - Uses [Skia](https://skia.org/) for rendering (all non-browser platforms via [alphaSkia](https://github.com/CoderLine/alphaSkia), default for non-web) + * - `gdi` - Uses [GDI+](https://docs.microsoft.com/en-us/dotnet/framework/winforms/advanced/graphics-and-drawing-in-windows-forms) for rendering (only on .net) + * - `android` - Uses [android.graphics.Canvas](https://developer.android.com/reference/android/graphics/Canvas) for rendering (only on Android) + * @defaultValue `"default"` + * @category Core + * @since 0.9.6 */ public engine: string = 'default'; /** * The log level to use within alphaTab + * @remarks + * AlphaTab internally does quite a bit of logging for debugging and informational purposes. The log level of alphaTab can be controlled via this setting. + * @defaultValue `LogLevel.Info` + * @category Core + * @since 0.9.6 */ public logLevel: LogLevel = LogLevel.Info; /** - * Gets or sets whether the rendering should be done in a worker if possible. + * Whether the rendering should be done in a worker if possible. + * @remarks + * AlphaTab normally tries to render the music sheet asynchronously in a worker. This reduces the load on the UI side and avoids hanging. However sometimes it might be more desirable to have + * a synchronous rendering behavior. This setting can be set to false to synchronously render the music sheet on the UI side. + * @defaultValue `true` + * @category Core + * @since 0.9.6 */ public useWorkers: boolean = true; /** - * Gets or sets whether in the {@link BoundsLookup} also the - * position and area of each individual note is provided. + * Whether in the {@link BoundsLookup} also the position and area of each individual note is provided. + * @remarks + * AlphaTab collects the position of the rendered music notation elements during the rendering process. This way some level of interactivity can be provided like the feature that seeks to the corresponding position when clicking on a beat. + * By default the position of the individual notes is not collected due to performance reasons. If access to note position information is needed, this setting can enable it. + * @defaultValue `false` + * @category Core + * @since 0.9.6 + * @example + * JavaScript + * ```js + * const settings = new alphaTab.model.Settings(); + * settings.core.includeNoteBounds = true; + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'), settings); + * api.renderFinished.on(() => { + * const lookup = api.renderer.boundsLookup; + * const x = 100; + * const y = 100; + * const beat = lookup.getBeatAtPos(x, y); + * const note = lookup.getNoteAtPos(beat, x, y); + * }); + * ``` */ public includeNoteBounds: boolean = false; diff --git a/src/DisplaySettings.ts b/src/DisplaySettings.ts index a50909fa1..9cf59dd09 100644 --- a/src/DisplaySettings.ts +++ b/src/DisplaySettings.ts @@ -1,6 +1,8 @@ import { RenderingResources } from '@src/RenderingResources'; import { LayoutMode } from '@src/LayoutMode'; import { StaveProfile } from '@src/StaveProfile'; +// biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 +import type { Staff } from '@src/model/Staff'; /** * Lists the different modes in which the staves and systems are arranged. @@ -9,12 +11,12 @@ export enum SystemsLayoutMode { /** * Use the automatic alignment system provided by alphaTab (default) */ - Automatic, + Automatic = 0, /** * Use the systems layout and sizing information stored from the score model. */ - UseModelLayout + UseModelLayout = 1 } /** @@ -24,129 +26,335 @@ export enum SystemsLayoutMode { */ export class DisplaySettings { /** - * Sets the zoom level of the rendered notation + * The zoom level of the rendered notation. + * @since 0.9.6 + * @category Display + * @defaultValue `1.0` + * @remarks + * AlphaTab can scale up or down the rendered music notation for more optimized display scenarios. By default music notation is rendered at 100% scale (value 1) and can be scaled up or down by + * percental values. */ public scale: number = 1.0; /** * The default stretch force to use for layouting. + * @since 0.9.6 + * @category Display + * @defaultValue `1` + * @remarks + * The stretch force is a setting that controls the spacing of the music notation. AlphaTab uses a varaint of the Gourlay algorithm for spacing which has springs and rods for + * aligning elements. This setting controls the "strength" of the springs. The stronger the springs, the wider the spacing. + * + * | Force 1 | Force 0.5 | + * |--------------------------------------------------------------|-------------------------------------------------------| + * | ![Default](https://alphatab.net/img/reference/property/stretchforce-default.png) | ![0.5](https://alphatab.net/img/reference/property/stretchforce-half.png) | */ public stretchForce: number = 1.0; /** * The layouting mode used to arrange the the notation. + * @remarks + * AlphaTab has various layout engines that arrange the rendered bars differently. This setting controls which layout mode is used. + * + * @since 0.9.6 + * @category Display + * @defaultValue `LayoutMode.Page` */ public layoutMode: LayoutMode = LayoutMode.Page; /** - * The stave profile to use. + * The stave profile defining which staves are shown for the music sheet. + * @since 0.9.6 + * @category Display + * @defaultValue `StaveProfile.Default` + * @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 */ public staveProfile: StaveProfile = StaveProfile.Default; /** - * Limit the displayed bars per row. + * Limit the displayed bars per system (row). (-1 for automatic mode) + * @since 0.9.6 + * @category Display + * @defaultValue `-1` + * @remarks + * This setting sets the number of bars that should be put into one row during layouting. This setting is only respected + * when using the {@link LayoutMode.Page} where bars are aligned in systems. [Demo](https://alphatab.net/docs/showcase/layouts#page-layout-5-bars-per-row). */ public barsPerRow: number = -1; /** - * The bar start number to start layouting with. Note that this is the bar number and not an index! + * The bar start index to start layouting with. + * @since 0.9.6 + * @category Display + * @defaultValue `1` + * @remarks + * This setting sets the index of the first bar that should be rendered from the overall song. This setting can be used to + * achieve a paging system or to only show partial bars of the same file. By this a tutorial alike display can be achieved + * that explains various parts of the song. Please note that this is the bar number as shown in the music sheet (1-based) not the array index (0-based). + * [Demo](https://alphatab.net/docs/showcase/layouts#page-layout-bar-5-to-8) */ public startBar: number = 1; /** - * The amount of bars to render overall. + * The total number of bars that should be rendered from the song. (-1 for all bars) + * @since 0.9.6 + * @category Display + * @defaultValue `-1` + * @remarks + * This setting sets the number of bars that should be rendered from the overall song. This setting can be used to + * achieve a paging system or to only show partial bars of the same file. By this a tutorial alike display can be achieved + * that explains various parts of the song. [Demo](https://alphatab.net/docs/showcase/layouts) */ public barCount: number = -1; /** - * The number of bars that should be rendered per partial. This setting is not used by all layouts. + * The number of bars that should be placed within one partial render. + * @since 0.9.6 + * @category Display + * @defaultValue `10` + * @remarks + * AlphaTab renders the whole music sheet in smaller chunks named "partials". This is to reduce the risk of + * encountering browser performance restrictions and it gives faster visual feedback to the user. This + * setting controls how many bars are placed within such a partial. */ public barCountPerPartial: number = 10; /** - * Whether the last system (row) should be also justified to the whole width of the music sheet. - * (applies only for page layout). + * Whether to justify also the last system in page layouts. + * @remarks + * Setting this option to `true` tells alphaTab to also justify the last system (row) like it + * already does for the systems which are full. + * | Justification Disabled | Justification Enabled | + * |--------------------------------------------------------------|-------------------------------------------------------| + * | ![Disabled](https://alphatab.net/img/reference/property/justify-last-system-false.png) | ![Enabled](https://alphatab.net/img/reference/property/justify-last-system-true.png) | + * @since 1.3.0 + * @category Display + * @defaultValue `false` */ public justifyLastSystem: boolean = false; /** - * Gets or sets the resources used during rendering. This defines all fonts and colors used. + * Allows adjusting of the used fonts and colors for rendering. * @json_partial_names + * @since 0.9.6 + * @category Display + * @defaultValue `false` + * @domWildcard + * @remarks + * AlphaTab allows configuring the colors and fonts used for rendering via the rendering resources settings. Please note that as of today + * this is the primary way of changing the way how alphaTab styles elements. CSS styling in the browser cannot be guaranteed to work due to its flexibility. + * + * + * Due to space reasons in the following table the common prefix of the settings are removed. Please refer to these examples to eliminate confusion on the usage: + * + * | Platform | Prefix | Example Usage | + * |------------|---------------------------|--------------------------------------------------------------------| + * | JavaScript | `display.resources.` | `settings.display.resources.wordsFont = ...` | + * | JSON | `display.resources.` | `var settings = { display: { resources: { wordsFonts: '...'} } };` | + * | JSON | `resources.` | `var settings = { resources: { wordsFonts: '...'} };` | + * | .net | `Display.Resources.` | `settings.Display.Resources.WordsFonts = ...` | + * | Android | `display.resources.` | `settings.display.resources.wordsFonts = ...` | + * ## Types + * + * ### Fonts + * + * For the JavaScript platform any font that might be installed on the client machines can be used. + * Any additional fonts can be added via WebFonts. The rendering of the score will be delayed until it is detected that the font was loaded. + * Simply use any CSS font property compliant string as configuration. Relative font sizes with percentual values are not supported, remaining values will be considered if supported. + * + * {@since 1.2.3} Multiple fonts are also supported for the Web version. alphaTab will check if any of the fonts in the list is loaded instead of all. If none is available at the time alphaTab is initialized, it will try to initiate the load of the specified fonts individual through the Browser Font APIs. + * + * For the .net platform any installed font on the system can be used. Simply construct the `Font` object to configure your desired fonts. + * + * ### Colors + * + * For JavaScript you can use any CSS font property compliant string. (#RGB, #RGBA, #RRGGBB, #RRGGBBAA, rgb(r,g,b), rgba(r,g,b,a) ) + * + * On .net simply construct the `Color` object to configure your desired color. */ public resources: RenderingResources = new RenderingResources(); /** - * Gets or sets the padding between the music notation and the border. + * Adjusts the padding between the music notation and the border. + * @remarks + * Adjusts the padding between the music notation and the outer border of the container element. + * The array is either: + * * 2 elements: `[left-right, top-bottom]` + * * 4 elements: ``[left, top, right, bottom]`` + * @since 0.9.6 + * @category Display + * @defaultValue `[35, 35]` */ public padding: number[] = [35, 35]; /** - * Gets or sets the top padding applied to first system. + * The top padding applied to first system. + * @since 1.4.0 + * @category Display + * @defaultValue `5` */ public firstSystemPaddingTop: number = 5; /** - * Gets or sets the top padding applied to systems. + * The top padding applied systems beside the first one. + * @since 1.4.0 + * @category Display + * @defaultValue `10` */ public systemPaddingTop: number = 10; /** - * Gets or sets the bottom padding applied to systems. + * The bottom padding applied to systems beside the last one. + * @since 1.4.0 + * @category Display + * @defaultValue `20` */ public systemPaddingBottom: number = 20; /** - * Gets or sets the bottom padding applied to last system. + * The bottom padding applied to the last system. + * @since 1.4.0 + * @category Display + * @defaultValue `0` */ public lastSystemPaddingBottom: number = 0; /** - * Gets or sets the padding left to the track name label of the system. + * The padding left to the track name label of the system. + * @since 1.4.0 + * @category Display + * @defaultValue `0` */ public systemLabelPaddingLeft: number = 0; /** - * Gets or sets the padding right to the track name label of the system. + * The padding left to the track name label of the system. + * @since 1.4.0 + * @category Display + * @defaultValue `3` */ - public systemLabelPaddingRight: number = 5; + public systemLabelPaddingRight: number = 3; /** - * Gets or sets the padding between the accolade bar and the start of the bar itself. + * The padding between the accolade bar and the start of the bar itself. + * @since 1.4.0 + * @category Display + * @defaultValue `3` */ public accoladeBarPaddingRight: number = 3; /** - * Gets or sets the top padding applied to main notation staffs. + * The bottom padding applied to main notation staves (standard, tabs, numbered, slash). + * @since 1.4.0 + * @category Display + * @defaultValue `5` */ public notationStaffPaddingTop: number = 5; /** - * Gets or sets the bottom padding applied to main notation staffs. + * The bottom padding applied to main notation staves (standard, tabs, numbered, slash). + * @since 1.4.0 + * @category Display + * @defaultValue `5` */ public notationStaffPaddingBottom: number = 5; /** - * Gets or sets the top padding applied to effect annotation staffs. + * The top padding applied to effect annotation staffs. + * @since 1.4.0 + * @category Display + * @defaultValue `0` */ public effectStaffPaddingTop: number = 0; /** - * Gets or sets the bottom padding applied to effect annotation staffs. + * The bottom padding applied to effect annotation staffs. + * @since 1.4.0 + * @category Display + * @defaultValue `0` */ public effectStaffPaddingBottom: number = 0; /** - * Gets or sets the left padding applied between the left line and the first glyph in the first staff in a system. + * The left padding applied between the left line and the first glyph in the first staff in a system. + * @since 1.4.0 + * @category Display + * @defaultValue `6` */ public firstStaffPaddingLeft: number = 6; /** - * Gets or sets the left padding applied between the left line and the first glyph in the following staff in a system. + * The left padding applied between the left line and the first glyph in the following staff in a system. + * @since 1.4.0 + * @category Display + * @defaultValue `2` */ public staffPaddingLeft: number = 2; /** - * Gets how the systems should be layed out. + * The mode used to arrange staves and systems. + * @since 1.3.0 + * @category Display + * @defaultValue `1` + * @remarks + * By default alphaTab uses an own (automatic) mode to arrange and scale the bars when + * putting them into staves. This property allows changing this mode to change the music sheet arrangement. + * + * ## Supported File Formats: + * * Guitar Pro 6-8 {@since 1.3.0} + * If you want/need support for more file formats to respect the sizing information feel free to [open a discussion](https://github.com/CoderLine/alphaTab/discussions/new?category=ideas) on GitHub. + * + * ## Automatic Mode + * + * In the automatic mode alphaTab arranges the bars and staves using its internal mechanisms. + * + * For the `page` layout this means it will scale the bars according to the `stretchForce` and available width. + * Wrapping into new systems (rows) will happen when the row is considered "full". + * + * For the `horizontal` layout the `stretchForce` defines the sizing and no wrapping happens at all. + * + * ## Model Layout mode + * + * File formats like Guitar Pro embed information about the layout in the file and alphaTab can read and use this information. + * When this mode is enabled, alphaTab will also actively use this information and try to respect it. + * + * 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. + * * `score.tracks[index].staves[index].bars[index].displayWidth` - The absolute size of this bar when displayed. + * + * **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. + * * `score.masterBars[index].displayWidth` - Like the `displayWidth` on bar level. + * + * ### Page Layout + * + * The page 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. As indicated above, the scale is rather a ratio than a percentage value but percentages work also: + * + * ![Page Layout](https://alphatab.net/img/reference/property/systems-layout-page-examples.png) + * + * The page layout does not use `displayWidth`. The use of absolute widths would break the proper alignments needed for this kind of display. + * + * Also note that the sizing is including any glyphs and notation elements within the bar. e.g. if there are clefs in the bar, they are still "squeezed" into the available size. + * It is not the case that the actual notes with their lengths are sized accordingly. This fits the sizing system of Guitar Pro and when files are customized there, + * alphaTab will match this layout quite close. + * + * ### Horizontal Layout + * + * The horizontal layout uses the `displayWidth` to scale the bars to size the bars exactly as specified. This kind of sizing and layout can be useful for usecases like: + * + * * 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). */ public systemsLayoutMode: SystemsLayoutMode = SystemsLayoutMode.Automatic; } diff --git a/src/Environment.ts b/src/Environment.ts index 1a597a943..45515da50 100644 --- a/src/Environment.ts +++ b/src/Environment.ts @@ -5,15 +5,15 @@ import { Gp3To5Importer } from '@src/importer/Gp3To5Importer'; import { Gp7To8Importer } from '@src/importer/Gp7To8Importer'; import { GpxImporter } from '@src/importer/GpxImporter'; import { MusicXmlImporter } from '@src/importer/MusicXmlImporter'; -import { ScoreImporter } from '@src/importer/ScoreImporter'; +import type { ScoreImporter } from '@src/importer/ScoreImporter'; import { HarmonicType } from '@src/model/HarmonicType'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { AlphaSynthWebWorker } from '@src/platform/javascript/AlphaSynthWebWorker'; import { AlphaTabWebWorker } from '@src/platform/javascript/AlphaTabWebWorker'; import { Html5Canvas } from '@src/platform/javascript/Html5Canvas'; import { JQueryAlphaTab } from '@src/platform/javascript/JQueryAlphaTab'; import { CssFontSvgCanvas } from '@src/platform/svg/CssFontSvgCanvas'; -import { BarRendererFactory } from '@src/rendering/BarRendererFactory'; +import type { BarRendererFactory } from '@src/rendering/BarRendererFactory'; import { EffectBarRendererFactory } from '@src/rendering/EffectBarRendererFactory'; import { AlternateEndingsEffectInfo } from '@src/rendering/effects/AlternateEndingsEffectInfo'; import { CapoEffectInfo } from '@src/rendering/effects/CapoEffectInfo'; @@ -43,9 +43,9 @@ import { WideBeatVibratoEffectInfo } from '@src/rendering/effects/WideBeatVibrat import { WideNoteVibratoEffectInfo } from '@src/rendering/effects/WideNoteVibratoEffectInfo'; import { HorizontalScreenLayout } from '@src/rendering/layout/HorizontalScreenLayout'; import { PageViewLayout } from '@src/rendering/layout/PageViewLayout'; -import { ScoreLayout } from '@src/rendering/layout/ScoreLayout'; +import type { ScoreLayout } from '@src/rendering/layout/ScoreLayout'; import { ScoreBarRendererFactory } from '@src/rendering/ScoreBarRendererFactory'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; import { TabBarRendererFactory } from '@src/rendering/TabBarRendererFactory'; import { FontLoadingChecker } from '@src/util/FontLoadingChecker'; import { Logger } from '@src/Logger'; @@ -55,27 +55,28 @@ import { ResizeObserverPolyfill } from '@src/platform/javascript/ResizeObserverP import { WebPlatform } from '@src/platform/javascript/WebPlatform'; import { IntersectionObserverPolyfill } from '@src/platform/javascript/IntersectionObserverPolyfill'; import { AlphaSynthWebWorklet } from '@src/platform/javascript/AlphaSynthAudioWorkletOutput'; -import { SkiaCanvas } from './platform/skia/SkiaCanvas'; -import { Font } from './model'; -import { Settings } from './Settings'; -import { AlphaTabError, AlphaTabErrorType } from './AlphaTabError'; -import { SlashBarRendererFactory } from './rendering/SlashBarRendererFactory'; -import { NumberedBarRendererFactory } from './rendering/NumberedBarRendererFactory'; -import { FreeTimeEffectInfo } from './rendering/effects/FreeTimeEffectInfo'; -import { ScoreBarRenderer } from './rendering/ScoreBarRenderer'; -import { TabBarRenderer } from './rendering/TabBarRenderer'; -import { SustainPedalEffectInfo } from './rendering/effects/SustainPedalEffectInfo'; -import { GolpeEffectInfo } from './rendering/effects/GolpeEffectInfo'; -import { GolpeType } from './model/GolpeType'; -import { WahPedalEffectInfo } from './rendering/effects/WahPedalEffectInfo'; -import { BeatBarreEffectInfo } from './rendering/effects/BeatBarreEffectInfo'; -import { NoteOrnamentEffectInfo } from './rendering/effects/NoteOrnamentEffectInfo'; -import { RasgueadoEffectInfo } from './rendering/effects/RasgueadoEffectInfo'; -import { DirectionsEffectInfo } from './rendering/effects/DirectionsEffectInfo'; -import { BeatTimerEffectInfo } from './rendering/effects/BeatTimerEffectInfo'; +import { SkiaCanvas } from '@src/platform/skia/SkiaCanvas'; +import type { Font } from '@src/model/Font'; +import type { Settings } from '@src/Settings'; +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; +import { SlashBarRendererFactory } from '@src/rendering/SlashBarRendererFactory'; +import { NumberedBarRendererFactory } from '@src/rendering/NumberedBarRendererFactory'; +import { FreeTimeEffectInfo } from '@src/rendering/effects/FreeTimeEffectInfo'; +import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; +import { SustainPedalEffectInfo } from '@src/rendering/effects/SustainPedalEffectInfo'; +import { GolpeEffectInfo } from '@src/rendering/effects/GolpeEffectInfo'; +import { GolpeType } from '@src/model/GolpeType'; +import { WahPedalEffectInfo } from '@src/rendering/effects/WahPedalEffectInfo'; +import { BeatBarreEffectInfo } from '@src/rendering/effects/BeatBarreEffectInfo'; +import { NoteOrnamentEffectInfo } from '@src/rendering/effects/NoteOrnamentEffectInfo'; +import { RasgueadoEffectInfo } from '@src/rendering/effects/RasgueadoEffectInfo'; +import { DirectionsEffectInfo } from '@src/rendering/effects/DirectionsEffectInfo'; +import { BeatTimerEffectInfo } from '@src/rendering/effects/BeatTimerEffectInfo'; +import { VersionInfo } from '@src/generated/VersionInfo'; /** - * A factory for custom layout engines. + * A factory for custom layout engines. */ export class LayoutEngineFactory { /** @@ -94,13 +95,13 @@ export class LayoutEngineFactory { } /** - * A factory for custom render engines. - * Note for Web: To use a custom engine in workers you have to ensure the engine and registration to the environment are + * A factory for custom render engines. + * Note for Web: To use a custom engine in workers you have to ensure the engine and registration to the environment are * also done in the background worker files (e.g. when bundling) */ export class RenderEngineFactory { /** - * Whether the layout supports background workers. + * Whether the layout supports background workers. */ public readonly supportsWorkers: boolean; public readonly createCanvas: () => ICanvas; @@ -131,16 +132,19 @@ export class Environment { /** * The font size of the music font in pixel. + * @internal */ public static readonly MusicFontSize = 34; /** * The scaling factor to use when rending raster graphics for sharper rendering on high-dpi displays. + * @internal */ public static HighDpiFactor = 1; /** * @target web + * @internal */ public static createStyleElement(elementDocument: HTMLDocument, fontDirectory: string | null) { let styleElement: HTMLStyleElement = elementDocument.getElementById('alphaTabStyle') as HTMLStyleElement; @@ -152,7 +156,7 @@ export class Environment { styleElement = elementDocument.createElement('style'); styleElement.id = 'alphaTabStyle'; - let css: string = ` + const css: string = ` @font-face { font-display: block; font-family: 'alphaTab'; @@ -201,6 +205,7 @@ export class Environment { /** * @target web + * @internal */ public static get globalThis(): any { if (Environment._globalThis === undefined) { @@ -224,38 +229,39 @@ export class Environment { } } - return this._globalThis; + return Environment._globalThis; } /** * @target web */ - public static webPlatform: WebPlatform = Environment.detectWebPlatform(); + public static readonly webPlatform: WebPlatform = Environment.detectWebPlatform(); /** * @target web */ - public static isWebPackBundled: boolean = Environment.detectWebPack(); + public static readonly isWebPackBundled: boolean = Environment.detectWebPack(); /** * @target web */ - public static isViteBundled: boolean = Environment.detectVite(); + public static readonly isViteBundled: boolean = Environment.detectVite(); /** * @target web */ - public static scriptFile: string | null = Environment.detectScriptFile(); + public static readonly scriptFile: string | null = Environment.detectScriptFile(); /** * @target web */ - public static fontDirectory: string | null = Environment.detectFontDirectory(); + public static readonly fontDirectory: string | null = Environment.detectFontDirectory(); /** * @target web + * @internal */ - public static bravuraFontChecker: FontLoadingChecker = new FontLoadingChecker(['alphaTab']); + public static readonly bravuraFontChecker: FontLoadingChecker = new FontLoadingChecker(['alphaTab']); /** * @target web @@ -332,6 +338,7 @@ export class Environment { /** * @target web + * @internal */ public static ensureFullUrl(relativeUrl: string | null): string { if (!relativeUrl) { @@ -340,7 +347,7 @@ export class Environment { if (!relativeUrl.startsWith('http') && !relativeUrl.startsWith('https') && !relativeUrl.startsWith('file')) { let root: string = ''; - let location: Location = Environment.globalThis['location']; + const location: Location = Environment.globalThis.location; root += location.protocol?.toString(); root += '//'?.toString(); if (location.hostname) { @@ -353,7 +360,7 @@ export class Environment { // as it is not clearly defined how slashes are treated in the location object // better be safe than sorry here if (!relativeUrl.startsWith('/')) { - let directory: string = location.pathname.split('/').slice(0, -1).join('/'); + const directory: string = location.pathname.split('/').slice(0, -1).join('/'); if (directory.length > 0) { if (!directory.startsWith('/')) { root += '/'?.toString(); @@ -386,14 +393,14 @@ export class Environment { */ private static detectFontDirectory(): string | null { if (!Environment.isRunningInWorker && Environment.globalThis.ALPHATAB_FONT) { - return Environment.ensureFullUrl(Environment.globalThis['ALPHATAB_FONT']); + return Environment.ensureFullUrl(Environment.globalThis.ALPHATAB_FONT); } const scriptFile = Environment.scriptFile; if (scriptFile) { - let lastSlash: number = scriptFile.lastIndexOf(String.fromCharCode(47)); + const lastSlash: number = scriptFile.lastIndexOf(String.fromCharCode(47)); if (lastSlash >= 0) { - return scriptFile.substr(0, lastSlash) + '/font/'; + return `${scriptFile.substr(0, lastSlash)}/font/`; } } @@ -405,9 +412,10 @@ export class Environment { */ private static registerJQueryPlugin(): void { if (!Environment.isRunningInWorker && Environment.globalThis && 'jQuery' in Environment.globalThis) { - let jquery: any = Environment.globalThis['jQuery']; - let api: JQueryAlphaTab = new JQueryAlphaTab(); + const jquery: any = Environment.globalThis.jQuery; + const api: JQueryAlphaTab = new JQueryAlphaTab(); jquery.fn.alphaTab = function (this: any, method: string) { + // biome-ignore lint/style/noArguments: Legacy jQuery plugin argument forwarding const args = Array.prototype.slice.call(arguments, 1); // if only a single element is affected, we use this if (this.length === 1) { @@ -425,9 +433,19 @@ export class Environment { } } - public static renderEngines: Map = Environment.createDefaultRenderEngines(); - public static layoutEngines: Map = Environment.createDefaultLayoutEngines(); - public static staveProfiles: Map = Environment.createDefaultStaveProfiles(); + public static readonly renderEngines: Map = Environment.createDefaultRenderEngines(); + + /** + * @internal + */ + public static readonly layoutEngines: Map = + Environment.createDefaultLayoutEngines(); + + /** + * @internal + */ + public static readonly staveProfiles: Map = + Environment.createDefaultStaveProfiles(); public static getRenderEngineFactory(engine: string): RenderEngineFactory { if (!engine || !Environment.renderEngines.has(engine)) { @@ -436,6 +454,9 @@ export class Environment { return Environment.renderEngines.get(engine)!; } + /** + * @internal + */ public static getLayoutEngineFactory(layoutMode: LayoutMode): LayoutEngineFactory { if (!layoutMode || !Environment.layoutEngines.has(layoutMode)) { return Environment.layoutEngines.get(LayoutMode.Page)!; @@ -489,14 +510,12 @@ export class Environment { } /** - * Registers a new custom font for the usage in the alphaSkia rendering backend using - * provided font information. + * Registers a new custom font for the usage in the alphaSkia rendering backend. * @param fontData The raw binary data of the font. - * @param fontInfo If provided the font info provided overrules * @returns The font info under which the font was registered. */ - public static registerAlphaSkiaCustomFont(fontData: Uint8Array, fontInfo?: Font | undefined): Font { - return SkiaCanvas.registerFont(fontData, fontInfo); + public static registerAlphaSkiaCustomFont(fontData: Uint8Array): Font { + return SkiaCanvas.registerFont(fontData); } /** @@ -537,7 +556,7 @@ export class Environment { new BeatBarreEffectInfo(), new NoteOrnamentEffectInfo(), new RasgueadoEffectInfo(), - new WahPedalEffectInfo(), + new WahPedalEffectInfo() ]), new EffectBarRendererFactory( Environment.StaffIdBeforeScoreHideable, @@ -735,16 +754,18 @@ export class Environment { /** * @target web + * @internal */ public static get alphaTabWorker(): any { - return this.globalThis.Worker; + return Environment.globalThis.Worker; } /** * @target web + * @internal */ public static get alphaTabUrl(): any { - return this.globalThis.URL; + return Environment.globalThis.URL; } /** @@ -836,4 +857,36 @@ export class Environment { return WebPlatform.Browser; } + + /** + * Prints the environment information for easier troubleshooting. + * @param force Whether to force printing. + */ + public static printEnvironmentInfo(force: boolean = true) { + const printer: (message: string) => void = force + ? message => { + Logger.log.debug('VersionInfo', message); + } + : message => { + Logger.debug('VersionInfo', message); + }; + VersionInfo.print(printer); + printer(`High DPI: ${Environment.HighDpiFactor}`); + Environment.printPlatformInfo(printer); + } + + /** + * @target web + * @partial + */ + private static printPlatformInfo(print: (message: string) => void) { + print(`Browser: ${navigator.userAgent}`); + print(`Platform: ${WebPlatform[Environment.webPlatform]}`); + print(`WebPack: ${Environment.isWebPackBundled}`); + print(`Vite: ${Environment.isViteBundled}`); + if (Environment.webPlatform !== WebPlatform.NodeJs) { + print(`Window Size: ${window.outerWidth}x${window.outerHeight}`); + print(`Screen Size: ${window.screen.width}x${window.screen.height}`); + } + } } diff --git a/src/FileLoadError.ts b/src/FileLoadError.ts index ca1282823..942654195 100644 --- a/src/FileLoadError.ts +++ b/src/FileLoadError.ts @@ -1,4 +1,4 @@ -import { AlphaTabError, AlphaTabErrorType } from "./AlphaTabError"; +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; /** * @target web diff --git a/src/FormatError.ts b/src/FormatError.ts index 8f5802f3f..8b39e12a7 100644 --- a/src/FormatError.ts +++ b/src/FormatError.ts @@ -1,4 +1,4 @@ -import { AlphaTabError, AlphaTabErrorType} from "@src/AlphaTabError"; +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; /** * An invalid input format was detected (e.g. invalid setting values, file formats,...) diff --git a/src/ImporterSettings.ts b/src/ImporterSettings.ts index 6b3c289c4..24c9f7e37 100644 --- a/src/ImporterSettings.ts +++ b/src/ImporterSettings.ts @@ -5,19 +5,61 @@ */ export class ImporterSettings { /** - * The text encoding to use when decoding strings. By default UTF-8 is used. + * The text encoding to use when decoding strings. + * @since 0.9.6 + * @defaultValue `utf-8` + * @category Importer + * @remarks + * By default strings are interpreted as UTF-8 from the input files. This is sometimes not the case and leads to strong display + * of strings in the rendered notation. Via this setting the text encoding for decoding the strings can be changed. The supported + * encodings depend on the browser or operating system. This setting is considered for the importers + * + * * Guitar Pro 7 + * * Guitar Pro 6 + * * Guitar Pro 3-5 + * * MusicXML */ public encoding: string = 'utf-8'; /** - * If part-groups should be merged into a single track. + * If part-groups should be merged into a single track (MusicXML). + * @since 0.9.6 + * @defaultValue `false` + * @category Importer + * @remarks + * This setting controls whether multiple `part-group` tags will result into a single track with multiple staves. */ public mergePartGroupsInMusicXml: boolean = false; /** - * If set to true, text annotations on beats are attempted to be parsed as - * lyrics considering spaces as separators and removing underscores. - * If a track/staff has explicit lyrics the beat texts will not be detected as lyrics. + * Enables detecting lyrics from beat texts + * @since 1.2.0 + * @category Importer + * @defaultValue `false` + * @remarks + * + * On various old Guitar Pro 3-5 files tab authors often used the "beat text" feature to add lyrics to the individual tracks. + * This was easier and quicker than using the lyrics feature. + * + * These texts were optimized to align correctly when viewed in Guitar Pro with the default layout but can lead to + * disturbed display in alphaTab. When `beatTextAsLyrics` is set to true, alphaTab will try to rather parse beat text + * values as lyrics using typical text patterns like dashes, underscores and spaces. + * + * The lyrics are only detected if not already proper lyrics are applied to the track. + * + * Enable this option for input files which suffer from this practice. + * + * > [!NOTE] + * > alphaTab tries to relate the texts and chunks to the beats but this is not perfect. + * > Errors are likely to happen with such kind of files. + * + * **Enabled** + * + * ![Enabled](https://alphatab.net/img/reference/property/beattextaslyrics-enabled.png) + * + * **Disabled** + * + * ![Disabled](https://alphatab.net/img/reference/property/beattextaslyrics-disabled.png) */ public beatTextAsLyrics: boolean = false; } diff --git a/src/LayoutMode.ts b/src/LayoutMode.ts index 1c976602f..9be32de56 100644 --- a/src/LayoutMode.ts +++ b/src/LayoutMode.ts @@ -1,13 +1,13 @@ /** * Lists all layout modes that are supported. */ - export enum LayoutMode { +export enum LayoutMode { /** - * Bars are aligned in rows using a fixed width. + * The bars are aligned in an [vertically endless page-style fashion](https://alphatab.net/docs/showcase/layouts#page-layout) */ - Page, + Page = 0, /** - * Bars are aligned horizontally in one row + * Bars are aligned horizontally in [one horizontally endless system (row)](https://alphatab.net/docs/showcase/layouts#horizontal-layout) */ - Horizontal -} \ No newline at end of file + Horizontal = 1 +} diff --git a/src/LogLevel.ts b/src/LogLevel.ts index 2200bc17e..022b8d4bf 100644 --- a/src/LogLevel.ts +++ b/src/LogLevel.ts @@ -6,21 +6,21 @@ export enum LogLevel { /** * No logging */ - None, + None = 0, /** * Debug level (internal details are displayed). */ - Debug, + Debug = 1, /** * Info level (only important details are shown) */ - Info, + Info = 2, /** * Warning level */ - Warning, + Warning = 3, /** * Error level. */ - Error -} \ No newline at end of file + Error = 4 +} diff --git a/src/NotationSettings.ts b/src/NotationSettings.ts index 23511565c..e4949d7d2 100644 --- a/src/NotationSettings.ts +++ b/src/NotationSettings.ts @@ -5,19 +5,20 @@ export enum TabRhythmMode { /** * Rhythm notation is hidden. */ - Hidden, + Hidden = 0, /** * Rhythm notation is shown with individual beams per beat. */ - ShowWithBeams, + ShowWithBeams = 1, /** * Rhythm notation is shown and behaves like normal score notation with connected bars. */ - ShowWithBars, + ShowWithBars = 2, /** * Automatic detection whether the tabs should show rhythm based on hidden standard notation. + * @since 1.4.0 */ - Automatic, + Automatic = 3 } /** @@ -27,23 +28,23 @@ export enum FingeringMode { /** * Fingerings will be shown in the standard notation staff. */ - ScoreDefault, + ScoreDefault = 0, /** * Fingerings will be shown in the standard notation staff. Piano finger style is enforced, where * fingers are rendered as 1-5 instead of p,i,m,a,c and T,1,2,3,4. */ - ScoreForcePiano, + ScoreForcePiano = 1, /** * Fingerings will be shown in a effect band above the tabs in case * they have only a single note on the beat. */ - SingleNoteEffectBand, + SingleNoteEffectBand = 2, /** * Fingerings will be shown in a effect band above the tabs in case * they have only a single note on the beat. Piano finger style is enforced, where * fingers are rendered as 1-5 instead of p,i,m,a,c and T,1,2,3,4. */ - SingleNoteEffectBandForcePiano + SingleNoteEffectBandForcePiano = 3 } /** @@ -53,7 +54,7 @@ export enum NotationMode { /** * Music elements will be displayed and played as in Guitar Pro. */ - GuitarPro, + GuitarPro = 0, /** * Music elements will be displayed and played as in traditional songbooks. @@ -72,7 +73,7 @@ export enum NotationMode { * Tied notes with let ring are not shown in standard notation * Let ring does not cause a longer playback, duration is defined via tied notes. */ - SongBook + SongBook = 1 } /** @@ -84,259 +85,259 @@ export enum NotationElement { /** * The score title shown at the start of the music sheet. */ - ScoreTitle, + ScoreTitle = 0, /** * The score subtitle shown at the start of the music sheet. */ - ScoreSubTitle, + ScoreSubTitle = 1, /** * The score artist shown at the start of the music sheet. */ - ScoreArtist, + ScoreArtist = 2, /** * The score album shown at the start of the music sheet. */ - ScoreAlbum, + ScoreAlbum = 3, /** * The score words author shown at the start of the music sheet. */ - ScoreWords, + ScoreWords = 4, /** * The score music author shown at the start of the music sheet. */ - ScoreMusic, + ScoreMusic = 5, /** * The score words&music author shown at the start of the music sheet. */ - ScoreWordsAndMusic, + ScoreWordsAndMusic = 6, /** * The score copyright owner shown at the start of the music sheet. */ - ScoreCopyright, + ScoreCopyright = 7, /** * The tuning information of the guitar shown * above the staves. */ - GuitarTuning, + GuitarTuning = 8, /** * The track names which are shown in the accolade. */ - TrackNames, + TrackNames = 9, /** * The chord diagrams for guitars. Usually shown * below the score info. */ - ChordDiagrams, + ChordDiagrams = 10, /** * Parenthesis that are shown for tied bends * if they are preceeded by bends. */ - ParenthesisOnTiedBends, + ParenthesisOnTiedBends = 11, /** * The tab number for tied notes if the * bend of a note is increased at that point. */ - TabNotesOnTiedBends, + TabNotesOnTiedBends = 12, /** * Zero tab numbers on "dive whammys". */ - ZerosOnDiveWhammys, + ZerosOnDiveWhammys = 13, /** * The alternate endings information on repeats shown above the staff. */ - EffectAlternateEndings, + EffectAlternateEndings = 14, /** * The information about the fret on which the capo is placed shown above the staff. */ - EffectCapo, + EffectCapo = 15, /** * The chord names shown above beats shown above the staff. */ - EffectChordNames, + EffectChordNames = 16, /** * The crescendo/decrescendo angle shown above the staff. */ - EffectCrescendo, + EffectCrescendo = 17, /** * The beat dynamics shown above the staff. */ - EffectDynamics, + EffectDynamics = 18, /** * The curved angle for fade in/out effects shown above the staff. */ - EffectFadeIn, + EffectFadeIn = 19, /** * The fermata symbol shown above the staff. */ - EffectFermata, + EffectFermata = 20, /** * The fingering information. */ - EffectFingering, + EffectFingering = 21, /** * The harmonics names shown above the staff. * (does not represent the harmonic note heads) */ - EffectHarmonics, + EffectHarmonics = 22, /** * The let ring name and line above the staff. */ - EffectLetRing, + EffectLetRing = 23, /** * The lyrics of the track shown above the staff. */ - EffectLyrics, + EffectLyrics = 24, /** * The section markers shown above the staff. */ - EffectMarker, + EffectMarker = 25, /** * The ottava symbol and lines shown above the staff. */ - EffectOttavia, + EffectOttavia = 26, /** * The palm mute name and line shown above the staff. */ - EffectPalmMute, + EffectPalmMute = 27, /** * The pick slide information shown above the staff. * (does not control the pick slide lines) */ - EffectPickSlide, + EffectPickSlide = 28, /** * The pick stroke symbols shown above the staff. */ - EffectPickStroke, + EffectPickStroke = 29, /** * The slight beat vibrato waves shown above the staff. */ - EffectSlightBeatVibrato, + EffectSlightBeatVibrato = 30, /** * The slight note vibrato waves shown above the staff. */ - EffectSlightNoteVibrato, + EffectSlightNoteVibrato = 31, /** * The tap/slap/pop effect names shown above the staff. */ - EffectTap, + EffectTap = 32, /** * The tempo information shown above the staff. */ - EffectTempo, + EffectTempo = 33, /** * The additional beat text shown above the staff. */ - EffectText, + EffectText = 34, /** * The trill name and waves shown above the staff. */ - EffectTrill, + EffectTrill = 35, /** * The triplet feel symbol shown above the staff. */ - EffectTripletFeel, + EffectTripletFeel = 36, /** * The whammy bar information shown above the staff. * (does not control the whammy lines shown within the staff) */ - EffectWhammyBar, + EffectWhammyBar = 37, /** * The wide beat vibrato waves shown above the staff. */ - EffectWideBeatVibrato, + EffectWideBeatVibrato = 38, /** * The wide note vibrato waves shown above the staff. */ - EffectWideNoteVibrato, + EffectWideNoteVibrato = 39, /** * The left hand tap symbol shown above the staff. */ - EffectLeftHandTap, + EffectLeftHandTap = 40, /** * The "Free time" text shown above the staff. */ - EffectFreeTime, + EffectFreeTime = 41, /** * The Sustain pedal effect shown above the staff "Ped.____*" */ - EffectSustainPedal, + EffectSustainPedal = 42, /** * The Golpe effect signs above and below the staff. */ - EffectGolpe, + EffectGolpe = 43, /** * The Wah effect signs above and below the staff. */ - EffectWahPedal, + EffectWahPedal = 44, /** * The Beat barre effect signs above and below the staff "1/2B IV ─────┐" */ - EffectBeatBarre, + EffectBeatBarre = 45, /** * The note ornaments like turns and mordents. */ - EffectNoteOrnament, + EffectNoteOrnament = 46, /** * The Rasgueado indicator above the staff Rasg. ----|" */ - EffectRasgueado, + EffectRasgueado = 47, /** * The directions indicators like coda and segno. */ - EffectDirections, + EffectDirections = 48, /** * The absolute playback time of beats. */ - EffectBeatTimer, + EffectBeatTimer = 49 } /** @@ -346,18 +347,106 @@ export enum NotationElement { */ export class NotationSettings { /** - * Gets or sets the mode to use for display and play music notation elements. + * The mode to use for display and play music notation elements. + * @since 0.9.6 + * @category Notation + * @defaultValue `NotationMode.GuitarPro` + * @remarks + * AlphaTab provides 2 main music notation display modes `GuitarPro` and `SongBook`. + * As the names indicate they adjust the overall music notation rendering either to be more in line how [Arobas Guitar Pro](https://www.guitar-pro.com) displays it, + * or more like the common practice in paper song books practices the display. + * + * The main differences in the Songbook display mode are: + * + * 1. **Bends** + * For bends additional grace beats are introduced. Bends are categorized into gradual and fast bends. + * * Gradual bends are indicated by beat text "grad" or "grad.". Bend will sound along the beat duration. + * * Fast bends are done right before the next note. If the next note is tied even on-beat of the next note. + * 2. **Whammy Bars** + * Dips are shown as simple annotation over the beats. Whammy Bars are categorized into gradual and fast. + * * Gradual whammys are indicated by beat text "grad" or "grad.". Whammys will sound along the beat duration. + * * Fast whammys are done right the beat. + * + * 3. **Let Ring** + * Tied notes with let ring are not shown in standard notation. Let ring does not cause a longer playback, duration is defined via tied notes. + * + * 4. **Settings** + * Following default setting values are applied: + * ```js + * { + * notation: { + * smallGraceTabNotes: false, + * fingeringMode: alphaTab.FingeringMode.SingleNoteEffectBandm + * extendBendArrowsOnTiedNotes: false + * }, + * elements: { + * parenthesisOnTiedBends: false, + * tabNotesOnTiedBends: false, + * zerosOnDiveWhammys: true + * } + * } + * ``` */ public notationMode: NotationMode = NotationMode.GuitarPro; /** - * Gets or sets the fingering mode to use. + * The fingering mode to use. + * @since 0.9.6 + * @category Notation + * @defaultValue `FingeringMode.ScoreDefault` + * @remarks + * AlphaTab supports multiple modes on how to display fingering information in the music sheet. This setting controls how they should be displayed. The default behavior is to show the finger information + * directly in the score along the notes. For some use cases of training courses and for beginners this notation might be hard to read. The effect band mode allows to show a single finger information above the staff. + * + * | Score | Effect Band | + * |-------------------------------------------------------------|-------------------------------------------------------------------| + * | ![Enabled](https://alphatab.net/img/reference/property/fingeringmode-score.png) | ![Disabled](https://alphatab.net/img/reference/property/fingeringmode-effectband.png) | */ public fingeringMode: FingeringMode = FingeringMode.ScoreDefault; /** - * Gets or sets the configuration on whether music notation elements are visible or not. - * If notation elements are not specified, the default configuration will be applied. + * Whether music notation elements are visible or not. + * @since 0.9.8 + * @category Notation + * @defaultValue `[[NotationElement.ZerosOnDiveWhammys, false]]` + * @remarks + * AlphaTab has quite a set of notation elements that are usually shown by default or only shown when using + * the `SongBook` notation mode. This setting allows showing/hiding individual notation elements like the + * song information or the track names. + * + * For each element you can configure whether it is visible or not. The setting is a Map/Dictionary where + * the key is the element to configure and the value is a boolean value whether it should be visible or not. + * @example + * JavaScript + * Internally the setting is a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) where the key must be a {@link NotationElement} enumeration value. + * For JSON input the usual enumeration serialization applies where also the names can be used. The names + * are case insensitive. + * + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'), { + * notation: { + * elements: { + * scoreTitle: false, + * trackNames: false + * } + * } + * }); + * api.settings.notation.elements.set(alphaTab.NotationElement.EffectWhammyBar, false); + * ``` + * @example + * C# + * ```cs + * var settings = new AlphaTab.Settings(); + * settings.Notation.Elements[AlphaTab.NotationElement.ScoreTitle] = false; + * settings.Notation.Elements[AlphaTab.NotationElement.TrackNames] = false; + * ``` + * @example + * Android + * ```kotlin + * val settings = AlphaTab.Settings(); + * settings.notation.elements[alphaTab.NotationElement.ScoreTitle] = false; + * settings.notation.elements[alphaTab.NotationElement.TrackNames] = false; + * ``` */ public elements: Map = new Map(); @@ -371,47 +460,106 @@ export class NotationSettings { ]); /** - * Whether to show rhythm notation in the guitar tablature. + * Controls how the rhythm notation is rendered for tab staves. + * @since 0.9.6 + * @category Notation + * @defaultValue `TabRhythmMode.Automatic` + * @remarks + * This setting enables the display of rhythm notation on tab staffs. [Demo](https://alphatab.net/docs/showcase/guitar-tabs) + * {@since 1.4.0} its automatically detected whether rhythm notation should be shown on tabs (based on the visibility of other staves). */ public rhythmMode: TabRhythmMode = TabRhythmMode.Automatic; /** - * The height of the rythm bars. + * Controls how high the ryhthm notation is rendered below the tab staff + * @since 0.9.6 + * @category Notation + * @defaultValue `15` + * @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; /** - * The transposition pitch offsets for the individual tracks. - * They apply to rendering and playback. + * The transposition pitch offsets for the individual tracks used for rendering and playback. + * @since 0.9.6 + * @category Notation + * @defaultValue `[]` + * @remarks + * This setting allows transposing of tracks for display and playback. + * The `transpositionPitches` setting allows defining an additional pitch offset per track, that is then considered when displaying the music sheet. */ public transpositionPitches: number[] = []; /** - * The transposition pitch offsets for the individual tracks. - * They apply to rendering only. + * The transposition pitch offsets for the individual tracks used for rendering only. + * @since 0.9.6 + * @category Notation + * @defaultValue `[]` + * @remarks + * For some instruments the pitch shown on the standard notation has an additional transposition. One example is the Guitar. + * Notes are shown 1 octave higher than they are on the piano. The following image shows a C4 for a piano and a guitar, and a C5 for the piano as comparison: + * + * ![Display Transposition Pitches example](https://alphatab.net/img/reference/property/displaytranspositionpitches.png) + * + * The `DisplayTranspositionPitch` setting allows defining an additional pitch offset per track, that is then considered when displaying the music sheet. + * This setting does not affect the playback of the instrument in any way. Despite the 2 different standard notations in the above example, they both play the same note height. + * The transposition is defined as number of semitones and one value per track of the song can be defined. */ public displayTranspositionPitches: number[] = []; /** * If set to true the guitar tabs on grace beats are rendered smaller. + * @since 0.9.6 + * @category Notation + * @defaultValue `true` + * @remarks + * By default, grace notes are drawn smaller on the guitar tabs than the other numbers. With this setting alphaTab can be configured to show grace tab notes with normal text size. + * | Enabled | Disabled | + * |--------------------------------------------------------------------|----------------------------------------------------------------------| + * | ![Enabled](https://alphatab.net/img/reference/property/smallgracetabnotes-enabled.png) | ![Disabled](https://alphatab.net/img/reference/property/smallgracetabnotes-disabled.png) | */ public smallGraceTabNotes: boolean = true; /** - * If set to true bend arrows expand to the end of the last tied note - * of the string. Otherwise they end on the next beat. + * If set to true bend arrows expand to the end of the last tied note of the string. Otherwise they end on the next beat. + * @since 0.9.6 + * @category Notation + * @defaultValue `true` + * @remarks + * By default the arrows and lines on bend effects are extended to the space of tied notes. This behavior is the Guitar Pro default but some applications and songbooks practice it different. + * There the bend only is drawn to the next beat. + * | Enabled | Disabled | + * |-----------------------------------------------------------------------------|-------------------------------------------------------------------------------| + * | ![Enabled](https://alphatab.net/img/reference/property/extendbendarrowsontiednotes-enabled.png) | ![Disabled](https://alphatab.net/img/reference/property/extendbendarrowsontiednotes-disabled.png) | */ public extendBendArrowsOnTiedNotes: boolean = true; /** - * If set to true, line effects (like w/bar, let-ring etc) - * are drawn until the end of the beat instead of the start. + * If set to true, line effects like w/bar and let-ring are drawn until the end of the beat instead of the start + * @since 0.9.6 + * @category Notation + * @defaultValue `false` + * @remarks + * By default effect annotations that render a line above the staff, stop on the beat. This is the typical display of Guitar Pro. In songbooks and some other tools + * these effects are drawn to the end of this beat. + * | Enabled | Disabled | + * |-----------------------------------------------------------------------------|-------------------------------------------------------------------------------| + * | ![Enabled](https://alphatab.net/img/reference/property/extendlineeffectstobeatend-enabled.png) | ![Disabled](https://alphatab.net/img/reference/property/extendlineeffectstobeatend-disabled.png) | */ public extendLineEffectsToBeatEnd: boolean = false; /** - * Gets or sets the height for slurs. The factor is multiplied with the a logarithmic distance - * between slur start and end. + * The height scale factor for slurs + * @since 0.9.6 + * @category Notation + * @defaultValue `5` + * @remarks + * Slurs and ties currently calculate their height based on the distance they have from start to end note. Most music notation software do some complex collision detection to avoid a slur to overlap with other elements, alphaTab + * only has a simplified version of the slur positioning as of today. This setting allows adjusting the slur height to avoid collisions. The factor defined by this setting, is multiplied with the logarithmic distance between start and end. + * | Slur Height Default | Slur Height 14 | + * |------------------------------------------------------------------------|--------------------------------------------------------------| + * | ![Slur Height Default](https://alphatab.net/img/reference/property/slurheight-default.png) | ![Slur Height 14](https://alphatab.net/img/reference/property/slurheight-14.png) | */ public slurHeight: number = 5.0; diff --git a/src/PlayerSettings.ts b/src/PlayerSettings.ts index 9176f920f..7ab1384cf 100644 --- a/src/PlayerSettings.ts +++ b/src/PlayerSettings.ts @@ -5,15 +5,15 @@ export enum ScrollMode { /** * Do not scroll automatically */ - Off, + Off = 0, /** * Scrolling happens as soon the offsets of the cursors change. */ - Continuous, + Continuous = 1, /** * Scrolling happens as soon the cursors exceed the displayed range. */ - OffScreen + OffScreen = 2 } /** @@ -23,42 +23,50 @@ export enum ScrollMode { */ export class VibratoPlaybackSettings { /** - * Gets or sets the wavelength of the note-wide vibrato in midi ticks. + * The wavelength of the note-wide vibrato in midi ticks. + * @defaultValue `240` */ public noteWideLength: number = 240; /** - * Gets or sets the amplitude for the note-wide vibrato in semitones. + * The amplitude for the note-wide vibrato in semitones. + * @defaultValue `1` */ public noteWideAmplitude: number = 1; /** - * Gets or sets the wavelength of the note-slight vibrato in midi ticks. + * The wavelength of the note-slight vibrato in midi ticks. + * @defaultValue `360` */ public noteSlightLength: number = 360; /** - * Gets or sets the amplitude for the note-slight vibrato in semitones. + * The amplitude for the note-slight vibrato in semitones. + * @defaultValue `0.5` */ public noteSlightAmplitude: number = 0.5; /** - * Gets or sets the wavelength of the beat-wide vibrato in midi ticks. + * The wavelength of the beat-wide vibrato in midi ticks. + * @defaultValue `480` */ public beatWideLength: number = 480; /** - * Gets or sets the amplitude for the beat-wide vibrato in semitones. + * The amplitude for the beat-wide vibrato in semitones. + * @defaultValue `2` */ public beatWideAmplitude: number = 2; /** - * Gets or sets the wavelength of the beat-slight vibrato in midi ticks. + * The wavelength of the beat-slight vibrato in midi ticks. + * @defaultValue `480` */ public beatSlightLength: number = 480; /** - * Gets or sets the amplitude for the beat-slight vibrato in semitones. + * The amplitude for the beat-slight vibrato in semitones. + * @defaultValue `2` */ public beatSlightAmplitude: number = 2; } @@ -72,22 +80,25 @@ export class SlidePlaybackSettings { /** * Gets or sets 1/4 tones (bend value) offset that * simple slides like slide-out-below or slide-in-above use. + * @defaultValue `6` */ public simpleSlidePitchOffset: number = 6; /** - * Gets or sets the percentage which the simple slides should take up + * The percentage which the simple slides should take up * from the whole note. for "slide into" effects the slide will take place * from time 0 where the note is plucked to 25% of the overall note duration. * For "slide out" effects the slide will start 75% and finish at 100% of the overall * note duration. + * @defaultValue `0.25` */ public simpleSlideDurationRatio: number = 0.25; /** - * Gets or sets the percentage which the legato and shift slides should take up + * The percentage which the legato and shift slides should take up * from the whole note. For a value 0.5 the sliding will start at 50% of the overall note duration * and finish at 100% + * @defaultValue `0.5` */ public shiftSlideDurationRatio: number = 0.5; } @@ -99,13 +110,13 @@ export class SlidePlaybackSettings { export enum PlayerOutputMode { /** * If audio worklets are available in the browser, they will be used for playing the audio. - * It will fallback to the ScriptProcessor output if unavailable. + * It will fallback to the ScriptProcessor output if unavailable. */ - WebAudioAudioWorklets, + WebAudioAudioWorklets = 0, /** * Uses the legacy ScriptProcessor output which might perform worse. */ - WebAudioScriptProcessor + WebAudioScriptProcessor = 1 } /** @@ -115,107 +126,251 @@ export enum PlayerOutputMode { */ export class PlayerSettings { /** - * Gets or sets the URL of the sound font to be loaded. + * The sound font file to load for the player. + * @target web + * @since 0.9.6 + * @defaultValue `null` + * @category Player - JavaScript Specific + * @remarks + * When the player is enabled the soundfont from this URL will be loaded automatically after the player is ready. */ public soundFont: string | null = null; /** - * Gets or sets the element that should be used for scrolling. + * The element to apply the scrolling on. * @target web * @json_read_only + * @json_raw + * @since 0.9.6 + * @defaultValue `html,body` + * @category Player - JavaScript Specific + * @remarks + * When the player is active, it by default automatically scrolls the browser window to the currently played bar. This setting + * defines which elements should be scrolled to bring the played bar into the view port. By default scrolling happens on the `html,body` + * selector. */ public scrollElement: string | HTMLElement = 'html,body'; /** - * Gets or sets which output mode alphaTab should use. + * The mode used for playing audio samples * @target web + * @since 1.3.0 + * @defaultValue `PlayerOutputMode.WebAudioAudioWorklets` + * @category Player - JavaScript Specific + * @remarks + * Controls how alphaTab will play the audio samples in the browser. */ public outputMode: PlayerOutputMode = PlayerOutputMode.WebAudioAudioWorklets; /** - * Gets or sets whether the player should be enabled. + * Whether the player should be enabled. + * @since 0.9.6 + * @defaultValue `false` + * @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. + * 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. + * + * 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. */ public enablePlayer: boolean = false; /** - * Gets or sets whether playback cursors should be displayed. + * Whether playback cursors should be displayed. + * @since 0.9.6 + * @defaultValue `true` + * @category Player + * @remarks + * This setting configures whether the playback cursors are shown or not. In case a developer decides to built an own cursor system the default one can be disabled with this setting. Enabling the cursor also requires the player to be active. */ public enableCursor: boolean = true; /** - * Gets or sets whether the beat cursor should be animated or just ticking. + * Whether the beat cursor should be animated or just ticking. + * @since 1.2.3 + * @defaultValue `true` + * @category Player + * @remarks + * This setting configures whether the beat cursor is animated smoothly or whether it is ticking from beat to beat. + * The animation of the cursor might not be available on all targets so it might not have any effect. */ public enableAnimatedBeatCursor: boolean = true; /** - * Gets or sets whether the notation elements of the currently played beat should be - * highlighted. + * Whether the notation elements of the currently played beat should be highlighted. + * @since 1.2.3 + * @defaultValue `true` + * @category Player + * @remarks + * This setting configures whether the note elements are highlighted during playback. + * The highlighting of elements might not be available on all targets and render engine, so it might not have any effect. */ public enableElementHighlighting: boolean = true; /** - * Gets or sets alphaTab should provide user interaction features to - * select playback ranges and jump to the playback position by click (aka. seeking). + * Whether the default user interaction behavior should be active or not. + * @since 0.9.7 + * @defaultValue `true` + * @category Player + * @remarks + * This setting configures whether alphaTab provides the default user interaction features like selection of the playback range and "seek on click". + * By default users can select the desired playback range with the mouse and also jump to individual beats by click. This behavior can be contolled with this setting. */ public enableUserInteraction: boolean = true; /** - * Gets or sets the X-offset to add when scrolling. + * The X-offset to add when scrolling. + * @since 0.9.6 + * @defaultValue `0` + * @category Player + * @remarks + * When alphaTab does an auto-scrolling to the displayed bar, it will try to align the view port to the displayed bar. If due to + * some layout specifics or for aesthetics a small padding is needed, this setting allows an additional X-offset that is added to the + * scroll position. */ public scrollOffsetX: number = 0; /** - * Gets or sets the Y-offset to add when scrolling + * The Y-offset to add when scrolling. + * @since 0.9.6 + * @defaultValue `0` + * @category Player + * @remarks + * When alphaTab does an auto-scrolling to the displayed bar, it will try to align the view port to the displayed bar. If due to + * some layout specifics or for aesthetics a small padding is needed, this setting allows an additional Y-offset that is added to the + * scroll position. */ public scrollOffsetY: number = 0; /** - * Gets or sets the mode how to scroll. + * The mode how to scroll. + * @since 0.9.6 + * @defaultValue `ScrollMode.Continuous` + * @category Player + * @remarks + * This setting controls how alphaTab behaves for scrolling. */ public scrollMode: ScrollMode = ScrollMode.Continuous; /** - * Gets or sets how fast the scrolling to the new position should happen (in milliseconds) + * How fast the scrolling to the new position should happen. + * @since 0.9.6 + * @defaultValue `300` + * @category Player + * @remarks + * If possible from the platform, alphaTab will try to do a smooth scrolling to the played bar. + * This setting defines the speed of scrolling in milliseconds. + * Note that {@link nativeBrowserSmoothScroll} must be set to `false` for this to have an effect. */ public scrollSpeed: number = 300; /** - * Gets or sets whether the native browser smooth scroll mechanism should be used over a custom animation. + * Whether the native browser smooth scroll mechanism should be used over a custom animation. * @target web + * @since 1.2.3 + * @defaultValue `true` + * @category Player + * @remarks + * This setting configures whether the [native browser feature](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo) + * for smooth scrolling should be used over a custom animation. + * If this setting is enabled, options like {@link scrollSpeed} will not have an effect anymore. */ public nativeBrowserSmoothScroll: boolean = true; /** - * Gets or sets the bend duration in milliseconds for songbook bends. + * The bend duration in milliseconds for songbook bends. + * @since 0.9.6 + * @defaultValue `75` + * @category Player + * @remarks + * If the display mode `songbook` is enabled, this has an effect on the way bends are played. For songbook bends the bend is done very quickly at the end or start of the beat. + * This setting defines the play duration for those bends in milliseconds. This duration is in milliseconds unlike some other settings which are in midi ticks. The reason is that on songbook bends, + * the bends should always be played in the same speed, regardless of the song tempo. Midi ticks are tempo dependent. */ public songBookBendDuration: number = 75; /** - * Gets or sets the duration of whammy dips in milliseconds for songbook whammys. + * The duration of whammy dips in milliseconds for songbook whammys. + * @since 0.9.6 + * @defaultValue `150` + * @category Player + * @remarks + * If the display mode `songbook` is enabled, this has an effect on the way whammy dips are played. For songbook dips the whammy is pressed very quickly at the start of the beat. + * This setting defines the play duration for those whammy bars in milliseconds. This duration is in milliseconds unlike some other settings which are in midi ticks. The reason is that on songbook dips, + * the whammy should always be pressed in the same speed, regardless of the song tempo. Midi ticks are tempo dependent. */ public songBookDipDuration: number = 150; /** - * Gets or sets the settings on how the vibrato audio is generated. + * The Vibrato settings allow control how the different vibrato types are generated for audio. * @json_partial_names + * @since 0.9.6 + * @category Player + * @remarks + * AlphaTab supports 4 types of vibratos, for each vibrato the amplitude and the wavelength can be configured. The amplitude controls how many semitones + * the vibrato changes the pitch up and down while playback. The wavelength controls how many midi ticks it will take to complete one up and down vibrato. + * The 4 vibrato types are: + * + * 1. Beat Slight - A fast vibrato on the whole beat. This vibrato is usually done with the whammy bar. + * 2. Beat Wide - A slow vibrato on the whole beat. This vibrato is usually done with the whammy bar. + * 3. Note Slight - A fast vibrato on a single note. This vibrato is usually done with the finger on the fretboard. + * 4. Note Wide - A slow vibrato on a single note. This vibrato is usually done with the finger on the fretboard. */ public readonly vibrato: VibratoPlaybackSettings = new VibratoPlaybackSettings(); /** - * Gets or sets the setitngs on how the slide audio is generated. + * The slide settings allow control how the different slide types are generated for audio. * @json_partial_names + * @since 0.9.6 + * @domWildcard + * @category Player + * @remarks + * AlphaTab supports various types of slides which can be grouped into 3 types: + * + * * Shift Slides + * * Legato Slides + * + * + * * Slide into from below + * * Slide into from above + * * Slide out to below + * * Slide out to above + * + * + * * Pick Slide out to above + * * Pick Slide out to below + * + * For the first 2 groups the audio generation can be adapted. For the pick slide the audio generation cannot be adapted + * as there is no mechanism yet in alphaTab to play pick slides to make them sound real. + * + * For the first group only the duration or start point of the slide can be configured while for the second group + * the duration/start-point and the pitch offset can be configured. */ public readonly slide: SlidePlaybackSettings = new SlidePlaybackSettings(); /** - * Gets or sets whether the triplet feel should be applied/played during audio playback. + * Whether the triplet feel should be played or only displayed. + * @since 0.9.6 + * @defaultValue `true` + * @category Player + * @remarks + * If this setting is enabled alphaTab will play the triplet feels accordingly, if it is disabled the triplet feel is only displayed but not played. */ public playTripletFeel: boolean = true; /** - * Gets or sets how many milliseconds of audio samples should be buffered in total. - * Larger buffers cause a delay from when audio settings like volumes will be applied. - * Smaller buffers can cause audio crackling due to constant buffering that is happening. - */ - public bufferTimeInMilliseconds:number = 500; + * The number of milliseconds the player should buffer. + * @since 1.2.3 + * @defaultValue `500` + * @category Player + * @remarks + * Gets or sets how many milliseconds of audio samples should be buffered in total. + * + * * Larger buffers cause a delay from when audio settings like volumes will be applied. + * * Smaller buffers can cause audio crackling due to constant buffering that is happening. + * + * This buffer size can be changed whenever needed. + */ + public bufferTimeInMilliseconds: number = 500; } diff --git a/src/RenderingResources.ts b/src/RenderingResources.ts index 6f685c53b..4d1949917 100644 --- a/src/RenderingResources.ts +++ b/src/RenderingResources.ts @@ -1,5 +1,6 @@ import { Color } from '@src/model/Color'; import { Font, FontStyle, FontWeight } from '@src/model/Font'; +import { ScoreSubElement } from '@src/model/Score'; /** * This public class contains central definitions for controlling the visual appearance. @@ -11,112 +12,181 @@ export class RenderingResources { private static serifFont: string = 'Georgia, serif'; /** - * Gets or sets the font to use for displaying the songs copyright information in the header of the music sheet. + * 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 */ public copyrightFont: Font = new Font(RenderingResources.sansFont, 12, FontStyle.Plain, FontWeight.Bold); /** - * Gets or sets the font to use for displaying the songs title in the header of the music sheet. + * The font to use for displaying the songs title in the header of the music sheet. + * @defaultValue `32px Georgia, serif` + * @since 0.9.6 */ public titleFont: Font = new Font(RenderingResources.serifFont, 32, FontStyle.Plain); /** - * Gets or sets the font to use for displaying the songs subtitle in the header of the music sheet. + * The font to use for displaying the songs subtitle in the header of the music sheet. + * @defaultValue `20px Georgia, serif` + * @since 0.9.6 */ public subTitleFont: Font = new Font(RenderingResources.serifFont, 20, FontStyle.Plain); /** - * Gets or sets the font to use for displaying the lyrics information in the header of the music sheet. + * 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 */ public wordsFont: Font = new Font(RenderingResources.serifFont, 15, FontStyle.Plain); /** - * Gets or sets the font to use for displaying certain effect related elements in the music sheet. + * The font to use for displaying certain effect related elements in the music sheet. + * @defaultValue `italic 12px Georgia, serif` + * @since 0.9.6 */ public effectFont: Font = new Font(RenderingResources.serifFont, 12, FontStyle.Italic); /** - * Gets or sets the font to use for displaying beat time information in the music sheet. + * The font to use for displaying beat time information in the music sheet. + * @defaultValue `12px Georgia, serif` + * @since 1.4.0 */ public timerFont: Font = new Font(RenderingResources.serifFont, 12, FontStyle.Plain); /** - * Gets or sets the font to use for displaying the directions texts. + * The font to use for displaying the directions texts. + * @defaultValue `14px Georgia, serif` + * @since 1.4.0 */ public directionsFont: Font = new Font(RenderingResources.serifFont, 14, FontStyle.Plain); /** - * Gets or sets the font to use for displaying the fretboard numbers in chord diagrams. + * The font to use for displaying the fretboard numbers in chord diagrams. + * @defaultValue `11px Arial, sans-serif` + * @since 0.9.6 */ public fretboardNumberFont: Font = new Font(RenderingResources.sansFont, 11, FontStyle.Plain); /** - * Gets or sets the font to use for displaying the numbered music notation in the music sheet. + * The font to use for displaying the numbered music notation in the music sheet. + * @defaultValue `14px Arial, sans-serif` + * @since 1.4.0 */ public numberedNotationFont: Font = new Font(RenderingResources.sansFont, 16, FontStyle.Plain); /** - * Gets or sets the font to use for displaying the grace notes in numbered music notation in the music sheet. + * The font to use for displaying the grace notes in numbered music notation in the music sheet. + * @defaultValue `16px Arial, sans-serif` + * @since 1.4.0 */ public numberedNotationGraceFont: Font = new Font(RenderingResources.sansFont, 14, FontStyle.Plain); /** - * Gets or sets the font to use for displaying the guitar tablature numbers in the music sheet. + * The font to use for displaying the guitar tablature numbers in the music sheet. + * @defaultValue `13px Arial, sans-serif` + * @since 0.9.6 */ public tablatureFont: Font = new Font(RenderingResources.sansFont, 13, FontStyle.Plain); /** - * Gets or sets the font to use for grace notation related texts in the music sheet. + * The font to use for grace notation related texts in the music sheet. + * @defaultValue `11px Arial, sans-serif` + * @since 0.9.6 */ public graceFont: Font = new Font(RenderingResources.sansFont, 11, FontStyle.Plain); /** - * Gets or sets the color to use for rendering the lines of staves. + * The color to use for rendering the lines of staves. + * @defaultValue `rgb(165, 165, 165)` + * @since 0.9.6 */ public staffLineColor: Color = new Color(165, 165, 165, 0xff); /** - * Gets or sets the color to use for rendering bar separators, the accolade and repeat signs. + * The color to use for rendering bar separators, the accolade and repeat signs. + * @defaultValue `rgb(34, 34, 17)` + * @since 0.9.6 */ public barSeparatorColor: Color = new Color(34, 34, 17, 0xff); /** - * Gets or sets the font to use for displaying the bar numbers above the music sheet. + * 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); /** - * Gets or sets the color to use for displaying the bar numbers above the music sheet. + * The color to use for displaying the bar numbers above the music sheet. + * @defaultValue `rgb(200, 0, 0)` + * @since 0.9.6 */ public barNumberColor: Color = new Color(200, 0, 0, 0xff); /** - * Gets or sets the font to use for displaying finger information above the music sheet. + * The font to use for displaying finger information in the music sheet. + * @defaultValue `14px Georgia, serif` + * @since 0.9.6 */ public fingeringFont: Font = new Font(RenderingResources.serifFont, 14, FontStyle.Plain); /** - * Gets or sets the font to use for displaying finger information when inline into the music sheet. + * The font to use for displaying finger information when inline into the music sheet. + * @defaultValue `12px Georgia, serif` + * @since 1.4.0 */ public inlineFingeringFont: Font = new Font(RenderingResources.serifFont, 12, FontStyle.Plain); /** - * Gets or sets the font to use for section marker labels shown above the music sheet. + * 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); /** - * Gets or sets the color to use for music notation elements of the primary voice. + * The color to use for music notation elements of the primary voice. + * @defaultValue `rgb(0, 0, 0)` + * @since 0.9.6 */ public mainGlyphColor: Color = new Color(0, 0, 0, 0xff); /** - * Gets or sets the color to use for music notation elements of the secondary voices. + * The color to use for music notation elements of the secondary voices. + * @defaultValue `rgb(0,0,0,0.4)` + * @since 0.9.6 */ public secondaryGlyphColor: Color = new Color(0, 0, 0, 100); /** - * Gets or sets the color to use for displaying the song information above the music sheet. + * The color to use for displaying the song information above the music sheets. + * @defaultValue `rgb(0, 0, 0)` + * @since 0.9.6 */ public scoreInfoColor: Color = new Color(0, 0, 0, 0xff); + + /** + * @internal + * @param element + */ + public getFontForElement(element: ScoreSubElement): Font { + switch (element) { + case ScoreSubElement.Title: + return this.titleFont; + case ScoreSubElement.SubTitle: + case ScoreSubElement.Artist: + case ScoreSubElement.Album: + return this.subTitleFont; + case ScoreSubElement.Words: + case ScoreSubElement.Music: + case ScoreSubElement.WordsAndMusic: + case ScoreSubElement.Transcriber: + return this.wordsFont; + case ScoreSubElement.Copyright: + case ScoreSubElement.CopyrightSecondLine: + return this.copyrightFont; + } + + return this.wordsFont; + } } diff --git a/src/ResizeEventArgs.ts b/src/ResizeEventArgs.ts index fafdfa0c2..aa5f2a9bf 100644 --- a/src/ResizeEventArgs.ts +++ b/src/ResizeEventArgs.ts @@ -1,4 +1,4 @@ -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { CoreSettings } from '@src/CoreSettings'; /** @@ -20,8 +20,8 @@ export class ResizeEventArgs { */ public settings: Settings | null = null; - public core() : CoreSettings { - if(this.settings && this.causeIssue()) { + public core(): CoreSettings { + if (this.settings && this.causeIssue()) { return this.settings.core; } return new CoreSettings(); diff --git a/src/Settings.ts b/src/Settings.ts index 737d9e2da..eab19fdd6 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -4,7 +4,7 @@ import { ImporterSettings } from '@src/ImporterSettings'; import { FingeringMode, NotationMode, NotationSettings, NotationElement } from '@src/NotationSettings'; import { PlayerSettings } from '@src/PlayerSettings'; import { SettingsSerializer } from '@src/generated/SettingsSerializer'; -import { SettingsJson } from './generated/SettingsJson'; +import type { SettingsJson } from '@src/generated/SettingsJson'; /** * This public class contains instance specific settings for alphaTab @@ -44,7 +44,7 @@ export class Settings { * Contains all player related settings * @json_partial_names */ - public player: PlayerSettings = new PlayerSettings(); + public readonly player: PlayerSettings = new PlayerSettings(); public setSongBookModeSettings(): void { this.notation.notationMode = NotationMode.SongBook; @@ -57,7 +57,7 @@ export class Settings { } public static get songBook(): Settings { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.setSongBookModeSettings(); return settings; } diff --git a/src/StaveProfile.ts b/src/StaveProfile.ts index db91215cd..cb2b9a708 100644 --- a/src/StaveProfile.ts +++ b/src/StaveProfile.ts @@ -5,22 +5,22 @@ export enum StaveProfile { /** * The profile is auto detected by the track configurations. */ - Default, + Default = 0, /** * Standard music notation and guitar tablature are rendered. */ - ScoreTab, + ScoreTab = 1, /** * Only standard music notation is rendered. */ - Score, + Score = 2, /** * Only guitar tablature is rendered. */ - Tab, + Tab = 3, /** * Only guitar tablature is rendered, but also rests and time signatures are not shown. * This profile is typically used in multi-track scenarios. */ - TabMixed + TabMixed = 4 } diff --git a/src/alphaTab.core.ts b/src/alphaTab.core.ts index 34fb2e15e..e1c198c2c 100644 --- a/src/alphaTab.core.ts +++ b/src/alphaTab.core.ts @@ -4,7 +4,13 @@ export { LayoutMode } from '@src/LayoutMode'; export { StaveProfile } from '@src/StaveProfile'; export { ImporterSettings } from '@src/ImporterSettings'; export { FingeringMode, NotationMode, NotationSettings, TabRhythmMode, NotationElement } from '@src/NotationSettings'; -export { PlayerSettings, ScrollMode, VibratoPlaybackSettings, PlayerOutputMode } from '@src/PlayerSettings'; +export { + PlayerSettings, + ScrollMode, + SlidePlaybackSettings, + VibratoPlaybackSettings, + PlayerOutputMode +} from '@src/PlayerSettings'; export { ProgressEventArgs } from '@src/ProgressEventArgs'; export { RenderingResources } from '@src/RenderingResources'; export { ResizeEventArgs } from '@src/ResizeEventArgs'; @@ -15,19 +21,24 @@ export { LogLevel } from '@src/LogLevel'; export { Logger, ConsoleLogger } from '@src/Logger'; export type { ILogger } from '@src/Logger'; export { FileLoadError } from '@src/FileLoadError'; -export { Environment, LayoutEngineFactory, RenderEngineFactory } from '@src/Environment'; +export { Environment, RenderEngineFactory } from '@src/Environment'; export type { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; export { AlphaTabApi } from '@src/platform/javascript/AlphaTabApi'; +export { AlphaTabApiBase } from '@src/AlphaTabApiBase'; export { WebPlatform } from '@src/platform/javascript/WebPlatform'; export { VersionInfo as meta } from '@src/generated/VersionInfo'; -export * as importer from "./importer"; -export * as exporter from "./exporter"; -export * as midi from "./midi"; -export * as model from "./model"; -export * as rendering from "./rendering"; -export * as platform from "./platform"; -export * as synth from "./synth"; -export * as json from './generated/json' +// alphaTab2.0: We should reliminate the big bundles but ship individual +// modules which can be imported. e.g. import { Track } from '@coderline/alphatab/model/Track' +// for this generally some reoganization is likely needed to void circular dependencies better +export * as importer from '@src/importer/_barrel'; +export * as io from '@src/io/_barrel'; +export * as exporter from '@src/exporter/_barrel'; +export * as midi from '@src/midi/_barrel'; +export * as model from '@src/model/_barrel'; +export * as rendering from '@src/rendering/_barrel'; +export * as platform from '@src/platform/_barrel'; +export * as synth from '@src/synth/_barrel'; +export * as json from '@src/generated/_jsonbarrel'; diff --git a/src/alphaTab.main.ts b/src/alphaTab.main.ts index 1c1537a86..b5aa19444 100644 --- a/src/alphaTab.main.ts +++ b/src/alphaTab.main.ts @@ -1,6 +1,6 @@ /**@target web */ -export * from './alphaTab.core'; -import * as alphaTab from './alphaTab.core'; +export * from '@src/alphaTab.core'; +import * as alphaTab from '@src/alphaTab.core'; if (alphaTab.Environment.isRunningInWorker) { alphaTab.Environment.initializeWorker(); @@ -9,7 +9,7 @@ if (alphaTab.Environment.isRunningInWorker) { } else { alphaTab.Environment.initializeMain( settings => { - if (alphaTab.Environment.webPlatform == alphaTab.WebPlatform.NodeJs) { + if (alphaTab.Environment.webPlatform === alphaTab.WebPlatform.NodeJs) { throw new alphaTab.AlphaTabError( alphaTab.AlphaTabErrorType.General, 'Workers not yet supported in Node.js' @@ -17,17 +17,20 @@ if (alphaTab.Environment.isRunningInWorker) { } if ( - alphaTab.Environment.webPlatform == alphaTab.WebPlatform.BrowserModule || + alphaTab.Environment.webPlatform === alphaTab.WebPlatform.BrowserModule || alphaTab.Environment.isWebPackBundled || alphaTab.Environment.isViteBundled ) { alphaTab.Logger.debug('AlphaTab', 'Creating webworker'); try { - return new alphaTab.Environment.alphaTabWorker(new URL('./alphaTab.worker', import.meta.url), { - type: 'module' - }); + return new alphaTab.Environment.alphaTabWorker( + new alphaTab.Environment.alphaTabUrl('./alphaTab.worker.ts', import.meta.url), + { + type: 'module' + } + ); } catch (e) { - alphaTab.Logger.debug('AlphaTab', `ESM webworker construction with direct URL failed`, e); + alphaTab.Logger.debug('AlphaTab', 'ESM webworker construction with direct URL failed', e); // continue with fallbacks } @@ -35,7 +38,7 @@ if (alphaTab.Environment.isRunningInWorker) { let workerUrl: URL | string = ''; try { // Note: prevent bundlers to copy worker as asset via alphaTabUrl - workerUrl = new alphaTab.Environment.alphaTabUrl('./alphaTab.worker', import.meta.url); + workerUrl = new alphaTab.Environment.alphaTabUrl('./alphaTab.worker.ts', import.meta.url); const script: string = `import ${JSON.stringify(workerUrl)}`; const blob: Blob = new Blob([script], { type: 'application/javascript' @@ -46,7 +49,7 @@ if (alphaTab.Environment.isRunningInWorker) { } catch (e) { alphaTab.Logger.debug( 'AlphaTab', - `ESM webworker construction with blob import failed`, + 'ESM webworker construction with blob import failed', workerUrl, e ); @@ -69,7 +72,7 @@ if (alphaTab.Environment.isRunningInWorker) { } catch (e) { alphaTab.Logger.debug( 'AlphaTab', - `ESM webworker construction with blob import failed`, + 'ESM webworker construction with blob import failed', settings.core.scriptFile, e ); @@ -98,7 +101,7 @@ if (alphaTab.Environment.isRunningInWorker) { }, (context, settings) => { - if (alphaTab.Environment.webPlatform == alphaTab.WebPlatform.NodeJs) { + if (alphaTab.Environment.webPlatform === alphaTab.WebPlatform.NodeJs) { throw new alphaTab.AlphaTabError( alphaTab.AlphaTabErrorType.General, 'Audio Worklets not yet supported in Node.js' @@ -106,13 +109,15 @@ if (alphaTab.Environment.isRunningInWorker) { } if ( - alphaTab.Environment.webPlatform == alphaTab.WebPlatform.BrowserModule || + alphaTab.Environment.webPlatform === alphaTab.WebPlatform.BrowserModule || alphaTab.Environment.isWebPackBundled || alphaTab.Environment.isViteBundled ) { alphaTab.Logger.debug('AlphaTab', 'Creating Module worklet'); const alphaTabWorklet = context.audioWorklet; // this name triggers the WebPack Plugin - return alphaTabWorklet.addModule(new URL('./alphaTab.worklet', import.meta.url)); + return alphaTabWorklet.addModule( + new alphaTab.Environment.alphaTabUrl('./alphaTab.worklet.ts', import.meta.url) + ); } alphaTab.Logger.debug('AlphaTab', 'Creating Script worklet'); diff --git a/src/alphaTab.vite.ts b/src/alphaTab.vite.ts index 963360ca0..60ec1fb51 100644 --- a/src/alphaTab.vite.ts +++ b/src/alphaTab.vite.ts @@ -1,2 +1,2 @@ /**@target web */ -export { alphaTab } from './vite/alphaTabVitePlugin'; \ No newline at end of file +export { alphaTab } from '@src/vite/alphaTabVitePlugin'; diff --git a/src/alphaTab.webpack.ts b/src/alphaTab.webpack.ts index 5e06edfac..643af65ca 100644 --- a/src/alphaTab.webpack.ts +++ b/src/alphaTab.webpack.ts @@ -1,2 +1,2 @@ /**@target web */ -export { AlphaTabWebPackPlugin } from './webpack/AlphaTabWebPackPlugin'; \ No newline at end of file +export { AlphaTabWebPackPlugin } from '@src/webpack/AlphaTabWebPackPlugin'; diff --git a/src/alphaTab.worker.ts b/src/alphaTab.worker.ts index 139b73904..4aa8b1110 100644 --- a/src/alphaTab.worker.ts +++ b/src/alphaTab.worker.ts @@ -1,3 +1,3 @@ /**@target web */ -import * as alphaTab from './alphaTab.core'; -alphaTab.Environment.initializeWorker(); \ No newline at end of file +import * as alphaTab from '@src/alphaTab.core'; +alphaTab.Environment.initializeWorker(); diff --git a/src/alphaTab.worklet.ts b/src/alphaTab.worklet.ts index 81a3c97c8..374e476b7 100644 --- a/src/alphaTab.worklet.ts +++ b/src/alphaTab.worklet.ts @@ -1,3 +1,3 @@ /**@target web */ -import * as alphaTab from './alphaTab.core'; -alphaTab.Environment.initializeAudioWorklet(); \ No newline at end of file +import * as alphaTab from '@src/alphaTab.core'; +alphaTab.Environment.initializeAudioWorklet(); diff --git a/src/exporter/Gp7Exporter.ts b/src/exporter/Gp7Exporter.ts index 9ab3085f7..404077be2 100644 --- a/src/exporter/Gp7Exporter.ts +++ b/src/exporter/Gp7Exporter.ts @@ -1,5 +1,5 @@ import { Logger } from '@src/Logger'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { ZipEntry } from '@src/zip/ZipEntry'; import { ScoreExporter } from '@src/exporter/ScoreExporter'; import { GpifWriter } from '@src/exporter//GpifWriter'; @@ -16,10 +16,6 @@ export class Gp7Exporter extends ScoreExporter { return 'Guitar Pro 7'; } - public constructor() { - super(); - } - public writeScore(score: Score): void { Logger.debug(this.name, 'Writing data entries'); const gpifWriter: GpifWriter = new GpifWriter(); @@ -29,7 +25,7 @@ export class Gp7Exporter extends ScoreExporter { const layoutConfiguration = LayoutConfiguration.writeForScore(score); Logger.debug(this.name, 'Writing ZIP entries'); - let fileSystem: ZipWriter = new ZipWriter(this.data); + const fileSystem: ZipWriter = new ZipWriter(this.data); fileSystem.writeEntry(new ZipEntry('VERSION', IOHelper.stringToBytes('7.0'))); fileSystem.writeEntry(new ZipEntry('Content/', new Uint8Array(0))); fileSystem.writeEntry(new ZipEntry('Content/BinaryStylesheet', binaryStylesheet)); diff --git a/src/exporter/GpifWriter.ts b/src/exporter/GpifWriter.ts index 8451f2831..8ba30b144 100644 --- a/src/exporter/GpifWriter.ts +++ b/src/exporter/GpifWriter.ts @@ -2,9 +2,9 @@ import { GeneralMidi } from '@src/midi/GeneralMidi'; import { MidiUtils } from '@src/midi/MidiUtils'; import { AccentuationType } from '@src/model/AccentuationType'; import { AutomationType } from '@src/model/Automation'; -import { Bar, SustainPedalMarkerType } from '@src/model/Bar'; +import { type Bar, SustainPedalMarkerType } from '@src/model/Bar'; import { BarreShape } from '@src/model/BarreShape'; -import { Beat, BeatBeamingMode } from '@src/model/Beat'; +import { type Beat, BeatBeamingMode } from '@src/model/Beat'; import { BendPoint } from '@src/model/BendPoint'; import { BrushType } from '@src/model/BrushType'; import { Clef } from '@src/model/Clef'; @@ -13,33 +13,33 @@ import { Direction } from '@src/model/Direction'; import { Duration } from '@src/model/Duration'; import { DynamicValue } from '@src/model/DynamicValue'; import { FadeType } from '@src/model/FadeType'; -import { Fermata, FermataType } from '@src/model/Fermata'; +import { type Fermata, FermataType } from '@src/model/Fermata'; import { Fingers } from '@src/model/Fingers'; import { GolpeType } from '@src/model/GolpeType'; import { GraceType } from '@src/model/GraceType'; import { HarmonicType } from '@src/model/HarmonicType'; import { KeySignatureType } from '@src/model/KeySignatureType'; import { Lyrics } from '@src/model/Lyrics'; -import { MasterBar } from '@src/model/MasterBar'; +import type { MasterBar } from '@src/model/MasterBar'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { Note } from '@src/model/Note'; +import type { Note } from '@src/model/Note'; import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; import { NoteOrnament } from '@src/model/NoteOrnament'; import { Ottavia } from '@src/model/Ottavia'; import { PercussionMapper } from '@src/model/PercussionMapper'; import { PickStroke } from '@src/model/PickStroke'; -import { PlaybackInformation } from '@src/model/PlaybackInformation'; +import type { PlaybackInformation } from '@src/model/PlaybackInformation'; import { Rasgueado } from '@src/model/Rasgueado'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { SimileMark } from '@src/model/SimileMark'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; -import { Staff } from '@src/model/Staff'; -import { Track } from '@src/model/Track'; +import type { Staff } from '@src/model/Staff'; +import type { Track } from '@src/model/Track'; import { TripletFeel } from '@src/model/TripletFeel'; import { Tuning } from '@src/model/Tuning'; import { VibratoType } from '@src/model/VibratoType'; -import { Voice } from '@src/model/Voice'; +import type { Voice } from '@src/model/Voice'; import { WahPedal } from '@src/model/WahPedal'; import { TextBaseline } from '@src/platform/ICanvas'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; @@ -597,10 +597,10 @@ export class GpifWriter { private writeStandardBend(properties: XmlNode, bendPoints: BendPoint[]) { this.writeSimplePropertyNode(properties, 'Bended', 'Enable', null); - var bendOrigin = bendPoints[0]; - var bendDestination = bendPoints[bendPoints.length - 1]; - var bendMiddle1: BendPoint; - var bendMiddle2: BendPoint; + const bendOrigin = bendPoints[0]; + const bendDestination = bendPoints[bendPoints.length - 1]; + let bendMiddle1: BendPoint; + let bendMiddle2: BendPoint; switch (bendPoints.length) { case 4: @@ -768,7 +768,7 @@ export class GpifWriter { beatNode.addElement('Wah').innerText = WahPedal[beat.wahPedal]; } - if(beat.showTimer) { + if (beat.showTimer) { beatNode.addElement('Timer').innerText = (beat.timer ?? 0).toString(); } @@ -795,16 +795,16 @@ export class GpifWriter { this.writeSimpleXPropertyNode(beatProperties, '687935489', 'Int', beat.brushDuration.toString()); } - switch(beat.beamingMode) { + switch (beat.beamingMode) { case BeatBeamingMode.ForceSplitToNext: - this.writeSimpleXPropertyNode(beatProperties, '1124204546', 'Int', "2"); + this.writeSimpleXPropertyNode(beatProperties, '1124204546', 'Int', '2'); break; case BeatBeamingMode.ForceMergeWithNext: - this.writeSimpleXPropertyNode(beatProperties, '1124204546', 'Int', "1"); + this.writeSimpleXPropertyNode(beatProperties, '1124204546', 'Int', '1'); break; case BeatBeamingMode.ForceSplitOnSecondaryToNext: - this.writeSimpleXPropertyNode(beatProperties, '1124204552', 'Int', "1"); - break; + this.writeSimpleXPropertyNode(beatProperties, '1124204552', 'Int', '1'); + break; } } @@ -858,7 +858,7 @@ export class GpifWriter { } } - if (beat.rasgueado != Rasgueado.None) { + if (beat.rasgueado !== Rasgueado.None) { let rasgueado = ''; switch (beat.rasgueado) { case Rasgueado.Ii: @@ -993,10 +993,10 @@ export class GpifWriter { private writeStandardWhammy(parent: XmlNode, whammyBarPoints: BendPoint[]) { const whammyNode = parent.addElement('Whammy'); - var whammyOrigin = whammyBarPoints[0]; - var whammyDestination = whammyBarPoints[whammyBarPoints.length - 1]; - var whammyMiddle1: BendPoint; - var whammyMiddle2: BendPoint; + const whammyOrigin = whammyBarPoints[0]; + const whammyDestination = whammyBarPoints[whammyBarPoints.length - 1]; + let whammyMiddle1: BendPoint; + let whammyMiddle2: BendPoint; switch (whammyBarPoints.length) { case 4: @@ -1262,10 +1262,10 @@ export class GpifWriter { automation.addElement('Visible').innerText = 'true'; switch (sustainPedal.pedalType) { case SustainPedalMarkerType.Down: - automation.addElement('Value').innerText = `0 1`; + automation.addElement('Value').innerText = '0 1'; break; case SustainPedalMarkerType.Up: - automation.addElement('Value').innerText = `0 3`; + automation.addElement('Value').innerText = '0 3'; break; } } @@ -1323,13 +1323,13 @@ export class GpifWriter { case 4: if (staff.track.playbackInfo.program === 105) { tuningProperty.addElement('Instrument').innerText = 'Banjo'; - } else if (staff.track.playbackInfo.program == 42) { + } else if (staff.track.playbackInfo.program === 42) { tuningProperty.addElement('Instrument').innerText = 'Cello'; - } else if (staff.track.playbackInfo.program == 43) { + } else if (staff.track.playbackInfo.program === 43) { tuningProperty.addElement('Instrument').innerText = 'Contrabass'; - } else if (staff.track.playbackInfo.program == 40) { + } else if (staff.track.playbackInfo.program === 40) { tuningProperty.addElement('Instrument').innerText = 'Violin'; - } else if (staff.track.playbackInfo.program == 41) { + } else if (staff.track.playbackInfo.program === 41) { tuningProperty.addElement('Instrument').innerText = 'Viola'; } else { tuningProperty.addElement('Instrument').innerText = 'Bass'; @@ -1400,7 +1400,7 @@ export class GpifWriter { const fretToStrings = new Map(); for (let i = 0; i < chord.strings.length; i++) { - let chordFret = chord.strings[i]; + const chordFret = chord.strings[i]; if (chordFret !== -1) { const fretNode = diagram.addElement('Fret'); const chordString = chord.strings.length - 1 - i; @@ -1525,7 +1525,7 @@ export class GpifWriter { const lyrics = trackNode.addElement('Lyrics'); lyrics.attributes.set('dispatched', 'true'); - let lines: Lyrics[] = []; + const lines: Lyrics[] = []; for (const bar of track.staves[0].bars) { for (const voice of bar.voices) { @@ -1543,9 +1543,9 @@ export class GpifWriter { const line = lines[l]; line.text = - line.text == '[Empty]' + line.text === '[Empty]' ? beat.lyrics[l] - : line.text + ' ' + beat.lyrics[l].split(' ').join('+'); + : `${line.text} ${beat.lyrics[l].split(' ').join('+')}`; } } } @@ -1585,19 +1585,19 @@ export class GpifWriter { instrumentSet.addElement('Name').innerText = GpifWriter.DrumKitProgramInfo.instrumentSetName; instrumentSet.addElement('Type').innerText = GpifWriter.DrumKitProgramInfo.instrumentSetType; - let currentElementType: string = ''; + const currentElementType: string = ''; let currentElementName: string = ''; let currentArticulations: XmlNode = new XmlNode(); - let counterPerType = new Map(); + const counterPerType = new Map(); const elements = instrumentSet.addElement('Elements'); for (const articulation of articulations) { if (!currentElementType || currentElementType !== articulation.elementType) { - var currentElement = elements.addElement('Element'); + const currentElement = elements.addElement('Element'); let name = articulation.elementType; if (counterPerType.has(name)) { const counter = counterPerType.get(name)!; - name += ' ' + counter; + name += ` ${counter}`; counterPerType.set(name, counter + 1); } else { counterPerType.set(name, 1); @@ -1612,7 +1612,7 @@ export class GpifWriter { const articulationNode = currentArticulations.addElement('Articulation'); articulationNode.addElement('Name').innerText = - currentElementName + ' ' + currentArticulations.childNodes.length; + `${currentElementName} ${currentArticulations.childNodes.length}`; articulationNode.addElement('StaffLine').innerText = articulation.staffLine.toString(); articulationNode.addElement('Noteheads').innerText = [ this.mapMusicSymbol(articulation.noteHeadDefault), @@ -1671,7 +1671,7 @@ export class GpifWriter { if (symbol === MusicFontSymbol.None) { return ''; } - let s = MusicFontSymbol[symbol]; + const s = MusicFontSymbol[symbol]; return s.substring(0, 1).toLowerCase() + s.substring(1); } @@ -1690,15 +1690,14 @@ export class GpifWriter { key.addElement('Mode').innerText = KeySignatureType[masterBar.keySignatureType]; key.addElement('Sharps').innerText = 'Sharps'; - masterBarNode.addElement( - 'Time' - ).innerText = `${masterBar.timeSignatureNumerator}/${masterBar.timeSignatureDenominator}`; + masterBarNode.addElement('Time').innerText = + `${masterBar.timeSignatureNumerator}/${masterBar.timeSignatureDenominator}`; if (masterBar.isFreeTime) { masterBarNode.addElement('FreeTime'); } - let bars: string[] = []; + const bars: string[] = []; for (const tracks of masterBar.score.tracks) { for (const staves of tracks.staves) { bars.push(staves.bars[masterBar.index].id.toString()); @@ -1731,7 +1730,7 @@ export class GpifWriter { const alternateEndings: number[] = []; let bit = 0; while (remainingBits > 0) { - if (((remainingBits >> bit) & 0x01) == 0x01) { + if (((remainingBits >> bit) & 0x01) === 0x01) { alternateEndings.push(bit + 1); // clear bit remainingBits &= ~(1 << bit); diff --git a/src/exporter/ScoreExporter.ts b/src/exporter/ScoreExporter.ts index e6d06971a..018141de3 100644 --- a/src/exporter/ScoreExporter.ts +++ b/src/exporter/ScoreExporter.ts @@ -1,7 +1,7 @@ import { Settings } from '@src/Settings'; import { ByteBuffer } from '@src/io/ByteBuffer'; -import { IWriteable } from '@src/io/IWriteable'; -import { Score } from '@src/model/Score'; +import type { IWriteable } from '@src/io/IWriteable'; +import type { Score } from '@src/model/Score'; /** * This is the base class for creating new song exporters which @@ -20,7 +20,7 @@ export abstract class ScoreExporter { } /** - * Exports the given score to a binary buffer. + * Exports the given score to a binary buffer. * @param score The score to serialize * @param settings The settings to use during serialization * @returns A byte buffer with the serialized score. diff --git a/src/exporter/index.ts b/src/exporter/_barrel.ts similarity index 51% rename from src/exporter/index.ts rename to src/exporter/_barrel.ts index 0480fec7d..dd58f3307 100644 --- a/src/exporter/index.ts +++ b/src/exporter/_barrel.ts @@ -1,2 +1,2 @@ export { ScoreExporter } from '@src/exporter/ScoreExporter'; -export { Gp7Exporter } from '@src/exporter/Gp7Exporter'; +export { Gp7Exporter } from '@src/exporter/Gp7Exporter'; \ No newline at end of file diff --git a/src/generated/CoreSettingsJson.ts b/src/generated/CoreSettingsJson.ts index 37dddcd78..593edf918 100644 --- a/src/generated/CoreSettingsJson.ts +++ b/src/generated/CoreSettingsJson.ts @@ -5,61 +5,151 @@ // import { LogLevel } from "@src/LogLevel"; /** + * All main settings of alphaTab controlling rather general aspects of its behavior. * @json * @json_declaration * @target web */ export interface CoreSettingsJson { /** - * Gets or sets the script file url that will be used to spawn the workers. + * The full URL to the alphaTab JavaScript file. + * @remarks + * AlphaTab needs to know the full URL to the script file it is contained in to launch the web workers. AlphaTab will do its best to auto-detect + * this path but in case it fails, this setting can be used to explicitly define it. Altenatively also a global variable `ALPHATAB_ROOT` can + * be defined before initializing. Please be aware that bundling alphaTab together with other scripts might cause errors + * in case those scripts are not suitable for web workers. e.g. if there is a script bundled together with alphaTab that accesses the DOM, + * this will cause an error when alphaTab starts this script as worker. + * @defaultValue Absolute url to JavaScript file containing alphaTab. (auto detected) + * @category Core - JavaScript Specific * @target web + * @since 0.9.6 */ scriptFile?: string | null; /** - * Gets or sets the url to the fonts that will be used to generate the alphaTab font style. + * The full URL to the alphaTab font directory. + * @remarks + * AlphaTab will generate some dynamic CSS that is needed for displaying the music symbols correctly. For this it needs to know + * where the Web Font files of [Bravura](https://github.com/steinbergmedia/bravura) are. Normally alphaTab expects + * them to be in a `font` subfolder beside the script file. If this is not the case, this setting must be used to configure the path. + * Alternatively also a global variable `ALPHATAB_FONT` can be set on the page before initializing alphaTab. + * @defaultValue `"${AlphaTabScriptFolder}/font/"` + * @category Core - JavaScript Specific * @target web + * @since 0.9.6 */ fontDirectory?: string | null; /** - * Gets or sets the file to load directly after initializing alphaTab. + * The full URL to the input file to be loaded. + * @remarks + * AlphaTab can automatically load and render a file after initialization. This eliminates the need of manually calling + * one of the load methods which are available. alphaTab will automatically initiate an `XMLHttpRequest` after initialization + * to load and display the provided url of this setting. Note that this setting is only interpreted once on initialization. + * @defaultValue `null` + * @category Core - JavaScript Specific * @target web + * @since 0.9.6 */ file?: string | null; /** - * Gets or sets whether the UI element contains alphaTex code that should be - * used to initialize alphaTab. + * Whether the contents of the DOM element should be loaded as alphaTex. * @target web + * @remarks + * This setting allows you to fill alphaTex code into the DOM element and make alphaTab automatically + * load it when initializing. Note that this setting is only interpreted once on initialization. + * @defaultValue `false` + * @category Core - JavaScript Specific + * @since 0.9.6 + * @example + * JavaScript + * ```html + *
\title "Simple alphaTex init" . 3.3*4
+ * + * ``` */ tex?: boolean; /** - * Gets or sets the initial tracks that should be loaded for the score. - * @target web + * The tracks to display for the initally loaded file. * @json_raw + * @remarks + * This setting can be used in combinition with the {@link file} or {@link tex} option. It controls which of the tracks + * of the initially loaded file should be displayed. + * @defaultValue `null` + * @category Core - JavaScript Specific + * @target web + * @since 0.9.6 */ - tracks?: null | number | number[] | "all"; + tracks?: number | number[] | "all" | null; /** - * Gets or sets whether lazy loading for displayed elements is enabled. + * Enables lazy loading of the rendered music sheet chunks. + * @remarks + * AlphaTab renders the music sheet in smaller sub-chunks to have fast UI feedback. Not all of those sub-chunks are immediately + * appended to the DOM due to performance reasons. AlphaTab tries to detect which elements are visible on the screen, and only + * appends those elements to the DOM. This reduces the load of the browser heavily but is not working for all layouts and use cases. + * This setting set to false, ensures that all rendered items are instantly appended to the DOM. + * The lazy rendering of partial might not be available on all platforms. + * @defaultValue `true` + * @category Core + * @since 0.9.6 */ enableLazyLoading?: boolean; /** * The engine which should be used to render the the tablature. - * - * - **default**- Platform specific default engine - * - **html5**- HTML5 Canvas - * - **svg**- SVG + * @remarks + * AlphaTab can use various render engines to draw the music notation. The available render engines is specific to the platform. Please refer to the table below to find out which engines are available on which platform. + * - `default`- Platform specific default engine + * - `html5`- Uses HTML5 canvas elements to render the music notation (browser only) + * - `svg`- Outputs SVG strings (all platforms, default for web) + * - `skia` - Uses [Skia](https://skia.org/) for rendering (all non-browser platforms via [alphaSkia](https://github.com/CoderLine/alphaSkia), default for non-web) + * - `gdi` - Uses [GDI+](https://docs.microsoft.com/en-us/dotnet/framework/winforms/advanced/graphics-and-drawing-in-windows-forms) for rendering (only on .net) + * - `android` - Uses [android.graphics.Canvas](https://developer.android.com/reference/android/graphics/Canvas) for rendering (only on Android) + * @defaultValue `"default"` + * @category Core + * @since 0.9.6 */ engine?: string; /** * The log level to use within alphaTab + * @remarks + * AlphaTab internally does quite a bit of logging for debugging and informational purposes. The log level of alphaTab can be controlled via this setting. + * @defaultValue `LogLevel.Info` + * @category Core + * @since 0.9.6 */ - logLevel?: LogLevel | keyof typeof LogLevel; + logLevel?: LogLevel | keyof typeof LogLevel | Lowercase; /** - * Gets or sets whether the rendering should be done in a worker if possible. + * Whether the rendering should be done in a worker if possible. + * @remarks + * AlphaTab normally tries to render the music sheet asynchronously in a worker. This reduces the load on the UI side and avoids hanging. However sometimes it might be more desirable to have + * a synchronous rendering behavior. This setting can be set to false to synchronously render the music sheet on the UI side. + * @defaultValue `true` + * @category Core + * @since 0.9.6 */ useWorkers?: boolean; /** - * Gets or sets whether in the {@link BoundsLookup} also the - * position and area of each individual note is provided. + * Whether in the {@link BoundsLookup} also the position and area of each individual note is provided. + * @remarks + * AlphaTab collects the position of the rendered music notation elements during the rendering process. This way some level of interactivity can be provided like the feature that seeks to the corresponding position when clicking on a beat. + * By default the position of the individual notes is not collected due to performance reasons. If access to note position information is needed, this setting can enable it. + * @defaultValue `false` + * @category Core + * @since 0.9.6 + * @example + * JavaScript + * ```js + * const settings = new alphaTab.model.Settings(); + * settings.core.includeNoteBounds = true; + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'), settings); + * api.renderFinished.on(() => { + * const lookup = api.renderer.boundsLookup; + * const x = 100; + * const y = 100; + * const beat = lookup.getBeatAtPos(x, y); + * const note = lookup.getNoteAtPos(beat, x, y); + * }); + * ``` */ includeNoteBounds?: boolean; } diff --git a/src/generated/CoreSettingsSerializer.ts b/src/generated/CoreSettingsSerializer.ts index 212fab032..6eb442471 100644 --- a/src/generated/CoreSettingsSerializer.ts +++ b/src/generated/CoreSettingsSerializer.ts @@ -11,7 +11,7 @@ export class CoreSettingsSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + JsonHelper.forEach(m, (v, k) => CoreSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } public static toJson(obj: CoreSettings | null): Map | null { if (!obj) { @@ -55,7 +55,7 @@ export class CoreSettingsSerializer { return true; /*@target web*/ case "tracks": - obj.tracks = v! as number | number[] | "all" | null; + obj.tracks = v as number | number[] | "all" | null; return true; case "enablelazyloading": obj.enableLazyLoading = v! as boolean; diff --git a/src/generated/DisplaySettingsJson.ts b/src/generated/DisplaySettingsJson.ts index 4e49201f3..3fbc49819 100644 --- a/src/generated/DisplaySettingsJson.ts +++ b/src/generated/DisplaySettingsJson.ts @@ -5,7 +5,7 @@ // import { LayoutMode } from "@src/LayoutMode"; import { StaveProfile } from "@src/StaveProfile"; -import { RenderingResourcesJson } from "./RenderingResourcesJson"; +import { RenderingResourcesJson } from "@src/generated/RenderingResourcesJson"; import { SystemsLayoutMode } from "@src/DisplaySettings"; /** * The display settings control how the general layout and display of alphaTab is done. @@ -15,105 +15,311 @@ import { SystemsLayoutMode } from "@src/DisplaySettings"; */ export interface DisplaySettingsJson { /** - * Sets the zoom level of the rendered notation + * The zoom level of the rendered notation. + * @since 0.9.6 + * @category Display + * @defaultValue `1.0` + * @remarks + * AlphaTab can scale up or down the rendered music notation for more optimized display scenarios. By default music notation is rendered at 100% scale (value 1) and can be scaled up or down by + * percental values. */ scale?: number; /** * The default stretch force to use for layouting. + * @since 0.9.6 + * @category Display + * @defaultValue `1` + * @remarks + * The stretch force is a setting that controls the spacing of the music notation. AlphaTab uses a varaint of the Gourlay algorithm for spacing which has springs and rods for + * aligning elements. This setting controls the "strength" of the springs. The stronger the springs, the wider the spacing. + * + * | Force 1 | Force 0.5 | + * |--------------------------------------------------------------|-------------------------------------------------------| + * | ![Default](https://alphatab.net/img/reference/property/stretchforce-default.png) | ![0.5](https://alphatab.net/img/reference/property/stretchforce-half.png) | */ stretchForce?: number; /** * The layouting mode used to arrange the the notation. + * @remarks + * AlphaTab has various layout engines that arrange the rendered bars differently. This setting controls which layout mode is used. + * + * @since 0.9.6 + * @category Display + * @defaultValue `LayoutMode.Page` */ - layoutMode?: LayoutMode | keyof typeof LayoutMode; + layoutMode?: LayoutMode | keyof typeof LayoutMode | Lowercase; /** - * The stave profile to use. + * The stave profile defining which staves are shown for the music sheet. + * @since 0.9.6 + * @category Display + * @defaultValue `StaveProfile.Default` + * @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 */ - staveProfile?: StaveProfile | keyof typeof StaveProfile; + staveProfile?: StaveProfile | keyof typeof StaveProfile | Lowercase; /** - * Limit the displayed bars per row. + * Limit the displayed bars per system (row). (-1 for automatic mode) + * @since 0.9.6 + * @category Display + * @defaultValue `-1` + * @remarks + * This setting sets the number of bars that should be put into one row during layouting. This setting is only respected + * when using the {@link LayoutMode.Page} where bars are aligned in systems. [Demo](https://alphatab.net/docs/showcase/layouts#page-layout-5-bars-per-row). */ barsPerRow?: number; /** - * The bar start number to start layouting with. Note that this is the bar number and not an index! + * The bar start index to start layouting with. + * @since 0.9.6 + * @category Display + * @defaultValue `1` + * @remarks + * This setting sets the index of the first bar that should be rendered from the overall song. This setting can be used to + * achieve a paging system or to only show partial bars of the same file. By this a tutorial alike display can be achieved + * that explains various parts of the song. Please note that this is the bar number as shown in the music sheet (1-based) not the array index (0-based). + * [Demo](https://alphatab.net/docs/showcase/layouts#page-layout-bar-5-to-8) */ startBar?: number; /** - * The amount of bars to render overall. + * The total number of bars that should be rendered from the song. (-1 for all bars) + * @since 0.9.6 + * @category Display + * @defaultValue `-1` + * @remarks + * This setting sets the number of bars that should be rendered from the overall song. This setting can be used to + * achieve a paging system or to only show partial bars of the same file. By this a tutorial alike display can be achieved + * that explains various parts of the song. [Demo](https://alphatab.net/docs/showcase/layouts) */ barCount?: number; /** - * The number of bars that should be rendered per partial. This setting is not used by all layouts. + * The number of bars that should be placed within one partial render. + * @since 0.9.6 + * @category Display + * @defaultValue `10` + * @remarks + * AlphaTab renders the whole music sheet in smaller chunks named "partials". This is to reduce the risk of + * encountering browser performance restrictions and it gives faster visual feedback to the user. This + * setting controls how many bars are placed within such a partial. */ barCountPerPartial?: number; /** - * Whether the last system (row) should be also justified to the whole width of the music sheet. - * (applies only for page layout). + * Whether to justify also the last system in page layouts. + * @remarks + * Setting this option to `true` tells alphaTab to also justify the last system (row) like it + * already does for the systems which are full. + * | Justification Disabled | Justification Enabled | + * |--------------------------------------------------------------|-------------------------------------------------------| + * | ![Disabled](https://alphatab.net/img/reference/property/justify-last-system-false.png) | ![Enabled](https://alphatab.net/img/reference/property/justify-last-system-true.png) | + * @since 1.3.0 + * @category Display + * @defaultValue `false` */ justifyLastSystem?: boolean; /** - * Gets or sets the resources used during rendering. This defines all fonts and colors used. + * Allows adjusting of the used fonts and colors for rendering. * @json_partial_names + * @since 0.9.6 + * @category Display + * @defaultValue `false` + * @domWildcard + * @remarks + * AlphaTab allows configuring the colors and fonts used for rendering via the rendering resources settings. Please note that as of today + * this is the primary way of changing the way how alphaTab styles elements. CSS styling in the browser cannot be guaranteed to work due to its flexibility. + * + * + * Due to space reasons in the following table the common prefix of the settings are removed. Please refer to these examples to eliminate confusion on the usage: + * + * | Platform | Prefix | Example Usage | + * |------------|---------------------------|--------------------------------------------------------------------| + * | JavaScript | `display.resources.` | `settings.display.resources.wordsFont = ...` | + * | JSON | `display.resources.` | `var settings = { display: { resources: { wordsFonts: '...'} } };` | + * | JSON | `resources.` | `var settings = { resources: { wordsFonts: '...'} };` | + * | .net | `Display.Resources.` | `settings.Display.Resources.WordsFonts = ...` | + * | Android | `display.resources.` | `settings.display.resources.wordsFonts = ...` | + * ## Types + * + * ### Fonts + * + * For the JavaScript platform any font that might be installed on the client machines can be used. + * Any additional fonts can be added via WebFonts. The rendering of the score will be delayed until it is detected that the font was loaded. + * Simply use any CSS font property compliant string as configuration. Relative font sizes with percentual values are not supported, remaining values will be considered if supported. + * + * {@since 1.2.3} Multiple fonts are also supported for the Web version. alphaTab will check if any of the fonts in the list is loaded instead of all. If none is available at the time alphaTab is initialized, it will try to initiate the load of the specified fonts individual through the Browser Font APIs. + * + * For the .net platform any installed font on the system can be used. Simply construct the `Font` object to configure your desired fonts. + * + * ### Colors + * + * For JavaScript you can use any CSS font property compliant string. (#RGB, #RGBA, #RRGGBB, #RRGGBBAA, rgb(r,g,b), rgba(r,g,b,a) ) + * + * On .net simply construct the `Color` object to configure your desired color. */ resources?: RenderingResourcesJson; /** - * Gets or sets the padding between the music notation and the border. + * Adjusts the padding between the music notation and the border. + * @remarks + * Adjusts the padding between the music notation and the outer border of the container element. + * The array is either: + * * 2 elements: `[left-right, top-bottom]` + * * 4 elements: ``[left, top, right, bottom]`` + * @since 0.9.6 + * @category Display + * @defaultValue `[35, 35]` */ padding?: number[]; /** - * Gets or sets the top padding applied to first system. + * The top padding applied to first system. + * @since 1.4.0 + * @category Display + * @defaultValue `5` */ firstSystemPaddingTop?: number; /** - * Gets or sets the top padding applied to systems. + * The top padding applied systems beside the first one. + * @since 1.4.0 + * @category Display + * @defaultValue `10` */ systemPaddingTop?: number; /** - * Gets or sets the bottom padding applied to systems. + * The bottom padding applied to systems beside the last one. + * @since 1.4.0 + * @category Display + * @defaultValue `20` */ systemPaddingBottom?: number; /** - * Gets or sets the bottom padding applied to last system. + * The bottom padding applied to the last system. + * @since 1.4.0 + * @category Display + * @defaultValue `0` */ lastSystemPaddingBottom?: number; /** - * Gets or sets the padding left to the track name label of the system. + * The padding left to the track name label of the system. + * @since 1.4.0 + * @category Display + * @defaultValue `0` */ systemLabelPaddingLeft?: number; /** - * Gets or sets the padding right to the track name label of the system. + * The padding left to the track name label of the system. + * @since 1.4.0 + * @category Display + * @defaultValue `3` */ systemLabelPaddingRight?: number; /** - * Gets or sets the padding between the accolade bar and the start of the bar itself. + * The padding between the accolade bar and the start of the bar itself. + * @since 1.4.0 + * @category Display + * @defaultValue `3` */ accoladeBarPaddingRight?: number; /** - * Gets or sets the top padding applied to main notation staffs. + * The bottom padding applied to main notation staves (standard, tabs, numbered, slash). + * @since 1.4.0 + * @category Display + * @defaultValue `5` */ notationStaffPaddingTop?: number; /** - * Gets or sets the bottom padding applied to main notation staffs. + * The bottom padding applied to main notation staves (standard, tabs, numbered, slash). + * @since 1.4.0 + * @category Display + * @defaultValue `5` */ notationStaffPaddingBottom?: number; /** - * Gets or sets the top padding applied to effect annotation staffs. + * The top padding applied to effect annotation staffs. + * @since 1.4.0 + * @category Display + * @defaultValue `0` */ effectStaffPaddingTop?: number; /** - * Gets or sets the bottom padding applied to effect annotation staffs. + * The bottom padding applied to effect annotation staffs. + * @since 1.4.0 + * @category Display + * @defaultValue `0` */ effectStaffPaddingBottom?: number; /** - * Gets or sets the left padding applied between the left line and the first glyph in the first staff in a system. + * The left padding applied between the left line and the first glyph in the first staff in a system. + * @since 1.4.0 + * @category Display + * @defaultValue `6` */ firstStaffPaddingLeft?: number; /** - * Gets or sets the left padding applied between the left line and the first glyph in the following staff in a system. + * The left padding applied between the left line and the first glyph in the following staff in a system. + * @since 1.4.0 + * @category Display + * @defaultValue `2` */ staffPaddingLeft?: number; /** - * Gets how the systems should be layed out. + * The mode used to arrange staves and systems. + * @since 1.3.0 + * @category Display + * @defaultValue `1` + * @remarks + * By default alphaTab uses an own (automatic) mode to arrange and scale the bars when + * putting them into staves. This property allows changing this mode to change the music sheet arrangement. + * + * ## Supported File Formats: + * * Guitar Pro 6-8 {@since 1.3.0} + * If you want/need support for more file formats to respect the sizing information feel free to [open a discussion](https://github.com/CoderLine/alphaTab/discussions/new?category=ideas) on GitHub. + * + * ## Automatic Mode + * + * In the automatic mode alphaTab arranges the bars and staves using its internal mechanisms. + * + * For the `page` layout this means it will scale the bars according to the `stretchForce` and available width. + * Wrapping into new systems (rows) will happen when the row is considered "full". + * + * For the `horizontal` layout the `stretchForce` defines the sizing and no wrapping happens at all. + * + * ## Model Layout mode + * + * File formats like Guitar Pro embed information about the layout in the file and alphaTab can read and use this information. + * When this mode is enabled, alphaTab will also actively use this information and try to respect it. + * + * 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. + * * `score.tracks[index].staves[index].bars[index].displayWidth` - The absolute size of this bar when displayed. + * + * **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. + * * `score.masterBars[index].displayWidth` - Like the `displayWidth` on bar level. + * + * ### Page Layout + * + * The page 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. As indicated above, the scale is rather a ratio than a percentage value but percentages work also: + * + * ![Page Layout](https://alphatab.net/img/reference/property/systems-layout-page-examples.png) + * + * The page layout does not use `displayWidth`. The use of absolute widths would break the proper alignments needed for this kind of display. + * + * Also note that the sizing is including any glyphs and notation elements within the bar. e.g. if there are clefs in the bar, they are still "squeezed" into the available size. + * It is not the case that the actual notes with their lengths are sized accordingly. This fits the sizing system of Guitar Pro and when files are customized there, + * alphaTab will match this layout quite close. + * + * ### Horizontal Layout + * + * The horizontal layout uses the `displayWidth` to scale the bars to size the bars exactly as specified. This kind of sizing and layout can be useful for usecases like: + * + * * 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). */ - systemsLayoutMode?: SystemsLayoutMode | keyof typeof SystemsLayoutMode; + systemsLayoutMode?: SystemsLayoutMode | keyof typeof SystemsLayoutMode | Lowercase; } diff --git a/src/generated/DisplaySettingsSerializer.ts b/src/generated/DisplaySettingsSerializer.ts index 7ed1cfac2..d3191ea05 100644 --- a/src/generated/DisplaySettingsSerializer.ts +++ b/src/generated/DisplaySettingsSerializer.ts @@ -14,7 +14,7 @@ export class DisplaySettingsSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + JsonHelper.forEach(m, (v, k) => DisplaySettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } public static toJson(obj: DisplaySettings | null): Map | null { if (!obj) { @@ -127,12 +127,10 @@ export class DisplaySettingsSerializer { RenderingResourcesSerializer.fromJson(obj.resources, v as Map); return true; } - else { - for (const c of ["resources"]) { - if (property.indexOf(c) === 0) { - if (RenderingResourcesSerializer.setProperty(obj.resources, property.substring(c.length), v)) { - return true; - } + for (const c of ["resources"]) { + if (property.indexOf(c) === 0) { + if (RenderingResourcesSerializer.setProperty(obj.resources, property.substring(c.length), v)) { + return true; } } } diff --git a/src/generated/ImporterSettingsJson.ts b/src/generated/ImporterSettingsJson.ts index 4f74fa7e1..8e7e50653 100644 --- a/src/generated/ImporterSettingsJson.ts +++ b/src/generated/ImporterSettingsJson.ts @@ -11,17 +11,59 @@ */ export interface ImporterSettingsJson { /** - * The text encoding to use when decoding strings. By default UTF-8 is used. + * The text encoding to use when decoding strings. + * @since 0.9.6 + * @defaultValue `utf-8` + * @category Importer + * @remarks + * By default strings are interpreted as UTF-8 from the input files. This is sometimes not the case and leads to strong display + * of strings in the rendered notation. Via this setting the text encoding for decoding the strings can be changed. The supported + * encodings depend on the browser or operating system. This setting is considered for the importers + * + * * Guitar Pro 7 + * * Guitar Pro 6 + * * Guitar Pro 3-5 + * * MusicXML */ encoding?: string; /** - * If part-groups should be merged into a single track. + * If part-groups should be merged into a single track (MusicXML). + * @since 0.9.6 + * @defaultValue `false` + * @category Importer + * @remarks + * This setting controls whether multiple `part-group` tags will result into a single track with multiple staves. */ mergePartGroupsInMusicXml?: boolean; /** - * If set to true, text annotations on beats are attempted to be parsed as - * lyrics considering spaces as separators and removing underscores. - * If a track/staff has explicit lyrics the beat texts will not be detected as lyrics. + * Enables detecting lyrics from beat texts + * @since 1.2.0 + * @category Importer + * @defaultValue `false` + * @remarks + * + * On various old Guitar Pro 3-5 files tab authors often used the "beat text" feature to add lyrics to the individual tracks. + * This was easier and quicker than using the lyrics feature. + * + * These texts were optimized to align correctly when viewed in Guitar Pro with the default layout but can lead to + * disturbed display in alphaTab. When `beatTextAsLyrics` is set to true, alphaTab will try to rather parse beat text + * values as lyrics using typical text patterns like dashes, underscores and spaces. + * + * The lyrics are only detected if not already proper lyrics are applied to the track. + * + * Enable this option for input files which suffer from this practice. + * + * > [!NOTE] + * > alphaTab tries to relate the texts and chunks to the beats but this is not perfect. + * > Errors are likely to happen with such kind of files. + * + * **Enabled** + * + * ![Enabled](https://alphatab.net/img/reference/property/beattextaslyrics-enabled.png) + * + * **Disabled** + * + * ![Disabled](https://alphatab.net/img/reference/property/beattextaslyrics-disabled.png) */ beatTextAsLyrics?: boolean; } diff --git a/src/generated/ImporterSettingsSerializer.ts b/src/generated/ImporterSettingsSerializer.ts index 0dfd683b2..46fc214b6 100644 --- a/src/generated/ImporterSettingsSerializer.ts +++ b/src/generated/ImporterSettingsSerializer.ts @@ -10,7 +10,7 @@ export class ImporterSettingsSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + JsonHelper.forEach(m, (v, k) => ImporterSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } public static toJson(obj: ImporterSettings | null): Map | null { if (!obj) { diff --git a/src/generated/NotationSettingsJson.ts b/src/generated/NotationSettingsJson.ts index d821a5bfe..86000ccb9 100644 --- a/src/generated/NotationSettingsJson.ts +++ b/src/generated/NotationSettingsJson.ts @@ -15,53 +15,200 @@ import { TabRhythmMode } from "@src/NotationSettings"; */ export interface NotationSettingsJson { /** - * Gets or sets the mode to use for display and play music notation elements. + * The mode to use for display and play music notation elements. + * @since 0.9.6 + * @category Notation + * @defaultValue `NotationMode.GuitarPro` + * @remarks + * AlphaTab provides 2 main music notation display modes `GuitarPro` and `SongBook`. + * As the names indicate they adjust the overall music notation rendering either to be more in line how [Arobas Guitar Pro](https://www.guitar-pro.com) displays it, + * or more like the common practice in paper song books practices the display. + * + * The main differences in the Songbook display mode are: + * + * 1. **Bends** + * For bends additional grace beats are introduced. Bends are categorized into gradual and fast bends. + * * Gradual bends are indicated by beat text "grad" or "grad.". Bend will sound along the beat duration. + * * Fast bends are done right before the next note. If the next note is tied even on-beat of the next note. + * 2. **Whammy Bars** + * Dips are shown as simple annotation over the beats. Whammy Bars are categorized into gradual and fast. + * * Gradual whammys are indicated by beat text "grad" or "grad.". Whammys will sound along the beat duration. + * * Fast whammys are done right the beat. + * + * 3. **Let Ring** + * Tied notes with let ring are not shown in standard notation. Let ring does not cause a longer playback, duration is defined via tied notes. + * + * 4. **Settings** + * Following default setting values are applied: + * ```js + * { + * notation: { + * smallGraceTabNotes: false, + * fingeringMode: alphaTab.FingeringMode.SingleNoteEffectBandm + * extendBendArrowsOnTiedNotes: false + * }, + * elements: { + * parenthesisOnTiedBends: false, + * tabNotesOnTiedBends: false, + * zerosOnDiveWhammys: true + * } + * } + * ``` */ - notationMode?: NotationMode | keyof typeof NotationMode; + notationMode?: NotationMode | keyof typeof NotationMode | Lowercase; /** - * Gets or sets the fingering mode to use. + * The fingering mode to use. + * @since 0.9.6 + * @category Notation + * @defaultValue `FingeringMode.ScoreDefault` + * @remarks + * AlphaTab supports multiple modes on how to display fingering information in the music sheet. This setting controls how they should be displayed. The default behavior is to show the finger information + * directly in the score along the notes. For some use cases of training courses and for beginners this notation might be hard to read. The effect band mode allows to show a single finger information above the staff. + * + * | Score | Effect Band | + * |-------------------------------------------------------------|-------------------------------------------------------------------| + * | ![Enabled](https://alphatab.net/img/reference/property/fingeringmode-score.png) | ![Disabled](https://alphatab.net/img/reference/property/fingeringmode-effectband.png) | */ - fingeringMode?: FingeringMode | keyof typeof FingeringMode; + fingeringMode?: FingeringMode | keyof typeof FingeringMode | Lowercase; /** - * Gets or sets the configuration on whether music notation elements are visible or not. - * If notation elements are not specified, the default configuration will be applied. + * Whether music notation elements are visible or not. + * @since 0.9.8 + * @category Notation + * @defaultValue `[[NotationElement.ZerosOnDiveWhammys, false]]` + * @remarks + * AlphaTab has quite a set of notation elements that are usually shown by default or only shown when using + * the `SongBook` notation mode. This setting allows showing/hiding individual notation elements like the + * song information or the track names. + * + * For each element you can configure whether it is visible or not. The setting is a Map/Dictionary where + * the key is the element to configure and the value is a boolean value whether it should be visible or not. + * @example + * JavaScript + * Internally the setting is a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) where the key must be a {@link NotationElement} enumeration value. + * For JSON input the usual enumeration serialization applies where also the names can be used. The names + * are case insensitive. + * + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'), { + * notation: { + * elements: { + * scoreTitle: false, + * trackNames: false + * } + * } + * }); + * api.settings.notation.elements.set(alphaTab.NotationElement.EffectWhammyBar, false); + * ``` + * @example + * C# + * ```cs + * var settings = new AlphaTab.Settings(); + * settings.Notation.Elements[AlphaTab.NotationElement.ScoreTitle] = false; + * settings.Notation.Elements[AlphaTab.NotationElement.TrackNames] = false; + * ``` + * @example + * Android + * ```kotlin + * val settings = AlphaTab.Settings(); + * settings.notation.elements[alphaTab.NotationElement.ScoreTitle] = false; + * settings.notation.elements[alphaTab.NotationElement.TrackNames] = false; + * ``` */ - elements?: Map; + elements?: Map, boolean>; /** - * Whether to show rhythm notation in the guitar tablature. + * Controls how the rhythm notation is rendered for tab staves. + * @since 0.9.6 + * @category Notation + * @defaultValue `TabRhythmMode.Automatic` + * @remarks + * This setting enables the display of rhythm notation on tab staffs. [Demo](https://alphatab.net/docs/showcase/guitar-tabs) + * {@since 1.4.0} its automatically detected whether rhythm notation should be shown on tabs (based on the visibility of other staves). */ - rhythmMode?: TabRhythmMode | keyof typeof TabRhythmMode; + rhythmMode?: TabRhythmMode | keyof typeof TabRhythmMode | Lowercase; /** - * The height of the rythm bars. + * Controls how high the ryhthm notation is rendered below the tab staff + * @since 0.9.6 + * @category Notation + * @defaultValue `15` + * @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. */ rhythmHeight?: number; /** - * The transposition pitch offsets for the individual tracks. - * They apply to rendering and playback. + * The transposition pitch offsets for the individual tracks used for rendering and playback. + * @since 0.9.6 + * @category Notation + * @defaultValue `[]` + * @remarks + * This setting allows transposing of tracks for display and playback. + * The `transpositionPitches` setting allows defining an additional pitch offset per track, that is then considered when displaying the music sheet. */ transpositionPitches?: number[]; /** - * The transposition pitch offsets for the individual tracks. - * They apply to rendering only. + * The transposition pitch offsets for the individual tracks used for rendering only. + * @since 0.9.6 + * @category Notation + * @defaultValue `[]` + * @remarks + * For some instruments the pitch shown on the standard notation has an additional transposition. One example is the Guitar. + * Notes are shown 1 octave higher than they are on the piano. The following image shows a C4 for a piano and a guitar, and a C5 for the piano as comparison: + * + * ![Display Transposition Pitches example](https://alphatab.net/img/reference/property/displaytranspositionpitches.png) + * + * The `DisplayTranspositionPitch` setting allows defining an additional pitch offset per track, that is then considered when displaying the music sheet. + * This setting does not affect the playback of the instrument in any way. Despite the 2 different standard notations in the above example, they both play the same note height. + * The transposition is defined as number of semitones and one value per track of the song can be defined. */ displayTranspositionPitches?: number[]; /** * If set to true the guitar tabs on grace beats are rendered smaller. + * @since 0.9.6 + * @category Notation + * @defaultValue `true` + * @remarks + * By default, grace notes are drawn smaller on the guitar tabs than the other numbers. With this setting alphaTab can be configured to show grace tab notes with normal text size. + * | Enabled | Disabled | + * |--------------------------------------------------------------------|----------------------------------------------------------------------| + * | ![Enabled](https://alphatab.net/img/reference/property/smallgracetabnotes-enabled.png) | ![Disabled](https://alphatab.net/img/reference/property/smallgracetabnotes-disabled.png) | */ smallGraceTabNotes?: boolean; /** - * If set to true bend arrows expand to the end of the last tied note - * of the string. Otherwise they end on the next beat. + * If set to true bend arrows expand to the end of the last tied note of the string. Otherwise they end on the next beat. + * @since 0.9.6 + * @category Notation + * @defaultValue `true` + * @remarks + * By default the arrows and lines on bend effects are extended to the space of tied notes. This behavior is the Guitar Pro default but some applications and songbooks practice it different. + * There the bend only is drawn to the next beat. + * | Enabled | Disabled | + * |-----------------------------------------------------------------------------|-------------------------------------------------------------------------------| + * | ![Enabled](https://alphatab.net/img/reference/property/extendbendarrowsontiednotes-enabled.png) | ![Disabled](https://alphatab.net/img/reference/property/extendbendarrowsontiednotes-disabled.png) | */ extendBendArrowsOnTiedNotes?: boolean; /** - * If set to true, line effects (like w/bar, let-ring etc) - * are drawn until the end of the beat instead of the start. + * If set to true, line effects like w/bar and let-ring are drawn until the end of the beat instead of the start + * @since 0.9.6 + * @category Notation + * @defaultValue `false` + * @remarks + * By default effect annotations that render a line above the staff, stop on the beat. This is the typical display of Guitar Pro. In songbooks and some other tools + * these effects are drawn to the end of this beat. + * | Enabled | Disabled | + * |-----------------------------------------------------------------------------|-------------------------------------------------------------------------------| + * | ![Enabled](https://alphatab.net/img/reference/property/extendlineeffectstobeatend-enabled.png) | ![Disabled](https://alphatab.net/img/reference/property/extendlineeffectstobeatend-disabled.png) | */ extendLineEffectsToBeatEnd?: boolean; /** - * Gets or sets the height for slurs. The factor is multiplied with the a logarithmic distance - * between slur start and end. + * The height scale factor for slurs + * @since 0.9.6 + * @category Notation + * @defaultValue `5` + * @remarks + * Slurs and ties currently calculate their height based on the distance they have from start to end note. Most music notation software do some complex collision detection to avoid a slur to overlap with other elements, alphaTab + * only has a simplified version of the slur positioning as of today. This setting allows adjusting the slur height to avoid collisions. The factor defined by this setting, is multiplied with the logarithmic distance between start and end. + * | Slur Height Default | Slur Height 14 | + * |------------------------------------------------------------------------|--------------------------------------------------------------| + * | ![Slur Height Default](https://alphatab.net/img/reference/property/slurheight-default.png) | ![Slur Height 14](https://alphatab.net/img/reference/property/slurheight-14.png) | */ slurHeight?: number; } diff --git a/src/generated/NotationSettingsSerializer.ts b/src/generated/NotationSettingsSerializer.ts index 25221c46b..936bcd270 100644 --- a/src/generated/NotationSettingsSerializer.ts +++ b/src/generated/NotationSettingsSerializer.ts @@ -14,7 +14,7 @@ export class NotationSettingsSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + JsonHelper.forEach(m, (v, k) => NotationSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } public static toJson(obj: NotationSettings | null): Map | null { if (!obj) { diff --git a/src/generated/PlayerSettingsJson.ts b/src/generated/PlayerSettingsJson.ts index 11a227a15..1f9185c46 100644 --- a/src/generated/PlayerSettingsJson.ts +++ b/src/generated/PlayerSettingsJson.ts @@ -5,8 +5,8 @@ // import { PlayerOutputMode } from "@src/PlayerSettings"; import { ScrollMode } from "@src/PlayerSettings"; -import { VibratoPlaybackSettingsJson } from "./VibratoPlaybackSettingsJson"; -import { SlidePlaybackSettingsJson } from "./SlidePlaybackSettingsJson"; +import { VibratoPlaybackSettingsJson } from "@src/generated/VibratoPlaybackSettingsJson"; +import { SlidePlaybackSettingsJson } from "@src/generated/SlidePlaybackSettingsJson"; /** * The player settings control how the audio playback and UI is behaving. * @json @@ -15,89 +15,233 @@ import { SlidePlaybackSettingsJson } from "./SlidePlaybackSettingsJson"; */ export interface PlayerSettingsJson { /** - * Gets or sets the URL of the sound font to be loaded. + * The sound font file to load for the player. + * @target web + * @since 0.9.6 + * @defaultValue `null` + * @category Player - JavaScript Specific + * @remarks + * When the player is enabled the soundfont from this URL will be loaded automatically after the player is ready. */ soundFont?: string | null; /** - * Gets or sets the element that should be used for scrolling. + * The element to apply the scrolling on. * @target web * @json_read_only + * @json_raw + * @since 0.9.6 + * @defaultValue `html,body` + * @category Player - JavaScript Specific + * @remarks + * When the player is active, it by default automatically scrolls the browser window to the currently played bar. This setting + * defines which elements should be scrolled to bring the played bar into the view port. By default scrolling happens on the `html,body` + * selector. */ scrollElement?: string | HTMLElement; /** - * Gets or sets which output mode alphaTab should use. + * The mode used for playing audio samples * @target web - */ - outputMode?: PlayerOutputMode | keyof typeof PlayerOutputMode; - /** - * Gets or sets whether the player should be enabled. + * @since 1.3.0 + * @defaultValue `PlayerOutputMode.WebAudioAudioWorklets` + * @category Player - JavaScript Specific + * @remarks + * Controls how alphaTab will play the audio samples in the browser. + */ + outputMode?: PlayerOutputMode | keyof typeof PlayerOutputMode | Lowercase; + /** + * Whether the player should be enabled. + * @since 0.9.6 + * @defaultValue `false` + * @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. + * 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. + * + * 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. */ enablePlayer?: boolean; /** - * Gets or sets whether playback cursors should be displayed. + * Whether playback cursors should be displayed. + * @since 0.9.6 + * @defaultValue `true` + * @category Player + * @remarks + * This setting configures whether the playback cursors are shown or not. In case a developer decides to built an own cursor system the default one can be disabled with this setting. Enabling the cursor also requires the player to be active. */ enableCursor?: boolean; /** - * Gets or sets whether the beat cursor should be animated or just ticking. + * Whether the beat cursor should be animated or just ticking. + * @since 1.2.3 + * @defaultValue `true` + * @category Player + * @remarks + * This setting configures whether the beat cursor is animated smoothly or whether it is ticking from beat to beat. + * The animation of the cursor might not be available on all targets so it might not have any effect. */ enableAnimatedBeatCursor?: boolean; /** - * Gets or sets whether the notation elements of the currently played beat should be - * highlighted. + * Whether the notation elements of the currently played beat should be highlighted. + * @since 1.2.3 + * @defaultValue `true` + * @category Player + * @remarks + * This setting configures whether the note elements are highlighted during playback. + * The highlighting of elements might not be available on all targets and render engine, so it might not have any effect. */ enableElementHighlighting?: boolean; /** - * Gets or sets alphaTab should provide user interaction features to - * select playback ranges and jump to the playback position by click (aka. seeking). + * Whether the default user interaction behavior should be active or not. + * @since 0.9.7 + * @defaultValue `true` + * @category Player + * @remarks + * This setting configures whether alphaTab provides the default user interaction features like selection of the playback range and "seek on click". + * By default users can select the desired playback range with the mouse and also jump to individual beats by click. This behavior can be contolled with this setting. */ enableUserInteraction?: boolean; /** - * Gets or sets the X-offset to add when scrolling. + * The X-offset to add when scrolling. + * @since 0.9.6 + * @defaultValue `0` + * @category Player + * @remarks + * When alphaTab does an auto-scrolling to the displayed bar, it will try to align the view port to the displayed bar. If due to + * some layout specifics or for aesthetics a small padding is needed, this setting allows an additional X-offset that is added to the + * scroll position. */ scrollOffsetX?: number; /** - * Gets or sets the Y-offset to add when scrolling + * The Y-offset to add when scrolling. + * @since 0.9.6 + * @defaultValue `0` + * @category Player + * @remarks + * When alphaTab does an auto-scrolling to the displayed bar, it will try to align the view port to the displayed bar. If due to + * some layout specifics or for aesthetics a small padding is needed, this setting allows an additional Y-offset that is added to the + * scroll position. */ scrollOffsetY?: number; /** - * Gets or sets the mode how to scroll. + * The mode how to scroll. + * @since 0.9.6 + * @defaultValue `ScrollMode.Continuous` + * @category Player + * @remarks + * This setting controls how alphaTab behaves for scrolling. */ - scrollMode?: ScrollMode | keyof typeof ScrollMode; + scrollMode?: ScrollMode | keyof typeof ScrollMode | Lowercase; /** - * Gets or sets how fast the scrolling to the new position should happen (in milliseconds) + * How fast the scrolling to the new position should happen. + * @since 0.9.6 + * @defaultValue `300` + * @category Player + * @remarks + * If possible from the platform, alphaTab will try to do a smooth scrolling to the played bar. + * This setting defines the speed of scrolling in milliseconds. + * Note that {@link nativeBrowserSmoothScroll} must be set to `false` for this to have an effect. */ scrollSpeed?: number; /** - * Gets or sets whether the native browser smooth scroll mechanism should be used over a custom animation. + * Whether the native browser smooth scroll mechanism should be used over a custom animation. * @target web + * @since 1.2.3 + * @defaultValue `true` + * @category Player + * @remarks + * This setting configures whether the [native browser feature](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo) + * for smooth scrolling should be used over a custom animation. + * If this setting is enabled, options like {@link scrollSpeed} will not have an effect anymore. */ nativeBrowserSmoothScroll?: boolean; /** - * Gets or sets the bend duration in milliseconds for songbook bends. + * The bend duration in milliseconds for songbook bends. + * @since 0.9.6 + * @defaultValue `75` + * @category Player + * @remarks + * If the display mode `songbook` is enabled, this has an effect on the way bends are played. For songbook bends the bend is done very quickly at the end or start of the beat. + * This setting defines the play duration for those bends in milliseconds. This duration is in milliseconds unlike some other settings which are in midi ticks. The reason is that on songbook bends, + * the bends should always be played in the same speed, regardless of the song tempo. Midi ticks are tempo dependent. */ songBookBendDuration?: number; /** - * Gets or sets the duration of whammy dips in milliseconds for songbook whammys. + * The duration of whammy dips in milliseconds for songbook whammys. + * @since 0.9.6 + * @defaultValue `150` + * @category Player + * @remarks + * If the display mode `songbook` is enabled, this has an effect on the way whammy dips are played. For songbook dips the whammy is pressed very quickly at the start of the beat. + * This setting defines the play duration for those whammy bars in milliseconds. This duration is in milliseconds unlike some other settings which are in midi ticks. The reason is that on songbook dips, + * the whammy should always be pressed in the same speed, regardless of the song tempo. Midi ticks are tempo dependent. */ songBookDipDuration?: number; /** - * Gets or sets the settings on how the vibrato audio is generated. + * The Vibrato settings allow control how the different vibrato types are generated for audio. * @json_partial_names + * @since 0.9.6 + * @category Player + * @remarks + * AlphaTab supports 4 types of vibratos, for each vibrato the amplitude and the wavelength can be configured. The amplitude controls how many semitones + * the vibrato changes the pitch up and down while playback. The wavelength controls how many midi ticks it will take to complete one up and down vibrato. + * The 4 vibrato types are: + * + * 1. Beat Slight - A fast vibrato on the whole beat. This vibrato is usually done with the whammy bar. + * 2. Beat Wide - A slow vibrato on the whole beat. This vibrato is usually done with the whammy bar. + * 3. Note Slight - A fast vibrato on a single note. This vibrato is usually done with the finger on the fretboard. + * 4. Note Wide - A slow vibrato on a single note. This vibrato is usually done with the finger on the fretboard. */ vibrato?: VibratoPlaybackSettingsJson; /** - * Gets or sets the setitngs on how the slide audio is generated. + * The slide settings allow control how the different slide types are generated for audio. * @json_partial_names + * @since 0.9.6 + * @domWildcard + * @category Player + * @remarks + * AlphaTab supports various types of slides which can be grouped into 3 types: + * + * * Shift Slides + * * Legato Slides + * + * + * * Slide into from below + * * Slide into from above + * * Slide out to below + * * Slide out to above + * + * + * * Pick Slide out to above + * * Pick Slide out to below + * + * For the first 2 groups the audio generation can be adapted. For the pick slide the audio generation cannot be adapted + * as there is no mechanism yet in alphaTab to play pick slides to make them sound real. + * + * For the first group only the duration or start point of the slide can be configured while for the second group + * the duration/start-point and the pitch offset can be configured. */ slide?: SlidePlaybackSettingsJson; /** - * Gets or sets whether the triplet feel should be applied/played during audio playback. + * Whether the triplet feel should be played or only displayed. + * @since 0.9.6 + * @defaultValue `true` + * @category Player + * @remarks + * If this setting is enabled alphaTab will play the triplet feels accordingly, if it is disabled the triplet feel is only displayed but not played. */ playTripletFeel?: boolean; /** + * The number of milliseconds the player should buffer. + * @since 1.2.3 + * @defaultValue `500` + * @category Player + * @remarks * Gets or sets how many milliseconds of audio samples should be buffered in total. - * Larger buffers cause a delay from when audio settings like volumes will be applied. - * Smaller buffers can cause audio crackling due to constant buffering that is happening. + * + * * Larger buffers cause a delay from when audio settings like volumes will be applied. + * * Smaller buffers can cause audio crackling due to constant buffering that is happening. + * + * This buffer size can be changed whenever needed. */ bufferTimeInMilliseconds?: number; } diff --git a/src/generated/PlayerSettingsSerializer.ts b/src/generated/PlayerSettingsSerializer.ts index aec122f5d..441c4611f 100644 --- a/src/generated/PlayerSettingsSerializer.ts +++ b/src/generated/PlayerSettingsSerializer.ts @@ -14,13 +14,14 @@ export class PlayerSettingsSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + JsonHelper.forEach(m, (v, k) => PlayerSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } public static toJson(obj: PlayerSettings | null): Map | null { if (!obj) { return null; } const o = new Map(); + /*@target web*/ o.set("soundfont", obj.soundFont); /*@target web*/ o.set("outputmode", obj.outputMode as number); @@ -45,6 +46,7 @@ export class PlayerSettingsSerializer { } public static setProperty(obj: PlayerSettings, property: string, v: unknown): boolean { switch (property) { + /*@target web*/ case "soundfont": obj.soundFont = v as string | null; return true; @@ -104,12 +106,10 @@ export class PlayerSettingsSerializer { VibratoPlaybackSettingsSerializer.fromJson(obj.vibrato, v as Map); return true; } - else { - for (const c of ["vibrato"]) { - if (property.indexOf(c) === 0) { - if (VibratoPlaybackSettingsSerializer.setProperty(obj.vibrato, property.substring(c.length), v)) { - return true; - } + for (const c of ["vibrato"]) { + if (property.indexOf(c) === 0) { + if (VibratoPlaybackSettingsSerializer.setProperty(obj.vibrato, property.substring(c.length), v)) { + return true; } } } @@ -117,12 +117,10 @@ export class PlayerSettingsSerializer { SlidePlaybackSettingsSerializer.fromJson(obj.slide, v as Map); return true; } - else { - for (const c of ["slide"]) { - if (property.indexOf(c) === 0) { - if (SlidePlaybackSettingsSerializer.setProperty(obj.slide, property.substring(c.length), v)) { - return true; - } + for (const c of ["slide"]) { + if (property.indexOf(c) === 0) { + if (SlidePlaybackSettingsSerializer.setProperty(obj.slide, property.substring(c.length), v)) { + return true; } } } diff --git a/src/generated/RenderingResourcesJson.ts b/src/generated/RenderingResourcesJson.ts index ecefdcd53..21d3affe0 100644 --- a/src/generated/RenderingResourcesJson.ts +++ b/src/generated/RenderingResourcesJson.ts @@ -13,91 +13,135 @@ import { ColorJson } from "@src/model/Color"; */ export interface RenderingResourcesJson { /** - * Gets or sets the font to use for displaying the songs copyright information in the header of the music sheet. + * 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; /** - * Gets or sets the font to use for displaying the songs title in the header of the music sheet. + * 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; /** - * Gets or sets the font to use for displaying the songs subtitle in the header of the music sheet. + * The font to use for displaying the songs subtitle in the header of the music sheet. + * @defaultValue `20px Georgia, serif` + * @since 0.9.6 */ subTitleFont?: FontJson; /** - * Gets or sets the font to use for displaying the lyrics information in the header of the music sheet. + * 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 */ wordsFont?: FontJson; /** - * Gets or sets the font to use for displaying certain effect related elements in the music sheet. + * The font to use for displaying certain effect related elements in the music sheet. + * @defaultValue `italic 12px Georgia, serif` + * @since 0.9.6 */ effectFont?: FontJson; /** - * Gets or sets the font to use for displaying beat time information in the music sheet. + * The font to use for displaying beat time information in the music sheet. + * @defaultValue `12px Georgia, serif` + * @since 1.4.0 */ timerFont?: FontJson; /** - * Gets or sets the font to use for displaying the directions texts. + * The font to use for displaying the directions texts. + * @defaultValue `14px Georgia, serif` + * @since 1.4.0 */ directionsFont?: FontJson; /** - * Gets or sets the font to use for displaying the fretboard numbers in chord diagrams. + * The font to use for displaying the fretboard numbers in chord diagrams. + * @defaultValue `11px Arial, sans-serif` + * @since 0.9.6 */ fretboardNumberFont?: FontJson; /** - * Gets or sets the font to use for displaying the numbered music notation in the music sheet. + * The font to use for displaying the numbered music notation in the music sheet. + * @defaultValue `14px Arial, sans-serif` + * @since 1.4.0 */ numberedNotationFont?: FontJson; /** - * Gets or sets the font to use for displaying the grace notes in numbered music notation in the music sheet. + * The font to use for displaying the grace notes in numbered music notation in the music sheet. + * @defaultValue `16px Arial, sans-serif` + * @since 1.4.0 */ numberedNotationGraceFont?: FontJson; /** - * Gets or sets the font to use for displaying the guitar tablature numbers in the music sheet. + * The font to use for displaying the guitar tablature numbers in the music sheet. + * @defaultValue `13px Arial, sans-serif` + * @since 0.9.6 */ tablatureFont?: FontJson; /** - * Gets or sets the font to use for grace notation related texts in the music sheet. + * The font to use for grace notation related texts in the music sheet. + * @defaultValue `11px Arial, sans-serif` + * @since 0.9.6 */ graceFont?: FontJson; /** - * Gets or sets the color to use for rendering the lines of staves. + * The color to use for rendering the lines of staves. + * @defaultValue `rgb(165, 165, 165)` + * @since 0.9.6 */ staffLineColor?: ColorJson; /** - * Gets or sets the color to use for rendering bar separators, the accolade and repeat signs. + * The color to use for rendering bar separators, the accolade and repeat signs. + * @defaultValue `rgb(34, 34, 17)` + * @since 0.9.6 */ barSeparatorColor?: ColorJson; /** - * Gets or sets the font to use for displaying the bar numbers above the music sheet. + * The font to use for displaying the bar numbers above the music sheet. + * @defaultValue `11px Arial, sans-serif` + * @since 0.9.6 */ barNumberFont?: FontJson; /** - * Gets or sets the color to use for displaying the bar numbers above the music sheet. + * The color to use for displaying the bar numbers above the music sheet. + * @defaultValue `rgb(200, 0, 0)` + * @since 0.9.6 */ barNumberColor?: ColorJson; /** - * Gets or sets the font to use for displaying finger information above the music sheet. + * The font to use for displaying finger information in the music sheet. + * @defaultValue `14px Georgia, serif` + * @since 0.9.6 */ fingeringFont?: FontJson; /** - * Gets or sets the font to use for displaying finger information when inline into the music sheet. + * The font to use for displaying finger information when inline into the music sheet. + * @defaultValue `12px Georgia, serif` + * @since 1.4.0 */ inlineFingeringFont?: FontJson; /** - * Gets or sets the font to use for section marker labels shown above the music sheet. + * The font to use for section marker labels shown above the music sheet. + * @defaultValue `bold 14px Georgia, serif` + * @since 0.9.6 */ markerFont?: FontJson; /** - * Gets or sets the color to use for music notation elements of the primary voice. + * The color to use for music notation elements of the primary voice. + * @defaultValue `rgb(0, 0, 0)` + * @since 0.9.6 */ mainGlyphColor?: ColorJson; /** - * Gets or sets the color to use for music notation elements of the secondary voices. + * The color to use for music notation elements of the secondary voices. + * @defaultValue `rgb(0,0,0,0.4)` + * @since 0.9.6 */ secondaryGlyphColor?: ColorJson; /** - * Gets or sets the color to use for displaying the song information above the music sheet. + * The color to use for displaying the song information above the music sheets. + * @defaultValue `rgb(0, 0, 0)` + * @since 0.9.6 */ scoreInfoColor?: ColorJson; } diff --git a/src/generated/RenderingResourcesSerializer.ts b/src/generated/RenderingResourcesSerializer.ts index b053fb669..4639da7da 100644 --- a/src/generated/RenderingResourcesSerializer.ts +++ b/src/generated/RenderingResourcesSerializer.ts @@ -12,35 +12,35 @@ export class RenderingResourcesSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + JsonHelper.forEach(m, (v, k) => RenderingResourcesSerializer.setProperty(obj, k.toLowerCase(), v)); } public static toJson(obj: RenderingResources | null): Map | null { if (!obj) { return null; } const o = new Map(); - 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)); - 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)); + 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)!); + 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)!); return o; } public static setProperty(obj: RenderingResources, property: string, v: unknown): boolean { diff --git a/src/generated/SettingsJson.ts b/src/generated/SettingsJson.ts index bb8f467e2..b77aa74da 100644 --- a/src/generated/SettingsJson.ts +++ b/src/generated/SettingsJson.ts @@ -3,11 +3,11 @@ // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // -import { CoreSettingsJson } from "./CoreSettingsJson"; -import { DisplaySettingsJson } from "./DisplaySettingsJson"; -import { NotationSettingsJson } from "./NotationSettingsJson"; -import { ImporterSettingsJson } from "./ImporterSettingsJson"; -import { PlayerSettingsJson } from "./PlayerSettingsJson"; +import { CoreSettingsJson } from "@src/generated/CoreSettingsJson"; +import { DisplaySettingsJson } from "@src/generated/DisplaySettingsJson"; +import { NotationSettingsJson } from "@src/generated/NotationSettingsJson"; +import { ImporterSettingsJson } from "@src/generated/ImporterSettingsJson"; +import { PlayerSettingsJson } from "@src/generated/PlayerSettingsJson"; /** * This public class contains instance specific settings for alphaTab * @json diff --git a/src/generated/SettingsSerializer.ts b/src/generated/SettingsSerializer.ts index cbeb3fb26..73c0792a7 100644 --- a/src/generated/SettingsSerializer.ts +++ b/src/generated/SettingsSerializer.ts @@ -15,7 +15,7 @@ export class SettingsSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + JsonHelper.forEach(m, (v, k) => SettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } public static toJson(obj: Settings | null): Map | null { if (!obj) { @@ -34,12 +34,10 @@ export class SettingsSerializer { CoreSettingsSerializer.fromJson(obj.core, v as Map); return true; } - else { - for (const c of ["core", ""]) { - if (property.indexOf(c) === 0) { - if (CoreSettingsSerializer.setProperty(obj.core, property.substring(c.length), v)) { - return true; - } + for (const c of ["core", ""]) { + if (property.indexOf(c) === 0) { + if (CoreSettingsSerializer.setProperty(obj.core, property.substring(c.length), v)) { + return true; } } } @@ -47,12 +45,10 @@ export class SettingsSerializer { DisplaySettingsSerializer.fromJson(obj.display, v as Map); return true; } - else { - for (const c of ["display", ""]) { - if (property.indexOf(c) === 0) { - if (DisplaySettingsSerializer.setProperty(obj.display, property.substring(c.length), v)) { - return true; - } + for (const c of ["display", ""]) { + if (property.indexOf(c) === 0) { + if (DisplaySettingsSerializer.setProperty(obj.display, property.substring(c.length), v)) { + return true; } } } @@ -60,12 +56,10 @@ export class SettingsSerializer { NotationSettingsSerializer.fromJson(obj.notation, v as Map); return true; } - else { - for (const c of ["notation"]) { - if (property.indexOf(c) === 0) { - if (NotationSettingsSerializer.setProperty(obj.notation, property.substring(c.length), v)) { - return true; - } + for (const c of ["notation"]) { + if (property.indexOf(c) === 0) { + if (NotationSettingsSerializer.setProperty(obj.notation, property.substring(c.length), v)) { + return true; } } } @@ -73,12 +67,10 @@ export class SettingsSerializer { ImporterSettingsSerializer.fromJson(obj.importer, v as Map); return true; } - else { - for (const c of ["importer"]) { - if (property.indexOf(c) === 0) { - if (ImporterSettingsSerializer.setProperty(obj.importer, property.substring(c.length), v)) { - return true; - } + for (const c of ["importer"]) { + if (property.indexOf(c) === 0) { + if (ImporterSettingsSerializer.setProperty(obj.importer, property.substring(c.length), v)) { + return true; } } } @@ -86,12 +78,10 @@ export class SettingsSerializer { PlayerSettingsSerializer.fromJson(obj.player, v as Map); return true; } - else { - for (const c of ["player"]) { - if (property.indexOf(c) === 0) { - if (PlayerSettingsSerializer.setProperty(obj.player, property.substring(c.length), v)) { - return true; - } + for (const c of ["player"]) { + if (property.indexOf(c) === 0) { + if (PlayerSettingsSerializer.setProperty(obj.player, property.substring(c.length), v)) { + return true; } } } diff --git a/src/generated/SlidePlaybackSettingsJson.ts b/src/generated/SlidePlaybackSettingsJson.ts index 02fef3827..02653b19f 100644 --- a/src/generated/SlidePlaybackSettingsJson.ts +++ b/src/generated/SlidePlaybackSettingsJson.ts @@ -13,20 +13,23 @@ export interface SlidePlaybackSettingsJson { /** * Gets or sets 1/4 tones (bend value) offset that * simple slides like slide-out-below or slide-in-above use. + * @defaultValue `6` */ simpleSlidePitchOffset?: number; /** - * Gets or sets the percentage which the simple slides should take up + * The percentage which the simple slides should take up * from the whole note. for "slide into" effects the slide will take place * from time 0 where the note is plucked to 25% of the overall note duration. * For "slide out" effects the slide will start 75% and finish at 100% of the overall * note duration. + * @defaultValue `0.25` */ simpleSlideDurationRatio?: number; /** - * Gets or sets the percentage which the legato and shift slides should take up + * The percentage which the legato and shift slides should take up * from the whole note. For a value 0.5 the sliding will start at 50% of the overall note duration * and finish at 100% + * @defaultValue `0.5` */ shiftSlideDurationRatio?: number; } diff --git a/src/generated/SlidePlaybackSettingsSerializer.ts b/src/generated/SlidePlaybackSettingsSerializer.ts index 86b2ad92a..70498285c 100644 --- a/src/generated/SlidePlaybackSettingsSerializer.ts +++ b/src/generated/SlidePlaybackSettingsSerializer.ts @@ -10,7 +10,7 @@ export class SlidePlaybackSettingsSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + JsonHelper.forEach(m, (v, k) => SlidePlaybackSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } public static toJson(obj: SlidePlaybackSettings | null): Map | null { if (!obj) { diff --git a/src/generated/VibratoPlaybackSettingsJson.ts b/src/generated/VibratoPlaybackSettingsJson.ts index 0363a0a12..cfd05ad2b 100644 --- a/src/generated/VibratoPlaybackSettingsJson.ts +++ b/src/generated/VibratoPlaybackSettingsJson.ts @@ -11,35 +11,43 @@ */ export interface VibratoPlaybackSettingsJson { /** - * Gets or sets the wavelength of the note-wide vibrato in midi ticks. + * The wavelength of the note-wide vibrato in midi ticks. + * @defaultValue `240` */ noteWideLength?: number; /** - * Gets or sets the amplitude for the note-wide vibrato in semitones. + * The amplitude for the note-wide vibrato in semitones. + * @defaultValue `1` */ noteWideAmplitude?: number; /** - * Gets or sets the wavelength of the note-slight vibrato in midi ticks. + * The wavelength of the note-slight vibrato in midi ticks. + * @defaultValue `360` */ noteSlightLength?: number; /** - * Gets or sets the amplitude for the note-slight vibrato in semitones. + * The amplitude for the note-slight vibrato in semitones. + * @defaultValue `0.5` */ noteSlightAmplitude?: number; /** - * Gets or sets the wavelength of the beat-wide vibrato in midi ticks. + * The wavelength of the beat-wide vibrato in midi ticks. + * @defaultValue `480` */ beatWideLength?: number; /** - * Gets or sets the amplitude for the beat-wide vibrato in semitones. + * The amplitude for the beat-wide vibrato in semitones. + * @defaultValue `2` */ beatWideAmplitude?: number; /** - * Gets or sets the wavelength of the beat-slight vibrato in midi ticks. + * The wavelength of the beat-slight vibrato in midi ticks. + * @defaultValue `480` */ beatSlightLength?: number; /** - * Gets or sets the amplitude for the beat-slight vibrato in semitones. + * The amplitude for the beat-slight vibrato in semitones. + * @defaultValue `2` */ beatSlightAmplitude?: number; } diff --git a/src/generated/VibratoPlaybackSettingsSerializer.ts b/src/generated/VibratoPlaybackSettingsSerializer.ts index 1ba37a12c..a007da73f 100644 --- a/src/generated/VibratoPlaybackSettingsSerializer.ts +++ b/src/generated/VibratoPlaybackSettingsSerializer.ts @@ -10,7 +10,7 @@ export class VibratoPlaybackSettingsSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + JsonHelper.forEach(m, (v, k) => VibratoPlaybackSettingsSerializer.setProperty(obj, k.toLowerCase(), v)); } public static toJson(obj: VibratoPlaybackSettings | null): Map | null { if (!obj) { diff --git a/src/generated/_jsonbarrel.ts b/src/generated/_jsonbarrel.ts new file mode 100644 index 000000000..ac60eda34 --- /dev/null +++ b/src/generated/_jsonbarrel.ts @@ -0,0 +1,16 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +export type { CoreSettingsJson } from "@src/generated/CoreSettingsJson"; +export type { RenderingResourcesJson } from "@src/generated/RenderingResourcesJson"; +export type { DisplaySettingsJson } from "@src/generated/DisplaySettingsJson"; +export type { ImporterSettingsJson } from "@src/generated/ImporterSettingsJson"; +export type { NotationSettingsJson } from "@src/generated/NotationSettingsJson"; +export type { VibratoPlaybackSettingsJson } from "@src/generated/VibratoPlaybackSettingsJson"; +export type { SlidePlaybackSettingsJson } from "@src/generated/SlidePlaybackSettingsJson"; +export type { PlayerSettingsJson } from "@src/generated/PlayerSettingsJson"; +export type { FontJson } from "@src/model/Font"; +export type { ColorJson } from "@src/model/Color"; +export type { SettingsJson } from "@src/generated/SettingsJson"; diff --git a/src/generated/json.ts b/src/generated/json.ts deleted file mode 100644 index 16c3a84d2..000000000 --- a/src/generated/json.ts +++ /dev/null @@ -1,14 +0,0 @@ -// -// This code was auto-generated. -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -export type { CoreSettingsJson } from "./CoreSettingsJson"; -export type { DisplaySettingsJson } from "./DisplaySettingsJson"; -export type { ImporterSettingsJson } from "./ImporterSettingsJson"; -export type { NotationSettingsJson } from "./NotationSettingsJson"; -export type { VibratoPlaybackSettingsJson } from "./VibratoPlaybackSettingsJson"; -export type { SlidePlaybackSettingsJson } from "./SlidePlaybackSettingsJson"; -export type { PlayerSettingsJson } from "./PlayerSettingsJson"; -export type { RenderingResourcesJson } from "./RenderingResourcesJson"; -export type { SettingsJson } from "./SettingsJson"; diff --git a/src/generated/model/AutomationSerializer.ts b/src/generated/model/AutomationSerializer.ts index 41b24bdb0..82495b661 100644 --- a/src/generated/model/AutomationSerializer.ts +++ b/src/generated/model/AutomationSerializer.ts @@ -11,7 +11,7 @@ export class AutomationSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => AutomationSerializer.setProperty(obj, k, v)); } public static toJson(obj: Automation | null): Map | null { if (!obj) { diff --git a/src/generated/model/BarSerializer.ts b/src/generated/model/BarSerializer.ts index 0ea3bced7..1d5ac3da4 100644 --- a/src/generated/model/BarSerializer.ts +++ b/src/generated/model/BarSerializer.ts @@ -7,17 +7,22 @@ import { Bar } from "@src/model/Bar"; import { JsonHelper } from "@src/io/JsonHelper"; import { VoiceSerializer } from "@src/generated/model/VoiceSerializer"; import { SustainPedalMarkerSerializer } from "@src/generated/model/SustainPedalMarkerSerializer"; +import { BarStyleSerializer } from "@src/generated/model/BarStyleSerializer"; import { Clef } from "@src/model/Clef"; import { Ottavia } from "@src/model/Ottavia"; import { Voice } from "@src/model/Voice"; import { SimileMark } from "@src/model/SimileMark"; import { SustainPedalMarker } from "@src/model/Bar"; +import { BarLineStyle } from "@src/model/Bar"; +import { KeySignature } from "@src/model/KeySignature"; +import { KeySignatureType } from "@src/model/KeySignatureType"; +import { BarStyle } from "@src/model/Bar"; export class BarSerializer { public static fromJson(obj: Bar, m: unknown): void { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => BarSerializer.setProperty(obj, k, v)); } public static toJson(obj: Bar | null): Map | null { if (!obj) { @@ -32,6 +37,13 @@ export class BarSerializer { o.set("displayscale", obj.displayScale); o.set("displaywidth", obj.displayWidth); o.set("sustainpedals", obj.sustainPedals.map(i => SustainPedalMarkerSerializer.toJson(i))); + o.set("barlineleft", obj.barLineLeft as number); + o.set("barlineright", obj.barLineRight as number); + o.set("keysignature", obj.keySignature as number); + o.set("keysignaturetype", obj.keySignatureType as number); + if (obj.style) { + o.set("style", BarStyleSerializer.toJson(obj.style)); + } return o; } public static setProperty(obj: Bar, property: string, v: unknown): boolean { @@ -70,6 +82,27 @@ export class BarSerializer { obj.sustainPedals.push(i); } return true; + case "barlineleft": + obj.barLineLeft = JsonHelper.parseEnum(v, BarLineStyle)!; + return true; + case "barlineright": + obj.barLineRight = JsonHelper.parseEnum(v, BarLineStyle)!; + return true; + case "keysignature": + obj.keySignature = JsonHelper.parseEnum(v, KeySignature)!; + return true; + case "keysignaturetype": + obj.keySignatureType = JsonHelper.parseEnum(v, KeySignatureType)!; + return true; + case "style": + if (v) { + obj.style = new BarStyle(); + BarStyleSerializer.fromJson(obj.style, v); + } + else { + obj.style = undefined; + } + return true; } return false; } diff --git a/src/generated/model/BarStyleSerializer.ts b/src/generated/model/BarStyleSerializer.ts new file mode 100644 index 000000000..e4ea47a27 --- /dev/null +++ b/src/generated/model/BarStyleSerializer.ts @@ -0,0 +1,42 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { BarStyle } from "@src/model/Bar"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { BarSubElement } from "@src/model/Bar"; +import { Color } from "@src/model/Color"; +export class BarStyleSerializer { + public static fromJson(obj: BarStyle, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => BarStyleSerializer.setProperty(obj, k, v)); + } + public static toJson(obj: BarStyle | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + { + const m = new Map(); + o.set("colors", m); + for (const [k, v] of obj.colors!) { + m.set(k.toString(), Color.toJson(v)); + } + } + return o; + } + public static setProperty(obj: BarStyle, property: string, v: unknown): boolean { + switch (property) { + case "colors": + obj.colors = new Map(); + JsonHelper.forEach(v, (v, k) => { + obj.colors.set(JsonHelper.parseEnum(k, BarSubElement)!, Color.fromJson(v)); + }); + return true; + } + return false; + } +} diff --git a/src/generated/model/BeatCloner.ts b/src/generated/model/BeatCloner.ts index 6fc18f4bc..ab2451b30 100644 --- a/src/generated/model/BeatCloner.ts +++ b/src/generated/model/BeatCloner.ts @@ -4,9 +4,9 @@ // the code is regenerated. // import { Beat } from "@src/model/Beat"; -import { NoteCloner } from "./NoteCloner"; -import { AutomationCloner } from "./AutomationCloner"; -import { BendPointCloner } from "./BendPointCloner"; +import { NoteCloner } from "@src/generated/model/NoteCloner"; +import { AutomationCloner } from "@src/generated/model/AutomationCloner"; +import { BendPointCloner } from "@src/generated/model/BendPointCloner"; export class BeatCloner { public static clone(original: Beat): Beat { const clone = new Beat(); @@ -57,6 +57,7 @@ export class BeatCloner { clone.playbackStart = original.playbackStart; clone.displayDuration = original.displayDuration; clone.playbackDuration = original.playbackDuration; + clone.overrideDisplayDuration = original.overrideDisplayDuration; clone.golpe = original.golpe; clone.dynamics = original.dynamics; clone.invertBeamDirection = original.invertBeamDirection; diff --git a/src/generated/model/BeatSerializer.ts b/src/generated/model/BeatSerializer.ts index 41f7d5cc3..b99c0b711 100644 --- a/src/generated/model/BeatSerializer.ts +++ b/src/generated/model/BeatSerializer.ts @@ -8,6 +8,7 @@ import { JsonHelper } from "@src/io/JsonHelper"; import { NoteSerializer } from "@src/generated/model/NoteSerializer"; import { AutomationSerializer } from "@src/generated/model/AutomationSerializer"; import { BendPointSerializer } from "@src/generated/model/BendPointSerializer"; +import { BeatStyleSerializer } from "@src/generated/model/BeatStyleSerializer"; import { Note } from "@src/model/Note"; import { BendStyle } from "@src/model/BendStyle"; import { Ottavia } from "@src/model/Ottavia"; @@ -28,12 +29,13 @@ import { BeatBeamingMode } from "@src/model/Beat"; import { WahPedal } from "@src/model/WahPedal"; import { BarreShape } from "@src/model/BarreShape"; import { Rasgueado } from "@src/model/Rasgueado"; +import { BeatStyle } from "@src/model/Beat"; export class BeatSerializer { public static fromJson(obj: Beat, m: unknown): void { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => BeatSerializer.setProperty(obj, k, v)); } public static toJson(obj: Beat | null): Map | null { if (!obj) { @@ -76,6 +78,7 @@ export class BeatSerializer { o.set("playbackstart", obj.playbackStart); o.set("displayduration", obj.displayDuration); o.set("playbackduration", obj.playbackDuration); + o.set("overridedisplayduration", obj.overrideDisplayDuration); o.set("golpe", obj.golpe as number); o.set("dynamics", obj.dynamics as number); o.set("invertbeamdirection", obj.invertBeamDirection); @@ -87,6 +90,9 @@ export class BeatSerializer { o.set("rasgueado", obj.rasgueado as number); o.set("showtimer", obj.showTimer); o.set("timer", obj.timer); + if (obj.style) { + o.set("style", BeatStyleSerializer.toJson(obj.style)); + } return o; } public static setProperty(obj: Beat, property: string, v: unknown): boolean { @@ -193,7 +199,7 @@ export class BeatSerializer { obj.pickStroke = JsonHelper.parseEnum(v, PickStroke)!; return true; case "tremolospeed": - obj.tremoloSpeed = JsonHelper.parseEnum(v, Duration); + obj.tremoloSpeed = JsonHelper.parseEnum(v, Duration) ?? null; return true; case "crescendo": obj.crescendo = JsonHelper.parseEnum(v, CrescendoType)!; @@ -210,6 +216,9 @@ export class BeatSerializer { case "playbackduration": obj.playbackDuration = v! as number; return true; + case "overridedisplayduration": + obj.overrideDisplayDuration = v as number | undefined; + return true; case "golpe": obj.golpe = JsonHelper.parseEnum(v, GolpeType)!; return true; @@ -220,7 +229,7 @@ export class BeatSerializer { obj.invertBeamDirection = v! as boolean; return true; case "preferredbeamdirection": - obj.preferredBeamDirection = JsonHelper.parseEnum(v, BeamDirection); + obj.preferredBeamDirection = JsonHelper.parseEnum(v, BeamDirection) ?? null; return true; case "beamingmode": obj.beamingMode = JsonHelper.parseEnum(v, BeatBeamingMode)!; @@ -243,6 +252,15 @@ export class BeatSerializer { case "timer": obj.timer = v as number | null; return true; + case "style": + if (v) { + obj.style = new BeatStyle(); + BeatStyleSerializer.fromJson(obj.style, v); + } + else { + obj.style = undefined; + } + return true; } return false; } diff --git a/src/generated/model/BeatStyleSerializer.ts b/src/generated/model/BeatStyleSerializer.ts new file mode 100644 index 000000000..690f0e409 --- /dev/null +++ b/src/generated/model/BeatStyleSerializer.ts @@ -0,0 +1,42 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { BeatStyle } from "@src/model/Beat"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { BeatSubElement } from "@src/model/Beat"; +import { Color } from "@src/model/Color"; +export class BeatStyleSerializer { + public static fromJson(obj: BeatStyle, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => BeatStyleSerializer.setProperty(obj, k, v)); + } + public static toJson(obj: BeatStyle | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + { + const m = new Map(); + o.set("colors", m); + for (const [k, v] of obj.colors!) { + m.set(k.toString(), Color.toJson(v)); + } + } + return o; + } + public static setProperty(obj: BeatStyle, property: string, v: unknown): boolean { + switch (property) { + case "colors": + obj.colors = new Map(); + JsonHelper.forEach(v, (v, k) => { + obj.colors.set(JsonHelper.parseEnum(k, BeatSubElement)!, Color.fromJson(v)); + }); + return true; + } + return false; + } +} diff --git a/src/generated/model/BendPointSerializer.ts b/src/generated/model/BendPointSerializer.ts index 077496dd1..d471078b9 100644 --- a/src/generated/model/BendPointSerializer.ts +++ b/src/generated/model/BendPointSerializer.ts @@ -10,7 +10,7 @@ export class BendPointSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => BendPointSerializer.setProperty(obj, k, v)); } public static toJson(obj: BendPoint | null): Map | null { if (!obj) { diff --git a/src/generated/model/ChordSerializer.ts b/src/generated/model/ChordSerializer.ts index d522aa996..140fba11c 100644 --- a/src/generated/model/ChordSerializer.ts +++ b/src/generated/model/ChordSerializer.ts @@ -10,7 +10,7 @@ export class ChordSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => ChordSerializer.setProperty(obj, k, v)); } public static toJson(obj: Chord | null): Map | null { if (!obj) { diff --git a/src/generated/model/FermataSerializer.ts b/src/generated/model/FermataSerializer.ts index 195ae0216..279971de9 100644 --- a/src/generated/model/FermataSerializer.ts +++ b/src/generated/model/FermataSerializer.ts @@ -11,7 +11,7 @@ export class FermataSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => FermataSerializer.setProperty(obj, k, v)); } public static toJson(obj: Fermata | null): Map | null { if (!obj) { diff --git a/src/generated/model/HeaderFooterStyleSerializer.ts b/src/generated/model/HeaderFooterStyleSerializer.ts new file mode 100644 index 000000000..40d0cda6a --- /dev/null +++ b/src/generated/model/HeaderFooterStyleSerializer.ts @@ -0,0 +1,40 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { HeaderFooterStyle } from "@src/model/Score"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { TextAlign } from "@src/platform/ICanvas"; +export class HeaderFooterStyleSerializer { + public static fromJson(obj: HeaderFooterStyle, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => HeaderFooterStyleSerializer.setProperty(obj, k, v)); + } + public static toJson(obj: HeaderFooterStyle | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("template", obj.template); + o.set("isvisible", obj.isVisible); + o.set("textalign", obj.textAlign as number); + return o; + } + public static setProperty(obj: HeaderFooterStyle, property: string, v: unknown): boolean { + switch (property) { + case "template": + obj.template = v! as string; + return true; + case "isvisible": + obj.isVisible = v as boolean | undefined; + return true; + case "textalign": + obj.textAlign = JsonHelper.parseEnum(v, TextAlign)!; + return true; + } + return false; + } +} diff --git a/src/generated/model/InstrumentArticulationSerializer.ts b/src/generated/model/InstrumentArticulationSerializer.ts index 3c02c78cc..155420caa 100644 --- a/src/generated/model/InstrumentArticulationSerializer.ts +++ b/src/generated/model/InstrumentArticulationSerializer.ts @@ -12,7 +12,7 @@ export class InstrumentArticulationSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => InstrumentArticulationSerializer.setProperty(obj, k, v)); } public static toJson(obj: InstrumentArticulation | null): Map | null { if (!obj) { diff --git a/src/generated/model/MasterBarSerializer.ts b/src/generated/model/MasterBarSerializer.ts index 09a12244a..7dd3dcb70 100644 --- a/src/generated/model/MasterBarSerializer.ts +++ b/src/generated/model/MasterBarSerializer.ts @@ -8,8 +8,6 @@ import { JsonHelper } from "@src/io/JsonHelper"; import { SectionSerializer } from "@src/generated/model/SectionSerializer"; import { AutomationSerializer } from "@src/generated/model/AutomationSerializer"; import { FermataSerializer } from "@src/generated/model/FermataSerializer"; -import { KeySignature } from "@src/model/KeySignature"; -import { KeySignatureType } from "@src/model/KeySignatureType"; import { TripletFeel } from "@src/model/TripletFeel"; import { Section } from "@src/model/Section"; import { Automation } from "@src/model/Automation"; @@ -20,7 +18,7 @@ export class MasterBarSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => MasterBarSerializer.setProperty(obj, k, v)); } public static toJson(obj: MasterBar | null): Map | null { if (!obj) { @@ -28,8 +26,6 @@ export class MasterBarSerializer { } const o = new Map(); o.set("alternateendings", obj.alternateEndings); - o.set("keysignature", obj.keySignature as number); - o.set("keysignaturetype", obj.keySignatureType as number); o.set("isdoublebar", obj.isDoubleBar); o.set("isrepeatstart", obj.isRepeatStart); o.set("repeatcount", obj.repeatCount); @@ -38,7 +34,9 @@ export class MasterBarSerializer { o.set("timesignaturecommon", obj.timeSignatureCommon); o.set("isfreetime", obj.isFreeTime); o.set("tripletfeel", obj.tripletFeel as number); - o.set("section", SectionSerializer.toJson(obj.section)); + if (obj.section) { + o.set("section", SectionSerializer.toJson(obj.section)); + } o.set("tempoautomations", obj.tempoAutomations.map(i => AutomationSerializer.toJson(i))); if (obj.fermata !== null) { const m = new Map(); @@ -65,12 +63,6 @@ export class MasterBarSerializer { case "alternateendings": obj.alternateEndings = v! as number; return true; - case "keysignature": - obj.keySignature = JsonHelper.parseEnum(v, KeySignature)!; - return true; - case "keysignaturetype": - obj.keySignatureType = JsonHelper.parseEnum(v, KeySignatureType)!; - return true; case "isdoublebar": obj.isDoubleBar = v! as boolean; return true; @@ -95,6 +87,15 @@ export class MasterBarSerializer { case "tripletfeel": obj.tripletFeel = JsonHelper.parseEnum(v, TripletFeel)!; return true; + case "section": + if (v) { + obj.section = new Section(); + SectionSerializer.fromJson(obj.section, v); + } + else { + obj.section = null; + } + return true; case "tempoautomations": obj.tempoAutomations = []; for (const o of (v as (Map | null)[])) { @@ -108,7 +109,7 @@ export class MasterBarSerializer { JsonHelper.forEach(v, (v, k) => { const i = new Fermata(); FermataSerializer.fromJson(i, v as Map); - obj.addFermata(parseInt(k), i); + obj.addFermata(Number.parseInt(k), i); }); return true; case "start": @@ -129,16 +130,6 @@ export class MasterBarSerializer { } return true; } - if (["section"].indexOf(property) >= 0) { - if (v) { - obj.section = new Section(); - SectionSerializer.fromJson(obj.section, v as Map); - } - else { - obj.section = null; - } - return true; - } return false; } } diff --git a/src/generated/model/NoteCloner.ts b/src/generated/model/NoteCloner.ts index 4c8d06e13..b2f2566a6 100644 --- a/src/generated/model/NoteCloner.ts +++ b/src/generated/model/NoteCloner.ts @@ -4,7 +4,7 @@ // the code is regenerated. // import { Note } from "@src/model/Note"; -import { BendPointCloner } from "./BendPointCloner"; +import { BendPointCloner } from "@src/generated/model/BendPointCloner"; export class NoteCloner { public static clone(original: Note): Note { const clone = new Note(); diff --git a/src/generated/model/NoteSerializer.ts b/src/generated/model/NoteSerializer.ts index 75eee7fe9..3751cf5c1 100644 --- a/src/generated/model/NoteSerializer.ts +++ b/src/generated/model/NoteSerializer.ts @@ -6,6 +6,7 @@ import { Note } from "@src/model/Note"; import { JsonHelper } from "@src/io/JsonHelper"; import { BendPointSerializer } from "@src/generated/model/BendPointSerializer"; +import { NoteStyleSerializer } from "@src/generated/model/NoteStyleSerializer"; import { AccentuationType } from "@src/model/AccentuationType"; import { BendType } from "@src/model/BendType"; import { BendStyle } from "@src/model/BendStyle"; @@ -19,12 +20,13 @@ import { Duration } from "@src/model/Duration"; import { NoteAccidentalMode } from "@src/model/NoteAccidentalMode"; import { DynamicValue } from "@src/model/DynamicValue"; import { NoteOrnament } from "@src/model/NoteOrnament"; +import { NoteStyle } from "@src/model/Note"; export class NoteSerializer { public static fromJson(obj: Note, m: unknown): void { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => NoteSerializer.setProperty(obj, k, v)); } public static toJson(obj: Note | null): Map | null { if (!obj) { @@ -68,6 +70,9 @@ export class NoteSerializer { o.set("accidentalmode", obj.accidentalMode as number); o.set("dynamics", obj.dynamics as number); o.set("ornament", obj.ornament as number); + if (obj.style) { + o.set("style", NoteStyleSerializer.toJson(obj.style)); + } obj.toJson(o); return o; } @@ -185,6 +190,15 @@ export class NoteSerializer { case "ornament": obj.ornament = JsonHelper.parseEnum(v, NoteOrnament)!; return true; + case "style": + if (v) { + obj.style = new NoteStyle(); + NoteStyleSerializer.fromJson(obj.style, v); + } + else { + obj.style = undefined; + } + return true; } return obj.setProperty(property, v); } diff --git a/src/generated/model/NoteStyleSerializer.ts b/src/generated/model/NoteStyleSerializer.ts new file mode 100644 index 000000000..922b843d7 --- /dev/null +++ b/src/generated/model/NoteStyleSerializer.ts @@ -0,0 +1,51 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { NoteStyle } from "@src/model/Note"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { MusicFontSymbol } from "@src/model/MusicFontSymbol"; +import { NoteSubElement } from "@src/model/Note"; +import { Color } from "@src/model/Color"; +export class NoteStyleSerializer { + public static fromJson(obj: NoteStyle, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => NoteStyleSerializer.setProperty(obj, k, v)); + } + public static toJson(obj: NoteStyle | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("notehead", obj.noteHead as number | undefined); + o.set("noteheadcenteronstem", obj.noteHeadCenterOnStem); + { + const m = new Map(); + o.set("colors", m); + for (const [k, v] of obj.colors!) { + m.set(k.toString(), Color.toJson(v)); + } + } + return o; + } + public static setProperty(obj: NoteStyle, property: string, v: unknown): boolean { + switch (property) { + case "notehead": + obj.noteHead = JsonHelper.parseEnum(v, MusicFontSymbol); + return true; + case "noteheadcenteronstem": + obj.noteHeadCenterOnStem = v as boolean | undefined; + return true; + case "colors": + obj.colors = new Map(); + JsonHelper.forEach(v, (v, k) => { + obj.colors.set(JsonHelper.parseEnum(k, NoteSubElement)!, Color.fromJson(v)); + }); + return true; + } + return false; + } +} diff --git a/src/generated/model/PlaybackInformationSerializer.ts b/src/generated/model/PlaybackInformationSerializer.ts index 90d3229d5..842fbdd12 100644 --- a/src/generated/model/PlaybackInformationSerializer.ts +++ b/src/generated/model/PlaybackInformationSerializer.ts @@ -10,7 +10,7 @@ export class PlaybackInformationSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => PlaybackInformationSerializer.setProperty(obj, k, v)); } public static toJson(obj: PlaybackInformation | null): Map | null { if (!obj) { diff --git a/src/generated/model/RenderStylesheetSerializer.ts b/src/generated/model/RenderStylesheetSerializer.ts index fcb1936e0..2f5c3c573 100644 --- a/src/generated/model/RenderStylesheetSerializer.ts +++ b/src/generated/model/RenderStylesheetSerializer.ts @@ -14,7 +14,7 @@ export class RenderStylesheetSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => RenderStylesheetSerializer.setProperty(obj, k, v)); } public static toJson(obj: RenderStylesheet | null): Map | null { if (!obj) { @@ -46,6 +46,14 @@ export class RenderStylesheetSerializer { o.set("othersystemstracknamemode", obj.otherSystemsTrackNameMode as number); o.set("firstsystemtracknameorientation", obj.firstSystemTrackNameOrientation as number); o.set("othersystemstracknameorientation", obj.otherSystemsTrackNameOrientation as number); + o.set("multitrackmultibarrest", obj.multiTrackMultiBarRest); + if (obj.perTrackMultiBarRest !== null) { + const a: number[] = []; + o.set("pertrackmultibarrest", a); + for (const v of obj.perTrackMultiBarRest!) { + a.push(v); + } + } return o; } public static setProperty(obj: RenderStylesheet, property: string, v: unknown): boolean { @@ -65,7 +73,7 @@ export class RenderStylesheetSerializer { case "pertrackdisplaytuning": obj.perTrackDisplayTuning = new Map(); JsonHelper.forEach(v, (v, k) => { - obj.perTrackDisplayTuning!.set(parseInt(k), v as boolean); + obj.perTrackDisplayTuning!.set(Number.parseInt(k), v as boolean); }); return true; case "globaldisplaychorddiagramsontop": @@ -74,7 +82,7 @@ export class RenderStylesheetSerializer { case "pertrackchorddiagramsontop": obj.perTrackChordDiagramsOnTop = new Map(); JsonHelper.forEach(v, (v, k) => { - obj.perTrackChordDiagramsOnTop!.set(parseInt(k), v as boolean); + obj.perTrackChordDiagramsOnTop!.set(Number.parseInt(k), v as boolean); }); return true; case "singletracktracknamepolicy": @@ -95,6 +103,12 @@ export class RenderStylesheetSerializer { case "othersystemstracknameorientation": obj.otherSystemsTrackNameOrientation = JsonHelper.parseEnum(v, TrackNameOrientation)!; return true; + case "multitrackmultibarrest": + obj.multiTrackMultiBarRest = v! as boolean; + return true; + case "pertrackmultibarrest": + obj.perTrackMultiBarRest = new Set(v as number[]); + return true; } return false; } diff --git a/src/generated/model/ScoreSerializer.ts b/src/generated/model/ScoreSerializer.ts index 2faad82be..56df3c800 100644 --- a/src/generated/model/ScoreSerializer.ts +++ b/src/generated/model/ScoreSerializer.ts @@ -8,14 +8,16 @@ import { JsonHelper } from "@src/io/JsonHelper"; import { MasterBarSerializer } from "@src/generated/model/MasterBarSerializer"; import { TrackSerializer } from "@src/generated/model/TrackSerializer"; import { RenderStylesheetSerializer } from "@src/generated/model/RenderStylesheetSerializer"; +import { ScoreStyleSerializer } from "@src/generated/model/ScoreStyleSerializer"; import { MasterBar } from "@src/model/MasterBar"; import { Track } from "@src/model/Track"; +import { ScoreStyle } from "@src/model/Score"; export class ScoreSerializer { public static fromJson(obj: Score, m: unknown): void { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => ScoreSerializer.setProperty(obj, k, v)); } public static toJson(obj: Score | null): Map | null { if (!obj) { @@ -39,6 +41,9 @@ export class ScoreSerializer { o.set("defaultsystemslayout", obj.defaultSystemsLayout); o.set("systemslayout", obj.systemsLayout); o.set("stylesheet", RenderStylesheetSerializer.toJson(obj.stylesheet)); + if (obj.style) { + o.set("style", ScoreStyleSerializer.toJson(obj.style)); + } return o; } public static setProperty(obj: Score, property: string, v: unknown): boolean { @@ -101,10 +106,18 @@ export class ScoreSerializer { case "systemslayout": obj.systemsLayout = v! as number[]; return true; - } - if (["stylesheet"].indexOf(property) >= 0) { - RenderStylesheetSerializer.fromJson(obj.stylesheet, v as Map); - return true; + case "stylesheet": + RenderStylesheetSerializer.fromJson(obj.stylesheet, v); + return true; + case "style": + if (v) { + obj.style = new ScoreStyle(); + ScoreStyleSerializer.fromJson(obj.style, v); + } + else { + obj.style = undefined; + } + return true; } return false; } diff --git a/src/generated/model/ScoreStyleSerializer.ts b/src/generated/model/ScoreStyleSerializer.ts new file mode 100644 index 000000000..6d925f790 --- /dev/null +++ b/src/generated/model/ScoreStyleSerializer.ts @@ -0,0 +1,59 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { ScoreStyle } from "@src/model/Score"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { HeaderFooterStyleSerializer } from "@src/generated/model/HeaderFooterStyleSerializer"; +import { ScoreSubElement } from "@src/model/Score"; +import { HeaderFooterStyle } from "@src/model/Score"; +import { Color } from "@src/model/Color"; +export class ScoreStyleSerializer { + public static fromJson(obj: ScoreStyle, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => ScoreStyleSerializer.setProperty(obj, k, v)); + } + public static toJson(obj: ScoreStyle | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + { + const m = new Map(); + o.set("headerandfooter", m); + for (const [k, v] of obj.headerAndFooter!) { + m.set(k.toString(), HeaderFooterStyleSerializer.toJson(v)); + } + } + { + const m = new Map(); + o.set("colors", m); + for (const [k, v] of obj.colors!) { + m.set(k.toString(), Color.toJson(v)); + } + } + return o; + } + public static setProperty(obj: ScoreStyle, property: string, v: unknown): boolean { + switch (property) { + case "headerandfooter": + obj.headerAndFooter = new Map(); + JsonHelper.forEach(v, (v, k) => { + const i = new HeaderFooterStyle(); + HeaderFooterStyleSerializer.fromJson(i, v as Map); + obj.headerAndFooter.set(JsonHelper.parseEnum(k, ScoreSubElement)!, i); + }); + return true; + case "colors": + obj.colors = new Map(); + JsonHelper.forEach(v, (v, k) => { + obj.colors.set(JsonHelper.parseEnum(k, ScoreSubElement)!, Color.fromJson(v)); + }); + return true; + } + return false; + } +} diff --git a/src/generated/model/SectionSerializer.ts b/src/generated/model/SectionSerializer.ts index a847437d6..c7bfcd8e9 100644 --- a/src/generated/model/SectionSerializer.ts +++ b/src/generated/model/SectionSerializer.ts @@ -10,7 +10,7 @@ export class SectionSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => SectionSerializer.setProperty(obj, k, v)); } public static toJson(obj: Section | null): Map | null { if (!obj) { diff --git a/src/generated/model/StaffSerializer.ts b/src/generated/model/StaffSerializer.ts index 1f034972c..aeaa611b1 100644 --- a/src/generated/model/StaffSerializer.ts +++ b/src/generated/model/StaffSerializer.ts @@ -15,7 +15,7 @@ export class StaffSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => StaffSerializer.setProperty(obj, k, v)); } public static toJson(obj: Staff | null): Map | null { if (!obj) { @@ -69,6 +69,9 @@ export class StaffSerializer { case "displaytranspositionpitch": obj.displayTranspositionPitch = v! as number; return true; + case "stringtuning": + TuningSerializer.fromJson(obj.stringTuning, v); + return true; case "showslash": obj.showSlash = v! as boolean; return true; @@ -88,10 +91,6 @@ export class StaffSerializer { obj.standardNotationLineCount = v! as number; return true; } - if (["stringtuning"].indexOf(property) >= 0) { - TuningSerializer.fromJson(obj.stringTuning, v as Map); - return true; - } return false; } } diff --git a/src/generated/model/SustainPedalMarkerSerializer.ts b/src/generated/model/SustainPedalMarkerSerializer.ts index 6c771235b..9d0f58b80 100644 --- a/src/generated/model/SustainPedalMarkerSerializer.ts +++ b/src/generated/model/SustainPedalMarkerSerializer.ts @@ -11,7 +11,7 @@ export class SustainPedalMarkerSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => SustainPedalMarkerSerializer.setProperty(obj, k, v)); } public static toJson(obj: SustainPedalMarker | null): Map | null { if (!obj) { diff --git a/src/generated/model/TrackSerializer.ts b/src/generated/model/TrackSerializer.ts index b05951370..5ce4c8aaf 100644 --- a/src/generated/model/TrackSerializer.ts +++ b/src/generated/model/TrackSerializer.ts @@ -9,14 +9,16 @@ import { StaffSerializer } from "@src/generated/model/StaffSerializer"; import { PlaybackInformationSerializer } from "@src/generated/model/PlaybackInformationSerializer"; import { Color } from "@src/model/Color"; import { InstrumentArticulationSerializer } from "@src/generated/model/InstrumentArticulationSerializer"; +import { TrackStyleSerializer } from "@src/generated/model/TrackStyleSerializer"; import { Staff } from "@src/model/Staff"; import { InstrumentArticulation } from "@src/model/InstrumentArticulation"; +import { TrackStyle } from "@src/model/Track"; export class TrackSerializer { public static fromJson(obj: Track, m: unknown): void { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => TrackSerializer.setProperty(obj, k, v)); } public static toJson(obj: Track | null): Map | null { if (!obj) { @@ -25,13 +27,23 @@ export class TrackSerializer { const o = new Map(); o.set("staves", obj.staves.map(i => StaffSerializer.toJson(i))); o.set("playbackinfo", PlaybackInformationSerializer.toJson(obj.playbackInfo)); - o.set("color", Color.toJson(obj.color)); + o.set("color", Color.toJson(obj.color)!); o.set("name", obj.name); o.set("isvisibleonmultitrack", obj.isVisibleOnMultiTrack); o.set("shortname", obj.shortName); o.set("defaultsystemslayout", obj.defaultSystemsLayout); o.set("systemslayout", obj.systemsLayout); + if (obj.lineBreaks !== undefined) { + const a: number[] = []; + o.set("linebreaks", a); + for (const v of obj.lineBreaks!) { + a.push(v); + } + } o.set("percussionarticulations", obj.percussionArticulations.map(i => InstrumentArticulationSerializer.toJson(i))); + if (obj.style) { + o.set("style", TrackStyleSerializer.toJson(obj.style)); + } return o; } public static setProperty(obj: Track, property: string, v: unknown): boolean { @@ -44,6 +56,9 @@ export class TrackSerializer { obj.addStaff(i); } return true; + case "playbackinfo": + PlaybackInformationSerializer.fromJson(obj.playbackInfo, v); + return true; case "color": obj.color = Color.fromJson(v)!; return true; @@ -62,6 +77,11 @@ export class TrackSerializer { case "systemslayout": obj.systemsLayout = v! as number[]; return true; + case "linebreaks": + for (const i of (v as number[])) { + obj.addLineBreaks(i); + } + return true; case "percussionarticulations": obj.percussionArticulations = []; for (const o of (v as (Map | null)[])) { @@ -70,10 +90,15 @@ export class TrackSerializer { obj.percussionArticulations.push(i); } return true; - } - if (["playbackinfo"].indexOf(property) >= 0) { - PlaybackInformationSerializer.fromJson(obj.playbackInfo, v as Map); - return true; + case "style": + if (v) { + obj.style = new TrackStyle(); + TrackStyleSerializer.fromJson(obj.style, v); + } + else { + obj.style = undefined; + } + return true; } return false; } diff --git a/src/generated/model/TrackStyleSerializer.ts b/src/generated/model/TrackStyleSerializer.ts new file mode 100644 index 000000000..3b603aba0 --- /dev/null +++ b/src/generated/model/TrackStyleSerializer.ts @@ -0,0 +1,42 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { TrackStyle } from "@src/model/Track"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { TrackSubElement } from "@src/model/Track"; +import { Color } from "@src/model/Color"; +export class TrackStyleSerializer { + public static fromJson(obj: TrackStyle, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => TrackStyleSerializer.setProperty(obj, k, v)); + } + public static toJson(obj: TrackStyle | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + { + const m = new Map(); + o.set("colors", m); + for (const [k, v] of obj.colors!) { + m.set(k.toString(), Color.toJson(v)); + } + } + return o; + } + public static setProperty(obj: TrackStyle, property: string, v: unknown): boolean { + switch (property) { + case "colors": + obj.colors = new Map(); + JsonHelper.forEach(v, (v, k) => { + obj.colors.set(JsonHelper.parseEnum(k, TrackSubElement)!, Color.fromJson(v)); + }); + return true; + } + return false; + } +} diff --git a/src/generated/model/TuningSerializer.ts b/src/generated/model/TuningSerializer.ts index 3dadd78a1..e82cc78be 100644 --- a/src/generated/model/TuningSerializer.ts +++ b/src/generated/model/TuningSerializer.ts @@ -10,7 +10,7 @@ export class TuningSerializer { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => TuningSerializer.setProperty(obj, k, v)); } public static toJson(obj: Tuning | null): Map | null { if (!obj) { diff --git a/src/generated/model/VoiceSerializer.ts b/src/generated/model/VoiceSerializer.ts index 3d6820dc3..0bbb34121 100644 --- a/src/generated/model/VoiceSerializer.ts +++ b/src/generated/model/VoiceSerializer.ts @@ -6,13 +6,15 @@ import { Voice } from "@src/model/Voice"; import { JsonHelper } from "@src/io/JsonHelper"; import { BeatSerializer } from "@src/generated/model/BeatSerializer"; +import { VoiceStyleSerializer } from "@src/generated/model/VoiceStyleSerializer"; import { Beat } from "@src/model/Beat"; +import { VoiceStyle } from "@src/model/Voice"; export class VoiceSerializer { public static fromJson(obj: Voice, m: unknown): void { if (!m) { return; } - JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + JsonHelper.forEach(m, (v, k) => VoiceSerializer.setProperty(obj, k, v)); } public static toJson(obj: Voice | null): Map | null { if (!obj) { @@ -21,7 +23,9 @@ export class VoiceSerializer { const o = new Map(); o.set("id", obj.id); o.set("beats", obj.beats.map(i => BeatSerializer.toJson(i))); - o.set("isempty", obj.isEmpty); + if (obj.style) { + o.set("style", VoiceStyleSerializer.toJson(obj.style)); + } return o; } public static setProperty(obj: Voice, property: string, v: unknown): boolean { @@ -37,8 +41,14 @@ export class VoiceSerializer { obj.addBeat(i); } return true; - case "isempty": - obj.isEmpty = v! as boolean; + case "style": + if (v) { + obj.style = new VoiceStyle(); + VoiceStyleSerializer.fromJson(obj.style, v); + } + else { + obj.style = undefined; + } return true; } return false; diff --git a/src/generated/model/VoiceStyleSerializer.ts b/src/generated/model/VoiceStyleSerializer.ts new file mode 100644 index 000000000..08376abc0 --- /dev/null +++ b/src/generated/model/VoiceStyleSerializer.ts @@ -0,0 +1,42 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { VoiceStyle } from "@src/model/Voice"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { VoiceSubElement } from "@src/model/Voice"; +import { Color } from "@src/model/Color"; +export class VoiceStyleSerializer { + public static fromJson(obj: VoiceStyle, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => VoiceStyleSerializer.setProperty(obj, k, v)); + } + public static toJson(obj: VoiceStyle | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + { + const m = new Map(); + o.set("colors", m); + for (const [k, v] of obj.colors!) { + m.set(k.toString(), Color.toJson(v)); + } + } + return o; + } + public static setProperty(obj: VoiceStyle, property: string, v: unknown): boolean { + switch (property) { + case "colors": + obj.colors = new Map(); + JsonHelper.forEach(v, (v, k) => { + obj.colors.set(JsonHelper.parseEnum(k, VoiceSubElement)!, Color.fromJson(v)); + }); + return true; + } + return false; + } +} diff --git a/src/importer/AlphaTexImporter.ts b/src/importer/AlphaTexImporter.ts index cba77db68..93bc5bd0d 100644 --- a/src/importer/AlphaTexImporter.ts +++ b/src/importer/AlphaTexImporter.ts @@ -3,7 +3,7 @@ import { ScoreImporter } from '@src/importer/ScoreImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { AccentuationType } from '@src/model/AccentuationType'; import { Automation, AutomationType } from '@src/model/Automation'; -import { Bar, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; +import { Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; import { Beat, BeatBeamingMode } from '@src/model/Beat'; import { BendPoint } from '@src/model/BendPoint'; import { BrushType } from '@src/model/BrushType'; @@ -20,22 +20,22 @@ import { Lyrics } from '@src/model/Lyrics'; import { MasterBar } from '@src/model/MasterBar'; import { Note } from '@src/model/Note'; import { PickStroke } from '@src/model/PickStroke'; -import { Score } from '@src/model/Score'; +import { Score, ScoreSubElement } from '@src/model/Score'; import { Section } from '@src/model/Section'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; -import { Staff } from '@src/model/Staff'; +import type { Staff } from '@src/model/Staff'; import { Track } from '@src/model/Track'; import { TripletFeel } from '@src/model/TripletFeel'; import { Tuning } from '@src/model/Tuning'; import { VibratoType } from '@src/model/VibratoType'; import { Voice } from '@src/model/Voice'; import { Logger } from '@src/Logger'; -import { ModelUtils, TuningParseResult } from '@src/model/ModelUtils'; +import { ModelUtils, type TuningParseResult } from '@src/model/ModelUtils'; import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; import { BeatCloner } from '@src/generated/model/BeatCloner'; import { IOHelper } from '@src/io/IOHelper'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { PercussionMapper } from '@src/model/PercussionMapper'; import { KeySignatureType } from '@src/model/KeySignatureType'; @@ -57,32 +57,33 @@ import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy import { Color } from '@src/model/Color'; import { BendStyle } from '@src/model/BendStyle'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import { TextAlign } from '@src/platform/ICanvas'; /** * A list of terminals recognized by the alphaTex-parser */ export enum AlphaTexSymbols { - No, - Eof, - Number, - DoubleDot, - Dot, - String, - Tuning, - LParensis, - RParensis, - LBrace, - RBrace, - Pipe, - MetaCommand, - Multiply, - LowerThan + 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 } enum StaffMetaResult { - KnownStaffMeta, - UnknownStaffMeta, - EndOfMetaDetected + KnownStaffMeta = 0, + UnknownStaffMeta = 1, + EndOfMetaDetected = 2 } export class AlphaTexError extends AlphaTabError { @@ -143,8 +144,8 @@ export class AlphaTexError extends AlphaTabError { } enum AlphaTexAccidentalMode { - Auto, - Explicit + Auto = 0, + Explicit = 1 } /** @@ -186,14 +187,12 @@ export class AlphaTexImporter extends ScoreImporter { private _slurs: Map = new Map(); + private _articulationValueToIndex = new Map(); + private _accidentalMode: AlphaTexAccidentalMode = AlphaTexAccidentalMode.Explicit; public logErrors: boolean = false; - public constructor() { - super(); - } - public get name(): string { return 'AlphaTex'; } @@ -202,6 +201,8 @@ export class AlphaTexImporter extends ScoreImporter { this.data = ByteBuffer.empty(); this._input = tex; this.settings = settings; + // when beginning reading a new score we reset the IDs. + Score.resetIds(); } public readScore(): Score { @@ -236,85 +237,22 @@ export class AlphaTexImporter extends ScoreImporter { } } - this.consolidate(); + ModelUtils.consolidate(this._score); this._score.finish(this.settings); this._score.rebuildRepeatGroups(); for (const [track, lyrics] of this._lyrics) { this._score.tracks[track].applyLyrics(lyrics); } for (const [sustainPedal, beat] of this._sustainPedalToBeat) { - if (sustainPedal.ratioPosition === 0) { - const duration = beat.voice.bar.masterBar.calculateDuration(); - sustainPedal.ratioPosition = beat.playbackStart / duration; - } + 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); - } else { - throw e; - } - } - } - - /** - * Ensures all staffs of all tracks have the correct number of bars - * (the number of bars per staff and track could be inconsistent) - */ - private consolidate(): void { - // empty score? - if (this._score.masterBars.length === 0) { - const master: MasterBar = new MasterBar(); - this._score.addMasterBar(master); - - const tempoAutomation: Automation = new Automation(); - tempoAutomation.isLinear = false; - tempoAutomation.type = AutomationType.Tempo; - tempoAutomation.value = this._score.tempo; - master.tempoAutomations.push(tempoAutomation); - - const bar: Bar = new Bar(); - this._score.tracks[0].staves[0].addBar(bar); - - const v = new Voice(); - bar.addVoice(v); - - const emptyBeat: Beat = new Beat(); - emptyBeat.isEmpty = true; - v.addBeat(emptyBeat); - return; - } - - for (let track of this._score.tracks) { - for (let staff of track.staves) { - // fill empty beats - for (const b of staff.bars) { - for (const v of b.voices) { - if (v.isEmpty && v.beats.length == 0) { - const emptyBeat: Beat = new Beat(); - emptyBeat.isEmpty = true; - v.addBeat(emptyBeat); - } - } - } - - // fill missing bars - const voiceCount = staff.bars.length === 0 ? 1 : staff.bars[0].voices.length; - while (staff.bars.length < this._score.masterBars.length) { - const bar: Bar = new Bar(); - staff.addBar(bar); - - for (let i = 0; i < voiceCount; i++) { - const v = new Voice(); - bar.addVoice(v); - - const emptyBeat: Beat = new Beat(); - emptyBeat.isEmpty = true; - v.addBeat(emptyBeat); - } - } } + throw e; } } @@ -336,7 +274,7 @@ export class AlphaTexImporter extends ScoreImporter { } else { receivedSymbol = expected; } - let e = AlphaTexError.symbolError( + const e = AlphaTexError.symbolError( this._lastValidSpot[0], this._lastValidSpot[1], this._lastValidSpot[2], @@ -352,7 +290,7 @@ export class AlphaTexImporter extends ScoreImporter { } private errorMessage(message: string): void { - let e: AlphaTexError = AlphaTexError.errorMessage( + const e: AlphaTexError = AlphaTexError.errorMessage( message, this._lastValidSpot[0], this._lastValidSpot[1], @@ -384,6 +322,7 @@ export class AlphaTexImporter extends ScoreImporter { const staff = this._currentTrack.staves[0]; staff.displayTranspositionPitch = 0; staff.stringTuning.tunings = Tuning.getDefaultTuningFor(6)!.tunings; + this._articulationValueToIndex.clear(); this.beginStaff(staff); @@ -640,7 +579,7 @@ export class AlphaTexImporter extends ScoreImporter { } this.saveValidSpot(); } else if (this._ch === 0x22 /* " */ || this._ch === 0x27 /* ' */) { - let startChar: number = this._ch; + const startChar: number = this._ch; this._ch = this.nextChar(); let s: string = ''; this._sy = AlphaTexSymbols.String; @@ -650,13 +589,13 @@ export class AlphaTexImporter extends ScoreImporter { this._ch = this.nextChar(); if (this._ch === 0x5c /* \\ */) { s += '\\'; - } else if (this._ch == startChar /* \ */) { + } else if (this._ch === startChar /* \ */) { s += String.fromCharCode(this._ch); - } else if (this._ch == 0x52 /* \R */ || this._ch == 0x72 /* \r */) { + } else if (this._ch === 0x52 /* \R */ || this._ch === 0x72 /* \r */) { s += '\r'; - } else if (this._ch == 0x4e /* \N */ || this._ch == 0x6e /* \n */) { + } else if (this._ch === 0x4e /* \N */ || this._ch === 0x6e /* \n */) { s += '\n'; - } else if (this._ch == 0x54 /* \T */ || this._ch == 0x74 /* \t */) { + } else if (this._ch === 0x54 /* \T */ || this._ch === 0x74 /* \t */) { s += '\t'; } else { this.errorMessage('Unsupported escape sequence'); @@ -693,7 +632,7 @@ export class AlphaTexImporter extends ScoreImporter { this._ch = this.nextChar(); this._sy = AlphaTexSymbols.MetaCommand; // allow double backslash (easier to test when copying from escaped Strings) - if(this._ch === 0x5c /* \ */) { + if (this._ch === 0x5c /* \ */) { this._ch = this.nextChar(); } @@ -719,8 +658,8 @@ export class AlphaTexImporter extends ScoreImporter { } else if (this.isDigit(this._ch)) { this.readNumberOrName(); } else if (AlphaTexImporter.isNameLetter(this._ch)) { - let name: string = this.readName(); - let tuning: TuningParseResult | null = this._allowTuning ? ModelUtils.parseTuning(name) : null; + const name: string = this.readName(); + const tuning: TuningParseResult | null = this._allowTuning ? ModelUtils.parseTuning(name) : null; if (tuning) { this._sy = AlphaTexSymbols.Tuning; this._syData = tuning; @@ -750,7 +689,7 @@ export class AlphaTexImporter extends ScoreImporter { if (isNumber) { this._sy = AlphaTexSymbols.Number; - this._syData = this._allowFloat ? parseFloat(str) : parseInt(str); + this._syData = this._allowFloat ? Number.parseFloat(str) : Number.parseInt(str); } else { this._sy = AlphaTexSymbols.String; this._syData = str; @@ -821,7 +760,7 @@ export class AlphaTexImporter extends ScoreImporter { let anyOtherMeta = false; let continueReading: boolean = true; while (this._sy === AlphaTexSymbols.MetaCommand && continueReading) { - let metadataTag: string = (this._syData as string).toLowerCase(); + const metadataTag: string = (this._syData as string).toLowerCase(); switch (metadataTag) { case 'title': case 'subtitle': @@ -839,28 +778,40 @@ export class AlphaTexImporter extends ScoreImporter { // Need to use quotes in that case, or rewrite parsing logic. this.error(metadataTag, AlphaTexSymbols.String, true); } - let metadataValue: string = this._syData as string; + + 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; @@ -870,9 +821,31 @@ export class AlphaTexImporter extends ScoreImporter { 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': @@ -933,6 +906,11 @@ export class AlphaTexImporter extends ScoreImporter { 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) { @@ -1028,6 +1006,37 @@ export class AlphaTexImporter extends ScoreImporter { 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()) { @@ -1035,7 +1044,7 @@ export class AlphaTexImporter extends ScoreImporter { return TrackNamePolicy.Hidden; case 'allsystems': return TrackNamePolicy.AllSystems; - case 'firstsystem': + // case 'firstsystem': default: return TrackNamePolicy.FirstSystem; } @@ -1045,7 +1054,7 @@ export class AlphaTexImporter extends ScoreImporter { switch (v.toLowerCase()) { case 'fullname': return TrackNameMode.FullName; - case 'shortname': + // case 'shortname': default: return TrackNameMode.ShortName; } @@ -1055,7 +1064,7 @@ export class AlphaTexImporter extends ScoreImporter { switch (v.toLowerCase()) { case 'horizontal': return TrackNameOrientation.Horizontal; - case 'vertical': + //case 'vertical': default: return TrackNameOrientation.Vertical; } @@ -1074,12 +1083,12 @@ export class AlphaTexImporter extends ScoreImporter { return StaffMetaResult.KnownStaffMeta; case 'tuning': this._sy = this.newSy(); - let strings: number = this._currentStaff.tuning.length; + const strings: number = this._currentStaff.tuning.length; this._staffHasExplicitTuning = true; this._staffTuningApplied = false; switch (this._sy) { case AlphaTexSymbols.String: - let text: string = (this._syData as string).toLowerCase(); + const text: string = (this._syData as string).toLowerCase(); if (text === 'piano' || text === 'none' || text === 'voice') { this.makeCurrentStaffPitched(); } else { @@ -1088,9 +1097,9 @@ export class AlphaTexImporter extends ScoreImporter { this._sy = this.newSy(); break; case AlphaTexSymbols.Tuning: - let tuning: number[] = []; + const tuning: number[] = []; do { - let t: TuningParseResult = this._syData as TuningParseResult; + const t: TuningParseResult = this._syData as TuningParseResult; tuning.push(t.realValue); this._sy = this.newSy(); } while (this._sy === AlphaTexSymbols.Tuning); @@ -1117,14 +1126,14 @@ export class AlphaTexImporter extends ScoreImporter { this._sy = this.newSy(); this._staffTuningApplied = false; if (this._sy === AlphaTexSymbols.Number) { - let instrument: number = this._syData as 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) { - let instrumentName: string = (this._syData as string).toLowerCase(); + const instrumentName: string = (this._syData as string).toLowerCase(); if (instrumentName === 'percussion') { for (const staff of this._currentTrack.staves) { this.applyPercussionStaff(staff); @@ -1141,7 +1150,7 @@ export class AlphaTexImporter extends ScoreImporter { return StaffMetaResult.KnownStaffMeta; case 'lyrics': this._sy = this.newSy(); - let lyrics: Lyrics = new Lyrics(); + const lyrics: Lyrics = new Lyrics(); lyrics.startBar = 0; lyrics.text = ''; if (this._sy === AlphaTexSymbols.Number) { @@ -1158,7 +1167,7 @@ export class AlphaTexImporter extends ScoreImporter { return StaffMetaResult.KnownStaffMeta; case 'chord': this._sy = this.newSy(); - let chord: Chord = new Chord(); + const chord: Chord = new Chord(); this.chordProperties(chord); if (this._sy === AlphaTexSymbols.String) { chord.name = this._syData as string; @@ -1248,9 +1257,8 @@ export class AlphaTexImporter extends ScoreImporter { this._sy = this.newSy(); if (this.handleNewVoice()) { return StaffMetaResult.EndOfMetaDetected; - } else { - return StaffMetaResult.KnownStaffMeta; } + return StaffMetaResult.KnownStaffMeta; default: return StaffMetaResult.UnknownStaffMeta; } @@ -1286,7 +1294,7 @@ export class AlphaTexImporter extends ScoreImporter { * Encodes a given string to a shorthand text form without spaces or special characters */ private static toArticulationId(plain: string): string { - return plain.replace(new RegExp('[^a-zA-Z0-9]', 'g'), '').toLowerCase(); + return plain.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); } private applyPercussionStaff(staff: Staff) { @@ -1377,7 +1385,7 @@ export class AlphaTexImporter extends ScoreImporter { } private bars(): boolean { - let anyData = this.bar(); + const anyData = this.bar(); while (this._sy !== AlphaTexSymbols.Eof) { // read pipe from last bar if (this._sy === AlphaTexSymbols.Pipe) { @@ -1452,23 +1460,22 @@ export class AlphaTexImporter extends ScoreImporter { private handleNewVoice(): boolean { if ( this._voiceIndex === 0 && - (this._currentStaff.bars.length == 0 || + (this._currentStaff.bars.length === 0 || (this._currentStaff.bars.length === 1 && this._currentStaff.bars[0].isEmpty)) ) { // voice marker on the begining of the first voice without any bar yet? // -> ignore return false; - } else { - // 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; } + // 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) { @@ -1534,6 +1541,13 @@ export class AlphaTexImporter extends ScoreImporter { 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; default: this.error('track-properties', AlphaTexSymbols.String, false); break; @@ -1598,13 +1612,11 @@ export class AlphaTexImporter extends ScoreImporter { private bar(): boolean { const anyStaffMeta = this.trackStaffMeta(); - let bar: Bar = this.newBar(this._currentStaff); + const bar: Bar = this.newBar(this._currentStaff); if (this._currentStaff.bars.length > this._score.masterBars.length) { - let master: MasterBar = new MasterBar(); + const master: MasterBar = new MasterBar(); this._score.addMasterBar(master); if (master.index > 0) { - master.keySignature = master.previousMasterBar!.keySignature; - master.keySignatureType = master.previousMasterBar!.keySignatureType; master.timeSignatureDenominator = master.previousMasterBar!.timeSignatureDenominator; master.timeSignatureNumerator = master.previousMasterBar!.timeSignatureNumerator; master.tripletFeel = master.previousMasterBar!.tripletFeel; @@ -1618,7 +1630,7 @@ export class AlphaTexImporter extends ScoreImporter { // reset to defaults this._currentStaff.stringTuning.tunings = []; - if (program == 15) { + if (program === 15) { // dulcimer E4 B3 G3 D3 A2 E2 this._currentStaff.stringTuning.tunings = Tuning.getDefaultTuningFor(6)!.tunings; } else if (program >= 24 && program <= 31) { @@ -1628,39 +1640,39 @@ export class AlphaTexImporter extends ScoreImporter { // 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 + 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) { + } else if (program === 41) { // viola A3 D3 G2 C2 this._currentStaff.stringTuning.tunings = [57, 50, 43, 36]; - } else if (program == 42) { + } else if (program === 42) { // cello A2 D2 G1 C1 this._currentStaff.stringTuning.tunings = [45, 38, 31, 24]; - } else if (program == 43) { + } else if (program === 43) { // contrabass // G2 D2 A1 E1 this._currentStaff.stringTuning.tunings = [43, 38, 33, 28]; - } else if (program == 105) { + } else if (program === 105) { // banjo // D3 B2 G2 D2 G3 this._currentStaff.stringTuning.tunings = [50, 47, 43, 38, 55]; - } else if (program == 106) { + } else if (program === 106) { // shamisen // A3 E3 A2 this._currentStaff.stringTuning.tunings = [57, 52, 45]; - } else if (program == 107) { + } else if (program === 107) { // koto // E3 A2 D2 G1 this._currentStaff.stringTuning.tunings = [52, 45, 38, 31]; - } else if (program == 110) { + } else if (program === 110) { // Fiddle // E4 A3 D3 G2 this._currentStaff.stringTuning.tunings = [64, 57, 50, 43]; @@ -1674,7 +1686,7 @@ export class AlphaTexImporter extends ScoreImporter { if ( (program >= 24 && program <= 31) || // Guitar (program >= 32 && program <= 39) || // Bass - program == 43 // Contrabass + program === 43 // Contrabass ) { // guitar E4 B3 G3 D3 A2 E2 this._currentStaff.displayTranspositionPitch = -12; @@ -1684,7 +1696,7 @@ export class AlphaTexImporter extends ScoreImporter { } let anyBeatData = false; - let voice: Voice = bar.voices[this._voiceIndex]; + 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 @@ -1702,7 +1714,7 @@ export class AlphaTexImporter extends ScoreImporter { } if (voice.beats.length === 0) { - let emptyBeat: Beat = new Beat(); + const emptyBeat: Beat = new Beat(); emptyBeat.isEmpty = true; voice.addBeat(emptyBeat); } @@ -1723,14 +1735,16 @@ export class AlphaTexImporter extends ScoreImporter { // need new bar const newBar: Bar = new Bar(); staff.addBar(newBar); - if(newBar.previousBar) { + 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++; for (let i = 0; i < voiceCount; i++) { - let voice: Voice = new Voice(); + const voice: Voice = new Voice(); newBar.addVoice(voice); } @@ -1741,7 +1755,7 @@ export class AlphaTexImporter extends ScoreImporter { // duration specifier? this.beatDuration(); - let beat: Beat = new Beat(); + const beat: Beat = new Beat(); voice.addBeat(beat); this._allowTuning = !this._currentStaff.isPercussion; @@ -1822,7 +1836,7 @@ export class AlphaTexImporter extends ScoreImporter { } this._sy = this.newSy(); while (this._sy === AlphaTexSymbols.String) { - let effect: string = (this._syData as string).toLowerCase(); + const effect: string = (this._syData as string).toLowerCase(); switch (effect) { case 'tu': this._sy = this.newSy(); @@ -1864,7 +1878,7 @@ export class AlphaTexImporter extends ScoreImporter { * @returns true if a effect could be applied, otherwise false */ private applyBeatEffect(beat: Beat): boolean { - let syData: string = (this._syData as string).toLowerCase(); + const syData: string = (this._syData as string).toLowerCase(); if (syData === 'f') { beat.fade = FadeType.FadeIn; } else if (syData === 'fo') { @@ -1919,7 +1933,7 @@ export class AlphaTexImporter extends ScoreImporter { } else if (syData === 'tb' || syData === 'tbe') { this._sy = this.newSy(); - let exact: boolean = syData === 'tbe'; + const exact: boolean = syData === 'tbe'; // Type if (this._sy === AlphaTexSymbols.String) { @@ -1969,8 +1983,8 @@ export class AlphaTexImporter extends ScoreImporter { } // set positions if (!exact) { - let count: number = beat.whammyBarPoints.length; - let step: number = (60 / count) | 0; + const count: number = beat.whammyBarPoints.length; + const step: number = (60 / count) | 0; let i: number = 0; while (i < count) { beat.whammyBarPoints[i].offset = Math.min(60, i * step); @@ -2017,10 +2031,10 @@ export class AlphaTexImporter extends ScoreImporter { return true; } else if (syData === 'ch') { this._sy = this.newSy(); - let chordName: string = this._syData as string; - let chordId: string = this.getChordId(this._currentStaff, chordName); + const chordName: string = this._syData as string; + const chordId: string = this.getChordId(this._currentStaff, chordName); if (!this._currentStaff.hasChord(chordId)) { - let chord: Chord = new Chord(); + const chord: Chord = new Chord(); chord.showDiagram = false; chord.name = chordName; this._currentStaff.addChord(chordId, chord); @@ -2040,30 +2054,35 @@ export class AlphaTexImporter extends ScoreImporter { return true; } else if (syData === 'dy') { this._sy = this.newSy(); - switch ((this._syData as string).toLowerCase()) { - case 'ppp': - beat.dynamics = DynamicValue.PPP; - break; - case 'pp': - beat.dynamics = DynamicValue.PP; - break; - case 'p': - beat.dynamics = DynamicValue.P; - break; - case 'mp': - beat.dynamics = DynamicValue.MP; - break; - case 'mf': - beat.dynamics = DynamicValue.MF; - break; - case 'f': - beat.dynamics = DynamicValue.F; - break; - case 'ff': - beat.dynamics = DynamicValue.FF; - break; - case 'fff': - beat.dynamics = DynamicValue.FFF; + 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; @@ -2102,6 +2121,7 @@ export class AlphaTexImporter extends ScoreImporter { 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(); @@ -2110,6 +2130,7 @@ export class AlphaTexImporter extends ScoreImporter { 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(); @@ -2118,7 +2139,6 @@ export class AlphaTexImporter extends ScoreImporter { const sustainPedal = new SustainPedalMarker(); sustainPedal.pedalType = SustainPedalMarkerType.Up; sustainPedal.ratioPosition = 1; - this._sustainPedalToBeat.set(sustainPedal, beat); beat.voice.bar.sustainPedals.push(sustainPedal); this._sy = this.newSy(); return true; @@ -2461,10 +2481,10 @@ export class AlphaTexImporter extends ScoreImporter { this.makeCurrentStaffPitched(); } - let tuning: TuningParseResult = this._syData as TuningParseResult; + const tuning: TuningParseResult = this._syData as TuningParseResult; octave = tuning.octave; tone = tuning.tone.noteValue; - if (this._accidentalMode == AlphaTexAccidentalMode.Explicit) { + if (this._accidentalMode === AlphaTexAccidentalMode.Explicit) { accidentalMode = tuning.tone.accidentalMode; } break; @@ -2473,7 +2493,7 @@ export class AlphaTexImporter extends ScoreImporter { } this._sy = this.newSy(); // Fret done - let isFretted: boolean = + const isFretted: boolean = octave === -1 && this._currentStaff.tuning.length > 0 && !this._currentStaff.isPercussion; let noteString: number = -1; if (isFretted) { @@ -2493,7 +2513,7 @@ export class AlphaTexImporter extends ScoreImporter { this._sy = this.newSy(); // string done } // read effects - let note: Note = new Note(); + const note: Note = new Note(); if (isFretted) { note.string = this._currentStaff.tuning.length - (noteString - 1); note.isDead = isDead; @@ -2502,7 +2522,22 @@ export class AlphaTexImporter extends ScoreImporter { note.fret = fret; } } else if (this._currentStaff.isPercussion) { - note.percussionArticulation = fret; + 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; @@ -2520,10 +2555,10 @@ export class AlphaTexImporter extends ScoreImporter { } this._sy = this.newSy(); while (this._sy === AlphaTexSymbols.String) { - let syData = (this._syData as string).toLowerCase(); + const syData = (this._syData as string).toLowerCase(); if (syData === 'b' || syData === 'be') { this._sy = this.newSy(); - let exact: boolean = syData === 'be'; + const exact: boolean = syData === 'be'; // Type if (this._sy === AlphaTexSymbols.String) { @@ -2576,8 +2611,8 @@ export class AlphaTexImporter extends ScoreImporter { return a.offset - b.offset; }); } else { - let count: number = points.length; - let step: number = (60 / (count - 1)) | 0; + 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); @@ -2615,7 +2650,7 @@ export class AlphaTexImporter extends ScoreImporter { if (this._sy !== AlphaTexSymbols.Number) { this.error('trill-effect', AlphaTexSymbols.Number, true); } - let fret: number = this._syData as number; + const fret: number = this._syData as number; this._sy = this.newSy(); let duration: Duration = Duration.Sixteenth; if (this._sy === AlphaTexSymbols.Number) { @@ -2867,11 +2902,11 @@ export class AlphaTexImporter extends ScoreImporter { private barMeta(bar: Bar): boolean { let anyMeta = false; - let master: MasterBar = bar.masterBar; + const master: MasterBar = bar.masterBar; let endOfMeta = false; while (!endOfMeta && this._sy === AlphaTexSymbols.MetaCommand) { anyMeta = true; - let syData: string = (this._syData as string).toLowerCase(); + const syData: string = (this._syData as string).toLowerCase(); if (syData === 'ts') { this._sy = this.newSy(); if (this._sy === AlphaTexSymbols.String) { @@ -2895,7 +2930,7 @@ export class AlphaTexImporter extends ScoreImporter { master.timeSignatureDenominator = this._syData as number; this._sy = this.newSy(); } - } else if (syData == 'ft') { + } else if (syData === 'ft') { master.isFreeTime = true; this._sy = this.newSy(); } else if (syData === 'ro') { @@ -2937,8 +2972,8 @@ export class AlphaTexImporter extends ScoreImporter { if (this._sy !== AlphaTexSymbols.String) { this.error('keysignature', AlphaTexSymbols.String, true); } - master.keySignature = this.parseKeySignature(this._syData as string); - master.keySignatureType = this.parseKeySignatureType(this._syData as string); + 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(); @@ -2950,7 +2985,7 @@ export class AlphaTexImporter extends ScoreImporter { bar.clef = this.parseClefFromInt(this._syData as number); break; case AlphaTexSymbols.Tuning: - let parseResult: TuningParseResult = this._syData as TuningParseResult; + const parseResult: TuningParseResult = this._syData as TuningParseResult; bar.clef = this.parseClefFromInt(parseResult.realValue); break; default: @@ -2974,7 +3009,7 @@ export class AlphaTexImporter extends ScoreImporter { text = this._syData as string; this._sy = this.newSy(); } - let section: Section = new Section(); + const section: Section = new Section(); section.marker = marker; section.text = text; master.section = section; @@ -2999,6 +3034,23 @@ export class AlphaTexImporter extends ScoreImporter { 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(); @@ -3058,13 +3110,20 @@ export class AlphaTexImporter extends ScoreImporter { break; } } else { - this.error('measure-effects', AlphaTexSymbols.String, false); + 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) { - let tempoAutomation: Automation = new Automation(); + const tempoAutomation: Automation = new Automation(); tempoAutomation.isLinear = false; tempoAutomation.type = AutomationType.Tempo; tempoAutomation.value = this._score.tempo; @@ -3074,6 +3133,37 @@ export class AlphaTexImporter extends ScoreImporter { 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': @@ -3187,7 +3277,7 @@ export class AlphaTexImporter extends ScoreImporter { } private applyAlternateEnding(master: MasterBar): void { - let num = this._syData as number; + const num = this._syData as number; if (num < 1) { // Repeat numberings start from 1 this.error('alternateending', AlphaTexSymbols.Number, true); diff --git a/src/importer/BinaryStylesheet.ts b/src/importer/BinaryStylesheet.ts index 6d9236158..3f490b279 100644 --- a/src/importer/BinaryStylesheet.ts +++ b/src/importer/BinaryStylesheet.ts @@ -1,23 +1,30 @@ -import { Score } from '@src/model/Score'; +import { type HeaderFooterStyle, type Score, ScoreSubElement } from '@src/model/Score'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { IOHelper } from '@src/io/IOHelper'; import { GpBinaryHelpers } from '@src/importer/Gp3To5Importer'; import { BendPoint } from '@src/model/BendPoint'; import { Bounds } from '@src/rendering/utils/Bounds'; import { Color } from '@src/model/Color'; -import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@src/model/RenderStylesheet'; -import { IWriteable } from '@src/io/IWriteable'; +import { + type BracketExtendMode, + TrackNameMode, + TrackNameOrientation, + TrackNamePolicy +} from '@src/model/RenderStylesheet'; +import type { IWriteable } from '@src/io/IWriteable'; import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; +import { ModelUtils } from '@src/model/ModelUtils'; +import { TextAlign } from '@src/platform/ICanvas'; enum DataType { - Boolean, - Integer, - Float, - String, - Point, - Size, - Rectangle, - Color + Boolean = 0, + Integer = 1, + Float = 2, + String = 3, + Point = 4, + Size = 5, + Rectangle = 6, + Color = 7 } /** @@ -72,41 +79,41 @@ export class BinaryStylesheet { private read(data: Uint8Array) { // BinaryStylesheet apears to be big-endien - let readable: ByteBuffer = ByteBuffer.fromBuffer(data); - let entryCount: number = IOHelper.readInt32BE(readable); + const readable: ByteBuffer = ByteBuffer.fromBuffer(data); + const entryCount: number = IOHelper.readInt32BE(readable); for (let i: number = 0; i < entryCount; i++) { - let key: string = GpBinaryHelpers.gpReadString(readable, readable.readByte(), 'utf-8'); - let type: DataType = readable.readByte() as DataType; + const key: string = GpBinaryHelpers.gpReadString(readable, readable.readByte(), 'utf-8'); + const type: DataType = readable.readByte() as DataType; this._types.set(key, type); switch (type) { case DataType.Boolean: - let flag: boolean = readable.readByte() === 1; + const flag: boolean = readable.readByte() === 1; this.addValue(key, flag); break; case DataType.Integer: - let ivalue: number = IOHelper.readInt32BE(readable); + const ivalue: number = IOHelper.readInt32BE(readable); this.addValue(key, ivalue); break; case DataType.Float: - let fvalue: number = IOHelper.readFloat32BE(readable); + const fvalue: number = IOHelper.readFloat32BE(readable); this.addValue(key, fvalue); break; case DataType.String: - let s: string = GpBinaryHelpers.gpReadString(readable, IOHelper.readInt16BE(readable), 'utf-8'); + const s: string = GpBinaryHelpers.gpReadString(readable, IOHelper.readInt16BE(readable), 'utf-8'); this.addValue(key, s); break; case DataType.Point: - let x: number = IOHelper.readInt32BE(readable); - let y: number = IOHelper.readInt32BE(readable); + const x: number = IOHelper.readInt32BE(readable); + const y: number = IOHelper.readInt32BE(readable); this.addValue(key, new BendPoint(x, y)); break; case DataType.Size: - let width: number = IOHelper.readInt32BE(readable); - let height: number = IOHelper.readInt32BE(readable); + const width: number = IOHelper.readInt32BE(readable); + const height: number = IOHelper.readInt32BE(readable); this.addValue(key, new BendPoint(width, height)); break; case DataType.Rectangle: - let rect = new Bounds(); + const rect = new Bounds(); rect.x = IOHelper.readInt32BE(readable); rect.y = IOHelper.readInt32BE(readable); rect.w = IOHelper.readInt32BE(readable); @@ -114,7 +121,7 @@ export class BinaryStylesheet { this.addValue(key, rect); break; case DataType.Color: - let color: Color = GpBinaryHelpers.gpReadColor(readable, true); + const color: Color = GpBinaryHelpers.gpReadColor(readable, true); this.addValue(key, color); break; } @@ -207,9 +214,143 @@ export class BinaryStylesheet { score.stylesheet.otherSystemsTrackNameOrientation = TrackNameOrientation.Vertical; } break; + case 'Header/Title': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Title).template = value as string; + break; + case 'Header/TitleAlignment': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Title).textAlign = this.toTextAlign( + value as number + ); + break; + case 'Header/drawTitle': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Title).isVisible = value as boolean; + break; + + case 'Header/Subtitle': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.SubTitle).template = value as string; + break; + case 'Header/SubtitleAlignment': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.SubTitle).textAlign = + this.toTextAlign(value as number); + break; + case 'Header/drawSubtitle': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.SubTitle).isVisible = + value as boolean; + break; + + case 'Header/Artist': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Artist).template = value as string; + break; + case 'Header/ArtistAlignment': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Artist).textAlign = this.toTextAlign( + value as number + ); + break; + case 'Header/drawArtist': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Artist).isVisible = value as boolean; + break; + + case 'Header/Album': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Album).template = value as string; + break; + case 'Header/AlbumAlignment': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Album).textAlign = this.toTextAlign( + value as number + ); + break; + case 'Header/drawAlbum': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Album).isVisible = value as boolean; + break; + + case 'Header/Words': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Words).template = value as string; + break; + case 'Header/WordsAlignment': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Words).textAlign = this.toTextAlign( + value as number + ); + break; + case 'Header/drawWords': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Words).isVisible = value as boolean; + break; + + case 'Header/Music': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Music).template = value as string; + break; + case 'Header/MusicAlignment': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Music).textAlign = this.toTextAlign( + value as number + ); + break; + case 'Header/drawMusic': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Music).isVisible = value as boolean; + break; + + case 'Header/WordsAndMusic': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.WordsAndMusic).template = + value as string; + break; + case 'Header/WordsAndMusicAlignment': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.WordsAndMusic).textAlign = + this.toTextAlign(value as number); + break; + case 'Header/drawWordsAndMusic': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.WordsAndMusic).isVisible = + value as boolean; + break; + + case 'Header/Tabber': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Transcriber).template = + value as string; + break; + case 'Header/TabberAlignment': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Transcriber).textAlign = + this.toTextAlign(value as number); + break; + case 'Header/drawTabber': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Transcriber).isVisible = + value as boolean; + break; + + case 'Footer/Copyright': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Copyright).template = + value as string; + break; + case 'Footer/CopyrightAlignment': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Copyright).textAlign = + this.toTextAlign(value as number); + break; + case 'Footer/drawCopyright': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.Copyright).isVisible = + value as boolean; + break; + + case 'Footer/Copyright2': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.CopyrightSecondLine).template = + value as string; + break; + case 'Footer/Copyright2Alignment': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.CopyrightSecondLine).textAlign = + this.toTextAlign(value as number); + break; + case 'Footer/drawCopyright2': + ModelUtils.getOrCreateHeaderFooterStyle(score, ScoreSubElement.CopyrightSecondLine).isVisible = + value as boolean; + break; } } } + private toTextAlign(value: number): TextAlign { + switch (value) { + case 0: + return TextAlign.Left; + case 1: + return TextAlign.Center; + case 2: + return TextAlign.Right; + } + return TextAlign.Left; + } public addValue(key: string, value: unknown, type?: DataType): void { this.raw.set(key, value); @@ -276,7 +417,7 @@ export class BinaryStylesheet { return DataType.String; case 'number': const withoutFraction: number = (value as number) | 0; - return (value as number) == withoutFraction ? DataType.Integer : DataType.Float; + return (value as number) === withoutFraction ? DataType.Integer : DataType.Float; case 'object': if (value instanceof BendPoint) { return DataType.Point; @@ -373,8 +514,63 @@ export class BinaryStylesheet { break; } + const scoreStyle = score.style; + if (scoreStyle) { + for (const [k, v] of scoreStyle.headerAndFooter) { + switch (k) { + case ScoreSubElement.Title: + BinaryStylesheet.addHeaderAndFooter(binaryStylesheet, v, 'Header/', 'Title'); + break; + case ScoreSubElement.SubTitle: + BinaryStylesheet.addHeaderAndFooter(binaryStylesheet, v, 'Header/', 'Subtitle'); + break; + case ScoreSubElement.Artist: + BinaryStylesheet.addHeaderAndFooter(binaryStylesheet, v, 'Header/', 'Artist'); + break; + case ScoreSubElement.Album: + BinaryStylesheet.addHeaderAndFooter(binaryStylesheet, v, 'Header/', 'Album'); + break; + case ScoreSubElement.Words: + BinaryStylesheet.addHeaderAndFooter(binaryStylesheet, v, 'Header/', 'Words'); + break; + case ScoreSubElement.Music: + BinaryStylesheet.addHeaderAndFooter(binaryStylesheet, v, 'Header/', 'Music'); + break; + case ScoreSubElement.WordsAndMusic: + BinaryStylesheet.addHeaderAndFooter(binaryStylesheet, v, 'Header/', 'WordsAndMusic'); + break; + case ScoreSubElement.Transcriber: + BinaryStylesheet.addHeaderAndFooter(binaryStylesheet, v, 'Header/', 'Tabber'); + break; + case ScoreSubElement.Copyright: + BinaryStylesheet.addHeaderAndFooter(binaryStylesheet, v, 'Footer/', 'Copyright'); + break; + case ScoreSubElement.CopyrightSecondLine: + BinaryStylesheet.addHeaderAndFooter(binaryStylesheet, v, 'Footer/', 'Copyright2'); + break; + } + } + } + const writer = ByteBuffer.withCapacity(128); binaryStylesheet.writeTo(writer); return writer.toArray(); } + + private static addHeaderAndFooter( + binaryStylesheet: BinaryStylesheet, + style: HeaderFooterStyle, + prefix: string, + name: string + ) { + if (style.template !== undefined) { + binaryStylesheet.addValue(`${prefix}${name}`, style.template!, DataType.String); + } + + binaryStylesheet.addValue(`${prefix}${name}Alignment`, style.textAlign as number, DataType.Integer); + + if (style.isVisible !== undefined) { + binaryStylesheet.addValue(`${prefix}draw${name}`, style.isVisible! as boolean, DataType.Boolean); + } + } } diff --git a/src/importer/CapellaImporter.ts b/src/importer/CapellaImporter.ts index bc2c91f82..8ec9d537c 100644 --- a/src/importer/CapellaImporter.ts +++ b/src/importer/CapellaImporter.ts @@ -1,12 +1,12 @@ import { ScoreImporter } from '@src/importer/ScoreImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Logger } from '@src/Logger'; import { ZipReader } from '@src/zip/ZipReader'; -import { ZipEntry } from "@src/zip/ZipEntry"; +import type { ZipEntry } from '@src/zip/ZipEntry'; import { IOHelper } from '@src/io/IOHelper'; import { CapellaParser } from '@src/importer/CapellaParser'; @@ -18,20 +18,16 @@ export class CapellaImporter extends ScoreImporter { return 'Capella'; } - public constructor() { - super(); - } - public readScore(): Score { Logger.debug(this.name, 'Loading ZIP entries'); - let fileSystem: ZipReader = new ZipReader(this.data); + const fileSystem: ZipReader = new ZipReader(this.data); let entries: ZipEntry[]; let xml: string | null = null; entries = fileSystem.read(); Logger.debug(this.name, 'Zip entries loaded'); if (entries.length > 0) { - for (let entry of entries) { + for (const entry of entries) { switch (entry.fileName) { case 'score.xml': xml = IOHelper.toString(entry.data, this.settings.importer.encoding); @@ -49,10 +45,10 @@ export class CapellaImporter extends ScoreImporter { Logger.debug(this.name, 'Start Parsing score.xml'); try { - let capellaParser: CapellaParser = new CapellaParser(); + const capellaParser: CapellaParser = new CapellaParser(); capellaParser.parseXml(xml, this.settings); Logger.debug(this.name, 'score.xml parsed'); - let score: Score = capellaParser.score; + const score: Score = capellaParser.score; return score; } catch (e) { throw new UnsupportedFormatError('Failed to parse CapXML', e as Error); diff --git a/src/importer/CapellaParser.ts b/src/importer/CapellaParser.ts index 7a5712804..549ce3e43 100644 --- a/src/importer/CapellaParser.ts +++ b/src/importer/CapellaParser.ts @@ -1,7 +1,7 @@ import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { AccentuationType } from '@src/model/AccentuationType'; import { Automation, AutomationType } from '@src/model/Automation'; -import { Bar } from '@src/model/Bar'; +import { Bar, BarLineStyle } from '@src/model/Bar'; import { Beat, BeatBeamingMode } from '@src/model/Beat'; import { Chord } from '@src/model/Chord'; import { Clef } from '@src/model/Clef'; @@ -10,14 +10,14 @@ import { Duration } from '@src/model/Duration'; import { MasterBar } from '@src/model/MasterBar'; import { Note } from '@src/model/Note'; import { Score } from '@src/model/Score'; -import { Staff } from '@src/model/Staff'; +import type { Staff } from '@src/model/Staff'; import { Track } from '@src/model/Track'; import { VibratoType } from '@src/model/VibratoType'; import { Voice } from '@src/model/Voice'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { XmlDocument } from '@src/xml/XmlDocument'; -import { XmlNode, XmlNodeType } from '@src/xml/XmlNode'; +import type { XmlNode } from '@src/xml/XmlNode'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { TextAlign } from '@src/platform/ICanvas'; import { ModelUtils } from '@src/model/ModelUtils'; @@ -25,7 +25,7 @@ import { Logger } from '@src/Logger'; import { Fermata, FermataType } from '@src/model/Fermata'; import { DynamicValue } from '@src/model/DynamicValue'; import { Ottavia } from '@src/model/Ottavia'; -import { KeySignature } from '@src/model/KeySignature'; +import type { KeySignature } from '@src/model/KeySignature'; class DrawObject { public noteRange: number = 1; @@ -34,10 +34,10 @@ class DrawObject { } enum FrameType { - None, - Rectangle, - Ellipse, - Circle + None = 0, + Rectangle = 1, + Ellipse = 2, + Circle = 3 } class TextDrawObject extends DrawObject { @@ -53,9 +53,9 @@ class GuitarDrawObject extends DrawObject { public chord: Chord = new Chord(); } -class SlurDrawObject extends DrawObject { } +class SlurDrawObject extends DrawObject {} -class WavyLineDrawObject extends DrawObject { } +class WavyLineDrawObject extends DrawObject {} class TupletBracketDrawObject extends DrawObject { public number: number = 0; @@ -75,7 +75,7 @@ class OctaveClefDrawObject extends DrawObject { public octave: number = 1; } -class TrillDrawObject extends DrawObject { } +class TrillDrawObject extends DrawObject {} class StaffLayout { public defaultClef: Clef = Clef.G2; @@ -122,7 +122,7 @@ export class CapellaParser { this._crescendo = new Map(); this._isFirstSystem = true; - let dom: XmlDocument = new XmlDocument(); + const dom: XmlDocument = new XmlDocument(); try { dom.parse(xml); } catch (e) { @@ -137,31 +137,7 @@ export class CapellaParser { } private consolidate() { - // voice counts and contents might be inconsistent - // we need to ensure we have an equal amount of voices across all bars - // and voices must contain an empty beat at minimum - for (const track of this.score.tracks) { - const trackVoiceCount = this._voiceCounts.get(track.index)!; - for (const staff of track.staves) { - while (staff.bars.length < this.score.masterBars.length) { - this.addNewBar(staff); - } - - for (const bar of staff.bars) { - while (bar.voices.length < trackVoiceCount) { - bar.addVoice(new Voice()); - } - - for (const voice of bar.voices) { - if (voice.beats.length === 0) { - const emptyBeat = new Beat(); - emptyBeat.isEmpty = true; - voice.addBeat(emptyBeat); - } - } - } - } - } + ModelUtils.consolidate(this.score); CapellaParser.applyEffectRange(this._slurs, (_, beat) => { beat.isLegatoOrigin = true; @@ -176,7 +152,7 @@ export class CapellaParser { effects: Map, applyEffect: (effect: T, beat: Beat) => void ) { - for(const [startBeat, effect] of effects) { + for (const [startBeat, effect] of effects) { const noteRange = effect.noteRange; let endBeat = startBeat; for (let i = 0; i < noteRange; i++) { @@ -195,7 +171,7 @@ export class CapellaParser { } private parseDom(dom: XmlDocument): void { - let root: XmlNode | null = dom.firstElement; + const root: XmlNode | null = dom.firstElement; if (!root) { throw new UnsupportedFormatError('No valid XML'); } @@ -203,26 +179,24 @@ export class CapellaParser { this.score = new Score(); this.score.tempo = 120; // parse all children - for (let n of root.childNodes) { - if (n.nodeType === XmlNodeType.Element) { - switch (n.localName) { - case 'info': - this.parseInfo(n); - break; - case 'layout': - this.parseLayout(n); - break; - case 'gallery': - this.parseGallery(n); - break; - case 'pageObjects': - this.parsePageObjects(n); - break; - // barCount ignored - case 'systems': - this.parseSystems(n); - break; - } + for (const n of root.childElements()) { + switch (n.localName) { + case 'info': + this.parseInfo(n); + break; + case 'layout': + this.parseLayout(n); + break; + case 'gallery': + this.parseGallery(n); + break; + case 'pageObjects': + this.parsePageObjects(n); + break; + // barCount ignored + case 'systems': + this.parseSystems(n); + break; } } } else { @@ -232,16 +206,14 @@ export class CapellaParser { private _staffLookup: Map = new Map(); private parseLayout(element: XmlNode) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'staves': - this.parseLayoutStaves(c); - break; - case 'brackets': - this.parseBrackets(c); - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'staves': + this.parseLayoutStaves(c); + break; + case 'brackets': + this.parseBrackets(c); + break; } } @@ -298,21 +270,19 @@ export class CapellaParser { private _brackets: Bracket[] = []; private parseBrackets(element: XmlNode) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'bracket': - this.parseBracket(c); - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'bracket': + this.parseBracket(c); + break; } } } private parseBracket(element: XmlNode) { const bracket = new Bracket(); - bracket.from = parseInt(element.getAttribute('from')); - bracket.to = parseInt(element.getAttribute('to')); + bracket.from = Number.parseInt(element.getAttribute('from')); + bracket.to = Number.parseInt(element.getAttribute('to')); if (element.attributes.has('curly')) { bracket.curly = element.attributes.get('curly') === 'true'; } @@ -320,13 +290,11 @@ export class CapellaParser { } private parseLayoutStaves(element: XmlNode) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'staffLayout': - this.parseStaffLayout(c); - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'staffLayout': + this.parseStaffLayout(c); + break; } } } @@ -338,30 +306,28 @@ export class CapellaParser { const layout = new StaffLayout(); layout.description = element.getAttribute('description'); - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'notation': - if (c.attributes.has('defaultClef')) { - layout.defaultClef = this.parseClef(c.attributes.get('defaultClef')!); - } - break; + for (const c of element.childElements()) { + switch (c.localName) { + case 'notation': + if (c.attributes.has('defaultClef')) { + layout.defaultClef = this.parseClef(c.attributes.get('defaultClef')!); + } + break; - case 'sound': - if (c.attributes.has('percussion')) { - layout.percussion = c.attributes.get('percussion') === 'true'; - } - if (c.attributes.has('instr')) { - layout.instrument = parseInt(c.attributes.get('instr')!); - } - if (c.attributes.has('volume')) { - layout.volume = parseInt(c.attributes.get('volume')!); - } - if (c.attributes.has('transpose')) { - layout.transpose = parseInt(c.attributes.get('transpose')!); - } - break; - } + case 'sound': + if (c.attributes.has('percussion')) { + layout.percussion = c.attributes.get('percussion') === 'true'; + } + if (c.attributes.has('instr')) { + layout.instrument = Number.parseInt(c.attributes.get('instr')!); + } + if (c.attributes.has('volume')) { + layout.volume = Number.parseInt(c.attributes.get('volume')!); + } + if (c.attributes.has('transpose')) { + layout.transpose = Number.parseInt(c.attributes.get('transpose')!); + } + break; } } @@ -387,7 +353,8 @@ export class CapellaParser { private parseClefOttava(v: string): Ottavia { if (v.endsWith('-')) { return Ottavia._8vb; - } else if (v.endsWith('+')) { + } + if (v.endsWith('+')) { return Ottavia._8va; } @@ -395,13 +362,11 @@ export class CapellaParser { } private parseSystems(element: XmlNode) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'system': - this.parseSystem(c); - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'system': + this.parseSystem(c); + break; } } } @@ -409,7 +374,7 @@ export class CapellaParser { private parseSystem(element: XmlNode) { if (element.attributes.has('tempo')) { if (this.score.masterBars.length === 0) { - this.score.tempo = parseInt(element.attributes.get('tempo')!); + this.score.tempo = Number.parseInt(element.attributes.get('tempo')!); } } @@ -417,13 +382,11 @@ export class CapellaParser { this._beamingMode = BeatBeamingMode.ForceSplitToNext; } - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'staves': - this.parseStaves(element, c); - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'staves': + this.parseStaves(element, c); + break; } } @@ -431,14 +394,12 @@ export class CapellaParser { } private parseStaves(systemElement: XmlNode, element: XmlNode) { - let firstBarIndex = this.score.masterBars.length; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'staff': - this.parseStaff(systemElement, firstBarIndex, c); - break; - } + const firstBarIndex = this.score.masterBars.length; + for (const c of element.childElements()) { + switch (c.localName) { + case 'staff': + this.parseStaff(systemElement, firstBarIndex, c); + break; } } } @@ -463,13 +424,11 @@ export class CapellaParser { this.addNewBar(staff); } - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'voices': - this.parseVoices(staffId, staff, systemElement, firstBarIndex, c); - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'voices': + this.parseVoices(staffId, staff, systemElement, firstBarIndex, c); + break; } } } @@ -489,8 +448,8 @@ export class CapellaParser { default: if (value.indexOf('/') > 0) { const parts = value.split('/'); - this._timeSignature.timeSignatureNumerator = parseInt(parts[0]); - this._timeSignature.timeSignatureDenominator = parseInt(parts[1]); + this._timeSignature.timeSignatureNumerator = Number.parseInt(parts[0]); + this._timeSignature.timeSignatureDenominator = Number.parseInt(parts[1]); this._timeSignature.timeSignatureCommon = false; } break; @@ -504,14 +463,12 @@ export class CapellaParser { element: XmlNode ) { let voiceIndex = 0; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'voice': - this.parseVoice(staffId, staff, systemElement, voiceIndex, firstBarIndex, c); - voiceIndex++; - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'voice': + this.parseVoice(staffId, staff, systemElement, voiceIndex, firstBarIndex, c); + voiceIndex++; + break; } } } @@ -525,10 +482,13 @@ export class CapellaParser { private addNewBar(staff: Staff) { // voice tags always start a new bar - let currentBar: Bar = new Bar(); + const currentBar: Bar = new Bar(); if (staff.bars.length > 0) { currentBar.clef = staff.bars[staff.bars.length - 1].clef; currentBar.clefOttava = staff.bars[staff.bars.length - 1].clefOttava; + currentBar.keySignature = staff.bars[staff.bars.length - 1].keySignature; + currentBar.keySignatureType = staff.bars[staff.bars.length - 1].keySignatureType; + } else { currentBar.clef = this._currentStaffLayout.defaultClef; } @@ -536,11 +496,9 @@ export class CapellaParser { // create masterbar if needed if (staff.bars.length > this.score.masterBars.length) { - let master: MasterBar = new MasterBar(); + const master: MasterBar = new MasterBar(); this.score.addMasterBar(master); if (master.index > 0) { - master.keySignature = master.previousMasterBar!.keySignature; - master.keySignatureType = master.previousMasterBar!.keySignatureType; master.tripletFeel = master.previousMasterBar!.tripletFeel; } @@ -573,7 +531,7 @@ export class CapellaParser { firstBarIndex: number, element: XmlNode ) { - const voiceStateKey = staffId + '_' + voiceIndex; + const voiceStateKey = `${staffId}_${voiceIndex}`; if (this._currentVoiceState && !this._currentVoiceState.currentBarComplete) { this._currentBar.masterBar.isAnacrusis = true; } @@ -609,142 +567,135 @@ export class CapellaParser { const noteObjects = element.findChildElement('noteObjects'); if (systemElement.attributes.has('tempo')) { - const automation = new Automation() + const automation = new Automation(); automation.isLinear = true; automation.type = AutomationType.Tempo; - automation.value = parseInt(systemElement.attributes.get('tempo')!); - automation.ratioPosition = this._currentVoiceState.currentPosition / this._currentVoiceState.currentBarDuration; - this._currentBar.masterBar.tempoAutomations.push(automation); + automation.value = Number.parseInt(systemElement.attributes.get('tempo')!); + automation.ratioPosition = + this._currentVoiceState.currentPosition / this._currentVoiceState.currentBarDuration; + this._currentBar.masterBar.tempoAutomations.push(automation); } if (noteObjects) { - for (let c of noteObjects.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - if (this._currentVoiceState.currentBarComplete && c.localName !== 'barline') { - this.newBar(staff, voiceIndex); - } + for (const c of noteObjects.childElements()) { + if (this._currentVoiceState.currentBarComplete && c.localName !== 'barline') { + this.newBar(staff, voiceIndex); + } - switch (c.localName) { - case 'clefSign': - this._currentBar.clef = this.parseClef(c.getAttribute('clef')); - this._currentBar.clefOttava = this.parseClefOttava(c.getAttribute('clef')); - break; - case 'keySign': - this._currentBar.masterBar.keySignature = parseInt( - c.getAttribute('fifths') - ) as KeySignature; - break; - case 'timeSign': - this.parseTime(c.getAttribute('time')); - this._currentBar.masterBar.timeSignatureDenominator = this._timeSignature.timeSignatureDenominator; - this._currentBar.masterBar.timeSignatureNumerator = this._timeSignature.timeSignatureNumerator; - this._currentBar.masterBar.timeSignatureCommon = this._timeSignature.timeSignatureCommon; - // NOTE: capella resets the current bar position to 0 whenever a timeSign is placed - this._currentVoiceState.currentPosition = 0; - this._currentVoiceState.currentBarDuration = this._currentBar.masterBar.calculateDuration( - false - ); - break; - case 'barline': - switch (c.getAttribute('type')) { - case 'double': - this._currentBar.masterBar.isDoubleBar = true; - if (!this._currentVoiceState.currentBarComplete) { - this._currentBar.masterBar.isAnacrusis = true; - } - this._currentVoiceState.currentBarComplete = true; - break; - case 'end': - if (!this._currentVoiceState.currentBarComplete) { - this._currentBar.masterBar.isAnacrusis = true; - } - break; - case 'repEnd': - this._currentVoiceState.repeatEnd = this._currentBar.masterBar; - if (this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) { - this._currentBar.masterBar.repeatCount = this._currentVoiceState.repeatCount; - } - this.parseBarDrawObject(c); - if (!this._currentVoiceState.currentBarComplete) { - this._currentBar.masterBar.isAnacrusis = true; - } - this._currentVoiceState.currentBarComplete = true; - break; - case 'repBegin': - this.newBar(staff, voiceIndex); // repeat-start requires instant new bar - this._currentBar.masterBar.isRepeatStart = true; - this._currentVoiceState.repeatEnd = null; - this._currentVoiceState.repeatCount = 0; - break; - case 'repEndBegin': - this._currentVoiceState.repeatEnd = this._currentBar.masterBar; - if (this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) { - this._currentBar.masterBar.repeatCount = this._currentVoiceState.repeatCount; - } - this.parseBarDrawObject(c); - this.newBar(staff, voiceIndex); // end-begin requires instant new bar - this._currentBar.masterBar.isRepeatStart = true; - break; - case 'dashed': - if (!this._currentVoiceState.currentBarComplete) { - this._currentBar.masterBar.isAnacrusis = true; - } - this._currentVoiceState.currentBarComplete = true; - break; - // case 'single': - default: - if (!this._currentVoiceState.currentBarComplete) { - this._currentBar.masterBar.isAnacrusis = true; - } - this._currentVoiceState.currentBarComplete = true; - break; - } - break; - case 'chord': - let chordBeat = new Beat(); - this.initFromPreviousBeat(chordBeat, this._currentVoice); - chordBeat.beamingMode = this._beamingMode; - if (this._currentVoiceState.voiceStemDir) { - chordBeat.preferredBeamDirection = this._currentVoiceState.voiceStemDir; - } - this.parseDuration(this._currentBar, chordBeat, c.findChildElement('duration')!); - chordBeat.updateDurations(); - this._currentVoiceState.currentPosition += chordBeat.playbackDuration; - this._currentVoice.addBeat(chordBeat); + switch (c.localName) { + case 'clefSign': + this._currentBar.clef = this.parseClef(c.getAttribute('clef')); + this._currentBar.clefOttava = this.parseClefOttava(c.getAttribute('clef')); + break; + case 'keySign': + this._currentBar.keySignature = Number.parseInt( + c.getAttribute('fifths') + ) as KeySignature; + break; + case 'timeSign': + this.parseTime(c.getAttribute('time')); + this._currentBar.masterBar.timeSignatureDenominator = + this._timeSignature.timeSignatureDenominator; + this._currentBar.masterBar.timeSignatureNumerator = this._timeSignature.timeSignatureNumerator; + this._currentBar.masterBar.timeSignatureCommon = this._timeSignature.timeSignatureCommon; + // NOTE: capella resets the current bar position to 0 whenever a timeSign is placed + this._currentVoiceState.currentPosition = 0; + this._currentVoiceState.currentBarDuration = + this._currentBar.masterBar.calculateDuration(false); + break; + case 'barline': + switch (c.getAttribute('type')) { + case 'double': + this._currentBar.barLineRight = BarLineStyle.LightLight; + if (!this._currentVoiceState.currentBarComplete) { + this._currentBar.masterBar.isAnacrusis = true; + } + this._currentVoiceState.currentBarComplete = true; + break; + case 'end': + if (!this._currentVoiceState.currentBarComplete) { + this._currentBar.masterBar.isAnacrusis = true; + } + break; + case 'repEnd': + this._currentVoiceState.repeatEnd = this._currentBar.masterBar; + if (this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) { + this._currentBar.masterBar.repeatCount = this._currentVoiceState.repeatCount; + } + this.parseBarDrawObject(c); + if (!this._currentVoiceState.currentBarComplete) { + this._currentBar.masterBar.isAnacrusis = true; + } + this._currentVoiceState.currentBarComplete = true; + break; + case 'repBegin': + this.newBar(staff, voiceIndex); // repeat-start requires instant new bar + this._currentBar.masterBar.isRepeatStart = true; + this._currentVoiceState.repeatEnd = null; + this._currentVoiceState.repeatCount = 0; + break; + case 'repEndBegin': + this._currentVoiceState.repeatEnd = this._currentBar.masterBar; + if (this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) { + this._currentBar.masterBar.repeatCount = this._currentVoiceState.repeatCount; + } + this.parseBarDrawObject(c); + this.newBar(staff, voiceIndex); // end-begin requires instant new bar + this._currentBar.masterBar.isRepeatStart = true; + break; + case 'dashed': + if (!this._currentVoiceState.currentBarComplete) { + this._currentBar.masterBar.isAnacrusis = true; + } + this._currentVoiceState.currentBarComplete = true; + break; + // case 'single': + default: + if (!this._currentVoiceState.currentBarComplete) { + this._currentBar.masterBar.isAnacrusis = true; + } + this._currentVoiceState.currentBarComplete = true; + break; + } + break; + case 'chord': + const chordBeat = new Beat(); + this.initFromPreviousBeat(chordBeat, this._currentVoice); + chordBeat.beamingMode = this._beamingMode; + if (this._currentVoiceState.voiceStemDir) { + chordBeat.preferredBeamDirection = this._currentVoiceState.voiceStemDir; + } + this.parseDuration(this._currentBar, chordBeat, c.findChildElement('duration')!); + chordBeat.updateDurations(); + this._currentVoiceState.currentPosition += chordBeat.playbackDuration; + this._currentVoice.addBeat(chordBeat); - this.parseChord(chordBeat, c); + this.parseChord(chordBeat, c); + + if (this._currentVoiceState.currentPosition >= this._currentVoiceState.currentBarDuration) { + this._currentVoiceState.currentBarComplete = true; + } + break; + case 'rest': + const restBeat = this.parseRestDurations(this._currentBar, c.findChildElement('duration')!); + if (restBeat) { + this.initFromPreviousBeat(restBeat, this._currentVoice); + restBeat.updateDurations(); + this._currentVoiceState.currentPosition += restBeat.playbackDuration; + this._currentVoice.addBeat(restBeat); if (this._currentVoiceState.currentPosition >= this._currentVoiceState.currentBarDuration) { this._currentVoiceState.currentBarComplete = true; } - break; - case 'rest': - const restBeat = this.parseRestDurations( - this._currentBar, - c.findChildElement('duration')! - ); - if (restBeat) { - this.initFromPreviousBeat(restBeat, this._currentVoice); - restBeat.updateDurations(); - this._currentVoiceState.currentPosition += restBeat.playbackDuration; - this._currentVoice.addBeat(restBeat); - - if ( - this._currentVoiceState.currentPosition >= - this._currentVoiceState.currentBarDuration - ) { - this._currentVoiceState.currentBarComplete = true; - } - } - break; - } + } + break; } } } } private initFromPreviousBeat(chordBeat: Beat, currentVoice: Voice) { - let previousBeat = this.getLastBeat(currentVoice); + const previousBeat = this.getLastBeat(currentVoice); if (previousBeat) { chordBeat.dynamics = previousBeat.dynamics; } @@ -753,7 +704,8 @@ export class CapellaParser { private getLastBeat(voice: Voice): Beat | null { if (voice.beats.length > 0) { return voice.beats[voice.beats.length - 1]; - } else if (voice.bar.index > 0) { + } + if (voice.bar.index > 0) { const previousBar = voice.bar.staff.bars[voice.bar.index - 1]; if (voice.index < previousBar.voices.length) { const previousVoice = previousBar.voices[voice.index]; @@ -780,65 +732,61 @@ export class CapellaParser { private parseChord(beat: Beat, element: XmlNode) { const articulation: Note = new Note(); - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'stem': - switch (c.getAttribute('dir')) { - case 'up': - beat.preferredBeamDirection = BeamDirection.Up; - break; - case 'down': - beat.preferredBeamDirection = BeamDirection.Down; - break; - } - break; - case 'articulation': - switch (c.getAttribute('type')) { - case 'staccato': - articulation.isStaccato = true; - break; - case 'normalAccent': - articulation.accentuated = AccentuationType.Normal; - break; - case 'strongAccent': - articulation.accentuated = AccentuationType.Heavy; - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'stem': + switch (c.getAttribute('dir')) { + case 'up': + beat.preferredBeamDirection = BeamDirection.Up; + break; + case 'down': + beat.preferredBeamDirection = BeamDirection.Down; + break; + } + break; + case 'articulation': + switch (c.getAttribute('type')) { + case 'staccato': + articulation.isStaccato = true; + break; + case 'normalAccent': + articulation.accentuated = AccentuationType.Normal; + break; + case 'strongAccent': + articulation.accentuated = AccentuationType.Heavy; + break; + } - break; - case 'lyric': - this.parseLyric(beat, c); - break; - case 'drawObjects': - this.parseBeatDrawObject(beat, c); - break; - case 'heads': - this.parseHeads(beat, articulation, c); - break; - case 'beam': - switch (c.getAttribute('group')) { - case 'force': - beat.beamingMode = BeatBeamingMode.ForceMergeWithNext; - break; - case 'divide': - beat.beamingMode = BeatBeamingMode.ForceSplitToNext; - break; - } - break; - } + break; + case 'lyric': + this.parseLyric(beat, c); + break; + case 'drawObjects': + this.parseBeatDrawObject(beat, c); + break; + case 'heads': + this.parseHeads(beat, articulation, c); + break; + case 'beam': + switch (c.getAttribute('group')) { + case 'force': + beat.beamingMode = BeatBeamingMode.ForceMergeWithNext; + break; + case 'divide': + beat.beamingMode = BeatBeamingMode.ForceSplitToNext; + break; + } + break; } } } private parseHeads(beat: Beat, articulation: Note, element: XmlNode) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'head': - this.parseHead(beat, articulation, c); - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'head': + this.parseHead(beat, articulation, c); + break; } } } @@ -860,110 +808,104 @@ export class CapellaParser { // TODO: based on the shape attribute apply effects or // right percussion value - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'alter': - if (c.attributes.has('step')) { - note.tone += parseInt(c.attributes.get('step')!); - } - break; - case 'tie': - if (c.attributes.has('begin')) { - if (!this._tieStartIds.has(note.id)) { - this._tieStartIds.set(note.id, true); - this._tieStarts.push(note); - } - } else if (c.attributes.has('end') && this._tieStarts.length > 0 && !note.isTieDestination) { - note.isTieDestination = true; - note.tieOrigin = this._tieStarts[0]; - this._tieStarts.splice(0, 1); - this._tieStartIds.delete(note.id); + for (const c of element.childElements()) { + switch (c.localName) { + case 'alter': + if (c.attributes.has('step')) { + note.tone += Number.parseInt(c.attributes.get('step')!); + } + break; + case 'tie': + if (c.attributes.has('begin')) { + if (!this._tieStartIds.has(note.id)) { + this._tieStartIds.set(note.id, true); + this._tieStarts.push(note); } - break; - } + } else if (c.attributes.has('end') && this._tieStarts.length > 0 && !note.isTieDestination) { + note.isTieDestination = true; + note.tieOrigin = this._tieStarts[0]; + this._tieStarts.splice(0, 1); + this._tieStartIds.delete(note.id); + } + break; } } } private parseBeatDrawObject(beat: Beat, element: XmlNode) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'drawObj': - const obj = this.parseDrawObj(c); - if (obj) { - if (obj instanceof TextDrawObject) { - if (obj.fontFace.startsWith('capella')) { - if (obj.text === 'u') { - beat.fermata = new Fermata(); - beat.fermata.type = FermataType.Medium; - } else if (obj.text === 'f') { - beat.dynamics = DynamicValue.F; - } else if (obj.text === 'j') { - beat.dynamics = DynamicValue.MF; - } - } else if ( - this._isFirstSystem && - this.score.title === '' && - obj.align === TextAlign.Center && - obj.height > 16 && - obj.weight > 400 - ) { - // bold large centered text is very likely the title - this.score.title = obj.text; - } else if ( - this._isFirstSystem && - this.score.artist === '' && - obj.align === TextAlign.Center && - obj.y < 0 - ) { - this.score.artist = obj.text; - } else if ( - this._isFirstSystem && - this.score.music === '' && - obj.align === TextAlign.Right && - obj.y < 0 - ) { - this.score.music = obj.text; - } else if (!obj.text.startsWith('by capella')) { - beat.text = obj.text; + for (const c of element.childElements()) { + switch (c.localName) { + case 'drawObj': + const obj = this.parseDrawObj(c); + if (obj) { + if (obj instanceof TextDrawObject) { + if (obj.fontFace.startsWith('capella')) { + if (obj.text === 'u') { + beat.fermata = new Fermata(); + beat.fermata.type = FermataType.Medium; + } else if (obj.text === 'f') { + beat.dynamics = DynamicValue.F; + } else if (obj.text === 'j') { + beat.dynamics = DynamicValue.MF; } - } else if (obj instanceof GuitarDrawObject) { - // TODO: Chord - } else if (obj instanceof WavyLineDrawObject) { - beat.vibrato = VibratoType.Slight; - } else if (obj instanceof WedgeDrawObject) { - beat.crescendo = obj.decrescendo ? CrescendoType.Decrescendo : CrescendoType.Crescendo; - obj.noteRange++; - this._crescendo.set(beat, obj); - } else if (obj instanceof SlurDrawObject) { - // NOTE: casting needed for C# - const slur = obj as any as SlurDrawObject; - this._slurs.set(beat, slur); - } else if (obj instanceof VoltaDrawObject) { - this.applyVolta(obj); + } else if ( + this._isFirstSystem && + this.score.title === '' && + obj.align === TextAlign.Center && + obj.height > 16 && + obj.weight > 400 + ) { + // bold large centered text is very likely the title + this.score.title = obj.text; + } else if ( + this._isFirstSystem && + this.score.artist === '' && + obj.align === TextAlign.Center && + obj.y < 0 + ) { + this.score.artist = obj.text; + } else if ( + this._isFirstSystem && + this.score.music === '' && + obj.align === TextAlign.Right && + obj.y < 0 + ) { + this.score.music = obj.text; + } else if (!obj.text.startsWith('by capella')) { + beat.text = obj.text; } + } else if (obj instanceof GuitarDrawObject) { + // TODO: Chord + } else if (obj instanceof WavyLineDrawObject) { + beat.vibrato = VibratoType.Slight; + } else if (obj instanceof WedgeDrawObject) { + beat.crescendo = obj.decrescendo ? CrescendoType.Decrescendo : CrescendoType.Crescendo; + obj.noteRange++; + this._crescendo.set(beat, obj); + } else if (obj instanceof SlurDrawObject) { + // NOTE: casting needed for C# + const slur = obj as any as SlurDrawObject; + this._slurs.set(beat, slur); + } else if (obj instanceof VoltaDrawObject) { + this.applyVolta(obj); } - break; - } + } + break; } } } private parseBarDrawObject(element: XmlNode) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'drawObj': - const obj = this.parseDrawObj(c); - if (obj) { - if (obj instanceof VoltaDrawObject) { - this.applyVolta(obj); - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'drawObj': + const obj = this.parseDrawObj(c); + if (obj) { + if (obj instanceof VoltaDrawObject) { + this.applyVolta(obj); } - break; - } + } + break; } } } @@ -971,14 +913,18 @@ export class CapellaParser { private applyVolta(obj: VoltaDrawObject) { if (obj.lastNumber > 0) { this._currentVoiceState.repeatCount = obj.lastNumber; - if (this._currentVoiceState.repeatEnd && - this._currentVoiceState.repeatEnd.repeatCount < this._currentVoiceState.repeatCount) { + if ( + this._currentVoiceState.repeatEnd && + this._currentVoiceState.repeatEnd.repeatCount < this._currentVoiceState.repeatCount + ) { this._currentVoiceState.repeatEnd.repeatCount = this._currentVoiceState.repeatCount; } } else if (obj.firstNumber > 0) { this._currentVoiceState.repeatCount = obj.firstNumber; - if (this._currentVoiceState.repeatEnd && - this._currentVoiceState.repeatEnd.repeatCount < this._currentVoiceState.repeatCount) { + if ( + this._currentVoiceState.repeatEnd && + this._currentVoiceState.repeatEnd.repeatCount < this._currentVoiceState.repeatCount + ) { this._currentVoiceState.repeatEnd.repeatCount = this._currentVoiceState.repeatCount; } } @@ -986,7 +932,7 @@ export class CapellaParser { if (obj.lastNumber > 0 && obj.firstNumber > 0) { let alternateEndings = 0; for (let i = obj.firstNumber; i <= obj.lastNumber; i++) { - alternateEndings = alternateEndings | (0x01 << (i - 1)); + alternateEndings = alternateEndings | (0x01 << (i - 1)); } this._currentBar.masterBar.alternateEndings = alternateEndings; } else if (obj.lastNumber > 0) { @@ -997,20 +943,18 @@ export class CapellaParser { } private parseLyric(beat: Beat, element: XmlNode) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'verse': - if (!beat.lyrics) { - beat.lyrics = []; - } - let text = c.innerText; - if (c.getAttribute('hyphen') === 'true') { - text += '-'; - } - beat.lyrics.push(text); - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'verse': + if (!beat.lyrics) { + beat.lyrics = []; + } + let text = c.innerText; + if (c.getAttribute('hyphen') === 'true') { + text += '-'; + } + beat.lyrics.push(text); + break; } } } @@ -1018,24 +962,23 @@ export class CapellaParser { private parseRestDurations(bar: Bar, element: XmlNode): Beat | null { const durationBase = element.getAttribute('base'); if (durationBase.indexOf('/') !== -1) { - let restBeat = new Beat(); + const restBeat = new Beat(); restBeat.beamingMode = this._beamingMode; this.parseDuration(bar, restBeat, element); return restBeat; } // for - const fullBars = parseInt(durationBase); + const fullBars = Number.parseInt(durationBase); if (fullBars === 1) { - let restBeat = new Beat(); + const restBeat = new Beat(); restBeat.beamingMode = this._beamingMode; restBeat.duration = Duration.Whole; return restBeat; - } else { - // TODO: multibar rests - Logger.warning('Importer', `Multi-Bar rests are not supported`); - return null; } + // TODO: multibar rests + Logger.warning('Importer', 'Multi-Bar rests are not supported'); + return null; } private parseDurationValue(s: string): Duration { @@ -1069,12 +1012,12 @@ export class CapellaParser { beat.duration = this.parseDurationValue(durationBase); if (element.attributes.has('dots')) { - beat.dots = parseInt(element.attributes.get('dots')!); + beat.dots = Number.parseInt(element.attributes.get('dots')!); } const tuplet = element.findChildElement('tuplet'); if (tuplet) { - beat.tupletNumerator = parseInt(tuplet.getAttribute('count')); + beat.tupletNumerator = Number.parseInt(tuplet.getAttribute('count')); const tripartiteMultiplicator = tuplet.getAttribute('tripartite') === 'true' ? 3 : 1; const prolongDiff = tuplet.getAttribute('prolong') === 'true' ? 0 : 1; @@ -1087,47 +1030,43 @@ export class CapellaParser { } private parsePageObjects(element: XmlNode) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'drawObj': - const obj = this.parseDrawObj(c); - if (obj) { - if (obj instanceof TextDrawObject) { - switch (obj.align) { - case TextAlign.Center: - if (!this.score.title) { - this.score.title = c.innerText; - } else if (!this.score.subTitle) { - this.score.subTitle = c.innerText; - } - break; - case TextAlign.Right: - if (!this.score.artist) { - this.score.artist = c.innerText; - } - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'drawObj': + const obj = this.parseDrawObj(c); + if (obj) { + if (obj instanceof TextDrawObject) { + switch (obj.align) { + case TextAlign.Center: + if (!this.score.title) { + this.score.title = c.innerText; + } else if (!this.score.subTitle) { + this.score.subTitle = c.innerText; + } + break; + case TextAlign.Right: + if (!this.score.artist) { + this.score.artist = c.innerText; + } + break; } } + } - break; - } + break; } } } private parseGallery(element: XmlNode) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'drawObj': - const obj = this.parseDrawObj(c); - if (obj) { - this._galleryObjects.set(c.getAttribute('name'), obj); - } - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'drawObj': + const obj = this.parseDrawObj(c); + if (obj) { + this._galleryObjects.set(c.getAttribute('name'), obj); + } + break; } } } @@ -1137,42 +1076,40 @@ export class CapellaParser { let noteRange = 1; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'text': - obj = this.parseText(c); - break; - case 'guitar': - obj = this.parseGuitar(c); - break; - case 'slur': - obj = this.parseSlur(c); - break; - case 'wavyLine': - obj = this.parseWavyLine(c); - break; - case 'bracket': - obj = this.parseTupletBracket(c); - break; - case 'wedge': - obj = this.parseWedge(c); - break; - case 'volta': - obj = this.parseVolta(c); - break; - case 'octaveClef': - obj = this.parseOctaveClef(c); - break; - case 'trill': - obj = this.parseTrill(c); - break; - case 'basic': - if (c.attributes.has('noteRange')) { - noteRange = parseInt(c.attributes.get('noteRange')!); - } - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'text': + obj = this.parseText(c); + break; + case 'guitar': + obj = this.parseGuitar(c); + break; + case 'slur': + obj = this.parseSlur(c); + break; + case 'wavyLine': + obj = this.parseWavyLine(c); + break; + case 'bracket': + obj = this.parseTupletBracket(c); + break; + case 'wedge': + obj = this.parseWedge(c); + break; + case 'volta': + obj = this.parseVolta(c); + break; + case 'octaveClef': + obj = this.parseOctaveClef(c); + break; + case 'trill': + obj = this.parseTrill(c); + break; + case 'basic': + if (c.attributes.has('noteRange')) { + noteRange = Number.parseInt(c.attributes.get('noteRange')!); + } + break; } } @@ -1192,7 +1129,7 @@ export class CapellaParser { const obj = new OctaveClefDrawObject(); if (element.attributes.has('octave')) { - obj.octave = parseInt(element.attributes.get('octave')!); + obj.octave = Number.parseInt(element.attributes.get('octave')!); } return obj; @@ -1203,10 +1140,10 @@ export class CapellaParser { obj.allNumbers = element.attributes.get('allNumbers') === 'true'; if (element.attributes.has('firstNumber')) { - obj.firstNumber = parseInt(element.attributes.get('firstNumber')!); + obj.firstNumber = Number.parseInt(element.attributes.get('firstNumber')!); } if (element.attributes.has('lastNumber')) { - obj.lastNumber = parseInt(element.attributes.get('lastNumber')!); + obj.lastNumber = Number.parseInt(element.attributes.get('lastNumber')!); } return obj; @@ -1224,7 +1161,7 @@ export class CapellaParser { const obj = new TupletBracketDrawObject(); if (element.attributes.has('number')) { - obj.number = parseInt(element.attributes.get('number')!); + obj.number = Number.parseInt(element.attributes.get('number')!); } return obj; @@ -1249,7 +1186,7 @@ export class CapellaParser { if (strings.charAt(i) === '/') { obj.chord.strings.push(0); } else { - obj.chord.strings.push(parseInt(strings.charAt(i))); + obj.chord.strings.push(Number.parseInt(strings.charAt(i))); } } @@ -1260,10 +1197,10 @@ export class CapellaParser { const obj = new TextDrawObject(); if (element.attributes.has('x')) { - obj.x = parseFloat(element.attributes.get('x')!); + obj.x = Number.parseFloat(element.attributes.get('x')!); } if (element.attributes.has('x')) { - obj.y = parseFloat(element.attributes.get('y')!); + obj.y = Number.parseFloat(element.attributes.get('y')!); } switch (element.getAttribute('align')) { @@ -1294,25 +1231,23 @@ export class CapellaParser { } if (element.firstElement) { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'font': - obj.fontFace = c.getAttribute('face'); - - if (c.attributes.has('weight')) { - obj.weight = parseInt(c.attributes.get('weight')!); - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'font': + obj.fontFace = c.getAttribute('face'); - if (c.attributes.has('height')) { - obj.height = parseInt(c.attributes.get('height')!); - } + if (c.attributes.has('weight')) { + obj.weight = Number.parseInt(c.attributes.get('weight')!); + } - break; - case 'content': - obj.text = c.innerText; - break; - } + if (c.attributes.has('height')) { + obj.height = Number.parseInt(c.attributes.get('height')!); + } + + break; + case 'content': + obj.text = c.innerText; + break; } } } else { @@ -1323,19 +1258,17 @@ export class CapellaParser { } private parseInfo(element: XmlNode): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - // encodingSoftware ignored - case 'author': - this.score.tab = c.firstChild!.innerText; - break; - // keywords ignored - case 'comment': - this.score.notices = c.firstChild!.innerText; - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + // encodingSoftware ignored + case 'author': + this.score.tab = c.firstChild!.innerText; + break; + // keywords ignored + case 'comment': + this.score.notices = c.firstChild!.innerText; + break; } } } -} \ No newline at end of file +} diff --git a/src/importer/Gp3To5Importer.ts b/src/importer/Gp3To5Importer.ts index 80be3189e..2cf4805a4 100644 --- a/src/importer/Gp3To5Importer.ts +++ b/src/importer/Gp3To5Importer.ts @@ -4,10 +4,10 @@ import { ScoreImporter } from '@src/importer/ScoreImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { IOHelper } from '@src/io/IOHelper'; -import { IReadable } from '@src/io/IReadable'; +import type { IReadable } from '@src/io/IReadable'; import { AccentuationType } from '@src/model/AccentuationType'; import { Automation, AutomationType } from '@src/model/Automation'; -import { Bar } from '@src/model/Bar'; +import { Bar, BarLineStyle } from '@src/model/Bar'; import { Beat, BeatBeamingMode } from '@src/model/Beat'; import { BendPoint } from '@src/model/BendPoint'; import { BrushType } from '@src/model/BrushType'; @@ -16,22 +16,22 @@ import { Clef } from '@src/model/Clef'; import { Color } from '@src/model/Color'; import { Duration } from '@src/model/Duration'; import { DynamicValue } from '@src/model/DynamicValue'; -import { Fingers } from '@src/model/Fingers'; +import type { Fingers } from '@src/model/Fingers'; import { GraceType } from '@src/model/GraceType'; import { HarmonicType } from '@src/model/HarmonicType'; -import { KeySignature } from '@src/model/KeySignature'; -import { KeySignatureType } from '@src/model/KeySignatureType'; +import type { KeySignature } from '@src/model/KeySignature'; +import type { KeySignatureType } from '@src/model/KeySignatureType'; import { Lyrics } from '@src/model/Lyrics'; import { MasterBar } from '@src/model/MasterBar'; import { Note } from '@src/model/Note'; import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; import { PickStroke } from '@src/model/PickStroke'; import { PlaybackInformation } from '@src/model/PlaybackInformation'; -import { Score } from '@src/model/Score'; +import { Score, ScoreSubElement } from '@src/model/Score'; import { Section } from '@src/model/Section'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; -import { Staff } from '@src/model/Staff'; +import type { Staff } from '@src/model/Staff'; import { Track } from '@src/model/Track'; import { TripletFeel } from '@src/model/TripletFeel'; import { VibratoType } from '@src/model/VibratoType'; @@ -39,13 +39,13 @@ import { Voice } from '@src/model/Voice'; import { Logger } from '@src/Logger'; import { ModelUtils } from '@src/model/ModelUtils'; -import { IWriteable } from '@src/io/IWriteable'; +import type { IWriteable } from '@src/io/IWriteable'; import { Tuning } from '@src/model/Tuning'; import { FadeType } from '@src/model/FadeType'; import { Rasgueado } from '@src/model/Rasgueado'; import { Direction } from '@src/model/Direction'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; -import { Ottavia } from '@src/model'; +import { Ottavia } from '@src/model/Ottavia'; import { WahPedal } from '@src/model/WahPedal'; export class Gp3To5Importer extends ScoreImporter { @@ -58,7 +58,11 @@ export class Gp3To5Importer extends ScoreImporter { private _barCount: number = 0; private _trackCount: number = 0; private _playbackInfos: PlaybackInformation[] = []; - + private _doubleBars: Set = new Set(); + private _keySignatures: Map = new Map< + number, + [KeySignature, KeySignatureType] + >(); private _beatTextChunksByTrack: Map = new Map(); private _directionLookup: Map = new Map(); @@ -67,10 +71,6 @@ export class Gp3To5Importer extends ScoreImporter { return 'Guitar Pro 3-5'; } - public constructor() { - super(); - } - public readScore(): Score { this._directionLookup.clear(); @@ -156,6 +156,7 @@ export class Gp3To5Importer extends ScoreImporter { this._score.masterBars[0].tempoAutomations.push(automation); } + ModelUtils.consolidate(this._score); this._score.finish(this.settings); if (this._lyrics && this._lyricsTrack >= 0) { this._score.tracks[this._lyricsTrack].applyLyrics(this._lyrics); @@ -189,9 +190,9 @@ export class Gp3To5Importer extends ScoreImporter { throw new UnsupportedFormatError('Unsupported format'); } version = version.substr(Gp3To5Importer.VersionString.length + 1); - let dot: number = version.indexOf(String.fromCharCode(46)); - this._versionNumber = 100 * parseInt(version.substr(0, dot)) + parseInt(version.substr(dot + 1)); - Logger.debug(this.name, 'Guitar Pro version ' + version + ' detected'); + const dot: number = version.indexOf(String.fromCharCode(46)); + this._versionNumber = 100 * Number.parseInt(version.substr(0, dot)) + Number.parseInt(version.substr(dot + 1)); + Logger.debug(this.name, `Guitar Pro version ${version} detected`); } public readScoreInformation(): void { @@ -207,7 +208,7 @@ export class Gp3To5Importer extends ScoreImporter { this._score.copyright = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); this._score.tab = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); this._score.instructions = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); - let noticeLines: number = IOHelper.readInt32LE(this.data); + const noticeLines: number = IOHelper.readInt32LE(this.data); let notice: string = ''; for (let i: number = 0; i < noticeLines; i++) { if (i > 0) { @@ -222,7 +223,7 @@ export class Gp3To5Importer extends ScoreImporter { this._lyrics = []; this._lyricsTrack = IOHelper.readInt32LE(this.data) - 1; for (let i: number = 0; i < 5; i++) { - let lyrics: Lyrics = new Lyrics(); + const lyrics: Lyrics = new Lyrics(); lyrics.startBar = IOHelper.readInt32LE(this.data) - 1; lyrics.text = GpBinaryHelpers.gpReadStringInt(this.data, this.settings.importer.encoding); this._lyrics.push(lyrics); @@ -237,28 +238,64 @@ export class Gp3To5Importer extends ScoreImporter { // Padding Top (4) // Padding Bottom (4) // Size Proportion(4) - // Header and Footer display flags (2) - this.data.skip(30); - // title format - // subtitle format - // artist format - // album format - // words format - // music format - // words and music format - // copyright format - // pagpublic enumber format - for (let i: number = 0; i < 10; i++) { + this.data.skip(28); + + const flags = IOHelper.readInt16LE(this.data); + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Title).isVisible = + (flags & (0x01 << 0)) !== 0; + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Title).template = GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); - } + + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.SubTitle).isVisible = + (flags & (0x01 << 1)) !== 0; + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.SubTitle).template = + GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Artist).isVisible = + (flags & (0x01 << 2)) !== 0; + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Artist).template = + GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Album).isVisible = + (flags & (0x01 << 3)) !== 0; + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Album).template = + GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Words).isVisible = + (flags & (0x01 << 4)) !== 0; + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Words).template = + GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Music).isVisible = + (flags & (0x01 << 5)) !== 0; + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Music).template = + GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.WordsAndMusic).isVisible = + (flags & (0x01 << 6)) !== 0; + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.WordsAndMusic).template = + GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Copyright).isVisible = + (flags & (0x01 << 7)) !== 0; + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Copyright).template = + GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.CopyrightSecondLine).isVisible = + (flags & (0x01 << 7)) !== 0; + ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.CopyrightSecondLine).template = + GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + // page number format + GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); } public readPlaybackInfos(): void { this._playbackInfos = []; + let channel = 0; for (let i: number = 0; i < 64; i++) { - let info: PlaybackInformation = new PlaybackInformation(); - info.primaryChannel = i; - info.secondaryChannel = i; + const info: PlaybackInformation = new PlaybackInformation(); + info.primaryChannel = channel++; + info.secondaryChannel = channel++; info.program = IOHelper.readInt32LE(this.data); info.volume = this.data.readByte(); info.balance = this.data.readByte(); @@ -278,8 +315,8 @@ export class Gp3To5Importer extends ScoreImporter { if (this._score.masterBars.length > 0) { previousMasterBar = this._score.masterBars[this._score.masterBars.length - 1]; } - let newMasterBar: MasterBar = new MasterBar(); - let flags: number = this.data.readByte(); + const newMasterBar: MasterBar = new MasterBar(); + const flags: number = this.data.readByte(); // time signature if ((flags & 0x01) !== 0) { newMasterBar.timeSignatureNumerator = this.data.readByte(); @@ -315,10 +352,10 @@ export class Gp3To5Importer extends ScoreImporter { } // now calculate the alternative for this bar let repeatAlternative: number = 0; - let repeatMask: number = this.data.readByte(); + const repeatMask: number = this.data.readByte(); for (let i: number = 0; i < 8; i++) { // only add the repeating if it is not existing - let repeating: number = 1 << i; + const repeating: number = 1 << i; if (repeatMask > i && (existentAlternatives & repeating) === 0) { repeatAlternative = repeatAlternative | repeating; } @@ -327,7 +364,7 @@ export class Gp3To5Importer extends ScoreImporter { } // marker if ((flags & 0x20) !== 0) { - let section: Section = new Section(); + const section: Section = new Section(); section.text = GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); section.marker = ''; GpBinaryHelpers.gpReadColor(this.data, false); @@ -335,11 +372,10 @@ export class Gp3To5Importer extends ScoreImporter { } // keysignature if ((flags & 0x40) !== 0) { - newMasterBar.keySignature = IOHelper.readSInt8(this.data) as KeySignature; - newMasterBar.keySignatureType = this.data.readByte() as KeySignatureType; - } else if (previousMasterBar) { - newMasterBar.keySignature = previousMasterBar.keySignature; - newMasterBar.keySignatureType = previousMasterBar.keySignatureType; + this._keySignatures.set(this._score.masterBars.length, [ + IOHelper.readSInt8(this.data) as KeySignature, + this.data.readByte() as KeySignatureType + ]); } if (this._versionNumber >= 500 && (flags & 0x03) !== 0) { this.data.skip(4); @@ -350,7 +386,7 @@ export class Gp3To5Importer extends ScoreImporter { } // tripletfeel if (this._versionNumber >= 500) { - let tripletFeel: number = this.data.readByte(); + const tripletFeel: number = this.data.readByte(); switch (tripletFeel) { case 1: newMasterBar.tripletFeel = TripletFeel.Triplet8th; @@ -363,7 +399,8 @@ export class Gp3To5Importer extends ScoreImporter { } else { newMasterBar.tripletFeel = this._globalTripletFeel; } - newMasterBar.isDoubleBar = (flags & 0x80) !== 0; + const isDoubleBar = (flags & 0x80) !== 0; + newMasterBar.isDoubleBar = isDoubleBar; const barIndexForDirection = this._score.masterBars.length; if (this._directionLookup.has(barIndexForDirection)) { @@ -373,6 +410,10 @@ export class Gp3To5Importer extends ScoreImporter { } this._score.addMasterBar(newMasterBar); + + if (isDoubleBar) { + this._doubleBars.add(newMasterBar.index); + } } public readTracks(): void { @@ -382,10 +423,10 @@ export class Gp3To5Importer extends ScoreImporter { } public readTrack(): void { - let newTrack: Track = new Track(); + const newTrack: Track = new Track(); newTrack.ensureStaveCount(1); this._score.addTrack(newTrack); - let mainStaff: Staff = newTrack.staves[0]; + const mainStaff: Staff = newTrack.staves[0]; // Track Flags: // 1 - Percussion Track @@ -397,7 +438,7 @@ export class Gp3To5Importer extends ScoreImporter { // 64 - Unknown // 128 - Show Tuning - let flags: number = this.data.readByte(); + const flags: number = this.data.readByte(); newTrack.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 40, this.settings.importer.encoding); if ((flags & 0x01) !== 0) { mainStaff.isPercussion = true; @@ -412,23 +453,23 @@ export class Gp3To5Importer extends ScoreImporter { this._score.stylesheet.perTrackDisplayTuning!.set(newTrack.index, (flags & 0x80) !== 0); // - let stringCount: number = IOHelper.readInt32LE(this.data); - let tuning: number[] = []; + const stringCount: number = IOHelper.readInt32LE(this.data); + const tuning: number[] = []; for (let i: number = 0; i < 7; i++) { - let stringTuning: number = IOHelper.readInt32LE(this.data); + const stringTuning: number = IOHelper.readInt32LE(this.data); if (stringCount > i) { tuning.push(stringTuning); } } mainStaff.stringTuning.tunings = tuning; - let port: number = IOHelper.readInt32LE(this.data); - let index: number = IOHelper.readInt32LE(this.data) - 1; - let effectChannel: number = IOHelper.readInt32LE(this.data) - 1; + const port: number = IOHelper.readInt32LE(this.data); + const index: number = IOHelper.readInt32LE(this.data) - 1; + const effectChannel: number = IOHelper.readInt32LE(this.data) - 1; this.data.skip(4); // Fretcount if (index >= 0 && index < this._playbackInfos.length) { - let info: PlaybackInformation = this._playbackInfos[index]; + const info: PlaybackInformation = this._playbackInfos[index]; info.port = port; info.isSolo = (flags & 0x10) !== 0; info.isMute = (flags & 0x20) !== 0; @@ -477,12 +518,26 @@ export class Gp3To5Importer extends ScoreImporter { } public readBar(track: Track): void { - let newBar: Bar = new Bar(); - let mainStaff: Staff = track.staves[0]; + const newBar: Bar = new Bar(); + const mainStaff: Staff = track.staves[0]; if (mainStaff.isPercussion) { newBar.clef = Clef.Neutral; } mainStaff.addBar(newBar); + + if (this._keySignatures.has(newBar.index)) { + const newKeySignature = this._keySignatures.get(newBar.index)!; + newBar.keySignature = newKeySignature[0]; + newBar.keySignatureType = newKeySignature[1]; + } else if (newBar.index > 0) { + newBar.keySignature = newBar.previousBar!.keySignature; + newBar.keySignatureType = newBar.previousBar!.keySignatureType; + } + + if (this._doubleBars.has(newBar.index)) { + newBar.barLineRight = BarLineStyle.LightLight; + } + let voiceCount: number = 1; if (this._versionNumber >= 500) { this.data.readByte(); @@ -494,11 +549,11 @@ export class Gp3To5Importer extends ScoreImporter { } public readVoice(track: Track, bar: Bar): void { - let beatCount: number = IOHelper.readInt32LE(this.data); + const beatCount: number = IOHelper.readInt32LE(this.data); if (beatCount === 0) { return; } - let newVoice: Voice = new Voice(); + const newVoice: Voice = new Voice(); bar.addVoice(newVoice); for (let i: number = 0; i < beatCount; i++) { this.readBeat(track, bar, newVoice); @@ -506,17 +561,17 @@ export class Gp3To5Importer extends ScoreImporter { } public readBeat(track: Track, bar: Bar, voice: Voice): void { - let newBeat: Beat = new Beat(); - let flags: number = this.data.readByte(); + const newBeat: Beat = new Beat(); + const flags: number = this.data.readByte(); if ((flags & 0x01) !== 0) { newBeat.dots = 1; } if ((flags & 0x40) !== 0) { - let type: number = this.data.readByte(); + const type: number = this.data.readByte(); newBeat.isEmpty = (type & 0x02) === 0; } voice.addBeat(newBeat); - let duration: number = IOHelper.readSInt8(this.data); + const duration: number = IOHelper.readSInt8(this.data); switch (duration) { case -2: newBeat.duration = Duration.Whole; @@ -578,7 +633,7 @@ export class Gp3To5Importer extends ScoreImporter { this.readChord(newBeat); } - let beatTextAsLyrics = this.settings.importer.beatTextAsLyrics && track.index !== this._lyricsTrack; // detect if not lyrics track + const beatTextAsLyrics = this.settings.importer.beatTextAsLyrics && track.index !== this._lyricsTrack; // detect if not lyrics track if ((flags & 0x04) !== 0) { const text = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); @@ -606,7 +661,7 @@ export class Gp3To5Importer extends ScoreImporter { if ((flags & 0x10) !== 0) { this.readMixTableChange(newBeat); } - let stringFlags: number = this.data.readByte(); + const stringFlags: number = this.data.readByte(); for (let i: number = 6; i >= 0; i--) { if ((stringFlags & (1 << i)) !== 0 && 6 - i < bar.staff.tuning.length) { const note = this.readNote(track, bar, voice, newBeat, 6 - i); @@ -678,7 +733,7 @@ export class Gp3To5Importer extends ScoreImporter { // 2048 - Break Secondary Beams info set? -> read another byte for flag if ((flags2 & 0x800) !== 0) { - const breakSecondaryBeams = this.data.readByte() != 0; + const breakSecondaryBeams = this.data.readByte() !== 0; if (newBeat.index > 0 && breakSecondaryBeams) { voice.beats[newBeat.index - 1].beamingMode = BeatBeamingMode.ForceSplitOnSecondaryToNext; } @@ -699,21 +754,21 @@ export class Gp3To5Importer extends ScoreImporter { } public readChord(beat: Beat): void { - let chord: Chord = new Chord(); - let chordId: string = ModelUtils.newGuid(); + const chord: Chord = new Chord(); + const chordId: string = ModelUtils.newGuid(); if (this._versionNumber >= 500) { this.data.skip(17); chord.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 21, this.settings.importer.encoding); this.data.skip(4); chord.firstFret = IOHelper.readInt32LE(this.data); for (let i: number = 0; i < 7; i++) { - let fret: number = IOHelper.readInt32LE(this.data); + const fret: number = IOHelper.readInt32LE(this.data); if (i < beat.voice.bar.staff.tuning.length) { chord.strings.push(fret); } } - let numberOfBarres: number = this.data.readByte(); - let barreFrets: Uint8Array = new Uint8Array(5); + const numberOfBarres: number = this.data.readByte(); + const barreFrets: Uint8Array = new Uint8Array(5); this.data.read(barreFrets, 0, barreFrets.length); for (let i: number = 0; i < numberOfBarres; i++) { chord.barreFrets.push(barreFrets[i]); @@ -740,13 +795,13 @@ export class Gp3To5Importer extends ScoreImporter { this.data.skip(4); chord.firstFret = IOHelper.readInt32LE(this.data); for (let i: number = 0; i < 7; i++) { - let fret: number = IOHelper.readInt32LE(this.data); + const fret: number = IOHelper.readInt32LE(this.data); if (i < beat.voice.bar.staff.tuning.length) { chord.strings.push(fret); } } - let numberOfBarres: number = this.data.readByte(); - let barreFrets: Uint8Array = new Uint8Array(5); + const numberOfBarres: number = this.data.readByte(); + const barreFrets: Uint8Array = new Uint8Array(5); this.data.read(barreFrets, 0, barreFrets.length); for (let i: number = 0; i < numberOfBarres; i++) { chord.barreFrets.push(barreFrets[i]); @@ -764,7 +819,7 @@ export class Gp3To5Importer extends ScoreImporter { chord.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 34, this.settings.importer.encoding); chord.firstFret = IOHelper.readInt32LE(this.data); for (let i: number = 0; i < 6; i++) { - let fret: number = IOHelper.readInt32LE(this.data); + const fret: number = IOHelper.readInt32LE(this.data); if (i < beat.voice.bar.staff.tuning.length) { chord.strings.push(fret); } @@ -773,12 +828,12 @@ export class Gp3To5Importer extends ScoreImporter { this.data.skip(36); } } else { - let strings: number = this._versionNumber >= 406 ? 7 : 6; + const strings: number = this._versionNumber >= 406 ? 7 : 6; chord.name = GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); chord.firstFret = IOHelper.readInt32LE(this.data); if (chord.firstFret > 0) { for (let i: number = 0; i < strings; i++) { - let fret: number = IOHelper.readInt32LE(this.data); + const fret: number = IOHelper.readInt32LE(this.data); if (i < beat.voice.bar.staff.tuning.length) { chord.strings.push(fret); } @@ -793,7 +848,7 @@ export class Gp3To5Importer extends ScoreImporter { } public readBeatEffects(beat: Beat): HarmonicType { - let flags: number = this.data.readByte(); + const flags: number = this.data.readByte(); let flags2: number = 0; if (this._versionNumber >= 400) { flags2 = this.data.readByte(); @@ -808,7 +863,7 @@ export class Gp3To5Importer extends ScoreImporter { beat.rasgueado = Rasgueado.Ii; } if ((flags & 0x20) !== 0 && this._versionNumber >= 400) { - let slapPop: number = IOHelper.readSInt8(this.data); + const slapPop: number = IOHelper.readSInt8(this.data); switch (slapPop) { case 1: beat.tap = true; @@ -821,7 +876,7 @@ export class Gp3To5Importer extends ScoreImporter { break; } } else if ((flags & 0x20) !== 0) { - let slapPop: number = IOHelper.readSInt8(this.data); + const slapPop: number = IOHelper.readSInt8(this.data); switch (slapPop) { case 1: beat.tap = true; @@ -873,7 +928,8 @@ export class Gp3To5Importer extends ScoreImporter { if (this._versionNumber < 400) { if ((flags & 0x04) !== 0) { return HarmonicType.Natural; - } else if ((flags & 0x08) !== 0) { + } + if ((flags & 0x08) !== 0) { return HarmonicType.Artificial; } } @@ -886,10 +942,10 @@ export class Gp3To5Importer extends ScoreImporter { IOHelper.readInt32LE(this.data); // value - let pointCount: number = IOHelper.readInt32LE(this.data); + const pointCount: number = IOHelper.readInt32LE(this.data); if (pointCount > 0) { for (let i: number = 0; i < pointCount; i++) { - let point: BendPoint = new BendPoint(0, 0); + const point: BendPoint = new BendPoint(0, 0); point.offset = IOHelper.readInt32LE(this.data); // 0...60 point.value = (IOHelper.readInt32LE(this.data) / Gp3To5Importer.BendStep) | 0; // 0..12 (amount of quarters) @@ -921,17 +977,17 @@ export class Gp3To5Importer extends ScoreImporter { } public readMixTableChange(beat: Beat): void { - let tableChange: MixTableChange = new MixTableChange(); + const tableChange: MixTableChange = new MixTableChange(); tableChange.instrument = IOHelper.readSInt8(this.data); if (this._versionNumber >= 500) { this.data.skip(16); // Rse Info } tableChange.volume = IOHelper.readSInt8(this.data); tableChange.balance = IOHelper.readSInt8(this.data); - let chorus: number = IOHelper.readSInt8(this.data); - let reverb: number = IOHelper.readSInt8(this.data); - let phaser: number = IOHelper.readSInt8(this.data); - let tremolo: number = IOHelper.readSInt8(this.data); + const chorus: number = IOHelper.readSInt8(this.data); + const reverb: number = IOHelper.readSInt8(this.data); + const phaser: number = IOHelper.readSInt8(this.data); + const tremolo: number = IOHelper.readSInt8(this.data); if (this._versionNumber >= 500) { tableChange.tempoName = GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); } @@ -981,28 +1037,28 @@ export class Gp3To5Importer extends ScoreImporter { GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); } if (tableChange.volume >= 0) { - let volumeAutomation: Automation = new Automation(); + const volumeAutomation: Automation = new Automation(); volumeAutomation.isLinear = true; volumeAutomation.type = AutomationType.Volume; volumeAutomation.value = tableChange.volume; beat.automations.push(volumeAutomation); } if (tableChange.balance >= 0) { - let balanceAutomation: Automation = new Automation(); + const balanceAutomation: Automation = new Automation(); balanceAutomation.isLinear = true; balanceAutomation.type = AutomationType.Balance; balanceAutomation.value = tableChange.balance; beat.automations.push(balanceAutomation); } if (tableChange.instrument >= 0) { - let instrumentAutomation: Automation = new Automation(); + const instrumentAutomation: Automation = new Automation(); instrumentAutomation.isLinear = true; instrumentAutomation.type = AutomationType.Instrument; instrumentAutomation.value = tableChange.instrument; beat.automations.push(instrumentAutomation); } if (tableChange.tempo >= 0) { - let tempoAutomation: Automation = new Automation(); + const tempoAutomation: Automation = new Automation(); tempoAutomation.isLinear = true; tempoAutomation.type = AutomationType.Tempo; tempoAutomation.value = tableChange.tempo; @@ -1012,9 +1068,9 @@ export class Gp3To5Importer extends ScoreImporter { } public readNote(track: Track, bar: Bar, voice: Voice, beat: Beat, stringIndex: number): Note { - let newNote: Note = new Note(); + const newNote: Note = new Note(); newNote.string = bar.staff.tuning.length - stringIndex; - let flags: number = this.data.readByte(); + const flags: number = this.data.readByte(); if ((flags & 0x02) !== 0) { newNote.accentuated = AccentuationType.Heavy; } else if ((flags & 0x40) !== 0) { @@ -1022,7 +1078,7 @@ export class Gp3To5Importer extends ScoreImporter { } newNote.isGhost = (flags & 0x04) !== 0; if ((flags & 0x20) !== 0) { - let noteType: number = this.data.readByte(); + const noteType: number = this.data.readByte(); if (noteType === 3) { newNote.isDead = true; } else if (noteType === 2) { @@ -1035,7 +1091,7 @@ export class Gp3To5Importer extends ScoreImporter { this.data.readByte(); // tuplet } if ((flags & 0x10) !== 0) { - let dynamicNumber: number = IOHelper.readSInt8(this.data); + const dynamicNumber: number = IOHelper.readSInt8(this.data); newNote.dynamics = this.toDynamicValue(dynamicNumber); beat.dynamics = newNote.dynamics; } @@ -1051,7 +1107,7 @@ export class Gp3To5Importer extends ScoreImporter { if ((flags & 0x01) !== 0) { newNote.durationPercent = IOHelper.readFloat64BE(this.data); } - let flags2: number = this.data.readByte(); + const flags2: number = this.data.readByte(); swapAccidentals = (flags2 & 0x02) !== 0; } beat.addNote(newNote); @@ -1100,7 +1156,7 @@ export class Gp3To5Importer extends ScoreImporter { } public readNoteEffects(track: Track, voice: Voice, beat: Beat, note: Note): void { - let flags: number = this.data.readByte(); + const flags: number = this.data.readByte(); let flags2: number = 0; if (this._versionNumber >= 400) { flags2 = this.data.readByte(); @@ -1143,10 +1199,10 @@ export class Gp3To5Importer extends ScoreImporter { IOHelper.readInt32LE(this.data); // value - let pointCount: number = IOHelper.readInt32LE(this.data); + const pointCount: number = IOHelper.readInt32LE(this.data); if (pointCount > 0) { for (let i: number = 0; i < pointCount; i++) { - let point: BendPoint = new BendPoint(0, 0); + const point: BendPoint = new BendPoint(0, 0); point.offset = IOHelper.readInt32LE(this.data); // 0...60 point.value = (IOHelper.readInt32LE(this.data) / Gp3To5Importer.BendStep) | 0; // 0..12 (amount of quarters) @@ -1159,13 +1215,13 @@ export class Gp3To5Importer extends ScoreImporter { } public readGrace(voice: Voice, note: Note): void { - let graceBeat: Beat = new Beat(); - let graceNote: Note = new Note(); + const graceBeat: Beat = new Beat(); + const graceNote: Note = new Note(); graceNote.string = note.string; graceNote.fret = IOHelper.readSInt8(this.data); graceBeat.duration = Duration.ThirtySecond; graceBeat.dynamics = this.toDynamicValue(IOHelper.readSInt8(this.data)); - let transition: number = IOHelper.readSInt8(this.data); + const transition: number = IOHelper.readSInt8(this.data); switch (transition) { case 0: break; @@ -1185,7 +1241,7 @@ export class Gp3To5Importer extends ScoreImporter { if (this._versionNumber < 500) { graceBeat.graceType = GraceType.BeforeBeat; } else { - let flags: number = this.data.readByte(); + const flags: number = this.data.readByte(); graceNote.isDead = (flags & 0x01) !== 0; graceBeat.graceType = (flags & 0x02) !== 0 ? GraceType.OnBeat : GraceType.BeforeBeat; } @@ -1194,7 +1250,7 @@ export class Gp3To5Importer extends ScoreImporter { } public readTremoloPicking(beat: Beat): void { - let speed: number = this.data.readByte(); + const speed: number = this.data.readByte(); switch (speed) { case 1: beat.tremoloSpeed = Duration.Eighth; @@ -1210,7 +1266,7 @@ export class Gp3To5Importer extends ScoreImporter { public readSlide(note: Note): void { if (this._versionNumber >= 500) { - let type: number = IOHelper.readSInt8(this.data); + const type: number = IOHelper.readSInt8(this.data); if ((type & 1) !== 0) { note.slideOutType = SlideOutType.Shift; } else if ((type & 2) !== 0) { @@ -1226,7 +1282,7 @@ export class Gp3To5Importer extends ScoreImporter { note.slideInType = SlideInType.IntoFromAbove; } } else { - let type: number = IOHelper.readSInt8(this.data); + const type: number = IOHelper.readSInt8(this.data); switch (type) { case 1: note.slideOutType = SlideOutType.Shift; @@ -1251,7 +1307,7 @@ export class Gp3To5Importer extends ScoreImporter { } public readArtificialHarmonic(note: Note): void { - let type: number = this.data.readByte(); + const type: number = this.data.readByte(); if (this._versionNumber >= 500) { switch (type) { case 1: @@ -1322,9 +1378,9 @@ export class Gp3To5Importer extends ScoreImporter { export class GpBinaryHelpers { public static gpReadColor(data: IReadable, readAlpha: boolean = false): Color { - let r: number = data.readByte(); - let g: number = data.readByte(); - let b: number = data.readByte(); + const r: number = data.readByte(); + const g: number = data.readByte(); + const b: number = data.readByte(); let a: number = 255; if (readAlpha) { a = data.readByte(); @@ -1358,13 +1414,13 @@ export class GpBinaryHelpers { * Reads an integer as size, skips a byte and reads the string itself */ public static gpReadStringIntByte(data: IReadable, encoding: string): string { - let length: number = IOHelper.readInt32LE(data) - 1; + const length: number = IOHelper.readInt32LE(data) - 1; data.readByte(); return GpBinaryHelpers.gpReadString(data, length, encoding); } public static gpReadString(data: IReadable, length: number, encoding: string): string { - let b: Uint8Array = new Uint8Array(length); + const b: Uint8Array = new Uint8Array(length); data.read(b, 0, b.length); return IOHelper.toString(b, encoding); } @@ -1384,8 +1440,8 @@ export class GpBinaryHelpers { * @returns */ public static gpReadStringByteLength(data: IReadable, length: number, encoding: string): string { - let stringLength: number = data.readByte(); - let s: string = GpBinaryHelpers.gpReadString(data, stringLength, encoding); + const stringLength: number = data.readByte(); + const s: string = GpBinaryHelpers.gpReadString(data, stringLength, encoding); if (stringLength < length) { data.skip(length - stringLength); } diff --git a/src/importer/Gp7To8Importer.ts b/src/importer/Gp7To8Importer.ts index bf2d5e50b..135b1fa36 100644 --- a/src/importer/Gp7To8Importer.ts +++ b/src/importer/Gp7To8Importer.ts @@ -4,14 +4,14 @@ import { PartConfiguration } from '@src/importer/PartConfiguration'; import { ScoreImporter } from '@src/importer/ScoreImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Logger } from '@src/Logger'; import { ZipReader } from '@src/zip/ZipReader'; -import { ZipEntry } from '@src/zip/ZipEntry'; +import type { ZipEntry } from '@src/zip/ZipEntry'; import { IOHelper } from '@src/io/IOHelper'; -import { LayoutConfiguration } from './LayoutConfiguration'; +import { LayoutConfiguration } from '@src/importer/LayoutConfiguration'; /** * This ScoreImporter can read Guitar Pro 7 and 8 (gp) files. @@ -21,15 +21,11 @@ export class Gp7To8Importer extends ScoreImporter { return 'Guitar Pro 7-8'; } - public constructor() { - super(); - } - public readScore(): Score { // at first we need to load the binary file system // from the GPX container Logger.debug(this.name, 'Loading ZIP entries'); - let fileSystem: ZipReader = new ZipReader(this.data); + const fileSystem: ZipReader = new ZipReader(this.data); let entries: ZipEntry[]; try { entries = fileSystem.read(); @@ -42,7 +38,7 @@ export class Gp7To8Importer extends ScoreImporter { let binaryStylesheetData: Uint8Array | null = null; let partConfigurationData: Uint8Array | null = null; let layoutConfigurationData: Uint8Array | null = null; - for (let entry of entries) { + for (const entry of entries) { switch (entry.fileName) { case 'score.gpif': xml = IOHelper.toString(entry.data, this.settings.importer.encoding); @@ -66,14 +62,14 @@ export class Gp7To8Importer extends ScoreImporter { // the score.gpif file within this filesystem stores // the score information as XML we need to parse. Logger.debug(this.name, 'Start Parsing score.gpif'); - let gpifParser: GpifParser = new GpifParser(); + const gpifParser: GpifParser = new GpifParser(); gpifParser.parseXml(xml, this.settings); Logger.debug(this.name, 'score.gpif parsed'); - let score: Score = gpifParser.score; + const score: Score = gpifParser.score; if (binaryStylesheetData) { Logger.debug(this.name, 'Start Parsing BinaryStylesheet'); - let stylesheet: BinaryStylesheet = new BinaryStylesheet(binaryStylesheetData); + const stylesheet: BinaryStylesheet = new BinaryStylesheet(binaryStylesheetData); stylesheet.apply(score); Logger.debug(this.name, 'BinaryStylesheet parsed'); } @@ -87,7 +83,7 @@ export class Gp7To8Importer extends ScoreImporter { } if (layoutConfigurationData && partConfigurationParser != null) { Logger.debug(this.name, 'Start Parsing Layout Configuration'); - let layoutConfigurationParser: LayoutConfiguration = new LayoutConfiguration( + const layoutConfigurationParser: LayoutConfiguration = new LayoutConfiguration( partConfigurationParser, layoutConfigurationData ); diff --git a/src/importer/GpifParser.ts b/src/importer/GpifParser.ts index 3df4160d0..96bb5d8ee 100644 --- a/src/importer/GpifParser.ts +++ b/src/importer/GpifParser.ts @@ -1,7 +1,7 @@ import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { AccentuationType } from '@src/model/AccentuationType'; import { Automation, AutomationType } from '@src/model/Automation'; -import { Bar, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; +import { Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; import { Beat, BeatBeamingMode } from '@src/model/Beat'; import { BendPoint } from '@src/model/BendPoint'; import { BrushType } from '@src/model/BrushType'; @@ -27,15 +27,15 @@ import { Section } from '@src/model/Section'; import { SimileMark } from '@src/model/SimileMark'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; -import { Staff } from '@src/model/Staff'; +import type { Staff } from '@src/model/Staff'; import { Track } from '@src/model/Track'; import { TripletFeel } from '@src/model/TripletFeel'; import { VibratoType } from '@src/model/VibratoType'; import { Voice } from '@src/model/Voice'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { XmlDocument } from '@src/xml/XmlDocument'; -import { XmlNode, XmlNodeType } from '@src/xml/XmlNode'; +import { type XmlNode, XmlNodeType } from '@src/xml/XmlNode'; import { MidiUtils } from '@src/midi/MidiUtils'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; @@ -53,6 +53,7 @@ import { BarreShape } from '@src/model/BarreShape'; import { NoteOrnament } from '@src/model/NoteOrnament'; import { Rasgueado } from '@src/model/Rasgueado'; import { Direction } from '@src/model/Direction'; +import { ModelUtils } from '@src/model/ModelUtils'; /** * This structure represents a duration within a gpif @@ -70,7 +71,7 @@ class GpifSound { public path: string = ''; public role: string = ''; public get uniqueId(): string { - return this.path + ';' + this.name + ';' + this.role; + return `${this.path};${this.name};${this.role}`; } public program: number = 0; @@ -119,6 +120,12 @@ export class GpifParser { private _articulationByName!: Map; private _skipApplyLyrics: boolean = false; + private _doubleBars: Set = new Set(); + private _keySignatures: Map = new Map< + number, + [KeySignature, KeySignatureType] + >(); + public parseXml(xml: string, settings: Settings): void { this._masterTrackAutomations = new Map(); this._automationsPerTrackIdAndBarIndex = new Map>(); @@ -141,7 +148,7 @@ export class GpifParser { this._soundsByTrack = new Map>(); this._skipApplyLyrics = false; - let dom: XmlDocument = new XmlDocument(); + const dom: XmlDocument = new XmlDocument(); try { dom.parse(xml); } catch (e) { @@ -150,17 +157,18 @@ export class GpifParser { this.parseDom(dom); this.buildModel(); + ModelUtils.consolidate(this.score); this.score.finish(settings); if (!this._skipApplyLyrics && this._lyricsByTrack.size > 0) { for (const [t, lyrics] of this._lyricsByTrack) { - let track: Track = this._tracksById.get(t)!; + const track: Track = this._tracksById.get(t)!; track.applyLyrics(lyrics); } } } private parseDom(dom: XmlDocument): void { - let root: XmlNode | null = dom.firstElement; + const root: XmlNode | null = dom.firstElement; if (!root) { return; } @@ -171,37 +179,35 @@ export class GpifParser { if (root.localName === 'GPIF') { this.score = new Score(); // parse all children - for (let n of root.childNodes) { - if (n.nodeType === XmlNodeType.Element) { - switch (n.localName) { - case 'Score': - this.parseScoreNode(n); - break; - case 'MasterTrack': - this.parseMasterTrackNode(n); - break; - case 'Tracks': - this.parseTracksNode(n); - break; - case 'MasterBars': - this.parseMasterBarsNode(n); - break; - case 'Bars': - this.parseBars(n); - break; - case 'Voices': - this.parseVoices(n); - break; - case 'Beats': - this.parseBeats(n); - break; - case 'Notes': - this.parseNotes(n); - break; - case 'Rhythms': - this.parseRhythms(n); - break; - } + for (const n of root.childElements()) { + switch (n.localName) { + case 'Score': + this.parseScoreNode(n); + break; + case 'MasterTrack': + this.parseMasterTrackNode(n); + break; + case 'Tracks': + this.parseTracksNode(n); + break; + case 'MasterBars': + this.parseMasterBarsNode(n); + break; + case 'Bars': + this.parseBars(n); + break; + case 'Voices': + this.parseVoices(n); + break; + case 'Beats': + this.parseBeats(n); + break; + case 'Notes': + this.parseNotes(n); + break; + case 'Rhythms': + this.parseRhythms(n); + break; } } } else { @@ -213,78 +219,108 @@ export class GpifParser { // ... // private parseScoreNode(element: XmlNode): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Title': - this.score.title = c.firstChild!.innerText; - break; - case 'SubTitle': - this.score.subTitle = c.firstChild!.innerText; - break; - case 'Artist': - this.score.artist = c.firstChild!.innerText; - break; - case 'Album': - this.score.album = c.firstChild!.innerText; - break; - case 'Words': - this.score.words = c.firstChild!.innerText; - break; - case 'Music': - this.score.music = c.firstChild!.innerText; - break; - case 'WordsAndMusic': - if (c.firstChild && c.firstChild.innerText !== '') { - let wordsAndMusic: string = c.firstChild.innerText; - if (wordsAndMusic && !this.score.words) { - this.score.words = wordsAndMusic; - } - if (wordsAndMusic && !this.score.music) { - this.score.music = wordsAndMusic; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'Title': + this.score.title = c.innerText; + break; + case 'SubTitle': + this.score.subTitle = c.innerText; + break; + case 'Artist': + this.score.artist = c.innerText; + break; + case 'Album': + this.score.album = c.innerText; + break; + case 'Words': + this.score.words = c.innerText; + break; + case 'Music': + this.score.music = c.innerText; + break; + case 'WordsAndMusic': + const wordsAndMusic: string = c.innerText; + if (wordsAndMusic !== '') { + if (wordsAndMusic && !this.score.words) { + this.score.words = wordsAndMusic; } - break; - case 'Copyright': - this.score.copyright = c.firstChild!.innerText; - break; - case 'Tabber': - this.score.tab = c.firstChild!.innerText; - break; - case 'Instructions': - this.score.instructions = c.firstChild!.innerText; - break; - case 'Notices': - this.score.notices = c.firstChild!.innerText; - break; - case 'ScoreSystemsDefaultLayout': - this.score.defaultSystemsLayout = parseInt(c.innerText); - break; - case 'ScoreSystemsLayout': - this.score.systemsLayout = c.innerText.split(' ').map(i => parseInt(i)); - break; - } + if (wordsAndMusic && !this.score.music) { + this.score.music = wordsAndMusic; + } + } + break; + case 'Copyright': + this.score.copyright = c.innerText; + break; + case 'Tabber': + this.score.tab = c.innerText; + break; + case 'Instructions': + this.score.instructions = c.innerText; + break; + case 'Notices': + this.score.notices = c.innerText; + break; + case 'ScoreSystemsDefaultLayout': + this.score.defaultSystemsLayout = GpifParser.parseIntSafe(c.innerText, 4); + break; + case 'ScoreSystemsLayout': + this.score.systemsLayout = GpifParser.splitSafe(c.innerText).map(i => + GpifParser.parseIntSafe(i, 4) + ); + break; } } } + private static parseIntSafe(text: string | undefined, fallback: number) { + if (!text) { + return fallback; + } + + const i = Number.parseInt(text); + if (!Number.isNaN(i)) { + return i; + } + return fallback; + } + + private static parseFloatSafe(text: string | undefined, fallback: number) { + if (!text) { + return fallback; + } + + const i = Number.parseFloat(text); + if (!Number.isNaN(i)) { + return i; + } + return fallback; + } + + private static splitSafe(text: string | undefined, separator: string = ' '): string[] { + if (!text) { + return []; + } + + return text.split(separator).map(t => t.trim()).filter(t => t.length > 0); + } + // // ... // private parseMasterTrackNode(node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Automations': - this.parseAutomations(c, this._masterTrackAutomations, null, null); - break; - case 'Tracks': - this._tracksMapping = c.innerText.split(' '); - break; - case 'Anacrusis': - this._hasAnacrusis = true; - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Automations': + this.parseAutomations(c, this._masterTrackAutomations, null, null); + break; + case 'Tracks': + this._tracksMapping = GpifParser.splitSafe(c.innerText); + break; + case 'Anacrusis': + this._hasAnacrusis = true; + break; } } } @@ -295,13 +331,11 @@ export class GpifParser { sounds: Map | null, sustainPedals: Map | null ): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Automation': - this.parseAutomation(c, automations, sounds, sustainPedals); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Automation': + this.parseAutomation(c, automations, sounds, sustainPedals); + break; } } } @@ -320,41 +354,39 @@ export class GpifParser { let textValue: string | null = null; let reference: number = 0; let text: string | null = null; - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Type': - type = c.innerText; - break; - case 'Linear': - isLinear = c.innerText.toLowerCase() === 'true'; - break; - case 'Bar': - barIndex = parseInt(c.innerText); - break; - case 'Position': - ratioPosition = parseFloat(c.innerText); - break; - case 'Value': - if (c.firstElement && c.firstElement.nodeType === XmlNodeType.CDATA) { - textValue = c.innerText; + for (const c of node.childElements()) { + switch (c.localName) { + case 'Type': + type = c.innerText; + break; + case 'Linear': + isLinear = c.innerText.toLowerCase() === 'true'; + break; + case 'Bar': + barIndex = GpifParser.parseIntSafe(c.innerText, 0); + break; + case 'Position': + ratioPosition = GpifParser.parseFloatSafe(c.innerText, 0); + break; + case 'Value': + if (c.firstElement && c.firstElement.nodeType === XmlNodeType.CDATA) { + textValue = c.innerText; + } else { + const parts: string[] = GpifParser.splitSafe(c.innerText); + // Issue 391: Some GPX files might have + // single floating point value. + if (parts.length === 1) { + numberValue = GpifParser.parseFloatSafe(parts[0], 0); + reference = 1; } else { - let parts: string[] = c.innerText.split(' '); - // Issue 391: Some GPX files might have - // single floating point value. - if (parts.length === 1) { - numberValue = parseFloat(parts[0]); - reference = 1; - } else { - numberValue = parseFloat(parts[0]); - reference = parseInt(parts[1]); - } + numberValue = GpifParser.parseFloatSafe(parts[0], 0); + reference = GpifParser.parseIntSafe(parts[1], 0); } - break; - case 'Text': - text = c.innerText; - break; - } + } + break; + case 'Text': + text = c.innerText; + break; } } if (!type) { @@ -419,13 +451,11 @@ export class GpifParser { // ... // private parseTracksNode(node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Track': - this.parseTrack(c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Track': + this.parseTrack(c); + break; } } } @@ -433,84 +463,82 @@ export class GpifParser { private parseTrack(node: XmlNode): void { this._articulationByName = new Map(); - let track: Track = new Track(); + const track: Track = new Track(); track.ensureStaveCount(1); - let staff: Staff = track.staves[0]; + const staff: Staff = track.staves[0]; staff.showStandardNotation = true; - let trackId: string = node.getAttribute('id'); + const trackId: string = node.getAttribute('id'); - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Name': - track.name = c.innerText; - break; - case 'Color': - let parts: string[] = c.innerText.split(' '); - if (parts.length >= 3) { - let r: number = parseInt(parts[0]); - let g: number = parseInt(parts[1]); - let b: number = parseInt(parts[2]); - track.color = new Color(r, g, b, 0xff); - } - break; - case 'Instrument': - let instrumentName: string = c.getAttribute('ref'); - if (instrumentName.endsWith('-gs') || instrumentName.endsWith('GrandStaff')) { - track.ensureStaveCount(2); - track.staves[1].showStandardNotation = true; - } - break; - case 'InstrumentSet': - this.parseInstrumentSet(track, c); - break; - case 'NotationPatch': - this.parseNotationPatch(track, c); - break; - case 'ShortName': - track.shortName = c.innerText; - break; - case 'SystemsDefautLayout': // not a typo by alphaTab, this is a typo in the GPIF files. - track.defaultSystemsLayout = parseInt(c.innerText); - break; - case 'SystemsLayout': - track.systemsLayout = c.innerText.split(' ').map(i => parseInt(i)); - break; - case 'Lyrics': - this.parseLyrics(trackId, c); - break; - case 'Properties': - this.parseTrackProperties(track, c); - break; - case 'GeneralMidi': - case 'MidiConnection': - case 'MIDISettings': - this.parseGeneralMidi(track, c); - break; - case 'Sounds': - this.parseSounds(trackId, track, c); - break; - case 'PlaybackState': - let state: string = c.innerText; - track.playbackInfo.isSolo = state === 'Solo'; - track.playbackInfo.isMute = state === 'Mute'; - break; - case 'PartSounding': - this.parsePartSounding(track, c); - break; - case 'Staves': - this.parseStaves(track, c); - break; - case 'Transpose': - this.parseTranspose(track, c); - break; - case 'RSE': - this.parseRSE(track, c); - break; - case 'Automations': - this.parseTrackAutomations(trackId, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Name': + track.name = c.innerText; + break; + case 'Color': + const parts: string[] = GpifParser.splitSafe(c.innerText); + if (parts.length >= 3) { + const r: number = GpifParser.parseIntSafe(parts[0], 0); + const g: number = GpifParser.parseIntSafe(parts[1], 0); + const b: number = GpifParser.parseIntSafe(parts[2], 0); + track.color = new Color(r, g, b, 0xff); + } + break; + case 'Instrument': + const instrumentName: string = c.getAttribute('ref'); + if (instrumentName.endsWith('-gs') || instrumentName.endsWith('GrandStaff')) { + track.ensureStaveCount(2); + track.staves[1].showStandardNotation = true; + } + break; + case 'InstrumentSet': + this.parseInstrumentSet(track, c); + break; + case 'NotationPatch': + this.parseNotationPatch(track, c); + break; + case 'ShortName': + track.shortName = c.innerText; + break; + case 'SystemsDefautLayout': // not a typo by alphaTab, this is a typo in the GPIF files. + track.defaultSystemsLayout = GpifParser.parseIntSafe(c.innerText, 4); + break; + case 'SystemsLayout': + track.systemsLayout = GpifParser.splitSafe(c.innerText).map(i => GpifParser.parseIntSafe(i, 4)); + break; + case 'Lyrics': + this.parseLyrics(trackId, c); + break; + case 'Properties': + this.parseTrackProperties(track, c); + break; + case 'GeneralMidi': + case 'MidiConnection': + case 'MIDISettings': + this.parseGeneralMidi(track, c); + break; + case 'Sounds': + this.parseSounds(trackId, track, c); + break; + case 'PlaybackState': + const state: string = c.innerText; + track.playbackInfo.isSolo = state === 'Solo'; + track.playbackInfo.isMute = state === 'Mute'; + break; + case 'PartSounding': + this.parsePartSounding(track, c); + break; + case 'Staves': + this.parseStaves(track, c); + break; + case 'Transpose': + this.parseTranspose(track, c); + break; + case 'RSE': + this.parseRSE(track, c); + break; + case 'Automations': + this.parseTrackAutomations(trackId, c); + break; } } this._tracksById.set(trackId, track); @@ -527,88 +555,70 @@ export class GpifParser { } private parseNotationPatch(track: Track, node: XmlNode) { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'LineCount': - const lineCount = parseInt(c.innerText); - for (let staff of track.staves) { - staff.standardNotationLineCount = lineCount; - } - break; - case 'Elements': - this.parseElements(track, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'LineCount': + const lineCount = GpifParser.parseIntSafe(c.innerText, 5); + for (const staff of track.staves) { + staff.standardNotationLineCount = lineCount; + } + break; + case 'Elements': + this.parseElements(track, c); + break; } } } private parseInstrumentSet(track: Track, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Type': - switch (c.innerText) { - case 'drumKit': - for (let staff of track.staves) { - staff.isPercussion = true; - } - break; - } - if (c.innerText === 'drumKit') { - for (let staff of track.staves) { - staff.isPercussion = true; - } - } - break; - case 'Elements': - this.parseElements(track, c); - break; - case 'LineCount': - const lineCount = parseInt(c.innerText); - for (let staff of track.staves) { - staff.standardNotationLineCount = lineCount; + for (const c of node.childElements()) { + switch (c.localName) { + case 'Type': + if (c.innerText === 'drumKit') { + for (const staff of track.staves) { + staff.isPercussion = true; } - break; - } + } + break; + case 'Elements': + this.parseElements(track, c); + break; + case 'LineCount': + const lineCount = GpifParser.parseIntSafe(c.innerText, 5); + for (const staff of track.staves) { + staff.standardNotationLineCount = lineCount; + } + break; } } } private parseElements(track: Track, node: XmlNode) { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Element': - this.parseElement(track, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Element': + this.parseElement(track, c); + break; } } } private parseElement(track: Track, node: XmlNode) { - const typeElement = node.findChildElement('Type'); - const type = typeElement ? typeElement.innerText : ''; - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Name': - case 'Articulations': - this.parseArticulations(track, c, type); - break; - } + const type = node.findChildElement('Type')?.innerText ?? ''; + for (const c of node.childElements()) { + switch (c.localName) { + case 'Name': + case 'Articulations': + this.parseArticulations(track, c, type); + break; } } } private parseArticulations(track: Track, node: XmlNode, elementType: string) { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Articulation': - this.parseArticulation(track, c, elementType); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Articulation': + this.parseArticulation(track, c, elementType); + break; } } } @@ -618,64 +628,58 @@ export class GpifParser { articulation.outputMidiNumber = -1; articulation.elementType = elementType; let name = ''; - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - const txt = c.innerText; - switch (c.localName) { - case 'Name': - name = c.innerText; - break; - case 'OutputMidiNumber': - if (txt.length > 0) { - articulation.outputMidiNumber = parseInt(txt); - } - break; - case 'TechniqueSymbol': - articulation.techniqueSymbol = this.parseTechniqueSymbol(txt); - break; - case 'TechniquePlacement': - switch (txt) { - case 'outside': - articulation.techniqueSymbolPlacement = TextBaseline.Bottom; - break; - case 'inside': - articulation.techniqueSymbolPlacement = TextBaseline.Middle; - break; - case 'above': - articulation.techniqueSymbolPlacement = TextBaseline.Bottom; - break; - case 'below': - articulation.techniqueSymbolPlacement = TextBaseline.Top; - break; - } - break; - case 'Noteheads': - const noteHeadsTxt = txt.split(' '); - if (noteHeadsTxt.length >= 1) { - articulation.noteHeadDefault = this.parseNoteHead(noteHeadsTxt[0]); - } - if (noteHeadsTxt.length >= 2) { - articulation.noteHeadHalf = this.parseNoteHead(noteHeadsTxt[1]); - } - if (noteHeadsTxt.length >= 3) { - articulation.noteHeadWhole = this.parseNoteHead(noteHeadsTxt[2]); - } + for (const c of node.childElements()) { + const txt = c.innerText; + switch (c.localName) { + case 'Name': + name = c.innerText; + break; + case 'OutputMidiNumber': + articulation.outputMidiNumber = GpifParser.parseIntSafe(txt, 0); + break; + case 'TechniqueSymbol': + articulation.techniqueSymbol = this.parseTechniqueSymbol(txt); + break; + case 'TechniquePlacement': + switch (txt) { + case 'outside': + articulation.techniqueSymbolPlacement = TextBaseline.Bottom; + break; + case 'inside': + articulation.techniqueSymbolPlacement = TextBaseline.Middle; + break; + case 'above': + articulation.techniqueSymbolPlacement = TextBaseline.Bottom; + break; + case 'below': + articulation.techniqueSymbolPlacement = TextBaseline.Top; + break; + } + break; + case 'Noteheads': + const noteHeadsTxt = GpifParser.splitSafe(txt); + if (noteHeadsTxt.length >= 1) { + articulation.noteHeadDefault = this.parseNoteHead(noteHeadsTxt[0]); + } + if (noteHeadsTxt.length >= 2) { + articulation.noteHeadHalf = this.parseNoteHead(noteHeadsTxt[1]); + } + if (noteHeadsTxt.length >= 3) { + articulation.noteHeadWhole = this.parseNoteHead(noteHeadsTxt[2]); + } - if (articulation.noteHeadHalf == MusicFontSymbol.None) { - articulation.noteHeadHalf = articulation.noteHeadDefault; - } + if (articulation.noteHeadHalf === MusicFontSymbol.None) { + articulation.noteHeadHalf = articulation.noteHeadDefault; + } - if (articulation.noteHeadWhole == MusicFontSymbol.None) { - articulation.noteHeadWhole = articulation.noteHeadDefault; - } + if (articulation.noteHeadWhole === MusicFontSymbol.None) { + articulation.noteHeadWhole = articulation.noteHeadDefault; + } - break; - case 'StaffLine': - if (txt.length > 0) { - articulation.staffLine = parseInt(txt); - } - break; - } + break; + case 'StaffLine': + articulation.staffLine = GpifParser.parseIntSafe(txt, 0); + break; } } @@ -762,63 +766,57 @@ export class GpifParser { private parseStaves(track: Track, node: XmlNode): void { let staffIndex: number = 0; - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Staff': - track.ensureStaveCount(staffIndex + 1); - let staff: Staff = track.staves[staffIndex]; - this.parseStaff(staff, c); - staffIndex++; - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Staff': + track.ensureStaveCount(staffIndex + 1); + const staff: Staff = track.staves[staffIndex]; + this.parseStaff(staff, c); + staffIndex++; + break; } } } private parseStaff(staff: Staff, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Properties': - this.parseStaffProperties(staff, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Properties': + this.parseStaffProperties(staff, c); + break; } } } private parseStaffProperties(staff: Staff, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Property': - this.parseStaffProperty(staff, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Property': + this.parseStaffProperty(staff, c); + break; } } } private parseStaffProperty(staff: Staff, node: XmlNode): void { - let propertyName: string = node.getAttribute('name'); + const propertyName: string = node.getAttribute('name'); switch (propertyName) { case 'Tuning': - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Pitches': - let tuningParts: string[] = node.findChildElement('Pitches')!.innerText.split(' '); - let tuning = new Array(tuningParts.length); - for (let i: number = 0; i < tuning.length; i++) { - tuning[tuning.length - 1 - i] = parseInt(tuningParts[i]); - } - staff.stringTuning.tunings = tuning; - break; - case 'Label': - staff.stringTuning.name = c.innerText; - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Pitches': + const tuningParts: string[] = GpifParser.splitSafe( + node.findChildElement('Pitches')?.innerText + ); + const tuning = new Array(tuningParts.length); + for (let i: number = 0; i < tuning.length; i++) { + tuning[tuning.length - 1 - i] = GpifParser.parseIntSafe(tuningParts[i], 0); + } + staff.stringTuning.tunings = tuning; + break; + case 'Label': + staff.stringTuning.name = c.innerText; + break; } } @@ -832,47 +830,43 @@ export class GpifParser { this.parseDiagramCollectionForStaff(staff, node); break; case 'CapoFret': - let capo: number = parseInt(node.findChildElement('Fret')!.innerText); + const capo: number = GpifParser.parseIntSafe(node.findChildElement('Fret')?.innerText, 0); staff.capo = capo; break; } } private parseLyrics(trackId: string, node: XmlNode): void { - let tracks: Lyrics[] = []; - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Line': - tracks.push(this.parseLyricsLine(c)); - break; - } + const tracks: Lyrics[] = []; + for (const c of node.childElements()) { + switch (c.localName) { + case 'Line': + tracks.push(this.parseLyricsLine(c)); + break; } } this._lyricsByTrack.set(trackId, tracks); } private parseLyricsLine(node: XmlNode): Lyrics { - let lyrics: Lyrics = new Lyrics(); - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Offset': - lyrics.startBar = parseInt(c.innerText); - break; - case 'Text': - lyrics.text = c.innerText; - break; - } + const lyrics: Lyrics = new Lyrics(); + for (const c of node.childElements()) { + switch (c.localName) { + case 'Offset': + lyrics.startBar = GpifParser.parseIntSafe(c.innerText, 0); + break; + case 'Text': + lyrics.text = c.innerText; + break; } } return lyrics; } private parseDiagramCollectionForTrack(track: Track, node: XmlNode): void { - let items: XmlNode = node.findChildElement('Items')!; - for (let c of items.childNodes) { - if (c.nodeType === XmlNodeType.Element) { + const items = node.findChildElement('Items'); + if (items) { + for (const c of items.childElements()) { switch (c.localName) { case 'Item': this.parseDiagramItemForTrack(track, c); @@ -883,9 +877,9 @@ export class GpifParser { } private parseDiagramCollectionForStaff(staff: Staff, node: XmlNode): void { - let items: XmlNode = node.findChildElement('Items')!; - for (let c of items.childNodes) { - if (c.nodeType === XmlNodeType.Element) { + const items = node.findChildElement('Items'); + if (items) { + for (const c of items.childElements()) { switch (c.localName) { case 'Item': this.parseDiagramItemForStaff(staff, c); @@ -896,17 +890,17 @@ export class GpifParser { } private parseDiagramItemForTrack(track: Track, node: XmlNode): void { - let chord: Chord = new Chord(); - let chordId: string = node.getAttribute('id'); - for (let staff of track.staves) { + const chord: Chord = new Chord(); + const chordId: string = node.getAttribute('id'); + for (const staff of track.staves) { staff.addChord(chordId, chord); } this.parseDiagramItemForChord(chord, node); } private parseDiagramItemForStaff(staff: Staff, node: XmlNode): void { - let chord: Chord = new Chord(); - let chordId: string = node.getAttribute('id'); + const chord: Chord = new Chord(); + const chordId: string = node.getAttribute('id'); staff.addChord(chordId, chord); this.parseDiagramItemForChord(chord, node); } @@ -914,104 +908,99 @@ export class GpifParser { private parseDiagramItemForChord(chord: Chord, node: XmlNode): void { chord.name = node.getAttribute('name'); - let diagram = node.findChildElement('Diagram'); + const diagram = node.findChildElement('Diagram'); if (!diagram) { chord.showDiagram = false; chord.showFingering = false; return; } - let stringCount: number = parseInt(diagram.getAttribute('stringCount')); - let baseFret: number = diagram.attributes.has('baseFret') ? parseInt(diagram.getAttribute('baseFret')) : 0; + const stringCount: number = GpifParser.parseIntSafe(diagram.getAttribute('stringCount'), 6); + const baseFret: number = GpifParser.parseIntSafe(diagram.getAttribute('baseFret'), 0); chord.firstFret = baseFret + 1; for (let i: number = 0; i < stringCount; i++) { chord.strings.push(-1); } - for (let c of diagram.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Fret': - let guitarString: number = parseInt(c.getAttribute('string')); - chord.strings[stringCount - guitarString - 1] = baseFret + parseInt(c.getAttribute('fret')); - break; - case 'Fingering': - let existingFingers: Map = new Map(); - for (let p of c.childNodes) { - if (p.nodeType === XmlNodeType.Element) { - switch (p.localName) { - case 'Position': - let finger: Fingers = Fingers.Unknown; - let fret: number = baseFret + parseInt(p.getAttribute('fret')); - switch (p.getAttribute('finger')) { - case 'Index': - finger = Fingers.IndexFinger; - break; - case 'Middle': - finger = Fingers.MiddleFinger; - break; - case 'Rank': - finger = Fingers.AnnularFinger; - break; - case 'Pinky': - finger = Fingers.LittleFinger; - break; - case 'Thumb': - finger = Fingers.Thumb; - break; - case 'None': - break; - } - if (finger !== Fingers.Unknown) { - if (existingFingers.has(finger)) { - chord.barreFrets.push(fret); - } else { - existingFingers.set(finger, true); - } - } + for (const c of diagram.childElements()) { + switch (c.localName) { + case 'Fret': + const guitarString: number = GpifParser.parseIntSafe(c.getAttribute('string'), 0); + chord.strings[stringCount - guitarString - 1] = + baseFret + GpifParser.parseIntSafe(c.getAttribute('fret'), 0); + break; + case 'Fingering': + const existingFingers: Map = new Map(); + for (const p of c.childElements()) { + switch (p.localName) { + case 'Position': + let finger: Fingers = Fingers.Unknown; + const fret: number = baseFret + GpifParser.parseIntSafe(p.getAttribute('fret'), 0); + switch (p.getAttribute('finger')) { + case 'Index': + finger = Fingers.IndexFinger; + break; + case 'Middle': + finger = Fingers.MiddleFinger; + break; + case 'Rank': + finger = Fingers.AnnularFinger; + break; + case 'Pinky': + finger = Fingers.LittleFinger; + break; + case 'Thumb': + finger = Fingers.Thumb; + break; + case 'None': break; } - } - } - break; - case 'Property': - switch (c.getAttribute('name')) { - case 'ShowName': - chord.showName = c.getAttribute('value') === 'true'; - break; - case 'ShowDiagram': - chord.showDiagram = c.getAttribute('value') === 'true'; - break; - case 'ShowFingering': - chord.showFingering = c.getAttribute('value') === 'true'; + if (finger !== Fingers.Unknown) { + if (existingFingers.has(finger)) { + chord.barreFrets.push(fret); + } else { + existingFingers.set(finger, true); + } + } break; } - break; - } + } + break; + case 'Property': + switch (c.getAttribute('name')) { + case 'ShowName': + chord.showName = c.getAttribute('value') === 'true'; + break; + case 'ShowDiagram': + chord.showDiagram = c.getAttribute('value') === 'true'; + break; + case 'ShowFingering': + chord.showFingering = c.getAttribute('value') === 'true'; + break; + } + break; } } } private parseTrackProperties(track: Track, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Property': - this.parseTrackProperty(track, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Property': + this.parseTrackProperty(track, c); + break; } } } private parseTrackProperty(track: Track, node: XmlNode): void { - let propertyName: string = node.getAttribute('name'); + const propertyName: string = node.getAttribute('name'); switch (propertyName) { case 'Tuning': - let tuningParts: string[] = node.findChildElement('Pitches')!.innerText.split(' '); - let tuning = new Array(tuningParts.length); + const tuningParts: string[] = GpifParser.splitSafe(node.findChildElement('Pitches')?.innerText); + const tuning = new Array(tuningParts.length); for (let i: number = 0; i < tuning.length; i++) { - tuning[tuning.length - 1 - i] = parseInt(tuningParts[i]); + tuning[tuning.length - 1 - i] = GpifParser.parseIntSafe(tuningParts[i], 0); } - for (let staff of track.staves) { + for (const staff of track.staves) { staff.stringTuning.tunings = tuning; staff.showStandardNotation = true; staff.showTablature = true; @@ -1022,8 +1011,8 @@ export class GpifParser { this.parseDiagramCollectionForTrack(track, node); break; case 'CapoFret': - let capo: number = parseInt(node.findChildElement('Fret')!.innerText); - for (let staff of track.staves) { + const capo: number = GpifParser.parseIntSafe(node.findChildElement('Fret')?.innerText, 0); + for (const staff of track.staves) { staff.capo = capo; } break; @@ -1031,62 +1020,56 @@ export class GpifParser { } private parseGeneralMidi(track: Track, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Program': - track.playbackInfo.program = parseInt(c.innerText); - break; - case 'Port': - track.playbackInfo.port = parseInt(c.innerText); - break; - case 'PrimaryChannel': - track.playbackInfo.primaryChannel = parseInt(c.innerText); - break; - case 'SecondaryChannel': - track.playbackInfo.secondaryChannel = parseInt(c.innerText); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Program': + track.playbackInfo.program = GpifParser.parseIntSafe(c.innerText, 0); + break; + case 'Port': + track.playbackInfo.port = GpifParser.parseIntSafe(c.innerText, 0); + break; + case 'PrimaryChannel': + track.playbackInfo.primaryChannel = GpifParser.parseIntSafe(c.innerText, 0); + break; + case 'SecondaryChannel': + track.playbackInfo.secondaryChannel = GpifParser.parseIntSafe(c.innerText, 0); + break; } } - let isPercussion: boolean = node.getAttribute('table') === 'Percussion'; + const isPercussion: boolean = node.getAttribute('table') === 'Percussion'; if (isPercussion) { - for (let staff of track.staves) { + for (const staff of track.staves) { staff.isPercussion = true; } } } private parseSounds(trackId: string, track: Track, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Sound': - this.parseSound(trackId, track, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Sound': + this.parseSound(trackId, track, c); + break; } } } private parseSound(trackId: string, track: Track, node: XmlNode): void { const sound = new GpifSound(); - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Name': - sound.name = c.innerText; - break; - case 'Path': - sound.path = c.innerText; - break; - case 'Role': - sound.role = c.innerText; - break; - case 'MIDI': - this.parseSoundMidi(sound, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Name': + sound.name = c.innerText; + break; + case 'Path': + sound.path = c.innerText; + break; + case 'Role': + sound.role = c.innerText; + break; + case 'MIDI': + this.parseSoundMidi(sound, c); + break; } } @@ -1102,27 +1085,23 @@ export class GpifParser { } private parseSoundMidi(sound: GpifSound, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Program': - sound.program = parseInt(c.innerText); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Program': + sound.program = GpifParser.parseIntSafe(c.innerText, 0); + break; } } } private parsePartSounding(track: Track, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'TranspositionPitch': - for (let staff of track.staves) { - staff.displayTranspositionPitch = parseInt(c.innerText); - } - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'TranspositionPitch': + for (const staff of track.staves) { + staff.displayTranspositionPitch = GpifParser.parseIntSafe(c.innerText, 0); + } + break; } } } @@ -1130,53 +1109,47 @@ export class GpifParser { private parseTranspose(track: Track, node: XmlNode): void { let octave: number = 0; let chromatic: number = 0; - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Chromatic': - chromatic = parseInt(c.innerText); - break; - case 'Octave': - octave = parseInt(c.innerText); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Chromatic': + chromatic = GpifParser.parseIntSafe(c.innerText, 0); + break; + case 'Octave': + octave = GpifParser.parseIntSafe(c.innerText, 0); + break; } } - for (let staff of track.staves) { + for (const staff of track.staves) { staff.displayTranspositionPitch = octave * 12 + chromatic; } } private parseRSE(track: Track, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'ChannelStrip': - this.parseChannelStrip(track, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'ChannelStrip': + this.parseChannelStrip(track, c); + break; } } } private parseChannelStrip(track: Track, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Parameters': - this.parseChannelStripParameters(track, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Parameters': + this.parseChannelStripParameters(track, c); + break; } } } private parseChannelStripParameters(track: Track, node: XmlNode): void { if (node.firstChild && node.firstChild.value) { - let parameters = node.firstChild.value.split(' '); + const parameters = GpifParser.splitSafe(node.firstChild.value); if (parameters.length >= 12) { - track.playbackInfo.balance = Math.floor(parseFloat(parameters[11]) * 16); - track.playbackInfo.volume = Math.floor(parseFloat(parameters[12]) * 16); + track.playbackInfo.balance = Math.floor(GpifParser.parseFloatSafe(parameters[11], 0.5) * 16); + track.playbackInfo.volume = Math.floor(GpifParser.parseFloatSafe(parameters[12], 0.9) * 16); } } } @@ -1185,237 +1158,232 @@ export class GpifParser { // ... // private parseMasterBarsNode(node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'MasterBar': - this.parseMasterBar(c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'MasterBar': + this.parseMasterBar(c); + break; } } } private parseMasterBar(node: XmlNode): void { - let masterBar: MasterBar = new MasterBar(); + const masterBar: MasterBar = new MasterBar(); if (this._masterBars.length === 0 && this._hasAnacrusis) { masterBar.isAnacrusis = true; } - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Time': - let timeParts: string[] = c.innerText.split('/'); - masterBar.timeSignatureNumerator = parseInt(timeParts[0]); - masterBar.timeSignatureDenominator = parseInt(timeParts[1]); - break; - case 'FreeTime': - masterBar.isFreeTime = true; - break; - case 'DoubleBar': - masterBar.isDoubleBar = true; - break; - case 'Section': - masterBar.section = new Section(); - masterBar.section.marker = c.findChildElement('Letter')!.innerText; - masterBar.section.text = c.findChildElement('Text')!.innerText; - break; - case 'Repeat': - if (c.getAttribute('start').toLowerCase() === 'true') { - masterBar.isRepeatStart = true; - } - if (c.getAttribute('end').toLowerCase() === 'true' && c.getAttribute('count')) { - masterBar.repeatCount = parseInt(c.getAttribute('count')); - } - break; - case 'AlternateEndings': - let alternateEndings: string[] = c.innerText.split(' '); - let i: number = 0; - for (let k: number = 0; k < alternateEndings.length; k++) { - i = i | (1 << (-1 + parseInt(alternateEndings[k]))); - } - masterBar.alternateEndings = i; - break; - case 'Bars': - this._barsOfMasterBar.push(c.innerText.split(' ')); - break; - case 'TripletFeel': - switch (c.innerText) { - case 'NoTripletFeel': - masterBar.tripletFeel = TripletFeel.NoTripletFeel; - break; - case 'Triplet8th': - masterBar.tripletFeel = TripletFeel.Triplet8th; - break; - case 'Triplet16th': - masterBar.tripletFeel = TripletFeel.Triplet16th; - break; - case 'Dotted8th': - masterBar.tripletFeel = TripletFeel.Dotted8th; - break; - case 'Dotted16th': - masterBar.tripletFeel = TripletFeel.Dotted16th; - break; - case 'Scottish8th': - masterBar.tripletFeel = TripletFeel.Scottish8th; - break; - case 'Scottish16th': - masterBar.tripletFeel = TripletFeel.Scottish16th; + for (const c of node.childElements()) { + switch (c.localName) { + case 'Time': + const timeParts: string[] = c.innerText.split('/'); + masterBar.timeSignatureNumerator = GpifParser.parseIntSafe(timeParts[0], 4); + masterBar.timeSignatureDenominator = GpifParser.parseIntSafe(timeParts[1], 4); + break; + case 'FreeTime': + masterBar.isFreeTime = true; + break; + case 'DoubleBar': + masterBar.isDoubleBar = true; + this._doubleBars.add(masterBar); + break; + case 'Section': + masterBar.section = new Section(); + masterBar.section.marker = c.findChildElement('Letter')?.innerText ?? ''; + masterBar.section.text = c.findChildElement('Text')?.innerText ?? ''; + break; + case 'Repeat': + if (c.getAttribute('start').toLowerCase() === 'true') { + masterBar.isRepeatStart = true; + } + if (c.getAttribute('end').toLowerCase() === 'true' && c.getAttribute('count')) { + masterBar.repeatCount = GpifParser.parseIntSafe(c.getAttribute('count'), 1); + } + break; + case 'AlternateEndings': + const alternateEndings: string[] = GpifParser.splitSafe(c.innerText); + let i: number = 0; + for (let k: number = 0; k < alternateEndings.length; k++) { + i = i | (1 << (-1 + GpifParser.parseIntSafe(alternateEndings[k], 0))); + } + masterBar.alternateEndings = i; + break; + case 'Bars': + this._barsOfMasterBar.push(GpifParser.splitSafe(c.innerText)); + break; + case 'TripletFeel': + switch (c.innerText) { + case 'NoTripletFeel': + masterBar.tripletFeel = TripletFeel.NoTripletFeel; + break; + case 'Triplet8th': + masterBar.tripletFeel = TripletFeel.Triplet8th; + break; + case 'Triplet16th': + masterBar.tripletFeel = TripletFeel.Triplet16th; + break; + case 'Dotted8th': + masterBar.tripletFeel = TripletFeel.Dotted8th; + break; + case 'Dotted16th': + masterBar.tripletFeel = TripletFeel.Dotted16th; + break; + case 'Scottish8th': + masterBar.tripletFeel = TripletFeel.Scottish8th; + break; + case 'Scottish16th': + masterBar.tripletFeel = TripletFeel.Scottish16th; + break; + } + break; + case 'Key': + const keySignature = GpifParser.parseIntSafe( + c.findChildElement('AccidentalCount')?.innerText, + 0 + ) as KeySignature; + const mode = c.findChildElement('Mode'); + let keySignatureType = KeySignatureType.Major; + if (mode) { + switch (mode.innerText.toLowerCase()) { + case 'major': + keySignatureType = KeySignatureType.Major; + break; + case 'minor': + keySignatureType = KeySignatureType.Minor; break; } - break; - case 'Key': - masterBar.keySignature = parseInt( - c.findChildElement('AccidentalCount')!.innerText - ) as KeySignature; - let mode: XmlNode = c.findChildElement('Mode')!; - if (mode) { - switch (mode.innerText.toLowerCase()) { - case 'major': - masterBar.keySignatureType = KeySignatureType.Major; - break; - case 'minor': - masterBar.keySignatureType = KeySignatureType.Minor; - break; - } - } - break; - case 'Fermatas': - this.parseFermatas(masterBar, c); - break; - case 'XProperties': - this.parseMasterBarXProperties(masterBar, c); - break; - case 'Directions': - this.parseDirections(masterBar, c); - break; - } + } + + this._keySignatures.set(this._masterBars.length, [keySignature, keySignatureType]); + break; + case 'Fermatas': + this.parseFermatas(masterBar, c); + break; + case 'XProperties': + this.parseMasterBarXProperties(masterBar, c); + break; + case 'Directions': + this.parseDirections(masterBar, c); + break; } } this._masterBars.push(masterBar); } private parseDirections(masterBar: MasterBar, node: XmlNode) { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Target': - switch (c.innerText) { - case 'Coda': - masterBar.addDirection(Direction.TargetCoda); - break; - case 'DoubleCoda': - masterBar.addDirection(Direction.TargetDoubleCoda); - break; - case 'Segno': - masterBar.addDirection(Direction.TargetSegno); - break; - case 'SegnoSegno': - masterBar.addDirection(Direction.TargetSegnoSegno); - break; - case 'Fine': - masterBar.addDirection(Direction.TargetFine); - break; - } - break; - case 'Jump': - switch (c.innerText) { - case 'DaCapo': - masterBar.addDirection(Direction.JumpDaCapo); - break; - case 'DaCapoAlCoda': - masterBar.addDirection(Direction.JumpDaCapoAlCoda); - break; - case 'DaCapoAlDoubleCoda': - masterBar.addDirection(Direction.JumpDaCapoAlDoubleCoda); - break; - case 'DaCapoAlFine': - masterBar.addDirection(Direction.JumpDaCapoAlFine); - break; + for (const c of node.childElements()) { + switch (c.localName) { + case 'Target': + switch (c.innerText) { + case 'Coda': + masterBar.addDirection(Direction.TargetCoda); + break; + case 'DoubleCoda': + masterBar.addDirection(Direction.TargetDoubleCoda); + break; + case 'Segno': + masterBar.addDirection(Direction.TargetSegno); + break; + case 'SegnoSegno': + masterBar.addDirection(Direction.TargetSegnoSegno); + break; + case 'Fine': + masterBar.addDirection(Direction.TargetFine); + break; + } + break; + case 'Jump': + switch (c.innerText) { + case 'DaCapo': + masterBar.addDirection(Direction.JumpDaCapo); + break; + case 'DaCapoAlCoda': + masterBar.addDirection(Direction.JumpDaCapoAlCoda); + break; + case 'DaCapoAlDoubleCoda': + masterBar.addDirection(Direction.JumpDaCapoAlDoubleCoda); + break; + case 'DaCapoAlFine': + masterBar.addDirection(Direction.JumpDaCapoAlFine); + break; - // Note: no typo on our side, GPIF has wrongly "DaSegno" instead of "DalSegno" - case 'DaSegno': - masterBar.addDirection(Direction.JumpDalSegno); - break; - case 'DaSegnoAlCoda': - masterBar.addDirection(Direction.JumpDalSegnoAlCoda); - break; - case 'DaSegnoAlDoubleCoda': - masterBar.addDirection(Direction.JumpDalSegnoAlDoubleCoda); - break; - case 'DaSegnoAlFine': - masterBar.addDirection(Direction.JumpDalSegnoAlFine); - break; + // Note: no typo on our side, GPIF has wrongly "DaSegno" instead of "DalSegno" + case 'DaSegno': + masterBar.addDirection(Direction.JumpDalSegno); + break; + case 'DaSegnoAlCoda': + masterBar.addDirection(Direction.JumpDalSegnoAlCoda); + break; + case 'DaSegnoAlDoubleCoda': + masterBar.addDirection(Direction.JumpDalSegnoAlDoubleCoda); + break; + case 'DaSegnoAlFine': + masterBar.addDirection(Direction.JumpDalSegnoAlFine); + break; - case 'DaSegnoSegno': - masterBar.addDirection(Direction.JumpDalSegnoSegno); - break; - case 'DaSegnoSegnoAlCoda': - masterBar.addDirection(Direction.JumpDalSegnoSegnoAlCoda); - break; - case 'DaSegnoSegnoAlDoubleCoda': - masterBar.addDirection(Direction.JumpDalSegnoSegnoAlDoubleCoda); - break; - case 'DaSegnoSegnoAlFine': - masterBar.addDirection(Direction.JumpDalSegnoSegnoAlFine); - break; + case 'DaSegnoSegno': + masterBar.addDirection(Direction.JumpDalSegnoSegno); + break; + case 'DaSegnoSegnoAlCoda': + masterBar.addDirection(Direction.JumpDalSegnoSegnoAlCoda); + break; + case 'DaSegnoSegnoAlDoubleCoda': + masterBar.addDirection(Direction.JumpDalSegnoSegnoAlDoubleCoda); + break; + case 'DaSegnoSegnoAlFine': + masterBar.addDirection(Direction.JumpDalSegnoSegnoAlFine); + break; - case 'DaCoda': - masterBar.addDirection(Direction.JumpDaCoda); - break; - case 'DaDoubleCoda': - masterBar.addDirection(Direction.JumpDaDoubleCoda); - break; - } - break; - } + case 'DaCoda': + masterBar.addDirection(Direction.JumpDaCoda); + break; + case 'DaDoubleCoda': + masterBar.addDirection(Direction.JumpDaDoubleCoda); + break; + } + break; } } } private parseFermatas(masterBar: MasterBar, node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Fermata': - this.parseFermata(masterBar, c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Fermata': + this.parseFermata(masterBar, c); + break; } } } private parseFermata(masterBar: MasterBar, node: XmlNode): void { let offset: number = 0; - let fermata: Fermata = new Fermata(); - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Type': - switch (c.innerText) { - case 'Short': - fermata.type = FermataType.Short; - break; - case 'Medium': - fermata.type = FermataType.Medium; - break; - case 'Long': - fermata.type = FermataType.Long; - break; - } - break; - case 'Length': - fermata.length = parseFloat(c.innerText); - break; - case 'Offset': - let parts: string[] = c.innerText.split('/'); - if (parts.length === 2) { - let numerator: number = parseInt(parts[0]); - let denominator: number = parseInt(parts[1]); - offset = ((numerator / denominator) * MidiUtils.QuarterTime) | 0; - } - break; - } + const fermata: Fermata = new Fermata(); + for (const c of node.childElements()) { + switch (c.localName) { + case 'Type': + switch (c.innerText) { + case 'Short': + fermata.type = FermataType.Short; + break; + case 'Medium': + fermata.type = FermataType.Medium; + break; + case 'Long': + fermata.type = FermataType.Long; + break; + } + break; + case 'Length': + fermata.length = GpifParser.parseFloatSafe(c.innerText, 0); + break; + case 'Offset': + const parts: string[] = c.innerText.split('/'); + if (parts.length === 2) { + const numerator: number = GpifParser.parseIntSafe(parts[0], 4); + const denominator: number = GpifParser.parseIntSafe(parts[1], 4); + offset = ((numerator / denominator) * MidiUtils.QuarterTime) | 0; + } + break; } } masterBar.addFermata(offset, fermata); @@ -1425,108 +1393,100 @@ export class GpifParser { // ... // private parseBars(node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Bar': - this.parseBar(c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Bar': + this.parseBar(c); + break; } } } private parseBar(node: XmlNode): void { - let bar: Bar = new Bar(); - let barId: string = node.getAttribute('id'); - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Voices': - this._voicesOfBar.set(barId, c.innerText.split(' ')); - break; - case 'Clef': - switch (c.innerText) { - case 'Neutral': - bar.clef = Clef.Neutral; - break; - case 'G2': - bar.clef = Clef.G2; - break; - case 'F4': - bar.clef = Clef.F4; - break; - case 'C4': - bar.clef = Clef.C4; - break; - case 'C3': - bar.clef = Clef.C3; - break; - } - break; - case 'Ottavia': - switch (c.innerText) { - case '8va': - bar.clefOttava = Ottavia._8va; - break; - case '15ma': - bar.clefOttava = Ottavia._15ma; - break; - case '8vb': - bar.clefOttava = Ottavia._8vb; - break; - case '15mb': - bar.clefOttava = Ottavia._15mb; - break; - } - break; - case 'SimileMark': - switch (c.innerText) { - case 'Simple': - bar.simileMark = SimileMark.Simple; - break; - case 'FirstOfDouble': - bar.simileMark = SimileMark.FirstOfDouble; - break; - case 'SecondOfDouble': - bar.simileMark = SimileMark.SecondOfDouble; - break; - } - break; - case 'XProperties': - this.parseBarXProperties(c, bar); - break; - } - } - } - this._barsById.set(barId, bar); - } - - // - // ... - // - private parseVoices(node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Voice': - this.parseVoice(c); - break; - } - } - } - } - - private parseVoice(node: XmlNode): void { - let voice: Voice = new Voice(); - let voiceId: string = node.getAttribute('id'); - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Beats': - this._beatsOfVoice.set(voiceId, c.innerText.split(' ')); - break; - } + const bar: Bar = new Bar(); + const barId: string = node.getAttribute('id'); + for (const c of node.childElements()) { + switch (c.localName) { + case 'Voices': + this._voicesOfBar.set(barId, GpifParser.splitSafe(c.innerText)); + break; + case 'Clef': + switch (c.innerText) { + case 'Neutral': + bar.clef = Clef.Neutral; + break; + case 'G2': + bar.clef = Clef.G2; + break; + case 'F4': + bar.clef = Clef.F4; + break; + case 'C4': + bar.clef = Clef.C4; + break; + case 'C3': + bar.clef = Clef.C3; + break; + } + break; + case 'Ottavia': + switch (c.innerText) { + case '8va': + bar.clefOttava = Ottavia._8va; + break; + case '15ma': + bar.clefOttava = Ottavia._15ma; + break; + case '8vb': + bar.clefOttava = Ottavia._8vb; + break; + case '15mb': + bar.clefOttava = Ottavia._15mb; + break; + } + break; + case 'SimileMark': + switch (c.innerText) { + case 'Simple': + bar.simileMark = SimileMark.Simple; + break; + case 'FirstOfDouble': + bar.simileMark = SimileMark.FirstOfDouble; + break; + case 'SecondOfDouble': + bar.simileMark = SimileMark.SecondOfDouble; + break; + } + break; + case 'XProperties': + this.parseBarXProperties(c, bar); + break; + } + } + this._barsById.set(barId, bar); + } + + // + // ... + // + private parseVoices(node: XmlNode): void { + for (const c of node.childElements()) { + switch (c.localName) { + case 'Voice': + this.parseVoice(c); + break; + } + } + } + + private parseVoice(node: XmlNode): void { + const voice: Voice = new Voice(); + const voiceId: string = node.getAttribute('id'); + for (const c of node.childElements()) { + switch (c.localName) { + case 'Beats': + this._beatsOfVoice.set(voiceId, GpifParser.splitSafe(c.innerText)); + break; } } this._voiceById.set(voiceId, voice); @@ -1536,218 +1496,225 @@ export class GpifParser { // ... // private parseBeats(node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Beat': - this.parseBeat(c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Beat': + this.parseBeat(c); + break; } } } private parseBeat(node: XmlNode): void { - let beat: Beat = new Beat(); - let beatId: string = node.getAttribute('id'); - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Notes': - this._notesOfBeat.set(beatId, c.innerText.split(' ')); - break; - case 'Rhythm': - this._rhythmOfBeat.set(beatId, c.getAttribute('ref')); - break; - case 'Fadding': - switch (c.innerText) { - case 'FadeIn': - beat.fade = FadeType.FadeIn; - break; - case 'FadeOut': - beat.fade = FadeType.FadeOut; - break; - case 'VolumeSwell': - beat.fade = FadeType.VolumeSwell; - break; - } - break; - case 'Tremolo': - switch (c.innerText) { - case '1/2': - beat.tremoloSpeed = Duration.Eighth; - break; - case '1/4': - beat.tremoloSpeed = Duration.Sixteenth; - break; - case '1/8': - beat.tremoloSpeed = Duration.ThirtySecond; - break; - } - break; - case 'Chord': - beat.chordId = c.innerText; - break; - case 'Hairpin': - switch (c.innerText) { - case 'Crescendo': - beat.crescendo = CrescendoType.Crescendo; - break; - case 'Decrescendo': - beat.crescendo = CrescendoType.Decrescendo; - break; - } - break; - case 'Arpeggio': - if (c.innerText === 'Up') { - beat.brushType = BrushType.ArpeggioUp; - } else { - beat.brushType = BrushType.ArpeggioDown; - } - break; - case 'Properties': - this.parseBeatProperties(c, beat); - break; - case 'XProperties': - this.parseBeatXProperties(c, beat); - break; - case 'FreeText': - beat.text = c.innerText; - break; - case 'TransposedPitchStemOrientation': - switch (c.innerText) { - case 'Upward': - beat.preferredBeamDirection = BeamDirection.Up; - break; - case 'Downward': - beat.preferredBeamDirection = BeamDirection.Down; - break; - } - break; - case 'Dynamic': - switch (c.innerText) { - case 'PPP': - beat.dynamics = DynamicValue.PPP; - break; - case 'PP': - beat.dynamics = DynamicValue.PP; - break; - case 'P': - beat.dynamics = DynamicValue.P; - break; - case 'MP': - beat.dynamics = DynamicValue.MP; - break; - case 'MF': - beat.dynamics = DynamicValue.MF; - break; - case 'F': - beat.dynamics = DynamicValue.F; - break; - case 'FF': - beat.dynamics = DynamicValue.FF; - break; - case 'FFF': - beat.dynamics = DynamicValue.FFF; - break; - } - break; - case 'GraceNotes': - switch (c.innerText) { - case 'OnBeat': - beat.graceType = GraceType.OnBeat; - break; - case 'BeforeBeat': - beat.graceType = GraceType.BeforeBeat; - break; - } - break; - case 'Legato': - if (c.getAttribute('origin') === 'true') { - beat.isLegatoOrigin = true; - } - break; - case 'Whammy': - let whammyOrigin: BendPoint = new BendPoint(0, 0); - whammyOrigin.value = this.toBendValue(parseFloat(c.getAttribute('originValue'))); - whammyOrigin.offset = this.toBendOffset(parseFloat(c.getAttribute('originOffset'))); - beat.addWhammyBarPoint(whammyOrigin); - let whammyMiddle1: BendPoint = new BendPoint(0, 0); - whammyMiddle1.value = this.toBendValue(parseFloat(c.getAttribute('middleValue'))); - whammyMiddle1.offset = this.toBendOffset(parseFloat(c.getAttribute('middleOffset1'))); - beat.addWhammyBarPoint(whammyMiddle1); - let whammyMiddle2: BendPoint = new BendPoint(0, 0); - whammyMiddle2.value = this.toBendValue(parseFloat(c.getAttribute('middleValue'))); - whammyMiddle2.offset = this.toBendOffset(parseFloat(c.getAttribute('middleOffset2'))); - beat.addWhammyBarPoint(whammyMiddle2); - let whammyDestination: BendPoint = new BendPoint(0, 0); - whammyDestination.value = this.toBendValue(parseFloat(c.getAttribute('destinationValue'))); - whammyDestination.offset = this.toBendOffset(parseFloat(c.getAttribute('destinationOffset'))); - beat.addWhammyBarPoint(whammyDestination); - break; - case 'Ottavia': - switch (c.innerText) { - case '8va': - beat.ottava = Ottavia._8va; - break; - case '8vb': - beat.ottava = Ottavia._8vb; - break; - case '15ma': - beat.ottava = Ottavia._15ma; - break; - case '15mb': - beat.ottava = Ottavia._15mb; - break; - } - break; - case 'Lyrics': - beat.lyrics = this.parseBeatLyrics(c); - this._skipApplyLyrics = true; - break; - case 'Slashed': - beat.slashed = true; - break; - case 'DeadSlapped': - beat.deadSlapped = true; - break; - case 'Golpe': - switch (c.innerText) { - case 'Finger': - beat.golpe = GolpeType.Finger; - break; - case 'Thumb': - beat.golpe = GolpeType.Thumb; - break; - } - break; - case 'Wah': - switch (c.innerText) { - case 'Open': - beat.wahPedal = WahPedal.Open; - break; - case 'Closed': - beat.wahPedal = WahPedal.Closed; - break; - } - break; - case 'UserTransposedPitchStemOrientation': - switch (c.innerText) { - case 'Downward': - beat.preferredBeamDirection = BeamDirection.Down; - break; - case 'Upward': - beat.preferredBeamDirection = BeamDirection.Up; - break; - } - break; - case 'Timer': - beat.showTimer = true; - if(c.innerText.length > 0) { - beat.timer = parseInt(c.innerText); - } - break; - } + const beat: Beat = new Beat(); + const beatId: string = node.getAttribute('id'); + for (const c of node.childElements()) { + switch (c.localName) { + case 'Notes': + this._notesOfBeat.set(beatId, GpifParser.splitSafe(c.innerText)); + break; + case 'Rhythm': + this._rhythmOfBeat.set(beatId, c.getAttribute('ref')); + break; + case 'Fadding': + switch (c.innerText) { + case 'FadeIn': + beat.fade = FadeType.FadeIn; + break; + case 'FadeOut': + beat.fade = FadeType.FadeOut; + break; + case 'VolumeSwell': + beat.fade = FadeType.VolumeSwell; + break; + } + break; + case 'Tremolo': + switch (c.innerText) { + case '1/2': + beat.tremoloSpeed = Duration.Eighth; + break; + case '1/4': + beat.tremoloSpeed = Duration.Sixteenth; + break; + case '1/8': + beat.tremoloSpeed = Duration.ThirtySecond; + break; + } + break; + case 'Chord': + beat.chordId = c.innerText; + break; + case 'Hairpin': + switch (c.innerText) { + case 'Crescendo': + beat.crescendo = CrescendoType.Crescendo; + break; + case 'Decrescendo': + beat.crescendo = CrescendoType.Decrescendo; + break; + } + break; + case 'Arpeggio': + if (c.innerText === 'Up') { + beat.brushType = BrushType.ArpeggioUp; + } else { + beat.brushType = BrushType.ArpeggioDown; + } + break; + case 'Properties': + this.parseBeatProperties(c, beat); + break; + case 'XProperties': + this.parseBeatXProperties(c, beat); + break; + case 'FreeText': + beat.text = c.innerText; + break; + case 'TransposedPitchStemOrientation': + switch (c.innerText) { + case 'Upward': + beat.preferredBeamDirection = BeamDirection.Up; + break; + case 'Downward': + beat.preferredBeamDirection = BeamDirection.Down; + break; + } + break; + case 'Dynamic': + switch (c.innerText) { + case 'PPP': + beat.dynamics = DynamicValue.PPP; + break; + case 'PP': + beat.dynamics = DynamicValue.PP; + break; + case 'P': + beat.dynamics = DynamicValue.P; + break; + case 'MP': + beat.dynamics = DynamicValue.MP; + break; + case 'MF': + beat.dynamics = DynamicValue.MF; + break; + case 'F': + beat.dynamics = DynamicValue.F; + break; + case 'FF': + beat.dynamics = DynamicValue.FF; + break; + case 'FFF': + beat.dynamics = DynamicValue.FFF; + break; + } + break; + case 'GraceNotes': + switch (c.innerText) { + case 'OnBeat': + beat.graceType = GraceType.OnBeat; + break; + case 'BeforeBeat': + beat.graceType = GraceType.BeforeBeat; + break; + } + break; + case 'Legato': + if (c.getAttribute('origin') === 'true') { + beat.isLegatoOrigin = true; + } + break; + case 'Whammy': + const whammyOrigin: BendPoint = new BendPoint(0, 0); + whammyOrigin.value = this.toBendValue(GpifParser.parseFloatSafe(c.getAttribute('originValue'), 0)); + whammyOrigin.offset = this.toBendOffset( + GpifParser.parseFloatSafe(c.getAttribute('originOffset'), 0) + ); + beat.addWhammyBarPoint(whammyOrigin); + const whammyMiddle1: BendPoint = new BendPoint(0, 0); + whammyMiddle1.value = this.toBendValue(GpifParser.parseFloatSafe(c.getAttribute('middleValue'), 0)); + whammyMiddle1.offset = this.toBendOffset( + GpifParser.parseFloatSafe(c.getAttribute('middleOffset1'), 0) + ); + beat.addWhammyBarPoint(whammyMiddle1); + const whammyMiddle2: BendPoint = new BendPoint(0, 0); + whammyMiddle2.value = this.toBendValue(GpifParser.parseFloatSafe(c.getAttribute('middleValue'), 0)); + whammyMiddle2.offset = this.toBendOffset( + GpifParser.parseFloatSafe(c.getAttribute('middleOffset2'), 0) + ); + beat.addWhammyBarPoint(whammyMiddle2); + const whammyDestination: BendPoint = new BendPoint(0, 0); + whammyDestination.value = this.toBendValue( + GpifParser.parseFloatSafe(c.getAttribute('destinationValue'), 0) + ); + whammyDestination.offset = this.toBendOffset( + GpifParser.parseFloatSafe(c.getAttribute('destinationOffset'), 0) + ); + beat.addWhammyBarPoint(whammyDestination); + break; + case 'Ottavia': + switch (c.innerText) { + case '8va': + beat.ottava = Ottavia._8va; + break; + case '8vb': + beat.ottava = Ottavia._8vb; + break; + case '15ma': + beat.ottava = Ottavia._15ma; + break; + case '15mb': + beat.ottava = Ottavia._15mb; + break; + } + break; + case 'Lyrics': + beat.lyrics = this.parseBeatLyrics(c); + this._skipApplyLyrics = true; + break; + case 'Slashed': + beat.slashed = true; + break; + case 'DeadSlapped': + beat.deadSlapped = true; + break; + case 'Golpe': + switch (c.innerText) { + case 'Finger': + beat.golpe = GolpeType.Finger; + break; + case 'Thumb': + beat.golpe = GolpeType.Thumb; + break; + } + break; + case 'Wah': + switch (c.innerText) { + case 'Open': + beat.wahPedal = WahPedal.Open; + break; + case 'Closed': + beat.wahPedal = WahPedal.Closed; + break; + } + break; + case 'UserTransposedPitchStemOrientation': + switch (c.innerText) { + case 'Downward': + beat.preferredBeamDirection = BeamDirection.Down; + break; + case 'Upward': + beat.preferredBeamDirection = BeamDirection.Up; + break; + } + break; + case 'Timer': + beat.showTimer = true; + beat.timer = GpifParser.parseIntSafe(c.innerText, -1); + if (beat.timer < 0) { + beat.timer = null; + } + break; } } this._beatById.set(beatId, beat); @@ -1756,13 +1723,11 @@ export class GpifParser { private parseBeatLyrics(node: XmlNode): string[] { const lines: string[] = []; - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Line': - lines.push(c.innerText); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Line': + lines.push(c.innerText); + break; } } @@ -1770,80 +1735,77 @@ export class GpifParser { } private parseBeatXProperties(node: XmlNode, beat: Beat): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'XProperty': - let id: string = c.getAttribute('id'); - let value: number = 0; - switch (id) { - case '1124204546': - value = parseInt(c.findChildElement('Int')!.innerText); - switch (value) { - case 1: - beat.beamingMode = BeatBeamingMode.ForceMergeWithNext; - break; - case 2: - beat.beamingMode = BeatBeamingMode.ForceSplitToNext; - break; - } - break; - case '1124204552': - value = parseInt(c.findChildElement('Int')!.innerText); - switch (value) { - case 1: - if (beat.beamingMode !== BeatBeamingMode.ForceSplitToNext) { - beat.beamingMode = BeatBeamingMode.ForceSplitOnSecondaryToNext; - } - break; - } - break; - case '1124204545': - value = parseInt(c.findChildElement('Int')!.innerText); - beat.invertBeamDirection = value === 1; - break; - case '687935489': - value = parseInt(c.findChildElement('Int')!.innerText); - beat.brushDuration = value; - break; - } - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'XProperty': + const id: string = c.getAttribute('id'); + let value: number = 0; + switch (id) { + case '1124204546': + value = GpifParser.parseIntSafe(c.findChildElement('Int')?.innerText, 0); + switch (value) { + case 1: + beat.beamingMode = BeatBeamingMode.ForceMergeWithNext; + break; + case 2: + beat.beamingMode = BeatBeamingMode.ForceSplitToNext; + break; + } + break; + case '1124204552': + value = GpifParser.parseIntSafe(c.findChildElement('Int')?.innerText, 0); + switch (value) { + case 1: + if (beat.beamingMode !== BeatBeamingMode.ForceSplitToNext) { + beat.beamingMode = BeatBeamingMode.ForceSplitOnSecondaryToNext; + } + break; + } + break; + case '1124204545': + value = GpifParser.parseIntSafe(c.findChildElement('Int')?.innerText, 0); + beat.invertBeamDirection = value === 1; + break; + case '687935489': + value = GpifParser.parseIntSafe(c.findChildElement('Int')?.innerText, 0); + beat.brushDuration = value; + break; + } + break; } } } private parseBarXProperties(node: XmlNode, bar: Bar) { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'XProperty': - const id: string = c.getAttribute('id'); - switch (id) { - case '1124139520': - const childNode = c.findChildElement('Double') ?? c.findChildElement('Float'); - bar.displayScale = parseFloat(childNode!.innerText); - break; - } - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'XProperty': + const id: string = c.getAttribute('id'); + switch (id) { + case '1124139520': + const childNode = c.findChildElement('Double') ?? c.findChildElement('Float'); + bar.displayScale = GpifParser.parseFloatSafe(childNode?.innerText, 1); + break; + } + break; } } } private parseMasterBarXProperties(masterBar: MasterBar, node: XmlNode) { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'XProperty': - const id: string = c.getAttribute('id'); - switch (id) { - case '1124073984': - masterBar.displayScale = parseFloat(c.findChildElement('Double')!.innerText); - break; - } - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'XProperty': + const id: string = c.getAttribute('id'); + switch (id) { + case '1124073984': + masterBar.displayScale = GpifParser.parseFloatSafe( + c.findChildElement('Double')?.innerText, + 1 + ); + break; + } + break; } } } @@ -1855,173 +1817,171 @@ export class GpifParser { let whammyMiddleOffset1: number | null = null; let whammyMiddleOffset2: number | null = null; let whammyDestination: BendPoint | null = null; - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Property': - let name: string = c.getAttribute('name'); - switch (name) { - case 'Brush': - if (c.findChildElement('Direction')!.innerText === 'Up') { - beat.brushType = BrushType.BrushUp; - } else { - beat.brushType = BrushType.BrushDown; - } - break; - case 'PickStroke': - if (c.findChildElement('Direction')!.innerText === 'Up') { - beat.pickStroke = PickStroke.Up; - } else { - beat.pickStroke = PickStroke.Down; - } - break; - case 'Slapped': - if (c.findChildElement('Enable')) { - beat.slap = true; - } - break; - case 'Popped': - if (c.findChildElement('Enable')) { - beat.pop = true; - } - break; - case 'VibratoWTremBar': - switch (c.findChildElement('Strength')!.innerText) { - case 'Wide': - beat.vibrato = VibratoType.Wide; - break; - case 'Slight': - beat.vibrato = VibratoType.Slight; - break; - } - break; - case 'WhammyBar': - isWhammy = true; - break; - case 'WhammyBarExtend': - // not clear what this is used for - break; - case 'WhammyBarOriginValue': - if (!whammyOrigin) { - whammyOrigin = new BendPoint(0, 0); - } - whammyOrigin.value = this.toBendValue( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'WhammyBarOriginOffset': - if (!whammyOrigin) { - whammyOrigin = new BendPoint(0, 0); - } - whammyOrigin.offset = this.toBendOffset( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'WhammyBarMiddleValue': - whammyMiddleValue = this.toBendValue( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'WhammyBarMiddleOffset1': - whammyMiddleOffset1 = this.toBendOffset( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'WhammyBarMiddleOffset2': - whammyMiddleOffset2 = this.toBendOffset( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'WhammyBarDestinationValue': - if (!whammyDestination) { - whammyDestination = new BendPoint(BendPoint.MaxPosition, 0); - } - whammyDestination.value = this.toBendValue( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'WhammyBarDestinationOffset': - if (!whammyDestination) { - whammyDestination = new BendPoint(0, 0); - } - whammyDestination.offset = this.toBendOffset( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'BarreFret': - beat.barreFret = parseInt(c.findChildElement('Fret')!.innerText); - break; - case 'BarreString': - switch (c.findChildElement('String')!.innerText) { - case '0': - beat.barreShape = BarreShape.Full; - break; - case '1': - beat.barreShape = BarreShape.Half; - break; - } - break; - case 'Rasgueado': - switch (c.findChildElement('Rasgueado')!.innerText) { - case 'ii_1': - beat.rasgueado = Rasgueado.Ii; - break; - case 'mi_1': - beat.rasgueado = Rasgueado.Mi; - break; - case 'mii_1': - beat.rasgueado = Rasgueado.MiiTriplet; - break; - case 'mii_2': - beat.rasgueado = Rasgueado.MiiAnapaest; - break; - case 'pmp_1': - beat.rasgueado = Rasgueado.PmpTriplet; - break; - case 'pmp_2': - beat.rasgueado = Rasgueado.PmpAnapaest; - break; - case 'pei_1': - beat.rasgueado = Rasgueado.PeiTriplet; - break; - case 'pei_2': - beat.rasgueado = Rasgueado.PeiAnapaest; - break; - case 'pai_1': - beat.rasgueado = Rasgueado.PaiTriplet; - break; - case 'pai_2': - beat.rasgueado = Rasgueado.PaiAnapaest; - break; - case 'ami_1': - beat.rasgueado = Rasgueado.AmiTriplet; - break; - case 'ami_2': - beat.rasgueado = Rasgueado.AmiAnapaest; - break; - case 'ppp_1': - beat.rasgueado = Rasgueado.Ppp; - break; - case 'amii_1': - beat.rasgueado = Rasgueado.Amii; - break; - case 'amip_1': - beat.rasgueado = Rasgueado.Amip; - break; - case 'eami_1': - beat.rasgueado = Rasgueado.Eami; - break; - case 'eamii_1': - beat.rasgueado = Rasgueado.Eamii; - break; - case 'peami_1': - beat.rasgueado = Rasgueado.Peami; - break; - } - break; - } - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Property': + const name: string = c.getAttribute('name'); + switch (name) { + case 'Brush': + if (c.findChildElement('Direction')?.innerText === 'Up') { + beat.brushType = BrushType.BrushUp; + } else { + beat.brushType = BrushType.BrushDown; + } + break; + case 'PickStroke': + if (c.findChildElement('Direction')?.innerText === 'Up') { + beat.pickStroke = PickStroke.Up; + } else { + beat.pickStroke = PickStroke.Down; + } + break; + case 'Slapped': + if (c.findChildElement('Enable')) { + beat.slap = true; + } + break; + case 'Popped': + if (c.findChildElement('Enable')) { + beat.pop = true; + } + break; + case 'VibratoWTremBar': + switch (c.findChildElement('Strength')?.innerText) { + case 'Wide': + beat.vibrato = VibratoType.Wide; + break; + case 'Slight': + beat.vibrato = VibratoType.Slight; + break; + } + break; + case 'WhammyBar': + isWhammy = true; + break; + case 'WhammyBarExtend': + // not clear what this is used for + break; + case 'WhammyBarOriginValue': + if (!whammyOrigin) { + whammyOrigin = new BendPoint(0, 0); + } + whammyOrigin.value = this.toBendValue( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'WhammyBarOriginOffset': + if (!whammyOrigin) { + whammyOrigin = new BendPoint(0, 0); + } + whammyOrigin.offset = this.toBendOffset( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'WhammyBarMiddleValue': + whammyMiddleValue = this.toBendValue( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'WhammyBarMiddleOffset1': + whammyMiddleOffset1 = this.toBendOffset( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'WhammyBarMiddleOffset2': + whammyMiddleOffset2 = this.toBendOffset( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'WhammyBarDestinationValue': + if (!whammyDestination) { + whammyDestination = new BendPoint(BendPoint.MaxPosition, 0); + } + whammyDestination.value = this.toBendValue( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'WhammyBarDestinationOffset': + if (!whammyDestination) { + whammyDestination = new BendPoint(0, 0); + } + whammyDestination.offset = this.toBendOffset( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'BarreFret': + beat.barreFret = GpifParser.parseIntSafe(c.findChildElement('Fret')?.innerText, 0); + break; + case 'BarreString': + switch (c.findChildElement('String')?.innerText) { + case '0': + beat.barreShape = BarreShape.Full; + break; + case '1': + beat.barreShape = BarreShape.Half; + break; + } + break; + case 'Rasgueado': + switch (c.findChildElement('Rasgueado')?.innerText) { + case 'ii_1': + beat.rasgueado = Rasgueado.Ii; + break; + case 'mi_1': + beat.rasgueado = Rasgueado.Mi; + break; + case 'mii_1': + beat.rasgueado = Rasgueado.MiiTriplet; + break; + case 'mii_2': + beat.rasgueado = Rasgueado.MiiAnapaest; + break; + case 'pmp_1': + beat.rasgueado = Rasgueado.PmpTriplet; + break; + case 'pmp_2': + beat.rasgueado = Rasgueado.PmpAnapaest; + break; + case 'pei_1': + beat.rasgueado = Rasgueado.PeiTriplet; + break; + case 'pei_2': + beat.rasgueado = Rasgueado.PeiAnapaest; + break; + case 'pai_1': + beat.rasgueado = Rasgueado.PaiTriplet; + break; + case 'pai_2': + beat.rasgueado = Rasgueado.PaiAnapaest; + break; + case 'ami_1': + beat.rasgueado = Rasgueado.AmiTriplet; + break; + case 'ami_2': + beat.rasgueado = Rasgueado.AmiAnapaest; + break; + case 'ppp_1': + beat.rasgueado = Rasgueado.Ppp; + break; + case 'amii_1': + beat.rasgueado = Rasgueado.Amii; + break; + case 'amip_1': + beat.rasgueado = Rasgueado.Amip; + break; + case 'eami_1': + beat.rasgueado = Rasgueado.Eami; + break; + case 'eamii_1': + beat.rasgueado = Rasgueado.Eamii; + break; + case 'peami_1': + beat.rasgueado = Rasgueado.Peami; + break; + } + break; + } + break; } } if (isWhammy) { @@ -2049,126 +2009,122 @@ export class GpifParser { // ... // private parseNotes(node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Note': - this.parseNote(c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Note': + this.parseNote(c); + break; } } } private parseNote(node: XmlNode): void { - let note: Note = new Note(); - let noteId: string = node.getAttribute('id'); - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Properties': - this.parseNoteProperties(c, note, noteId); - break; - case 'AntiAccent': - if (c.innerText.toLowerCase() === 'normal') { - note.isGhost = true; - } - break; - case 'LetRing': - note.isLetRing = true; - break; - case 'Trill': - note.trillValue = parseInt(c.innerText); - note.trillSpeed = Duration.Sixteenth; - break; - case 'Accent': - let accentFlags: number = parseInt(c.innerText); - if ((accentFlags & 0x01) !== 0) { - note.isStaccato = true; - } - if ((accentFlags & 0x04) !== 0) { - note.accentuated = AccentuationType.Heavy; - } - if ((accentFlags & 0x08) !== 0) { - note.accentuated = AccentuationType.Normal; - } - if ((accentFlags & 0x10) !== 0) { - note.accentuated = AccentuationType.Tenuto; - } - break; - case 'Tie': - if (c.getAttribute('destination').toLowerCase() === 'true') { - note.isTieDestination = true; - } - break; - case 'Vibrato': - switch (c.innerText) { - case 'Slight': - note.vibrato = VibratoType.Slight; - break; - case 'Wide': - note.vibrato = VibratoType.Wide; - break; - } - break; - case 'LeftFingering': - switch (c.innerText) { - case 'P': - note.leftHandFinger = Fingers.Thumb; - break; - case 'I': - note.leftHandFinger = Fingers.IndexFinger; - break; - case 'M': - note.leftHandFinger = Fingers.MiddleFinger; - break; - case 'A': - note.leftHandFinger = Fingers.AnnularFinger; - break; - case 'C': - note.leftHandFinger = Fingers.LittleFinger; - break; - } - break; - case 'RightFingering': - switch (c.innerText) { - case 'P': - note.rightHandFinger = Fingers.Thumb; - break; - case 'I': - note.rightHandFinger = Fingers.IndexFinger; - break; - case 'M': - note.rightHandFinger = Fingers.MiddleFinger; - break; - case 'A': - note.rightHandFinger = Fingers.AnnularFinger; - break; - case 'C': - note.rightHandFinger = Fingers.LittleFinger; - break; - } - break; - case 'InstrumentArticulation': - note.percussionArticulation = parseInt(c.innerText); - break; - case 'Ornament': - switch (c.innerText) { - case 'Turn': - note.ornament = NoteOrnament.Turn; - break; - case 'InvertedTurn': - note.ornament = NoteOrnament.InvertedTurn; - break; - case 'UpperMordent': - note.ornament = NoteOrnament.UpperMordent; - break; - case 'LowerMordent': - note.ornament = NoteOrnament.LowerMordent; - break; - } - break; - } + const note: Note = new Note(); + const noteId: string = node.getAttribute('id'); + for (const c of node.childElements()) { + switch (c.localName) { + case 'Properties': + this.parseNoteProperties(c, note, noteId); + break; + case 'AntiAccent': + if (c.innerText.toLowerCase() === 'normal') { + note.isGhost = true; + } + break; + case 'LetRing': + note.isLetRing = true; + break; + case 'Trill': + note.trillValue = GpifParser.parseIntSafe(c.innerText, -1); + note.trillSpeed = Duration.Sixteenth; + break; + case 'Accent': + const accentFlags: number = GpifParser.parseIntSafe(c.innerText, 0); + if ((accentFlags & 0x01) !== 0) { + note.isStaccato = true; + } + if ((accentFlags & 0x04) !== 0) { + note.accentuated = AccentuationType.Heavy; + } + if ((accentFlags & 0x08) !== 0) { + note.accentuated = AccentuationType.Normal; + } + if ((accentFlags & 0x10) !== 0) { + note.accentuated = AccentuationType.Tenuto; + } + break; + case 'Tie': + if (c.getAttribute('destination').toLowerCase() === 'true') { + note.isTieDestination = true; + } + break; + case 'Vibrato': + switch (c.innerText) { + case 'Slight': + note.vibrato = VibratoType.Slight; + break; + case 'Wide': + note.vibrato = VibratoType.Wide; + break; + } + break; + case 'LeftFingering': + switch (c.innerText) { + case 'P': + note.leftHandFinger = Fingers.Thumb; + break; + case 'I': + note.leftHandFinger = Fingers.IndexFinger; + break; + case 'M': + note.leftHandFinger = Fingers.MiddleFinger; + break; + case 'A': + note.leftHandFinger = Fingers.AnnularFinger; + break; + case 'C': + note.leftHandFinger = Fingers.LittleFinger; + break; + } + break; + case 'RightFingering': + switch (c.innerText) { + case 'P': + note.rightHandFinger = Fingers.Thumb; + break; + case 'I': + note.rightHandFinger = Fingers.IndexFinger; + break; + case 'M': + note.rightHandFinger = Fingers.MiddleFinger; + break; + case 'A': + note.rightHandFinger = Fingers.AnnularFinger; + break; + case 'C': + note.rightHandFinger = Fingers.LittleFinger; + break; + } + break; + case 'InstrumentArticulation': + note.percussionArticulation = GpifParser.parseIntSafe(c.innerText, 0); + break; + case 'Ornament': + switch (c.innerText) { + case 'Turn': + note.ornament = NoteOrnament.Turn; + break; + case 'InvertedTurn': + note.ornament = NoteOrnament.InvertedTurn; + break; + case 'UpperMordent': + note.ornament = NoteOrnament.UpperMordent; + break; + case 'LowerMordent': + note.ornament = NoteOrnament.LowerMordent; + break; + } + break; } } this._noteById.set(noteId, note); @@ -2185,173 +2141,178 @@ export class GpifParser { // GP6 had percussion as element+variation let element: number = -1; let variation: number = -1; - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Property': - let name: string = c.getAttribute('name'); - switch (name) { - case 'ShowStringNumber': - if (c.findChildElement('Enable')) { - note.showStringNumber = true; - } - break; - case 'String': - note.string = parseInt(c.findChildElement('String')!.innerText) + 1; - break; - case 'Fret': - note.fret = parseInt(c.findChildElement('Fret')!.innerText); - break; - case 'Element': - element = parseInt(c.findChildElement('Element')!.innerText); - break; - case 'Variation': - variation = parseInt(c.findChildElement('Variation')!.innerText); - break; - case 'Tapped': - this._tappedNotes.set(noteId, true); - break; - case 'HarmonicType': - let htype: XmlNode = c.findChildElement('HType')!; - if (htype) { - switch (htype.innerText) { - case 'NoHarmonic': - note.harmonicType = HarmonicType.None; - break; - case 'Natural': - note.harmonicType = HarmonicType.Natural; - break; - case 'Artificial': - note.harmonicType = HarmonicType.Artificial; - break; - case 'Pinch': - note.harmonicType = HarmonicType.Pinch; - break; - case 'Tap': - note.harmonicType = HarmonicType.Tap; - break; - case 'Semi': - note.harmonicType = HarmonicType.Semi; - break; - case 'Feedback': - note.harmonicType = HarmonicType.Feedback; - break; - } - } - break; - case 'HarmonicFret': - let hfret: XmlNode = c.findChildElement('HFret')!; - if (hfret) { - note.harmonicValue = parseFloat(hfret.innerText); - } - break; - case 'Muted': - if (c.findChildElement('Enable')) { - note.isDead = true; - } - break; - case 'PalmMuted': - if (c.findChildElement('Enable')) { - note.isPalmMute = true; - } - break; - case 'Octave': - note.octave = parseInt(c.findChildElement('Number')!.innerText); - // when exporting GP6 from GP7 the tone might be missing - if (note.tone === -1) { - note.tone = 0; - } - break; - case 'Tone': - note.tone = parseInt(c.findChildElement('Step')!.innerText); - break; - case 'ConcertPitch': - this.parseConcertPitch(c, note); - break; - case 'Bended': - isBended = true; - break; - case 'BendOriginValue': - if (!bendOrigin) { - bendOrigin = new BendPoint(0, 0); - } - bendOrigin.value = this.toBendValue(parseFloat(c.findChildElement('Float')!.innerText)); - break; - case 'BendOriginOffset': - if (!bendOrigin) { - bendOrigin = new BendPoint(0, 0); - } - bendOrigin.offset = this.toBendOffset( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'BendMiddleValue': - bendMiddleValue = this.toBendValue(parseFloat(c.findChildElement('Float')!.innerText)); - break; - case 'BendMiddleOffset1': - bendMiddleOffset1 = this.toBendOffset( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'BendMiddleOffset2': - bendMiddleOffset2 = this.toBendOffset( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'BendDestinationValue': - if (!bendDestination) { - bendDestination = new BendPoint(BendPoint.MaxPosition, 0); - } - bendDestination.value = this.toBendValue( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'BendDestinationOffset': - if (!bendDestination) { - bendDestination = new BendPoint(0, 0); - } - bendDestination.offset = this.toBendOffset( - parseFloat(c.findChildElement('Float')!.innerText) - ); - break; - case 'HopoOrigin': - if (c.findChildElement('Enable')) { - note.isHammerPullOrigin = true; - } - break; - case 'HopoDestination': - // NOTE: gets automatically calculated - // if (FindChildElement(node, "Enable")) - // note.isHammerPullDestination = true; - break; - case 'LeftHandTapped': - note.isLeftHandTapped = true; - break; - case 'Slide': - let slideFlags: number = parseInt(c.findChildElement('Flags')!.innerText); - if ((slideFlags & 1) !== 0) { - note.slideOutType = SlideOutType.Shift; - } else if ((slideFlags & 2) !== 0) { - note.slideOutType = SlideOutType.Legato; - } else if ((slideFlags & 4) !== 0) { - note.slideOutType = SlideOutType.OutDown; - } else if ((slideFlags & 8) !== 0) { - note.slideOutType = SlideOutType.OutUp; - } - if ((slideFlags & 16) !== 0) { - note.slideInType = SlideInType.IntoFromBelow; - } else if ((slideFlags & 32) !== 0) { - note.slideInType = SlideInType.IntoFromAbove; - } - if ((slideFlags & 64) !== 0) { - note.slideOutType = SlideOutType.PickSlideDown; - } else if ((slideFlags & 128) !== 0) { - note.slideOutType = SlideOutType.PickSlideUp; + for (const c of node.childElements()) { + switch (c.localName) { + case 'Property': + const name: string = c.getAttribute('name'); + switch (name) { + case 'ShowStringNumber': + if (c.findChildElement('Enable')) { + note.showStringNumber = true; + } + break; + case 'String': + note.string = GpifParser.parseIntSafe(c.findChildElement('String')?.innerText, 0) + 1; + break; + case 'Fret': + note.fret = GpifParser.parseIntSafe(c.findChildElement('Fret')?.innerText, 0); + break; + case 'Element': + element = GpifParser.parseIntSafe(c.findChildElement('Element')?.innerText, 0); + break; + case 'Variation': + variation = GpifParser.parseIntSafe(c.findChildElement('Variation')?.innerText, 0); + break; + case 'Tapped': + this._tappedNotes.set(noteId, true); + break; + case 'HarmonicType': + const htype = c.findChildElement('HType'); + if (htype) { + switch (htype.innerText) { + case 'NoHarmonic': + note.harmonicType = HarmonicType.None; + break; + case 'Natural': + note.harmonicType = HarmonicType.Natural; + break; + case 'Artificial': + note.harmonicType = HarmonicType.Artificial; + break; + case 'Pinch': + note.harmonicType = HarmonicType.Pinch; + break; + case 'Tap': + note.harmonicType = HarmonicType.Tap; + break; + case 'Semi': + note.harmonicType = HarmonicType.Semi; + break; + case 'Feedback': + note.harmonicType = HarmonicType.Feedback; + break; } - break; - } - break; - } + } + break; + case 'HarmonicFret': + const hfret = c.findChildElement('HFret'); + if (hfret) { + note.harmonicValue = GpifParser.parseFloatSafe(hfret.innerText, 0); + } + break; + case 'Muted': + if (c.findChildElement('Enable')) { + note.isDead = true; + } + break; + case 'PalmMuted': + if (c.findChildElement('Enable')) { + note.isPalmMute = true; + } + break; + case 'Octave': + note.octave = GpifParser.parseIntSafe(c.findChildElement('Number')?.innerText, 0); + // when exporting GP6 from GP7 the tone might be missing + if (note.tone === -1) { + note.tone = 0; + } + break; + case 'Tone': + note.tone = GpifParser.parseIntSafe(c.findChildElement('Step')?.innerText, 0); + break; + case 'ConcertPitch': + this.parseConcertPitch(c, note); + break; + case 'Bended': + isBended = true; + break; + case 'BendOriginValue': + if (!bendOrigin) { + bendOrigin = new BendPoint(0, 0); + } + bendOrigin.value = this.toBendValue( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'BendOriginOffset': + if (!bendOrigin) { + bendOrigin = new BendPoint(0, 0); + } + bendOrigin.offset = this.toBendOffset( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'BendMiddleValue': + bendMiddleValue = this.toBendValue( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'BendMiddleOffset1': + bendMiddleOffset1 = this.toBendOffset( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'BendMiddleOffset2': + bendMiddleOffset2 = this.toBendOffset( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'BendDestinationValue': + if (!bendDestination) { + bendDestination = new BendPoint(BendPoint.MaxPosition, 0); + } + bendDestination.value = this.toBendValue( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'BendDestinationOffset': + if (!bendDestination) { + bendDestination = new BendPoint(0, 0); + } + bendDestination.offset = this.toBendOffset( + GpifParser.parseFloatSafe(c.findChildElement('Float')?.innerText, 0) + ); + break; + case 'HopoOrigin': + if (c.findChildElement('Enable')) { + note.isHammerPullOrigin = true; + } + break; + case 'HopoDestination': + // NOTE: gets automatically calculated + // if (FindChildElement(node, "Enable")) + // note.isHammerPullDestination = true; + break; + case 'LeftHandTapped': + note.isLeftHandTapped = true; + break; + case 'Slide': + const slideFlags: number = GpifParser.parseIntSafe( + c.findChildElement('Flags')?.innerText, + 0 + ); + if ((slideFlags & 1) !== 0) { + note.slideOutType = SlideOutType.Shift; + } else if ((slideFlags & 2) !== 0) { + note.slideOutType = SlideOutType.Legato; + } else if ((slideFlags & 4) !== 0) { + note.slideOutType = SlideOutType.OutDown; + } else if ((slideFlags & 8) !== 0) { + note.slideOutType = SlideOutType.OutUp; + } + if ((slideFlags & 16) !== 0) { + note.slideInType = SlideInType.IntoFromBelow; + } else if ((slideFlags & 32) !== 0) { + note.slideInType = SlideInType.IntoFromAbove; + } + if ((slideFlags & 64) !== 0) { + note.slideOutType = SlideOutType.PickSlideDown; + } else if ((slideFlags & 128) !== 0) { + note.slideOutType = SlideOutType.PickSlideUp; + } + break; + } + break; } } @@ -2384,26 +2345,24 @@ export class GpifParser { private parseConcertPitch(node: XmlNode, note: Note) { const pitch = node.findChildElement('Pitch'); if (pitch) { - for (let c of pitch.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Accidental': - switch (c.innerText) { - case 'x': - note.accidentalMode = NoteAccidentalMode.ForceDoubleSharp; - break; - case '#': - note.accidentalMode = NoteAccidentalMode.ForceSharp; - break; - case 'b': - note.accidentalMode = NoteAccidentalMode.ForceFlat; - break; - case 'bb': - note.accidentalMode = NoteAccidentalMode.ForceDoubleFlat; - break; - } - break; - } + for (const c of pitch.childElements()) { + switch (c.localName) { + case 'Accidental': + switch (c.innerText) { + case 'x': + note.accidentalMode = NoteAccidentalMode.ForceDoubleSharp; + break; + case '#': + note.accidentalMode = NoteAccidentalMode.ForceSharp; + break; + case 'b': + note.accidentalMode = NoteAccidentalMode.ForceFlat; + break; + case 'bb': + note.accidentalMode = NoteAccidentalMode.ForceDoubleFlat; + break; + } + break; } } } @@ -2418,69 +2377,65 @@ export class GpifParser { } private parseRhythms(node: XmlNode): void { - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'Rhythm': - this.parseRhythm(c); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'Rhythm': + this.parseRhythm(c); + break; } } } private parseRhythm(node: XmlNode): void { - let rhythm: GpifRhythm = new GpifRhythm(); - let rhythmId: string = node.getAttribute('id'); + const rhythm: GpifRhythm = new GpifRhythm(); + const rhythmId: string = node.getAttribute('id'); rhythm.id = rhythmId; - for (let c of node.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'NoteValue': - switch (c.innerText) { - case 'Long': - rhythm.value = Duration.QuadrupleWhole; - break; - case 'DoubleWhole': - rhythm.value = Duration.DoubleWhole; - break; - case 'Whole': - rhythm.value = Duration.Whole; - break; - case 'Half': - rhythm.value = Duration.Half; - break; - case 'Quarter': - rhythm.value = Duration.Quarter; - break; - case 'Eighth': - rhythm.value = Duration.Eighth; - break; - case '16th': - rhythm.value = Duration.Sixteenth; - break; - case '32nd': - rhythm.value = Duration.ThirtySecond; - break; - case '64th': - rhythm.value = Duration.SixtyFourth; - break; - case '128th': - rhythm.value = Duration.OneHundredTwentyEighth; - break; - case '256th': - rhythm.value = Duration.TwoHundredFiftySixth; - break; - } - break; - case 'PrimaryTuplet': - rhythm.tupletNumerator = parseInt(c.getAttribute('num')); - rhythm.tupletDenominator = parseInt(c.getAttribute('den')); - break; - case 'AugmentationDot': - rhythm.dots = parseInt(c.getAttribute('count')); - break; - } + for (const c of node.childElements()) { + switch (c.localName) { + case 'NoteValue': + switch (c.innerText) { + case 'Long': + rhythm.value = Duration.QuadrupleWhole; + break; + case 'DoubleWhole': + rhythm.value = Duration.DoubleWhole; + break; + case 'Whole': + rhythm.value = Duration.Whole; + break; + case 'Half': + rhythm.value = Duration.Half; + break; + case 'Quarter': + rhythm.value = Duration.Quarter; + break; + case 'Eighth': + rhythm.value = Duration.Eighth; + break; + case '16th': + rhythm.value = Duration.Sixteenth; + break; + case '32nd': + rhythm.value = Duration.ThirtySecond; + break; + case '64th': + rhythm.value = Duration.SixtyFourth; + break; + case '128th': + rhythm.value = Duration.OneHundredTwentyEighth; + break; + case '256th': + rhythm.value = Duration.TwoHundredFiftySixth; + break; + } + break; + case 'PrimaryTuplet': + rhythm.tupletNumerator = GpifParser.parseIntSafe(c.getAttribute('num'), -1); + rhythm.tupletDenominator = GpifParser.parseIntSafe(c.getAttribute('den'), -1); + break; + case 'AugmentationDot': + rhythm.dots = GpifParser.parseIntSafe(c.getAttribute('count'), 0); + break; } } this._rhythmById.set(rhythmId, rhythm); @@ -2489,48 +2444,73 @@ export class GpifParser { private buildModel(): void { // build score for (let i: number = 0, j: number = this._masterBars.length; i < j; i++) { - let masterBar: MasterBar = this._masterBars[i]; + const masterBar: MasterBar = this._masterBars[i]; this.score.addMasterBar(masterBar); } + + // Its a bit wierd. but the last bar might be flagged as "DoubleBar" + // we have to clear this + const lastMasterBar = this._masterBars[this._masterBars.length - 1]; + if (this._doubleBars.has(lastMasterBar)) { + this._doubleBars.delete(lastMasterBar); + lastMasterBar.isDoubleBar = false; + } + // add tracks to score - for (let trackId of this._tracksMapping) { + for (const trackId of this._tracksMapping) { if (!trackId) { continue; } - let track: Track = this._tracksById.get(trackId)!; + const track: Track = this._tracksById.get(trackId)!; this.score.addTrack(track); } // process all masterbars - for (let barIds of this._barsOfMasterBar) { + let keySignature: [KeySignature, KeySignatureType]; + + for (const barIds of this._barsOfMasterBar) { // add all bars of masterbar vertically to all tracks let staffIndex: number = 0; + keySignature = [KeySignature.C, KeySignatureType.Major]; for ( let barIndex: number = 0, trackIndex: number = 0; barIndex < barIds.length && trackIndex < this.score.tracks.length; barIndex++ ) { - let barId: string = barIds[barIndex]; + const barId: string = barIds[barIndex]; if (barId !== GpifParser.InvalidId) { - let bar: Bar = this._barsById.get(barId)!; - let track: Track = this.score.tracks[trackIndex]; - let staff: Staff = track.staves[staffIndex]; + const bar: Bar = this._barsById.get(barId)!; + const track: Track = this.score.tracks[trackIndex]; + const staff: Staff = track.staves[staffIndex]; staff.addBar(bar); + + const masterBarIndex = staff.bars.length - 1; + if (this._keySignatures.has(masterBarIndex)) { + keySignature = this._keySignatures.get(masterBarIndex)!; + } + + bar.keySignature = keySignature[0]; + bar.keySignatureType = keySignature[1]; + + if (this._doubleBars.has(bar.masterBar)) { + bar.barLineRight = BarLineStyle.LightLight; + } + if (this._voicesOfBar.has(barId)) { // add voices to bars - for (let voiceId of this._voicesOfBar.get(barId)!) { + for (const voiceId of this._voicesOfBar.get(barId)!) { if (voiceId !== GpifParser.InvalidId) { - let voice: Voice = this._voiceById.get(voiceId)!; + const voice: Voice = this._voiceById.get(voiceId)!; bar.addVoice(voice); if (this._beatsOfVoice.has(voiceId)) { // add beats to voices - for (let beatId of this._beatsOfVoice.get(voiceId)!) { + for (const beatId of this._beatsOfVoice.get(voiceId)!) { if (beatId !== GpifParser.InvalidId) { // important! we clone the beat because beats get reused // in gp6, our model needs to have unique beats. - let beat: Beat = BeatCloner.clone(this._beatById.get(beatId)!); + const beat: Beat = BeatCloner.clone(this._beatById.get(beatId)!); voice.addBeat(beat); - let rhythmId: string = this._rhythmOfBeat.get(beatId)!; - let rhythm: GpifRhythm = this._rhythmById.get(rhythmId)!; + const rhythmId: string = this._rhythmOfBeat.get(beatId)!; + const rhythm: GpifRhythm = this._rhythmById.get(rhythmId)!; // set beat duration beat.duration = rhythm.value; beat.dots = rhythm.dots; @@ -2538,7 +2518,7 @@ export class GpifParser { beat.tupletDenominator = rhythm.tupletDenominator; // add notes to beat if (this._notesOfBeat.has(beatId)) { - for (let noteId of this._notesOfBeat.get(beatId)!) { + for (const noteId of this._notesOfBeat.get(beatId)!) { if (noteId !== GpifParser.InvalidId) { const note = NoteCloner.clone(this._noteById.get(noteId)!); // reset midi value for non-percussion staves @@ -2560,9 +2540,9 @@ export class GpifParser { } } else { // invalid voice -> empty voice - let voice: Voice = new Voice(); + const voice: Voice = new Voice(); bar.addVoice(voice); - let beat: Beat = new Beat(); + const beat: Beat = new Beat(); beat.isEmpty = true; beat.duration = Duration.Quarter; voice.addBeat(beat); @@ -2573,8 +2553,10 @@ export class GpifParser { if (staffIndex === track.staves.length - 1) { trackIndex++; staffIndex = 0; + keySignature = [KeySignature.C, KeySignatureType.Major]; } else { staffIndex++; + keySignature = [KeySignature.C, KeySignatureType.Major]; } } else { // no bar for track @@ -2585,11 +2567,11 @@ export class GpifParser { // clear out percussion articulations where not needed // and add automations - for (let trackId of this._tracksMapping) { + for (const trackId of this._tracksMapping) { if (!trackId) { continue; } - let track: Track = this._tracksById.get(trackId)!; + const track: Track = this._tracksById.get(trackId)!; let hasPercussion = false; for (const staff of track.staves) { @@ -2634,9 +2616,9 @@ export class GpifParser { // build masterbar automations for (const [barNumber, automations] of this._masterTrackAutomations) { - let masterBar: MasterBar = this.score.masterBars[barNumber]; + const masterBar: MasterBar = this.score.masterBars[barNumber]; for (let i: number = 0, j: number = automations.length; i < j; i++) { - let automation: Automation = automations[i]; + const automation: Automation = automations[i]; if (automation.type === AutomationType.Tempo) { if (barNumber === 0) { this.score.tempo = automation.value | 0; diff --git a/src/importer/GpxFileSystem.ts b/src/importer/GpxFileSystem.ts index 47e8912fe..a50d53cbd 100644 --- a/src/importer/GpxFileSystem.ts +++ b/src/importer/GpxFileSystem.ts @@ -1,7 +1,7 @@ import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { BitReader } from '@src/io/BitReader'; import { ByteBuffer } from '@src/io/ByteBuffer'; -import { EndOfReaderError, IReadable } from '@src/io/IReadable'; +import { EndOfReaderError, type IReadable } from '@src/io/IReadable'; /** * this public class represents a file within the GpxFileSystem @@ -49,7 +49,7 @@ export class GpxFileSystem { * @returns */ public load(s: IReadable): void { - let src: BitReader = new BitReader(s); + const src: BitReader = new BitReader(s); this.readBlock(src); } @@ -71,30 +71,30 @@ export class GpxFileSystem { * @returns the decompressed byte data. if skipHeader is set to false the BCFS header is included. */ public decompress(src: BitReader, skipHeader: boolean = false): Uint8Array { - let uncompressed: ByteBuffer = ByteBuffer.empty(); + const uncompressed: ByteBuffer = ByteBuffer.empty(); let buffer: Uint8Array; - let expectedLength: number = this.getInteger(src.readBytes(4), 0); + const expectedLength: number = this.getInteger(src.readBytes(4), 0); try { // as long we reach our expected length we try to decompress, a EOF might occure. while (uncompressed.length < expectedLength) { // compression flag - let flag: number = src.readBits(1); + const flag: number = src.readBits(1); if (flag === 1) { // get offset and size of the content we need to read. // compressed does mean we already have read the data and need // to copy it from our uncompressed buffer to the end - let wordSize: number = src.readBits(4); - let offset: number = src.readBitsReversed(wordSize); - let size: number = src.readBitsReversed(wordSize); + const wordSize: number = src.readBits(4); + const offset: number = src.readBitsReversed(wordSize); + const size: number = src.readBitsReversed(wordSize); // the offset is relative to the end - let sourcePosition: number = uncompressed.length - offset; - let toRead: number = Math.min(offset, size); + const sourcePosition: number = uncompressed.length - offset; + const toRead: number = Math.min(offset, size); // get the subbuffer storing the data and add it again to the end buffer = uncompressed.getBuffer(); uncompressed.write(buffer, sourcePosition, toRead); } else { // on raw content we need to read the data from the source buffer - let size: number = src.readBitsReversed(2); + const size: number = src.readBitsReversed(2); for (let i: number = 0; i < size; i++) { uncompressed.writeByte(src.readByte()); } @@ -106,10 +106,10 @@ export class GpxFileSystem { } } buffer = uncompressed.getBuffer(); - let resultOffset: number = skipHeader ? 4 : 0; - let resultSize: number = uncompressed.length - resultOffset; - let result: Uint8Array = new Uint8Array(resultSize); - let count: number = resultSize; + const resultOffset: number = skipHeader ? 4 : 0; + const resultSize: number = uncompressed.length - resultOffset; + const result: Uint8Array = new Uint8Array(resultSize); + const count: number = resultSize; result.set(buffer.subarray(resultOffset, resultOffset + count), 0); return result; } @@ -120,7 +120,7 @@ export class GpxFileSystem { * @returns */ private readBlock(data: BitReader): void { - let header: string = this.readHeader(data); + const header: string = this.readHeader(data); if (header === 'BCFZ') { // decompress the data and use this // we will skip the header @@ -142,11 +142,11 @@ export class GpxFileSystem { // the first sector (0x1000 bytes) is empty (filled with 0xFF) // so the first sector starts at 0x1000 // (we already skipped the 4 byte header so we don't have to take care of this) - let sectorSize: number = 0x1000; + const sectorSize: number = 0x1000; let offset: number = sectorSize; // we always need 4 bytes (+3 including offset) to read the type while (offset + 3 < data.length) { - let entryType: number = this.getInteger(data, offset); + const entryType: number = this.getInteger(data, offset); if (entryType === 2) { // file structure: // offset | type | size | what @@ -157,22 +157,22 @@ export class GpxFileSystem { // 0x90 | ? | 4byte | Unknown // 0x94 | int[] | n*4byte | Indices of the sector containing the data (end is marked with 0) // The sectors marked at 0x94 are absolutely positioned ( 1*0x1000 is sector 1, 2*0x1000 is sector 2,...) - let file: GpxFile = new GpxFile(); + const file: GpxFile = new GpxFile(); file.fileName = this.getString(data, offset + 0x04, 127); file.fileSize = this.getInteger(data, offset + 0x8c); // store file if needed - let storeFile: boolean = !this.fileFilter || this.fileFilter(file.fileName); + const storeFile: boolean = !this.fileFilter || this.fileFilter(file.fileName); if (storeFile) { this.files.push(file); } // we need to iterate the blocks because we need to move after the last datasector - let dataPointerOffset: number = offset + 0x94; + const dataPointerOffset: number = offset + 0x94; let sector: number = 0; // this var is storing the sector index let sectorCount: number = 0; // we're keeping count so we can calculate the offset of the array item // as long we have data blocks we need to iterate them, - let fileData: ByteBuffer | null = storeFile ? ByteBuffer.withCapacity(file.fileSize) : null; + const fileData: ByteBuffer | null = storeFile ? ByteBuffer.withCapacity(file.fileSize) : null; while (true) { sector = this.getInteger(data, dataPointerOffset + 4 * sectorCount++); if (sector !== 0) { @@ -192,7 +192,7 @@ export class GpxFileSystem { // trim data to filesize if needed file.data = new Uint8Array(Math.min(file.fileSize, fileData!.length)); // we can use the getBuffer here because we are intelligent and know not to read the empty data. - let raw: Uint8Array = fileData!.toArray(); + const raw: Uint8Array = fileData!.toArray(); file.data.set(raw.subarray(0, 0 + file.data.length), 0); } } @@ -211,7 +211,7 @@ export class GpxFileSystem { private getString(data: Uint8Array, offset: number, length: number): string { let buf: string = ''; for (let i: number = 0; i < length; i++) { - let code: number = data[offset + i] & 0xff; + const code: number = data[offset + i] & 0xff; if (code === 0) { break; // zero terminated string diff --git a/src/importer/GpxImporter.ts b/src/importer/GpxImporter.ts index bd79ba57e..e3d1e0e36 100644 --- a/src/importer/GpxImporter.ts +++ b/src/importer/GpxImporter.ts @@ -3,11 +3,11 @@ import { GpifParser } from '@src/importer/GpifParser'; import { GpxFileSystem } from '@src/importer/GpxFileSystem'; import { PartConfiguration } from '@src/importer/PartConfiguration'; import { ScoreImporter } from '@src/importer/ScoreImporter'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Logger } from '@src/Logger'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { IOHelper } from '@src/io/IOHelper'; -import { LayoutConfiguration } from './LayoutConfiguration'; +import { LayoutConfiguration } from '@src/importer/LayoutConfiguration'; /** * This ScoreImporter can read Guitar Pro 6 (gpx) files. @@ -17,17 +17,18 @@ export class GpxImporter extends ScoreImporter { return 'Guitar Pro 6'; } - public constructor() { - super(); - } - public readScore(): Score { // at first we need to load the binary file system // from the GPX container Logger.debug(this.name, 'Loading GPX filesystem'); - let fileSystem: GpxFileSystem = new GpxFileSystem(); + const fileSystem: GpxFileSystem = new GpxFileSystem(); fileSystem.fileFilter = s => { - return s.endsWith('score.gpif') || s.endsWith('BinaryStylesheet') || s.endsWith('PartConfiguration') || s.endsWith('LayoutConfiguration'); + return ( + s.endsWith('score.gpif') || + s.endsWith('BinaryStylesheet') || + s.endsWith('PartConfiguration') || + s.endsWith('LayoutConfiguration') + ); }; fileSystem.load(this.data); Logger.debug(this.name, 'GPX filesystem loaded'); @@ -36,7 +37,7 @@ export class GpxImporter extends ScoreImporter { let binaryStylesheetData: Uint8Array | null = null; let partConfigurationData: Uint8Array | null = null; let layoutConfigurationData: Uint8Array | null = null; - for (let entry of fileSystem.files) { + for (const entry of fileSystem.files) { switch (entry.fileName) { case 'score.gpif': xml = IOHelper.toString(entry.data!, this.settings.importer.encoding); @@ -60,14 +61,14 @@ export class GpxImporter extends ScoreImporter { // the score.gpif file within this filesystem stores // the score information as XML we need to parse. Logger.debug(this.name, 'Start Parsing score.gpif'); - let gpifParser: GpifParser = new GpifParser(); + const gpifParser: GpifParser = new GpifParser(); gpifParser.parseXml(xml, this.settings); Logger.debug(this.name, 'score.gpif parsed'); - let score: Score = gpifParser.score; + const score: Score = gpifParser.score; if (binaryStylesheetData) { Logger.debug(this.name, 'Start Parsing BinaryStylesheet'); - let binaryStylesheet: BinaryStylesheet = new BinaryStylesheet(binaryStylesheetData); + const binaryStylesheet: BinaryStylesheet = new BinaryStylesheet(binaryStylesheetData); binaryStylesheet.apply(score); Logger.debug(this.name, 'BinaryStylesheet parsed'); } @@ -82,7 +83,7 @@ export class GpxImporter extends ScoreImporter { if (layoutConfigurationData && partConfigurationParser != null) { Logger.debug(this.name, 'Start Parsing Layout Configuration'); - let layoutConfigurationParser: LayoutConfiguration = new LayoutConfiguration( + const layoutConfigurationParser: LayoutConfiguration = new LayoutConfiguration( partConfigurationParser, layoutConfigurationData ); diff --git a/src/importer/LayoutConfiguration.ts b/src/importer/LayoutConfiguration.ts index 7cfd2f6a4..1d10d19c3 100644 --- a/src/importer/LayoutConfiguration.ts +++ b/src/importer/LayoutConfiguration.ts @@ -1,12 +1,12 @@ -import { ByteBuffer } from "@src/io/ByteBuffer"; -import { IOHelper } from "@src/io/IOHelper"; -import { Score } from "@src/model/Score"; -import { PartConfiguration } from "./PartConfiguration"; +import { ByteBuffer } from '@src/io/ByteBuffer'; +import { IOHelper } from '@src/io/IOHelper'; +import type { Score } from '@src/model/Score'; +import type { PartConfiguration } from '@src/importer/PartConfiguration'; // PartConfiguration File Format Notes. // Based off Guitar Pro 8 // The LayoutConfiguration is aligned with the data in the PartConfiguration. -// We haven't fully deciphered how they handle everything but its enough for our needs. +// We haven't fully deciphered how they handle everything but its enough for our needs. // File: // int32 (big endian) | Zoom Level encoded @@ -36,13 +36,12 @@ import { PartConfiguration } from "./PartConfiguration"; // 1 byte (bool) | MultiVoice Cursor (CTRL+M) // ScoreView[] | Data for all score views (number is aligned with PartConfiguration) -// ScoreView: +// ScoreView: // TrackViewGroup[] | The individual track view groups (number is aligned with PartConfiguration) // TrackViewGroup: // 1 byte (bool) | isVisible (true -> 0xFF, false -> 0x00) - class LayoutConfigurationScoreView { public trackViewGroups: LayoutConfigurationTrackViewGroup[] = []; } @@ -61,44 +60,39 @@ enum GuitarProView { } export class LayoutConfiguration { - public zoomLevel:number = 4; - public view:GuitarProView = GuitarProView.PageVertical; - public muiltiVoiceCursor:boolean = false; + public zoomLevel: number = 4; + public view: GuitarProView = GuitarProView.PageVertical; + public muiltiVoiceCursor: boolean = false; public scoreViews: LayoutConfigurationScoreView[] = []; - public constructor( - partConfiguration:PartConfiguration, - layoutConfigurationData: Uint8Array) { + public constructor(partConfiguration: PartConfiguration, layoutConfigurationData: Uint8Array) { const readable: ByteBuffer = ByteBuffer.fromBuffer(layoutConfigurationData); this.zoomLevel = IOHelper.readInt32BE(readable); this.view = readable.readByte() as GuitarProView; - this.muiltiVoiceCursor = readable.readByte() != 0 + this.muiltiVoiceCursor = readable.readByte() !== 0; const scoreViewCount: number = partConfiguration.scoreViews.length; for (let i: number = 0; i < scoreViewCount; i++) { - const scoreView = new LayoutConfigurationScoreView(); this.scoreViews.push(scoreView); const partScoreView = partConfiguration.scoreViews[i]; for (let j: number = 0; j < partScoreView.trackViewGroups.length; j++) { - const trackViewGroup = new LayoutConfigurationTrackViewGroup(); - trackViewGroup.isVisible = readable.readByte() != 0; + trackViewGroup.isVisible = readable.readByte() !== 0; scoreView.trackViewGroups.push(trackViewGroup); } } - } public apply(score: Score): void { - if(this.scoreViews.length > 0) { + if (this.scoreViews.length > 0) { let trackIndex = 0; - for (let trackConfig of this.scoreViews[0].trackViewGroups) { + for (const trackConfig of this.scoreViews[0].trackViewGroups) { if (trackIndex < score.tracks.length) { const track = score.tracks[trackIndex]; track.isVisibleOnMultiTrack = trackConfig.isVisible; @@ -108,25 +102,25 @@ export class LayoutConfiguration { } } - public static writeForScore(score: Score,): Uint8Array { + public static writeForScore(score: Score): Uint8Array { const writer = ByteBuffer.withCapacity(128); IOHelper.writeInt32BE(writer, 4); // 100% Zoom - writer.writeByte(0x00) // Page - Vertical + writer.writeByte(0x00); // Page - Vertical const isMultiVoice = score.tracks.length > 0 && score.tracks[0].staves[0].bars[0].isMultiVoice; - writer.writeByte(isMultiVoice ? 0xFF : 0x00); + writer.writeByte(isMultiVoice ? 0xff : 0x00); // ScoreView[0] => Multi Track Score View for (const track of score.tracks) { - writer.writeByte(track.isVisibleOnMultiTrack ? 0xFF : 0x00); + writer.writeByte(track.isVisibleOnMultiTrack ? 0xff : 0x00); } // Single Track Views for each track for (const _track of score.tracks) { - writer.writeByte(0xFF); + writer.writeByte(0xff); } return writer.toArray(); } -} \ No newline at end of file +} diff --git a/src/importer/MusicXmlImporter.ts b/src/importer/MusicXmlImporter.ts index 16011c8ab..66010209c 100644 --- a/src/importer/MusicXmlImporter.ts +++ b/src/importer/MusicXmlImporter.ts @@ -1,83 +1,211 @@ import { ScoreImporter } from '@src/importer/ScoreImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; +import { IOHelper } from '@src/io/IOHelper'; +import { Logger } from '@src/Logger'; +import { MidiUtils } from '@src/midi/MidiUtils'; import { AccentuationType } from '@src/model/AccentuationType'; import { Automation, AutomationType } from '@src/model/Automation'; -import { Bar } from '@src/model/Bar'; -import { Beat } from '@src/model/Beat'; +import { Bar, BarLineStyle, SustainPedalMarkerType, SustainPedalMarker } from '@src/model/Bar'; +import { Beat, BeatBeamingMode } from '@src/model/Beat'; import { BendPoint } from '@src/model/BendPoint'; +import { BrushType } from '@src/model/BrushType'; import { Chord } from '@src/model/Chord'; import { Clef } from '@src/model/Clef'; +import { CrescendoType } from '@src/model/CrescendoType'; +import { Direction } from '@src/model/Direction'; import { Duration } from '@src/model/Duration'; import { DynamicValue } from '@src/model/DynamicValue'; +import { FermataType, Fermata } from '@src/model/Fermata'; +import { Fingers } from '@src/model/Fingers'; +import { GolpeType } from '@src/model/GolpeType'; import { GraceType } from '@src/model/GraceType'; +import { InstrumentArticulation } from '@src/model/InstrumentArticulation'; import { KeySignature } from '@src/model/KeySignature'; import { KeySignatureType } from '@src/model/KeySignatureType'; import { MasterBar } from '@src/model/MasterBar'; -import { Note } from '@src/model/Note'; +import { ModelUtils } from '@src/model/ModelUtils'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import { Note, NoteStyle } from '@src/model/Note'; import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; +import { NoteOrnament } from '@src/model/NoteOrnament'; import { Ottavia } from '@src/model/Ottavia'; import { PickStroke } from '@src/model/PickStroke'; import { Score } from '@src/model/Score'; +import { Section } from '@src/model/Section'; +import { SimileMark } from '@src/model/SimileMark'; import { SlideOutType } from '@src/model/SlideOutType'; import { Staff } from '@src/model/Staff'; import { Track } from '@src/model/Track'; +import { TripletFeel } from '@src/model/TripletFeel'; +import { VibratoType } from '@src/model/VibratoType'; import { Voice } from '@src/model/Voice'; - -import { ModelUtils } from '@src/model/ModelUtils'; - +import { AccidentalHelper } from '@src/rendering/utils/AccidentalHelper'; +import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { XmlDocument } from '@src/xml/XmlDocument'; -import { XmlNode, XmlNodeType } from '@src/xml/XmlNode'; -import { IOHelper } from '@src/io/IOHelper'; +import type { XmlNode } from '@src/xml/XmlNode'; +import type { ZipEntry } from '@src/zip/ZipEntry'; import { ZipReader } from '@src/zip/ZipReader'; -import { ZipEntry } from '@src/zip/ZipEntry'; + +class StaffContext { + public slurStarts!: Map; + public currentDynamics = DynamicValue.F; + public tieStarts!: Set; + public tieStartIds!: Map; + public slideOrigins: Map = new Map(); + public transpose: number = 0; + public isExplicitlyBeamed = false; + + constructor() { + this.tieStarts = new Set(); + this.tieStartIds = new Map(); + this.slideOrigins = new Map(); + this.slurStarts = new Map(); + } +} + +class InstrumentArticulationWithPlaybackInfo extends InstrumentArticulation { + /** + * The midi channel number to use when playing the note (-1 if using the default track channels). + */ + public outputMidiChannel: number = -1; + + /** + * The midi channel program to use when playing the note (-1 if using the default track program). + */ + public outputMidiProgram: number = -1; + + /** + * The volume to use when playing the note (-1 if using the default track volume). + */ + public outputVolume: number = -1; + + /** + * The balance to use when playing the note (-1 if using the default track balance). + */ + public outputBalance: number = -1; +} + +class TrackInfo { + public track: Track; + public firstArticulation?: InstrumentArticulationWithPlaybackInfo; + public instruments: Map = new Map< + string, + InstrumentArticulationWithPlaybackInfo + >(); + + private _instrumentIdToArticulationIndex: Map = new Map(); + + private _lyricsLine = 0; + private _lyricsLines: Map = new Map(); + + public constructor(track: Track) { + this.track = track; + } + + public getLyricLine(number: string) { + if (this._lyricsLines.has(number)) { + return this._lyricsLines.get(number)!; + } + const line = this._lyricsLine; + this._lyricsLines.set(number, line); + this._lyricsLine++; + return line; + } + + private static defaultNoteArticulation: InstrumentArticulation = new InstrumentArticulation( + 'Default', + 0, + 0, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ); + + public getOrCreateArticulation(instrumentId: string, note: Note) { + const noteValue = note.octave * 12 + note.tone; + const lookup = `${instrumentId}_${noteValue}`; + if (this._instrumentIdToArticulationIndex.has(lookup)) { + return this._instrumentIdToArticulationIndex.get(lookup)!; + } + + let articulation: InstrumentArticulation; + if (this.instruments.has(instrumentId)) { + articulation = this.instruments.get(instrumentId)!; + } else { + articulation = TrackInfo.defaultNoteArticulation; + } + const index = this.track.percussionArticulations.length; + + const bar = note.beat.voice.bar; + + // the calculation in the AccidentalHelper assumes a standard 5-line staff. + let musicXmlStaffSteps: number; + if (noteValue === 0) { + // no display pitch defined? + musicXmlStaffSteps = 4; // middle of bar + } else { + musicXmlStaffSteps = AccidentalHelper.calculateNoteSteps(bar.keySignature, bar.clef, noteValue); + } + + // to translate this into the "staffLine" semantics we need to subtract additionally the steps "missing" from the absent lines + const actualSteps = note.beat.voice.bar.staff.standardNotationLineCount * 2 - 1; + const fiveLineSteps = 5 * 2 - 1; + const stepDifference = fiveLineSteps - actualSteps; + + const staffLine = musicXmlStaffSteps - stepDifference; + + const newArticulation = new InstrumentArticulation( + articulation.elementType, + staffLine, + articulation.outputMidiNumber, + articulation.noteHeadDefault, + articulation.noteHeadHalf, + articulation.noteHeadWhole, + articulation.techniqueSymbol, + articulation.techniqueSymbolPlacement + ); + + this._instrumentIdToArticulationIndex.set(lookup, index); + this.track.percussionArticulations.push(newArticulation); + return index; + } +} export class MusicXmlImporter extends ScoreImporter { private _score!: Score; - private _trackById!: Map; - private _partGroups!: Map; - private _currentPartGroup: string | null = null; - private _trackFirstMeasureNumber: number = 0; - private _maxVoices: number = 0; - private _currentDirection: string | null = null; - private _tieStarts!: Note[]; - private _tieStartIds!: Map; - private _slurStarts!: Map; + private _idToTrackInfo: Map = new Map(); + private _indexToTrackInfo: Map = new Map(); + private _staffToContext: Map = new Map(); + + private _divisionsPerQuarterNote: number = 1; + private _currentDynamics = DynamicValue.F; public get name(): string { return 'MusicXML'; } - public constructor() { - super(); - } - public readScore(): Score { - this._trackById = new Map(); - this._partGroups = new Map(); - this._tieStarts = []; - this._tieStartIds = new Map(); - this._slurStarts = new Map(); - - let xml: string = this.extractMusicXml(); - let dom: XmlDocument = new XmlDocument(); + const xml: string = this.extractMusicXml(); + const dom: XmlDocument = new XmlDocument(); try { dom.parse(xml); } catch (e) { - throw new UnsupportedFormatError('Unsupported format'); + throw new UnsupportedFormatError('Unsupported format', e as Error); } this._score = new Score(); this._score.tempo = 120; + this._score.stylesheet.hideDynamics = true; + this.parseDom(dom); - // merge partgroups into a single track with multiple staves - if (this.settings.importer.mergePartGroupsInMusicXml) { - this.mergePartGroups(); - } + ModelUtils.consolidate(this._score); this._score.finish(this.settings); this._score.rebuildRepeatGroups(); + return this._score; } - extractMusicXml(): string { + + private extractMusicXml(): string { const zip = new ZipReader(this.data); let entries: ZipEntry[]; try { @@ -104,7 +232,7 @@ export class MusicXmlImporter extends ScoreImporter { throw new UnsupportedFormatError('Malformed container.xml, could not parse as XML', e as Error); } - let root: XmlNode | null = containerDom.firstElement; + const root: XmlNode | null = containerDom.firstElement; if (!root || root.localName !== 'container') { throw new UnsupportedFormatError("Malformed container.xml, root element not 'container'"); } @@ -115,8 +243,8 @@ export class MusicXmlImporter extends ScoreImporter { } let uncompressedFileFullPath: string = ''; - for (const c of rootFiles.childNodes) { - if (c.nodeType == XmlNodeType.Element && c.localName === 'rootfile') { + for (const c of rootFiles.childElements()) { + if (c.localName === 'rootfile') { // The MusicXML root must be described in the first element. // https://www.w3.org/2021/06/musicxml40/tutorial/compressed-mxl-files/ uncompressedFileFullPath = c.getAttribute('full-path'); @@ -138,38 +266,8 @@ export class MusicXmlImporter extends ScoreImporter { return IOHelper.toString(file.data, this.settings.importer.encoding); } - private mergePartGroups(): void { - let anyMerged: boolean = false; - for (const tracks of this._partGroups.values()) { - if (tracks.length > 1) { - this.mergeGroup(tracks); - anyMerged = true; - } - } - // if any groups were merged, we need to rebuild the indexes - if (anyMerged) { - for (let i: number = 0; i < this._score.tracks.length; i++) { - this._score.tracks[i].index = i; - } - } - } - - private mergeGroup(partGroup: Track[]): void { - let primaryTrack: Track = partGroup[0]; - for (let i: number = 1; i < partGroup.length; i++) { - // merge staves over to primary track - let secondaryTrack: Track = partGroup[i]; - for (let staff of secondaryTrack.staves) { - primaryTrack.addStaff(staff); - } - // remove track from score - let trackIndex: number = this._score.tracks.indexOf(secondaryTrack); - this._score.tracks.splice(trackIndex, 1); - } - } - private parseDom(dom: XmlDocument): void { - let root: XmlNode | null = dom.firstElement; + const root: XmlNode | null = dom.firstElement; if (!root) { throw new UnsupportedFormatError('Unsupported format'); } @@ -178,7 +276,7 @@ export class MusicXmlImporter extends ScoreImporter { this.parsePartwise(root); break; case 'score-timewise': - // ParseTimewise(root); + this.parseTimewise(root); break; default: throw new UnsupportedFormatError('Unsupported format'); @@ -186,375 +284,1114 @@ export class MusicXmlImporter extends ScoreImporter { } private parsePartwise(element: XmlNode): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'work': - this.parseWork(c); + for (const c of element.childElements()) { + switch (c.localName) { + case 'credit': + this.parseCredit(c); + break; + // case 'defaults': Ignored (see below) + case 'identification': + this.parseIdentification(c); + break; + // case 'movement-number': Ignored + case 'movement-title': + this.parseMovementTitle(c); + break; + case 'part': + this.parsePartwisePart(c); + break; + case 'part-list': + this.parsePartList(c); + break; + case 'work': + this.parseWork(c); + break; + } + } + } + + private parseTimewise(element: XmlNode): void { + let index = 0; + for (const c of element.childElements()) { + switch (c.localName) { + case 'credit': + this.parseCredit(c); + break; + // case 'defaults': Ignored (see below) + case 'identification': + this.parseIdentification(c); + break; + // case 'movement-number': Ignored + case 'movement-title': + this.parseMovementTitle(c); + break; + case 'part-list': + this.parsePartList(c); + break; + case 'work': + this.parseWork(c); + break; + case 'measure': + this.parseTimewiseMeasure(c, index); + index++; + break; + } + } + } + + private parseCredit(element: XmlNode) { + // credit texts are absolutely positioned texts which we don't support + // but it is very common to place song information in there, + // we do our best to parse information into our song details + + // only consider first page info + if (element.getAttribute('page', '1') !== '1') { + return; + } + + const creditTypes: string[] = []; + let firstWords: XmlNode | null = null; + + let fullText = ''; + + for (const c of element.childElements()) { + switch (c.localName) { + case 'credit-type': + creditTypes.push(c.innerText); + break; + // case 'link': Ignored + // case 'bookmark': Ignored + // case 'credit-image': Not supported + case 'credit-words': + if (firstWords === null) { + firstWords = c; + } + fullText += c.innerText; + break; + // case 'credit-symbol' Not supported + } + } + + // we have types defined? awesome, no need to guess + if (creditTypes.length > 0) { + for (const type of creditTypes) { + switch (type) { + case 'title': + this._score.title = MusicXmlImporter.sanitizeDisplay(fullText); break; - case 'movement-title': - this._score.title = c.innerText; + case 'subtitle': + this._score.subTitle = MusicXmlImporter.sanitizeDisplay(fullText); break; - case 'identification': - this.parseIdentification(c); + case 'composer': + this._score.artist = MusicXmlImporter.sanitizeDisplay(fullText); break; - case 'part-list': - this.parsePartList(c); + case 'arranger': + this._score.artist = MusicXmlImporter.sanitizeDisplay(fullText); break; - case 'part': - this.parsePart(c); + case 'lyricist': + this._score.words = MusicXmlImporter.sanitizeDisplay(fullText); break; - } - } - } - } - - private parseWork(element: XmlNode): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'work-title': - this._score.title = c.innerText; + case 'rights': + this._score.copyright = MusicXmlImporter.sanitizeDisplay(fullText); + break; + case 'part name': break; } } - } - } + } else if (firstWords) { + // here comes the hard part, guessing the credits. + + // position (relative to bottom(!) left) + //const defaultX = parseInt(firstWords.getAttribute('default-x', '0')); + //const defaultY = parseInt(firstWords.getAttribute('default-y', '0')); + + //const fontSize = parseInt(firstWords.getAttribute('font-size', '0')); + const justify = firstWords.getAttribute('font-size', '0'); + const valign = firstWords.getAttribute('font-size', 'top'); + const halign = firstWords.getAttribute('halign', 'left'); + + // titles are typically centered on top, use it there if + // there is no info about it yet + if (valign === 'top') { + // indicator for copyright? so be it + if ( + fullText.includes('copyright') || + fullText.includes('Copyright') || + fullText.includes('©') || + fullText.includes('(c)') || + fullText.includes('(C)') + ) { + this._score.copyright = MusicXmlImporter.sanitizeDisplay(fullText); + return; + } + + // title and subtitle are typically centered, + // use the typical alphaTab placement as reference for valid props + if (halign === 'center' || justify === 'center') { + if (this._score.title.length === 0) { + this._score.title = MusicXmlImporter.sanitizeDisplay(fullText); + return; + } + + if (this._score.subTitle.length === 0) { + this._score.subTitle = MusicXmlImporter.sanitizeDisplay(fullText); + return; + } - private parsePart(element: XmlNode): void { - let id: string = element.getAttribute('id'); - if (!this._trackById.has(id)) { - if (this._trackById.size === 1) { - for (const [x, t] of this._trackById) { - if (t.staves.length === 0 || t.staves[0].bars.length === 0) { - id = x; + if (this._score.album.length === 0) { + this._score.album = MusicXmlImporter.sanitizeDisplay(fullText); + return; + } + } else if (halign === 'right' || justify === 'right') { + // in alphaTab only `music` is right + if (this._score.music.length === 0) { + this._score.music = MusicXmlImporter.sanitizeDisplay(fullText); + return; } } - if (!this._trackById.has(id)) { + + // from here we simply fallback to filling any remaining information (first one wins approach) + if (this._score.artist.length === 0) { + this._score.artist = MusicXmlImporter.sanitizeDisplay(fullText); + return; + } + + if (this._score.words.length === 0) { + this._score.words = MusicXmlImporter.sanitizeDisplay(fullText); return; } - } else { - return; } } - let track: Track = this._trackById.get(id)!; - let isFirstMeasure: boolean = true; - this._maxVoices = 0; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'measure': - if (this.parseMeasure(c, track, isFirstMeasure)) { - isFirstMeasure = false; + } + + private static sanitizeDisplay(text: string): string { + // no newlines or tabs, and non-breaking spaces + return text.replaceAll('\r', '').replaceAll('\n', ' ').replaceAll('\t', '\xA0\xA0').replaceAll(' ', '\xA0'); + } + + // visual aspects of credits are ignored + // private parseCredit(element: XmlNode) { } + + // visual aspects of music notation are ignored. + // with https://github.com/CoderLine/alphaTab/issues/1949 we could use some more information + // but we also need the "real" page layout (or parchment) for some alignment aspects. + // also for some styling stuff we need the settings as part of the renderstylesheet. + // private parseDefaults(element: XmlNode) { + // for (const c of element.childElements()) { + // switch (c.localName) { + // // case 'scaling': + // // case 'concert-score': + // // case 'page-layout': + // // case 'system-layout': + // // case 'staff-layout': + // // case 'appearance': + // // case 'music-font': + // // case 'word-font': + // // case 'lyric-font': + // // case 'lyric-language': + // } + // } + // } + + private parseIdentification(element: XmlNode) { + for (const c of element.childElements()) { + switch (c.localName) { + case 'creator': + if (c.attributes.has('type')) { + switch (c.attributes.get('type')!) { + case 'composer': + this._score.artist = MusicXmlImporter.sanitizeDisplay(c.innerText); + break; + case 'lyricist': + this._score.words = MusicXmlImporter.sanitizeDisplay(c.innerText); + break; + case 'arranger': + this._score.music = MusicXmlImporter.sanitizeDisplay(c.innerText); + break; } - break; - } + } else { + this._score.artist = MusicXmlImporter.sanitizeDisplay(c.innerText); + } + break; + case 'rights': + if (this._score.copyright.length > 0) { + this._score.copyright += ', '; + } + this._score.copyright += c.innerText; + if (c.attributes.has('type')) { + this._score.copyright += ` (${c.attributes.get('type')})`; + } + break; + case 'encoding': + this.parseEncoding(c); + break; + // case 'source': Ignored + // case 'relation': Ignored + // case 'miscellaneous': Ignored + } + } + } + private parseEncoding(element: XmlNode) { + for (const c of element.childElements()) { + switch (c.localName) { + // case 'encoding-date': Ignored + case 'encoder': + if (this._score.tab.length > 0) { + this._score.tab += ', '; + } + this._score.tab += c.innerText; + if (c.attributes.has('type')) { + this._score.tab += ` (${c.attributes.get('type')})`; + } + break; + // case 'software': Ignored + case 'encoding-description': + this._score.notices += MusicXmlImporter.sanitizeDisplay(c.innerText); + break; + // case 'supports': Ignored } } - // ensure voices for all bars - for (let staff of track.staves) { - for (let bar of staff.bars) { - this.ensureVoices(bar); + } + + private parseMovementTitle(element: XmlNode) { + if (this._score.title.length === 0) { + // we have no "work title", then use the "movement title" as main title + this._score.title = MusicXmlImporter.sanitizeDisplay(element.innerText); + } else { + // we have a "work title", then use the "movement title" as subtitle + this._score.subTitle = MusicXmlImporter.sanitizeDisplay(element.innerText); + } + } + + private parsePartList(element: XmlNode) { + for (const c of element.childElements()) { + switch (c.localName) { + // case 'part-group': Ignore + // We currently ignore information from part-group + // The Track > Staff structure is handled by the element on measure level + // we only support automatic placement of brackets/braces, not explicit. + case 'score-part': + this.parseScorePart(c); + break; } } } - private parseMeasure(element: XmlNode, track: Track, isFirstMeasure: boolean): boolean { - if (element.getAttribute('implicit') === 'yes' && element.getElementsByTagName('note', false).length === 0) { - return false; + private parseScorePart(element: XmlNode) { + const track = new Track(); + track.ensureStaveCount(1); + this._score.addTrack(track); + + const id = element.attributes.get('id')!; + const trackInfo = new TrackInfo(track); + this._idToTrackInfo.set(id, trackInfo); + this._indexToTrackInfo.set(track.index, trackInfo); + + for (const c of element.childElements()) { + switch (c.localName) { + // case 'identification': Ignored, no part-wise information. + // case 'part-link': Not supported + case 'part-name': + track.name = MusicXmlImporter.sanitizeDisplay(c.innerText); + break; + case 'part-name-display': + track.name = this.parsePartDisplayAsText(c); + break; + case 'part-abbreviation': + track.shortName = MusicXmlImporter.sanitizeDisplay(c.innerText); + break; + case 'part-abbreviation-display': + track.shortName = this.parsePartDisplayAsText(c); + break; + // case 'group': Ignored + case 'score-instrument': + this.parseScoreInstrument(c, trackInfo); + break; + // case 'player': Ignored + case 'midi-device': + if (c.attributes.has('port')) { + track.playbackInfo.port = Number.parseInt(c.attributes.get('port')!, 10); + } + break; + case 'midi-instrument': + this.parseScorePartMidiInstrument(c, trackInfo); + break; + } } - let barIndex: number = 0; - if (isFirstMeasure) { - this._divisionsPerQuarterNote = 0; - this._trackFirstMeasureNumber = parseInt(element.getAttribute('number')); - if (!this._trackFirstMeasureNumber) { - this._trackFirstMeasureNumber = 0; + + if (trackInfo.firstArticulation) { + if (trackInfo.firstArticulation.outputMidiProgram >= 0) { + track.playbackInfo.program = trackInfo.firstArticulation.outputMidiProgram; } - barIndex = 0; - } else { - barIndex = parseInt(element.getAttribute('number')); - if (!barIndex) { - return false; - } - barIndex -= this._trackFirstMeasureNumber; - } - // try to find out the number of staffs required - if (isFirstMeasure) { - let attributes: XmlNode[] = element.getElementsByTagName('attributes', false); - if (attributes.length > 0) { - let stavesElements: XmlNode[] = attributes[0].getElementsByTagName('staves', false); - if (stavesElements.length > 0) { - let staves: number = parseInt(stavesElements[0].innerText); - track.ensureStaveCount(staves); - } + if (trackInfo.firstArticulation.outputBalance >= 0) { + track.playbackInfo.balance = trackInfo.firstArticulation.outputBalance; + } + if (trackInfo.firstArticulation.outputVolume >= 0) { + track.playbackInfo.volume = trackInfo.firstArticulation.outputVolume; + } + if (trackInfo.firstArticulation.outputMidiChannel >= 0) { + track.playbackInfo.primaryChannel = trackInfo.firstArticulation.outputMidiChannel; + track.playbackInfo.secondaryChannel = trackInfo.firstArticulation.outputMidiChannel; } } - // create empty bars to the current index - let bars: Bar[] = new Array(track.staves.length); - let masterBar: MasterBar | null = null; - for (let b: number = track.staves[0].bars.length; b <= barIndex; b++) { - for (let s: number = 0; s < track.staves.length; s++) { - let bar: Bar = new Bar(); - bars[s] = bar; - if (track.staves[s].bars.length > 0) { - let previousBar: Bar = track.staves[s].bars[track.staves[s].bars.length - 1]; - bar.clef = previousBar.clef; - } - masterBar = this.getOrCreateMasterBar(barIndex); - track.staves[s].addBar(bar); - this.ensureVoices(bar); + } + + private parseScoreInstrument(element: XmlNode, trackInfo: TrackInfo) { + const articulation = new InstrumentArticulationWithPlaybackInfo(); + if (!trackInfo.firstArticulation) { + trackInfo.firstArticulation = articulation; + } + trackInfo.instruments.set(element.getAttribute('id', ''), articulation); + } + + private parseScorePartMidiInstrument(element: XmlNode, trackInfo: TrackInfo) { + const id = element.getAttribute('id', ''); + if (!trackInfo.instruments.has(id)) { + return; + } + const articulation = trackInfo.instruments.get(id)!; + + for (const c of element.childElements()) { + switch (c.localName) { + case 'midi-channel': + articulation.outputMidiChannel = Number.parseInt(c.innerText) - 1; + break; + // case 'midi-name': Ignored + // case 'midi-bank': Not supported (https://github.com/CoderLine/alphaTab/issues/1986) + case 'midi-program': + articulation.outputMidiProgram = Number.parseInt(c.innerText) - 1; + break; + case 'midi-unpitched': + articulation.outputMidiNumber = Number.parseInt(c.innerText) - 1; + break; + case 'volume': + articulation.outputVolume = MusicXmlImporter.interpolatePercent(Number.parseFloat(c.innerText)); + break; + case 'pan': + articulation.outputBalance = MusicXmlImporter.interpolatePan(Number.parseFloat(c.innerText)); + break; + // case 'elevation': Ignored } } + } + + private static interpolatePercent(value: number) { + return MusicXmlImporter.interpolate(0, 100, 0, 16, value) | 0; + } + + private static interpolatePan(value: number) { + return MusicXmlImporter.interpolate(-90, 90, 0, 16, value) | 0; + } + + private static interpolate( + inputStart: number, + inputEnd: number, + outputStart: number, + outputEnd: number, + value: number + ): number { + const t = (value - inputStart) / (inputEnd - inputStart); + return outputStart + (outputEnd - outputStart) * t; + } - let chordsByIdForTrack = new Map(); - if (masterBar) { - let attributesParsed: boolean = false; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'note': - this.parseNoteBeat(c, bars); + private parsePartDisplayAsText(element: XmlNode): string { + let text = ''; + for (const c of element.childElements()) { + switch (c.localName) { + case 'display-text': + text += c.innerText; + break; + case 'accidental-text': + // to our best to have a plain text accidental using the unicode blocks + // we don't have a SmuFL Text font in place there to use MusicFontSymbols + switch (c.innerText) { + case 'sharp': + text += '♯'; break; - case 'forward': - this.parseForward(c, bars); + case 'natural': + text += '♮'; break; - case 'direction': - this.parseDirection(c, masterBar); + case 'flat': + text += '♭'; break; - case 'attributes': - if (!attributesParsed) { - this.parseAttributes(c, bars, masterBar, track); - attributesParsed = true; - } + case 'double-sharp': + text += '𝄪'; + break; + case 'sharp-sharp': + text += '♯♯'; + break; + case 'flat-flat': + text += '𝄫'; + break; + case 'natural-sharp': + text += '♮♯'; + break; + case 'natural-flat': + text += '♮♭'; + break; + // case 'quarter-flat': Not supported + // case 'quarter-sharp': Not supported + // case 'three-quarters-flat': Not supported + // case 'three-quarters-sharp': Not supported + case 'sharp-down': + text += '𝄱'; + break; + case 'sharp-up': + text += '𝄰'; + break; + case 'natural-down': + text += '𝄯'; + break; + case 'natural-up': + text += '𝄮'; + break; + case 'flat-down': + text += '𝄭'; + break; + case 'flat-up': + text += '𝄬'; + break; + // case 'double-sharp-down': Not supported + // case 'double-sharp-up': Not supported + // case 'flat-flat-down': Not supported + // case 'flat-flat-up': Not supported + case 'arrow-down': + text += '↓'; + break; + case 'arrow-up': + text += '↑'; break; - case 'harmony': - this.parseHarmony(c, track, chordsByIdForTrack); + case 'triple-sharp': + text += '♯𝄪'; break; - case 'sound': - // TODO + case 'triple-flat': + text += '𝄬𝄬𝄬'; break; - case 'barline': - this.parseBarline(c, masterBar); + // case 'slash-quarter-sharp': Not supported + // case 'slash-sharp': Not supported + // case 'slash-flat': Not supported + // case 'double-slash-flat': Not supported + case 'sharp-1': + text += '♯¹'; break; + case 'sharp-2': + text += '♯²'; + break; + case 'sharp-3': + text += '♯³'; + break; + case 'sharp-4': + text += '♯⁴'; + break; + case 'sharp-5': + text += '♯⁵'; + break; + case 'flat-1': + text += '♭¹'; + break; + case 'flat-2': + text += '♭²'; + break; + case 'flat-3': + text += '♭³'; + break; + case 'flat-4': + text += '♭⁴'; + break; + case 'flat-5': + text += '♭⁵'; + break; + // case 'sori': Not supported + // case 'kokon': Not supported + // case 'other': Not supported } - } + + break; } } - return true; + return MusicXmlImporter.sanitizeDisplay(text); } - private ensureVoices(bar: Bar): void { - while (bar.voices.length < this._maxVoices) { - let emptyVoice: Voice = new Voice(); - bar.addVoice(emptyVoice); - let emptyBeat: Beat = new Beat(); - emptyBeat.isEmpty = true; - emptyBeat.chordId = this._currentChord; - emptyVoice.addBeat(emptyBeat); + private parseWork(element: XmlNode) { + for (const c of element.childElements()) { + switch (c.localName) { + // case 'work-number': Ignored + // case 'opus': Ignored + case 'work-title': + this._score.title = MusicXmlImporter.sanitizeDisplay(c.innerText); + break; + } } } - private getOrCreateBeat(element: XmlNode, bars: Bar[], chord: boolean): Beat { - let voiceIndex: number = 0; - let voiceNodes: XmlNode[] = element.getElementsByTagName('voice', false); - if (voiceNodes.length > 0) { - voiceIndex = parseInt(voiceNodes[0].innerText) - 1; + private parsePartwisePart(element: XmlNode) { + const id = element.attributes.get('id'); + if (!id || !this._idToTrackInfo.has(id)) { + return; } - let previousBeatWasPulled: boolean = this._previousBeatWasPulled; - this._previousBeatWasPulled = false; - let staffElement: XmlNode[] = element.getElementsByTagName('staff', false); - let staff: number = 1; - if (staffElement.length > 0) { - staff = parseInt(staffElement[0].innerText); - // in case we have a beam with a staff-jump we pull the note to the previous staff - if ( - (this._isBeamContinue || previousBeatWasPulled) && - this._previousBeat!.voice.bar.staff.index !== staff - 1 - ) { - staff = this._previousBeat!.voice.bar.staff.index + 1; - this._previousBeatWasPulled = true; - } - let staffId: string = bars[0].staff.track.index + '-' + staff; - if (!this._voiceOfStaff.has(staffId)) { - this._voiceOfStaff.set(staffId, voiceIndex); + const track = this._idToTrackInfo.get(id)!.track; + let index = 0; + for (const c of element.childElements()) { + switch (c.localName) { + case 'measure': + this.parsePartwiseMeasure(c, track, index); + index++; + break; } } - staff--; - let bar: Bar; - if (staff < 0) { - bar = bars[0]; - } else if (staff >= bars.length) { - bar = bars[bars.length - 1]; - } else { - bar = bars[staff]; - } - let beat: Beat; - let voice: Voice = this.getOrCreateVoice(bar, voiceIndex); - if ((chord && voice.beats.length > 0) || (voice.beats.length === 1 && voice.isEmpty)) { - beat = voice.beats[voice.beats.length - 1]; - } else { - beat = new Beat(); - beat.isEmpty = false; - voice.addBeat(beat); - } - this._isBeamContinue = false; - this._previousBeat = beat; - return beat; } - private parseForward(element: XmlNode, bars: Bar[]): void { - let beat: Beat = this.getOrCreateBeat(element, bars, false); - let durationInDivisions: number = parseInt(element.findChildElement('duration')!.innerText); - let duration: number = (durationInDivisions * Duration.Quarter) / this._divisionsPerQuarterNote; - let durations = [ - Duration.SixtyFourth, - Duration.ThirtySecond, - Duration.Sixteenth, - Duration.Eighth, - Duration.Quarter, - Duration.Half, - Duration.Whole - ]; - for (let d of durations) { - if (duration >= d) { - beat.duration = d; - duration -= d; - break; + private parsePartwiseMeasure(element: XmlNode, track: Track, index: number) { + const masterBar = this.getOrCreateMasterBar(element, index); + this.parsePartMeasure(element, masterBar, track); + } + + private parseTimewiseMeasure(element: XmlNode, index: number) { + const masterBar = this.getOrCreateMasterBar(element, index); + + for (const c of element.childElements()) { + switch (c.localName) { + case 'part': + this.parseTimewisePart(c, masterBar); + break; } } - if (duration > 0) { - // TODO: Handle remaining duration - // (additional beats, dotted durations,...) - } - beat.isEmpty = false; } - private parseStaffDetails(element: XmlNode, track: Track): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'staff-lines': - for (let staff of track.staves) { - staff.stringTuning.tunings = new Array(parseInt(c.innerText)).fill(0); - } - break; - case 'staff-tuning': - this.parseStaffTuning(c, track); - break; - } + private getOrCreateMasterBar(element: XmlNode, index: number) { + const implicit = element.attributes.get('implicit') === 'yes'; + while (this._score.masterBars.length <= index) { + const newMasterBar = new MasterBar(); + if (implicit) { + newMasterBar.isAnacrusis = true; } - } - for (let staff of track.staves) { - if (this.isEmptyTuning(staff.tuning)) { - staff.stringTuning.tunings = []; + this._score.addMasterBar(newMasterBar); + if (newMasterBar.index > 0) { + newMasterBar.timeSignatureDenominator = newMasterBar.previousMasterBar!.timeSignatureDenominator; + newMasterBar.timeSignatureNumerator = newMasterBar.previousMasterBar!.timeSignatureNumerator; + newMasterBar.tripletFeel = newMasterBar.previousMasterBar!.tripletFeel; } } + + const masterBar = this._score.masterBars[index]; + return masterBar; } - private parseStaffTuning(element: XmlNode, track: Track): void { - let line: number = parseInt(element.getAttribute('line')); - let tuningStep: string = 'C'; - let tuningOctave: string = ''; - let tuningAlter: number = 0; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'tuning-step': - tuningStep = c.innerText; - break; - case 'tuning-alter': - tuningAlter = parseInt(c.innerText); - break; - case 'tuning-octave': - tuningOctave = c.innerText; - break; - } - } - } - let tuning: number = ModelUtils.getTuningForText(tuningStep + tuningOctave) + tuningAlter; - for (let staff of track.staves) { - staff.tuning[staff.tuning.length - line] = tuning; + private parseTimewisePart(element: XmlNode, masterBar: MasterBar) { + 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); } - private _currentChord: string | null = null; - private _divisionsPerQuarterNote: number = 0; + // current measure state - private parseHarmony(element: XmlNode, track: Track, chordsByIdForTrack: Map): void { - let chord: Chord = new Chord(); - for (let childNode of element.childNodes) { - if (childNode.nodeType === XmlNodeType.Element) { - switch (childNode.localName) { - case 'root': - chord.name = this.parseHarmonyRoot(childNode); - break; - case 'kind': - chord.name = chord.name + this.parseHarmonyKind(childNode); - break; - case 'frame': - this.parseHarmonyFrame(childNode, chord); - break; - } - } - } + /** + * The current musical position within the bar. + */ + private _musicalPosition: number = 0; - // var degree = element.GetElementsByTagName("degree"); - // if (degree.Length > 0) - // { - // var degreeValue = Platform.GetNodeValue(degree[0].GetElementsByTagName("degree-value")[0]); - // var degreeAlter = Platform.GetNodeValue(degree[0].GetElementsByTagName("degree-alter")[0]); - // var degreeType = Platform.GetNodeValue(degree[0].GetElementsByTagName("degree-type")[0]); - // if (!string.IsNullOrEmpty(degreeType)) - // { - // chord.Name += degreeType; - // } - // if (!string.IsNullOrEmpty(degreeValue)) - // { - // chord.Name += "#" + degreeValue; - // } - // } - this._currentChord = ModelUtils.newGuid(); - const chordKey: string = chord.uniqueId; - if (chordsByIdForTrack.has(chordKey)) { - // check if the chord is already present - chord.showDiagram = false; - } - for (let staff of track.staves) { - staff.addChord(this._currentChord, chord); - } - chordsByIdForTrack.set(chordKey, chord); - } + /** + * The last known beat which was parsed. Might be used + * to access the current voice/staff (e.g. on rests when we don't have notes) + */ + private _lastBeat: Beat | null = null; - private parseHarmonyRoot(xmlNode: XmlNode): string { - let rootStep: string = ''; - let rootAlter: string = ''; - for (let rootChild of xmlNode.childNodes) { - if (rootChild.nodeType === XmlNodeType.Element) { - switch (rootChild.localName) { - case 'root-step': - rootStep = rootChild.innerText; - break; - case 'root-alter': - switch (parseInt(xmlNode.innerText)) { - case -2: - rootAlter = 'bb'; - break; - case -1: - rootAlter = 'b'; - break; - case 0: - rootAlter = ''; - break; - case 1: - rootAlter = '#'; - break; - case 2: - rootAlter = '##'; - break; - } - break; + private parsePartMeasure(element: XmlNode, masterBar: MasterBar, track: Track) { + this._musicalPosition = 0; + this._lastBeat = null; + + masterBar.alternateEndings = this._nextMasterBarRepeatEnding; + + const barLines: XmlNode[] = []; + + for (const c of element.childElements()) { + switch (c.localName) { + case 'note': + this.parseNote(c, masterBar, track); + break; + case 'backup': + this.parseBackup(c); + break; + case 'forward': + this.parseForward(c); + break; + case 'direction': + this.parseDirection(c, masterBar, track); + break; + case 'attributes': + this.parseAttributes(c, masterBar, track); + break; + case 'harmony': + this.parseHarmony(c, track); + break; + // case 'figured-bass': Not supported + case 'print': + this.parsePrint(c, masterBar, track); + break; + case 'sound': + this.parseSound(c, masterBar, track); + break; + // case 'listening': Ignored + case 'barline': + barLines.push(c); // delayed + break; + // case 'grouping': Ignored + // case 'link': Not supported + // case 'bookmark': Not supported + } + } + + // parse barline at end of bar (to apply style to all bars of all staves) + for (const barLine of barLines) { + this.parseBarLine(barLine, masterBar, track); + } + + this.applySimileMarks(masterBar, track); + + // initial empty staff and voice (if no other elements created something already) + const staff = this.getOrCreateStaff(track, 0); + this.getOrCreateBar(staff, masterBar); + + // 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 applySimileMarks(masterBar: MasterBar, track: Track) { + if (this._simileMarkAllStaves !== null) { + for (const s of track.staves) { + const bar = this.getOrCreateBar(s, masterBar); + bar.simileMark = this._simileMarkAllStaves!; + if (bar.simileMark !== SimileMark.None) { + this.clearBar(bar); + } + } + + if (this._simileMarkAllStaves === SimileMark.FirstOfDouble) { + this._simileMarkAllStaves = SimileMark.SecondOfDouble; + } else { + this._simileMarkAllStaves = null; + } + } + + if (this._simileMarkPerStaff !== null) { + const keys = Array.from(this._simileMarkPerStaff!.keys()); + for (const i of keys) { + const s = this.getOrCreateStaff(track, i); + const bar = this.getOrCreateBar(s, masterBar); + bar.simileMark = this._simileMarkPerStaff!.get(i)!; + + if (bar.simileMark !== SimileMark.None) { + this.clearBar(bar); + } + + if (bar.simileMark === SimileMark.FirstOfDouble) { + this._simileMarkPerStaff!.set(i, SimileMark.SecondOfDouble); + } else { + this._simileMarkPerStaff!.delete(i); } } + if (this._simileMarkPerStaff.size === 0) { + this._simileMarkPerStaff = null; + } + } + } + + private clearBar(bar: Bar) { + for (const v of bar.voices) { + const emptyBeat: Beat = new Beat(); + emptyBeat.isEmpty = true; + v.addBeat(emptyBeat); + } + } + + private parseBarLine(element: XmlNode, masterBar: MasterBar, track: Track) { + for (const c of element.childElements()) { + switch (c.localName) { + case 'bar-style': + this.parseBarStyle(c, masterBar, track, element.getAttribute('location', 'right')); + break; + // case 'footnote' Ignored + // case 'level' Ignored + // case 'wavy-line' Ignored + // case 'segno': Ignored (use directions) + // case 'coda': Ignored (use directions) + // case 'fermata': Ignored (on barline, they exist on beat notations) + case 'ending': + this.parseEnding(c, masterBar); + break; + case 'repeat': + this.parseRepeat(c, masterBar); + break; + } + } + } + + private parseRepeat(element: XmlNode, masterBar: MasterBar): void { + const direction: string = element.getAttribute('direction'); + let times: number = Number.parseInt(element.getAttribute('times')); + if (times < 0 || Number.isNaN(times)) { + times = 2; + } + if (direction === 'backward') { + masterBar.repeatCount = times; + } else if (direction === 'forward') { + masterBar.isRepeatStart = true; + } + } + + private _nextMasterBarRepeatEnding: number = 0; + private parseEnding(element: XmlNode, masterBar: MasterBar): void { + const numbers = element + .getAttribute('number') + .split(',') + .map(v => Number.parseInt(v)); + + let flags = 0; + for (const num of numbers) { + flags = flags | ((0x01 << (num - 1)) & 0xff); + } + + masterBar.alternateEndings = flags; + + switch (element.getAttribute('type', '')) { + case 'start': + this._nextMasterBarRepeatEnding = this._nextMasterBarRepeatEnding | flags; + break; + case 'stop': + case 'discontinue': + this._nextMasterBarRepeatEnding = this._nextMasterBarRepeatEnding & ~flags; + break; + case 'continue': + // keep + break; + } + } + + private parseBarStyle(element: XmlNode, masterBar: MasterBar, track: Track, location: string) { + let style = BarLineStyle.Automatic; + + switch (element.innerText) { + case 'dashed': + style = BarLineStyle.Dashed; + break; + case 'dotted': + style = BarLineStyle.Dotted; + break; + case 'heavy': + style = BarLineStyle.Heavy; + break; + case 'heavy-heavy': + style = BarLineStyle.HeavyHeavy; + break; + case 'heavy-light': + style = BarLineStyle.HeavyLight; + break; + case 'light-heavy': + style = BarLineStyle.LightHeavy; + break; + case 'light-light': + style = BarLineStyle.LightLight; + break; + case 'none': + style = BarLineStyle.None; + break; + case 'regular': + style = BarLineStyle.Regular; + break; + case 'short': + style = BarLineStyle.Short; + break; + case 'tick': + style = BarLineStyle.Tick; + break; + } + + for (const s of track.staves) { + const bar = this.getOrCreateBar(s, masterBar); + switch (location) { + case 'left': + bar.barLineLeft = style; + break; + case 'right': + bar.barLineRight = style; + break; + } + } + } + + private parseSound(element: XmlNode, masterBar: MasterBar, track: Track) { + for (const c of element.childElements()) { + switch (c.localName) { + // case 'instrument-change': Ignored + // case 'midi-device': Ignored + case 'midi-instrument': + this.parseSoundMidiInstrument(c, masterBar); + break; + // case 'play': Ignored + case 'swing': + this.parseSwing(c, masterBar); + break; + case 'offset': + break; + } + } + + if (element.attributes.has('coda')) { + masterBar.addDirection(Direction.TargetCoda); + } + + if (element.attributes.has('tocoda')) { + masterBar.addDirection(Direction.JumpDaCoda); + } + + if (element.attributes.has('dacapo')) { + masterBar.addDirection(Direction.JumpDaCapo); + } + + if (element.attributes.has('dalsegno')) { + masterBar.addDirection(Direction.JumpDalSegno); + } + + if (element.attributes.has('fine')) { + masterBar.addDirection(Direction.TargetFine); + } + + if (element.attributes.has('segno')) { + masterBar.addDirection(Direction.TargetSegno); + } + + // damper-pedal="" Ignored -> Handled via pedal direction + // dynamics="" Ignored -> Handled via dynamics direction + // elevation="" Ignored + // forward-repeat="" Ignored + // pizzicato="" Ignored + // pizzicato="" Ignored + // soft-pedal="" Ignored + // sostenuto-pedal="" Ignored + // time-only="" Ignored + + if (element.attributes.has('pan')) { + if (!this._nextBeatAutomations) { + this._nextBeatAutomations = []; + } + + const automation = new Automation(); + automation.type = AutomationType.Balance; + automation.value = MusicXmlImporter.interpolatePan(Number.parseFloat(element.attributes.get('pan')!)); + this._nextBeatAutomations.push(automation); + } + + if (element.attributes.has('tempo')) { + if (!this._nextBeatAutomations) { + this._nextBeatAutomations = []; + } + + const automation = new Automation(); + automation.type = AutomationType.Tempo; + automation.value = MusicXmlImporter.interpolatePercent(Number.parseFloat(element.attributes.get('tempo')!)); + this._nextBeatAutomations.push(automation); + } + } + private parseSwing(element: XmlNode, masterBar: MasterBar) { + let first = 0; + let second = 0; + let swingType: Duration | null = null; + for (const c of element.childElements()) { + switch (c.localName) { + case 'straight': + masterBar.tripletFeel = TripletFeel.NoTripletFeel; + return; + case 'first': + first = Number.parseInt(c.innerText); + break; + case 'second': + second = Number.parseInt(c.innerText); + break; + case 'swing-type': + swingType = this.parseBeatDuration(c); + break; + // case 'swing-style': Ignored + } + } + + // spec is a bit vague here + if (!swingType) { + swingType = Duration.Eighth; + } + + if (swingType === Duration.Eighth) { + if (first === 2 && second === 1) { + masterBar.tripletFeel = TripletFeel.Triplet8th; + } else if (first === 3 && second === 1) { + masterBar.tripletFeel = TripletFeel.Dotted8th; + } else if (first === 1 && second === 3) { + masterBar.tripletFeel = TripletFeel.Scottish8th; + } + } else if (swingType === Duration.Sixteenth) { + if (first === 2 && second === 1) { + masterBar.tripletFeel = TripletFeel.Triplet16th; + } else if (first === 3 && second === 1) { + masterBar.tripletFeel = TripletFeel.Dotted16th; + } else if (first === 1 && second === 3) { + masterBar.tripletFeel = TripletFeel.Scottish16th; + } + } + } + + private _nextBeatAutomations: Automation[] | null = null; + private _nextBeatChord: Chord | null = null; + private _nextBeatCrescendo: CrescendoType | null = null; + private _nextBeatLetRing: boolean = false; + private _nextBeatPalmMute: boolean = false; + private _nextBeatOttavia: Ottavia | null = null; + private _nextBeatText: string | null = null; + + private parseSoundMidiInstrument(element: XmlNode, masterBar: MasterBar) { + let automation: Automation; + for (const c of element.childElements()) { + switch (c.localName) { + // case 'midi-channel': Ignored + // case 'midi-name': Ignored + // case 'midi-bank': Ignored + case 'midi-program': + if (!this._nextBeatAutomations) { + this._nextBeatAutomations = []; + } + + automation = new Automation(); + automation.type = AutomationType.Instrument; + automation.value = Number.parseInt(c.innerText) - 1; + this._nextBeatAutomations!.push(automation); + + break; + // case 'midi-unpitched': Ignored + case 'volume': + if (!this._nextBeatAutomations) { + this._nextBeatAutomations = []; + } + + automation = new Automation(); + automation.type = AutomationType.Volume; + automation.value = MusicXmlImporter.interpolatePercent(Number.parseFloat(c.innerText)); + this._nextBeatAutomations!.push(automation); + + break; + case 'pan': + if (!this._nextBeatAutomations) { + this._nextBeatAutomations = []; + } + + automation = new Automation(); + automation.type = AutomationType.Balance; + automation.value = MusicXmlImporter.interpolatePan(Number.parseFloat(c.innerText)); + this._nextBeatAutomations!.push(automation); + break; + // case 'elevation': Ignored + } + } + } + + private parseHarmony(element: XmlNode, track: Track) { + const chord = new Chord(); + let degreeParenthesis = false; + let degree = ''; + for (const childNode of element.childElements()) { + switch (childNode.localName) { + case 'root': + chord.name = this.parseHarmonyRoot(childNode); + break; + case 'kind': + chord.name = chord.name + this.parseHarmonyKind(childNode); + if (childNode.getAttribute('parentheses-degrees', 'no') === 'yes') { + degreeParenthesis = true; + } + break; + case 'frame': + this.parseHarmonyFrame(childNode, chord); + break; + case 'degree': + degree += this.parseDegree(childNode); + break; + } + } + + if (degree) { + chord.name += degreeParenthesis ? `(${degree})` : degree; + } + + if (this._nextBeatChord === null) { + this._nextBeatChord = chord; + } + } + + private parseDegree(element: XmlNode) { + let value = ''; + let alter = ''; + let type = ''; + for (const c of element.childElements()) { + switch (c.localName) { + case 'degree-value': + value = c.innerText; + break; + case 'degree-alter': + switch (c.innerText) { + case '-1': + alter = '♭'; + break; + case '1': + alter = '♯'; + break; + } + break; + case 'degree-type': + type += c.getAttribute('text', ''); + break; + } + } + + return `${type}${alter}${value}`; + } + + private parseHarmonyRoot(element: XmlNode): string { + let rootStep: string = ''; + let rootAlter: string = ''; + for (const c of element.childElements()) { + switch (c.localName) { + case 'root-step': + rootStep = c.innerText; + break; + case 'root-alter': + switch (Number.parseFloat(c.innerText)) { + case -2: + rootAlter = 'bb'; + break; + case -1: + rootAlter = 'b'; + break; + case 0: + rootAlter = ''; + break; + case 1: + rootAlter = '#'; + break; + case 2: + rootAlter = '##'; + break; + } + break; + } } return rootStep + rootAlter; } @@ -647,23 +1484,21 @@ export class MusicXmlImporter extends ScoreImporter { case 'suspended-fourth': resultKind = 'sus4'; break; - // TODO: find proper names for the rest - // Functional sixths - // case "Neapolitan": - // break; - // case "Italian": - // break; - // case "French": - // break; - // case "German": - // break; - // // Other - // case "pedal": - // break; - // case "power": - // break; - // case "Tristan": - // break; + case 'Neapolitan': + resultKind = '♭II'; + break; + case 'Italian': + resultKind = 'It⁺⁶'; + break; + case 'French': + resultKind = 'Fr⁺⁶'; + break; + case 'German': + resultKind = 'Fr⁺⁶'; + break; + default: + resultKind = kindContent; + break; } } @@ -671,987 +1506,2313 @@ export class MusicXmlImporter extends ScoreImporter { } private parseHarmonyFrame(xmlNode: XmlNode, chord: Chord) { - for (let frameChild of xmlNode.childNodes) { - if (frameChild.nodeType === XmlNodeType.Element) { - switch (frameChild.localName) { - case 'frame-strings': - const stringsCount: number = parseInt(frameChild.innerText); - chord.strings = new Array(stringsCount); - for (let i = 0; i < stringsCount; i++) { - // set strings unplayed as default - chord.strings[i] = -1; - } - break; - case 'first-fret': - chord.firstFret = parseInt(frameChild.innerText); - break; - case 'frame-note': - let stringNo: number | null = null; - let fretNo: number | null = null; - for (let noteChild of frameChild.childNodes) { - switch (noteChild.localName) { - case 'string': - stringNo = parseInt(noteChild.innerText); - break; - case 'fret': - fretNo = parseInt(noteChild.innerText); - if (stringNo && fretNo >= 0) { - chord.strings[stringNo - 1] = fretNo; - } - break; - case 'barre': - if (stringNo && fretNo && noteChild.getAttribute('type') === 'start') { - chord.barreFrets.push(fretNo); - } - break; - } + for (const frameChild of xmlNode.childElements()) { + switch (frameChild.localName) { + case 'frame-strings': + const stringsCount: number = Number.parseInt(frameChild.innerText); + chord.strings = new Array(stringsCount); + for (let i = 0; i < stringsCount; i++) { + // set strings unplayed as default + chord.strings[i] = -1; + } + break; + case 'first-fret': + chord.firstFret = Number.parseInt(frameChild.innerText); + break; + case 'frame-note': + let stringNo: number | null = null; + let fretNo: number | null = null; + for (const noteChild of frameChild.childElements()) { + switch (noteChild.localName) { + case 'string': + stringNo = Number.parseInt(noteChild.innerText); + break; + case 'fret': + fretNo = Number.parseInt(noteChild.innerText); + if (stringNo && fretNo >= 0) { + chord.strings[stringNo - 1] = fretNo; + } + break; + case 'barre': + if (stringNo && fretNo && noteChild.getAttribute('type') === 'start') { + chord.barreFrets.push(fretNo); + } + break; } - break; - } - } - } - } - - private parseBarline(element: XmlNode, masterBar: MasterBar): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'repeat': - this.parseRepeat(c, masterBar); - break; - case 'ending': - this.parseEnding(c, masterBar); - break; - } + } + break; } } } - private parseEnding(element: XmlNode, masterBar: MasterBar): void { - let num: number = parseInt(element.getAttribute('number')); - if (num > 0) { - --num; - masterBar.alternateEndings = masterBar.alternateEndings | ((0x01 << num) & 0xff); - } - } - - private parseRepeat(element: XmlNode, masterBar: MasterBar): void { - let direction: string = element.getAttribute('direction'); - let times: number = parseInt(element.getAttribute('times')); - if (times < 0 || isNaN(times)) { - times = 2; - } - if (direction === 'backward') { - masterBar.repeatCount = times; - } else if (direction === 'forward') { - masterBar.isRepeatStart = true; - } - } - - private _voiceOfStaff: Map = new Map(); - private _isBeamContinue: boolean = false; - private _previousBeatWasPulled: boolean = false; - private _previousBeat: Beat | null = null; + private parseAttributes(element: XmlNode, masterBar: MasterBar, track: Track) { + let staffIndex: number; + let staff: Staff; + let bar: Bar; - private parseNoteBeat(element: XmlNode, bars: Bar[]): void { - let chord: boolean = element.getElementsByTagName('chord', false).length > 0; - let beat: Beat = this.getOrCreateBeat(element, bars, chord); - if (!beat.chordId && this._currentChord) { - beat.chordId = this._currentChord; - this._currentChord = null; - } - if (this._currentDirection) { - beat.text = this._currentDirection; - this._currentDirection = null; - } - let note: Note = new Note(); - beat.voice.isEmpty = false; - beat.isEmpty = false; - beat.addNote(note); - beat.dots = 0; - let isFullBarRest = false; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { + if (this._lastBeat == null) { + // attributes directly at the start of the bar + for (const c of element.childElements()) { switch (c.localName) { - case 'grace': - // var slash = e.GetAttribute("slash"); - // var makeTime = Platform.ParseInt(e.GetAttribute("make-time")); - // var stealTimePrevious = Platform.ParseInt(e.GetAttribute("steal-time-previous")); - // var stealTimeFollowing = Platform.ParseInt(e.GetAttribute("steal-time-following")); - beat.graceType = GraceType.BeforeBeat; - beat.duration = Duration.ThirtySecond; - break; - case 'duration': - if (beat.isRest && !isFullBarRest) { - // unit: divisions per quarter note - let duration: number = parseInt(c.innerText); - switch (duration) { - case 1: - beat.duration = Duration.Whole; - break; - case 2: - beat.duration = Duration.Half; - break; - case 4: - beat.duration = Duration.Quarter; - break; - case 8: - beat.duration = Duration.Eighth; - break; - case 16: - beat.duration = Duration.Sixteenth; - break; - case 32: - beat.duration = Duration.ThirtySecond; - break; - case 64: - beat.duration = Duration.SixtyFourth; - break; - default: - beat.duration = Duration.Quarter; - break; - } - } - break; - case 'tie': - this.parseTied(c, note); + // case 'footnote': Ignored + // case 'level': Ignored + case 'divisions': + this._divisionsPerQuarterNote = Number.parseFloat(c.innerText); break; - case 'cue': - // not supported + case 'key': + this.parseKey(c, masterBar, track); break; - case 'instrument': - // not supported + case 'time': + this.parseTime(c, masterBar); break; - case 'type': - beat.duration = this.getDuration(c.innerText); - if (beat.graceType !== GraceType.None && beat.duration < Duration.Sixteenth) { - beat.duration = Duration.Eighth; - } + case 'staves': + // will create staves + track.ensureStaveCount(Number.parseInt(c.innerText)); break; - case 'dot': - beat.dots++; + // case 'part-symbol': Ignored (https://github.com/CoderLine/alphaTab/issues/1989) + // case 'instruments': Ignored, auto-detected via `note/instrument` and handled via instrument articulations + case 'clef': + staffIndex = Number.parseInt(c.getAttribute('number', '1')) - 1; + staff = this.getOrCreateStaff(track, staffIndex); + bar = this.getOrCreateBar(staff, masterBar); + this.parseClef(c, bar); break; - case 'accidental': - this.parseAccidental(c, note); + case 'staff-details': + staffIndex = Number.parseInt(c.getAttribute('number', '1')) - 1; + staff = this.getOrCreateStaff(track, staffIndex); + this.parseStaffDetails(c, staff); break; - case 'time-modification': - this.parseTimeModification(c, beat); + case 'transpose': + this.parseTranspose(c, track); break; - case 'stem': - // not supported + // case 'for-part': not supported + // case 'directive': Ignored + case 'measure-style': + this.parseMeasureStyle(c, track, false); break; - case 'notehead': - if (c.getAttribute('parentheses') === 'yes') { - note.isGhost = true; - } - break; - case 'beam': - let beamMode: string = c.innerText; - if (beamMode === 'continue') { - this._isBeamContinue = true; - } - break; - case 'notations': - this.parseNotations(c, beat, note); - break; - case 'lyric': - this.parseLyric(c, beat); - break; - case 'pitch': - this.parsePitch(c, note); - break; - case 'unpitched': - this.parseUnpitched(c, note); - break; - case 'rest': - isFullBarRest = c.getAttribute('measure') === 'yes'; - beat.isEmpty = false; - beat.notes = []; - beat.duration = Duration.Whole; + } + } + } else { + // attribute changes during bar + for (const c of element.childElements()) { + switch (c.localName) { + // case 'footnote': Ignored + // case 'level': Ignored + case 'divisions': + this._divisionsPerQuarterNote = Number.parseFloat(c.innerText); + break; + // https://github.com/CoderLine/alphaTab/issues/1991 + // case 'key': Not supported + // case 'time': Not supported + // case 'part-symbol': Not supported + // case 'instruments': Ignored + // case 'clef': Not supported + // case 'staff-details': Not supported + // case 'transpose': Not supported + // case 'for-part': not supported + // case 'directive': Ignored + case 'measure-style': + this.parseMeasureStyle(c, track, true); break; } } } - // check if new note is duplicate on string - if (note.isStringed) { - for (let i: number = 0; i < beat.notes.length; i++) { - if (beat.notes[i].string === note.string && beat.notes[i] !== note) { - beat.removeNote(note); + } + + private _simileMarkAllStaves: SimileMark | null = null; + private _simileMarkPerStaff: Map | null = null; + private _isBeatSlash: boolean = false; + private parseMeasureStyle(element: XmlNode, track: Track, midBar: boolean) { + for (const c of element.childElements()) { + switch (c.localName) { + // case 'multiple-rest': Ignored, when multibar rests are enabled for rendering this info shouldn't matter. + case 'measure-repeat': + if (!midBar) { + let simileMark: SimileMark | null = null; + switch (c.getAttribute('type')) { + case 'start': + switch (Number.parseInt(c.getAttribute('slashes', '1'))) { + case 1: + simileMark = SimileMark.Simple; + break; + case 2: + simileMark = SimileMark.FirstOfDouble; + break; + default: + // not supported + break; + } + break; + case 'stop': + simileMark = null; + break; + } + + if (element.attributes.has('number')) { + this._simileMarkPerStaff = this._simileMarkPerStaff ?? new Map(); + const staff = Number.parseInt(element.attributes.get('number')!) - 1; + if (simileMark == null) { + this._simileMarkPerStaff!.delete(staff); + } else { + this._simileMarkPerStaff!.set(staff, simileMark!); + } + } else { + this._simileMarkAllStaves = simileMark; + } + } + + break; + // case 'beat-repeat': Not supported + case 'slash': + // use-stems: not supported + switch (c.getAttribute('type')) { + case 'start': + this._isBeatSlash = true; + break; + case 'stop': + this._isBeatSlash = false; + break; + } break; - } } } } - private getDuration(text: string): Duration { - switch (text) { - case '256th': - case '128th': - case '64th': - return Duration.SixtyFourth; - case '32nd': - return Duration.ThirtySecond; - case '16th': - return Duration.Sixteenth; - case 'eighth': - return Duration.Eighth; - case 'quarter': - return Duration.Quarter; - case 'half': - return Duration.Half; - case 'long': - case 'breve': - case 'whole': - return Duration.Whole; + private parseTranspose(element: XmlNode, track: Track): void { + let semitones: number = 0; + for (const c of element.childElements()) { + switch (c.localName) { + // case 'diatonic': Not supported + case 'chromatic': + semitones += Number.parseFloat(c.innerText); + break; + case 'octave-change': + semitones += Number.parseFloat(c.innerText) * 12; + break; + // case 'double': Not supported + } + } + + if (element.attributes.has('number')) { + const staff = this.getOrCreateStaff(track, Number.parseInt(element.attributes.get('number')!) - 1); + this.getStaffContext(staff).transpose = semitones; + staff.displayTranspositionPitch = semitones; + } else { + for (const staff of track.staves) { + this.getStaffContext(staff).transpose = semitones; + staff.displayTranspositionPitch = semitones; + } } - return Duration.Quarter; } - private parseLyric(element: XmlNode, beat: Beat): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'text': - if (beat.text) { - beat.text += ' ' + c.innerText; - } else { - beat.text = c.innerText; - } - break; - } + private parseStaffDetails(element: XmlNode, staff: Staff): void { + for (const c of element.childElements()) { + switch (c.localName) { + // case 'staff-type': Ignored + case 'staff-lines': + staff.standardNotationLineCount = Number.parseInt(c.innerText); + break; + // case 'line-detail': Not supported + case 'staff-tuning': + this.parseStaffTuning(c, staff); + break; + case 'capo': + staff.capo = Number.parseInt(c.innerText); + break; + // case 'staff-size': Not supported } } } - private parseAccidental(element: XmlNode, note: Note): void { - switch (element.innerText) { - case 'sharp': - note.accidentalMode = NoteAccidentalMode.ForceSharp; - break; - case 'natural': - note.accidentalMode = NoteAccidentalMode.ForceNatural; - break; - case 'flat': - note.accidentalMode = NoteAccidentalMode.ForceFlat; - break; - case 'double-sharp': - break; - case 'sharp-sharp': - break; - case 'flat-flat': - break; - case 'natural-sharp': + private parseStaffTuning(element: XmlNode, staff: Staff): void { + if (staff.stringTuning.tunings.length === 0) { + staff.showTablature = true; + staff.showStandardNotation = false; + staff.stringTuning.tunings = new Array(staff.standardNotationLineCount).fill(0); + } + + const line: number = Number.parseInt(element.getAttribute('line')); + let tuningStep: string = 'C'; + let tuningOctave: string = ''; + let tuningAlter: number = 0; + for (const c of element.childElements()) { + switch (c.localName) { + case 'tuning-step': + tuningStep = c.innerText; + break; + case 'tuning-alter': + tuningAlter = Number.parseFloat(c.innerText); + break; + case 'tuning-octave': + tuningOctave = c.innerText; + break; + } + } + const tuning: number = ModelUtils.getTuningForText(tuningStep + tuningOctave) + tuningAlter; + staff.tuning[staff.tuning.length - line] = tuning; + } + + private parseClef(element: XmlNode, bar: Bar): void { + let sign: string = 's'; + let line: number = 0; + for (const c of element.childElements()) { + switch (c.localName) { + case 'sign': + sign = c.innerText.toLowerCase(); + break; + case 'line': + line = Number.parseInt(c.innerText); + break; + case 'clef-octave-change': + switch (Number.parseInt(c.innerText)) { + case -2: + bar.clefOttava = Ottavia._15mb; + break; + case -1: + bar.clefOttava = Ottavia._8vb; + break; + case 1: + bar.clefOttava = Ottavia._8va; + break; + case 2: + bar.clefOttava = Ottavia._15mb; + break; + } + break; + } + } + switch (sign) { + case 'g': + bar.clef = Clef.G2; break; - case 'natural-flat': + case 'f': + bar.clef = Clef.F4; break; - case 'quarter-flat': + case 'c': + if (line === 3) { + bar.clef = Clef.C3; + } else { + bar.clef = Clef.C4; + } break; - case 'quarter-sharp': + case 'percussion': + bar.clef = Clef.Neutral; + bar.staff.isPercussion = true; break; - case 'three-quarters-flat': + case 'tab': + bar.clef = Clef.G2; + bar.staff.showTablature = true; break; - case 'three-quarters-sharp': + default: + bar.clef = Clef.G2; break; } } - private parseTied(element: XmlNode, note: Note): void { - if (element.getAttribute('type') === 'start') { - if (!this._tieStartIds.has(note.id)) { - this._tieStartIds.set(note.id, true); - this._tieStarts.push(note); - } - } else if (element.getAttribute('type') === 'stop' && this._tieStarts.length > 0 && !note.isTieDestination) { - const tieOrigin = this._tieStarts[0]; - // no cross track/staff or voice ties supported for now - if ( - tieOrigin.beat.voice.index === note.beat.voice.index && - tieOrigin.beat.voice.bar.staff.index === note.beat.voice.bar.staff.index && - tieOrigin.beat.voice.bar.staff.track.index === note.beat.voice.bar.staff.track.index - ) { - note.isTieDestination = true; - note.tieOrigin = this._tieStarts[0]; + private parseTime(element: XmlNode, masterBar: MasterBar): void { + let beatsParsed: boolean = false; + let beatTypeParsed: boolean = false; + for (const c of element.childElements()) { + const v: string = c.innerText; + switch (c.localName) { + case 'beats': + if (!beatsParsed) { + if (v.indexOf('+') === -1) { + masterBar.timeSignatureNumerator = Number.parseInt(v); + } else { + masterBar.timeSignatureNumerator = v + .split('+') + .map(v => Number.parseInt(v)) + .reduce((sum, v) => v + sum, 0); + } + beatsParsed = true; + } + break; + case 'beat-type': + if (!beatTypeParsed) { + if (v.indexOf('+') === -1) { + masterBar.timeSignatureDenominator = Number.parseInt(v); + } else { + masterBar.timeSignatureDenominator = v + .split('+') + .map(v => Number.parseInt(v)) + .reduce((sum, v) => v + sum, 0); + } + beatTypeParsed = true; + } + break; + // case 'interchangeable': Not supported + // case 'senza-misura': Not supported } + } - this._tieStarts.splice(0, 1); - this._tieStartIds.delete(note.id); + switch (element.getAttribute('symbol', '')) { + case 'common': + case 'cut': + masterBar.timeSignatureCommon = true; + break; + // case 'dotted-note': Not supported + // case 'normal': implicit + // case 'note': Not supported + // case 'single-number': Not supported } } - private parseNotations(element: XmlNode, beat: Beat, note: Note): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'articulations': - this.parseArticulations(c, note); - break; - case 'tied': - this.parseTied(c, note); - break; - case 'slide': - case 'glissando': - if (c.getAttribute('type') === 'start') { - note.slideOutType = SlideOutType.Shift; - } - break; - case 'dynamics': - this.parseDynamics(c, beat); - break; - case 'technical': - this.parseTechnical(c, note); - break; - case 'ornaments': - this.parseOrnaments(c, note); - break; - case 'slur': - let slurNumber: string = c.getAttribute('number'); - if (!slurNumber) { - slurNumber = '1'; - } + private _keyAllStaves: [KeySignature, KeySignatureType] | null = null; - // slur numbers are unique in the way that they have the same ID across - // staffs/tracks etc. as long they represent the logically same slur. - // but in our case it must be globally unique to link the correct notes. - // adding the staff ID should be enough to achieve this - slurNumber = beat.voice.bar.staff.index + '_' + slurNumber; + private parseKey(element: XmlNode, masterBar: MasterBar, track: Track): void { + let fifths: number = -(KeySignature.C as number); + let mode: string = ''; - switch (c.getAttribute('type')) { - case 'start': - this._slurStarts.set(slurNumber, note); - break; - case 'stop': - if (this._slurStarts.has(slurNumber)) { - note.isSlurDestination = true; - let slurStart: Note = this._slurStarts.get(slurNumber)!; - slurStart.slurDestination = note; - note.slurOrigin = slurStart; - } - break; - } - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + // case 'cancel': not supported + case 'fifths': + fifths = Number.parseInt(c.innerText); + break; + case 'mode': + mode = c.innerText; + break; + + // case 'key-step': Not supported + // case 'key-alter': Not supported + // case 'key-accidental': Not supported + // case 'key-octave': Not supported } } - } - private parseOrnaments(element: XmlNode, note: Note): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'tremolo': - let tremoloSpeed: number = parseInt(c.innerText); - switch (tremoloSpeed) { - case 1: - note.beat.tremoloSpeed = Duration.Eighth; - break; - case 2: - note.beat.tremoloSpeed = Duration.Sixteenth; - break; - case 3: - note.beat.tremoloSpeed = Duration.ThirtySecond; - break; - } - break; + let keySignature: KeySignature; + if (-7 <= fifths && fifths <= 7) { + keySignature = fifths as KeySignature; + } else { + keySignature = KeySignature.C; + } + let keySignatureType: KeySignatureType; + if (mode === 'minor') { + keySignatureType = KeySignatureType.Minor; + } else { + keySignatureType = KeySignatureType.Major; + } + + if (element.attributes.has('number')) { + const staff = this.getOrCreateStaff(track, Number.parseInt(element.attributes.get('number')!) - 1); + const bar = this.getOrCreateBar(staff, masterBar); + bar.keySignature = keySignature; + bar.keySignatureType = keySignatureType; + } else { + // remember for bars which will be created + this._keyAllStaves = [keySignature, keySignatureType]; + // apply to potentially created bars + for (const s of track.staves) { + if (s.bars.length > masterBar.index) { + s.bars[masterBar.index].keySignature = keySignature; + s.bars[masterBar.index].keySignatureType = keySignatureType; } } } } - private parseTechnical(element: XmlNode, note: Note): void { - let bends: XmlNode[] = []; + private parseDirection(element: XmlNode, masterBar: MasterBar, track: Track) { + const directionTypes: XmlNode[] = []; + let offset: number | null = null; + // let voiceIndex = -1; + let staffIndex = -1; + let tempo = -1; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'string': - note.string = parseInt(c.innerText); - if (note.string !== -2147483648) { - note.string = note.beat.voice.bar.staff.tuning.length - note.string + 1; - } - break; - case 'fret': - note.fret = parseInt(c.innerText); - break; - case 'down-bow': - note.beat.pickStroke = PickStroke.Down; - break; - case 'up-bow': - note.beat.pickStroke = PickStroke.Up; - break; - case 'bend': - bends.push(c); - break; - } + for (const c of element.childElements()) { + switch (c.localName) { + case 'direction-type': + directionTypes.push(c.firstElement!); + break; + case 'offset': + offset = Number.parseFloat(c.innerText); + break; + // case 'footnote': Ignored + // case 'level': Ignored + case 'voice': + // voiceIndex = parseInt(c.innerText) - 1; + break; + case 'staff': + staffIndex = Number.parseInt(c.innerText) - 1; + break; + case 'sound': + if (c.attributes.has('tempo')) { + tempo = Number.parseFloat(c.attributes.get('tempo')!); + } + break; + // case 'listening': Ignored } } - if (bends.length > 0) { - this.parseBends(bends, note); + let staff: Staff | null = null; + if (staffIndex >= 0) { + staff = this.getOrCreateStaff(track, staffIndex); + } else if (this._lastBeat !== null) { + staff = this._lastBeat.voice.bar.staff; + } else { + staff = this.getOrCreateStaff(track, 0); } - if (note.string === -2147483648 || note.fret === -2147483648) { - note.string = -1; - note.fret = -1; - } - } + const bar = staff ? this.getOrCreateBar(staff, masterBar) : null; - private parseBends(elements: XmlNode[], note: Note): void { - let baseOffset: number = BendPoint.MaxPosition / elements.length; - let currentValue: number = 0; // stores the current pitch alter when going through the bends (in 1/4 tones) - let currentOffset: number = 0; // stores the current offset when going through the bends (from 0 to 60) - let isFirstBend: boolean = true; + const getRatioPosition = () => { + let timelyPosition = this._musicalPosition; + if (offset !== null) { + timelyPosition += offset!; + } - for (let bend of elements) { - let bendAlterElement: XmlNode | null = bend.findChildElement('bend-alter'); - if (bendAlterElement) { - let absValue: number = Math.round(Math.abs(parseFloat(bendAlterElement.innerText)) * 2); - if (bend.findChildElement('pre-bend')) { - if (isFirstBend) { - currentValue += absValue; - note.addBendPoint(new BendPoint(currentOffset, currentValue)); - currentOffset += baseOffset; - note.addBendPoint(new BendPoint(currentOffset, currentValue)); - isFirstBend = false; - } else { - currentOffset += baseOffset; - } - } else if (bend.findChildElement('release')) { - if (isFirstBend) { - currentValue += absValue; - } - note.addBendPoint(new BendPoint(currentOffset, currentValue)); - currentOffset += baseOffset; - currentValue -= absValue; - note.addBendPoint(new BendPoint(currentOffset, currentValue)); - isFirstBend = false; - } else { - // "regular" bend - note.addBendPoint(new BendPoint(currentOffset, currentValue)); - currentValue += absValue; - currentOffset += baseOffset; - note.addBendPoint(new BendPoint(currentOffset, currentValue)); - isFirstBend = false; + const totalDuration = masterBar.calculateDuration(false); + return timelyPosition / totalDuration; + }; + + if (tempo > 0) { + const tempoAutomation = new Automation(); + tempoAutomation.type = AutomationType.Tempo; + tempoAutomation.value = tempo; + tempoAutomation.ratioPosition = getRatioPosition(); + + if (!this.hasSameTempo(masterBar, tempoAutomation)) { + masterBar.tempoAutomations.push(tempoAutomation); + if (masterBar.index === 0) { + masterBar.score.tempo = tempoAutomation.value; } } } - } - private parseArticulations(element: XmlNode, note: Note): void { - for (let c of element.childNodes) { - switch (c.localName) { - case 'accent': - note.accentuated = AccentuationType.Normal; + let previousWords: string = ''; + + for (const direction of directionTypes) { + switch (direction.localName) { + case 'rehearsal': + masterBar.section = new Section(); + masterBar.section.marker = direction.innerText; break; - case 'strong-accent': - note.accentuated = AccentuationType.Heavy; + case 'segno': + masterBar.addDirection(Direction.TargetSegno); break; - case 'staccato': - case 'detached-legato': - note.isStaccato = true; + case 'coda': + masterBar.addDirection(Direction.TargetCoda); break; - case 'tenuto': - note.accentuated = AccentuationType.Tenuto; + case 'words': + previousWords = direction.innerText; break; + // case 'symbol': Not supported + case 'wedge': + switch (direction.getAttribute('type')) { + case 'crescendo': + this._nextBeatCrescendo = CrescendoType.Crescendo; + break; + case 'diminuendo': + this._nextBeatCrescendo = CrescendoType.Decrescendo; + break; + // case 'continue': Ignore + case 'stop': + this._nextBeatCrescendo = null; + break; + } + break; + case 'dynamics': + const newDynamics = this.parseDynamics(direction); + if (newDynamics !== null) { + this._currentDynamics = newDynamics; + this._score.stylesheet.hideDynamics = false; + } + break; + case 'dashes': + const type = direction.getAttribute('type', 'start'); + switch (previousWords) { + case 'LetRing': + this._nextBeatLetRing = type === 'start' || type === 'continue'; + break; + case 'P.M.': + this._nextBeatPalmMute = type === 'start' || type === 'continue'; + break; + } + previousWords = ''; + break; + // case 'bracket': Ignored + case 'pedal': + const pedal = this.parsePedal(direction); + if (pedal && bar) { + pedal.ratioPosition = getRatioPosition(); + + // up or holds without a previous down/hold? + const canHaveUp = + bar.sustainPedals.length > 0 && + bar.sustainPedals[bar.sustainPedals.length - 1].pedalType !== SustainPedalMarkerType.Up; + + if (pedal.pedalType !== SustainPedalMarkerType.Up || canHaveUp) { + bar.sustainPedals.push(pedal); + } + } + break; + case 'metronome': + this.parseMetronome(direction, masterBar, getRatioPosition()); + break; + case 'octave-shift': + this._nextBeatOttavia = this.parseOctaveShift(direction); + break; + // case 'harp-pedals': Not supported + // case 'damp': Not supported + // case 'damp-all': Not supported + // case 'eyeglasses': Not supported + // case 'string-mute': Not supported + // case 'scordatura': Not supported + // case 'image': Not supported + // case 'principal-voice': Not supported + // case 'percussion': Not supported + // case 'accordion-registration': Not supported + // case 'staff-divide': Not supported + // case 'other-direction': Not supported } } - } - - private parseDynamics(element: XmlNode, beat: Beat): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'p': - beat.dynamics = DynamicValue.P; - break; - case 'pp': - beat.dynamics = DynamicValue.PP; - break; - case 'ppp': - beat.dynamics = DynamicValue.PPP; - break; - case 'f': - beat.dynamics = DynamicValue.F; - break; - case 'ff': - beat.dynamics = DynamicValue.FF; - break; - case 'fff': - beat.dynamics = DynamicValue.FFF; - break; - case 'mp': - beat.dynamics = DynamicValue.MP; - break; - case 'mf': - beat.dynamics = DynamicValue.MF; - break; - } - } - } - } - private parseTimeModification(element: XmlNode, beat: Beat): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'actual-notes': - beat.tupletNumerator = parseInt(c.innerText); - break; - case 'normal-notes': - beat.tupletDenominator = parseInt(c.innerText); - break; - } - } + if (previousWords) { + this._nextBeatText = previousWords; } } - - private parseUnpitched(element: XmlNode, note: Note): void { - let step: string = ''; - let semitones: number = 0; - let octave: number = 0; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'display-step': - step = c.innerText; - break; - case 'display-alter': - semitones = parseInt(c.innerText); - break; - case 'display-octave': - // 0-9, 4 for middle C - octave = parseInt(c.innerText); - break; + private parseOctaveShift(element: XmlNode): Ottavia | null { + const type = element.getAttribute('type'); + const size = Number.parseInt(element.getAttribute('size', '8')); + + switch (size) { + case 15: + switch (type) { + case 'up': + return Ottavia._15mb; + case 'down': + return Ottavia._15ma; + case 'stop': + return Ottavia.Regular; + case 'continue': + return this._nextBeatOttavia; } - } - } - let value: number = octave * 12 + ModelUtils.getToneForText(step).noteValue + semitones; - note.octave = (value / 12) | 0; - note.tone = value - note.octave * 12; - } - - private parsePitch(element: XmlNode, note: Note): void { - let step: string = ''; - let semitones: number = 0; - let octave: number = 0; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'step': - step = c.innerText; - break; - case 'alter': - semitones = parseFloat(c.innerText); - if (isNaN(semitones)) { - semitones = 0; - } - break; - case 'octave': - // 0-9, 4 for middle C - octave = parseInt(c.innerText) + 1; - break; + break; + case 8: + switch (type) { + case 'up': + return Ottavia._8vb; + case 'down': + return Ottavia._8va; + case 'stop': + return Ottavia.Regular; + case 'continue': + return this._nextBeatOttavia; } - } + break; } - let value: number = octave * 12 + ModelUtils.getToneForText(step).noteValue + (semitones | 0); - note.octave = (value / 12) | 0; - note.tone = value - note.octave * 12; - } - private getOrCreateVoice(bar: Bar, index: number): Voice { - if (index < bar.voices.length) { - return bar.voices[index]; - } - for (let i: number = bar.voices.length; i <= index; i++) { - bar.addVoice(new Voice()); - } - this._maxVoices = Math.max(this._maxVoices, bar.voices.length); - return bar.voices[index]; + return null; } - - private parseDirection(element: XmlNode, masterBar: MasterBar): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'sound': - let tempo: string = c.getAttribute('tempo'); - if (tempo) { - let tempoAutomation: Automation = new Automation(); - tempoAutomation.isLinear = true; - tempoAutomation.type = AutomationType.Tempo; - tempoAutomation.value = parseInt(tempo); - masterBar.tempoAutomations.push(tempoAutomation); - if (masterBar.index === 0) { - masterBar.score.tempo = tempoAutomation.value; - } - } - break; - case 'direction-type': - let directionType: XmlNode = c.firstElement!; - switch (directionType.localName) { - case 'words': - this._currentDirection = directionType.innerText; - break; - case 'metronome': - this.parseMetronome(directionType, masterBar); - break; - } - break; - } + private parseMetronome(element: XmlNode, masterBar: MasterBar, ratioPosition: number) { + let unit: Duration | null = null; + let perMinute: number = -1; + for (const c of element.childElements()) { + switch (c.localName) { + case 'beat-unit': + unit = this.parseBeatDuration(c); + break; + // case 'beat-unit-dot' not supported + // case 'beat-unit-tied' not supported + case 'per-minute': + perMinute = Number.parseFloat(c.innerText); + break; + // case 'metronome-arrows': not supported + // case 'metronome-note': not supported + // case 'metronome-relation': not supported } } - } - private parseMetronome(element: XmlNode, masterBar: MasterBar): void { - let unit: Duration = Duration.Quarter; - let perMinute: number = 120; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'beat-unit': - unit = this.getDuration(c.innerText); - break; - case 'per-minute': - perMinute = parseInt(c.innerText); - break; - } - } - } - let tempoAutomation: Automation = new Automation(); - tempoAutomation.type = AutomationType.Tempo; - tempoAutomation.value = (perMinute * (unit / 4)) | 0; + if (unit !== null && perMinute > 0) { + const tempoAutomation: Automation = new Automation(); + tempoAutomation.type = AutomationType.Tempo; + tempoAutomation.value = (perMinute * (unit / 4)) | 0; + tempoAutomation.ratioPosition = ratioPosition; - if (!this.hasSameTempo(masterBar, tempoAutomation)) { - masterBar.tempoAutomations.push(tempoAutomation); - if (masterBar.index === 0) { - masterBar.score.tempo = tempoAutomation.value; + if (!this.hasSameTempo(masterBar, tempoAutomation)) { + masterBar.tempoAutomations.push(tempoAutomation); + if (masterBar.index === 0) { + masterBar.score.tempo = tempoAutomation.value; + } } } } + private hasSameTempo(masterBar: MasterBar, tempoAutomation: Automation) { for (const existing of masterBar.tempoAutomations) { - if (tempoAutomation.ratioPosition === existing.ratioPosition && tempoAutomation.value == existing.value) { + if (tempoAutomation.ratioPosition === existing.ratioPosition && tempoAutomation.value === existing.value) { return true; } } return false; } - private parseAttributes(element: XmlNode, bars: Bar[], masterBar: MasterBar, track: Track): void { - let num: number = 0; - let hasTime: boolean = false; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'divisions': - this._divisionsPerQuarterNote = parseInt(c.innerText); - break; - case 'key': - this.parseKey(c, masterBar); - break; - case 'time': - this.parseTime(c, masterBar); - hasTime = true; - break; - case 'clef': - num = parseInt(c.getAttribute('number')); - if (isNaN(num)) { - num = 1; - } - this.parseClef(c, bars[num - 1]); - break; - case 'staff-details': - this.parseStaffDetails(c, track); - break; - case 'transpose': - this.parseTranspose(c, track); - break; - } - } - } - if (!hasTime) { - masterBar.timeSignatureCommon = true; + private parsePedal(element: XmlNode): SustainPedalMarker | null { + const marker = new SustainPedalMarker(); + switch (element.getAttribute('type')) { + case 'start': + marker.pedalType = SustainPedalMarkerType.Down; + break; + case 'stop': + marker.pedalType = SustainPedalMarkerType.Up; + break; + // case 'sostenuto': Not supported + // case 'change': Not supported + case 'continue': + marker.pedalType = SustainPedalMarkerType.Hold; + break; + // case 'discontinue': Not supported + // case 'resume': Not supported + default: + return null; } + return marker; } - private parseTranspose(element: XmlNode, track: Track): void { - let semitones: number = 0; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'chromatic': - semitones += parseInt(c.innerText); - break; - case 'octave-change': - semitones += parseInt(c.innerText) * 12; - break; - } + private parseDynamics(element: XmlNode) { + for (const c of element.childElements()) { + // we are having the same enum names as MusicXML uses as tagnames + const dynamicString = c.localName!.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': + return DynamicValue[dynamicString]; + // case 'other-dynamics': not supported } } - for (let staff of track.staves) { - staff.transpositionPitch = semitones; - } + + return null; } - private parseClef(element: XmlNode, bar: Bar): void { - let sign: string = 's'; - let line: number = 0; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'sign': - sign = c.innerText.toLowerCase(); - break; - case 'line': - line = parseInt(c.innerText); - break; - case 'clef-octave-change': - switch (parseInt(c.innerText)) { - case -2: - bar.clefOttava = Ottavia._15mb; - break; - case -1: - bar.clefOttava = Ottavia._8vb; - break; - case 1: - bar.clefOttava = Ottavia._8va; - break; - case 2: - bar.clefOttava = Ottavia._15mb; - break; - } - break; - } + private parseForward(element: XmlNode) { + for (const c of element.childElements()) { + switch (c.localName) { + case 'duration': + this._musicalPosition += this.musicXmlDivisionsToAlphaTabTicks(Number.parseFloat(c.innerText)); + break; + // case 'footnote': Ignored + // case 'level': Ignored + // case 'voice': Not supported, spec is quite vague how to this should behave, we keep it simple for now + // case 'staff': Not supported, spec is quite vague how to this should behave, we keep it simple for now } } - switch (sign) { - case 'g': - bar.clef = Clef.G2; - break; - case 'f': - bar.clef = Clef.F4; - break; - case 'c': - if (line === 3) { - bar.clef = Clef.C3; - } else { - bar.clef = Clef.C4; - } - break; - case 'percussion': - bar.clef = Clef.Neutral; - bar.staff.isPercussion = true; - break; - case 'tab': - bar.clef = Clef.G2; - bar.staff.showTablature = true; - break; - default: - bar.clef = Clef.G2; - break; - } } - private parseTime(element: XmlNode, masterBar: MasterBar): void { - if (element.getAttribute('symbol') === 'common') { - masterBar.timeSignatureCommon = true; - } - let beatsParsed: boolean = false; - let beatTypeParsed: boolean = false; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - let v: string = c.innerText; - switch (c.localName) { - case 'beats': - if (!beatsParsed) { - if (v.indexOf('+') === -1) { - masterBar.timeSignatureNumerator = parseInt(v); - } else { - masterBar.timeSignatureNumerator = 4; - } - beatsParsed = true; - } - break; - case 'beat-type': - if (!beatTypeParsed) { - if (v.indexOf('+') === -1) { - masterBar.timeSignatureDenominator = parseInt(v); - } else { - masterBar.timeSignatureDenominator = 4; - } - beatTypeParsed = true; + private parseBackup(element: XmlNode) { + for (const c of element.childElements()) { + switch (c.localName) { + case 'duration': + const beat = this._lastBeat; + if (beat) { + let musicalPosition = this._musicalPosition; + musicalPosition -= this.musicXmlDivisionsToAlphaTabTicks(Number.parseFloat(c.innerText)); + if (musicalPosition < 0) { + musicalPosition = 0; } - break; - } + this._musicalPosition = musicalPosition; + } + break; + // case 'footnote': Ignored + // case 'level': Ignored } } } - private parseKey(element: XmlNode, masterBar: MasterBar): void { - let fifths: number = -2147483648; - //let keyStep: number = -2147483648; - //let keyAlter: number = -2147483648; - let mode: string = ''; - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'fifths': - fifths = parseInt(c.innerText); - break; - case 'key-step': - //keyStep = parseInt(c.innerText); - break; - case 'key-alter': - //keyAlter = parseInt(c.innerText); - break; - case 'mode': - mode = c.innerText; - break; - } - } - } - if (-7 <= fifths && fifths <= 7) { - // TODO: check if this is conrrect - masterBar.keySignature = fifths as KeySignature; - } else { - masterBar.keySignature = KeySignature.C; - // TODO: map keyStep/keyAlter to internal keysignature - } - if (mode === 'minor') { - masterBar.keySignatureType = KeySignatureType.Minor; - } else { - masterBar.keySignatureType = KeySignatureType.Major; - } - } + private getOrCreateStaff(track: Track, staffIndex: number): Staff { + while (track.staves.length <= staffIndex) { + const staff = new Staff(); + track.addStaff(staff); - private getOrCreateMasterBar(index: number): MasterBar { - if (index < this._score.masterBars.length) { - return this._score.masterBars[index]; - } - for (let i: number = this._score.masterBars.length; i <= index; i++) { - let mb: MasterBar = new MasterBar(); + // ensure bars on new staff if (this._score.masterBars.length > 0) { - let prev: MasterBar = this._score.masterBars[this._score.masterBars.length - 1]; - mb.timeSignatureDenominator = prev.timeSignatureDenominator; - mb.timeSignatureNumerator = prev.timeSignatureNumerator; - mb.keySignature = prev.keySignature; - mb.keySignatureType = prev.keySignatureType; + this.getOrCreateBar(staff, this._score.masterBars[this._score.masterBars.length - 1]); } - this._score.addMasterBar(mb); } - return this._score.masterBars[index]; + + return track.staves[staffIndex]; } - private parseIdentification(element: XmlNode): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'creator': - if (c.getAttribute('type') === 'composer') { - this._score.music = c.innerText; - } - break; - case 'rights': - if (this._score.copyright) { - this._score.copyright += '\n'; - } - this._score.copyright += c.innerText; - break; - } + private getOrCreateBar(staff: Staff, masterBar: MasterBar): Bar { + const voiceCount = staff.bars.length === 0 ? 1 : staff.bars[0].voices.length; + + while (staff.bars.length <= masterBar.index) { + const newBar = 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; } - } - } - private parsePartList(element: XmlNode): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'part-group': - this.parsePartGroup(c); - break; - case 'score-part': - this.parseScorePart(c); - break; - } + if (this._keyAllStaves != null) { + newBar.keySignature = this._keyAllStaves![0]; + newBar.keySignatureType = this._keyAllStaves![1]; } - } - } - private parsePartGroup(element: XmlNode): void { - let type: string = element.getAttribute('type'); - switch (type) { - case 'start': - this._currentPartGroup = element.getAttribute('number'); - this._partGroups.set(this._currentPartGroup, []); - break; - case 'stop': - this._currentPartGroup = null; - break; + for (let i = 0; i < voiceCount; i++) { + const voice: Voice = new Voice(); + newBar.addVoice(voice); + } } + + return staff.bars[masterBar.index]; } - private parseScorePart(element: XmlNode): void { - let id: string = element.getAttribute('id'); - let track: Track = new Track(); - track.ensureStaveCount(1); - let staff: Staff = track.staves[0]; - staff.showStandardNotation = true; - this._trackById.set(id, track); - this._score.addTrack(track); - if (this._currentPartGroup) { - this._partGroups.get(this._currentPartGroup)!.push(track); + private getOrCreateVoice(bar: Bar, voiceIndex: number): Voice { + let voicesCreated = false; + while (bar.voices.length <= voiceIndex) { + bar.addVoice(new Voice()); + voicesCreated = true; } - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'part-name': - track.name = c.innerText; - break; - case 'part-abbreviation': - track.shortName = c.innerText; - break; - case 'midi-instrument': - this.parseMidiInstrument(c, track); - break; + + // ensure voices on all bars + if (voicesCreated) { + for (const b of bar.staff.bars) { + while (b.voices.length <= voiceIndex) { + b.addVoice(new Voice()); } } } - if (this.isEmptyTuning(track.staves[0].tuning)) { - track.staves[0].stringTuning.tunings = []; - } + + return bar.voices[voiceIndex]; } - private isEmptyTuning(tuning: number[]): boolean { - if (!tuning) { - return true; - } - for (let i: number = 0; i < tuning.length; i++) { - if (tuning[i] !== 0) { - return false; + private parseNote(element: XmlNode, masterBar: MasterBar, track: Track) { + // Beat level information + let beat: Beat | null = null; + let graceType = GraceType.None; + let graceDurationInDivisions = 0; + let beamMode: BeatBeamingMode | null = null; + // let graceTimeStealPrevious = 0; + // let graceTimeStealFollowing = 0; + + let isChord = false; + + let staffIndex = 0; + let voiceIndex = 0; + + let durationInTicks = -1; + let beatDuration: Duration | null = null; + let dots = 0; + + let tupletNumerator = -1; + let tupletDenominator = -1; + + let preferredBeamDirection: BeamDirection | null = null; + + // Note level + let note: Note | null = null; + let isPitched = false; + let instrumentId: string | null = null; + const noteIsVisible = element.getAttribute('print-object', 'yes') !== 'no'; + + // will create new beat with all information in the correct tree + // or add the note to an existing beat if specified accordingly. + const ensureBeat = () => { + if (beat !== null) { + return; } - } - return true; - } - private parseMidiInstrument(element: XmlNode, track: Track): void { - for (let c of element.childNodes) { - if (c.nodeType === XmlNodeType.Element) { - switch (c.localName) { - case 'midi-channel': - track.playbackInfo.primaryChannel = parseInt(c.innerText); - break; - case 'midi-program': - track.playbackInfo.program = parseInt(c.innerText); - break; - case 'volume': - track.playbackInfo.volume = Math.floor((parseInt(c.innerText) / 100) * 16); - break; - case 'pan': - track.playbackInfo.balance = Math.max( - 0, - Math.min(16, Math.floor(((parseInt(c.innerText) + 90) / 180) * 16)) - ); - break; - } + if (isChord && !this._lastBeat) { + Logger.warning( + 'MusicXML', + 'Malformed MusicXML, cannot be set on the first note of a measure' + ); + isChord = false; + } + + if (isChord && !note) { + Logger.warning('MusicXML', 'Cannot mix and '); + isChord = false; + } + + const staff = this.getOrCreateStaff(track, staffIndex); + if (isChord) { + beat = this._lastBeat!; + beat!.addNote(note!); + return; + } + + const bar = this.getOrCreateBar(staff, masterBar); + const voice = this.getOrCreateVoice(bar, voiceIndex); + + const actualMusicalPosition = voice.beats.length === 0 ? 0 : voice.beats[voice.beats.length - 1].displayEnd; + + let gap = this._musicalPosition - actualMusicalPosition; + if (gap > 0) { + // we do not support cross staff beams yet and its a bigger thing to implement + // until then we try to detect whether we have a beam-group + // which starts at this staff, swaps to another, and comes back. + // then we create matching rests here + + if ( + // Previously created beat has forced beams and is on another stuff + this._lastBeat && + this._lastBeat.beamingMode === BeatBeamingMode.ForceMergeWithNext && + this._lastBeat.voice.bar.staff.index !== staffIndex && + // previous beat on this staff is also forced + voice.beats.length > 0 && + voice.beats[voice.beats.length - 1].beamingMode === BeatBeamingMode.ForceMergeWithNext + ) { + // chances are high that we have notes like this + // staff1Note -> staff2Note -> staff2Note -> staff1Note + // in this case we create rests for the gap caused by the staff2Notes + const preferredDuration = voice.beats[voice.beats.length - 1].duration; + while (gap > 0) { + const restGap = this.createRestForGap(gap, preferredDuration); + if (restGap !== null) { + this.insertBeatToVoice(restGap, voice); + gap -= restGap.playbackDuration; + } else { + break; + } + } + } + + // need an empty placeholder beat for the gap + if (gap > 0) { + const placeholder = new Beat(); + placeholder.dynamics = this._currentDynamics; + placeholder.isEmpty = true; + placeholder.duration = Duration.TwoHundredFiftySixth; // smallest we have + placeholder.overrideDisplayDuration = gap; + placeholder.updateDurations(); + this.insertBeatToVoice(placeholder, voice); + } + } else if (gap < 0) { + Logger.error( + 'MusicXML', + 'Unsupported forward/backup detected. Cannot fill new beats into already filled area of voice' + ); + } + + if (durationInTicks < 0 && beatDuration !== null) { + durationInTicks = MidiUtils.toTicks(beatDuration!); + if (dots > 0) { + durationInTicks = MidiUtils.applyDot(durationInTicks, dots === 2); + } + } + + const newBeat = new Beat(); + beat = newBeat; + if (beamMode === null) { + newBeat.beamingMode = this.getStaffContext(staff).isExplicitlyBeamed + ? BeatBeamingMode.ForceSplitToNext + : BeatBeamingMode.Auto; + } else { + newBeat.beamingMode = beamMode; + this.getStaffContext(staff).isExplicitlyBeamed = true; + } + newBeat.isEmpty = false; + newBeat.dynamics = this._currentDynamics; + if (this._isBeatSlash) { + newBeat.slashed = true; + } + + const automations = this._nextBeatAutomations; + this._nextBeatAutomations = null; + if (automations !== null) { + for (const automation of automations) { + newBeat.automations.push(automation); + } + } + + const chord = this._nextBeatChord; + this._nextBeatChord = null; + if (chord !== null) { + newBeat.chordId = chord.uniqueId; + if (!voice.bar.staff.hasChord(chord.uniqueId)) { + voice.bar.staff.addChord(newBeat.chordId!, chord); + } + } + + const crescendo = this._nextBeatCrescendo; + // Don't reset until 'stop' this._nextBeatCrescendo = null; + if (crescendo !== null) { + newBeat.crescendo = crescendo; + } + + const ottavia = this._nextBeatOttavia; + // Don't set until 'stop' + if (ottavia !== null) { + newBeat.ottava = ottavia; + } + + newBeat.isLetRing = this._nextBeatLetRing; + newBeat.isPalmMute = this._nextBeatPalmMute; + if (this._nextBeatText) { + newBeat.text = this._nextBeatText; + this._nextBeatText = null; + } + + if (note !== null) { + newBeat.addNote(note!); + } + + this.insertBeatToVoice(newBeat, voice); + + if (note !== null) { + note!.isVisible = noteIsVisible; + const trackInfo = this._indexToTrackInfo.get(track.index)!; + if (instrumentId !== null) { + note!.percussionArticulation = trackInfo.getOrCreateArticulation(instrumentId!, note!); + } else if (!isPitched) { + note!.percussionArticulation = trackInfo.getOrCreateArticulation('', note!); + } + } + + // duration only after we added it into the tree + if (graceType !== GraceType.None) { + newBeat.graceType = graceType; + this.applyBeatDurationFromTicks(newBeat, graceDurationInDivisions, null, false); + } else { + newBeat.tupletNumerator = tupletNumerator; + newBeat.tupletDenominator = tupletDenominator; + newBeat.dots = dots; + newBeat.preferredBeamDirection = preferredBeamDirection; + this.applyBeatDurationFromTicks(newBeat, durationInTicks, beatDuration, true); + } + + this._musicalPosition = newBeat.displayEnd; + this._lastBeat = newBeat; + }; + + for (const c of element.childElements()) { + switch (c.localName) { + case 'grace': + const makeTime = Number.parseFloat(c.getAttribute('make-time', '-1')); + if (makeTime >= 0) { + graceDurationInDivisions = this.musicXmlDivisionsToAlphaTabTicks(makeTime); + graceType = GraceType.BeforeBeat; + } else { + graceType = GraceType.OnBeat; + } + + if (c.getAttribute('slash') === 'yes') { + graceType = GraceType.BeforeBeat; + } + + // graceTimeStealPrevious = parseInt(c.getAttribute('steal-time-following', '0')) / 100.0; + // graceTimeStealFollowing = parseInt(c.getAttribute('steal-time-previous', '0')) / 100.0; + break; + + case 'chord': + isChord = true; + break; + + case 'cue': + // not supported + // as they are meant to not be played, we skip them completely + // instead of handling them wrong. + return; + + case 'pitch': + note = this.parsePitch(c); + isPitched = true; + break; + case 'unpitched': + note = this.parseUnpitched(c, track); + break; + case 'rest': + note = null; // rest beat + if (beatDuration === null) { + beatDuration = Duration.Whole; + } + break; + + case 'duration': + durationInTicks = this.parseDuration(c); + break; + // case 'tie': Ignored -> "tie" is sound, "tied" is notation + case 'instrument': + instrumentId = c.getAttribute('id', ''); + break; + + // case 'footnote': Ignored + // case 'level': Ignored + case 'voice': + voiceIndex = Number.parseInt(c.innerText); + if (Number.isNaN(voiceIndex)) { + Logger.warning('MusicXML', 'Voices need to be specified as numbers'); + voiceIndex = 0; + } else { + voiceIndex = voiceIndex - 1; + } + break; + case 'type': + beatDuration = this.parseBeatDuration(c); + break; + case 'dot': + dots++; + break; + case 'accidental': + if (note === null) { + Logger.warning('MusicXML', 'Malformed MusicXML, missing pitch or unpitched for note'); + } else { + this.parseAccidental(c, note); + } + break; + case 'time-modification': + for (const tmc of c.childElements()) { + switch (tmc.localName) { + case 'actual-notes': + tupletNumerator = Number.parseInt(tmc.innerText); + break; + case 'normal-notes': + tupletDenominator = Number.parseInt(tmc.innerText); + break; + // case 'normal-type': not supported + // case 'normal-dot': not supported + } + } + break; + case 'stem': + preferredBeamDirection = this.parseStem(c); + break; + case 'notehead': + if (note === null) { + Logger.warning('MusicXML', 'Malformed MusicXML, missing pitch or unpitched for note'); + } else { + this.parseNoteHead( + c, + note, + beatDuration ?? Duration.Quarter, + preferredBeamDirection ?? this.estimateBeamDirection(note) + ); + } + break; + // case 'notehead-text': Not supported + case 'staff': + staffIndex = Number.parseInt(c.innerText) - 1; + break; + case 'beam': + // use the first beam as indicator whether to beam or split + if (c.getAttribute('number', '1') === '1') { + switch (c.innerText) { + case 'begin': + beamMode = BeatBeamingMode.ForceMergeWithNext; + break; + case 'continue': + beamMode = BeatBeamingMode.ForceMergeWithNext; + break; + case 'end': + beamMode = BeatBeamingMode.ForceSplitToNext; + break; + } + } + break; + case 'notations': + ensureBeat(); + this.parseNotations(c, note, beat!); + break; + case 'lyric': + ensureBeat(); + this.parseLyric(c, beat!, track); + break; + case 'play': + this.parsePlay(c, note); + break; + // case 'listen': Ignored + } + } + + if (isPitched) { + const staff = this.getOrCreateStaff(track, staffIndex); + const transpose = this.getStaffContext(staff).transpose; + if (transpose !== 0) { + const value = note!.octave * 12 + note!.tone + transpose; + note!.octave = (value / 12) | 0; + note!.tone = value - note!.octave * 12; + } + } + + // if not yet created do it befor we exit to ensure we created the beat/note + ensureBeat(); + } + + private parsePlay(element: XmlNode, note: Note | null) { + for (const c of element.childElements()) { + switch (c.localName) { + // case 'ipa': Ignored + case 'mute': + if(note && c.innerText === 'palm') { + note.isPalmMute = true; + } + break; + case 'semi-pitched': + break; + // case 'other-play': Ignored + } + } + } + + private static readonly B4Value = 71; + 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) { + if (element.getAttribute('parentheses', 'no') === 'yes') { + note.isGhost = true; + } + + const filled = element.getAttribute('filled', ''); + let forceFill: boolean | undefined = undefined; + if (filled === 'yes') { + forceFill = true; + } else if (filled === 'no') { + forceFill = false; + } + + note.style = new NoteStyle(); + switch (element.innerText) { + case 'arrow down': + note.style!.noteHeadCenterOnStem = true; + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadTriangleDownDoubleWhole, + MusicFontSymbol.NoteheadTriangleDownWhole, + MusicFontSymbol.NoteheadTriangleDownHalf, + MusicFontSymbol.NoteheadTriangleDownBlack + ); + break; + case 'arrow up': + note.style!.noteHeadCenterOnStem = true; + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadTriangleUpDoubleWhole, + MusicFontSymbol.NoteheadTriangleUpWhole, + MusicFontSymbol.NoteheadTriangleUpHalf, + MusicFontSymbol.NoteheadTriangleUpBlack + ); + break; + case 'back slashed': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadSlashedDoubleWhole2, + MusicFontSymbol.NoteheadSlashedWhole2, + MusicFontSymbol.NoteheadSlashedHalf2, + MusicFontSymbol.NoteheadSlashedBlack2 + ); + break; + case 'circle dot': + note.style.noteHead = MusicFontSymbol.NoteheadRoundWhiteWithDot; + break; + case 'circle-x': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadCircleXDoubleWhole, + MusicFontSymbol.NoteheadCircleXWhole, + MusicFontSymbol.NoteheadCircleXHalf, + MusicFontSymbol.NoteheadCircleX + ); + break; + case 'circled': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadCircledDoubleWhole, + MusicFontSymbol.NoteheadCircledWhole, + MusicFontSymbol.NoteheadCircledHalf, + MusicFontSymbol.NoteheadCircledBlack + ); + break; + case 'cluster': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadClusterDoubleWhole3rd, + MusicFontSymbol.NoteheadClusterWhole3rd, + MusicFontSymbol.NoteheadClusterHalf3rd, + MusicFontSymbol.NoteheadClusterQuarter3rd + ); + break; + case 'cross': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadPlusDoubleWhole, + MusicFontSymbol.NoteheadPlusWhole, + MusicFontSymbol.NoteheadPlusHalf, + MusicFontSymbol.NoteheadPlusBlack + ); + break; + case 'diamond': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadDiamondDoubleWhole, + MusicFontSymbol.NoteheadDiamondWhole, + MusicFontSymbol.NoteheadDiamondHalf, + MusicFontSymbol.NoteheadDiamondBlack + ); + break; + case 'do': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteShapeTriangleUpWhite, + MusicFontSymbol.NoteShapeTriangleUpWhite, + MusicFontSymbol.NoteShapeTriangleUpWhite, + MusicFontSymbol.NoteShapeTriangleUpBlack + ); + break; + case 'fa': + if (beamDirection === BeamDirection.Up) { + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteShapeTriangleRightWhite, + MusicFontSymbol.NoteShapeTriangleRightWhite, + MusicFontSymbol.NoteShapeTriangleRightWhite, + MusicFontSymbol.NoteShapeTriangleRightBlack + ); + } else { + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteShapeTriangleLeftWhite, + MusicFontSymbol.NoteShapeTriangleLeftWhite, + MusicFontSymbol.NoteShapeTriangleLeftWhite, + MusicFontSymbol.NoteShapeTriangleLeftBlack + ); + } + break; + case 'fa up': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteShapeTriangleLeftWhite, + MusicFontSymbol.NoteShapeTriangleLeftWhite, + MusicFontSymbol.NoteShapeTriangleLeftWhite, + MusicFontSymbol.NoteShapeTriangleLeftBlack + ); + break; + case 'inverted triangle': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadTriangleDownDoubleWhole, + MusicFontSymbol.NoteheadTriangleDownWhole, + MusicFontSymbol.NoteheadTriangleDownHalf, + MusicFontSymbol.NoteheadTriangleDownBlack + ); + break; + case 'la': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteShapeSquareWhite, + MusicFontSymbol.NoteShapeSquareWhite, + MusicFontSymbol.NoteShapeSquareWhite, + MusicFontSymbol.NoteShapeSquareBlack + ); + break; + case 'left triangle': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadTriangleRightWhite, + MusicFontSymbol.NoteheadTriangleRightWhite, + MusicFontSymbol.NoteheadTriangleRightWhite, + MusicFontSymbol.NoteheadTriangleRightBlack + ); + break; + case 'mi': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteShapeDiamondWhite, + MusicFontSymbol.NoteShapeDiamondWhite, + MusicFontSymbol.NoteShapeDiamondWhite, + MusicFontSymbol.NoteShapeDiamondBlack + ); + break; + case 'none': + note.style!.noteHead = MusicFontSymbol.NoteheadNull; + break; + case 'normal': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadDoubleWhole, + MusicFontSymbol.NoteheadWhole, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadBlack + ); + break; + case 're': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteShapeMoonWhite, + MusicFontSymbol.NoteShapeMoonWhite, + MusicFontSymbol.NoteShapeMoonWhite, + MusicFontSymbol.NoteShapeMoonBlack + ); + break; + case 'rectangle': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadSquareWhite, + MusicFontSymbol.NoteheadSquareWhite, + MusicFontSymbol.NoteheadSquareWhite, + MusicFontSymbol.NoteheadSquareBlack + ); + break; + case 'slash': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadSlashWhiteWhole, + MusicFontSymbol.NoteheadSlashWhiteWhole, + MusicFontSymbol.NoteheadSlashWhiteHalf, + MusicFontSymbol.NoteheadSlashVerticalEnds + ); + break; + case 'slashed': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadSlashedDoubleWhole1, + MusicFontSymbol.NoteheadSlashedWhole1, + MusicFontSymbol.NoteheadSlashedHalf1, + MusicFontSymbol.NoteheadSlashedBlack1 + ); + break; + case 'so': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteShapeRoundWhite, + MusicFontSymbol.NoteShapeRoundWhite, + MusicFontSymbol.NoteShapeRoundWhite, + MusicFontSymbol.NoteShapeRoundBlack + ); + break; + case 'square': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteShapeSquareWhite, + MusicFontSymbol.NoteShapeSquareWhite, + MusicFontSymbol.NoteShapeSquareWhite, + MusicFontSymbol.NoteShapeSquareBlack + ); + break; + case 'ti': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteShapeTriangleRoundWhite, + MusicFontSymbol.NoteShapeTriangleRoundWhite, + MusicFontSymbol.NoteShapeTriangleRoundWhite, + MusicFontSymbol.NoteShapeTriangleRoundBlack + ); + break; + case 'triangle': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadTriangleUpDoubleWhole, + MusicFontSymbol.NoteheadTriangleUpWhole, + MusicFontSymbol.NoteheadTriangleUpHalf, + MusicFontSymbol.NoteheadTriangleUpBlack + ); + break; + case 'x': + this.applyNoteHead( + note, + beatDuration, + forceFill, + MusicFontSymbol.NoteheadXDoubleWhole, + MusicFontSymbol.NoteheadXWhole, + MusicFontSymbol.NoteheadXHalf, + MusicFontSymbol.NoteheadXBlack + ); + break; + } + } + + private createRestForGap(gap: number, preferredDuration: Duration): Beat | null { + let preferredDurationTicks = MidiUtils.toTicks(preferredDuration); + + // shorten the beat duration until we fit + while (preferredDurationTicks > gap) { + if (preferredDuration === Duration.TwoHundredFiftySixth) { + return null; // cannot get shorter + } + + preferredDuration = (preferredDuration * 2) as Duration; + preferredDurationTicks = MidiUtils.toTicks(preferredDuration); + } + + const placeholder = new Beat(); + placeholder.dynamics = this._currentDynamics; + placeholder.isEmpty = false; + placeholder.duration = preferredDuration; + placeholder.overrideDisplayDuration = preferredDurationTicks; + placeholder.updateDurations(); + return placeholder; + } + + private insertBeatToVoice(newBeat: Beat, voice: Voice) { + // for handling the correct musical position we already need to do some basic beat linking + // and assignments of start/durations as we progress. + + if (voice.beats.length > 0) { + const lastBeat = voice.beats[voice.beats.length - 1]; + + // chain beats already + lastBeat.nextBeat = newBeat; + newBeat.previousBeat = lastBeat!; + + // find display start from previous non-grace beat, + // reminder: we give grace a display position of 0, that's why we skip them. + // visually they 'stick' to their next beat. + let previousNonGraceBeat: Beat | null = lastBeat; + while (previousNonGraceBeat !== null) { + if (previousNonGraceBeat.graceType === GraceType.None) { + // found + break; + } + + if (previousNonGraceBeat.index > 0) { + previousNonGraceBeat = previousNonGraceBeat.previousBeat; + } else { + previousNonGraceBeat = null; + } + } + + if (previousNonGraceBeat !== null) { + newBeat.displayStart = previousNonGraceBeat.displayEnd; + } + } + + voice.addBeat(newBeat); + } + + private musicXmlDivisionsToAlphaTabTicks(divisions: number): number { + // we translate the Divisions-per-quarter-note of the MusicXML to our fixed MidiUtils.QuarterTime + + return (divisions * MidiUtils.QuarterTime) / this._divisionsPerQuarterNote; + } + + private parseBeatDuration(element: XmlNode): Duration | null { + switch (element.innerText) { + case '1024th': // not supported + return Duration.TwoHundredFiftySixth; + case '512th': // not supported + return Duration.TwoHundredFiftySixth; + case '256th': + return Duration.TwoHundredFiftySixth; + case '128th': + return Duration.OneHundredTwentyEighth; + case '64th': + return Duration.SixtyFourth; + case '32nd': + return Duration.ThirtySecond; + case '16th': + return Duration.Sixteenth; + case 'eighth': + return Duration.Eighth; + case 'quarter': + return Duration.Quarter; + case 'half': + return Duration.Half; + case 'whole': + return Duration.Whole; + case 'breve': + return Duration.DoubleWhole; + case 'long': + return Duration.QuadrupleWhole; + // case "maxima": not supported + } + + return null; + } + + private static allDurations = [ + Duration.TwoHundredFiftySixth, + Duration.OneHundredTwentyEighth, + Duration.SixtyFourth, + Duration.ThirtySecond, + Duration.Sixteenth, + Duration.Eighth, + Duration.Quarter, + Duration.Half, + Duration.Whole, + Duration.DoubleWhole, + Duration.QuadrupleWhole + ]; + + private static allDurationTicks = MusicXmlImporter.allDurations.map(d => MidiUtils.toTicks(d)); + + private applyBeatDurationFromTicks( + newBeat: Beat, + ticks: number, + beatDuration: Duration | null, + applyDisplayDuration: boolean + ) { + if (!beatDuration) { + for (let i = 0; i < MusicXmlImporter.allDurations.length; i++) { + const dt = MusicXmlImporter.allDurationTicks[i]; + if (ticks >= dt) { + beatDuration = MusicXmlImporter.allDurations[i]; + } else { + break; + } + } + } + + newBeat.duration = beatDuration ?? Duration.Sixteenth; + if (applyDisplayDuration) { + newBeat.overrideDisplayDuration = ticks; + } + + newBeat.updateDurations(); + } + + private parseLyric(element: XmlNode, beat: Beat, track: Track) { + const info = this._indexToTrackInfo.get(track.index)!; + const index = info.getLyricLine(element.getAttribute('number', '')); + if (beat.lyrics === null) { + beat.lyrics = []; + } + while (beat.lyrics.length <= index) { + beat.lyrics.push(''); + } + + for (const c of element.childElements()) { + switch (c.localName) { + // case 'syllabic' not supported + case 'text': + if (beat.lyrics[index]) { + beat.lyrics[index] += ` ${c.innerText}`; + } else { + beat.lyrics[index] = c.innerText; + } + break; + case 'elision': + beat.lyrics[index] += c.innerText; + break; + } + } + } + + private parseNotations(element: XmlNode, note: Note | null, beat: Beat) { + for (const c of element.childElements()) { + switch (c.localName) { + // case 'footnote': Ignored + // case 'level': Ignored + case 'tied': + if (note) { + this.parseTied(c, note, beat.voice.bar.staff); + } + break; + case 'slur': + if (note) { + this.parseSlur(c, note); + } + break; + // case 'tuplet': Handled via time-modification + case 'glissando': + if (note) { + this.parseGlissando(c, note); + } + break; + case 'slide': + if (note) { + this.parseSlide(c, note); + } + break; + case 'ornaments': + if (note) { + this.parseOrnaments(c, note); + } + break; + case 'technical': + this.parseTechnical(c, note, beat); + break; + case 'articulations': + if (note) { + this.parseArticulations(c, note); + } + break; + case 'dynamics': + const dynamics = this.parseDynamics(c); + if (dynamics !== null) { + beat.dynamics = dynamics; + this._currentDynamics = dynamics; + } + break; + case 'fermata': + this.parseFermata(c, beat); + break; + case 'arpeggiate': + this.parseArpeggiate(c, beat); + break; + // case 'non-arpeggiate': Not supported + // case 'accidental-mark': Not supported + // case 'other-notation': Not supported + } + } + } + + private getStaffContext(staff: Staff) { + if (!this._staffToContext.has(staff)) { + const context = new StaffContext(); + this._staffToContext.set(staff, context); + return context; + } + return this._staffToContext.get(staff)!; + } + + private parseGlissando(element: XmlNode, note: Note) { + const type = element.getAttribute('type'); + const number = element.getAttribute('number', '1'); + + const context = this.getStaffContext(note.beat.voice.bar.staff); + + switch (type) { + case 'start': + context.slideOrigins.set(number, note); + break; + case 'stop': + if (context.slideOrigins.has(number)) { + const origin = context.slideOrigins.get(number)!; + origin.slideTarget = note; + note.slideOrigin = origin; + origin.slideOutType = SlideOutType.Shift; // TODO: wavy lines + } + break; + } + } + + private parseSlur(element: XmlNode, note: Note) { + const slurNumber: string = element.getAttribute('number', '1'); + + const context = this.getStaffContext(note.beat.voice.bar.staff); + + switch (element.getAttribute('type')) { + case 'start': + context.slurStarts.set(slurNumber, note); + break; + case 'stop': + if (context.slurStarts.has(slurNumber)) { + note.isSlurDestination = true; + const slurStart = context.slurStarts.get(slurNumber)!; + slurStart.slurDestination = note; + note.slurOrigin = slurStart; + + context.slurStarts.delete(slurNumber); + } + break; + } + } + + private parseArpeggiate(element: XmlNode, beat: Beat) { + const direction = element.getAttribute('direction', 'down'); + switch (direction) { + case 'down': + beat.brushType = BrushType.ArpeggioDown; + break; + case 'up': + beat.brushType = BrushType.ArpeggioUp; + break; + } + } + + private parseFermata(element: XmlNode, beat: Beat) { + let fermata: FermataType; + switch (element.innerText) { + case 'normal': + fermata = FermataType.Medium; + break; + case 'angled': + fermata = FermataType.Short; + break; + case 'square': + fermata = FermataType.Long; + break; + // case 'double-angled': Not Supported + // case 'double-square': Not Supported + // case 'double-dot': Not Supported + // case 'half-curve': Not Supported + // case 'curlew': Not Supported + default: + fermata = FermataType.Medium; + break; + } + + beat.fermata = new Fermata(); + beat.fermata.type = fermata; + } + + private parseArticulations(element: XmlNode, note: Note) { + for (const c of element.childElements()) { + switch (c.localName) { + case 'accent': + note.accentuated = AccentuationType.Normal; + break; + case 'strong-accent': + note.accentuated = AccentuationType.Heavy; + break; + case 'staccato': + note.isStaccato = true; + break; + case 'tenuto': + note.accentuated = AccentuationType.Tenuto; + break; + // case 'detached-legato': Not Supported + // case 'staccatissimo': Not Supported + // case 'spiccato': Not Supported + // case 'scoop': Not Supported + // case 'plop': Not Supported + // case 'doit': Not Supported + // case 'falloff': Not Supported + // case 'breath-mark': Not Supported + // case 'caesura': Not Supported + // case 'stress': Not Supported + // case 'unstress': Not Supported + // case 'soft-accent': Not Supported + // case 'other-articulation': Not Supported + } + } + } + private parseTechnical(element: XmlNode, note: Note | null, beat: Beat) { + const bends: XmlNode[] = []; + + for (const c of element.childElements()) { + switch (c.localName) { + case 'up-bow': + beat.pickStroke = PickStroke.Up; + break; + case 'down-bow': + beat.pickStroke = PickStroke.Down; + break; + case 'harmonic': + break; + // case 'open-string': Not supported + // case 'thumb-position': Not supported + case 'fingering': + if (note) { + note.leftHandFinger = this.parseFingering(c); + } + break; + case 'pluck': + if (note) { + note.rightHandFinger = this.parseFingering(c); + } + break; + // case 'double-tongue': Not supported + // case 'triple-tongue': Not supported + // case 'stopped': Not supported + // case 'snap-pizzicato': Not supported + case 'fret': + if (note) { + note.fret = Number.parseInt(c.innerText); + } + break; + case 'string': + if (note) { + note.string = beat.voice.bar.staff.tuning.length - Number.parseInt(c.innerText) + 1; + } + break; + case 'hammer-on': + case 'pull-off': + if (note) { + note.isHammerPullOrigin = true; + } + break; + case 'bend': + bends.push(c); + break; + case 'tap': + beat.tap = true; + break; + // case 'heel': Not supported + // case 'toe': Not supported + // case 'fingernails': Not supported + // case 'hole': Not supported + // case 'arrow': Not supported + // case 'handbell': Not supported + // case 'brass-bend': Not supported + // case 'flip': Not supported + case 'smear': + if (note) { + note.vibrato = VibratoType.Slight; + } + break; + // case 'open': Not supported + // case 'half-muted': Not supported + // case 'harmon-mute': Not supported + case 'golpe': + switch (c.getAttribute('placement', 'above')) { + case 'above': + beat.golpe = GolpeType.Finger; + break; + case 'below': + beat.golpe = GolpeType.Thumb; + break; + } + break; + // case 'other-technical': Not supported + } + } + + if (note && bends.length > 0) { + this.parseBends(bends, note); + } + } + + private parseBends(elements: XmlNode[], note: Note): void { + const baseOffset: number = BendPoint.MaxPosition / elements.length; + let currentValue: number = 0; // stores the current pitch alter when going through the bends (in 1/4 tones) + let currentOffset: number = 0; // stores the current offset when going through the bends (from 0 to 60) + let isFirstBend: boolean = true; + + for (const bend of elements) { + const bendAlterElement: XmlNode | null = bend.findChildElement('bend-alter'); + if (bendAlterElement) { + const absValue: number = Math.round(Math.abs(Number.parseFloat(bendAlterElement.innerText)) * 2); + if (bend.findChildElement('pre-bend')) { + if (isFirstBend) { + currentValue += absValue; + note.addBendPoint(new BendPoint(currentOffset, currentValue)); + currentOffset += baseOffset; + note.addBendPoint(new BendPoint(currentOffset, currentValue)); + isFirstBend = false; + } else { + currentOffset += baseOffset; + } + } else if (bend.findChildElement('release')) { + if (isFirstBend) { + currentValue += absValue; + } + note.addBendPoint(new BendPoint(currentOffset, currentValue)); + currentOffset += baseOffset; + currentValue -= absValue; + note.addBendPoint(new BendPoint(currentOffset, currentValue)); + isFirstBend = false; + } else { + // "regular" bend + note.addBendPoint(new BendPoint(currentOffset, currentValue)); + currentValue += absValue; + currentOffset += baseOffset; + note.addBendPoint(new BendPoint(currentOffset, currentValue)); + isFirstBend = false; + } + } + } + } + + private parseFingering(c: XmlNode): Fingers { + switch (c.innerText) { + case '0': + return Fingers.NoOrDead; + case '1': + case 'p': + case 't': + return Fingers.Thumb; + case '2': + case 'i': + return Fingers.IndexFinger; + case '3': + case 'm': + return Fingers.MiddleFinger; + case '4': + case 'a': + return Fingers.AnnularFinger; + case '5': + case 'c': + return Fingers.LittleFinger; + } + + return Fingers.Unknown; + } + + private _currentTrillStep: number = -1; + + private parseOrnaments(element: XmlNode, note: Note): void { + let currentTrillStep = -1; + for (const c of element.childElements()) { + switch (c.localName) { + case 'trill-mark': + currentTrillStep = Number.parseInt(c.getAttribute('trill-step', '2')); + if (note.isStringed) { + note.trillValue = note.stringTuning + currentTrillStep; + } else if (!note.isPercussion) { + note.trillValue = note.calculateRealValue(false, false) + currentTrillStep; + } + break; + case 'turn': + note.ornament = NoteOrnament.Turn; + break; + // case 'delayed-turn': Not supported + case 'inverted-turn': + note.ornament = NoteOrnament.InvertedTurn; + break; + // case 'delayed-inverted-turn': Not supported + // case 'vertical-turn': Not supported + // case 'inverted-vertical-turn': Not supported + // case 'shake': Not supported + case 'wavy-line': + if (currentTrillStep > 0) { + if (c.getAttribute('type') === 'start') { + this._currentTrillStep = currentTrillStep; + } + } else if (this._currentTrillStep > 0) { + if (c.getAttribute('type') === 'stop') { + this._currentTrillStep = -1; + } else if (note.isStringed) { + note.trillValue = note.stringTuning + this._currentTrillStep; + } else if (!note.isPercussion) { + note.trillValue = note.calculateRealValue(false, false) + this._currentTrillStep; + } + } else { + note.vibrato = VibratoType.Slight; + } + break; + case 'mordent': + note.ornament = NoteOrnament.LowerMordent; + break; + case 'inverted-mordent': + note.ornament = NoteOrnament.UpperMordent; + 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; + } + break; + // case 'haydn': Not supported + // case 'other-element': Not supported + } + } + } + + private parseSlide(element: XmlNode, note: Note) { + const type = element.getAttribute('type'); + const number = element.getAttribute('number', '1'); + + const context = this.getStaffContext(note.beat.voice.bar.staff); + + switch (type) { + case 'start': + context.slideOrigins.set(number, note); + break; + case 'stop': + if (context.slideOrigins.has(number)) { + const origin = context.slideOrigins.get(number)!; + origin.slideTarget = note; + note.slideOrigin = origin; + origin.slideOutType = SlideOutType.Shift; + } + break; + } + } + + private parseTied(element: XmlNode, note: Note, staff: Staff): void { + const type = element.getAttribute('type'); + const number = element.getAttribute('number', ''); + + const context = this.getStaffContext(staff); + + if (type === 'start') { + if (number) { + // start without end + if (context.tieStartIds.has(number)) { + const unclosed = context.tieStartIds.get(number)!; + context.tieStarts.delete(unclosed); + } + + context.tieStartIds.set(number, note); + } + + context.tieStarts.add(note); + } else if (type === 'stop' && !note.isTieDestination) { + let tieOrigin: Note | null = null; + if (number) { + if (!context.tieStartIds.has(number)) { + return; + } + + tieOrigin = context.tieStartIds.get(number)!; + context.tieStartIds.delete(number); + context.tieStarts.delete(note); + } else { + const realValue = this.calculatePitchedNoteValue(note); + for (const t of context.tieStarts) { + if (this.calculatePitchedNoteValue(t) === realValue) { + tieOrigin = t; + context.tieStarts.delete(tieOrigin); + break; + } + } + } + + if (!tieOrigin) { + return; + } + + note.isTieDestination = true; + note.tieOrigin = tieOrigin; + } + } + + private parseStem(element: XmlNode): BeamDirection | null { + switch (element.innerText) { + case 'down': + return BeamDirection.Down; + case 'up': + return BeamDirection.Up; + // case 'none': + default: + return null; + } + } + + private parseAccidental(element: XmlNode, note: Note) { + // NOTE: this can currently lead to wrong notes shown, + // TODO: check partwise-complex-measures.xml where accidentals and notes get wrong + // in combination with key signatures + switch (element.innerText) { + case 'sharp': + note.accidentalMode = NoteAccidentalMode.ForceSharp; + break; + case 'natural': + note.accidentalMode = NoteAccidentalMode.ForceNatural; + break; + case 'flat': + note.accidentalMode = NoteAccidentalMode.ForceFlat; + break; + case 'double-sharp': + note.accidentalMode = NoteAccidentalMode.ForceDoubleSharp; + break; + case 'flat-flat': + note.accidentalMode = NoteAccidentalMode.ForceDoubleFlat; + break; + // case 'sharp-sharp': Not supported + // case 'natural-sharp': Not supported + // case 'natural-flat': Not supported + // case 'quarter-flat': Not supported + // case 'quarter-sharp': Not supported + // case 'three-quarters-flat': Not supported + // case 'three-quarters-sharp': Not supported + // case 'sharp-down': + // case 'sharp-up': + // case 'natural-down': + // case 'natural-up': + // case 'flat-down': + // case 'flat-up': + // case 'double-sharp-down': Not supported + // case 'double-sharp-up': Not supported + // case 'flat-flat-down': Not supported + // case 'flat-flat-up': Not supported + // case 'arrow-down': + // case 'arrow-up': + // case 'triple-sharp': + // case 'triple-flat': + // case 'slash-quarter-sharp': Not supported + // case 'slash-sharp': Not supported + // case 'slash-flat': Not supported + // case 'double-slash-flat': Not supported + // case 'sharp-1': + // case 'sharp-2': + // case 'sharp-3': + // case 'sharp-4': + // case 'sharp-5': + // case 'flat-1': + // case 'flat-2': + // case 'flat-3': + // case 'flat-4': + // case 'flat-5': + // case 'sori': Not supported + // case 'kokon': Not supported + // case 'other': Not supported + // default: + // Logger.warning('MusicXML', `Unsupported accidental ${element.innerText}`); + // break; + } + } + + private calculatePitchedNoteValue(note: Note) { + return note.octave * 12 + note.tone; + } + + private parseDuration(element: XmlNode): number { + return this.musicXmlDivisionsToAlphaTabTicks(Number.parseFloat(element.innerText)); + } + + private parseUnpitched(element: XmlNode, track: Track): Note { + let step: string = ''; + let octave: number = 0; + for (const c of element.childElements()) { + switch (c.localName) { + case 'display-step': + step = c.innerText; + break; + case 'display-octave': + // 0-9, 4 for middle C + octave = Number.parseInt(c.innerText) + 1; + break; + } + } + + // if no display information -> middle of staff (handled in getOrCreateArticulation) + const note = new Note(); + if (step === '') { + note.octave = 0; + note.tone = 0; + } else { + const value: number = octave * 12 + ModelUtils.getToneForText(step).noteValue; + note.octave = (value / 12) | 0; + note.tone = value - note.octave * 12; + } + + return note; + } + + private parsePitch(element: XmlNode): Note { + let step: string = ''; + let semitones: number = 0; + let octave: number = 0; + for (const c of element.childElements()) { + switch (c.localName) { + case 'step': + step = c.innerText; + break; + case 'alter': + semitones = Number.parseFloat(c.innerText); + if (Number.isNaN(semitones)) { + semitones = 0; + } + break; + case 'octave': + // 0-9, 4 for middle C + octave = Number.parseInt(c.innerText) + 1; + break; + } + } + + semitones = semitones | 0; // no microtones supported + const value: number = octave * 12 + ModelUtils.getToneForText(step).noteValue + semitones; + const note = new Note(); + + note.octave = (value / 12) | 0; + note.tone = value - note.octave * 12; + + return note; + } + + private applyNoteHead( + note: Note, + beatDuration: Duration, + forceFill: boolean | undefined, + doubleWhole: MusicFontSymbol, + whole: MusicFontSymbol, + half: MusicFontSymbol, + filled: MusicFontSymbol + ) { + if (forceFill === undefined) { + switch (beatDuration) { + case Duration.QuadrupleWhole: + case Duration.DoubleWhole: + note.style!.noteHead = doubleWhole; + break; + case Duration.Whole: + note.style!.noteHead = whole; + break; + case Duration.Half: + note.style!.noteHead = half; + break; + default: + note.style!.noteHead = filled; + break; + } + } else if (forceFill! === true) { + note.style!.noteHead = filled; + } else { + switch (beatDuration) { + case Duration.QuadrupleWhole: + case Duration.DoubleWhole: + note.style!.noteHead = doubleWhole; + break; + case Duration.Whole: + note.style!.noteHead = whole; + break; + case Duration.Half: + note.style!.noteHead = half; + break; + default: + note.style!.noteHead = half; + break; } } } diff --git a/src/importer/PartConfiguration.ts b/src/importer/PartConfiguration.ts index 650e670a2..a437b392e 100644 --- a/src/importer/PartConfiguration.ts +++ b/src/importer/PartConfiguration.ts @@ -1,22 +1,22 @@ import { GpBinaryHelpers } from '@src/importer/Gp3To5Importer'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { IOHelper } from '@src/io/IOHelper'; -import { Score } from '@src/model/Score'; -import { Track } from '@src/model/Track'; - +import type { Score } from '@src/model/Score'; +import type { Track } from '@src/model/Track'; // PartConfiguration File Format Notes. // Based off Guitar Pro 8 -// The file contains a serialized "Score View Collection" filled like this: +// The file contains a serialized "Score View Collection" filled like this: // There is always 1 ScoreView holding a TrackViewGroup for each Track contained in the file. This is the multi-track layout. // Additionally there is 1 ScoreView individually for each track with only 1 TrackViewGroup of this Group. -// The Guitar Pro UI seem to update both the multi-track and the single-track layouts when changin the displayed staves. -// But technically it would support showing tracks different in multi-track. +// The Guitar Pro UI seem to update both the multi-track and the single-track layouts when changing the displayed staves. +// But technically it would support showing alternating staves in multi-track. +// For the multi-rest flag the respective TrackViewGroups need to be respected. // File: -// int32 (big endian) | Number of Score Views +// int32 (big endian) | Number of Score Views // ScoreView[] | The individual score views // int32 (big endian) | The index to the currently active view @@ -27,13 +27,12 @@ import { Track } from '@src/model/Track'; // TrackViewGroup: // 1 byte | Track View Group Type Bitflag -// | 0th bit: showStandardNotation -// | 1th bit: showTablature +// | 0th bit: showStandardNotation +// | 1th bit: showTablature // | 2nd bit: showSlash // | 3rd bit: numberedNotation (GP8 feature - jiǎnpǔ aka Chinese Number Notation) // | if no bits set -> activate standard notation -// - +// class PartConfigurationScoreView { public isMultiRest: boolean = false; @@ -52,13 +51,16 @@ export class PartConfiguration { public apply(score: Score): void { // for now we only look at the first score view which seem to hold - // the config for all tracks. - if(this.scoreViews.length > 0) { + // the config for all tracks. + if (this.scoreViews.length > 0) { let trackIndex = 0; - for (let trackConfig of this.scoreViews[0].trackViewGroups) { + + score.stylesheet.multiTrackMultiBarRest = this.scoreViews[0].isMultiRest; + + for (const trackConfig of this.scoreViews[0].trackViewGroups) { if (trackIndex < score.tracks.length) { const track: Track = score.tracks[trackIndex]; - for(const staff of track.staves) { + for (const staff of track.staves) { staff.showTablature = trackConfig.showTablature; staff.showStandardNotation = trackConfig.showStandardNotation; staff.showSlash = trackConfig.showSlash; @@ -67,16 +69,27 @@ export class PartConfiguration { } trackIndex++; } + + for (let scoreViewIndex = 1; scoreViewIndex < this.scoreViews.length; scoreViewIndex++) { + if (this.scoreViews[scoreViewIndex].isMultiRest) { + // lazy init + if (!score.stylesheet.perTrackMultiBarRest) { + score.stylesheet.perTrackMultiBarRest = new Set(); + } + + trackIndex = scoreViewIndex - 1; + score.stylesheet.perTrackMultiBarRest!.add(trackIndex); + } + } } } public constructor(partConfigurationData: Uint8Array) { - let readable: ByteBuffer = ByteBuffer.fromBuffer(partConfigurationData); - + const readable: ByteBuffer = ByteBuffer.fromBuffer(partConfigurationData); + const scoreViewCount: number = IOHelper.readInt32BE(readable); for (let i: number = 0; i < scoreViewCount; i++) { - const scoreView = new PartConfigurationScoreView(); this.scoreViews.push(scoreView); @@ -89,7 +102,7 @@ export class PartConfiguration { if (flags === 0) { flags = 1; } - let trackConfiguration = new PartConfigurationTrackViewGroup(); + const trackConfiguration = new PartConfigurationTrackViewGroup(); trackConfiguration.showStandardNotation = (flags & 0x01) !== 0; trackConfiguration.showTablature = (flags & 0x02) !== 0; trackConfiguration.showSlash = (flags & 0x04) !== 0; @@ -106,6 +119,8 @@ export class PartConfiguration { new PartConfigurationScoreView() // Multi Track Score View ]; + scoreViews[0].isMultiRest = score.stylesheet.multiTrackMultiBarRest; + for (const track of score.tracks) { const trackConfiguration = new PartConfigurationTrackViewGroup(); // NOTE: unclear how multi staff settings are meant in this format @@ -118,6 +133,7 @@ export class PartConfiguration { scoreViews[0].trackViewGroups.push(trackConfiguration); const singleTrackScoreView = new PartConfigurationScoreView(); + singleTrackScoreView.isMultiRest = score.stylesheet.perTrackMultiBarRest?.has(track.index) === true; singleTrackScoreView.trackViewGroups.push(trackConfiguration); scoreViews.push(singleTrackScoreView); } @@ -126,18 +142,18 @@ export class PartConfiguration { for (const part of scoreViews) { writer.writeByte(part.isMultiRest ? 1 : 0); IOHelper.writeInt32BE(writer, part.trackViewGroups.length); - for(const track of part.trackViewGroups) { + for (const track of part.trackViewGroups) { let flags = 0; - if(track.showStandardNotation) { + if (track.showStandardNotation) { flags = flags | 0x01; } - if(track.showTablature) { + if (track.showTablature) { flags = flags | 0x02; } - if(track.showSlash) { + if (track.showSlash) { flags = flags | 0x04; } - if(track.showNumbered) { + if (track.showNumbered) { flags = flags | 0x08; } writer.writeByte(flags); diff --git a/src/importer/ScoreImporter.ts b/src/importer/ScoreImporter.ts index e86fe2e3a..b757fa9cd 100644 --- a/src/importer/ScoreImporter.ts +++ b/src/importer/ScoreImporter.ts @@ -1,6 +1,6 @@ -import { IReadable } from '@src/io/IReadable'; +import type { IReadable } from '@src/io/IReadable'; import { Score } from '@src/model/Score'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; /** * This is the base public class for creating new song importers which @@ -16,6 +16,8 @@ export abstract class ScoreImporter { public init(data: IReadable, settings: Settings): void { this.data = data; this.settings = settings; + // when beginning reading a new score we reset the IDs. + Score.resetIds(); } public abstract get name(): string; diff --git a/src/importer/ScoreLoader.ts b/src/importer/ScoreLoader.ts index 15e98d47a..5d6f337b0 100644 --- a/src/importer/ScoreLoader.ts +++ b/src/importer/ScoreLoader.ts @@ -1,11 +1,11 @@ import { Environment } from '@src/Environment'; import { FileLoadError } from '@src/FileLoadError'; -import { ScoreImporter } from '@src/importer/ScoreImporter'; +import type { ScoreImporter } from '@src/importer/ScoreImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { ByteBuffer } from '@src/io/ByteBuffer'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; @@ -29,17 +29,17 @@ export class ScoreLoader { error: (error: any) => void, settings?: Settings ): void { - let xhr: XMLHttpRequest = new XMLHttpRequest(); + const xhr: XMLHttpRequest = new XMLHttpRequest(); xhr.open('GET', path, true, null, null); xhr.responseType = 'arraybuffer'; xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { - let response: unknown = xhr.response; + const response: unknown = xhr.response; if (xhr.status === 200 || (xhr.status === 0 && response)) { try { - let buffer: ArrayBuffer = xhr.response; - let reader: Uint8Array = new Uint8Array(buffer); - let score: Score = ScoreLoader.loadScoreFromBytes(reader, settings); + const buffer: ArrayBuffer = xhr.response; + const reader: Uint8Array = new Uint8Array(buffer); + const score: Score = ScoreLoader.loadScoreFromBytes(reader, settings); success(score); } catch (e) { error(e); @@ -55,7 +55,7 @@ export class ScoreLoader { } else if (xhr.statusText === 'timeout') { error(new FileLoadError('Request Time out.', xhr)); } else { - error(new FileLoadError('Unknow Error: ' + xhr.responseText, xhr)); + error(new FileLoadError(`Unknow Error: ${xhr.responseText}`, xhr)); } } }; @@ -73,24 +73,21 @@ export class ScoreLoader { settings = new Settings(); } - let importers: ScoreImporter[] = Environment.buildImporters(); - Logger.debug( - 'ScoreLoader', - `Loading score from ${data.length} bytes using ${importers.length} importers` - ); + const importers: ScoreImporter[] = Environment.buildImporters(); + Logger.debug('ScoreLoader', `Loading score from ${data.length} bytes using ${importers.length} importers`); let score: Score | null = null; - let bb: ByteBuffer = ByteBuffer.fromBuffer(data); - for (let importer of importers) { + const bb: ByteBuffer = ByteBuffer.fromBuffer(data); + for (const importer of importers) { bb.reset(); try { - Logger.debug('ScoreLoader', 'Importing using importer ' + importer.name); + Logger.debug('ScoreLoader', `Importing using importer ${importer.name}`); importer.init(bb, settings); score = importer.readScore(); - Logger.debug('ScoreLoader', 'Score imported using ' + importer.name); + Logger.debug('ScoreLoader', `Score imported using ${importer.name}`); break; } catch (e) { if (e instanceof UnsupportedFormatError) { - Logger.debug('ScoreLoader', importer.name + ' does not support the file'); + Logger.debug('ScoreLoader', `${importer.name} does not support the file`); } else { Logger.error('ScoreLoader', 'Score import failed due to unexpected error: ', e); throw e; diff --git a/src/importer/UnsupportedFormatError.ts b/src/importer/UnsupportedFormatError.ts index 3446ba851..1420d97e8 100644 --- a/src/importer/UnsupportedFormatError.ts +++ b/src/importer/UnsupportedFormatError.ts @@ -1,4 +1,4 @@ -import { AlphaTabError, AlphaTabErrorType } from "@src/AlphaTabError"; +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; /** * The exception thrown by a {@link ScoreImporter} in case the diff --git a/src/importer/index.ts b/src/importer/_barrel.ts similarity index 100% rename from src/importer/index.ts rename to src/importer/_barrel.ts diff --git a/src/io/BitReader.ts b/src/io/BitReader.ts index 408606cc5..3cb8f30a5 100644 --- a/src/io/BitReader.ts +++ b/src/io/BitReader.ts @@ -1,5 +1,5 @@ import { ByteBuffer } from '@src/io/ByteBuffer'; -import { EndOfReaderError, IReadable } from '@src/io/IReadable'; +import { EndOfReaderError, type IReadable } from '@src/io/IReadable'; /** * This utility public class allows bitwise reading of a stream @@ -62,7 +62,7 @@ export class BitReader { } public readAll(): Uint8Array { - let all: ByteBuffer = ByteBuffer.empty(); + const all: ByteBuffer = ByteBuffer.empty(); try { while (true) { all.writeByte(this.readByte() & 0xff); diff --git a/src/io/ByteBuffer.ts b/src/io/ByteBuffer.ts index ce59ad4c5..274c1caab 100644 --- a/src/io/ByteBuffer.ts +++ b/src/io/ByteBuffer.ts @@ -1,5 +1,5 @@ -import { IReadable } from '@src/io/IReadable'; -import { IWriteable } from '@src/io/IWriteable'; +import type { IReadable } from '@src/io/IReadable'; +import type { IWriteable } from '@src/io/IWriteable'; import { IOHelper } from '@src/io/IOHelper'; export class ByteBuffer implements IWriteable, IReadable { @@ -21,20 +21,20 @@ export class ByteBuffer implements IWriteable, IReadable { } public static withCapacity(capacity: number): ByteBuffer { - let buffer: ByteBuffer = new ByteBuffer(); + const buffer: ByteBuffer = new ByteBuffer(); buffer._buffer = new Uint8Array(capacity); return buffer; } public static fromBuffer(data: Uint8Array): ByteBuffer { - let buffer: ByteBuffer = new ByteBuffer(); + const buffer: ByteBuffer = new ByteBuffer(); buffer._buffer = data; - buffer.length = data.length + buffer.length = data.length; return buffer; } public static fromString(contents: string): ByteBuffer { - let byteArray: Uint8Array = IOHelper.stringToBytes(contents); + const byteArray: Uint8Array = IOHelper.stringToBytes(contents); return ByteBuffer.fromBuffer(byteArray); } @@ -47,7 +47,7 @@ export class ByteBuffer implements IWriteable, IReadable { } public readByte(): number { - let n: number = this.length - this.position; + const n: number = this.length - this.position; if (n <= 0) { return -1; } @@ -68,9 +68,9 @@ export class ByteBuffer implements IWriteable, IReadable { } public writeByte(value: number): void { - let i: number = this.position + 1; + const i: number = this.position + 1; this.ensureCapacity(i); - this._buffer[this.position] = value & 0xFF; + this._buffer[this.position] = value & 0xff; if (i > this.length) { this.length = i; } @@ -78,10 +78,10 @@ export class ByteBuffer implements IWriteable, IReadable { } public write(buffer: Uint8Array, offset: number, count: number): void { - let i: number = this.position + count; + const i: number = this.position + count; this.ensureCapacity(i); - - let count1: number = Math.min(count, buffer.length - offset); + + const count1: number = Math.min(count, buffer.length - offset); this._buffer.set(buffer.subarray(offset, offset + count1), this.position); if (i > this.length) { @@ -100,7 +100,7 @@ export class ByteBuffer implements IWriteable, IReadable { newCapacity = this._buffer.length * 2; } - let newBuffer: Uint8Array = new Uint8Array(newCapacity); + const newBuffer: Uint8Array = new Uint8Array(newCapacity); if (this.length > 0) { newBuffer.set(this._buffer.subarray(0, 0 + this.length), 0); } @@ -113,12 +113,12 @@ export class ByteBuffer implements IWriteable, IReadable { } public toArray(): Uint8Array { - let copy: Uint8Array = new Uint8Array(this.length); + const copy: Uint8Array = new Uint8Array(this.length); copy.set(this._buffer.subarray(0, 0 + this.length), 0); return copy; } - public copyTo(destination:IWriteable) { + public copyTo(destination: IWriteable) { destination.write(this._buffer, 0, this.length); } } diff --git a/src/io/IOHelper.ts b/src/io/IOHelper.ts index 9cf5d8bdf..efa83fe38 100644 --- a/src/io/IOHelper.ts +++ b/src/io/IOHelper.ts @@ -1,13 +1,13 @@ -import { IReadable } from '@src/io/IReadable'; +import type { IReadable } from '@src/io/IReadable'; import { TypeConversions } from '@src/io/TypeConversions'; -import { IWriteable } from '@src/io/IWriteable'; +import type { IWriteable } from '@src/io/IWriteable'; export class IOHelper { public static readInt32BE(input: IReadable): number { - let ch1: number = input.readByte(); - let ch2: number = input.readByte(); - let ch3: number = input.readByte(); - let ch4: number = input.readByte(); + const ch1: number = input.readByte(); + const ch2: number = input.readByte(); + const ch3: number = input.readByte(); + const ch4: number = input.readByte(); return (ch1 << 24) | (ch2 << 16) | (ch3 << 8) | ch4; } @@ -26,10 +26,10 @@ export class IOHelper { } public static readInt32LE(input: IReadable): number { - let ch1: number = input.readByte(); - let ch2: number = input.readByte(); - let ch3: number = input.readByte(); - let ch4: number = input.readByte(); + const ch1: number = input.readByte(); + const ch2: number = input.readByte(); + const ch3: number = input.readByte(); + const ch4: number = input.readByte(); return (ch4 << 24) | (ch3 << 16) | (ch2 << 8) | ch1; } @@ -40,61 +40,61 @@ export class IOHelper { } public static readUInt32LE(input: IReadable): number { - let ch1: number = input.readByte(); - let ch2: number = input.readByte(); - let ch3: number = input.readByte(); - let ch4: number = input.readByte(); + const ch1: number = input.readByte(); + const ch2: number = input.readByte(); + const ch3: number = input.readByte(); + const ch4: number = input.readByte(); return (ch4 << 24) | (ch3 << 16) | (ch2 << 8) | ch1; } public static decodeUInt32LE(data: Uint8Array, index: number): number { - let ch1: number = data[index]; - let ch2: number = data[index + 1]; - let ch3: number = data[index + 2]; - let ch4: number = data[index + 3]; + const ch1: number = data[index]; + const ch2: number = data[index + 1]; + const ch3: number = data[index + 2]; + const ch4: number = data[index + 3]; return (ch4 << 24) | (ch3 << 16) | (ch2 << 8) | ch1; } public static readUInt16LE(input: IReadable): number { - let ch1: number = input.readByte(); - let ch2: number = input.readByte(); + const ch1: number = input.readByte(); + const ch2: number = input.readByte(); return TypeConversions.int32ToUint16((ch2 << 8) | ch1); } public static readInt16LE(input: IReadable): number { - let ch1: number = input.readByte(); - let ch2: number = input.readByte(); + const ch1: number = input.readByte(); + const ch2: number = input.readByte(); return TypeConversions.int32ToInt16((ch2 << 8) | ch1); } public static readUInt32BE(input: IReadable): number { - let ch1: number = input.readByte(); - let ch2: number = input.readByte(); - let ch3: number = input.readByte(); - let ch4: number = input.readByte(); + const ch1: number = input.readByte(); + const ch2: number = input.readByte(); + const ch3: number = input.readByte(); + const ch4: number = input.readByte(); return TypeConversions.int32ToUint32((ch1 << 24) | (ch2 << 16) | (ch3 << 8) | ch4); } public static readUInt16BE(input: IReadable): number { - let ch1: number = input.readByte(); - let ch2: number = input.readByte(); + const ch1: number = input.readByte(); + const ch2: number = input.readByte(); return TypeConversions.int32ToInt16((ch1 << 8) | ch2); } public static readInt16BE(input: IReadable): number { - let ch1: number = input.readByte(); - let ch2: number = input.readByte(); + const ch1: number = input.readByte(); + const ch2: number = input.readByte(); return TypeConversions.int32ToInt16((ch1 << 8) | ch2); } public static readByteArray(input: IReadable, length: number): Uint8Array { - let v: Uint8Array = new Uint8Array(length); + const v: Uint8Array = new Uint8Array(length); input.read(v, 0, length); return v; } public static read8BitChars(input: IReadable, length: number): string { - let b: Uint8Array = new Uint8Array(length); + const b: Uint8Array = new Uint8Array(length); input.read(b, 0, b.length); return IOHelper.toString(b, 'utf-8'); } @@ -113,13 +113,13 @@ export class IOHelper { let s: string = ''; let z: number = -1; for (let i: number = 0; i < length; i++) { - let c: number = input.readByte(); + const c: number = input.readByte(); if (c === 0 && z === -1) { z = i; } s += String.fromCharCode(c); } - let t: string = s; + const t: string = s; if (z >= 0) { return t.substr(0, z); } @@ -127,7 +127,7 @@ export class IOHelper { } public static readSInt8(input: IReadable): number { - let v: number = input.readByte(); + const v: number = input.readByte(); return ((v & 255) >> 7) * -256 + (v & 255); } @@ -144,14 +144,14 @@ export class IOHelper { } public static toString(data: Uint8Array, encoding: string): string { - let detectedEncoding: string | null = IOHelper.detectEncoding(data); + const detectedEncoding: string | null = IOHelper.detectEncoding(data); if (detectedEncoding) { encoding = detectedEncoding; } if (!encoding) { encoding = 'utf-8'; } - let decoder: TextDecoder = new TextDecoder(encoding); + const decoder: TextDecoder = new TextDecoder(encoding); return decoder.decode(data.buffer as ArrayBuffer); } @@ -172,7 +172,7 @@ export class IOHelper { } public static stringToBytes(str: string): Uint8Array { - let decoder: TextEncoder = new TextEncoder(); + const decoder: TextEncoder = new TextEncoder(); return decoder.encode(str); } diff --git a/src/io/IReadable.ts b/src/io/IReadable.ts index 56965ba25..d40f10abd 100644 --- a/src/io/IReadable.ts +++ b/src/io/IReadable.ts @@ -1,4 +1,4 @@ -import { AlphaTabError, AlphaTabErrorType } from "@src/AlphaTabError"; +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; /** * Represents a stream of binary data that can be read from. diff --git a/src/io/IWriteable.ts b/src/io/IWriteable.ts index 569ac6311..8cf7df9fe 100644 --- a/src/io/IWriteable.ts +++ b/src/io/IWriteable.ts @@ -2,12 +2,11 @@ * Represents a writer where binary data can be written to. */ export interface IWriteable { - /** - * Gets the current number of written bytes. + * Gets the current number of written bytes. */ - readonly bytesWritten:number; - + readonly bytesWritten: number; + /** * Write a single byte to the stream. * @param value The value to write. diff --git a/src/io/JsonHelper.ts b/src/io/JsonHelper.ts index 7c6e0810c..acf1ab112 100644 --- a/src/io/JsonHelper.ts +++ b/src/io/JsonHelper.ts @@ -1,4 +1,4 @@ -import { AlphaTabError, AlphaTabErrorType } from "@src/AlphaTabError"; +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; /** * @partial @@ -8,18 +8,18 @@ export class JsonHelper { * @target web * @partial */ - public static parseEnum(s: unknown, enumType: any): T | null { + public static parseEnum(s: unknown, enumType: any): T | undefined { switch (typeof s) { case 'string': - const num = parseInt(s); - return isNaN(num) - ? enumType[Object.keys(enumType).find(k => k.toLowerCase() === s.toLowerCase()) as any] as any - : num as unknown as T; + const num = Number.parseInt(s); + return Number.isNaN(num) + ? (enumType[Object.keys(enumType).find(k => k.toLowerCase() === s.toLowerCase()) as any] as any) + : (num as unknown as T); case 'number': return s as unknown as T; case 'undefined': case 'object': - return null; + return undefined; } throw new AlphaTabError(AlphaTabErrorType.Format, `Could not parse enum value '${s}'`); } @@ -33,7 +33,7 @@ export class JsonHelper { (s as Map).forEach(func); } else if (typeof s === 'object') { for (const k in s) { - func((s as any)[k], k) + func((s as any)[k], k); } } // skip @@ -46,9 +46,11 @@ export class JsonHelper { public static getValue(s: unknown, key: string): unknown { if (s instanceof Map) { return (s as Map).get(key); - } else if (typeof s === 'object') { + } + + if (typeof s === 'object') { return (s as any)[key]; } return null; } -} \ No newline at end of file +} diff --git a/src/io/TypeConversions.ts b/src/io/TypeConversions.ts index 5fc87ad5e..54411307f 100644 --- a/src/io/TypeConversions.ts +++ b/src/io/TypeConversions.ts @@ -8,7 +8,7 @@ export class TypeConversions { public static float64ToBytes(v: number): Uint8Array { TypeConversions._dataView.setFloat64(0, v, true); - return this._conversionByteArray; + return TypeConversions._conversionByteArray; } public static bytesToInt64LE(bytes: Uint8Array): number { diff --git a/src/io/_barrel.ts b/src/io/_barrel.ts new file mode 100644 index 000000000..2fd996acf --- /dev/null +++ b/src/io/_barrel.ts @@ -0,0 +1,4 @@ +export type { IWriteable } from '@src/io/IWriteable'; +export type { IReadable } from '@src/io/IReadable'; +export { ByteBuffer } from '@src/io/ByteBuffer'; +export { IOHelper } from '@src/io/IOHelper'; diff --git a/src/midi/AlphaSynthMidiFileHandler.ts b/src/midi/AlphaSynthMidiFileHandler.ts index f086d3536..1512008e0 100644 --- a/src/midi/AlphaSynthMidiFileHandler.ts +++ b/src/midi/AlphaSynthMidiFileHandler.ts @@ -1,8 +1,19 @@ -import { AlphaTabRestEvent, ControlChangeEvent, EndOfTrackEvent, NoteBendEvent, NoteOffEvent, NoteOnEvent, PitchBendEvent, ProgramChangeEvent, TempoChangeEvent, TimeSignatureEvent } from '@src/midi/MidiEvent'; -import { IMidiFileHandler } from '@src/midi/IMidiFileHandler'; -import { MidiFile, MidiFileFormat } from '@src/midi/MidiFile'; +import { + AlphaTabRestEvent, + ControlChangeEvent, + EndOfTrackEvent, + NoteBendEvent, + NoteOffEvent, + NoteOnEvent, + PitchBendEvent, + ProgramChangeEvent, + TempoChangeEvent, + TimeSignatureEvent +} from '@src/midi/MidiEvent'; +import type { IMidiFileHandler } from '@src/midi/IMidiFileHandler'; +import { type MidiFile, MidiFileFormat } from '@src/midi/MidiFile'; import { SynthConstants } from '@src/synth/SynthConstants'; -import { ControllerType } from './ControllerType'; +import type { ControllerType } from '@src/midi/ControllerType'; /** * This implementation of the {@link IMidiFileHandler} @@ -34,37 +45,35 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { } } - this._midiFile.addEvent(new TimeSignatureEvent( - 0, - tick, - timeSignatureNumerator, - denominatorIndex, - 48, - 8 - )); + this._midiFile.addEvent(new TimeSignatureEvent(0, tick, timeSignatureNumerator, denominatorIndex, 48, 8)); } public addRest(track: number, tick: number, channel: number): void { - if(!this._smf1Mode) { + 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 { - this._midiFile.addEvent(new NoteOnEvent(track, start, channel, - AlphaSynthMidiFileHandler.fixValue(key), - AlphaSynthMidiFileHandler.fixValue(velocity))); + public addNote(track: number, start: number, length: number, key: number, velocity: number, channel: number): void { + this._midiFile.addEvent( + new NoteOnEvent( + track, + start, + channel, + AlphaSynthMidiFileHandler.fixValue(key), + AlphaSynthMidiFileHandler.fixValue(velocity) + ) + ); - this._midiFile.addEvent(new NoteOffEvent(track, start + length, channel, - AlphaSynthMidiFileHandler.fixValue(key), - AlphaSynthMidiFileHandler.fixValue(velocity))); + this._midiFile.addEvent( + new NoteOffEvent( + track, + start + length, + channel, + AlphaSynthMidiFileHandler.fixValue(key), + AlphaSynthMidiFileHandler.fixValue(velocity) + ) + ); } private static fixValue(value: number): number { @@ -77,14 +86,16 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { return value; } - public addControlChange(track: number, tick: number, channel: number, controller: ControllerType, value: number): void { - this._midiFile.addEvent(new ControlChangeEvent( - track, - tick, - channel, - controller, - AlphaSynthMidiFileHandler.fixValue(value) - )); + public addControlChange( + track: number, + tick: number, + channel: number, + controller: ControllerType, + value: number + ): void { + this._midiFile.addEvent( + new ControlChangeEvent(track, tick, channel, controller, AlphaSynthMidiFileHandler.fixValue(value)) + ); } public addProgramChange(track: number, tick: number, channel: number, program: number): void { @@ -112,20 +123,14 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { } else { // map midi 1.0 range of 0-16384 (0x4000) // to midi 2.0 range of 0-4294967296 (0x100000000) - value = value * SynthConstants.MaxPitchWheel20 / SynthConstants.MaxPitchWheel + value = (value * SynthConstants.MaxPitchWheel20) / SynthConstants.MaxPitchWheel; - this._midiFile.addEvent(new NoteBendEvent( - track, - tick, - channel, - key, - value - )); + this._midiFile.addEvent(new NoteBendEvent(track, tick, channel, key, value)); } } public finishTrack(track: number, tick: number): void { - if (this._midiFile.format == MidiFileFormat.MultiTrack || track == 0) { + if (this._midiFile.format === MidiFileFormat.MultiTrack || track === 0) { this._midiFile.addEvent(new EndOfTrackEvent(track, tick)); } } diff --git a/src/midi/BeatTickLookup.ts b/src/midi/BeatTickLookup.ts index 47753f8c8..bb1474c7f 100644 --- a/src/midi/BeatTickLookup.ts +++ b/src/midi/BeatTickLookup.ts @@ -1,4 +1,4 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; /** * Represents a beat and when it is actually played according to the generated audio. @@ -14,10 +14,7 @@ export class BeatTickLookupItem { */ public readonly playbackStart: number; - public constructor( - beat: Beat, - playbackStart: number - ) { + public constructor(beat: Beat, playbackStart: number) { this.beat = beat; this.playbackStart = playbackStart; } @@ -41,7 +38,7 @@ export class BeatTickLookup { /** * Gets or sets a list of all beats that should be highlighted when - * the beat of this lookup starts playing. This might not mean + * the beat of this lookup starts playing. This might not mean * the beats start at this position. */ public highlightedBeats: BeatTickLookupItem[] = []; @@ -91,7 +88,7 @@ export class BeatTickLookup { */ public getVisibleBeatAtStart(visibleTracks: Set): Beat | null { for (const b of this.highlightedBeats) { - if (b.playbackStart == this.start && visibleTracks.has(b.beat.voice.bar.staff.track.index)) { + if (b.playbackStart === this.start && visibleTracks.has(b.beat.voice.bar.staff.track.index)) { return b.beat; } } diff --git a/src/midi/ControllerType.ts b/src/midi/ControllerType.ts index 9a17097cf..a7fbc6adf 100644 --- a/src/midi/ControllerType.ts +++ b/src/midi/ControllerType.ts @@ -29,12 +29,12 @@ export enum ControllerType { /** * Pan MSB */ - PanCoarse = 0x0A, + PanCoarse = 0x0a, /** * Expression Controller MSB */ - ExpressionControllerCoarse = 0x0B, + ExpressionControllerCoarse = 0x0b, //EffectControl1Coarse = 0x0C, //EffectControl2Coarse = 0x0D, @@ -65,12 +65,12 @@ export enum ControllerType { /** * Pan LSB */ - PanFine = 0x2A, + PanFine = 0x2a, /** * Expression controller LSB */ - ExpressionControllerFine = 0x2B, + ExpressionControllerFine = 0x2b, //EffectControl1Fine = 0x2C, //EffectControl2Fine = 0x2D, @@ -139,7 +139,7 @@ export enum ControllerType { /** * All notes of. */ - AllNotesOff = 0x7B + AllNotesOff = 0x7b //OmniModeOff = 0x7C, //OmniModeOn = 0x7D, diff --git a/src/midi/DeprecatedEvents.ts b/src/midi/DeprecatedEvents.ts index f2ca462f6..c96d5df57 100644 --- a/src/midi/DeprecatedEvents.ts +++ b/src/midi/DeprecatedEvents.ts @@ -1,9 +1,9 @@ -import { IWriteable } from "@src/io/IWriteable"; -import { MidiEvent, MidiEventType } from "./MidiEvent"; -import { AlphaTabError, AlphaTabErrorType } from "@src/AlphaTabError"; +import type { IWriteable } from '@src/io/IWriteable'; +import { MidiEvent, MidiEventType } from '@src/midi/MidiEvent'; +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; /** - * @deprecated Move to the new concrete Midi Event Types. + * @deprecated Move to the new concrete Midi Event Types. */ export class DeprecatedMidiEvent extends MidiEvent { public constructor() { @@ -16,7 +16,7 @@ export class DeprecatedMidiEvent extends MidiEvent { } /** - * @deprecated Move to the new concrete Midi Event Types. + * @deprecated Move to the new concrete Midi Event Types. */ export enum MetaEventType { SequenceNumber = 0x00, @@ -31,16 +31,16 @@ export enum MetaEventType { PortName = 0x09, MidiChannel = 0x20, MidiPort = 0x21, - EndOfTrack = 0x2F, + EndOfTrack = 0x2f, Tempo = 0x51, SmpteOffset = 0x54, TimeSignature = 0x58, KeySignature = 0x59, - SequencerSpecific = 0x7F + SequencerSpecific = 0x7f } /** - * @deprecated Move to the new concrete Midi Event Types. + * @deprecated Move to the new concrete Midi Event Types. */ export class MetaEvent extends DeprecatedMidiEvent { public get metaStatus(): MetaEventType { @@ -49,21 +49,21 @@ export class MetaEvent extends DeprecatedMidiEvent { } /** - * @deprecated Move to the new concrete Midi Event Types. + * @deprecated Move to the new concrete Midi Event Types. */ export class MetaDataEvent extends MetaEvent { public data: Uint8Array = new Uint8Array(); } /** - * @deprecated Move to the new concrete Midi Event Types. + * @deprecated Move to the new concrete Midi Event Types. */ export class MetaNumberEvent extends MetaEvent { public value: number = 0; } /** - * @deprecated Move to the new concrete Midi Event Types. + * @deprecated Move to the new concrete Midi Event Types. */ export class Midi20PerNotePitchBendEvent extends DeprecatedMidiEvent { public noteKey: number = 0; @@ -71,25 +71,24 @@ export class Midi20PerNotePitchBendEvent extends DeprecatedMidiEvent { } /** - * @deprecated Move to the new concrete Midi Event Types. + * @deprecated Move to the new concrete Midi Event Types. */ export enum SystemCommonType { - SystemExclusive = 0xF0, - MtcQuarterFrame = 0xF1, - SongPosition = 0xF2, - SongSelect = 0xF3, - TuneRequest = 0xF6, - SystemExclusive2 = 0xF7 + SystemExclusive = 0xf0, + MtcQuarterFrame = 0xf1, + SongPosition = 0xf2, + SongSelect = 0xf3, + TuneRequest = 0xf6, + SystemExclusive2 = 0xf7 } /** - * @deprecated Move to the new concrete Midi Event Types. + * @deprecated Move to the new concrete Midi Event Types. */ -export class SystemCommonEvent extends DeprecatedMidiEvent { -} +export class SystemCommonEvent extends DeprecatedMidiEvent {} /** - * @deprecated Move to the new concrete Midi Event Types. + * @deprecated Move to the new concrete Midi Event Types. */ export enum AlphaTabSystemExclusiveEvents { MetronomeTick = 0, @@ -97,10 +96,10 @@ export enum AlphaTabSystemExclusiveEvents { } /** - * @deprecated Move to the new concrete Midi Event Types. + * @deprecated Move to the new concrete Midi Event Types. */ export class SystemExclusiveEvent extends SystemCommonEvent { - public static readonly AlphaTabManufacturerId = 0x7D; + public static readonly AlphaTabManufacturerId = 0x7d; public data: Uint8Array = new Uint8Array(); @@ -127,4 +126,4 @@ export class SystemExclusiveEvent extends SystemCommonEvent { public get manufacturerId(): number { return 0; } -} \ No newline at end of file +} diff --git a/src/midi/GeneralMidi.ts b/src/midi/GeneralMidi.ts index 82556fd0f..55b10f01c 100644 --- a/src/midi/GeneralMidi.ts +++ b/src/midi/GeneralMidi.ts @@ -3,38 +3,134 @@ */ export class GeneralMidi { private static _values: Map = new Map([ - ['acousticgrandpiano', 0], ['brightacousticpiano', 1], ['electricgrandpiano', 2], - ['honkytonkpiano', 3], ['electricpiano1', 4], ['electricpiano2', 5], ['harpsichord', 6], - ['clavinet', 7], ['celesta', 8], ['glockenspiel', 9], ['musicbox', 10], ['vibraphone', 11], - ['marimba', 12], ['xylophone', 13], ['tubularbells', 14], ['dulcimer', 15], - ['drawbarorgan', 16], ['percussiveorgan', 17], ['rockorgan', 18], ['churchorgan', 19], - ['reedorgan', 20], ['accordion', 21], ['harmonica', 22], ['tangoaccordion', 23], - ['acousticguitarnylon', 24], ['acousticguitarsteel', 25], ['electricguitarjazz', 26], - ['electricguitarclean', 27], ['electricguitarmuted', 28], ['overdrivenguitar', 29], - ['distortionguitar', 30], ['guitarharmonics', 31], ['acousticbass', 32], - ['electricbassfinger', 33], ['electricbasspick', 34], ['fretlessbass', 35], - ['slapbass1', 36], ['slapbass2', 37], ['synthbass1', 38], ['synthbass2', 39], - ['violin', 40], ['viola', 41], ['cello', 42], ['contrabass', 43], ['tremolostrings', 44], - ['pizzicatostrings', 45], ['orchestralharp', 46], ['timpani', 47], ['stringensemble1', 48], - ['stringensemble2', 49], ['synthstrings1', 50], ['synthstrings2', 51], ['choiraahs', 52], - ['voiceoohs', 53], ['synthvoice', 54], ['orchestrahit', 55], ['trumpet', 56], - ['trombone', 57], ['tuba', 58], ['mutedtrumpet', 59], ['frenchhorn', 60], - ['brasssection', 61], ['synthbrass1', 62], ['synthbrass2', 63], ['sopranosax', 64], - ['altosax', 65], ['tenorsax', 66], ['baritonesax', 67], ['oboe', 68], ['englishhorn', 69], - ['bassoon', 70], ['clarinet', 71], ['piccolo', 72], ['flute', 73], ['recorder', 74], - ['panflute', 75], ['blownbottle', 76], ['shakuhachi', 77], ['whistle', 78], ['ocarina', 79], - ['lead1square', 80], ['lead2sawtooth', 81], ['lead3calliope', 82], ['lead4chiff', 83], - ['lead5charang', 84], ['lead6voice', 85], ['lead7fifths', 86], ['lead8bassandlead', 87], - ['pad1newage', 88], ['pad2warm', 89], ['pad3polysynth', 90], ['pad4choir', 91], - ['pad5bowed', 92], ['pad6metallic', 93], ['pad7halo', 94], ['pad8sweep', 95], - ['fx1rain', 96], ['fx2soundtrack', 97], ['fx3crystal', 98], ['fx4atmosphere', 99], - ['fx5brightness', 100], ['fx6goblins', 101], ['fx7echoes', 102], ['fx8scifi', 103], - ['sitar', 104], ['banjo', 105], ['shamisen', 106], ['koto', 107], ['kalimba', 108], - ['bagpipe', 109], ['fiddle', 110], ['shanai', 111], ['tinklebell', 112], ['agogo', 113], - ['steeldrums', 114], ['woodblock', 115], ['taikodrum', 116], ['melodictom', 117], - ['synthdrum', 118], ['reversecymbal', 119], ['guitarfretnoise', 120], ['breathnoise', 121], - ['seashore', 122], ['birdtweet', 123], ['telephonering', 124], ['helicopter', 125], - ['applause', 126], ['gunshot', 127] + ['acousticgrandpiano', 0], + ['brightacousticpiano', 1], + ['electricgrandpiano', 2], + ['honkytonkpiano', 3], + ['electricpiano1', 4], + ['electricpiano2', 5], + ['harpsichord', 6], + ['clavinet', 7], + ['celesta', 8], + ['glockenspiel', 9], + ['musicbox', 10], + ['vibraphone', 11], + ['marimba', 12], + ['xylophone', 13], + ['tubularbells', 14], + ['dulcimer', 15], + ['drawbarorgan', 16], + ['percussiveorgan', 17], + ['rockorgan', 18], + ['churchorgan', 19], + ['reedorgan', 20], + ['accordion', 21], + ['harmonica', 22], + ['tangoaccordion', 23], + ['acousticguitarnylon', 24], + ['acousticguitarsteel', 25], + ['electricguitarjazz', 26], + ['electricguitarclean', 27], + ['electricguitarmuted', 28], + ['overdrivenguitar', 29], + ['distortionguitar', 30], + ['guitarharmonics', 31], + ['acousticbass', 32], + ['electricbassfinger', 33], + ['electricbasspick', 34], + ['fretlessbass', 35], + ['slapbass1', 36], + ['slapbass2', 37], + ['synthbass1', 38], + ['synthbass2', 39], + ['violin', 40], + ['viola', 41], + ['cello', 42], + ['contrabass', 43], + ['tremolostrings', 44], + ['pizzicatostrings', 45], + ['orchestralharp', 46], + ['timpani', 47], + ['stringensemble1', 48], + ['stringensemble2', 49], + ['synthstrings1', 50], + ['synthstrings2', 51], + ['choiraahs', 52], + ['voiceoohs', 53], + ['synthvoice', 54], + ['orchestrahit', 55], + ['trumpet', 56], + ['trombone', 57], + ['tuba', 58], + ['mutedtrumpet', 59], + ['frenchhorn', 60], + ['brasssection', 61], + ['synthbrass1', 62], + ['synthbrass2', 63], + ['sopranosax', 64], + ['altosax', 65], + ['tenorsax', 66], + ['baritonesax', 67], + ['oboe', 68], + ['englishhorn', 69], + ['bassoon', 70], + ['clarinet', 71], + ['piccolo', 72], + ['flute', 73], + ['recorder', 74], + ['panflute', 75], + ['blownbottle', 76], + ['shakuhachi', 77], + ['whistle', 78], + ['ocarina', 79], + ['lead1square', 80], + ['lead2sawtooth', 81], + ['lead3calliope', 82], + ['lead4chiff', 83], + ['lead5charang', 84], + ['lead6voice', 85], + ['lead7fifths', 86], + ['lead8bassandlead', 87], + ['pad1newage', 88], + ['pad2warm', 89], + ['pad3polysynth', 90], + ['pad4choir', 91], + ['pad5bowed', 92], + ['pad6metallic', 93], + ['pad7halo', 94], + ['pad8sweep', 95], + ['fx1rain', 96], + ['fx2soundtrack', 97], + ['fx3crystal', 98], + ['fx4atmosphere', 99], + ['fx5brightness', 100], + ['fx6goblins', 101], + ['fx7echoes', 102], + ['fx8scifi', 103], + ['sitar', 104], + ['banjo', 105], + ['shamisen', 106], + ['koto', 107], + ['kalimba', 108], + ['bagpipe', 109], + ['fiddle', 110], + ['shanai', 111], + ['tinklebell', 112], + ['agogo', 113], + ['steeldrums', 114], + ['woodblock', 115], + ['taikodrum', 116], + ['melodictom', 117], + ['synthdrum', 118], + ['reversecymbal', 119], + ['guitarfretnoise', 120], + ['breathnoise', 121], + ['seashore', 122], + ['birdtweet', 123], + ['telephonering', 124], + ['helicopter', 125], + ['applause', 126], + ['gunshot', 127] ]); public static getValue(name: string): number { @@ -46,10 +142,10 @@ export class GeneralMidi { } public static isPiano(program: number): boolean { - return program <= 7 || program >= 16 && program <= 23; + return program <= 7 || (program >= 16 && program <= 23); } public static isGuitar(program: number): boolean { - return program >= 24 && program <= 39 || program === 105 || program === 43; + return (program >= 24 && program <= 39) || program === 105 || program === 43; } } diff --git a/src/midi/IMidiFileHandler.ts b/src/midi/IMidiFileHandler.ts index f33cd4fb1..c4e7fad67 100644 --- a/src/midi/IMidiFileHandler.ts +++ b/src/midi/IMidiFileHandler.ts @@ -1,4 +1,4 @@ -import { ControllerType } from "./ControllerType"; +import type { ControllerType } from '@src/midi/ControllerType'; /** * A handler is responsible for writing midi events to a custom structure @@ -29,14 +29,7 @@ export interface IMidiFileHandler { * @param velocity The velocity which should be applied to the note (derived from the note dynamics). * @param channel The midi channel on which the note should be played. */ - addNote( - track: number, - start: number, - length: number, - key: number, - velocity: number, - channel: number - ): void; + addNote(track: number, start: number, length: number, key: number, velocity: number, channel: number): void; /** * Adds a control change to the generated midi file. @@ -66,12 +59,12 @@ export interface IMidiFileHandler { /** * Add a bend specific to a note to the generated midi file. - * The note does not need to be started, if this event is signaled, the next time a note - * on this channel and key is played it will be affected. The note bend is cleared on a note-off for this key. + * The note does not need to be started, if this event is signaled, the next time a note + * on this channel and key is played it will be affected. The note bend is cleared on a note-off for this key. * @param track The midi track on which the bend should change. * @param tick The midi ticks when the bend should change. * @param channel The midi channel on which the bend should change. - * @param channel The key of the note that should be affected by the bend. + * @param channel The key of the note that should be affected by the bend. * @param value The new bend for the selected note. */ addNoteBend(track: number, tick: number, channel: number, key: number, value: number): void; diff --git a/src/midi/MasterBarTickLookup.ts b/src/midi/MasterBarTickLookup.ts index c182e851e..cdc353a26 100644 --- a/src/midi/MasterBarTickLookup.ts +++ b/src/midi/MasterBarTickLookup.ts @@ -1,7 +1,7 @@ import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; import { BeatTickLookup } from '@src/midi/BeatTickLookup'; -import { Beat } from '@src/model/Beat'; -import { MasterBar } from '@src/model/MasterBar'; +import type { Beat } from '@src/model/Beat'; +import type { MasterBar } from '@src/model/MasterBar'; /** * Represents a single point in time defining the tempo of a {@link MasterBarTickLookup}. @@ -9,7 +9,7 @@ import { MasterBar } from '@src/model/MasterBar'; */ export class MasterBarTickLookupTempoChange { /** - * Gets or sets the tick position within the {@link MasterBarTickLookup#start} and {@link MasterBarTickLookup#end} range. + * Gets or sets the tick position within the {@link MasterBarTickLookup.start} and {@link MasterBarTickLookup.end} range. */ public tick: number; @@ -80,7 +80,7 @@ export class MasterBarTickLookup { } currentBeat.nextBeat = newBeat; - if (currentBeat == this.lastBeat) { + if (currentBeat === this.lastBeat) { this.lastBeat = newBeat; } } @@ -106,7 +106,7 @@ export class MasterBarTickLookup { currentBeat.previousBeat.nextBeat = newBeat; } currentBeat.previousBeat = newBeat; - if (currentBeat == this.firstBeat) { + if (currentBeat === this.firstBeat) { this.firstBeat = newBeat; } } @@ -276,7 +276,7 @@ export class MasterBarTickLookup { if (sliceStart < l1.start) { // Variant D // Variant E - if (end == l1.start) { + if (end === l1.start) { // using firstBeat.start here allows merge of D & E const n1 = new BeatTickLookup(sliceStart, l1.start); n1.highlightBeat(beat, beatPlaybackStart); @@ -299,7 +299,7 @@ export class MasterBarTickLookup { l1.start = end; } // Variant G - else if (end == l1.end) { + else if (end === l1.end) { const n1 = new BeatTickLookup(sliceStart, l1.start); n1.highlightBeat(beat, beatPlaybackStart); @@ -320,7 +320,7 @@ export class MasterBarTickLookup { } } else if (sliceStart > l1.start) { // variant I - if (end == l1.end) { + if (end === l1.end) { const n1 = new BeatTickLookup(l1.start, sliceStart); for (const b of l1.highlightedBeats) { n1.highlightBeat(b.beat, b.playbackStart); diff --git a/src/midi/MidiEvent.ts b/src/midi/MidiEvent.ts index 8c6b51b19..22d835ac8 100644 --- a/src/midi/MidiEvent.ts +++ b/src/midi/MidiEvent.ts @@ -1,6 +1,6 @@ -import { IWriteable } from '@src/io/IWriteable'; -import { MidiFile } from './MidiFile'; -import { ControllerType } from './ControllerType'; +import type { IWriteable } from '@src/io/IWriteable'; +import { MidiFile } from '@src/midi/MidiFile'; +import type { ControllerType } from '@src/midi/ControllerType'; import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { IOHelper } from '@src/io/IOHelper'; @@ -9,7 +9,7 @@ import { IOHelper } from '@src/io/IOHelper'; * Lists all midi event types. Based on the type the instance is a specific subclass. */ export enum MidiEventType { - // NOTE: the values try to be backwards compatible with alphaTab 1.2. + // NOTE: the values try to be backwards compatible with alphaTab 1.2. // Some values are aligned with the MIDI1.0 bytes while some others // try to resemble the kind (e.g. 0xF1 -> 0xF0 as system exclusive, and +1 for the first event we define) // For the custom values we try to not overlap with real MIDI values. @@ -17,28 +17,28 @@ export enum MidiEventType { TimeSignature = 0x58, // 0xFF _0x58_ in Midi 1.0 NoteOn = 0x80, // Aligned with Midi 1.0 NoteOff = 0x90, // Aligned with Midi 1.0 - ControlChange = 0xB0, // Aligned with Midi 1.0 - ProgramChange = 0xC0, // Aligned with Midi 1.0 - TempoChange = 0x51, // 0xFF _0x51_ in Midi 1.0 - PitchBend = 0xE0, // Aligned with Midi 1.0 + ControlChange = 0xb0, // Aligned with Midi 1.0 + ProgramChange = 0xc0, // Aligned with Midi 1.0 + TempoChange = 0x51, // 0xFF _0x51_ in Midi 1.0 + PitchBend = 0xe0, // Aligned with Midi 1.0 PerNotePitchBend = 0x60, // Aligned with Midi 2.0 - EndOfTrack = 0x2F, // 0xFF _0x2F_ in Midi 1.0 - AlphaTabRest = 0xF1, // SystemExclusive + 1 - AlphaTabMetronome = 0xF2, // SystemExclusive + 2 + EndOfTrack = 0x2f, // 0xFF _0x2F_ in Midi 1.0 + AlphaTabRest = 0xf1, // SystemExclusive + 1 + AlphaTabMetronome = 0xf2, // SystemExclusive + 2 // deprecated events /** * @deprecated Not used anymore internally. move to the other concrete types. */ - SystemExclusive = 0xF0, // Aligned with Midi 1.0 + SystemExclusive = 0xf0, // Aligned with Midi 1.0 /** * @deprecated Not used anymore internally. move to the other concrete types. */ - SystemExclusive2 = 0xF7, // Aligned with Midi 1.0 + SystemExclusive2 = 0xf7, // Aligned with Midi 1.0 /** * @deprecated Not used anymore internally. move to the other concrete types. */ - Meta = 0xFF // Aligned with Midi 1.0 + Meta = 0xff // Aligned with Midi 1.0 } /** @@ -46,7 +46,7 @@ export enum MidiEventType { */ export abstract class MidiEvent { /** - * Gets or sets the track to which the midi event belongs. + * Gets or sets the track to which the midi event belongs. */ public track: number; @@ -81,6 +81,7 @@ export abstract class MidiEvent { } /** + * The 32-bit encoded raw midi message. Deprecated {@since 1.3.0}. Use the properties of the subclasses instead. * @deprecated Use individual properties to access data. */ public get message(): number { @@ -88,12 +89,14 @@ export abstract class MidiEvent { } /** + * The first data byte. Meaning depends on midi event type. (Deprecated {@since 1.3.0}, use the specific properties of the midi event depending on type) * @deprecated Use individual properties to access data. */ public get data1(): number { return 0; } /** + * The second data byte Meaning depends on midi event type. (Deprecated {@since 1.3.0}, use the specific properties of the midi event depending on type) * @deprecated Use individual properties to access data. */ public get data2(): number { @@ -108,11 +111,11 @@ export abstract class MidiEvent { } /** - * Represents a time signature change event. + * Represents a time signature change event. */ export class TimeSignatureEvent extends MidiEvent { /** - * The time signature numerator. + * The time signature numerator. */ public numerator: number; @@ -132,11 +135,14 @@ export class TimeSignatureEvent extends MidiEvent { */ public thirtySecondNodesInQuarter: number; - public constructor(track: number, tick: number, + public constructor( + track: number, + tick: number, numerator: number, denominatorIndex: number, midiClocksPerMetronomeClick: number, - thirtySecondNodesInQuarter: number) { + thirtySecondNodesInQuarter: number + ) { super(track, tick, MidiEventType.TimeSignature); this.track = track; this.tick = tick; @@ -148,16 +154,16 @@ export class TimeSignatureEvent extends MidiEvent { public override writeTo(s: IWriteable): void { // meta header - s.writeByte(0xFF); + s.writeByte(0xff); // time signature s.writeByte(0x58); // size MidiFile.writeVariableInt(s, 4); // Data - s.writeByte(this.numerator & 0xFF); - s.writeByte(this.denominatorIndex & 0xFF); - s.writeByte(this.midiClocksPerMetronomeClick & 0xFF); - s.writeByte(this.thirtySecondNodesInQuarter & 0xFF); + s.writeByte(this.numerator & 0xff); + s.writeByte(this.denominatorIndex & 0xff); + s.writeByte(this.midiClocksPerMetronomeClick & 0xff); + s.writeByte(this.thirtySecondNodesInQuarter & 0xff); } } @@ -165,24 +171,20 @@ export class TimeSignatureEvent extends MidiEvent { * The base class for alphaTab specific midi events (like metronomes and rests). */ export abstract class AlphaTabSysExEvent extends MidiEvent { - public static readonly AlphaTabManufacturerId = 0x7D; + public static readonly AlphaTabManufacturerId = 0x7d; public static readonly MetronomeEventId = 0x00; public static readonly RestEventId = 0x01; - public constructor(track: number, tick: number, type: MidiEventType) { - super(track, tick, type); - } - public override writeTo(s: IWriteable): void { // sysex - s.writeByte(0xF0); + s.writeByte(0xf0); // data const data = ByteBuffer.withCapacity(16); data.writeByte(AlphaTabSysExEvent.AlphaTabManufacturerId); this.writeEventData(data); // syntactic sysex end - data.writeByte(0xF7); + data.writeByte(0xf7); MidiFile.writeVariableInt(s, data.length); data.copyTo(s); @@ -192,7 +194,7 @@ export abstract class AlphaTabSysExEvent extends MidiEvent { } /** - * Represents a metronome event. This event is emitted by the synthesizer only during playback and + * Represents a metronome event. This event is emitted by the synthesizer only during playback and * is typically not part of the midi file itself. */ export class AlphaTabMetronomeEvent extends AlphaTabSysExEvent { @@ -202,16 +204,15 @@ export class AlphaTabMetronomeEvent extends AlphaTabSysExEvent { public metronomeNumerator: number; /** - * The duration of the metronome tick in MIDI ticks. + * The duration of the metronome tick in MIDI ticks. */ public metronomeDurationInTicks: number; /** - * The duration of the metronome tick in milliseconds. + * The duration of the metronome tick in milliseconds. */ public metronomeDurationInMilliseconds: number; - // for backwards compatibility. /** @@ -219,7 +220,9 @@ export class AlphaTabMetronomeEvent extends AlphaTabSysExEvent { */ public readonly isMetronome: boolean = true; - public constructor(track: number, tick: number, + public constructor( + track: number, + tick: number, counter: number, durationInTicks: number, durationInMillis: number @@ -239,7 +242,7 @@ export class AlphaTabMetronomeEvent extends AlphaTabSysExEvent { } /** - * Represents a REST beat being 'played'. This event supports alphaTab in placing the cursor. + * Represents a REST beat being 'played'. This event supports alphaTab in placing the cursor. */ export class AlphaTabRestEvent extends AlphaTabSysExEvent { public channel: number; @@ -265,7 +268,7 @@ export abstract class NoteEvent extends MidiEvent { public channel: number; /** - * The key of the note being played (aka. the note height). + * The key of the note being played (aka. the note height). */ public noteKey: number; @@ -274,12 +277,14 @@ export abstract class NoteEvent extends MidiEvent { */ public noteVelocity: number; - public constructor(track: number, + public constructor( + track: number, tick: number, type: MidiEventType, channel: number, noteKey: number, - noteVelocity: number) { + noteVelocity: number + ) { super(track, tick, type); this.channel = channel; @@ -300,19 +305,15 @@ export abstract class NoteEvent extends MidiEvent { * Represents a note being played */ export class NoteOnEvent extends NoteEvent { - public constructor(track: number, - tick: number, - channel: number, - noteKey: number, - noteVelocity: number) { + public constructor(track: number, tick: number, channel: number, noteKey: number, noteVelocity: number) { super(track, tick, MidiEventType.NoteOn, channel, noteKey, noteVelocity); } public override writeTo(s: IWriteable): void { // status byte - s.writeByte((this.channel & 0x0F) | 0x90) - s.writeByte(this.noteKey & 0xFF); - s.writeByte(this.noteVelocity & 0xFF); + s.writeByte((this.channel & 0x0f) | 0x90); + s.writeByte(this.noteKey & 0xff); + s.writeByte(this.noteVelocity & 0xff); } } @@ -320,32 +321,29 @@ export class NoteOnEvent extends NoteEvent { * Represents a note stop being played. */ export class NoteOffEvent extends NoteEvent { - public constructor(track: number, tick: number, - channel: number, - noteKey: number, - noteVelocity: number) { + public constructor(track: number, tick: number, channel: number, noteKey: number, noteVelocity: number) { super(track, tick, MidiEventType.NoteOff, channel, noteKey, noteVelocity); } public override writeTo(s: IWriteable): void { // status byte - s.writeByte((this.channel & 0x0F) | 0x80) - s.writeByte(this.noteKey & 0xFF); - s.writeByte(this.noteVelocity & 0xFF); + s.writeByte((this.channel & 0x0f) | 0x80); + s.writeByte(this.noteKey & 0xff); + s.writeByte(this.noteVelocity & 0xff); } } /** - * Represents the change of a value on a midi controller. + * Represents the change of a value on a midi controller. */ export class ControlChangeEvent extends MidiEvent { /** - * The channel for which the controller is changing. + * The channel for which the controller is changing. */ public channel: number; /** - * The type of the controller which is changing. + * The type of the controller which is changing. */ public controller: ControllerType; @@ -354,11 +352,7 @@ export class ControlChangeEvent extends MidiEvent { */ public value: number; - public constructor(track: number, - tick: number, - channel: number, - controller: ControllerType, - value: number) { + public constructor(track: number, tick: number, channel: number, controller: ControllerType, value: number) { super(track, tick, MidiEventType.ControlChange); this.channel = channel; this.controller = controller; @@ -366,9 +360,9 @@ export class ControlChangeEvent extends MidiEvent { } public override writeTo(s: IWriteable): void { - s.writeByte((this.channel & 0x0F) | 0xB0) - s.writeByte((this.controller as number) & 0xFF); - s.writeByte(this.value & 0xFF); + s.writeByte((this.channel & 0x0f) | 0xb0); + s.writeByte((this.controller as number) & 0xff); + s.writeByte(this.value & 0xff); } public override get data1(): number { @@ -381,31 +375,28 @@ export class ControlChangeEvent extends MidiEvent { } /** - * Represents the change of the midi program on a channel. + * Represents the change of the midi program on a channel. */ export class ProgramChangeEvent extends MidiEvent { /** - * The midi channel for which the program changes. + * The midi channel for which the program changes. */ public channel: number; /** - * The numeric value of the program indicating the instrument bank to choose. + * The numeric value of the program indicating the instrument bank to choose. */ public program: number; - public constructor(track: number, - tick: number, - channel: number, - program: number) { + public constructor(track: number, tick: number, channel: number, program: number) { super(track, tick, MidiEventType.ProgramChange); this.channel = channel; this.program = program; } public override writeTo(s: IWriteable): void { - s.writeByte((this.channel & 0x0F) | 0xC0) - s.writeByte(this.program & 0xFF); + s.writeByte((this.channel & 0x0f) | 0xc0); + s.writeByte(this.program & 0xff); } public override get data1(): number { @@ -414,11 +405,11 @@ export class ProgramChangeEvent extends MidiEvent { } /** - * Represents a change of the tempo in the song. + * Represents a change of the tempo in the song. */ export class TempoChangeEvent extends MidiEvent { /** - * The tempo in microseconds per quarter note (aka USQ). A time format typically for midi. + * The tempo in microseconds per quarter note (aka USQ). A time format typically for midi. */ public microSecondsPerQuarterNote: number; @@ -429,20 +420,20 @@ export class TempoChangeEvent extends MidiEvent { public override writeTo(s: IWriteable): void { // meta - s.writeByte(0xFF); + s.writeByte(0xff); // set tempo s.writeByte(0x51); // size s.writeByte(0x03); - // tempo - s.writeByte((this.microSecondsPerQuarterNote >> 16) & 0xFF); - s.writeByte((this.microSecondsPerQuarterNote >> 8) & 0xFF); - s.writeByte(this.microSecondsPerQuarterNote & 0xFF); + // tempo + s.writeByte((this.microSecondsPerQuarterNote >> 16) & 0xff); + s.writeByte((this.microSecondsPerQuarterNote >> 8) & 0xff); + s.writeByte(this.microSecondsPerQuarterNote & 0xff); } } /** - * Represents a change of the pitch bend (aka. pitch wheel) on a specific channel. + * Represents a change of the pitch bend (aka. pitch wheel) on a specific channel. */ export class PitchBendEvent extends MidiEvent { /** @@ -455,42 +446,38 @@ export class PitchBendEvent extends MidiEvent { */ public value: number; - public constructor(track: number, - tick: number, - channel: number, - value: number) { + public constructor(track: number, tick: number, channel: number, value: number) { super(track, tick, MidiEventType.PitchBend); this.channel = channel; this.value = value; } public override writeTo(s: IWriteable): void { - s.writeByte((this.channel & 0x0F) | 0xE0) - s.writeByte(this.value & 0x7F); - s.writeByte((this.value >> 7) & 0x7F); + s.writeByte((this.channel & 0x0f) | 0xe0); + s.writeByte(this.value & 0x7f); + s.writeByte((this.value >> 7) & 0x7f); } public override get data1(): number { - return this.value & 0x7F; + return this.value & 0x7f; } public override get data2(): number { - return (this.value >> 7) & 0x7F; + return (this.value >> 7) & 0x7f; } } - /** - * Represents a single note pitch bend change. + * Represents a single note pitch bend change. */ export class NoteBendEvent extends MidiEvent { /** - * The channel on which the note is played for which the pitch changes. + * The channel on which the note is played for which the pitch changes. */ public channel: number; /** - * The key of the note for which the pitch changes. + * The key of the note for which the pitch changes. */ public noteKey: number; @@ -499,11 +486,7 @@ export class NoteBendEvent extends MidiEvent { */ public value: number; - public constructor(track: number, - tick: number, - channel: number, - noteKey: number, - value: number) { + public constructor(track: number, tick: number, channel: number, noteKey: number, value: number) { super(track, tick, MidiEventType.PerNotePitchBend); this.channel = channel; this.noteKey = noteKey; @@ -524,8 +507,8 @@ export class EndOfTrackEvent extends MidiEvent { } public override writeTo(s: IWriteable): void { - s.writeByte(0xFF); - s.writeByte(0x2F); + s.writeByte(0xff); + s.writeByte(0x2f); s.writeByte(0x00); } } diff --git a/src/midi/MidiFile.ts b/src/midi/MidiFile.ts index f98431535..d537ffadb 100644 --- a/src/midi/MidiFile.ts +++ b/src/midi/MidiFile.ts @@ -1,7 +1,7 @@ -import { MidiEvent } from '@src/midi/MidiEvent'; +import type { MidiEvent } from '@src/midi/MidiEvent'; import { MidiUtils } from '@src/midi/MidiUtils'; import { ByteBuffer } from '@src/io/ByteBuffer'; -import { IWriteable } from '@src/io/IWriteable'; +import type { IWriteable } from '@src/io/IWriteable'; import { IOHelper } from '@src/io/IOHelper'; /** @@ -18,7 +18,6 @@ export enum MidiFileFormat { MultiTrack = 1 } - export class MidiTrack { /** * Gets a list of midi events sorted by time. @@ -31,8 +30,7 @@ export class MidiTrack { public addEvent(e: MidiEvent): void { if (this.events.length === 0 || e.tick >= this.events[this.events.length - 1].tick) { this.events.push(e); - } - else { + } else { let insertPos: number = this.events.length; while (insertPos > 0) { const prevItem: MidiEvent = this.events[insertPos - 1]; @@ -52,10 +50,10 @@ export class MidiTrack { */ public writeTo(s: IWriteable): void { // build track data first - let trackData: ByteBuffer = ByteBuffer.empty(); + const trackData: ByteBuffer = ByteBuffer.empty(); let previousTick: number = 0; - for (let midiEvent of this.events) { - let delta: number = midiEvent.tick - previousTick; + for (const midiEvent of this.events) { + const delta: number = midiEvent.tick - previousTick; MidiFile.writeVariableInt(trackData, delta); midiEvent.writeTo(trackData); previousTick = midiEvent.tick; @@ -65,7 +63,7 @@ export class MidiTrack { const b = new Uint8Array([0x4d, 0x54, 0x72, 0x6b]); s.write(b, 0, b.length); // size as integer - let data: Uint8Array = trackData.toArray(); + const data: Uint8Array = trackData.toArray(); IOHelper.writeInt32BE(s, data.length); s.write(data, 0, data.length); } @@ -89,17 +87,16 @@ export class MidiFile { * Gets a list of midi events sorted by time. */ public get events(): MidiEvent[] { - if (this.tracks.length == 1) { + if (this.tracks.length === 1) { return this.tracks[0].events; - } else { - const events: MidiEvent[] = []; - for (const t of this.tracks) { - this.events.push(...t.events); - } - - events.sort((a, b) => a.tick - b.tick); - return events; } + const events: MidiEvent[] = []; + for (const t of this.tracks) { + this.events.push(...t.events); + } + + events.sort((a, b) => a.tick - b.tick); + return events; } /** @@ -117,7 +114,7 @@ export class MidiFile { * Adds the given midi event a the correct time position into the file. */ public addEvent(e: MidiEvent): void { - if (this.format == MidiFileFormat.SingleTrackMultiChannel) { + if (this.format === MidiFileFormat.SingleTrackMultiChannel) { this.ensureTracks(1); this.tracks[0].addEvent(e); } else { @@ -131,7 +128,7 @@ export class MidiFile { * @returns The binary midi file. */ public toBinary(): Uint8Array { - let data: ByteBuffer = ByteBuffer.empty(); + const data: ByteBuffer = ByteBuffer.empty(); this.writeTo(data); return data.toArray(); } @@ -142,7 +139,7 @@ export class MidiFile { */ public writeTo(s: IWriteable): void { // magic number "MThd" (0x4D546864) - let b: Uint8Array = new Uint8Array([0x4d, 0x54, 0x68, 0x64]); + const b: Uint8Array = new Uint8Array([0x4d, 0x54, 0x68, 0x64]); s.write(b, 0, b.length); // Header Length 6 (0x00000006) IOHelper.writeInt32BE(s, 6); @@ -159,7 +156,7 @@ export class MidiFile { } public static writeVariableInt(s: IWriteable, value: number): void { - let array: Uint8Array = new Uint8Array(4); + const array: Uint8Array = new Uint8Array(4); let n: number = 0; do { array[n++] = value & 0x7f; diff --git a/src/midi/MidiFileGenerator.ts b/src/midi/MidiFileGenerator.ts index 4314ef57e..70a647194 100644 --- a/src/midi/MidiFileGenerator.ts +++ b/src/midi/MidiFileGenerator.ts @@ -1,5 +1,5 @@ import { ControllerType } from '@src/midi/ControllerType'; -import { IMidiFileHandler } from '@src/midi/IMidiFileHandler'; +import type { IMidiFileHandler } from '@src/midi/IMidiFileHandler'; import { MidiPlaybackController } from '@src/midi/MidiPlaybackController'; import { MasterBarTickLookup, MasterBarTickLookupTempoChange } from '@src/midi/MasterBarTickLookup'; @@ -7,27 +7,27 @@ import { MidiTickLookup } from '@src/midi/MidiTickLookup'; import { MidiUtils } from '@src/midi/MidiUtils'; import { AccentuationType } from '@src/model/AccentuationType'; -import { Automation, AutomationType } from '@src/model/Automation'; -import { Bar } from '@src/model/Bar'; -import { Beat } from '@src/model/Beat'; +import { type Automation, AutomationType } from '@src/model/Automation'; +import type { Bar } from '@src/model/Bar'; +import type { Beat } from '@src/model/Beat'; import { BendPoint } from '@src/model/BendPoint'; import { BendStyle } from '@src/model/BendStyle'; import { BendType } from '@src/model/BendType'; import { BrushType } from '@src/model/BrushType'; import { Duration } from '@src/model/Duration'; import { GraceType } from '@src/model/GraceType'; -import { MasterBar } from '@src/model/MasterBar'; -import { Note } from '@src/model/Note'; -import { PlaybackInformation } from '@src/model/PlaybackInformation'; -import { Score } from '@src/model/Score'; +import type { MasterBar } from '@src/model/MasterBar'; +import type { Note } from '@src/model/Note'; +import type { PlaybackInformation } from '@src/model/PlaybackInformation'; +import type { Score } from '@src/model/Score'; import { SimileMark } from '@src/model/SimileMark'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; -import { Staff } from '@src/model/Staff'; -import { Track } from '@src/model/Track'; +import type { Staff } from '@src/model/Staff'; +import type { Track } from '@src/model/Track'; import { TripletFeel } from '@src/model/TripletFeel'; import { VibratoType } from '@src/model/VibratoType'; -import { Voice } from '@src/model/Voice'; +import type { Voice } from '@src/model/Voice'; import { WhammyType } from '@src/model/WhammyType'; import { NotationMode } from '@src/NotationSettings'; import { Settings } from '@src/Settings'; @@ -35,7 +35,7 @@ import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { SynthConstants } from '@src/synth/SynthConstants'; import { PercussionMapper } from '@src/model/PercussionMapper'; -import { DynamicValue } from '@src/model'; +import { DynamicValue } from '@src/model/DynamicValue'; import { FadeType } from '@src/model/FadeType'; import { NoteOrnament } from '@src/model/NoteOrnament'; import { Rasgueado } from '@src/model/Rasgueado'; @@ -193,8 +193,8 @@ export class MidiFileGenerator { : -track.staves[0].transpositionPitch; this.transpositionPitches.set(channel, transpositionPitch); - let volume: number = MidiFileGenerator.toChannelShort(playbackInfo.volume); - let balance: number = MidiFileGenerator.toChannelShort(playbackInfo.balance); + const volume: number = MidiFileGenerator.toChannelShort(playbackInfo.volume); + const balance: number = MidiFileGenerator.toChannelShort(playbackInfo.balance); this._handler.addControlChange(track.index, 0, channel, ControllerType.VolumeCoarse, volume); this._handler.addControlChange(track.index, 0, channel, ControllerType.PanCoarse, balance); this._handler.addControlChange(track.index, 0, channel, ControllerType.ExpressionControllerCoarse, 127); @@ -267,7 +267,7 @@ export class MidiFileGenerator { } private generateBar(bar: Bar, barStartTick: number, tempoOnBarStart: number): void { - let playbackBar: Bar = this.getPlaybackBar(bar); + const playbackBar: Bar = this.getPlaybackBar(bar); const barStartTime = this._currentTime; for (const v of playbackBar.voices) { @@ -315,11 +315,7 @@ export class MidiFileGenerator { // in case of simile marks where we repeat we register the empty beat for the whole bar if (playbackBar.id !== bar.id) { - this.tickLookup.addBeat( - bar.voices[0].beats[0], - 0, - tickDuration - ); + this.tickLookup.addBeat(bar.voices[0].beats[0], 0, tickDuration); //this.tickLookup.addBeat(beat, 0, audioDuration); } } @@ -411,8 +407,8 @@ export class MidiFileGenerator { } else if (beat.deadSlapped) { this.generateDeadSlap(beat, barStartTick + beatStart); } else { - let brushInfo = this.getBrushInfo(beat); - let rasgueadoInfo = this.getRasgueadoInfo(beat, audioDuration); + const brushInfo = this.getBrushInfo(beat); + const rasgueadoInfo = this.getRasgueadoInfo(beat, audioDuration); for (const n of beat.notes) { this.generateNote( n, @@ -547,7 +543,7 @@ export class MidiFileGenerator { beatStart, deadSlapDuration, t, - MidiUtils.dynamicToVelocity(DynamicValue.F as number), + MidiUtils.dynamicToVelocity(DynamicValue.F), staff.track.playbackInfo.primaryChannel ); } @@ -615,7 +611,7 @@ export class MidiFileGenerator { const channel: number = this.determineChannel(track, note); let initialBend: number = 0; - let noteSoundDuration: number = Math.max(noteDuration.untilTieOrSlideEnd, noteDuration.letRingEnd); + const noteSoundDuration: number = Math.max(noteDuration.untilTieOrSlideEnd, noteDuration.letRingEnd); if (note.hasBend) { initialBend = MidiFileGenerator.getPitchWheel(note.bendPoints![0].value); @@ -695,18 +691,8 @@ export class MidiFileGenerator { */ // prettier-ignore private static readonly OrnamentKeysUp = [ - /* C -> D */ 2, - /* C# -> D# */ 2, - /* D -> E */ 2, - /* D# -> E */ 1, - /* E -> F */ 1, - /* F -> G */ 2, - /* F# -> G# */ 2, - /* G -> A */ 2, - /* G# -> A# */ 2, - /* A -> B */ 2, - /* A# -> B */ 1, - /* B -> C */ 1 + /* C -> D */ 2, /* C# -> D# */ 2, /* D -> E */ 2, /* D# -> E */ 1, /* E -> F */ 1, /* F -> G */ 2, + /* F# -> G# */ 2, /* G -> A */ 2, /* G# -> A# */ 2, /* A -> B */ 2, /* A# -> B */ 1, /* B -> C */ 1 ]; /** @@ -717,18 +703,8 @@ export class MidiFileGenerator { */ // prettier-ignore private static readonly OrnamentKeysDown = [ - /* C -> B */ -1, - /* C# -> C */ -1, - /* D -> C# */ -1, - /* D# -> D */ -1, - /* E -> D# */ -1, - /* F -> E */ -1, - /* F# -> F */ -1, - /* G -> F# */ -1, - /* G# -> G */ -1, - /* A -> G# */ -1, - /* A# -> A */ -1, - /* B -> A# */ -1 + /* C -> B */ -1, /* C# -> C */ -1, /* D -> C# */ -1, /* D# -> D */ -1, /* E -> D# */ -1, /* F -> E */ -1, + /* F# -> F */ -1, /* G -> F# */ -1, /* G# -> G */ -1, /* A -> G# */ -1, /* A# -> A */ -1, /* B -> A# */ -1 ]; private generateOrnament( @@ -912,7 +888,7 @@ export class MidiFileGenerator { let letRingEnd: number = 0; const maxDuration: number = note.beat.voice.bar.masterBar.calculateDuration(); while (lastLetRingBeat.nextBeat) { - let next: Beat = lastLetRingBeat.nextBeat; + const next: Beat = lastLetRingBeat.nextBeat; if (next.isRest) { break; } @@ -947,26 +923,26 @@ export class MidiFileGenerator { } private static getNoteVelocity(note: Note): number { - let dynamicValue: number = note.dynamics as number; + let adjustment: number = 0; // more silent on hammer destination if (!note.beat.voice.bar.staff.isPercussion && note.hammerPullOrigin) { - dynamicValue--; + adjustment--; } // more silent on ghost notes if (note.isGhost) { - dynamicValue--; + adjustment--; } // louder on accent switch (note.accentuated) { case AccentuationType.Normal: - dynamicValue++; + adjustment++; break; case AccentuationType.Heavy: - dynamicValue += 2; + adjustment += 2; break; } - return MidiUtils.dynamicToVelocity(dynamicValue); + return MidiUtils.dynamicToVelocity(note.dynamics, adjustment); } private generateFade(beat: Beat, beatStart: number, beatDuration: number): void { @@ -1063,8 +1039,8 @@ export class MidiFileGenerator { note.vibrato !== VibratoType.None ? note.vibrato : note.isTieDestination - ? note.tieOrigin!.vibrato - : VibratoType.Slight; /* should never happen unless called wrongly */ + ? note.tieOrigin!.vibrato + : VibratoType.Slight; /* should never happen unless called wrongly */ switch (vibratoType) { case VibratoType.Slight: phaseLength = this._settings.player.vibrato.noteSlightLength; @@ -1113,7 +1089,7 @@ export class MidiFileGenerator { let phase: number = 0; const phaseDuration: number = noteStart + phaseLength < noteEnd ? phaseLength : noteEnd - noteStart; while (phase < phaseDuration) { - let bend: number = bendBase + bendAmplitude * Math.sin((phase * Math.PI) / phaseHalf); + const bend: number = bendBase + bendAmplitude * Math.sin((phase * Math.PI) / phaseHalf); addBend((noteStart + phase) | 0, MidiFileGenerator.getPitchWheel(bend)); phase += resolution; } @@ -1161,10 +1137,10 @@ export class MidiFileGenerator { channel: number, tempoOnBeatStart: number ) { - let duration: number = + const duration: number = note.slideOutType === SlideOutType.Legato ? noteDuration.noteOnly : noteDuration.untilTieOrSlideEnd; - let playedBendPoints: BendPoint[] = []; - let track: Track = note.beat.voice.bar.staff.track; + const playedBendPoints: BendPoint[] = []; + const track: Track = note.beat.voice.bar.staff.track; const simpleSlidePitchOffset = this._settings.player.slide.simpleSlidePitchOffset; const simpleSlideDurationOffset = Math.floor( @@ -1227,8 +1203,8 @@ export class MidiFileGenerator { channel: number, tempoOnBeatStart: number ): void { - let bendPoints: BendPoint[] = note.bendPoints!; - let track: Track = note.beat.voice.bar.staff.track; + const bendPoints: BendPoint[] = note.bendPoints!; + const track: Track = note.beat.voice.bar.staff.track; const addBend: (tick: number, value: number) => void = (tick, value) => { this._handler.addNoteBend(track.index, tick, channel, noteKey, value); @@ -1243,7 +1219,7 @@ export class MidiFileGenerator { while ( endNote.isTieOrigin && !endNote.tieDestination!.hasBend && - endNote.tieDestination!.vibrato == VibratoType.None + endNote.tieDestination!.vibrato === VibratoType.None ) { endNote = endNote.tieDestination!; } @@ -1646,7 +1622,7 @@ export class MidiFileGenerator { let trillLength: number = MidiUtils.toTicks(note.trillSpeed); let realKey: boolean = true; let tick: number = noteStart; - let end: number = noteStart + noteDuration.untilTieOrSlideEnd; + const end: number = noteStart + noteDuration.untilTieOrSlideEnd; while (tick + 10 < end) { // only the rest on last trill play if (tick + trillLength >= end) { @@ -1947,7 +1923,7 @@ export class MidiFileGenerator { let brushMove: number = 0; const brushIncrement: number = (brushDuration / (stringCount - 1)) | 0; for (let i: number = 0; i < beat.voice.bar.staff.tuning.length; i++) { - let index: number = down ? i : brushInfo.length - 1 - i; + const index: number = down ? i : brushInfo.length - 1 - i; if ((stringUsed & (0x01 << index)) !== 0) { brushInfo[index] = brushMove; brushMove += brushIncrement; @@ -1975,7 +1951,7 @@ export class MidiFileGenerator { ); break; case AutomationType.Balance: - let balance: number = MidiFileGenerator.toChannelShort(automation.value); + const balance: number = MidiFileGenerator.toChannelShort(automation.value); this._handler.addControlChange( beat.voice.bar.staff.track.index, beat.playbackStart + startMove, @@ -1992,7 +1968,7 @@ export class MidiFileGenerator { ); break; case AutomationType.Volume: - let volume: number = MidiFileGenerator.toChannelShort(automation.value); + const volume: number = MidiFileGenerator.toChannelShort(automation.value); this._handler.addControlChange( beat.voice.bar.staff.track.index, beat.playbackStart + startMove, @@ -2058,7 +2034,7 @@ export class MidiFileGenerator { this._handler.addTimeSignature(0, masterBar.timeSignatureNumerator, masterBar.timeSignatureDenominator); this._handler.addTempo(0, tempo); - let volumeCoarse: number = MidiFileGenerator.toChannelShort(volume); + const volumeCoarse: number = MidiFileGenerator.toChannelShort(volume); this._handler.addControlChange( 0, 0, diff --git a/src/midi/MidiPlaybackController.ts b/src/midi/MidiPlaybackController.ts index 74ddc5240..ab9b5f704 100644 --- a/src/midi/MidiPlaybackController.ts +++ b/src/midi/MidiPlaybackController.ts @@ -1,7 +1,7 @@ -import { RepeatGroup } from '@src/model'; +import type { RepeatGroup } from '@src/model/RepeatGroup'; import { Direction } from '@src/model/Direction'; -import { MasterBar } from '@src/model/MasterBar'; -import { Score } from '@src/model/Score'; +import type { MasterBar } from '@src/model/MasterBar'; +import type { Score } from '@src/model/Score'; /** * Helper container to handle repeats correctly @@ -25,27 +25,27 @@ enum MidiPlaybackControllerState { /** * Normally playing with repeats. */ - PlayingNormally, + PlayingNormally = 0, /** * We "jumped" to a new location (e.g. via Da Capo). So we're ignoring repeats. */ - DirectionJumped, + DirectionJumped = 1, /** * We "jumped" to a new location via a 'al Coda' jump, hence respecting 'DaCoda' now. */ - DirectionJumpedAlCoda, + DirectionJumpedAlCoda = 2, /** * We "jumped" to a new location via a 'al Double Coda' jump, hence respecting 'DaDoubleCoda' now. */ - DirectionJumpedAlDoubleCoda, + DirectionJumpedAlDoubleCoda = 3, /** * We "jumped" to a new location via a 'al Fine' jump, hence respecting 'Fine' now. */ - DirectionJumpedAlFine + DirectionJumpedAlFine = 4 } export class MidiPlaybackController { @@ -213,28 +213,90 @@ export class MidiPlaybackController { case MidiPlaybackControllerState.PlayingNormally: // Da capo Jumps (to start) // prettier-ignore - if (this.handleDaCapo(masterBar.directions!, Direction.JumpDaCapo, MidiPlaybackControllerState.DirectionJumped) || - this.handleDaCapo(masterBar.directions!, Direction.JumpDaCapoAlCoda, MidiPlaybackControllerState.DirectionJumpedAlCoda) || - this.handleDaCapo(masterBar.directions!, Direction.JumpDaCapoAlDoubleCoda, MidiPlaybackControllerState.DirectionJumpedAlDoubleCoda) || - this.handleDaCapo(masterBar.directions!, Direction.JumpDaCapoAlFine, MidiPlaybackControllerState.DirectionJumpedAlFine)) { + if ( + this.handleDaCapo( + masterBar.directions!, + Direction.JumpDaCapo, + MidiPlaybackControllerState.DirectionJumped + ) || + this.handleDaCapo( + masterBar.directions!, + Direction.JumpDaCapoAlCoda, + MidiPlaybackControllerState.DirectionJumpedAlCoda + ) || + this.handleDaCapo( + masterBar.directions!, + Direction.JumpDaCapoAlDoubleCoda, + MidiPlaybackControllerState.DirectionJumpedAlDoubleCoda + ) || + this.handleDaCapo( + masterBar.directions!, + Direction.JumpDaCapoAlFine, + MidiPlaybackControllerState.DirectionJumpedAlFine + ) + ) { return true; } // Dal Segno Jumps // prettier-ignore - if (this.handleDalSegno(masterBar.directions!, Direction.JumpDalSegno, MidiPlaybackControllerState.DirectionJumped, Direction.TargetSegno) || - this.handleDalSegno(masterBar.directions!, Direction.JumpDalSegnoAlCoda, MidiPlaybackControllerState.DirectionJumpedAlCoda, Direction.TargetSegno) || - this.handleDalSegno(masterBar.directions!, Direction.JumpDalSegnoAlDoubleCoda, MidiPlaybackControllerState.DirectionJumpedAlDoubleCoda, Direction.TargetSegno) || - this.handleDalSegno(masterBar.directions!, Direction.JumpDalSegnoAlFine, MidiPlaybackControllerState.DirectionJumpedAlFine, Direction.TargetSegno)) { + if ( + this.handleDalSegno( + masterBar.directions!, + Direction.JumpDalSegno, + MidiPlaybackControllerState.DirectionJumped, + Direction.TargetSegno + ) || + this.handleDalSegno( + masterBar.directions!, + Direction.JumpDalSegnoAlCoda, + MidiPlaybackControllerState.DirectionJumpedAlCoda, + Direction.TargetSegno + ) || + this.handleDalSegno( + masterBar.directions!, + Direction.JumpDalSegnoAlDoubleCoda, + MidiPlaybackControllerState.DirectionJumpedAlDoubleCoda, + Direction.TargetSegno + ) || + this.handleDalSegno( + masterBar.directions!, + Direction.JumpDalSegnoAlFine, + MidiPlaybackControllerState.DirectionJumpedAlFine, + Direction.TargetSegno + ) + ) { return true; } // Dal SegnoSegno Jumps // prettier-ignore - if (this.handleDalSegno(masterBar.directions!, Direction.JumpDalSegnoSegno, MidiPlaybackControllerState.DirectionJumped, Direction.TargetSegnoSegno) || - this.handleDalSegno(masterBar.directions!, Direction.JumpDalSegnoSegnoAlCoda, MidiPlaybackControllerState.DirectionJumpedAlCoda, Direction.TargetSegnoSegno) || - this.handleDalSegno(masterBar.directions!, Direction.JumpDalSegnoSegnoAlDoubleCoda, MidiPlaybackControllerState.DirectionJumpedAlDoubleCoda, Direction.TargetSegnoSegno) || - this.handleDalSegno(masterBar.directions!, Direction.JumpDalSegnoSegnoAlFine, MidiPlaybackControllerState.DirectionJumpedAlFine, Direction.TargetSegnoSegno)) { + if ( + this.handleDalSegno( + masterBar.directions!, + Direction.JumpDalSegnoSegno, + MidiPlaybackControllerState.DirectionJumped, + Direction.TargetSegnoSegno + ) || + this.handleDalSegno( + masterBar.directions!, + Direction.JumpDalSegnoSegnoAlCoda, + MidiPlaybackControllerState.DirectionJumpedAlCoda, + Direction.TargetSegnoSegno + ) || + this.handleDalSegno( + masterBar.directions!, + Direction.JumpDalSegnoSegnoAlDoubleCoda, + MidiPlaybackControllerState.DirectionJumpedAlDoubleCoda, + Direction.TargetSegnoSegno + ) || + this.handleDalSegno( + masterBar.directions!, + Direction.JumpDalSegnoSegnoAlFine, + MidiPlaybackControllerState.DirectionJumpedAlFine, + Direction.TargetSegnoSegno + ) + ) { return true; } @@ -289,19 +351,20 @@ export class MidiPlaybackController { * @returns the index of the masterbar found with the given direction or -1 if no masterbar with the given direction was found. */ private findJumpTarget(toFind: Direction, searchIndex: number, backwardsFirst: boolean): number { + let index: number; if (backwardsFirst) { - let index = this.findJumpTargetBackwards(toFind, searchIndex); + index = this.findJumpTargetBackwards(toFind, searchIndex); if (index === -1) { index = this.findJumpTargetForwards(toFind, searchIndex); } return index; - } else { - let index = this.findJumpTargetForwards(toFind, searchIndex); - if (index === -1) { - index = this.findJumpTargetBackwards(toFind, searchIndex); - } - return index; } + + index = this.findJumpTargetForwards(toFind, searchIndex); + if (index === -1) { + index = this.findJumpTargetBackwards(toFind, searchIndex); + } + return index; } private findJumpTargetForwards(toFind: Direction, searchIndex: number): number { diff --git a/src/midi/MidiTickLookup.ts b/src/midi/MidiTickLookup.ts index 59b8839aa..8a8ecd0f7 100644 --- a/src/midi/MidiTickLookup.ts +++ b/src/midi/MidiTickLookup.ts @@ -1,8 +1,9 @@ -import { BeatTickLookup } from '@src/midi/BeatTickLookup'; +import { Logger } from '@src/Logger'; +import type { BeatTickLookup } from '@src/midi/BeatTickLookup'; import { MasterBarTickLookup } from '@src/midi/MasterBarTickLookup'; import { MidiUtils } from '@src/midi/MidiUtils'; -import { Beat } from '@src/model/Beat'; -import { MasterBar } from '@src/model/MasterBar'; +import type { Beat } from '@src/model/Beat'; +import type { MasterBar } from '@src/model/MasterBar'; /** * Describes how a cursor should be moving. @@ -11,17 +12,17 @@ export enum MidiTickLookupFindBeatResultCursorMode { /** * Unknown/Undetermined mode. Should not happen on user level. */ - Unknown, + Unknown = 0, /** * The cursor should animate to the next beat. */ - ToNextBext, + ToNextBext = 1, /** * The cursor should animate to the end of the bar (typically on repeats and jumps) */ - ToEndOfBar + ToEndOfBar = 2 } /** @@ -63,7 +64,7 @@ export class MidiTickLookupFindBeatResult { /** * The mode how the cursor should be handled. */ - public cursorMode: MidiTickLookupFindBeatResultCursorMode = MidiTickLookupFindBeatResultCursorMode.Unknown; + public cursorMode: MidiTickLookupFindBeatResultCursorMode = MidiTickLookupFindBeatResultCursorMode.Unknown; public get start(): number { return this.masterBar.start + this.beatLookup.start; @@ -170,10 +171,18 @@ export class MidiTickLookup { */ public readonly masterBars: MasterBarTickLookup[] = []; + /** + * The information about which bars are displayed via multi-bar rests. + * The key is the start bar, and the value is the additional bars in sequential order. + * This info allows building the correct "next" beat and duration. + */ + public multiBarRestInfo: Map | null = null; + /** * Finds the currently played beat given a list of tracks and the current time. * @param trackLookup The tracks indices in which to search the played beat for. * @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 findBeat( @@ -203,11 +212,7 @@ export class MidiTickLookup { return currentBeatHint; } // already on the next beat? - else if ( - currentBeatHint.nextBeat && - tick >= currentBeatHint.nextBeat.start && - tick < currentBeatHint.nextBeat.end - ) { + if (currentBeatHint.nextBeat && tick >= currentBeatHint.nextBeat.start && tick < currentBeatHint.nextBeat.end) { const next = currentBeatHint.nextBeat!; // fill next in chain this.fillNextBeat(next, trackLookup); @@ -218,7 +223,74 @@ export class MidiTickLookup { return null; } + private fillNextBeatMultiBarRest(current: MidiTickLookupFindBeatResult, trackLookup: Set) { + 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 + // chained nextMasterBar equal. so we just jump over X bars + let endMasterBar: MasterBarTickLookup | null = current.masterBar; + for (let i = 0; i < group.length; i++) { + if (!endMasterBar) { + break; + } + endMasterBar = endMasterBar.nextMasterBar; + } + + if (endMasterBar) { + // one more following -> use start of next + if (endMasterBar.nextMasterBar) { + current.nextBeat = this.firstBeatInMasterBar( + trackLookup, + endMasterBar.nextMasterBar!, + endMasterBar.nextMasterBar!.start, + true + ); + + // if we have the next beat take the difference between the times as duration + if (current.nextBeat) { + current.tickDuration = current.nextBeat.start - current.start; + current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToNextBext; + + if ( + current.nextBeat.masterBar.masterBar.index !== endMasterBar.masterBar.index + 1 && + (current.nextBeat.masterBar.masterBar.index !== endMasterBar.masterBar.index || + current.nextBeat.beat.playbackStart <= current.beat.playbackStart) + ) { + current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar; + } + } + // no next beat, animate to the end of the bar (could be an incomplete bar) + else { + current.tickDuration = endMasterBar.nextMasterBar.end - current.start; + current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar; + } + } else { + current.tickDuration = endMasterBar.end - current.start; + current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar; + } + } else { + Logger.warning( + 'Synth', + 'MultiBar Rest Info and the nextMasterBar are out of sync, this is an unexpected error. Please report it as bug. (broken chain fill-next)' + ); + // this is wierd, we have a masterbar without known tick? + // make a best guess with the number of bars + current.tickDuration = (current.masterBar.end - current.masterBar.start) * (group.length + 1); + current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar; + } + + current.calculateDuration(); + } + private fillNextBeat(current: MidiTickLookupFindBeatResult, trackLookup: Set) { + // on multibar rests take the duration until the end. + if (this.isMultiBarRestResult(current)) { + this.fillNextBeatMultiBarRest(current, trackLookup); + } else { + this.fillNextBeatDefault(current, trackLookup); + } + } + private fillNextBeatDefault(current: MidiTickLookupFindBeatResult, trackLookup: Set) { current.nextBeat = this.findBeatInMasterBar( current.masterBar, current.beatLookup.nextBeat, @@ -237,9 +309,8 @@ export class MidiTickLookup { current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToNextBext; current.calculateDuration(); } - // no next beat, animate to the end of the bar (could be an incomplete bar) - if (!current.nextBeat) { + else { current.tickDuration = current.masterBar.end - current.start; current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar; current.calculateDuration(); @@ -249,14 +320,25 @@ export class MidiTickLookup { // we report no next beat and animate to the end if ( current.nextBeat && - current.nextBeat.masterBar.masterBar.index != current.masterBar.masterBar.index + 1 && - ( - current.nextBeat.masterBar.masterBar.index != current.masterBar.masterBar.index || - current.nextBeat.beat.playbackStart <= current.beat.playbackStart - ) + current.nextBeat.masterBar.masterBar.index !== current.masterBar.masterBar.index + 1 && + (current.nextBeat.masterBar.masterBar.index !== current.masterBar.masterBar.index || + current.nextBeat.beat.playbackStart <= current.beat.playbackStart) ) { current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar; - } + } + } + + private isMultiBarRestResult(current: MidiTickLookupFindBeatResult) { + return this.internalIsMultiBarRestResult(current.masterBar.masterBar.index, current.beat); + } + + private internalIsMultiBarRestResult(masterBarIndex: number, beat: Beat) { + return ( + this.multiBarRestInfo && + this.multiBarRestInfo!.has(masterBarIndex) && + beat.isRest && + beat.voice.bar.isRestOnly + ); } private findBeatSlow( @@ -292,16 +374,20 @@ export class MidiTickLookup { return null; } + return this.firstBeatInMasterBar(trackLookup, masterBar, tick, isNextSearch); + } + + private firstBeatInMasterBar( + trackLookup: Set, + startMasterBar: MasterBarTickLookup, + tick: number, + isNextSearch: boolean + ) { + let masterBar: MasterBarTickLookup | null = startMasterBar; // scan through beats and find first one which has a beat visible while (masterBar) { if (masterBar.firstBeat) { - let beat = this.findBeatInMasterBar( - masterBar, - masterBar.firstBeat, - tick, - trackLookup, - isNextSearch - ); + const beat = this.findBeatInMasterBar(masterBar, masterBar.firstBeat, tick, trackLookup, isNextSearch); if (beat) { return beat; @@ -310,7 +396,6 @@ export class MidiTickLookup { masterBar = masterBar.nextMasterBar; } - return null; } @@ -340,7 +425,12 @@ export class MidiTickLookup { const relativeTick = tick - masterBar.start; while (currentStartLookup != null && startBeat == null) { - if (currentStartLookup.start <= relativeTick && relativeTick < currentStartLookup.end) { + if ( + // either within exact range or if we're in the "next search" also allow using the first beat + // of the next bars + (currentStartLookup.start <= relativeTick || (isNextSearch && relativeTick < 0)) && + relativeTick < currentStartLookup.end + ) { startBeatLookup = currentStartLookup; startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks); @@ -420,8 +510,40 @@ export class MidiTickLookup { result.tickDuration = beatLookup!.end - beatLookup!.start; if (!isNextSearch) { + // the next beat filling will adjust this result with the respective durations this.fillNextBeat(result, visibleTracks); } + // 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 + else if (this.isMultiBarRestResult(result)) { + const multiRest = this.multiBarRestInfo!.get(masterBar.masterBar.index)!; + // this is a bit sensitive. we assume that the sequence of multi-rest bars and the + // chained nextMasterBar equal. so we just jump over X bars + let endMasterBar: MasterBarTickLookup | null = masterBar; + for (let i = 0; i < multiRest.length; i++) { + if (!endMasterBar) { + break; + } + + endMasterBar = endMasterBar.nextMasterBar; + } + + if (endMasterBar) { + // one more following -> use start of next + if (endMasterBar.nextMasterBar) { + result.tickDuration = endMasterBar.nextMasterBar.start - beatLookup!.start; + } + // no more following -> use end + else { + result.tickDuration = endMasterBar.end - beatLookup!.start; + } + } else { + Logger.warning( + 'Synth', + 'MultiBar Rest Info and the nextMasterBar are out of sync, this is an unexpected error. Please report it as bug. (broken chain stretch-result)' + ); + } + } result.calculateDuration(); diff --git a/src/midi/MidiUtils.ts b/src/midi/MidiUtils.ts index faf5be653..ac4f81b60 100644 --- a/src/midi/MidiUtils.ts +++ b/src/midi/MidiUtils.ts @@ -1,4 +1,5 @@ -import { Duration } from '@src/model/Duration'; +import { DynamicValue } from '@src/model/DynamicValue'; +import type { Duration } from '@src/model/Duration'; export class MidiUtils { public static readonly QuarterTime: number = 960; @@ -59,7 +60,94 @@ export class MidiUtils { return ((ticks * numerator) / denominator) | 0; } - public static dynamicToVelocity(dynamicsSteps: number): number { - return MidiUtils.MinVelocity + dynamicsSteps * MidiUtils.VelocityIncrement; + public static dynamicToVelocity(dynamicValue: DynamicValue, adjustment: number = 0): number { + let velocity: number = 1; + switch (dynamicValue) { + case DynamicValue.PPP: + velocity = MidiUtils.MinVelocity + 0 * MidiUtils.VelocityIncrement; + break; + case DynamicValue.PP: + velocity = MidiUtils.MinVelocity + 1 * MidiUtils.VelocityIncrement; + break; + case DynamicValue.P: + velocity = MidiUtils.MinVelocity + 2 * MidiUtils.VelocityIncrement; + break; + case DynamicValue.MP: + velocity = MidiUtils.MinVelocity + 3 * MidiUtils.VelocityIncrement; + break; + case DynamicValue.MF: + velocity = MidiUtils.MinVelocity + 4 * MidiUtils.VelocityIncrement; + break; + case DynamicValue.F: + velocity = MidiUtils.MinVelocity + 5 * MidiUtils.VelocityIncrement; + break; + case DynamicValue.FF: + velocity = MidiUtils.MinVelocity + 6 * MidiUtils.VelocityIncrement; + break; + case DynamicValue.FFF: + velocity = MidiUtils.MinVelocity + 7 * MidiUtils.VelocityIncrement; + break; + + // special + case DynamicValue.PPPP: + velocity = 10; + break; + + case DynamicValue.PPPPP: + velocity = 5; + break; + + case DynamicValue.PPPPPP: + velocity = 3; + break; + + case DynamicValue.FFFF: + velocity = MidiUtils.MinVelocity + 8 * MidiUtils.VelocityIncrement; + break; + + case DynamicValue.FFFFF: + velocity = MidiUtils.MinVelocity + 9 * MidiUtils.VelocityIncrement; + break; + + case DynamicValue.FFFFFF: + velocity = MidiUtils.MinVelocity + 10 * MidiUtils.VelocityIncrement; + break; + + // "forced" variants -> a bit louder than normal, same as FF for us + case DynamicValue.SF: + case DynamicValue.SFP: + case DynamicValue.SFZP: + case DynamicValue.SFPP: + case DynamicValue.SFZ: + case DynamicValue.FZ: + velocity = MidiUtils.MinVelocity + 6 * MidiUtils.VelocityIncrement; + break; + + // force -> piano, same as F for us + case DynamicValue.FP: + velocity = MidiUtils.MinVelocity + 5 * MidiUtils.VelocityIncrement; + break; + + // "rinforced" varaints -> like "forced" but typically for a whole passage + // not a single note, same as FF for us + case DynamicValue.RF: + case DynamicValue.RFZ: + case DynamicValue.SFFZ: + velocity = MidiUtils.MinVelocity + 5 * MidiUtils.VelocityIncrement; + break; + + // almost not hearable but still a value + case DynamicValue.N: + velocity = 1; + break; + // A bit weaker than standard F but stronger than MF + case DynamicValue.PF: + velocity = MidiUtils.MinVelocity + ((4.5 * MidiUtils.VelocityIncrement) | 0); + break; + } + + // 0 would means note-off (not played) so we need a minimum of 1 to have still a note played + velocity += adjustment * MidiUtils.VelocityIncrement; + return Math.min(Math.max(velocity, 1), 127); } } diff --git a/src/midi/_barrel.ts b/src/midi/_barrel.ts new file mode 100644 index 000000000..05b20f807 --- /dev/null +++ b/src/midi/_barrel.ts @@ -0,0 +1,41 @@ +export { BeatTickLookup, BeatTickLookupItem } from '@src/midi/BeatTickLookup'; +export { MasterBarTickLookup, MasterBarTickLookupTempoChange } from '@src/midi/MasterBarTickLookup'; +export { + MidiTickLookup, + MidiTickLookupFindBeatResult, + MidiTickLookupFindBeatResultCursorMode +} from '@src/midi/MidiTickLookup'; +export { MidiFile, MidiFileFormat, MidiTrack } from '@src/midi/MidiFile'; +export { ControllerType } from '@src/midi/ControllerType'; +export { + MidiEvent, + MidiEventType, + TimeSignatureEvent, + AlphaTabRestEvent, + AlphaTabMetronomeEvent, + NoteEvent, + NoteOnEvent, + NoteOffEvent, + ControlChangeEvent, + ProgramChangeEvent, + TempoChangeEvent, + PitchBendEvent, + NoteBendEvent, + EndOfTrackEvent, + AlphaTabSysExEvent +} from '@src/midi/MidiEvent'; +export { + DeprecatedMidiEvent, + MetaEventType, + MetaEvent, + MetaDataEvent, + MetaNumberEvent, + Midi20PerNotePitchBendEvent, + SystemCommonType, + SystemCommonEvent, + AlphaTabSystemExclusiveEvents, + SystemExclusiveEvent +} from '@src/midi/DeprecatedEvents'; +export { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; +export { AlphaSynthMidiFileHandler } from '@src/midi/AlphaSynthMidiFileHandler'; +export type { IMidiFileHandler } from '@src/midi/IMidiFileHandler'; diff --git a/src/midi/index.ts b/src/midi/index.ts deleted file mode 100644 index 4b576745e..000000000 --- a/src/midi/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -export { BeatTickLookup } from '@src/midi/BeatTickLookup'; -export { MasterBarTickLookup } from '@src/midi/MasterBarTickLookup'; -export { MidiTickLookup, MidiTickLookupFindBeatResult } from '@src/midi/MidiTickLookup'; -export { MidiFile, MidiFileFormat} from '@src/midi/MidiFile'; -export { ControllerType } from '@src/midi/ControllerType'; -export { - MidiEvent, MidiEventType, TimeSignatureEvent, - AlphaTabRestEvent, - AlphaTabMetronomeEvent, - NoteEvent, - NoteOnEvent, - NoteOffEvent, - ControlChangeEvent, - ProgramChangeEvent, - TempoChangeEvent, - PitchBendEvent, - NoteBendEvent, - EndOfTrackEvent -} from '@src/midi/MidiEvent'; -export { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; -export { AlphaSynthMidiFileHandler } from '@src/midi/AlphaSynthMidiFileHandler'; diff --git a/src/model/AccentuationType.ts b/src/model/AccentuationType.ts index 07759a623..2657986f0 100644 --- a/src/model/AccentuationType.ts +++ b/src/model/AccentuationType.ts @@ -5,17 +5,17 @@ export enum AccentuationType { /** * No accentuation */ - None, + None = 0, /** * Normal accentuation */ - Normal, + Normal = 1, /** * Heavy accentuation */ - Heavy, + Heavy = 2, /** * Tenuto accentuation */ - Tenuto + Tenuto = 3 } diff --git a/src/model/AccidentalType.ts b/src/model/AccidentalType.ts index ef4308ff5..26e2d3d42 100644 --- a/src/model/AccidentalType.ts +++ b/src/model/AccidentalType.ts @@ -5,37 +5,37 @@ export enum AccidentalType { /** * No accidental */ - None, + None = 0, /** * Naturalize */ - Natural, + Natural = 1, /** * Sharp */ - Sharp, + Sharp = 2, /** * Flat */ - Flat, + Flat = 3, /** * Natural for smear bends */ - NaturalQuarterNoteUp, + NaturalQuarterNoteUp = 4, /** * Sharp for smear bends */ - SharpQuarterNoteUp, + SharpQuarterNoteUp = 5, /** * Flat for smear bends */ - FlatQuarterNoteUp, + FlatQuarterNoteUp = 6, /** * Double Sharp, indicated by an 'x' */ - DoubleSharp, + DoubleSharp = 7, /** * Double Flat, indicated by 'bb' */ - DoubleFlat + DoubleFlat = 8 } diff --git a/src/model/Automation.ts b/src/model/Automation.ts index 1804adc84..7b38c3414 100644 --- a/src/model/Automation.ts +++ b/src/model/Automation.ts @@ -5,19 +5,19 @@ export enum AutomationType { /** * Tempo change. */ - Tempo, + Tempo = 0, /** * Colume change. */ - Volume, + Volume = 1, /** * Instrument change. */ - Instrument, + Instrument = 2, /** * Balance change. */ - Balance + Balance = 3 } /** @@ -61,8 +61,8 @@ export class Automation { if (reference < 1 || reference > 5) { reference = 2; } - let references: Float32Array = new Float32Array([1, 0.5, 1.0, 1.5, 2.0, 3.0]); - let automation: Automation = new Automation(); + const references: Float32Array = new Float32Array([1, 0.5, 1.0, 1.5, 2.0, 3.0]); + const automation: Automation = new Automation(); automation.type = AutomationType.Tempo; automation.isLinear = isLinear; automation.ratioPosition = ratioPosition; @@ -70,13 +70,8 @@ export class Automation { return automation; } - - public static buildInstrumentAutomation( - isLinear: boolean, - ratioPosition: number, - value: number - ): Automation { - let automation: Automation = new Automation(); + public static buildInstrumentAutomation(isLinear: boolean, ratioPosition: number, value: number): Automation { + const automation: Automation = new Automation(); automation.type = AutomationType.Instrument; automation.isLinear = isLinear; automation.ratioPosition = ratioPosition; diff --git a/src/model/Bar.ts b/src/model/Bar.ts index ee9de9b35..25712e1f2 100644 --- a/src/model/Bar.ts +++ b/src/model/Bar.ts @@ -1,10 +1,13 @@ import { Clef } from '@src/model/Clef'; -import { MasterBar } from '@src/model/MasterBar'; +import type { MasterBar } from '@src/model/MasterBar'; import { Ottavia } from '@src/model/Ottavia'; import { SimileMark } from '@src/model/SimileMark'; -import { Staff } from '@src/model/Staff'; -import { Voice } from '@src/model/Voice'; -import { Settings } from '@src/Settings'; +import type { Staff } from '@src/model/Staff'; +import type { Voice } from '@src/model/Voice'; +import type { Settings } from '@src/Settings'; +import { ElementStyle } from '@src/model/ElementStyle'; +import { KeySignature } from '@src/model/KeySignature'; +import { KeySignatureType } from '@src/model/KeySignatureType'; /** * The different pedal marker types. @@ -13,15 +16,15 @@ export enum SustainPedalMarkerType { /** * Indicates that the pedal should be pressed from this time on. */ - Down, + Down = 0, /** * Indicates that the pedal should be held on this marker (used when the pedal is held for the whole bar) */ - Hold, + Hold = 1, /** * indicates that the pedal should be lifted up at this time. */ - Up + Up = 2 } /** @@ -60,6 +63,159 @@ export class SustainPedalMarker { public previousPedalMarker: SustainPedalMarker | null = null; } +/** + * Lists all graphical sub elements within a {@link Bar} which can be styled via {@link Bar.style} + */ +export enum BarSubElement { + /** + * The repeat signs on the standard notation staff. + */ + StandardNotationRepeats = 0, + + /** + * The repeat signs on the guitar tab staff. + */ + GuitarTabsRepeats = 1, + + /** + * The repeat signs on the slash staff. + */ + SlashRepeats = 2, + + /** + * The repeat signs on the numbered notation staff. + */ + NumberedRepeats = 3, + + /** + * The bar numbers on the standard notation staff. + */ + StandardNotationBarNumber = 4, + + /** + * The bar numbers on the guitar tab staff. + */ + GuitarTabsBarNumber = 5, + + /** + * The bar numbers on the slash staff. + */ + SlashBarNumber = 6, + + /** + * The bar numbers on the numbered notation staff. + */ + NumberedBarNumber = 7, + + /** + * The bar lines on the standard notation staff. + */ + StandardNotationBarLines = 8, + + /** + * The bar lines on the guitar tab staff. + */ + GuitarTabsBarLines = 9, + + /** + * The bar lines on the slash staff. + */ + SlashBarLines = 10, + + /** + * The bar lines on the numbered notation staff. + */ + NumberedBarLines = 11, + + /** + * The clefs on the standard notation staff. + */ + StandardNotationClef = 12, + + /** + * The clefs on the guitar tab staff. + */ + GuitarTabsClef = 13, + + /** + * The key signatures on the standard notation staff. + */ + StandardNotationKeySignature = 14, + + /** + * The key signatures on the numbered notation staff. + */ + NumberedKeySignature = 15, + + /** + * The time signatures on the standard notation staff. + */ + StandardNotationTimeSignature = 16, + + /** + * The time signatures on the guitar tab staff. + */ + GuitarTabsTimeSignature = 17, + + /** + * The time signatures on the slash staff. + */ + SlashTimeSignature = 18, + + /** + * The time signature on the numbered notation staff. + */ + NumberedTimeSignature = 19, + + /** + * The staff lines on the standard notation staff. + */ + StandardNotationStaffLine = 20, + + /** + * The staff lines on the guitar tab staff. + */ + GuitarTabsStaffLine = 21, + + /** + * The staff lines on the slash staff. + */ + SlashStaffLine = 22, + + /** + * The staff lines on the numbered notation staff. + */ + NumberedStaffLine = 23 +} + +/** + * Defines the custom styles for bars. + * @json + * @json_strict + */ +export class BarStyle extends ElementStyle {} + +/** + * Lists all bar line styles. + */ +export enum BarLineStyle { + /** + * No special custom line style, automatic handling (e.g. last bar might be LightHeavy) + */ + Automatic = 0, + Dashed = 1, + Dotted = 2, + Heavy = 3, + HeavyHeavy = 4, + HeavyLight = 5, + LightHeavy = 6, + LightLight = 7, + None = 8, + Regular = 9, + Short = 10, + Tick = 11 +} + /** * A bar is a single block within a track, also known as Measure. * @json @@ -68,6 +224,13 @@ export class SustainPedalMarker { export class Bar { private static _globalBarId: number = 0; + /** + * @internal + */ + public static resetIds() { + Bar._globalBarId = 0; + } + /** * Gets or sets the unique id of this bar. */ @@ -145,15 +308,114 @@ export class Bar { return this.staff.track.score.masterBars[this.index]; } + private _isEmpty: boolean = true; + private _isRestOnly: boolean = true; + + /** + * Whether this bar is fully empty (not even having rests). + */ public get isEmpty(): boolean { - for (let i: number = 0, j: number = this.voices.length; i < j; i++) { - if (!this.voices[i].isEmpty) { - return false; + return this._isEmpty; + } + + /** + * Whether this bar is empty or has only rests. + */ + public get isRestOnly(): boolean { + return this._isRestOnly; + } + + /** + * The bar line to draw on the left side of the bar. + * @remarks + * Note that the combination with {@link barLineRight} of the previous bar matters. + * If this bar has a Regular/Automatic style but the previous bar is customized, no additional line is drawn by this bar. + * If both bars have a custom style, both bar styles are drawn. + */ + public barLineLeft: BarLineStyle = BarLineStyle.Automatic; + + /** + * The bar line to draw on the right side of the bar. + * @remarks + * Note that the combination with {@link barLineLeft} of the next bar matters. + * If this bar has a Regular/Automatic style but the next bar is customized, no additional line is drawn by this bar. + * If both bars have a custom style, both bar styles are drawn. + */ + public barLineRight: BarLineStyle = BarLineStyle.Automatic; + + /** + * Gets or sets the key signature used on all bars. + */ + public keySignature: KeySignature = KeySignature.C; + + /** + * Gets or sets the type of key signature (major/minor) + */ + public keySignatureType: KeySignatureType = KeySignatureType.Major; + + /** + * 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. + */ + public getActualBarLineLeft(isFirstOfSystem: boolean): BarLineStyle { + return Bar.actualBarLine(this, false, isFirstOfSystem); + } + + /** + * The bar line to draw on the right 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. + */ + public getActualBarLineRight(): BarLineStyle { + return Bar.actualBarLine(this, true, false /* not relevant */); + } + + private static automaticToActualType(masterBar: MasterBar, isRight: boolean, firstOfSystem: boolean) { + let actualLineType: BarLineStyle; + + if (isRight) { + if (masterBar.isRepeatEnd) { + actualLineType = BarLineStyle.LightHeavy; + } else if (!masterBar.nextMasterBar) { + actualLineType = BarLineStyle.LightHeavy; + } else if (masterBar.isFreeTime) { + actualLineType = BarLineStyle.Dashed; + } else if (masterBar.isDoubleBar) { + actualLineType = BarLineStyle.LightLight; + } else { + actualLineType = BarLineStyle.Regular; + } + } else { + if (masterBar.isRepeatStart) { + actualLineType = BarLineStyle.HeavyLight; + } else if (firstOfSystem) { + actualLineType = BarLineStyle.Regular; + } else { + actualLineType = BarLineStyle.None; } } - return true; + + return actualLineType; } + private static actualBarLine(bar: Bar, isRight: boolean, firstOfSystem: boolean) { + const masterBar = bar.masterBar; + const requestedLineType = isRight ? bar.barLineRight : bar.barLineLeft; + + let actualLineType: BarLineStyle; + if (requestedLineType === BarLineStyle.Automatic) { + actualLineType = Bar.automaticToActualType(masterBar, isRight, firstOfSystem); + } else { + actualLineType = requestedLineType; + } + + return actualLineType; + } + + /** + * The style customizations for this item. + */ + public style?: BarStyle; + public addVoice(voice: Voice): void { voice.bar = this; voice.index = this.voices.length; @@ -162,29 +424,53 @@ export class Bar { public finish(settings: Settings, sharedDataBag: Map | null = null): void { this.isMultiVoice = false; + this._isEmpty = true; + this._isRestOnly = true; for (let i: number = 0, j: number = this.voices.length; i < j; i++) { - let voice: Voice = this.voices[i]; + const voice: Voice = this.voices[i]; voice.finish(settings, sharedDataBag); if (i > 0 && !voice.isEmpty) { this.isMultiVoice = true; } + + if (!voice.isEmpty) { + this._isEmpty = false; + } + + if (!voice.isRestOnly) { + this._isRestOnly = false; + } } - // chain sustain pedal markers - if (this.sustainPedals.length > 0) { + // chain sustain pedal markers (and merge overlaps) + const sustainPedals = this.sustainPedals; + if (sustainPedals.length > 0) { let previousMarker: SustainPedalMarker | null = null; + this.sustainPedals = []; if (this.previousBar && this.previousBar.sustainPedals.length > 0) { previousMarker = this.previousBar.sustainPedals[this.previousBar.sustainPedals.length - 1]; } - for (const marker of this.sustainPedals) { + const isDown = previousMarker !== null && previousMarker.pedalType !== SustainPedalMarkerType.Up; + + for (const marker of sustainPedals) { if (previousMarker && previousMarker.pedalType !== SustainPedalMarkerType.Up) { + //duplicate or out-of-order markers + if (previousMarker.bar === this && marker.ratioPosition <= previousMarker.ratioPosition) { + continue; + } + previousMarker.nextPedalMarker = marker; marker.previousPedalMarker = previousMarker; } + if (isDown && marker.pedalType === SustainPedalMarkerType.Down) { + marker.pedalType = SustainPedalMarkerType.Hold; + } + marker.bar = this; + this.sustainPedals.push(marker); previousMarker = marker; } } else if (this.previousBar && this.previousBar.sustainPedals.length > 0) { @@ -205,8 +491,8 @@ export class Bar { public calculateDuration(): number { let duration: number = 0; - for (let voice of this.voices) { - let voiceDuration: number = voice.calculateDuration(); + for (const voice of this.voices) { + const voiceDuration: number = voice.calculateDuration(); if (voiceDuration > duration) { duration = voiceDuration; } diff --git a/src/model/BarreShape.ts b/src/model/BarreShape.ts index db17c0ff6..b41a184a5 100644 --- a/src/model/BarreShape.ts +++ b/src/model/BarreShape.ts @@ -5,15 +5,15 @@ export enum BarreShape { /** * No Barré */ - None, + None = 0, /** * Full Barré (play all strings) */ - Full, + Full = 1, /** * 1/2 Barré (play only half the strings) */ - Half + Half = 2 } diff --git a/src/model/Beat.ts b/src/model/Beat.ts index 38f6d2bd6..d9ba0236f 100644 --- a/src/model/Beat.ts +++ b/src/model/Beat.ts @@ -4,29 +4,30 @@ import { BendPoint } from '@src/model/BendPoint'; import { BendStyle } from '@src/model/BendStyle'; import { BendType } from '@src/model/BendType'; import { BrushType } from '@src/model/BrushType'; -import { Chord } from '@src/model/Chord'; +import type { Chord } from '@src/model/Chord'; import { CrescendoType } from '@src/model/CrescendoType'; import { Duration } from '@src/model/Duration'; import { DynamicValue } from '@src/model/DynamicValue'; -import { Fermata } from '@src/model/Fermata'; +import type { Fermata } from '@src/model/Fermata'; import { GraceType } from '@src/model/GraceType'; import { Note } from '@src/model/Note'; import { Ottavia } from '@src/model/Ottavia'; import { PickStroke } from '@src/model/PickStroke'; import { TupletGroup } from '@src/model/TupletGroup'; import { VibratoType } from '@src/model/VibratoType'; -import { Voice } from '@src/model/Voice'; +import type { Voice } from '@src/model/Voice'; import { WhammyType } from '@src/model/WhammyType'; import { NotationMode } from '@src/NotationSettings'; -import { Settings } from '@src/Settings'; -import { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import type { Settings } from '@src/Settings'; +import type { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { BeatCloner } from '@src/generated/model/BeatCloner'; import { GraceGroup } from '@src/model/GraceGroup'; -import { GolpeType } from './GolpeType'; -import { FadeType } from './FadeType'; -import { WahPedal } from './WahPedal'; -import { BarreShape } from './BarreShape'; -import { Rasgueado } from './Rasgueado'; +import { GolpeType } from '@src/model/GolpeType'; +import { FadeType } from '@src/model/FadeType'; +import { WahPedal } from '@src/model/WahPedal'; +import { BarreShape } from '@src/model/BarreShape'; +import { Rasgueado } from '@src/model/Rasgueado'; +import { ElementStyle } from '@src/model/ElementStyle'; /** * Lists the different modes on how beaming for a beat should be done. @@ -35,21 +36,153 @@ export enum BeatBeamingMode { /** * Automatic beaming based on the timing rules. */ - Auto, + Auto = 0, /** * Force a split to the next beat. */ - ForceSplitToNext, + ForceSplitToNext = 1, /** * Force a merge with the next beat. */ - ForceMergeWithNext, + ForceMergeWithNext = 2, /** * Force a split to the next beat on the secondary beam. */ - ForceSplitOnSecondaryToNext + ForceSplitOnSecondaryToNext = 3 } +/** + * Lists all graphical sub elements within a {@link Beat} which can be styled via {@link Beat.style} + */ +export enum BeatSubElement { + /** + * The effects and annotations shown in dedicated effect bands above the staves (e.g. fermata). + * Only applies to items which are on beat level but not any individual note level effects. + */ + Effects = 0, + + /** + * The stems drawn for note heads in this beat on the standard notation staff. + */ + StandardNotationStem = 1, + + /** + * The flags drawn for note heads in this beat on the standard notation staff. + */ + StandardNotationFlags = 2, + + /** + * The beams drawn between this and the next beat on the standard notation staff. + */ + StandardNotationBeams = 3, + + /** + * The tuplet drawn on the standard notation staff (the first beat affects the whole tuplet if grouped). + */ + StandardNotationTuplet = 4, + + /** + * The effects and annotations applied to this beat on the standard notation staff (e.g. brushes). + * Only applies to items which are on beat level but not any individual note level effects. + */ + StandardNotationEffects = 5, + + /** + * The rest symbol on the standard notation staff. + */ + StandardNotationRests = 6, + + /** + * The stems drawn for note heads in this beat on the guitar tab staff. + */ + GuitarTabStem = 7, + + /** + * The flags drawn for note heads in this beat on the guitar tab staff. + */ + GuitarTabFlags = 8, + + /** + * The beams drawn between this and the next beat on the guitar tab staff. + */ + GuitarTabBeams = 9, + + /** + * The tuplet drawn on the guitar tab staff (the first beat affects the whole tuplet if grouped). + */ + GuitarTabTuplet = 10, + + /** + * The effects and annotations applied to this beat on the guitar tab staff (e.g. brushes). + * Only applies to items which are on beat level but not any individual note level effects. + */ + GuitarTabEffects = 11, + + /** + * The rest symbol on the guitar tab staff. + */ + GuitarTabRests = 12, + + /** + * The stems drawn for note heads in this beat on the slash staff. + */ + SlashStem = 13, + + /** + * The flags drawn for note heads in this beat on the slash staff. + */ + SlashFlags = 14, + + /** + * The beams drawn between this and the next beat on the slash staff. + */ + SlashBeams = 15, + + /** + * The tuplet drawn on the slash staff (the first beat affects the whole tuplet if grouped). + */ + SlashTuplet = 16, + + /** + * The rest symbol on the slash staff. + */ + SlashRests = 17, + + /** + * The effects and annotations applied to this beat on the slash staff (e.g. brushes). + * Only applies to items which are on beat level but not any individual note level effects. + */ + SlashEffects = 18, + + /** + * The duration lines drawn for this beat on the numbered notation staff. + */ + NumberedDuration = 19, + + /** + * The effects and annotations applied to this beat on the numbered notation staff (e.g. brushes). + * Only applies to items which are on beat level but not any individual note level effects. + */ + NumberedEffects = 20, + + /** + * The rest (0) on the numbered notation staff. + */ + NumberedRests = 21, + + /** + * The tuplet drawn on the numbered notation staff (the first beat affects the whole tuplet if grouped). + */ + NumberedTuplet = 22 +} + +/** + * Defines the custom styles for beats. + * @json + * @json_strict + */ +export class BeatStyle extends ElementStyle {} + /** * A beat is a single block within a bar. A beat is a combination * of several notes played at the same time. @@ -60,6 +193,13 @@ export enum BeatBeamingMode { export class Beat { private static _globalBeatId: number = 0; + /** + * @internal + */ + public static resetIds() { + Beat._globalBeatId = 0; + } + /** * Gets or sets the unique id of this beat. * @clone_ignore @@ -410,6 +550,13 @@ export class Beat { */ public displayStart: number = 0; + /** + * The calculated visual end position of this beat in midi ticks. + */ + public get displayEnd(): number { + return this.displayStart + this.displayDuration; + } + /** * The timeline position of the voice within the current bar as it is played. (unit: midi ticks) * This might differ from the actual playback time due to special grace types. @@ -427,6 +574,15 @@ export class Beat { */ public playbackDuration: number = 0; + /** + * The duration in midi ticks to use for this beat on the {@link displayDuration} + * controlling the visual display of the beat. + * @remarks + * This is used in scenarios where the bar might not have 100% exactly + * a linear structure between the beats. e.g. in MusicXML when using ``. + */ + public overrideDisplayDuration?: number; + /** * The type of golpe to play. */ @@ -513,13 +669,19 @@ export class Beat { * (requires that the midi for the song is generated so that times are calculated). * If no midi is generated the timer value might be filled from the input file (or manually). */ - public showTimer:boolean = false; + public showTimer: boolean = false; /** * The absolute time in milliseconds when this beat will be played the first time. */ public timer: number | null = null; + /** + * The style customizations for this item. + * @clone_ignore + */ + public style?: BeatStyle; + public addWhammyBarPoint(point: BendPoint): void { let points = this.whammyBarPoints; if (points === null) { @@ -547,12 +709,12 @@ export class Beat { // remove point points.splice(index, 1); - let point: BendPoint = points[index]; + const point: BendPoint = points[index]; // update maxWhammy point if required if (point === this.maxWhammyPoint) { this.maxWhammyPoint = null; - for (let currentPoint of points) { + for (const currentPoint of points) { if (!this.maxWhammyPoint || currentPoint.value > this.maxWhammyPoint.value) { this.maxWhammyPoint = currentPoint; } @@ -561,7 +723,7 @@ export class Beat { if (point === this.minWhammyPoint) { this.minWhammyPoint = null; - for (let currentPoint of points) { + for (const currentPoint of points) { if (!this.minWhammyPoint || currentPoint.value < this.minWhammyPoint.value) { this.minWhammyPoint = currentPoint; } @@ -579,7 +741,7 @@ export class Beat { } public removeNote(note: Note): void { - let index: number = this.notes.indexOf(note); + const index: number = this.notes.indexOf(note); if (index >= 0) { this.notes.splice(index, 1); if (note.isStringed) { @@ -590,7 +752,7 @@ export class Beat { public getAutomation(type: AutomationType): Automation | null { for (let i: number = 0, j: number = this.automations.length; i < j; i++) { - let automation: Automation = this.automations[i]; + const automation: Automation = this.automations[i]; if (automation.type === type) { return automation; } @@ -606,6 +768,9 @@ export class Beat { } private calculateDuration(): number { + if (this.overrideDisplayDuration !== undefined) { + return this.overrideDisplayDuration!; + } if (this.isFullBarRest) { return this.voice.bar.masterBar.calculateDuration(); } @@ -622,7 +787,7 @@ export class Beat { } public updateDurations(): void { - let ticks: number = this.calculateDuration(); + const ticks: number = this.calculateDuration(); this.playbackDuration = ticks; switch (this.graceType) { @@ -647,7 +812,7 @@ export class Beat { break; default: this.displayDuration = ticks; - let previous: Beat | null = this.previousBeat; + const previous: Beat | null = this.previousBeat; if (previous && previous.graceType === GraceType.BendGrace) { this.playbackDuration = previous.playbackDuration; } @@ -656,7 +821,7 @@ export class Beat { } public finishTuplet(): void { - let previousBeat: Beat | null = this.previousBeat; + const previousBeat: Beat | null = this.previousBeat; let currentTupletGroup: TupletGroup | null = previousBeat ? previousBeat.tupletGroup : null; if (this.hasTuplet || (this.graceType !== GraceType.None && currentTupletGroup)) { if (!previousBeat || !currentTupletGroup || !currentTupletGroup.check(this)) { @@ -699,7 +864,7 @@ export class Beat { switch (this.graceType) { case GraceType.OnBeat: case GraceType.BeforeBeat: - let numberOfGraceBeats: number = this.graceGroup!.beats.length; + const numberOfGraceBeats: number = this.graceGroup!.beats.length; // set right duration for beaming/display if (numberOfGraceBeats === 1) { this.duration = Duration.Eighth; @@ -711,7 +876,7 @@ export class Beat { break; } - let displayMode: NotationMode = !settings ? NotationMode.GuitarPro : settings.notation.notationMode; + const displayMode: NotationMode = !settings ? NotationMode.GuitarPro : settings.notation.notationMode; let isGradual: boolean = this.text === 'grad' || this.text === 'grad.'; if (isGradual && displayMode === NotationMode.SongBook) { this.text = ''; @@ -724,7 +889,7 @@ export class Beat { let visibleNotes: number = 0; let isEffectSlurBeat: boolean = false; for (let i: number = 0, j: number = this.notes.length; i < j; i++) { - let note: Note = this.notes[i]; + const note: Note = this.notes[i]; note.dynamics = this.dynamics; note.finish(settings, sharedDataBag); if (note.isLetRing) { @@ -830,10 +995,10 @@ export class Beat { this.whammyStyle = isGradual ? BendStyle.Gradual : BendStyle.Fast; } if (points!.length === 4) { - let origin: BendPoint = points[0]; - let middle1: BendPoint = points[1]; - let middle2: BendPoint = points[2]; - let destination: BendPoint = points[3]; + const origin: BendPoint = points[0]; + const middle1: BendPoint = points[1]; + const middle2: BendPoint = points[2]; + const destination: BendPoint = points[3]; // the middle points are used for holds, anything else is a new feature we do not support yet if (middle1.value === middle2.value) { // constant decrease or increase @@ -873,12 +1038,12 @@ export class Beat { if (needCopyBeatForBend) { // if this beat is a simple bend convert it to a grace beat // and generate a placeholder beat with tied notes - let cloneBeat: Beat = BeatCloner.clone(this); + const cloneBeat: Beat = BeatCloner.clone(this); cloneBeat.id = Beat._globalBeatId++; cloneBeat.pickStroke = PickStroke.None; for (let i: number = 0, j: number = cloneBeat.notes.length; i < j; i++) { - let cloneNote: Note = cloneBeat.notes[i]; - let note: Note = this.notes[i]; + const cloneNote: Note = cloneBeat.notes[i]; + const note: Note = this.notes[i]; // remove bend on cloned note cloneNote.bendType = BendType.None; @@ -900,10 +1065,10 @@ export class Beat { // if the note has a bend which is continued on the next note // we need to convert this note into a hold bend if (note.hasBend && note.isTieOrigin) { - let tieDestination: Note | null = Note.findTieOrigin(note); + const tieDestination: Note | null = Note.findTieOrigin(note); if (tieDestination && tieDestination.hasBend) { cloneNote.bendType = BendType.Hold; - let lastPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; + const lastPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; cloneNote.addBendPoint(new BendPoint(0, lastPoint.value)); cloneNote.addBendPoint(new BendPoint(BendPoint.MaxPosition, lastPoint.value)); } diff --git a/src/model/BendStyle.ts b/src/model/BendStyle.ts index 248cca79a..6469a3342 100644 --- a/src/model/BendStyle.ts +++ b/src/model/BendStyle.ts @@ -5,13 +5,13 @@ export enum BendStyle { /** * The bends are as described by the bend points */ - Default, + Default = 0, /** * The bends are gradual over the beat duration. */ - Gradual, + Gradual = 1, /** * The bends are done fast before the next note. */ - Fast, + Fast = 2 } diff --git a/src/model/BendType.ts b/src/model/BendType.ts index 840bb9fa8..49a1fc7ee 100644 --- a/src/model/BendType.ts +++ b/src/model/BendType.ts @@ -5,41 +5,41 @@ export enum BendType { /** * No bend at all */ - None, + None = 0, /** * Individual points define the bends in a flexible manner. * This system was mainly used in Guitar Pro 3-5 */ - Custom, + Custom = 1, /** * Simple Bend from an unbended string to a higher note. */ - Bend, + Bend = 2, /** * Release of a bend that was started on an earlier note. */ - Release, + Release = 3, /** * A bend that starts from an unbended string, * and also releases the bend after some time. */ - BendRelease, + BendRelease = 4, /** * Holds a bend that was started on an earlier note */ - Hold, + Hold = 5, /** * A bend that is already started before the note is played then it is held until the end. */ - Prebend, + Prebend = 6, /** * A bend that is already started before the note is played and * bends even further, then it is held until the end. */ - PrebendBend, + PrebendBend = 7, /** * A bend that is already started before the note is played and * then releases the bend to a lower note where it is held until the end. */ - PrebendRelease + PrebendRelease = 8 } diff --git a/src/model/BrushType.ts b/src/model/BrushType.ts index 6af2356a7..15918fa59 100644 --- a/src/model/BrushType.ts +++ b/src/model/BrushType.ts @@ -5,21 +5,21 @@ export enum BrushType { /** * No brush. */ - None, + None = 0, /** * Normal brush up. */ - BrushUp, + BrushUp = 1, /** * Normal brush down. */ - BrushDown, + BrushDown = 2, /** * Arpeggio up. */ - ArpeggioUp, + ArpeggioUp = 3, /** * Arpeggio down. */ - ArpeggioDown + ArpeggioDown = 4 } diff --git a/src/model/Chord.ts b/src/model/Chord.ts index 914588276..e565eb147 100644 --- a/src/model/Chord.ts +++ b/src/model/Chord.ts @@ -1,4 +1,4 @@ -import { Staff } from '@src/model/Staff'; +import type { Staff } from '@src/model/Staff'; // TODO: rework model to specify for each finger // on which frets they are placed. diff --git a/src/model/Clef.ts b/src/model/Clef.ts index 3540b10fb..266749e4d 100644 --- a/src/model/Clef.ts +++ b/src/model/Clef.ts @@ -5,21 +5,21 @@ export enum Clef { /** * Neutral clef. */ - Neutral, + Neutral = 0, /** * C3 clef */ - C3, + C3 = 1, /** * C4 clef */ - C4, + C4 = 2, /** * F4 clef */ - F4, + F4 = 3, /** * G2 clef */ - G2 + G2 = 4 } diff --git a/src/model/Color.ts b/src/model/Color.ts index 60b09a4b2..afb90e3ab 100644 --- a/src/model/Color.ts +++ b/src/model/Color.ts @@ -5,7 +5,7 @@ import { ModelUtils } from '@src/model/ModelUtils'; * Describes a color for rendering. * If provided as string one of these formats needs to be used: #RGB, #RGBA, #RRGGBB, #RRGGBBAA, rgb(r,g,b), rgba(r,g,b,a) * If provided as number a raw RGBA value needs to be used. - * + * * @target web */ export type ColorJson = Color | string | number; @@ -30,11 +30,7 @@ export class Color { public updateRgba(): void { if (this.a === 0xff) { - this.rgba = - '#' + - ModelUtils.toHexString(this.r, 2) + - ModelUtils.toHexString(this.g, 2) + - ModelUtils.toHexString(this.b, 2); + this.rgba = `#${ModelUtils.toHexString(this.r, 2)}${ModelUtils.toHexString(this.g, 2)}${ModelUtils.toHexString(this.b, 2)}`; } else { this.rgba = `rgba(${this.r},${this.g},${this.b},${this.a / 255.0})`; } @@ -88,38 +84,38 @@ export class Color { if (json.length === 4) { // #RGB return new Color( - parseInt(json[1], 16) * 17, - parseInt(json[2], 16) * 17, - parseInt(json[3], 16) * 17 + Number.parseInt(json[1], 16) * 17, + Number.parseInt(json[2], 16) * 17, + Number.parseInt(json[3], 16) * 17 ); } if (json.length === 5) { // #RGBA return new Color( - parseInt(json[1], 16) * 17, - parseInt(json[2], 16) * 17, - parseInt(json[3], 16) * 17, - parseInt(json[4], 16) * 17 + Number.parseInt(json[1], 16) * 17, + Number.parseInt(json[2], 16) * 17, + Number.parseInt(json[3], 16) * 17, + Number.parseInt(json[4], 16) * 17 ); } if (json.length === 7) { // #RRGGBB return new Color( - parseInt(json.substring(1, 3), 16), - parseInt(json.substring(3, 5), 16), - parseInt(json.substring(5, 7), 16) + Number.parseInt(json.substring(1, 3), 16), + Number.parseInt(json.substring(3, 5), 16), + Number.parseInt(json.substring(5, 7), 16) ); } if (json.length === 9) { // #RRGGBBAA return new Color( - parseInt(json.substring(1, 3), 16), - parseInt(json.substring(3, 5), 16), - parseInt(json.substring(5, 7), 16), - parseInt(json.substring(7, 9), 16) + Number.parseInt(json.substring(1, 3), 16), + Number.parseInt(json.substring(3, 5), 16), + Number.parseInt(json.substring(5, 7), 16), + Number.parseInt(json.substring(7, 9), 16) ); } } else if (json.startsWith('rgba') || json.startsWith('rgb')) { @@ -132,15 +128,19 @@ export class Color { const numbers = json.substring(start + 1, end).split(','); if (numbers.length === 3) { - return new Color(parseInt(numbers[0]), parseInt(numbers[1]), parseInt(numbers[2])); + return new Color( + Number.parseInt(numbers[0]), + Number.parseInt(numbers[1]), + Number.parseInt(numbers[2]) + ); } if (numbers.length === 4) { return new Color( - parseInt(numbers[0]), - parseInt(numbers[1]), - parseInt(numbers[2]), - parseFloat(numbers[3]) * 255 + Number.parseInt(numbers[0]), + Number.parseInt(numbers[1]), + Number.parseInt(numbers[2]), + Number.parseFloat(numbers[3]) * 255 ); } } @@ -151,7 +151,7 @@ export class Color { throw new FormatError('Unsupported format for color'); } - public static toJson(obj: Color): number { - return obj.raw; + public static toJson(obj: Color | null): number | null { + return obj === null ? null : obj.raw; } } diff --git a/src/model/CrescendoType.ts b/src/model/CrescendoType.ts index d06775d6e..02f1d42ef 100644 --- a/src/model/CrescendoType.ts +++ b/src/model/CrescendoType.ts @@ -5,13 +5,13 @@ export enum CrescendoType { /** * No crescendo applied. */ - None, + None = 0, /** * Normal crescendo applied. */ - Crescendo, + Crescendo = 1, /** * Normal decrescendo applied. */ - Decrescendo + Decrescendo = 2 } diff --git a/src/model/Direction.ts b/src/model/Direction.ts index 027cf1e8d..13a8c80a3 100644 --- a/src/model/Direction.ts +++ b/src/model/Direction.ts @@ -2,27 +2,27 @@ * Lists all directions which can be applied to a masterbar. */ export enum Direction { - TargetFine, - TargetSegno, - TargetSegnoSegno, - TargetCoda, - TargetDoubleCoda, + TargetFine = 0, + TargetSegno = 1, + TargetSegnoSegno = 2, + TargetCoda = 3, + TargetDoubleCoda = 4, - JumpDaCapo, - JumpDaCapoAlCoda, - JumpDaCapoAlDoubleCoda, - JumpDaCapoAlFine, + JumpDaCapo = 5, + JumpDaCapoAlCoda = 6, + JumpDaCapoAlDoubleCoda = 7, + JumpDaCapoAlFine = 8, - JumpDalSegno, - JumpDalSegnoAlCoda, - JumpDalSegnoAlDoubleCoda, - JumpDalSegnoAlFine, + JumpDalSegno = 9, + JumpDalSegnoAlCoda = 10, + JumpDalSegnoAlDoubleCoda = 11, + JumpDalSegnoAlFine = 12, - JumpDalSegnoSegno, - JumpDalSegnoSegnoAlCoda, - JumpDalSegnoSegnoAlDoubleCoda, - JumpDalSegnoSegnoAlFine, + JumpDalSegnoSegno = 13, + JumpDalSegnoSegnoAlCoda = 14, + JumpDalSegnoSegnoAlDoubleCoda = 15, + JumpDalSegnoSegnoAlFine = 16, - JumpDaCoda, - JumpDaDoubleCoda + JumpDaCoda = 17, + JumpDaDoubleCoda = 18 } diff --git a/src/model/DynamicValue.ts b/src/model/DynamicValue.ts index f3a88f610..fd4fb7261 100644 --- a/src/model/DynamicValue.ts +++ b/src/model/DynamicValue.ts @@ -2,36 +2,101 @@ * Lists all dynamics. */ export enum DynamicValue { + // common dynamics /** * pianississimo (very very soft) */ - PPP, + PPP = 0, /** * pianissimo (very soft) */ - PP, + PP = 1, /** * piano (soft) */ - P, + P = 2, /** * mezzo-piano (half soft) */ - MP, + MP = 3, /** * mezzo-forte (half loud) */ - MF, + MF = 4, /** * forte (loud) */ - F, + F = 5, /** * fortissimo (very loud) */ - FF, + FF = 6, /** * fortississimo (very very loud) */ - FFF + FFF = 7, + + // special dynamics + + PPPP = 8, + PPPPP = 9, + PPPPPP = 10, + + FFFF = 11, + FFFFF = 12, + FFFFFF = 13, + + /** + * Sforzando + */ + SF = 14, + /** + * SforzandoPiano + */ + SFP = 15, + /** + * SforzandoPianissimo + */ + SFPP = 16, + + /** + * FortePiano + */ + FP = 17, + + /** + * Rinforzando 1 + */ + RF = 18, + /** + * Rinforzando 2 + */ + RFZ = 19, + + /** + * Sforzato + */ + SFZ = 20, + /** + * SforzatoFF + */ + SFFZ = 21, + /** + * Forzando + */ + FZ = 22, + /** + * Niente + */ + N = 23, + + /** + * Poco forte + */ + PF = 24, + + /** + * SforzatoPiano + */ + SFZP = 25 } diff --git a/src/model/ElementStyle.ts b/src/model/ElementStyle.ts new file mode 100644 index 000000000..7ae518059 --- /dev/null +++ b/src/model/ElementStyle.ts @@ -0,0 +1,17 @@ +// biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 +import type { RenderingResources } from '@src/RenderingResources'; +import type { Color } from '@src/model/Color'; + +/** + * Defines the custom styles for an element in the music sheet (like bars, voices, notes etc). + */ +export class ElementStyle { + /** + * Changes the color of the specified sub-element within the element this style container belongs to. + * Null indicates that a certain element should use the default color from {@link RenderingResources} + * 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/src/model/FadeType.ts b/src/model/FadeType.ts index 3533ffb88..6cc824e64 100644 --- a/src/model/FadeType.ts +++ b/src/model/FadeType.ts @@ -5,17 +5,17 @@ export enum FadeType { /** * No fading */ - None, + None = 0, /** * Fade-in the sound. */ - FadeIn, + FadeIn = 1, /** * Fade-out the sound. */ - FadeOut, + FadeOut = 2, /** * Fade-in and then fade-out the sound. */ - VolumeSwell + VolumeSwell = 3 } diff --git a/src/model/Fermata.ts b/src/model/Fermata.ts index e6c315dcc..263b289d0 100644 --- a/src/model/Fermata.ts +++ b/src/model/Fermata.ts @@ -5,15 +5,15 @@ export enum FermataType { /** * A short fermata (triangle symbol) */ - Short, + Short = 0, /** * A medium fermata (round symbol) */ - Medium, + Medium = 1, /** * A long fermata (rectangular symbol) */ - Long + Long = 2 } /** diff --git a/src/model/Font.ts b/src/model/Font.ts index c712186c6..e22388ff7 100644 --- a/src/model/Font.ts +++ b/src/model/Font.ts @@ -89,16 +89,15 @@ class FontParser { if (!this._currentToken) { if (this.parseOnlyFamilies) { return; - } else { - throw new Error(`Missing font list`); } + throw new Error('Missing font list'); } const familyListInput = this._input.substr(this._currentToken.startPos).trim(); let pos = 0; while (pos < familyListInput.length) { - let c = familyListInput.charAt(pos); - if (c === ' ' || c == ',') { + const c = familyListInput.charAt(pos); + if (c === ' ' || c === ',') { // skip whitespace and quotes pos++; } else if (c === '"' || c === "'") { @@ -107,7 +106,7 @@ class FontParser { this.families.push( familyListInput .substring(pos + 1, endOfString) - .split('\\' + c) + .split(`\\${c}`) .join(c) ); pos = endOfString + 1; @@ -142,7 +141,7 @@ class FontParser { private fontSizeLineHeight() { if (!this._currentToken) { - throw new Error(`Missing font size`); + throw new Error('Missing font size'); } const parts = this._currentToken.text.split('/'); @@ -183,7 +182,7 @@ class FontParser { } } } else { - throw new Error(`Missing font size`); + throw new Error('Missing font size'); } } @@ -201,13 +200,13 @@ class FontParser { let hasVariant = false; let hasWeight = false; let valuesNeeded = 3; - let ambiguous: string[] = []; + const ambiguous: string[] = []; while (true) { if (!this._currentToken) { return; } - let text: string = this._currentToken.text; + const text: string = this._currentToken.text; switch (text) { // ambiguous case 'normal': @@ -317,7 +316,7 @@ export enum FontWeight { } /** - * Describes a font to be used. + * Describes a font to be used. * If specified as string, a CSS `font` shorthand property compliant value needs to be used. * @target web */ @@ -450,7 +449,7 @@ export class Font { this._css = this.toCssString(); } - public withSize(newSize:number) : Font { + public withSize(newSize: number): Font { return Font.withFamilyList(this._families, newSize, this._style, this._weight); } @@ -491,7 +490,7 @@ export class Font { } public static fromJson(v: unknown): Font | null { - if(v instanceof Font) { + if (v instanceof Font) { return v; } @@ -500,19 +499,19 @@ export class Font { return null; case 'object': { const m = v as Map; - let families = m.get('families') as string[]; + const families = m.get('families') as string[]; // tslint:disable-next-line: no-unnecessary-type-assertion - let size = m.get('size')! as number; - let style = JsonHelper.parseEnum(m.get('style'), FontStyle)!; - let weight = JsonHelper.parseEnum(m.get('weight'), FontWeight)!; + const size = m.get('size')! as number; + const style = JsonHelper.parseEnum(m.get('style'), FontStyle)!; + const weight = JsonHelper.parseEnum(m.get('weight'), FontWeight)!; return Font.withFamilyList(families, size, style, weight); } case 'string': { const parser = new FontParser(v as string); parser.parse(); - let families: string[] = parser.families; - let fontSizeString: string = parser.size.toLowerCase(); + const families: string[] = parser.families; + const fontSizeString: string = parser.size.toLowerCase(); let fontSize: number = 0; // as per https://websemantics.uk/articles/font-size-conversion/ switch (fontSizeString) { @@ -542,12 +541,13 @@ export class Font { default: try { if (fontSizeString.endsWith('em')) { - fontSize = parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)) * 16; + fontSize = Number.parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)) * 16; } else if (fontSizeString.endsWith('pt')) { fontSize = - (parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)) * 16.0) / 12.0; + (Number.parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)) * 16.0) / + 12.0; } else if (fontSizeString.endsWith('px')) { - fontSize = parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)); + fontSize = Number.parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)); } else { fontSize = 12; } @@ -563,7 +563,7 @@ export class Font { } let fontWeight: FontWeight = FontWeight.Regular; - let fontWeightString: string = parser.weight.toLowerCase(); + const fontWeightString: string = parser.weight.toLowerCase(); switch (fontWeightString) { case 'normal': case 'lighter': diff --git a/src/model/GolpeType.ts b/src/model/GolpeType.ts index 9bdd73fd3..329af6766 100644 --- a/src/model/GolpeType.ts +++ b/src/model/GolpeType.ts @@ -5,15 +5,15 @@ export enum GolpeType { /** * No Golpe played. */ - None, + None = 0, /** * Play a golpe with the thumb. */ - Thumb, + Thumb = 1, /** * Play a golpe with a finger. */ - Finger + Finger = 2 } diff --git a/src/model/GraceGroup.ts b/src/model/GraceGroup.ts index b54ea29dd..aae2146c7 100644 --- a/src/model/GraceGroup.ts +++ b/src/model/GraceGroup.ts @@ -1,4 +1,4 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; /** * Represents a group of grace beats that belong together @@ -32,7 +32,7 @@ export class GraceGroup { public finish() { if (this.beats.length > 0) { - this.id = this.beats[0].absoluteDisplayStart + '_' + this.beats[0].voice.index; + this.id = `${this.beats[0].absoluteDisplayStart}_${this.beats[0].voice.index}`; } } } diff --git a/src/model/GraceType.ts b/src/model/GraceType.ts index cdc8bddb1..a1896e212 100644 --- a/src/model/GraceType.ts +++ b/src/model/GraceType.ts @@ -5,17 +5,17 @@ export enum GraceType { /** * No grace, normal beat. */ - None, + None = 0, /** * The beat contains on-beat grace notes. */ - OnBeat, + OnBeat = 1, /** * The beat contains before-beat grace notes. */ - BeforeBeat, + BeforeBeat = 2, /** * The beat contains very special bend-grace notes used in SongBook style displays. */ - BendGrace + BendGrace = 3 } diff --git a/src/model/HarmonicType.ts b/src/model/HarmonicType.ts index 13da56f9b..8b79487f0 100644 --- a/src/model/HarmonicType.ts +++ b/src/model/HarmonicType.ts @@ -5,29 +5,29 @@ export enum HarmonicType { /** * No harmonics. */ - None, + None = 0, /** * Natural harmonic */ - Natural, + Natural = 1, /** * Artificial harmonic */ - Artificial, + Artificial = 2, /** * Pinch harmonics */ - Pinch, + Pinch = 3, /** * Tap harmonics */ - Tap, + Tap = 4, /** * Semi harmonics */ - Semi, + Semi = 5, /** * Feedback harmonics */ - Feedback + Feedback = 6 } diff --git a/src/model/InstrumentArticulation.ts b/src/model/InstrumentArticulation.ts index 66ca9dc0a..35108378b 100644 --- a/src/model/InstrumentArticulation.ts +++ b/src/model/InstrumentArticulation.ts @@ -1,9 +1,9 @@ -import { TextBaseline } from "@src/platform/ICanvas"; -import { Duration } from "./Duration"; -import { MusicFontSymbol } from "./MusicFontSymbol"; +import { TextBaseline } from '@src/platform/ICanvas'; +import { Duration } from '@src/model/Duration'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; /** - * Describes an instrument articulation which is used for percussions. + * Describes an instrument articulation which is used for percussions. * @json * @json_strict */ @@ -13,43 +13,50 @@ export class InstrumentArticulation { */ public elementType: string; /** - * Gets or sets the line the note head should be shown for standard notation + * The line the note head should be shown for standard notation. + * + * @remarks + * This value is a bit special and its semantics are adopted from Guitar Pro: + * Staff lines are actually "steps" including lines and spaces on the staff. + * 1 means the note is on the top line of the staff and from there its counting downwards. */ public staffLine: number; /** - * Gets or sets the note head to display by default. + * Gets or sets the note head to display by default. */ public noteHeadDefault: MusicFontSymbol; /** - * Gets or sets the note head to display for half duration notes. + * Gets or sets the note head to display for half duration notes. */ public noteHeadHalf: MusicFontSymbol; /** - * Gets or sets the note head to display for whole duration notes. + * Gets or sets the note head to display for whole duration notes. */ public noteHeadWhole: MusicFontSymbol; /** - * Gets or sets which additional technique symbol should be placed for the note head. + * Gets or sets which additional technique symbol should be placed for the note head. */ public techniqueSymbol: MusicFontSymbol; /** - * Gets or sets where the technique symbol should be placed. + * Gets or sets where the technique symbol should be placed. */ public techniqueSymbolPlacement: TextBaseline; + /** - * Gets or sets which midi number to use when playing the note. + * Gets or sets which midi key to use when playing the note. */ public outputMidiNumber: number; public constructor( - elementType: string = "", + 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: TextBaseline = TextBaseline.Middle) { + techniqueSymbolPlacement: TextBaseline = TextBaseline.Middle + ) { this.elementType = elementType; this.outputMidiNumber = outputMidiNumber; this.staffLine = staffLine; diff --git a/src/model/JsonConverter.ts b/src/model/JsonConverter.ts index 6208b65f1..d1ee80d7b 100644 --- a/src/model/JsonConverter.ts +++ b/src/model/JsonConverter.ts @@ -1,11 +1,26 @@ -import { AlphaTabMetronomeEvent, AlphaTabRestEvent, ControlChangeEvent, EndOfTrackEvent, MidiEvent, MidiEventType, NoteBendEvent, NoteEvent, NoteOffEvent, NoteOnEvent, PitchBendEvent, ProgramChangeEvent, TempoChangeEvent, TimeSignatureEvent } from '@src/midi/MidiEvent'; +import { + AlphaTabMetronomeEvent, + AlphaTabRestEvent, + ControlChangeEvent, + EndOfTrackEvent, + type MidiEvent, + MidiEventType, + NoteBendEvent, + type NoteEvent, + NoteOffEvent, + NoteOnEvent, + PitchBendEvent, + ProgramChangeEvent, + TempoChangeEvent, + TimeSignatureEvent +} from '@src/midi/MidiEvent'; import { MidiFile, MidiTrack } from '@src/midi/MidiFile'; import { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { ScoreSerializer } from '@src/generated/model/ScoreSerializer'; import { SettingsSerializer } from '@src/generated/SettingsSerializer'; import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; -import { ControllerType } from '@src/midi'; +import type { ControllerType } from '@src/midi/ControllerType'; import { JsonHelper } from '@src/io/JsonHelper'; /** @@ -19,14 +34,15 @@ export class JsonConverter { private static jsonReplacer(_: any, v: any) { if (v instanceof Map) { if ('fromEntries' in Object) { - return (Object as any).fromEntries(v); - } else { - const o: any = {}; - for (const [k, mv] of v) { o[k] = mv; } - return o; + return Object.fromEntries(v); } + const o: any = {}; + for (const [k, mv] of v) { + o[k] = mv; + } + return o; } - else if (ArrayBuffer.isView(v)) { + if (ArrayBuffer.isView(v)) { return Array.apply([], [v]); } return v; @@ -39,7 +55,7 @@ export class JsonConverter { * @target web */ public static scoreToJson(score: Score): string { - let obj: unknown = JsonConverter.scoreToJsObject(score); + const obj: unknown = JsonConverter.scoreToJsObject(score); return JSON.stringify(obj, JsonConverter.jsonReplacer); } @@ -70,13 +86,12 @@ export class JsonConverter { * @returns The converted score object. */ public static jsObjectToScore(jsObject: unknown, settings?: Settings): Score { - let score: Score = new Score(); + const score: Score = new Score(); ScoreSerializer.fromJson(score, jsObject); score.finish(settings ?? new Settings()); return score; } - /** * Converts the given settings into a JSON encoded string. * @param settings The settings to serialize. @@ -84,7 +99,7 @@ export class JsonConverter { * @target web */ public static settingsToJson(settings: Settings): string { - let obj: unknown = JsonConverter.settingsToJsObject(settings); + const obj: unknown = JsonConverter.settingsToJsObject(settings); return JSON.stringify(obj, JsonConverter.jsonReplacer); } @@ -113,18 +128,18 @@ export class JsonConverter { * @returns The converted Settings object. */ public static jsObjectToSettings(jsObject: unknown): Settings { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); SettingsSerializer.fromJson(settings, jsObject); return settings; } /** * Converts the given JavaScript object into a MidiFile object. - * @param jsObject The javascript object to deserialize. + * @param jsObject The javascript object to deserialize. * @returns The converted MidiFile. */ public static jsObjectToMidiFile(jsObject: unknown): MidiFile { - let midi2: MidiFile = new MidiFile(); + const midi2: MidiFile = new MidiFile(); JsonHelper.forEach(jsObject, (v, k) => { switch (k) { @@ -132,8 +147,8 @@ export class JsonConverter { midi2.division = v as number; break; case 'tracks': - for (let midiTrack of (v as unknown[])) { - let midiTrack2: MidiTrack = JsonConverter.jsObjectToMidiTrack(midiTrack); + for (const midiTrack of v as unknown[]) { + const midiTrack2: MidiTrack = JsonConverter.jsObjectToMidiTrack(midiTrack); midi2.tracks.push(midiTrack2); } break; @@ -143,14 +158,14 @@ export class JsonConverter { return midi2; } - private static jsObjectToMidiTrack(jsObject:unknown): MidiTrack { - let midi2: MidiTrack = new MidiTrack(); + private static jsObjectToMidiTrack(jsObject: unknown): MidiTrack { + const midi2: MidiTrack = new MidiTrack(); JsonHelper.forEach(jsObject, (v, k) => { switch (k) { case 'events': - for (let midiEvent of (v as unknown[])) { - let midiEvent2: MidiEvent = JsonConverter.jsObjectToMidiEvent(midiEvent); + for (const midiEvent of v as unknown[]) { + const midiEvent2: MidiEvent = JsonConverter.jsObjectToMidiEvent(midiEvent); midi2.events.push(midiEvent2); } break; @@ -162,13 +177,13 @@ export class JsonConverter { /** * Converts the given JavaScript object into a MidiEvent object. - * @param jsObject The javascript object to deserialize. + * @param jsObject The javascript object to deserialize. * @returns The converted MidiEvent. */ public static jsObjectToMidiEvent(midiEvent: unknown): MidiEvent { - let track: number = JsonHelper.getValue(midiEvent, 'track') as number; - let tick: number = JsonHelper.getValue(midiEvent, 'tick') as number; - let type: MidiEventType = JsonHelper.getValue(midiEvent, 'type') as number as MidiEventType; + const track: number = JsonHelper.getValue(midiEvent, 'track') as number; + const tick: number = JsonHelper.getValue(midiEvent, 'tick') as number; + const type: MidiEventType = JsonHelper.getValue(midiEvent, 'type') as number as MidiEventType; switch (type) { case MidiEventType.TimeSignature: @@ -181,11 +196,7 @@ export class JsonConverter { JsonHelper.getValue(midiEvent, 'thirdySecondNodesInQuarter') as number ); case MidiEventType.AlphaTabRest: - return new AlphaTabRestEvent( - track, - tick, - JsonHelper.getValue(midiEvent, 'channel') as number - ); + return new AlphaTabRestEvent(track, tick, JsonHelper.getValue(midiEvent, 'channel') as number); case MidiEventType.AlphaTabMetronome: return new AlphaTabMetronomeEvent( track, @@ -249,12 +260,12 @@ export class JsonConverter { return new EndOfTrackEvent(track, tick); } - throw new AlphaTabError(AlphaTabErrorType.Format, 'Unknown Midi Event type: ' + type); + throw new AlphaTabError(AlphaTabErrorType.Format, `Unknown Midi Event type: ${type}`); } /** * Converts the given MidiFile object into a serialized JavaScript object. - * @param midi The midi file to convert. + * @param midi The midi file to convert. * @returns A serialized MidiFile object without ciruclar dependencies that can be used for further serializations. */ public static midiFileToJsObject(midi: MidiFile): Map { @@ -262,7 +273,7 @@ export class JsonConverter { o.set('division', midi.division); const tracks: Map[] = []; - for (let track of midi.tracks) { + for (const track of midi.tracks) { tracks.push(JsonConverter.midiTrackToJsObject(track)); } o.set('tracks', tracks); @@ -274,7 +285,7 @@ export class JsonConverter { const o = new Map(); const events: Map[] = []; - for (let track of midi.events) { + for (const track of midi.events) { events.push(JsonConverter.midiEventToJsObject(track)); } o.set('events', events); @@ -284,7 +295,7 @@ export class JsonConverter { /** * Converts the given MidiEvent object into a serialized JavaScript object. - * @param midi The midi file to convert. + * @param midi The midi file to convert. * @returns A serialized MidiEvent object without ciruclar dependencies that can be used for further serializations. */ public static midiEventToJsObject(midiEvent: MidiEvent): Map { @@ -304,7 +315,10 @@ export class JsonConverter { break; case MidiEventType.AlphaTabMetronome: o.set('metronomeNumerator', (midiEvent as AlphaTabMetronomeEvent).metronomeNumerator); - o.set('metronomeDurationInMilliseconds', (midiEvent as AlphaTabMetronomeEvent).metronomeDurationInMilliseconds); + o.set( + 'metronomeDurationInMilliseconds', + (midiEvent as AlphaTabMetronomeEvent).metronomeDurationInMilliseconds + ); o.set('metronomeDurationInTicks', (midiEvent as AlphaTabMetronomeEvent).metronomeDurationInTicks); break; case MidiEventType.NoteOn: diff --git a/src/model/KeySignatureType.ts b/src/model/KeySignatureType.ts index 3d8bb4b56..9ccec4681 100644 --- a/src/model/KeySignatureType.ts +++ b/src/model/KeySignatureType.ts @@ -5,9 +5,9 @@ export enum KeySignatureType { /** * Major */ - Major, + Major = 0, /** * Minor */ - Minor + Minor = 1 } diff --git a/src/model/Lyrics.ts b/src/model/Lyrics.ts index 84baa54be..595a4cf3b 100644 --- a/src/model/Lyrics.ts +++ b/src/model/Lyrics.ts @@ -1,9 +1,9 @@ enum LyricsState { - IgnoreSpaces, - Begin, - Text, - Comment, - Dash + IgnoreSpaces = 0, + Begin = 1, + Text = 2, + Comment = 3, + Dash = 4 } /** @@ -50,7 +50,7 @@ export class Lyrics { let start: number = 0; while (p < str.length) { - let c: number = str.charCodeAt(p); + const c: number = str.charCodeAt(p); switch (state) { case LyricsState.IgnoreSpaces: switch (c) { @@ -96,7 +96,7 @@ export class Lyrics { case Lyrics.CharCodeCR: case Lyrics.CharCodeLF: case Lyrics.CharCodeSpace: - let txt: string = str.substr(start, p - start); + const txt: string = str.substr(start, p - start); this.addChunk(txt, skipEmptyEntries); state = LyricsState.IgnoreSpaces; next = LyricsState.Begin; @@ -108,7 +108,7 @@ export class Lyrics { case Lyrics.CharCodeDash: break; default: - let txt: string = str.substr(start, p - start); + const txt: string = str.substr(start, p - start); this.addChunk(txt, skipEmptyEntries); skipSpace = true; state = LyricsState.IgnoreSpaces; @@ -134,7 +134,7 @@ export class Lyrics { } private prepareChunk(txt: string): string { - let chunk = txt.split('+').join(' '); + const chunk = txt.split('+').join(' '); // trim off trailing _ like "You____" becomes "You" let endLength = chunk.length; diff --git a/src/model/MasterBar.ts b/src/model/MasterBar.ts index 46be7857c..11686af96 100644 --- a/src/model/MasterBar.ts +++ b/src/model/MasterBar.ts @@ -1,14 +1,16 @@ import { MidiUtils } from '@src/midi/MidiUtils'; -import { Automation } from '@src/model/Automation'; -import { Beat } from '@src/model/Beat'; -import { Fermata } from '@src/model/Fermata'; -import { KeySignature } from '@src/model/KeySignature'; -import { KeySignatureType } from '@src/model/KeySignatureType'; -import { RepeatGroup } from '@src/model/RepeatGroup'; -import { Score } from '@src/model/Score'; -import { Section } from '@src/model/Section'; +import type { Automation } from '@src/model/Automation'; +import type { Beat } from '@src/model/Beat'; +import type { Fermata } from '@src/model/Fermata'; +// biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 +import type { Bar } from '@src/model/Bar'; +import type { KeySignature } from '@src/model/KeySignature'; +import type { KeySignatureType } from '@src/model/KeySignatureType'; +import type { RepeatGroup } from '@src/model/RepeatGroup'; +import type { Score } from '@src/model/Score'; +import type { Section } from '@src/model/Section'; import { TripletFeel } from '@src/model/TripletFeel'; -import { Direction } from './Direction'; +import type { Direction } from '@src/model/Direction'; /** * The MasterBar stores information about a bar which affects @@ -43,17 +45,40 @@ export class MasterBar { public index: number = 0; /** - * Gets or sets the key signature used on all bars. + * The key signature used on all bars. + * @deprecated Use key signatures on bar level */ - public keySignature: KeySignature = KeySignature.C; + public get keySignature(): KeySignature { + return this.score.tracks[0].staves[0].bars[this.index].keySignature; + } + + /** + * The key signature used on all bars. + * @deprecated Use key signatures on bar level + */ + public set keySignature(value: KeySignature) { + this.score.tracks[0].staves[0].bars[this.index].keySignature = value; + } /** - * Gets or sets the type of key signature (major/minor) + * The type of key signature (major/minor) + * @deprecated Use key signatures on bar level */ - public keySignatureType: KeySignatureType = KeySignatureType.Major; + public get keySignatureType(): KeySignatureType { + return this.score.tracks[0].staves[0].bars[this.index].keySignatureType; + } + + /** + * The type of key signature (major/minor) + * @deprecated Use key signatures on bar level + */ + public set keySignatureType(value: KeySignatureType) { + this.score.tracks[0].staves[0].bars[this.index].keySignatureType = value; + } /** * Gets or sets whether a double bar is shown for this masterbar. + * @deprecated Use {@link Bar.barLineLeft} and {@link Bar.barLineRight} */ public isDoubleBar: boolean = false; @@ -168,9 +193,9 @@ export class MasterBar { public calculateDuration(respectAnacrusis: boolean = true): number { if (this.isAnacrusis && respectAnacrusis) { let duration: number = 0; - for (let track of this.score.tracks) { - for (let staff of track.staves) { - let barDuration: number = + for (const track of this.score.tracks) { + for (const staff of track.staves) { + const barDuration: number = this.index < staff.bars.length ? staff.bars[this.index].calculateDuration() : 0; if (barDuration > duration) { duration = barDuration; @@ -200,8 +225,8 @@ export class MasterBar { * Adds a direction to the masterbar. * @param direction The direction to add. */ - public addDirection(direction:Direction):void { - if(this.directions == null){ + public addDirection(direction: Direction): void { + if (this.directions == null) { this.directions = new Set(); } this.directions.add(direction); diff --git a/src/model/ModelUtils.ts b/src/model/ModelUtils.ts index b4df427e7..e948833ac 100644 --- a/src/model/ModelUtils.ts +++ b/src/model/ModelUtils.ts @@ -1,12 +1,17 @@ import { GeneralMidi } from '@src/midi/GeneralMidi'; import { Beat } from '@src/model/Beat'; -import { Duration } from '@src/model/Duration'; +import type { Duration } from '@src/model/Duration'; import { Fingers } from '@src/model/Fingers'; -import { Score } from '@src/model/Score'; +import { HeaderFooterStyle, type Score, ScoreStyle, type ScoreSubElement } from '@src/model/Score'; import { FingeringMode } from '@src/NotationSettings'; -import { Settings } from '@src/Settings'; -import { NoteAccidentalMode } from './NoteAccidentalMode'; -import { MasterBar } from './MasterBar'; +import type { Settings } from '@src/Settings'; +import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; +import { MasterBar } from '@src/model/MasterBar'; +import type { Track } from '@src/model/Track'; +import { SynthConstants } from '@src/synth/SynthConstants'; +import { Bar } from '@src/model/Bar'; +import { Voice } from '@src/model/Voice'; +import { Automation, AutomationType } from '@src/model/Automation'; export class TuningParseResult { public note: string | null = null; @@ -32,8 +37,8 @@ export class TuningParseResultTone { */ export class ModelUtils { public static getIndex(duration: Duration): number { - let index: number = 0; - let value: number = duration; + const index: number = 0; + const value: number = duration; if (value < 0) { return index; } @@ -55,12 +60,12 @@ export class ModelUtils { public static applyPitchOffsets(settings: Settings, score: Score): void { for (let i: number = 0; i < score.tracks.length; i++) { if (i < settings.notation.displayTranspositionPitches.length) { - for (let staff of score.tracks[i].staves) { + for (const staff of score.tracks[i].staves) { staff.displayTranspositionPitch = -settings.notation.displayTranspositionPitches[i]; } } if (i < settings.notation.transpositionPitches.length) { - for (let staff of score.tracks[i].staves) { + for (const staff of score.tracks[i].staves) { staff.transpositionPitch = -settings.notation.transpositionPitches[i]; } } @@ -146,7 +151,7 @@ export class ModelUtils { let note: string = ''; let octave: string = ''; for (let i: number = 0; i < name.length; i++) { - let c: number = name.charCodeAt(i); + const c: number = name.charCodeAt(i); if (c >= 0x30 && c <= 0x39 /* 0-9 */) { // number without note? if (!note) { @@ -162,9 +167,9 @@ export class ModelUtils { if (!octave || !note) { return null; } - let result: TuningParseResult = new TuningParseResult(); + const result: TuningParseResult = new TuningParseResult(); - result.octave = parseInt(octave) + 1; + result.octave = Number.parseInt(octave) + 1; result.note = note.toLowerCase(); result.tone = ModelUtils.getToneForText(result.note); @@ -179,7 +184,7 @@ export class ModelUtils { } public static getTuningForText(str: string): number { - let result: TuningParseResult | null = ModelUtils.parseTuning(str); + const result: TuningParseResult | null = ModelUtils.parseTuning(str); if (!result) { return -1; } @@ -281,36 +286,26 @@ export class ModelUtils { } public static newGuid(): string { - return ( - Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1) + - Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1) + - '-' + - Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1) + - '-' + - Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1) + - '-' + - Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1) + - '-' + - Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1) + + return `${ Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1) + Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1) - ); + }-${Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1)}-${Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1)}-${Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1)}-${Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1)}${Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1)}${Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1)}`; } public static isAlmostEqualTo(a: number, b: number): boolean { @@ -319,13 +314,13 @@ export class ModelUtils { public static toHexString(n: number, digits: number = 0): string { let s: string = ''; - let hexChars: string = '0123456789ABCDEF'; + const hexChars: string = '0123456789ABCDEF'; do { s = String.fromCharCode(hexChars.charCodeAt(n & 15)) + s; n = n >> 4; } while (n > 0); while (s.length < digits) { - s = '0' + s; + s = `0${s}`; } return s; } @@ -384,4 +379,248 @@ export class ModelUtils { } return value; } + + public static buildMultiBarRestInfo( + tracks: Track[] | null, + startIndex: number, + endIndexInclusive: number + ): Map | null { + if (!tracks) { + return null; + } + const stylesheet = tracks[0].score.stylesheet; + const shouldDrawMultiBarRests: boolean = + tracks.length > 1 + ? stylesheet.multiTrackMultiBarRest + : stylesheet.perTrackMultiBarRest?.has(tracks[0].index) === true; + if (!shouldDrawMultiBarRests) { + return null; + } + + const lookup = new Map(); + + const score = tracks[0].score; + + let currentIndex = startIndex; + while (currentIndex <= endIndexInclusive) { + const currentGroupStartIndex = currentIndex; + let currentGroup: number[] | null = null; + + while (currentIndex <= endIndexInclusive) { + const masterBar = score.masterBars[currentIndex]; + + // check if masterbar breaks multibar rests, it must be fully empty with no annotations + if ( + masterBar.alternateEndings || + (masterBar.isRepeatStart && masterBar.index !== currentGroupStartIndex) || + masterBar.isFreeTime || + masterBar.isAnacrusis || + masterBar.section !== null || + (masterBar.index !== currentGroupStartIndex && masterBar.tempoAutomations.length > 0) || + (masterBar.fermata !== null && masterBar.fermata.size > 0) || + (masterBar.directions !== null && masterBar.directions.size > 0) + ) { + break; + } + + // check if masterbar breaks multibar rests because of change to previous + if ( + currentGroupStartIndex > startIndex && + masterBar.previousMasterBar && + (masterBar.timeSignatureCommon !== masterBar.previousMasterBar!.timeSignatureCommon || + masterBar.timeSignatureNumerator !== masterBar.previousMasterBar!.timeSignatureNumerator || + masterBar.timeSignatureDenominator !== masterBar.previousMasterBar!.timeSignatureDenominator || + masterBar.tripletFeel !== masterBar.previousMasterBar!.tripletFeel) + ) { + break; + } + + // masterbar is good, now check bars across staves + let areAllBarsSuitable = true; + for (const t of tracks) { + for (const s of t.staves) { + const bar = s.bars[masterBar.index]; + + if (!bar.isRestOnly) { + areAllBarsSuitable = false; + break; + } + + if ( + bar.index > 0 && + (bar.keySignature !== bar.previousBar!.keySignature || + bar.keySignatureType !== bar.previousBar!.keySignatureType) + ) { + areAllBarsSuitable = false; + break; + } + } + + if (!areAllBarsSuitable) { + break; + } + } + + if (!areAllBarsSuitable) { + break; + } + + // skip initial bar as it is not "additional" but we are checking it + currentIndex++; + if (masterBar.index > currentGroupStartIndex) { + if (currentGroup === null) { + currentGroup = [masterBar.index]; + } else { + currentGroup.push(masterBar.index); + } + } + + // special scenario -> repeat ends are included but then we stop + if (masterBar.isRepeatEnd) { + break; + } + } + + if (currentGroup) { + lookup.set(currentGroupStartIndex, currentGroup); + } else { + currentIndex++; + } + } + + return lookup; + } + + public static computeFirstDisplayedBarIndex(score: Score, settings: Settings) { + let startIndex: number = settings.display.startBar; + startIndex--; // map to array index + + startIndex = Math.min(score.masterBars.length - 1, Math.max(0, startIndex)); + return startIndex; + } + + public static computeLastDisplayedBarIndex(score: Score, settings: Settings, startIndex: number) { + let endBarIndex: number = settings.display.barCount; + if (endBarIndex < 0) { + endBarIndex = score.masterBars.length; + } + endBarIndex = startIndex + endBarIndex - 1; // map count to array index + + endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex)); + return endBarIndex; + } + + public static getOrCreateHeaderFooterStyle(score: Score, element: ScoreSubElement) { + let style = score.style; + if (!score.style) { + style = new ScoreStyle(); + score.style = style; + } + + let headerFooterStyle: HeaderFooterStyle; + if (style!.headerAndFooter.has(element)) { + headerFooterStyle = style!.headerAndFooter.get(element)!; + } else { + headerFooterStyle = new HeaderFooterStyle(); + + if (ScoreStyle.defaultHeaderAndFooter.has(element)) { + const defaults = ScoreStyle.defaultHeaderAndFooter.get(element)!; + headerFooterStyle.template = defaults.template; + headerFooterStyle.textAlign = defaults.textAlign; + } + + style!.headerAndFooter.set(element, headerFooterStyle); + } + + return headerFooterStyle; + } + + /** + * Performs some general consolidations of inconsistencies on the given score like + * missing bars, beats, duplicated midi channels etc + */ + public static consolidate(score: Score) { + // empty score? + if (score.masterBars.length === 0) { + const master: MasterBar = new MasterBar(); + score.addMasterBar(master); + + const tempoAutomation = new Automation(); + tempoAutomation.isLinear = false; + tempoAutomation.type = AutomationType.Tempo; + tempoAutomation.value = score.tempo; + master.tempoAutomations.push(tempoAutomation); + + const bar: Bar = new Bar(); + score.tracks[0].staves[0].addBar(bar); + + const v = new Voice(); + bar.addVoice(v); + + const emptyBeat: Beat = new Beat(); + emptyBeat.isEmpty = true; + v.addBeat(emptyBeat); + return; + } + + const usedChannels = new Set([SynthConstants.PercussionChannel]); + for (const track of score.tracks) { + // ensure percussion channel + if (track.staves.length === 1 && track.staves[0].isPercussion) { + track.playbackInfo.primaryChannel = SynthConstants.PercussionChannel; + track.playbackInfo.secondaryChannel = SynthConstants.PercussionChannel; + } else { + // unique midi channels and generate secondary channels + if (track.playbackInfo.primaryChannel !== SynthConstants.PercussionChannel) { + while (usedChannels.has(track.playbackInfo.primaryChannel)) { + track.playbackInfo.primaryChannel++; + } + } + usedChannels.add(track.playbackInfo.primaryChannel); + + if (track.playbackInfo.secondaryChannel !== SynthConstants.PercussionChannel) { + while (usedChannels.has(track.playbackInfo.secondaryChannel)) { + track.playbackInfo.secondaryChannel++; + } + } + usedChannels.add(track.playbackInfo.secondaryChannel); + } + + for (const staff of track.staves) { + // fill empty beats + for (const b of staff.bars) { + for (const v of b.voices) { + if (v.isEmpty && v.beats.length === 0) { + const emptyBeat: Beat = new Beat(); + emptyBeat.isEmpty = true; + v.addBeat(emptyBeat); + } + } + } + + // fill missing bars + const voiceCount = staff.bars.length === 0 ? 1 : staff.bars[0].voices.length; + while (staff.bars.length < score.masterBars.length) { + const bar: Bar = new Bar(); + staff.addBar(bar); + const previousBar = bar.previousBar; + if (previousBar) { + bar.clef = previousBar.clef; + bar.clefOttava = previousBar.clefOttava; + bar.keySignature = bar.previousBar!.keySignature; + bar.keySignatureType = bar.previousBar!.keySignatureType; + } + + for (let i = 0; i < voiceCount; i++) { + const v = new Voice(); + bar.addVoice(v); + + const emptyBeat: Beat = new Beat(); + emptyBeat.isEmpty = true; + v.addBeat(emptyBeat); + } + } + } + } + } } diff --git a/src/model/MusicFontSymbol.ts b/src/model/MusicFontSymbol.ts index 40aa285d2..6dd898295 100644 --- a/src/model/MusicFontSymbol.ts +++ b/src/model/MusicFontSymbol.ts @@ -39,18 +39,59 @@ export enum MusicFontSymbol { NoteheadBlack = 0xe0a4, NoteheadNull = 0xe0a5, NoteheadXOrnate = 0xe0aa, + + NoteheadPlusDoubleWhole = 0xe0ac, + NoteheadPlusWhole = 0xe0ad, + NoteheadPlusHalf = 0xe0ae, + NoteheadPlusBlack = 0xe0af, + + NoteheadSquareWhite = 0xe0b8, + NoteheadSquareBlack = 0xe0b9, + + NoteheadTriangleUpDoubleWhole = 0xe0ba, NoteheadTriangleUpWhole = 0xe0bb, NoteheadTriangleUpHalf = 0xe0bc, NoteheadTriangleUpBlack = 0xe0be, + + NoteheadTriangleRightWhite = 0xe0c1, + NoteheadTriangleRightBlack = 0xe0c2, + + NoteheadTriangleDownDoubleWhole = 0xe0cc, + NoteheadTriangleDownWhole = 0xe0c4, + NoteheadTriangleDownHalf = 0xe0c5, + NoteheadTriangleDownBlack = 0xe0c7, + + NoteheadDiamondDoubleWhole = 0xe0d7, + NoteheadDiamondWhole = 0xe0d8, + NoteheadDiamondHalf = 0xe0d9, + NoteheadDiamondBlack = 0xe0db, + NoteheadDiamondBlackWide = 0xe0dc, NoteheadDiamondWhite = 0xe0dd, NoteheadDiamondWhiteWide = 0xe0de, + NoteheadCircleXDoubleWhole = 0xe0b0, + NoteheadCircleXWhole = 0xe0b1, + NoteheadCircleXHalf = 0xe0b2, NoteheadCircleX = 0xe0b3, + NoteheadXDoubleWhole = 0xe0a6, NoteheadXWhole = 0xe0a7, NoteheadXHalf = 0xe0a8, NoteheadXBlack = 0xe0a9, NoteheadParenthesis = 0xe0ce, + NoteheadSlashedBlack1 = 0xe0cf, NoteheadSlashedBlack2 = 0xe0d0, + NoteheadSlashedHalf1 = 0xe0d1, + NoteheadSlashedHalf2 = 0xe0d2, + NoteheadSlashedWhole1 = 0xe0d3, + NoteheadSlashedWhole2 = 0xe0d4, + NoteheadSlashedDoubleWhole1 = 0xe0d5, + NoteheadSlashedDoubleWhole2 = 0xe0d6, + + NoteheadCircledBlack = 0xe0e4, + NoteheadCircledHalf = 0xe0e5, + NoteheadCircledWhole = 0xe0e6, + NoteheadCircledDoubleWhole = 0xe0e7, + NoteheadCircleSlash = 0xe0f7, NoteheadHeavyX = 0xe0f8, NoteheadHeavyXHat = 0xe0f9, @@ -59,22 +100,56 @@ export enum MusicFontSymbol { NoteheadSlashWhiteWhole = 0xe102, NoteheadSlashWhiteHalf = 0xe103, + NoteheadRoundWhiteWithDot = 0xe115, + + NoteheadSquareBlackLarge = 0xe11a, + NoteheadSquareBlackWhite = 0xe11b, + + NoteheadClusterDoubleWhole3rd = 0xe128, + NoteheadClusterWhole3rd = 0xe129, + NoteheadClusterHalf3rd = 0xe12a, + NoteheadClusterQuarter3rd = 0xe12b, + + NoteShapeRoundWhite = 0xe1b0, + NoteShapeRoundBlack = 0xe1b1, + + NoteShapeSquareWhite = 0xe1b2, + NoteShapeSquareBlack = 0xe1b3, + + NoteShapeTriangleRightWhite = 0xe1b4, + NoteShapeTriangleRightBlack = 0xe1b5, + + NoteShapeTriangleLeftWhite = 0xe1b6, + NoteShapeTriangleLeftBlack = 0xe1b7, + + NoteShapeDiamondWhite = 0xe1b8, + NoteShapeDiamondBlack = 0xe1b9, + + NoteShapeTriangleUpWhite = 0xe1ba, + NoteShapeTriangleUpBlack = 0xe1bb, + + NoteShapeMoonWhite = 0xe1bc, + NoteShapeMoonBlack = 0xe1bd, + + NoteShapeTriangleRoundWhite = 0xe1be, + NoteShapeTriangleRoundBlack = 0xe1bf, + NoteQuarterUp = 0xe1d5, NoteEighthUp = 0xe1d7, - TextBlackNoteLongStem = 0xE1F1, - TextBlackNoteFrac8thLongStem = 0xE1F3, - TextBlackNoteFrac16thLongStem = 0xE1F5, - TextBlackNoteFrac32ndLongStem = 0xE1F6, + TextBlackNoteLongStem = 0xe1f1, + TextBlackNoteFrac8thLongStem = 0xe1f3, + TextBlackNoteFrac16thLongStem = 0xe1f5, + TextBlackNoteFrac32ndLongStem = 0xe1f6, - TextCont8thBeamLongStem = 0xE1F8, - TextCont16thBeamLongStem = 0xE1FA, - TextCont32ndBeamLongStem = 0xE1FB, + TextCont8thBeamLongStem = 0xe1f8, + TextCont16thBeamLongStem = 0xe1fa, + TextCont32ndBeamLongStem = 0xe1fb, - TextAugmentationDot = 0xE1FC, - TextTupletBracketStartLongStem = 0xE201, - TextTuplet3LongStem = 0xE202, - TextTupletBracketEndLongStem = 0xE203, + TextAugmentationDot = 0xe1fc, + TextTupletBracketStartLongStem = 0xe201, + TextTuplet3LongStem = 0xe202, + TextTupletBracketEndLongStem = 0xe203, Tremolo3 = 0xe222, Tremolo2 = 0xe221, @@ -130,6 +205,10 @@ export enum MusicFontSymbol { RestOneHundredTwentyEighth = 0xe4ea, RestTwoHundredFiftySixth = 0xe4eb, + RestHBarLeft = 0xe4ef, + RestHBarMiddle = 0xe4f0, + RestHBarRight = 0xe4f1, + Repeat1Bar = 0xe500, Repeat2Bars = 0xe501, @@ -139,14 +218,32 @@ export enum MusicFontSymbol { Quindicesima = 0xe514, QuindicesimaAlta = 0xe515, + DynamicPPPPPP = 0xe527, + DynamicPPPPP = 0xe528, + DynamicPPPP = 0xe529, DynamicPPP = 0xe52a, DynamicPP = 0xe52b, DynamicPiano = 0xe520, DynamicMP = 0xe52c, DynamicMF = 0xe52d, + DynamicPF = 0xe52e, DynamicForte = 0xe522, DynamicFF = 0xe52f, DynamicFFF = 0xe530, + DynamicFFFF = 0xe531, + DynamicFFFFF = 0xe532, + DynamicFFFFFF = 0xe533, + DynamicFortePiano = 0xe534, + DynamicNiente = 0xe526, + DynamicSforzando1 = 0xe536, + DynamicSforzandoPiano = 0xe537, + DynamicSforzandoPianissimo = 0xe538, + DynamicSforzato = 0xe539, + DynamicSforzatoPiano = 0xe53a, + DynamicSforzatoFF = 0xe53b, + DynamicRinforzando1 = 0xe53c, + DynamicRinforzando2 = 0xe53d, + DynamicForzando = 0xe535, OrnamentTrill = 0xe566, OrnamentTurn = 0xe567, @@ -157,8 +254,8 @@ export enum MusicFontSymbol { StringsDownBow = 0xe610, StringsUpBow = 0xe612, - KeyboardPedalPed = 0xE650, - KeyboardPedalUp = 0xE655, + KeyboardPedalPed = 0xe650, + KeyboardPedalUp = 0xe655, PictEdgeOfCymbal = 0xe729, diff --git a/src/model/Note.ts b/src/model/Note.ts index cca704ac3..b569148df 100644 --- a/src/model/Note.ts +++ b/src/model/Note.ts @@ -1,6 +1,6 @@ import { AccentuationType } from '@src/model/AccentuationType'; -import { Beat } from '@src/model/Beat'; -import { BendPoint } from '@src/model/BendPoint'; +import type { Beat } from '@src/model/Beat'; +import type { BendPoint } from '@src/model/BendPoint'; import { BendStyle } from '@src/model/BendStyle'; import { BendType } from '@src/model/BendType'; import { Duration } from '@src/model/Duration'; @@ -11,16 +11,18 @@ import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; import { Ottavia } from '@src/model/Ottavia'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; -import { Staff } from '@src/model/Staff'; +import type { Staff } from '@src/model/Staff'; import { VibratoType } from '@src/model/VibratoType'; import { NotationMode } from '@src/NotationSettings'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { Lazy } from '@src/util/Lazy'; import { Logger } from '@src/Logger'; import { ModelUtils } from '@src/model/ModelUtils'; import { PickStroke } from '@src/model/PickStroke'; import { PercussionMapper } from '@src/model/PercussionMapper'; -import { NoteOrnament } from './NoteOrnament'; +import { NoteOrnament } from '@src/model/NoteOrnament'; +import { ElementStyle } from '@src/model/ElementStyle'; +import type { MusicFontSymbol } from '@src/model/MusicFontSymbol'; class NoteIdBag { public tieDestinationNoteId: number = -1; @@ -29,6 +31,90 @@ class NoteIdBag { public slurOriginNoteId: number = -1; public hammerPullDestinationNoteId: number = -1; public hammerPullOriginNoteId: number = -1; + public slideTargetNoteId: number = -1; + public slideOriginNoteId: number = -1; +} + +/** + * Lists all graphical sub elements within a {@link Note} which can be styled via {@link Note.style} + */ +export enum NoteSubElement { + /** + * The effects and annotations shown in dedicated effect bands above the staves (e.g. vibrato). + * The style of the first note with the effect wins. + */ + Effects = 0, + + /** + * The note head on the standard notation staff. + */ + StandardNotationNoteHead = 1, + + /** + * The accidentals on the standard notation staff. + */ + StandardNotationAccidentals = 2, + + /** + * The effects and annotations applied to this note on the standard notation staff (e.g. bends). + * If effects on beats result in individual note elements shown, this color will apply. + */ + StandardNotationEffects = 3, + + /** + * The fret number on the guitar tab staff. + */ + GuitarTabFretNumber = 4, + + /** + * The effects and annotations applied to this note on the guitar tab staff (e.g. bends). + * If effects on beats result in individual note elements shown, this color will apply. + */ + GuitarTabEffects = 5, + + /** + * The note head on the slash notation staff. + */ + SlashNoteHead = 6, + + /** + * The effects and annotations applied to this note on the slash notation staff (e.g. dots). + * If effects on beats result in individual note elements shown, this color will apply. + */ + SlashEffects = 7, + + /** + * The note number on the numbered notation staff. + */ + NumberedNumber = 8, + + /** + * The accidentals on the numbered notation staff. + */ + NumberedAccidentals = 9, + + /** + * The effects and annotations applied to this note on the number notation staff (e.g. dots). + * If effects on beats result in individual note elements shown, this color will apply. + */ + NumberedEffects = 10 +} + +/** + * Defines the custom styles for notes. + * @json + * @json_strict + */ +export class NoteStyle extends ElementStyle { + /** + * The symbol that should be used as note head. + */ + public noteHead?: MusicFontSymbol; + + /** + * Whether the note head symbol should be centered on the stem (e.g. for arrow notes) + */ + public noteHeadCenterOnStem?: boolean; } /** @@ -40,7 +126,18 @@ class NoteIdBag { * @json_strict */ export class Note { + /** + * @internal + */ public static GlobalNoteId: number = 0; + + /** + * @internal + */ + public static resetIds() { + Note.GlobalNoteId = 0; + } + /** * Gets or sets the unique id of this note. * @clone_ignore @@ -403,7 +500,7 @@ export class Note { /** * Gets the desination of the tie. * @clone_ignore - * @json_ignore + * @json_ignore */ public tieDestination: Note | null = null; @@ -506,6 +603,12 @@ export class Note { */ public ornament: NoteOrnament = NoteOrnament.None; + /** + * The style customizations for this item. + * @clone_ignore + */ + public style?: NoteStyle; + public get stringTuning(): number { return this.beat.voice.bar.staff.capo + Note.getStringTuning(this.beat.voice.bar.staff, this.string); } @@ -527,8 +630,8 @@ export class Note { /** * Calculates the real note value of this note as midi key respecting the given options. - * @param applyTranspositionPitch Whether or not to apply the transposition pitch of the current staff. - * @param applyHarmonic Whether or not to apply harmonic pitches to the note. + * @param applyTranspositionPitch Whether or not to apply the transposition pitch of the current staff. + * @param applyHarmonic Whether or not to apply harmonic pitches to the note. * @returns The calculated note value as midi key. */ public calculateRealValue(applyTranspositionPitch: boolean, applyHarmonic: boolean): number { @@ -545,25 +648,24 @@ export class Note { } return realValue; } - else { - if (this.isPercussion) { - return this.percussionArticulation; - } - if (this.isStringed) { - return this.fret + this.stringTuning - transpositionPitch; - } - if (this.isPiano) { - return this.octave * 12 + this.tone - transpositionPitch; - } - return 0; + + if (this.isPercussion) { + return this.percussionArticulation; + } + if (this.isStringed) { + return this.fret + this.stringTuning - transpositionPitch; } + if (this.isPiano) { + return this.octave * 12 + this.tone - transpositionPitch; + } + return 0; } public get harmonicPitch(): number { if (this.harmonicType === HarmonicType.None || !this.isStringed) { return 0; } - let value: number = this.harmonicValue; + const value: number = this.harmonicValue; // add semitones to reach corresponding harmonic frets if (ModelUtils.isAlmostEqualTo(value, 2.4)) { return 36; @@ -642,14 +744,22 @@ export class Note { public get initialBendValue(): number { if (this.hasBend) { return Math.floor(this.bendPoints![0].value / 2); - } else if (this.bendOrigin) { + } + if (this.bendOrigin) { return Math.floor(this.bendOrigin.bendPoints![this.bendOrigin.bendPoints!.length - 1].value / 2); - } else if (this.isTieDestination && this.tieOrigin!.bendOrigin) { - return Math.floor(this.tieOrigin!.bendOrigin.bendPoints![this.tieOrigin!.bendOrigin.bendPoints!.length - 1].value / 2); - } else if (this.beat.hasWhammyBar) { + } + if (this.isTieDestination && this.tieOrigin!.bendOrigin) { + return Math.floor( + this.tieOrigin!.bendOrigin.bendPoints![this.tieOrigin!.bendOrigin.bendPoints!.length - 1].value / 2 + ); + } + if (this.beat.hasWhammyBar) { return Math.floor(this.beat.whammyBarPoints![0].value / 2); - } else if (this.beat.isContinuedWhammy) { - return Math.floor(this.beat.previousBeat!.whammyBarPoints![this.beat.previousBeat!.whammyBarPoints!.length - 1].value / 2); + } + if (this.beat.isContinuedWhammy) { + return Math.floor( + this.beat.previousBeat!.whammyBarPoints![this.beat.previousBeat!.whammyBarPoints!.length - 1].value / 2 + ); } return 0; } @@ -711,7 +821,7 @@ export class Note { if (this.beat.isContinuedWhammy) { return ( this.beat.previousBeat!.whammyBarPoints![this.beat.previousBeat!.whammyBarPoints!.length - 1].value % - 2 !== + 2 !== 0 ); } @@ -734,8 +844,8 @@ export class Note { } public finish(settings: Settings, sharedDataBag: Map | null = null): void { - let nextNoteOnLine: Lazy = new Lazy(() => Note.nextNoteOnSameLine(this)); - let isSongBook: boolean = settings && settings.notation.notationMode === NotationMode.SongBook; + const nextNoteOnLine: Lazy = new Lazy(() => Note.nextNoteOnSameLine(this)); + const isSongBook: boolean = settings && settings.notation.notationMode === NotationMode.SongBook; // connect ties if (this.isTieDestination) { @@ -766,7 +876,7 @@ export class Note { } // set hammeron/pulloffs if (this.isHammerPullOrigin) { - let hammerPullDestination = Note.findHammerPullDestination(this); + const hammerPullDestination = Note.findHammerPullDestination(this); if (!hammerPullDestination) { this.isHammerPullOrigin = false; } else { @@ -778,7 +888,10 @@ export class Note { switch (this.slideOutType) { case SlideOutType.Shift: case SlideOutType.Legato: - this.slideTarget = nextNoteOnLine.value; + if (!this.slideTarget) { + this.slideTarget = nextNoteOnLine.value; + } + if (!this.slideTarget) { this.slideOutType = SlideOutType.None; } else { @@ -810,7 +923,7 @@ export class Note { const hasBend = points != null && points.length > 0; if (hasBend) { - let isContinuedBend: boolean = this.isTieDestination && this.tieOrigin!.hasBend; + const isContinuedBend: boolean = this.isTieDestination && this.tieOrigin!.hasBend; this.isContinuedBend = isContinuedBend; } else { this.bendType = BendType.None; @@ -818,10 +931,10 @@ export class Note { if (hasBend && this.bendType === BendType.Custom) { if (points!.length === 4) { - let origin: BendPoint = points[0]; - let middle1: BendPoint = points[1]; - let middle2: BendPoint = points[2]; - let destination: BendPoint = points[3]; + const origin: BendPoint = points[0]; + const middle1: BendPoint = points[1]; + const middle2: BendPoint = points[2]; + const destination: BendPoint = points[3]; // the middle points are used for holds, anything else is a new feature we do not support yet if (middle1.value === middle2.value) { // bend higher? @@ -865,8 +978,8 @@ export class Note { Logger.warning('Model', 'Unsupported bend type detected, fallback to custom', null); } } else if (points.length === 2) { - let origin: BendPoint = points[0]; - let destination: BendPoint = points[1]; + const origin: BendPoint = points[0]; + const destination: BendPoint = points[1]; // bend higher? if (destination.value > origin.value) { if (!this.isContinuedBend && origin.value > 0) { @@ -904,7 +1017,7 @@ export class Note { let nextBeat: Beat | null = note.beat.nextBeat; // keep searching in same bar while (nextBeat && nextBeat.voice.bar.index <= note.beat.voice.bar.index + Note.MaxOffsetForSameLineSearch) { - let noteOnString: Note | null = nextBeat.getNoteOnString(note.string); + const noteOnString: Note | null = nextBeat.getNoteOnString(note.string); if (noteOnString) { return noteOnString; } @@ -938,9 +1051,8 @@ export class Note { if (noteOnString) { if (noteOnString.isLeftHandTapped) { return noteOnString; - } else { - break; } + break; } } @@ -950,9 +1062,8 @@ export class Note { if (noteOnString) { if (noteOnString.isLeftHandTapped) { return noteOnString; - } else { - break; } + break; } } @@ -970,7 +1081,7 @@ export class Note { previousBeat.voice.bar.index >= note.beat.voice.bar.index - Note.MaxOffsetForSameLineSearch ) { if (note.isStringed) { - let noteOnString: Note | null = previousBeat.getNoteOnString(note.string); + const noteOnString: Note | null = previousBeat.getNoteOnString(note.string); if (noteOnString) { return noteOnString; } @@ -982,7 +1093,7 @@ export class Note { return previousBeat.notes[note.index]; } } else { - let noteWithValue: Note | null = previousBeat.getNoteWithRealValue(note.realValue); + const noteWithValue: Note | null = previousBeat.getNoteWithRealValue(note.realValue); if (noteWithValue) { return noteWithValue; } @@ -1016,13 +1127,16 @@ export class Note { // if this note is a source note for any effect, remember it for later // the destination note will look it up for linking - if (this._noteIdBag.hammerPullDestinationNoteId !== -1 || + if ( + this._noteIdBag.hammerPullDestinationNoteId !== -1 || this._noteIdBag.tieDestinationNoteId !== -1 || - this._noteIdBag.slurDestinationNoteId !== -1) { + this._noteIdBag.slurDestinationNoteId !== -1 || + this._noteIdBag.slideTargetNoteId !== -1 + ) { noteIdLookup.set(this.id, this); } - // on any effect destiniation, lookup the origin which should already be + // on any effect destiniation, lookup the origin which should already be // registered if (this._noteIdBag.hammerPullOriginNoteId !== -1) { this.hammerPullOrigin = noteIdLookup.get(this._noteIdBag.hammerPullOriginNoteId)!; @@ -1037,6 +1151,11 @@ export class Note { this.slurOrigin.slurDestination = this; } + if (this._noteIdBag.slideOriginNoteId !== -1) { + this.slideOrigin = noteIdLookup.get(this._noteIdBag.slideOriginNoteId)!; + this.slideOrigin.slideTarget = this; + } + this._noteIdBag = null; // not needed anymore } else { // no tie destination at all? @@ -1044,7 +1163,7 @@ export class Note { return; } - let tieOrigin = this.tieOrigin ?? Note.findTieOrigin(this); + const tieOrigin = this.tieOrigin ?? Note.findTieOrigin(this); if (!tieOrigin) { this.isTieDestination = false; } else { @@ -1083,6 +1202,13 @@ export class Note { if (this.hammerPullDestination !== null) { o.set('hammerpulldestinationnoteid', this.hammerPullDestination.id); } + + if (this.slideTarget !== null) { + o.set('slidetargetnoteid', this.slideTarget.id); + } + if (this.slideOrigin !== null) { + o.set('slideoriginnoteid', this.slideOrigin.id); + } } /** @@ -1090,44 +1216,56 @@ export class Note { */ public setProperty(property: string, v: unknown): boolean { switch (property) { - case "tiedestinationnoteid": + case 'tiedestinationnoteid': if (this._noteIdBag == null) { this._noteIdBag = new NoteIdBag(); } this._noteIdBag.tieDestinationNoteId = v as number; return true; - case "tieoriginnoteid": + case 'tieoriginnoteid': if (this._noteIdBag == null) { this._noteIdBag = new NoteIdBag(); } this._noteIdBag.tieOriginNoteId = v as number; return true; - case "slurdestinationnoteid": + case 'slurdestinationnoteid': if (this._noteIdBag == null) { this._noteIdBag = new NoteIdBag(); } this._noteIdBag.slurDestinationNoteId = v as number; return true; - case "sluroriginnoteid": + case 'sluroriginnoteid': if (this._noteIdBag == null) { this._noteIdBag = new NoteIdBag(); } this._noteIdBag.slurOriginNoteId = v as number; return true; - case "hammerpulloriginnoteid": + case 'hammerpulloriginnoteid': if (this._noteIdBag == null) { this._noteIdBag = new NoteIdBag(); } this._noteIdBag.hammerPullOriginNoteId = v as number; return true; - case "hammerpulldestinationnoteid": + case 'hammerpulldestinationnoteid': if (this._noteIdBag == null) { this._noteIdBag = new NoteIdBag(); } this._noteIdBag.hammerPullDestinationNoteId = v as number; return true; + case 'slidetargetnoteid': + if (this._noteIdBag == null) { + this._noteIdBag = new NoteIdBag(); + } + this._noteIdBag.slideTargetNoteId = v as number; + return true; + case 'slideoriginnoteid': + if (this._noteIdBag == null) { + this._noteIdBag = new NoteIdBag(); + } + this._noteIdBag.slideOriginNoteId = v as number; + return true; } return false; } diff --git a/src/model/NoteAccidentalMode.ts b/src/model/NoteAccidentalMode.ts index 9bb651eae..fdcfa4932 100644 --- a/src/model/NoteAccidentalMode.ts +++ b/src/model/NoteAccidentalMode.ts @@ -5,29 +5,29 @@ export enum NoteAccidentalMode { /** * Accidentals are calculated automatically. */ - Default, + Default = 0, /** * This will try to ensure that no accidental is shown. */ - ForceNone, + ForceNone = 1, /** * This will move the note one line down and applies a Naturalize. */ - ForceNatural, + ForceNatural = 2, /** * This will move the note one line down and applies a Sharp. */ - ForceSharp, + ForceSharp = 3, /** * This will move the note to be shown 2 half-notes deeper with a double sharp symbol */ - ForceDoubleSharp, + ForceDoubleSharp = 4, /** * This will move the note one line up and applies a Flat. */ - ForceFlat, + ForceFlat = 5, /** * This will move the note two half notes up with a double flag symbol. */ - ForceDoubleFlat, + ForceDoubleFlat = 6 } diff --git a/src/model/NoteOrnament.ts b/src/model/NoteOrnament.ts index 456b3c1ff..49b908838 100644 --- a/src/model/NoteOrnament.ts +++ b/src/model/NoteOrnament.ts @@ -2,9 +2,9 @@ * Lists all note ornaments. */ export enum NoteOrnament { - None, - InvertedTurn, - Turn, - UpperMordent, - LowerMordent + None = 0, + InvertedTurn = 1, + Turn = 2, + UpperMordent = 3, + LowerMordent = 4 } diff --git a/src/model/Ottavia.ts b/src/model/Ottavia.ts index 3c40d0162..0c8255a06 100644 --- a/src/model/Ottavia.ts +++ b/src/model/Ottavia.ts @@ -5,21 +5,21 @@ export enum Ottavia { /** * 2 octaves higher */ - _15ma, + _15ma = 0, /** * 1 octave higher */ - _8va, + _8va = 1, /** * Normal */ - Regular, + Regular = 2, /** * 1 octave lower */ - _8vb, + _8vb = 3, /** * 2 octaves lower. */ - _15mb + _15mb = 4 } diff --git a/src/model/PercussionMapper.ts b/src/model/PercussionMapper.ts index aa5c8aa30..9351a2242 100644 --- a/src/model/PercussionMapper.ts +++ b/src/model/PercussionMapper.ts @@ -1,14 +1,14 @@ -import { MusicFontSymbol } from "@src/model/MusicFontSymbol"; -import { TextBaseline } from "@src/platform/ICanvas"; -import { InstrumentArticulation } from "@src/model/InstrumentArticulation"; -import { Note } from "@src/model/Note"; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import { TextBaseline } from '@src/platform/ICanvas'; +import { InstrumentArticulation } from '@src/model/InstrumentArticulation'; +import type { Note } from '@src/model/Note'; 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) + [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) @@ -24,10 +24,9 @@ export class PercussionMapper { [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) + [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) { @@ -40,11 +39,11 @@ export class PercussionMapper { } /* - * This map was generated using the following steps: + * 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): + * 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'); @@ -75,100 +74,1084 @@ export class PercussionMapper { * copy(s) */ public static instrumentArticulations: Map = new Map([ - [38, new InstrumentArticulation("snare", 3, 38, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [37, new InstrumentArticulation("snare", 3, 37, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [91, new InstrumentArticulation("snare", 3, 38, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite)], - [42, new InstrumentArticulation("hiHat", -1, 42, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [92, new InstrumentArticulation("hiHat", -1, 46, MusicFontSymbol.NoteheadCircleSlash, MusicFontSymbol.NoteheadCircleSlash, MusicFontSymbol.NoteheadCircleSlash)], - [46, new InstrumentArticulation("hiHat", -1, 46, MusicFontSymbol.NoteheadCircleX, MusicFontSymbol.NoteheadCircleX, MusicFontSymbol.NoteheadCircleX)], - [44, new InstrumentArticulation("hiHat", 9, 44, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [35, new InstrumentArticulation("kickDrum", 8, 35, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [36, new InstrumentArticulation("kickDrum", 7, 36, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [50, new InstrumentArticulation("tom", 1, 50, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [48, new InstrumentArticulation("tom", 2, 48, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [47, new InstrumentArticulation("tom", 4, 47, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [45, new InstrumentArticulation("tom", 5, 45, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [43, new InstrumentArticulation("tom", 6, 43, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [93, new InstrumentArticulation("ride", 0, 51, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.PictEdgeOfCymbal, TextBaseline.Bottom)], - [51, new InstrumentArticulation("ride", 0, 51, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [53, new InstrumentArticulation("ride", 0, 53, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite)], - [94, new InstrumentArticulation("ride", 0, 51, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.ArticStaccatoAbove, TextBaseline.Top)], - [55, new InstrumentArticulation("splash", -2, 55, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [95, new InstrumentArticulation("splash", -2, 55, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.ArticStaccatoAbove, TextBaseline.Bottom)], - [52, new InstrumentArticulation("china", -3, 52, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat)], - [96, new InstrumentArticulation("china", -3, 52, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat, MusicFontSymbol.NoteheadHeavyXHat)], - [49, new InstrumentArticulation("crash", -2, 49, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX)], - [97, new InstrumentArticulation("crash", -2, 49, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.ArticStaccatoAbove, TextBaseline.Bottom)], - [57, new InstrumentArticulation("crash", -1, 57, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX)], - [98, new InstrumentArticulation("crash", -1, 57, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.NoteheadHeavyX, MusicFontSymbol.ArticStaccatoAbove, TextBaseline.Bottom)], - [99, new InstrumentArticulation("cowbell", 1, 56, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpHalf, MusicFontSymbol.NoteheadTriangleUpWhole)], - [100, new InstrumentArticulation("cowbell", 1, 56, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXHalf, MusicFontSymbol.NoteheadXWhole)], - [56, new InstrumentArticulation("cowbell", 0, 56, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpHalf, MusicFontSymbol.NoteheadTriangleUpWhole)], - [101, new InstrumentArticulation("cowbell", 0, 56, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXHalf, MusicFontSymbol.NoteheadXWhole)], - [102, new InstrumentArticulation("cowbell", -1, 56, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpHalf, MusicFontSymbol.NoteheadTriangleUpWhole)], - [103, new InstrumentArticulation("cowbell", -1, 56, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXHalf, MusicFontSymbol.NoteheadXWhole)], - [77, new InstrumentArticulation("woodblock", -9, 77, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack)], - [76, new InstrumentArticulation("woodblock", -10, 76, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack)], - [60, new InstrumentArticulation("bongo", -4, 60, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [104, new InstrumentArticulation("bongo", -5, 60, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TextBaseline.Middle)], - [105, new InstrumentArticulation("bongo", -6, 60, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [61, new InstrumentArticulation("bongo", -7, 61, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [106, new InstrumentArticulation("bongo", -8, 61, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TextBaseline.Middle)], - [107, new InstrumentArticulation("bongo", -16, 61, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [66, new InstrumentArticulation("timbale", 10, 66, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [65, new InstrumentArticulation("timbale", 9, 65, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [68, new InstrumentArticulation("agogo", 12, 68, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [67, new InstrumentArticulation("agogo", 11, 67, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [64, new InstrumentArticulation("conga", 17, 64, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [108, new InstrumentArticulation("conga", 16, 64, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [109, new InstrumentArticulation("conga", 15, 64, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TextBaseline.Middle)], - [63, new InstrumentArticulation("conga", 14, 63, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [110, new InstrumentArticulation("conga", 13, 63, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [62, new InstrumentArticulation("conga", 19, 62, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.NoteheadParenthesis, TextBaseline.Middle)], - [72, new InstrumentArticulation("whistle", -11, 72, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [71, new InstrumentArticulation("whistle", -17, 71, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [73, new InstrumentArticulation("guiro", 38, 73, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [74, new InstrumentArticulation("guiro", 37, 74, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [86, new InstrumentArticulation("surdo", 36, 86, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [87, new InstrumentArticulation("surdo", 35, 87, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadParenthesis, TextBaseline.Middle)], - [54, new InstrumentArticulation("tambourine", 3, 54, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack)], - [111, new InstrumentArticulation("tambourine", 2, 54, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.StringsUpBow, TextBaseline.Bottom)], - [112, new InstrumentArticulation("tambourine", 1, 54, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.NoteheadTriangleUpBlack, MusicFontSymbol.StringsDownBow, TextBaseline.Bottom)], - [113, new InstrumentArticulation("tambourine", -7, 54, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [79, new InstrumentArticulation("cuica", 30, 79, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [78, new InstrumentArticulation("cuica", 29, 78, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [58, new InstrumentArticulation("vibraslap", 28, 58, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [81, new InstrumentArticulation("triangle", 27, 81, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [80, new InstrumentArticulation("triangle", 26, 80, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadParenthesis, TextBaseline.Middle)], - [114, new InstrumentArticulation("grancassa", 25, 43, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [115, new InstrumentArticulation("piatti", 18, 49, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [116, new InstrumentArticulation("piatti", 24, 49, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [69, new InstrumentArticulation("cabasa", 23, 69, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [117, new InstrumentArticulation("cabasa", 22, 69, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, TextBaseline.Bottom)], - [85, new InstrumentArticulation("castanets", 21, 85, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [75, new InstrumentArticulation("claves", 20, 75, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [70, new InstrumentArticulation("maraca", -12, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [118, new InstrumentArticulation("maraca", -13, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, TextBaseline.Bottom)], - [119, new InstrumentArticulation("maraca", -14, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [120, new InstrumentArticulation("maraca", -15, 70, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, TextBaseline.Bottom)], - [82, new InstrumentArticulation("shaker", -23, 54, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [122, new InstrumentArticulation("shaker", -24, 54, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, TextBaseline.Bottom)], - [84, new InstrumentArticulation("bellTree", -18, 53, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [123, new InstrumentArticulation("bellTree", -19, 53, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole, MusicFontSymbol.StringsUpBow, TextBaseline.Bottom)], - [83, new InstrumentArticulation("jingleBell", -20, 53, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [124, new InstrumentArticulation("unpitched", -21, 62, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.GuitarGolpe, TextBaseline.Top)], - [125, new InstrumentArticulation("unpitched", -22, 62, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.NoteheadNull, MusicFontSymbol.GuitarGolpe, TextBaseline.Bottom)], - [39, new InstrumentArticulation("handClap", 3, 39, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [40, new InstrumentArticulation("snare", 3, 40, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [31, new InstrumentArticulation("snare", 3, 40, MusicFontSymbol.NoteheadSlashedBlack2, MusicFontSymbol.NoteheadSlashedBlack2, MusicFontSymbol.NoteheadSlashedBlack2)], - [41, new InstrumentArticulation("tom", 5, 41, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadHalf, MusicFontSymbol.NoteheadWhole)], - [59, new InstrumentArticulation("ride", 2, 59, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.PictEdgeOfCymbal, TextBaseline.Bottom)], - [126, new InstrumentArticulation("ride", 2, 59, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [127, new InstrumentArticulation("ride", 2, 59, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite, MusicFontSymbol.NoteheadDiamondWhite)], - [29, new InstrumentArticulation("ride", 2, 59, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.ArticStaccatoAbove, TextBaseline.Top)], - [30, new InstrumentArticulation("crash", -3, 49, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [33, new InstrumentArticulation("snare", 3, 37, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack, MusicFontSymbol.NoteheadXBlack)], - [34, new InstrumentArticulation("snare", 3, 38, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadBlack)] + [ + 38, + new InstrumentArticulation( + 'snare', + 3, + 38, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 37, + new InstrumentArticulation( + 'snare', + 3, + 37, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 91, + new InstrumentArticulation( + 'snare', + 3, + 38, + MusicFontSymbol.NoteheadDiamondWhite, + MusicFontSymbol.NoteheadDiamondWhite, + MusicFontSymbol.NoteheadDiamondWhite + ) + ], + [ + 42, + new InstrumentArticulation( + 'hiHat', + -1, + 42, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 92, + new InstrumentArticulation( + 'hiHat', + -1, + 46, + MusicFontSymbol.NoteheadCircleSlash, + MusicFontSymbol.NoteheadCircleSlash, + MusicFontSymbol.NoteheadCircleSlash + ) + ], + [ + 46, + new InstrumentArticulation( + 'hiHat', + -1, + 46, + MusicFontSymbol.NoteheadCircleX, + MusicFontSymbol.NoteheadCircleX, + MusicFontSymbol.NoteheadCircleX + ) + ], + [ + 44, + new InstrumentArticulation( + 'hiHat', + 9, + 44, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 35, + new InstrumentArticulation( + 'kickDrum', + 8, + 35, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 36, + new InstrumentArticulation( + 'kickDrum', + 7, + 36, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 50, + new InstrumentArticulation( + 'tom', + 1, + 50, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 48, + new InstrumentArticulation( + 'tom', + 2, + 48, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 47, + new InstrumentArticulation( + 'tom', + 4, + 47, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 45, + new InstrumentArticulation( + 'tom', + 5, + 45, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 43, + new InstrumentArticulation( + 'tom', + 6, + 43, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 93, + new InstrumentArticulation( + 'ride', + 0, + 51, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.PictEdgeOfCymbal, + TextBaseline.Bottom + ) + ], + [ + 51, + new InstrumentArticulation( + 'ride', + 0, + 51, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 53, + new InstrumentArticulation( + 'ride', + 0, + 53, + MusicFontSymbol.NoteheadDiamondWhite, + MusicFontSymbol.NoteheadDiamondWhite, + MusicFontSymbol.NoteheadDiamondWhite + ) + ], + [ + 94, + new InstrumentArticulation( + 'ride', + 0, + 51, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.ArticStaccatoAbove, + TextBaseline.Top + ) + ], + [ + 55, + new InstrumentArticulation( + 'splash', + -2, + 55, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 95, + new InstrumentArticulation( + 'splash', + -2, + 55, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.ArticStaccatoAbove, + TextBaseline.Bottom + ) + ], + [ + 52, + new InstrumentArticulation( + 'china', + -3, + 52, + MusicFontSymbol.NoteheadHeavyXHat, + MusicFontSymbol.NoteheadHeavyXHat, + MusicFontSymbol.NoteheadHeavyXHat + ) + ], + [ + 96, + new InstrumentArticulation( + 'china', + -3, + 52, + MusicFontSymbol.NoteheadHeavyXHat, + MusicFontSymbol.NoteheadHeavyXHat, + MusicFontSymbol.NoteheadHeavyXHat + ) + ], + [ + 49, + new InstrumentArticulation( + 'crash', + -2, + 49, + MusicFontSymbol.NoteheadHeavyX, + MusicFontSymbol.NoteheadHeavyX, + MusicFontSymbol.NoteheadHeavyX + ) + ], + [ + 97, + new InstrumentArticulation( + 'crash', + -2, + 49, + MusicFontSymbol.NoteheadHeavyX, + MusicFontSymbol.NoteheadHeavyX, + MusicFontSymbol.NoteheadHeavyX, + MusicFontSymbol.ArticStaccatoAbove, + TextBaseline.Bottom + ) + ], + [ + 57, + new InstrumentArticulation( + 'crash', + -1, + 57, + MusicFontSymbol.NoteheadHeavyX, + MusicFontSymbol.NoteheadHeavyX, + MusicFontSymbol.NoteheadHeavyX + ) + ], + [ + 98, + new InstrumentArticulation( + 'crash', + -1, + 57, + MusicFontSymbol.NoteheadHeavyX, + MusicFontSymbol.NoteheadHeavyX, + MusicFontSymbol.NoteheadHeavyX, + MusicFontSymbol.ArticStaccatoAbove, + TextBaseline.Bottom + ) + ], + [ + 99, + new InstrumentArticulation( + 'cowbell', + 1, + 56, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpHalf, + MusicFontSymbol.NoteheadTriangleUpWhole + ) + ], + [ + 100, + new InstrumentArticulation( + 'cowbell', + 1, + 56, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXHalf, + MusicFontSymbol.NoteheadXWhole + ) + ], + [ + 56, + new InstrumentArticulation( + 'cowbell', + 0, + 56, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpHalf, + MusicFontSymbol.NoteheadTriangleUpWhole + ) + ], + [ + 101, + new InstrumentArticulation( + 'cowbell', + 0, + 56, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXHalf, + MusicFontSymbol.NoteheadXWhole + ) + ], + [ + 102, + new InstrumentArticulation( + 'cowbell', + -1, + 56, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpHalf, + MusicFontSymbol.NoteheadTriangleUpWhole + ) + ], + [ + 103, + new InstrumentArticulation( + 'cowbell', + -1, + 56, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXHalf, + MusicFontSymbol.NoteheadXWhole + ) + ], + [ + 77, + new InstrumentArticulation( + 'woodblock', + -9, + 77, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpBlack + ) + ], + [ + 76, + new InstrumentArticulation( + 'woodblock', + -10, + 76, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpBlack + ) + ], + [ + 60, + new InstrumentArticulation( + 'bongo', + -4, + 60, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 104, + new InstrumentArticulation( + 'bongo', + -5, + 60, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole, + MusicFontSymbol.NoteheadParenthesis, + TextBaseline.Middle + ) + ], + [ + 105, + new InstrumentArticulation( + 'bongo', + -6, + 60, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 61, + new InstrumentArticulation( + 'bongo', + -7, + 61, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 106, + new InstrumentArticulation( + 'bongo', + -8, + 61, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole, + MusicFontSymbol.NoteheadParenthesis, + TextBaseline.Middle + ) + ], + [ + 107, + new InstrumentArticulation( + 'bongo', + -16, + 61, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 66, + new InstrumentArticulation( + 'timbale', + 10, + 66, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 65, + new InstrumentArticulation( + 'timbale', + 9, + 65, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 68, + new InstrumentArticulation( + 'agogo', + 12, + 68, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 67, + new InstrumentArticulation( + 'agogo', + 11, + 67, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 64, + new InstrumentArticulation( + 'conga', + 17, + 64, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 108, + new InstrumentArticulation( + 'conga', + 16, + 64, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 109, + new InstrumentArticulation( + 'conga', + 15, + 64, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole, + MusicFontSymbol.NoteheadParenthesis, + TextBaseline.Middle + ) + ], + [ + 63, + new InstrumentArticulation( + 'conga', + 14, + 63, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 110, + new InstrumentArticulation( + 'conga', + 13, + 63, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 62, + new InstrumentArticulation( + 'conga', + 19, + 62, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole, + MusicFontSymbol.NoteheadParenthesis, + TextBaseline.Middle + ) + ], + [ + 72, + new InstrumentArticulation( + 'whistle', + -11, + 72, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 71, + new InstrumentArticulation( + 'whistle', + -17, + 71, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 73, + new InstrumentArticulation( + 'guiro', + 38, + 73, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 74, + new InstrumentArticulation( + 'guiro', + 37, + 74, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 86, + new InstrumentArticulation( + 'surdo', + 36, + 86, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 87, + new InstrumentArticulation( + 'surdo', + 35, + 87, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadParenthesis, + TextBaseline.Middle + ) + ], + [ + 54, + new InstrumentArticulation( + 'tambourine', + 3, + 54, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpBlack + ) + ], + [ + 111, + new InstrumentArticulation( + 'tambourine', + 2, + 54, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.StringsUpBow, + TextBaseline.Bottom + ) + ], + [ + 112, + new InstrumentArticulation( + 'tambourine', + 1, + 54, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.NoteheadTriangleUpBlack, + MusicFontSymbol.StringsDownBow, + TextBaseline.Bottom + ) + ], + [ + 113, + new InstrumentArticulation( + 'tambourine', + -7, + 54, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 79, + new InstrumentArticulation( + 'cuica', + 30, + 79, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 78, + new InstrumentArticulation( + 'cuica', + 29, + 78, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 58, + new InstrumentArticulation( + 'vibraslap', + 28, + 58, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 81, + new InstrumentArticulation( + 'triangle', + 27, + 81, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 80, + new InstrumentArticulation( + 'triangle', + 26, + 80, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadParenthesis, + TextBaseline.Middle + ) + ], + [ + 114, + new InstrumentArticulation( + 'grancassa', + 25, + 43, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 115, + new InstrumentArticulation( + 'piatti', + 18, + 49, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 116, + new InstrumentArticulation( + 'piatti', + 24, + 49, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 69, + new InstrumentArticulation( + 'cabasa', + 23, + 69, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 117, + new InstrumentArticulation( + 'cabasa', + 22, + 69, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole, + MusicFontSymbol.StringsUpBow, + TextBaseline.Bottom + ) + ], + [ + 85, + new InstrumentArticulation( + 'castanets', + 21, + 85, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 75, + new InstrumentArticulation( + 'claves', + 20, + 75, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 70, + new InstrumentArticulation( + 'maraca', + -12, + 70, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 118, + new InstrumentArticulation( + 'maraca', + -13, + 70, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole, + MusicFontSymbol.StringsUpBow, + TextBaseline.Bottom + ) + ], + [ + 119, + new InstrumentArticulation( + 'maraca', + -14, + 70, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 120, + new InstrumentArticulation( + 'maraca', + -15, + 70, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole, + MusicFontSymbol.StringsUpBow, + TextBaseline.Bottom + ) + ], + [ + 82, + new InstrumentArticulation( + 'shaker', + -23, + 54, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 122, + new InstrumentArticulation( + 'shaker', + -24, + 54, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole, + MusicFontSymbol.StringsUpBow, + TextBaseline.Bottom + ) + ], + [ + 84, + new InstrumentArticulation( + 'bellTree', + -18, + 53, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 123, + new InstrumentArticulation( + 'bellTree', + -19, + 53, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole, + MusicFontSymbol.StringsUpBow, + TextBaseline.Bottom + ) + ], + [ + 83, + new InstrumentArticulation( + 'jingleBell', + -20, + 53, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 124, + new InstrumentArticulation( + 'unpitched', + -21, + 62, + MusicFontSymbol.NoteheadNull, + MusicFontSymbol.NoteheadNull, + MusicFontSymbol.NoteheadNull, + MusicFontSymbol.GuitarGolpe, + TextBaseline.Top + ) + ], + [ + 125, + new InstrumentArticulation( + 'unpitched', + -22, + 62, + MusicFontSymbol.NoteheadNull, + MusicFontSymbol.NoteheadNull, + MusicFontSymbol.NoteheadNull, + MusicFontSymbol.GuitarGolpe, + TextBaseline.Bottom + ) + ], + [ + 39, + new InstrumentArticulation( + 'handClap', + 3, + 39, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 40, + new InstrumentArticulation( + 'snare', + 3, + 40, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 31, + new InstrumentArticulation( + 'snare', + 3, + 40, + MusicFontSymbol.NoteheadSlashedBlack2, + MusicFontSymbol.NoteheadSlashedBlack2, + MusicFontSymbol.NoteheadSlashedBlack2 + ) + ], + [ + 41, + new InstrumentArticulation( + 'tom', + 5, + 41, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadHalf, + MusicFontSymbol.NoteheadWhole + ) + ], + [ + 59, + new InstrumentArticulation( + 'ride', + 2, + 59, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.PictEdgeOfCymbal, + TextBaseline.Bottom + ) + ], + [ + 126, + new InstrumentArticulation( + 'ride', + 2, + 59, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 127, + new InstrumentArticulation( + 'ride', + 2, + 59, + MusicFontSymbol.NoteheadDiamondWhite, + MusicFontSymbol.NoteheadDiamondWhite, + MusicFontSymbol.NoteheadDiamondWhite + ) + ], + [ + 29, + new InstrumentArticulation( + 'ride', + 2, + 59, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.ArticStaccatoAbove, + TextBaseline.Top + ) + ], + [ + 30, + new InstrumentArticulation( + 'crash', + -3, + 49, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 33, + new InstrumentArticulation( + 'snare', + 3, + 37, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack, + MusicFontSymbol.NoteheadXBlack + ) + ], + [ + 34, + new InstrumentArticulation( + 'snare', + 3, + 38, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadBlack, + MusicFontSymbol.NoteheadBlack + ) + ] ]); // these are manually defined names/identifiers for the articulation list above. @@ -274,13 +1257,16 @@ export class PercussionMapper { public static getArticulation(n: Note): InstrumentArticulation | null { const articulationIndex = n.percussionArticulation; + if (articulationIndex < 0) { + return null; + } const trackArticulations = n.beat.voice.bar.staff.track.percussionArticulations; if (articulationIndex < trackArticulations.length) { return trackArticulations[articulationIndex]; } - return PercussionMapper.getArticulationByValue(articulationIndex);; + return PercussionMapper.getArticulationByInputMidiNumber(articulationIndex); } public static getElementAndVariation(n: Note): number[] { @@ -289,11 +1275,11 @@ export class PercussionMapper { return [-1, -1]; } - // search for the first element/variation combination with the same midi output + // search for the first element/variation combination with the same midi output 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.getArticulationByValue(variations[variation]); + const gp6Articulation = PercussionMapper.getArticulationByInputMidiNumber(variations[variation]); if (gp6Articulation?.outputMidiNumber === articulation.outputMidiNumber) { return [element, variation]; } @@ -303,9 +1289,9 @@ export class PercussionMapper { return [-1, -1]; } - public static getArticulationByValue(midiNumber: number): InstrumentArticulation | null { - if (PercussionMapper.instrumentArticulations.has(midiNumber)) { - return PercussionMapper.instrumentArticulations.get(midiNumber)!; + public static getArticulationByInputMidiNumber(inputMidiNumber: number): InstrumentArticulation | null { + if (PercussionMapper.instrumentArticulations.has(inputMidiNumber)) { + return PercussionMapper.instrumentArticulations.get(inputMidiNumber)!; } return null; } diff --git a/src/model/PickStroke.ts b/src/model/PickStroke.ts index 10f1525ac..5b43a8290 100644 --- a/src/model/PickStroke.ts +++ b/src/model/PickStroke.ts @@ -5,13 +5,13 @@ export enum PickStroke { /** * No pickstroke used. */ - None, + None = 0, /** * Pickstroke up. */ - Up, + Up = 1, /** * Pickstroke down */ - Down + Down = 2 } diff --git a/src/model/Rasgueado.ts b/src/model/Rasgueado.ts index 11ef5dafd..772d0df57 100644 --- a/src/model/Rasgueado.ts +++ b/src/model/Rasgueado.ts @@ -2,23 +2,23 @@ * Lists all Rasgueado types. */ export enum Rasgueado { - None, - Ii, - Mi, - MiiTriplet, - MiiAnapaest, - PmpTriplet, - PmpAnapaest, - PeiTriplet, - PeiAnapaest, - PaiTriplet, - PaiAnapaest, - AmiTriplet, - AmiAnapaest, - Ppp, - Amii, - Amip, - Eami, - Eamii, - Peami -} \ No newline at end of file + None = 0, + Ii = 1, + Mi = 2, + MiiTriplet = 3, + MiiAnapaest = 4, + PmpTriplet = 5, + PmpAnapaest = 6, + PeiTriplet = 7, + PeiAnapaest = 8, + PaiTriplet = 9, + PaiAnapaest = 10, + AmiTriplet = 11, + AmiAnapaest = 12, + Ppp = 13, + Amii = 14, + Amip = 15, + Eami = 16, + Eamii = 17, + Peami = 18 +} diff --git a/src/model/RenderStylesheet.ts b/src/model/RenderStylesheet.ts index 3645ee860..8d89f024b 100644 --- a/src/model/RenderStylesheet.ts +++ b/src/model/RenderStylesheet.ts @@ -1,5 +1,6 @@ // @ts-ignore https://github.com/microsoft/TypeScript/issues/61183 -import type { Track } from './Track'; +// biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 +import type { Track } from '@src/model/Track'; /** * Lists the different modes on how the brackets/braces are drawn and extended. @@ -30,15 +31,15 @@ export enum TrackNamePolicy { /** * Track names are hidden everywhere. */ - Hidden, + Hidden = 0, /** * Track names are displayed on the first system. */ - FirstSystem, + FirstSystem = 1, /** * Track names are displayed on all systems. */ - AllSystems + AllSystems = 2 } /** @@ -48,11 +49,11 @@ export enum TrackNameMode { /** * Full track names are displayed {@link Track.name} */ - FullName, + FullName = 0, /** * Short Track names (abbreviations) are displayed {@link Track.shortName} */ - ShortName + ShortName = 1 } /** @@ -62,11 +63,11 @@ export enum TrackNameOrientation { /** * Text is shown horizontally (left-to-right) */ - Horizontal, + Horizontal = 0, /** - * Vertically rotated (bottom-to-top) + * Vertically rotated (bottom-to-top) */ - Vertical + Vertical = 1 } /** @@ -140,4 +141,14 @@ export class RenderStylesheet { * The orientation of the the track names on other systems */ public otherSystemsTrackNameOrientation: TrackNameOrientation = TrackNameOrientation.Vertical; + + /** + * If multi track: Whether to render multiple subsequent empty (or rest-only) bars together as multi-bar rest. + */ + public multiTrackMultiBarRest: boolean = false; + + /** + * If single track: Whether to render multiple subsequent empty (or rest-only) bars together as multi-bar rest. + */ + public perTrackMultiBarRest: Set | null = null; } diff --git a/src/model/RepeatGroup.ts b/src/model/RepeatGroup.ts index 81fade806..0e8027a27 100644 --- a/src/model/RepeatGroup.ts +++ b/src/model/RepeatGroup.ts @@ -1,4 +1,4 @@ -import { MasterBar } from '@src/model/MasterBar'; +import type { MasterBar } from '@src/model/MasterBar'; /** * This public class can store the information about a group of measures which are repeated @@ -12,7 +12,7 @@ export class RepeatGroup { /** * the masterbars which opens the group. */ - public opening: MasterBar|null = null; + public opening: MasterBar | null = null; /** * a list of masterbars which open the group. @@ -29,9 +29,11 @@ export class RepeatGroup { public closings: MasterBar[] = []; /** - * Gets whether this repeat group is really opened as a repeat. + * Gets whether this repeat group is really opened as a repeat. */ - public get isOpened():boolean { return this.opening?.isRepeatStart === true; } + public get isOpened(): boolean { + return this.opening?.isRepeatStart === true; + } /** * true if the repeat group was closed well diff --git a/src/model/Score.ts b/src/model/Score.ts index 4edf172e1..8469329e7 100644 --- a/src/model/Score.ts +++ b/src/model/Score.ts @@ -1,8 +1,198 @@ -import { MasterBar } from '@src/model/MasterBar'; +import type { MasterBar } from '@src/model/MasterBar'; import { RenderStylesheet } from '@src/model/RenderStylesheet'; import { RepeatGroup } from '@src/model/RepeatGroup'; -import { Track } from '@src/model/Track'; -import { Settings } from '@src/Settings'; +import type { Track } from '@src/model/Track'; +import type { Settings } from '@src/Settings'; +import { ElementStyle } from '@src/model/ElementStyle'; +import { Bar } from '@src/model/Bar'; +import { Beat } from '@src/model/Beat'; +import { Voice } from '@src/model/Voice'; +import { Note } from '@src/model/Note'; +// biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 +import type { NotationSettings } from '@src/NotationSettings'; +import { TextAlign } from '@src/platform/ICanvas'; + +/** + * Lists all graphical sub elements within a {@link Score} which can be styled via {@link Score.style} + */ +export enum ScoreSubElement { + /** + * The title of the song + */ + Title = 0, + /** + * The subtitle of the song + */ + SubTitle = 1, + /** + * The artist of the song + */ + Artist = 2, + /** + * The album of the song + */ + Album = 3, + /** + * The word author of the song + */ + Words = 4, + /** + * The Music author of the song + */ + Music = 5, + /** + * The Words&Music author of the song + */ + WordsAndMusic = 6, + /** + * The transcriber of the music sheet + */ + Transcriber = 7, + + /** + * The copyright holder of the song + */ + Copyright = 8, + + /** + * The second copyright line (typically something like 'All Rights Reserved') + */ + CopyrightSecondLine = 9, + + /** + * The chord diagram list shown on top of the score. + */ + ChordDiagramList = 10 +} + +/** + * The additional style and display information for header and footer elements. + * @json + * @json_strict + */ +export class HeaderFooterStyle { + /** + * The template how the text should be formatted. Following placeholders exist and are filled from the song information: + * * `%TITLE%` + * * `%SUBTITLE%` + * * `%ARTIST%` + * * `%ALBUM%` + * * `%WORDS%` + * * `%WORDSMUSIC%` + * * `%MUSIC%` + * * `%TABBER%` + * * `%COPYRIGHT%` + */ + public template: string; + + /** + * Whether the element should be visible. Overriden by {@link NotationSettings.elements} if specified. + */ + public isVisible?: boolean; + + /** + * The alignment of the element on the page. + */ + public textAlign: TextAlign; + + public constructor( + template: string = '', + isVisible: boolean | undefined = undefined, + textAlign: TextAlign = TextAlign.Left + ) { + this.template = template; + this.isVisible = isVisible; + this.textAlign = textAlign; + } + + public buildText(score: Score) { + let anyPlaceholderFilled = false; + let anyPlaceholder = false; + const replaced = this.template.replace( + HeaderFooterStyle.PlaceholderPattern, + (_match: string, variable: string) => { + anyPlaceholder = true; + let value = ''; + switch (variable) { + case 'TITLE': + value = score.title; + break; + case 'SUBTITLE': + value = score.subTitle; + break; + case 'ARTIST': + value = score.artist; + break; + case 'ALBUM': + value = score.album; + break; + case 'WORDS': + case 'WORDSMUSIC': + value = score.words; + break; + case 'MUSIC': + value = score.music; + break; + case 'TABBER': + value = score.tab; + break; + case 'COPYRIGHT': + value = score.copyright; + break; + default: + value = ''; + break; + } + + if (value) { + anyPlaceholderFilled = true; + } + return value; + } + ); + + if (anyPlaceholder && !anyPlaceholderFilled) { + return ''; + } + return replaced; + } + + private static readonly PlaceholderPattern = /%([^%]+)%/g; +} + +/** + * Defines the custom styles for Scores. + * @json + * @json_strict + */ +export class ScoreStyle extends ElementStyle { + /** + * Changes additional style aspects fo the of the specified sub-element. + */ + public headerAndFooter: Map = new Map(); + + /** + * The default styles applied to headers and footers if not specified + */ + public static readonly defaultHeaderAndFooter: Map = new Map< + ScoreSubElement, + HeaderFooterStyle + >([ + [ScoreSubElement.Title, new HeaderFooterStyle('%TITLE%', undefined, TextAlign.Center)], + [ScoreSubElement.SubTitle, new HeaderFooterStyle('%SUBTITLE%', undefined, TextAlign.Center)], + [ScoreSubElement.Artist, new HeaderFooterStyle('%ARTIST%', undefined, TextAlign.Center)], + [ScoreSubElement.Album, new HeaderFooterStyle('%ALBUM%', undefined, TextAlign.Center)], + [ScoreSubElement.Words, new HeaderFooterStyle('Words by %WORDS%', undefined, TextAlign.Left)], + [ScoreSubElement.Music, new HeaderFooterStyle('Music by %MUSIC%', undefined, TextAlign.Right)], + [ScoreSubElement.WordsAndMusic, new HeaderFooterStyle('Words & Music by %MUSIC%', undefined, TextAlign.Right)], + [ScoreSubElement.Transcriber, new HeaderFooterStyle('Tabbed by %TABBER%', false, TextAlign.Right)], + [ScoreSubElement.Copyright, new HeaderFooterStyle('%COPYRIGHT%', undefined, TextAlign.Center)], + [ + ScoreSubElement.CopyrightSecondLine, + new HeaderFooterStyle('All Rights Reserved - International Copyright Secured', true, TextAlign.Center) + ] + ]); +} /** * The score is the root node of the complete @@ -16,6 +206,16 @@ export class Score { private _openedRepeatGroups: RepeatGroup[] = []; private _properlyOpenedRepeatGroups: number = 0; + /** + * Resets all internal ID generators. + */ + public static resetIds() { + Bar.resetIds(); + Beat.resetIds(); + Voice.resetIds(); + Note.resetIds(); + } + /** * The album of this song. */ @@ -67,7 +267,7 @@ export class Score { public tab: string = ''; /** - * Gets or sets the global tempo of the song in BPM. The tempo might change via {@link MasterBar.tempo}. + * Gets or sets the global tempo of the song in BPM. The tempo might change via {@link MasterBar.tempoAutomations}. */ public tempo: number = 120; @@ -100,12 +300,16 @@ export class Score { */ public systemsLayout: number[] = []; - /** * Gets or sets the rendering stylesheet for this song. */ public stylesheet: RenderStylesheet = new RenderStylesheet(); + /** + * The style customizations for this item. + */ + public style?: ScoreStyle; + public rebuildRepeatGroups(): void { this._currentRepeatGroup = null; this._openedRepeatGroups = []; @@ -178,7 +382,7 @@ export class Score { ? this._openedRepeatGroups[this._openedRepeatGroups.length - 1] : null; } - // else: if only one group is opened, this group stays active for + // else: if only one group is opened, this group stays active for // scenarios like open close bar close } } diff --git a/src/model/SimileMark.ts b/src/model/SimileMark.ts index f6c98896c..126ff0aaf 100644 --- a/src/model/SimileMark.ts +++ b/src/model/SimileMark.ts @@ -5,19 +5,19 @@ export enum SimileMark { /** * No simile mark is applied */ - None, + None = 0, /** * A simple simile mark. The previous bar is repeated. */ - Simple, + Simple = 1, /** * A double simile mark. This value is assigned to the first * bar of the 2 repeat bars. */ - FirstOfDouble, + FirstOfDouble = 2, /** * A double simile mark. This value is assigned to the second * bar of the 2 repeat bars. */ - SecondOfDouble + SecondOfDouble = 3 } diff --git a/src/model/SlideInType.ts b/src/model/SlideInType.ts index 479adfd46..fca006df5 100644 --- a/src/model/SlideInType.ts +++ b/src/model/SlideInType.ts @@ -5,13 +5,13 @@ export enum SlideInType { /** * No slide. */ - None, + None = 0, /** * Slide into the note from below on the same string. */ - IntoFromBelow, + IntoFromBelow = 1, /** * Slide into the note from above on the same string. */ - IntoFromAbove + IntoFromAbove = 2 } diff --git a/src/model/SlideOutType.ts b/src/model/SlideOutType.ts index c1f5b333b..c084086e8 100644 --- a/src/model/SlideOutType.ts +++ b/src/model/SlideOutType.ts @@ -5,29 +5,29 @@ export enum SlideOutType { /** * No slide. */ - None, + None = 0, /** * Shift slide to next note on same string */ - Shift, + Shift = 1, /** * Legato slide to next note on same string. */ - Legato, + Legato = 2, /** * Slide out from the note from upwards on the same string. */ - OutUp, + OutUp = 3, /** * Slide out from the note from downwards on the same string. */ - OutDown, + OutDown = 4, /** * Pickslide down on this note */ - PickSlideDown, + PickSlideDown = 5, /** * Pickslide up on this note */ - PickSlideUp + PickSlideUp = 6 } diff --git a/src/model/Staff.ts b/src/model/Staff.ts index 2964e7698..448b3731b 100644 --- a/src/model/Staff.ts +++ b/src/model/Staff.ts @@ -1,12 +1,12 @@ -import { Bar } from '@src/model/Bar'; -import { Chord } from '@src/model/Chord'; -import { Track } from '@src/model/Track'; -import { Settings } from '@src/Settings'; +import type { Bar } from '@src/model/Bar'; +import type { Chord } from '@src/model/Chord'; +import type { Track } from '@src/model/Track'; +import type { Settings } from '@src/Settings'; import { Tuning } from '@src/model/Tuning'; /** * This class describes a single staff within a track. There are instruments like pianos - * where a single track can contain multiple staffs. + * where a single track can contain multiple staves. * @json * @json_strict */ @@ -128,16 +128,15 @@ export class Staff { } public hasChord(chordId: string): boolean { - return this.chords?.has(chordId) ?? false + return this.chords?.has(chordId) ?? false; } public getChord(chordId: string): Chord | null { - return this.chords?.get(chordId) ?? null + return this.chords?.get(chordId) ?? null; } - public addBar(bar: Bar): void { - let bars: Bar[] = this.bars; + const bars: Bar[] = this.bars; bar.staff = this; bar.index = bars.length; if (bars.length > 0) { diff --git a/src/model/Track.ts b/src/model/Track.ts index a7f601c7f..5236db18e 100644 --- a/src/model/Track.ts +++ b/src/model/Track.ts @@ -1,15 +1,49 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { Color } from '@src/model/Color'; -import { Lyrics } from '@src/model/Lyrics'; +import type { Lyrics } from '@src/model/Lyrics'; import { PlaybackInformation } from '@src/model/PlaybackInformation'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Staff } from '@src/model/Staff'; -import { Settings } from '@src/Settings'; -import { InstrumentArticulation } from '@src/model/InstrumentArticulation'; +import type { Settings } from '@src/Settings'; +import type { InstrumentArticulation } from '@src/model/InstrumentArticulation'; +import { ElementStyle } from '@src/model/ElementStyle'; + +/** + * Lists all graphical sub elements within a {@link Track} which can be styled via {@link Track.style} + */ +export enum TrackSubElement { + /** + * The track names shown before the staves. + */ + TrackName = 0, + + /** + * The braces and brackets grouping the staves. + * If a bracket spans multiple tracks, the color of the first track counts. + */ + BracesAndBrackets = 1, + + /** + * The system separator. + */ + SystemSeparator = 2, + + /** + * The tuning of the strings. + */ + StringTuning = 3 +} + +/** + * Defines the custom styles for tracks. + * @json + * @json_strict + */ +export class TrackStyle extends ElementStyle {} /** * This public class describes a single track or instrument of score. - * It is bascially a list of staffs containing individual music notation kinds. + * It is primarily a list of staves containing individual music notation kinds. * @json * @json_strict */ @@ -28,7 +62,7 @@ export class Track { public score!: Score; /** - * Gets or sets the list of staffs that are defined for this track. + * Gets or sets the list of staves that are defined for this track. * @json_add addStaff */ public staves: Staff[] = []; @@ -54,7 +88,7 @@ export class Track { * In formats like Guitar Pro this flag indicates whether on the default "multi-track" layout * tracks should be visible or not. */ - public isVisibleOnMultiTrack:boolean = true; + public isVisibleOnMultiTrack: boolean = true; /** * Gets or sets the short name of this track. @@ -73,12 +107,35 @@ export class Track { */ public systemsLayout: number[] = []; + /** + * Defines on which bars specifically a line break is forced. + * @json_add addLineBreaks + */ + public lineBreaks?: Set; + + /** + * Adds a new line break. + * @param index The index of the bar before which a line break should happen. + */ + public addLineBreaks(index: number) { + if (!this.lineBreaks) { + this.lineBreaks = new Set(); + } + + this.lineBreaks!.add(index); + } + /** * Gets or sets a mapping on which staff lines particular percussion instruments * should be shown. */ public percussionArticulations: InstrumentArticulation[] = []; + /** + * The style customizations for this item. + */ + public style?: TrackStyle; + public ensureStaveCount(staveCount: number): void { while (this.staves.length < staveCount) { this.addStaff(new Staff()); @@ -104,12 +161,12 @@ export class Track { } public applyLyrics(lyrics: Lyrics[]): void { - for (let lyric of lyrics) { + for (const lyric of lyrics) { lyric.finish(); } - let staff: Staff = this.staves[0]; + const staff: Staff = this.staves[0]; for (let li: number = 0; li < lyrics.length; li++) { - let lyric: Lyrics = lyrics[li]; + const lyric: Lyrics = lyrics[li]; if (lyric.startBar >= 0 && lyric.startBar < staff.bars.length) { let beat: Beat | null = staff.bars[lyric.startBar].voices[0].beats[0]; for (let ci: number = 0; ci < lyric.chunks.length && beat; ci++) { @@ -122,7 +179,7 @@ export class Track { // initialize lyrics list for beat if required if (!beat.lyrics) { beat.lyrics = new Array(lyrics.length); - beat.lyrics.fill(""); + beat.lyrics.fill(''); } // assign chunk beat.lyrics[li] = lyric.chunks[ci]; diff --git a/src/model/TripletFeel.ts b/src/model/TripletFeel.ts index e0db927e0..87a0def64 100644 --- a/src/model/TripletFeel.ts +++ b/src/model/TripletFeel.ts @@ -5,29 +5,29 @@ export enum TripletFeel { /** * No triplet feel */ - NoTripletFeel, + NoTripletFeel = 0, /** * Triplet 16th */ - Triplet16th, + Triplet16th = 1, /** * Triplet 8th */ - Triplet8th, + Triplet8th = 2, /** * Dotted 16th */ - Dotted16th, + Dotted16th = 3, /** * Dotted 8th */ - Dotted8th, + Dotted8th = 4, /** * Scottish 16th */ - Scottish16th, + Scottish16th = 5, /** * Scottish 8th */ - Scottish8th + Scottish8th = 6 } diff --git a/src/model/Tuning.ts b/src/model/Tuning.ts index ff8e70896..e39347596 100644 --- a/src/model/Tuning.ts +++ b/src/model/Tuning.ts @@ -14,14 +14,14 @@ export class Tuning { public static readonly defaultSteps: string[] = ['C', 'C', 'D', 'D', 'E', 'F', 'F', 'G', 'G', 'A', 'A', 'B']; public static getTextForTuning(tuning: number, includeOctave: boolean): string { - let parts = Tuning.getTextPartsForTuning(tuning); + const parts = Tuning.getTextPartsForTuning(tuning); return includeOctave ? parts.join('') : parts[0]; } public static getTextPartsForTuning(tuning: number, octaveShift: number = -1): string[] { - let octave: number = (tuning / 12) | 0; - let note: number = tuning % 12; - let notes: string[] = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; + const octave: number = (tuning / 12) | 0; + const note: number = tuning % 12; + const notes: string[] = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; return [notes[note], (octave + octaveShift).toString()]; } @@ -57,55 +57,37 @@ export class Tuning { } public static initialize(): void { - Tuning._defaultTunings.set( - 7, - new Tuning('Guitar 7 strings', [64, 59, 55, 50, 45, 40, 35], true) - ); + Tuning._defaultTunings.set(7, new Tuning('Guitar 7 strings', [64, 59, 55, 50, 45, 40, 35], true)); Tuning._sevenStrings.push(Tuning._defaultTunings.get(7)!); - Tuning._defaultTunings.set( - 6, - new Tuning('Guitar Standard Tuning', [64, 59, 55, 50, 45, 40], true) - ); + Tuning._defaultTunings.set(6, new Tuning('Guitar Standard Tuning', [64, 59, 55, 50, 45, 40], true)); Tuning._sixStrings.push(Tuning._defaultTunings.get(6)!); Tuning._sixStrings.push(new Tuning('Guitar Tune down ½ step', [63, 58, 54, 49, 44, 39], false)); Tuning._sixStrings.push(new Tuning('Guitar Tune down 1 step', [62, 57, 53, 48, 43, 38], false)); Tuning._sixStrings.push(new Tuning('Guitar Tune down 2 step', [60, 55, 51, 46, 41, 36], false)); Tuning._sixStrings.push(new Tuning('Guitar Dropped D Tuning', [64, 59, 55, 50, 45, 38], false)); - Tuning._sixStrings.push( - new Tuning('Guitar Dropped D Tuning variant', [64, 57, 55, 50, 45, 38], false) - ); - Tuning._sixStrings.push( - new Tuning('Guitar Double Dropped D Tuning', [62, 59, 55, 50, 45, 38], false) - ); + Tuning._sixStrings.push(new Tuning('Guitar Dropped D Tuning variant', [64, 57, 55, 50, 45, 38], false)); + Tuning._sixStrings.push(new Tuning('Guitar Double Dropped D Tuning', [62, 59, 55, 50, 45, 38], false)); Tuning._sixStrings.push(new Tuning('Guitar Dropped E Tuning', [66, 61, 57, 52, 47, 40], false)); Tuning._sixStrings.push(new Tuning('Guitar Dropped C Tuning', [62, 57, 53, 48, 43, 36], false)); Tuning._sixStrings.push(new Tuning('Guitar Open C Tuning', [64, 60, 55, 48, 43, 36], false)); Tuning._sixStrings.push(new Tuning('Guitar Open Cm Tuning', [63, 60, 55, 48, 43, 36], false)); Tuning._sixStrings.push(new Tuning('Guitar Open C6 Tuning', [64, 57, 55, 48, 43, 36], false)); - Tuning._sixStrings.push( - new Tuning('Guitar Open Cmaj7 Tuning', [64, 59, 55, 52, 43, 36], false) - ); + Tuning._sixStrings.push(new Tuning('Guitar Open Cmaj7 Tuning', [64, 59, 55, 52, 43, 36], false)); Tuning._sixStrings.push(new Tuning('Guitar Open D Tuning', [62, 57, 54, 50, 45, 38], false)); Tuning._sixStrings.push(new Tuning('Guitar Open Dm Tuning', [62, 57, 53, 50, 45, 38], false)); Tuning._sixStrings.push(new Tuning('Guitar Open D5 Tuning', [62, 57, 50, 50, 45, 38], false)); Tuning._sixStrings.push(new Tuning('Guitar Open D6 Tuning', [62, 59, 54, 50, 45, 38], false)); - Tuning._sixStrings.push( - new Tuning('Guitar Open Dsus4 Tuning', [62, 57, 55, 50, 45, 38], false) - ); + Tuning._sixStrings.push(new Tuning('Guitar Open Dsus4 Tuning', [62, 57, 55, 50, 45, 38], false)); Tuning._sixStrings.push(new Tuning('Guitar Open E Tuning', [64, 59, 56, 52, 47, 40], false)); Tuning._sixStrings.push(new Tuning('Guitar Open Em Tuning', [64, 59, 55, 52, 47, 40], false)); - Tuning._sixStrings.push( - new Tuning('Guitar Open Esus11 Tuning', [64, 59, 55, 52, 45, 40], false) - ); + Tuning._sixStrings.push(new Tuning('Guitar Open Esus11 Tuning', [64, 59, 55, 52, 45, 40], false)); Tuning._sixStrings.push(new Tuning('Guitar Open F Tuning', [65, 60, 53, 48, 45, 41], false)); Tuning._sixStrings.push(new Tuning('Guitar Open G Tuning', [62, 59, 55, 50, 43, 38], false)); Tuning._sixStrings.push(new Tuning('Guitar Open Gm Tuning', [62, 58, 55, 50, 43, 38], false)); Tuning._sixStrings.push(new Tuning('Guitar Open G6 Tuning', [64, 59, 55, 50, 43, 38], false)); - Tuning._sixStrings.push( - new Tuning('Guitar Open Gsus4 Tuning', [62, 60, 55, 50, 43, 38], false) - ); + Tuning._sixStrings.push(new Tuning('Guitar Open Gsus4 Tuning', [62, 60, 55, 50, 43, 38], false)); Tuning._sixStrings.push(new Tuning('Guitar Open A Tuning', [64, 61, 57, 52, 45, 40], false)); Tuning._sixStrings.push(new Tuning('Guitar Open Am Tuning', [64, 60, 57, 52, 45, 40], false)); Tuning._sixStrings.push(new Tuning('Guitar Nashville Tuning', [64, 59, 67, 62, 57, 52], false)); @@ -140,9 +122,9 @@ export class Tuning { * @returns The known tuning. */ public static findTuning(strings: number[]): Tuning | null { - let tunings: Tuning[] = Tuning.getPresetsFor(strings.length); + const tunings: Tuning[] = Tuning.getPresetsFor(strings.length); for (let t: number = 0, tc: number = tunings.length; t < tc; t++) { - let tuning: Tuning = tunings[t]; + const tuning: Tuning = tunings[t]; let equals: boolean = true; for (let i: number = 0, j: number = strings.length; i < j; i++) { if (strings[i] !== tuning.tunings[i]) { @@ -186,7 +168,7 @@ export class Tuning { /** * Tries to detect the name and standard flag of the tuning from a known tuning list based - * on the string values. + * on the string values. */ public finish() { const knownTuning = Tuning.findTuning(this.tunings); diff --git a/src/model/TupletGroup.ts b/src/model/TupletGroup.ts index 40904ef7d..c4f9d7e9e 100644 --- a/src/model/TupletGroup.ts +++ b/src/model/TupletGroup.ts @@ -1,6 +1,6 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { GraceType } from '@src/model/GraceType'; -import { Voice } from '@src/model/Voice'; +import type { Voice } from '@src/model/Voice'; /** * Represents a list of beats that are grouped within the same tuplet. @@ -14,7 +14,7 @@ export class TupletGroup { private static readonly SixtyFourthTicks: number = 60; private static readonly OneHundredTwentyEighthTicks: number = 30; private static readonly TwoHundredFiftySixthTicks: number = 15; - + private static AllTicks: number[] = [ TupletGroup.HalfTicks, TupletGroup.QuarterTicks, @@ -91,8 +91,8 @@ export class TupletGroup { this.isFull = true; } } else { - let factor: number = (this.beats[0].tupletNumerator / this.beats[0].tupletDenominator) | 0; - for (let potentialMatch of TupletGroup.AllTicks) { + const factor: number = (this.beats[0].tupletNumerator / this.beats[0].tupletDenominator) | 0; + for (const potentialMatch of TupletGroup.AllTicks) { if (this.totalDuration === potentialMatch * factor) { this.isFull = true; break; diff --git a/src/model/VibratoType.ts b/src/model/VibratoType.ts index e40d882a4..e6aa2bb81 100644 --- a/src/model/VibratoType.ts +++ b/src/model/VibratoType.ts @@ -5,13 +5,13 @@ export enum VibratoType { /** * No vibrato. */ - None, + None = 0, /** * A slight vibrato. */ - Slight, + Slight = 1, /** * A wide vibrato. */ - Wide + Wide = 2 } diff --git a/src/model/Voice.ts b/src/model/Voice.ts index 5530a88e6..5fca05490 100644 --- a/src/model/Voice.ts +++ b/src/model/Voice.ts @@ -1,8 +1,26 @@ -import { Bar } from '@src/model/Bar'; -import { Beat } from '@src/model/Beat'; +import type { Bar } from '@src/model/Bar'; +import type { Beat } from '@src/model/Beat'; import { GraceType } from '@src/model/GraceType'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { GraceGroup } from '@src/model/GraceGroup'; +import { ElementStyle } from '@src/model/ElementStyle'; + +/** + * Lists all graphical sub elements within a {@link Voice} which can be styled via {@link Voice.style} + */ +export enum VoiceSubElement { + /** + * All general glyphs (like notes heads and rests). + */ + Glyphs = 0 +} + +/** + * Defines the custom styles for voices. + * @json + * @json_strict + */ +export class VoiceStyle extends ElementStyle {} /** * A voice represents a group of beats @@ -12,13 +30,22 @@ import { GraceGroup } from '@src/model/GraceGroup'; */ export class Voice { private _beatLookup!: Map; + private _isEmpty: boolean = true; + private _isRestOnly: boolean = true; + + private static _globalVoiceId: number = 0; - private static _globalBarId: number = 0; + /** + * @internal + */ + public static resetIds() { + Voice._globalVoiceId = 0; + } /** * Gets or sets the unique id of this bar. */ - public id: number = Voice._globalBarId++; + public id: number = Voice._globalVoiceId++; /** * Gets or sets the zero-based index of this voice within the bar. @@ -41,7 +68,28 @@ export class Voice { /** * Gets or sets a value indicating whether this voice is empty. */ - public isEmpty: boolean = true; + public get isEmpty(): boolean { + return this._isEmpty; + } + + /** + * The style customizations for this item. + */ + public style?: VoiceStyle; + + /** + * @internal + */ + public forceNonEmpty() { + this._isEmpty = false; + } + + /** + * Gets or sets a value indicating whether this voice is empty. + */ + public get isRestOnly() { + return this._isRestOnly; + } public insertBeat(after: Beat, newBeat: Beat): void { newBeat.nextBeat = after.nextBeat; @@ -59,7 +107,10 @@ export class Voice { beat.index = this.beats.length; this.beats.push(beat); if (!beat.isEmpty) { - this.isEmpty = false; + this._isEmpty = false; + } + if (!beat.isRest) { + this._isRestOnly = false; } } @@ -71,7 +122,7 @@ export class Voice { beat.nextBeat = this.beats[beat.index + 1]; beat.nextBeat.previousBeat = beat; } else if (beat.isLastOfVoice && beat.voice.bar.nextBar) { - let nextVoice: Voice = this.bar.nextBar!.voices[this.index]; + const nextVoice: Voice = this.bar.nextBar!.voices[this.index]; if (nextVoice.beats.length > 0) { beat.nextBeat = nextVoice.beats[0]; beat.nextBeat.previousBeat = beat; @@ -89,13 +140,14 @@ export class Voice { return; } // remove last beat - let lastBeat: Beat = this.beats[this.beats.length - 1]; + const lastBeat: Beat = this.beats[this.beats.length - 1]; this.beats.splice(this.beats.length - 1, 1); // insert grace beat this.addBeat(beat); // reinsert last beat this.addBeat(lastBeat); - this.isEmpty = false; + this._isEmpty = false; + this._isRestOnly = false; } public getBeatAtPlaybackStart(playbackStart: number): Beat | null { @@ -106,10 +158,12 @@ export class Voice { } public finish(settings: Settings, sharedDataBag: Map | null = null): void { + this._isEmpty = true; + this._isRestOnly = true; this._beatLookup = new Map(); let currentGraceGroup: GraceGroup | null = null; for (let index: number = 0; index < this.beats.length; index++) { - let beat: Beat = this.beats[index]; + const beat: Beat = this.beats[index]; beat.index = index; this.chain(beat, sharedDataBag); if (beat.graceType === GraceType.None) { @@ -124,12 +178,18 @@ export class Voice { } currentGraceGroup.addBeat(beat); } + if (!beat.isEmpty) { + this._isEmpty = false; + } + if (!beat.isRest) { + this._isRestOnly = false; + } } let currentDisplayTick: number = 0; let currentPlaybackTick: number = 0; for (let i: number = 0; i < this.beats.length; i++) { - let beat: Beat = this.beats[i]; + const beat: Beat = this.beats[i]; beat.index = i; beat.finish(settings, sharedDataBag); @@ -142,7 +202,7 @@ export class Voice { const lastGraceBeat = beat.graceGroup!.beats[beat.graceGroup!.beats.length - 1]; if (firstGraceBeat.graceType !== GraceType.BendGrace) { // find out the stolen duration first - let stolenDuration: number = + const stolenDuration: number = lastGraceBeat.playbackStart + lastGraceBeat.playbackDuration - firstGraceBeat.playbackStart; switch (firstGraceBeat.graceType) { @@ -151,7 +211,7 @@ export class Voice { if (firstGraceBeat.previousBeat) { firstGraceBeat.previousBeat.playbackDuration -= stolenDuration; // place beats starting after new beat end - if (firstGraceBeat.previousBeat.voice == this) { + if (firstGraceBeat.previousBeat.voice === this) { currentPlaybackTick = firstGraceBeat.previousBeat.playbackStart + firstGraceBeat.previousBeat.playbackDuration; @@ -215,8 +275,8 @@ export class Voice { if (this.isEmpty || this.beats.length === 0) { return 0; } - let lastBeat: Beat = this.beats[this.beats.length - 1]; - let firstBeat: Beat = this.beats[0]; + const lastBeat: Beat = this.beats[this.beats.length - 1]; + const firstBeat: Beat = this.beats[0]; return lastBeat.playbackStart + lastBeat.playbackDuration - firstBeat.playbackStart; } } diff --git a/src/model/WahPedal.ts b/src/model/WahPedal.ts index c3dbd3a3f..d7a751f88 100644 --- a/src/model/WahPedal.ts +++ b/src/model/WahPedal.ts @@ -2,7 +2,7 @@ * Lists all wah pedal modes. */ export enum WahPedal { - None, - Open, - Closed + None = 0, + Open = 1, + Closed = 2 } diff --git a/src/model/WhammyType.ts b/src/model/WhammyType.ts index 0ae38af9e..8f722cda6 100644 --- a/src/model/WhammyType.ts +++ b/src/model/WhammyType.ts @@ -5,31 +5,31 @@ export enum WhammyType { /** * No whammy at all */ - None, + None = 0, /** * Individual points define the whammy in a flexible manner. * This system was mainly used in Guitar Pro 3-5 */ - Custom, + Custom = 1, /** * Simple dive to a lower or higher note. */ - Dive, + Dive = 2, /** * A dive to a lower or higher note and releasing it back to normal. */ - Dip, + Dip = 3, /** * Continue to hold the whammy at the position from a previous whammy. */ - Hold, + Hold = 4, /** * Dive to a lower or higher note before playing it. */ - Predive, + Predive = 5, /** * Dive to a lower or higher note before playing it, then change to another * note. */ - PrediveDive + PrediveDive = 6 } diff --git a/src/model/index.ts b/src/model/_barrel.ts similarity index 76% rename from src/model/index.ts rename to src/model/_barrel.ts index 68ddefba6..ad1daddb6 100644 --- a/src/model/index.ts +++ b/src/model/_barrel.ts @@ -1,9 +1,9 @@ export { AccentuationType } from '@src/model/AccentuationType'; export { AccidentalType } from '@src/model/AccidentalType'; export { AutomationType, Automation } from '@src/model/Automation'; -export { Bar, SustainPedalMarkerType, SustainPedalMarker } from '@src/model/Bar'; +export { Bar, SustainPedalMarkerType, SustainPedalMarker, BarSubElement, BarStyle, BarLineStyle } from '@src/model/Bar'; export { BarreShape } from '@src/model/BarreShape'; -export { Beat, BeatBeamingMode } from '@src/model/Beat'; +export { Beat, BeatBeamingMode, BeatSubElement, BeatStyle } from '@src/model/Beat'; export { BendPoint } from '@src/model/BendPoint'; export { BendStyle } from '@src/model/BendStyle'; export { BendType } from '@src/model/BendType'; @@ -12,6 +12,7 @@ export { Chord } from '@src/model/Chord'; export { Clef } from '@src/model/Clef'; export { Color } from '@src/model/Color'; export { CrescendoType } from '@src/model/CrescendoType'; +export { Direction } from '@src/model/Direction'; export { Duration } from '@src/model/Duration'; export { DynamicValue } from '@src/model/DynamicValue'; export { FadeType } from '@src/model/FadeType'; @@ -19,6 +20,8 @@ export { FermataType, Fermata } from '@src/model/Fermata'; export { Fingers } from '@src/model/Fingers'; export { FontStyle, FontWeight, Font } from '@src/model/Font'; export { GraceType } from '@src/model/GraceType'; +export { GraceGroup } from '@src/model/GraceGroup'; +export { GolpeType } from '@src/model/GolpeType'; export { HarmonicType } from '@src/model/HarmonicType'; export { InstrumentArticulation } from '@src/model/InstrumentArticulation'; export { JsonConverter } from '@src/model/JsonConverter'; @@ -27,7 +30,7 @@ export { KeySignatureType } from '@src/model/KeySignatureType'; export { Lyrics } from '@src/model/Lyrics'; export { MasterBar } from '@src/model/MasterBar'; export { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -export { Note } from '@src/model/Note'; +export { Note, NoteSubElement, NoteStyle } from '@src/model/Note'; export { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; export { NoteOrnament } from '@src/model/NoteOrnament'; export { Ottavia } from '@src/model/Ottavia'; @@ -42,17 +45,18 @@ export { TrackNameOrientation } from '@src/model/RenderStylesheet'; export { RepeatGroup } from '@src/model/RepeatGroup'; -export { Score } from '@src/model/Score'; +export { Score, ScoreSubElement, ScoreStyle, HeaderFooterStyle } from '@src/model/Score'; export { Section } from '@src/model/Section'; export { SimileMark } from '@src/model/SimileMark'; export { SlideInType } from '@src/model/SlideInType'; export { SlideOutType } from '@src/model/SlideOutType'; export { Staff } from '@src/model/Staff'; -export { Track } from '@src/model/Track'; +export { Track, TrackSubElement, TrackStyle } from '@src/model/Track'; export { TripletFeel } from '@src/model/TripletFeel'; export { Tuning } from '@src/model/Tuning'; export { TupletGroup } from '@src/model/TupletGroup'; export { VibratoType } from '@src/model/VibratoType'; -export { Voice } from '@src/model/Voice'; -export { WahPedal } from '@src/model/WahPedal' +export { Voice, VoiceSubElement, VoiceStyle } from '@src/model/Voice'; +export { WahPedal } from '@src/model/WahPedal'; export { WhammyType } from '@src/model/WhammyType'; +export { ElementStyle } from '@src/model/ElementStyle'; diff --git a/src/platform/Cursors.ts b/src/platform/Cursors.ts index fa2bd4e5c..38ce9f190 100644 --- a/src/platform/Cursors.ts +++ b/src/platform/Cursors.ts @@ -1,4 +1,4 @@ -import { IContainer } from '@src/platform/IContainer'; +import type { IContainer } from '@src/platform/IContainer'; /** * This wrapper holds all cursor related elements. diff --git a/src/platform/ICanvas.ts b/src/platform/ICanvas.ts index c95b79621..5eb14201b 100644 --- a/src/platform/ICanvas.ts +++ b/src/platform/ICanvas.ts @@ -1,7 +1,7 @@ -import { Color } from '@src/model/Color'; -import { Font } from '@src/model/Font'; -import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { Settings } from '@src/Settings'; +import type { Color } from '@src/model/Color'; +import type { Font } from '@src/model/Font'; +import type { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import type { Settings } from '@src/Settings'; /** * This public enum lists all different text alignments @@ -10,13 +10,13 @@ export enum TextAlign { /** * Text is left aligned. */ - Left, + Left = 0, /** * Text is centered. - */ Center, + */ Center = 1, /** * Text is right aligned. - */ Right + */ Right = 2 } /** @@ -26,21 +26,21 @@ export enum TextBaseline { /** * Text is aligned on top. */ - Top, + Top = 0, /** * Text is aligned middle */ - Middle, + Middle = 1, /** * Text is aligend on the bottom. */ - Bottom + Bottom = 2 } /** - * The TextMetrics class represents the dimensions of a piece of text in the canvas; + * The MeasuredText class represents the dimensions of a piece of text in the canvas; */ -export class TextMetrics { +export class MeasuredText { /** * Returns the width of a segment of inline text in CSS pixels. */ @@ -86,9 +86,15 @@ export interface ICanvas { fillText(text: string, x: number, y: number): void; - measureText(text: string): TextMetrics; + measureText(text: string): MeasuredText; - fillMusicFontSymbol(x: number, y: number, relativeScale: number, symbol: MusicFontSymbol, centerAtPosition?: boolean): void; + fillMusicFontSymbol( + x: number, + y: number, + relativeScale: number, + symbol: MusicFontSymbol, + centerAtPosition?: boolean + ): void; fillMusicFontSymbols( x: number, diff --git a/src/platform/IContainer.ts b/src/platform/IContainer.ts index a1fd3262d..e43971b30 100644 --- a/src/platform/IContainer.ts +++ b/src/platform/IContainer.ts @@ -1,5 +1,5 @@ -import { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; -import { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; +import type { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; +import type { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; /** * This interface represents a container control in the UI layer. diff --git a/src/platform/IMouseEventArgs.ts b/src/platform/IMouseEventArgs.ts index 964f41588..6fba8a636 100644 --- a/src/platform/IMouseEventArgs.ts +++ b/src/platform/IMouseEventArgs.ts @@ -1,4 +1,4 @@ -import { IContainer } from '@src/platform/IContainer'; +import type { IContainer } from '@src/platform/IContainer'; /** * This interface represents the information about a mouse event that occured on the UI. diff --git a/src/platform/IUiFacade.ts b/src/platform/IUiFacade.ts index 323e8faeb..300fcf76c 100644 --- a/src/platform/IUiFacade.ts +++ b/src/platform/IUiFacade.ts @@ -1,13 +1,13 @@ -import { AlphaTabApiBase } from '@src/AlphaTabApiBase'; -import { IAlphaSynth } from '@src/synth/IAlphaSynth'; -import { IEventEmitter } from '@src/EventEmitter'; -import { Score } from '@src/model/Score'; -import { IContainer } from '@src/platform/IContainer'; -import { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; -import { Cursors } from '@src/platform/Cursors'; -import { IScoreRenderer } from '@src/rendering/IScoreRenderer'; -import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; -import { Bounds } from '@src/rendering/utils/Bounds'; +import type { AlphaTabApiBase } from '@src/AlphaTabApiBase'; +import type { IAlphaSynth } from '@src/synth/IAlphaSynth'; +import type { IEventEmitter } from '@src/EventEmitter'; +import type { Score } from '@src/model/Score'; +import type { IContainer } from '@src/platform/IContainer'; +import type { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; +import type { Cursors } from '@src/platform/Cursors'; +import type { IScoreRenderer } from '@src/rendering/IScoreRenderer'; +import type { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; +import type { Bounds } from '@src/rendering/utils/Bounds'; /** * This interface represents the UI abstraction between alphaTab and the corresponding UI framework being used. @@ -69,7 +69,7 @@ export interface IUiFacade { /** * Tells the UI layer to append the given render results to the UI. At this point - * the partial result is not actually rendered yet, only the layouting process + * the partial result is not actually rendered yet, only the layouting process * completed. * @param renderResults The rendered partial that should be added to the UI. null indicates the rendering finished. */ diff --git a/src/platform/_barrel.ts b/src/platform/_barrel.ts new file mode 100644 index 000000000..9c34b5e27 --- /dev/null +++ b/src/platform/_barrel.ts @@ -0,0 +1,8 @@ +export { Cursors } from '@src/platform/Cursors'; +export { type ICanvas, TextAlign, TextBaseline, MeasuredText } from '@src/platform/ICanvas'; +export type { IContainer } from '@src/platform/IContainer'; +export type { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; +export type { IUiFacade } from '@src/platform/IUiFacade'; +export { CssFontSvgCanvas } from '@src/platform/svg/CssFontSvgCanvas'; +export { FontSizes, FontSizeDefinition } from '@src/platform/svg/FontSizes'; +export { SvgCanvas } from '@src/platform/svg/SvgCanvas'; diff --git a/src/platform/index.ts b/src/platform/index.ts deleted file mode 100644 index 77c305295..000000000 --- a/src/platform/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { Cursors } from '@src/platform/Cursors'; -export { type ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; -export { type IContainer } from '@src/platform/IContainer'; -export { type IMouseEventArgs } from '@src/platform/IMouseEventArgs'; -export { type IUiFacade } from '@src/platform/IUiFacade'; -export { CssFontSvgCanvas } from '@src/platform/svg/CssFontSvgCanvas'; -export { FontSizes } from '@src/platform/svg/FontSizes'; -export { SvgCanvas } from '@src/platform/svg/SvgCanvas'; diff --git a/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts b/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts index 1e5151caa..5a167a2c7 100644 --- a/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts +++ b/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts @@ -4,7 +4,7 @@ import { Logger } from '@src/Logger'; import { AlphaSynthWorkerSynthOutput } from '@src/platform/javascript/AlphaSynthWorkerSynthOutput'; import { AlphaSynthWebAudioOutputBase } from '@src/platform/javascript/AlphaSynthWebAudioOutputBase'; import { SynthConstants } from '@src/synth/SynthConstants'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; /** * @target web @@ -17,9 +17,9 @@ interface AudioWorkletProcessor { /** * @target web */ -declare var AudioWorkletProcessor: { +declare let AudioWorkletProcessor: { prototype: AudioWorkletProcessor; - new(options?: AudioWorkletNodeOptions): AudioWorkletProcessor; + new (options?: AudioWorkletNodeOptions): AudioWorkletProcessor; }; // Bug 646: Safari 14.1 is buggy regarding audio worklets @@ -28,11 +28,11 @@ declare var AudioWorkletProcessor: { /** * @target web */ -declare var registerProcessor: any; +declare let registerProcessor: any; /** * @target web */ -declare var sampleRate: number; +declare let sampleRate: number; /** * This class implements a HTML5 Web Audio API based audio output device @@ -64,8 +64,8 @@ export class AlphaSynthWebWorklet { this._bufferCount = Math.floor( (options.processorOptions.bufferTimeInMilliseconds * sampleRate) / - 1000 / - AlphaSynthWebWorkletProcessor.BufferSize + 1000 / + AlphaSynthWebWorkletProcessor.BufferSize ); this._circularBuffer = new CircularSampleBuffer( AlphaSynthWebWorkletProcessor.BufferSize * this._bufferCount @@ -75,8 +75,8 @@ export class AlphaSynthWebWorklet { } private handleMessage(e: MessageEvent) { - let data: any = e.data; - let cmd: any = data.cmd; + const data: any = e.data; + const cmd: any = data.cmd; switch (cmd) { case AlphaSynthWorkerSynthOutput.CmdOutputAddSamples: const f: Float32Array = data.samples; @@ -101,14 +101,14 @@ export class AlphaSynthWebWorklet { return false; } - let left: Float32Array = outputs[0][0]; - let right: Float32Array = outputs[0][1]; + const left: Float32Array = outputs[0][0]; + const right: Float32Array = outputs[0][1]; if (!left || !right) { return true; } - let samples: number = left.length + right.length; + const samples: number = left.length + right.length; let buffer = this._outputBuffer; if (buffer.length !== samples) { buffer = new Float32Array(samples); @@ -126,13 +126,13 @@ export class AlphaSynthWebWorklet { right[i] = buffer[s++]; } - if(samplesFromBuffer < left.length) { - for(let i = samplesFromBuffer; i < left.length; i++) { + if (samplesFromBuffer < left.length) { + for (let i = samplesFromBuffer; i < left.length; i++) { left[i] = 0; right[i] = 0; } } - + this.port.postMessage({ cmd: AlphaSynthWorkerSynthOutput.CmdOutputSamplesPlayed, samples: samplesFromBuffer / SynthConstants.AudioChannels @@ -146,11 +146,11 @@ export class AlphaSynthWebWorklet { // if we fall under the half of buffers // we request one half const halfBufferCount = (this._bufferCount / 2) | 0; - let halfSamples: number = halfBufferCount * AlphaSynthWebWorkletProcessor.BufferSize; + const halfSamples: number = halfBufferCount * AlphaSynthWebWorkletProcessor.BufferSize; // Issue #631: it can happen that requestBuffers is called multiple times // before we already get samples via addSamples, therefore we need to // remember how many buffers have been requested, and consider them as available. - let bufferedSamples = + const bufferedSamples = this._circularBuffer.count + this._requestedBufferCount * AlphaSynthWebWorkletProcessor.BufferSize; if (bufferedSamples < halfSamples) { @@ -190,7 +190,7 @@ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase { public override play(): void { super.play(); - let ctx = this._context!; + const ctx = this._context!; // create a script processor node which will replace the silence with the generated audio Environment.createAudioWorklet(ctx, this._settings).then( () => { @@ -213,8 +213,8 @@ export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase { } private handleMessage(e: MessageEvent) { - let data: any = e.data; - let cmd: any = data.cmd; + const data: any = e.data; + const cmd: any = data.cmd; switch (cmd) { case AlphaSynthWorkerSynthOutput.CmdOutputSamplesPlayed: this.onSamplesPlayed(data.samples); diff --git a/src/platform/javascript/AlphaSynthScriptProcessorOutput.ts b/src/platform/javascript/AlphaSynthScriptProcessorOutput.ts index 09395c723..59f984481 100644 --- a/src/platform/javascript/AlphaSynthScriptProcessorOutput.ts +++ b/src/platform/javascript/AlphaSynthScriptProcessorOutput.ts @@ -26,7 +26,7 @@ export class AlphaSynthScriptProcessorOutput extends AlphaSynthWebAudioOutputBas public override play(): void { super.play(); - let ctx = this._context!; + const ctx = this._context!; // create a script processor node which will replace the silence with the generated audio this._audioNode = ctx.createScriptProcessor(4096, 0, 2); this._audioNode.onaudioprocess = this.generateSound.bind(this); @@ -61,11 +61,11 @@ export class AlphaSynthScriptProcessorOutput extends AlphaSynthWebAudioOutputBas // if we fall under the half of buffers // we request one half const halfBufferCount = (this._bufferCount / 2) | 0; - let halfSamples: number = halfBufferCount * AlphaSynthWebAudioOutputBase.BufferSize; + const halfSamples: number = halfBufferCount * AlphaSynthWebAudioOutputBase.BufferSize; // Issue #631: it can happen that requestBuffers is called multiple times // before we already get samples via addSamples, therefore we need to // remember how many buffers have been requested, and consider them as available. - let bufferedSamples = + const bufferedSamples = this._circularBuffer.count + this._requestedBufferCount * AlphaSynthWebAudioOutputBase.BufferSize; if (bufferedSamples < halfSamples) { for (let i: number = 0; i < halfBufferCount; i++) { @@ -77,9 +77,9 @@ export class AlphaSynthScriptProcessorOutput extends AlphaSynthWebAudioOutputBas private _outputBuffer: Float32Array = new Float32Array(0); private generateSound(e: AudioProcessingEvent): void { - let left: Float32Array = e.outputBuffer.getChannelData(0); - let right: Float32Array = e.outputBuffer.getChannelData(1); - let samples: number = left.length + right.length; + const left: Float32Array = e.outputBuffer.getChannelData(0); + const right: Float32Array = e.outputBuffer.getChannelData(1); + const samples: number = left.length + right.length; let buffer = this._outputBuffer; if (buffer.length !== samples) { buffer = new Float32Array(samples); @@ -96,8 +96,8 @@ export class AlphaSynthScriptProcessorOutput extends AlphaSynthWebAudioOutputBas left[i] = buffer[s++]; right[i] = buffer[s++]; } - if(samplesFromBuffer < left.length) { - for(let i = samplesFromBuffer; i < left.length; i++) { + if (samplesFromBuffer < left.length) { + for (let i = samplesFromBuffer; i < left.length; i++) { left[i] = 0; right[i] = 0; } diff --git a/src/platform/javascript/AlphaSynthWebAudioOutputBase.ts b/src/platform/javascript/AlphaSynthWebAudioOutputBase.ts index 3c1e3b1fd..e8ebb578f 100644 --- a/src/platform/javascript/AlphaSynthWebAudioOutputBase.ts +++ b/src/platform/javascript/AlphaSynthWebAudioOutputBase.ts @@ -1,10 +1,28 @@ import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; import { Environment } from '@src/Environment'; -import { EventEmitter, EventEmitterOfT, IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; +import { EventEmitter, EventEmitterOfT, type IEventEmitter, type IEventEmitterOfT } from '@src/EventEmitter'; import { Logger } from '@src/Logger'; -import { ISynthOutput } from '@src/synth/ISynthOutput'; +import type { ISynthOutput, ISynthOutputDevice } from '@src/synth/ISynthOutput'; -declare var webkitAudioContext: any; +declare const webkitAudioContext: any; + +/** + * @target web + */ +export class AlphaSynthWebAudioSynthOutputDevice implements ISynthOutputDevice { + public device: MediaDeviceInfo; + + public constructor(device: MediaDeviceInfo) { + this.device = device; + } + public get deviceId(): string { + return this.device.deviceId; + } + public get label(): string { + return this.device.label; + } + public isDefault: boolean = false; +} /** * @target web @@ -51,11 +69,11 @@ export abstract class AlphaSynthWebAudioOutputBase implements ISynthOutput { } private patchIosSampleRate(): void { - let ua: string = navigator.userAgent; + const ua: string = navigator.userAgent; if (ua.indexOf('iPhone') !== -1 || ua.indexOf('iPad') !== -1) { - let context: AudioContext = this.createAudioContext(); - let buffer: AudioBuffer = context.createBuffer(1, 1, AlphaSynthWebAudioOutputBase.PreferredSampleRate); - let dummy: AudioBufferSourceNode = context.createBufferSource(); + const context: AudioContext = this.createAudioContext(); + const buffer: AudioBuffer = context.createBuffer(1, 1, AlphaSynthWebAudioOutputBase.PreferredSampleRate); + const dummy: AudioBufferSourceNode = context.createBufferSource(); dummy.buffer = buffer; dummy.connect(context.destination); dummy.start(0); @@ -68,7 +86,8 @@ export abstract class AlphaSynthWebAudioOutputBase implements ISynthOutput { private createAudioContext(): AudioContext { if ('AudioContext' in Environment.globalThis) { return new AudioContext(); - } else if ('webkitAudioContext' in Environment.globalThis) { + } + if ('webkitAudioContext' in Environment.globalThis) { return new webkitAudioContext(); } throw new AlphaTabError(AlphaTabErrorType.General, 'AudioContext not found'); @@ -77,7 +96,7 @@ export abstract class AlphaSynthWebAudioOutputBase implements ISynthOutput { public open(bufferTimeInMilliseconds: number): void { this.patchIosSampleRate(); this._context = this.createAudioContext(); - let ctx: any = this._context; + const ctx: any = this._context; if (ctx.state === 'suspended') { this.registerResumeHandler(); } @@ -102,7 +121,7 @@ export abstract class AlphaSynthWebAudioOutputBase implements ISynthOutput { } public play(): void { - let ctx = this._context!; + const ctx = this._context!; this.activate(); // create an empty buffer source (silence) this._buffer = ctx.createBuffer(2, AlphaSynthWebAudioOutputBase.BufferSize, ctx.sampleRate); @@ -144,4 +163,118 @@ export abstract class AlphaSynthWebAudioOutputBase implements ISynthOutput { protected onReady() { (this.ready as EventEmitter).trigger(); } + + private async checkSinkIdSupport() { + // https://caniuse.com/mdn-api_audiocontext_sinkid + const context = this._context ?? this.createAudioContext(); + if (!('setSinkId' in context)) { + Logger.warning('WebAudio', 'Browser does not support changing the output device'); + return false; + } + return true; + } + + private _knownDevices: ISynthOutputDevice[] = []; + + public async enumerateOutputDevices(): Promise { + try { + if (!(await this.checkSinkIdSupport())) { + return []; + } + + // Request permissions + try { + await navigator.mediaDevices.getUserMedia({ audio: true }); + } catch (e) { + // sometimes we get an error but can still enumerate, e.g. if microphone access is denied, + // we can still load the output devices in some cases. + Logger.warning('WebAudio', 'Output device permission rejected', e); + } + + // load devices + const devices = await navigator.mediaDevices.enumerateDevices(); + + // default device candidates + let defaultDeviceGroupId = ''; + let defaultDeviceId = ''; + + const realDevices = new Map(); + for (const device of devices) { + if (device.kind === 'audiooutput') { + realDevices.set(device.groupId, new AlphaSynthWebAudioSynthOutputDevice(device)); + + // chromium has the default device as deviceID: 'default' + // the standard defines empty-string as default + if (device.deviceId === 'default' || device.deviceId === '') { + defaultDeviceGroupId = device.groupId; + defaultDeviceId = device.deviceId; + } + } + } + + const final = Array.from(realDevices.values()); + + // flag default device + let defaultDevice = final.find(d => d.deviceId === defaultDeviceId); + if (!defaultDevice) { + defaultDevice = final.find(d => d.device.groupId === defaultDeviceGroupId); + } + if (!defaultDevice && final.length > 0) { + defaultDevice = final[0]; + } + + if (defaultDevice) { + defaultDevice.isDefault = true; + } + + this._knownDevices = final; + + return final; + } catch (e) { + Logger.error('WebAudio', 'Failed to enumerate output devices', e); + return []; + } + } + + public async setOutputDevice(device: ISynthOutputDevice | null): Promise { + if (!(await this.checkSinkIdSupport())) { + return; + } + + // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId + if (!device) { + await (this._context as any).setSinkId(''); + } else { + await (this._context as any).setSinkId(device.deviceId); + } + } + + public async getOutputDevice(): Promise { + if (!(await this.checkSinkIdSupport())) { + return null; + } + + // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/sinkId + const sinkId = (this._context as any).sinkId; + + if (typeof sinkId !== 'string' || sinkId === '' || sinkId === 'default') { + return null; + } + + // fast path -> cached devices list + let device = this._knownDevices.find(d => d.deviceId === sinkId); + if (device) { + return device; + } + + // slow path -> enumerate devices + const allDevices = await this.enumerateOutputDevices(); + device = allDevices.find(d => d.deviceId === sinkId); + if (device) { + return device; + } + + Logger.warning('WebAudio', 'Could not find output device in device list', sinkId, allDevices); + return null; + } } diff --git a/src/platform/javascript/AlphaSynthWebWorker.ts b/src/platform/javascript/AlphaSynthWebWorker.ts index 7a6cb2ab2..091a96826 100644 --- a/src/platform/javascript/AlphaSynthWebWorker.ts +++ b/src/platform/javascript/AlphaSynthWebWorker.ts @@ -1,13 +1,13 @@ import { AlphaSynth } from '@src/synth/AlphaSynth'; -import { PlayerStateChangedEventArgs } from '@src/synth/PlayerStateChangedEventArgs'; -import { PositionChangedEventArgs } from '@src/synth/PositionChangedEventArgs'; +import type { PlayerStateChangedEventArgs } from '@src/synth/PlayerStateChangedEventArgs'; +import type { PositionChangedEventArgs } from '@src/synth/PositionChangedEventArgs'; import { JsonConverter } from '@src/model/JsonConverter'; import { AlphaSynthWorkerSynthOutput } from '@src/platform/javascript/AlphaSynthWorkerSynthOutput'; -import { IWorkerScope } from '@src/platform/javascript/IWorkerScope'; +import type { IWorkerScope } from '@src/platform/javascript/IWorkerScope'; import { Logger } from '@src/Logger'; import { Environment } from '@src/Environment'; -import { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; -import { PlaybackRangeChangedEventArgs } from '@src/synth/PlaybackRangeChangedEventArgs'; +import type { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; +import type { PlaybackRangeChangedEventArgs } from '@src/synth/PlaybackRangeChangedEventArgs'; /** * This class implements a HTML5 WebWorker based version of alphaSynth @@ -40,23 +40,26 @@ export class AlphaSynthWebWorker { } public static init(): void { - let main: IWorkerScope = Environment.globalThis as IWorkerScope; + const main: IWorkerScope = Environment.globalThis as IWorkerScope; main.addEventListener('message', e => { - let data: any = e.data; - let cmd: string = data.cmd; + const data: any = e.data; + const cmd: string = data.cmd; switch (cmd) { case 'alphaSynth.initialize': AlphaSynthWorkerSynthOutput.preferredSampleRate = data.sampleRate; Logger.logLevel = data.logLevel; - Environment.globalThis.alphaSynthWebWorker = new AlphaSynthWebWorker(main, data.bufferTimeInMilliseconds); + Environment.globalThis.alphaSynthWebWorker = new AlphaSynthWebWorker( + main, + data.bufferTimeInMilliseconds + ); break; } }); } public handleMessage(e: MessageEvent): void { - let data: any = e.data; - let cmd: string = data.cmd; + const data: any = e.data; + const cmd: string = data.cmd; switch (cmd) { case 'alphaSynth.setLogLevel': Logger.logLevel = data.value; @@ -178,7 +181,7 @@ export class AlphaSynthWebWorker { } private serializeException(e: any): unknown { - let error: any = JSON.parse(JSON.stringify(e)); + const error: any = JSON.parse(JSON.stringify(e)); if (e.message) { error.message = e.message; } diff --git a/src/platform/javascript/AlphaSynthWebWorkerApi.ts b/src/platform/javascript/AlphaSynthWebWorkerApi.ts index 4b91946f9..203e8f86e 100644 --- a/src/platform/javascript/AlphaSynthWebWorkerApi.ts +++ b/src/platform/javascript/AlphaSynthWebWorkerApi.ts @@ -1,22 +1,22 @@ -import { MidiFile } from '@src/midi/MidiFile'; -import { IAlphaSynth } from '@src/synth/IAlphaSynth'; -import { ISynthOutput } from '@src/synth/ISynthOutput'; -import { PlaybackRange } from '@src/synth/PlaybackRange'; +import type { MidiFile } from '@src/midi/MidiFile'; +import type { IAlphaSynth } from '@src/synth/IAlphaSynth'; +import type { ISynthOutput } from '@src/synth/ISynthOutput'; +import type { PlaybackRange } from '@src/synth/PlaybackRange'; import { PlayerState } from '@src/synth/PlayerState'; import { PlayerStateChangedEventArgs } from '@src/synth/PlayerStateChangedEventArgs'; import { PositionChangedEventArgs } from '@src/synth/PositionChangedEventArgs'; -import { EventEmitter, IEventEmitter, IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; +import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; import { JsonConverter } from '@src/model/JsonConverter'; import { Logger } from '@src/Logger'; -import { LogLevel } from '@src/LogLevel'; +import type { LogLevel } from '@src/LogLevel'; import { SynthConstants } from '@src/synth/SynthConstants'; import { ProgressEventArgs } from '@src/ProgressEventArgs'; import { FileLoadError } from '@src/FileLoadError'; import { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; -import { MidiEventType } from '@src/midi/MidiEvent'; +import type { MidiEventType } from '@src/midi/MidiEvent'; import { Environment } from '@src/Environment'; import { PlaybackRangeChangedEventArgs } from '@src/synth/PlaybackRangeChangedEventArgs'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { ModelUtils } from '@src/model/ModelUtils'; /** @@ -40,6 +40,10 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { private _playbackRange: PlaybackRange | null = null; private _midiEventsPlayedFilter: MidiEventType[] = []; + public get output(): ISynthOutput { + return this._output; + } + public get isReady(): boolean { return this._workerIsReady && this._outputIsReady; } @@ -189,10 +193,7 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { }); } - public constructor( - player: ISynthOutput, - settings: Settings - ) { + public constructor(player: ISynthOutput, settings: Settings) { this._workerIsReadyForPlayback = false; this._workerIsReady = false; this._outputIsReady = false; @@ -212,7 +213,7 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { try { this._synth = Environment.createWebWorker(settings); } catch (e) { - Logger.error('AlphaSynth', 'Failed to create WebWorker: ' + e); + Logger.error('AlphaSynth', `Failed to create WebWorker: ${e}`); } this._synth.addEventListener('message', this.handleWorkerMessage.bind(this), false); this._synth.postMessage({ @@ -278,15 +279,15 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { public loadSoundFontFromUrl(url: string, append: boolean, progress: (e: ProgressEventArgs) => void): void { Logger.debug('AlphaSynth', `Start loading Soundfont from url ${url}`); - let request: XMLHttpRequest = new XMLHttpRequest(); + const request: XMLHttpRequest = new XMLHttpRequest(); request.open('GET', url, true, null, null); request.responseType = 'arraybuffer'; request.onload = _ => { - let buffer: Uint8Array = new Uint8Array(request.response); + const buffer: Uint8Array = new Uint8Array(request.response); this.loadSoundFont(buffer, append); }; request.onerror = e => { - Logger.error('AlphaSynth', 'Loading failed: ' + (e as any).message); + Logger.error('AlphaSynth', `Loading failed: ${(e as any).message}`); (this.soundFontLoadFailed as EventEmitterOfT).trigger( new FileLoadError((e as any).message, request) ); @@ -358,8 +359,8 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { } public handleWorkerMessage(e: MessageEvent): void { - let data: any = e.data; - let cmd: string = data.cmd; + const data: any = e.data; + const cmd: string = data.cmd; switch (cmd) { case 'alphaSynth.ready': this._workerIsReady = true; diff --git a/src/platform/javascript/AlphaSynthWorkerSynthOutput.ts b/src/platform/javascript/AlphaSynthWorkerSynthOutput.ts index 73702d117..820fd5cfb 100644 --- a/src/platform/javascript/AlphaSynthWorkerSynthOutput.ts +++ b/src/platform/javascript/AlphaSynthWorkerSynthOutput.ts @@ -1,6 +1,6 @@ -import { ISynthOutput } from '@src/synth/ISynthOutput'; -import { EventEmitter, IEventEmitter, IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; -import { IWorkerScope } from '@src/platform/javascript/IWorkerScope'; +import type { ISynthOutput, ISynthOutputDevice } from '@src/synth/ISynthOutput'; +import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; +import type { IWorkerScope } from '@src/platform/javascript/IWorkerScope'; import { Logger } from '@src/Logger'; import { Environment } from '@src/Environment'; @@ -9,15 +9,15 @@ import { Environment } from '@src/Environment'; */ export class AlphaSynthWorkerSynthOutput implements ISynthOutput { public static readonly CmdOutputPrefix: string = 'alphaSynth.output.'; - public static readonly CmdOutputAddSamples: string = AlphaSynthWorkerSynthOutput.CmdOutputPrefix + 'addSamples'; - public static readonly CmdOutputPlay: string = AlphaSynthWorkerSynthOutput.CmdOutputPrefix + 'play'; - public static readonly CmdOutputPause: string = AlphaSynthWorkerSynthOutput.CmdOutputPrefix + 'pause'; - public static readonly CmdOutputResetSamples: string = AlphaSynthWorkerSynthOutput.CmdOutputPrefix + 'resetSamples'; - public static readonly CmdOutputStop: string = AlphaSynthWorkerSynthOutput.CmdOutputPrefix + 'stop'; + public static readonly CmdOutputAddSamples: string = `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}addSamples`; + public static readonly CmdOutputPlay: string = `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}play`; + public static readonly CmdOutputPause: string = `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}pause`; + public static readonly CmdOutputResetSamples: string = `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}resetSamples`; + public static readonly CmdOutputStop: string = `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}stop`; public static readonly CmdOutputSampleRequest: string = - AlphaSynthWorkerSynthOutput.CmdOutputPrefix + 'sampleRequest'; + `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}sampleRequest`; public static readonly CmdOutputSamplesPlayed: string = - AlphaSynthWorkerSynthOutput.CmdOutputPrefix + 'samplesPlayed'; + `${AlphaSynthWorkerSynthOutput.CmdOutputPrefix}samplesPlayed`; // this value is initialized by the alphaSynth WebWorker wrapper // that also includes the alphaSynth library into the worker. @@ -43,8 +43,8 @@ export class AlphaSynthWorkerSynthOutput implements ISynthOutput { } private handleMessage(e: MessageEvent): void { - let data: any = e.data; - let cmd: any = data.cmd; + const data: any = e.data; + const cmd: any = data.cmd; switch (cmd) { case AlphaSynthWorkerSynthOutput.CmdOutputSampleRequest: (this.sampleRequest as EventEmitter).trigger(); @@ -87,4 +87,12 @@ export class AlphaSynthWorkerSynthOutput implements ISynthOutput { public activate(): void { // nothing to do } + + public async enumerateOutputDevices(): Promise { + return []; + } + public async setOutputDevice(device: ISynthOutputDevice | null): Promise {} + public async getOutputDevice(): Promise { + return null; + } } diff --git a/src/platform/javascript/AlphaTabApi.ts b/src/platform/javascript/AlphaTabApi.ts index 80b131345..f667019a7 100644 --- a/src/platform/javascript/AlphaTabApi.ts +++ b/src/platform/javascript/AlphaTabApi.ts @@ -3,33 +3,71 @@ import { AlphaSynthMidiFileHandler } from '@src/midi/AlphaSynthMidiFileHandler'; import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; import { MidiFile, MidiFileFormat } from '@src/midi/MidiFile'; import { LayoutMode } from '@src/LayoutMode'; -import { IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; -import { Track } from '@src/model/Track'; -import { AlphaSynthWebWorkerApi } from '@src/platform/javascript/AlphaSynthWebWorkerApi'; +import { type IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; +import type { Track } from '@src/model/Track'; +import type { AlphaSynthWebWorkerApi } from '@src/platform/javascript/AlphaSynthWebWorkerApi'; import { BrowserUiFacade } from '@src/platform/javascript/BrowserUiFacade'; -import { ProgressEventArgs } from '@src/ProgressEventArgs'; -import { Settings } from '@src/Settings'; +import type { ProgressEventArgs } from '@src/ProgressEventArgs'; +import type { Settings } from '@src/Settings'; import { JsonConverter } from '@src/model/JsonConverter'; import { SettingsSerializer } from '@src/generated/SettingsSerializer'; -import { SettingsJson } from '@src/generated/SettingsJson'; +import type { SettingsJson } from '@src/generated/SettingsJson'; /** * @target web */ export class AlphaTabApi extends AlphaTabApiBase { + /** + * Initializes a new instance of the {@link AlphaTabApi} class. + * @param element The HTML element into which alphaTab should be initialized. + * @param settings The settings to use. + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'), { display: { scale: 1.2 }}); + * ``` + */ public constructor(element: HTMLElement, options: SettingsJson | Settings) { super(new BrowserUiFacade(element), options); } + /** + * @inheritdoc + */ public override tex(tex: string, tracks?: number[]): void { - let browser: BrowserUiFacade = this.uiFacade as BrowserUiFacade; + const browser: BrowserUiFacade = this.uiFacade as BrowserUiFacade; super.tex(tex, browser.parseTracks(tracks)); } + /** + * Opens a popup window with the rendered music notation for printing. + * @param width An optional custom width as CSS width that should be used. Best is to use a CSS width that is suitable for your preferred page size. + * @param additionalSettings An optional parameter to specify additional setting values which should be respected during printing ({@since 1.2.0}) + * @remarks + * Opens a popup window with the rendered music notation for printing. The default display of alphaTab in the browser is not very + * suitable for printing. The items are lazy loaded, the width can be dynamic, and the scale might be better suitable for screens. + * This function opens a popup window which is filled with a by-default A4 optimized view of the rendered score: + * + * * Lazy loading is disabled + * * The scale is reduced to 0.8 + * * The stretch force is reduced to 0.8 + * * The width is optimized to A4. Portrait if the page-layout is used, landscape if the horizontal-layout is used. + * + * @category Methods - Core + * @since 0.9.4 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.print(); + * api.print(undefined, { display: { barsPerRow: 5 } }); + * ``` + */ public print(width?: string, additionalSettings: unknown = null): void { // prepare a popup window for printing (a4 width, window height, centered) - let preview: Window = window.open('', '', 'width=0,height=0')!; - let a4: HTMLElement = preview.document.createElement('div'); + const preview: Window = window.open('', '', 'width=0,height=0')!; + const a4: HTMLElement = preview.document.createElement('div'); if (width) { a4.style.width = width; } else { @@ -39,7 +77,7 @@ export class AlphaTabApi extends AlphaTabApiBase { a4.style.width = '210mm'; } } - // the style is a workaround for browser having problems with printing using absolute positions. + // the style is a workaround for browser having problems with printing using absolute positions. preview.document.write(` @@ -69,33 +107,31 @@ export class AlphaTabApi extends AlphaTabApiBase { } } preview.document.body.appendChild(a4); - let dualScreenLeft: number = - typeof (window as any)['screenLeft'] !== 'undefined' - ? (window as any)['screenLeft'] - : (window as any)['left']; - let dualScreenTop: number = - typeof (window as any)['screenTop'] !== 'undefined' ? (window as any)['screenTop'] : (window as any)['top']; - let screenWidth: number = - "innerWidth" in window + const dualScreenLeft: number = + typeof (window as any).screenLeft !== 'undefined' ? (window as any).screenLeft : (window as any).left; + const dualScreenTop: number = + typeof (window as any).screenTop !== 'undefined' ? (window as any).screenTop : (window as any).top; + const screenWidth: number = + 'innerWidth' in window ? window.innerWidth - : "clientWidth" in document.documentElement - ? document.documentElement.clientWidth - : (window as Window).screen.width; - let screenHeight: number = - "innerHeight" in window + : 'clientWidth' in document.documentElement + ? document.documentElement.clientWidth + : (window as Window).screen.width; + const screenHeight: number = + 'innerHeight' in window ? window.innerHeight - : "clientHeight" in document.documentElement - ? document.documentElement.clientHeight - : (window as Window).screen.height; - let w: number = a4.offsetWidth + 50; - let h: number = window.innerHeight; - let left: number = ((screenWidth / 2) | 0) - ((w / 2) | 0) + dualScreenLeft; - let top: number = ((screenHeight / 2) | 0) - ((h / 2) | 0) + dualScreenTop; + : 'clientHeight' in document.documentElement + ? document.documentElement.clientHeight + : (window as Window).screen.height; + const w: number = a4.offsetWidth + 50; + const h: number = window.innerHeight; + const left: number = ((screenWidth / 2) | 0) - ((w / 2) | 0) + dualScreenLeft; + const top: number = ((screenHeight / 2) | 0) - ((h / 2) | 0) + dualScreenTop; preview.resizeTo(w, h); preview.moveTo(left, top); preview.focus(); // render alphaTab - let settings: Settings = JsonConverter.jsObjectToSettings(JsonConverter.settingsToJsObject(this.settings)); + const settings: Settings = JsonConverter.jsObjectToSettings(JsonConverter.settingsToJsObject(this.settings)); settings.core.enableLazyLoading = false; settings.core.useWorkers = true; settings.core.file = null; @@ -108,7 +144,7 @@ export class AlphaTabApi extends AlphaTabApiBase { settings.display.scale = 0.8; settings.display.stretchForce = 0.8; SettingsSerializer.fromJson(settings, additionalSettings); - let alphaTab: AlphaTabApi = new AlphaTabApi(a4, settings); + const alphaTab: AlphaTabApi = new AlphaTabApi(a4, settings); preview.onunload = () => { alphaTab.destroy(); }; @@ -116,27 +152,42 @@ export class AlphaTabApi extends AlphaTabApiBase { preview.print(); }); alphaTab.renderTracks(this.tracks); - } + /** + * Generates an SMF1.0 file and downloads it + * @remarks + * Generates a SMF1.0 compliant MIDI file of the currently loaded song and starts the download of it. + * Please be aware that SMF1.0 does not support bends per note which might result in wrong bend effects + * in case multiple bends are applied on the same beat (e.g. two notes bending or vibrato + bends). + * + * @category Methods - Core + * @since 1.3.0 + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.downloadMidi(); + * ``` + */ public downloadMidi(format: MidiFileFormat = MidiFileFormat.SingleTrackMultiChannel): void { if (!this.score) { return; } - let midiFile: MidiFile = new MidiFile(); + const midiFile: MidiFile = new MidiFile(); midiFile.format = format; - let handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile, true); - let generator: MidiFileGenerator = new MidiFileGenerator(this.score, this.settings, handler); + const handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(midiFile, true); + const generator: MidiFileGenerator = new MidiFileGenerator(this.score, this.settings, handler); generator.generate(); - let binary: Uint8Array = midiFile.toBinary(); - let fileName: string = !this.score.title ? 'File.mid' : `${this.score.title}.mid`; - let dlLink: HTMLAnchorElement = document.createElement('a'); + const binary: Uint8Array = midiFile.toBinary(); + const fileName: string = !this.score.title ? 'File.mid' : `${this.score.title}.mid`; + const dlLink: HTMLAnchorElement = document.createElement('a'); dlLink.download = fileName; - let blob: Blob = new Blob([binary], { + const blob: Blob = new Blob([binary], { type: 'audio/midi' }); - let url: string = URL.createObjectURL(blob); + const url: string = URL.createObjectURL(blob); dlLink.href = url; dlLink.style.display = 'none'; document.body.appendChild(dlLink); @@ -144,18 +195,27 @@ export class AlphaTabApi extends AlphaTabApiBase { document.body.removeChild(dlLink); } + /** + * @inheritdoc + */ public override changeTrackMute(tracks: Track[], mute: boolean): void { - let trackList: Track[] = this.trackIndexesToTracks((this.uiFacade as BrowserUiFacade).parseTracks(tracks)); + const trackList: Track[] = this.trackIndexesToTracks((this.uiFacade as BrowserUiFacade).parseTracks(tracks)); super.changeTrackMute(trackList, mute); } + /** + * @inheritdoc + */ public override changeTrackSolo(tracks: Track[], solo: boolean): void { - let trackList: Track[] = this.trackIndexesToTracks((this.uiFacade as BrowserUiFacade).parseTracks(tracks)); + const trackList: Track[] = this.trackIndexesToTracks((this.uiFacade as BrowserUiFacade).parseTracks(tracks)); super.changeTrackSolo(trackList, solo); } + /** + * @inheritdoc + */ public override changeTrackVolume(tracks: Track[], volume: number): void { - let trackList: Track[] = this.trackIndexesToTracks((this.uiFacade as BrowserUiFacade).parseTracks(tracks)); + const trackList: Track[] = this.trackIndexesToTracks((this.uiFacade as BrowserUiFacade).parseTracks(tracks)); super.changeTrackVolume(trackList, volume); } @@ -163,13 +223,13 @@ export class AlphaTabApi extends AlphaTabApiBase { if (!this.score) { return []; } - let tracks: Track[] = []; + const tracks: Track[] = []; if (trackIndexes.length === 1 && trackIndexes[0] === -1) { - for (let track of this.score.tracks) { + for (const track of this.score.tracks) { tracks.push(track); } } else { - for (let index of trackIndexes) { + for (const index of trackIndexes) { if (index >= 0 && index < this.score.tracks.length) { tracks.push(this.score.tracks[index]); } @@ -178,18 +238,40 @@ export class AlphaTabApi extends AlphaTabApiBase { return tracks; } + /** + * This event is fired when the SoundFont is being loaded. + * @remarks + * This event is fired when the SoundFont is being loaded and reports the progress accordingly. + * + * @eventProperty + * @category Events - Player + * @since 0.9.4 + * + * @example + * JavaScript + * ```js + * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab')); + * api.soundFontLoad.on((e) => { + * updateProgress(e.loaded, e.total); + * }); + * ``` + */ public soundFontLoad: IEventEmitterOfT = new EventEmitterOfT(); + + /** + * Triggers a load of the soundfont from the given URL. + * @param url The URL from which to load the soundfont + * @param append Whether to fully replace or append the data from the given soundfont. + * @category Methods - Player + * @since 0.9.4 + */ public loadSoundFontFromUrl(url: string, append: boolean): void { if (!this.player) { return; } - (this.player as AlphaSynthWebWorkerApi).loadSoundFontFromUrl( - url, - append, - e => { - (this.soundFontLoad as EventEmitterOfT).trigger(e); - this.uiFacade.triggerEvent(this.container, 'soundFontLoad', e); - } - ); + (this.player as AlphaSynthWebWorkerApi).loadSoundFontFromUrl(url, append, e => { + (this.soundFontLoad as EventEmitterOfT).trigger(e); + this.uiFacade.triggerEvent(this.container, 'soundFontLoad', e); + }); } } diff --git a/src/platform/javascript/AlphaTabWebWorker.ts b/src/platform/javascript/AlphaTabWebWorker.ts index 05100d505..bed45e8ee 100644 --- a/src/platform/javascript/AlphaTabWebWorker.ts +++ b/src/platform/javascript/AlphaTabWebWorker.ts @@ -1,9 +1,9 @@ import { JsonConverter } from '@src/model/JsonConverter'; -import { Score } from '@src/model/Score'; -import { IWorkerScope } from '@src/platform/javascript/IWorkerScope'; -import { FontSizeDefinition, FontSizes } from '@src/platform/svg/FontSizes'; +import type { Score } from '@src/model/Score'; +import type { IWorkerScope } from '@src/platform/javascript/IWorkerScope'; +import { type FontSizeDefinition, FontSizes } from '@src/platform/svg/FontSizes'; import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { Environment } from '@src/Environment'; import { SettingsSerializer } from '@src/generated/SettingsSerializer'; @@ -27,11 +27,11 @@ export class AlphaTabWebWorker { } private handleMessage(e: MessageEvent): void { - let data: any = e.data; - let cmd: any = data ? data.cmd : ''; + const data: any = e.data; + const cmd: any = data ? data.cmd : ''; switch (cmd) { case 'alphaTab.initialize': - let settings: Settings = JsonConverter.jsObjectToSettings(data.settings); + const settings: Settings = JsonConverter.jsObjectToSettings(data.settings); Logger.logLevel = settings.core.logLevel; this._renderer = new ScoreRenderer(settings); this._renderer.partialRenderFinished.on(result => { @@ -80,7 +80,7 @@ export class AlphaTabWebWorker { break; case 'alphaTab.renderScore': this.updateFontSizes(data.fontSizes); - let score: any = + const score: any = data.score == null ? null : JsonConverter.jsObjectToScore(data.score, this._renderer.settings); this.renderMultiple(score, data.trackIndexes); break; @@ -103,7 +103,7 @@ export class AlphaTabWebWorker { if (!FontSizes.FontSizeLookupTables) { FontSizes.FontSizeLookupTables = new Map(); } - for (let [k, v] of fontSizes) { + for (const [k, v] of fontSizes) { FontSizes.FontSizeLookupTables.set(k, v); } } diff --git a/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts b/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts index 5fd4de4a8..22c5cf71a 100644 --- a/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts +++ b/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts @@ -1,12 +1,12 @@ -import { AlphaTabApiBase } from '@src/AlphaTabApiBase'; -import { EventEmitter, IEventEmitterOfT, IEventEmitter, EventEmitterOfT } from '@src/EventEmitter'; +import type { AlphaTabApiBase } from '@src/AlphaTabApiBase'; +import { EventEmitter, type IEventEmitterOfT, type IEventEmitter, EventEmitterOfT } from '@src/EventEmitter'; import { JsonConverter } from '@src/model/JsonConverter'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { FontSizes } from '@src/platform/svg/FontSizes'; -import { IScoreRenderer } from '@src/rendering/IScoreRenderer'; -import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; +import type { IScoreRenderer } from '@src/rendering/IScoreRenderer'; +import type { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; import { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { Environment } from '@src/Environment'; @@ -73,7 +73,6 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { }); } - public get width(): number { return this._width; } @@ -87,8 +86,8 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { } private handleWorkerMessage(e: MessageEvent): void { - let data: any = e.data; - let cmd: string = data.cmd; + const data: any = e.data; + const cmd: string = data.cmd; switch (cmd) { case 'alphaTab.preRender': (this.preRender as EventEmitterOfT).trigger(data.resize); @@ -114,7 +113,7 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { } public renderScore(score: Score | null, trackIndexes: number[] | null): void { - let jsObject: unknown = score == null ? null : JsonConverter.scoreToJsObject(score); + const jsObject: unknown = score == null ? null : JsonConverter.scoreToJsObject(score); this._worker.postMessage({ cmd: 'alphaTab.renderScore', score: jsObject, @@ -124,8 +123,10 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { } public preRender: IEventEmitterOfT = new EventEmitterOfT(); - public partialRenderFinished: IEventEmitterOfT = new EventEmitterOfT(); - public partialLayoutFinished: IEventEmitterOfT = new EventEmitterOfT(); + public partialRenderFinished: IEventEmitterOfT = + new EventEmitterOfT(); + public partialLayoutFinished: IEventEmitterOfT = + new EventEmitterOfT(); public renderFinished: IEventEmitterOfT = new EventEmitterOfT(); public postRenderFinished: IEventEmitter = new EventEmitter(); public error: IEventEmitterOfT = new EventEmitterOfT(); diff --git a/src/platform/javascript/BrowserMouseEventArgs.ts b/src/platform/javascript/BrowserMouseEventArgs.ts index 6b9a669f7..05771fb42 100644 --- a/src/platform/javascript/BrowserMouseEventArgs.ts +++ b/src/platform/javascript/BrowserMouseEventArgs.ts @@ -1,6 +1,6 @@ -import { IContainer } from '@src/platform/IContainer'; -import { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; -import { HtmlElementContainer } from '@src/platform/javascript/HtmlElementContainer'; +import type { IContainer } from '@src/platform/IContainer'; +import type { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; +import type { HtmlElementContainer } from '@src/platform/javascript/HtmlElementContainer'; /** * @target web @@ -13,16 +13,16 @@ export class BrowserMouseEventArgs implements IMouseEventArgs { } public getX(relativeTo: IContainer): number { - let relativeToElement: HTMLElement = (relativeTo as HtmlElementContainer).element; - let bounds: DOMRect = relativeToElement.getBoundingClientRect(); - let left: number = bounds.left + relativeToElement.ownerDocument!.defaultView!.pageXOffset; + const relativeToElement: HTMLElement = (relativeTo as HtmlElementContainer).element; + const bounds: DOMRect = relativeToElement.getBoundingClientRect(); + const left: number = bounds.left + relativeToElement.ownerDocument!.defaultView!.pageXOffset; return this.mouseEvent.pageX - left; } public getY(relativeTo: IContainer): number { - let relativeToElement: HTMLElement = (relativeTo as HtmlElementContainer).element; - let bounds: DOMRect = relativeToElement.getBoundingClientRect(); - let top: number = bounds.top + relativeToElement.ownerDocument!.defaultView!.pageYOffset; + const relativeToElement: HTMLElement = (relativeTo as HtmlElementContainer).element; + const bounds: DOMRect = relativeToElement.getBoundingClientRect(); + const top: number = bounds.top + relativeToElement.ownerDocument!.defaultView!.pageYOffset; return this.mouseEvent.pageY - top; } diff --git a/src/platform/javascript/BrowserUiFacade.ts b/src/platform/javascript/BrowserUiFacade.ts index 7fb82bc5e..a15ba78df 100644 --- a/src/platform/javascript/BrowserUiFacade.ts +++ b/src/platform/javascript/BrowserUiFacade.ts @@ -1,45 +1,45 @@ -import { AlphaTabApiBase } from '@src/AlphaTabApiBase'; -import { IAlphaSynth } from '@src/synth/IAlphaSynth'; +import type { AlphaTabApiBase } from '@src/AlphaTabApiBase'; +import type { IAlphaSynth } from '@src/synth/IAlphaSynth'; import { Environment } from '@src/Environment'; -import { EventEmitter, IEventEmitter } from '@src/EventEmitter'; +import { EventEmitter, type IEventEmitter } from '@src/EventEmitter'; import { ScoreLoader } from '@src/importer/ScoreLoader'; -import { Font } from '@src/model/Font'; +import type { Font } from '@src/model/Font'; import { Score } from '@src/model/Score'; import { NotationMode } from '@src/NotationSettings'; -import { IContainer } from '@src/platform/IContainer'; +import type { IContainer } from '@src/platform/IContainer'; import { HtmlElementContainer } from '@src/platform/javascript/HtmlElementContainer'; import { FontSizes } from '@src/platform/svg/FontSizes'; -import { IScoreRenderer } from '@src/rendering/IScoreRenderer'; -import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; +import type { IScoreRenderer } from '@src/rendering/IScoreRenderer'; +import type { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; import { Bounds } from '@src/rendering/utils/Bounds'; import { Settings } from '@src/Settings'; import { FontLoadingChecker } from '@src/util/FontLoadingChecker'; import { Logger } from '@src/Logger'; -import { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; -import { IUiFacade } from '@src/platform/IUiFacade'; +import type { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; +import type { IUiFacade } from '@src/platform/IUiFacade'; import { AlphaSynthScriptProcessorOutput } from '@src/platform/javascript/AlphaSynthScriptProcessorOutput'; import { AlphaSynthWebWorkerApi } from '@src/platform/javascript/AlphaSynthWebWorkerApi'; -import { AlphaTabApi } from '@src/platform/javascript/AlphaTabApi'; +import type { AlphaTabApi } from '@src/platform/javascript/AlphaTabApi'; import { AlphaTabWorkerScoreRenderer } from '@src/platform/javascript/AlphaTabWorkerScoreRenderer'; -import { BrowserMouseEventArgs } from '@src/platform/javascript/BrowserMouseEventArgs'; +import type { BrowserMouseEventArgs } from '@src/platform/javascript/BrowserMouseEventArgs'; import { Cursors } from '@src/platform/Cursors'; import { JsonConverter } from '@src/model/JsonConverter'; import { SettingsSerializer } from '@src/generated/SettingsSerializer'; import { WebPlatform } from '@src/platform/javascript/WebPlatform'; import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; import { AlphaSynthAudioWorkletOutput } from '@src/platform/javascript/AlphaSynthAudioWorkletOutput'; -import { ScalableHtmlElementContainer } from './ScalableHtmlElementContainer'; +import { ScalableHtmlElementContainer } from '@src/platform/javascript/ScalableHtmlElementContainer'; import { PlayerOutputMode } from '@src/PlayerSettings'; -import { SettingsJson } from '@src/generated/SettingsJson'; +import type { SettingsJson } from '@src/generated/SettingsJson'; /** * @target web */ enum ResultState { - LayoutDone, - RenderRequested, - RenderDone, - Detached + LayoutDone = 0, + RenderRequested = 1, + RenderDone = 2, + Detached = 3 } /** @@ -97,7 +97,7 @@ export class BrowserUiFacade implements IUiFacade { return false; } - Logger.debug('Font', 'All fonts loaded: ' + this._fontCheckers.size); + Logger.debug('Font', `All fonts loaded: ${this._fontCheckers.size}`); return true; } @@ -176,7 +176,7 @@ export class BrowserUiFacade implements IUiFacade { settings = JsonConverter.jsObjectToSettings(raw); } - let dataAttributes: Map = this.getDataAttributes(); + const dataAttributes: Map = this.getDataAttributes(); SettingsSerializer.fromJson(settings, dataAttributes); if (settings.notation.notationMode === NotationMode.SongBook) { settings.setSongBookModeSettings(); @@ -186,7 +186,7 @@ export class BrowserUiFacade implements IUiFacade { this._initialTrackIndexes = this.parseTracks(settings.core.tracks); this._contents = ''; - let element: HtmlElementContainer = api.container as HtmlElementContainer; + const element: HtmlElementContainer = api.container as HtmlElementContainer; if (settings.core.tex) { this._contents = element.element.innerHTML; element.element.innerHTML = ''; @@ -211,7 +211,7 @@ export class BrowserUiFacade implements IUiFacade { private registerFontChecker(font: Font): void { if (!this._fontCheckers.has(font.families.join(', '))) { - let checker: FontLoadingChecker = new FontLoadingChecker(font.families); + const checker: FontLoadingChecker = new FontLoadingChecker(font.families); this._fontCheckers.set(font.families.join(', '), checker); checker.fontLoaded.on(this.onFontLoaded.bind(this)); checker.checkForFontAvailability(); @@ -223,7 +223,7 @@ export class BrowserUiFacade implements IUiFacade { } public createCanvasElement(): IContainer { - let canvasElement: HTMLElement = document.createElement('div'); + const canvasElement: HTMLElement = document.createElement('div'); canvasElement.className = 'at-surface'; canvasElement.style.fontSize = '0'; canvasElement.style.overflow = 'hidden'; @@ -238,10 +238,10 @@ export class BrowserUiFacade implements IUiFacade { details: unknown = null, originalEvent?: IMouseEventArgs ): void { - let element: HTMLElement = (container as HtmlElementContainer).element; - name = 'alphaTab.' + name; - let e: any = document.createEvent('CustomEvent'); - let originalMouseEvent: MouseEvent | null = originalEvent + const element: HTMLElement = (container as HtmlElementContainer).element; + name = `alphaTab.${name}`; + const e: any = document.createEvent('CustomEvent'); + const originalMouseEvent: MouseEvent | null = originalEvent ? (originalEvent as BrowserMouseEventArgs).mouseEvent : null; e.initCustomEvent(name, false, false, details); @@ -250,8 +250,8 @@ export class BrowserUiFacade implements IUiFacade { } element.dispatchEvent(e); if (window && 'jQuery' in window) { - let jquery: any = (window as any)['jQuery']; - let args: unknown[] = []; + const jquery: any = (window as any).jQuery; + const args: unknown[] = []; args.push(details); if (originalMouseEvent) { args.push(originalMouseEvent); @@ -266,7 +266,7 @@ export class BrowserUiFacade implements IUiFacade { return true; } if (data instanceof ArrayBuffer) { - let byteArray: Uint8Array = new Uint8Array(data); + const byteArray: Uint8Array = new Uint8Array(data); success(ScoreLoader.loadScoreFromBytes(byteArray, this._api.settings)); return true; } @@ -339,7 +339,7 @@ export class BrowserUiFacade implements IUiFacade { } private createStyleElement(settings: Settings): void { - let elementDocument: HTMLDocument = (this._api.container as HtmlElementContainer).element.ownerDocument!; + const elementDocument: HTMLDocument = (this._api.container as HtmlElementContainer).element.ownerDocument!; Environment.createStyleElement(elementDocument, settings.core.fontDirectory); } @@ -347,7 +347,7 @@ export class BrowserUiFacade implements IUiFacade { if (!tracksData) { return []; } - let tracks: number[] = []; + const tracks: number[] = []; // decode string if (typeof tracksData === 'string') { try { @@ -363,17 +363,17 @@ export class BrowserUiFacade implements IUiFacade { if (typeof tracksData === 'number') { tracks.push(tracksData); } else if ('length' in (tracksData as any)) { - let length: number = (tracksData as any).length; - let array: unknown[] = tracksData as unknown[]; + const length: number = (tracksData as any).length; + const array: unknown[] = tracksData as unknown[]; for (let i: number = 0; i < length; i++) { - let item: unknown = array[i]; + const item: unknown = array[i]; let value: number = 0; if (typeof item === 'number') { value = item; } else if ('index' in (item as any)) { value = (item as any).index; } else { - value = parseInt((item as any).toString()); + value = Number.parseInt((item as any).toString()); } if (value >= 0 || value === -1) { tracks.push(value); @@ -386,13 +386,13 @@ export class BrowserUiFacade implements IUiFacade { } private getDataAttributes(): Map { - let dataAttributes: Map = new Map(); - let element: HTMLElement = (this._api.container as HtmlElementContainer).element; + const dataAttributes: Map = new Map(); + const element: HTMLElement = (this._api.container as HtmlElementContainer).element; if (element.dataset) { - for (let key of Object.keys(element.dataset)) { + for (const key of Object.keys(element.dataset)) { let value: unknown = (element.dataset as any)[key]; try { - let stringValue: string = value as string; + const stringValue: string = value as string; value = JSON.parse(stringValue); } catch (e) { if (value === '') { @@ -403,10 +403,10 @@ export class BrowserUiFacade implements IUiFacade { } } else { for (let i: number = 0; i < element.attributes.length; i++) { - let attr: Attr = element.attributes.item(i)!; - let nodeName: string = attr.nodeName; + const attr: Attr = element.attributes.item(i)!; + const nodeName: string = attr.nodeName; if (nodeName.startsWith('data-')) { - let keyParts: string[] = nodeName.substr(5).split('-'); + const keyParts: string[] = nodeName.substr(5).split('-'); let key: string = keyParts[0]; for (let j: number = 1; j < keyParts.length; j++) { key += keyParts[j].substr(0, 1).toUpperCase() + keyParts[j].substr(1); @@ -465,15 +465,15 @@ export class BrowserUiFacade implements IUiFacade { } placeholder.style.zIndex = '1'; placeholder.style.position = 'absolute'; - placeholder.style.left = renderResult.x + 'px'; - placeholder.style.top = renderResult.y + 'px'; - placeholder.style.width = renderResult.width + 'px'; - placeholder.style.height = renderResult.height + 'px'; + placeholder.style.left = `${renderResult.x}px`; + placeholder.style.top = `${renderResult.y}px`; + placeholder.style.width = `${renderResult.width}px`; + placeholder.style.height = `${renderResult.height}px`; placeholder.style.display = 'inline-block'; placeholder.layoutResultId = renderResult.id; placeholder.resultState = ResultState.LayoutDone; - delete placeholder.renderedResultId; - delete placeholder.renderedResult; + placeholder.renderedResultId = undefined; + placeholder.renderedResult = undefined; this._resultIdToElementLookup.set(renderResult.id, placeholder); @@ -501,10 +501,9 @@ export class BrowserUiFacade implements IUiFacade { */ public createWorkerPlayer(): IAlphaSynth | null { let player: AlphaSynthWebWorkerApi | null = null; - let supportsScriptProcessor: boolean = 'ScriptProcessorNode' in window; + const supportsScriptProcessor: boolean = 'ScriptProcessorNode' in window; - let supportsAudioWorklets: boolean = - window.isSecureContext && 'AudioWorkletNode' in window; + const supportsAudioWorklets: boolean = window.isSecureContext && 'AudioWorkletNode' in window; if (supportsAudioWorklets && this._api.settings.player.outputMode === PlayerOutputMode.WebAudioAudioWorklets) { Logger.debug('Player', 'Will use webworkers for synthesizing and web audio api with worklets for playback'); @@ -513,11 +512,11 @@ export class BrowserUiFacade implements IUiFacade { this._api.settings ); } else if (supportsScriptProcessor) { - Logger.debug('Player', 'Will use webworkers for synthesizing and web audio api with ScriptProcessor for playback'); - player = new AlphaSynthWebWorkerApi( - new AlphaSynthScriptProcessorOutput(), - this._api.settings + Logger.debug( + 'Player', + 'Will use webworkers for synthesizing and web audio api with ScriptProcessor for playback' ); + player = new AlphaSynthWebWorkerApi(new AlphaSynthScriptProcessorOutput(), this._api.settings); } if (!player) { @@ -542,7 +541,7 @@ export class BrowserUiFacade implements IUiFacade { public highlightElements(groupId: string, masterBarIndex: number): void { const element = this._barToElementLookup.get(masterBarIndex); if (element) { - let elementsToHighlight: HTMLCollection = element.getElementsByClassName(groupId); + const elementsToHighlight: HTMLCollection = element.getElementsByClassName(groupId); for (let i: number = 0; i < elementsToHighlight.length; i++) { elementsToHighlight.item(i)!.classList.add('at-highlight'); this._highlightedElements.push(elementsToHighlight.item(i) as HTMLElement); @@ -562,24 +561,24 @@ export class BrowserUiFacade implements IUiFacade { } public destroyCursors(): void { - let element: HTMLElement = (this._api.container as HtmlElementContainer).element; - let cursorWrapper: HTMLElement = element.querySelector('.at-cursors') as HTMLElement; + const element: HTMLElement = (this._api.container as HtmlElementContainer).element; + const cursorWrapper: HTMLElement = element.querySelector('.at-cursors') as HTMLElement; element.removeChild(cursorWrapper); } public createCursors(): Cursors | null { - let element: HTMLElement = (this._api.container as HtmlElementContainer).element; - let cursorWrapper: HTMLElement = document.createElement('div'); + const element: HTMLElement = (this._api.container as HtmlElementContainer).element; + const cursorWrapper: HTMLElement = document.createElement('div'); cursorWrapper.classList.add('at-cursors'); - let selectionWrapper: HTMLElement = document.createElement('div'); + const selectionWrapper: HTMLElement = document.createElement('div'); selectionWrapper.classList.add('at-selection'); const barCursorContainer = this.createScalingElement(); const beatCursorContainer = this.createScalingElement(); - let barCursor: HTMLElement = barCursorContainer.element; + const barCursor: HTMLElement = barCursorContainer.element; barCursor.classList.add('at-cursor-bar'); - let beatCursor: HTMLElement = beatCursorContainer.element; + const beatCursor: HTMLElement = beatCursorContainer.element; beatCursor.classList.add('at-cursor-beat'); // required css styles element.style.position = 'relative'; @@ -623,21 +622,21 @@ export class BrowserUiFacade implements IUiFacade { } public getOffset(scrollContainer: IContainer | null, container: IContainer): Bounds { - let element: HTMLElement = (container as HtmlElementContainer).element; - let bounds: DOMRect = element.getBoundingClientRect(); + const element: HTMLElement = (container as HtmlElementContainer).element; + const bounds: DOMRect = element.getBoundingClientRect(); let top: number = bounds.top + element.ownerDocument!.defaultView!.pageYOffset; let left: number = bounds.left + element.ownerDocument!.defaultView!.pageXOffset; if (scrollContainer) { - let scrollElement: HTMLElement = (scrollContainer as HtmlElementContainer).element; - let nodeName: string = scrollElement.nodeName.toLowerCase(); + const scrollElement: HTMLElement = (scrollContainer as HtmlElementContainer).element; + const nodeName: string = scrollElement.nodeName.toLowerCase(); if (nodeName !== 'html' && nodeName !== 'body') { - let scrollElementOffset: Bounds = this.getOffset(null, scrollContainer); + const scrollElementOffset: Bounds = this.getOffset(null, scrollContainer); top = top + scrollElement.scrollTop - scrollElementOffset.y; left = left + scrollElement.scrollLeft - scrollElementOffset.x; } } - let b = new Bounds(); + const b = new Bounds(); b.x = left; b.y = top; b.w = bounds.width; @@ -656,7 +655,7 @@ export class BrowserUiFacade implements IUiFacade { typeof this._api.settings.player.scrollElement === 'string' ? (document.querySelector(this._api.settings.player.scrollElement) as HTMLElement) : (this._api.settings.player.scrollElement as HTMLElement); - let nodeName: string = scrollElement.nodeName.toLowerCase(); + const nodeName: string = scrollElement.nodeName.toLowerCase(); if (nodeName === 'html' || nodeName === 'body') { // https://github.com/CoderLine/alphaTab/issues/205 // https://github.com/CoderLine/alphaTab/issues/354 @@ -713,16 +712,16 @@ export class BrowserUiFacade implements IUiFacade { behavior: 'smooth' }); } else { - let startY: number = element.scrollTop; - let diff: number = scrollTargetY - startY; + const startY: number = element.scrollTop; + const diff: number = scrollTargetY - startY; let start: number = 0; - let step = (x: number) => { + const step = (x: number) => { if (start === 0) { start = x; } - let time: number = x - start; - let percent: number = Math.min(time / speed, 1); + 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); @@ -739,15 +738,15 @@ export class BrowserUiFacade implements IUiFacade { behavior: 'smooth' }); } else { - let startX: number = element.scrollLeft; - let diff: number = scrollTargetX - startX; + const startX: number = element.scrollLeft; + const diff: number = scrollTargetX - startX; let start: number = 0; - let step = (t: number) => { + const step = (t: number) => { if (start === 0) { start = t; } - let time: number = t - start; - let percent: number = Math.min(time / speed, 1); + 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); diff --git a/src/platform/javascript/Html5Canvas.ts b/src/platform/javascript/Html5Canvas.ts index e91221fa9..df252846a 100644 --- a/src/platform/javascript/Html5Canvas.ts +++ b/src/platform/javascript/Html5Canvas.ts @@ -2,8 +2,8 @@ import { Environment } from '@src/Environment'; import { Color } from '@src/model/Color'; import { Font, FontStyle } from '@src/model/Font'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { ICanvas, TextAlign, TextBaseline, TextMetrics } from '@src/platform/ICanvas'; -import { Settings } from '@src/Settings'; +import { type ICanvas, TextAlign, TextBaseline, MeasuredText } from '@src/platform/ICanvas'; +import type { Settings } from '@src/Settings'; /** * A canvas implementation for HTML5 canvas @@ -22,15 +22,15 @@ export class Html5Canvas implements ICanvas { public settings!: Settings; public constructor() { - let fontElement: HTMLElement = document.createElement('span'); + const fontElement: HTMLElement = document.createElement('span'); fontElement.classList.add('at'); document.body.appendChild(fontElement); - let style: CSSStyleDeclaration = window.getComputedStyle(fontElement); + const style: CSSStyleDeclaration = window.getComputedStyle(fontElement); let family: string = style.fontFamily; if (family.startsWith('"') || family.startsWith("'")) { family = family.substr(1, family.length - 2); } - this._musicFont = new Font(family, parseFloat(style.fontSize), FontStyle.Plain); + this._musicFont = new Font(family, Number.parseFloat(style.fontSize), FontStyle.Plain); this._measureCanvas = document.createElement('canvas'); this._measureCanvas.width = 10; this._measureCanvas.height = 10; @@ -52,8 +52,8 @@ export class Html5Canvas implements ICanvas { this._canvas = document.createElement('canvas'); this._canvas.width = (width * Environment.HighDpiFactor) | 0; this._canvas.height = (height * Environment.HighDpiFactor) | 0; - this._canvas.style.width = width + 'px'; - this._canvas.style.height = height + 'px'; + this._canvas.style.width = `${width}px`; + this._canvas.style.height = `${height}px`; this._context = this._canvas.getContext('2d')!; this._context.textBaseline = 'hanging'; this._context.scale(Environment.HighDpiFactor * scale, Environment.HighDpiFactor * scale); @@ -61,7 +61,7 @@ export class Html5Canvas implements ICanvas { } public endRender(): unknown { - let result: HTMLCanvasElement = this._canvas!; + const result: HTMLCanvasElement = this._canvas!; this._canvas = null; return result; } @@ -225,9 +225,9 @@ export class Html5Canvas implements ICanvas { this._context.fillText(text, x, y); } - public measureText(text: string): TextMetrics { + public measureText(text: string): MeasuredText { const metrics = this._measureContext.measureText(text); - return new TextMetrics(metrics.width, metrics.actualBoundingBoxDescent - metrics.actualBoundingBoxAscent); + return new MeasuredText(metrics.width, metrics.actualBoundingBoxDescent + metrics.actualBoundingBoxAscent); } public fillMusicFontSymbol( @@ -251,7 +251,7 @@ export class Html5Canvas implements ICanvas { centerAtPosition: boolean = false ): void { let s: string = ''; - for (let symbol of symbols) { + for (const symbol of symbols) { if (symbol !== MusicFontSymbol.None) { s += String.fromCharCode(symbol); } @@ -266,9 +266,9 @@ export class Html5Canvas implements ICanvas { symbols: string, centerAtPosition: boolean ): void { - let textAlign = this._context.textAlign; - let baseLine = this._context.textBaseline; - let font: string = this._context.font; + const textAlign = this._context.textAlign; + const baseLine = this._context.textBaseline; + const font: string = this._context.font; this._context.font = this._musicFont.toCssString(relativeScale); this._context.textBaseline = 'middle'; if (centerAtPosition) { diff --git a/src/platform/javascript/HtmlElementContainer.ts b/src/platform/javascript/HtmlElementContainer.ts index 90f54a4aa..d217c07ab 100644 --- a/src/platform/javascript/HtmlElementContainer.ts +++ b/src/platform/javascript/HtmlElementContainer.ts @@ -1,6 +1,6 @@ -import { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; -import { IContainer } from '@src/platform/IContainer'; -import { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; +import type { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; +import type { IContainer } from '@src/platform/IContainer'; +import type { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; import { BrowserMouseEventArgs } from '@src/platform/javascript/BrowserMouseEventArgs'; import { Bounds } from '@src/rendering/utils/Bounds'; import { Lazy } from '@src/util/Lazy'; @@ -13,7 +13,7 @@ export class HtmlElementContainer implements IContainer { () => new ResizeObserver((entries: ResizeObserverEntry[]) => { for (const e of entries) { - let evt = new CustomEvent('resize', { + const evt = new CustomEvent('resize', { detail: e }); e.target.dispatchEvent(evt); @@ -28,7 +28,7 @@ export class HtmlElementContainer implements IContainer { } public set width(value: number) { - this.element.style.width = value + 'px'; + this.element.style.width = `${value}px`; } public get scrollLeft(): number { @@ -53,7 +53,7 @@ export class HtmlElementContainer implements IContainer { public set height(value: number) { if (value >= 0) { - this.element.style.height = value + 'px'; + this.element.style.height = `${value}px`; } else { this.element.style.height = '100%'; } @@ -138,22 +138,22 @@ export class HtmlElementContainer implements IContainer { public transitionToX(duration: number, x: number): void { this.element.style.transition = `transform ${duration}ms linear`; - this.setBounds(x, NaN, NaN, NaN); + this.setBounds(x, Number.NaN, Number.NaN, Number.NaN); } protected lastBounds: Bounds = new Bounds(); public setBounds(x: number, y: number, w: number, h: number) { - if (isNaN(x)) { + if (Number.isNaN(x)) { x = this.lastBounds.x; } - if (isNaN(y)) { + if (Number.isNaN(y)) { y = this.lastBounds.y; } - if (isNaN(w)) { + if (Number.isNaN(w)) { w = this.lastBounds.w; } - if (isNaN(h)) { + if (Number.isNaN(h)) { h = this.lastBounds.h; } this.element.style.transform = `translate(${x}px, ${y}px) scale(${w}, ${h})`; diff --git a/src/platform/javascript/IntersectionObserverPolyfill.ts b/src/platform/javascript/IntersectionObserverPolyfill.ts index bd294690a..18f7bc2d0 100644 --- a/src/platform/javascript/IntersectionObserverPolyfill.ts +++ b/src/platform/javascript/IntersectionObserverPolyfill.ts @@ -8,8 +8,8 @@ export class IntersectionObserverPolyfill { public constructor(callback: IntersectionObserverCallback) { let timer: any = null; - const oldCheck = this._check.bind(this); - this._check = () => { + const oldCheck = this.check.bind(this); + this.check = () => { if (!timer) { timer = setTimeout(() => { oldCheck(); @@ -20,8 +20,8 @@ export class IntersectionObserverPolyfill { this._callback = callback; - window.addEventListener('resize', this._check, true); - document.addEventListener('scroll', this._check, true); + window.addEventListener('resize', this.check, true); + document.addEventListener('scroll', this.check, true); } public observe(target: HTMLElement) { @@ -29,25 +29,24 @@ export class IntersectionObserverPolyfill { return; } this._elements.push(target); - this._check(); + this.check(); } public unobserve(target: HTMLElement) { this._elements = this._elements.filter(item => { - return item != target; + return item !== target; }); - }; + } - private _check() { + private check() { const entries: IntersectionObserverEntry[] = []; - this._elements.forEach(element => { + for (const element of this._elements) { const rect = element.getBoundingClientRect(); - const isVisible = ( + const isVisible = rect.top + rect.height >= 0 && rect.top <= window.innerHeight && rect.left + rect.width >= 0 && - rect.left <= window.innerWidth - ); + rect.left <= window.innerWidth; if (isVisible) { entries.push({ @@ -55,10 +54,10 @@ export class IntersectionObserverPolyfill { isIntersecting: true } as any); } - }); + } if (entries.length) { this._callback(entries, this as any); } } -} \ No newline at end of file +} diff --git a/src/platform/javascript/JQueryAlphaTab.ts b/src/platform/javascript/JQueryAlphaTab.ts index 5890692f5..934ffa84e 100644 --- a/src/platform/javascript/JQueryAlphaTab.ts +++ b/src/platform/javascript/JQueryAlphaTab.ts @@ -1,12 +1,12 @@ -import { IAlphaSynth } from '@src/synth/IAlphaSynth'; -import { PlayerState } from '@src/synth/PlayerState'; -import { Score } from '@src/model/Score'; -import { Track } from '@src/model/Track'; +import type { IAlphaSynth } from '@src/synth/IAlphaSynth'; +import type { PlayerState } from '@src/synth/PlayerState'; +import type { Score } from '@src/model/Score'; +import type { Track } from '@src/model/Track'; import { AlphaTabApi } from '@src/platform/javascript/AlphaTabApi'; -import { IScoreRenderer } from '@src/rendering/IScoreRenderer'; -import { Settings } from '@src/Settings'; +import type { IScoreRenderer } from '@src/rendering/IScoreRenderer'; +import type { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; -import { MidiEventType } from '@src/midi/MidiEvent'; +import type { MidiEventType } from '@src/midi/MidiEvent'; /** * @target web @@ -30,6 +30,7 @@ export declare class jQuery extends Array { /** * @target web + * @deprecated Migrate to {@link AlphaTabApi}. */ export class JQueryAlphaTab { public exec(element: HTMLElement, method: string, args: any[]): unknown { @@ -40,29 +41,29 @@ export class JQueryAlphaTab { if (method.charCodeAt(0) === 95 || method === 'exec') { return null; } - let jElement: jQuery = new jQuery(element); - let context: AlphaTabApi = jElement.data('alphaTab') as AlphaTabApi; + const jElement: jQuery = new jQuery(element); + const context: AlphaTabApi = jElement.data('alphaTab') as AlphaTabApi; if (method === 'destroy' && !context) { return null; } if (method !== 'init' && !context) { throw new Error('alphaTab not initialized'); } - let apiMethod: Function = (this as any)[method]; + // biome-ignore lint/complexity/noBannedTypes: Special use within jQuery plugin + const apiMethod: Function = (this as any)[method]; if (apiMethod) { - let realArgs: string[] = ([jElement, context] as any[]).concat(args); + const realArgs: string[] = ([jElement, context] as any[]).concat(args); return apiMethod.apply(this, realArgs); - } else { - Logger.error('Api', "Method '" + method + "' does not exist on jQuery.alphaTab"); - return null; } + Logger.error('Api', `Method '${method}' does not exist on jQuery.alphaTab`); + return null; } public init(element: jQuery, context: AlphaTabApi, options: any): void { if (!context) { context = new AlphaTabApi(element[0], options); element.data('alphaTab', context); - for (let listener of this._initListeners) { + for (const listener of this._initListeners) { listener(element, context, options); } } @@ -174,7 +175,11 @@ export class JQueryAlphaTab { return context.countInVolume; } - public midiEventsPlayedFilter(element: jQuery, context: AlphaTabApi, midiEventsPlayedFilter?: MidiEventType[]): MidiEventType[] { + public midiEventsPlayedFilter( + element: jQuery, + context: AlphaTabApi, + midiEventsPlayedFilter?: MidiEventType[] + ): MidiEventType[] { if (Array.isArray(midiEventsPlayedFilter)) { context.midiEventsPlayedFilter = midiEventsPlayedFilter; } diff --git a/src/platform/javascript/ResizeObserverPolyfill.ts b/src/platform/javascript/ResizeObserverPolyfill.ts index b7147ba0c..e30bb8ae4 100644 --- a/src/platform/javascript/ResizeObserverPolyfill.ts +++ b/src/platform/javascript/ResizeObserverPolyfill.ts @@ -1,5 +1,5 @@ /** - * A very basic polyfill of the ResizeObserver which triggers + * A very basic polyfill of the ResizeObserver which triggers * a the callback on window resize for all registered targets. * @target web */ @@ -24,7 +24,6 @@ export class ResizeObserverPolyfill { this._targets.clear(); } - private onWindowResize() { const entries: ResizeObserverEntry[] = []; for (const t of this._targets) { @@ -39,4 +38,4 @@ export class ResizeObserverPolyfill { } this._callback(entries, this); } -} \ No newline at end of file +} diff --git a/src/platform/javascript/ScalableHtmlElementContainer.ts b/src/platform/javascript/ScalableHtmlElementContainer.ts index 4215f4260..cbd453860 100644 --- a/src/platform/javascript/ScalableHtmlElementContainer.ts +++ b/src/platform/javascript/ScalableHtmlElementContainer.ts @@ -1,4 +1,4 @@ -import { HtmlElementContainer } from './HtmlElementContainer'; +import { HtmlElementContainer } from '@src/platform/javascript/HtmlElementContainer'; /** * An IContainer implementation which can be used for cursors and select ranges @@ -29,7 +29,7 @@ export class ScalableHtmlElementContainer extends HtmlElementContainer { } public override set width(value: number) { - this.element.style.width = value * this._xscale + 'px'; + this.element.style.width = `${value * this._xscale}px`; } public override get height(): number { @@ -38,25 +38,25 @@ export class ScalableHtmlElementContainer extends HtmlElementContainer { public override set height(value: number) { if (value >= 0) { - this.element.style.height = value * this._yscale + 'px'; + this.element.style.height = `${value * this._yscale}px`; } else { this.element.style.height = '100%'; } } public override setBounds(x: number, y: number, w: number, h: number) { - if (isNaN(x)) { + if (Number.isNaN(x)) { x = this.lastBounds.x; } - if (isNaN(y)) { + if (Number.isNaN(y)) { y = this.lastBounds.y; } - if (isNaN(w)) { + if (Number.isNaN(w)) { w = this.lastBounds.w; } else { w = w / this._xscale; } - if (isNaN(h)) { + if (Number.isNaN(h)) { h = this.lastBounds.h; } else { h = h / this._yscale; diff --git a/src/platform/javascript/WebPlatform.ts b/src/platform/javascript/WebPlatform.ts index f98c4e945..a041a99c4 100644 --- a/src/platform/javascript/WebPlatform.ts +++ b/src/platform/javascript/WebPlatform.ts @@ -1,9 +1,9 @@ /** - * Lists all web specific platforms alphaTab might run in - * like browser, nodejs. + * Lists all web specific platforms alphaTab might run in + * like browser, nodejs. */ export enum WebPlatform { - Browser, - NodeJs, - BrowserModule + Browser = 0, + NodeJs = 1, + BrowserModule = 2 } diff --git a/src/platform/skia/SkiaCanvas.ts b/src/platform/skia/SkiaCanvas.ts index d0a64c4c1..e48a9ed81 100644 --- a/src/platform/skia/SkiaCanvas.ts +++ b/src/platform/skia/SkiaCanvas.ts @@ -2,21 +2,22 @@ import { Environment } from '@src/Environment'; import { Color } from '@src/model/Color'; import { Font, FontStyle, FontWeight } from '@src/model/Font'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { ICanvas, TextAlign, TextBaseline, TextMetrics } from '@src/platform/ICanvas'; -import { Settings } from '@src/Settings'; +import { type ICanvas, TextAlign, TextBaseline, MeasuredText } from '@src/platform/ICanvas'; +import type { Settings } from '@src/Settings'; import type * as alphaSkia from '@coderline/alphaskia'; -import type { AlphaSkiaTypeface } from '@coderline/alphaskia'; /** * Describes the members of the alphaSkia module. * @target web */ export interface AlphaSkiaModule { - AlphaSkiaCanvas: typeof alphaSkia.AlphaSkiaCanvas, - AlphaSkiaImage: typeof alphaSkia.AlphaSkiaImage, - AlphaSkiaTextAlign: typeof alphaSkia.AlphaSkiaTextAlign, - AlphaSkiaTextBaseline: typeof alphaSkia.AlphaSkiaTextBaseline, - AlphaSkiaTypeface: typeof alphaSkia.AlphaSkiaTypeface, + AlphaSkiaCanvas: typeof alphaSkia.AlphaSkiaCanvas; + AlphaSkiaImage: typeof alphaSkia.AlphaSkiaImage; + AlphaSkiaTextAlign: typeof alphaSkia.AlphaSkiaTextAlign; + AlphaSkiaTextBaseline: typeof alphaSkia.AlphaSkiaTextBaseline; + AlphaSkiaTypeface: typeof alphaSkia.AlphaSkiaTypeface; + AlphaSkiaTextStyle: typeof alphaSkia.AlphaSkiaTextStyle; + AlphaSkiaTextMetrics: typeof alphaSkia.AlphaSkiaTextMetrics; } /** @@ -31,87 +32,87 @@ export class SkiaCanvas implements ICanvas { */ private static alphaSkia: AlphaSkiaModule; - private static musicFont: AlphaSkiaTypeface | null = null; - - private static readonly customTypeFaces = new Map(); + private static musicTextStyle: alphaSkia.AlphaSkiaTextStyle | null = null; /** * @target web * @partial */ - public static enable(musicFontData: ArrayBuffer, - alphaSkia: unknown) { + public static enable(musicFontData: ArrayBuffer, alphaSkia: unknown) { SkiaCanvas.alphaSkia = alphaSkia as AlphaSkiaModule; SkiaCanvas.initializeMusicFont(SkiaCanvas.alphaSkia.AlphaSkiaTypeface.register(musicFontData)!); } - public static initializeMusicFont(musicFont: AlphaSkiaTypeface) { - SkiaCanvas.musicFont = musicFont; + public static initializeMusicFont(musicFont: alphaSkia.AlphaSkiaTypeface) { + SkiaCanvas.musicTextStyle = new SkiaCanvas.alphaSkia.AlphaSkiaTextStyle( + [musicFont.familyName], + musicFont.weight, + musicFont.isItalic + ); } public static registerFont(fontData: Uint8Array, fontInfo?: Font | undefined): Font { const typeface = SkiaCanvas.alphaSkia.AlphaSkiaTypeface.register(fontData.buffer as ArrayBuffer)!; if (!fontInfo) { - fontInfo = Font.withFamilyList([typeface.familyName], 12, typeface.isItalic ? FontStyle.Italic : FontStyle.Plain, - typeface.isBold ? FontWeight.Bold : FontWeight.Regular); - } - - for (const family of fontInfo.families) { - this.customTypeFaces.set(SkiaCanvas.customTypefaceKey(family, fontInfo.isBold, fontInfo.isItalic), typeface); + fontInfo = Font.withFamilyList( + [typeface.familyName], + 12, + typeface.isItalic ? FontStyle.Italic : FontStyle.Plain, + typeface.weight > 400 ? FontWeight.Bold : FontWeight.Regular + ); } return fontInfo; } - private static customTypefaceKey(fontFamily: string, isBold: boolean, isItalic: boolean): string { - return fontFamily.toLowerCase() + "_" + isBold + "_" + isItalic; - } - private _canvas: alphaSkia.AlphaSkiaCanvas; private _color: Color = new Color(0, 0, 0, 0); private _lineWidth: number = 0; - private _typeFaceCache: string = ""; - private _typeFaceIsSystem: boolean = false; - private _typeFace: alphaSkia.AlphaSkiaTypeface | null = null; + private _textStyle: alphaSkia.AlphaSkiaTextStyle | null = null; private _scale = 1; + private _textStyles: Map = new Map(); + private _font: Font = new Font('Arial', 10, FontStyle.Plain); public settings!: Settings; - private getTypeFace(): alphaSkia.AlphaSkiaTypeface { - if (this._typeFaceCache != this.font.toCssString(this._scale)) { - if (this._typeFaceIsSystem) { - using _ = this._typeFace!; - } - - for (const family of this.font.families) { - var key = SkiaCanvas.customTypefaceKey(family, this.font.isBold, this.font.isItalic); - if (!SkiaCanvas.customTypeFaces.has(key)) { - this._typeFaceIsSystem = true; - this._typeFace = SkiaCanvas.alphaSkia.AlphaSkiaTypeface.create(family, - this.font.isBold, - this.font.isItalic - )!; - } - else { - this._typeFaceIsSystem = false; - this._typeFace = SkiaCanvas.customTypeFaces.get(key)!; - } - - } + public get font(): Font { + return this._font; + } - this._typeFaceCache = this.font.toCssString(this._scale); + public set font(value: Font) { + if (this._font === value) { + return; + } + this._font = value; + + const key = this.textStyleKey(value); + if (this._textStyles.has(key)) { + this._textStyle = this._textStyles.get(key)!; + } else { + const textStyle = new SkiaCanvas.alphaSkia.AlphaSkiaTextStyle( + value.families, + value.weight === FontWeight.Bold ? 700 : 400, + value.isItalic + ); + this._textStyles.set(key, textStyle); + this._textStyle = textStyle; } + } - return this._typeFace!; + private textStyleKey(font: Font): string { + return [...font.families, font.weight.toString(), font.isItalic ? 'italic' : 'upright'].join('_'); } public constructor() { this._canvas = new SkiaCanvas.alphaSkia.AlphaSkiaCanvas(); - this.color = new Color(0, 0, 0, 0xff) + this.color = new Color(0, 0, 0, 0xff); } public destroy() { - using _ = this._canvas; + this._canvas[Symbol.dispose](); + for (const textStyle of this._textStyles.values()) { + textStyle[Symbol.dispose](); + } } public onRenderFinished(): unknown { @@ -150,13 +151,18 @@ export class SkiaCanvas implements ICanvas { public fillRect(x: number, y: number, w: number, h: number): void { if (w > 0) { - this._canvas.fillRect(((x * this._scale) | 0), ((y * this._scale) | 0), w * this._scale, h * this._scale); + this._canvas.fillRect((x * this._scale) | 0, (y * this._scale) | 0, w * this._scale, h * this._scale); } } public strokeRect(x: number, y: number, w: number, h: number): void { const blurOffset = this.lineWidth % 2 === 0 ? 0 : 0.5; - this._canvas.strokeRect(((x * this._scale) | 0) + blurOffset, ((y * this._scale) | 0) + blurOffset, w * this._scale, h * this._scale); + this._canvas.strokeRect( + ((x * this._scale) | 0) + blurOffset, + ((y * this._scale) | 0) + blurOffset, + w * this._scale, + h * this._scale + ); } public beginPath(): void { @@ -180,23 +186,22 @@ export class SkiaCanvas implements ICanvas { } public bezierCurveTo(cp1X: number, cp1Y: number, cp2X: number, cp2Y: number, x: number, y: number): void { - this._canvas.bezierCurveTo(cp1X * this._scale, cp1Y * this._scale, cp2X * this._scale, cp2Y * this._scale, x * this._scale, y * this._scale); + this._canvas.bezierCurveTo( + cp1X * this._scale, + cp1Y * this._scale, + cp2X * this._scale, + cp2Y * this._scale, + x * this._scale, + y * this._scale + ); } public fillCircle(x: number, y: number, radius: number): void { - this._canvas.fillCircle( - x * this._scale, - y * this._scale, - radius * this._scale, - ); + this._canvas.fillCircle(x * this._scale, y * this._scale, radius * this._scale); } public strokeCircle(x: number, y: number, radius: number): void { - this._canvas.strokeCircle( - x * this._scale, - y * this._scale, - radius * this._scale, - ); + this._canvas.strokeCircle(x * this._scale, y * this._scale, radius * this._scale); } public fill(): void { @@ -207,7 +212,6 @@ export class SkiaCanvas implements ICanvas { this._canvas.stroke(); } - public font: Font = new Font('Arial', 10, FontStyle.Plain); public textAlign: TextAlign = TextAlign.Left; public textBaseline: TextBaseline = TextBaseline.Top; @@ -220,7 +224,7 @@ export class SkiaCanvas implements ICanvas { } public fillText(text: string, x: number, y: number): void { - if (text.length == 0) { + if (text.length === 0) { return; } @@ -250,23 +254,28 @@ export class SkiaCanvas implements ICanvas { break; } - - this._canvas.fillText(text, this.getTypeFace(), this.font.size * this._scale, x * this._scale, y * this._scale, textAlign, textBaseline); + // NOTE: Avoiding sub-pixel text positions as they can lead to strange artifacts. + this._canvas.fillText( + text, + this._textStyle!, + this._font.size * this._scale, + (x * this._scale) | 0, + (y * this._scale) | 0, + textAlign, + textBaseline + ); } - /** - * TODO: extend alphaSkia to provide font metrics. - */ - private static readonly FontSizeToLineHeight = 1.2; - - private _initialMeasure = true; public measureText(text: string) { - // BUG: for some reason the very initial measure text in alphaSkia delivers wrong results, so we it twice - if(this._initialMeasure) { - this._canvas.measureText(text, this.getTypeFace(), this.font.size * this._scale); - this._initialMeasure = false; - } - return new TextMetrics(this._canvas.measureText(text, this.getTypeFace(), this.font.size * this._scale), this.font.size * this._scale * SkiaCanvas.FontSizeToLineHeight); + using metrics = this._canvas.measureText( + text, + this._textStyle!, + this._font.size, + SkiaCanvas.alphaSkia.AlphaSkiaTextAlign.Left, + SkiaCanvas.alphaSkia.AlphaSkiaTextBaseline.Alphabetic + ); + + return new MeasuredText(metrics.width, metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent); } public fillMusicFontSymbol( @@ -290,7 +299,7 @@ export class SkiaCanvas implements ICanvas { centerAtPosition?: boolean ): void { let s: string = ''; - for (let symbol of symbols) { + for (const symbol of symbols) { if (symbol !== MusicFontSymbol.None) { s += String.fromCharCode(symbol); } @@ -307,10 +316,13 @@ export class SkiaCanvas implements ICanvas { ): void { this._canvas.fillText( symbols, - SkiaCanvas.musicFont!, + SkiaCanvas.musicTextStyle!, Environment.MusicFontSize * this._scale * relativeScale, - x * this._scale, y * this._scale, - centerAtPosition ? SkiaCanvas.alphaSkia.AlphaSkiaTextAlign.Center : SkiaCanvas.alphaSkia.AlphaSkiaTextAlign.Left, + x * this._scale, + y * this._scale, + centerAtPosition + ? SkiaCanvas.alphaSkia.AlphaSkiaTextAlign.Center + : SkiaCanvas.alphaSkia.AlphaSkiaTextAlign.Left, SkiaCanvas.alphaSkia.AlphaSkiaTextBaseline.Alphabetic ); } @@ -322,4 +334,4 @@ export class SkiaCanvas implements ICanvas { public endRotate(): void { this._canvas.endRotate(); } -} \ No newline at end of file +} diff --git a/src/platform/svg/CssFontSvgCanvas.ts b/src/platform/svg/CssFontSvgCanvas.ts index e2741e434..84808e165 100644 --- a/src/platform/svg/CssFontSvgCanvas.ts +++ b/src/platform/svg/CssFontSvgCanvas.ts @@ -6,10 +6,6 @@ import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; * This SVG canvas renders the music symbols by adding a CSS class 'at' to all elements. */ export class CssFontSvgCanvas extends SvgCanvas { - public constructor() { - super(); - } - public fillMusicFontSymbol( x: number, y: number, @@ -31,7 +27,7 @@ export class CssFontSvgCanvas extends SvgCanvas { centerAtPosition?: boolean ): void { let s: string = ''; - for (let symbol of symbols) { + for (const symbol of symbols) { if (symbol !== MusicFontSymbol.None) { s += `&#${symbol};`; } @@ -60,7 +56,7 @@ export class CssFontSvgCanvas extends SvgCanvas { this.buffer += ` fill="${this.color.rgba}"`; } if (centerAtPosition) { - this.buffer += ' text-anchor="' + this.getSvgTextAlignment(TextAlign.Center) + '"'; + this.buffer += ` text-anchor="${this.getSvgTextAlignment(TextAlign.Center)}"`; } this.buffer += `>${symbols}`; } diff --git a/src/platform/svg/FontSizes.ts b/src/platform/svg/FontSizes.ts index 44b3f261e..c7f6ee678 100644 --- a/src/platform/svg/FontSizes.ts +++ b/src/platform/svg/FontSizes.ts @@ -1,7 +1,7 @@ import { FontStyle, FontWeight } from '@src/model/Font'; import { Environment } from '@src/Environment'; -import { TextMetrics } from '../ICanvas'; -import { WebPlatform } from '../javascript/WebPlatform'; +import { MeasuredText } from '@src/platform/ICanvas'; +import { WebPlatform } from '@src/platform/javascript/WebPlatform'; /** * Describes the sizes of a font for measuring purposes. @@ -41,21 +41,21 @@ export class FontSizes { return; } - if (!Environment.isRunningInWorker && Environment.webPlatform != WebPlatform.NodeJs) { - let canvas: HTMLCanvasElement = document.createElement('canvas'); - let measureContext: CanvasRenderingContext2D = canvas.getContext('2d')!; + if (!Environment.isRunningInWorker && Environment.webPlatform !== WebPlatform.NodeJs) { + const canvas: HTMLCanvasElement = document.createElement('canvas'); + const measureContext: CanvasRenderingContext2D = canvas.getContext('2d')!; const measureSize = 11; measureContext.font = `${measureSize}px ${family}`; const widths: number[] = []; let fullTxt = ''; for (let i: number = FontSizes.ControlChars; i < 255; i++) { - let s: string = String.fromCharCode(i); + const s: string = String.fromCharCode(i); fullTxt += s; const metrics = measureContext.measureText(s); widths.push(metrics.width); } - const heightMetrics = measureContext.measureText(fullTxt + 'ÄÖÜÁÈ'); + const heightMetrics = measureContext.measureText(`${fullTxt}ÄÖÜÁÈ`); const top = 0 - Math.abs(heightMetrics.fontBoundingBoxAscent); const bottom = 0 + Math.abs(heightMetrics.fontBoundingBoxDescent); @@ -75,9 +75,9 @@ export class FontSizes { size: number, style: FontStyle, weight: FontWeight - ): TextMetrics { + ): MeasuredText { let data: FontSizeDefinition; - let dataSize: number = 11; + const dataSize: number = 11; let family = families[0]; // default to first font // find a font which is maybe registered already @@ -102,16 +102,16 @@ export class FontSizes { let stringSize: number = 0; for (let i: number = 0; i < s.length; i++) { - let code: number = Math.min(data.characterWidths.length - 1, s.charCodeAt(i) - FontSizes.ControlChars); + const code: number = Math.min(data.characterWidths.length - 1, s.charCodeAt(i) - FontSizes.ControlChars); if (code >= 0) { stringSize += (data.characterWidths[code] * size) / dataSize; } } // add a small increase of size for spacing/kerning etc. - // we really need to improve the width calculation, maybe by using offscreencanvas? - factor *= 1.07; + // we really need to improve the width calculation, maybe by using offscreencanvas? + factor *= 1.07; - return new TextMetrics(stringSize * factor, size * data.fontSizeToHeight); + return new MeasuredText(stringSize * factor, size * data.fontSizeToHeight); } } diff --git a/src/platform/svg/SvgCanvas.ts b/src/platform/svg/SvgCanvas.ts index f2f439e3a..a7f757962 100644 --- a/src/platform/svg/SvgCanvas.ts +++ b/src/platform/svg/SvgCanvas.ts @@ -1,9 +1,9 @@ import { Color } from '@src/model/Color'; import { Font, FontStyle } from '@src/model/Font'; -import { ICanvas, TextAlign, TextBaseline, TextMetrics } from '@src/platform/ICanvas'; +import { type ICanvas, TextAlign, TextBaseline, MeasuredText } from '@src/platform/ICanvas'; import { FontSizes } from '@src/platform/svg/FontSizes'; -import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { Settings } from '@src/Settings'; +import type { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import type { Settings } from '@src/Settings'; /** * A canvas implementation storing SVG data @@ -59,8 +59,8 @@ export abstract class SvgCanvas implements ICanvas { public strokeRect(x: number, y: number, w: number, h: number): void { const blurOffset = (this.lineWidth * this.scale) % 2 === 0 ? 0 : 0.5; this.buffer += `'; + this.buffer += ``; } public endRotate(): void { diff --git a/src/rendering/BarRendererBase.ts b/src/rendering/BarRendererBase.ts index b2a60caf8..ee7656f11 100644 --- a/src/rendering/BarRendererBase.ts +++ b/src/rendering/BarRendererBase.ts @@ -1,29 +1,31 @@ -import { Bar } from '@src/model/Bar'; -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; +import type { Bar } from '@src/model/Bar'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; import { SimileMark } from '@src/model/SimileMark'; -import { Voice } from '@src/model/Voice'; -import { ICanvas } from '@src/platform/ICanvas'; +import { type Voice, VoiceSubElement } from '@src/model/Voice'; +import type { ICanvas } from '@src/platform/ICanvas'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; -import { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; import { LeftToRightLayoutingGlyphGroup } from '@src/rendering/glyphs/LeftToRightLayoutingGlyphGroup'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; -import { RenderStaff } from '@src/rendering/staves/RenderStaff'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; +import type { RenderStaff } from '@src/rendering/staves/RenderStaff'; import { BarBounds } from '@src/rendering/utils/BarBounds'; import { BarHelpers } from '@src/rendering/utils/BarHelpers'; import { Bounds } from '@src/rendering/utils/Bounds'; -import { MasterBarBounds } from '@src/rendering/utils/MasterBarBounds'; -import { RenderingResources } from '@src/RenderingResources'; -import { Settings } from '@src/Settings'; -import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; -import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; -import { InternalSystemsLayoutMode } from './layout/ScoreLayout'; -import { BeamDirection } from './utils/BeamDirection'; +import type { MasterBarBounds } from '@src/rendering/utils/MasterBarBounds'; +import type { RenderingResources } from '@src/RenderingResources'; +import type { Settings } from '@src/Settings'; +import type { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import { InternalSystemsLayoutMode } from '@src/rendering/layout/ScoreLayout'; +import type { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import { MultiBarRestBeatContainerGlyph } from '@src/rendering/MultiBarRestBeatContainerGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; /** * Lists the different position modes for {@link BarRendererBase.getNoteY} @@ -32,23 +34,23 @@ export enum NoteYPosition { /** * Gets the note y-position on top of the note stem or tab number. */ - TopWithStem, + TopWithStem = 0, /** * Gets the note y-position on top of the note head or tab number. */ - Top, + Top = 1, /** * Gets the note y-position on the center of the note head or tab number. */ - Center, + Center = 2, /** * Gets the note y-position on the bottom of the note head or tab number. */ - Bottom, + Bottom = 3, /** * Gets the note y-position on the bottom of the note stem or tab number. */ - BottomWithStem + BottomWithStem = 4 } /** @@ -58,15 +60,15 @@ export enum NoteXPosition { /** * Gets the note x-position on left of the note head or tab number. */ - Left, + Left = 0, /** * Gets the note x-position on the center of the note head or tab number. */ - Center, + Center = 1, /** * Gets the note x-position on the right of the note head or tab number. */ - Right + Right = 2 } /** @@ -90,20 +92,28 @@ export class BarRendererBase { if (!this.bar || !this.bar.nextBar) { return null; } - return this.scoreRenderer.layout!.getRendererForBar(this.staff.staveId, 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.staveId, this.bar.previousBar); + return this.scoreRenderer.layout!.getRendererForBar(this.staff.staffId, this.bar.previousBar); } public scoreRenderer: ScoreRenderer; public staff!: RenderStaff; public layoutingInfo!: BarLayoutingInfo; public bar: Bar; + public additionalMultiRestBars: Bar[] | null = null; + + public get lastBar(): Bar { + if (this.additionalMultiRestBars) { + return this.additionalMultiRestBars[this.additionalMultiRestBars.length - 1]; + } + return this.bar; + } public x: number = 0; public y: number = 0; @@ -128,6 +138,10 @@ export class BarRendererBase { */ public canWrap: boolean = true; + public get showMultiBarRest(): boolean { + return false; + } + public constructor(renderer: ScoreRenderer, bar: Bar) { this.scoreRenderer = renderer; this.bar = bar; @@ -162,7 +176,7 @@ export class BarRendererBase { public scaleToWidth(width: number): void { // preBeat and postBeat glyphs do not get resized - let containerWidth: number = width - this._preBeatGlyphs.width - this._postBeatGlyphs.width; + const containerWidth: number = width - this._preBeatGlyphs.width - this._postBeatGlyphs.width; for (const container of this._voiceContainers.values()) { container.scaleToWidth(containerWidth); } @@ -194,7 +208,7 @@ export class BarRendererBase { return this.staff.system.staves.length > 1 ? this.bar.masterBar.displayWidth : this.bar.displayWidth; } - private _wasFirstOfLine: boolean = false; + protected wasFirstOfLine: boolean = false; public get isFirstOfLine(): boolean { return this.index === 0; @@ -205,20 +219,20 @@ export class BarRendererBase { } public registerLayoutingInfo(): void { - let info: BarLayoutingInfo = this.layoutingInfo; - let preSize: number = this._preBeatGlyphs.width; + 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); - let x: number = container.x + container.width; + const x: number = container.x + container.width; if (postBeatStart < x) { postBeatStart = x; } } - let postSize: number = this._postBeatGlyphs.width; + const postSize: number = this._postBeatGlyphs.width; if (info.postBeatSize < postSize) { info.postBeatSize = postSize; } @@ -239,7 +253,7 @@ export class BarRendererBase { for (const c of this._voiceContainers.values()) { c.x = this._preBeatGlyphs.x + this._preBeatGlyphs.width; c.applyLayoutingInfo(this.layoutingInfo); - let newEnd: number = c.x + c.width; + const newEnd: number = c.x + c.width; if (voiceEnd < newEnd) { voiceEnd = newEnd; } @@ -256,7 +270,7 @@ export class BarRendererBase { const fixedBarWidth = this.barDisplayWidth; if ( fixedBarWidth > 0 && - this.scoreRenderer.layout!.systemsLayoutMode == InternalSystemsLayoutMode.FromModelWithWidths + this.scoreRenderer.layout!.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithWidths ) { this.width = fixedBarWidth; this.computedWidth = fixedBarWidth; @@ -320,9 +334,9 @@ export class BarRendererBase { this._postBeatGlyphs = new LeftToRightLayoutingGlyphGroup(); this._postBeatGlyphs.renderer = this; for (let i: number = 0; i < this.bar.voices.length; i++) { - let voice: Voice = this.bar.voices[i]; + const voice: Voice = this.bar.voices[i]; if (this.hasVoiceContainer(voice)) { - let c: VoiceContainerGlyph = new VoiceContainerGlyph(0, 0, voice); + const c: VoiceContainerGlyph = new VoiceContainerGlyph(0, 0, voice); c.renderer = this; this._voiceContainers.set(this.bar.voices[i].index, c); } @@ -331,7 +345,15 @@ export class BarRendererBase { this.canWrap = false; } this.createPreBeatGlyphs(); - this.createBeatGlyphs(); + + // multibar rest + if (this.additionalMultiRestBars) { + const container = new MultiBarRestBeatContainerGlyph(this.getVoiceContainer(this.bar.voices[0])!); + this.addBeatGlyph(container); + } else { + this.createBeatGlyphs(); + } + this.createPostBeatGlyphs(); this.updateSizes(); @@ -346,19 +368,22 @@ export class BarRendererBase { } protected hasVoiceContainer(voice: Voice): boolean { - return !voice.isEmpty || voice.index === 0; + if (this.additionalMultiRestBars || voice.index === 0) { + return true; + } + return !voice.isEmpty; } protected updateSizes(): void { this.staff.registerStaffTop(this.topPadding); this.staff.registerStaffBottom(this.height - this.bottomPadding); - let voiceContainers: Map = this._voiceContainers; - let beatGlyphsStart: number = this.beatGlyphsStart; + 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(); - let x: number = c.x + c.width; + const x: number = c.x + c.width; if (postBeatStart < x) { postBeatStart = x; } @@ -399,12 +424,14 @@ export class BarRendererBase { public paint(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); + for (const c of this._voiceContainers.values()) { - canvas.color = c.voice.index === 0 ? this.resources.mainGlyphColor : this.resources.secondaryGlyphColor; c.paint(cx + this.x, cy + this.y, canvas); } + canvas.color = this.resources.mainGlyphColor; this._postBeatGlyphs.paint(cx + this.x, cy + this.y, canvas); } @@ -420,7 +447,7 @@ export class BarRendererBase { } public buildBoundingsLookup(masterBarBounds: MasterBarBounds, cx: number, cy: number): void { - let barBounds: BarBounds = new BarBounds(); + const barBounds: BarBounds = new BarBounds(); barBounds.bar = this.bar; barBounds.visualBounds = new Bounds(); barBounds.visualBounds.x = cx + this.x; @@ -436,10 +463,10 @@ export class BarRendererBase { masterBarBounds.addBar(barBounds); for (const [index, c] of this._voiceContainers) { - let isEmptyBar: boolean = this.bar.isEmpty && index === 0; + 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++) { - let bc: BeatContainerGlyph = c.beatGlyphs[i]; + const bc: BeatContainerGlyph = c.beatGlyphs[i]; bc.buildBoundingsLookup(barBounds, cx + this.x + c.x, cy + this.y + c.y, isEmptyBar); } } @@ -451,7 +478,7 @@ export class BarRendererBase { } protected createPreBeatGlyphs(): void { - this._wasFirstOfLine = this.isFirstOfLine; + this.wasFirstOfLine = this.isFirstOfLine; } protected createBeatGlyphs(): void { @@ -479,7 +506,7 @@ export class BarRendererBase { } public getBeatX(beat: Beat, requestedPosition: BeatXPosition = BeatXPosition.PreNotes): number { - let container = this.getBeatContainer(beat); + const container = this.getBeatContainer(beat); if (container) { switch (requestedPosition) { case BeatXPosition.PreNotes: @@ -512,7 +539,7 @@ export class BarRendererBase { } public getNoteX(note: Note, requestedPosition: NoteXPosition): number { - let container = this.getBeatContainer(note.beat); + const container = this.getBeatContainer(note.beat); if (container) { return ( container.voiceContainer.x + @@ -525,17 +552,17 @@ export class BarRendererBase { } public getNoteY(note: Note, requestedPosition: NoteYPosition): number { - let beat = this.getOnNotesGlyphForBeat(note.beat); + const beat = this.getOnNotesGlyphForBeat(note.beat); if (beat) { return beat.getNoteY(note, requestedPosition); } - return NaN; + return Number.NaN; } public reLayout(): void { // 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.wasFirstOfLine && !this.isFirstOfLine) || (!this.wasFirstOfLine && this.isFirstOfLine)) { this.recreatePreBeatGlyphs(); } this.updateSizes(); @@ -549,8 +576,11 @@ export class BarRendererBase { } protected paintSimileMark(cx: number, cy: number, canvas: ICanvas): void { + using _ = ElementStyleHelper.voice(canvas, VoiceSubElement.Glyphs, this.bar.voices[0], true); + switch (this.bar.simileMark) { case SimileMark.Simple: + canvas.beginGroup(BeatContainerGlyph.getGroupId(this.bar.voices[0].beats[0])); canvas.fillMusicFontSymbol( cx + this.x + (this.width - 20) / 2, cy + this.y + this.height / 2, @@ -558,8 +588,12 @@ export class BarRendererBase { MusicFontSymbol.Repeat1Bar, false ); + canvas.endGroup(); break; case SimileMark.SecondOfDouble: + canvas.beginGroup(BeatContainerGlyph.getGroupId(this.bar.voices[0].beats[0])); + canvas.beginGroup(BeatContainerGlyph.getGroupId(this.bar.previousBar!.voices[0].beats[0])); + canvas.fillMusicFontSymbol( cx + this.x - 28 / 2, cy + this.y + this.height / 2, @@ -567,6 +601,10 @@ export class BarRendererBase { MusicFontSymbol.Repeat2Bars, false ); + + canvas.endGroup(); + canvas.endGroup(); + break; } } diff --git a/src/rendering/BarRendererFactory.ts b/src/rendering/BarRendererFactory.ts index 14771baae..7cd63f4ff 100644 --- a/src/rendering/BarRendererFactory.ts +++ b/src/rendering/BarRendererFactory.ts @@ -1,9 +1,9 @@ -import { Bar } from '@src/model/Bar'; -import { Staff } from '@src/model/Staff'; -import { Track } from '@src/model/Track'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { RenderStaff } from './staves/RenderStaff'; +import type { Bar } from '@src/model/Bar'; +import type { Staff } from '@src/model/Staff'; +import type { Track } from '@src/model/Track'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { RenderStaff } from '@src/rendering/staves/RenderStaff'; /** * This is the base public class for creating factories providing BarRenderers @@ -15,8 +15,8 @@ export abstract class BarRendererFactory { public hideOnPercussionTrack: boolean = false; public abstract get staffId(): string; - public abstract getStaffPaddingTop(staff:RenderStaff): number; - public abstract getStaffPaddingBottom(staff:RenderStaff): number; + public abstract getStaffPaddingTop(staff: RenderStaff): number; + public abstract getStaffPaddingBottom(staff: RenderStaff): number; public canCreate(track: Track, staff: Staff): boolean { return !this.hideOnPercussionTrack || !staff.isPercussion; diff --git a/src/rendering/BeatXPosition.ts b/src/rendering/BeatXPosition.ts index e5b997f80..47d464d7d 100644 --- a/src/rendering/BeatXPosition.ts +++ b/src/rendering/BeatXPosition.ts @@ -5,26 +5,26 @@ export enum BeatXPosition { /** * Gets the pre-notes position which is located before the accidentals */ - PreNotes, + PreNotes = 0, /** * Gets the on-notes position which is located after the accidentals but before the note heads. */ - OnNotes, + OnNotes = 1, /** * Gets the middle-notes position which is located after in the middle the note heads. */ - MiddleNotes, + MiddleNotes = 2, /** * Gets position of the stem for this beat */ - Stem, + Stem = 3, /** * Get the post-notes position which is located at after the note heads. */ - PostNotes, + PostNotes = 4, /** * Get the end-beat position which is located at the end of the beat. This position is almost * equal to the pre-notes position of the next beat. */ - EndBeat + EndBeat = 5 } diff --git a/src/rendering/EffectBand.ts b/src/rendering/EffectBand.ts index 876bd55c8..bfbac60ea 100644 --- a/src/rendering/EffectBand.ts +++ b/src/rendering/EffectBand.ts @@ -1,13 +1,14 @@ -import { Beat } from '@src/model/Beat'; -import { Voice } from '@src/model/Voice'; -import { ICanvas } from '@src/platform/ICanvas'; -import { EffectBandSlot } from '@src/rendering/EffectBandSlot'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; +import type { Voice } from '@src/model/Voice'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { EffectBandSlot } from '@src/rendering/EffectBandSlot'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectBarRenderer } from '@src/rendering/EffectBarRenderer'; -import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectBarRenderer } from '@src/rendering/EffectBarRenderer'; +import type { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { Glyph } from '@src/rendering/glyphs/Glyph'; -import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; +import type { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; export class EffectBand extends Glyph { private _uniqueEffectGlyphs: EffectGlyph[][] = []; @@ -64,7 +65,7 @@ export class EffectBand extends Glyph { break; } } - let glyph: EffectGlyph = this.createOrResizeGlyph(this.info.sizingMode, beat); + const glyph: EffectGlyph = this.createOrResizeGlyph(this.info.sizingMode, beat); if (glyph.height > this.height) { this.height = glyph.height; this.originalHeight = glyph.height; @@ -99,13 +100,13 @@ export class EffectBand extends Glyph { return g; case EffectBarGlyphSizing.GroupedOnBeat: case EffectBarGlyphSizing.GroupedOnBeatToEnd: - let singleSizing: EffectBarGlyphSizing = + const singleSizing: EffectBarGlyphSizing = sizing === EffectBarGlyphSizing.GroupedOnBeat ? EffectBarGlyphSizing.SingleOnBeat : EffectBarGlyphSizing.SingleOnBeatToEnd; if (b.index > 0 || this.renderer.index > 0) { // check if the previous beat also had this effect - let prevBeat = b.previousBeat!; + const prevBeat = b.previousBeat!; if (this.info.shouldCreateGlyph(this.renderer.settings, prevBeat)) { // first load the effect bar renderer and glyph let prevEffect: EffectGlyph | null = null; @@ -114,12 +115,12 @@ 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. - let previousRenderer: EffectBarRenderer = this.renderer + const previousRenderer: EffectBarRenderer = this.renderer .previousRenderer as EffectBarRenderer; - let previousBand = previousRenderer.getBand(prevBeat.voice, this.info.effectId); + const previousBand = previousRenderer.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) { - let voiceGlyphs: Map = + const voiceGlyphs: Map = previousBand._effectGlyphs[prevBeat.voice.index]; if (voiceGlyphs.has(prevBeat.index)) { prevEffect = voiceGlyphs.get(prevBeat.index)!; @@ -129,7 +130,7 @@ export class EffectBand extends Glyph { // if the effect cannot be expanded, create a new glyph // in case of expansion also create a new glyph, but also link the glyphs together // so for rendering it might be expanded. - let newGlyph: EffectGlyph = this.createOrResizeGlyph(singleSizing, b); + const newGlyph: EffectGlyph = this.createOrResizeGlyph(singleSizing, b); if (prevEffect && this.info.canExpand(prevBeat, b)) { // link glyphs prevEffect.nextGlyph = newGlyph; @@ -158,9 +159,10 @@ export class EffectBand extends Glyph { // canvas.color = c; for (let i: number = 0, j: number = this._uniqueEffectGlyphs.length; i < j; i++) { - let v: EffectGlyph[] = this._uniqueEffectGlyphs[i]; + const v: EffectGlyph[] = this._uniqueEffectGlyphs[i]; for (let k: number = 0, l: number = v.length; k < l; k++) { - let g: EffectGlyph = v[k]; + const g: EffectGlyph = v[k]; + using _ = ElementStyleHelper.beat(canvas, BeatSubElement.Effects, g.beat!, false); g.paint(cx + this.x, cy + this.y, canvas); } } @@ -175,11 +177,11 @@ export class EffectBand extends Glyph { } private alignGlyph(sizing: EffectBarGlyphSizing, beat: Beat): void { - let g: EffectGlyph = this._effectGlyphs[beat.voice.index].get(beat.index)!; - let container: BeatContainerGlyph = this.renderer.getBeatContainer(beat)!; - + 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 - + switch (sizing) { case EffectBarGlyphSizing.SinglePreBeat: // shift to the start using the biggest pre-beat size of the respective beat diff --git a/src/rendering/EffectBandSizingInfo.ts b/src/rendering/EffectBandSizingInfo.ts index 1f0a2fa0c..562ca169e 100644 --- a/src/rendering/EffectBandSizingInfo.ts +++ b/src/rendering/EffectBandSizingInfo.ts @@ -1,4 +1,4 @@ -import { EffectBand } from '@src/rendering/EffectBand'; +import type { EffectBand } from '@src/rendering/EffectBand'; import { EffectBandSlot } from '@src/rendering/EffectBandSlot'; export class EffectBandSizingInfo { @@ -13,25 +13,25 @@ export class EffectBandSizingInfo { public getOrCreateSlot(band: EffectBand): EffectBandSlot { // first check preferrable slot depending on type if (this._effectSlot.has(band.info.effectId)) { - let slot: EffectBandSlot = this._effectSlot.get(band.info.effectId)!; + const slot: EffectBandSlot = this._effectSlot.get(band.info.effectId)!; if (slot.canBeUsed(band)) { return slot; } } // find any slot that can be used - for (let slot of this.slots) { + for (const slot of this.slots) { if (slot.canBeUsed(band)) { return slot; } } // create a new slot if required - let newSlot: EffectBandSlot = new EffectBandSlot(); + const newSlot: EffectBandSlot = new EffectBandSlot(); this.slots.push(newSlot); return newSlot; } public register(effectBand: EffectBand): void { - let freeSlot: EffectBandSlot = this.getOrCreateSlot(effectBand); + const freeSlot: EffectBandSlot = this.getOrCreateSlot(effectBand); freeSlot.update(effectBand); this._effectSlot.set(effectBand.info.effectId, freeSlot); } diff --git a/src/rendering/EffectBandSlot.ts b/src/rendering/EffectBandSlot.ts index 95dbb49e1..bd63c7268 100644 --- a/src/rendering/EffectBandSlot.ts +++ b/src/rendering/EffectBandSlot.ts @@ -1,5 +1,5 @@ -import { Beat } from '@src/model/Beat'; -import { EffectBand } from '@src/rendering/EffectBand'; +import type { Beat } from '@src/model/Beat'; +import type { EffectBand } from '@src/rendering/EffectBand'; export class EffectBandSlotShared { public uniqueEffectId: string | null = null; diff --git a/src/rendering/EffectBarGlyphSizing.ts b/src/rendering/EffectBarGlyphSizing.ts index a08d38388..4700a40ca 100644 --- a/src/rendering/EffectBarGlyphSizing.ts +++ b/src/rendering/EffectBarGlyphSizing.ts @@ -6,32 +6,32 @@ export enum EffectBarGlyphSizing { * The effect glyph is placed above the pre-beat glyph which is before * the actual note in the area where also accidentals are renderered. */ - SinglePreBeat, + SinglePreBeat = 0, /** * The effect glyph is placed above the on-beat glyph which is where * the actual note head glyphs are placed. */ - SingleOnBeat, + SingleOnBeat = 1, /** * The effect glyph is placed above the on-beat glyph which is where * the actual note head glyphs are placed. The glyph will size to the end of * the applied beat. */ - SingleOnBeatToEnd, + SingleOnBeatToEnd = 2, /** * The effect glyph is placed above the on-beat glyph and expaded to the * on-beat position of the next beat. */ - GroupedOnBeat, + GroupedOnBeat = 3, /** * The effect glyph is placed above the on-beat glyph and expaded to the * on-beat position of the next beat. The glyph will size to the end of * the applied beat. */ - GroupedOnBeatToEnd, - + GroupedOnBeatToEnd = 4, + /** * The effect glyph is placed on the whole bar covering the whole width */ - FullBar + FullBar = 5 } diff --git a/src/rendering/EffectBarRenderer.ts b/src/rendering/EffectBarRenderer.ts index 161d5ea50..fcb650fac 100644 --- a/src/rendering/EffectBarRenderer.ts +++ b/src/rendering/EffectBarRenderer.ts @@ -1,14 +1,14 @@ -import { Bar } from '@src/model/Bar'; -import { Voice } from '@src/model/Voice'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { Bar } from '@src/model/Bar'; +import type { Voice } from '@src/model/Voice'; +import type { ICanvas } from '@src/platform/ICanvas'; import { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBand } from '@src/rendering/EffectBand'; import { EffectBandSizingInfo } from '@src/rendering/EffectBandSizingInfo'; import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; import { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; -import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; /** * This renderer is responsible for displaying effects above or below the other staves @@ -47,9 +47,9 @@ export class EffectBarRenderer extends BarRendererBase { return false; } let y: number = 0; - for (let slot of this.sizingInfo.slots) { + for (const slot of this.sizingInfo.slots) { slot.shared.y = y; - for (let band of slot.bands) { + for (const band of slot.bands) { band.y = y; band.height = slot.shared.height; } @@ -66,12 +66,12 @@ export class EffectBarRenderer extends BarRendererBase { const result = !super.applyLayoutingInfo(); // we create empty slots for the same group if (this.index > 0) { - let previousRenderer: EffectBarRenderer = this.previousRenderer as EffectBarRenderer; + const previousRenderer: EffectBarRenderer = this.previousRenderer as EffectBarRenderer; this.sizingInfo = previousRenderer.sizingInfo; } else { this.sizingInfo = new EffectBandSizingInfo(); } - for (let effectBand of this._bands) { + for (const effectBand of this._bands) { effectBand.resetHeight(); effectBand.alignGlyphs(); if (!effectBand.isEmpty) { @@ -85,7 +85,7 @@ export class EffectBarRenderer extends BarRendererBase { public override scaleToWidth(width: number): void { super.scaleToWidth(width); - for (let effectBand of this._bands) { + for (const effectBand of this._bands) { effectBand.alignGlyphs(); } } @@ -93,23 +93,19 @@ export class EffectBarRenderer extends BarRendererBase { protected override createBeatGlyphs(): void { this._bands = []; this._bandLookup = new Map(); - for (let voice of this.bar.voices) { + for (const voice of this.bar.voices) { if (this.hasVoiceContainer(voice)) { - for (let info of this._infos) { - let band: EffectBand = new EffectBand(voice, info); + 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); + this._bandLookup.set(`${voice.index}.${info.effectId}`, band); } } } - for (let voice of this.bar.voices) { - if (this.hasVoiceContainer(voice)) { - this.createVoiceGlyphs(voice); - } - } - for (let effectBand of this._bands) { + super.createBeatGlyphs(); + for (const effectBand of this._bands) { if (effectBand.isLinkedToPrevious) { this.isLinkedToPrevious = true; } @@ -117,14 +113,14 @@ export class EffectBarRenderer extends BarRendererBase { } protected override createVoiceGlyphs(v: Voice): void { - for (let b of v.beats) { + for (const b of v.beats) { // we create empty glyphs as alignment references and to get the // effect bar sized - let container: BeatContainerGlyph = new BeatContainerGlyph(b, this.getVoiceContainer(v)!); + const container: BeatContainerGlyph = new BeatContainerGlyph(b, this.getVoiceContainer(v)!); container.preNotes = new BeatGlyphBase(); container.onNotes = new BeatOnNoteGlyphBase(); this.addBeatGlyph(container); - for (let effectBand of this._bands) { + for (const effectBand of this._bands) { effectBand.createGlyph(b); } } @@ -135,7 +131,7 @@ export class EffectBarRenderer extends BarRendererBase { // canvas.color = new Color(255, 0, 0, 100); // canvas.fillRect(cx + this.x, cy + this.y, this.width, this.height); - for (let effectBand of this._bands) { + for (const effectBand of this._bands) { canvas.color = effectBand.voice.index === 0 ? this.resources.mainGlyphColor : this.resources.secondaryGlyphColor; if (!effectBand.isEmpty) { @@ -145,7 +141,7 @@ export class EffectBarRenderer extends BarRendererBase { } public getBand(voice: Voice, effectId: string): EffectBand | null { - let id: string = voice.index + '.' + effectId; + const id: string = `${voice.index}.${effectId}`; if (this._bandLookup.has(id)) { return this._bandLookup.get(id)!; } diff --git a/src/rendering/EffectBarRendererFactory.ts b/src/rendering/EffectBarRendererFactory.ts index 6d38d1914..d7550886d 100644 --- a/src/rendering/EffectBarRendererFactory.ts +++ b/src/rendering/EffectBarRendererFactory.ts @@ -1,11 +1,12 @@ -import { Bar } from '@src/model/Bar'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Bar } from '@src/model/Bar'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { BarRendererFactory } from '@src/rendering/BarRendererFactory'; import { EffectBarRenderer } from '@src/rendering/EffectBarRenderer'; -import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { RenderStaff } from './staves/RenderStaff'; -import { Staff, Track } from '@src/model'; +import type { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { RenderStaff } from '@src/rendering/staves/RenderStaff'; +import type { Staff } from '@src/model/Staff'; +import type { Track } from '@src/model/Track'; export class EffectBarRendererFactory extends BarRendererFactory { public infos: EffectBarRendererInfo[]; @@ -14,7 +15,7 @@ export class EffectBarRendererFactory extends BarRendererFactory { return this._staffId; } - public shouldShow: ((track:Track, staff:Staff) => boolean) | null; + public shouldShow: ((track: Track, staff: Staff) => boolean) | null; public override getStaffPaddingTop(staff: RenderStaff): number { return staff.system.layout.renderer.settings.display.effectStaffPaddingTop; @@ -24,7 +25,11 @@ export class EffectBarRendererFactory extends BarRendererFactory { return staff.system.layout.renderer.settings.display.effectStaffPaddingBottom; } - public constructor(staffId: string, infos: EffectBarRendererInfo[], shouldShow: ((track:Track, staff:Staff) => boolean) | null = null) { + public constructor( + staffId: string, + infos: EffectBarRendererInfo[], + shouldShow: ((track: Track, staff: Staff) => boolean) | null = null + ) { super(); this.infos = infos; this._staffId = staffId; @@ -38,7 +43,6 @@ export class EffectBarRendererFactory extends BarRendererFactory { return super.canCreate(track, staff) && (!shouldShow || shouldShow(track, staff)); } - public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { return new EffectBarRenderer( renderer, diff --git a/src/rendering/EffectBarRendererInfo.ts b/src/rendering/EffectBarRendererInfo.ts index 872c3998c..8add3bd69 100644 --- a/src/rendering/EffectBarRendererInfo.ts +++ b/src/rendering/EffectBarRendererInfo.ts @@ -1,9 +1,9 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; -import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; -import { Settings } from '@src/Settings'; -import { NotationElement } from '@src/NotationSettings'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { Settings } from '@src/Settings'; +import type { NotationElement } from '@src/NotationSettings'; /** * A classes inheriting from this base can provide the @@ -14,7 +14,7 @@ export abstract class EffectBarRendererInfo { * Gets the unique effect name for this effect. (Used for grouping) */ public get effectId(): string { - return this.notationElement.toString(); + return this.notationElement.toString(); } /** diff --git a/src/rendering/IScoreRenderer.ts b/src/rendering/IScoreRenderer.ts index b220d5917..9fa0440e6 100644 --- a/src/rendering/IScoreRenderer.ts +++ b/src/rendering/IScoreRenderer.ts @@ -1,8 +1,10 @@ -import { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; -import { Score } from '@src/model/Score'; -import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; -import { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; -import { Settings } from '@src/Settings'; +import type { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; +import type { Score } from '@src/model/Score'; +// biome-ignore lint/correctness/noUnusedImports: https://github.com/biomejs/biome/issues/4677 +import type { IUiFacade } from '@src/platform/IUiFacade'; +import type { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; +import type { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; +import type { Settings } from '@src/Settings'; /** * Represents the public interface of the component that can render scores. @@ -14,17 +16,36 @@ export interface IScoreRenderer { readonly boundsLookup: BoundsLookup | null; /** - * Gets or sets the width of the score to be rendered. + * The width of the rendered score. + * @remarks + * For layouts that grow from top to bottom (like `page`), it is required to specify a width for the renderer. + * The renderer will fit then the bars into this area for rendering. The alphaTab API object uses a link to the + * graphical user interface via a {@link IUiFacade} to get the available width for rendering. When using the low-level APIs + * this width must be specified manually. + * + * For layouts that grow from left to right the width and height are usually calculated automatically based on + * the contents. + * @since 0.9.6 */ width: number; /** - * Initiates a full re-rendering of the score using the current settings. + * Initiates a re-rendering of the current setup. + * @since 0.9.6 */ render(): void; /** * Initiates a resize-optimized re-rendering of the score using the current settings. + * @remarks + * This method can be used if only re-fitting of the score into a new width should be done. + * alphaTab internally keeps all the information about the music notation elements like + * where they are placed and how they are connected. This is a rather expensive operation + * but it is only required to be done once. + * + * In case the UI is resized, this method can be used to trigger a rearrangement of the existing elements + * into the newly available space. + * @since 0.9.6 */ resizeRender(): void; @@ -32,54 +53,114 @@ 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. + * @since 0.9.6 */ renderScore(score: Score | null, trackIndexes: number[] | null): void; /** - * Initiates the rendering of a partial render result which the renderer - * should have layed out already. + * Requests the rendering of a chunk which was layed out before. * @param resultId the result ID as provided by the {@link partialLayoutFinished} event. + * @remarks + * This method initiates the rendering of a layed out chunk advertised through {@link partialLayoutFinished} + * @since 1.2.3 */ renderResult(resultId: string): void; /** * Updates the settings to the given object. - * @param settings + * @remarks + * This method updates the settings to the given object. On some platforms like JavaScript + * the settings object will need to be passed on to the corresponding worker to be really updated. + * It is recommended to make this call after updating any properties of the settings object to ensure + * it is really passed on to all components. + * + * This method will not trigger automatically any re-rendering. + * @since 0.9.6 */ updateSettings(settings: Settings): void; /** - * Destroys the renderer. + * Destroys the renderer and all related components. + * @remarks + * This method destroys the full renderer and by this closes all potentially opened + * contexts and shuts down any worker. + * + * If you dynamically create/destroy renderers it is recommended to always call this method + * to ensure all resources are leaked. + + * @since 0.9.6 */ destroy(): void; /** - * Occurs before the rendering of the tracks starts. + * Occurs before the rendering of the tracks starts + * @remarks + * This event is fired when the rendering of the whole music sheet is starting. All + * preparations are completed and the layout and render sequence is about to start. + * + * The provided boolean indicates the rendering is triggered from a resize + * + * @eventProperty + * @since 0.9.4 */ readonly preRender: IEventEmitterOfT; /** - * Occurs after the rendering of the tracks finished. + * This event is fired when the rendering of the whole music sheet is finished. + * @remarks + * This event is fired when the rendering of the whole music sheet is finished from the render engine side. There might be still tasks open for + * the display component to visually display the rendered components when this event is notified. + * @eventProperty + * @since 0.9.4 */ readonly renderFinished: IEventEmitterOfT; /** * Occurs whenever a part of the whole music sheet is rendered and can be displayed. + * @remarks + * AlphaTab does not render the whole music sheet into a single canvas but rather + * splits it down into smaller chunks. This allows faster display of results to the user + * and avoids issues related to browser restrictions (like maximum canvas sizes). + * + * This event is fired whenever one chunk of the music sheet is fully rendered. + * + * {@since 1.2.3} the rendering of a chunk needs to be requested via the {@link renderResult} method after + * a chunk was advertised through the {@link partialLayoutFinished}. + * @eventProperty + * @since 1.2.3 */ readonly partialRenderFinished: IEventEmitterOfT; /** * Occurs whenever a part of the whole music sheet is layed out but not yet rendered. + * @remarks + * AlphaTab does not render the whole music sheet into a single canvas but rather + * splits it down into smaller chunks. This allows faster display of results to the user + * and avoids issues related to browser restrictions (like maximum canvas sizes). + * + * This event is fired whenever one chunk of the music sheet was fully layed out. + * @eventProperty + * @since 1.2.3 */ readonly partialLayoutFinished: IEventEmitterOfT; /** - * Occurs when the whole rendering and layout process finished. + * This event is fired when the rendering of the whole music sheet is finished, and all handlers of {@link renderFinished} ran. + * @remarks + * This event is fired when the rendering of the whole music sheet is finished, and all handlers of {@link renderFinished} ran. When this + * handlers are called, the whole rendering and display pipeline is completed. + * @eventProperty + * @since 0.9.4 */ readonly postRenderFinished: IEventEmitter; /** - * Occurs whenever an error happens. + * This event is fired when an error within alphatab occurred. + * @remarks + * This event is fired when an error within alphatab occurred. Use this event as global error handler to show errors + * to end-users. Due to the asynchronous nature of alphaTab, no call to the API will directly throw an error if it fails. + * Instead a signal to this error handlers will be sent. + * @since 0.9.4 */ readonly error: IEventEmitterOfT; } diff --git a/src/rendering/LineBarRenderer.ts b/src/rendering/LineBarRenderer.ts index b65341b5f..53a9d97f7 100644 --- a/src/rendering/LineBarRenderer.ts +++ b/src/rendering/LineBarRenderer.ts @@ -1,20 +1,21 @@ -import { Bar, Beat, Duration, GraceType, TupletGroup } from '@src/model'; -import { BarRendererBase } from './BarRendererBase'; -import { ScoreRenderer } from './ScoreRenderer'; -import { ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; -import { SpacingGlyph } from './glyphs/SpacingGlyph'; -import { BeamingHelper } from './utils/BeamingHelper'; -import { BeamDirection } from './utils/BeamDirection'; +import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import { type ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; +import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; +import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { NotationMode } from '@src/NotationSettings'; -import { FlagGlyph } from './glyphs/FlagGlyph'; -import { NoteHeadGlyph } from './glyphs/NoteHeadGlyph'; +import { FlagGlyph } from '@src/rendering/glyphs/FlagGlyph'; +import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; import { ModelUtils } from '@src/model/ModelUtils'; -import { RepeatOpenGlyph } from './glyphs/RepeatOpenGlyph'; -import { BarSeperatorGlyph } from './glyphs/BarSeperatorGlyph'; -import { RepeatCloseGlyph } from './glyphs/RepeatCloseGlyph'; -import { RepeatCountGlyph } from './glyphs/RepeatCountGlyph'; -import { BarNumberGlyph } from './glyphs/BarNumberGlyph'; -import { BeatBeamingMode } from '@src/model/Beat'; +import { BarLineGlyph } from '@src/rendering/glyphs/BarLineGlyph'; +import { RepeatCountGlyph } from '@src/rendering/glyphs/RepeatCountGlyph'; +import { BarNumberGlyph } from '@src/rendering/glyphs/BarNumberGlyph'; +import { type Beat, BeatBeamingMode, type BeatSubElement } from '@src/model/Beat'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import type { BarSubElement } from '@src/model/Bar'; +import { Duration } from '@src/model/Duration'; +import { GraceType } from '@src/model/GraceType'; +import type { TupletGroup } from '@src/model/TupletGroup'; /** * This is a base class for any bar renderer which renders music notation on a staff @@ -28,10 +29,6 @@ export abstract class LineBarRenderer extends BarRendererBase { private _startSpacing = false; protected tupletSize: number = 0; - public constructor(renderer: ScoreRenderer, bar: Bar) { - super(renderer, bar); - } - public get lineOffset(): number { return this.lineSpacing + 1; } @@ -73,8 +70,8 @@ export abstract class LineBarRenderer extends BarRendererBase { } protected updateFirstLineY() { - let fullLineHeight = this.lineOffset * (this.heightLineCount - 1); - let actualLineHeight = (this.drawnLineCount - 1) * this.lineOffset; + const fullLineHeight = this.lineOffset * (this.heightLineCount - 1); + const actualLineHeight = (this.drawnLineCount - 1) * this.lineOffset; this.firstLineY = this.topPadding + (fullLineHeight - actualLineHeight) / 2; } @@ -96,13 +93,19 @@ export abstract class LineBarRenderer extends BarRendererBase { // private static readonly Random Random = new Random(); protected override paintBackground(cx: number, cy: number, canvas: ICanvas): void { super.paintBackground(cx, cy, canvas); - const res = this.resources; // canvas.color = Color.random(100); // canvas.fillRect(cx + this.x, cy + this.y, this.width, this.height); // // draw string lines // - canvas.color = res.staffLineColor; + + this.paintStaffLines(cx, cy, canvas); + + this.paintSimileMark(cx, cy, canvas); + } + + private paintStaffLines(cx: number, cy: number, canvas: ICanvas) { + using _ = ElementStyleHelper.bar(canvas, this.staffLineBarSubElement, this.bar, true); // collect tab note position for spaces const spaces: Float32Array[][] = []; @@ -110,7 +113,10 @@ export abstract class LineBarRenderer extends BarRendererBase { spaces.push([]); } - this.collectSpaces(spaces); + // on multibar rest glyphs we don't have spaces as they are empty + if (!this.additionalMultiRestBars) { + this.collectSpaces(spaces); + } // if we have multiple voices we need to sort by X-position, otherwise have a wild mix in the list // but painting relies on ascending X-position @@ -120,11 +126,16 @@ 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 + const lineWidth = Math.ceil(this.width); + for (let i: number = 0; i < this.drawnLineCount; i++) { const lineY = this.getLineY(i); let lineX: number = 0; - for (let line of spaces[i]) { + for (const line of spaces[i]) { canvas.fillRect( cx + this.x + lineX, (cy + this.y + lineY) | 0, @@ -136,13 +147,10 @@ export abstract class LineBarRenderer extends BarRendererBase { canvas.fillRect( cx + this.x + lineX, (cy + this.y + lineY) | 0, - this.width - lineX, + lineWidth - lineX, BarRendererBase.StaffLineThickness ); } - canvas.color = res.mainGlyphColor; - - this.paintSimileMark(cx, cy, canvas); } protected collectSpaces(spaces: Float32Array[][]) { @@ -159,12 +167,25 @@ export abstract class LineBarRenderer extends BarRendererBase { this._startSpacing = true; } - protected paintTuplets(cx: number, cy: number, canvas: ICanvas, bracketsAsArcs: boolean = false): void { + protected paintTuplets( + cx: number, + cy: number, + canvas: ICanvas, + 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, bracketsAsArcs); + this.paintTupletHelper( + cx + this.beatGlyphsStart, + cy, + canvas, + tupletGroup, + beatElement, + bracketsAsArcs + ); } } } @@ -177,16 +198,23 @@ export abstract class LineBarRenderer extends BarRendererBase { protected abstract calculateBeamYWithDirection(h: BeamingHelper, x: number, direction: BeamDirection): number; - private paintTupletHelper(cx: number, cy: number, canvas: ICanvas, h: TupletGroup, bracketsAsArcs: boolean): void { + private paintTupletHelper( + cx: number, + cy: number, + canvas: ICanvas, + h: TupletGroup, + beatElement: BeatSubElement, + bracketsAsArcs: boolean + ): void { const res = this.resources; - let oldAlign: TextAlign = canvas.textAlign; - let oldBaseLine = canvas.textBaseline; + const oldAlign: TextAlign = canvas.textAlign; + const oldBaseLine = canvas.textBaseline; canvas.color = h.voice.index === 0 ? this.resources.mainGlyphColor : this.resources.secondaryGlyphColor; canvas.textAlign = TextAlign.Center; canvas.textBaseline = TextBaseline.Middle; let s: string; - let num: number = h.beats[0].tupletNumerator; - let den: number = h.beats[0].tupletDenominator; + const num: number = h.beats[0].tupletNumerator; + const den: number = h.beats[0].tupletDenominator; // list as in Guitar Pro 7. for certain tuplets only the numerator is shown if (num === 2 && den === 3) { s = '2'; @@ -211,23 +239,25 @@ export abstract class LineBarRenderer extends BarRendererBase { } else if (num === 13 && den === 8) { s = '13'; } else { - s = num + ':' + den; + s = `${num}:${den}`; } // check if we need to paint simple footer let offset: number = this.tupletOffset; let size: number = 5; + using _ = ElementStyleHelper.beat(canvas, beatElement, h.beats[0]); + if (h.beats.length === 1 || !h.isFull) { for (const beat of h.beats) { - let beamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(beat.index)!; + const beamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(beat.index)!; if (!beamingHelper) { continue; } - let direction: BeamDirection = this.getTupletBeamDirection(beamingHelper); + const direction: BeamDirection = this.getTupletBeamDirection(beamingHelper); - let tupletX: number = beamingHelper.getBeatLineX(beat); + const tupletX: number = beamingHelper.getBeatLineX(beat); let tupletY: number = this.calculateBeamYWithDirection(beamingHelper, tupletX, direction); if (direction === BeamDirection.Down) { @@ -240,8 +270,8 @@ export abstract class LineBarRenderer extends BarRendererBase { canvas.fillText(s, cx + this.x + tupletX, cy + this.y + tupletY); } } else { - let firstBeat: Beat = h.beats[0]; - let lastBeat: Beat = h.beats[h.beats.length - 1]; + const firstBeat: Beat = h.beats[0]; + const lastBeat: Beat = h.beats[h.beats.length - 1]; let firstNonRestBeat: Beat | null = null; let lastNonRestBeat: Beat | null = null; @@ -270,16 +300,16 @@ export abstract class LineBarRenderer extends BarRendererBase { // // Calculate the overall area of the tuplet bracket - let firstBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(firstBeat.index)!; - let lastBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(lastBeat.index)!; - let startX: number = firstBeamingHelper.getBeatLineX(firstBeat); - let endX: number = lastBeamingHelper.getBeatLineX(lastBeat); + const firstBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(firstBeat.index)!; + const lastBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(lastBeat.index)!; + const startX: number = firstBeamingHelper.getBeatLineX(firstBeat); + const endX: number = lastBeamingHelper.getBeatLineX(lastBeat); // // calculate the y positions for our bracket - let firstNonRestBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(firstNonRestBeat.index)!; - let lastNonRestBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(lastNonRestBeat.index)!; - let direction = this.getTupletBeamDirection(firstBeamingHelper); + const firstNonRestBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(firstNonRestBeat.index)!; + const lastNonRestBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(lastNonRestBeat.index)!; + const direction = this.getTupletBeamDirection(firstBeamingHelper); let startY: number = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction); let endY: number = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction); if (isRestOnly) { @@ -290,19 +320,19 @@ export abstract class LineBarRenderer extends BarRendererBase { // // Calculate how many space the text will need canvas.font = res.effectFont; - let sw: number = canvas.measureText(s).width; - let sp: number = 3; + const sw: number = canvas.measureText(s).width; + const sp: number = 3; // // Calculate the offsets where to break the bracket - let middleX: number = (startX + endX) / 2; - let offset1X: number = middleX - sw / 2 - sp; - let offset2X: number = middleX + sw / 2 + sp; - - let k: number = (endY - startY) / (endX - startX); - let d: number = startY - k * startX; - let offset1Y: number = k * offset1X + d; - let middleY: number = k * middleX + d; - let offset2Y: number = k * offset2X + d; + const middleX: number = (startX + endX) / 2; + const offset1X: number = middleX - sw / 2 - sp; + const offset2X: number = middleX + sw / 2 + sp; + + const k: number = (endY - startY) / (endX - startX); + const d: number = startY - k * startX; + const offset1Y: number = k * offset1X + d; + const middleY: number = k * middleX + d; + const offset2Y: number = k * offset2X + d; if (direction === BeamDirection.Down) { offset *= -1; size *= -1; @@ -349,10 +379,16 @@ export abstract class LineBarRenderer extends BarRendererBase { canvas.textBaseline = oldBaseLine; } - protected paintBeams(cx: number, cy: number, canvas: ICanvas): void { + protected paintBeams( + cx: number, + cy: number, + canvas: ICanvas, + 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); + this.paintBeamHelper(cx + this.beatGlyphsStart, cy, canvas, h, flagsElement, beamsElement); } } } @@ -361,15 +397,22 @@ export abstract class LineBarRenderer extends BarRendererBase { return h.beats.length === 1; } - private paintBeamHelper(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { + private paintBeamHelper( + cx: number, + cy: number, + canvas: ICanvas, + h: BeamingHelper, + flagsElement: BeatSubElement, + beamsElement: BeatSubElement + ): void { canvas.color = h.voice!.index === 0 ? this.resources.mainGlyphColor : this.resources.secondaryGlyphColor; // TODO: draw stem at least at the center of the score staff. // check if we need to paint simple footer if (!h.isRestBeamHelper) { if (this.drawBeamHelperAsFlags(h)) { - this.paintFlag(cx, cy, canvas, h); + this.paintFlag(cx, cy, canvas, h, flagsElement); } else { - this.paintBar(cx, cy, canvas, h); + this.paintBar(cx, cy, canvas, h, beamsElement); } } } @@ -408,20 +451,20 @@ export abstract class LineBarRenderer extends BarRendererBase { return true; } - protected paintFlag(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { + protected paintFlag(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper, flagsElement: BeatSubElement): void { for (const beat of h.beats) { if (!this.shouldPaintFlag(beat, h)) { continue; } - let isGrace: boolean = beat.graceType !== GraceType.None; - let scaleMod: number = isGrace ? NoteHeadGlyph.GraceScale : 1; + const isGrace: boolean = beat.graceType !== GraceType.None; + const scaleMod: number = isGrace ? NoteHeadGlyph.GraceScale : 1; // // draw line // - let stemSize: number = this.getFlagStemSize(h.shortestDuration); - let beatLineX: number = h.getBeatLineX(beat); - let direction: BeamDirection = this.getBeamDirection(h); + const stemSize: number = this.getFlagStemSize(h.shortestDuration); + const beatLineX: number = h.getBeatLineX(beat); + const direction: BeamDirection = this.getBeamDirection(h); let topY: number = this.getFlagTopY(beat, direction); let bottomY: number = this.getFlagBottomY(beat, direction); let beamY: number = 0; @@ -446,9 +489,11 @@ export abstract class LineBarRenderer extends BarRendererBase { canvas ); + using _ = ElementStyleHelper.beat(canvas, flagsElement, beat); + if (beat.graceType === GraceType.BeforeBeat) { - let graceSizeY: number = 15; - let graceSizeX: number = 12; + const graceSizeY: number = 15; + const graceSizeX: number = 12; canvas.beginPath(); if (direction === BeamDirection.Down) { canvas.moveTo(cx + this.x + beatLineX - graceSizeX / 2, cy + this.y + bottomY - graceSizeY); @@ -459,11 +504,12 @@ export abstract class LineBarRenderer extends BarRendererBase { } canvas.stroke(); } + // // Draw flag // if (h.hasFlag(true, beat)) { - let glyph: FlagGlyph = new FlagGlyph(beatLineX - 1 / 2, beamY, beat.duration, direction, isGrace); + const glyph: FlagGlyph = new FlagGlyph(beatLineX - 1 / 2, beamY, beat.duration, direction, isGrace); glyph.renderer = this; glyph.doLayout(); glyph.paint(cx + this.x, cy + this.y, canvas); @@ -511,11 +557,10 @@ export abstract class LineBarRenderer extends BarRendererBase { public calculateBeamY(h: BeamingHelper, x: number): number { return this.calculateBeamYWithDirection(h, x, this.getBeamDirection(h)); } + protected override createPreBeatGlyphs(): void { super.createPreBeatGlyphs(); - if (this.bar.masterBar.isRepeatStart) { - this.addPreBeatGlyph(new RepeatOpenGlyph(0, 0, 1.5, 3)); - } + this.addPreBeatGlyph(new BarLineGlyph(false)); this.createLinePreBeatGlyphs(); this.addPreBeatGlyph(new BarNumberGlyph(0, this.getLineHeight(-0.25), this.bar.index + 1)); } @@ -524,34 +569,36 @@ export abstract class LineBarRenderer extends BarRendererBase { protected override createPostBeatGlyphs(): void { super.createPostBeatGlyphs(); - if (this.bar.masterBar.isRepeatEnd) { - this.addPostBeatGlyph(new RepeatCloseGlyph(this.x, 0)); - if (this.bar.masterBar.repeatCount > 2) { - this.addPostBeatGlyph( - new RepeatCountGlyph(0, this.getLineHeight(-0.25), this.bar.masterBar.repeatCount) - ); - } - } else { - this.addPostBeatGlyph(new BarSeperatorGlyph(0, 0)); + const lastBar = this.lastBar; + + this.addPostBeatGlyph(new BarLineGlyph(true)); + + if (lastBar.masterBar.isRepeatEnd && lastBar.masterBar.repeatCount > 2) { + this.addPostBeatGlyph(new RepeatCountGlyph(0, this.getLineHeight(-0.25), this.bar.masterBar.repeatCount)); } } - protected paintBar(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { + public abstract get repeatsBarSubElement(): BarSubElement; + public abstract get barNumberBarSubElement(): BarSubElement; + public abstract get barLineBarSubElement(): BarSubElement; + public abstract get staffLineBarSubElement(): BarSubElement; + + protected paintBar(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper, beamsElement: BeatSubElement): void { for (let i: number = 0, j: number = h.beats.length; i < j; i++) { - let beat: Beat = h.beats[i]; + const beat: Beat = h.beats[i]; if (!h.hasBeatLineX(beat) || beat.deadSlapped) { continue; } - let isGrace: boolean = beat.graceType !== GraceType.None; - let scaleMod: number = isGrace ? NoteHeadGlyph.GraceScale : 1; + const isGrace: boolean = beat.graceType !== GraceType.None; + const scaleMod: number = isGrace ? NoteHeadGlyph.GraceScale : 1; // // draw line // - let beatLineX: number = h.getBeatLineX(beat); - let direction: BeamDirection = this.getBeamDirection(h); - let y1: number = cy + this.y + this.getBarLineStart(beat, direction); - let y2: number = cy + this.y + this.calculateBeamY(h, beatLineX); + const beatLineX: number = h.getBeatLineX(beat); + const direction: BeamDirection = this.getBeamDirection(h); + const y1: number = cy + this.y + this.getBarLineStart(beat, direction); + const y2: number = cy + this.y + this.calculateBeamY(h, beatLineX); // canvas.lineWidth = BarRendererBase.StemWidth; // canvas.beginPath(); @@ -562,17 +609,19 @@ export abstract class LineBarRenderer extends BarRendererBase { this.paintBeamingStem(beat, cy + this.y, cx + this.x + beatLineX, y1, y2, canvas); + using _ = ElementStyleHelper.beat(canvas, beamsElement, beat); + let fingeringY: number = y2; if (direction === BeamDirection.Down) { fingeringY += canvas.font.size * 2; } else if (i !== 0) { fingeringY -= canvas.font.size * 1.5; } - let brokenBarOffset: number = 6 * scaleMod; + const brokenBarOffset: number = 6 * scaleMod; let barSpacing: number = (BarRendererBase.BeamSpacing + BarRendererBase.BeamThickness) * scaleMod; let barSize: number = BarRendererBase.BeamThickness * scaleMod; - let barCount: number = ModelUtils.getIndex(beat.duration) - 2; - let barStart: number = cy + this.y; + const barCount: number = ModelUtils.getIndex(beat.duration) - 2; + const barStart: number = cy + this.y; if (direction === BeamDirection.Down) { barSpacing = -barSpacing; barSize = -barSize; @@ -582,7 +631,7 @@ export abstract class LineBarRenderer extends BarRendererBase { let barEndX: number = 0; let barStartY: number = 0; let barEndY: number = 0; - let barY: number = barStart + barIndex * barSpacing; + const barY: number = barStart + barIndex * barSpacing; // // Bar to Next? // diff --git a/src/rendering/MultiBarRestBeatContainerGlyph.ts b/src/rendering/MultiBarRestBeatContainerGlyph.ts new file mode 100644 index 000000000..34a672773 --- /dev/null +++ b/src/rendering/MultiBarRestBeatContainerGlyph.ts @@ -0,0 +1,31 @@ +import { Beat } from '@src/model/Beat'; +import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; +import type { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; +import { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; +import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; +import { MultiBarRestGlyph } from '@src/rendering/glyphs/MultiBarRestGlyph'; + +export class MultiBarRestBeatContainerGlyph extends BeatContainerGlyph { + public constructor(voiceContainer: VoiceContainerGlyph) { + super(MultiBarRestBeatContainerGlyph.getOrCreatePlaceholderBeat(voiceContainer), voiceContainer); + this.preNotes = new BeatGlyphBase(); + this.onNotes = new BeatOnNoteGlyphBase(); + } + + public override doLayout(): void { + if (this.renderer.showMultiBarRest) { + this.onNotes.addNormal(new MultiBarRestGlyph()); + } + + super.doLayout(); + } + + 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; + } +} diff --git a/src/rendering/NumberedBarRenderer.ts b/src/rendering/NumberedBarRenderer.ts index 88970c405..4190342f6 100644 --- a/src/rendering/NumberedBarRenderer.ts +++ b/src/rendering/NumberedBarRenderer.ts @@ -1,24 +1,28 @@ -import { Bar } from '@src/model/Bar'; -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; -import { Voice } from '@src/model/Voice'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import { type Bar, BarSubElement } from '@src/model/Bar'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import type { Voice } from '@src/model/Voice'; +import type { ICanvas } from '@src/platform/ICanvas'; +import { BarRendererBase, type NoteYPosition } from '@src/rendering/BarRendererBase'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; -import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; -import { LineBarRenderer } from './LineBarRenderer'; -import { SlashNoteHeadGlyph } from './glyphs/SlashNoteHeadGlyph'; -import { BeatGlyphBase } from './glyphs/BeatGlyphBase'; -import { BeatOnNoteGlyphBase } from './glyphs/BeatOnNoteGlyphBase'; -import { NumberedBeatContainerGlyph } from './NumberedBeatContainerGlyph'; -import { NumberedBeatGlyph, NumberedBeatPreNotesGlyph } from './glyphs/NumberedBeatGlyph'; -import { ScoreTimeSignatureGlyph } from './glyphs/ScoreTimeSignatureGlyph'; -import { SpacingGlyph } from './glyphs/SpacingGlyph'; -import { NumberedKeySignatureGlyph } from './glyphs/NumberedKeySignatureGlyph'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import { LineBarRenderer } from '@src/rendering/LineBarRenderer'; +import { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; +import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; +import { NumberedBeatContainerGlyph } from '@src/rendering/NumberedBeatContainerGlyph'; +import { NumberedBeatGlyph, NumberedBeatPreNotesGlyph } from '@src/rendering/glyphs/NumberedBeatGlyph'; +import { ScoreTimeSignatureGlyph } from '@src/rendering/glyphs/ScoreTimeSignatureGlyph'; +import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; +import { NumberedKeySignatureGlyph } from '@src/rendering/glyphs/NumberedKeySignatureGlyph'; import { ModelUtils } from '@src/model/ModelUtils'; -import { Duration } from '@src/model'; -import { BeatXPosition } from './BeatXPosition'; +import { BeatXPosition } from '@src/rendering/BeatXPosition'; +import { BarNumberGlyph } from '@src/rendering/glyphs/BarNumberGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; +import { BarLineGlyph } from '@src/rendering/glyphs/BarLineGlyph'; +import { Duration } from '@src/model/Duration'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; /** * This BarRenderer renders a bar using (Jianpu) Numbered Music Notation @@ -30,8 +34,8 @@ export class NumberedBarRenderer extends LineBarRenderer { private _isOnlyNumbered: boolean; public shortestDuration = Duration.QuadrupleWhole; - public lowestOctave:number | null = null; - public highestOctave:number | null = null; + public lowestOctave: number | null = null; + public highestOctave: number | null = null; public registerOctave(octave: number) { if (this.lowestOctave === null) { @@ -47,6 +51,22 @@ export class NumberedBarRenderer extends LineBarRenderer { } } + public override get repeatsBarSubElement(): BarSubElement { + return BarSubElement.NumberedRepeats; + } + + public override get barNumberBarSubElement(): BarSubElement { + return BarSubElement.NumberedBarNumber; + } + + public override get barLineBarSubElement(): BarSubElement { + return BarSubElement.NumberedBarLines; + } + + public override get staffLineBarSubElement(): BarSubElement { + return BarSubElement.NumberedStaffLine; + } + public constructor(renderer: ScoreRenderer, bar: Bar) { super(renderer, bar); this._isOnlyNumbered = !bar.staff.showSlash && !bar.staff.showTablature && !bar.staff.showStandardNotation; @@ -70,16 +90,16 @@ export class NumberedBarRenderer extends LineBarRenderer { public override paint(cx: number, cy: number, canvas: ICanvas): void { super.paint(cx, cy, canvas); - this.paintBeams(cx, cy, canvas); - this.paintTuplets(cx, cy, canvas, true); + this.paintBeams(cx, cy, canvas, BeatSubElement.NumberedDuration, BeatSubElement.NumberedDuration); + this.paintTuplets(cx, cy, canvas, BeatSubElement.NumberedTuplet, true); } public override doLayout(): void { super.doLayout(); let hasTuplets: boolean = false; - for (let voice of this.bar.voices) { + for (const voice of this.bar.voices) { if (this.hasVoiceContainer(voice)) { - let c = this.getVoiceContainer(voice)!; + const c = this.getVoiceContainer(voice)!; if (c.tupletGroups.length > 0) { hasTuplets = true; break; @@ -91,17 +111,16 @@ export class NumberedBarRenderer extends LineBarRenderer { } if (!this.bar.isEmpty) { - let barCount: number = ModelUtils.getIndex(this.shortestDuration) - 2; + const barCount: number = ModelUtils.getIndex(this.shortestDuration) - 2; if (barCount > 0) { - let barSpacing: number = NumberedBarRenderer.BarSpacing; - let barSize: number = NumberedBarRenderer.BarSize; - let barOverflow = (barCount - 1) * barSpacing + barSize; + const barSpacing: number = NumberedBarRenderer.BarSpacing; + const barSize: number = NumberedBarRenderer.BarSize; + const barOverflow = (barCount - 1) * barSpacing + barSize; let dotOverflow = 0; const lowestOctave = this.lowestOctave; - if (lowestOctave !== null) { - dotOverflow = - (Math.abs(lowestOctave) * NumberedBarRenderer.DotSpacing + NumberedBarRenderer.DotSize); + if (lowestOctave !== null) { + dotOverflow = Math.abs(lowestOctave) * NumberedBarRenderer.DotSpacing + NumberedBarRenderer.DotSize; } this.registerOverflowBottom(barOverflow + dotOverflow); @@ -110,7 +129,7 @@ export class NumberedBarRenderer extends LineBarRenderer { const highestOctave = this.highestOctave; if (highestOctave !== null) { const dotOverflow = - (Math.abs(highestOctave) * NumberedBarRenderer.DotSpacing + NumberedBarRenderer.DotSize); + Math.abs(highestOctave) * NumberedBarRenderer.DotSpacing + NumberedBarRenderer.DotSize; this.registerOverflowTop(dotOverflow); } } @@ -122,32 +141,49 @@ export class NumberedBarRenderer extends LineBarRenderer { private static DotSpacing = 5; public static DotSize = 2; - protected override paintFlag(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { - this.paintBar(cx, cy, canvas, h); + protected override paintFlag( + cx: number, + cy: number, + canvas: ICanvas, + h: BeamingHelper, + flagsElement: BeatSubElement + ): void { + this.paintBar(cx, cy, canvas, h, flagsElement); } - protected override paintBar(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { + protected override paintBar( + cx: number, + cy: number, + canvas: ICanvas, + h: BeamingHelper, + flagsElement: BeatSubElement + ): void { + if (h.beats.length === 0) { + return; + } const res = this.resources; - for (let i: number = 0, j: number = h.beats.length; i < j; i++) { - let beat: Beat = h.beats[i]; + const beat: Beat = h.beats[i]; + + using _ = ElementStyleHelper.beat(canvas, flagsElement, beat); + // // draw line // - let barSpacing: number = NumberedBarRenderer.BarSpacing; - let barSize: number = NumberedBarRenderer.BarSize; - let barCount: number = ModelUtils.getIndex(beat.duration) - 2; - let barStart: number = cy + this.y; + const barSpacing: number = NumberedBarRenderer.BarSpacing; + const barSize: number = NumberedBarRenderer.BarSize; + const barCount: number = ModelUtils.getIndex(beat.duration) - 2; + const barStart: number = cy + this.y; - let beatLineX: number = this.getBeatX(beat, BeatXPosition.PreNotes) - this.beatGlyphsStart; + const beatLineX: number = this.getBeatX(beat, BeatXPosition.PreNotes) - this.beatGlyphsStart; - var beamY = this.calculateBeamY(h, beatLineX); + const beamY = this.calculateBeamY(h, beatLineX); for (let barIndex: number = 0; barIndex < barCount; barIndex++) { let barStartX: number = 0; let barEndX: number = 0; let barStartY: number = 0; - let barY: number = barStart + barIndex * barSpacing; + const barY: number = barStart + barIndex * barSpacing; if (i === h.beats.length - 1) { barStartX = beatLineX; barEndX = this.getBeatX(beat, BeatXPosition.PostNotes) - this.beatGlyphsStart; @@ -168,17 +204,17 @@ export class NumberedBarRenderer extends LineBarRenderer { } const onNotes = this.getBeatContainer(beat)!.onNotes; - let dotCount = (onNotes as NumberedBeatGlyph).octaveDots; + let dotCount = onNotes instanceof NumberedBeatGlyph ? (onNotes as NumberedBeatGlyph).octaveDots : 0; let dotsY = 0; let dotsOffset = 0; if (dotCount > 0) { dotsY = barStart + this.getLineY(0) - res.numberedNotationFont.size / 1.5; - dotsOffset = NumberedBarRenderer.DotSpacing * (-1); + dotsOffset = NumberedBarRenderer.DotSpacing * -1; } else if (dotCount < 0) { dotsY = barStart + beamY + barCount * barSpacing; dotsOffset = NumberedBarRenderer.DotSpacing; } - let dotX: number = this.getBeatX(beat, BeatXPosition.OnNotes) + 4 - this.beatGlyphsStart; + const dotX: number = this.getBeatX(beat, BeatXPosition.OnNotes) + 4 - this.beatGlyphsStart; dotCount = Math.abs(dotCount); @@ -194,15 +230,17 @@ export class NumberedBarRenderer extends LineBarRenderer { } public override get tupletOffset(): number { - return super.tupletOffset + this.resources.numberedNotationFont.size; + return super.tupletOffset + this.resources.numberedNotationFont.size; } - protected override getFlagTopY(_beat: Beat, _direction:BeamDirection): number { - return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2); + protected override getFlagTopY(_beat: Beat, _direction: BeamDirection): number { + const noteHeadHeight = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.NoteheadBlack)!; + return this.getLineY(0) - noteHeadHeight / 2; } - protected override getFlagBottomY(_beat: Beat, _direction:BeamDirection): number { - return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2); + protected override getFlagBottomY(_beat: Beat, _direction: BeamDirection): number { + const noteHeadHeight = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.NoteheadBlack)!; + return this.getLineY(0) - noteHeadHeight / 2; } protected override getBeamDirection(_helper: BeamingHelper): BeamDirection { @@ -215,7 +253,7 @@ export class NumberedBarRenderer extends LineBarRenderer { public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { let y = super.getNoteY(note, requestedPosition); - if (isNaN(y)) { + if (Number.isNaN(y)) { y = this.getLineY(0); } return y; @@ -227,32 +265,39 @@ export class NumberedBarRenderer extends LineBarRenderer { } protected override getBarLineStart(_beat: Beat, _direction: BeamDirection): number { - return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2); + const noteHeadHeight = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.NoteheadBlack)!; + return this.getLineY(0) - noteHeadHeight / 2; + } + + protected override createPreBeatGlyphs(): void { + this.wasFirstOfLine = this.isFirstOfLine; + if (this.index === 0 || (this.bar.masterBar.isRepeatStart && this._isOnlyNumbered)) { + this.addPreBeatGlyph(new BarLineGlyph(false)); + } + this.createLinePreBeatGlyphs(); + this.addPreBeatGlyph(new BarNumberGlyph(0, this.getLineHeight(-0.25), this.bar.index + 1)); } protected override createLinePreBeatGlyphs(): void { // Key signature - if ( - !this.bar.previousBar || - (this.bar.masterBar.keySignature !== this.bar.previousBar.masterBar.keySignature) - ) { + if (!this.bar.previousBar || this.bar.keySignature !== this.bar.previousBar.keySignature) { this.createStartSpacing(); this.createKeySignatureGlyphs(); } - if (this._isOnlyNumbered && - ( - !this.bar.previousBar || + if ( + this._isOnlyNumbered && + (!this.bar.previousBar || (this.bar.previousBar && - this.bar.masterBar.timeSignatureNumerator !== this.bar.previousBar.masterBar.timeSignatureNumerator) || + this.bar.masterBar.timeSignatureNumerator !== + this.bar.previousBar.masterBar.timeSignatureNumerator) || (this.bar.previousBar && this.bar.masterBar.timeSignatureDenominator !== this.bar.previousBar.masterBar.timeSignatureDenominator) || (this.bar.previousBar && this.bar.masterBar.isFreeTime && - this.bar.masterBar.isFreeTime !== this.bar.previousBar.masterBar.isFreeTime) - - )) { + this.bar.masterBar.isFreeTime !== this.bar.previousBar.masterBar.isFreeTime)) + ) { this.createStartSpacing(); this.createTimeSignatureGlyphs(); } @@ -262,8 +307,8 @@ export class NumberedBarRenderer extends LineBarRenderer { new NumberedKeySignatureGlyph( 0, this.getLineY(0), - this.bar.masterBar.keySignature, - this.bar.masterBar.keySignatureType + this.bar.keySignature, + this.bar.keySignatureType ) ); } @@ -272,16 +317,18 @@ export class NumberedBarRenderer extends LineBarRenderer { this.addPreBeatGlyph(new SpacingGlyph(0, 0, 5)); const masterBar = this.bar.masterBar; - this.addPreBeatGlyph( - new ScoreTimeSignatureGlyph( - 0, - this.getLineY(0), - masterBar.timeSignatureNumerator, - masterBar.timeSignatureDenominator, - masterBar.timeSignatureCommon, - masterBar.isFreeTime && (masterBar.previousMasterBar == null || masterBar.isFreeTime !== masterBar.previousMasterBar!.isFreeTime), - ) + const g = new ScoreTimeSignatureGlyph( + 0, + this.getLineY(0), + masterBar.timeSignatureNumerator, + masterBar.timeSignatureDenominator, + masterBar.timeSignatureCommon, + masterBar.isFreeTime && + (masterBar.previousMasterBar == null || + masterBar.isFreeTime !== masterBar.previousMasterBar!.isFreeTime) ); + g.barSubElement = BarSubElement.NumberedTimeSignature; + this.addPreBeatGlyph(g); } protected override createPostBeatGlyphs(): void { @@ -292,9 +339,9 @@ export class NumberedBarRenderer extends LineBarRenderer { protected override createVoiceGlyphs(v: Voice): void { for (const b of v.beats) { - let 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(); + 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); } } @@ -306,12 +353,5 @@ export class NumberedBarRenderer extends LineBarRenderer { topY: number, bottomY: number, canvas: ICanvas - ): void { - canvas.lineWidth = BarRendererBase.StemWidth; - canvas.beginPath(); - canvas.moveTo(x, topY); - canvas.lineTo(x, bottomY); - canvas.stroke(); - canvas.lineWidth = 1; - } + ): void {} } diff --git a/src/rendering/NumberedBarRendererFactory.ts b/src/rendering/NumberedBarRendererFactory.ts index b72ec445b..6551335ac 100644 --- a/src/rendering/NumberedBarRendererFactory.ts +++ b/src/rendering/NumberedBarRendererFactory.ts @@ -1,19 +1,18 @@ -import { Bar } from '@src/model/Bar'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Bar } from '@src/model/Bar'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { BarRendererFactory } from '@src/rendering/BarRendererFactory'; -import { SlashBarRenderer } from '@src/rendering/SlashBarRenderer'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { Track } from '@src/model/Track'; -import { Staff } from '@src/model'; -import { RenderStaff } from './staves/RenderStaff'; -import { NumberedBarRenderer } from './NumberedBarRenderer'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { Track } from '@src/model/Track'; +import type { Staff } from '@src/model/Staff'; +import type { RenderStaff } from '@src/rendering/staves/RenderStaff'; +import { NumberedBarRenderer } from '@src/rendering/NumberedBarRenderer'; /** * This Factory produces NumberedBarRenderer instances */ export class NumberedBarRendererFactory extends BarRendererFactory { public get staffId(): string { - return SlashBarRenderer.StaffId; + return NumberedBarRenderer.StaffId; } public override getStaffPaddingTop(staff: RenderStaff): number { @@ -31,8 +30,4 @@ export class NumberedBarRendererFactory extends BarRendererFactory { public override canCreate(track: Track, staff: Staff): boolean { return super.canCreate(track, staff) && staff.showNumbered; } - - public constructor() { - super(); - } } diff --git a/src/rendering/NumberedBeatContainerGlyph.ts b/src/rendering/NumberedBeatContainerGlyph.ts index fcb236e2d..8f51ab6f6 100644 --- a/src/rendering/NumberedBeatContainerGlyph.ts +++ b/src/rendering/NumberedBeatContainerGlyph.ts @@ -1,17 +1,11 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; +import type { Note } from '@src/model/Note'; import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; -import { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; -import { NumberedTieGlyph } from './glyphs/NumberedTieGlyph'; -import { NumberedSlurGlyph } from './glyphs/NumberedSlurGlyph'; +import { NumberedTieGlyph } from '@src/rendering//glyphs/NumberedTieGlyph'; +import { NumberedSlurGlyph } from '@src/rendering/glyphs/NumberedSlurGlyph'; export class NumberedBeatContainerGlyph extends BeatContainerGlyph { private _effectSlurs: NumberedSlurGlyph[] = []; - public constructor(beat: Beat, voiceContainer: VoiceContainerGlyph) { - super(beat, voiceContainer); - } - protected override createTies(n: Note): void { // create a tie if any effect requires it if (!n.isVisible) { @@ -19,28 +13,28 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { } if (n.isTieOrigin && n.tieDestination!.isVisible) { - let tie = new NumberedTieGlyph(n, n.tieDestination!, false); + const tie = new NumberedTieGlyph(n, n.tieDestination!, false); this.addTie(tie); } if (n.isTieDestination) { - let tie = new NumberedTieGlyph(n.tieOrigin!, n, true); + const tie = new NumberedTieGlyph(n.tieOrigin!, n, true); this.addTie(tie); } if (n.isLeftHandTapped && !n.isHammerPullDestination) { - let tapSlur = new NumberedTieGlyph(n, n, false); + const tapSlur = new NumberedTieGlyph(n, n, false); this.addTie(tapSlur); } // start effect slur on first beat if (n.isEffectSlurOrigin && n.effectSlurDestination) { let expanded: boolean = false; - for (let slur of this._effectSlurs) { + for (const slur of this._effectSlurs) { if (slur.tryExpand(n, n.effectSlurDestination, false, false)) { expanded = true; break; } } if (!expanded) { - let effectSlur = new NumberedSlurGlyph(n, n.effectSlurDestination, false, false); + const effectSlur = new NumberedSlurGlyph(n, n.effectSlurDestination, false, false); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); } @@ -48,14 +42,14 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { // end effect slur on last beat if (n.isEffectSlurDestination && n.effectSlurOrigin) { let expanded: boolean = false; - for (let slur of this._effectSlurs) { + for (const slur of this._effectSlurs) { if (slur.tryExpand(n.effectSlurOrigin, n, false, true)) { expanded = true; break; } } if (!expanded) { - let effectSlur = new NumberedSlurGlyph(n.effectSlurOrigin, n, false, true); + const effectSlur = new NumberedSlurGlyph(n.effectSlurOrigin, n, false, true); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); } diff --git a/src/rendering/RenderFinishedEventArgs.ts b/src/rendering/RenderFinishedEventArgs.ts index 6db796581..7f81e32c1 100644 --- a/src/rendering/RenderFinishedEventArgs.ts +++ b/src/rendering/RenderFinishedEventArgs.ts @@ -1,4 +1,4 @@ -import { ModelUtils } from "@src/model/ModelUtils"; +import { ModelUtils } from '@src/model/ModelUtils'; /** * This eventargs define the details about the rendering and layouting process and are diff --git a/src/rendering/ScoreBarRenderer.ts b/src/rendering/ScoreBarRenderer.ts index 8ee9cd9fb..665db1482 100644 --- a/src/rendering/ScoreBarRenderer.ts +++ b/src/rendering/ScoreBarRenderer.ts @@ -1,28 +1,30 @@ import { AccidentalType } from '@src/model/AccidentalType'; -import { Bar } from '@src/model/Bar'; -import { Beat } from '@src/model/Beat'; +import { type Bar, BarSubElement } from '@src/model/Bar'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; import { Clef } from '@src/model/Clef'; import { Duration } from '@src/model/Duration'; -import { Note } from '@src/model/Note'; -import { Voice } from '@src/model/Voice'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { Note } from '@src/model/Note'; +import type { Voice } from '@src/model/Voice'; +import type { ICanvas } from '@src/platform/ICanvas'; import { BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; import { AccidentalGlyph } from '@src/rendering/glyphs/AccidentalGlyph'; import { ClefGlyph } from '@src/rendering/glyphs/ClefGlyph'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; import { ScoreBeatGlyph } from '@src/rendering/glyphs/ScoreBeatGlyph'; import { ScoreBeatPreNotesGlyph } from '@src/rendering/glyphs/ScoreBeatPreNotesGlyph'; import { ScoreTimeSignatureGlyph } from '@src/rendering/glyphs/ScoreTimeSignatureGlyph'; import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; import { ScoreBeatContainerGlyph } from '@src/rendering/ScoreBeatContainerGlyph'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; import { AccidentalHelper } from '@src/rendering/utils/AccidentalHelper'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; -import { BeamingHelper, BeamingHelperDrawInfo } from '@src/rendering/utils/BeamingHelper'; +import { type BeamingHelper, BeamingHelperDrawInfo } from '@src/rendering/utils/BeamingHelper'; import { ModelUtils } from '@src/model/ModelUtils'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; import { KeySignature } from '@src/model/KeySignature'; -import { LineBarRenderer } from './LineBarRenderer'; +import { LineBarRenderer } from '@src/rendering/LineBarRenderer'; +import { KeySignatureGlyph } from '@src/rendering/glyphs/KeySignatureGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; /** * This BarRenderer renders a bar using standard music notation. @@ -44,6 +46,26 @@ export class ScoreBarRenderer extends LineBarRenderer { this.accidentalHelper = new AccidentalHelper(this); } + public override get repeatsBarSubElement(): BarSubElement { + return BarSubElement.StandardNotationRepeats; + } + + public override get barNumberBarSubElement(): BarSubElement { + return BarSubElement.StandardNotationBarNumber; + } + + public override get barLineBarSubElement(): BarSubElement { + return BarSubElement.StandardNotationBarLines; + } + + public override get staffLineBarSubElement(): BarSubElement { + return BarSubElement.StandardNotationStaffLine; + } + + public override get showMultiBarRest(): boolean { + return true; + } + public override get lineSpacing(): number { return BarRendererBase.RawLineSpacing; } @@ -90,9 +112,9 @@ export class ScoreBarRenderer extends LineBarRenderer { public override doLayout(): void { super.doLayout(); if (!this.bar.isEmpty && this.accidentalHelper.maxLineBeat) { - let top: number = this.getScoreY(-2); - let bottom: number = this.getScoreY(10); - let whammyOffset: number = this.simpleWhammyOverflow; + const top: number = this.getScoreY(-2); + const bottom: number = this.getScoreY(10); + const whammyOffset: number = this.simpleWhammyOverflow; const beatEffectsMinY = this.beatEffectsMinY; if (beatEffectsMinY !== null) { @@ -113,7 +135,9 @@ export class ScoreBarRenderer extends LineBarRenderer { this.registerOverflowTop(whammyOffset); let maxNoteY: number = this.getScoreY(this.accidentalHelper.maxLine); - let maxNoteHelper: BeamingHelper = this.helpers.getBeamingHelperForBeat(this.accidentalHelper.maxLineBeat); + const maxNoteHelper: BeamingHelper = this.helpers.getBeamingHelperForBeat( + this.accidentalHelper.maxLineBeat + ); if (maxNoteHelper.direction === BeamDirection.Up) { maxNoteY -= this.getStemSize(maxNoteHelper); if (maxNoteHelper.hasTuplet) { @@ -124,7 +148,9 @@ export class ScoreBarRenderer extends LineBarRenderer { this.registerOverflowTop(Math.abs(maxNoteY) + whammyOffset); } let minNoteY: number = this.getScoreY(this.accidentalHelper.minLine); - let minNoteHelper: BeamingHelper = this.helpers.getBeamingHelperForBeat(this.accidentalHelper.minLineBeat!); + const minNoteHelper: BeamingHelper = this.helpers.getBeamingHelperForBeat( + this.accidentalHelper.minLineBeat! + ); if (minNoteHelper.direction === BeamDirection.Down) { minNoteY += this.getStemSize(minNoteHelper); if (minNoteHelper.hasTuplet) { @@ -139,8 +165,8 @@ export class ScoreBarRenderer extends LineBarRenderer { public override paint(cx: number, cy: number, canvas: ICanvas): void { super.paint(cx, cy, canvas); - this.paintBeams(cx, cy, canvas); - this.paintTuplets(cx, cy, canvas); + this.paintBeams(cx, cy, canvas, BeatSubElement.StandardNotationFlags, BeatSubElement.StandardNotationBeams); + this.paintTuplets(cx, cy, canvas, BeatSubElement.StandardNotationTuplet); } private getSlashFlagY(duration: Duration, direction: BeamDirection) { @@ -157,7 +183,7 @@ export class ScoreBarRenderer extends LineBarRenderer { break; } - if (direction == BeamDirection.Down) { + if (direction === BeamDirection.Down) { line += offset; } else { line -= offset; @@ -231,12 +257,12 @@ export class ScoreBarRenderer extends LineBarRenderer { public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { if (note.beat.slashed) { - let line = (this.heightLineCount - 1) / 2; + const line = (this.heightLineCount - 1) / 2; return this.getLineY(line); } let y = super.getNoteY(note, requestedPosition); - if (isNaN(y)) { + 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 @@ -250,9 +276,9 @@ export class ScoreBarRenderer extends LineBarRenderer { const result = super.applyLayoutingInfo(); if (result && this.bar.isMultiVoice) { // consider rest overflows - let top: number = this.getScoreY(-2); - let bottom: number = this.getScoreY(10); - let minMax = this.helpers.collisionHelper.getBeatMinMaxY(); + const top: number = this.getScoreY(-2); + const bottom: number = this.getScoreY(10); + const minMax = this.helpers.collisionHelper.getBeatMinMaxY(); if (minMax[0] < top) { this.registerOverflowTop(Math.abs(minMax[0])); } @@ -264,10 +290,10 @@ export class ScoreBarRenderer extends LineBarRenderer { } protected override calculateBeamYWithDirection(h: BeamingHelper, x: number, direction: BeamDirection): number { - let stemSize: number = this.getStemSize(h); + const stemSize: number = this.getStemSize(h); if (!h.drawingInfos.has(direction)) { - let drawingInfo = new BeamingHelperDrawInfo(); + const drawingInfo = new BeamingHelperDrawInfo(); h.drawingInfos.set(direction, drawingInfo); // the beaming logic works like this: @@ -308,7 +334,7 @@ export class ScoreBarRenderer extends LineBarRenderer { // 2. ensure max height // 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 - let maxDistance: number = 10; + const maxDistance: number = 10; if ( direction === BeamDirection.Down && drawingInfo.startY > drawingInfo.endY && @@ -342,7 +368,7 @@ export class ScoreBarRenderer extends LineBarRenderer { if (h.beats.length > 1) { // check if highest note shifts bar up or down if (direction === BeamDirection.Up) { - let yNeededForHighestNote = + const yNeededForHighestNote = this.getScoreY(this.accidentalHelper.getMinLine(h.beatOfHighestNote)) - stemSize; const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfHighestNote)); @@ -352,7 +378,7 @@ export class ScoreBarRenderer extends LineBarRenderer { drawingInfo.endY -= diff; } } else { - let yNeededForLowestNote = + const yNeededForLowestNote = this.getScoreY(this.accidentalHelper.getMaxLine(h.beatOfLowestNote)) + stemSize; const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfLowestNote)); @@ -366,13 +392,13 @@ export class ScoreBarRenderer extends LineBarRenderer { // check if rest shifts bar up or down if (h.minRestLine !== null || h.maxRestLine !== null) { const barCount: number = ModelUtils.getIndex(h.shortestDuration) - 2; - let scaleMod: number = h.isGrace ? NoteHeadGlyph.GraceScale : 1; + const scaleMod: number = h.isGrace ? NoteHeadGlyph.GraceScale : 1; let barSpacing: number = barCount * (BarRendererBase.BeamSpacing + BarRendererBase.BeamThickness) * scaleMod; barSpacing += BarRendererBase.BeamSpacing; if (direction === BeamDirection.Up && h.minRestLine !== null) { - let yNeededForRest = this.getScoreY(h.minRestLine!) - barSpacing; + const yNeededForRest = this.getScoreY(h.minRestLine!) - barSpacing; const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfMinRestLine!)); const diff = yGivenByCurrentValues - yNeededForRest; @@ -381,7 +407,7 @@ export class ScoreBarRenderer extends LineBarRenderer { drawingInfo.endY -= diff; } } else if (direction === BeamDirection.Down && h.maxRestLine !== null) { - let yNeededForRest = this.getScoreY(h.maxRestLine!) + barSpacing; + const yNeededForRest = this.getScoreY(h.maxRestLine!) + barSpacing; const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(h.beatOfMaxRestLine!)); const diff = yNeededForRest - yGivenByCurrentValues; @@ -429,6 +455,7 @@ export class ScoreBarRenderer extends LineBarRenderer { protected override createLinePreBeatGlyphs(): void { // Clef + let hasClef = false; if ( this.isFirstOfLine || this.bar.clef !== this.bar.previousBar!.clef || @@ -462,11 +489,13 @@ export class ScoreBarRenderer extends LineBarRenderer { this.bar.clefOttava ) ); + hasClef = true; } // Key signature if ( - (this.index === 0 && this.bar.masterBar.keySignature !== KeySignature.C) || - (this.bar.previousBar && this.bar.masterBar.keySignature !== this.bar.previousBar.masterBar.keySignature) + hasClef || + (this.index === 0 && this.bar.keySignature !== KeySignature.C) || + (this.bar.previousBar && this.bar.keySignature !== this.bar.previousBar.keySignature) ) { this.createStartSpacing(); this.createKeySignatureGlyphs(); @@ -490,8 +519,8 @@ export class ScoreBarRenderer extends LineBarRenderer { private createKeySignatureGlyphs(): void { let offsetClef: number = 0; - let currentKey: number = this.bar.masterBar.keySignature; - let previousKey: number = !this.bar.previousBar ? 0 : this.bar.previousBar.masterBar.keySignature; + const currentKey: number = this.bar.keySignature; + const previousKey: number = !this.bar.previousBar ? 0 : this.bar.previousBar.keySignature; switch (this.bar.clef) { case Clef.Neutral: offsetClef = 0; @@ -509,35 +538,39 @@ export class ScoreBarRenderer extends LineBarRenderer { offsetClef = 0; break; } - let newLines: Map = new Map(); - let newGlyphs: Glyph[] = []; + + const glyph = new KeySignatureGlyph(); + glyph.renderer = this; + + const newLines: Map = new Map(); + const newGlyphs: Glyph[] = []; // how many symbols do we need to get from a C-keysignature // to the new one // var offsetSymbols = (currentKey <= 7) ? currentKey : currentKey - 7; // a sharp keysignature if (ModelUtils.keySignatureIsSharp(currentKey)) { for (let i: number = 0; i < Math.abs(currentKey); i++) { - let step: number = ScoreBarRenderer.SharpKsSteps[i] + offsetClef; + const step: number = ScoreBarRenderer.SharpKsSteps[i] + offsetClef; newGlyphs.push(new AccidentalGlyph(0, this.getScoreY(step), AccidentalType.Sharp, 1)); newLines.set(step, true); } } else { for (let i: number = 0; i < Math.abs(currentKey); i++) { - let step: number = ScoreBarRenderer.FlatKsSteps[i] + offsetClef; + const step: number = ScoreBarRenderer.FlatKsSteps[i] + offsetClef; newGlyphs.push(new AccidentalGlyph(0, this.getScoreY(step), AccidentalType.Flat, 1)); newLines.set(step, true); } } // naturalize previous key if naturalizing - if (this.bar.masterBar.keySignature === KeySignature.C) { - let naturalizeSymbols: number = Math.abs(previousKey); - let previousKeyPositions = ModelUtils.keySignatureIsSharp(previousKey) + if (this.bar.keySignature === KeySignature.C) { + const naturalizeSymbols: number = Math.abs(previousKey); + const previousKeyPositions = ModelUtils.keySignatureIsSharp(previousKey) ? ScoreBarRenderer.SharpKsSteps : ScoreBarRenderer.FlatKsSteps; for (let i: number = 0; i < naturalizeSymbols; i++) { - let step: number = previousKeyPositions[i] + offsetClef; + const step: number = previousKeyPositions[i] + offsetClef; if (!newLines.has(step)) { - this.addPreBeatGlyph( + glyph.addGlyph( new AccidentalGlyph( 0, this.getScoreY(previousKeyPositions[i] + offsetClef), @@ -549,9 +582,11 @@ export class ScoreBarRenderer extends LineBarRenderer { } } - for (let newGlyph of newGlyphs) { - this.addPreBeatGlyph(newGlyph); + for (const newGlyph of newGlyphs) { + glyph.addGlyph(newGlyph); } + + this.addPreBeatGlyph(glyph); } private createTimeSignatureGlyphs(): void { @@ -572,7 +607,7 @@ export class ScoreBarRenderer extends LineBarRenderer { protected override createVoiceGlyphs(v: Voice): void { for (const b of v.beats) { - let container: ScoreBeatContainerGlyph = new ScoreBeatContainerGlyph(b, this.getVoiceContainer(v)!); + const container: ScoreBeatContainerGlyph = new ScoreBeatContainerGlyph(b, this.getVoiceContainer(v)!); container.preNotes = new ScoreBeatPreNotesGlyph(); container.onNotes = new ScoreBeatGlyph(); this.addBeatGlyph(container); @@ -595,7 +630,7 @@ export class ScoreBarRenderer extends LineBarRenderer { offset += this.resources.effectFont.size * 2; } - if (helper.direction == BeamDirection.Up) { + if (helper.direction === BeamDirection.Up) { highestNotePosition -= offset; } else { lowestNotePosition += offset; @@ -615,6 +650,7 @@ export class ScoreBarRenderer extends LineBarRenderer { bottomY: number, canvas: ICanvas ): void { + using _ = ElementStyleHelper.beat(canvas, BeatSubElement.StandardNotationStem, beat); canvas.lineWidth = BarRendererBase.StemWidth; canvas.beginPath(); canvas.moveTo(x, topY); diff --git a/src/rendering/ScoreBarRendererFactory.ts b/src/rendering/ScoreBarRendererFactory.ts index 357599a3c..07fd19dd2 100644 --- a/src/rendering/ScoreBarRendererFactory.ts +++ b/src/rendering/ScoreBarRendererFactory.ts @@ -1,11 +1,11 @@ -import { Bar } from '@src/model/Bar'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Bar } from '@src/model/Bar'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { BarRendererFactory } from '@src/rendering/BarRendererFactory'; import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { RenderStaff } from './staves/RenderStaff'; -import { Track } from '@src/model/Track'; -import { Staff } from '@src/model/Staff'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { RenderStaff } from '@src/rendering/staves/RenderStaff'; +import type { Track } from '@src/model/Track'; +import type { Staff } from '@src/model/Staff'; /** * This Factory produces ScoreBarRenderer instances @@ -30,8 +30,4 @@ export class ScoreBarRendererFactory extends BarRendererFactory { public override canCreate(track: Track, staff: Staff): boolean { return super.canCreate(track, staff) && staff.showStandardNotation; } - - public constructor() { - super(); - } } diff --git a/src/rendering/ScoreBeatContainerGlyph.ts b/src/rendering/ScoreBeatContainerGlyph.ts index 5747c54cb..f34458f3c 100644 --- a/src/rendering/ScoreBeatContainerGlyph.ts +++ b/src/rendering/ScoreBeatContainerGlyph.ts @@ -1,6 +1,6 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { GraceType } from '@src/model/GraceType'; -import { Note } from '@src/model/Note'; +import type { Note } from '@src/model/Note'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; @@ -9,7 +9,6 @@ import { ScoreLegatoGlyph } from '@src/rendering/glyphs/ScoreLegatoGlyph'; import { ScoreSlideLineGlyph } from '@src/rendering/glyphs/ScoreSlideLineGlyph'; import { ScoreSlurGlyph } from '@src/rendering/glyphs/ScoreSlurGlyph'; import { ScoreTieGlyph } from '@src/rendering/glyphs/ScoreTieGlyph'; -import { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; export class ScoreBeatContainerGlyph extends BeatContainerGlyph { @@ -17,10 +16,6 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { private _effectSlur: ScoreSlurGlyph | null = null; private _effectEndSlur: ScoreSlurGlyph | null = null; - public constructor(beat: Beat, voiceContainer: VoiceContainerGlyph) { - super(beat, voiceContainer); - } - public override doLayout(): void { this._effectSlur = null; this._effectEndSlur = null; @@ -48,26 +43,26 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { n.tieDestination.isVisible ) { // tslint:disable-next-line: no-unnecessary-type-assertion - let tie: ScoreTieGlyph = new ScoreTieGlyph(n, n.tieDestination!, false); + const tie: ScoreTieGlyph = new ScoreTieGlyph(n, n.tieDestination!, false); this.addTie(tie); } if (n.isTieDestination && !n.tieOrigin!.hasBend && !n.beat.hasWhammyBar) { - let tie: ScoreTieGlyph = new ScoreTieGlyph(n.tieOrigin!, n, true); + const tie: ScoreTieGlyph = new ScoreTieGlyph(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) { - let l: ScoreSlideLineGlyph = new ScoreSlideLineGlyph(n.slideInType, n.slideOutType, n, this); + 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 - let tie: ScoreSlurGlyph = new ScoreSlurGlyph(n, n.slurDestination!, false); + const tie: ScoreSlurGlyph = new ScoreSlurGlyph(n, n.slurDestination!, false); this.addTie(tie); } if (n.isSlurDestination) { - let tie: ScoreSlurGlyph = new ScoreSlurGlyph(n.slurOrigin!, n, true); + const tie: ScoreSlurGlyph = new ScoreSlurGlyph(n.slurOrigin!, n, true); this.addTie(tie); } // start effect slur on first beat @@ -78,10 +73,10 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { } // end effect slur on last beat if (!this._effectEndSlur && n.beat.isEffectSlurDestination && n.beat.effectSlurOrigin) { - let direction: BeamDirection = this.onNotes.beamingHelper.direction; - let startNote: Note = + const direction: BeamDirection = this.onNotes.beamingHelper.direction; + const startNote: Note = direction === BeamDirection.Up ? n.beat.effectSlurOrigin.minNote! : n.beat.effectSlurOrigin.maxNote!; - let endNote: Note = direction === BeamDirection.Up ? n.beat.minNote! : n.beat.maxNote!; + const endNote: Note = direction === BeamDirection.Up ? n.beat.minNote! : n.beat.maxNote!; const effectEndSlur = new ScoreSlurGlyph(startNote, endNote, true); this._effectEndSlur = effectEndSlur; this.addTie(effectEndSlur); diff --git a/src/rendering/ScoreRenderer.ts b/src/rendering/ScoreRenderer.ts index 531df5577..7d47bf348 100644 --- a/src/rendering/ScoreRenderer.ts +++ b/src/rendering/ScoreRenderer.ts @@ -1,14 +1,14 @@ import { LayoutMode } from '@src/LayoutMode'; import { Environment } from '@src/Environment'; -import { EventEmitter, IEventEmitter, IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; -import { Score } from '@src/model/Score'; -import { Track } from '@src/model/Track'; -import { ICanvas } from '@src/platform/ICanvas'; -import { IScoreRenderer } from '@src/rendering/IScoreRenderer'; -import { ScoreLayout } from '@src/rendering/layout/ScoreLayout'; +import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; +import type { Score } from '@src/model/Score'; +import type { Track } from '@src/model/Track'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { IScoreRenderer } from '@src/rendering/IScoreRenderer'; +import type { ScoreLayout } from '@src/rendering/layout/ScoreLayout'; import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; import { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; /** @@ -79,7 +79,7 @@ export class ScoreRenderer implements IScoreRenderer { tracks = score.tracks.slice(0); } else { tracks = []; - for (let track of trackIndexes) { + for (const track of trackIndexes) { if (track >= 0 && track < score.tracks.length) { tracks.push(score.tracks[track]); } @@ -119,10 +119,10 @@ export class ScoreRenderer implements IScoreRenderer { try { const layout = this.layout; if (layout) { - Logger.debug('Rendering', 'Request render of lazy partial ' + resultId); + Logger.debug('Rendering', `Request render of lazy partial ${resultId}`); layout.renderLazyPartial(resultId); } else { - Logger.warning('Rendering', 'Request render of lazy partial ' + resultId + ' ignored, no layout exists'); + Logger.warning('Rendering', `Request render of lazy partial ${resultId} ignored, no layout exists`); } } catch (e) { (this.error as EventEmitterOfT).trigger(e as Error); @@ -147,10 +147,10 @@ export class ScoreRenderer implements IScoreRenderer { (this.postRenderFinished as EventEmitter).trigger(); Logger.debug('Rendering', 'Clearing finished'); } else { - Logger.debug('Rendering', 'Rendering ' + this.tracks.length + ' tracks'); + Logger.debug('Rendering', `Rendering ${this.tracks.length} tracks`); for (let i: number = 0; i < this.tracks.length; i++) { - let track: Track = this.tracks[i]; - Logger.debug('Rendering', 'Track ' + i + ': ' + track.name); + const track: Track = this.tracks[i]; + Logger.debug('Rendering', `Track ${i}: ${track.name}`); } (this.preRender as EventEmitterOfT).trigger(false); this.recreateLayout(); @@ -180,7 +180,7 @@ export class ScoreRenderer implements IScoreRenderer { private layoutAndRender(): void { Logger.debug( 'Rendering', - 'Rendering at scale ' + this.settings.display.scale + ' with layout ' + this.layout!.name, + `Rendering at scale ${this.settings.display.scale} with layout ${this.layout!.name}`, null ); this.layout!.layoutAndRender(); diff --git a/src/rendering/SlashBarRenderer.ts b/src/rendering/SlashBarRenderer.ts index 7e4ee4df6..88e247820 100644 --- a/src/rendering/SlashBarRenderer.ts +++ b/src/rendering/SlashBarRenderer.ts @@ -1,20 +1,22 @@ -import { Bar } from '@src/model/Bar'; -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; -import { Voice } from '@src/model/Voice'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import { type Bar, BarSubElement } from '@src/model/Bar'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import type { Voice } from '@src/model/Voice'; +import type { ICanvas } from '@src/platform/ICanvas'; +import { BarRendererBase, type NoteYPosition } from '@src/rendering/BarRendererBase'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; -import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; -import { LineBarRenderer } from './LineBarRenderer'; -import { SlashNoteHeadGlyph } from './glyphs/SlashNoteHeadGlyph'; -import { SlashBeatContainerGlyph } from './SlashBeatContainerGlyph'; -import { BeatGlyphBase } from './glyphs/BeatGlyphBase'; -import { SlashBeatGlyph } from './glyphs/SlashBeatGlyph'; -import { BeatOnNoteGlyphBase } from './glyphs/BeatOnNoteGlyphBase'; -import { SpacingGlyph } from './glyphs/SpacingGlyph'; -import { ScoreTimeSignatureGlyph } from './glyphs/ScoreTimeSignatureGlyph'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import { LineBarRenderer } from '@src/rendering//LineBarRenderer'; +import { SlashBeatContainerGlyph } from '@src/rendering/SlashBeatContainerGlyph'; +import { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; +import { SlashBeatGlyph } from '@src/rendering/glyphs/SlashBeatGlyph'; +import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; +import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; +import { ScoreTimeSignatureGlyph } from '@src/rendering/glyphs/ScoreTimeSignatureGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; /** * This BarRenderer renders a bar using Slash Rhythm notation @@ -32,6 +34,22 @@ export class SlashBarRenderer extends LineBarRenderer { this.helpers.preferredBeamDirection = BeamDirection.Up; } + public override get repeatsBarSubElement(): BarSubElement { + return BarSubElement.SlashRepeats; + } + + public override get barNumberBarSubElement(): BarSubElement { + return BarSubElement.SlashBarNumber; + } + + public override get barLineBarSubElement(): BarSubElement { + return BarSubElement.SlashBarLines; + } + + public override get staffLineBarSubElement(): BarSubElement { + return BarSubElement.SlashStaffLine; + } + public override get lineSpacing(): number { return BarRendererBase.RawLineSpacing; } @@ -50,16 +68,16 @@ export class SlashBarRenderer extends LineBarRenderer { public override paint(cx: number, cy: number, canvas: ICanvas): void { super.paint(cx, cy, canvas); - this.paintBeams(cx, cy, canvas); - this.paintTuplets(cx, cy, canvas); + this.paintBeams(cx, cy, canvas, BeatSubElement.SlashFlags, BeatSubElement.SlashBeams); + this.paintTuplets(cx, cy, canvas, BeatSubElement.SlashTuplet); } public override doLayout(): void { super.doLayout(); let hasTuplets: boolean = false; - for (let voice of this.bar.voices) { + for (const voice of this.bar.voices) { if (this.hasVoiceContainer(voice)) { - let c = this.getVoiceContainer(voice)!; + const c = this.getVoiceContainer(voice)!; if (c.tupletGroups.length > 0) { hasTuplets = true; break; @@ -76,11 +94,13 @@ export class SlashBarRenderer extends LineBarRenderer { } protected override getFlagTopY(_beat: Beat, _direction: BeamDirection): number { - return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2); + const noteHeadHeight = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.NoteheadSlashWhiteHalf)!; + return this.getLineY(0) - noteHeadHeight / 2; } protected override getFlagBottomY(_beat: Beat, _direction: BeamDirection): number { - return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2); + const noteHeadHeight = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.NoteheadSlashWhiteHalf)!; + return this.getLineY(0) - noteHeadHeight / 2; } protected override getBeamDirection(_helper: BeamingHelper): BeamDirection { @@ -89,7 +109,7 @@ export class SlashBarRenderer extends LineBarRenderer { public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { let y = super.getNoteY(note, requestedPosition); - if (isNaN(y)) { + if (Number.isNaN(y)) { y = this.getLineY(0); } return y; @@ -100,7 +120,8 @@ export class SlashBarRenderer extends LineBarRenderer { } protected override getBarLineStart(_beat: Beat, _direction: BeamDirection): number { - return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2); + const noteHeadHeight = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.NoteheadSlashWhiteHalf)!; + return this.getLineY(0) - noteHeadHeight / 2; } protected override createLinePreBeatGlyphs(): void { @@ -127,37 +148,38 @@ export class SlashBarRenderer extends LineBarRenderer { this.addPreBeatGlyph(new SpacingGlyph(0, 0, 5)); const masterBar = this.bar.masterBar; - this.addPreBeatGlyph( - new ScoreTimeSignatureGlyph( - 0, - this.getLineY(0), - masterBar.timeSignatureNumerator, - masterBar.timeSignatureDenominator, - masterBar.timeSignatureCommon, - masterBar.isFreeTime && - (masterBar.previousMasterBar == null || - masterBar.isFreeTime !== masterBar.previousMasterBar!.isFreeTime) - ) + const g = new ScoreTimeSignatureGlyph( + 0, + this.getLineY(0), + masterBar.timeSignatureNumerator, + masterBar.timeSignatureDenominator, + masterBar.timeSignatureCommon, + masterBar.isFreeTime && + (masterBar.previousMasterBar == null || + masterBar.isFreeTime !== masterBar.previousMasterBar!.isFreeTime) ); + g.barSubElement = BarSubElement.SlashTimeSignature; + this.addPreBeatGlyph(g); } protected override createVoiceGlyphs(v: Voice): void { for (const b of v.beats) { - let container: SlashBeatContainerGlyph = new SlashBeatContainerGlyph(b, this.getVoiceContainer(v)!); + const container: SlashBeatContainerGlyph = new SlashBeatContainerGlyph(b, this.getVoiceContainer(v)!); container.preNotes = new BeatGlyphBase(); - container.onNotes = v.index == 0 ? new SlashBeatGlyph() : new BeatOnNoteGlyphBase(); + container.onNotes = v.index === 0 ? new SlashBeatGlyph() : new BeatOnNoteGlyphBase(); this.addBeatGlyph(container); } } protected override paintBeamingStem( - _beat: Beat, + beat: Beat, _cy: number, x: number, topY: number, bottomY: number, canvas: ICanvas ): void { + using _ = ElementStyleHelper.beat(canvas, BeatSubElement.SlashStem, beat); canvas.lineWidth = BarRendererBase.StemWidth; canvas.beginPath(); canvas.moveTo(x, topY); diff --git a/src/rendering/SlashBarRendererFactory.ts b/src/rendering/SlashBarRendererFactory.ts index e2cb04b2d..942aaa701 100644 --- a/src/rendering/SlashBarRendererFactory.ts +++ b/src/rendering/SlashBarRendererFactory.ts @@ -1,11 +1,11 @@ -import { Bar } from '@src/model/Bar'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Bar } from '@src/model/Bar'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { BarRendererFactory } from '@src/rendering/BarRendererFactory'; import { SlashBarRenderer } from '@src/rendering/SlashBarRenderer'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { Track } from '@src/model/Track'; -import { Staff } from '@src/model'; -import { RenderStaff } from './staves/RenderStaff'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { Track } from '@src/model/Track'; +import type { Staff } from '@src/model/Staff'; +import type { RenderStaff } from '@src/rendering/staves/RenderStaff'; /** * This Factory produces SlashBarRenderer instances @@ -23,7 +23,6 @@ export class SlashBarRendererFactory extends BarRendererFactory { return staff.system.layout.renderer.settings.display.notationStaffPaddingBottom; } - public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { return new SlashBarRenderer(renderer, bar); } @@ -31,8 +30,4 @@ export class SlashBarRendererFactory extends BarRendererFactory { public override canCreate(track: Track, staff: Staff): boolean { return super.canCreate(track, staff) && staff.showSlash; } - - public constructor() { - super(); - } } diff --git a/src/rendering/SlashBeatContainerGlyph.ts b/src/rendering/SlashBeatContainerGlyph.ts index f863f48f1..fb52dcfb0 100644 --- a/src/rendering/SlashBeatContainerGlyph.ts +++ b/src/rendering/SlashBeatContainerGlyph.ts @@ -1,16 +1,10 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; +import type { Note } from '@src/model/Note'; import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; -import { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; -import { SlashTieGlyph } from './glyphs/SlashTieGlyph'; +import { SlashTieGlyph } from '@src/rendering/glyphs/SlashTieGlyph'; export class SlashBeatContainerGlyph extends BeatContainerGlyph { private _tiedNoteTie: SlashTieGlyph | null = null; - public constructor(beat: Beat, voiceContainer: VoiceContainerGlyph) { - super(beat, voiceContainer); - } - protected override createTies(n: Note): void { // create a tie if any effect requires it if (!n.isVisible) { @@ -18,13 +12,13 @@ export class SlashBeatContainerGlyph extends BeatContainerGlyph { } if (!this._tiedNoteTie && n.isTieOrigin && n.tieDestination!.isVisible) { - let tie: SlashTieGlyph = new SlashTieGlyph(n, n.tieDestination!, false); - this._tiedNoteTie= tie; + const tie: SlashTieGlyph = new SlashTieGlyph(n, n.tieDestination!, false); + this._tiedNoteTie = tie; this.addTie(tie); } if (!this._tiedNoteTie && n.isTieDestination) { - let tie: SlashTieGlyph = new SlashTieGlyph(n.tieOrigin!, n, true); - this._tiedNoteTie= tie; + const tie: SlashTieGlyph = new SlashTieGlyph(n.tieOrigin!, n, true); + this._tiedNoteTie = tie; this.addTie(tie); } } diff --git a/src/rendering/TabBarRenderer.ts b/src/rendering/TabBarRenderer.ts index ed5c720bf..2e22b65e0 100644 --- a/src/rendering/TabBarRenderer.ts +++ b/src/rendering/TabBarRenderer.ts @@ -1,24 +1,26 @@ -import { Bar } from '@src/model/Bar'; -import { Beat } from '@src/model/Beat'; +import { type Bar, BarSubElement } from '@src/model/Bar'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; import { Duration } from '@src/model/Duration'; -import { Voice } from '@src/model/Voice'; +import type { Voice } from '@src/model/Voice'; import { TabRhythmMode } from '@src/NotationSettings'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; import { TabBeatContainerGlyph } from '@src/rendering/glyphs/TabBeatContainerGlyph'; import { TabBeatGlyph } from '@src/rendering/glyphs/TabBeatGlyph'; import { TabBeatPreNotesGlyph } from '@src/rendering/glyphs/TabBeatPreNotesGlyph'; import { TabClefGlyph } from '@src/rendering/glyphs/TabClefGlyph'; -import { TabNoteChordGlyph } from '@src/rendering/glyphs/TabNoteChordGlyph'; +import type { TabNoteChordGlyph } from '@src/rendering/glyphs/TabNoteChordGlyph'; import { TabTimeSignatureGlyph } from '@src/rendering/glyphs/TabTimeSignatureGlyph'; -import { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; -import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; -import { LineBarRenderer } from './LineBarRenderer'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import { LineBarRenderer } from '@src/rendering/LineBarRenderer'; import { GraceType } from '@src/model/GraceType'; -import { ReservedLayoutAreaSlot } from './utils/BarCollisionHelper'; +import type { ReservedLayoutAreaSlot } from '@src/rendering/utils/BarCollisionHelper'; +import { MultiBarRestBeatContainerGlyph } from '@src/rendering/MultiBarRestBeatContainerGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; /** * This BarRenderer renders a bar using guitar tablature notation @@ -33,6 +35,28 @@ export class TabBarRenderer extends LineBarRenderer { public showRests: boolean = false; public showTiedNotes: boolean = false; + private _showMultiBarRest: boolean = false; + + public override get showMultiBarRest(): boolean { + return this._showMultiBarRest; + } + + public override get repeatsBarSubElement(): BarSubElement { + return BarSubElement.GuitarTabsRepeats; + } + + public override get barNumberBarSubElement(): BarSubElement { + return BarSubElement.GuitarTabsBarNumber; + } + + public override get barLineBarSubElement(): BarSubElement { + return BarSubElement.GuitarTabsBarLines; + } + + public override get staffLineBarSubElement(): BarSubElement { + return BarSubElement.GuitarTabsStaffLine; + } + public constructor(renderer: ScoreRenderer, bar: Bar) { super(renderer, bar); @@ -40,6 +64,7 @@ export class TabBarRenderer extends LineBarRenderer { this.showTimeSignature = true; this.showRests = true; this.showTiedNotes = true; + this._showMultiBarRest = true; } } @@ -51,7 +76,7 @@ export class TabBarRenderer extends LineBarRenderer { return this.bar.staff.tuning.length; } - public override get drawnLineCount(): number { + public override get drawnLineCount(): number { return this.bar.staff.tuning.length; } @@ -79,12 +104,12 @@ export class TabBarRenderer extends LineBarRenderer { protected override collectSpaces(spaces: Float32Array[][]): void { const padding: number = 1; - for (let voice of this.bar.voices) { + for (const voice of this.bar.voices) { if (this.hasVoiceContainer(voice)) { - let vc: VoiceContainerGlyph = this.getVoiceContainer(voice)!; - for (let bg of vc.beatGlyphs) { - let notes: TabBeatGlyph = bg.onNotes as TabBeatGlyph; - let noteNumbers: TabNoteChordGlyph | null = notes.noteNumbers; + 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) { @@ -113,9 +138,9 @@ export class TabBarRenderer extends LineBarRenderer { super.doLayout(); if (this.rhythmMode !== TabRhythmMode.Hidden) { this._hasTuplets = false; - for (let voice of this.bar.voices) { + for (const voice of this.bar.voices) { if (this.hasVoiceContainer(voice)) { - let c: VoiceContainerGlyph = this.getVoiceContainer(voice)!; + const c: VoiceContainerGlyph = this.getVoiceContainer(voice)!; if (c.tupletGroups.length > 0) { this._hasTuplets = true; break; @@ -131,7 +156,7 @@ export class TabBarRenderer extends LineBarRenderer { protected override createLinePreBeatGlyphs(): void { // Clef if (this.isFirstOfLine) { - let center: number = (this.bar.staff.tuning.length - 1) / 2; + const center: number = (this.bar.staff.tuning.length - 1) / 2; this.addPreBeatGlyph(new TabClefGlyph(5, this.getTabY(center))); } // Time Signature @@ -170,19 +195,25 @@ export class TabBarRenderer extends LineBarRenderer { } protected override createVoiceGlyphs(v: Voice): void { - for (const b of v.beats) { - let container: TabBeatContainerGlyph = new TabBeatContainerGlyph(b, this.getVoiceContainer(v)!); - container.preNotes = new TabBeatPreNotesGlyph(); - container.onNotes = new TabBeatGlyph(); + // 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); + } } } 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); - this.paintTuplets(cx, cy, canvas); + this.paintBeams(cx, cy, canvas, BeatSubElement.GuitarTabFlags, BeatSubElement.GuitarTabBeams); + this.paintTuplets(cx, cy, canvas, BeatSubElement.GuitarTabTuplet); } } @@ -194,9 +225,8 @@ export class TabBarRenderer extends LineBarRenderer { const startGlyph: TabBeatGlyph = this.getOnNotesGlyphForBeat(beat) as TabBeatGlyph; if (!startGlyph.noteNumbers || beat.duration === Duration.Half) { return this.height - this.settings.notation.rhythmHeight - this.tupletSize; - } else { - return startGlyph.noteNumbers.getNoteY(startGlyph.noteNumbers.minStringNote!, NoteYPosition.Bottom); } + return startGlyph.noteNumbers.getNoteY(startGlyph.noteNumbers.minStringNote!, NoteYPosition.Bottom); } protected override getFlagBottomY(_beat: Beat, _direction: BeamDirection): number { @@ -208,15 +238,14 @@ export class TabBarRenderer extends LineBarRenderer { } protected override getBarLineStart(beat: Beat, direction: BeamDirection): number { - let startGlyph: TabBeatGlyph = this.getOnNotesGlyphForBeat(beat) as TabBeatGlyph; + const startGlyph: TabBeatGlyph = this.getOnNotesGlyphForBeat(beat) as TabBeatGlyph; if (!startGlyph.noteNumbers || beat.duration === Duration.Half) { return this.height - this.settings.notation.rhythmHeight - this.tupletSize; - } else { - return ( - startGlyph.noteNumbers.getNoteY(startGlyph.noteNumbers.minStringNote!, NoteYPosition.Bottom) + - this.lineOffset / 2 - ); } + return ( + startGlyph.noteNumbers.getNoteY(startGlyph.noteNumbers.minStringNote!, NoteYPosition.Bottom) + + this.lineOffset / 2 + ); } protected override getBeamDirection(_helper: BeamingHelper): BeamDirection { @@ -258,6 +287,8 @@ export class TabBarRenderer extends LineBarRenderer { topY = t; } + using _ = ElementStyleHelper.beat(canvas, BeatSubElement.GuitarTabStem, beat); + canvas.lineWidth = BarRendererBase.StemWidth; canvas.beginPath(); diff --git a/src/rendering/TabBarRendererFactory.ts b/src/rendering/TabBarRendererFactory.ts index e6914b43d..dd4602e2b 100644 --- a/src/rendering/TabBarRendererFactory.ts +++ b/src/rendering/TabBarRendererFactory.ts @@ -1,19 +1,19 @@ -import { Bar } from '@src/model/Bar'; -import { Staff } from '@src/model/Staff'; -import { Track } from '@src/model/Track'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Bar } from '@src/model/Bar'; +import type { Staff } from '@src/model/Staff'; +import type { Track } from '@src/model/Track'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { BarRendererFactory } from '@src/rendering/BarRendererFactory'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; -import { RenderStaff } from './staves/RenderStaff'; +import type { RenderStaff } from '@src/rendering/staves/RenderStaff'; /** * This Factory produces TabBarRenderer instances */ export class TabBarRendererFactory extends BarRendererFactory { public showTimeSignature: boolean | null = null; - public showRests: boolean| null = null; - public showTiedNotes: boolean| null = null; + public showRests: boolean | null = null; + public showTiedNotes: boolean | null = null; public get staffId(): string { return TabBarRenderer.StaffId; @@ -37,14 +37,14 @@ export class TabBarRendererFactory extends BarRendererFactory { } public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { - let tabBarRenderer: TabBarRenderer = new TabBarRenderer(renderer, bar); - if(this.showRests !== null){ + const tabBarRenderer: TabBarRenderer = new TabBarRenderer(renderer, bar); + if (this.showRests !== null) { tabBarRenderer.showRests = this.showRests!; } - if(this.showTimeSignature !== null){ + if (this.showTimeSignature !== null) { tabBarRenderer.showTimeSignature = this.showTimeSignature!; } - if(this.showTiedNotes !== null){ + if (this.showTiedNotes !== null) { tabBarRenderer.showTiedNotes = this.showTiedNotes!; } return tabBarRenderer; diff --git a/src/rendering/index.ts b/src/rendering/_barrel.ts similarity index 71% rename from src/rendering/index.ts rename to src/rendering/_barrel.ts index 4f5666307..a65e9c93f 100644 --- a/src/rendering/index.ts +++ b/src/rendering/_barrel.ts @@ -1,5 +1,5 @@ export { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; -export { type IScoreRenderer } from '@src/rendering/IScoreRenderer'; +export type { IScoreRenderer } from '@src/rendering/IScoreRenderer'; export { ScoreRenderer } from '@src/rendering/ScoreRenderer'; export { BarBounds } from '@src/rendering/utils/BarBounds'; @@ -8,4 +8,5 @@ export { Bounds } from '@src/rendering/utils/Bounds'; export { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; export { MasterBarBounds } from '@src/rendering/utils/MasterBarBounds'; export { NoteBounds } from '@src/rendering/utils/NoteBounds'; -export { StaffSystemBounds as StaffSystemBounds } from '@src/rendering/utils/StaffSystemBounds'; +export { StaffSystemBounds } from '@src/rendering/utils/StaffSystemBounds'; +export { BeamDirection } from '@src/rendering/utils/BeamDirection'; diff --git a/src/rendering/effects/AlternateEndingsEffectInfo.ts b/src/rendering/effects/AlternateEndingsEffectInfo.ts index c4c759010..0a06b28a9 100644 --- a/src/rendering/effects/AlternateEndingsEffectInfo.ts +++ b/src/rendering/effects/AlternateEndingsEffectInfo.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { AlternateEndingsGlyph } from '@src/rendering/glyphs/AlternateEndingsGlyph'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class AlternateEndingsEffectInfo extends EffectBarRendererInfo { @@ -29,7 +29,20 @@ export class AlternateEndingsEffectInfo extends EffectBarRendererInfo { } public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { - return new AlternateEndingsGlyph(0, 0, beat.voice.bar.masterBar.alternateEndings); + const masterBar = beat.voice.bar.masterBar; + const openLine = + masterBar.previousMasterBar === null || + masterBar.alternateEndings !== masterBar.previousMasterBar!.alternateEndings; + let closeLine = + masterBar.isRepeatEnd || + masterBar.nextMasterBar === null || + masterBar.alternateEndings !== masterBar.nextMasterBar!.alternateEndings; + + if (!masterBar.repeatGroup.closings.some(c => c.index >= masterBar.index)) { + closeLine = false; + } + + return new AlternateEndingsGlyph(0, 0, masterBar.alternateEndings, openLine, closeLine); } public canExpand(from: Beat, to: Beat): boolean { diff --git a/src/rendering/effects/BeatBarreEffectInfo.ts b/src/rendering/effects/BeatBarreEffectInfo.ts index 4ca31ff29..6318a580f 100644 --- a/src/rendering/effects/BeatBarreEffectInfo.ts +++ b/src/rendering/effects/BeatBarreEffectInfo.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@src/rendering/glyphs/LineRangedGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; import { BarreShape } from '@src/model/BarreShape'; @@ -40,7 +40,7 @@ export class BeatBarreEffectInfo extends EffectBarRendererInfo { break; } - barre += 'B ' + BeatBarreEffectInfo.toRoman(beat.barreFret); + barre += `B ${BeatBarreEffectInfo.toRoman(beat.barreFret)}`; return new LineRangedGlyph(barre, false); } @@ -65,7 +65,7 @@ export class BeatBarreEffectInfo extends EffectBarRendererInfo { let str = ''; if (num > 0) { - for (var [romanLetter, romanNumber] of BeatBarreEffectInfo.RomanLetters) { + for (const [romanLetter, romanNumber] of BeatBarreEffectInfo.RomanLetters) { const q = Math.floor(num / romanNumber); num -= q * romanNumber; str += romanLetter.repeat(q); diff --git a/src/rendering/effects/BeatTimerEffectInfo.ts b/src/rendering/effects/BeatTimerEffectInfo.ts index 601b91dc7..196bb5c32 100644 --- a/src/rendering/effects/BeatTimerEffectInfo.ts +++ b/src/rendering/effects/BeatTimerEffectInfo.ts @@ -1,11 +1,11 @@ import { NotationElement } from '@src/NotationSettings'; -import { EffectBarRendererInfo } from '../EffectBarRendererInfo'; -import { EffectBarGlyphSizing } from '../EffectBarGlyphSizing'; -import { Settings } from '@src/Settings'; -import { Beat } from '@src/model'; -import { BarRendererBase } from '../BarRendererBase'; -import { EffectGlyph } from '../glyphs/EffectGlyph'; -import { BeatTimerGlyph } from '../glyphs/BeatTimerGlyph'; +import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; +import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; +import type { Settings } from '@src/Settings'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { BeatTimerGlyph } from '@src/rendering/glyphs/BeatTimerGlyph'; export class BeatTimerEffectInfo extends EffectBarRendererInfo { public get notationElement(): NotationElement { diff --git a/src/rendering/effects/CapoEffectInfo.ts b/src/rendering/effects/CapoEffectInfo.ts index 851794257..3aedc9dcb 100644 --- a/src/rendering/effects/CapoEffectInfo.ts +++ b/src/rendering/effects/CapoEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { TextAlign } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class CapoEffectInfo extends EffectBarRendererInfo { @@ -33,7 +33,7 @@ export class CapoEffectInfo extends EffectBarRendererInfo { return new TextGlyph( 0, 0, - 'Capo. fret ' + beat.voice.bar.staff.capo, + `Capo. fret ${beat.voice.bar.staff.capo}`, renderer.resources.effectFont, TextAlign.Left ); diff --git a/src/rendering/effects/ChordsEffectInfo.ts b/src/rendering/effects/ChordsEffectInfo.ts index 2a0c5a697..014dfe1c6 100644 --- a/src/rendering/effects/ChordsEffectInfo.ts +++ b/src/rendering/effects/ChordsEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { TextAlign } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class ChordsEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/CrescendoEffectInfo.ts b/src/rendering/effects/CrescendoEffectInfo.ts index 70bcfe4ee..b6a195589 100644 --- a/src/rendering/effects/CrescendoEffectInfo.ts +++ b/src/rendering/effects/CrescendoEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { CrescendoType } from '@src/model/CrescendoType'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { CrescendoGlyph } from '@src/rendering/glyphs/CrescendoGlyph'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class CrescendoEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/DirectionsEffectInfo.ts b/src/rendering/effects/DirectionsEffectInfo.ts index 088090a08..1ae43e7ab 100644 --- a/src/rendering/effects/DirectionsEffectInfo.ts +++ b/src/rendering/effects/DirectionsEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; -import { DirectionsContainerGlyph } from '../glyphs/DirectionsContainerGlyph'; +import { DirectionsContainerGlyph } from '@src/rendering/glyphs/DirectionsContainerGlyph'; export class DirectionsEffectInfo extends EffectBarRendererInfo { public get notationElement(): NotationElement { diff --git a/src/rendering/effects/DummyEffectGlyph.ts b/src/rendering/effects/DummyEffectGlyph.ts index 54f8139a7..8ae037850 100644 --- a/src/rendering/effects/DummyEffectGlyph.ts +++ b/src/rendering/effects/DummyEffectGlyph.ts @@ -1,10 +1,10 @@ import { Color } from '@src/model/Color'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; export class DummyEffectGlyph extends EffectGlyph { - private _w:number; - private _h:number; + private _w: number; + private _h: number; public constructor(x: number, y: number, w: number = 20, h: number = 20) { super(x, y); @@ -18,7 +18,7 @@ export class DummyEffectGlyph extends EffectGlyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let c = canvas.color; + const c = canvas.color; canvas.color = Color.random(); canvas.fillRect(cx + this.x, cy + this.y, this.width, this.height); canvas.color = c; diff --git a/src/rendering/effects/DynamicsEffectInfo.ts b/src/rendering/effects/DynamicsEffectInfo.ts index 7930d55a0..c1f14af3e 100644 --- a/src/rendering/effects/DynamicsEffectInfo.ts +++ b/src/rendering/effects/DynamicsEffectInfo.ts @@ -1,16 +1,15 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { DynamicsGlyph } from '@src/rendering/glyphs/DynamicsGlyph'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; -import { GraceType } from '@src/model/GraceType'; export class DynamicsEffectInfo extends EffectBarRendererInfo { public get notationElement(): NotationElement { - return NotationElement.EffectDynamics + return NotationElement.EffectDynamics; } public get hideOnMultiTrack(): boolean { @@ -30,20 +29,23 @@ export class DynamicsEffectInfo extends EffectBarRendererInfo { } private internalShouldCreateGlyph(beat: Beat): boolean { - if (beat.voice.bar.staff.track.score.stylesheet.hideDynamics || beat.isEmpty || beat.voice.isEmpty || beat.isRest || beat.graceType !== GraceType.None) { + if ( + beat.voice.bar.staff.track.score.stylesheet.hideDynamics || + beat.isEmpty || + beat.voice.isEmpty || + beat.isRest + ) { return false; } - let previousBeat = this.getPreviousDynamicsBeat(beat); + const previousBeat = this.getPreviousDynamicsBeat(beat); - let show: boolean = - (beat.voice.index === 0 && !previousBeat) || - (beat.dynamics !== previousBeat?.dynamics); + let show: boolean = (beat.voice.index === 0 && !previousBeat) || beat.dynamics !== previousBeat?.dynamics; // ensure we do not show duplicate dynamics if (show && beat.voice.index > 0) { - for (let voice of beat.voice.bar.voices) { + for (const voice of beat.voice.bar.voices) { if (voice.index < beat.voice.index) { - let beatAtSamePos = voice.getBeatAtPlaybackStart(beat.playbackStart); + const beatAtSamePos = voice.getBeatAtPlaybackStart(beat.playbackStart); if ( beatAtSamePos && beat.dynamics === beatAtSamePos.dynamics && @@ -59,7 +61,7 @@ export class DynamicsEffectInfo extends EffectBarRendererInfo { private getPreviousDynamicsBeat(beat: Beat) { let previousBeat = beat.previousBeat; while (previousBeat != null) { - if (!previousBeat.isRest && previousBeat.graceType === GraceType.None) { + if (!previousBeat.isRest) { return previousBeat; } previousBeat = previousBeat.previousBeat; diff --git a/src/rendering/effects/FadeEffectInfo.ts b/src/rendering/effects/FadeEffectInfo.ts index 5d6c68a55..6a7f2a63e 100644 --- a/src/rendering/effects/FadeEffectInfo.ts +++ b/src/rendering/effects/FadeEffectInfo.ts @@ -1,12 +1,12 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; import { FadeType } from '@src/model/FadeType'; -import { FadeGlyph } from '../glyphs/FadeGlyph'; +import { FadeGlyph } from '@src/rendering/glyphs/FadeGlyph'; export class FadeEffectInfo extends EffectBarRendererInfo { public get notationElement(): NotationElement { @@ -26,7 +26,7 @@ export class FadeEffectInfo extends EffectBarRendererInfo { } public shouldCreateGlyph(settings: Settings, beat: Beat): boolean { - return beat.fade != FadeType.None; + return beat.fade !== FadeType.None; } public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { diff --git a/src/rendering/effects/FermataEffectInfo.ts b/src/rendering/effects/FermataEffectInfo.ts index 3aa862b24..c9565b823 100644 --- a/src/rendering/effects/FermataEffectInfo.ts +++ b/src/rendering/effects/FermataEffectInfo.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { FermataGlyph } from '@src/rendering/glyphs/FermataGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class FermataEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/FingeringEffectInfo.ts b/src/rendering/effects/FingeringEffectInfo.ts index f8c589efd..0b75429a1 100644 --- a/src/rendering/effects/FingeringEffectInfo.ts +++ b/src/rendering/effects/FingeringEffectInfo.ts @@ -1,14 +1,14 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { Fingers } from '@src/model/Fingers'; -import { Note } from '@src/model/Note'; +import type { Note } from '@src/model/Note'; import { FingeringMode, NotationElement } from '@src/NotationSettings'; import { TextAlign } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { ModelUtils } from '@src/model/ModelUtils'; export class FingeringEffectInfo extends EffectBarRendererInfo { @@ -46,14 +46,14 @@ export class FingeringEffectInfo extends EffectBarRendererInfo { public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { let finger: Fingers = Fingers.Unknown; let isLeft: boolean = false; - let note: Note = beat.notes[0]; + const note: Note = beat.notes[0]; if (note.leftHandFinger !== Fingers.Unknown) { finger = note.leftHandFinger; isLeft = true; } else if (note.rightHandFinger !== Fingers.Unknown) { finger = note.rightHandFinger; } - let s: string = ModelUtils.fingerToString(renderer.settings, beat, finger, isLeft) ?? ""; + const s: string = ModelUtils.fingerToString(renderer.settings, beat, finger, isLeft) ?? ''; return new TextGlyph(0, 0, s, renderer.resources.fingeringFont, TextAlign.Left); } diff --git a/src/rendering/effects/FreeTimeEffectInfo.ts b/src/rendering/effects/FreeTimeEffectInfo.ts index 300df46a3..4e7f1f97c 100644 --- a/src/rendering/effects/FreeTimeEffectInfo.ts +++ b/src/rendering/effects/FreeTimeEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { TextAlign } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class FreeTimeEffectInfo extends EffectBarRendererInfo { @@ -31,7 +31,7 @@ export class FreeTimeEffectInfo extends EffectBarRendererInfo { return ( isFirstBeat && masterBar.isFreeTime && - (masterBar.index === 0 || masterBar.isFreeTime != masterBar.previousMasterBar!.isFreeTime) + (masterBar.index === 0 || masterBar.isFreeTime !== masterBar.previousMasterBar!.isFreeTime) ); } diff --git a/src/rendering/effects/GolpeEffectInfo.ts b/src/rendering/effects/GolpeEffectInfo.ts index 64bced15d..03b8371f1 100644 --- a/src/rendering/effects/GolpeEffectInfo.ts +++ b/src/rendering/effects/GolpeEffectInfo.ts @@ -1,12 +1,12 @@ import { NotationElement } from '@src/NotationSettings'; -import { EffectBarGlyphSizing } from '../EffectBarGlyphSizing'; -import { Settings } from '@src/Settings'; -import { Beat } from '@src/model'; -import { GolpeType } from '@src/model/GolpeType'; -import { EffectBarRendererInfo } from '../EffectBarRendererInfo'; -import { BarRendererBase } from '../BarRendererBase'; -import { EffectGlyph } from '../glyphs/EffectGlyph'; -import { GuitarGolpeGlyph } from '../glyphs/GuitarGolpeGlyph'; +import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; +import type { Settings } from '@src/Settings'; +import type { Beat } from '@src/model/Beat'; +import type { GolpeType } from '@src/model/GolpeType'; +import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { GuitarGolpeGlyph } from '@src/rendering/glyphs/GuitarGolpeGlyph'; export class GolpeEffectInfo extends EffectBarRendererInfo { private _type: GolpeType; @@ -36,7 +36,7 @@ export class GolpeEffectInfo extends EffectBarRendererInfo { public shouldCreateGlyph(settings: Settings, beat: Beat): boolean { const shouldCreate = this._shouldCreate; - return beat.golpe == this._type && (!shouldCreate || shouldCreate(settings, beat)); + return beat.golpe === this._type && (!shouldCreate || shouldCreate(settings, beat)); } public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { diff --git a/src/rendering/effects/HarmonicsEffectInfo.ts b/src/rendering/effects/HarmonicsEffectInfo.ts index 74b38b181..cfa8149f6 100644 --- a/src/rendering/effects/HarmonicsEffectInfo.ts +++ b/src/rendering/effects/HarmonicsEffectInfo.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { HarmonicType } from '@src/model/HarmonicType'; -import { Note } from '@src/model/Note'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Note } from '@src/model/Note'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { NoteEffectInfoBase } from '@src/rendering/effects/NoteEffectInfoBase'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@src/rendering/glyphs/LineRangedGlyph'; import { NotationElement } from '@src/NotationSettings'; diff --git a/src/rendering/effects/LeftHandTapEffectInfo.ts b/src/rendering/effects/LeftHandTapEffectInfo.ts index 95862f167..7cb22ff9c 100644 --- a/src/rendering/effects/LeftHandTapEffectInfo.ts +++ b/src/rendering/effects/LeftHandTapEffectInfo.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { NotationElement } from '@src/NotationSettings'; import { NoteEffectInfoBase } from '@src/rendering/effects/NoteEffectInfoBase'; -import { Note } from '@src/model/Note'; +import type { Note } from '@src/model/Note'; import { LeftHandTapGlyph } from '@src/rendering/glyphs/LeftHandTapGlyph'; export class LeftHandTapEffectInfo extends NoteEffectInfoBase { diff --git a/src/rendering/effects/LetRingEffectInfo.ts b/src/rendering/effects/LetRingEffectInfo.ts index 822be0589..3dd4c9082 100644 --- a/src/rendering/effects/LetRingEffectInfo.ts +++ b/src/rendering/effects/LetRingEffectInfo.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@src/rendering/glyphs/LineRangedGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class LetRingEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/LyricsEffectInfo.ts b/src/rendering/effects/LyricsEffectInfo.ts index 2b9446e92..c5ad02883 100644 --- a/src/rendering/effects/LyricsEffectInfo.ts +++ b/src/rendering/effects/LyricsEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { TextAlign } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { LyricsGlyph } from '@src/rendering/glyphs/LyricsGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class LyricsEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/MarkerEffectInfo.ts b/src/rendering/effects/MarkerEffectInfo.ts index 44c1d20bd..c226f368f 100644 --- a/src/rendering/effects/MarkerEffectInfo.ts +++ b/src/rendering/effects/MarkerEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { TextAlign } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class MarkerEffectInfo extends EffectBarRendererInfo { @@ -40,7 +40,7 @@ export class MarkerEffectInfo extends EffectBarRendererInfo { 0, !beat.voice.bar.masterBar.section!.marker ? beat.voice.bar.masterBar.section!.text - : '[' + 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, TextAlign.Left ); diff --git a/src/rendering/effects/NoteEffectInfoBase.ts b/src/rendering/effects/NoteEffectInfoBase.ts index 14cfb7fa1..c03be3ab5 100644 --- a/src/rendering/effects/NoteEffectInfoBase.ts +++ b/src/rendering/effects/NoteEffectInfoBase.ts @@ -1,7 +1,7 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; export abstract class NoteEffectInfoBase extends EffectBarRendererInfo { protected lastCreateInfo: Note[] | null = null; @@ -9,7 +9,7 @@ export abstract class NoteEffectInfoBase extends EffectBarRendererInfo { public shouldCreateGlyph(settings: Settings, beat: Beat): boolean { this.lastCreateInfo = []; for (let i: number = 0, j: number = beat.notes.length; i < j; i++) { - let n: Note = beat.notes[i]; + const n: Note = beat.notes[i]; if (this.shouldCreateGlyphForNote(n)) { this.lastCreateInfo.push(n); } diff --git a/src/rendering/effects/NoteOrnamentEffectInfo.ts b/src/rendering/effects/NoteOrnamentEffectInfo.ts index 5afcb69a4..591c751f8 100644 --- a/src/rendering/effects/NoteOrnamentEffectInfo.ts +++ b/src/rendering/effects/NoteOrnamentEffectInfo.ts @@ -1,12 +1,12 @@ -import { Beat } from '@src/model'; -import { NotationElement } from '@src/NotationSettings'; -import { BarRendererBase } from '../BarRendererBase'; -import { EffectBarGlyphSizing } from '../EffectBarGlyphSizing'; -import { EffectBarRendererInfo } from '../EffectBarRendererInfo'; -import { EffectGlyph } from '../glyphs/EffectGlyph'; -import { Settings } from '@src/Settings'; +import type { Beat } from '@src/model/Beat'; import { NoteOrnament } from '@src/model/NoteOrnament'; -import { NoteOrnamentGlyph } from '../glyphs/NoteOrnamentGlyph'; +import { NotationElement } from '@src/NotationSettings'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; +import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { NoteOrnamentGlyph } from '@src/rendering/glyphs/NoteOrnamentGlyph'; +import type { Settings } from '@src/Settings'; export class NoteOrnamentEffectInfo extends EffectBarRendererInfo { public get notationElement(): NotationElement { @@ -30,7 +30,7 @@ export class NoteOrnamentEffectInfo extends EffectBarRendererInfo { } public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { - return new NoteOrnamentGlyph(beat.notes.find(n=>n.ornament != NoteOrnament.None)!.ornament); + return new NoteOrnamentGlyph(beat.notes.find(n => n.ornament !== NoteOrnament.None)!.ornament); } public canExpand(from: Beat, to: Beat): boolean { diff --git a/src/rendering/effects/OttaviaEffectInfo.ts b/src/rendering/effects/OttaviaEffectInfo.ts index a95098696..d91cba713 100644 --- a/src/rendering/effects/OttaviaEffectInfo.ts +++ b/src/rendering/effects/OttaviaEffectInfo.ts @@ -1,18 +1,18 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { Ottavia } from '@src/model/Ottavia'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { OttavaGlyph } from '@src/rendering/glyphs/OttavaGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class OttaviaEffectInfo extends EffectBarRendererInfo { private _aboveStaff: boolean; public override get effectId(): string { - return 'ottavia-' + (this._aboveStaff ? 'above' : 'below'); + return `ottavia-${this._aboveStaff ? 'above' : 'below'}`; } public get notationElement(): NotationElement { diff --git a/src/rendering/effects/PalmMuteEffectInfo.ts b/src/rendering/effects/PalmMuteEffectInfo.ts index 6c9103095..83fd79998 100644 --- a/src/rendering/effects/PalmMuteEffectInfo.ts +++ b/src/rendering/effects/PalmMuteEffectInfo.ts @@ -1,9 +1,9 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { NoteEffectInfoBase } from '@src/rendering/effects/NoteEffectInfoBase'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@src/rendering/glyphs/LineRangedGlyph'; import { NotationElement } from '@src/NotationSettings'; @@ -23,8 +23,4 @@ export class PalmMuteEffectInfo extends NoteEffectInfoBase { public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { return new LineRangedGlyph('P.M.'); } - - public constructor() { - super(); - } } diff --git a/src/rendering/effects/PickSlideEffectInfo.ts b/src/rendering/effects/PickSlideEffectInfo.ts index f6337dd5b..a3b3460c4 100644 --- a/src/rendering/effects/PickSlideEffectInfo.ts +++ b/src/rendering/effects/PickSlideEffectInfo.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; import { SlideOutType } from '@src/model/SlideOutType'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { NoteEffectInfoBase } from '@src/rendering/effects/NoteEffectInfoBase'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@src/rendering/glyphs/LineRangedGlyph'; import { NotationElement } from '@src/NotationSettings'; @@ -24,8 +24,4 @@ export class PickSlideEffectInfo extends NoteEffectInfoBase { public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { return new LineRangedGlyph('P.S.'); } - - public constructor() { - super(); - } } diff --git a/src/rendering/effects/PickStrokeEffectInfo.ts b/src/rendering/effects/PickStrokeEffectInfo.ts index 288c27cdd..114df74ab 100644 --- a/src/rendering/effects/PickStrokeEffectInfo.ts +++ b/src/rendering/effects/PickStrokeEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { PickStroke } from '@src/model/PickStroke'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { PickStrokeGlyph } from '@src/rendering/glyphs/PickStrokeGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class PickStrokeEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/RasgueadoEffectInfo.ts b/src/rendering/effects/RasgueadoEffectInfo.ts index 141b8a54b..7a12d5d4e 100644 --- a/src/rendering/effects/RasgueadoEffectInfo.ts +++ b/src/rendering/effects/RasgueadoEffectInfo.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@src/rendering/glyphs/LineRangedGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class RasgueadoEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/SlightBeatVibratoEffectInfo.ts b/src/rendering/effects/SlightBeatVibratoEffectInfo.ts index a4028c44d..30cc0518a 100644 --- a/src/rendering/effects/SlightBeatVibratoEffectInfo.ts +++ b/src/rendering/effects/SlightBeatVibratoEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { VibratoType } from '@src/model/VibratoType'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { BeatVibratoGlyph } from '@src/rendering/glyphs/BeatVibratoGlyph'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class SlightBeatVibratoEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/SlightNoteVibratoEffectInfo.ts b/src/rendering/effects/SlightNoteVibratoEffectInfo.ts index a13601bef..f751edb85 100644 --- a/src/rendering/effects/SlightNoteVibratoEffectInfo.ts +++ b/src/rendering/effects/SlightNoteVibratoEffectInfo.ts @@ -1,15 +1,14 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; import { VibratoType } from '@src/model/VibratoType'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { NoteEffectInfoBase } from '@src/rendering/effects/NoteEffectInfoBase'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { NoteVibratoGlyph } from '@src/rendering/glyphs/NoteVibratoGlyph'; import { NotationElement } from '@src/NotationSettings'; export class SlightNoteVibratoEffectInfo extends NoteEffectInfoBase { - // for tied bends ending in a vibrato, the vibrato is drawn by the TabBendGlyph for proper alignment private _hideOnTiedBend: boolean; diff --git a/src/rendering/effects/SustainPedalEffectInfo.ts b/src/rendering/effects/SustainPedalEffectInfo.ts index 9a4824872..3a7a7bffa 100644 --- a/src/rendering/effects/SustainPedalEffectInfo.ts +++ b/src/rendering/effects/SustainPedalEffectInfo.ts @@ -1,11 +1,11 @@ import { NotationElement } from '@src/NotationSettings'; -import { EffectBarGlyphSizing } from '../EffectBarGlyphSizing'; -import { Settings } from '@src/Settings'; -import { Beat } from '@src/model'; -import { BarRendererBase } from '../BarRendererBase'; -import { EffectGlyph } from '../glyphs/EffectGlyph'; -import { EffectBarRendererInfo } from '../EffectBarRendererInfo'; -import { SustainPedalGlyph } from '../glyphs/SustainPedalGlyph'; +import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; +import type { Settings } from '@src/Settings'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; +import { SustainPedalGlyph } from '@src/rendering/glyphs/SustainPedalGlyph'; export class SustainPedalEffectInfo extends EffectBarRendererInfo { public get notationElement(): NotationElement { diff --git a/src/rendering/effects/TapEffectInfo.ts b/src/rendering/effects/TapEffectInfo.ts index cd78237af..d511dc472 100644 --- a/src/rendering/effects/TapEffectInfo.ts +++ b/src/rendering/effects/TapEffectInfo.ts @@ -1,12 +1,12 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { TextAlign } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { RenderingResources } from '@src/RenderingResources'; -import { Settings } from '@src/Settings'; +import type { RenderingResources } from '@src/RenderingResources'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class TapEffectInfo extends EffectBarRendererInfo { @@ -31,7 +31,7 @@ export class TapEffectInfo extends EffectBarRendererInfo { } public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { - let res: RenderingResources = renderer.resources; + const res: RenderingResources = renderer.resources; if (beat.slap) { return new TextGlyph(0, 0, 'S', res.effectFont, TextAlign.Left); } diff --git a/src/rendering/effects/TempoEffectInfo.ts b/src/rendering/effects/TempoEffectInfo.ts index 855d8ea2c..e45a22498 100644 --- a/src/rendering/effects/TempoEffectInfo.ts +++ b/src/rendering/effects/TempoEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; -import { BarTempoGlyph } from '../glyphs/BarTempoGlyph'; +import { BarTempoGlyph } from '@src/rendering/glyphs/BarTempoGlyph'; export class TempoEffectInfo extends EffectBarRendererInfo { public get notationElement(): NotationElement { diff --git a/src/rendering/effects/TextEffectInfo.ts b/src/rendering/effects/TextEffectInfo.ts index 62a133dea..8d1274f77 100644 --- a/src/rendering/effects/TextEffectInfo.ts +++ b/src/rendering/effects/TextEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { TextAlign } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class TextEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/TrillEffectInfo.ts b/src/rendering/effects/TrillEffectInfo.ts index 3292425c3..5a4ad1dba 100644 --- a/src/rendering/effects/TrillEffectInfo.ts +++ b/src/rendering/effects/TrillEffectInfo.ts @@ -1,9 +1,9 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { NoteEffectInfoBase } from '@src/rendering/effects/NoteEffectInfoBase'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { TrillGlyph } from '@src/rendering/glyphs/TrillGlyph'; import { NotationElement } from '@src/NotationSettings'; @@ -17,7 +17,7 @@ export class TrillEffectInfo extends NoteEffectInfoBase { } public get sizingMode(): EffectBarGlyphSizing { - return EffectBarGlyphSizing.SingleOnBeat; + return EffectBarGlyphSizing.GroupedOnBeatToEnd; } public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { diff --git a/src/rendering/effects/TripletFeelEffectInfo.ts b/src/rendering/effects/TripletFeelEffectInfo.ts index 0f33bbb0f..40a4a665a 100644 --- a/src/rendering/effects/TripletFeelEffectInfo.ts +++ b/src/rendering/effects/TripletFeelEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { TripletFeel } from '@src/model/TripletFeel'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { TripletFeelGlyph } from '@src/rendering/glyphs/TripletFeelGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class TripletFeelEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/WahPedalEffectInfo.ts b/src/rendering/effects/WahPedalEffectInfo.ts index 68f89c879..885d28fdf 100644 --- a/src/rendering/effects/WahPedalEffectInfo.ts +++ b/src/rendering/effects/WahPedalEffectInfo.ts @@ -1,12 +1,12 @@ -import { Beat } from '@src/model'; -import { NotationElement } from '@src/NotationSettings'; -import { BarRendererBase } from '../BarRendererBase'; -import { EffectBarGlyphSizing } from '../EffectBarGlyphSizing'; -import { EffectBarRendererInfo } from '../EffectBarRendererInfo'; -import { EffectGlyph } from '../glyphs/EffectGlyph'; +import type { Beat } from '@src/model/Beat'; import { WahPedal } from '@src/model/WahPedal'; -import { Settings } from '@src/Settings'; -import { WahPedalGlyph } from '../glyphs/WahPedalGlyph'; +import { NotationElement } from '@src/NotationSettings'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; +import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { WahPedalGlyph } from '@src/rendering/glyphs/WahPedalGlyph'; +import type { Settings } from '@src/Settings'; export class WahPedalEffectInfo extends EffectBarRendererInfo { public get notationElement(): NotationElement { diff --git a/src/rendering/effects/WhammyBarEffectInfo.ts b/src/rendering/effects/WhammyBarEffectInfo.ts index a9eaf9fa5..c59695d24 100644 --- a/src/rendering/effects/WhammyBarEffectInfo.ts +++ b/src/rendering/effects/WhammyBarEffectInfo.ts @@ -1,14 +1,14 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { LineRangedGlyph } from '@src/rendering/glyphs/LineRangedGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class WhammyBarEffectInfo extends EffectBarRendererInfo { - public get notationElement(): NotationElement{ + public get notationElement(): NotationElement { return NotationElement.EffectWhammyBar; } diff --git a/src/rendering/effects/WideBeatVibratoEffectInfo.ts b/src/rendering/effects/WideBeatVibratoEffectInfo.ts index 6325b7dea..6e35eaa65 100644 --- a/src/rendering/effects/WideBeatVibratoEffectInfo.ts +++ b/src/rendering/effects/WideBeatVibratoEffectInfo.ts @@ -1,11 +1,11 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { VibratoType } from '@src/model/VibratoType'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { BeatVibratoGlyph } from '@src/rendering/glyphs/BeatVibratoGlyph'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; -import { Settings } from '@src/Settings'; +import type { Settings } from '@src/Settings'; import { NotationElement } from '@src/NotationSettings'; export class WideBeatVibratoEffectInfo extends EffectBarRendererInfo { diff --git a/src/rendering/effects/WideNoteVibratoEffectInfo.ts b/src/rendering/effects/WideNoteVibratoEffectInfo.ts index cb68a90ce..d86fd2fa1 100644 --- a/src/rendering/effects/WideNoteVibratoEffectInfo.ts +++ b/src/rendering/effects/WideNoteVibratoEffectInfo.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; import { VibratoType } from '@src/model/VibratoType'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; import { NoteEffectInfoBase } from '@src/rendering/effects/NoteEffectInfoBase'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { NoteVibratoGlyph } from '@src/rendering/glyphs/NoteVibratoGlyph'; import { NotationElement } from '@src/NotationSettings'; diff --git a/src/rendering/glyphs/AccentuationGlyph.ts b/src/rendering/glyphs/AccentuationGlyph.ts index f1c960cbf..aefb007ea 100644 --- a/src/rendering/glyphs/AccentuationGlyph.ts +++ b/src/rendering/glyphs/AccentuationGlyph.ts @@ -1,9 +1,10 @@ import { AccentuationType } from '@src/model/AccentuationType'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { ICanvas } from '@src/platform/ICanvas'; -import { Note } from '@src/model'; -import { EffectGlyph } from './EffectGlyph'; -import { BeamDirection } from '../utils/BeamDirection'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { Note } from '@src/model/Note'; +import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; export class AccentuationGlyph extends EffectGlyph { private _note: Note; @@ -28,16 +29,16 @@ export class AccentuationGlyph extends EffectGlyph { } public override doLayout(): void { - this.width = 9; - this.height = 9; + this.width = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.ArticAccentAbove)!; + this.height = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.ArticAccentAbove)!; } public override paint(cx: number, cy: number, canvas: ICanvas): void { const dir = this.renderer.getBeatDirection(this._note.beat); - const symbol = AccentuationGlyph.getSymbol(this._note.accentuated, dir == BeamDirection.Down); - + const symbol = AccentuationGlyph.getSymbol(this._note.accentuated, dir === BeamDirection.Down); + const padding = 2; - const y = dir == BeamDirection.Up ? cy + this.y : cy + this.y + this.height - padding; + const y = dir === BeamDirection.Up ? cy + this.y : cy + this.y + this.height - padding; canvas.fillMusicFontSymbol(cx + this.x - 2, y, 1, symbol, false); } } diff --git a/src/rendering/glyphs/AccidentalGlyph.ts b/src/rendering/glyphs/AccidentalGlyph.ts index 0bceefbc8..191efc397 100644 --- a/src/rendering/glyphs/AccidentalGlyph.ts +++ b/src/rendering/glyphs/AccidentalGlyph.ts @@ -3,11 +3,8 @@ import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; export class AccidentalGlyph extends MusicFontGlyph { - private _accidentalType: AccidentalType; - public constructor(x: number, y: number, accidentalType: AccidentalType, scale: number) { super(x, y, scale, AccidentalGlyph.getMusicSymbol(accidentalType)); - this._accidentalType = accidentalType; } public static getMusicSymbol(accidentalType: AccidentalType): MusicFontSymbol { @@ -31,16 +28,4 @@ export class AccidentalGlyph extends MusicFontGlyph { } return MusicFontSymbol.None; } - - public override doLayout(): void { - switch (this._accidentalType) { - case AccidentalType.DoubleFlat: - this.width = 18; - break; - default: - this.width = 8; - break; - } - this.width = this.width * this.glyphScale; - } } diff --git a/src/rendering/glyphs/AccidentalGroupGlyph.ts b/src/rendering/glyphs/AccidentalGroupGlyph.ts index 71aebb514..af4b8b50f 100644 --- a/src/rendering/glyphs/AccidentalGroupGlyph.ts +++ b/src/rendering/glyphs/AccidentalGroupGlyph.ts @@ -1,6 +1,6 @@ -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; -import { AccidentalGlyph } from '@src/rendering/glyphs/AccidentalGlyph'; +import type { AccidentalGlyph } from '@src/rendering/glyphs/AccidentalGlyph'; class AccidentalColumnInfo { public x: number = 0; @@ -32,11 +32,11 @@ export class AccidentalGroupGlyph extends GlyphGroup { }); // defines the reserved y position of the columns - let columns: AccidentalColumnInfo[] = []; + const columns: AccidentalColumnInfo[] = []; columns.push(new AccidentalColumnInfo()); - let accidentalHeight: number = 21; + const accidentalHeight: number = 21; for (let i: number = 0, j: number = this.glyphs.length; i < j; i++) { - let g = this.glyphs[i] as AccidentalGlyph; + const g = this.glyphs[i] as AccidentalGlyph; g.renderer = this.renderer; g.doLayout(); // find column where glyph fits into @@ -68,10 +68,10 @@ export class AccidentalGroupGlyph extends GlyphGroup { } for (let i: number = 0, j: number = this.glyphs.length; i < j; i++) { - let g: Glyph = this.glyphs[i]; + const g: Glyph = this.glyphs[i]; const column = columns[g.x]; - g.x = (this.width - column.x); + g.x = this.width - column.x; } } } diff --git a/src/rendering/glyphs/AlternateEndingsGlyph.ts b/src/rendering/glyphs/AlternateEndingsGlyph.ts index 1547bba84..c53636cf5 100644 --- a/src/rendering/glyphs/AlternateEndingsGlyph.ts +++ b/src/rendering/glyphs/AlternateEndingsGlyph.ts @@ -1,16 +1,20 @@ -import { ICanvas, TextBaseline } from '@src/platform/ICanvas'; +import { type ICanvas, TextBaseline } from '@src/platform/ICanvas'; import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; -import { RenderingResources } from '@src/RenderingResources'; +import type { RenderingResources } from '@src/RenderingResources'; import { ModelUtils } from '@src/model/ModelUtils'; export class AlternateEndingsGlyph extends EffectGlyph { private static readonly Padding: number = 3; private _endings: number[]; - private _endingsString: string = ""; + private _endingsString: string = ''; + private _openLine: boolean; + private _closeLine: boolean; - public constructor(x: number, y: number, alternateEndings: number) { + public constructor(x: number, y: number, alternateEndings: number, openLine: boolean, closeLine: boolean) { super(x, y); this._endings = ModelUtils.getAlternateEndingsList(alternateEndings); + this._openLine = openLine; + this._closeLine = closeLine; } public override doLayout(): void { @@ -25,18 +29,33 @@ export class AlternateEndingsGlyph extends EffectGlyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - super.paint(cx, cy, canvas); - let baseline: TextBaseline = canvas.textBaseline; - canvas.textBaseline = TextBaseline.Top; - if (this._endings.length > 0) { - let res: RenderingResources = this.renderer.resources; - canvas.font = res.wordsFont; + const width = this._closeLine ? this.width - 4 : this.width; + + cx = (cx | 0) + 0.5; + cy = (cy | 0) + 0.5; + + if (this._openLine) { canvas.moveTo(cx + this.x, cy + this.y + this.height); canvas.lineTo(cx + this.x, cy + this.y); - canvas.lineTo(cx + this.x + this.width, cy + this.y); - canvas.stroke(); + } else { + canvas.moveTo(cx + this.x, cy + this.y); + } + + canvas.lineTo(cx + this.x + width, cy + this.y); + + if (this._closeLine) { + canvas.lineTo(cx + this.x + width, cy + this.y + this.height); + } + + canvas.stroke(); + + if (this._openLine) { + const baseline: TextBaseline = canvas.textBaseline; + canvas.textBaseline = TextBaseline.Top; + const res: RenderingResources = this.renderer.resources; + canvas.font = res.wordsFont; canvas.fillText(this._endingsString, cx + this.x + AlternateEndingsGlyph.Padding, cy + this.y); + canvas.textBaseline = baseline; } - canvas.textBaseline = baseline; } } diff --git a/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts b/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts index 4f6d10c29..28e3a560f 100644 --- a/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts +++ b/src/rendering/glyphs/ArticStaccatoAboveGlyph.ts @@ -1,18 +1,13 @@ import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; export class ArticStaccatoAboveGlyph extends MusicFontGlyph { public constructor(x: number, y: number) { super(x, y, NoteHeadGlyph.GraceScale, MusicFontSymbol.ArticStaccatoAbove); } - public override doLayout(): void { - this.width = NoteHeadGlyph.QuarterNoteHeadWidth; - this.height = 7; - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { super.paint(cx + 3, cy + 5, canvas); } diff --git a/src/rendering/glyphs/BarLineGlyph.ts b/src/rendering/glyphs/BarLineGlyph.ts new file mode 100644 index 000000000..b063f670a --- /dev/null +++ b/src/rendering/glyphs/BarLineGlyph.ts @@ -0,0 +1,243 @@ +import type { ICanvas } from '@src/platform/ICanvas'; +import { Glyph } from '@src/rendering/glyphs/Glyph'; +import { BarLineStyle } from '@src/model/Bar'; +import { LeftToRightLayoutingGlyphGroup } from '@src/rendering/glyphs/LeftToRightLayoutingGlyphGroup'; +import type { LineBarRenderer } from '@src/rendering/LineBarRenderer'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; + +abstract class BarLineGlyphBase extends Glyph { + public override doLayout(): void { + this.width = 1; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const left: number = cx + this.x; + const top: number = cy + this.y + this.renderer.topPadding; + const bottom: number = cy + this.y + this.renderer.height - this.renderer.bottomPadding; + const h: number = bottom - top; + this.paintInternal(left, top, h, canvas); + } + protected abstract paintInternal(left: number, top: number, h: number, canvas: ICanvas): void; +} +class BarLineLightGlyph extends BarLineGlyphBase { + protected override paintInternal(left: number, top: number, h: number, canvas: ICanvas): void { + canvas.fillRect(left, top, 1, h); + } +} + +class BarLineDottedGlyph extends BarLineGlyphBase { + protected override paintInternal(left: number, top: number, h: number, canvas: ICanvas): void { + const circleRadius: number = 1; + const x = left; + + const lineHeight = (this.renderer as LineBarRenderer).getLineHeight(1); + + let circleY = top + lineHeight * 0.5 + circleRadius; + + const bottom = top + h; + while (circleY < bottom) { + canvas.fillCircle(x, circleY, circleRadius); + circleY += lineHeight; + } + } +} + +class BarLineDashedGlyph extends BarLineGlyphBase { + private static readonly DashSize: number = 4; + + protected override paintInternal(left: number, top: number, h: number, canvas: ICanvas): void { + const dashSize: number = BarLineDashedGlyph.DashSize; + const x = left + 0.5; + const dashes: number = Math.ceil(h / 2 / dashSize); + const bottom = top + h; + + canvas.beginPath(); + if (dashes < 1) { + canvas.moveTo(x, top); + canvas.lineTo(x, bottom); + } else { + let dashY = top; + + // spread the dashes so they complete directly on the end-Y + const freeSpace = h - dashes * dashSize; + const freeSpacePerDash = freeSpace / (dashes - 1); + + while (dashY < bottom) { + canvas.moveTo(x, dashY); + canvas.lineTo(x, dashY + dashSize); + dashY += dashSize + freeSpacePerDash; + } + } + canvas.stroke(); + } +} + +class BarLineHeavyGlyph extends BarLineGlyphBase { + public override doLayout(): void { + this.width = 4; + } + + protected override paintInternal(left: number, top: number, h: number, canvas: ICanvas): void { + canvas.fillRect(left, top, this.width, h); + } +} + +class BarLineRepeatDotsGlyph extends BarLineGlyphBase { + protected override paintInternal(left: number, top: number, h: number, canvas: ICanvas): void { + const bottom = top + h; + const circleSize: number = 1.5; + const middle: number = (top + bottom) / 2; + const dotOffset: number = 3; + canvas.fillCircle(left, middle - circleSize * dotOffset, circleSize); + canvas.fillCircle(left, middle + circleSize * dotOffset, circleSize); + } +} +class BarLineShortGlyph extends BarLineGlyphBase { + protected override paintInternal(left: number, top: number, h: number, canvas: ICanvas): void { + const renderer = this.renderer as LineBarRenderer; + const lines = renderer.drawnLineCount; + const gaps = lines - 1; + if (gaps <= 2) { + return; + } + + const lineHeight = renderer.getLineHeight(1); + const height = lineHeight * 2; + const centerY = (gaps / 2) * lineHeight; + const lineY = centerY - height / 2; + + canvas.fillRect(left, top + lineY, 1, height); + } +} +class BarLineTickGlyph extends BarLineGlyphBase { + protected override paintInternal(left: number, top: number, h: number, canvas: ICanvas): void { + const renderer = this.renderer as LineBarRenderer; + + const lineHeight = renderer.getLineHeight(1); + const lineY = -(lineHeight / 2) + 1; + + canvas.fillRect(left, top + lineY, 1, lineHeight); + } +} + +export class BarLineGlyph extends LeftToRightLayoutingGlyphGroup { + private _isRight: boolean; + + public constructor(isRight: boolean) { + super(); + this._isRight = isRight; + } + + public override doLayout(): void { + const bar = this.renderer.bar; + const masterBar = bar.masterBar; + const actualLineType = this._isRight + ? bar.getActualBarLineRight() + : bar.getActualBarLineLeft(this.renderer.index === 0); + + // ensure we don't draw the same line type twice (we prefer drawing it as part of the "right" line) + let previousLineType = BarLineStyle.Automatic; + if (!this._isRight) { + const previousRenderer = this.renderer.previousRenderer; + if (previousRenderer && previousRenderer.staff === this.renderer.staff) { + previousLineType = previousRenderer.bar.getActualBarLineRight(); + if (actualLineType === previousLineType) { + return; + } + } + } + + const barLineSpace = 3; + + if (this._isRight) { + if (masterBar.isRepeatEnd) { + this.addGlyph(new BarLineRepeatDotsGlyph(0, 0)); + this.width += barLineSpace; + } + } + + switch (actualLineType) { + case BarLineStyle.Dashed: + this.addGlyph(new BarLineDashedGlyph(0, 0)); + break; + case BarLineStyle.Dotted: + this.addGlyph(new BarLineDottedGlyph(0, 0)); + break; + case BarLineStyle.Heavy: + // use previous heavy bar (no double heavy) + if (previousLineType !== BarLineStyle.LightHeavy && previousLineType !== BarLineStyle.HeavyHeavy) { + this.addGlyph(new BarLineHeavyGlyph(0, 0)); + } + break; + case BarLineStyle.HeavyHeavy: + // use previous heavy bar (no double heavy) + if (previousLineType !== BarLineStyle.LightHeavy && previousLineType !== BarLineStyle.Heavy) { + this.addGlyph(new BarLineHeavyGlyph(0, 0)); + } + this.width += barLineSpace; + this.addGlyph(new BarLineHeavyGlyph(0, 0)); + break; + case BarLineStyle.HeavyLight: + // use previous heavy bar (no double heavy) + if ( + previousLineType !== BarLineStyle.LightHeavy && + previousLineType !== BarLineStyle.Heavy && + previousLineType !== BarLineStyle.HeavyHeavy + ) { + this.addGlyph(new BarLineHeavyGlyph(0, 0)); + } + this.width += barLineSpace; + this.addGlyph(new BarLineLightGlyph(0, 0)); + break; + + case BarLineStyle.LightHeavy: + // use previous light bar + if ( + previousLineType !== BarLineStyle.HeavyLight && + previousLineType !== BarLineStyle.Regular && + previousLineType !== BarLineStyle.LightLight + ) { + this.addGlyph(new BarLineLightGlyph(0, 0)); + } + this.width += barLineSpace; + this.addGlyph(new BarLineHeavyGlyph(0, 0)); + break; + case BarLineStyle.LightLight: + // use previous light bar + if (previousLineType !== BarLineStyle.HeavyLight && previousLineType !== BarLineStyle.Regular) { + this.addGlyph(new BarLineLightGlyph(0, 0)); + } + this.width += barLineSpace; + this.addGlyph(new BarLineLightGlyph(0, 0)); + break; + case BarLineStyle.None: + break; + case BarLineStyle.Regular: + // use previous light bar + if (previousLineType !== BarLineStyle.HeavyLight && previousLineType !== BarLineStyle.LightLight) { + this.addGlyph(new BarLineLightGlyph(0, 0)); + } + break; + case BarLineStyle.Short: + this.addGlyph(new BarLineShortGlyph(0, 0)); + break; + + case BarLineStyle.Tick: + this.addGlyph(new BarLineTickGlyph(0, 0)); + break; + } + + if (!this._isRight) { + if (masterBar.isRepeatStart) { + this.width += barLineSpace; + this.addGlyph(new BarLineRepeatDotsGlyph(0, 0)); + } + } + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const renderer = this.renderer as LineBarRenderer; + using _ = ElementStyleHelper.bar(canvas, renderer.barLineBarSubElement, this.renderer.bar, true); + super.paint(cx, cy, canvas); + } +} diff --git a/src/rendering/glyphs/BarNumberGlyph.ts b/src/rendering/glyphs/BarNumberGlyph.ts index 12cec0b68..c1cbc8c44 100644 --- a/src/rendering/glyphs/BarNumberGlyph.ts +++ b/src/rendering/glyphs/BarNumberGlyph.ts @@ -1,7 +1,8 @@ -import { Color } from '@src/model/Color'; -import { ICanvas, TextBaseline } from '@src/platform/ICanvas'; +import { type ICanvas, TextBaseline } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; -import { RenderingResources } from '@src/RenderingResources'; +import type { RenderingResources } from '@src/RenderingResources'; +import type { LineBarRenderer } from '@src/rendering/LineBarRenderer'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; export class BarNumberGlyph extends Glyph { private _number: number = 0; @@ -20,15 +21,19 @@ export class BarNumberGlyph extends Glyph { if (!this.renderer.staff.isFirstInSystem) { return; } - let res: RenderingResources = this.renderer.resources; - let c: Color = canvas.color; + + using _ = ElementStyleHelper.bar( + canvas, + (this.renderer as LineBarRenderer).barNumberBarSubElement, + this.renderer.bar, + true + ); + + const res: RenderingResources = this.renderer.resources; const baseline = canvas.textBaseline; canvas.textBaseline = TextBaseline.Top; - canvas.color = res.barNumberColor; canvas.font = res.barNumberFont; canvas.fillText(this._number.toString(), cx + this.x, cy + this.y); - canvas.color = c; canvas.textBaseline = baseline; } - } diff --git a/src/rendering/glyphs/BarSeperatorGlyph.ts b/src/rendering/glyphs/BarSeperatorGlyph.ts deleted file mode 100644 index f9feec8f2..000000000 --- a/src/rendering/glyphs/BarSeperatorGlyph.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ICanvas } from '@src/platform/ICanvas'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; - -export class BarSeperatorGlyph extends Glyph { - private static readonly DashSize: number = 4; - - public constructor(x: number, y: number) { - super(x, y); - } - - public override doLayout(): void { - if (this.renderer.isLast) { - this.width = 15; - } else if ( - !this.renderer.nextRenderer || - this.renderer.nextRenderer.staff !== this.renderer.staff || - !this.renderer.nextRenderer.bar.masterBar.isRepeatStart - ) { - this.width = 2; - if (this.renderer.bar.masterBar.isDoubleBar) { - this.width += 2; - } - } else { - this.width = 2; - } - } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - let blockWidth: number = 4; - let top: number = cy + this.y + this.renderer.topPadding; - let bottom: number = cy + this.y + this.renderer.height - this.renderer.bottomPadding; - let left: number = cx + this.x; - let h: number = bottom - top; - if (this.renderer.isLast) { - // small bar - canvas.fillRect(left + this.width - blockWidth - blockWidth, top, 1, h); - // big bar - canvas.fillRect(left + this.width - blockWidth, top, blockWidth, h); - } else if ( - !this.renderer.nextRenderer || - this.renderer.nextRenderer.staff !== this.renderer.staff || - !this.renderer.nextRenderer.bar.masterBar.isRepeatStart - ) { - // small bar - if (this.renderer.bar.masterBar.isFreeTime) { - const dashSize: number = BarSeperatorGlyph.DashSize; - const x = ((left + this.width - 1) | 0) + 0.5; - const dashes: number = Math.ceil(h / 2 / dashSize); - - canvas.beginPath(); - if (dashes < 1) { - canvas.moveTo(x, top); - canvas.lineTo(x, bottom); - } else { - let dashY = top; - - // spread the dashes so they complete directly on the end-Y - const freeSpace = h - dashes * dashSize; - const freeSpacePerDash = freeSpace / (dashes - 1); - - while (dashY < bottom) { - canvas.moveTo(x, dashY); - canvas.lineTo(x, dashY + dashSize); - dashY += dashSize + freeSpacePerDash; - } - } - canvas.stroke(); - } else { - canvas.fillRect(left + this.width - 1, top, 1, h); - if (this.renderer.bar.masterBar.isDoubleBar) { - canvas.fillRect(left + this.width - 5, top, 1, h); - } - } - } - } -} diff --git a/src/rendering/glyphs/BarTempoGlyph.ts b/src/rendering/glyphs/BarTempoGlyph.ts index c5598583d..a03d6c676 100644 --- a/src/rendering/glyphs/BarTempoGlyph.ts +++ b/src/rendering/glyphs/BarTempoGlyph.ts @@ -1,6 +1,8 @@ -import { ICanvas, TextBaseline } from '@src/platform'; -import { EffectGlyph } from './EffectGlyph'; -import { Automation, MusicFontSymbol } from '@src/model'; +import type { Automation } from '@src/model/Automation'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import { type ICanvas, TextBaseline } from '@src/platform/ICanvas'; +import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; /** * This glyph renders tempo annotations for tempo automations @@ -32,16 +34,10 @@ export class BarTempoGlyph extends EffectGlyph { canvas.fillText(automation.text, x, cy + this.y + canvas.font.size / 2); x += size.width + canvas.font.size * 0.7; } - canvas.fillMusicFontSymbol( - x, - cy + this.y + this.height * 0.8, - 0.5, - MusicFontSymbol.NoteQuarterUp, - true - ); + canvas.fillMusicFontSymbol(x, cy + this.y + this.height * 0.8, 0.5, MusicFontSymbol.NoteQuarterUp, true); canvas.fillText( - '= ' + automation.value.toString(), - x + 8, + `= ${automation.value.toString()}`, + x + MusicFontSymbolSizes.Widths.get(MusicFontSymbol.NoteQuarterUp)! * 0.5 + 3, cy + this.y + canvas.font.size / 2 ); } diff --git a/src/rendering/glyphs/BeatContainerGlyph.ts b/src/rendering/glyphs/BeatContainerGlyph.ts index a4924a4f8..36d74d8b3 100644 --- a/src/rendering/glyphs/BeatContainerGlyph.ts +++ b/src/rendering/glyphs/BeatContainerGlyph.ts @@ -1,19 +1,19 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { Duration } from '@src/model/Duration'; import { GraceType } from '@src/model/GraceType'; -import { Note } from '@src/model/Note'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; -import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; +import type { Note } from '@src/model/Note'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; +import type { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; import { Glyph } from '@src/rendering/glyphs/Glyph'; -import { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; -import { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; -import { BarBounds } from '@src/rendering/utils/BarBounds'; +import type { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; +import type { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; +import type { BarBounds } from '@src/rendering/utils/BarBounds'; import { BeatBounds } from '@src/rendering/utils/BeatBounds'; import { Bounds } from '@src/rendering/utils/Bounds'; import { FlagGlyph } from '@src/rendering/glyphs/FlagGlyph'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; -import { BeamingHelper } from '../utils/BeamingHelper'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; export class BeatContainerGlyph extends Glyph { public static readonly GraceBeatPadding: number = 3; @@ -45,25 +45,23 @@ export class BeatContainerGlyph extends Glyph { } public registerLayoutingInfo(layoutings: BarLayoutingInfo): void { - let preBeatStretch: number = this.preNotes.computedWidth + this.onNotes.centerX; + 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) { + if (this.beat.graceType !== GraceType.None) { // flagged grace - if(this.beat.graceGroup!.beats.length === 1) { + if (this.beat.graceGroup!.beats.length === 1) { postBeatStretch += FlagGlyph.FlagWidth * NoteHeadGlyph.GraceScale; } // grace with bars, some space for bar unless last - else if(this.beat.graceIndex < this.beat.graceGroup!.beats.length - 1) { + else if (this.beat.graceIndex < this.beat.graceGroup!.beats.length - 1) { postBeatStretch += 7; - } - else { + } else { postBeatStretch += BeatContainerGlyph.GraceBeatPadding; } - } - else if (helper && this.drawBeamHelperAsFlags(helper)) { + } else if (helper && this.drawBeamHelperAsFlags(helper)) { postBeatStretch += FlagGlyph.FlagWidth; } for (const tie of this.ties) { @@ -114,7 +112,7 @@ export class BeatContainerGlyph extends Glyph { } } let tieWidth: number = 0; - for (let tie of this.ties) { + for (const tie of this.ties) { if (tie.width > tieWidth) { tieWidth = tie.width; } @@ -133,13 +131,10 @@ export class BeatContainerGlyph extends Glyph { } public static getGroupId(beat: Beat): string { - return 'b' + beat.id; + return `b${beat.id}`; } public override paint(cx: number, cy: number, canvas: ICanvas): void { - if (this.beat.voice.isEmpty) { - return; - } // var c = canvas.color; // var ta = canvas.textAlign; // canvas.color = new Color(255, 0, 0); @@ -175,7 +170,7 @@ export class BeatContainerGlyph extends Glyph { // } // canvas.color = c; - let 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; } @@ -185,10 +180,10 @@ 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. - let staffX: number = cx - this.voiceContainer.x - this.renderer.x; - let staffY: number = cy - this.voiceContainer.y - this.renderer.y; + 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++) { - let t: Glyph = this.ties[i]; + const t: Glyph = this.ties[i]; t.renderer = this.renderer; t.paint(staffX, staffY, canvas); } @@ -196,7 +191,7 @@ export class BeatContainerGlyph extends Glyph { } public buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number, isEmptyBar: boolean) { - let beatBoundings: BeatBounds = new BeatBounds(); + const beatBoundings: BeatBounds = new BeatBounds(); beatBoundings.beat = this.beat; if (this.beat.isEmpty) { diff --git a/src/rendering/glyphs/BeatGlyphBase.ts b/src/rendering/glyphs/BeatGlyphBase.ts index 1c9066036..5227bfe2e 100644 --- a/src/rendering/glyphs/BeatGlyphBase.ts +++ b/src/rendering/glyphs/BeatGlyphBase.ts @@ -1,9 +1,15 @@ -import { Note } from '@src/model/Note'; -import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { BeatSubElement } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import type { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; +import type { ICanvas } from '@src/platform/ICanvas'; export class BeatGlyphBase extends GlyphGroup { + private _effectGlyphs: Glyph[] = []; + private _normalGlyphs: Glyph[] = []; + public container!: BeatContainerGlyph; public computedWidth: number = 0; @@ -16,7 +22,7 @@ export class BeatGlyphBase extends GlyphGroup { let w: number = 0; if (this.glyphs) { for (let i: number = 0, j: number = this.glyphs.length; i < j; i++) { - let g: Glyph = this.glyphs[i]; + const g: Glyph = this.glyphs[i]; g.x = w; g.renderer = this.renderer; g.doLayout(); @@ -32,4 +38,38 @@ export class BeatGlyphBase extends GlyphGroup { action(this.container.beat.notes[i]); } } + + public addEffect(g: Glyph) { + super.addGlyph(g); + this._effectGlyphs.push(g); + } + + public addNormal(g: Glyph) { + super.addGlyph(g); + this._normalGlyphs.push(g); + } + + protected get effectElement(): BeatSubElement | undefined { + return undefined; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + this.paintEffects(cx, cy, canvas); + this.paintNormal(cx, cy, canvas); + } + + private paintNormal(cx: number, cy: number, canvas: ICanvas) { + for (const g of this._normalGlyphs) { + g.paint(cx + this.x, cy + this.y, canvas); + } + } + + private paintEffects(cx: number, cy: number, canvas: ICanvas) { + using _ = this.effectElement + ? ElementStyleHelper.beat(canvas, this.effectElement!, this.container.beat) + : undefined; + for (const g of this._effectGlyphs) { + g.paint(cx + this.x, cy + this.y, canvas); + } + } } diff --git a/src/rendering/glyphs/BeatOnNoteGlyphBase.ts b/src/rendering/glyphs/BeatOnNoteGlyphBase.ts index 793bb073a..b6f79d17f 100644 --- a/src/rendering/glyphs/BeatOnNoteGlyphBase.ts +++ b/src/rendering/glyphs/BeatOnNoteGlyphBase.ts @@ -1,8 +1,8 @@ import { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; -import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; -import { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; -import { Note } from '@src/model/Note'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import type { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; +import type { Note } from '@src/model/Note'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; export class BeatOnNoteGlyphBase extends BeatGlyphBase { public beamingHelper!: BeamingHelper; @@ -12,7 +12,7 @@ export class BeatOnNoteGlyphBase extends BeatGlyphBase { // } - public buildBoundingsLookup(beatBounds:BeatBounds, cx:number, cy:number) { + public buildBoundingsLookup(beatBounds: BeatBounds, cx: number, cy: number) { // implemented in subclasses } diff --git a/src/rendering/glyphs/BeatTimerGlyph.ts b/src/rendering/glyphs/BeatTimerGlyph.ts index 0bb19eea1..ca05b9653 100644 --- a/src/rendering/glyphs/BeatTimerGlyph.ts +++ b/src/rendering/glyphs/BeatTimerGlyph.ts @@ -1,5 +1,5 @@ -import { ICanvas, TextAlign, TextBaseline } from '@src/platform'; -import { EffectGlyph } from './EffectGlyph'; +import { type ICanvas, TextBaseline, TextAlign } from '@src/platform/ICanvas'; +import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; export class BeatTimerGlyph extends EffectGlyph { private static readonly PaddingX = 2; @@ -33,7 +33,12 @@ export class BeatTimerGlyph extends EffectGlyph { public override paint(cx: number, cy: number, canvas: ICanvas): void { const halfWidth = (this._textWidth / 2) | 0; - canvas.strokeRect(cx + this.x - halfWidth, cy + this.y + BeatTimerGlyph.MarginY, this._textWidth, this._textHeight); + canvas.strokeRect( + cx + this.x - halfWidth, + cy + this.y + BeatTimerGlyph.MarginY, + this._textWidth, + this._textHeight + ); const f = canvas.font; const b = canvas.textBaseline; const a = canvas.textAlign; diff --git a/src/rendering/glyphs/BeatVibratoGlyph.ts b/src/rendering/glyphs/BeatVibratoGlyph.ts index ce6f0dab9..2ac6760a4 100644 --- a/src/rendering/glyphs/BeatVibratoGlyph.ts +++ b/src/rendering/glyphs/BeatVibratoGlyph.ts @@ -1,5 +1,5 @@ import { VibratoType } from '@src/model/VibratoType'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { GroupedEffectGlyph } from '@src/rendering/glyphs/GroupedEffectGlyph'; @@ -27,8 +27,8 @@ export class BeatVibratoGlyph extends GroupedEffectGlyph { protected paintGrouped(cx: number, cy: number, endX: number, canvas: ICanvas): void { let startX: number = cx + this.x; - let width: number = endX - startX; - let loops: number = Math.max(1, width / this._stepSize); + const width: number = endX - startX; + const loops: number = Math.max(1, width / this._stepSize); canvas.beginPath(); canvas.moveTo(startX, cy + this.y); for (let i: number = 0; i < loops; i++) { diff --git a/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts b/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts index 8e7c2d670..9a8c71a31 100644 --- a/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts +++ b/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts @@ -1,14 +1,15 @@ +import type { Color } from '@src/model/Color'; import { AccidentalType } from '@src/model/AccidentalType'; -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { Duration } from '@src/model/Duration'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { AccidentalGlyph } from '@src/rendering/glyphs/AccidentalGlyph'; import { AccidentalGroupGlyph } from '@src/rendering/glyphs/AccidentalGroupGlyph'; import { GhostNoteContainerGlyph } from '@src/rendering/glyphs/GhostNoteContainerGlyph'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; import { ScoreNoteChordGlyphBase } from '@src/rendering/glyphs/ScoreNoteChordGlyphBase'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { @@ -49,16 +50,16 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { return 0; } - public addGlyph(noteValue: number, quarterBend: boolean = false): void { - let sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; - let noteHeadGlyph: NoteHeadGlyph = new NoteHeadGlyph(0, 0, Duration.Quarter, true); - let accidental: AccidentalType = sr.accidentalHelper.applyAccidentalForValue( + public addGlyph(noteValue: number, quarterBend: boolean, color: Color | undefined): void { + const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const noteHeadGlyph: NoteHeadGlyph = new NoteHeadGlyph(0, 0, Duration.Quarter, true); + const accidental: AccidentalType = sr.accidentalHelper.applyAccidentalForValue( this._beat, noteValue, quarterBend, true ); - let line: number = sr.accidentalHelper.getNoteLineForValue(noteValue, false); + const line: number = sr.accidentalHelper.getNoteLineForValue(noteValue, false); noteHeadGlyph.y = sr.getScoreY(line); if (this._showParenthesis) { this._preNoteParenthesis!.renderer = this.renderer; @@ -67,7 +68,7 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { this._postNoteParenthesis!.addParenthesisOnLine(line, true); } if (accidental !== AccidentalType.None) { - let g = new AccidentalGlyph(0, noteHeadGlyph.y, accidental, NoteHeadGlyph.GraceScale); + const g = new AccidentalGlyph(0, noteHeadGlyph.y, accidental, NoteHeadGlyph.GraceScale); g.renderer = this.renderer; this._accidentals.renderer = this.renderer; this._accidentals.addGlyph(g); diff --git a/src/rendering/glyphs/ChordDiagramContainerGlyph.ts b/src/rendering/glyphs/ChordDiagramContainerGlyph.ts index 4f74294dd..57edf5987 100644 --- a/src/rendering/glyphs/ChordDiagramContainerGlyph.ts +++ b/src/rendering/glyphs/ChordDiagramContainerGlyph.ts @@ -1,18 +1,28 @@ -import { Chord } from '@src/model/Chord'; +import type { Chord } from '@src/model/Chord'; +import type { ICanvas } from '@src/platform/ICanvas'; import { ChordDiagramGlyph } from '@src/rendering/glyphs/ChordDiagramGlyph'; import { RowContainerGlyph } from '@src/rendering/glyphs/RowContainerGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { ScoreSubElement } from '@src/model/Score'; export class ChordDiagramContainerGlyph extends RowContainerGlyph { - public constructor(x: number, y: number) { - super(x, y); - } - public addChord(chord: Chord): void { if (chord.strings.length > 0) { - let chordDiagram: ChordDiagramGlyph = new ChordDiagramGlyph(0, 0, chord); + const chordDiagram: ChordDiagramGlyph = new ChordDiagramGlyph(0, 0, chord); chordDiagram.renderer = this.renderer; chordDiagram.doLayout(); this.glyphs!.push(chordDiagram); } } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + if (this.glyphs!.length > 0) { + using _ = ElementStyleHelper.score( + canvas, + ScoreSubElement.ChordDiagramList, + this.renderer.scoreRenderer.score! + ); + super.paint(cx, cy, canvas); + } + } } diff --git a/src/rendering/glyphs/ChordDiagramGlyph.ts b/src/rendering/glyphs/ChordDiagramGlyph.ts index 92e7c565e..7232dc395 100644 --- a/src/rendering/glyphs/ChordDiagramGlyph.ts +++ b/src/rendering/glyphs/ChordDiagramGlyph.ts @@ -1,8 +1,8 @@ -import { Chord } from '@src/model/Chord'; -import { ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; +import type { Chord } from '@src/model/Chord'; +import { type ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { RenderingResources } from '@src/RenderingResources'; +import type { RenderingResources } from '@src/RenderingResources'; export class ChordDiagramGlyph extends EffectGlyph { private static readonly Padding: number[] = [5, 2]; @@ -23,7 +23,7 @@ export class ChordDiagramGlyph extends EffectGlyph { public override doLayout(): void { super.doLayout(); - let res: RenderingResources = this.renderer.resources; + 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) { @@ -38,21 +38,21 @@ export class ChordDiagramGlyph extends EffectGlyph { 2 * ChordDiagramGlyph.Padding[1]; this.width = this._firstFretSpacing + - (this._chord.staff.tuning.length - 1) * ChordDiagramGlyph.StringSpacing + + (this._chord.strings.length - 1) * ChordDiagramGlyph.StringSpacing + 2 * ChordDiagramGlyph.Padding[0]; } public override paint(cx: number, cy: number, canvas: ICanvas): void { cx += this.x + ChordDiagramGlyph.Padding[0] + this._firstFretSpacing; cy += this.y; - let stringSpacing: number = ChordDiagramGlyph.StringSpacing; - let fretSpacing: number = ChordDiagramGlyph.FretSpacing; - let res: RenderingResources = this.renderer.resources; - let circleRadius: number = ChordDiagramGlyph.CircleRadius; - let w: number = this.width - 2 * ChordDiagramGlyph.Padding[0] - this._firstFretSpacing; + const stringSpacing: number = ChordDiagramGlyph.StringSpacing; + const fretSpacing: number = ChordDiagramGlyph.FretSpacing; + const res: RenderingResources = this.renderer.resources; + const circleRadius: number = ChordDiagramGlyph.CircleRadius; + const w: number = this.width - 2 * ChordDiagramGlyph.Padding[0] - this._firstFretSpacing; - let align: TextAlign = canvas.textAlign; - let baseline: TextBaseline = canvas.textBaseline; + const align: TextAlign = canvas.textAlign; + const baseline: TextBaseline = canvas.textBaseline; canvas.font = res.effectFont; canvas.textAlign = TextAlign.Center; canvas.textBaseline = TextBaseline.Top; @@ -63,10 +63,10 @@ export class ChordDiagramGlyph extends EffectGlyph { cy += this._textRow; canvas.font = res.fretboardNumberFont; canvas.textBaseline = TextBaseline.Middle; - for (let i: number = 0; i < this._chord.staff.tuning.length; i++) { - let x: number = cx + i * stringSpacing; - let y: number = cy + this._fretRow / 2; - let fret: number = this._chord.strings[this._chord.staff.tuning.length - i - 1]; + 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) { canvas.fillMusicFontSymbol(x, y, 1, MusicFontSymbol.FretboardX, true); } else if (fret === 0) { @@ -78,8 +78,8 @@ export class ChordDiagramGlyph extends EffectGlyph { } cy += this._fretRow; - for (let i: number = 0; i < this._chord.staff.tuning.length; i++) { - let x: number = cx + i * stringSpacing; + for (let i: number = 0; i < this._chord.strings.length; i++) { + const x: number = cx + i * stringSpacing; canvas.fillRect(x, cy, 1, fretSpacing * ChordDiagramGlyph.Frets + 1); } @@ -90,13 +90,13 @@ export class ChordDiagramGlyph extends EffectGlyph { canvas.fillRect(cx, cy - 1, w, 2); for (let i: number = 0; i <= ChordDiagramGlyph.Frets; i++) { - let y: number = cy + i * fretSpacing; + const y: number = cy + i * fretSpacing; canvas.fillRect(cx, y, w, 1); } - let barreLookup = new Map(); - for (let barreFret of this._chord.barreFrets) { - let strings: number[] = [-1, -1]; + const barreLookup = new Map(); + for (const barreFret of this._chord.barreFrets) { + const strings: number[] = [-1, -1]; barreLookup.set(barreFret - this._chord.firstFret, strings); } @@ -105,7 +105,7 @@ export class ChordDiagramGlyph extends EffectGlyph { if (fret > 0) { fret -= this._chord.firstFret; if (barreLookup.has(fret)) { - let info = barreLookup.get(fret)!; + const info = barreLookup.get(fret)!; if (info[0] === -1 || guitarString < info[0]) { info[0] = guitarString; } @@ -113,16 +113,16 @@ export class ChordDiagramGlyph extends EffectGlyph { info[1] = guitarString; } } - let y: number = cy + fret * fretSpacing + fretSpacing / 2 + 0.5; - let x: number = cx + (this._chord.strings.length - guitarString - 1) * stringSpacing; + const y: number = cy + fret * fretSpacing + fretSpacing / 2 + 0.5; + const x: number = cx + (this._chord.strings.length - guitarString - 1) * stringSpacing; canvas.fillCircle(x, y, circleRadius); } } for (const [fret, strings] of barreLookup) { - let y: number = cy + fret * fretSpacing + fretSpacing / 2 + 0.5; - let xLeft: number = cx + (this._chord.strings.length - strings[1] - 1) * stringSpacing; - let xRight: number = cx + (this._chord.strings.length - strings[0] - 1) * stringSpacing; + const y: number = cy + fret * fretSpacing + fretSpacing / 2 + 0.5; + const xLeft: number = cx + (this._chord.strings.length - strings[1] - 1) * stringSpacing; + const xRight: number = cx + (this._chord.strings.length - strings[0] - 1) * stringSpacing; canvas.fillRect(xLeft, y - circleRadius, xRight - xLeft, circleRadius * 2); } diff --git a/src/rendering/glyphs/CircleGlyph.ts b/src/rendering/glyphs/CircleGlyph.ts index 94528249c..db2df1936 100644 --- a/src/rendering/glyphs/CircleGlyph.ts +++ b/src/rendering/glyphs/CircleGlyph.ts @@ -1,9 +1,12 @@ -import { ICanvas } from '@src/platform/ICanvas'; +import type { Color } from '@src/model/Color'; +import type { ICanvas } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; export class CircleGlyph extends Glyph { private _size: number = 0; + public colorOverride?: Color; + public constructor(x: number, y: number, size: number) { super(x, y); this._size = size; @@ -14,6 +17,11 @@ export class CircleGlyph extends Glyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { + const c = canvas.color; + if (this.colorOverride) { + canvas.color = this.colorOverride; + } canvas.fillCircle(cx + this.x, cy + this.y, this._size); + canvas.color = c; } } diff --git a/src/rendering/glyphs/ClefGlyph.ts b/src/rendering/glyphs/ClefGlyph.ts index c15c75555..612b82592 100644 --- a/src/rendering/glyphs/ClefGlyph.ts +++ b/src/rendering/glyphs/ClefGlyph.ts @@ -1,9 +1,11 @@ import { Clef } from '@src/model/Clef'; import { Ottavia } from '@src/model/Ottavia'; -import { ICanvas } from '@src/platform/ICanvas'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { BarSubElement } from '@src/model/Bar'; export class ClefGlyph extends MusicFontGlyph { private _clef: Clef; @@ -15,20 +17,6 @@ export class ClefGlyph extends MusicFontGlyph { this._clefOttava = clefOttava; } - public override doLayout(): void { - switch (this._clef) { - case Clef.Neutral: - this.width = 15; - break; - case Clef.C3: - case Clef.C4: - case Clef.F4: - case Clef.G2: - this.width = 28; - break; - } - } - private static getSymbol(clef: Clef): MusicFontSymbol { switch (clef) { case Clef.Neutral: @@ -47,6 +35,8 @@ export class ClefGlyph extends MusicFontGlyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { + using _ = ElementStyleHelper.bar(canvas, BarSubElement.StandardNotationClef, this.renderer.bar); + super.paint(cx, cy, canvas); let numberGlyph: Glyph; let top: boolean = false; @@ -96,7 +86,7 @@ export class ClefGlyph extends MusicFontGlyph { } numberGlyph.renderer = this.renderer; numberGlyph.doLayout(); - let x: number = this.width / 2; + const x: number = this.width / 2; numberGlyph.paint(cx + this.x + x + offsetX, cy + this.y + offsetY, canvas); } } diff --git a/src/rendering/glyphs/CrescendoGlyph.ts b/src/rendering/glyphs/CrescendoGlyph.ts index b32d18a4d..5d5a105ce 100644 --- a/src/rendering/glyphs/CrescendoGlyph.ts +++ b/src/rendering/glyphs/CrescendoGlyph.ts @@ -1,11 +1,11 @@ import { CrescendoType } from '@src/model/CrescendoType'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { GroupedEffectGlyph } from '@src/rendering/glyphs/GroupedEffectGlyph'; -import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; export class CrescendoGlyph extends GroupedEffectGlyph { - private static readonly Padding: number = (NoteHeadGlyph.QuarterNoteHeadWidth / 2) | 0; private _crescendo: CrescendoType; public constructor(x: number, y: number, crescendo: CrescendoType) { @@ -22,16 +22,17 @@ export class CrescendoGlyph extends GroupedEffectGlyph { } protected paintGrouped(cx: number, cy: number, endX: number, canvas: ICanvas): void { - let startX: number = cx + this.x; - let height: number = this.height; + const startX: number = cx + this.x; + const height: number = this.height; + const padding = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.NoteheadBlack)! / 2; canvas.beginPath(); if (this._crescendo === CrescendoType.Crescendo) { - endX -= CrescendoGlyph.Padding; + endX -= padding; canvas.moveTo(endX, cy + this.y); canvas.lineTo(startX, cy + this.y + height / 2); canvas.lineTo(endX, cy + this.y + height); } else { - endX -= CrescendoGlyph.Padding; + endX -= padding; canvas.moveTo(startX, cy + this.y); canvas.lineTo(endX, cy + this.y + height / 2); canvas.lineTo(startX, cy + this.y + height); diff --git a/src/rendering/glyphs/DeadNoteHeadGlyph.ts b/src/rendering/glyphs/DeadNoteHeadGlyph.ts index 64903e6d9..9957fdfdf 100644 --- a/src/rendering/glyphs/DeadNoteHeadGlyph.ts +++ b/src/rendering/glyphs/DeadNoteHeadGlyph.ts @@ -3,15 +3,7 @@ import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; export class DeadNoteHeadGlyph extends MusicFontGlyph { - private _isGrace: boolean; - public constructor(x: number, y: number, isGrace: boolean) { super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, MusicFontSymbol.NoteheadXOrnate); - this._isGrace = isGrace; - } - - public override doLayout(): void { - this.width = 9 * (this._isGrace ? NoteHeadGlyph.GraceScale : 1); - this.height = NoteHeadGlyph.NoteHeadHeight; } } diff --git a/src/rendering/glyphs/DeadSlappedBeatGlyph.ts b/src/rendering/glyphs/DeadSlappedBeatGlyph.ts index 542839984..cca373fba 100644 --- a/src/rendering/glyphs/DeadSlappedBeatGlyph.ts +++ b/src/rendering/glyphs/DeadSlappedBeatGlyph.ts @@ -1,6 +1,6 @@ -import { ICanvas } from '@src/platform'; -import { Glyph } from './Glyph'; -import { LineBarRenderer } from '../LineBarRenderer'; +import type { ICanvas } from '@src/platform/ICanvas'; +import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { LineBarRenderer } from '@src/rendering/LineBarRenderer'; export class DeadSlappedBeatGlyph extends Glyph { public constructor() { @@ -12,15 +12,14 @@ export class DeadSlappedBeatGlyph extends Glyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - const renderer = (this.renderer as LineBarRenderer); + 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); // center X on staff - const centerY = (staffTop + staffHeight / 2) - (crossHeight / 2); - + const centerY = staffTop + staffHeight / 2 - crossHeight / 2; const lw = canvas.lineWidth; canvas.lineWidth = 2; diff --git a/src/rendering/glyphs/DiamondNoteHeadGlyph.ts b/src/rendering/glyphs/DiamondNoteHeadGlyph.ts index 8a61cbb3d..b61c21060 100644 --- a/src/rendering/glyphs/DiamondNoteHeadGlyph.ts +++ b/src/rendering/glyphs/DiamondNoteHeadGlyph.ts @@ -4,11 +4,8 @@ import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; export class DiamondNoteHeadGlyph extends MusicFontGlyph { - private _isGrace: boolean; - public constructor(x: number, y: number, duration: Duration, isGrace: boolean) { super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, DiamondNoteHeadGlyph.getSymbol(duration)); - this._isGrace = isGrace; } private static getSymbol(duration: Duration): MusicFontSymbol { @@ -22,9 +19,4 @@ export class DiamondNoteHeadGlyph extends MusicFontGlyph { return MusicFontSymbol.NoteheadDiamondBlackWide; } } - - public override doLayout(): void { - this.width = 9 * (this._isGrace ? NoteHeadGlyph.GraceScale : 1); - this.height = NoteHeadGlyph.NoteHeadHeight; - } } diff --git a/src/rendering/glyphs/DigitGlyph.ts b/src/rendering/glyphs/DigitGlyph.ts deleted file mode 100644 index 7f18debc2..000000000 --- a/src/rendering/glyphs/DigitGlyph.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; -import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; - -export class DigitGlyph extends MusicFontGlyph { - private _digit: number = 0; - private _scale: number = 0; - - public constructor(x: number, y: number, digit: number, scale: number) { - super(x, y, scale, DigitGlyph.getSymbol(digit)); - this._digit = digit; - this._scale = scale; - } - - public override doLayout(): void { - this.width = this.getDigitWidth(this._digit) * this._scale; - } - - private getDigitWidth(digit: number): number { - switch (digit) { - case 0: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - case 9: - return 14; - case 1: - return 10; - default: - return 0; - } - } - - private static getSymbol(digit: number): MusicFontSymbol { - switch (digit) { - case 0: - return MusicFontSymbol.TimeSig0; - case 1: - return MusicFontSymbol.TimeSig1; - case 2: - return MusicFontSymbol.TimeSig2; - case 3: - return MusicFontSymbol.TimeSig3; - case 4: - return MusicFontSymbol.TimeSig4; - case 5: - return MusicFontSymbol.TimeSig5; - case 6: - return MusicFontSymbol.TimeSig6; - case 7: - return MusicFontSymbol.TimeSig7; - case 8: - return MusicFontSymbol.TimeSig8; - case 9: - return MusicFontSymbol.TimeSig9; - default: - return MusicFontSymbol.None; - } - } -} diff --git a/src/rendering/glyphs/DirectionsContainerGlyph.ts b/src/rendering/glyphs/DirectionsContainerGlyph.ts index 3a5db9000..678d8a9c2 100644 --- a/src/rendering/glyphs/DirectionsContainerGlyph.ts +++ b/src/rendering/glyphs/DirectionsContainerGlyph.ts @@ -1,8 +1,8 @@ import { Direction } from '@src/model/Direction'; -import { EffectGlyph } from './EffectGlyph'; -import { ICanvas, TextAlign, TextBaseline } from '@src/platform'; -import { Glyph } from './Glyph'; -import { MusicFontSymbol } from '@src/model'; +import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { Glyph } from '@src/rendering/glyphs/Glyph'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import { type ICanvas, TextBaseline, TextAlign } from '@src/platform/ICanvas'; class TargetDirectionGlyph extends Glyph { private _symbols: MusicFontSymbol[]; diff --git a/src/rendering/glyphs/DynamicsGlyph.ts b/src/rendering/glyphs/DynamicsGlyph.ts index 2e08741b0..ea80a668d 100644 --- a/src/rendering/glyphs/DynamicsGlyph.ts +++ b/src/rendering/glyphs/DynamicsGlyph.ts @@ -5,11 +5,11 @@ import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; export class DynamicsGlyph extends MusicFontGlyph { public constructor(x: number, y: number, dynamics: DynamicValue) { super(x, y, 0.6, DynamicsGlyph.getSymbol(dynamics)); + this.center = true; } public override doLayout(): void { super.doLayout(); - this.height = 17; this.y += this.height / 2; } @@ -31,6 +31,42 @@ export class DynamicsGlyph extends MusicFontGlyph { return MusicFontSymbol.DynamicFF; case DynamicValue.FFF: return MusicFontSymbol.DynamicFFF; + case DynamicValue.PPPP: + return MusicFontSymbol.DynamicPPPP; + case DynamicValue.PPPPP: + return MusicFontSymbol.DynamicPPPPP; + case DynamicValue.PPPPPP: + return MusicFontSymbol.DynamicPPPPP; + case DynamicValue.FFFF: + return MusicFontSymbol.DynamicFFFF; + case DynamicValue.FFFFF: + return MusicFontSymbol.DynamicFFFFF; + case DynamicValue.FFFFFF: + return MusicFontSymbol.DynamicFFFFFF; + case DynamicValue.SF: + return MusicFontSymbol.DynamicSforzando1; + case DynamicValue.SFP: + return MusicFontSymbol.DynamicSforzandoPiano; + case DynamicValue.SFPP: + return MusicFontSymbol.DynamicSforzandoPianissimo; + case DynamicValue.FP: + return MusicFontSymbol.DynamicFortePiano; + case DynamicValue.RF: + return MusicFontSymbol.DynamicRinforzando1; + case DynamicValue.RFZ: + return MusicFontSymbol.DynamicRinforzando2; + case DynamicValue.SFZ: + return MusicFontSymbol.DynamicSforzato; + case DynamicValue.SFFZ: + return MusicFontSymbol.DynamicSforzatoFF; + case DynamicValue.FZ: + return MusicFontSymbol.DynamicForzando; + case DynamicValue.N: + return MusicFontSymbol.DynamicNiente; + case DynamicValue.PF: + return MusicFontSymbol.DynamicPF; + case DynamicValue.SFZP: + return MusicFontSymbol.DynamicSforzatoPiano; default: return MusicFontSymbol.None; } diff --git a/src/rendering/glyphs/EffectGlyph.ts b/src/rendering/glyphs/EffectGlyph.ts index b2dbacfec..db9c2fdad 100644 --- a/src/rendering/glyphs/EffectGlyph.ts +++ b/src/rendering/glyphs/EffectGlyph.ts @@ -1,4 +1,4 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { Glyph } from '@src/rendering/glyphs/Glyph'; /** diff --git a/src/rendering/glyphs/FadeGlyph.ts b/src/rendering/glyphs/FadeGlyph.ts index b27d97f1f..14902f0b3 100644 --- a/src/rendering/glyphs/FadeGlyph.ts +++ b/src/rendering/glyphs/FadeGlyph.ts @@ -1,7 +1,7 @@ import { FadeType } from '@src/model/FadeType'; -import { MusicFontGlyph } from './MusicFontGlyph'; -import { MusicFontSymbol } from '@src/model'; -import { ICanvas } from '@src/platform'; +import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import type { ICanvas } from '@src/platform/ICanvas'; export class FadeGlyph extends MusicFontGlyph { public constructor(type: FadeType) { @@ -20,15 +20,7 @@ export class FadeGlyph extends MusicFontGlyph { return MusicFontSymbol.None; } - public override doLayout(): void { - this.width = 13; - if (this.symbol == MusicFontSymbol.GuitarVolumeSwell) { - this.width *= 2; - } - this.height = 13; - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { - super.paint(cx, cy + this.height, canvas) + super.paint(cx, cy + this.height, canvas); } } diff --git a/src/rendering/glyphs/FermataGlyph.ts b/src/rendering/glyphs/FermataGlyph.ts index c5e696adc..d25c8866d 100644 --- a/src/rendering/glyphs/FermataGlyph.ts +++ b/src/rendering/glyphs/FermataGlyph.ts @@ -1,5 +1,5 @@ import { FermataType } from '@src/model/Fermata'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; @@ -21,11 +21,6 @@ export class FermataGlyph extends MusicFontGlyph { } } - public override doLayout(): void { - this.width = 23; - this.height = 12; - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { super.paint(cx - this.width / 2, cy + this.height, canvas); } diff --git a/src/rendering/glyphs/FingeringGroupGlyph.ts b/src/rendering/glyphs/FingeringGroupGlyph.ts index c22eb0f15..9f276639f 100644 --- a/src/rendering/glyphs/FingeringGroupGlyph.ts +++ b/src/rendering/glyphs/FingeringGroupGlyph.ts @@ -1,14 +1,17 @@ -import { Note } from '@src/model'; import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; -import { ScoreBarRenderer } from '../ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { ModelUtils } from '@src/model/ModelUtils'; import { FingeringMode } from '@src/NotationSettings'; -import { TextGlyph } from './TextGlyph'; -import { TextAlign, TextBaseline } from '@src/platform'; +import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { type Note, NoteSubElement } from '@src/model/Note'; +import { TextAlign, TextBaseline } from '@src/platform/ICanvas'; +import type { Color } from '@src/model/Color'; export class FingeringInfo { public line: number = 0; public text: string; + public color: Color | undefined; public constructor(line: number, text: string) { this.line = line; @@ -47,11 +50,12 @@ export class FingeringGroupGlyph extends GlyphGroup { } private addFinger(note: Note, text: string) { - let sr = this.renderer as ScoreBarRenderer; - let line: number = sr.getNoteLine(note); + const sr = this.renderer as ScoreBarRenderer; + const line: number = sr.getNoteLine(note); if (!this._infos.has(line)) { const info = new FingeringInfo(line, text); + info.color = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.StandardNotationEffects, note); this._infos.set(line, info); } else { const info = this._infos.get(line)!; @@ -60,7 +64,7 @@ export class FingeringGroupGlyph extends GlyphGroup { } public override doLayout(): void { - let sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; for (const [_, info] of this._infos) { const g = new TextGlyph( @@ -71,6 +75,7 @@ export class FingeringGroupGlyph extends GlyphGroup { TextAlign.Left, TextBaseline.Middle ); + g.colorOverride = info.color; g.renderer = sr; g.y = sr.getScoreY(info.line); g.doLayout(); diff --git a/src/rendering/glyphs/FlagGlyph.ts b/src/rendering/glyphs/FlagGlyph.ts index b4e2d8550..c31098557 100644 --- a/src/rendering/glyphs/FlagGlyph.ts +++ b/src/rendering/glyphs/FlagGlyph.ts @@ -5,16 +5,12 @@ import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; export class FlagGlyph extends MusicFontGlyph { - public static readonly FlagWidth:number = 11; + public static readonly FlagWidth: number = 11; public constructor(x: number, y: number, duration: Duration, direction: BeamDirection, isGrace: boolean) { super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, FlagGlyph.getSymbol(duration, direction, isGrace)); } - public override doLayout(): void { - this.width = 0; - } - private static getSymbol(duration: Duration, direction: BeamDirection, isGrace: boolean): MusicFontSymbol { if (isGrace) { duration = Duration.Eighth; diff --git a/src/rendering/glyphs/GhostNoteContainerGlyph.ts b/src/rendering/glyphs/GhostNoteContainerGlyph.ts index ccc81cece..7f9512454 100644 --- a/src/rendering/glyphs/GhostNoteContainerGlyph.ts +++ b/src/rendering/glyphs/GhostNoteContainerGlyph.ts @@ -1,17 +1,21 @@ -import { Note } from '@src/model/Note'; -import { ICanvas } from '@src/platform/ICanvas'; +import { type Note, NoteSubElement } from '@src/model/Note'; +import type { ICanvas } from '@src/platform/ICanvas'; import { GhostParenthesisGlyph } from '@src/rendering/glyphs/GhostParenthesisGlyph'; import { Glyph } from '@src/rendering/glyphs/Glyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { NotationElement } from '@src/NotationSettings'; +import type { Color } from '@src/model/Color'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; export class GhostNoteInfo { public line: number = 0; public isGhost: boolean; + public color: Color | undefined; - public constructor(line: number, isGhost: boolean) { + public constructor(line: number, isGhost: boolean, color: Color | undefined) { this.line = line; this.isGhost = isGhost; + this.color = color; } } @@ -27,17 +31,26 @@ export class GhostNoteContainerGlyph extends Glyph { } public addParenthesis(n: Note): void { - let sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; - let line: number = sr.getNoteLine(n); - let hasParenthesis: boolean = - n.isGhost || (this.isTiedBend(n) && sr.settings.notation.isNotationElementVisible(NotationElement.ParenthesisOnTiedBends)); - this.addParenthesisOnLine(line, hasParenthesis); + const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const line: number = sr.getNoteLine(n); + const hasParenthesis: boolean = + n.isGhost || + (this.isTiedBend(n) && + sr.settings.notation.isNotationElementVisible(NotationElement.ParenthesisOnTiedBends)); + + const color = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.Effects, n); + + this.add(new GhostNoteInfo(line, hasParenthesis, color)); } public addParenthesisOnLine(line: number, hasParenthesis: boolean): void { - let info: GhostNoteInfo = new GhostNoteInfo(line, hasParenthesis); + const info: GhostNoteInfo = new GhostNoteInfo(line, hasParenthesis, undefined); + this.add(info); + } + + private add(info: GhostNoteInfo): void { this._infos.push(info); - if (hasParenthesis) { + if (info.isGhost) { this.isEmpty = false; } } @@ -53,18 +66,20 @@ export class GhostNoteContainerGlyph extends Glyph { } public override doLayout(): void { - let sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; this._infos.sort((a, b) => { return a.line - b.line; }); let previousGlyph: GhostParenthesisGlyph | null = null; - let sizePerLine: number = sr.getScoreHeight(1); + const sizePerLine: number = sr.getScoreHeight(1); + for (let i: number = 0, j: number = this._infos.length; i < j; i++) { let g: GhostParenthesisGlyph; if (!this._infos[i].isGhost) { previousGlyph = null; } else if (!previousGlyph) { 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.height = sizePerLine * 2; @@ -72,7 +87,7 @@ export class GhostNoteContainerGlyph extends Glyph { this._glyphs.push(g); previousGlyph = g; } else { - let y: number = sr.getScoreY(this._infos[i].line) + sizePerLine; + const y: number = sr.getScoreY(this._infos[i].line) + sizePerLine; previousGlyph.height = y - previousGlyph.y; } } @@ -81,7 +96,7 @@ export class GhostNoteContainerGlyph extends Glyph { public override paint(cx: number, cy: number, canvas: ICanvas): void { super.paint(cx, cy, canvas); - for (let g of this._glyphs) { + for (const g of this._glyphs) { g.paint(cx + this.x, cy + this.y, canvas); } } diff --git a/src/rendering/glyphs/GhostParenthesisGlyph.ts b/src/rendering/glyphs/GhostParenthesisGlyph.ts index 2d5ad1603..91115c1b1 100644 --- a/src/rendering/glyphs/GhostParenthesisGlyph.ts +++ b/src/rendering/glyphs/GhostParenthesisGlyph.ts @@ -1,4 +1,5 @@ -import { ICanvas } from '@src/platform/ICanvas'; +import type { Color } from '@src/model/Color'; +import type { ICanvas } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { TieGlyph } from '@src/rendering/glyphs/TieGlyph'; @@ -6,6 +7,8 @@ export class GhostParenthesisGlyph extends Glyph { private static readonly Size: number = 6; private _isOpen: boolean; + public colorOverride?: Color; + public constructor(isOpen: boolean) { super(0, 0); this._isOpen = isOpen; @@ -17,6 +20,10 @@ export class GhostParenthesisGlyph extends Glyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { + const c = canvas.color; + if (this.colorOverride) { + canvas.color = this.colorOverride; + } if (this._isOpen) { TieGlyph.paintTie( canvas, @@ -30,17 +37,8 @@ export class GhostParenthesisGlyph extends Glyph { 3 ); } else { - TieGlyph.paintTie( - canvas, - 1, - cx + this.x, - cy + this.y, - cx + this.x, - cy + this.y + this.height, - false, - 6, - 3 - ); + TieGlyph.paintTie(canvas, 1, cx + this.x, cy + this.y, cx + this.x, cy + this.y + this.height, false, 6, 3); } + canvas.color = c; } } diff --git a/src/rendering/glyphs/Glyph.ts b/src/rendering/glyphs/Glyph.ts index 522f911c0..56c38f6b3 100644 --- a/src/rendering/glyphs/Glyph.ts +++ b/src/rendering/glyphs/Glyph.ts @@ -1,5 +1,5 @@ -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; /** * A glyph is a single symbol which can be added to a GlyphBarRenderer for automated diff --git a/src/rendering/glyphs/GlyphGroup.ts b/src/rendering/glyphs/GlyphGroup.ts index dfb177230..ecc5c92b3 100644 --- a/src/rendering/glyphs/GlyphGroup.ts +++ b/src/rendering/glyphs/GlyphGroup.ts @@ -1,4 +1,4 @@ -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; /** @@ -12,10 +12,6 @@ export class GlyphGroup extends Glyph { return !this.glyphs || this.glyphs.length === 0; } - public constructor(x: number, y: number) { - super(x, y); - } - public override doLayout(): void { if (!this.glyphs || this.glyphs.length === 0) { this.width = 0; @@ -23,7 +19,7 @@ export class GlyphGroup extends Glyph { } let w: number = 0; for (let i: number = 0, j: number = this.glyphs.length; i < j; i++) { - let g: Glyph = this.glyphs[i]; + const g: Glyph = this.glyphs[i]; g.renderer = this.renderer; g.doLayout(); w = Math.max(w, g.width); @@ -35,18 +31,18 @@ export class GlyphGroup extends Glyph { if (!this.glyphs) { this.glyphs = []; } - if(this.renderer) { + if (this.renderer) { g.renderer = this.renderer; } this.glyphs.push(g); } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let glyphs = this.glyphs; + const glyphs = this.glyphs; if (!glyphs || glyphs.length === 0) { return; } - for (let g of glyphs) { + for (const g of glyphs) { g.paint(cx + this.x, cy + this.y, canvas); } } diff --git a/src/rendering/glyphs/GroupedEffectGlyph.ts b/src/rendering/glyphs/GroupedEffectGlyph.ts index 55a52e378..c10ff1f80 100644 --- a/src/rendering/glyphs/GroupedEffectGlyph.ts +++ b/src/rendering/glyphs/GroupedEffectGlyph.ts @@ -1,7 +1,7 @@ -import { Beat } from '@src/model/Beat'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; -import { BeatXPosition } from '@src/rendering/BeatXPosition'; +import type { Beat } from '@src/model/Beat'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BeatXPosition } from '@src/rendering/BeatXPosition'; import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; export abstract class GroupedEffectGlyph extends EffectGlyph { @@ -47,18 +47,18 @@ export abstract class GroupedEffectGlyph extends EffectGlyph { } } // use start position of next beat when possible - let endBeatRenderer: BarRendererBase = lastLinkedGlyph.renderer; - let endBeat: Beat = lastLinkedGlyph.beat!; - let position: BeatXPosition = this.endPosition; + const endBeatRenderer: BarRendererBase = lastLinkedGlyph.renderer; + const endBeat: Beat = lastLinkedGlyph.beat!; + const position: BeatXPosition = this.endPosition; // calculate end X-position - let cxRenderer: number = cx - this.renderer.x; - let endX: number = this.calculateEndX(endBeatRenderer, endBeat, cxRenderer, position); + const cxRenderer: number = cx - this.renderer.x; + const endX: number = this.calculateEndX(endBeatRenderer, endBeat, cxRenderer, position); this.paintGrouped(cx, cy, endX, canvas); } protected calculateEndX( endBeatRenderer: BarRendererBase, - endBeat: Beat|null, + endBeat: Beat | null, cx: number, endPosition: BeatXPosition ): number { @@ -69,8 +69,8 @@ export abstract class GroupedEffectGlyph extends EffectGlyph { } protected paintNonGrouped(cx: number, cy: number, canvas: ICanvas): void { - let cxRenderer: number = cx - this.renderer.x; - let endX: number = this.calculateEndX(this.renderer, this.beat, cxRenderer, this.endPosition); + const cxRenderer: number = cx - this.renderer.x; + const endX: number = this.calculateEndX(this.renderer, this.beat, cxRenderer, this.endPosition); this.paintGrouped(cx, cy, endX, canvas); } diff --git a/src/rendering/glyphs/GuitarGolpeGlyph.ts b/src/rendering/glyphs/GuitarGolpeGlyph.ts index c106acdcf..f1d40829a 100644 --- a/src/rendering/glyphs/GuitarGolpeGlyph.ts +++ b/src/rendering/glyphs/GuitarGolpeGlyph.ts @@ -1,27 +1,15 @@ -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; -import { EffectGlyph } from './EffectGlyph'; +import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; -export class GuitarGolpeGlyph extends EffectGlyph { - private _center: boolean; +export class GuitarGolpeGlyph extends MusicFontGlyph { public constructor(x: number, y: number, center: boolean = false) { - super(x, y); - this._center = center; - } - - public override doLayout(): void { - this.width = 9; - this.height = 10; + super(x, y, NoteHeadGlyph.GraceScale, MusicFontSymbol.GuitarGolpe); + this.center = center; } public override paint(cx: number, cy: number, canvas: ICanvas): void { - canvas.fillMusicFontSymbol( - cx + this.x, - cy + this.y + this.height, - NoteHeadGlyph.GraceScale, - MusicFontSymbol.GuitarGolpe, - this._center - ); + super.paint(cx, cy + this.height, canvas); } } diff --git a/src/rendering/glyphs/KeySignatureGlyph.ts b/src/rendering/glyphs/KeySignatureGlyph.ts new file mode 100644 index 000000000..b9912ad85 --- /dev/null +++ b/src/rendering/glyphs/KeySignatureGlyph.ts @@ -0,0 +1,11 @@ +import type { ICanvas } from '@src/platform/ICanvas'; +import { LeftToRightLayoutingGlyphGroup } from '@src/rendering/glyphs/LeftToRightLayoutingGlyphGroup'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { BarSubElement } from '@src/model/Bar'; + +export class KeySignatureGlyph extends LeftToRightLayoutingGlyphGroup { + public override paint(cx: number, cy: number, canvas: ICanvas): void { + using _ = ElementStyleHelper.bar(canvas, BarSubElement.StandardNotationKeySignature, this.renderer.bar); + super.paint(cx, cy, canvas); + } +} diff --git a/src/rendering/glyphs/LeftHandTapGlyph.ts b/src/rendering/glyphs/LeftHandTapGlyph.ts index 0cd4b0ee0..cbdf60932 100644 --- a/src/rendering/glyphs/LeftHandTapGlyph.ts +++ b/src/rendering/glyphs/LeftHandTapGlyph.ts @@ -1,6 +1,6 @@ -import { ICanvas, TextAlign } from '@src/platform/ICanvas'; +import { type ICanvas, TextAlign } from '@src/platform/ICanvas'; import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; -import { RenderingResources } from '@src/RenderingResources'; +import type { RenderingResources } from '@src/RenderingResources'; export class LeftHandTapGlyph extends EffectGlyph { private static readonly Padding = 4; @@ -16,12 +16,16 @@ export class LeftHandTapGlyph extends EffectGlyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let res: RenderingResources = this.renderer.resources; + const res: RenderingResources = this.renderer.resources; canvas.font = res.effectFont; - let old: TextAlign = canvas.textAlign; + const old: TextAlign = canvas.textAlign; canvas.textAlign = TextAlign.Center; canvas.fillText('T', cx + this.x, cy + this.y + canvas.font.size / 2); canvas.textAlign = old; - canvas.strokeCircle(cx + this.x, cy + this.y + canvas.font.size / 2 + (LeftHandTapGlyph.Padding - 1), canvas.font.size / 1.6); + canvas.strokeCircle( + cx + this.x, + cy + this.y + canvas.font.size / 2 + (LeftHandTapGlyph.Padding - 1), + canvas.font.size / 1.6 + ); } } diff --git a/src/rendering/glyphs/LeftToRightLayoutingGlyphGroup.ts b/src/rendering/glyphs/LeftToRightLayoutingGlyphGroup.ts index 7565a1c65..8b736c851 100644 --- a/src/rendering/glyphs/LeftToRightLayoutingGlyphGroup.ts +++ b/src/rendering/glyphs/LeftToRightLayoutingGlyphGroup.ts @@ -1,4 +1,4 @@ -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; export class LeftToRightLayoutingGlyphGroup extends GlyphGroup { @@ -7,11 +7,12 @@ export class LeftToRightLayoutingGlyphGroup extends GlyphGroup { this.glyphs = []; } + public override doLayout(): void { + // skip + } + public override addGlyph(g: Glyph): void { - g.x = - this.glyphs!.length === 0 - ? 0 - : this.glyphs![this.glyphs!.length - 1].x + this.glyphs![this.glyphs!.length - 1].width; + g.x = this.width; g.renderer = this.renderer; g.doLayout(); this.width = g.x + g.width; diff --git a/src/rendering/glyphs/LineRangedGlyph.ts b/src/rendering/glyphs/LineRangedGlyph.ts index 40c76c854..b9b5dc6d2 100644 --- a/src/rendering/glyphs/LineRangedGlyph.ts +++ b/src/rendering/glyphs/LineRangedGlyph.ts @@ -1,7 +1,7 @@ -import { ICanvas, TextAlign } from '@src/platform/ICanvas'; +import { type ICanvas, TextAlign } from '@src/platform/ICanvas'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { GroupedEffectGlyph } from '@src/rendering/glyphs/GroupedEffectGlyph'; -import { RenderingResources } from '@src/RenderingResources'; +import type { RenderingResources } from '@src/RenderingResources'; export class LineRangedGlyph extends GroupedEffectGlyph { public static readonly LineSpacing: number = 3; @@ -27,9 +27,9 @@ export class LineRangedGlyph extends GroupedEffectGlyph { } protected override paintNonGrouped(cx: number, cy: number, canvas: ICanvas): void { - let res: RenderingResources = this.renderer.resources; + const res: RenderingResources = this.renderer.resources; canvas.font = res.effectFont; - let x: TextAlign = canvas.textAlign; + const x: TextAlign = canvas.textAlign; canvas.textAlign = TextAlign.Center; canvas.fillText(this._label, cx + this.x, cy + this.y); canvas.textAlign = x; @@ -37,11 +37,11 @@ export class LineRangedGlyph extends GroupedEffectGlyph { protected paintGrouped(cx: number, cy: number, endX: number, canvas: ICanvas): void { this.paintNonGrouped(cx, cy, canvas); - let lineSpacing: number = 3; - let textWidth: number = canvas.measureText(this._label).width; - let startX: number = cx + this.x + textWidth / 2 + lineSpacing; - let lineY: number = cy + this.y + 4; - let lineSize: number = 8; + const lineSpacing: number = 3; + const textWidth: number = canvas.measureText(this._label).width; + const startX: number = cx + this.x + textWidth / 2 + lineSpacing; + const lineY: number = cy + this.y + 4; + const lineSize: number = 8; if (this._dashed) { if (endX > startX) { let lineX: number = startX; diff --git a/src/rendering/glyphs/LyricsGlyph.ts b/src/rendering/glyphs/LyricsGlyph.ts index 90cf0785c..3e9b7a6e3 100644 --- a/src/rendering/glyphs/LyricsGlyph.ts +++ b/src/rendering/glyphs/LyricsGlyph.ts @@ -1,5 +1,5 @@ -import { Font } from '@src/model/Font'; -import { ICanvas, TextAlign } from '@src/platform/ICanvas'; +import type { Font } from '@src/model/Font'; +import { type ICanvas, TextAlign } from '@src/platform/ICanvas'; import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; export class LyricsGlyph extends EffectGlyph { @@ -22,7 +22,7 @@ export class LyricsGlyph extends EffectGlyph { public override paint(cx: number, cy: number, canvas: ICanvas): void { canvas.font = this.font; - let old: TextAlign = canvas.textAlign; + const old: TextAlign = canvas.textAlign; canvas.textAlign = this.textAlign; for (let i: number = 0; i < this._lines.length; i++) { if (this._lines[i]) { diff --git a/src/rendering/glyphs/MultiBarRestGlyph.ts b/src/rendering/glyphs/MultiBarRestGlyph.ts new file mode 100644 index 000000000..93b983e53 --- /dev/null +++ b/src/rendering/glyphs/MultiBarRestGlyph.ts @@ -0,0 +1,40 @@ +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { LineBarRenderer } from '@src/rendering/LineBarRenderer'; +import { Glyph } from '@src/rendering/glyphs/Glyph'; +import { NumberGlyph } from '@src/rendering/glyphs/NumberGlyph'; + +export class MultiBarRestGlyph extends Glyph { + private _numberGlyph: MusicFontSymbol[] = []; + private static readonly BarWidth = 60; + constructor() { + super(0, 0); + } + + public override doLayout(): void { + this.width = 70; + this.renderer.registerOverflowTop((this.renderer as LineBarRenderer).getLineHeight(1)); + const i: number = this.renderer.additionalMultiRestBars!.length + 1; + this._numberGlyph = NumberGlyph.getSymbols(i); + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + canvas.fillMusicFontSymbols(cx + this.x, cy + this.y + this.renderer.height / 2, 1, [ + MusicFontSymbol.RestHBarLeft, + MusicFontSymbol.RestHBarMiddle, + MusicFontSymbol.RestHBarMiddle, + MusicFontSymbol.RestHBarMiddle, + MusicFontSymbol.RestHBarRight + ]); + + const numberTop = (this.renderer as LineBarRenderer).getLineY(-1.5); + + canvas.fillMusicFontSymbols( + cx + this.x + MultiBarRestGlyph.BarWidth / 2, + (cy + this.y + numberTop) | 0, + 1, + this._numberGlyph, + true + ); + } +} diff --git a/src/rendering/glyphs/MusicFontGlyph.ts b/src/rendering/glyphs/MusicFontGlyph.ts index 76c1b1dda..76bf5e18e 100644 --- a/src/rendering/glyphs/MusicFontGlyph.ts +++ b/src/rendering/glyphs/MusicFontGlyph.ts @@ -1,11 +1,14 @@ -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; -import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import type { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import type { Color } from '@src/model/Color'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; export class MusicFontGlyph extends EffectGlyph { protected glyphScale: number = 0; - protected symbol: MusicFontSymbol; + public symbol: MusicFontSymbol; protected center: boolean = false; + public colorOverride?: Color; public constructor(x: number, y: number, glyphScale: number, symbol: MusicFontSymbol) { super(x, y); @@ -13,7 +16,22 @@ export class MusicFontGlyph extends EffectGlyph { this.symbol = symbol; } + public override doLayout(): void { + this.width = MusicFontSymbolSizes.Widths.has(this.symbol) + ? MusicFontSymbolSizes.Widths.get(this.symbol)! * this.glyphScale + : 0; + + this.height = MusicFontSymbolSizes.Heights.has(this.symbol) + ? MusicFontSymbolSizes.Heights.get(this.symbol)! * this.glyphScale + : 0; + } + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const c = canvas.color; + if (this.colorOverride) { + canvas.color = this.colorOverride!; + } canvas.fillMusicFontSymbol(cx + this.x, cy + this.y, this.glyphScale, this.symbol, this.center); + canvas.color = c; } } diff --git a/src/rendering/glyphs/NoteHeadGlyph.ts b/src/rendering/glyphs/NoteHeadGlyph.ts index b6cd71500..332cea6fd 100644 --- a/src/rendering/glyphs/NoteHeadGlyph.ts +++ b/src/rendering/glyphs/NoteHeadGlyph.ts @@ -1,43 +1,25 @@ import { Duration } from '@src/model/Duration'; -import { ICanvas } from '@src/platform/ICanvas'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import type { ICanvas } from '@src/platform/ICanvas'; export class NoteHeadGlyph extends MusicFontGlyph { public static readonly GraceScale: number = 0.75; - public static readonly NoteHeadHeight: number = 8; - public static readonly QuarterNoteHeadWidth: number = 9; + private _isGrace: boolean; - private _duration: Duration; + public centerOnStem = false; public constructor(x: number, y: number, duration: Duration, isGrace: boolean) { super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, NoteHeadGlyph.getSymbol(duration)); this._isGrace = isGrace; - this._duration = duration; } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let offset: number = this._isGrace ? 1 : 0; - canvas.fillMusicFontSymbol(cx + this.x, cy + this.y + offset, this.glyphScale, this.symbol, false); - } - - public override doLayout(): void { - let scale: number = (this._isGrace ? NoteHeadGlyph.GraceScale : 1); - switch (this._duration) { - case Duration.QuadrupleWhole: - this.width = 14 * scale; - break; - case Duration.DoubleWhole: - this.width = 14 * (this._isGrace ? NoteHeadGlyph.GraceScale : 1); - break; - case Duration.Whole: - this.width = 14 * (this._isGrace ? NoteHeadGlyph.GraceScale : 1); - break; - default: - this.width = NoteHeadGlyph.QuarterNoteHeadWidth * (this._isGrace ? NoteHeadGlyph.GraceScale : 1); - break; + const offset: number = this._isGrace ? 1 : 0; + if (this.centerOnStem) { + this.center = true; } - this.height = NoteHeadGlyph.NoteHeadHeight * scale; + super.paint(cx, cy + offset, canvas); } private static getSymbol(duration: Duration): MusicFontSymbol { diff --git a/src/rendering/glyphs/NoteNumberGlyph.ts b/src/rendering/glyphs/NoteNumberGlyph.ts index 3d510489d..3205e5e8a 100644 --- a/src/rendering/glyphs/NoteNumberGlyph.ts +++ b/src/rendering/glyphs/NoteNumberGlyph.ts @@ -1,14 +1,15 @@ import { BendType } from '@src/model/BendType'; -import { Font } from '@src/model/Font'; +import type { Font } from '@src/model/Font'; import { HarmonicType } from '@src/model/HarmonicType'; -import { Note } from '@src/model/Note'; -import { ICanvas } from '@src/platform/ICanvas'; +import { type Note, NoteSubElement } from '@src/model/Note'; +import type { ICanvas } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { Bounds } from '@src/rendering/utils/Bounds'; import { NoteBounds } from '@src/rendering/utils/NoteBounds'; import { ModelUtils } from '@src/model/ModelUtils'; import { NotationElement, NotationMode } from '@src/NotationSettings'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; export class NoteNumberGlyph extends Glyph { private _note: Note; @@ -25,7 +26,7 @@ export class NoteNumberGlyph extends Glyph { } public override doLayout(): void { - let n: Note = this._note; + const n: Note = this._note; let fret: number = n.fret - n.beat.voice.bar.staff.transpositionPitch; if (n.harmonicType === HarmonicType.Natural && n.harmonicValue !== 0) { fret = n.harmonicValue - n.beat.voice.bar.staff.transpositionPitch; @@ -33,26 +34,26 @@ export class NoteNumberGlyph extends Glyph { if (!n.isTieDestination) { this._noteString = n.isDead ? 'x' : fret.toString(); if (n.isGhost) { - this._noteString = '(' + this._noteString + ')'; + this._noteString = `(${this._noteString})`; } else if (n.harmonicType === HarmonicType.Natural) { // only first decimal char - let i: number = this._noteString.indexOf(String.fromCharCode(46)); + const i: number = this._noteString.indexOf(String.fromCharCode(46)); if (i >= 0) { this._noteString = this._noteString.substr(0, i + 2); } - this._noteString = '<' + this._noteString + '>'; + this._noteString = `<${this._noteString}>`; } } else if ( - (n.beat.index === 0 && this.renderer.settings.notation.notationMode == NotationMode.GuitarPro) || + (n.beat.index === 0 && this.renderer.settings.notation.notationMode === NotationMode.GuitarPro) || ((n.bendType === BendType.Bend || n.bendType === BendType.BendRelease) && this.renderer.settings.notation.isNotationElementVisible(NotationElement.TabNotesOnTiedBends)) ) { - this._noteString = '(' + (n.tieOrigin!.fret - n.beat.voice.bar.staff.transpositionPitch).toString() + ')'; + this._noteString = `(${(n.tieOrigin!.fret - n.beat.voice.bar.staff.transpositionPitch).toString()})`; } else { this._noteString = ''; } if (n.isTrill) { - this._trillNoteString = '(' + (n.trillFret - n.beat.voice.bar.staff.transpositionPitch).toString() + ')'; + this._trillNoteString = `(${(n.trillFret - n.beat.voice.bar.staff.transpositionPitch).toString()})`; } else if (!ModelUtils.isAlmostEqualTo(n.harmonicValue, 0)) { switch (n.harmonicType) { case HarmonicType.Artificial: @@ -62,11 +63,11 @@ export class NoteNumberGlyph extends Glyph { case HarmonicType.Feedback: let s: string = (fret + n.harmonicValue).toString(); // only first decimal char - let i: number = s.indexOf(String.fromCharCode(46)); + const i: number = s.indexOf(String.fromCharCode(46)); if (i >= 0) { s = s.substr(0, i + 2); } - this._trillNoteString = '<' + s + '>'; + this._trillNoteString = `<${s}>`; break; default: this._trillNoteString = ''; @@ -81,7 +82,7 @@ export class NoteNumberGlyph extends Glyph { this.noteStringWidth = this.renderer.scoreRenderer.canvas!.measureText(this._noteString).width; this.width = this.noteStringWidth; this.height = this.renderer.scoreRenderer.canvas!.font.size; - let hasTrill: boolean = !!this._trillNoteString; + const hasTrill: boolean = !!this._trillNoteString; if (hasTrill) { this.renderer.scoreRenderer.canvas!.font = this.renderer.resources.graceFont; this._trillNoteStringWidth = @@ -95,21 +96,28 @@ export class NoteNumberGlyph extends Glyph { if (this.isEmpty) { return; } - let textWidth: number = this.noteStringWidth + this._trillNoteStringWidth; - let x: number = cx + this.x + (this.width - textWidth) / 2; - let prevFont: Font = this.renderer.scoreRenderer.canvas!.font; + const textWidth: number = this.noteStringWidth + this._trillNoteStringWidth; + const x: number = cx + this.x + (this.width - textWidth) / 2; + + this.paintTrill(x, cy, canvas); + + using _ = ElementStyleHelper.note(canvas, NoteSubElement.GuitarTabFretNumber, this._note); + canvas.fillText(this._noteString!, x, cy + this.y); + } + 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 + 3, cy + this.y); this.renderer.scoreRenderer.canvas!.font = prevFont; - canvas.fillText(this._noteString!, x, cy + this.y); } public buildBoundingsLookup(beatBounds: BeatBounds, cx: number, cy: number) { - let noteBounds: NoteBounds = new NoteBounds(); + const noteBounds: NoteBounds = new NoteBounds(); noteBounds.note = this._note; noteBounds.noteHeadBounds = new Bounds(); noteBounds.noteHeadBounds.x = cx + this.x; - noteBounds.noteHeadBounds.y = cy + this.y - this.height/2; + noteBounds.noteHeadBounds.y = cy + this.y - this.height / 2; noteBounds.noteHeadBounds.w = this.width; noteBounds.noteHeadBounds.h = this.height; beatBounds.addNote(noteBounds); diff --git a/src/rendering/glyphs/NoteOrnamentGlyph.ts b/src/rendering/glyphs/NoteOrnamentGlyph.ts index 507125b27..59e4b1038 100644 --- a/src/rendering/glyphs/NoteOrnamentGlyph.ts +++ b/src/rendering/glyphs/NoteOrnamentGlyph.ts @@ -1,7 +1,7 @@ import { NoteOrnament } from '@src/model/NoteOrnament'; -import { MusicFontGlyph } from './MusicFontGlyph'; -import { MusicFontSymbol } from '@src/model'; -import { ICanvas } from '@src/platform'; +import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import type { ICanvas } from '@src/platform/ICanvas'; export class NoteOrnamentGlyph extends MusicFontGlyph { constructor(ornament: NoteOrnament) { @@ -9,11 +9,6 @@ export class NoteOrnamentGlyph extends MusicFontGlyph { this.center = true; } - public override doLayout(): void { - this.width = 26; - this.height = 18; - } - private static getSymbol(ornament: NoteOrnament): MusicFontSymbol { switch (ornament) { case NoteOrnament.InvertedTurn: diff --git a/src/rendering/glyphs/NoteVibratoGlyph.ts b/src/rendering/glyphs/NoteVibratoGlyph.ts index a4bb06348..583485ac0 100644 --- a/src/rendering/glyphs/NoteVibratoGlyph.ts +++ b/src/rendering/glyphs/NoteVibratoGlyph.ts @@ -1,8 +1,9 @@ import { VibratoType } from '@src/model/VibratoType'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { GroupedEffectGlyph } from '@src/rendering/glyphs/GroupedEffectGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; export class NoteVibratoGlyph extends GroupedEffectGlyph { private _type: VibratoType; @@ -22,26 +23,23 @@ export class NoteVibratoGlyph extends GroupedEffectGlyph { public override doLayout(): void { super.doLayout(); - let symbolHeight: number = 0; switch (this._type) { case VibratoType.Slight: this._symbol = MusicFontSymbol.WiggleTrill; - this._symbolSize = 9 * this._scale; - symbolHeight = 6 * this._scale; break; case VibratoType.Wide: this._symbol = MusicFontSymbol.WiggleVibratoMediumFast; - this._symbolSize = 10 * this._scale; - symbolHeight = 10 * this._scale; break; } - this.height = symbolHeight; + + this._symbolSize = MusicFontSymbolSizes.Widths.get(this._symbol)! * this._scale; + this.height = MusicFontSymbolSizes.Heights.get(this._symbol)! * this._scale; } protected paintGrouped(cx: number, cy: number, endX: number, canvas: ICanvas): void { - let startX: number = cx + this.x; - let width: number = endX - startX; - let step: number = this._symbolSize; + const startX: number = cx + this.x; + const width: number = endX - startX; + const step: number = this._symbolSize; let loops: number = width / step; if (!this._partialWaves) { diff --git a/src/rendering/glyphs/NumberGlyph.ts b/src/rendering/glyphs/NumberGlyph.ts index 10d33f7d6..59e430b7b 100644 --- a/src/rendering/glyphs/NumberGlyph.ts +++ b/src/rendering/glyphs/NumberGlyph.ts @@ -1,40 +1,67 @@ -import { DigitGlyph } from '@src/rendering/glyphs/DigitGlyph'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { Glyph } from '@src/rendering/glyphs/Glyph'; -import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; +import type { ICanvas } from '@src/platform/ICanvas'; + +export class NumberGlyph extends Glyph { + public static readonly numberHeight: number = 18; -export class NumberGlyph extends GlyphGroup { - public static readonly numberHeight:number = 18; - - private _number: number = 0; private _scale: number = 0; + private _symbols: MusicFontSymbol[] = []; + public constructor(x: number, y: number, num: number, scale: number = 1.0) { super(x, y); - this._number = num; + this._symbols = NumberGlyph.getSymbols(num); this._scale = scale; } - public override doLayout(): void { - let i: number = this._number; - while (i > 0) { - let num: number = i % 10; - let gl: DigitGlyph = new DigitGlyph(0, 0, num, this._scale); - this.addGlyph(gl); - i = (i / 10) | 0; + public static getSymbols(number: number): MusicFontSymbol[] { + const symbols: MusicFontSymbol[] = []; + while (number > 0) { + const digit: number = number % 10; + symbols.unshift(NumberGlyph.getSymbol(digit)); + number = (number / 10) | 0; + } + return symbols; + } + + public static getSymbol(digit: number): MusicFontSymbol { + switch (digit) { + case 0: + return MusicFontSymbol.TimeSig0; + case 1: + return MusicFontSymbol.TimeSig1; + case 2: + return MusicFontSymbol.TimeSig2; + case 3: + return MusicFontSymbol.TimeSig3; + case 4: + return MusicFontSymbol.TimeSig4; + case 5: + return MusicFontSymbol.TimeSig5; + case 6: + return MusicFontSymbol.TimeSig6; + case 7: + return MusicFontSymbol.TimeSig7; + case 8: + return MusicFontSymbol.TimeSig8; + case 9: + return MusicFontSymbol.TimeSig9; + default: + return MusicFontSymbol.None; } + } - if (this.glyphs) { - this.glyphs.reverse(); - let cx: number = 0; - for (let j: number = 0, k: number = this.glyphs.length; j < k; j++) { - let g: Glyph = this.glyphs[j]; - g.x = cx; - g.y = 0; - g.renderer = this.renderer; - g.doLayout(); - cx += g.width; - } - this.width = cx; + public override doLayout(): void { + let w = 0; + for (const d of this._symbols) { + w += MusicFontSymbolSizes.Widths.get(d)!; } + this.width = w; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + canvas.fillMusicFontSymbols(cx + this.x, cy + this.y, this._scale, this._symbols); } } diff --git a/src/rendering/glyphs/NumberedBeatGlyph.ts b/src/rendering/glyphs/NumberedBeatGlyph.ts index a4882c826..96e04a002 100644 --- a/src/rendering/glyphs/NumberedBeatGlyph.ts +++ b/src/rendering/glyphs/NumberedBeatGlyph.ts @@ -1,32 +1,41 @@ import { GraceType } from '@src/model/GraceType'; -import { Note } from '@src/model/Note'; +import { type Note, NoteSubElement } from '@src/model/Note'; import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; import { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; -import { NoteBounds } from '../utils/NoteBounds'; -import { Bounds } from '../utils/Bounds'; -import { NumberedNoteHeadGlyph } from './NumberedNoteHeadGlyph'; -import { AccidentalType, Duration, KeySignatureType, NoteAccidentalMode } from '@src/model'; -import { NumberedBarRenderer } from '../NumberedBarRenderer'; -import { AccidentalHelper } from '../utils/AccidentalHelper'; -import { BeatGlyphBase } from './BeatGlyphBase'; -import { AccidentalGroupGlyph } from './AccidentalGroupGlyph'; -import { AccidentalGlyph } from './AccidentalGlyph'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import { NoteBounds } from '@src/rendering/utils/NoteBounds'; +import { Bounds } from '@src/rendering/utils/Bounds'; +import { NumberedNoteHeadGlyph } from '@src/rendering/glyphs/NumberedNoteHeadGlyph'; +import type { NumberedBarRenderer } from '@src/rendering/NumberedBarRenderer'; +import { AccidentalHelper } from '@src/rendering/utils/AccidentalHelper'; +import { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; +import { AccidentalGroupGlyph } from '@src/rendering/glyphs/AccidentalGroupGlyph'; +import { AccidentalGlyph } from '@src/rendering/glyphs/AccidentalGlyph'; import { ModelUtils } from '@src/model/ModelUtils'; -import { NoteHeadGlyph } from './NoteHeadGlyph'; -import { SpacingGlyph } from './SpacingGlyph'; -import { CircleGlyph } from './CircleGlyph'; -import { NumberedDashGlyph } from './NumberedDashGlyph'; -import { Glyph } from './Glyph'; -import { DeadSlappedBeatGlyph } from './DeadSlappedBeatGlyph'; +import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; +import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; +import { CircleGlyph } from '@src/rendering/glyphs/CircleGlyph'; +import { NumberedDashGlyph } from '@src/rendering/glyphs/NumberedDashGlyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; +import { DeadSlappedBeatGlyph } from '@src/rendering/glyphs/DeadSlappedBeatGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { AccidentalType } from '@src/model/AccidentalType'; +import { BeatSubElement } from '@src/model/Beat'; +import { Duration } from '@src/model/Duration'; +import { KeySignatureType } from '@src/model/KeySignatureType'; +import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; export class NumberedBeatPreNotesGlyph extends BeatGlyphBase { public isNaturalizeAccidental = false; public accidental: AccidentalType = AccidentalType.None; + protected override get effectElement() { + return BeatSubElement.NumberedEffects; + } + public override doLayout(): void { if (!this.container.beat.isRest && !this.container.beat.isEmpty) { - let accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); + const accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); accidentals.renderer = this.renderer; if (this.container.beat.notes.length > 0) { @@ -47,14 +56,14 @@ export class NumberedBeatPreNotesGlyph extends BeatGlyphBase { const accidentalMode = note ? note.accidentalMode : NoteAccidentalMode.Default; const noteValue = AccidentalHelper.getNoteValue(note); let accidentalToSet: AccidentalType = AccidentalHelper.computeAccidental( - this.renderer.bar.masterBar.keySignature, + this.renderer.bar.keySignature, accidentalMode, noteValue, note.hasQuarterToneOffset ); - if (accidentalToSet == AccidentalType.Natural) { - const ks: number = this.renderer.bar.masterBar.keySignature; + if (accidentalToSet === AccidentalType.Natural) { + const ks: number = this.renderer.bar.keySignature; const ksi: number = ks + 7; const naturalizeAccidentalForKeySignature: AccidentalType = ksi < 7 ? AccidentalType.Sharp : AccidentalType.Flat; @@ -65,9 +74,10 @@ export class NumberedBeatPreNotesGlyph extends BeatGlyphBase { // do we need an accidental on the note? if (accidentalToSet !== AccidentalType.None) { this.accidental = accidentalToSet; - let sr: NumberedBarRenderer = this.renderer as NumberedBarRenderer; + const sr: NumberedBarRenderer = this.renderer as NumberedBarRenderer; + const color = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.NumberedAccidentals, note); - let g = new AccidentalGlyph( + const g = new AccidentalGlyph( 0, sr.getLineY(0), accidentalToSet, @@ -75,10 +85,11 @@ export class NumberedBeatPreNotesGlyph extends BeatGlyphBase { ? NoteHeadGlyph.GraceScale * NoteHeadGlyph.GraceScale : NoteHeadGlyph.GraceScale ); + g.colorOverride = color; g.renderer = this.renderer; accidentals.addGlyph(g); - this.addGlyph(accidentals); - this.addGlyph(new SpacingGlyph(0, 0, 4)); + this.addNormal(accidentals); + this.addNormal(new SpacingGlyph(0, 0, 4)); } } } @@ -92,6 +103,10 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { 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) { @@ -199,7 +214,7 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { public override doLayout(): void { // create glyphs - let sr = this.renderer as NumberedBarRenderer; + const sr = this.renderer as NumberedBarRenderer; if (sr.shortestDuration < this.container.beat.duration) { sr.shortestDuration = this.container.beat.duration; @@ -210,8 +225,8 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { if (!this.container.beat.isEmpty) { let numberWithinOctave = '0'; if (this.container.beat.notes.length > 0) { - const kst: number = this.renderer.bar.masterBar.keySignatureType; - const ks: number = this.renderer.bar.masterBar.keySignature; + const kst: number = this.renderer.bar.keySignatureType; + const ks: number = this.renderer.bar.keySignature; const ksi: number = ks + 7; const oneNoteValues = @@ -225,9 +240,9 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { if (note.isDead) { numberWithinOctave = 'X'; } else { - let noteValue = note.displayValue - oneNoteValue; + const noteValue = note.displayValue - oneNoteValue; - let index = noteValue < 0 ? ((noteValue % 12) + 12) % 12 : noteValue % 12; + const index = noteValue < 0 ? ((noteValue % 12) + 12) % 12 : noteValue % 12; let dots = noteValue < 0 ? ((Math.abs(noteValue) + 12) / 12) | 0 : (noteValue / 12) | 0; if (noteValue < 0) { @@ -264,23 +279,29 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { deadSlapped.renderer = this.renderer; deadSlapped.doLayout(); this.deadSlapped = deadSlapped; - this.addGlyph(deadSlapped); + this.addEffect(deadSlapped); } else { const isGrace: boolean = this.container.beat.graceType !== GraceType.None; - const noteHeadGlyph = new NumberedNoteHeadGlyph(0, glyphY, numberWithinOctave, isGrace); + const noteHeadGlyph = new NumberedNoteHeadGlyph( + 0, + glyphY, + numberWithinOctave, + isGrace, + this.container.beat + ); this.noteHeads = noteHeadGlyph; - this.addGlyph(noteHeadGlyph); + this.addNormal(noteHeadGlyph); } // // Note dots if (this.container.beat.dots > 0 && this.container.beat.duration >= Duration.Quarter) { - this.addGlyph(new SpacingGlyph(0, 0, 5)); + this.addNormal(new SpacingGlyph(0, 0, 5)); for (let i: number = 0; i < this.container.beat.dots; i++) { const dot = new CircleGlyph(0, sr.getLineY(0), 1.5); dot.renderer = this.renderer; - this.addGlyph(dot); + this.addEffect(dot); } } @@ -308,9 +329,9 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { numberOfQuarterNotes += numberOfAddedQuarters; } for (let i = 0; i < numberOfQuarterNotes - 1; i++) { - const dash = new NumberedDashGlyph(0, sr.getLineY(0)); + const dash = new NumberedDashGlyph(0, sr.getLineY(0), this.container.beat); dash.renderer = this.renderer; - this.addGlyph(dash); + this.addNormal(dash); } } diff --git a/src/rendering/glyphs/NumberedDashGlyph.ts b/src/rendering/glyphs/NumberedDashGlyph.ts index f878c1862..44a427d3e 100644 --- a/src/rendering/glyphs/NumberedDashGlyph.ts +++ b/src/rendering/glyphs/NumberedDashGlyph.ts @@ -1,15 +1,25 @@ -import { ICanvas } from '@src/platform'; -import { Glyph } from './Glyph'; -import { NumberedBarRenderer } from '../NumberedBarRenderer'; +import { Glyph } from '@src/rendering/glyphs/Glyph'; +import { NumberedBarRenderer } from '@src/rendering/NumberedBarRenderer'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; +import type { ICanvas } from '@src/platform/ICanvas'; export class NumberedDashGlyph extends Glyph { private static Padding = 3; + private _beat: Beat; + + public constructor(x: number, y: number, beat: Beat) { + super(x, y); + this._beat = beat; + } + public override doLayout(): void { - this.width = (14 + NumberedDashGlyph.Padding); + this.width = 14 + NumberedDashGlyph.Padding; this.height = NumberedBarRenderer.BarSize; } public override paint(cx: number, cy: number, canvas: ICanvas): void { + using _ = ElementStyleHelper.beat(canvas, BeatSubElement.NumberedDuration, this._beat); const padding = NumberedDashGlyph.Padding; canvas.fillRect(cx + this.x, cy + this.y, this.width - padding, this.height); } diff --git a/src/rendering/glyphs/NumberedKeySignatureGlyph.ts b/src/rendering/glyphs/NumberedKeySignatureGlyph.ts index 9a4c7ea93..4842dfd22 100644 --- a/src/rendering/glyphs/NumberedKeySignatureGlyph.ts +++ b/src/rendering/glyphs/NumberedKeySignatureGlyph.ts @@ -1,7 +1,11 @@ -import { ICanvas, TextBaseline } from '@src/platform'; -import { Glyph } from './Glyph'; -import { AccidentalType, KeySignature, KeySignatureType } from '@src/model'; -import { AccidentalGlyph } from './AccidentalGlyph'; +import { Glyph } from '@src/rendering/glyphs/Glyph'; +import { AccidentalGlyph } from '@src/rendering/glyphs/AccidentalGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { AccidentalType } from '@src/model/AccidentalType'; +import { KeySignatureType } from '@src/model/KeySignatureType'; +import { KeySignature } from '@src/model/KeySignature'; +import { type ICanvas, TextBaseline } from '@src/platform/ICanvas'; +import { BarSubElement } from '@src/model/Bar'; export class NumberedKeySignatureGlyph extends Glyph { private _keySignature: KeySignature; @@ -161,12 +165,14 @@ export class NumberedKeySignatureGlyph extends Glyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { + using _ = ElementStyleHelper.bar(canvas, BarSubElement.NumberedKeySignature, this.renderer.bar); + const res = this.renderer.resources; canvas.font = res.numberedNotationFont; canvas.textBaseline = TextBaseline.Middle; canvas.fillText(this._text, cx + this.x, cy + this.y); - if (this._accidental != AccidentalType.None) { + if (this._accidental !== AccidentalType.None) { canvas.fillMusicFontSymbol( cx + this.x + this._accidentalOffset, cy + this.y, diff --git a/src/rendering/glyphs/NumberedNoteHeadGlyph.ts b/src/rendering/glyphs/NumberedNoteHeadGlyph.ts index 2506a5d30..2153494b8 100644 --- a/src/rendering/glyphs/NumberedNoteHeadGlyph.ts +++ b/src/rendering/glyphs/NumberedNoteHeadGlyph.ts @@ -1,21 +1,32 @@ -import { ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; -import { NoteHeadGlyph } from './NoteHeadGlyph'; -import { Glyph } from './Glyph'; +import { type ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; +import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; +import { Glyph } from '@src/rendering/glyphs/Glyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; +import { NoteSubElement } from '@src/model/Note'; export class NumberedNoteHeadGlyph extends Glyph { public static readonly NoteHeadHeight: number = 17; public static readonly NoteHeadWidth: number = 12; private _isGrace: boolean; + private _beat: Beat; private _number: string; - public constructor(x: number, y: number, number: string, isGrace: boolean) { + public constructor(x: number, y: number, number: string, isGrace: boolean, beat: Beat) { super(x, y); this._isGrace = isGrace; this._number = number; + this._beat = beat; } public override paint(cx: number, cy: number, canvas: ICanvas): void { + using _ = this._beat.isRest + ? ElementStyleHelper.beat(canvas, BeatSubElement.NumberedRests, this._beat) + : this._beat.notes.length > 0 + ? ElementStyleHelper.note(canvas, NoteSubElement.NumberedNumber, this._beat.notes[0]) + : undefined; + const res = this.renderer.resources; canvas.font = this._isGrace ? res.numberedNotationGraceFont : res.numberedNotationFont; canvas.textBaseline = TextBaseline.Middle; @@ -24,7 +35,7 @@ export class NumberedNoteHeadGlyph extends Glyph { } public override doLayout(): void { - const scale: number = (this._isGrace ? NoteHeadGlyph.GraceScale : 1); + const scale: number = this._isGrace ? NoteHeadGlyph.GraceScale : 1; this.width = NumberedNoteHeadGlyph.NoteHeadWidth * scale; this.height = NumberedNoteHeadGlyph.NoteHeadHeight * scale; } diff --git a/src/rendering/glyphs/NumberedSlurGlyph.ts b/src/rendering/glyphs/NumberedSlurGlyph.ts index 11c98c7e3..e227dadd3 100644 --- a/src/rendering/glyphs/NumberedSlurGlyph.ts +++ b/src/rendering/glyphs/NumberedSlurGlyph.ts @@ -1,6 +1,6 @@ -import { Note } from '@src/model/Note'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Note } from '@src/model/Note'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { TabTieGlyph } from '@src/rendering/glyphs/TabTieGlyph'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; @@ -60,14 +60,14 @@ export class NumberedSlurGlyph extends TabTieGlyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let startNoteRenderer: BarRendererBase = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, + const startNoteRenderer: BarRendererBase = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff.staffId, this.startBeat!.voice.bar )!; - let direction: BeamDirection = this.getBeamDirection(this.startBeat!, startNoteRenderer); - let slurId: string = 'numbered.slur.' + this.startNote.beat.id + '.' + this.endNote.beat.id + '.' + direction; - let renderer = this.renderer; - let isSlurRendered: boolean = renderer.staff.getSharedLayoutData(slurId, false); + 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); diff --git a/src/rendering/glyphs/NumberedTieGlyph.ts b/src/rendering/glyphs/NumberedTieGlyph.ts index 34e3127f3..4a4178ff5 100644 --- a/src/rendering/glyphs/NumberedTieGlyph.ts +++ b/src/rendering/glyphs/NumberedTieGlyph.ts @@ -1,6 +1,6 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; -import { BarRendererBase, NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import { type BarRendererBase, NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; import { TieGlyph } from '@src/rendering/glyphs/TieGlyph'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; @@ -15,7 +15,11 @@ export class NumberedTieGlyph extends TieGlyph { } protected override shouldDrawBendSlur() { - return this.renderer.settings.notation.extendBendArrowsOnTiedNotes && !!this.startNote.bendOrigin && this.startNote.isTieOrigin; + return ( + this.renderer.settings.notation.extendBendArrowsOnTiedNotes && + !!this.startNote.bendOrigin && + this.startNote.isTieOrigin + ); } public override doLayout(): void { @@ -24,7 +28,6 @@ export class NumberedTieGlyph extends TieGlyph { protected override getBeamDirection(beat: Beat, noteRenderer: BarRendererBase): BeamDirection { return BeamDirection.Up; - } protected override getStartY(): number { @@ -36,14 +39,14 @@ export class NumberedTieGlyph extends TieGlyph { } protected override getStartX(): number { - if(this.startNote === this.endNote) { + if (this.startNote === this.endNote) { return this.getEndX() - 20; } return this.startNoteRenderer!.getNoteX(this.startNote, NoteXPosition.Center); } protected override getEndX(): number { - if(this.startNote === this.endNote) { + if (this.startNote === this.endNote) { return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); } return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Center); diff --git a/src/rendering/glyphs/OttavaGlyph.ts b/src/rendering/glyphs/OttavaGlyph.ts index 5201f76aa..ee51b241b 100644 --- a/src/rendering/glyphs/OttavaGlyph.ts +++ b/src/rendering/glyphs/OttavaGlyph.ts @@ -1,8 +1,9 @@ import { Ottavia } from '@src/model/Ottavia'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { GroupedEffectGlyph } from '@src/rendering/glyphs/GroupedEffectGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; export class OttavaGlyph extends GroupedEffectGlyph { private _ottava: Ottavia; @@ -16,7 +17,7 @@ export class OttavaGlyph extends GroupedEffectGlyph { public override doLayout(): void { super.doLayout(); - this.height = 13; + this.height = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.QuindicesimaAlta)!; } protected override paintNonGrouped(cx: number, cy: number, canvas: ICanvas): void { @@ -27,7 +28,7 @@ export class OttavaGlyph extends GroupedEffectGlyph { let size: number = 0; switch (this._ottava) { case Ottavia._15ma: - size = 37; + size = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.QuindicesimaAlta)! * 0.8; canvas.fillMusicFontSymbol( cx + this.x - size / 2, cy + this.y + this.height, @@ -37,7 +38,7 @@ export class OttavaGlyph extends GroupedEffectGlyph { ); break; case Ottavia._8va: - size = 26; + size = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.OttavaAlta)! * 0.8; canvas.fillMusicFontSymbol( cx + this.x - size / 2, cy + this.y + this.height, @@ -47,7 +48,7 @@ export class OttavaGlyph extends GroupedEffectGlyph { ); break; case Ottavia._8vb: - size = 23; + size = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.OttavaBassaVb)! * 0.8; canvas.fillMusicFontSymbol( cx + this.x - size / 2, cy + this.y + this.height, @@ -57,7 +58,12 @@ export class OttavaGlyph extends GroupedEffectGlyph { ); break; case Ottavia._15mb: - size = 36; + size = + (MusicFontSymbolSizes.Widths.get(MusicFontSymbol.Quindicesima)! + + MusicFontSymbolSizes.Widths.get(MusicFontSymbol.OctaveBaselineM)! + + MusicFontSymbolSizes.Widths.get(MusicFontSymbol.OctaveBaselineB)!) * + 0.8; + // NOTE: SMUFL does not have a glyph for 15mb so we build it canvas.fillMusicFontSymbols( cx + this.x - size / 2, @@ -72,12 +78,12 @@ export class OttavaGlyph extends GroupedEffectGlyph { } protected paintGrouped(cx: number, cy: number, endX: number, canvas: ICanvas): void { - let size: number = this.paintOttava(cx, cy, canvas); - let lineSpacing: number = 3; - let startX: number = cx + this.x + size + lineSpacing; + const size: number = this.paintOttava(cx, cy, canvas); + const lineSpacing: number = 3; + const startX: number = cx + this.x + size + lineSpacing; let lineY: number = cy + this.y; lineY += this._aboveStaff ? 2 : this.height - 2; - let lineSize: number = 8; + const lineSize: number = 8; if (endX > startX) { let lineX: number = startX; while (lineX < endX) { diff --git a/src/rendering/glyphs/PercussionNoteHeadGlyph.ts b/src/rendering/glyphs/PercussionNoteHeadGlyph.ts index 6cedefbd6..908341b27 100644 --- a/src/rendering/glyphs/PercussionNoteHeadGlyph.ts +++ b/src/rendering/glyphs/PercussionNoteHeadGlyph.ts @@ -1,49 +1,58 @@ -import { ICanvas, TextBaseline } from '@src/platform/ICanvas'; +import { type ICanvas, TextBaseline } from '@src/platform/ICanvas'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; -import { Duration } from '@src/model/Duration'; -import { InstrumentArticulation } from '@src/model/InstrumentArticulation'; +import type { Duration } from '@src/model/Duration'; +import type { InstrumentArticulation } from '@src/model/InstrumentArticulation'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; export class PercussionNoteHeadGlyph extends MusicFontGlyph { private _isGrace: boolean; private _articulation: InstrumentArticulation; - public constructor(x: number, y: number, articulation: InstrumentArticulation, duration: Duration, isGrace: boolean) { + public constructor( + x: number, + y: number, + articulation: InstrumentArticulation, + duration: Duration, + isGrace: boolean + ) { super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, articulation.getSymbol(duration)); this._isGrace = isGrace; this._articulation = articulation; } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let offset: number = this._isGrace ? 1 : 0; + const c = canvas.color; + if (this.colorOverride) { + canvas.color = this.colorOverride!; + } + + const offset: number = this._isGrace ? 1 : 0; canvas.fillMusicFontSymbol(cx + this.x, cy + this.y + offset, this.glyphScale, this.symbol, false); - if (this._articulation.techniqueSymbol !== MusicFontSymbol.None && this._articulation.techniqueSymbolPlacement === TextBaseline.Middle) { - canvas.fillMusicFontSymbol(cx + this.x, cy + this.y + offset, this.glyphScale, this._articulation.techniqueSymbol, false); + if ( + this._articulation.techniqueSymbol !== MusicFontSymbol.None && + this._articulation.techniqueSymbolPlacement === TextBaseline.Middle + ) { + canvas.fillMusicFontSymbol( + cx + this.x, + cy + this.y + offset, + this.glyphScale, + this._articulation.techniqueSymbol, + false + ); } + canvas.color = c; } public override doLayout(): void { - const scale: number = (this._isGrace ? NoteHeadGlyph.GraceScale : 1); - switch (this.symbol) { - case MusicFontSymbol.NoteheadWhole: - this.width = 14; - break; - case MusicFontSymbol.NoteheadCircleX: - case MusicFontSymbol.NoteheadDiamondWhite: - this.width = 9; - break; - case MusicFontSymbol.NoteheadHeavyXHat: - case MusicFontSymbol.NoteheadHeavyX: - this.width = 13; - break; - default: - this.width = 10; - break; + super.doLayout(); + if (this.width === 0) { + this.height = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.NoteheadBlack)!; + } + if (this.height === 0) { + this.height = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.NoteheadBlack)!; } - - this.width = this.width * scale; - this.height = NoteHeadGlyph.NoteHeadHeight * scale; } } diff --git a/src/rendering/glyphs/PickStrokeGlyph.ts b/src/rendering/glyphs/PickStrokeGlyph.ts index 0d61fb656..59010978f 100644 --- a/src/rendering/glyphs/PickStrokeGlyph.ts +++ b/src/rendering/glyphs/PickStrokeGlyph.ts @@ -1,20 +1,14 @@ import { PickStroke } from '@src/model/PickStroke'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; export class PickStrokeGlyph extends MusicFontGlyph { - public constructor(x: number, y: number, pickStroke: PickStroke) { super(x, y, NoteHeadGlyph.GraceScale, PickStrokeGlyph.getSymbol(pickStroke)); } - public override doLayout(): void { - this.width = 9; - this.height = 13; - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { super.paint(cx, cy + this.height, canvas); } diff --git a/src/rendering/glyphs/PictEdgeOfCymbalGlyph.ts b/src/rendering/glyphs/PictEdgeOfCymbalGlyph.ts index b44e2c5be..d2cae3571 100644 --- a/src/rendering/glyphs/PictEdgeOfCymbalGlyph.ts +++ b/src/rendering/glyphs/PictEdgeOfCymbalGlyph.ts @@ -1,4 +1,4 @@ -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; @@ -7,11 +7,6 @@ export class PictEdgeOfCymbalGlyph extends MusicFontGlyph { super(x, y, 0.5, MusicFontSymbol.PictEdgeOfCymbal); } - public override doLayout(): void { - this.width = 22; - this.height = 15; - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { super.paint(cx - 3, cy + this.height, canvas); } diff --git a/src/rendering/glyphs/RepeatCloseGlyph.ts b/src/rendering/glyphs/RepeatCloseGlyph.ts deleted file mode 100644 index 3cc0a2fa4..000000000 --- a/src/rendering/glyphs/RepeatCloseGlyph.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ICanvas } from '@src/platform/ICanvas'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; - -export class RepeatCloseGlyph extends Glyph { - public constructor(x: number, y: number) { - super(x, y); - } - - public override doLayout(): void { - this.width = 11; - } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - let blockWidth: number = 4; - let top: number = cy + this.y + this.renderer.topPadding; - let bottom: number = cy + this.y + this.renderer.height - this.renderer.bottomPadding; - let left: number = cx + this.x; - let h: number = bottom - top; - // circles - let circleSize: number = 1.5; - let middle: number = (top + bottom) / 2; - let dotOffset: number = 3; - canvas.fillCircle(left, middle - circleSize * dotOffset, circleSize); - canvas.fillCircle(left, middle + circleSize * dotOffset, circleSize); - // line - left += 4; - canvas.beginPath(); - canvas.moveTo(left, top); - canvas.lineTo(left, bottom); - canvas.stroke(); - // big bar - left += 3.5; - canvas.fillRect(left, top, blockWidth, h); - } -} diff --git a/src/rendering/glyphs/RepeatCountGlyph.ts b/src/rendering/glyphs/RepeatCountGlyph.ts index 0b8969206..b29470e33 100644 --- a/src/rendering/glyphs/RepeatCountGlyph.ts +++ b/src/rendering/glyphs/RepeatCountGlyph.ts @@ -1,6 +1,8 @@ -import { ICanvas, TextAlign } from '@src/platform/ICanvas'; +import { type ICanvas, TextAlign } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; -import { RenderingResources } from '@src/RenderingResources'; +import type { RenderingResources } from '@src/RenderingResources'; +import type { LineBarRenderer } from '@src/rendering/LineBarRenderer'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; export class RepeatCountGlyph extends Glyph { private _count: number = 0; @@ -16,12 +18,18 @@ export class RepeatCountGlyph extends Glyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let res: RenderingResources = this.renderer.resources; - let oldAlign: TextAlign = canvas.textAlign; + using _ = ElementStyleHelper.bar( + canvas, + (this.renderer as LineBarRenderer).repeatsBarSubElement, + this.renderer.bar + ); + + const res: RenderingResources = this.renderer.resources; + const oldAlign: TextAlign = canvas.textAlign; canvas.font = res.barNumberFont; canvas.textAlign = TextAlign.Right; - let s: string = 'x' + this._count; - let w: number = canvas.measureText(s).width / 1.5; + const s: string = `x${this._count}`; + const w: number = canvas.measureText(s).width / 1.5; canvas.fillText(s, cx + this.x - w, cy + this.y); canvas.textAlign = oldAlign; } diff --git a/src/rendering/glyphs/RepeatOpenGlyph.ts b/src/rendering/glyphs/RepeatOpenGlyph.ts deleted file mode 100644 index 9b38a7184..000000000 --- a/src/rendering/glyphs/RepeatOpenGlyph.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ICanvas } from '@src/platform/ICanvas'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; - -export class RepeatOpenGlyph extends Glyph { - private _dotOffset: number = 0; - private _circleSize: number = 0; - - public constructor(x: number, y: number, circleSize: number, dotOffset: number) { - super(x, y); - this._dotOffset = 0.0; - this._circleSize = 0.0; - this._dotOffset = dotOffset; - this._circleSize = circleSize; - } - - public override doLayout(): void { - this.width = 13; - } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - let blockWidth: number = 4; - let top: number = cy + this.y + this.renderer.topPadding; - let bottom: number = cy + this.y + this.renderer.height - this.renderer.bottomPadding; - let left: number = cx + this.x + 0.5; - // big bar - let h: number = bottom - top; - canvas.fillRect(left, top, blockWidth, h); - // line - left += blockWidth * 2 - 0.5; - canvas.beginPath(); - canvas.moveTo(left, top); - canvas.lineTo(left, bottom); - canvas.stroke(); - // circles - left += 3; - let circleSize: number = this._circleSize; - let middle: number = (top + bottom) / 2; - canvas.fillCircle(left, middle - circleSize * this._dotOffset, circleSize); - canvas.fillCircle(left, middle + circleSize * this._dotOffset, circleSize); - } -} diff --git a/src/rendering/glyphs/RowContainerGlyph.ts b/src/rendering/glyphs/RowContainerGlyph.ts index fc461bd54..20969f2fd 100644 --- a/src/rendering/glyphs/RowContainerGlyph.ts +++ b/src/rendering/glyphs/RowContainerGlyph.ts @@ -1,4 +1,4 @@ -import { ICanvas, TextAlign } from '@src/platform/ICanvas'; +import { type ICanvas, TextAlign } from '@src/platform/ICanvas'; import { RowGlyphContainer } from '@src/rendering/glyphs/RowGlyphContainer'; import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; @@ -17,11 +17,11 @@ export class RowContainerGlyph extends GlyphGroup { public override doLayout(): void { let x: number = 0; let y: number = 0; - let padding: number = RowContainerGlyph.Padding; + const padding: number = RowContainerGlyph.Padding; this._rows = []; let row: RowGlyphContainer = new RowGlyphContainer(x, y, this._align); row.width = this.width; - for (let g of this.glyphs!) { + for (const g of this.glyphs!) { if (x + g.width < this.width) { row.addGlyphToRow(g); x += g.width; @@ -47,7 +47,7 @@ export class RowContainerGlyph extends GlyphGroup { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - for (let row of this._rows) { + for (const row of this._rows) { row.paint(cx + this.x, cy + this.y, canvas); } } diff --git a/src/rendering/glyphs/RowGlyphContainer.ts b/src/rendering/glyphs/RowGlyphContainer.ts index 3b392dcc4..62182a6c0 100644 --- a/src/rendering/glyphs/RowGlyphContainer.ts +++ b/src/rendering/glyphs/RowGlyphContainer.ts @@ -1,6 +1,6 @@ import { TextAlign } from '@src/platform/ICanvas'; import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; export class RowGlyphContainer extends GlyphGroup { private _glyphWidth: number = 0; @@ -25,7 +25,7 @@ export class RowGlyphContainer extends GlyphGroup { x = this.width - this._glyphWidth; break; } - for (let glyph of this.glyphs!) { + for (const glyph of this.glyphs!) { glyph.x = x; x += glyph.width; } diff --git a/src/rendering/glyphs/ScoreBeatGlyph.ts b/src/rendering/glyphs/ScoreBeatGlyph.ts index 23dc4c972..fca740963 100644 --- a/src/rendering/glyphs/ScoreBeatGlyph.ts +++ b/src/rendering/glyphs/ScoreBeatGlyph.ts @@ -2,13 +2,12 @@ import { AccentuationType } from '@src/model/AccentuationType'; import { Duration } from '@src/model/Duration'; import { GraceType } from '@src/model/GraceType'; import { HarmonicType } from '@src/model/HarmonicType'; -import { Note } from '@src/model/Note'; +import { type Note, NoteSubElement } from '@src/model/Note'; import { AccentuationGlyph } from '@src/rendering/glyphs/AccentuationGlyph'; import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; import { CircleGlyph } from '@src/rendering/glyphs/CircleGlyph'; import { DeadNoteHeadGlyph } from '@src/rendering/glyphs/DeadNoteHeadGlyph'; import { DiamondNoteHeadGlyph } from '@src/rendering/glyphs/DiamondNoteHeadGlyph'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { GhostNoteContainerGlyph } from '@src/rendering/glyphs/GhostNoteContainerGlyph'; import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; @@ -16,22 +15,25 @@ import { ScoreNoteChordGlyph } from '@src/rendering/glyphs/ScoreNoteChordGlyph'; import { ScoreRestGlyph } from '@src/rendering/glyphs/ScoreRestGlyph'; import { ScoreWhammyBarGlyph } from '@src/rendering/glyphs/ScoreWhammyBarGlyph'; import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; -import { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; import { PercussionMapper } from '@src/model/PercussionMapper'; import { PercussionNoteHeadGlyph } from '@src/rendering/glyphs/PercussionNoteHeadGlyph'; import { Logger } from '@src/Logger'; import { ArticStaccatoAboveGlyph } from '@src/rendering/glyphs/ArticStaccatoAboveGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { ICanvas, TextBaseline } from '@src/platform/ICanvas'; +import { type ICanvas, TextBaseline } from '@src/platform/ICanvas'; import { PictEdgeOfCymbalGlyph } from '@src/rendering/glyphs/PictEdgeOfCymbalGlyph'; import { PickStrokeGlyph } from '@src/rendering/glyphs/PickStrokeGlyph'; import { PickStroke } from '@src/model/PickStroke'; import { GuitarGolpeGlyph } from '@src/rendering/glyphs/GuitarGolpeGlyph'; import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; -import { StringNumberContainerGlyph } from './StringNumberContainerGlyph'; -import { SlashNoteHeadGlyph } from './SlashNoteHeadGlyph'; +import { StringNumberContainerGlyph } from '@src/rendering/glyphs/StringNumberContainerGlyph'; +import { SlashNoteHeadGlyph } from '@src/rendering/glyphs/SlashNoteHeadGlyph'; +import { BeatSubElement } from '@src/model/Beat'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import type { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { private _collisionOffset: number = -1000; @@ -41,6 +43,10 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { public noteHeads: ScoreNoteChordGlyph | null = null; public restGlyph: ScoreRestGlyph | null = null; + protected override get effectElement() { + return BeatSubElement.StandardNotationEffects; + } + public override getNoteX(note: Note, requestedPosition: NoteXPosition): number { return this.noteHeads ? this.noteHeads.getNoteX(note, requestedPosition) : 0; } @@ -90,18 +96,26 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { public override doLayout(): void { // create glyphs - let sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; if (!this.container.beat.isEmpty) { if (!this.container.beat.isRest) { if (this.container.beat.slashed) { const isGrace = this.container.beat.graceType !== GraceType.None; const line = (sr.heightLineCount - 1) / 2; const slashY = sr.getLineY(line); - const slashNoteHead = new SlashNoteHeadGlyph(0, slashY, this.container.beat.duration, isGrace); + const slashNoteHead = new SlashNoteHeadGlyph( + 0, + slashY, + this.container.beat.duration, + isGrace, + this.container.beat + ); + slashNoteHead.noteHeadElement = NoteSubElement.StandardNotationNoteHead; + slashNoteHead.effectElement = BeatSubElement.StandardNotationEffects; this.slash = slashNoteHead; slashNoteHead.beat = this.container.beat; slashNoteHead.beamingHelper = this.beamingHelper; - this.addGlyph(slashNoteHead); + this.addNormal(slashNoteHead); } else { // // Note heads @@ -110,31 +124,31 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { this.noteHeads = noteHeads; noteHeads.beat = this.container.beat; noteHeads.beamingHelper = this.beamingHelper; - let ghost: GhostNoteContainerGlyph = new GhostNoteContainerGlyph(false); + const ghost: GhostNoteContainerGlyph = new GhostNoteContainerGlyph(false); ghost.renderer = this.renderer; - for (let note of this.container.beat.notes) { + for (const note of this.container.beat.notes) { if (note.isVisible) { this.createNoteGlyph(note); ghost.addParenthesis(note); } } - this.addGlyph(noteHeads); + this.addNormal(noteHeads); if (!ghost.isEmpty) { - this.addGlyph( + this.addNormal( new SpacingGlyph( 0, 0, 4 * (this.container.beat.graceType !== GraceType.None ? NoteHeadGlyph.GraceScale : 1) ) ); - this.addGlyph(ghost); + this.addEffect(ghost); } } // // Whammy Bar if (this.container.beat.hasWhammyBar) { - let whammy: ScoreWhammyBarGlyph = new ScoreWhammyBarGlyph(this.container.beat); + const whammy: ScoreWhammyBarGlyph = new ScoreWhammyBarGlyph(this.container.beat); whammy.renderer = this.renderer; whammy.doLayout(); this.container.ties.push(whammy); @@ -143,14 +157,19 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { // Note dots // if (this.container.beat.dots > 0) { - this.addGlyph(new SpacingGlyph(0, 0, 5)); + this.addNormal(new SpacingGlyph(0, 0, 5)); for (let i: number = 0; i < this.container.beat.dots; i++) { - let group: GlyphGroup = new GlyphGroup(0, 0); + const group: GlyphGroup = new GlyphGroup(0, 0); group.renderer = this.renderer; - for (let note of this.container.beat.notes) { - this.createBeatDot(sr.getNoteLine(note), group); + 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.addGlyph(group); + this.addEffect(group); } } } else { @@ -171,13 +190,13 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { this.restGlyph = restGlyph; restGlyph.beat = this.container.beat; restGlyph.beamingHelper = this.beamingHelper; - this.addGlyph(restGlyph); + this.addNormal(restGlyph); if (this.renderer.bar.isMultiVoice) { if (this.container.beat.voice.index === 0) { const restSizes = BeamingHelper.computeLineHeightsForRest(this.container.beat.duration); - let restTop = restGlyph.y - sr.getScoreHeight(restSizes[0]); - let restBottom = restGlyph.y + sr.getScoreHeight(restSizes[1]); + 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); @@ -192,12 +211,12 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { // Note dots // if (this.container.beat.dots > 0) { - this.addGlyph(new SpacingGlyph(0, 0, 5)); + this.addNormal(new SpacingGlyph(0, 0, 5)); for (let i: number = 0; i < this.container.beat.dots; i++) { - let group: GlyphGroup = new GlyphGroup(0, 0); + const group: GlyphGroup = new GlyphGroup(0, 0); group.renderer = this.renderer; this.createBeatDot(line, group); - this.addGlyph(group); + this.addEffect(group); } } } @@ -214,23 +233,36 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { } } - private createBeatDot(line: number, group: GlyphGroup): void { - let sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; - group.addGlyph(new CircleGlyph(0, sr.getScoreY(line), 1.5)); + private createBeatDot(line: number, group: GlyphGroup) { + const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const g = new CircleGlyph(0, sr.getScoreY(line), 1.5); + group.addGlyph(g); + return g; } - private createNoteHeadGlyph(n: Note): EffectGlyph { - let isGrace: boolean = this.container.beat.graceType !== GraceType.None; + private createNoteHeadGlyph(n: Note): MusicFontGlyph { + const isGrace: boolean = this.container.beat.graceType !== GraceType.None; + + const style = n.style; + if (style?.noteHead !== undefined) { + const noteHead = new NoteHeadGlyph(0, 0, n.beat.duration, isGrace); + // NOTE: sizes are not yet perfect + // will be done in https://github.com/CoderLine/alphaTab/issues/1949 + noteHead.symbol = style!.noteHead!; + if (style.noteHeadCenterOnStem) { + noteHead.centerOnStem = true; + } + return noteHead; + } + + // TODO: here we should unify it to one common glyph which knows all sizes. + if (n.beat.voice.bar.staff.isPercussion) { const articulation = PercussionMapper.getArticulation(n); if (articulation) { return new PercussionNoteHeadGlyph(0, 0, articulation, n.beat.duration, isGrace); - } else { - Logger.warning( - 'Rendering', - `No articulation found for percussion instrument ${n.percussionArticulation}` - ); } + Logger.warning('Rendering', `No articulation found for percussion instrument ${n.percussionArticulation}`); } if (n.isDead) { return new DeadNoteHeadGlyph(0, 0, isGrace); @@ -241,6 +273,7 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { if (n.harmonicType === HarmonicType.Natural) { return new DiamondNoteHeadGlyph(0, 0, n.beat.duration, isGrace); } + return new NoteHeadGlyph(0, 0, n.beat.duration, isGrace); } @@ -248,24 +281,31 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { if (n.beat.graceType === GraceType.BendGrace && !n.hasBend) { return; } - let sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; - let noteHeadGlyph: EffectGlyph = this.createNoteHeadGlyph(n); + const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const noteHeadGlyph = this.createNoteHeadGlyph(n); + noteHeadGlyph.colorOverride = ElementStyleHelper.noteColor( + sr.resources, + NoteSubElement.StandardNotationNoteHead, + n + ); + // calculate y position let line: number = sr.getNoteLine(n); noteHeadGlyph.y = sr.getScoreY(line); this.noteHeads!.addNoteGlyph(noteHeadGlyph, n, line); if (n.harmonicType !== HarmonicType.None && n.harmonicType !== HarmonicType.Natural) { // create harmonic note head. - let harmonicFret: number = n.displayValue + n.harmonicPitch; - noteHeadGlyph = new DiamondNoteHeadGlyph( + const harmonicFret: number = n.displayValue + n.harmonicPitch; + const harmonicsGlyph = new DiamondNoteHeadGlyph( 0, 0, n.beat.duration, this.container.beat.graceType !== GraceType.None ); + harmonicsGlyph.colorOverride = noteHeadGlyph.colorOverride; line = sr.accidentalHelper.getNoteLineForValue(harmonicFret, false); - noteHeadGlyph.y = sr.getScoreY(line); - this.noteHeads!.addNoteGlyph(noteHeadGlyph, n, line); + harmonicsGlyph.y = sr.getScoreY(line); + this.noteHeads!.addNoteGlyph(harmonicsGlyph, n, line); } const belowBeatEffects = this.noteHeads!.belowBeatEffects; diff --git a/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts b/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts index 8aa26a849..e5502df31 100644 --- a/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts +++ b/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts @@ -3,7 +3,7 @@ import { BendType } from '@src/model/BendType'; import { BrushType } from '@src/model/BrushType'; import { GraceType } from '@src/model/GraceType'; import { HarmonicType } from '@src/model/HarmonicType'; -import { Note } from '@src/model/Note'; +import { type Note, NoteSubElement } from '@src/model/Note'; import { WhammyType } from '@src/model/WhammyType'; import { AccidentalGlyph } from '@src/rendering/glyphs/AccidentalGlyph'; import { AccidentalGroupGlyph } from '@src/rendering/glyphs/AccidentalGroupGlyph'; @@ -12,9 +12,11 @@ import { BendNoteHeadGroupGlyph } from '@src/rendering/glyphs/BendNoteHeadGroupG import { GhostNoteContainerGlyph } from '@src/rendering/glyphs/GhostNoteContainerGlyph'; import { ScoreBrushGlyph } from '@src/rendering/glyphs/ScoreBrushGlyph'; import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; -import { FingeringGroupGlyph } from './FingeringGroupGlyph'; +import { FingeringGroupGlyph } from '@src/rendering/glyphs/FingeringGroupGlyph'; +import { BeatSubElement } from '@src/model/Beat'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { private _prebends: BendNoteHeadGroupGlyph | null = null; @@ -22,30 +24,43 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { return this._prebends ? this._prebends.x + this._prebends.noteHeadOffset : 0; } + protected override get effectElement() { + return BeatSubElement.StandardNotationEffects; + } + public accidentals: AccidentalGroupGlyph | null = null; public override doLayout(): void { if (!this.container.beat.isRest) { - let accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); + const accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); accidentals.renderer = this.renderer; - let fingering: FingeringGroupGlyph = new FingeringGroupGlyph(); + const fingering: FingeringGroupGlyph = new FingeringGroupGlyph(); fingering.renderer = this.renderer; - let ghost: GhostNoteContainerGlyph = new GhostNoteContainerGlyph(true); + 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; - for (let note of this.container.beat.notes) { + 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); + preBends.addGlyph( + note.displayValue - ((note.bendPoints![0].value / 2) | 0), + false, + color + ); break; } } else if (note.beat.hasWhammyBar) { @@ -54,7 +69,8 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { case WhammyType.Predive: this._prebends.addGlyph( note.displayValue - ((note.beat.whammyBarPoints![0].value / 2) | 0), - false + false, + color ); break; } @@ -65,8 +81,8 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { } } if (!preBends.isEmpty) { - this.addGlyph(preBends); - this.addGlyph( + this.addEffect(preBends); + this.addNormal( new SpacingGlyph( 0, 0, @@ -75,12 +91,12 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { ); } if (this.container.beat.brushType !== BrushType.None) { - this.addGlyph(new ScoreBrushGlyph(this.container.beat)); - this.addGlyph(new SpacingGlyph(0, 0, 4)); + this.addEffect(new ScoreBrushGlyph(this.container.beat)); + this.addNormal(new SpacingGlyph(0, 0, 4)); } if (!fingering.isEmpty) { if (!this.isEmpty) { - this.addGlyph( + this.addNormal( new SpacingGlyph( 0, 0, @@ -89,8 +105,8 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { ); } - this.addGlyph(fingering); - this.addGlyph( + this.addEffect(fingering); + this.addNormal( new SpacingGlyph( 0, 0, @@ -100,8 +116,8 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { } if (!ghost.isEmpty) { - this.addGlyph(ghost); - this.addGlyph( + this.addEffect(ghost); + this.addNormal( new SpacingGlyph( 0, 0, @@ -112,7 +128,7 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { if (!accidentals.isEmpty) { this.accidentals = accidentals; if (!this.isEmpty) { - this.addGlyph( + this.addNormal( new SpacingGlyph( 0, 0, @@ -120,9 +136,9 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { ) ); } - - this.addGlyph(accidentals); - this.addGlyph( + + this.addNormal(accidentals); + this.addNormal( new SpacingGlyph( 0, 0, @@ -135,27 +151,26 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { } private createAccidentalGlyph(n: Note, accidentals: AccidentalGroupGlyph): void { - let sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const sr: ScoreBarRenderer = this.renderer as ScoreBarRenderer; let accidental: AccidentalType = sr.accidentalHelper.applyAccidental(n); let noteLine: number = sr.getNoteLine(n); - let isGrace: boolean = this.container.beat.graceType !== GraceType.None; + const isGrace: boolean = this.container.beat.graceType !== GraceType.None; + const color = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.StandardNotationAccidentals, n); const graceScale = isGrace ? NoteHeadGlyph.GraceScale : 1; if (accidental !== AccidentalType.None) { - let g = new AccidentalGlyph(0, sr.getScoreY(noteLine), accidental, graceScale); + const g = new AccidentalGlyph(0, sr.getScoreY(noteLine), accidental, graceScale); + g.colorOverride = color; g.renderer = this.renderer; accidentals.addGlyph(g); } if (n.harmonicType !== HarmonicType.None && n.harmonicType !== HarmonicType.Natural) { - let harmonicFret: number = n.displayValue + n.harmonicPitch; + const harmonicFret: number = n.displayValue + n.harmonicPitch; accidental = sr.accidentalHelper.applyAccidentalForValue(n.beat, harmonicFret, isGrace, false); noteLine = sr.accidentalHelper.getNoteLineForValue(harmonicFret, false); - let g = new AccidentalGlyph(0, sr.getScoreY(noteLine), accidental, graceScale); + const g = new AccidentalGlyph(0, sr.getScoreY(noteLine), accidental, graceScale); + g.colorOverride = color; g.renderer = this.renderer; accidentals.addGlyph(g); } } - - public constructor() { - super(); - } } diff --git a/src/rendering/glyphs/ScoreBendGlyph.ts b/src/rendering/glyphs/ScoreBendGlyph.ts index 257a69f39..d6ae3a112 100644 --- a/src/rendering/glyphs/ScoreBendGlyph.ts +++ b/src/rendering/glyphs/ScoreBendGlyph.ts @@ -1,19 +1,22 @@ -import { Beat } from '@src/model/Beat'; -import { BendPoint } from '@src/model/BendPoint'; +import type { Beat } from '@src/model/Beat'; +import type { BendPoint } from '@src/model/BendPoint'; import { BendStyle } from '@src/model/BendStyle'; import { BendType } from '@src/model/BendType'; import { GraceType } from '@src/model/GraceType'; -import { Note } from '@src/model/Note'; -import { ICanvas } from '@src/platform/ICanvas'; +import { type Note, NoteSubElement } from '@src/model/Note'; +import type { ICanvas } from '@src/platform/ICanvas'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { BendNoteHeadGroupGlyph } from '@src/rendering/glyphs/BendNoteHeadGroupGlyph'; -import { ScoreBeatPreNotesGlyph } from '@src/rendering/glyphs/ScoreBeatPreNotesGlyph'; +import type { ScoreBeatPreNotesGlyph } from '@src/rendering/glyphs/ScoreBeatPreNotesGlyph'; import { ScoreHelperNotesBaseGlyph } from '@src/rendering/glyphs/ScoreHelperNotesBaseGlyph'; import { TieGlyph } from '@src/rendering/glyphs/TieGlyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; import { NoteYPosition } from '@src/rendering/BarRendererBase'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { private _beat: Beat; @@ -22,7 +25,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { private _middleNoteGlyph: BendNoteHeadGroupGlyph | null = null; public constructor(beat: Beat) { - super(0,0); + super(0, 0); this._beat = beat; } @@ -31,6 +34,13 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { if (note.isTieOrigin) { return; } + + const color = ElementStyleHelper.noteColor( + this.renderer.resources, + NoteSubElement.StandardNotationEffects, + note + ); + switch (note.bendType) { case BendType.Bend: case BendType.PrebendRelease: @@ -43,8 +53,12 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { this._endNoteGlyph = endGlyphs; this.BendNoteHeads.push(endGlyphs); } - let lastBendPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; - endGlyphs.addGlyph(this.getBendNoteValue(note, lastBendPoint), lastBendPoint.value % 2 !== 0); + const lastBendPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; + endGlyphs.addGlyph( + this.getBendNoteValue(note, lastBendPoint), + lastBendPoint.value % 2 !== 0, + color + ); } break; case BendType.Release: @@ -54,11 +68,15 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { if (!endGlyphs) { endGlyphs = new BendNoteHeadGroupGlyph(note.beat, false); endGlyphs.renderer = this.renderer; - this._endNoteGlyph = endGlyphs; + this._endNoteGlyph = endGlyphs; this.BendNoteHeads.push(endGlyphs); } - let lastBendPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; - endGlyphs.addGlyph(this.getBendNoteValue(note, lastBendPoint), lastBendPoint.value % 2 !== 0); + const lastBendPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; + endGlyphs.addGlyph( + this.getBendNoteValue(note, lastBendPoint), + lastBendPoint.value % 2 !== 0, + color + ); } } break; @@ -71,10 +89,11 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { middleGlyphs.renderer = this.renderer; this.BendNoteHeads.push(middleGlyphs); } - let middleBendPoint: BendPoint = note.bendPoints![1]; + const middleBendPoint: BendPoint = note.bendPoints![1]; middleGlyphs.addGlyph( this.getBendNoteValue(note, note.bendPoints![1]), - middleBendPoint.value % 2 !== 0 + middleBendPoint.value % 2 !== 0, + color ); let endGlyphs = this._endNoteGlyph; if (!endGlyphs) { @@ -83,8 +102,12 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { this._endNoteGlyph = endGlyphs; this.BendNoteHeads.push(endGlyphs); } - let lastBendPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; - endGlyphs.addGlyph(this.getBendNoteValue(note, lastBendPoint), lastBendPoint.value % 2 !== 0); + const lastBendPoint: BendPoint = note.bendPoints![note.bendPoints!.length - 1]; + endGlyphs.addGlyph( + this.getBendNoteValue(note, lastBendPoint), + lastBendPoint.value % 2 !== 0, + color + ); } break; } @@ -92,11 +115,11 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { public override paint(cx: number, cy: number, canvas: ICanvas): void { // Draw note heads - let startNoteRenderer: ScoreBarRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, + const startNoteRenderer: ScoreBarRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff.staffId, this._beat.voice.bar )! as ScoreBarRenderer; - let startX: number = + const startX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.MiddleNotes); let endBeatX: number = cx + startNoteRenderer.x; if (this._beat.isLastOfVoice) { @@ -105,7 +128,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { endBeatX += startNoteRenderer.getBeatX(this._beat.nextBeat!, BeatXPosition.PreNotes); } endBeatX -= 8; - let middleX: number = (startX + endBeatX) / 2; + const middleX: number = (startX + endBeatX) / 2; if (this._middleNoteGlyph) { this._middleNoteGlyph.x = middleX - this._middleNoteGlyph.noteHeadOffset; this._middleNoteGlyph.y = cy + startNoteRenderer.y; @@ -119,35 +142,39 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { this._notes.sort((a, b) => { return b.displayValue - a.displayValue; }); - let directionBeat: Beat = this._beat.graceType === GraceType.BendGrace ? this._beat.nextBeat! : this._beat; + 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 = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.NoteheadBlack)!; + // draw slurs for (let i: number = 0; i < this._notes.length; i++) { - let note: Note = this._notes[i]; + const note: Note = this._notes[i]; + using _ = ElementStyleHelper.note(canvas, NoteSubElement.StandardNotationEffects, note); if (i > 0 && i >= ((this._notes.length / 2) | 0)) { direction = BeamDirection.Down; } let startY: number = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(note, NoteYPosition.Top); - let heightOffset: number = NoteHeadGlyph.NoteHeadHeight * NoteHeadGlyph.GraceScale * 0.5; + let heightOffset: number = noteHeadHeight * NoteHeadGlyph.GraceScale * 0.5; if (direction === BeamDirection.Down) { - startY += NoteHeadGlyph.NoteHeadHeight; + startY += noteHeadHeight; } - let slurText: string = note.bendStyle === BendStyle.Gradual ? 'grad.' : ''; + const slurText: string = note.bendStyle === BendStyle.Gradual ? 'grad.' : ''; if (note.isTieOrigin) { - let endNote: Note = note.tieDestination!; - let endNoteRenderer: ScoreBarRenderer | null = !endNote + const endNote: Note = note.tieDestination!; + const endNoteRenderer: ScoreBarRenderer | null = !endNote ? null - : this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, + : (this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff.staffId, endNote.beat.voice.bar - ) as ScoreBarRenderer; + ) as ScoreBarRenderer); // if we have a line break we draw only a line until the end if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { - let endX: number = cx + startNoteRenderer.x + startNoteRenderer.width; - let noteValueToDraw: number = note.tieDestination!.realValue; + const endX: number = cx + startNoteRenderer.x + startNoteRenderer.width; + const noteValueToDraw: number = note.tieDestination!.realValue; startNoteRenderer.accidentalHelper.applyAccidentalForValue(note.beat, noteValueToDraw, false, true); - let endY: number = + const endY: number = cy + startNoteRenderer.y + startNoteRenderer.getScoreY( @@ -178,11 +205,11 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { ); } } else { - let endX: number = + const endX: number = cx + endNoteRenderer.x + endNoteRenderer.getBeatX(endNote.beat, BeatXPosition.MiddleNotes); let endY: number = cy + endNoteRenderer.y + endNoteRenderer.getNoteY(endNote, NoteYPosition.Top); if (direction === BeamDirection.Down) { - endY += NoteHeadGlyph.NoteHeadHeight; + endY += noteHeadHeight; } if (note.bendType === BendType.Hold || note.bendType === BendType.Prebend) { TieGlyph.paintTie( @@ -217,7 +244,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { cx + startNoteRenderer.x + startNoteRenderer.getBeatX(note.beat, BeatXPosition.PreNotes); preX += (startNoteRenderer.getPreNotesGlyphForBeat(note.beat) as ScoreBeatPreNotesGlyph) .prebendNoteHeadOffset; - let preY: number = + const preY: number = cy + startNoteRenderer.y + startNoteRenderer.getScoreY( @@ -227,15 +254,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { ) ) + heightOffset; - this.drawBendSlur( - canvas, - preX, - preY, - startX, - startY, - direction === BeamDirection.Down, - 1 - ); + this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down, 1); break; } } else { @@ -260,8 +279,8 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { ); break; case BendType.BendRelease: - let middleValue: number = this.getBendNoteValue(note, note.bendPoints![1]); - let middleY: number = this._middleNoteGlyph!.getNoteValueY(middleValue) + heightOffset; + const middleValue: number = this.getBendNoteValue(note, note.bendPoints![1]); + const middleY: number = this._middleNoteGlyph!.getNoteValueY(middleValue) + heightOffset; this.drawBendSlur( canvas, startX, @@ -308,7 +327,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { cx + startNoteRenderer.x + startNoteRenderer.getBeatX(note.beat, BeatXPosition.PreNotes); preX += (startNoteRenderer.getPreNotesGlyphForBeat(note.beat) as ScoreBeatPreNotesGlyph) .prebendNoteHeadOffset; - let preY: number = + const preY: number = cy + startNoteRenderer.y + startNoteRenderer.getScoreY( @@ -318,15 +337,7 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph { ) ) + heightOffset; - this.drawBendSlur( - canvas, - preX, - preY, - startX, - startY, - direction === BeamDirection.Down, - 1 - ); + this.drawBendSlur(canvas, preX, preY, startX, startY, direction === BeamDirection.Down, 1); if (this.BendNoteHeads.length > 0) { endValue = this.getBendNoteValue(note, note.bendPoints![note.bendPoints!.length - 1]); endY = this.BendNoteHeads[0].getNoteValueY(endValue) + heightOffset; diff --git a/src/rendering/glyphs/ScoreBrushGlyph.ts b/src/rendering/glyphs/ScoreBrushGlyph.ts index ee20659a5..7bd37904e 100644 --- a/src/rendering/glyphs/ScoreBrushGlyph.ts +++ b/src/rendering/glyphs/ScoreBrushGlyph.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { BrushType } from '@src/model/BrushType'; import { VibratoType } from '@src/model/VibratoType'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { NoteVibratoGlyph } from '@src/rendering/glyphs/NoteVibratoGlyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { NoteYPosition } from '@src/rendering/BarRendererBase'; export class ScoreBrushGlyph extends Glyph { @@ -17,31 +17,29 @@ export class ScoreBrushGlyph extends Glyph { public override doLayout(): void { this.width = - this._beat.brushType === BrushType.ArpeggioUp || this._beat.brushType === BrushType.ArpeggioDown - ? 10 - : 0; + this._beat.brushType === BrushType.ArpeggioUp || this._beat.brushType === BrushType.ArpeggioDown ? 10 : 0; } public override paint(cx: number, cy: number, canvas: ICanvas): void { if (this._beat.brushType === BrushType.ArpeggioUp || this._beat.brushType === BrushType.ArpeggioDown) { - let scoreBarRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; - let lineSize: number = scoreBarRenderer.lineOffset; - let startY: number = + const scoreBarRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const lineSize: number = scoreBarRenderer.lineOffset; + const startY: number = cy + this.y + (scoreBarRenderer.getNoteY(this._beat.maxNote!, NoteYPosition.Bottom) - lineSize); - let endY: number = + const endY: number = cy + this.y + scoreBarRenderer.getNoteY(this._beat.minNote!, NoteYPosition.Top) + lineSize; - let arrowX: number = cx + this.x + this.width / 2; - let arrowSize: number = 8; + const arrowX: number = cx + this.x + this.width / 2; + const arrowSize: number = 8; - let glyph: NoteVibratoGlyph = new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2, true); + const glyph: NoteVibratoGlyph = new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2, true); glyph.renderer = this.renderer; glyph.doLayout(); - let waveOffset = -glyph.height / 2; + const waveOffset = -glyph.height / 2; if (this._beat.brushType === BrushType.ArpeggioUp) { - let lineStartY: number = startY + arrowSize; - let lineEndY: number = endY - arrowSize; + const lineStartY: number = startY + arrowSize; + const lineEndY: number = endY - arrowSize; glyph.width = Math.abs(lineEndY - lineStartY); canvas.beginRotate(cx + this.x + 5, lineEndY, -90); @@ -55,8 +53,8 @@ export class ScoreBrushGlyph extends Glyph { canvas.closePath(); canvas.fill(); } else if (this._beat.brushType === BrushType.ArpeggioDown) { - let lineStartY: number = startY + arrowSize; - let lineEndY: number = endY; + const lineStartY: number = startY + arrowSize; + const lineEndY: number = endY; glyph.width = Math.abs(lineEndY - lineStartY); canvas.beginRotate(cx + this.x + 5, lineStartY, 90); diff --git a/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts b/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts index 0f1f6da5f..22b5edce0 100644 --- a/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts +++ b/src/rendering/glyphs/ScoreHelperNotesBaseGlyph.ts @@ -1,8 +1,8 @@ -import { Beat } from '@src/model/Beat'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BendNoteHeadGroupGlyph } from '@src/rendering/glyphs/BendNoteHeadGroupGlyph'; +import type { Beat } from '@src/model/Beat'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { BendNoteHeadGroupGlyph } from '@src/rendering/glyphs/BendNoteHeadGroupGlyph'; import { Glyph } from '@src/rendering/glyphs/Glyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { TieGlyph } from '@src/rendering/glyphs/TieGlyph'; @@ -26,7 +26,7 @@ export class ScoreHelperNotesBaseGlyph extends Glyph { public override doLayout(): void { super.doLayout(); this.width = 0; - for (let noteHeads of this.BendNoteHeads) { + for (const noteHeads of this.BendNoteHeads) { noteHeads.doLayout(); this.width += noteHeads.width + 10; } diff --git a/src/rendering/glyphs/ScoreLegatoGlyph.ts b/src/rendering/glyphs/ScoreLegatoGlyph.ts index 4d46b79bb..afc523023 100644 --- a/src/rendering/glyphs/ScoreLegatoGlyph.ts +++ b/src/rendering/glyphs/ScoreLegatoGlyph.ts @@ -1,8 +1,8 @@ -import { Beat } from '@src/model/Beat'; -import { BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import { type BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { TieGlyph } from '@src/rendering/glyphs/TieGlyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { Duration } from '@src/model/Duration'; import { GraceType } from '@src/model/GraceType'; @@ -67,15 +67,14 @@ export class ScoreLegatoGlyph extends TieGlyph { // stem lower end return endNoteScoreRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.BottomWithStem); } - } else { - switch (this.tieDirection) { - case BeamDirection.Up: - // stem upper end - return endNoteScoreRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.BottomWithStem); - default: - // stem lower end - return endNoteScoreRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.TopWithStem); - } + } + switch (this.tieDirection) { + case BeamDirection.Up: + // stem upper end + return endNoteScoreRenderer.getNoteY(this.endBeat!.maxNote!, NoteYPosition.BottomWithStem); + default: + // stem lower end + return endNoteScoreRenderer.getNoteY(this.endBeat!.minNote!, NoteYPosition.TopWithStem); } } @@ -97,8 +96,9 @@ export class ScoreLegatoGlyph extends TieGlyph { 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 + this.endBeat!.duration > Duration.Whole && endBeamDirection === this.tieDirection + ? BeatXPosition.Stem + : BeatXPosition.MiddleNotes ); } } diff --git a/src/rendering/glyphs/ScoreNoteChordGlyph.ts b/src/rendering/glyphs/ScoreNoteChordGlyph.ts index 7c56a0154..fdb230653 100644 --- a/src/rendering/glyphs/ScoreNoteChordGlyph.ts +++ b/src/rendering/glyphs/ScoreNoteChordGlyph.ts @@ -1,20 +1,21 @@ -import { Beat } from '@src/model/Beat'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; import { Duration } from '@src/model/Duration'; -import { Note } from '@src/model/Note'; -import { ICanvas } from '@src/platform/ICanvas'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { Note } from '@src/model/Note'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; import { ScoreNoteChordGlyphBase } from '@src/rendering/glyphs/ScoreNoteChordGlyphBase'; -import { ScoreNoteGlyphInfo } from '@src/rendering/glyphs/ScoreNoteGlyphInfo'; +import type { ScoreNoteGlyphInfo } from '@src/rendering/glyphs/ScoreNoteGlyphInfo'; import { TremoloPickingGlyph } from '@src/rendering/glyphs/TremoloPickingGlyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; -import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; import { Bounds } from '@src/rendering/utils/Bounds'; import { NoteBounds } from '@src/rendering/utils/NoteBounds'; import { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; -import { DeadSlappedBeatGlyph } from './DeadSlappedBeatGlyph'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import { DeadSlappedBeatGlyph } from '@src/rendering/glyphs/DeadSlappedBeatGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { private _noteGlyphLookup: Map = new Map(); @@ -27,17 +28,13 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { public beat!: Beat; public beamingHelper!: BeamingHelper; - public constructor() { - super(); - } - public get direction(): BeamDirection { return this.beamingHelper.direction; } public getNoteX(note: Note, requestedPosition: NoteXPosition): number { if (this._noteGlyphLookup.has(note.id)) { - let n = this._noteGlyphLookup.get(note.id)!; + const n = this._noteGlyphLookup.get(note.id)!; let pos = this.x + n.x + this._noteHeadPadding; switch (requestedPosition) { @@ -101,7 +98,7 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { public override doLayout(): void { super.doLayout(); - let scoreRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const scoreRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; if (this.beat.deadSlapped) { this._deadSlapped = new DeadSlappedBeatGlyph(); @@ -110,7 +107,7 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { this.width = this._deadSlapped.width; } - let direction: BeamDirection = this.direction; + const direction: BeamDirection = this.direction; let aboveBeatEffectsY = 0; let belowBeatEffectsY = 0; let belowEffectSpacing = 1; @@ -126,17 +123,17 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { if (this.direction === BeamDirection.Up) { belowEffectSpacingShiftBefore = false; aboveEffectSpacingShiftBefore = true; - belowBeatEffectsY = scoreRenderer.getScoreY(this.maxNote!.line + 2); + belowBeatEffectsY = scoreRenderer.getScoreY(this.maxNote!.steps + 2); aboveBeatEffectsY = - scoreRenderer.getScoreY(this.minNote!.line) - scoreRenderer.getStemSize(this.beamingHelper, true); + scoreRenderer.getScoreY(this.minNote!.steps) - scoreRenderer.getStemSize(this.beamingHelper, true); } else { belowEffectSpacingShiftBefore = true; aboveEffectSpacingShiftBefore = false; - belowBeatEffectsY = scoreRenderer.getScoreY(this.minNote!.line - 1); + belowBeatEffectsY = scoreRenderer.getScoreY(this.minNote!.steps - 1); belowEffectSpacing *= -1; aboveEffectSpacing *= -1; aboveBeatEffectsY = - scoreRenderer.getScoreY(this.maxNote!.line) + scoreRenderer.getStemSize(this.beamingHelper, true); + scoreRenderer.getScoreY(this.maxNote!.steps) + scoreRenderer.getStemSize(this.beamingHelper, true); } } @@ -192,9 +189,9 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { if (this.beat.isTremolo && !this.beat.deadSlapped) { let offset: number = 0; - let baseNote: ScoreNoteGlyphInfo = direction === BeamDirection.Up ? this.minNote! : this.maxNote!; - let tremoloX: number = direction === BeamDirection.Up ? this.displacedX : 0; - let speed: Duration = this.beat.tremoloSpeed!; + const baseNote: ScoreNoteGlyphInfo = direction === BeamDirection.Up ? this.minNote! : this.maxNote!; + let tremoloX: number = direction === BeamDirection.Up ? this.upLineX : this.downLineX; + const speed: Duration = this.beat.tremoloSpeed!; switch (speed) { case Duration.ThirtySecond: offset = direction === BeamDirection.Up ? -15 : 15; @@ -221,10 +218,10 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { } public buildBoundingsLookup(beatBounds: BeatBounds, cx: number, cy: number) { - for (let note of this._notes) { + for (const note of this._notes) { if (this._noteGlyphLookup.has(note.id)) { - let glyph: EffectGlyph = this._noteGlyphLookup.get(note.id)!; - let noteBounds: NoteBounds = new NoteBounds(); + const glyph: EffectGlyph = this._noteGlyphLookup.get(note.id)!; + const noteBounds: NoteBounds = new NoteBounds(); noteBounds.note = note; noteBounds.noteHeadBounds = new Bounds(); noteBounds.noteHeadBounds.x = cx + this.x + this._noteHeadPadding + glyph.x; @@ -237,13 +234,19 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { } public override paint(cx: number, cy: number, canvas: ICanvas): void { + this.paintEffects(cx, cy, canvas); + + super.paint(cx, cy, canvas); + } + + private paintEffects(cx: number, cy: number, canvas: ICanvas) { + using _ = ElementStyleHelper.beat(canvas, BeatSubElement.StandardNotationEffects, this.beat); for (const g of this.aboveBeatEffects.values()) { g.paint(cx + this.x + 2, cy + this.y, canvas); } for (const g of this.belowBeatEffects.values()) { g.paint(cx + this.x + 2, cy + this.y, canvas); } - super.paint(cx, cy, canvas); if (this._tremoloPicking) { this._tremoloPicking.paint(cx, cy, canvas); } diff --git a/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts b/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts index 99f22b0c3..40a936b90 100644 --- a/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts +++ b/src/rendering/glyphs/ScoreNoteChordGlyphBase.ts @@ -1,10 +1,13 @@ -import { EventEmitter, IEventEmitter } from '@src/EventEmitter'; -import { Color } from '@src/model/Color'; -import { ICanvas } from '@src/platform/ICanvas'; +import { EventEmitter, type IEventEmitter } from '@src/EventEmitter'; +import { BarSubElement } from '@src/model/Bar'; +import type { ICanvas } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { ScoreNoteGlyphInfo } from '@src/rendering/glyphs/ScoreNoteGlyphInfo'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; export abstract class ScoreNoteChordGlyphBase extends Glyph { private _infos: ScoreNoteGlyphInfo[] = []; @@ -25,36 +28,28 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { public abstract get direction(): BeamDirection; protected add(noteGlyph: Glyph, noteLine: number): void { - let info: ScoreNoteGlyphInfo = new ScoreNoteGlyphInfo(noteGlyph, noteLine); + const info: ScoreNoteGlyphInfo = new ScoreNoteGlyphInfo(noteGlyph, noteLine); this._infos.push(info); - if (!this.minNote || this.minNote.line > info.line) { + if (!this.minNote || this.minNote.steps > info.steps) { this.minNote = info; } - if (!this.maxNote || this.maxNote.line < info.line) { + if (!this.maxNote || this.maxNote.steps < info.steps) { this.maxNote = info; } } - public get hasTopOverflow(): boolean { - return !!this.minNote && this.minNote.line <= 0; - } - - public get hasBottomOverflow(): boolean { - return !!this.maxNote && this.maxNote.line > 8; - } - public override doLayout(): void { this._infos.sort((a, b) => { - return b.line - a.line; + return b.steps - a.steps; }); let displacedX: number = 0; let lastDisplaced: boolean = false; - let lastLine: number = 0; + let lastStep: number = 0; let anyDisplaced: boolean = false; - let direction: BeamDirection = this.direction; + const direction: BeamDirection = this.direction; let w: number = 0; for (let i: number = 0, j: number = this._infos.length; i < j; i++) { - let g: Glyph = this._infos[i].glyph; + const g: Glyph = this._infos[i].glyph; g.renderer = this.renderer; g.doLayout(); let displace: boolean = false; @@ -62,7 +57,7 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { displacedX = g.width; } else { // check if note needs to be repositioned - if (Math.abs(lastLine - this._infos[i].line) <= 1) { + if (Math.abs(lastStep - this._infos[i].steps) <= 1) { // reposition if needed if (!lastDisplaced) { displace = true; @@ -84,8 +79,13 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { g.x = displace ? displacedX : 0; } g.x += this.noteStartX; - lastLine = this._infos[i].line; + lastStep = this._infos[i].steps; w = Math.max(w, g.x + g.width); + + // after size calculation, re-align glyph to stem if needed + if (g instanceof NoteHeadGlyph && (g as NoteHeadGlyph).centerOnStem) { + g.x = displacedX; + } } if (anyDisplaced) { this._noteHeadPadding = 0; @@ -105,38 +105,53 @@ export abstract class ScoreNoteChordGlyphBase extends Glyph { cx += this.x; cy += this.y; // TODO: this method seems to be quite heavy according to the profiler, why? - let scoreRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; // TODO: Take care of beateffects in overflow - let linePadding: number = 3; - let lineWidth: number = this.width - this.noteStartX + linePadding * 2; - if (this.hasTopOverflow) { - let color: Color = canvas.color; - canvas.color = scoreRenderer.resources.staffLineColor; - let l: number = -2; - while (l >= this.minNote!.line) { - // + 1 Because we want to place the line in the center of the note, not at the top - let lY: number = cy + scoreRenderer.getScoreY(l); - canvas.fillRect(cx - linePadding + this.noteStartX, lY, lineWidth, 1); - l -= 2; - } - canvas.color = color; - } - if (this.hasBottomOverflow) { - let color: Color = canvas.color; - canvas.color = scoreRenderer.resources.staffLineColor; - let l: number = 10; - while (l <= this.maxNote!.line) { - let lY: number = cy + scoreRenderer.getScoreY(l); - canvas.fillRect(cx - linePadding + this.noteStartX, lY, lineWidth, 1); - l += 2; - } - canvas.color = color; - } - let infos: ScoreNoteGlyphInfo[] = this._infos; - let x: number = cx + this._noteHeadPadding; - for (let g of infos) { + this.paintLedgerLines(cx, cy, canvas); + const infos: ScoreNoteGlyphInfo[] = this._infos; + const x: number = cx + this._noteHeadPadding; + for (const g of infos) { g.glyph.renderer = this.renderer; g.glyph.paint(x, cy, canvas); } } + private paintLedgerLines(cx: number, cy: number, canvas: ICanvas) { + if (!this.minNote) { + return; + } + + const scoreRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + + using _ = ElementStyleHelper.bar(canvas, BarSubElement.StandardNotationStaffLine, scoreRenderer.bar, true); + + const linePadding: number = 3; + const lineWidth: number = this.width - this.noteStartX + linePadding * 2; + + 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); + + let y = firstTopLedgerY; + while (y >= minNoteLineY) { + canvas.fillRect( + cx - linePadding + this.noteStartX, + (cy + y) | 0, + lineWidth, + BarRendererBase.StaffLineThickness + ); + y -= lineSpacing; + } + + y = firstBottomLedgerY; + while (y <= maxNoteLineY) { + canvas.fillRect( + cx - linePadding + this.noteStartX, + (cy + y) | 0, + lineWidth, + BarRendererBase.StaffLineThickness + ); + y += lineSpacing; + } + } } diff --git a/src/rendering/glyphs/ScoreNoteGlyphInfo.ts b/src/rendering/glyphs/ScoreNoteGlyphInfo.ts index 8eb559999..d9b558681 100644 --- a/src/rendering/glyphs/ScoreNoteGlyphInfo.ts +++ b/src/rendering/glyphs/ScoreNoteGlyphInfo.ts @@ -1,11 +1,11 @@ -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; export class ScoreNoteGlyphInfo { public glyph: Glyph; - public line: number = 0; + public steps: number = 0; public constructor(glyph: Glyph, line: number) { this.glyph = glyph; - this.line = line; + this.steps = line; } } diff --git a/src/rendering/glyphs/ScoreRestGlyph.ts b/src/rendering/glyphs/ScoreRestGlyph.ts index 955277289..0f74ee529 100644 --- a/src/rendering/glyphs/ScoreRestGlyph.ts +++ b/src/rendering/glyphs/ScoreRestGlyph.ts @@ -1,15 +1,16 @@ import { Duration } from '@src/model/Duration'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import type { ICanvas } from '@src/platform/ICanvas'; +import { BeatSubElement } from '@src/model/Beat'; export class ScoreRestGlyph extends MusicFontGlyph { - private _duration: Duration; public beamingHelper!: BeamingHelper; public constructor(x: number, y: number, duration: Duration) { super(x, y, 1, ScoreRestGlyph.getSymbol(duration)); - this._duration = duration; } public static getSymbol(duration: Duration): MusicFontSymbol { @@ -41,31 +42,6 @@ export class ScoreRestGlyph extends MusicFontGlyph { } } - public static getSize(duration: Duration): number { - switch (duration) { - case Duration.QuadrupleWhole: - case Duration.DoubleWhole: - case Duration.Whole: - case Duration.Half: - case Duration.Quarter: - case Duration.Eighth: - case Duration.Sixteenth: - return 9; - case Duration.ThirtySecond: - return 12; - case Duration.SixtyFourth: - return 14; - case Duration.OneHundredTwentyEighth: - case Duration.TwoHundredFiftySixth: - return 20; - } - return 10; - } - - public override doLayout(): void { - this.width = ScoreRestGlyph.getSize(this._duration); - } - public updateBeamingHelper(cx: number): void { if (this.beamingHelper) { this.beamingHelper.registerBeatLineX( @@ -76,4 +52,13 @@ export class ScoreRestGlyph extends MusicFontGlyph { ); } } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + this.internalPaint(cx, cy, canvas, BeatSubElement.StandardNotationRests); + } + + protected internalPaint(cx: number, cy: number, canvas: ICanvas, element: BeatSubElement): void { + using _ = ElementStyleHelper.beat(canvas, element, this.beat!); + super.paint(cx, cy, canvas); + } } diff --git a/src/rendering/glyphs/ScoreSlideLineGlyph.ts b/src/rendering/glyphs/ScoreSlideLineGlyph.ts index fc4870424..c5bfa091a 100644 --- a/src/rendering/glyphs/ScoreSlideLineGlyph.ts +++ b/src/rendering/glyphs/ScoreSlideLineGlyph.ts @@ -1,16 +1,16 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; import { VibratoType } from '@src/model/VibratoType'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase, NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; +import type { ICanvas } from '@src/platform/ICanvas'; +import { type BarRendererBase, NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; -import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; +import type { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { NoteVibratoGlyph } from '@src/rendering/glyphs/NoteVibratoGlyph'; -import { ScoreBeatPreNotesGlyph } from '@src/rendering/glyphs/ScoreBeatPreNotesGlyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBeatPreNotesGlyph } from '@src/rendering/glyphs/ScoreBeatPreNotesGlyph'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; export class ScoreSlideLineGlyph extends Glyph { private _outType: SlideOutType; @@ -36,10 +36,10 @@ export class ScoreSlideLineGlyph extends Glyph { } private paintSlideIn(cx: number, cy: number, canvas: ICanvas): void { - let startNoteRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; - let sizeX: number = 12; + const startNoteRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const sizeX: number = 12; let endX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Left) - 2; - let endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); + const endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); let startX = endX - sizeX; let startY: number = cy + startNoteRenderer.y; @@ -54,14 +54,14 @@ export class ScoreSlideLineGlyph extends Glyph { return; } - let accidentalsWidth: number = this.getAccidentalsWidth(startNoteRenderer, this._startNote.beat); + const accidentalsWidth: number = this.getAccidentalsWidth(startNoteRenderer, this._startNote.beat); startX -= accidentalsWidth; endX -= accidentalsWidth; this.paintSlideLine(canvas, false, startX, endX, startY, endY); } private getAccidentalsWidth(renderer: ScoreBarRenderer, beat: Beat): number { - let preNotes: ScoreBeatPreNotesGlyph = renderer.getPreNotesGlyphForBeat(beat) as ScoreBeatPreNotesGlyph; + const preNotes: ScoreBeatPreNotesGlyph = renderer.getPreNotesGlyphForBeat(beat) as ScoreBeatPreNotesGlyph; if (preNotes && preNotes.accidentals) { return preNotes.accidentals.width; } @@ -69,10 +69,10 @@ export class ScoreSlideLineGlyph extends Glyph { } private drawSlideOut(cx: number, cy: number, canvas: ICanvas): void { - let startNoteRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; - let sizeX: number = 12; - let offsetX: number = 2; - let offsetY: number = 2; + const startNoteRenderer: ScoreBarRenderer = this.renderer as ScoreBarRenderer; + const sizeX: number = 12; + const offsetX: number = 2; + const offsetY: number = 2; let startX: number = 0; let startY: number = 0; let endX: number = 0; @@ -84,14 +84,15 @@ export class ScoreSlideLineGlyph extends Glyph { startX = cx + startNoteRenderer.x + - startNoteRenderer.getBeatX(this._startNote.beat, BeatXPosition.PostNotes) + + startNoteRenderer.getBeatX(this._startNote.beat, BeatXPosition.PostNotes) + offsetX; startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); if (this._startNote.slideTarget) { - let endNoteRenderer: BarRendererBase | null = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, - this._startNote.slideTarget.beat.voice.bar - ); + const endNoteRenderer: BarRendererBase | null = + this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff.staffId, + this._startNote.slideTarget.beat.voice.bar + ); if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { endX = cx + startNoteRenderer.x + startNoteRenderer.width; endY = startY; @@ -101,10 +102,13 @@ export class ScoreSlideLineGlyph extends Glyph { endNoteRenderer.x + endNoteRenderer.getBeatX(this._startNote.slideTarget.beat, BeatXPosition.PreNotes) - offsetX; - endY = cy + endNoteRenderer.y + endNoteRenderer.getNoteY(this._startNote.slideTarget, NoteYPosition.Center); + endY = + cy + + endNoteRenderer.y + + endNoteRenderer.getNoteY(this._startNote.slideTarget, NoteYPosition.Center); } - if(this._startNote.slideTarget.realValue > this._startNote.realValue) { + if (this._startNote.slideTarget.realValue > this._startNote.realValue) { startY += offsetY; endY -= offsetY; } else { @@ -117,19 +121,31 @@ export class ScoreSlideLineGlyph extends Glyph { } break; case SlideOutType.OutUp: - startX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + offsetX; + startX = + cx + + startNoteRenderer.x + + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + + offsetX; startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); endX = startX + sizeX; endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Top); break; case SlideOutType.OutDown: - startX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + offsetX; + startX = + cx + + startNoteRenderer.x + + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + + offsetX; startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); endX = startX + sizeX; endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Bottom); break; case SlideOutType.PickSlideUp: - startX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + offsetX * 2; + startX = + cx + + startNoteRenderer.x + + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + + offsetX * 2; startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Top); endX = cx + startNoteRenderer.x + startNoteRenderer.width; @@ -145,7 +161,11 @@ export class ScoreSlideLineGlyph extends Glyph { waves = true; break; case SlideOutType.PickSlideDown: - startX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + offsetX * 2; + startX = + cx + + startNoteRenderer.x + + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + + offsetX * 2; startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Bottom); endX = cx + startNoteRenderer.x + startNoteRenderer.width; @@ -175,19 +195,19 @@ export class ScoreSlideLineGlyph extends Glyph { endY: number ): void { if (waves) { - let glyph: NoteVibratoGlyph = new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2); + const glyph: NoteVibratoGlyph = new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2); glyph.renderer = this.renderer; glyph.doLayout(); startY -= glyph.height / 2; endY -= glyph.height / 2; - let b: number = endX - startX; - let a: number = endY - startY; - let c: number = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)); + const b: number = endX - startX; + const a: number = endY - startY; + const c: number = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)); glyph.width = b; - let angle: number = Math.asin(a / c) * (180 / Math.PI); + const angle: number = Math.asin(a / c) * (180 / Math.PI); canvas.beginRotate(startX, startY, angle); glyph.paint(0, 0, canvas); canvas.endRotate(); diff --git a/src/rendering/glyphs/ScoreSlurGlyph.ts b/src/rendering/glyphs/ScoreSlurGlyph.ts index 3879c2cfe..2cc42cb8a 100644 --- a/src/rendering/glyphs/ScoreSlurGlyph.ts +++ b/src/rendering/glyphs/ScoreSlurGlyph.ts @@ -1,6 +1,6 @@ -import { Note } from '@src/model/Note'; +import type { Note } from '@src/model/Note'; import { ScoreLegatoGlyph } from '@src/rendering/glyphs/ScoreLegatoGlyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { NoteYPosition, NoteXPosition } from '@src/rendering/BarRendererBase'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { GraceType } from '@src/model/GraceType'; @@ -43,17 +43,15 @@ export class ScoreSlurGlyph extends ScoreLegatoGlyph { default: return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.BottomWithStem); } - } else { - switch (this.tieDirection) { - case BeamDirection.Up: - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Top); - default: - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Bottom); - } } - } else { - return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Center); + switch (this.tieDirection) { + case BeamDirection.Up: + return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Top); + default: + return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Bottom); + } } + return this.endNoteRenderer!.getNoteY(this._endNote, NoteYPosition.Center); } private isStartCentered() { @@ -63,9 +61,10 @@ export class ScoreSlurGlyph extends ScoreLegatoGlyph { ); } 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) + 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)) ); } @@ -88,11 +87,9 @@ export class ScoreSlurGlyph extends ScoreLegatoGlyph { if (this.isEndCentered()) { if (this.isEndOnStem()) { return this.endNoteRenderer!.getBeatX(this._endNote.beat, BeatXPosition.Stem); - } else { - return this.endNoteRenderer!.getNoteX(this._endNote, NoteXPosition.Center); } - } else { - return this.endNoteRenderer!.getBeatX(this._endNote.beat, BeatXPosition.PreNotes); + return this.endNoteRenderer!.getNoteX(this._endNote, NoteXPosition.Center); } + return this.endNoteRenderer!.getBeatX(this._endNote.beat, BeatXPosition.PreNotes); } } diff --git a/src/rendering/glyphs/ScoreTieGlyph.ts b/src/rendering/glyphs/ScoreTieGlyph.ts index 4ac97223e..f4c56741f 100644 --- a/src/rendering/glyphs/ScoreTieGlyph.ts +++ b/src/rendering/glyphs/ScoreTieGlyph.ts @@ -1,8 +1,8 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; -import { BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import { type BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; import { TieGlyph } from '@src/rendering/glyphs/TieGlyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; @@ -17,7 +17,11 @@ export class ScoreTieGlyph extends TieGlyph { } protected override shouldDrawBendSlur() { - return this.renderer.settings.notation.extendBendArrowsOnTiedNotes && !!this.startNote.bendOrigin && this.startNote.isTieOrigin; + return ( + this.renderer.settings.notation.extendBendArrowsOnTiedNotes && + !!this.startNote.bendOrigin && + this.startNote.isTieOrigin + ); } public override doLayout(): void { diff --git a/src/rendering/glyphs/ScoreWhammyBarGlyph.ts b/src/rendering/glyphs/ScoreWhammyBarGlyph.ts index 93c4d643c..965b97a67 100644 --- a/src/rendering/glyphs/ScoreWhammyBarGlyph.ts +++ b/src/rendering/glyphs/ScoreWhammyBarGlyph.ts @@ -1,21 +1,24 @@ -import { Beat } from '@src/model/Beat'; -import { BendPoint } from '@src/model/BendPoint'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; +import type { BendPoint } from '@src/model/BendPoint'; import { BendStyle } from '@src/model/BendStyle'; -import { Note } from '@src/model/Note'; +import { type Note, NoteSubElement } from '@src/model/Note'; import { WhammyType } from '@src/model/WhammyType'; import { NotationMode } from '@src/NotationSettings'; -import { ICanvas, TextAlign } from '@src/platform/ICanvas'; +import type { ICanvas, TextAlign } from '@src/platform/ICanvas'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { BendNoteHeadGroupGlyph } from '@src/rendering/glyphs/BendNoteHeadGroupGlyph'; -import { ScoreBeatPreNotesGlyph } from '@src/rendering/glyphs/ScoreBeatPreNotesGlyph'; +import type { ScoreBeatPreNotesGlyph } from '@src/rendering/glyphs/ScoreBeatPreNotesGlyph'; import { ScoreHelperNotesBaseGlyph } from '@src/rendering/glyphs/ScoreHelperNotesBaseGlyph'; import { TieGlyph } from '@src/rendering/glyphs/TieGlyph'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; -import { RenderingResources } from '@src/RenderingResources'; +import type { RenderingResources } from '@src/RenderingResources'; import { TabWhammyBarGlyph } from '@src/rendering/glyphs/TabWhammyBarGlyph'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; import { NoteYPosition } from '@src/rendering/BarRendererBase'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { public static readonly SimpleDipHeight: number = TabWhammyBarGlyph.PerHalfSize * 2; @@ -23,12 +26,12 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { private _beat: Beat; public constructor(beat: Beat) { - super(0,0); + super(0, 0); this._beat = beat; } public override doLayout(): void { - let whammyMode: NotationMode = this.renderer.settings.notation.notationMode; + const whammyMode: NotationMode = this.renderer.settings.notation.notationMode; switch (this._beat.whammyBarType) { case WhammyType.None: case WhammyType.Custom: @@ -37,14 +40,16 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { case WhammyType.Dive: case WhammyType.PrediveDive: { - let endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); + const endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); endGlyphs.renderer = this.renderer; - let lastWhammyPoint: BendPoint = this._beat.whammyBarPoints![this._beat.whammyBarPoints!.length - 1]; - for (let note of this._beat.notes) { + const lastWhammyPoint: BendPoint = + this._beat.whammyBarPoints![this._beat.whammyBarPoints!.length - 1]; + for (const note of this._beat.notes) { if (!note.isTieOrigin) { endGlyphs.addGlyph( this.getBendNoteValue(note, lastWhammyPoint), - lastWhammyPoint.value % 2 !== 0 + lastWhammyPoint.value % 2 !== 0, + undefined ); } } @@ -55,35 +60,36 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { case WhammyType.Dip: { if (whammyMode === NotationMode.SongBook) { - let res: RenderingResources = this.renderer.resources; + const res: RenderingResources = this.renderer.resources; (this.renderer as ScoreBarRenderer).simpleWhammyOverflow = res.tablatureFont.size * 1.5 + ScoreWhammyBarGlyph.SimpleDipHeight + ScoreWhammyBarGlyph.SimpleDipPadding; } else { - let middleGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); + const middleGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); middleGlyphs.renderer = this.renderer; if (this.renderer.settings.notation.notationMode === NotationMode.GuitarPro) { - let middleBendPoint: BendPoint = this._beat.whammyBarPoints![1]; - for (let note of this._beat.notes) { + const middleBendPoint: BendPoint = this._beat.whammyBarPoints![1]; + for (const note of this._beat.notes) { middleGlyphs.addGlyph( this.getBendNoteValue(note, this._beat.whammyBarPoints![1]), - middleBendPoint.value % 2 !== 0 + middleBendPoint.value % 2 !== 0, + undefined ); } } middleGlyphs.doLayout(); this.BendNoteHeads.push(middleGlyphs); - let endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); + const endGlyphs: BendNoteHeadGroupGlyph = new BendNoteHeadGroupGlyph(this._beat, false); endGlyphs.renderer = this.renderer; if (this.renderer.settings.notation.notationMode === NotationMode.GuitarPro) { - let lastBendPoint: BendPoint = this._beat.whammyBarPoints![ - this._beat.whammyBarPoints!.length - 1 - ]; - for (let note of this._beat.notes) { + const lastBendPoint: BendPoint = + this._beat.whammyBarPoints![this._beat.whammyBarPoints!.length - 1]; + for (const note of this._beat.notes) { endGlyphs.addGlyph( this.getBendNoteValue(note, lastBendPoint), - lastBendPoint.value % 2 !== 0 + lastBendPoint.value % 2 !== 0, + undefined ); } } @@ -99,23 +105,30 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let beat: Beat = this._beat; + const beat: Beat = this._beat; switch (beat.whammyBarType) { case WhammyType.None: case WhammyType.Custom: return; } - let whammyMode: NotationMode = this.renderer.settings.notation.notationMode; - let startNoteRenderer: ScoreBarRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, + const whammyMode: NotationMode = this.renderer.settings.notation.notationMode; + const startNoteRenderer: ScoreBarRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff.staffId, beat.voice.bar )! as ScoreBarRenderer; - let startX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(beat, BeatXPosition.MiddleNotes); - let beatDirection: BeamDirection = this.getTieDirection(beat, startNoteRenderer); + + using _beatStyle = ElementStyleHelper.beat(canvas, BeatSubElement.StandardNotationEffects, beat); + + const startX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(beat, BeatXPosition.MiddleNotes); + const beatDirection: BeamDirection = this.getTieDirection(beat, startNoteRenderer); let direction: BeamDirection = this._beat.notes.length === 1 ? beatDirection : BeamDirection.Up; - let textalign: TextAlign = canvas.textAlign; + const textalign: TextAlign = canvas.textAlign; + const noteHeadHeight = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.NoteheadBlack)!; + for (let i: number = 0; i < beat.notes.length; i++) { - let note: Note = beat.notes[i]; + const note: Note = beat.notes[i]; + using _noteStyle = ElementStyleHelper.note(canvas, NoteSubElement.StandardNotationEffects, note); + let startY: number = cy + startNoteRenderer.y; if (i > 0 && i >= ((this._beat.notes.length / 2) | 0)) { direction = BeamDirection.Down; @@ -134,11 +147,11 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { endX += startNoteRenderer.getBeatX(beat, BeatXPosition.EndBeat); } endX -= 8; - let slurText: string = beat.whammyStyle === BendStyle.Gradual && i === 0 ? 'grad.' : ''; + 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.staveId, + this.renderer.staff.staffId, note.tieDestination!.beat.voice.bar ) as ScoreBarRenderer | null; if (endNoteRenderer && endNoteRenderer.staff === startNoteRenderer.staff) { @@ -150,13 +163,14 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { endNoteRenderer = null; } } - let heightOffset: number = NoteHeadGlyph.NoteHeadHeight * NoteHeadGlyph.GraceScale * 0.5; + let heightOffset: number = noteHeadHeight * NoteHeadGlyph.GraceScale * 0.5; if (direction === BeamDirection.Up) { heightOffset = -heightOffset; } - let endValue: number = beat.whammyBarPoints!.length > 0 - ? this.getBendNoteValue(note, beat.whammyBarPoints![beat.whammyBarPoints!.length - 1]) - : 0; + const endValue: number = + beat.whammyBarPoints!.length > 0 + ? this.getBendNoteValue(note, beat.whammyBarPoints![beat.whammyBarPoints!.length - 1]) + : 0; let endY: number = 0; let bendTie: boolean = false; @@ -170,7 +184,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { endY = cy + endNoteRenderer.y + endNoteRenderer.getNoteY(note.tieDestination!, NoteYPosition.Top); bendTie = true; if (direction === BeamDirection.Down) { - endY += NoteHeadGlyph.NoteHeadHeight; + endY += noteHeadHeight; } } else if (note.isTieOrigin) { if (!endNoteRenderer) { @@ -179,7 +193,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { endY = cy + endNoteRenderer.y + endNoteRenderer.getNoteY(note.tieDestination!, NoteYPosition.Top); } if (direction === BeamDirection.Down) { - endY += NoteHeadGlyph.NoteHeadHeight; + endY += noteHeadHeight; } } @@ -202,9 +216,11 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { 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)) { + if (this.BendNoteHeads[0].containsNoteValue(endValue)) { + endY -= previousY; endY += this.BendNoteHeads[0].y; } } @@ -236,25 +252,25 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { case WhammyType.Dip: if (whammyMode === NotationMode.SongBook) { if (i === 0) { - let simpleStartX: number = + const simpleStartX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.OnNotes) - 2; - let simpleEndX: number = + const simpleEndX: number = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.PostNotes) + 2; - let middleX: number = (simpleStartX + simpleEndX) / 2; - let text: string = ( + 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); - let simpleStartY: number = cy + this.y + canvas.font.size + 2; - let simpleEndY: number = simpleStartY + ScoreWhammyBarGlyph.SimpleDipHeight; + const simpleStartY: number = cy + this.y + canvas.font.size + 2; + const simpleEndY: number = simpleStartY + ScoreWhammyBarGlyph.SimpleDipHeight; if (this._beat.whammyBarPoints![1].value > this._beat.whammyBarPoints![0].value) { canvas.moveTo(simpleStartX, simpleEndY); canvas.lineTo(middleX, simpleStartY); @@ -280,12 +296,12 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { ); } } else { - let middleX: number = (startX + endX) / 2; + 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); - let middleValue: number = this.getBendNoteValue(note, beat.whammyBarPoints![1]); - let middleY: number = this.BendNoteHeads[0].getNoteValueY(middleValue) + heightOffset; + const middleValue: number = this.getBendNoteValue(note, beat.whammyBarPoints![1]); + const middleY: number = this.BendNoteHeads[0].getNoteValueY(middleValue) + heightOffset; this.drawBendSlur( canvas, startX, @@ -318,7 +334,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph { cx + startNoteRenderer.x + startNoteRenderer.getBeatX(note.beat, BeatXPosition.PreNotes); preX += (startNoteRenderer.getPreNotesGlyphForBeat(note.beat) as ScoreBeatPreNotesGlyph) .prebendNoteHeadOffset; - let preY: number = + const preY: number = cy + startNoteRenderer.y + startNoteRenderer.getScoreY( diff --git a/src/rendering/glyphs/SlashBeatGlyph.ts b/src/rendering/glyphs/SlashBeatGlyph.ts index aee4d18ff..ef65e537f 100644 --- a/src/rendering/glyphs/SlashBeatGlyph.ts +++ b/src/rendering/glyphs/SlashBeatGlyph.ts @@ -1,23 +1,28 @@ import { GraceType } from '@src/model/GraceType'; -import { Note } from '@src/model/Note'; +import type { Note } from '@src/model/Note'; import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; import { CircleGlyph } from '@src/rendering/glyphs/CircleGlyph'; import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; import { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; -import { SlashNoteHeadGlyph } from './SlashNoteHeadGlyph'; -import { SlashBarRenderer } from '../SlashBarRenderer'; -import { NoteBounds } from '../utils/NoteBounds'; -import { Bounds } from '../utils/Bounds'; -import { SlashRestGlyph } from './SlashRestGlyph'; -import { DeadSlappedBeatGlyph } from './DeadSlappedBeatGlyph'; -import { Glyph } from './Glyph'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import { SlashNoteHeadGlyph } from '@src/rendering/glyphs/SlashNoteHeadGlyph'; +import type { SlashBarRenderer } from '@src/rendering/SlashBarRenderer'; +import { NoteBounds } from '@src/rendering/utils/NoteBounds'; +import { Bounds } from '@src/rendering/utils/Bounds'; +import { SlashRestGlyph } from '@src/rendering/glyphs/SlashRestGlyph'; +import { DeadSlappedBeatGlyph } from '@src/rendering/glyphs/DeadSlappedBeatGlyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; +import { BeatSubElement } from '@src/model/Beat'; export class SlashBeatGlyph extends BeatOnNoteGlyphBase { public noteHeads: SlashNoteHeadGlyph | null = null; public deadSlapped: DeadSlappedBeatGlyph | null = null; public restGlyph: SlashRestGlyph | null = null; + protected override get effectElement() { + return BeatSubElement.SlashEffects; + } + public override getNoteX(_note: Note, requestedPosition: NoteXPosition): number { let g: Glyph | null = null; if (this.noteHeads) { @@ -105,7 +110,7 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { public override doLayout(): void { // create glyphs - let sr = this.renderer as SlashBarRenderer; + const sr = this.renderer as SlashBarRenderer; const line: number = sr.getNoteLine(); const glyphY = sr.getLineY(line); @@ -114,21 +119,27 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { deadSlapped.renderer = this.renderer; deadSlapped.doLayout(); this.deadSlapped = deadSlapped; - this.addGlyph(deadSlapped); + 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); + const noteHeadGlyph = new SlashNoteHeadGlyph( + 0, + glyphY, + this.container.beat.duration, + isGrace, + this.container.beat + ); this.noteHeads = noteHeadGlyph; noteHeadGlyph.beat = this.container.beat; noteHeadGlyph.beamingHelper = this.beamingHelper; - this.addGlyph(noteHeadGlyph); + this.addNormal(noteHeadGlyph); } else { const restGlyph = new SlashRestGlyph(0, glyphY, this.container.beat.duration); this.restGlyph = restGlyph; restGlyph.beat = this.container.beat; restGlyph.beamingHelper = this.beamingHelper; - this.addGlyph(restGlyph); + this.addNormal(restGlyph); if (this.beamingHelper) { this.beamingHelper.applyRest(this.container.beat, 0); @@ -140,11 +151,9 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { // Note dots // if (this.container.beat.dots > 0) { - this.addGlyph(new SpacingGlyph(0, 0, 5)); + this.addNormal(new SpacingGlyph(0, 0, 5)); for (let i: number = 0; i < this.container.beat.dots; i++) { - this.addGlyph( - new CircleGlyph(0, sr.getLineY(sr.getNoteLine()) - sr.getLineHeight(0.5), 1.5) - ); + this.addEffect(new CircleGlyph(0, sr.getLineY(sr.getNoteLine()) - sr.getLineHeight(0.5), 1.5)); } } @@ -156,7 +165,7 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { this.centerX = this.restGlyph.x + this.restGlyph.width / 2; } else if (this.noteHeads) { this.centerX = this.noteHeads.x + this.noteHeads.width / 2; - } else if(this.deadSlapped) { + } else if (this.deadSlapped) { this.centerX = this.deadSlapped.x + this.deadSlapped.width / 2; } } diff --git a/src/rendering/glyphs/SlashNoteHeadGlyph.ts b/src/rendering/glyphs/SlashNoteHeadGlyph.ts index 436c5c0e8..453782999 100644 --- a/src/rendering/glyphs/SlashNoteHeadGlyph.ts +++ b/src/rendering/glyphs/SlashNoteHeadGlyph.ts @@ -1,60 +1,57 @@ import { Duration } from '@src/model/Duration'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { NoteHeadGlyph } from './NoteHeadGlyph'; -import { Glyph } from './Glyph'; -import { BeamingHelper } from '../utils/BeamingHelper'; -import { EffectGlyph } from './EffectGlyph'; +import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; +import { NoteSubElement } from '@src/model/Note'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; export class SlashNoteHeadGlyph extends EffectGlyph { - public static readonly NoteHeadHeight: number = 17; - - public static readonly QuarterNoteHeadWidth: number = 12; - public static readonly HalfNoteHeadWidth: number = 25; - public static readonly WholeNoteHeadWidth: number = 32; - private _isGrace: boolean; - private _duration: Duration; 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) { + public constructor(x: number, y: number, duration: Duration, isGrace: boolean, beat: Beat) { super(x, y); this._isGrace = isGrace; - this._duration = duration; - this._symbol = SlashNoteHeadGlyph.getSymbol(duration) + this._symbol = SlashNoteHeadGlyph.getSymbol(duration); + this.beat = beat; } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let offset: number = this._isGrace ? 1 : 0; + using _ = + this.beat!.notes.length === 0 + ? undefined + : ElementStyleHelper.note(canvas, this.noteHeadElement, this.beat!.notes[0]); + const offset: number = this._isGrace ? 1 : 0; const glyphScale = this._isGrace ? NoteHeadGlyph.GraceScale : 1; canvas.fillMusicFontSymbol(cx + this.x, cy + this.y + offset, glyphScale, this._symbol, false); + + this.paintEffects(cx, cy, canvas); + } + private paintEffects(cx: number, cy: number, canvas: ICanvas) { + using _ = ElementStyleHelper.beat(canvas, this.effectElement, this.beat!); for (const g of this.beatEffects.values()) { g.paint(cx + this.x, cy + this.y, canvas); } } public override doLayout(): void { - const scale: number = (this._isGrace ? NoteHeadGlyph.GraceScale : 1); - switch (this._duration) { - case Duration.QuadrupleWhole: - case Duration.DoubleWhole: - case Duration.Whole: - this.width = SlashNoteHeadGlyph.WholeNoteHeadWidth * scale; - break; - case Duration.Half: - this.width = SlashNoteHeadGlyph.HalfNoteHeadWidth * scale; - break; - default: - this.width = SlashNoteHeadGlyph.QuarterNoteHeadWidth * scale; - break; - } - this.height = SlashNoteHeadGlyph.NoteHeadHeight * scale; + const scale: number = this._isGrace ? NoteHeadGlyph.GraceScale : 1; + + this.width = MusicFontSymbolSizes.Widths.get(this._symbol)! * scale; + this.height = MusicFontSymbolSizes.Heights.get(this._symbol)! * scale; - let effectSpacing: number = 7; - let effectY = SlashNoteHeadGlyph.NoteHeadHeight; + const effectSpacing: number = 7; + let effectY = MusicFontSymbolSizes.Heights.get(this._symbol)!; for (const g of this.beatEffects.values()) { g.y += effectY; g.x += this.width / 2; @@ -79,13 +76,7 @@ export class SlashNoteHeadGlyph extends EffectGlyph { public updateBeamingHelper(cx: number) { if (this.beamingHelper) { - this.beamingHelper.registerBeatLineX( - 'slash', - this.beat!, - cx + this.x + this.width, - cx + this.x - ); + this.beamingHelper.registerBeatLineX('slash', this.beat!, cx + this.x + this.width, cx + this.x); } } - } diff --git a/src/rendering/glyphs/SlashRestGlyph.ts b/src/rendering/glyphs/SlashRestGlyph.ts index 33679ba03..ab0b4fd63 100644 --- a/src/rendering/glyphs/SlashRestGlyph.ts +++ b/src/rendering/glyphs/SlashRestGlyph.ts @@ -1,11 +1,8 @@ -import { Duration } from '@src/model/Duration'; -import { ScoreRestGlyph } from './ScoreRestGlyph'; +import { BeatSubElement } from '@src/model/Beat'; +import type { ICanvas } from '@src/platform/ICanvas'; +import { ScoreRestGlyph } from '@src/rendering/glyphs/ScoreRestGlyph'; export class SlashRestGlyph extends ScoreRestGlyph { - public constructor(x: number, y: number, duration: Duration) { - super(x, y, duration); - } - public override updateBeamingHelper(cx: number): void { if (this.beamingHelper) { this.beamingHelper.registerBeatLineX( @@ -16,5 +13,8 @@ export class SlashRestGlyph extends ScoreRestGlyph { ); } } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + super.internalPaint(cx, cy, canvas, BeatSubElement.SlashRests); + } } - \ No newline at end of file diff --git a/src/rendering/glyphs/SlashTieGlyph.ts b/src/rendering/glyphs/SlashTieGlyph.ts index e8f45c20f..9ea3d2927 100644 --- a/src/rendering/glyphs/SlashTieGlyph.ts +++ b/src/rendering/glyphs/SlashTieGlyph.ts @@ -1,6 +1,6 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; -import { BarRendererBase, NoteYPosition, NoteXPosition } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import { type BarRendererBase, NoteYPosition, NoteXPosition } from '@src/rendering/BarRendererBase'; import { TieGlyph } from '@src/rendering/glyphs/TieGlyph'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; @@ -15,7 +15,7 @@ export class SlashTieGlyph extends TieGlyph { } protected override getTieHeight(startX: number, startY: number, endX: number, endY: number): number { - if(this.startNote === this.endNote) { + if (this.startNote === this.endNote) { return 15; } return super.getTieHeight(startX, startY, endX, endY); @@ -38,7 +38,7 @@ export class SlashTieGlyph extends TieGlyph { } protected override getStartX(): number { - if(this.startNote === this.endNote) { + if (this.startNote === this.endNote) { return this.getEndX() - 20; } return this.startNoteRenderer!.getNoteX(this.startNote, NoteXPosition.Right); diff --git a/src/rendering/glyphs/StringNumberContainerGlyph.ts b/src/rendering/glyphs/StringNumberContainerGlyph.ts index 9a91dc74d..c1ec59868 100644 --- a/src/rendering/glyphs/StringNumberContainerGlyph.ts +++ b/src/rendering/glyphs/StringNumberContainerGlyph.ts @@ -1,8 +1,8 @@ -import { ICanvas } from '@src/platform'; -import { EffectGlyph } from './EffectGlyph'; -import { TuningGlyph } from './TuningGlyph'; -import { MusicFontSymbol } from '@src/model'; -import { NoteHeadGlyph } from './NoteHeadGlyph'; +import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { TuningGlyph } from '@src/rendering/glyphs/TuningGlyph'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import type { ICanvas } from '@src/platform/ICanvas'; export class StringNumberContainerGlyph extends EffectGlyph { private _strings: Set = new Set(); @@ -12,7 +12,8 @@ export class StringNumberContainerGlyph extends EffectGlyph { } public override doLayout(): void { - const circleHeight = TuningGlyph.CircleNumberHeight * TuningGlyph.CircleNumberScale; + const circleHeight = + MusicFontSymbolSizes.Widths.get(MusicFontSymbol.GuitarString0)! * TuningGlyph.CircleNumberScale; this.height = (circleHeight + 3) * this._strings.size; this.width = circleHeight; } @@ -21,12 +22,14 @@ export class StringNumberContainerGlyph extends EffectGlyph { const tuningLength = this.renderer.bar.staff.tuning.length; let y = 0; - const circleHeight = TuningGlyph.CircleNumberHeight * TuningGlyph.CircleNumberScale; + const circleHeight = + MusicFontSymbolSizes.Widths.get(MusicFontSymbol.GuitarString0)! * TuningGlyph.CircleNumberScale; + const noteHeadHeight = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.NoteheadBlack)!; for (const s of this._strings) { const stringValue = tuningLength - s; const symbol = ((MusicFontSymbol.GuitarString1 as number) + stringValue) as MusicFontSymbol; canvas.fillMusicFontSymbol( - cx + this.x + NoteHeadGlyph.NoteHeadHeight / 2, + cx + this.x + noteHeadHeight / 2, cy + this.y + circleHeight + y, TuningGlyph.CircleNumberScale, symbol, diff --git a/src/rendering/glyphs/SustainPedalGlyph.ts b/src/rendering/glyphs/SustainPedalGlyph.ts index f6f3c4ef3..0f34cc884 100644 --- a/src/rendering/glyphs/SustainPedalGlyph.ts +++ b/src/rendering/glyphs/SustainPedalGlyph.ts @@ -1,14 +1,11 @@ -import { ICanvas } from '@src/platform'; -import { EffectGlyph } from './EffectGlyph'; -import { SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; -import { MusicFontSymbol } from '@src/model'; +import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { type SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import type { ICanvas } from '@src/platform/ICanvas'; export class SustainPedalGlyph extends EffectGlyph { - private static readonly TextHeight = 19; - private static readonly TextWidth = 35; private static readonly TextLinePadding = 3; - - private static readonly StarSize = 16; private static readonly StarLinePadding = 3; public constructor() { @@ -17,7 +14,7 @@ export class SustainPedalGlyph extends EffectGlyph { public override doLayout(): void { super.doLayout(); - this.height = SustainPedalGlyph.TextHeight; + this.height = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.KeyboardPedalPed)!; } public override paint(cx: number, cy: number, canvas: ICanvas): void { @@ -26,7 +23,10 @@ export class SustainPedalGlyph extends EffectGlyph { const y = cy + this.y; const h = this.height; - let markers = renderer.bar.sustainPedals; + const markers = renderer.bar.sustainPedals; + + const textWidth = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.KeyboardPedalPed)!; + const starSize = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.KeyboardPedalUp)!; let markerIndex = 0; while (markerIndex < markers.length) { @@ -38,10 +38,10 @@ export class SustainPedalGlyph extends EffectGlyph { let linePadding = 0; if (marker.pedalType === SustainPedalMarkerType.Down) { canvas.fillMusicFontSymbol(markerX, y + h, 1, MusicFontSymbol.KeyboardPedalPed, true); - linePadding = SustainPedalGlyph.TextWidth / 2 + SustainPedalGlyph.TextLinePadding; + linePadding = textWidth / 2 + SustainPedalGlyph.TextLinePadding; } else if (marker.pedalType === SustainPedalMarkerType.Up) { canvas.fillMusicFontSymbol(markerX, y + h, 1, MusicFontSymbol.KeyboardPedalUp, true); - linePadding = SustainPedalGlyph.StarSize / 2 + SustainPedalGlyph.StarLinePadding; + linePadding = starSize / 2 + SustainPedalGlyph.StarLinePadding; } // line to next marker or end-of-bar @@ -51,13 +51,13 @@ export class SustainPedalGlyph extends EffectGlyph { switch (marker.nextPedalMarker.pedalType) { case SustainPedalMarkerType.Down: - nextX -= SustainPedalGlyph.TextWidth / 2; + nextX -= textWidth / 2; break; case SustainPedalMarkerType.Hold: // no offset on hold break; case SustainPedalMarkerType.Up: - nextX -= SustainPedalGlyph.StarSize / 2; + nextX -= starSize / 2; break; } diff --git a/src/rendering/glyphs/TabBeatContainerGlyph.ts b/src/rendering/glyphs/TabBeatContainerGlyph.ts index ffc8d2c0f..8ab1d7d35 100644 --- a/src/rendering/glyphs/TabBeatContainerGlyph.ts +++ b/src/rendering/glyphs/TabBeatContainerGlyph.ts @@ -1,5 +1,4 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; +import type { Note } from '@src/model/Note'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; @@ -7,18 +6,13 @@ import { TabBendGlyph } from '@src/rendering/glyphs/TabBendGlyph'; import { TabSlideLineGlyph } from '@src/rendering/glyphs/TabSlideLineGlyph'; import { TabSlurGlyph } from '@src/rendering/glyphs/TabSlurGlyph'; import { TabTieGlyph } from '@src/rendering/glyphs/TabTieGlyph'; -import { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; -import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; -import { BeamingHelper } from '../utils/BeamingHelper'; +import type { TabBarRenderer } from '@src/rendering/TabBarRenderer'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; export class TabBeatContainerGlyph extends BeatContainerGlyph { private _bend: TabBendGlyph | null = null; private _effectSlurs: TabSlurGlyph[] = []; - public constructor(beat: Beat, voiceContainer: VoiceContainerGlyph) { - super(beat, voiceContainer); - } - protected override drawBeamHelperAsFlags(helper: BeamingHelper): boolean { return helper.hasFlag((this.renderer as TabBarRenderer).drawBeamHelperAsFlags(helper), this.beat); } @@ -37,30 +31,30 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { if (!n.isVisible) { return; } - let renderer: TabBarRenderer = this.renderer as TabBarRenderer; + const renderer: TabBarRenderer = this.renderer as TabBarRenderer; if (n.isTieOrigin && renderer.showTiedNotes && n.tieDestination!.isVisible) { - let tie: TabTieGlyph = new TabTieGlyph(n, n.tieDestination!, false); + const tie: TabTieGlyph = new TabTieGlyph(n, n.tieDestination!, false); this.addTie(tie); } if (n.isTieDestination && renderer.showTiedNotes) { - let tie: TabTieGlyph = new TabTieGlyph(n.tieOrigin!, n, true); + const tie: TabTieGlyph = new TabTieGlyph(n.tieOrigin!, n, true); this.addTie(tie); } if (n.isLeftHandTapped && !n.isHammerPullDestination) { - let tapSlur: TabTieGlyph = new TabTieGlyph(n, n, false); + const tapSlur: TabTieGlyph = new TabTieGlyph(n, n, false); this.addTie(tapSlur); } // start effect slur on first beat if (n.isEffectSlurOrigin && n.effectSlurDestination) { let expanded: boolean = false; - for (let slur of this._effectSlurs) { + for (const slur of this._effectSlurs) { if (slur.tryExpand(n, n.effectSlurDestination, false, false)) { expanded = true; break; } } if (!expanded) { - let effectSlur: TabSlurGlyph = new TabSlurGlyph(n, n.effectSlurDestination, false, false); + const effectSlur: TabSlurGlyph = new TabSlurGlyph(n, n.effectSlurDestination, false, false); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); } @@ -68,20 +62,20 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { // end effect slur on last beat if (n.isEffectSlurDestination && n.effectSlurOrigin) { let expanded: boolean = false; - for (let slur of this._effectSlurs) { + for (const slur of this._effectSlurs) { if (slur.tryExpand(n.effectSlurOrigin, n, false, true)) { expanded = true; break; } } if (!expanded) { - let effectSlur: TabSlurGlyph = new TabSlurGlyph(n.effectSlurOrigin, n, false, true); + const effectSlur: TabSlurGlyph = new TabSlurGlyph(n.effectSlurOrigin, n, false, true); this._effectSlurs.push(effectSlur); this.addTie(effectSlur); } } if (n.slideInType !== SlideInType.None || n.slideOutType !== SlideOutType.None) { - let l: TabSlideLineGlyph = new TabSlideLineGlyph(n.slideInType, n.slideOutType, n, this); + const l: TabSlideLineGlyph = new TabSlideLineGlyph(n.slideInType, n.slideOutType, n, this); this.addTie(l); } if (n.hasBend) { diff --git a/src/rendering/glyphs/TabBeatGlyph.ts b/src/rendering/glyphs/TabBeatGlyph.ts index c4fe1cda0..ccb010cf4 100644 --- a/src/rendering/glyphs/TabBeatGlyph.ts +++ b/src/rendering/glyphs/TabBeatGlyph.ts @@ -1,26 +1,31 @@ import { Duration } from '@src/model/Duration'; import { GraceType } from '@src/model/GraceType'; -import { Note } from '@src/model/Note'; +import { type Note, NoteSubElement } from '@src/model/Note'; import { TabRhythmMode } from '@src/NotationSettings'; import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; import { CircleGlyph } from '@src/rendering/glyphs/CircleGlyph'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; import { NoteNumberGlyph } from '@src/rendering/glyphs/NoteNumberGlyph'; import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; import { TabNoteChordGlyph } from '@src/rendering/glyphs/TabNoteChordGlyph'; import { TabRestGlyph } from '@src/rendering/glyphs/TabRestGlyph'; import { TabWhammyBarGlyph } from '@src/rendering/glyphs/TabWhammyBarGlyph'; import { TremoloPickingGlyph } from '@src/rendering/glyphs/TremoloPickingGlyph'; -import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; -import { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; -import { SlashNoteHeadGlyph } from './SlashNoteHeadGlyph'; +import type { TabBarRenderer } from '@src/rendering/TabBarRenderer'; +import type { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import { BeatSubElement } from '@src/model/Beat'; +import { SlashNoteHeadGlyph } from '@src/rendering/glyphs/SlashNoteHeadGlyph'; export class TabBeatGlyph extends BeatOnNoteGlyphBase { public slash: SlashNoteHeadGlyph | null = null; public noteNumbers: TabNoteChordGlyph | null = null; public restGlyph: TabRestGlyph | null = null; + protected override get effectElement() { + return BeatSubElement.GuitarTabEffects; + } + public override getNoteX(note: Note, requestedPosition: NoteXPosition): number { return this.noteNumbers ? this.noteNumbers.getNoteX(note, requestedPosition) : 0; } @@ -36,12 +41,12 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { } public override doLayout(): void { - let tabRenderer: TabBarRenderer = this.renderer as TabBarRenderer; + const tabRenderer: TabBarRenderer = this.renderer as TabBarRenderer; if (!this.container.beat.isRest) { // // Note numbers - let isGrace: boolean = + const isGrace: boolean = this.renderer.settings.notation.smallGraceTabNotes && this.container.beat.graceType !== GraceType.None; let beatEffects: Map; @@ -49,30 +54,38 @@ 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); + const slashNoteHead = new SlashNoteHeadGlyph( + 0, + slashY, + this.container.beat.duration, + isGrace, + this.container.beat + ); + slashNoteHead.noteHeadElement = NoteSubElement.GuitarTabFretNumber; + slashNoteHead.effectElement = BeatSubElement.GuitarTabEffects; this.slash = slashNoteHead; slashNoteHead.beat = this.container.beat; slashNoteHead.beamingHelper = this.beamingHelper; - this.addGlyph(slashNoteHead); + 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 (let note of this.container.beat.notes) { + for (const note of this.container.beat.notes) { if (note.isVisible) { this.createNoteGlyph(note); } } - this.addGlyph(tabNoteNumbers); + this.addNormal(tabNoteNumbers); beatEffects = tabNoteNumbers.beatEffects; } // // Whammy Bar if (this.container.beat.hasWhammyBar) { - let whammy: TabWhammyBarGlyph = new TabWhammyBarGlyph(this.container.beat); + const whammy: TabWhammyBarGlyph = new TabWhammyBarGlyph(this.container.beat); whammy.renderer = this.renderer; whammy.doLayout(); this.container.ties.push(whammy); @@ -81,7 +94,7 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { // Tremolo Picking if (this.container.beat.isTremolo && !beatEffects.has('tremolo')) { let offset: number = 0; - let speed = this.container.beat.tremoloSpeed!; + const speed = this.container.beat.tremoloSpeed!; let tremoloX = 5; switch (speed) { case Duration.ThirtySecond: @@ -105,9 +118,9 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { // Note dots // if (this.container.beat.dots > 0 && tabRenderer.rhythmMode !== TabRhythmMode.Hidden) { - this.addGlyph(new SpacingGlyph(0, 0, 5)); + this.addNormal(new SpacingGlyph(0, 0, 5)); for (let i: number = 0; i < this.container.beat.dots; i++) { - this.addGlyph( + this.addEffect( new CircleGlyph( 0, tabRenderer.lineOffset * tabRenderer.bar.staff.tuning.length + @@ -118,30 +131,30 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { } } } else { - let line = Math.floor((this.renderer.bar.staff.tuning.length - 1) / 2); - let y: number = tabRenderer.getTabY(line); + const line = Math.floor((this.renderer.bar.staff.tuning.length - 1) / 2); + const y: number = tabRenderer.getTabY(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.addGlyph(restGlyph); + this.addNormal(restGlyph); // // Note dots // if (this.container.beat.dots > 0 && tabRenderer.showRests) { - this.addGlyph(new SpacingGlyph(0, 0, 5)); + this.addNormal(new SpacingGlyph(0, 0, 5)); for (let i: number = 0; i < this.container.beat.dots; i++) { - this.addGlyph(new CircleGlyph(0, y, 1.5)); + this.addEffect(new CircleGlyph(0, y, 1.5)); } } } // left to right layout - if (!this.glyphs) { + if (this.isEmpty) { return; } let w: number = 0; - for (let i: number = 0, j: number = this.glyphs.length; i < j; i++) { - let g: Glyph = this.glyphs[i]; + for (let i: number = 0, j: number = this.glyphs!.length; i < j; i++) { + const g: Glyph = this.glyphs![i]; g.x = w; g.renderer = this.renderer; g.doLayout(); @@ -171,15 +184,15 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { } private createNoteGlyph(n: Note): void { - let tr: TabBarRenderer = this.renderer as TabBarRenderer; - let noteNumberGlyph: NoteNumberGlyph = new NoteNumberGlyph(0, 0, n); - let l: number = n.beat.voice.bar.staff.tuning.length - n.string; + 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); noteNumberGlyph.renderer = this.renderer; noteNumberGlyph.doLayout(); this.noteNumbers!.addNoteGlyph(noteNumberGlyph, n); - let topY = noteNumberGlyph.y - noteNumberGlyph.height / 2; - let bottomY = topY + noteNumberGlyph.height; + const topY = noteNumberGlyph.y - noteNumberGlyph.height / 2; + const bottomY = topY + noteNumberGlyph.height; this.renderer.helpers.collisionHelper.reserveBeatSlot(this.container.beat, topY, bottomY); } } diff --git a/src/rendering/glyphs/TabBeatPreNotesGlyph.ts b/src/rendering/glyphs/TabBeatPreNotesGlyph.ts index 2d3080718..34f1d2332 100644 --- a/src/rendering/glyphs/TabBeatPreNotesGlyph.ts +++ b/src/rendering/glyphs/TabBeatPreNotesGlyph.ts @@ -1,3 +1,4 @@ +import { BeatSubElement } from '@src/model/Beat'; import { BrushType } from '@src/model/BrushType'; import { BeatGlyphBase } from '@src/rendering/glyphs/BeatGlyphBase'; import { SpacingGlyph } from '@src/rendering/glyphs/SpacingGlyph'; @@ -6,13 +7,13 @@ import { TabBrushGlyph } from '@src/rendering/glyphs/TabBrushGlyph'; export class TabBeatPreNotesGlyph extends BeatGlyphBase { public override doLayout(): void { if (this.container.beat.brushType !== BrushType.None && !this.container.beat.isRest) { - this.addGlyph(new TabBrushGlyph(this.container.beat)); - this.addGlyph(new SpacingGlyph(0, 0, 4)); + this.addEffect(new TabBrushGlyph(this.container.beat)); + this.addNormal(new SpacingGlyph(0, 0, 4)); } super.doLayout(); } - public constructor() { - super(); + protected override get effectElement() { + return BeatSubElement.GuitarTabEffects; } } diff --git a/src/rendering/glyphs/TabBendGlyph.ts b/src/rendering/glyphs/TabBendGlyph.ts index f096587b2..b3d424c18 100644 --- a/src/rendering/glyphs/TabBendGlyph.ts +++ b/src/rendering/glyphs/TabBendGlyph.ts @@ -1,18 +1,18 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { BendStyle } from '@src/model/BendStyle'; import { BendType } from '@src/model/BendType'; -import { Color } from '@src/model/Color'; -import { Note } from '@src/model/Note'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase, NoteYPosition, NoteXPosition } from '@src/rendering/BarRendererBase'; +import type { Color } from '@src/model/Color'; +import type { Note } from '@src/model/Note'; +import type { ICanvas } from '@src/platform/ICanvas'; +import { type BarRendererBase, NoteYPosition, NoteXPosition } from '@src/rendering/BarRendererBase'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { TabBendRenderPoint } from '@src/rendering/glyphs/TabBendRenderPoint'; -import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; -import { RenderingResources } from '@src/RenderingResources'; +import type { TabBarRenderer } from '@src/rendering/TabBarRenderer'; +import type { RenderingResources } from '@src/RenderingResources'; import { BendPoint } from '@src/model/BendPoint'; -import { VibratoType } from '@src/model'; -import { NoteVibratoGlyph } from './NoteVibratoGlyph'; +import { VibratoType } from '@src/model/VibratoType'; +import { NoteVibratoGlyph } from '@src/rendering/glyphs/NoteVibratoGlyph'; export class TabBendGlyph extends Glyph { private static readonly ArrowSize: number = 6; @@ -35,7 +35,7 @@ export class TabBendGlyph extends Glyph { public addBends(note: Note): void { this._notes.push(note); - let renderPoints: TabBendRenderPoint[] = this.createRenderingPoints(note); + const renderPoints: TabBendRenderPoint[] = this.createRenderingPoints(note); this._renderPoints.set(note.id, renderPoints); if (this._maxBendValue === -1 || this._maxBendValue < note.maxBendPoint!.value) { this._maxBendValue = note.maxBendPoint!.value; @@ -126,11 +126,11 @@ export class TabBendGlyph extends Glyph { public override doLayout(): void { super.doLayout(); - let bendHeight: number = this._maxBendValue * TabBendGlyph.BendValueHeight; + const bendHeight: number = this._maxBendValue * TabBendGlyph.BendValueHeight; this.renderer.registerOverflowTop(bendHeight); let value: number = 0; - for (let note of this._notes) { - let renderPoints: TabBendRenderPoint[] = this._renderPoints.get(note.id)!; + for (const note of this._notes) { + const renderPoints: TabBendRenderPoint[] = this._renderPoints.get(note.id)!; switch (note.bendType) { case BendType.Bend: renderPoints[1].lineValue = note.isTieOrigin @@ -178,13 +178,13 @@ export class TabBendGlyph extends Glyph { } private createRenderingPoints(note: Note): TabBendRenderPoint[] { - let renderingPoints: TabBendRenderPoint[] = []; + const renderingPoints: TabBendRenderPoint[] = []; // Guitar Pro Rendering Note: // Last point of bend is always at end of the note even // though it might not be 100% correct from timing perspective. switch (note.bendType) { case BendType.Custom: - for (let bendPoint of note.bendPoints!) { + for (const bendPoint of note.bendPoints!) { renderingPoints.push(new TabBendRenderPoint(bendPoint.offset, bendPoint.value)); } break; @@ -209,23 +209,23 @@ export class TabBendGlyph extends Glyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let color: Color = canvas.color; + const color: Color = canvas.color; if (this._notes.length > 1) { canvas.color = this.renderer.resources.secondaryGlyphColor; } - for (let note of this._notes) { - let renderPoints: TabBendRenderPoint[] = this._renderPoints.get(note.id)!; - let startNoteRenderer: BarRendererBase = this.renderer; + 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; let endNoteRenderer: BarRendererBase | null = null; let endNoteHasBend: boolean = false; - let slurText: string = note.bendStyle === BendStyle.Gradual ? 'grad.' : ''; + const slurText: string = note.bendStyle === BendStyle.Gradual ? 'grad.' : ''; let endBeat: Beat | null = null; while (endNote.isTieOrigin) { - let nextNote: Note = endNote.tieDestination!; + const nextNote: Note = endNote.tieDestination!; endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, + this.renderer.staff.staffId, nextNote.beat.voice.bar ); if (!endNoteRenderer || startNoteRenderer.staff !== endNoteRenderer.staff) { @@ -236,7 +236,7 @@ export class TabBendGlyph extends Glyph { if ( endNote.hasBend || !this.renderer.settings.notation.extendBendArrowsOnTiedNotes || - endNote.vibrato != VibratoType.None + endNote.vibrato !== VibratoType.None ) { endNoteHasBend = true; break; @@ -245,7 +245,7 @@ export class TabBendGlyph extends Glyph { endBeat = endNote.beat; endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, + this.renderer.staff.staffId, endBeat.voice.bar ) as TabBarRenderer; if ( @@ -257,7 +257,7 @@ export class TabBendGlyph extends Glyph { } let startX: number = 0; let endX: number = 0; - let topY: number = cy + startNoteRenderer.y; + const topY: number = cy + startNoteRenderer.y; // float bottomY = cy + startNoteRenderer.Y + startNoteRenderer.GetNoteY(note); startX = cx + startNoteRenderer.x; if (renderPoints[0].value > 0 || note.isContinuedBend) { @@ -286,12 +286,12 @@ export class TabBendGlyph extends Glyph { } // we need some pixels for the arrow. otherwise we might draw into the next // note - let width: number = endX - startX; + const width: number = endX - startX; // calculate offsets per step - let dX: number = width / BendPoint.MaxPosition; + const dX: number = width / BendPoint.MaxPosition; canvas.beginPath(); for (let i: number = 0, j: number = renderPoints.length - 1; i < j; i++) { - let firstPt: TabBendRenderPoint = renderPoints[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) { @@ -313,9 +313,7 @@ export class TabBendGlyph extends Glyph { if (endNote.vibrato !== VibratoType.None) { const vibratoStartX = endX - cx + TabBendGlyph.ArrowSize - endNoteRenderer.x; const vibratoStartY: number = - topY - - cy - - TabBendGlyph.BendValueHeight * renderPoints[renderPoints.length - 1].lineValue; + topY - cy - TabBendGlyph.BendValueHeight * renderPoints[renderPoints.length - 1].lineValue; const vibrato = new NoteVibratoGlyph(vibratoStartX, vibratoStartY, endNote.vibrato, 1.2); vibrato.beat = endNote.beat; @@ -338,11 +336,11 @@ export class TabBendGlyph extends Glyph { slurText: string, canvas: ICanvas ): void { - let r: TabBarRenderer = this.renderer as TabBarRenderer; - let res: RenderingResources = this.renderer.resources; - let overflowOffset: number = r.lineOffset / 2; - let x1: number = cx + dX * firstPt.offset; - let bendValueHeight: number = TabBendGlyph.BendValueHeight; + 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 = TabBendGlyph.BendValueHeight; let y1: number = cy - bendValueHeight * firstPt.lineValue; if (firstPt.value === 0) { if (secondPt.offset === firstPt.offset) { @@ -353,7 +351,7 @@ export class TabBendGlyph extends Glyph { } else { y1 += overflowOffset; } - let x2: number = cx + dX * secondPt.offset; + const x2: number = cx + dX * secondPt.offset; let y2: number = cy - bendValueHeight * secondPt.lineValue; if (secondPt.lineValue === 0) { y2 += r.getNoteY(note, NoteYPosition.Center); @@ -362,7 +360,7 @@ export class TabBendGlyph extends Glyph { } // what type of arrow? (up/down) let arrowOffset: number = 0; - let arrowSize: number = TabBendGlyph.ArrowSize; + const arrowSize: number = TabBendGlyph.ArrowSize; if (secondPt.value > firstPt.value) { if (y2 + arrowSize > y1) { y2 = y1 - arrowSize; @@ -393,9 +391,9 @@ export class TabBendGlyph extends Glyph { // we draw from right to left. it's okay if the space is at the beginning if (firstPt.lineValue > 0) { let dashX: number = x2; - let dashSize: number = TabBendGlyph.DashSize; - let end: number = x1 + dashSize; - let dashes: number = (dashX - x1) / (dashSize * 2); + const dashSize: number = TabBendGlyph.DashSize; + const end: number = x1 + dashSize; + const dashes: number = (dashX - x1) / (dashSize * 2); if (dashes < 1) { canvas.moveTo(dashX, y1); canvas.lineTo(x1, y1); @@ -422,11 +420,11 @@ export class TabBendGlyph extends Glyph { } if (slurText && firstPt.offset < secondPt.offset) { canvas.font = res.graceFont; - let size: number = canvas.measureText(slurText).width; + const size: number = canvas.measureText(slurText).width; let y: number = 0; let x: number = 0; if (y1 > y2) { - let h: number = Math.abs(y1 - y2); + const h: number = Math.abs(y1 - y2); y = h > canvas.font.size * 1.3 ? y1 - h / 2 : y1; x = (x1 + x2 - size) / 2; } else { @@ -437,7 +435,7 @@ export class TabBendGlyph extends Glyph { } if (secondPt.value !== 0 && firstPt.value !== secondPt.value) { let dV: number = secondPt.value; - let up: boolean = secondPt.value > firstPt.value; + const up: boolean = secondPt.value > firstPt.value; dV = Math.abs(dV); // calculate label let s: string = ''; @@ -446,7 +444,7 @@ export class TabBendGlyph extends Glyph { s = 'full'; dV -= 4; } else if (dV >= 4 || dV <= -4) { - let steps: number = (dV / 4) | 0; + const steps: number = (dV / 4) | 0; s += steps; // Quaters dV -= steps * 4; @@ -462,9 +460,9 @@ export class TabBendGlyph extends Glyph { } // draw label canvas.font = res.tablatureFont; - let size: number = canvas.measureText(s).width; - let y: number = startY - res.tablatureFont.size * 0.5 - 2; - let x: number = x2 - size / 2; + const size: number = canvas.measureText(s).width; + const y: number = startY - res.tablatureFont.size * 0.5 - 2; + const x: number = x2 - size / 2; canvas.fillText(s, x, y); } } @@ -479,7 +477,7 @@ export class TabBendGlyph extends Glyph { case 3: return '¾'; default: - return steps + '/ 4'; + return `${steps}/ 4`; } } } diff --git a/src/rendering/glyphs/TabBrushGlyph.ts b/src/rendering/glyphs/TabBrushGlyph.ts index 0cd371b2f..a3b950a93 100644 --- a/src/rendering/glyphs/TabBrushGlyph.ts +++ b/src/rendering/glyphs/TabBrushGlyph.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { BrushType } from '@src/model/BrushType'; import { VibratoType } from '@src/model/VibratoType'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { NoteVibratoGlyph } from '@src/rendering/glyphs/NoteVibratoGlyph'; -import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; +import type { TabBarRenderer } from '@src/rendering/TabBarRenderer'; import { NoteYPosition } from '@src/rendering/BarRendererBase'; export class TabBrushGlyph extends Glyph { @@ -20,13 +20,11 @@ export class TabBrushGlyph extends Glyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let tabBarRenderer: TabBarRenderer = this.renderer as TabBarRenderer; - let startY: number = - cy + this.x + (tabBarRenderer.getNoteY(this._beat.maxStringNote!, NoteYPosition.Top)); - let endY: number = - cy + this.y + tabBarRenderer.getNoteY(this._beat.minStringNote!, NoteYPosition.Bottom); - let arrowX: number = (cx + this.x + this.width / 2) | 0; - let arrowSize: number = 8; + const tabBarRenderer: TabBarRenderer = this.renderer as TabBarRenderer; + const startY: number = cy + this.x + tabBarRenderer.getNoteY(this._beat.maxStringNote!, NoteYPosition.Top); + const endY: number = cy + this.y + tabBarRenderer.getNoteY(this._beat.minStringNote!, NoteYPosition.Bottom); + const arrowX: number = (cx + this.x + this.width / 2) | 0; + const arrowSize: number = 8; if (this._beat.brushType !== BrushType.None) { if (this._beat.brushType === BrushType.BrushUp || this._beat.brushType === BrushType.BrushDown) { canvas.beginPath(); @@ -34,26 +32,26 @@ export class TabBrushGlyph extends Glyph { canvas.lineTo(arrowX, endY); canvas.stroke(); } else if (this._beat.brushType === BrushType.ArpeggioUp) { - let glyph: NoteVibratoGlyph = new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2, true); + const glyph: NoteVibratoGlyph = new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2, true); glyph.renderer = this.renderer; glyph.doLayout(); - - let lineStartY: number = startY; - let lineEndY: number = endY - arrowSize; + + const lineStartY: number = startY; + const lineEndY: number = endY - arrowSize; glyph.width = Math.abs(lineEndY - lineStartY); canvas.beginRotate(cx + this.x + 4, lineEndY, -90); glyph.paint(0, -glyph.height / 2, canvas); canvas.endRotate(); } else if (this._beat.brushType === BrushType.ArpeggioDown) { - let glyph: NoteVibratoGlyph = new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2, true); + const glyph: NoteVibratoGlyph = new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2, true); glyph.renderer = this.renderer; glyph.doLayout(); - let lineStartY: number = startY + arrowSize; - let lineEndY: number = endY; + const lineStartY: number = startY + arrowSize; + const lineEndY: number = endY; glyph.width = Math.abs(lineEndY - lineStartY); - + canvas.beginRotate(cx + this.x + 4, lineStartY, 90); glyph.paint(0, -glyph.height / 2, canvas); canvas.endRotate(); diff --git a/src/rendering/glyphs/TabClefGlyph.ts b/src/rendering/glyphs/TabClefGlyph.ts index 0cd77431a..b0e9a7e06 100644 --- a/src/rendering/glyphs/TabClefGlyph.ts +++ b/src/rendering/glyphs/TabClefGlyph.ts @@ -1,26 +1,22 @@ -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { BarSubElement } from '@src/model/Bar'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; export class TabClefGlyph extends Glyph { - public constructor(x: number, y: number) { - super(x, y); - } - public override doLayout(): void { - this.width = 28; + this.width = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.SixStringTabClef)!; } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let strings: number = this.renderer.bar.staff.tuning.length; - let symbol: MusicFontSymbol = strings <= 4 ? MusicFontSymbol.FourStringTabClef : MusicFontSymbol.SixStringTabClef; - let scale: number = strings <= 4 ? strings / 4.5 : strings / 6.5; - canvas.fillMusicFontSymbol( - cx + this.x + 5, - cy + this.y, - scale, - symbol, - false - ); + using _ = ElementStyleHelper.bar(canvas, BarSubElement.GuitarTabsClef, this.renderer.bar); + + const strings: number = this.renderer.bar.staff.tuning.length; + const symbol: MusicFontSymbol = + strings <= 4 ? MusicFontSymbol.FourStringTabClef : MusicFontSymbol.SixStringTabClef; + const scale: number = strings <= 4 ? strings / 4.5 : strings / 6.5; + canvas.fillMusicFontSymbol(cx + this.x + 5, cy + this.y, scale, symbol, false); } } diff --git a/src/rendering/glyphs/TabNoteChordGlyph.ts b/src/rendering/glyphs/TabNoteChordGlyph.ts index e8c3ce6b2..433cc7791 100644 --- a/src/rendering/glyphs/TabNoteChordGlyph.ts +++ b/src/rendering/glyphs/TabNoteChordGlyph.ts @@ -1,17 +1,18 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; -import { ICanvas, TextBaseline } from '@src/platform/ICanvas'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import { type ICanvas, TextBaseline } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; -import { NoteNumberGlyph } from '@src/rendering/glyphs/NoteNumberGlyph'; -import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; -import { RenderingResources } from '@src/RenderingResources'; +import type { NoteNumberGlyph } from '@src/rendering/glyphs/NoteNumberGlyph'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import type { RenderingResources } from '@src/RenderingResources'; import { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; -import { DeadSlappedBeatGlyph } from './DeadSlappedBeatGlyph'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import { DeadSlappedBeatGlyph } from '@src/rendering/glyphs/DeadSlappedBeatGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; export class TabNoteChordGlyph extends Glyph { private _notes: NoteNumberGlyph[] = []; - private _deadSlapped:DeadSlappedBeatGlyph|null = null; + private _deadSlapped: DeadSlappedBeatGlyph | null = null; private _isGrace: boolean; public beat!: Beat; @@ -34,7 +35,7 @@ export class TabNoteChordGlyph extends Glyph { public getNoteX(note: Note, requestedPosition: NoteXPosition): number { if (this.notesPerString.has(note.string)) { - let n = this.notesPerString.get(note.string)!; + const n = this.notesPerString.get(note.string)!; let pos = this.x + n.x; switch (requestedPosition) { @@ -87,7 +88,7 @@ export class TabNoteChordGlyph extends Glyph { } else { let noteStringWidth: number = 0; for (let i: number = 0, j: number = this._notes.length; i < j; i++) { - let g: NoteNumberGlyph = this._notes[i]; + const g: NoteNumberGlyph = this._notes[i]; g.renderer = this.renderer; g.doLayout(); if (g.width > w) { @@ -98,10 +99,10 @@ export class TabNoteChordGlyph extends Glyph { } } this.noteStringWidth = noteStringWidth; - let tabHeight: number = this.renderer.resources.tablatureFont.size; + const tabHeight: number = this.renderer.resources.tablatureFont.size; let effectY: number = this.getNoteY(this.minStringNote!, NoteYPosition.Center) + tabHeight / 2; // TODO: take care of actual glyph height - let effectSpacing: number = 7; + const effectSpacing: number = 7; for (const g of this.beatEffects.values()) { g.y += effectY; g.x += this.width / 2; @@ -128,20 +129,22 @@ export class TabNoteChordGlyph extends Glyph { if (this.beat.deadSlapped) { this._deadSlapped?.paint(cx, cy, canvas); - } else{ - let res: RenderingResources = this.renderer.resources; - let oldBaseLine: TextBaseline = canvas.textBaseline; + } else { + const res: RenderingResources = this.renderer.resources; + const oldBaseLine: TextBaseline = canvas.textBaseline; canvas.textBaseline = TextBaseline.Middle; canvas.font = this._isGrace ? res.graceFont : res.tablatureFont; - - let notes: NoteNumberGlyph[] = this._notes; - let w: number = this.width; - for (let g of notes) { + + const notes: NoteNumberGlyph[] = this._notes; + const w: number = this.width; + for (const g of notes) { g.renderer = this.renderer; g.width = w; g.paint(cx, cy, canvas); } canvas.textBaseline = oldBaseLine; + + using _ = ElementStyleHelper.beat(canvas, BeatSubElement.GuitarTabEffects, this.beat); for (const g of this.beatEffects.values()) { g.paint(cx, cy, canvas); } diff --git a/src/rendering/glyphs/TabRestGlyph.ts b/src/rendering/glyphs/TabRestGlyph.ts index e6b53d166..ddb0a6054 100644 --- a/src/rendering/glyphs/TabRestGlyph.ts +++ b/src/rendering/glyphs/TabRestGlyph.ts @@ -1,31 +1,31 @@ -import { Duration } from '@src/model/Duration'; -import { ICanvas } from '@src/platform/ICanvas'; +import { BeatSubElement } from '@src/model/Beat'; +import type { Duration } from '@src/model/Duration'; +import type { ICanvas } from '@src/platform/ICanvas'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { ScoreRestGlyph } from '@src/rendering/glyphs/ScoreRestGlyph'; -import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import type { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; export class TabRestGlyph extends MusicFontGlyph { private _isVisibleRest: boolean; - private _duration: Duration; public beamingHelper!: BeamingHelper; public constructor(x: number, y: number, isVisibleRest: boolean, duration: Duration) { super(x, y, 1, ScoreRestGlyph.getSymbol(duration)); this._isVisibleRest = isVisibleRest; - this._duration = duration; } public override doLayout(): void { - if (this._isVisibleRest) { - this.width = ScoreRestGlyph.getSize(this._duration); - } else { + super.doLayout(); + if (!this._isVisibleRest) { this.width = 10; } } public updateBeamingHelper(cx: number): void { if (this.beamingHelper && this.beamingHelper.isPositionFrom('tab', this.beat!)) { - this.beamingHelper.registerBeatLineX('tab', + this.beamingHelper.registerBeatLineX( + 'tab', this.beat!, cx + this.x + this.width / 2, cx + this.x + this.width / 2 @@ -35,6 +35,7 @@ export class TabRestGlyph extends MusicFontGlyph { public override paint(cx: number, cy: number, canvas: ICanvas): void { if (this._isVisibleRest) { + using _ = ElementStyleHelper.beat(canvas, BeatSubElement.GuitarTabRests, this.beat!); super.paint(cx, cy, canvas); } } diff --git a/src/rendering/glyphs/TabSlideLineGlyph.ts b/src/rendering/glyphs/TabSlideLineGlyph.ts index 0e1c9807a..880594749 100644 --- a/src/rendering/glyphs/TabSlideLineGlyph.ts +++ b/src/rendering/glyphs/TabSlideLineGlyph.ts @@ -1,14 +1,14 @@ -import { Note } from '@src/model/Note'; +import type { Note } from '@src/model/Note'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; import { VibratoType } from '@src/model/VibratoType'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase, NoteYPosition, NoteXPosition } from '@src/rendering/BarRendererBase'; +import type { ICanvas } from '@src/platform/ICanvas'; +import { type BarRendererBase, NoteYPosition, NoteXPosition } from '@src/rendering/BarRendererBase'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; -import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; +import type { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { NoteVibratoGlyph } from '@src/rendering/glyphs/NoteVibratoGlyph'; -import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; +import type { TabBarRenderer } from '@src/rendering/TabBarRenderer'; export class TabSlideLineGlyph extends Glyph { private _inType: SlideInType; @@ -34,9 +34,9 @@ export class TabSlideLineGlyph extends Glyph { } private paintSlideIn(cx: number, cy: number, canvas: ICanvas): void { - let startNoteRenderer: TabBarRenderer = this.renderer as TabBarRenderer; - let sizeX: number = 12; - let sizeY: number = 3; + const startNoteRenderer: TabBarRenderer = this.renderer as TabBarRenderer; + const sizeX: number = 12; + const sizeY: number = 3; let startX: number = 0; let startY: number = 0; let endX: number = 0; @@ -44,16 +44,32 @@ export class TabSlideLineGlyph extends Glyph { const offsetX = 2; switch (this._inType) { case SlideInType.IntoFromBelow: - endX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Left) - offsetX; + endX = + cx + + startNoteRenderer.x + + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Left) - + offsetX; endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); startX = endX - sizeX; - startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) + sizeY; + startY = + cy + + startNoteRenderer.y + + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) + + sizeY; break; case SlideInType.IntoFromAbove: - endX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Left) - offsetX; + endX = + cx + + startNoteRenderer.x + + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Left) - + offsetX; endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); startX = endX - sizeX; - startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) - sizeY; + startY = + cy + + startNoteRenderer.y + + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) - + sizeY; break; default: return; @@ -62,9 +78,9 @@ export class TabSlideLineGlyph extends Glyph { } private paintSlideOut(cx: number, cy: number, canvas: ICanvas): void { - let startNoteRenderer: TabBarRenderer = this.renderer as TabBarRenderer; - let sizeX: number = 12; - let sizeY: number = 3; + const startNoteRenderer: TabBarRenderer = this.renderer as TabBarRenderer; + const sizeX: number = 12; + const sizeY: number = 3; let startX: number = 0; let startY: number = 0; let endX: number = 0; @@ -79,14 +95,15 @@ export class TabSlideLineGlyph extends Glyph { startX = cx + startNoteRenderer.x + - startNoteRenderer.getBeatX(this._startNote.beat, BeatXPosition.PostNotes) - + offsetX; + startNoteRenderer.getBeatX(this._startNote.beat, BeatXPosition.PostNotes) + + offsetX; startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); if (this._startNote.slideTarget) { - let endNoteRenderer: BarRendererBase | null = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, - this._startNote.slideTarget.beat.voice.bar - ); + const endNoteRenderer: BarRendererBase | null = + this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff.staffId, + this._startNote.slideTarget.beat.voice.bar + ); if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { endX = cx + startNoteRenderer.x + startNoteRenderer.width; endY = startY; @@ -94,8 +111,8 @@ export class TabSlideLineGlyph extends Glyph { endX = cx + endNoteRenderer.x + - endNoteRenderer.getBeatX(this._startNote.slideTarget.beat, BeatXPosition.OnNotes) - - offsetX; + endNoteRenderer.getBeatX(this._startNote.slideTarget.beat, BeatXPosition.OnNotes) - + offsetX; endY = cy + endNoteRenderer.y + @@ -115,19 +132,39 @@ export class TabSlideLineGlyph extends Glyph { } break; case SlideOutType.OutUp: - startX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + offsetX; + startX = + cx + + startNoteRenderer.x + + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + + offsetX; startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); endX = startX + sizeX; - endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) - sizeY; + endY = + cy + + startNoteRenderer.y + + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) - + sizeY; break; case SlideOutType.OutDown: - startX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + offsetX; + startX = + cx + + startNoteRenderer.x + + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + + offsetX; startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); endX = startX + sizeX; - endY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) + sizeY; + endY = + cy + + startNoteRenderer.y + + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center) + + sizeY; break; case SlideOutType.PickSlideDown: - startX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + offsetX * 2; + startX = + cx + + startNoteRenderer.x + + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + + offsetX * 2; startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); endX = cx + startNoteRenderer.x + startNoteRenderer.width; endY = startY + sizeY * 3; @@ -143,7 +180,11 @@ export class TabSlideLineGlyph extends Glyph { waves = true; break; case SlideOutType.PickSlideUp: - startX = cx + startNoteRenderer.x + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + offsetX * 2; + startX = + cx + + startNoteRenderer.x + + startNoteRenderer.getNoteX(this._startNote, NoteXPosition.Right) + + offsetX * 2; startY = cy + startNoteRenderer.y + startNoteRenderer.getNoteY(this._startNote, NoteYPosition.Center); endX = cx + startNoteRenderer.x + startNoteRenderer.width; endY = startY - sizeY * 3; @@ -173,19 +214,19 @@ export class TabSlideLineGlyph extends Glyph { endY: number ): void { if (waves) { - let glyph: NoteVibratoGlyph = new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2); + const glyph: NoteVibratoGlyph = new NoteVibratoGlyph(0, 0, VibratoType.Slight, 1.2); glyph.renderer = this.renderer; glyph.doLayout(); startY -= glyph.height / 2; endY -= glyph.height / 2; - let b: number = endX - startX; - let a: number = endY - startY; - let c: number = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)); + const b: number = endX - startX; + const a: number = endY - startY; + const c: number = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)); glyph.width = b; - let angle: number = Math.asin(a / c) * (180 / Math.PI); + const angle: number = Math.asin(a / c) * (180 / Math.PI); canvas.beginRotate(startX, startY, angle); glyph.paint(0, 0, canvas); canvas.endRotate(); diff --git a/src/rendering/glyphs/TabSlurGlyph.ts b/src/rendering/glyphs/TabSlurGlyph.ts index 7460f1642..83e0d0e00 100644 --- a/src/rendering/glyphs/TabSlurGlyph.ts +++ b/src/rendering/glyphs/TabSlurGlyph.ts @@ -1,8 +1,8 @@ -import { Note } from '@src/model/Note'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Note } from '@src/model/Note'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { TabTieGlyph } from '@src/rendering/glyphs/TabTieGlyph'; -import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; +import type { TabBarRenderer } from '@src/rendering/TabBarRenderer'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; export class TabSlurGlyph extends TabTieGlyph { @@ -65,14 +65,14 @@ export class TabSlurGlyph extends TabTieGlyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let startNoteRenderer: BarRendererBase = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, + const startNoteRenderer: BarRendererBase = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff.staffId, this.startBeat!.voice.bar )!; - let direction: BeamDirection = this.getBeamDirection(this.startBeat!, startNoteRenderer); - let slurId: string = 'tab.slur.' + this.startNote.beat.id + '.' + this.endNote.beat.id + '.' + direction; - let renderer: TabBarRenderer = this.renderer as TabBarRenderer; - let isSlurRendered: boolean = renderer.staff.getSharedLayoutData(slurId, false); + 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/src/rendering/glyphs/TabTieGlyph.ts b/src/rendering/glyphs/TabTieGlyph.ts index 9909040e0..dfd6186cd 100644 --- a/src/rendering/glyphs/TabTieGlyph.ts +++ b/src/rendering/glyphs/TabTieGlyph.ts @@ -1,6 +1,6 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; -import { BarRendererBase, NoteYPosition, NoteXPosition } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import { type BarRendererBase, NoteYPosition, NoteXPosition } from '@src/rendering/BarRendererBase'; import { TieGlyph } from '@src/rendering/glyphs/TieGlyph'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; @@ -15,14 +15,14 @@ export class TabTieGlyph extends TieGlyph { } protected override getTieHeight(startX: number, startY: number, endX: number, endY: number): number { - if(this.startNote === this.endNote) { + if (this.startNote === this.endNote) { return 15; } return super.getTieHeight(startX, startY, endX, endY); } protected override getBeamDirection(beat: Beat, noteRenderer: BarRendererBase): BeamDirection { - if(this.startNote === this.endNote) { + if (this.startNote === this.endNote) { return BeamDirection.Up; } return TabTieGlyph.getBeamDirectionForNote(this.startNote); @@ -33,11 +33,11 @@ export class TabTieGlyph extends TieGlyph { } protected override getStartY(): number { - if(this.startNote === this.endNote) { + if (this.startNote === this.endNote) { return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Center); } - if(this.tieDirection === BeamDirection.Up) { + if (this.tieDirection === BeamDirection.Up) { return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Top); } return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Bottom); @@ -48,14 +48,14 @@ export class TabTieGlyph extends TieGlyph { } protected override getStartX(): number { - if(this.startNote === this.endNote) { + if (this.startNote === this.endNote) { return this.getEndX() - 20; } return this.startNoteRenderer!.getNoteX(this.startNote, NoteXPosition.Center); } protected override getEndX(): number { - if(this.startNote === this.endNote) { + if (this.startNote === this.endNote) { return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); } return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Center); diff --git a/src/rendering/glyphs/TabTimeSignatureGlyph.ts b/src/rendering/glyphs/TabTimeSignatureGlyph.ts index e15d4082e..833d4089a 100644 --- a/src/rendering/glyphs/TabTimeSignatureGlyph.ts +++ b/src/rendering/glyphs/TabTimeSignatureGlyph.ts @@ -1,14 +1,20 @@ import { TimeSignatureGlyph } from '@src/rendering/glyphs/TimeSignatureGlyph'; -import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; +import type { TabBarRenderer } from '@src/rendering/TabBarRenderer'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; +import { BarSubElement } from '@src/model/Bar'; export class TabTimeSignatureGlyph extends TimeSignatureGlyph { + public override doLayout(): void { + this.barSubElement = BarSubElement.GuitarTabsTimeSignature; + super.doLayout(); + } + protected get commonScale(): number { return 1; } protected get numberScale(): number { - let renderer: TabBarRenderer = this.renderer as TabBarRenderer; + const renderer: TabBarRenderer = this.renderer as TabBarRenderer; if (renderer.bar.staff.tuning.length <= 4) { return NoteHeadGlyph.GraceScale; } diff --git a/src/rendering/glyphs/TabWhammyBarGlyph.ts b/src/rendering/glyphs/TabWhammyBarGlyph.ts index 119525644..48673ca71 100644 --- a/src/rendering/glyphs/TabWhammyBarGlyph.ts +++ b/src/rendering/glyphs/TabWhammyBarGlyph.ts @@ -1,15 +1,16 @@ -import { Beat } from '@src/model/Beat'; +import { type Beat, BeatSubElement } from '@src/model/Beat'; import { BendPoint } from '@src/model/BendPoint'; import { BendStyle } from '@src/model/BendStyle'; import { WhammyType } from '@src/model/WhammyType'; import { NotationMode, NotationElement } from '@src/NotationSettings'; -import { ICanvas, TextAlign } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import { type ICanvas, TextAlign } from '@src/platform/ICanvas'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { BeatXPosition } from '@src/rendering/BeatXPosition'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { TabBendGlyph } from '@src/rendering/glyphs/TabBendGlyph'; -import { TabBarRenderer } from '@src/rendering/TabBarRenderer'; -import { RenderingResources } from '@src/RenderingResources'; +import type { TabBarRenderer } from '@src/rendering/TabBarRenderer'; +import type { RenderingResources } from '@src/RenderingResources'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; export class TabWhammyBarGlyph extends Glyph { private static readonly TopOffsetSharedDataKey: string = 'tab.whammy.topoffset'; @@ -30,7 +31,7 @@ export class TabWhammyBarGlyph extends Glyph { if (beat.whammyBarType === WhammyType.Custom) { return beat.whammyBarPoints!; } - let renderingPoints: BendPoint[] = []; + const renderingPoints: BendPoint[] = []; // Guitar Pro Rendering Note: // Last point of bend is always at end of the beat even // though it might not be 100% correct from timing perspective. @@ -80,9 +81,9 @@ export class TabWhammyBarGlyph extends Glyph { ) { topOffset += this.renderer.resources.tablatureFont.size * 2; } - let bottomOffset: number = minValue!.value < 0 ? Math.abs(this.getOffset(minValue!.value)) : 0; + const bottomOffset: number = minValue!.value < 0 ? Math.abs(this.getOffset(minValue!.value)) : 0; this.renderer.registerOverflowTop(topOffset + bottomOffset); - let currentOffset: number = this.renderer.staff.getSharedLayoutData( + const currentOffset: number = this.renderer.staff.getSharedLayoutData( TabWhammyBarGlyph.TopOffsetSharedDataKey, -1 ); @@ -96,8 +97,7 @@ export class TabWhammyBarGlyph extends Glyph { return 0; } let offset: number = - TabWhammyBarGlyph.PerHalfSize + - Math.log2(Math.abs(value) / 2) * TabWhammyBarGlyph.PerHalfSize; + TabWhammyBarGlyph.PerHalfSize + Math.log2(Math.abs(value) / 2) * TabWhammyBarGlyph.PerHalfSize; if (value < 0) { offset = -offset; } @@ -105,13 +105,15 @@ export class TabWhammyBarGlyph extends Glyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let startNoteRenderer: BarRendererBase = this.renderer; + using _ = ElementStyleHelper.beat(canvas, BeatSubElement.StandardNotationEffects, this._beat); + + const startNoteRenderer: BarRendererBase = this.renderer; let endBeat: Beat | null = this._beat.nextBeat; let endNoteRenderer: TabBarRenderer | null = null; let endXPositionType: BeatXPosition = BeatXPosition.PreNotes; if (endBeat) { endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, + this.renderer.staff.staffId, endBeat.voice.bar ) as TabBarRenderer | null; if (!endNoteRenderer || endNoteRenderer.staff !== startNoteRenderer.staff) { @@ -132,34 +134,26 @@ 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) - - 2; - endX = - cx + - startNoteRenderer.x + - startNoteRenderer.getBeatX(this._beat, BeatXPosition.PostNotes) + - 2; + startX = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.OnNotes) - 2; + endX = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.PostNotes) + 2; } else { startX = cx + startNoteRenderer.x + startNoteRenderer.getBeatX(this._beat, BeatXPosition.MiddleNotes); endX = !endNoteRenderer ? cx + startNoteRenderer.x + startNoteRenderer.width - 2 : cx + endNoteRenderer.x + endNoteRenderer.getBeatX(endBeat!, endXPositionType); } - let old: TextAlign = canvas.textAlign; + const old: TextAlign = canvas.textAlign; canvas.textAlign = TextAlign.Center; if (this._renderPoints.length >= 2) { - let dx: number = (endX - startX) / BendPoint.MaxPosition; + const dx: number = (endX - startX) / BendPoint.MaxPosition; canvas.beginPath(); - let zeroY: number = + const zeroY: number = cy + this.renderer.staff.getSharedLayoutData(TabWhammyBarGlyph.TopOffsetSharedDataKey, 0); let slurText: string = this._beat.whammyStyle === BendStyle.Gradual ? 'grad.' : ''; for (let i: number = 0, j: number = this._renderPoints.length - 1; i < j; i++) { - let firstPt: BendPoint = this._renderPoints[i]; - let secondPt: BendPoint = this._renderPoints[i + 1]; - let nextPt: BendPoint | null = i < j - 2 ? this._renderPoints[i + 2] : null; + const firstPt: BendPoint = this._renderPoints[i]; + const secondPt: BendPoint = this._renderPoints[i + 1]; + const nextPt: BendPoint | null = i < j - 2 ? this._renderPoints[i + 2] : null; let isFirst: boolean = i === 0; // draw pre-bend if previous if (i === 0 && firstPt.value !== 0 && !this._beat.isContinuedWhammy) { @@ -185,18 +179,18 @@ export class TabWhammyBarGlyph extends Glyph { canvas: ICanvas, slurText?: string ): void { - let x1: number = cx + dx * firstPt.offset; - let x2: number = cx + dx * secondPt.offset; - let y1: number = cy - this.getOffset(firstPt.value); - let y2: number = cy - this.getOffset(secondPt.value); + const x1: number = cx + dx * firstPt.offset; + const x2: number = cx + dx * secondPt.offset; + const y1: number = cy - this.getOffset(firstPt.value); + const y2: number = cy - this.getOffset(secondPt.value); if (firstPt.offset === secondPt.offset) { - let dashSize: number = TabWhammyBarGlyph.DashSize; - let dashes: number = Math.abs(y2 - y1) / (dashSize * 2); + const dashSize: number = TabWhammyBarGlyph.DashSize; + const dashes: number = Math.abs(y2 - y1) / (dashSize * 2); if (dashes < 1) { canvas.moveTo(x1, y1); canvas.lineTo(x2, y2); } else { - let dashEndY: number = Math.max(y1, y2); + const dashEndY: number = Math.max(y1, y2); let dashStartY: number = Math.min(y1, y2); while (dashEndY > dashStartY) { canvas.moveTo(x1, dashStartY); @@ -206,14 +200,14 @@ export class TabWhammyBarGlyph extends Glyph { } canvas.stroke(); } else if (firstPt.value === secondPt.value) { - let dashSize: number = TabWhammyBarGlyph.DashSize; - let dashes: number = Math.abs(x2 - x1) / (dashSize * 2); + const dashSize: number = TabWhammyBarGlyph.DashSize; + const dashes: number = Math.abs(x2 - x1) / (dashSize * 2); if (dashes < 1) { canvas.moveTo(x1, y1); canvas.lineTo(x2, y2); } else { let dashEndX: number = Math.max(x1, x2); - let dashStartX: number = Math.min(x1, x2); + const dashStartX: number = Math.min(x1, x2); while (dashEndX > dashStartX) { canvas.moveTo(dashEndX, y1); canvas.lineTo(dashEndX - dashSize, y1); @@ -225,7 +219,7 @@ export class TabWhammyBarGlyph extends Glyph { canvas.moveTo(x1, y1); canvas.lineTo(x2, y2); } - let res: RenderingResources = this.renderer.resources; + const res: RenderingResources = this.renderer.resources; if (isFirst && !this._beat.isContinuedWhammy && !this._isSimpleDip) { let y: number = y1; y -= res.tablatureFont.size + 2; @@ -239,7 +233,9 @@ export class TabWhammyBarGlyph extends Glyph { } let dV: number = Math.abs(secondPt.value); if ( - (dV !== 0 || (this.renderer.settings.notation.isNotationElementVisible(NotationElement.ZerosOnDiveWhammys) && !this._isSimpleDip)) && + (dV !== 0 || + (this.renderer.settings.notation.isNotationElementVisible(NotationElement.ZerosOnDiveWhammys) && + !this._isSimpleDip)) && firstPt.value !== secondPt.value ) { let s: string = ''; @@ -247,7 +243,7 @@ export class TabWhammyBarGlyph extends Glyph { s += '-'; } if (dV >= 4) { - let steps: number = (dV / 4) | 0; + const steps: number = (dV / 4) | 0; s += steps; // Quaters dV -= steps * 4; @@ -267,7 +263,7 @@ export class TabWhammyBarGlyph extends Glyph { y -= 2; } } - let x: number = x2; + const x: number = x2; canvas.fillText(s, x, y); } } diff --git a/src/rendering/glyphs/TextGlyph.ts b/src/rendering/glyphs/TextGlyph.ts index 826b5f275..470e3351a 100644 --- a/src/rendering/glyphs/TextGlyph.ts +++ b/src/rendering/glyphs/TextGlyph.ts @@ -1,5 +1,6 @@ -import { Font } from '@src/model/Font'; -import { ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; +import type { Color } from '@src/model/Color'; +import type { Font } from '@src/model/Font'; +import { type ICanvas, TextAlign, type TextBaseline } from '@src/platform/ICanvas'; import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; export class TextGlyph extends EffectGlyph { @@ -10,19 +11,23 @@ export class TextGlyph extends EffectGlyph { public textAlign: TextAlign; public textBaseline: TextBaseline | null; + public colorOverride?: Color; + public constructor( x: number, y: number, text: string, font: Font, textAlign: TextAlign = TextAlign.Left, - testBaseline: TextBaseline | null = null + testBaseline: TextBaseline | null = null, + color?: Color ) { super(x, y); this._lines = text.split('\n'); this.font = font; this.textAlign = textAlign; this.textBaseline = testBaseline; + this.colorOverride = color; } public override doLayout(): void { @@ -41,11 +46,11 @@ export class TextGlyph extends EffectGlyph { } public override paint(cx: number, cy: number, canvas: ICanvas): void { - let color = canvas.color; - canvas.color = color; + const color = canvas.color; + canvas.color = this.colorOverride ?? color; canvas.font = this.font; - let old = canvas.textAlign; - let oldBaseLine = canvas.textBaseline; + const old = canvas.textAlign; + const oldBaseLine = canvas.textBaseline; canvas.textAlign = this.textAlign; if (this.textBaseline !== null) { canvas.textBaseline = this.textBaseline!; @@ -57,5 +62,6 @@ export class TextGlyph extends EffectGlyph { } canvas.textAlign = old; canvas.textBaseline = oldBaseLine; + canvas.color = color; } } diff --git a/src/rendering/glyphs/TieGlyph.ts b/src/rendering/glyphs/TieGlyph.ts index 71a02901b..2f758d583 100644 --- a/src/rendering/glyphs/TieGlyph.ts +++ b/src/rendering/glyphs/TieGlyph.ts @@ -1,9 +1,9 @@ -import { Beat } from '@src/model/Beat'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { Beat } from '@src/model/Beat'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { Glyph } from '@src/rendering/glyphs/Glyph'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; -import { Bounds } from '../utils/Bounds'; +import { Bounds } from '@src/rendering/utils/Bounds'; export class TieGlyph extends Glyph { protected startBeat: Beat | null; @@ -37,13 +37,13 @@ export class TieGlyph extends Glyph { return; } - let startNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, + const startNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff.staffId, this.startBeat!.voice.bar ); this.startNoteRenderer = startNoteRenderer; - let endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( - this.renderer.staff.staveId, + const endNoteRenderer = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff.staffId, this.endBeat.voice.bar ); this.endNoteRenderer = endNoteRenderer; @@ -93,7 +93,8 @@ export class TieGlyph extends Glyph { this._tieHeight = 0; // TODO: Bend slur height to be considered? } else { this._tieHeight = this.getTieHeight(this._startX, this._startY, this._endX, this._endY); - this.height = TieGlyph.calculateActualTieHeight( + + const tieBoundingBox = TieGlyph.calculateActualTieHeight( 1, this._startX, this._startY, @@ -102,11 +103,19 @@ export class TieGlyph extends Glyph { this.tieDirection === BeamDirection.Down, this._tieHeight, 4 - ).h; - } + ); - if (this.tieDirection === BeamDirection.Up) { - this.y -= this.height; + 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; + } + } } } } @@ -258,7 +267,7 @@ export class TieGlyph extends Glyph { // normal vector let normalVectorX: number = y2 - y1; let normalVectorY: number = x2 - x1; - let length: number = Math.sqrt(normalVectorX * normalVectorX + normalVectorY * normalVectorY); + const length: number = Math.sqrt(normalVectorX * normalVectorX + normalVectorY * normalVectorY); if (down) { normalVectorX *= -1; } else { @@ -268,13 +277,13 @@ export class TieGlyph extends Glyph { normalVectorX /= length; normalVectorY /= length; // center of connection - let centerX: number = (x2 + x1) / 2; - let centerY: number = (y2 + y1) / 2; + const centerX: number = (x2 + x1) / 2; + const centerY: number = (y2 + y1) / 2; // control points - let cp1X: number = centerX + offset * normalVectorX; - let cp1Y: number = centerY + offset * normalVectorY; - let cp2X: number = centerX + (offset - size) * normalVectorX; - let cp2Y: number = centerY + (offset - size) * normalVectorY; + const cp1X: number = centerX + offset * normalVectorX; + const cp1Y: number = centerY + offset * normalVectorY; + const cp2X: number = centerX + (offset - size) * normalVectorX; + const cp2Y: number = centerY + (offset - size) * normalVectorY; return [x1, y1, cp1X, cp1Y, cp2X, cp2Y, x2, y2]; } @@ -329,7 +338,7 @@ export class TieGlyph extends Glyph { ): void { let normalVectorX: number = y2 - y1; let normalVectorY: number = x2 - x1; - let length: number = Math.sqrt(normalVectorX * normalVectorX + normalVectorY * normalVectorY); + const length: number = Math.sqrt(normalVectorX * normalVectorX + normalVectorY * normalVectorY); if (down) { normalVectorX *= -1; } else { @@ -340,22 +349,22 @@ export class TieGlyph extends Glyph { normalVectorY /= length; // center of connection // TODO: should be 1/3 - let centerX: number = (x2 + x1) / 2; - let centerY: number = (y2 + y1) / 2; + const centerX: number = (x2 + x1) / 2; + const centerY: number = (y2 + y1) / 2; let offset: number = TieGlyph.BendSlurHeight * scale; if (x2 - x1 < 20) { offset /= 2; } - let cp1X: number = centerX + offset * normalVectorX; - let cp1Y: number = centerY + offset * normalVectorY; + const cp1X: number = centerX + offset * normalVectorX; + const cp1Y: number = centerY + offset * normalVectorY; canvas.beginPath(); canvas.moveTo(x1, y1); canvas.lineTo(cp1X, cp1Y); canvas.lineTo(x2, y2); canvas.stroke(); if (slurText) { - let w: number = canvas.measureText(slurText).width; - let textOffset: number = down ? 0 : -canvas.font.size; + const w: number = canvas.measureText(slurText).width; + const textOffset: number = down ? 0 : -canvas.font.size; canvas.fillText(slurText, cp1X - w / 2, cp1Y + textOffset); } } diff --git a/src/rendering/glyphs/TimeSignatureGlyph.ts b/src/rendering/glyphs/TimeSignatureGlyph.ts index 46b3e0ce8..aea21f961 100644 --- a/src/rendering/glyphs/TimeSignatureGlyph.ts +++ b/src/rendering/glyphs/TimeSignatureGlyph.ts @@ -2,13 +2,17 @@ import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { NumberGlyph } from '@src/rendering/glyphs/NumberGlyph'; -import { GhostParenthesisGlyph } from './GhostParenthesisGlyph'; +import { GhostParenthesisGlyph } from '@src/rendering/glyphs/GhostParenthesisGlyph'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { BarSubElement } from '@src/model/Bar'; +import type { ICanvas } from '@src/platform/ICanvas'; export abstract class TimeSignatureGlyph extends GlyphGroup { private _numerator: number = 0; private _denominator: number = 0; private _isCommon: boolean; private _isFreeTime: boolean; + public barSubElement: BarSubElement = BarSubElement.StandardNotationTimeSignature; public constructor( x: number, @@ -28,6 +32,10 @@ export abstract class TimeSignatureGlyph extends GlyphGroup { protected abstract get commonScale(): number; protected abstract get numberScale(): number; + public override paint(cx: number, cy: number, canvas: ICanvas): void { + using _ = ElementStyleHelper.bar(canvas, this.barSubElement, this.renderer.bar); + super.paint(cx, cy, canvas); + } public override doLayout(): void { let x = 0; @@ -43,18 +51,16 @@ export abstract class TimeSignatureGlyph extends GlyphGroup { } if (this._isCommon && this._numerator === 2 && this._denominator === 2) { - let common: MusicFontGlyph = new MusicFontGlyph(x, 0, this.commonScale, MusicFontSymbol.TimeSigCutCommon); - common.width = 14; + const common: MusicFontGlyph = new MusicFontGlyph(x, 0, this.commonScale, MusicFontSymbol.TimeSigCutCommon); this.addGlyph(common); super.doLayout(); } else if (this._isCommon && this._numerator === 4 && this._denominator === 4) { - let common: MusicFontGlyph = new MusicFontGlyph(x, 0, this.commonScale, MusicFontSymbol.TimeSigCommon); - common.width = 14; + const common: MusicFontGlyph = new MusicFontGlyph(x, 0, this.commonScale, MusicFontSymbol.TimeSigCommon); this.addGlyph(common); super.doLayout(); } else { - let numerator: NumberGlyph = new NumberGlyph(x, -numberHeight / 2, this._numerator, this.numberScale); - let denominator: NumberGlyph = new NumberGlyph(x, numberHeight / 2, this._denominator, this.numberScale); + const numerator: NumberGlyph = new NumberGlyph(x, -numberHeight / 2, this._numerator, this.numberScale); + const denominator: NumberGlyph = new NumberGlyph(x, numberHeight / 2, this._denominator, this.numberScale); this.addGlyph(numerator); this.addGlyph(denominator); super.doLayout(); diff --git a/src/rendering/glyphs/TremoloPickingGlyph.ts b/src/rendering/glyphs/TremoloPickingGlyph.ts index ed0dfab58..ce747b27f 100644 --- a/src/rendering/glyphs/TremoloPickingGlyph.ts +++ b/src/rendering/glyphs/TremoloPickingGlyph.ts @@ -7,10 +7,6 @@ export class TremoloPickingGlyph extends MusicFontGlyph { super(x, y, 1, TremoloPickingGlyph.getSymbol(duration)); } - public override doLayout(): void { - this.width = 12; - } - private static getSymbol(duration: Duration): MusicFontSymbol { switch (duration) { case Duration.ThirtySecond: diff --git a/src/rendering/glyphs/TrillGlyph.ts b/src/rendering/glyphs/TrillGlyph.ts index cb3229d67..0ebf21337 100644 --- a/src/rendering/glyphs/TrillGlyph.ts +++ b/src/rendering/glyphs/TrillGlyph.ts @@ -1,38 +1,35 @@ -import { ICanvas } from '@src/platform/ICanvas'; -import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import type { ICanvas } from '@src/platform/ICanvas'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { RenderingResources } from '@src/RenderingResources'; +import { BeatXPosition } from '@src/rendering/BeatXPosition'; +import { GroupedEffectGlyph } from '@src/rendering/glyphs/GroupedEffectGlyph'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; -export class TrillGlyph extends EffectGlyph { +export class TrillGlyph extends GroupedEffectGlyph { public constructor(x: number, y: number) { - super(x, y); + super(BeatXPosition.EndBeat); + this.x = x; + this.y = y; } public override doLayout(): void { super.doLayout(); - this.height = this.renderer.resources.markerFont.size; + this.height = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.OrnamentTrill)! / 2; } - public override paint(cx: number, cy: number, canvas: ICanvas): void { - let res: RenderingResources = this.renderer.resources; - canvas.font = res.markerFont; - let textw: number = canvas.measureText('tr').width; - canvas.fillText('tr', cx + this.x, cy + this.y); - let startX: number = textw + 3; - let endX: number = this.width - startX; - let waveScale: number = 1.2; - let step: number = 11 * waveScale; - let loops: number = Math.max(1, (endX - startX) / step); + protected override paintGrouped(cx: number, cy: number, endX: number, canvas: ICanvas): void { + let startX: number = cx + this.x; + + canvas.fillMusicFontSymbol(startX, cy + this.y + this.height, 1, MusicFontSymbol.OrnamentTrill, true); + + startX += MusicFontSymbolSizes.Widths.get(MusicFontSymbol.OrnamentTrill)! / 2; + + const waveScale: number = 1.2; + const step: number = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.WiggleTrill)! * waveScale; + const loops: number = Math.floor((endX - startX) / step); + const loopY: number = cy + this.y + this.height * 1.37; let loopX: number = startX; - let loopY: number = cy + this.y + this.height * 1.2; for (let i: number = 0; i < loops; i++) { - canvas.fillMusicFontSymbol( - cx + this.x + loopX, - loopY, - waveScale, - MusicFontSymbol.WiggleTrill, - false - ); + canvas.fillMusicFontSymbol(loopX, loopY, waveScale, MusicFontSymbol.WiggleTrill, false); loopX += step; } } diff --git a/src/rendering/glyphs/TripletFeelGlyph.ts b/src/rendering/glyphs/TripletFeelGlyph.ts index 5dacc06b5..82ea78719 100644 --- a/src/rendering/glyphs/TripletFeelGlyph.ts +++ b/src/rendering/glyphs/TripletFeelGlyph.ts @@ -1,13 +1,13 @@ import { TripletFeel } from '@src/model/TripletFeel'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; export enum TripletFeelGlyphBarType { - Full, - PartialLeft, - PartialRight + Full = 0, + PartialLeft = 1, + PartialRight = 2 } export class TripletFeelGlyph extends EffectGlyph { @@ -29,13 +29,13 @@ export class TripletFeelGlyph extends EffectGlyph { public override paint(cx: number, cy: number, canvas: ICanvas): void { cx += this.x; cy += this.y; - let noteY: number = cy + this.height * NoteHeadGlyph.GraceScale; - let tupletY: number = noteY + 8; + const noteY: number = cy + this.height * NoteHeadGlyph.GraceScale; + const tupletY: number = noteY + 8; canvas.font = this.renderer.resources.effectFont; canvas.fillText('(', cx, cy + this.height * 0.3); - let leftNoteX: number = cx + 10; - let rightNoteX: number = cx + 40; + const leftNoteX: number = cx + 10; + const rightNoteX: number = cx + 40; let leftNoteSymbols: MusicFontSymbol[] = []; let rightAugmentationSymbols: MusicFontSymbol[] = []; @@ -102,9 +102,7 @@ export class TripletFeelGlyph extends EffectGlyph { MusicFontSymbol.TextBlackNoteFrac8thLongStem ]; - rightAugmentationSymbols = [ - MusicFontSymbol.TextAugmentationDot, - ] + rightAugmentationSymbols = [MusicFontSymbol.TextAugmentationDot]; rightNoteSymbols = [ MusicFontSymbol.TextBlackNoteLongStem, @@ -119,9 +117,7 @@ export class TripletFeelGlyph extends EffectGlyph { MusicFontSymbol.TextBlackNoteFrac16thLongStem ]; - rightAugmentationSymbols = [ - MusicFontSymbol.TextAugmentationDot, - ] + rightAugmentationSymbols = [MusicFontSymbol.TextAugmentationDot]; rightNoteSymbols = [ MusicFontSymbol.TextBlackNoteLongStem, @@ -141,7 +137,7 @@ export class TripletFeelGlyph extends EffectGlyph { MusicFontSymbol.TextBlackNoteLongStem, MusicFontSymbol.TextCont16thBeamLongStem, MusicFontSymbol.TextBlackNoteFrac8thLongStem, - MusicFontSymbol.TextAugmentationDot, + MusicFontSymbol.TextAugmentationDot ]; break; case TripletFeel.Scottish16th: @@ -155,7 +151,7 @@ export class TripletFeelGlyph extends EffectGlyph { MusicFontSymbol.TextBlackNoteLongStem, MusicFontSymbol.TextCont32ndBeamLongStem, MusicFontSymbol.TextBlackNoteFrac16thLongStem, - MusicFontSymbol.TextAugmentationDot, + MusicFontSymbol.TextAugmentationDot ]; break; } @@ -163,8 +159,14 @@ export class TripletFeelGlyph extends EffectGlyph { canvas.fillMusicFontSymbols(leftNoteX, noteY, TripletFeelGlyph.NoteScale, leftNoteSymbols, false); canvas.fillText('=', cx + 32, cy + 5); canvas.fillMusicFontSymbols(rightNoteX, noteY, TripletFeelGlyph.NoteScale, rightNoteSymbols, false); - if(rightAugmentationSymbols.length > 0) { - canvas.fillMusicFontSymbols(rightNoteX + 7, noteY, TripletFeelGlyph.NoteScale, rightAugmentationSymbols, false); + if (rightAugmentationSymbols.length > 0) { + canvas.fillMusicFontSymbols( + rightNoteX + 7, + noteY, + TripletFeelGlyph.NoteScale, + rightAugmentationSymbols, + false + ); } if (rightTupletSymbols.length > 0) { canvas.fillMusicFontSymbols(rightNoteX, tupletY, TripletFeelGlyph.TupletScale, rightTupletSymbols, false); diff --git a/src/rendering/glyphs/TuningContainerGlyph.ts b/src/rendering/glyphs/TuningContainerGlyph.ts index 6c4c01ba3..71c6fd74f 100644 --- a/src/rendering/glyphs/TuningContainerGlyph.ts +++ b/src/rendering/glyphs/TuningContainerGlyph.ts @@ -1,19 +1,8 @@ -import { Tuning } from '@src/model/Tuning'; import { TextAlign } from '@src/platform/ICanvas'; import { RowContainerGlyph } from '@src/rendering/glyphs/RowContainerGlyph'; -import { TuningGlyph } from '@src/rendering/glyphs/TuningGlyph'; export class TuningContainerGlyph extends RowContainerGlyph { public constructor(x: number, y: number) { super(x, y, TextAlign.Left); } - - public addTuning(tuning: Tuning, trackLabel: string): void { - if (tuning.tunings.length > 0) { - let tuningGlyph: TuningGlyph = new TuningGlyph(0, 0, tuning, trackLabel); - tuningGlyph.renderer = this.renderer; - tuningGlyph.doLayout(); - this.glyphs!.push(tuningGlyph); - } - } } diff --git a/src/rendering/glyphs/TuningGlyph.ts b/src/rendering/glyphs/TuningGlyph.ts index f8e45726f..5058aa935 100644 --- a/src/rendering/glyphs/TuningGlyph.ts +++ b/src/rendering/glyphs/TuningGlyph.ts @@ -1,14 +1,18 @@ import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { Tuning } from '@src/model/Tuning'; -import { ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; +import { type ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; +import type { Color } from '@src/model/Color'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; export class TuningGlyph extends GlyphGroup { private _tuning: Tuning; private _trackLabel: string; + public colorOverride?: Color; + public constructor(x: number, y: number, tuning: Tuning, trackLabel: string) { super(x, y); this._tuning = tuning; @@ -29,13 +33,14 @@ export class TuningGlyph extends GlyphGroup { public override paint(cx: number, cy: number, canvas: ICanvas): void { canvas.textBaseline = TextBaseline.Middle; + const c = canvas.color; + if (this.colorOverride) { + canvas.color = this.colorOverride!; + } super.paint(cx, cy, canvas); + canvas.color = c; } - /** - * The height of the GuitarString# glyphs at scale 1 - */ - public static readonly CircleNumberHeight: number = 20; public static readonly CircleNumberScale: number = 0.7; private createGlyphs(tuning: Tuning): void { @@ -43,13 +48,16 @@ export class TuningGlyph extends GlyphGroup { this.height = 0; const rowHeight = 15; + const textPadding = 1; // Track name if (this._trackLabel.length > 0) { + this.height += textPadding; + const trackName = new TextGlyph(0, this.height, this._trackLabel, res.effectFont, TextAlign.Left); trackName.renderer = this.renderer; trackName.doLayout(); - this.height += trackName.height; + this.height += trackName.height + textPadding; trackName.y += trackName.height / 2; this.addGlyph(trackName); } @@ -73,17 +81,17 @@ export class TuningGlyph extends GlyphGroup { if (!tuning.isStandard) { this.height += rowHeight; const circleScale = TuningGlyph.CircleNumberScale; - const circleHeight = TuningGlyph.CircleNumberHeight * circleScale; + const circleHeight = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.GuitarString0)! * circleScale; // Strings - let stringsPerColumn: number = Math.ceil(tuning.tunings.length / 2.0) | 0; + const stringsPerColumn: number = Math.ceil(tuning.tunings.length / 2.0) | 0; let currentX: number = 0; let currentY: number = this.height; for (let i: number = 0, j: number = tuning.tunings.length; i < j; i++) { const symbol = ((MusicFontSymbol.GuitarString0 as number) + (i + 1)) as MusicFontSymbol; this.addGlyph(new MusicFontGlyph(currentX, currentY + circleHeight / 2.5, circleScale, symbol)); - const str: string = '= ' + Tuning.getTextForTuning(tuning.tunings[i], false); + const str: string = `= ${Tuning.getTextForTuning(tuning.tunings[i], false)}`; this.addGlyph( new TextGlyph(currentX + circleHeight + 1, currentY, str, res.effectFont, TextAlign.Left) ); diff --git a/src/rendering/glyphs/VoiceContainerGlyph.ts b/src/rendering/glyphs/VoiceContainerGlyph.ts index eb90b98c3..f222d7baa 100644 --- a/src/rendering/glyphs/VoiceContainerGlyph.ts +++ b/src/rendering/glyphs/VoiceContainerGlyph.ts @@ -1,11 +1,12 @@ import { GraceType } from '@src/model/GraceType'; -import { TupletGroup } from '@src/model/TupletGroup'; -import { Voice } from '@src/model/Voice'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { TupletGroup } from '@src/model/TupletGroup'; +import { type Voice, VoiceSubElement } from '@src/model/Voice'; +import type { ICanvas } from '@src/platform/ICanvas'; import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; -import { Glyph } from '@src/rendering/glyphs/Glyph'; +import type { Glyph } from '@src/rendering/glyphs/Glyph'; import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; -import { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; +import type { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; /** * This glyph acts as container for handling @@ -26,17 +27,17 @@ export class VoiceContainerGlyph extends GlyphGroup { } public scaleToWidth(width: number): void { - let force: number = this.renderer.layoutingInfo.spaceToForce(width); + const force: number = this.renderer.layoutingInfo.spaceToForce(width); this.scaleToForce(force); } private scaleToForce(force: number): void { this.width = this.renderer.layoutingInfo.calculateVoiceWidth(force); - let positions: Map = this.renderer.layoutingInfo.buildOnTimePositions(force); - let beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; + 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++) { - let currentBeatGlyph: BeatContainerGlyph = beatGlyphs[i]; + const currentBeatGlyph: BeatContainerGlyph = beatGlyphs[i]; switch (currentBeatGlyph.beat.graceType) { case GraceType.None: @@ -50,7 +51,7 @@ export class VoiceContainerGlyph extends GlyphGroup { if (currentBeatGlyph.beat.graceGroup!.isComplete && positions.has(graceDisplayStart)) { currentBeatGlyph.x = positions.get(graceDisplayStart)! - currentBeatGlyph.onTimeX; - let graceSprings = this.renderer.layoutingInfo.allGraceRods.get(graceGroupId)!; + 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). @@ -75,7 +76,7 @@ export class VoiceContainerGlyph extends GlyphGroup { currentBeatGlyph.x -= lastGraceSpring.graceBeatWidth; } else { // placement for improper grace beats where no beat in the same bar follows - let graceSpring = this.renderer.layoutingInfo.incompleteGraceRods.get(graceGroupId)!; + const graceSpring = this.renderer.layoutingInfo.incompleteGraceRods.get(graceGroupId)!; const relativeOffset = graceSpring[currentBeatGlyph.beat.graceIndex].postSpringWidth - graceSpring[currentBeatGlyph.beat.graceIndex].preSpringWidth; @@ -104,34 +105,34 @@ export class VoiceContainerGlyph extends GlyphGroup { // size always previous glyph after we know the position // of the next glyph if (i > 0) { - let beatWidth: number = currentBeatGlyph.x - beatGlyphs[i - 1].x; + 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) { - let beatWidth: number = this.width - beatGlyphs[beatGlyphs.length - 1].x; + const beatWidth: number = this.width - beatGlyphs[beatGlyphs.length - 1].x; currentBeatGlyph.scaleToWidth(beatWidth); } } } public registerLayoutingInfo(info: BarLayoutingInfo): void { - let beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; - for (let b of beatGlyphs) { + const beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; + for (const b of beatGlyphs) { b.registerLayoutingInfo(info); } } public applyLayoutingInfo(info: BarLayoutingInfo): void { - let beatGlyphs: BeatContainerGlyph[] = this.beatGlyphs; - for (let b of beatGlyphs) { + 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 { - let bg: BeatContainerGlyph = g as BeatContainerGlyph; + const bg: BeatContainerGlyph = g as BeatContainerGlyph; g.x = this.beatGlyphs.length === 0 ? 0 @@ -150,10 +151,8 @@ export class VoiceContainerGlyph extends GlyphGroup { 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); - canvas.color = - this.voice.index === 0 - ? this.renderer.resources.mainGlyphColor - : this.renderer.resources.secondaryGlyphColor; + 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/src/rendering/glyphs/WahPedalGlyph.ts b/src/rendering/glyphs/WahPedalGlyph.ts index 02d6f8d31..56b07ecb3 100644 --- a/src/rendering/glyphs/WahPedalGlyph.ts +++ b/src/rendering/glyphs/WahPedalGlyph.ts @@ -1,7 +1,7 @@ import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { WahPedal } from '@src/model/WahPedal'; -import { MusicFontGlyph } from './MusicFontGlyph'; -import { ICanvas } from '@src/platform'; +import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; +import type { ICanvas } from '@src/platform/ICanvas'; export class WahPedalGlyph extends MusicFontGlyph { public constructor(wahPedal: WahPedal) { @@ -18,11 +18,6 @@ export class WahPedalGlyph extends MusicFontGlyph { return MusicFontSymbol.None; } - public override doLayout(): void { - this.width = 11; - this.height = 11; - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { super.paint(cx, cy + this.height, canvas); } diff --git a/src/rendering/layout/HorizontalScreenLayout.ts b/src/rendering/layout/HorizontalScreenLayout.ts index fddf8d89c..bbd00cdf1 100644 --- a/src/rendering/layout/HorizontalScreenLayout.ts +++ b/src/rendering/layout/HorizontalScreenLayout.ts @@ -1,10 +1,9 @@ -import { MasterBar } from '@src/model/MasterBar'; -import { Score } from '@src/model/Score'; +import type { MasterBar } from '@src/model/MasterBar'; +import type { Score } from '@src/model/Score'; import { TextAlign } from '@src/platform/ICanvas'; import { InternalSystemsLayoutMode, ScoreLayout } from '@src/rendering/layout/ScoreLayout'; import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { StaffSystem } from '@src/rendering/staves/StaffSystem'; +import type { StaffSystem } from '@src/rendering/staves/StaffSystem'; import { Logger } from '@src/Logger'; import { SystemsLayoutMode } from '@src/DisplaySettings'; @@ -19,22 +18,17 @@ export class HorizontalScreenLayoutPartialInfo { */ export class HorizontalScreenLayout extends ScoreLayout { private _system: StaffSystem | null = null; - private _pagePadding: number[] | null = null; public get name(): string { return 'HorizontalScreen'; } - public constructor(renderer: ScoreRenderer) { - super(renderer); - } - public get supportsResize(): boolean { return false; } public get firstBarX(): number { - let x = this._pagePadding![0]; + let x = this.pagePadding![0]; if (this._system) { x += this._system.accoladeWidth; } @@ -46,8 +40,6 @@ export class HorizontalScreenLayout extends ScoreLayout { } protected doLayoutAndRender(): void { - this._pagePadding = this.renderer.settings.display.padding; - switch (this.renderer.settings.display.systemsLayoutMode) { case SystemsLayoutMode.Automatic: this.systemsLayoutMode = InternalSystemsLayoutMode.Automatic; @@ -57,25 +49,7 @@ export class HorizontalScreenLayout extends ScoreLayout { break; } - if (!this._pagePadding) { - this._pagePadding = [0, 0, 0, 0]; - } - if (this._pagePadding.length === 1) { - this._pagePadding = [ - this._pagePadding[0], - this._pagePadding[0], - this._pagePadding[0], - this._pagePadding[0] - ]; - } else if (this._pagePadding.length === 2) { - this._pagePadding = [ - this._pagePadding[0], - this._pagePadding[1], - this._pagePadding[0], - this._pagePadding[1] - ]; - } - let score: Score = this.renderer.score!; + const score: Score = this.renderer.score!; let startIndex: number = this.renderer.settings.display.startBar; startIndex--; // map to array index @@ -91,62 +65,65 @@ export class HorizontalScreenLayout extends ScoreLayout { endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex)); this._system = this.createEmptyStaffSystem(); this._system.isLast = true; - this._system.x = this._pagePadding[0]; - this._system.y = this._pagePadding[1]; - let countPerPartial: number = this.renderer.settings.display.barCountPerPartial; - let partials: HorizontalScreenLayoutPartialInfo[] = []; + 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) { - let result = this._system.addBars(this.renderer.tracks!, currentBarIndex); - - if (result) { - // 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) { - let 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; + const multiBarRestInfo = this.multiBarRestInfo; + const additionalMultiBarsRestBarIndices: number[] | null = + multiBarRestInfo !== null && multiBarRestInfo.has(currentBarIndex) + ? multiBarRestInfo.get(currentBarIndex)! + : null; + + const result = this._system.addBars( + this.renderer.tracks!, + currentBarIndex, + 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; } } + 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]; + 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, + `Finished partial from bar ${currentPartial.masterBars[0].index} to ${currentPartial.masterBars[currentPartial.masterBars.length - 1].index}`, null ); } @@ -155,8 +132,7 @@ export class HorizontalScreenLayout extends ScoreLayout { const scale = this.renderer.settings.display.scale; this.height = Math.floor(this._system.y + this._system.height) * scale; - this.width = - (this._system.x + this._system.width + this._pagePadding[2]) * scale; + this.width = (this._system.x + this._system.width + this.pagePadding![2]) * scale; currentBarIndex = 0; let x = 0; @@ -189,13 +165,10 @@ export class HorizontalScreenLayout extends ScoreLayout { canvas.textAlign = TextAlign.Left; Logger.debug( this.name, - 'Rendering partial from bar ' + - partial.masterBars[0].index + - ' to ' + - partial.masterBars[partial.masterBars.length - 1].index, + `Rendering partial from bar ${partial.masterBars[0].index} to ${partial.masterBars[partial.masterBars.length - 1].index}`, null ); - this._system!!.paintPartial( + this._system!.paintPartial( -renderX, this._system!.y, canvas, @@ -207,7 +180,10 @@ export class HorizontalScreenLayout extends ScoreLayout { currentBarIndex += partial.masterBars.length; } - this.height = this.layoutAndRenderAnnotation(this.height) + this._pagePadding[3]; + this.height = this.layoutAndRenderBottomScoreInfo(this.height); + this.height = this.layoutAndRenderAnnotation(this.height); + + this.height += this.pagePadding![3]; } private finalizeStaffSystem() { diff --git a/src/rendering/layout/PageViewLayout.ts b/src/rendering/layout/PageViewLayout.ts index ee91c8317..d97eb0699 100644 --- a/src/rendering/layout/PageViewLayout.ts +++ b/src/rendering/layout/PageViewLayout.ts @@ -1,14 +1,13 @@ -import { ICanvas, TextAlign } from '@src/platform/ICanvas'; -import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; +import { type ICanvas, TextAlign } from '@src/platform/ICanvas'; +import type { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; import { InternalSystemsLayoutMode, ScoreLayout } from '@src/rendering/layout/ScoreLayout'; import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { MasterBarsRenderers } from '@src/rendering/staves/MasterBarsRenderers'; -import { StaffSystem } from '@src/rendering/staves/StaffSystem'; -import { RenderingResources } from '@src/RenderingResources'; +import type { MasterBarsRenderers } from '@src/rendering/staves/MasterBarsRenderers'; +import type { StaffSystem } from '@src/rendering/staves/StaffSystem'; +import type { RenderingResources } from '@src/RenderingResources'; import { Logger } from '@src/Logger'; -import { NotationElement } from '@src/NotationSettings'; import { SystemsLayoutMode } from '@src/DisplaySettings'; +import { ScoreSubElement } from '@src/model/Score'; /** * This layout arranges the bars into a fixed width and dynamic height region. @@ -17,16 +16,11 @@ export class PageViewLayout extends ScoreLayout { private _systems: StaffSystem[] = []; private _allMasterBarRenderers: MasterBarsRenderers[] = []; private _barsFromPreviousSystem: MasterBarsRenderers[] = []; - private _pagePadding: number[] | null = null; public get name(): string { return 'PageView'; } - public constructor(renderer: ScoreRenderer) { - super(renderer); - } - protected doLayoutAndRender(): void { switch (this.renderer.settings.display.systemsLayoutMode) { case SystemsLayoutMode.Automatic: @@ -37,26 +31,7 @@ export class PageViewLayout extends ScoreLayout { break; } - this._pagePadding = this.renderer.settings.display.padding.map(p => p / this.renderer.settings.display.scale); - if (!this._pagePadding) { - this._pagePadding = [0, 0, 0, 0]; - } - if (this._pagePadding.length === 1) { - this._pagePadding = [ - this._pagePadding[0], - this._pagePadding[0], - this._pagePadding[0], - this._pagePadding[0] - ]; - } else if (this._pagePadding.length === 2) { - this._pagePadding = [ - this._pagePadding[0], - this._pagePadding[1], - this._pagePadding[0], - this._pagePadding[1] - ]; - } - let y: number = this._pagePadding[1]; + let y: number = this.pagePadding![1]; this.width = this.renderer.width; this._allMasterBarRenderers = []; // @@ -72,9 +47,11 @@ export class PageViewLayout extends ScoreLayout { // 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; + this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; } public get supportsResize(): boolean { @@ -82,7 +59,7 @@ export class PageViewLayout extends ScoreLayout { } public get firstBarX(): number { - let x = this._pagePadding![0]; + let x = this.pagePadding![0]; if (this._systems.length > 0) { x += this._systems[0].accoladeWidth; } @@ -90,9 +67,9 @@ export class PageViewLayout extends ScoreLayout { } public doResize(): void { - let y: number = this._pagePadding![1]; + let y: number = this.pagePadding![1]; this.width = this.renderer.width; - let oldHeight: number = this.height; + const oldHeight: number = this.height; // // 1. Score Info y = this.layoutAndRenderScoreInfo(y, oldHeight); @@ -106,9 +83,11 @@ export class PageViewLayout extends ScoreLayout { // 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; + this.height = (y + this.pagePadding![3]) * this.renderer.settings.display.scale; } private layoutAndRenderTunings(y: number, totalHeight: number = -1): number { @@ -116,12 +95,12 @@ export class PageViewLayout extends ScoreLayout { return y; } - let res: RenderingResources = this.renderer.settings.display.resources; - this.tuningGlyph.x = this._pagePadding![0]; + const res: RenderingResources = this.renderer.settings.display.resources; + this.tuningGlyph.x = this.pagePadding![0]; this.tuningGlyph.width = this.scaledWidth; this.tuningGlyph.doLayout(); - let tuningHeight = this.tuningGlyph.height; + const tuningHeight = Math.round(this.tuningGlyph.height); const e = new RenderFinishedEventArgs(); e.x = 0; @@ -148,7 +127,7 @@ export class PageViewLayout extends ScoreLayout { this.chordDiagrams.width = this.scaledWidth; this.chordDiagrams.doLayout(); - const diagramHeight = Math.floor(this.chordDiagrams.height); + const diagramHeight = Math.round(this.chordDiagrams.height); const e = new RenderFinishedEventArgs(); e.x = 0; @@ -176,46 +155,35 @@ export class PageViewLayout extends ScoreLayout { let infoHeight = 0; - let res: RenderingResources = this.renderer.settings.display.resources; - let centeredGlyphs: NotationElement[] = [ - NotationElement.ScoreTitle, - NotationElement.ScoreSubTitle, - NotationElement.ScoreArtist, - NotationElement.ScoreAlbum, - NotationElement.ScoreWordsAndMusic - ]; - for (let i: number = 0; i < centeredGlyphs.length; i++) { - if (this.scoreInfoGlyphs.has(centeredGlyphs[i])) { - let glyph: TextGlyph = this.scoreInfoGlyphs.get(centeredGlyphs[i])!; - glyph.x = this.scaledWidth / 2; + 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; - glyph.textAlign = TextAlign.Center; - infoHeight += glyph.font.size; + 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); } } - let musicOrWords: boolean = false; - let musicOrWordsHeight: number = 0; - if (this.scoreInfoGlyphs.has(NotationElement.ScoreMusic)) { - let glyph: TextGlyph = this.scoreInfoGlyphs.get(NotationElement.ScoreMusic)!; - glyph.x = this.scaledWidth - this._pagePadding![2]; - glyph.y = infoHeight; - glyph.textAlign = TextAlign.Right; - musicOrWords = true; - musicOrWordsHeight = glyph.font.size; - } - if (this.scoreInfoGlyphs.has(NotationElement.ScoreWords)) { - let glyph: TextGlyph = this.scoreInfoGlyphs.get(NotationElement.ScoreWords)!; - glyph.x = this._pagePadding![0]; - glyph.y = infoHeight; - glyph.textAlign = TextAlign.Left; - musicOrWords = true; - musicOrWordsHeight = glyph.font.size; - } - if (musicOrWords) { - infoHeight += musicOrWordsHeight; - } - if (this.scoreInfoGlyphs.size > 0) { + if (scoreInfoGlyphs.length > 0) { infoHeight = Math.floor(infoHeight + 17); e.width = this.scaledWidth; e.height = infoHeight; @@ -224,7 +192,7 @@ export class PageViewLayout extends ScoreLayout { this.registerPartial(e, (canvas: ICanvas) => { canvas.color = res.scoreInfoColor; canvas.textAlign = TextAlign.Center; - for (const g of this.scoreInfoGlyphs.values()) { + for (const g of scoreInfoGlyphs) { g.paint(0, 0, canvas); } }); @@ -237,21 +205,21 @@ export class PageViewLayout extends ScoreLayout { // 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; + this.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithScale; if (barsPerRowActive) { for (let i: number = 0; i < this._systems.length; i++) { - let system: StaffSystem = this._systems[i]; + const system: StaffSystem = this._systems[i]; this.fitSystem(system); y += this.paintSystem(system, oldHeight); } } else { this._systems = []; let currentIndex: number = 0; - let maxWidth: number = this.maxWidth; + const maxWidth: number = this.maxWidth; let system: StaffSystem = this.createEmptyStaffSystem(); system.index = this._systems.length; - system.x = this._pagePadding![0]; + 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 @@ -277,7 +245,7 @@ export class PageViewLayout extends ScoreLayout { // 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.x = this.pagePadding![0]; system.y = y; } } @@ -290,22 +258,23 @@ export class PageViewLayout extends ScoreLayout { } private layoutAndRenderScore(y: number): number { - let startIndex: number = this.firstBarIndex; + const startIndex: number = this.firstBarIndex; let currentBarIndex: number = startIndex; - let endBarIndex: number = this.lastBarIndex; + const endBarIndex: number = this.lastBarIndex; + this._systems = []; while (currentBarIndex <= endBarIndex) { // create system and align set proper coordinates - let system: StaffSystem = this.createStaffSystem(currentBarIndex, endBarIndex); + const system: StaffSystem = this.createStaffSystem(currentBarIndex, endBarIndex); this._systems.push(system); - system.x = this._pagePadding![0]; + 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, + `Rendering partial from bar ${system.firstBarIndex} to ${system.lastBarIndex}`, null ); y += this.paintSystem(system, y); @@ -315,7 +284,7 @@ export class PageViewLayout extends ScoreLayout { private paintSystem(system: StaffSystem, totalHeight: number): number { // paint into canvas - let height: number = Math.floor(system.height); + const height: number = Math.floor(system.height); const args: RenderFinishedEventArgs = new RenderFinishedEventArgs(); args.x = 0; @@ -355,7 +324,7 @@ export class PageViewLayout extends ScoreLayout { private getBarsPerSystem(rowIndex: number) { let barsPerRow: number = this.renderer.settings.display.barsPerRow; - if (this.systemsLayoutMode == InternalSystemsLayoutMode.FromModelWithScale) { + if (this.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithScale) { let defaultSystemsLayout: number; let systemsLayout: number[]; if (this.renderer.tracks!.length > 1) { @@ -374,24 +343,29 @@ export class PageViewLayout extends ScoreLayout { } private createStaffSystem(currentBarIndex: number, endIndex: number): StaffSystem { - let system: StaffSystem = this.createEmptyStaffSystem(); + const system: StaffSystem = this.createEmptyStaffSystem(); system.index = this._systems.length; - let barsPerRow: number = this.getBarsPerSystem(system.index); - let maxWidth: number = this.maxWidth; - let end: number = endIndex + 1; + 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 (let renderer of this._barsFromPreviousSystem) { + for (const renderer of this._barsFromPreviousSystem) { system.addMasterBarRenderers(this.renderer.tracks!, renderer); - barIndex = renderer.masterBar.index; + barIndex = renderer.lastMasterBarIndex; } } else { - let renderers: MasterBarsRenderers | null = system.addBars(this.renderer.tracks!, barIndex); - if (renderers) { - this._allMasterBarRenderers.push(renderers); - } + 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; @@ -417,6 +391,22 @@ export class PageViewLayout extends ScoreLayout { 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++; } @@ -425,6 +415,6 @@ export class PageViewLayout extends ScoreLayout { } private get maxWidth(): number { - return (this.scaledWidth - this._pagePadding![0] - this._pagePadding![2]) + return this.scaledWidth - this.pagePadding![0] - this.pagePadding![2]; } } diff --git a/src/rendering/layout/ScoreLayout.ts b/src/rendering/layout/ScoreLayout.ts index 9a91a32a4..cad5667c3 100644 --- a/src/rendering/layout/ScoreLayout.ts +++ b/src/rendering/layout/ScoreLayout.ts @@ -1,23 +1,28 @@ import { Environment } from '@src/Environment'; -import { Bar } from '@src/model/Bar'; +import type { Bar } from '@src/model/Bar'; import { Font, FontStyle, FontWeight } from '@src/model/Font'; -import { Score } from '@src/model/Score'; -import { Staff } from '@src/model/Staff'; -import { Track } from '@src/model/Track'; -import { ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; +import { type Score, ScoreStyle, ScoreSubElement } from '@src/model/Score'; +import type { Staff } from '@src/model/Staff'; +import { type Track, TrackSubElement } from '@src/model/Track'; +import { type ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; import { BarRendererBase } from '@src/rendering/BarRendererBase'; -import { BarRendererFactory } from '@src/rendering/BarRendererFactory'; +import type { BarRendererFactory } from '@src/rendering/BarRendererFactory'; import { ChordDiagramContainerGlyph } from '@src/rendering/glyphs/ChordDiagramContainerGlyph'; import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import type { ScoreRenderer } from '@src/rendering/ScoreRenderer'; import { RenderStaff } from '@src/rendering/staves/RenderStaff'; import { StaffSystem } from '@src/rendering/staves/StaffSystem'; -import { RenderingResources } from '@src/RenderingResources'; +import type { RenderingResources } from '@src/RenderingResources'; import { Logger } from '@src/Logger'; -import { EventEmitterOfT } from '@src/EventEmitter'; -import { NotationSettings, NotationElement } from '@src/NotationSettings'; +import type { EventEmitterOfT } from '@src/EventEmitter'; +import { NotationElement } from '@src/NotationSettings'; import { TuningContainerGlyph } from '@src/rendering/glyphs/TuningContainerGlyph'; +import { ModelUtils } from '@src/model/ModelUtils'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { TuningGlyph } from '@src/rendering/glyphs/TuningGlyph'; +import type { Settings } from '@src/Settings'; +import { Lazy } from '@src/util/Lazy'; class LazyPartial { public args: RenderFinishedEventArgs; @@ -35,17 +40,17 @@ export enum InternalSystemsLayoutMode { /** * Use the automatic alignment system provided by alphaTab (default) */ - Automatic, + Automatic = 0, /** * Use the relative scaling information stored in the score model. */ - FromModelWithScale, + FromModelWithScale = 1, /** * Use the absolute size information stored in the score model. */ - FromModelWithWidths + FromModelWithWidths = 2 } /** @@ -54,23 +59,28 @@ export enum InternalSystemsLayoutMode { export abstract class ScoreLayout { private _barRendererLookup: Map> = new Map(); + protected pagePadding: number[] | null = null; + public abstract get name(): string; public renderer: ScoreRenderer; public width: number = 0; public height: number = 0; + public multiBarRestInfo: Map | null = null; + public get scaledWidth() { - return this.width / this.renderer.settings.display.scale; + return Math.round(this.width / this.renderer.settings.display.scale); } - protected scoreInfoGlyphs: Map = new Map(); + protected headerGlyphs: Map = new Map(); + protected footerGlyphs: Map = new Map(); protected chordDiagrams: ChordDiagramContainerGlyph | null = null; protected tuningGlyph: TuningContainerGlyph | null = null; public systemsLayoutMode: InternalSystemsLayoutMode = InternalSystemsLayoutMode.Automatic; - protected constructor(renderer: ScoreRenderer) { + public constructor(renderer: ScoreRenderer) { this.renderer = renderer; } @@ -86,20 +96,26 @@ export abstract class ScoreLayout { public layoutAndRender(): void { this._lazyPartials.clear(); - let score: Score = this.renderer.score!; - let startIndex: number = this.renderer.settings.display.startBar; - startIndex--; // map to array index + const score: Score = this.renderer.score!; - startIndex = Math.min(score.masterBars.length - 1, Math.max(0, startIndex)); - this.firstBarIndex = startIndex; - let endBarIndex: number = this.renderer.settings.display.barCount; - if (endBarIndex < 0) { - endBarIndex = score.masterBars.length; + this.firstBarIndex = ModelUtils.computeFirstDisplayedBarIndex(score, this.renderer.settings); + this.lastBarIndex = ModelUtils.computeLastDisplayedBarIndex(score, this.renderer.settings, this.firstBarIndex); + this.multiBarRestInfo = ModelUtils.buildMultiBarRestInfo( + this.renderer.tracks, + this.firstBarIndex, + this.lastBarIndex + ); + + this.pagePadding = this.renderer.settings.display.padding.map(p => p / this.renderer.settings.display.scale); + if (!this.pagePadding) { + this.pagePadding = [0, 0, 0, 0]; + } + if (this.pagePadding.length === 1) { + this.pagePadding = [this.pagePadding[0], this.pagePadding[0], this.pagePadding[0], this.pagePadding[0]]; + } else if (this.pagePadding.length === 2) { + this.pagePadding = [this.pagePadding[0], this.pagePadding[1], this.pagePadding[0], this.pagePadding[1]]; } - endBarIndex = startIndex + endBarIndex - 1; // map count to array index - endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex)); - this.lastBarIndex = endBarIndex; this.createScoreInfoGlyphs(); this.doLayoutAndRender(); } @@ -107,7 +123,7 @@ export abstract class ScoreLayout { private _lazyPartials: Map = new Map(); protected registerPartial(args: RenderFinishedEventArgs, callback: (canvas: ICanvas) => void) { - if (args.height == 0) { + if (args.height === 0) { return; } @@ -147,71 +163,119 @@ export abstract class ScoreLayout { protected abstract doLayoutAndRender(): void; - private createScoreInfoGlyphs(): void { - Logger.debug('ScoreLayout', 'Creating score info glyphs'); - let notation: NotationSettings = this.renderer.settings.notation; - let score: Score = this.renderer.score!; - let res: RenderingResources = this.renderer.settings.display.resources; - this.scoreInfoGlyphs = new Map(); - if (score.title && notation.isNotationElementVisible(NotationElement.ScoreTitle)) { - this.scoreInfoGlyphs.set( - NotationElement.ScoreTitle, - new TextGlyph(0, 0, score.title, res.titleFont, TextAlign.Center) - ); - } - if (score.subTitle && notation.isNotationElementVisible(NotationElement.ScoreSubTitle)) { - this.scoreInfoGlyphs.set( - NotationElement.ScoreSubTitle, - new TextGlyph(0, 0, score.subTitle, res.subTitleFont, TextAlign.Center) - ); + protected static HeaderElements: Lazy> = new Lazy( + () => + new Map([ + [ScoreSubElement.Title, NotationElement.ScoreTitle], + [ScoreSubElement.SubTitle, NotationElement.ScoreSubTitle], + [ScoreSubElement.Artist, NotationElement.ScoreArtist], + [ScoreSubElement.Album, NotationElement.ScoreAlbum], + [ScoreSubElement.Words, NotationElement.ScoreWords], + [ScoreSubElement.Music, NotationElement.ScoreMusic], + [ScoreSubElement.WordsAndMusic, NotationElement.ScoreWordsAndMusic], + [ScoreSubElement.Transcriber, undefined] + ]) + ); + protected static FooterElements: Lazy> = new Lazy( + () => + new Map([ + [ScoreSubElement.Copyright, NotationElement.ScoreCopyright], + [ScoreSubElement.CopyrightSecondLine, undefined] + ]) + ); + + private createHeaderFooterGlyph( + settings: Settings, + score: Score, + element: ScoreSubElement, + notationElement: NotationElement | undefined + ): TextGlyph | undefined { + // special case 1: Words and Music combination + // special case 2: Copyright2 without Copyright 1 (Assuming order here) + switch (element) { + case ScoreSubElement.WordsAndMusic: + if (score.words !== score.music) { + return undefined; + } + break; + case ScoreSubElement.Words: + case ScoreSubElement.Music: + if (score.words === score.music) { + return undefined; + } + break; + case ScoreSubElement.CopyrightSecondLine: + if (!this.headerGlyphs.has(ScoreSubElement.Copyright)) { + return undefined; + } + break; } - if (score.artist && notation.isNotationElementVisible(NotationElement.ScoreArtist)) { - this.scoreInfoGlyphs.set( - NotationElement.ScoreArtist, - new TextGlyph(0, 0, score.artist, res.subTitleFont, TextAlign.Center) - ); + + const res = settings.display.resources; + const notation = settings.notation; + + const hasStyle = score.style && score.style!.headerAndFooter.has(element); + const style = hasStyle + ? score.style!.headerAndFooter.get(element)! + : ScoreStyle.defaultHeaderAndFooter.get(element)!; + + let isVisible = style.isVisible !== undefined ? style.isVisible! : true; + if (notationElement !== undefined) { + isVisible = notation.isNotationElementVisible(notationElement); } - if (score.album && notation.isNotationElementVisible(NotationElement.ScoreAlbum)) { - this.scoreInfoGlyphs.set( - NotationElement.ScoreAlbum, - new TextGlyph(0, 0, score.album, res.subTitleFont, TextAlign.Center) - ); + + if (!isVisible) { + return undefined; } - if ( - score.music && - score.music === score.words && - notation.isNotationElementVisible(NotationElement.ScoreWordsAndMusic) - ) { - this.scoreInfoGlyphs.set( - NotationElement.ScoreWordsAndMusic, - new TextGlyph(0, 0, 'Music and Words by ' + score.words, res.wordsFont, TextAlign.Center) - ); - } else { - if (score.music && notation.isNotationElementVisible(NotationElement.ScoreMusic)) { - this.scoreInfoGlyphs.set( - NotationElement.ScoreMusic, - new TextGlyph(0, 0, 'Music by ' + score.music, res.wordsFont, TextAlign.Right) - ); - } - if (score.words && notation.isNotationElementVisible(NotationElement.ScoreWords)) { - this.scoreInfoGlyphs.set( - NotationElement.ScoreWords, - new TextGlyph(0, 0, 'Words by ' + score.words, res.wordsFont, TextAlign.Left) - ); - } + + const text = style.buildText(score); + if (!text) { + return undefined; } + return new TextGlyph( + 0, + 0, + text, + res.getFontForElement(element), + style.textAlign, + undefined, + ElementStyleHelper.scoreColor(res, element, score) + ); + } + + private createScoreInfoGlyphs(): void { + Logger.debug('ScoreLayout', 'Creating score info glyphs'); + const settings = this.renderer.settings; + const score: Score = this.renderer.score!; + this.headerGlyphs = new Map(); + this.footerGlyphs = new Map(); const fakeBarRenderer = new BarRendererBase(this.renderer, this.renderer.tracks![0].staves[0].bars[0]); - for (const [_e, glyph] of this.scoreInfoGlyphs) { - glyph.renderer = fakeBarRenderer; - glyph.doLayout(); + for (const [scoreElement, notationElement] of ScoreLayout.HeaderElements.value) { + const glyph = this.createHeaderFooterGlyph(settings, score, scoreElement, notationElement); + if (glyph) { + glyph.renderer = fakeBarRenderer; + glyph.doLayout(); + this.headerGlyphs.set(scoreElement, glyph); + } } + for (const [scoreElement, notationElement] of ScoreLayout.FooterElements.value) { + const glyph = this.createHeaderFooterGlyph(settings, score, scoreElement, notationElement); + if (glyph) { + glyph.renderer = fakeBarRenderer; + glyph.doLayout(); + this.footerGlyphs.set(scoreElement, glyph); + } + } + + const notation = settings.notation; + const res = settings.display.resources; if (notation.isNotationElementVisible(NotationElement.GuitarTuning)) { - let tunings: Staff[] = []; - for (let track of this.renderer.tracks!) { - for (let staff of track.staves) { + const stavesWithTuning: Staff[] = []; + for (const track of this.renderer.tracks!) { + for (const staff of track.staves) { let showTuning = !staff.isPercussion && staff.isStringed && staff.tuning.length > 0 && staff.showTablature; @@ -224,17 +288,28 @@ export abstract class ScoreLayout { } if (showTuning) { - tunings.push(staff); + stavesWithTuning.push(staff); break; } } } // tuning info - if (tunings.length > 0 && score.stylesheet.globalDisplayTuning) { + if (stavesWithTuning.length > 0 && score.stylesheet.globalDisplayTuning) { this.tuningGlyph = new TuningContainerGlyph(0, 0); this.tuningGlyph.renderer = fakeBarRenderer; - for (const t of tunings) { - this.tuningGlyph.addTuning(t.stringTuning, tunings.length > 1 ? t.track.name : ''); + for (const staff of stavesWithTuning) { + if (staff.stringTuning.tunings.length > 0) { + const trackLabel = stavesWithTuning.length > 1 ? staff.track.name : ''; + const item: TuningGlyph = new TuningGlyph(0, 0, staff.stringTuning, trackLabel); + item.colorOverride = ElementStyleHelper.trackColor( + res, + TrackSubElement.StringTuning, + staff.track + ); + item.renderer = fakeBarRenderer; + item.doLayout(); + this.tuningGlyph.addGlyph(item); + } } } } @@ -242,9 +317,9 @@ export abstract class ScoreLayout { if (notation.isNotationElementVisible(NotationElement.ChordDiagrams)) { this.chordDiagrams = new ChordDiagramContainerGlyph(0, 0); this.chordDiagrams.renderer = fakeBarRenderer; - let chordIds: Set = new Set(); + const chordIds: Set = new Set(); - for (let track of this.renderer.tracks!) { + for (const track of this.renderer.tracks!) { const shouldShowDiagramsForTrack = score.stylesheet.globalDisplayChordDiagramsOnTop && (score.stylesheet.perTrackChordDiagramsOnTop == null || @@ -255,7 +330,7 @@ export abstract class ScoreLayout { continue; } - for (let staff of track.staves) { + for (const staff of track.staves) { const sc = staff.chords; if (sc) { for (const [, chord] of sc) { @@ -277,15 +352,15 @@ export abstract class ScoreLayout { public lastBarIndex: number = 0; protected createEmptyStaffSystem(): StaffSystem { - let system: StaffSystem = new StaffSystem(this); + const system: StaffSystem = new StaffSystem(this); for (let trackIndex: number = 0; trackIndex < this.renderer.tracks!.length; trackIndex++) { - let track: Track = this.renderer.tracks![trackIndex]; + const track: Track = this.renderer.tracks![trackIndex]; for (let staffIndex: number = 0; staffIndex < track.staves.length; staffIndex++) { - let staff: Staff = track.staves[staffIndex]; - let profile: BarRendererFactory[] = Environment.staveProfiles.get( + const staff: Staff = track.staves[staffIndex]; + const profile: BarRendererFactory[] = Environment.staveProfiles.get( this.renderer.settings.display.staveProfile )!; - for (let factory of profile) { + for (const factory of profile) { if (factory.canCreate(track, staff)) { system.addStaff(track, new RenderStaff(trackIndex, staff, factory)); } @@ -300,43 +375,119 @@ export abstract class ScoreLayout { this._barRendererLookup.set(key, new Map()); } this._barRendererLookup.get(key)!.set(renderer.bar.id, renderer); + if (renderer.additionalMultiRestBars) { + for (const b of renderer.additionalMultiRestBars) { + this._barRendererLookup.get(key)!.set(b.id, renderer); + } + } } public unregisterBarRenderer(key: string, renderer: BarRendererBase): void { if (this._barRendererLookup.has(key)) { - let lookup: Map = this._barRendererLookup.get(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 { - let barRendererId: number = bar.id; + const barRendererId: number = bar.id; if (this._barRendererLookup.has(key) && this._barRendererLookup.get(key)!.has(barRendererId)) { return this._barRendererLookup.get(key)!.get(barRendererId)!; } return null; } + protected layoutAndRenderBottomScoreInfo(y: number): number { + y = Math.round(y); + const e = new RenderFinishedEventArgs(); + e.x = 0; + e.y = y; + + let infoHeight = 0; + + const res: RenderingResources = this.renderer.settings.display.resources; + const scoreInfoGlyphs: TextGlyph[] = []; + + let width = 0; + + for (const [scoreElement, _notationElement] of ScoreLayout.FooterElements.value) { + if (this.footerGlyphs.has(scoreElement)) { + const glyph: TextGlyph = this.footerGlyphs.get(scoreElement)!; + glyph.y = infoHeight; + this.alignScoreInfoGlyph(glyph); + infoHeight += glyph.font.size * 1.2; + scoreInfoGlyphs.push(glyph); + width = Math.max(width, Math.round(glyph.x + glyph.width)); + } + } + + infoHeight = Math.round(infoHeight); + + if (scoreInfoGlyphs.length > 0) { + e.width = width; + e.height = infoHeight; + e.totalWidth = this.scaledWidth; + e.totalHeight = y + e.height; + this.registerPartial(e, (canvas: ICanvas) => { + canvas.color = res.scoreInfoColor; + canvas.textAlign = TextAlign.Left; + canvas.textBaseline = TextBaseline.Top; + for (const g of scoreInfoGlyphs) { + g.paint(0, 0, canvas); + } + }); + } + + return y + infoHeight; + } + + protected alignScoreInfoGlyph(glyph: TextGlyph) { + const isVertical = Environment.getLayoutEngineFactory(this.renderer.settings.display.layoutMode).vertical; + if (isVertical) { + switch (glyph.textAlign) { + case TextAlign.Left: + glyph.x = this.pagePadding![0]; + break; + case TextAlign.Center: + glyph.x = this.scaledWidth / 2; + break; + case TextAlign.Right: + glyph.x = this.scaledWidth - this.pagePadding![2]; + break; + } + } else { + glyph.x = this.firstBarX; + glyph.textAlign = TextAlign.Left; + } + } + public layoutAndRenderAnnotation(y: number): number { // attention, you are not allowed to remove change this notice within any version of this library without permission! - let msg: string = 'rendered by alphaTab'; - let resources: RenderingResources = this.renderer.settings.display.resources; - let size: number = 12; - let height: number = Math.floor(size); - - const e = new RenderFinishedEventArgs(); + 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); - this.renderer.canvas!.font = font; + 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); + glyph.renderer = fakeBarRenderer; + glyph.doLayout(); + this.alignScoreInfoGlyph(glyph); + + const e = new RenderFinishedEventArgs(); - const centered = Environment.getLayoutEngineFactory(this.renderer.settings.display.layoutMode).vertical; - e.width = this.renderer.canvas!.measureText(msg).width / this.renderer.settings.display.scale; - e.height = height; - e.x = centered ? (this.scaledWidth - e.width) / 2 : this.firstBarX; + e.width = glyph.x + glyph.width; + e.x = 0; + e.height = size; e.y = y; e.totalWidth = this.scaledWidth; - e.totalHeight = y + height; + e.totalHeight = y + size; e.firstMasterBarIndex = -1; e.lastMasterBarIndex = -1; @@ -345,9 +496,9 @@ export abstract class ScoreLayout { canvas.font = font; canvas.textAlign = TextAlign.Left; canvas.textBaseline = TextBaseline.Top; - canvas.fillText(msg, 0, 0); + glyph.paint(0, 0, canvas); }); - return y + height; + return y + size; } } diff --git a/src/rendering/staves/BarLayoutingInfo.ts b/src/rendering/staves/BarLayoutingInfo.ts index dc8adf3cb..76527e2ea 100644 --- a/src/rendering/staves/BarLayoutingInfo.ts +++ b/src/rendering/staves/BarLayoutingInfo.ts @@ -1,9 +1,9 @@ import { MidiUtils } from '@src/midi/MidiUtils'; -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { Duration } from '@src/model/Duration'; import { Spring } from '@src/rendering/staves/Spring'; import { ModelUtils } from '@src/model/ModelUtils'; -import { ICanvas } from '@src/platform/ICanvas'; +import type { ICanvas } from '@src/platform/ICanvas'; import { GraceType } from '@src/model/GraceType'; /** @@ -16,12 +16,14 @@ export class BarLayoutingInfo { private static readonly MinDurationWidth: number = 7; private _timeSortedSprings: Spring[] = []; - private _xMin: number = 0; private _minTime: number = -1; private _onTimePositionsForce: number = 0; private _onTimePositions: Map = new Map(); private _incompleteGraceRodsWidth: number = 0; + // the smallest duration we have between two springs to ensure we have positive spring constants + private _minDuration: number = BarLayoutingInfo.MinDuration; + /** * an internal version number that increments whenever a change was made. */ @@ -43,14 +45,13 @@ export class BarLayoutingInfo { const groupId = beat.graceGroup!.id; const graceRod = this.allGraceRods.get(groupId)![beat.graceIndex]; return graceRod.preBeatWidth; - } else { - const start: number = beat.absoluteDisplayStart; - if (!this.springs.has(start)) { - return 0; - } - - return this.springs.get(start)!.preBeatWidth; } + const start: number = beat.absoluteDisplayStart; + if (!this.springs.has(start)) { + return 0; + } + + return this.springs.get(start)!.preBeatWidth; } public getPostBeatSize(beat: Beat) { @@ -58,14 +59,13 @@ export class BarLayoutingInfo { const groupId = beat.graceGroup!.id; const graceRod = this.allGraceRods.get(groupId)![beat.graceIndex]; return graceRod.postSpringWidth; - } else { - const start: number = beat.absoluteDisplayStart; - if (!this.springs.has(start)) { - return 0; - } - - return this.springs.get(start)!.postSpringWidth; } + const start: number = beat.absoluteDisplayStart; + if (!this.springs.has(start)) { + return 0; + } + + return this.springs.get(start)!.postSpringWidth; } public incompleteGraceRods: Map = new Map(); @@ -89,20 +89,24 @@ export class BarLayoutingInfo { // Gourlay defines that we need the smallest note duration that either starts **or continues** on the current spring. if (this._timeSortedSprings.length > 0) { let smallestDuration: number = duration; - let previousSpring: Spring = this._timeSortedSprings[this._timeSortedSprings.length - 1]; + const previousSpring: Spring = this._timeSortedSprings[this._timeSortedSprings.length - 1]; for (const prevDuration of previousSpring.allDurations) { - let end: number = previousSpring.timePosition + prevDuration; + const end: number = previousSpring.timePosition + prevDuration; if (end >= start && prevDuration < smallestDuration) { smallestDuration = prevDuration; } } + //spring.smallestDuration = duration; + if (duration < this._minDuration) { + this._minDuration = duration; + } } spring.longestDuration = duration; spring.postSpringWidth = postSpringSize; spring.graceBeatWidth = graceBeatWidth; spring.preBeatWidth = preBeatWidth; this.springs.set(start, spring); - let timeSorted: Spring[] = this._timeSortedSprings; + const timeSorted: Spring[] = this._timeSortedSprings; let insertPos: number = timeSorted.length - 1; while (insertPos > 0 && timeSorted[insertPos].timePosition > start) { insertPos--; @@ -134,7 +138,7 @@ export class BarLayoutingInfo { } public addBeatSpring(beat: Beat, preBeatSize: number, postBeatSize: number): void { - let start: number = beat.absoluteDisplayStart; + const start: number = beat.absoluteDisplayStart; if (beat.graceType !== GraceType.None) { // For grace beats we just remember the the sizes required for them // these sizes are then considered when the target beat is added. @@ -149,7 +153,7 @@ export class BarLayoutingInfo { this.incompleteGraceRods.set(groupId, new Array(beat.graceGroup!.beats.length)); } - let existingSpring = this.allGraceRods.get(groupId)![beat.graceIndex]; + const existingSpring = this.allGraceRods.get(groupId)![beat.graceIndex]; if (existingSpring) { if (existingSpring.postSpringWidth < postBeatSize) { existingSpring.postSpringWidth = postBeatSize; @@ -203,27 +207,20 @@ export class BarLayoutingInfo { } private calculateSpringConstants(): void { - this._xMin = 0; - let springs: Map = this.springs; - for (const spring of springs.values()) { - if (spring.springWidth < this._xMin) { - this._xMin = spring.springWidth; - } - } let totalSpringConstant: number = 0; - let sortedSprings: Spring[] = this._timeSortedSprings; + const sortedSprings: Spring[] = this._timeSortedSprings; if (sortedSprings.length === 0) { this.totalSpringConstant = -1; this.minStretchForce = -1; return; } for (let i: number = 0; i < sortedSprings.length; i++) { - let currentSpring: Spring = sortedSprings[i]; + const currentSpring: Spring = sortedSprings[i]; let duration: number = 0; if (i === sortedSprings.length - 1) { duration = currentSpring.longestDuration; } else { - let nextSpring: Spring = sortedSprings[i + 1]; + const nextSpring: Spring = sortedSprings[i + 1]; duration = Math.abs(nextSpring.timePosition - currentSpring.timePosition); } currentSpring.springConstant = this.calculateSpringConstant(currentSpring, duration); @@ -238,13 +235,13 @@ export class BarLayoutingInfo { // reserves enough space for (let i: number = 0; i < sortedSprings.length; i++) { - let currentSpring = sortedSprings[i]; + const currentSpring = sortedSprings[i]; let requiredSpace = 0; if (i === sortedSprings.length - 1) { requiredSpace = currentSpring.postSpringWidth; } else { - let nextSpring = sortedSprings[i + 1]; + const nextSpring = sortedSprings[i + 1]; requiredSpace = currentSpring.postSpringWidth + nextSpring.preSpringWidth; } @@ -254,7 +251,7 @@ export class BarLayoutingInfo { requiredSpace += currentSpring.preSpringWidth; } - let requiredSpaceForce = requiredSpace * currentSpring.springConstant; + const requiredSpaceForce = requiredSpace * currentSpring.springConstant; this.updateMinStretchForce(requiredSpaceForce); } } @@ -303,14 +300,18 @@ export class BarLayoutingInfo { private calculateSpringConstant(spring: Spring, duration: number): number { if (duration <= 0) { - duration = MidiUtils.toTicks(Duration.SixtyFourth); + duration = MidiUtils.toTicks(Duration.TwoHundredFiftySixth); } if (spring.smallestDuration === 0) { spring.smallestDuration = duration; } - let minDuration: number = spring.smallestDuration; - let phi: number = 1 + 0.85 * Math.log2(duration / BarLayoutingInfo.MinDuration); - return (minDuration / duration) * (1 / (phi * BarLayoutingInfo.MinDurationWidth)); + const smallestDuration: number = spring.smallestDuration; + + const minDuration = this._minDuration; + const minDurationWidth = BarLayoutingInfo.MinDurationWidth; + + const phi: number = 1 + 0.85 * Math.log2(duration / minDuration); + return (smallestDuration / duration) * (1 / (phi * minDurationWidth)); } public spaceToForce(space: number): number { @@ -349,9 +350,9 @@ export class BarLayoutingInfo { return this._onTimePositions; } this._onTimePositionsForce = force; - let positions: Map = new Map(); + const positions: Map = new Map(); this._onTimePositions = positions; - let sortedSprings: Spring[] = this._timeSortedSprings; + const sortedSprings: Spring[] = this._timeSortedSprings; if (sortedSprings.length === 0) { return positions; } diff --git a/src/rendering/staves/MasterBarsRenderers.ts b/src/rendering/staves/MasterBarsRenderers.ts index 11dcfeb3f..b730c2dad 100644 --- a/src/rendering/staves/MasterBarsRenderers.ts +++ b/src/rendering/staves/MasterBarsRenderers.ts @@ -1,6 +1,6 @@ -import { MasterBar } from '@src/model/MasterBar'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; -import { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; +import type { MasterBar } from '@src/model/MasterBar'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; /** * This container represents a single column of bar renderers independent from any staves. @@ -11,6 +11,15 @@ export class MasterBarsRenderers { public isLinkedToPrevious: boolean = false; public canWrap: boolean = true; public masterBar!: MasterBar; + public additionalMultiBarRestIndexes: number[] | null = null; + + public get lastMasterBarIndex(): number { + if (this.additionalMultiBarRestIndexes) { + return this.additionalMultiBarRestIndexes[this.additionalMultiBarRestIndexes.length - 1]; + } + return this.masterBar.index; + } + public renderers: BarRendererBase[] = []; public layoutingInfo!: BarLayoutingInfo; } diff --git a/src/rendering/staves/RenderStaff.ts b/src/rendering/staves/RenderStaff.ts index 5f681fe21..8a5fea0ae 100644 --- a/src/rendering/staves/RenderStaff.ts +++ b/src/rendering/staves/RenderStaff.ts @@ -1,12 +1,12 @@ -import { Bar } from '@src/model/Bar'; -import { Staff } from '@src/model/Staff'; -import { ICanvas } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; -import { BarRendererFactory } from '@src/rendering/BarRendererFactory'; -import { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; -import { StaffSystem } from '@src/rendering/staves/StaffSystem'; -import { StaffTrackGroup } from '@src/rendering/staves/StaffTrackGroup'; -import { InternalSystemsLayoutMode } from '../layout/ScoreLayout'; +import type { Bar } from '@src/model/Bar'; +import type { Staff } from '@src/model/Staff'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererFactory } from '@src/rendering/BarRendererFactory'; +import type { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; +import type { StaffSystem } from '@src/rendering/staves/StaffSystem'; +import type { StaffTrackGroup } from '@src/rendering/staves/StaffTrackGroup'; +import { InternalSystemsLayoutMode } from '@src/rendering/layout/ScoreLayout'; /** * A Staff represents a single line within a StaffSystem. @@ -36,7 +36,7 @@ export class RenderStaff { public trackIndex: number = 0; public modelStaff: Staff; - public get staveId(): string { + public get staffId(): string { return this._factory.staffId; } @@ -103,16 +103,12 @@ export class RenderStaff { renderer.index = this.barRenderers.length; renderer.reLayout(); this.barRenderers.push(renderer); - this.system.layout.registerBarRenderer(this.staveId, renderer); + this.system.layout.registerBarRenderer(this.staffId, renderer); } - public addBar(bar: Bar, layoutingInfo: BarLayoutingInfo): void { - let renderer: BarRendererBase; - if (!bar) { - renderer = new BarRendererBase(this.system.layout.renderer, bar); - } else { - renderer = this._factory.create(this.system.layout.renderer, bar); - } + public addBar(bar: Bar, layoutingInfo: BarLayoutingInfo, additionalMultiBarsRestBars: Bar[] | null): void { + const renderer = this._factory.create(this.system.layout.renderer, bar); + renderer.additionalMultiRestBars = additionalMultiBarsRestBars; renderer.staff = this; renderer.index = this.barRenderers.length; renderer.layoutingInfo = layoutingInfo; @@ -124,21 +120,21 @@ export class RenderStaff { const barDisplayWidth = renderer.barDisplayWidth; if ( barDisplayWidth > 0 && - this.system.layout.systemsLayoutMode == InternalSystemsLayoutMode.FromModelWithWidths + this.system.layout.systemsLayoutMode === InternalSystemsLayoutMode.FromModelWithWidths ) { renderer.width = barDisplayWidth; } this.barRenderers.push(renderer); if (bar) { - this.system.layout.registerBarRenderer(this.staveId, renderer); + this.system.layout.registerBarRenderer(this.staffId, renderer); } } public revertLastBar(): BarRendererBase { - let lastBar: BarRendererBase = this.barRenderers[this.barRenderers.length - 1]; + const lastBar: BarRendererBase = this.barRenderers[this.barRenderers.length - 1]; this.barRenderers.splice(this.barRenderers.length - 1, 1); - this.system.layout.unregisterBarRenderer(this.staveId, lastBar); + this.system.layout.unregisterBarRenderer(this.staffId, lastBar); for (const r of this.barRenderers) { r.applyLayoutingInfo(); } @@ -147,20 +143,20 @@ export class RenderStaff { public scaleToWidth(width: number): void { this._sharedLayoutData = new Map(); - let topOverflow: number = this.topOverflow; + 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 - let difference: number = width - this.system.computedWidth; - let spacePerBar: number = difference / this.barRenderers.length; + 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; - let actualBarWidth = renderer.computedWidth + spacePerBar; + const actualBarWidth = renderer.computedWidth + spacePerBar; renderer.scaleToWidth(actualBarWidth); x += renderer.width; } @@ -203,7 +199,7 @@ export class RenderStaff { public get topOverflow(): number { let m: number = 0; for (let i: number = 0, j: number = this.barRenderers.length; i < j; i++) { - let r: BarRendererBase = this.barRenderers[i]; + const r: BarRendererBase = this.barRenderers[i]; if (r.topOverflow > m) { m = r.topOverflow; } @@ -214,7 +210,7 @@ export class RenderStaff { public get bottomOverflow(): number { let m: number = 0; for (let i: number = 0, j: number = this.barRenderers.length; i < j; i++) { - let r: BarRendererBase = this.barRenderers[i]; + const r: BarRendererBase = this.barRenderers[i]; if (r.bottomOverflow > m) { m = r.bottomOverflow; } @@ -231,7 +227,7 @@ export class RenderStaff { this.topSpacing = this._factory.getStaffPaddingTop(this); this.bottomSpacing = this._factory.getStaffPaddingBottom(this); - this.height = (this.barRenderers.length > 0) ? this.barRenderers[0].height : 0; + this.height = this.barRenderers.length > 0 ? this.barRenderers[0].height : 0; if (this.height > 0) { this.height += this.topSpacing + this.topOverflow + this.bottomOverflow + this.bottomSpacing; @@ -259,9 +255,13 @@ export class RenderStaff { // 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; - this.height = Math.max(this.height, this.barRenderers[i].height); + } + + // finalize again (to align ties) + for (let i: number = 0; i < this.barRenderers.length; i++) { this.barRenderers[i].finalizeRenderer(); } } diff --git a/src/rendering/staves/StaffSystem.ts b/src/rendering/staves/StaffSystem.ts index 18ac48125..1163f8e3a 100644 --- a/src/rendering/staves/StaffSystem.ts +++ b/src/rendering/staves/StaffSystem.ts @@ -1,21 +1,23 @@ -import { Bar } from '@src/model/Bar'; -import { Font } from '@src/model/Font'; -import { Track } from '@src/model/Track'; -import { ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; -import { ScoreLayout } from '@src/rendering/layout/ScoreLayout'; +import type { Bar } from '@src/model/Bar'; +import type { Font } from '@src/model/Font'; +import { type Track, TrackSubElement } from '@src/model/Track'; +import { type ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { ScoreLayout } from '@src/rendering/layout/ScoreLayout'; import { BarLayoutingInfo } from '@src/rendering/staves/BarLayoutingInfo'; import { MasterBarsRenderers } from '@src/rendering/staves/MasterBarsRenderers'; -import { RenderStaff } from '@src/rendering/staves/RenderStaff'; +import type { RenderStaff } from '@src/rendering/staves/RenderStaff'; import { StaffTrackGroup } from '@src/rendering/staves/StaffTrackGroup'; import { Bounds } from '@src/rendering/utils/Bounds'; import { MasterBarBounds } from '@src/rendering/utils/MasterBarBounds'; import { StaffSystemBounds } from '@src/rendering/utils/StaffSystemBounds'; -import { RenderingResources } from '@src/RenderingResources'; +import type { RenderingResources } from '@src/RenderingResources'; import { NotationElement } from '@src/NotationSettings'; import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@src/model/RenderStylesheet'; -import { MusicFontSymbol } from '@src/model'; -import { Environment } from '@src/Environment'; +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; +import { ElementStyleHelper } from '@src/rendering/utils/ElementStyleHelper'; +import { MusicFontSymbolSizes } from '@src/rendering/utils/MusicFontSymbolSizes'; +import type { LineBarRenderer } from '@src/rendering/LineBarRenderer'; export abstract class SystemBracket { public firstStaffInBracket: RenderStaff | null = null; @@ -34,8 +36,12 @@ export abstract class SystemBracket { return; } + // SMUFL: The brace glyph should have a height of 1em, i.e. the height of a single five-line stave, and should be scaled proportionally + const bravuraBraceHeightAtMusicFontSize = MusicFontSymbolSizes.Heights.get(MusicFontSymbol.Brace)!; + const bravuraBraceWidthAtMusicFontSize = MusicFontSymbolSizes.Widths.get(MusicFontSymbol.Brace)!; + // normal bracket width - this.width = 3; + this.width = bravuraBraceWidthAtMusicFontSize; if (!this.drawAsBrace || !this.firstStaffInBracket || !this.lastStaffInBracket) { return; } @@ -43,10 +49,6 @@ export abstract class SystemBracket { const firstStart: number = this.firstStaffInBracket.contentTop; const lastEnd: number = this.lastStaffInBracket.contentBottom; - // SMUFL: The brace glyph should have a height of 1em, i.e. the height of a single five-line stave, and should be scaled proportionally - const bravuraBraceHeightAtMusicFontSize = Environment.MusicFontSize; - const bravuraBraceWidthAtMusicFontSize = 3; - const requiredHeight = lastEnd - firstStart; const requiredScaleForBracket = requiredHeight / bravuraBraceHeightAtMusicFontSize; this.braceScale = requiredScaleForBracket; @@ -73,10 +75,6 @@ class SingleTrackSystemBracket extends SystemBracket { } class SimilarInstrumentSystemBracket extends SingleTrackSystemBracket { - public constructor(track: Track) { - super(track); - } - public override includesStaff(r: RenderStaff): boolean { // allow merging on same track (for braces, percussion and items belonging together) if (r.modelStaff.track === this.track) { @@ -89,7 +87,7 @@ class SimilarInstrumentSystemBracket extends SingleTrackSystemBracket { } // we allow cross track merging of staffs when they have the same program - return this.track.playbackInfo.program == r.modelStaff.track.playbackInfo.program; + return this.track.playbackInfo.program === r.modelStaff.track.playbackInfo.program; } } @@ -160,35 +158,43 @@ export class StaffSystem { renderers.layoutingInfo.preBeatSize = 0; let src: number = 0; for (let i: number = 0, j: number = this.staves.length; i < j; i++) { - let g: StaffTrackGroup = this.staves[i]; + const g: StaffTrackGroup = this.staves[i]; for (let k: number = 0, l: number = g.staves.length; k < l; k++) { - let s: RenderStaff = g.staves[k]; - let renderer: BarRendererBase = renderers.renderers[src++]; + const s: RenderStaff = g.staves[k]; + const renderer: BarRendererBase = renderers.renderers[src++]; s.addBarRenderer(renderer); } } this.calculateAccoladeSpacing(tracks); - // Width += renderers.Width; this.updateWidthFromLastBar(); return renderers; } - public addBars(tracks: Track[], barIndex: number): MasterBarsRenderers | null { - if (tracks.length === 0) { - return null; - } - let result: MasterBarsRenderers = new MasterBarsRenderers(); + public addBars( + tracks: Track[], + barIndex: number, + additionalMultiBarRestIndexes: number[] | null + ): MasterBarsRenderers { + const result: MasterBarsRenderers = new MasterBarsRenderers(); + result.additionalMultiBarRestIndexes = additionalMultiBarRestIndexes; result.layoutingInfo = new BarLayoutingInfo(); result.masterBar = tracks[0].score.masterBars[barIndex]; this.masterBarsRenderers.push(result); + // add renderers - let barLayoutingInfo: BarLayoutingInfo = result.layoutingInfo; - for (let g of this.staves) { - for (let s of g.staves) { - let bar: Bar = g.track.staves[s.modelStaff.index].bars[barIndex]; - s.addBar(bar, barLayoutingInfo); - let renderer: BarRendererBase = s.barRenderers[s.barRenderers.length - 1]; + const barLayoutingInfo: BarLayoutingInfo = result.layoutingInfo; + for (const g of this.staves) { + for (const s of g.staves) { + const bar: Bar = g.track.staves[s.modelStaff.index].bars[barIndex]; + + const additionalMultiBarsRestBars: Bar[] | null = + additionalMultiBarRestIndexes == null + ? null + : additionalMultiBarRestIndexes.map(b => g.track.staves[s.modelStaff.index].bars[b]); + + s.addBar(bar, barLayoutingInfo, additionalMultiBarsRestBars); + const renderer: BarRendererBase = s.barRenderers[s.barRenderers.length - 1]; result.renderers.push(renderer); if (renderer.isLinkedToPrevious) { result.isLinkedToPrevious = true; @@ -209,13 +215,13 @@ export class StaffSystem { public revertLastBar(): MasterBarsRenderers | null { if (this.masterBarsRenderers.length > 1) { - let toRemove: MasterBarsRenderers = this.masterBarsRenderers[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++) { - let s: RenderStaff = this._allStaves[i]; - let lastBar: BarRendererBase = s.revertLastBar(); + const s: RenderStaff = this._allStaves[i]; + const lastBar: BarRendererBase = s.revertLastBar(); const computedWidth = lastBar.computedWidth; if (computedWidth > width) { width = computedWidth; @@ -237,7 +243,7 @@ export class StaffSystem { let realWidth: number = 0; let barDisplayScale: number = 0; for (let i: number = 0, j: number = this._allStaves.length; i < j; i++) { - let s: RenderStaff = this._allStaves[i]; + const s: RenderStaff = this._allStaves[i]; const last = s.barRenderers[s.barRenderers.length - 1]; last.applyLayoutingInfo(); @@ -298,10 +304,10 @@ export class StaffSystem { let hasAnyTrackName = false; if (shouldRender) { - let canvas: ICanvas = this.layout.renderer.canvas!; - let res: Font = settings.display.resources.effectFont; + const canvas: ICanvas = this.layout.renderer.canvas!; + const res: Font = settings.display.resources.effectFont; canvas.font = res; - for (let t of tracks) { + for (const t of tracks) { let trackNameText = ''; switch (trackNameMode) { case TrackNameMode.FullName: @@ -347,7 +353,7 @@ export class StaffSystem { // - requires a feature to draw glyphs with a max-width or a horizontal stretch scale let currentY: number = 0; - for (let staff of this._allStaves) { + for (const staff of this._allStaves) { staff.y = currentY; staff.calculateHeightForAccolade(); currentY += staff.height; @@ -368,7 +374,7 @@ export class StaffSystem { private getStaffTrackGroup(track: Track): StaffTrackGroup | null { for (let i: number = 0, j: number = this.staves.length; i < j; i++) { - let g: StaffTrackGroup = this.staves[i]; + const g: StaffTrackGroup = this.staves[i]; if (g.track === track) { return g; } @@ -455,6 +461,12 @@ export class StaffSystem { this.paintPartial(cx + this.x, cy + this.y, canvas, 0, this.masterBarsRenderers.length); if (this._hasSystemSeparator) { + using _ = ElementStyleHelper.track( + canvas, + TrackSubElement.SystemSeparator, + this._allStaves[0].modelStaff.track + ); + canvas.fillMusicFontSymbol( cx + this.x, cy + this.y + this.height - 10, @@ -476,29 +488,14 @@ export class StaffSystem { for (let i: number = 0, j: number = this._allStaves.length; i < j; i++) { this._allStaves[i].paint(cx, cy, canvas, startIndex, count); } - let res: RenderingResources = this.layout.renderer.settings.display.resources; + const res: RenderingResources = this.layout.renderer.settings.display.resources; + if (this.staves.length > 0 && startIndex === 0) { // // Draw start grouping // canvas.color = res.barSeparatorColor; - const firstStaffInBracket = this._firstStaffInBrackets; - const lastStaffInBracket = this._lastStaffInBrackets; - - if (firstStaffInBracket && lastStaffInBracket) { - // - // draw grouping line for all staves - // - let firstStart: number = cy + firstStaffInBracket.contentTop; - let lastEnd: number = cy + lastStaffInBracket.contentBottom; - let acooladeX: number = cx + firstStaffInBracket.x; - canvas.beginPath(); - canvas.moveTo(acooladeX, firstStart); - canvas.lineTo(acooladeX, lastEnd); - canvas.stroke(); - } - // // Draw track names const settings = this.layout.renderer.settings; @@ -540,8 +537,8 @@ export class StaffSystem { const oldTextAlign = canvas.textAlign; for (const g of this.staves) { if (g.firstStaffInBracket && g.lastStaffInBracket) { - let firstStart: number = cy + g.firstStaffInBracket.contentTop; - let lastEnd: number = cy + g.lastStaffInBracket.contentBottom; + const firstStart: number = cy + g.firstStaffInBracket.contentTop; + const lastEnd: number = cy + g.lastStaffInBracket.contentBottom; let trackNameText = ''; switch (trackNameMode) { @@ -553,6 +550,12 @@ export class StaffSystem { break; } + using _trackNameStyle = ElementStyleHelper.track( + canvas, + TrackSubElement.TrackName, + g.track + ); + if (trackNameText.length > 0) { const textEndX = // start at beginning of first renderer @@ -595,6 +598,32 @@ export class StaffSystem { } } + if (this._allStaves.length > 0) { + 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; + + const accoladeX: number = cx + previousStaffInBracket.x; + + const firstLineBarRenderer = previousStaffInBracket.barRenderers[0] as LineBarRenderer; + + using _ = ElementStyleHelper.bar( + canvas, + firstLineBarRenderer.staffLineBarSubElement, + firstLineBarRenderer.bar + ); + const h = Math.ceil(thisTop - previousBottom); + canvas.fillRect(accoladeX, cy + previousBottom, 1, h); + } + + previousStaffInBracket = s; + } + } + } + // // Draw brackets for (const bracket of this._brackets!) { @@ -625,7 +654,7 @@ export class StaffSystem { Math.ceil(accoladeEnd - accoladeStart) ); - let spikeX: number = barStartX - barOffset - barSize - 0.5; + const spikeX: number = barStartX - barOffset - barSize - 0.5; canvas.fillMusicFontSymbol(spikeX, accoladeStart, 1, MusicFontSymbol.BracketTop); canvas.fillMusicFontSymbol(spikeX, Math.floor(accoladeEnd), 1, MusicFontSymbol.BracketBottom); } @@ -655,7 +684,7 @@ export class StaffSystem { } let currentY: number = 0; - for (let staff of this._allStaves) { + for (const staff of this._allStaves) { staff.x = this.accoladeWidth; staff.y = currentY; staff.finalizeStaff(); @@ -678,19 +707,19 @@ export class StaffSystem { } cy += this.topPadding; - let lastStaff: RenderStaff = this._allStaves[this._allStaves.length - 1]; - let visualTop: number = cy + this.y + _firstStaffInBrackets.y; - let visualBottom: number = cy + this.y + _lastStaffInBrackets.y + _lastStaffInBrackets.height; - let realTop: number = cy + this.y + this._allStaves[0].y; - let realBottom: number = cy + this.y + lastStaff.y + lastStaff.height; - let lineTop: number = + 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); - let lineBottom: number = + const lineBottom: number = cy + this.y + lastStaff.y + @@ -698,11 +727,11 @@ export class StaffSystem { lastStaff.bottomSpacing - lastStaff.bottomOverflow - (lastStaff.barRenderers.length > 0 ? lastStaff.barRenderers[0].bottomPadding : 0); - let visualHeight: number = visualBottom - visualTop; - let lineHeight: number = lineBottom - lineTop; - let realHeight: number = realBottom - realTop; - let x: number = this.x + _firstStaffInBrackets.x; - let staffSystemBounds = new StaffSystemBounds(); + const visualHeight: number = visualBottom - visualTop; + const lineHeight: number = lineBottom - lineTop; + const realHeight: number = realBottom - realTop; + const x: number = this.x + _firstStaffInBrackets.x; + const staffSystemBounds = new StaffSystemBounds(); staffSystemBounds.visualBounds = new Bounds(); staffSystemBounds.visualBounds.x = cx + this.x; staffSystemBounds.visualBounds.y = cy + this.y; @@ -714,10 +743,10 @@ export class StaffSystem { staffSystemBounds.realBounds.w = this.width; staffSystemBounds.realBounds.h = this.height; this.layout.renderer.boundsLookup!.addStaffSystem(staffSystemBounds); - let masterBarBoundsLookup: Map = new Map(); + const masterBarBoundsLookup: Map = new Map(); for (let i: number = 0; i < this.staves.length; i++) { - for (let staff of this.staves[i].stavesRelevantForBoundsLookup) { - for (let renderer of staff.barRenderers) { + for (const staff of this.staves[i].stavesRelevantForBoundsLookup) { + for (const renderer of staff.barRenderers) { let masterBarBounds: MasterBarBounds; if (!masterBarBoundsLookup.has(renderer.bar.masterBar.index)) { masterBarBounds = new MasterBarBounds(); @@ -755,8 +784,8 @@ export class StaffSystem { if (!this._firstStaffInBrackets || this.layout.renderer.tracks!.length === 0) { return 0; } - let bar: Bar = this.layout.renderer.tracks![0].staves[0].bars[index]; - let renderer: BarRendererBase = this.layout.getRendererForBar(this._firstStaffInBrackets.staveId, bar)!; + const bar: Bar = this.layout.renderer.tracks![0].staves[0].bars[index]; + const renderer: BarRendererBase = this.layout.getRendererForBar(this._firstStaffInBrackets.staffId, bar)!; return renderer.x; } } diff --git a/src/rendering/staves/StaffTrackGroup.ts b/src/rendering/staves/StaffTrackGroup.ts index 8061d21f2..50eb820e6 100644 --- a/src/rendering/staves/StaffTrackGroup.ts +++ b/src/rendering/staves/StaffTrackGroup.ts @@ -1,6 +1,6 @@ -import { Track } from '@src/model/Track'; -import { RenderStaff } from '@src/rendering/staves/RenderStaff'; -import { StaffSystem, SystemBracket } from '@src/rendering/staves/StaffSystem'; +import type { Track } from '@src/model/Track'; +import type { RenderStaff } from '@src/rendering/staves/RenderStaff'; +import type { StaffSystem, SystemBracket } from '@src/rendering/staves/StaffSystem'; /** * Represents the group of rendered staves belonging to an individual track. diff --git a/src/rendering/utils/AccidentalHelper.ts b/src/rendering/utils/AccidentalHelper.ts index d30e7aa22..7806fd43e 100644 --- a/src/rendering/utils/AccidentalHelper.ts +++ b/src/rendering/utils/AccidentalHelper.ts @@ -1,13 +1,14 @@ import { AccidentalType } from '@src/model/AccidentalType'; -import { Bar } from '@src/model/Bar'; -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; +import type { Bar } from '@src/model/Bar'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; import { ModelUtils } from '@src/model/ModelUtils'; import { PercussionMapper } from '@src/model/PercussionMapper'; -import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; -import { LineBarRenderer } from '../LineBarRenderer'; -import { KeySignature } from '@src/model'; +import type { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; +import type { LineBarRenderer } from '@src/rendering/LineBarRenderer'; +import type { Clef } from '@src/model/Clef'; +import type { KeySignature } from '@src/model/KeySignature'; class BeatLines { public maxLine: number = -1000; @@ -52,7 +53,18 @@ export class AccidentalHelper { */ // prettier-ignore public static AccidentalNotes: boolean[] = [ - false, true, false, true, false, false, true, false, true, false, true, false + false, + true, + false, + true, + false, + false, + true, + false, + true, + false, + true, + false ]; /** @@ -112,9 +124,8 @@ export class AccidentalHelper { public static getPercussionLine(bar: Bar, noteValue: number): number { if (noteValue < bar.staff.track.percussionArticulations.length) { return bar.staff.track.percussionArticulations[noteValue]!.staffLine; - } else { - return PercussionMapper.getArticulationByValue(noteValue)?.staffLine ?? 0; } + return PercussionMapper.getArticulationByInputMidiNumber(noteValue)?.staffLine ?? 0; } public static getNoteValue(note: Note) { @@ -151,7 +162,7 @@ export class AccidentalHelper { */ public applyAccidental(note: Note): AccidentalType { const noteValue = AccidentalHelper.getNoteValue(note); - let quarterBend: boolean = note.hasQuarterToneOffset; + const quarterBend: boolean = note.hasQuarterToneOffset; return this.getAccidental(noteValue, quarterBend, note.beat, false, note); } @@ -177,10 +188,10 @@ export class AccidentalHelper { let line: number = 0; const noteValue = AccidentalHelper.getNoteValue(note); - if (bar.staff.isPercussion) { + if (note.isPercussion) { line = AccidentalHelper.getPercussionLine(bar, noteValue); } else { - line = AccidentalHelper.calculateNoteLine(bar, noteValue); + line = AccidentalHelper.calculateNoteSteps(bar.keySignature, bar.clef, noteValue); } return line; } @@ -192,13 +203,13 @@ export class AccidentalHelper { quarterBend: boolean, currentAccidental: AccidentalType | null = null ) { - let ks: number = keySignature; - let ksi: number = ks + 7; - let index: number = noteValue % 12; + const ks: number = keySignature; + const ksi: number = ks + 7; + const index: number = noteValue % 12; - let accidentalForKeySignature: AccidentalType = ksi < 7 ? AccidentalType.Flat : AccidentalType.Sharp; - let hasKeySignatureAccidentalSetForNote: boolean = AccidentalHelper.KeySignatureLookup[ksi][index]; - let hasNoteAccidentalWithinOctave: boolean = AccidentalHelper.AccidentalNotes[index]; + const accidentalForKeySignature: AccidentalType = ksi < 7 ? AccidentalType.Flat : AccidentalType.Sharp; + const hasKeySignatureAccidentalSetForNote: boolean = AccidentalHelper.KeySignatureLookup[ksi][index]; + const hasNoteAccidentalWithinOctave: boolean = AccidentalHelper.AccidentalNotes[index]; // the general logic is like this: // - we check if the key signature has an accidental defined @@ -267,7 +278,7 @@ export class AccidentalHelper { // if we don't want an accidental, but there is already one applied, we place a naturalize accidental // and clear the registration if (currentAccidental !== null) { - if(currentAccidental === AccidentalType.Natural) { + if (currentAccidental === AccidentalType.Natural) { accidentalToSet = AccidentalType.None; } else { accidentalToSet = AccidentalType.Natural; @@ -286,22 +297,23 @@ export class AccidentalHelper { isHelperNote: boolean, note: Note | null = null ): AccidentalType { - let line: number = 0; + let steps: number = 0; let accidentalToSet = AccidentalType.None; - if (this._bar.staff.isPercussion) { - line = AccidentalHelper.getPercussionLine(this._bar, noteValue); + const isPercussion = note != null ? note.isPercussion : this._bar.staff.isPercussion; + if (isPercussion) { + steps = AccidentalHelper.getPercussionLine(this._bar, noteValue); } else { const accidentalMode = note ? note.accidentalMode : NoteAccidentalMode.Default; - line = AccidentalHelper.calculateNoteLine(this._bar, noteValue); + steps = AccidentalHelper.calculateNoteSteps(this._bar.keySignature, this._bar.clef, noteValue); - const currentAccidental = this._registeredAccidentals.has(line) - ? this._registeredAccidentals.get(line)! + const currentAccidental = this._registeredAccidentals.has(steps) + ? this._registeredAccidentals.get(steps)! : null; accidentalToSet = AccidentalHelper.computeAccidental( - this._bar.masterBar.keySignature, + this._bar.keySignature, accidentalMode, noteValue, quarterBend, @@ -321,10 +333,13 @@ export class AccidentalHelper { // 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 - const previousRenderer = this._barRenderer.previousRenderer as ScoreBarRenderer; - if (previousRenderer) { - const tieOriginLine = previousRenderer.accidentalHelper.getNoteLine(note.tieOrigin!); - if (tieOriginLine === line) { + const tieOriginBarRenderer = this._barRenderer.scoreRenderer.layout?.getRendererForBar( + 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) { skipAccidental = true; } } @@ -335,7 +350,7 @@ export class AccidentalHelper { } else { // do we need an accidental on the note? if (accidentalToSet !== AccidentalType.None) { - this._registeredAccidentals.set(line, accidentalToSet); + this._registeredAccidentals.set(steps, accidentalToSet); } } break; @@ -343,23 +358,23 @@ export class AccidentalHelper { } if (note) { - this._appliedScoreLines.set(note.id, line); + this._appliedScoreLines.set(note.id, steps); this._notesByValue.set(noteValue, note); } else { - this._appliedScoreLinesByValue.set(noteValue, line); + this._appliedScoreLinesByValue.set(noteValue, steps); } - if (this.minLine === -1000 || this.minLine < line) { - this.minLine = line; + if (this.minLine === -1000 || this.minLine < steps) { + this.minLine = steps; this.minLineBeat = relatedBeat; } - if (this.maxLine === -1000 || this.maxLine > line) { - this.maxLine = line; + if (this.maxLine === -1000 || this.maxLine > steps) { + this.maxLine = steps; this.maxLineBeat = relatedBeat; } if (!isHelperNote) { - this.registerLine(relatedBeat, line); + this.registerLine(relatedBeat, steps); } return accidentalToSet; @@ -389,19 +404,19 @@ export class AccidentalHelper { return this._beatLines.has(b.id) ? this._beatLines.get(b.id)!.minLine : 0; } - public static calculateNoteLine(bar: Bar, noteValue: number): number { - let value: number = noteValue; - let ks: number = bar.masterBar.keySignature; - let clef: number = bar.clef as number; - let index: number = value % 12; - let octave: number = ((value / 12) | 0) - 1; + public static calculateNoteSteps(keySignature: KeySignature, clef: Clef, noteValue: number): number { + const value: number = noteValue; + const ks: number = keySignature as number; + const clefValue: number = clef as number; + const index: number = value % 12; + const octave: number = ((value / 12) | 0) - 1; // Initial Position - let steps: number = AccidentalHelper.OctaveSteps[clef]; + let steps: number = AccidentalHelper.OctaveSteps[clefValue]; // Move to Octave steps -= octave * AccidentalHelper.StepsPerOctave; // get the step list for the current keySignature - let stepList = + const stepList = ModelUtils.keySignatureIsSharp(ks) || ModelUtils.keySignatureIsNatural(ks) ? AccidentalHelper.SharpNoteSteps : AccidentalHelper.FlatNoteSteps; diff --git a/src/rendering/utils/BarBounds.ts b/src/rendering/utils/BarBounds.ts index 850b01941..4da8ab66c 100644 --- a/src/rendering/utils/BarBounds.ts +++ b/src/rendering/utils/BarBounds.ts @@ -1,7 +1,7 @@ -import { Bar } from '@src/model/Bar'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; -import { Bounds } from '@src/rendering/utils/Bounds'; -import { MasterBarBounds } from '@src/rendering/utils/MasterBarBounds'; +import type { Bar } from '@src/model/Bar'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import type { Bounds } from '@src/rendering/utils/Bounds'; +import type { MasterBarBounds } from '@src/rendering/utils/MasterBarBounds'; /** * Represents the boundaries of a single bar. @@ -49,7 +49,7 @@ export class BarBounds { */ public findBeatAtPos(x: number): BeatBounds | null { let beat: BeatBounds | null = null; - for (let t of this.beats) { + for (const t of this.beats) { if (!beat || t.realBounds.x < x) { beat = t; } else if (t.realBounds.x > x) { @@ -67,7 +67,7 @@ export class BarBounds { this.visualBounds.scaleWith(scale); this.beats.sort((a, b) => a.realBounds.x - b.realBounds.x); - for(const b of this.beats){ + for (const b of this.beats) { b.finish(scale); } } diff --git a/src/rendering/utils/BarCollisionHelper.ts b/src/rendering/utils/BarCollisionHelper.ts index bf301ea19..3d702b934 100644 --- a/src/rendering/utils/BarCollisionHelper.ts +++ b/src/rendering/utils/BarCollisionHelper.ts @@ -1,5 +1,5 @@ -import { Beat } from "@src/model/Beat"; -import { BeamingHelper } from "./BeamingHelper"; +import type { Beat } from '@src/model/Beat'; +import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; export class ReservedLayoutAreaSlot { public topY: number = 0; @@ -40,12 +40,12 @@ export class ReservedLayoutArea { export class BarCollisionHelper { public reservedLayoutAreasByDisplayTime: Map = new Map(); - public restDurationsByDisplayTime: Map> = new Map(); + public restDurationsByDisplayTime: Map> = new Map(); public getBeatMinMaxY(): number[] { let minY = -1000; let maxY = -1000; - for(const v of this.reservedLayoutAreasByDisplayTime.values()) { + for (const v of this.reservedLayoutAreasByDisplayTime.values()) { if (minY === -1000) { minY = v.topY; maxY = v.bottomY; @@ -66,7 +66,7 @@ export class BarCollisionHelper { } public reserveBeatSlot(beat: Beat, topY: number, bottomY: number): void { - if (topY == bottomY) { + if (topY === bottomY) { return; } if (!this.reservedLayoutAreasByDisplayTime.has(beat.displayStart)) { @@ -92,24 +92,26 @@ export class BarCollisionHelper { // we just place it normally if (beat.voice.index > 0) { // From the Spring-Rod poisitioning we have the guarantee - // that 2 timewise subsequent elements can never collide + // that 2 timewise subsequent elements can never collide // on the horizontal axis. So we only need to check for collisions // of elements at the current time position // if there are none, we can just use the line if (this.reservedLayoutAreasByDisplayTime.has(beat.playbackStart)) { - // do check for collisions we need to obtain the range on which the + // do check for collisions we need to obtain the range on which the // restglyph is placed - // rest glyphs have their ancor + // rest glyphs have their ancor const restSizes = BeamingHelper.computeLineHeightsForRest(beat.duration).map(i => i * linesToPixel); - let oldRestTopY = currentY - restSizes[0]; - let oldRestBottomY = currentY + restSizes[1]; + const oldRestTopY = currentY - restSizes[0]; + const oldRestBottomY = currentY + restSizes[1]; let newRestTopY = oldRestTopY; const reservedSlots = this.reservedLayoutAreasByDisplayTime.get(beat.playbackStart)!; let hasCollision = false; for (const slot of reservedSlots.slots) { - if ((oldRestTopY >= slot.topY && oldRestTopY <= slot.bottomY) || - (oldRestBottomY >= slot.topY && oldRestBottomY <= slot.bottomY)) { + if ( + (oldRestTopY >= slot.topY && oldRestTopY <= slot.bottomY) || + (oldRestBottomY >= slot.topY && oldRestBottomY <= slot.bottomY) + ) { hasCollision = true; break; } @@ -117,7 +119,7 @@ export class BarCollisionHelper { if (hasCollision) { // second voice above, the others below - if (beat.voice.index == 1) { + if (beat.voice.index === 1) { // move rest above top position // TODO: rest must align with note lines newRestTopY = reservedSlots.topY - restSizes[1] - restSizes[0]; @@ -127,24 +129,23 @@ export class BarCollisionHelper { newRestTopY = reservedSlots.bottomY; } - let newRestBottomY = newRestTopY + restSizes[0] + restSizes[1]; + const newRestBottomY = newRestTopY + restSizes[0] + restSizes[1]; // moving always happens in full stave spaces const staveSpace = linesToPixel * 2; - let distanceInLines = Math.ceil(Math.abs(newRestTopY - oldRestTopY) / staveSpace); + const distanceInLines = Math.ceil(Math.abs(newRestTopY - oldRestTopY) / staveSpace); // register new min/max offsets reservedSlots.addSlot(newRestTopY, newRestBottomY); if (newRestTopY < oldRestTopY) { return distanceInLines * -staveSpace; - } else { - return distanceInLines * staveSpace; } + return distanceInLines * staveSpace; } } } return 0; } -} \ No newline at end of file +} diff --git a/src/rendering/utils/BarHelpers.ts b/src/rendering/utils/BarHelpers.ts index 4b72d9478..cce6d3699 100644 --- a/src/rendering/utils/BarHelpers.ts +++ b/src/rendering/utils/BarHelpers.ts @@ -1,10 +1,10 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { GraceType } from '@src/model/GraceType'; -import { Voice } from '@src/model/Voice'; +import type { Voice } from '@src/model/Voice'; import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; -import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import type { BarRendererBase } from '@src/rendering/BarRendererBase'; import { BarCollisionHelper } from '@src/rendering/utils/BarCollisionHelper'; -import { BeamDirection } from './BeamDirection'; +import type { BeamDirection } from '@src/rendering/utils/BeamDirection'; export class BarHelpers { private _renderer: BarRendererBase; @@ -19,17 +19,17 @@ export class BarHelpers { } public initialize() { - var barRenderer = this._renderer; - var bar = this._renderer.bar; + const barRenderer = this._renderer; + const bar = this._renderer.bar; let currentBeamHelper: BeamingHelper | null = null; let currentGraceBeamHelper: BeamingHelper | null = null; for (let i: number = 0, j: number = bar.voices.length; i < j; i++) { - let v: Voice = bar.voices[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++) { - let b: Beat = v.beats[k]; + const b: Beat = v.beats[k]; let helperForBeat: BeamingHelper | null; if (b.graceType !== GraceType.None) { helperForBeat = currentGraceBeamHelper; diff --git a/src/rendering/utils/BeamBarType.ts b/src/rendering/utils/BeamBarType.ts index 36723eb47..4e12f4289 100644 --- a/src/rendering/utils/BeamBarType.ts +++ b/src/rendering/utils/BeamBarType.ts @@ -5,13 +5,13 @@ export enum BeamBarType { /** * Full Bar from current to next */ - Full, + Full = 0, /** * A small Bar from current to previous */ - PartLeft, + PartLeft = 1, /** * A small bar from current to next */ - PartRight + PartRight = 2 } diff --git a/src/rendering/utils/BeamDirection.ts b/src/rendering/utils/BeamDirection.ts index 451022f63..892da574c 100644 --- a/src/rendering/utils/BeamDirection.ts +++ b/src/rendering/utils/BeamDirection.ts @@ -1,4 +1,4 @@ export enum BeamDirection { - Up, - Down -} \ No newline at end of file + Up = 0, + Down = 1 +} diff --git a/src/rendering/utils/BeamingHelper.ts b/src/rendering/utils/BeamingHelper.ts index 76f3e0454..b6da03e61 100644 --- a/src/rendering/utils/BeamingHelper.ts +++ b/src/rendering/utils/BeamingHelper.ts @@ -1,16 +1,16 @@ -import { Bar } from '@src/model/Bar'; -import { Beat, BeatBeamingMode } from '@src/model/Beat'; +import type { Bar } from '@src/model/Bar'; +import { type Beat, BeatBeamingMode } from '@src/model/Beat'; import { Duration } from '@src/model/Duration'; import { GraceType } from '@src/model/GraceType'; import { HarmonicType } from '@src/model/HarmonicType'; -import { Note } from '@src/model/Note'; -import { Staff } from '@src/model/Staff'; -import { Voice } from '@src/model/Voice'; +import type { Note } from '@src/model/Note'; +import type { Staff } from '@src/model/Staff'; +import type { Voice } from '@src/model/Voice'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { ModelUtils } from '@src/model/ModelUtils'; import { MidiUtils } from '@src/midi/MidiUtils'; import { AccidentalHelper } from '@src/rendering/utils/AccidentalHelper'; -import { BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; +import { type BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; class BeatLinePositions { public staffId: string = ''; @@ -108,7 +108,9 @@ export class BeamingHelper { } private beatHasFlag(beat: Beat) { - return !beat.isRest && (beat.duration > Duration.Quarter || beat.graceType !== GraceType.None); + return ( + !beat.deadSlapped && !beat.isRest && (beat.duration > Duration.Quarter || beat.graceType !== GraceType.None) + ); } public constructor(staff: Staff, renderer: BarRendererBase) { @@ -134,14 +136,14 @@ export class BeamingHelper { } public registerBeatLineX(staffId: string, beat: Beat, up: number, down: number): void { - let positions: BeatLinePositions = this.getOrCreateBeatPositions(beat); + const positions: BeatLinePositions = this.getOrCreateBeatPositions(beat); positions.staffId = staffId; positions.up = up; positions.down = down; for (const v of this.drawingInfos.values()) { - if (v.startBeat == beat) { + if (v.startBeat === beat) { v.startX = this.getBeatLineX(beat); - } else if (v.endBeat == beat) { + } else if (v.endBeat === beat) { v.endX = this.getBeatLineX(beat); } } @@ -182,8 +184,8 @@ export class BeamingHelper { // key lowerequal than middle line -> up // key higher than middle line -> down if (this.highestNoteInHelper && this.lowestNoteInHelper) { - let highestNotePosition = this._renderer.getNoteY(this.highestNoteInHelper, NoteYPosition.Center); - let lowestNotePosition = this._renderer.getNoteY(this.lowestNoteInHelper, NoteYPosition.Center); + const highestNotePosition = this._renderer.getNoteY(this.highestNoteInHelper, NoteYPosition.Center); + const lowestNotePosition = this._renderer.getNoteY(this.lowestNoteInHelper, NoteYPosition.Center); if (direction === null) { const avg = (highestNotePosition + lowestNotePosition) / 2; @@ -406,23 +408,25 @@ export class BeamingHelper { !b2 || b1.graceType !== b2.graceType || b1.graceType === GraceType.BendGrace || - b2.graceType === GraceType.BendGrace + b2.graceType === GraceType.BendGrace || + b1.deadSlapped || + b2.deadSlapped ) { return false; } if (b1.graceType !== GraceType.None && b2.graceType !== GraceType.None) { return true; } - let m1: Bar = b1.voice.bar; - let m2: Bar = b2.voice.bar; + const m1: Bar = b1.voice.bar; + const m2: Bar = b2.voice.bar; // only join on same measure if (m1 !== m2) { return false; } // get times of those voices and check if the times // are in the same division - let start1: number = b1.playbackStart; - let start2: number = b2.playbackStart; + const start1: number = b1.playbackStart; + const start2: number = b2.playbackStart; // we can only join 8th, 16th, 32th and 64th voices if (!BeamingHelper.canJoinDuration(b1.duration) || !BeamingHelper.canJoinDuration(b2.duration)) { return start1 === start2; @@ -447,8 +451,8 @@ export class BeamingHelper { break; } // check if they are on the same division - let division1: number = ((divisionLength + start1) / divisionLength) | 0 | 0; - let division2: number = ((divisionLength + start2) / divisionLength) | 0 | 0; + const division1: number = ((divisionLength + start1) / divisionLength) | 0 | 0; + const division2: number = ((divisionLength + start2) / divisionLength) | 0 | 0; return division1 === division2; } diff --git a/src/rendering/utils/BeatBounds.ts b/src/rendering/utils/BeatBounds.ts index 50af27a4e..62190de91 100644 --- a/src/rendering/utils/BeatBounds.ts +++ b/src/rendering/utils/BeatBounds.ts @@ -1,8 +1,8 @@ -import { Beat } from '@src/model/Beat'; -import { Note } from '@src/model/Note'; -import { BarBounds } from '@src/rendering/utils/BarBounds'; -import { Bounds } from '@src/rendering/utils/Bounds'; -import { NoteBounds } from '@src/rendering/utils/NoteBounds'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import type { BarBounds } from '@src/rendering/utils/BarBounds'; +import type { Bounds } from '@src/rendering/utils/Bounds'; +import type { NoteBounds } from '@src/rendering/utils/NoteBounds'; /** * Represents the bounds of a single beat. @@ -19,7 +19,7 @@ export class BeatBounds { public visualBounds!: Bounds; /** - * Gets or sets x-position where the timely center of the notes for this beat is. + * Gets or sets x-position where the timely center of the notes for this beat is. * This is where the cursor should be at the time when this beat is played. */ public onNotesX: number = 0; @@ -65,9 +65,9 @@ export class BeatBounds { // TODO: 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 (let note of notes) { - let bottom: number = note.noteHeadBounds.y + note.noteHeadBounds.h; - let right: number = note.noteHeadBounds.x + note.noteHeadBounds.w; + for (const note of notes) { + const bottom: number = note.noteHeadBounds.y + note.noteHeadBounds.h; + const right: number = note.noteHeadBounds.x + note.noteHeadBounds.w; if (note.noteHeadBounds.x <= x && note.noteHeadBounds.y <= y && x <= right && y <= bottom) { return note.note; } @@ -82,9 +82,9 @@ export class BeatBounds { this.realBounds.scaleWith(scale); this.visualBounds.scaleWith(scale); this.onNotesX *= scale; - - if(this.notes){ - for(const n of this.notes!){ + + if (this.notes) { + for (const n of this.notes!) { n.finish(scale); } } diff --git a/src/rendering/utils/BoundsLookup.ts b/src/rendering/utils/BoundsLookup.ts index eb6be6ab6..2eeddfd7d 100644 --- a/src/rendering/utils/BoundsLookup.ts +++ b/src/rendering/utils/BoundsLookup.ts @@ -1,7 +1,7 @@ -import { Beat } from '@src/model/Beat'; -import { MasterBar } from '@src/model/MasterBar'; -import { Note } from '@src/model/Note'; -import { Score } from '@src/model/Score'; +import type { Beat } from '@src/model/Beat'; +import type { MasterBar } from '@src/model/MasterBar'; +import type { Note } from '@src/model/Note'; +import type { Score } from '@src/model/Score'; import { BarBounds } from '@src/rendering/utils/BarBounds'; import { BeatBounds } from '@src/rendering/utils/BeatBounds'; import { Bounds } from '@src/rendering/utils/Bounds'; @@ -14,42 +14,43 @@ export class BoundsLookup { * @target web */ public toJson(): unknown { - let json: any = {} as any; - let systems: StaffSystemBounds[] = []; + const json: any = {} as any; + const systems: StaffSystemBounds[] = []; json.staffSystems = systems; - for (let system of this.staffSystems) { - let g: StaffSystemBounds = {} as any; + for (const system of this.staffSystems) { + const g: StaffSystemBounds = {} as any; g.visualBounds = this.boundsToJson(system.visualBounds); g.realBounds = this.boundsToJson(system.realBounds); g.bars = []; - for (let masterBar of system.bars) { - let mb: MasterBarBounds = {} as any; + for (const masterBar of system.bars) { + const mb: MasterBarBounds = {} as any; mb.lineAlignedBounds = this.boundsToJson(masterBar.lineAlignedBounds); mb.visualBounds = this.boundsToJson(masterBar.visualBounds); mb.realBounds = this.boundsToJson(masterBar.realBounds); mb.index = masterBar.index; mb.bars = []; - for (let bar of masterBar.bars) { - let b: BarBounds = {} as any; + for (const bar of masterBar.bars) { + const b: BarBounds = {} as any; b.visualBounds = this.boundsToJson(bar.visualBounds); b.realBounds = this.boundsToJson(bar.realBounds); b.beats = []; - for (let beat of bar.beats) { - let bb: BeatBounds = {} as any; + for (const beat of bar.beats) { + const bb: BeatBounds = {} as any; bb.visualBounds = this.boundsToJson(beat.visualBounds); bb.realBounds = this.boundsToJson(beat.realBounds); bb.onNotesX = beat.onNotesX; - let bbd: any = bb; + const bbd: any = bb; bbd.beatIndex = beat.beat.index; bbd.voiceIndex = beat.beat.voice.index; bbd.barIndex = beat.beat.voice.bar.index; bbd.staffIndex = beat.beat.voice.bar.staff.index; bbd.trackIndex = beat.beat.voice.bar.staff.track.index; if (beat.notes) { - let notes: NoteBounds[] = (bb.notes = []); - for (let note of beat.notes) { - let n: NoteBounds = {} as any; - let nd: any = n; + const notes: NoteBounds[] = []; + bb.notes = notes; + for (const note of beat.notes) { + const n: NoteBounds = {} as any; + const nd: any = n; nd.index = note.note.index; n.noteHeadBounds = this.boundsToJson(note.noteHeadBounds); notes.push(n); @@ -70,41 +71,41 @@ export class BoundsLookup { * @target web */ public static fromJson(json: unknown, score: Score): BoundsLookup { - let lookup: BoundsLookup = new BoundsLookup(); - let staffSystems: StaffSystemBounds[] = (json as any)['staffSystems']; - for (let staffSystem of staffSystems) { - let sg: StaffSystemBounds = new StaffSystemBounds(); + const lookup: BoundsLookup = new BoundsLookup(); + const staffSystems: StaffSystemBounds[] = (json as any).staffSystems; + for (const staffSystem of staffSystems) { + const sg: StaffSystemBounds = new StaffSystemBounds(); sg.visualBounds = BoundsLookup.boundsFromJson(staffSystem.visualBounds); sg.realBounds = BoundsLookup.boundsFromJson(staffSystem.realBounds); lookup.addStaffSystem(sg); - for (let masterBar of staffSystem.bars) { - let mb: MasterBarBounds = new MasterBarBounds(); + for (const masterBar of staffSystem.bars) { + const mb: MasterBarBounds = new MasterBarBounds(); mb.index = masterBar.index; mb.isFirstOfLine = masterBar.isFirstOfLine; mb.lineAlignedBounds = BoundsLookup.boundsFromJson(masterBar.lineAlignedBounds); mb.visualBounds = BoundsLookup.boundsFromJson(masterBar.visualBounds); mb.realBounds = BoundsLookup.boundsFromJson(masterBar.realBounds); sg.addBar(mb); - for (let bar of masterBar.bars) { - let b: BarBounds = new BarBounds(); + for (const bar of masterBar.bars) { + const b: BarBounds = new BarBounds(); b.visualBounds = BoundsLookup.boundsFromJson(bar.visualBounds); b.realBounds = BoundsLookup.boundsFromJson(bar.realBounds); mb.addBar(b); - for (let beat of bar.beats) { - let bb: BeatBounds = new BeatBounds(); + for (const beat of bar.beats) { + const bb: BeatBounds = new BeatBounds(); bb.visualBounds = BoundsLookup.boundsFromJson(beat.visualBounds); bb.realBounds = BoundsLookup.boundsFromJson(beat.realBounds); bb.onNotesX = beat.onNotesX; - let bd: any = beat; + const bd: any = beat; bb.beat = score.tracks[bd.trackIndex].staves[bd.staffIndex].bars[bd.barIndex].voices[ bd.voiceIndex ].beats[bd.beatIndex]; if (beat.notes) { bb.notes = []; - for (let note of beat.notes) { - let n: NoteBounds = new NoteBounds(); - let nd: any = note; + for (const note of beat.notes) { + const n: NoteBounds = new NoteBounds(); + const nd: any = note; n.note = bb.beat.notes[nd.index]; n.noteHeadBounds = BoundsLookup.boundsFromJson(note.noteHeadBounds); bb.addNote(n); @@ -136,7 +137,7 @@ export class BoundsLookup { * @target web */ private boundsToJson(bounds: Bounds): Bounds { - let json: Bounds = {} as any; + const json: Bounds = {} as any; json.x = bounds.x; json.y = bounds.y; json.w = bounds.w; @@ -161,7 +162,7 @@ export class BoundsLookup { * Finishes the lookup for optimized access. */ public finish(scale: number = 1): void { - for (let t of this.staffSystems) { + for (const t of this.staffSystems) { t.finish(scale); } this.isFinished = true; @@ -221,7 +222,7 @@ export class BoundsLookup { * @returns The master bar bounds if it was rendered, or null if no boundary information is available. */ public findMasterBar(bar: MasterBar): MasterBarBounds | null { - let id: number = bar.index; + const id: number = bar.index; if (this._masterBarLookup.has(id)) { return this._masterBarLookup.get(id)!; } @@ -244,7 +245,7 @@ export class BoundsLookup { * @returns The beat bounds if it was rendered, or null if no boundary information is available. */ public findBeats(beat: Beat): BeatBounds[] | null { - let id: number = beat.id; + const id: number = beat.id; if (this._beatLookup.has(id)) { return this._beatLookup.get(id)!; } @@ -264,8 +265,8 @@ export class BoundsLookup { let top: number = this.staffSystems.length - 1; let staffSystemIndex: number = -1; while (bottom <= top) { - let middle: number = ((top + bottom) / 2) | 0; - let system: StaffSystemBounds = this.staffSystems[middle]; + const middle: number = ((top + bottom) / 2) | 0; + const system: StaffSystemBounds = this.staffSystems[middle]; // found? if (y >= system.realBounds.y && y <= system.realBounds.y + system.realBounds.h) { staffSystemIndex = middle; @@ -284,8 +285,8 @@ export class BoundsLookup { } // // Find the matching bar in the row - let staffSystem: StaffSystemBounds = this.staffSystems[staffSystemIndex]; - let bar: MasterBarBounds | null = staffSystem.findBarAtPos(x); + const staffSystem: StaffSystemBounds = this.staffSystems[staffSystemIndex]; + const bar: MasterBarBounds | null = staffSystem.findBarAtPos(x); if (bar) { return bar.findBeatAtPos(x, y); } diff --git a/src/rendering/utils/ElementStyleHelper.ts b/src/rendering/utils/ElementStyleHelper.ts new file mode 100644 index 000000000..940d27c98 --- /dev/null +++ b/src/rendering/utils/ElementStyleHelper.ts @@ -0,0 +1,250 @@ +import { BarSubElement, type Bar } from '@src/model/Bar'; +import type { BeatSubElement, Beat } from '@src/model/Beat'; +import type { Color } from '@src/model/Color'; +import type { ElementStyle } from '@src/model/ElementStyle'; +import type { NoteSubElement, Note } from '@src/model/Note'; +import type { Score, ScoreSubElement } from '@src/model/Score'; +import { type Track, TrackSubElement } from '@src/model/Track'; +import type { VoiceSubElement, Voice } from '@src/model/Voice'; +import type { ICanvas } from '@src/platform/ICanvas'; +import type { RenderingResources } from '@src/RenderingResources'; + +/** + * A helper to apply element styles in a specific rendering scope via the `using` keyword + */ +export class ElementStyleHelper { + public static score( + canvas: ICanvas, + element: ScoreSubElement, + score: Score, + forceDefault: boolean = false + ): Disposable | undefined { + if (!score.style && !forceDefault) { + return undefined; + } + + const defaultColor: Color = ElementStyleHelper.scoreDefaultColor(canvas.settings.display.resources, element); + + return new ElementStyleScope(canvas, element, score.style, defaultColor); + } + + public static scoreColor(res: RenderingResources, element: ScoreSubElement, score: Score): Color | undefined { + const defaultColor: Color = ElementStyleHelper.scoreDefaultColor(res, element); + + if (score.style && score.style!.colors.has(element)) { + return score.style!.colors.get(element) ?? defaultColor; + } + + return undefined; + } + + private static scoreDefaultColor(res: RenderingResources, element: ScoreSubElement) { + const defaultColor: Color = res.mainGlyphColor; + + return defaultColor; + } + + public static bar( + canvas: ICanvas, + element: BarSubElement, + bar: Bar, + forceDefault: boolean = false + ): Disposable | undefined { + if (!bar.style && !forceDefault) { + return undefined; + } + + let defaultColor: Color = canvas.settings.display.resources.mainGlyphColor; + switch (element) { + case BarSubElement.StandardNotationRepeats: + case BarSubElement.GuitarTabsRepeats: + case BarSubElement.SlashRepeats: + case BarSubElement.NumberedRepeats: + case BarSubElement.StandardNotationClef: + case BarSubElement.GuitarTabsClef: + case BarSubElement.StandardNotationKeySignature: + case BarSubElement.NumberedKeySignature: + case BarSubElement.StandardNotationTimeSignature: + case BarSubElement.GuitarTabsTimeSignature: + case BarSubElement.SlashTimeSignature: + case BarSubElement.NumberedTimeSignature: + break; + + case BarSubElement.StandardNotationBarLines: + case BarSubElement.GuitarTabsBarLines: + case BarSubElement.SlashBarLines: + case BarSubElement.NumberedBarLines: + defaultColor = canvas.settings.display.resources.barSeparatorColor; + break; + + case BarSubElement.StandardNotationBarNumber: + case BarSubElement.SlashBarNumber: + case BarSubElement.NumberedBarNumber: + case BarSubElement.GuitarTabsBarNumber: + defaultColor = canvas.settings.display.resources.barNumberColor; + break; + + case BarSubElement.StandardNotationStaffLine: + case BarSubElement.GuitarTabsStaffLine: + case BarSubElement.SlashStaffLine: + case BarSubElement.NumberedStaffLine: + defaultColor = canvas.settings.display.resources.staffLineColor; + break; + } + + return new ElementStyleScope(canvas, element, bar.style, defaultColor); + } + + public static voice( + canvas: ICanvas, + element: VoiceSubElement, + voice: Voice, + forceDefault: boolean = false + ): Disposable | undefined { + if (!voice.style && !forceDefault) { + return undefined; + } + + const defaultColor: Color = + voice.index === 0 + ? canvas.settings.display.resources.mainGlyphColor + : canvas.settings.display.resources.secondaryGlyphColor; + + return new ElementStyleScope(canvas, element, voice.style, defaultColor); + } + + public static trackColor(res: RenderingResources, element: TrackSubElement, track: Track): Color | undefined { + const defaultColor = ElementStyleHelper.trackDefaultColor(res, element); + + if (track.style && track.style!.colors.has(element)) { + return track.style!.colors.get(element) ?? defaultColor; + } + + return undefined; + } + + private static trackDefaultColor(res: RenderingResources, element: TrackSubElement) { + let defaultColor: Color = res.mainGlyphColor; + switch (element) { + case TrackSubElement.TrackName: + case TrackSubElement.SystemSeparator: + case TrackSubElement.StringTuning: + break; + case TrackSubElement.BracesAndBrackets: + defaultColor = res.barSeparatorColor; + break; + } + + return defaultColor; + } + + public static track( + canvas: ICanvas, + element: TrackSubElement, + track: Track, + forceDefault: boolean = false + ): Disposable | undefined { + if (!track.style && !forceDefault) { + return undefined; + } + + const defaultColor = ElementStyleHelper.trackDefaultColor(canvas.settings.display.resources, element); + + return new ElementStyleScope(canvas, element, track.style, defaultColor); + } + + public static beatColor(res: RenderingResources, element: BeatSubElement, beat: Beat): Color | undefined { + const defaultColor = ElementStyleHelper.beatDefaultColor(res, element, beat); + + if (beat.style && beat.style!.colors.has(element)) { + return beat.style!.colors.get(element) ?? defaultColor; + } + + return undefined; + } + + private static beatDefaultColor(res: RenderingResources, element: BeatSubElement, beat: Beat) { + const defaultColor: Color = beat.voice.index === 0 ? res.mainGlyphColor : res.secondaryGlyphColor; + + return defaultColor; + } + public static beat( + canvas: ICanvas, + element: BeatSubElement, + beat: Beat, + forceDefault: boolean = false + ): Disposable | undefined { + if (!beat.style && !forceDefault) { + return undefined; + } + + const defaultColor = ElementStyleHelper.beatDefaultColor(canvas.settings.display.resources, element, beat); + + return new ElementStyleScope(canvas, element, beat.style, defaultColor); + } + + public static noteColor(res: RenderingResources, element: NoteSubElement, note: Note): Color | undefined { + const defaultColor = ElementStyleHelper.noteDefaultColor(res, element, note); + + if (note.style && note.style!.colors.has(element)) { + return note.style!.colors.get(element) ?? defaultColor; + } + + return undefined; + } + + private static noteDefaultColor(res: RenderingResources, element: NoteSubElement, note: Note) { + const defaultColor: Color = note.beat.voice.index === 0 ? res.mainGlyphColor : res.secondaryGlyphColor; + + return defaultColor; + } + + public static note( + canvas: ICanvas, + element: NoteSubElement, + note: Note, + forceDefault: boolean = false + ): Disposable | undefined { + if (!note.style && !forceDefault) { + return undefined; + } + + const defaultColor: Color = + note.beat.voice.index === 0 + ? canvas.settings.display.resources.mainGlyphColor + : canvas.settings.display.resources.secondaryGlyphColor; + + return new ElementStyleScope(canvas, element, note.style, defaultColor); + } +} + +/** + * A helper class for applying elements styles to the canvas and restoring the previous state afterwards. + */ +class ElementStyleScope implements Disposable { + private _canvas: ICanvas; + private _previousColor?: Color; + + public constructor( + canvas: ICanvas, + element: TSubElement, + container: ElementStyle | undefined, + defaultColor: Color + ) { + this._canvas = canvas; + + if (container && container.colors.has(element)) { + this._previousColor = canvas.color; + canvas.color = container.colors.get(element) ?? defaultColor; + } else if (!container) { + this._previousColor = canvas.color; + canvas.color = defaultColor; + } + } + + [Symbol.dispose]() { + if (this._previousColor) { + this._canvas.color = this._previousColor!; + } + } +} diff --git a/src/rendering/utils/MasterBarBounds.ts b/src/rendering/utils/MasterBarBounds.ts index 71c244f52..23df9ddc8 100644 --- a/src/rendering/utils/MasterBarBounds.ts +++ b/src/rendering/utils/MasterBarBounds.ts @@ -1,8 +1,8 @@ -import { Beat } from '@src/model/Beat'; -import { BarBounds } from '@src/rendering/utils/BarBounds'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; -import { Bounds } from '@src/rendering/utils/Bounds'; -import { StaffSystemBounds } from '@src/rendering/utils/StaffSystemBounds'; +import type { Beat } from '@src/model/Beat'; +import type { BarBounds } from '@src/rendering/utils/BarBounds'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import type { Bounds } from '@src/rendering/utils/Bounds'; +import type { StaffSystemBounds } from '@src/rendering/utils/StaffSystemBounds'; /** * Represents the boundaries of a list of bars related to a single master bar. @@ -68,9 +68,9 @@ export class MasterBarBounds { */ public findBeatAtPos(x: number, y: number): Beat | null { let beat: BeatBounds | null = null; - let distance = 10000000; - for (let bar of this.bars) { - let b = bar.findBeatAtPos(x); + const distance = 10000000; + for (const bar of this.bars) { + const b = bar.findBeatAtPos(x); if (b && (!beat || beat.realBounds.x < b.realBounds.x)) { const newDistance = Math.abs(b.realBounds.x - x); if (!beat || newDistance < distance) { diff --git a/src/rendering/utils/MusicFontSymbolSizes.ts b/src/rendering/utils/MusicFontSymbolSizes.ts new file mode 100644 index 000000000..f722b3d9e --- /dev/null +++ b/src/rendering/utils/MusicFontSymbolSizes.ts @@ -0,0 +1,578 @@ +import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; + +/** + * Stores the sizes for the {@link MusicFontSymbol} glyphs. + */ +export class MusicFontSymbolSizes { + // NOTE: with https://github.com/CoderLine/alphaTab/issues/1949 we fill this list properly from the input font metadata + + /** + * The widths of the bounding box for the respective glyphs. + */ + public static readonly Widths: Map = new Map([ + [MusicFontSymbol.Brace, 3], + [MusicFontSymbol.BracketTop, 0], + [MusicFontSymbol.BracketBottom, 0], + [MusicFontSymbol.SystemDivider, 0], + + [MusicFontSymbol.GClef, 28], + [MusicFontSymbol.CClef, 28], + [MusicFontSymbol.FClef, 28], + [MusicFontSymbol.UnpitchedPercussionClef1, 15], + [MusicFontSymbol.SixStringTabClef, 28], + [MusicFontSymbol.FourStringTabClef, 28], + + [MusicFontSymbol.TimeSig0, 14], + [MusicFontSymbol.TimeSig1, 10], + [MusicFontSymbol.TimeSig2, 14], + [MusicFontSymbol.TimeSig3, 14], + [MusicFontSymbol.TimeSig4, 14], + [MusicFontSymbol.TimeSig5, 14], + [MusicFontSymbol.TimeSig6, 14], + [MusicFontSymbol.TimeSig7, 14], + [MusicFontSymbol.TimeSig8, 14], + [MusicFontSymbol.TimeSig9, 14], + [MusicFontSymbol.TimeSigCommon, 14], + [MusicFontSymbol.TimeSigCutCommon, 14], + + [MusicFontSymbol.NoteheadDoubleWholeSquare, 14], + [MusicFontSymbol.NoteheadDoubleWhole, 14], + [MusicFontSymbol.NoteheadWhole, 14], + [MusicFontSymbol.NoteheadHalf, 9], + [MusicFontSymbol.NoteheadBlack, 9], + [MusicFontSymbol.NoteheadNull, 9], + [MusicFontSymbol.NoteheadXOrnate, 9], + + [MusicFontSymbol.NoteheadPlusDoubleWhole, 16], + [MusicFontSymbol.NoteheadPlusWhole, 10], + [MusicFontSymbol.NoteheadPlusHalf, 10], + [MusicFontSymbol.NoteheadPlusBlack, 10], + + [MusicFontSymbol.NoteheadSquareWhite, 11], + [MusicFontSymbol.NoteheadSquareBlack, 11], + + [MusicFontSymbol.NoteheadTriangleUpDoubleWhole, 16], + [MusicFontSymbol.NoteheadTriangleUpWhole, 11], + [MusicFontSymbol.NoteheadTriangleUpHalf, 11], + [MusicFontSymbol.NoteheadTriangleUpBlack, 11], + + [MusicFontSymbol.NoteheadTriangleRightWhite, 11], + [MusicFontSymbol.NoteheadTriangleRightBlack, 11], + + [MusicFontSymbol.NoteheadTriangleDownDoubleWhole, 16], + [MusicFontSymbol.NoteheadTriangleDownWhole, 11], + [MusicFontSymbol.NoteheadTriangleDownHalf, 11], + [MusicFontSymbol.NoteheadTriangleDownBlack, 11], + + [MusicFontSymbol.NoteheadDiamondDoubleWhole, 16], + [MusicFontSymbol.NoteheadDiamondWhole, 9], + [MusicFontSymbol.NoteheadDiamondHalf, 9], + [MusicFontSymbol.NoteheadDiamondBlack, 9], + + [MusicFontSymbol.NoteheadDiamondBlackWide, 10], + [MusicFontSymbol.NoteheadDiamondWhite, 9], + [MusicFontSymbol.NoteheadDiamondWhiteWide, 9], + [MusicFontSymbol.NoteheadCircleXDoubleWhole, 16], + [MusicFontSymbol.NoteheadCircleXWhole, 9], + [MusicFontSymbol.NoteheadCircleXHalf, 9], + [MusicFontSymbol.NoteheadCircleX, 9], + [MusicFontSymbol.NoteheadXDoubleWhole, 16], + [MusicFontSymbol.NoteheadXWhole, 9], + [MusicFontSymbol.NoteheadXHalf, 9], + [MusicFontSymbol.NoteheadXBlack, 9], + [MusicFontSymbol.NoteheadParenthesis, 9], + [MusicFontSymbol.NoteheadSlashedBlack1, 9], + [MusicFontSymbol.NoteheadSlashedBlack2, 9], + [MusicFontSymbol.NoteheadSlashedHalf1, 9], + [MusicFontSymbol.NoteheadSlashedHalf2, 9], + [MusicFontSymbol.NoteheadSlashedWhole1, 9], + [MusicFontSymbol.NoteheadSlashedWhole2, 9], + [MusicFontSymbol.NoteheadSlashedDoubleWhole1, 16], + [MusicFontSymbol.NoteheadSlashedDoubleWhole2, 16], + + [MusicFontSymbol.NoteheadCircledBlack, 9], + [MusicFontSymbol.NoteheadCircledHalf, 9], + [MusicFontSymbol.NoteheadCircledWhole, 9], + [MusicFontSymbol.NoteheadCircledDoubleWhole, 16], + + [MusicFontSymbol.NoteheadCircleSlash, 9], + [MusicFontSymbol.NoteheadHeavyX, 13], + [MusicFontSymbol.NoteheadHeavyXHat, 13], + + [MusicFontSymbol.NoteheadSlashVerticalEnds, 12], + [MusicFontSymbol.NoteheadSlashWhiteWhole, 32], + [MusicFontSymbol.NoteheadSlashWhiteHalf, 25], + + [MusicFontSymbol.NoteheadRoundWhiteWithDot, 9], + + [MusicFontSymbol.NoteheadSquareBlackLarge, 9], + [MusicFontSymbol.NoteheadSquareBlackWhite, 9], + + [MusicFontSymbol.NoteheadClusterDoubleWhole3rd, 16], + [MusicFontSymbol.NoteheadClusterWhole3rd, 12], + [MusicFontSymbol.NoteheadClusterHalf3rd, 12], + [MusicFontSymbol.NoteheadClusterQuarter3rd, 12], + + [MusicFontSymbol.NoteShapeRoundWhite, 9], + [MusicFontSymbol.NoteShapeRoundBlack, 9], + + [MusicFontSymbol.NoteShapeSquareWhite, 12], + [MusicFontSymbol.NoteShapeSquareBlack, 12], + + [MusicFontSymbol.NoteShapeTriangleRightWhite, 12], + [MusicFontSymbol.NoteShapeTriangleRightBlack, 12], + + [MusicFontSymbol.NoteShapeTriangleLeftWhite, 12], + [MusicFontSymbol.NoteShapeTriangleLeftBlack, 12], + + [MusicFontSymbol.NoteShapeDiamondWhite, 9], + [MusicFontSymbol.NoteShapeDiamondBlack, 9], + + [MusicFontSymbol.NoteShapeTriangleUpWhite, 12], + [MusicFontSymbol.NoteShapeTriangleUpBlack, 12], + + [MusicFontSymbol.NoteShapeMoonWhite, 12], + [MusicFontSymbol.NoteShapeMoonBlack, 12], + + [MusicFontSymbol.NoteShapeTriangleRoundWhite, 12], + [MusicFontSymbol.NoteShapeTriangleRoundBlack, 12], + + [MusicFontSymbol.NoteQuarterUp, 10], + [MusicFontSymbol.NoteEighthUp, 10], + + [MusicFontSymbol.TextBlackNoteLongStem, 0], + [MusicFontSymbol.TextBlackNoteFrac8thLongStem, 0], + [MusicFontSymbol.TextBlackNoteFrac16thLongStem, 0], + [MusicFontSymbol.TextBlackNoteFrac32ndLongStem, 0], + + [MusicFontSymbol.TextCont8thBeamLongStem, 0], + [MusicFontSymbol.TextCont16thBeamLongStem, 0], + [MusicFontSymbol.TextCont32ndBeamLongStem, 0], + + [MusicFontSymbol.TextAugmentationDot, 0], + [MusicFontSymbol.TextTupletBracketStartLongStem, 0], + [MusicFontSymbol.TextTuplet3LongStem, 0], + [MusicFontSymbol.TextTupletBracketEndLongStem, 0], + + [MusicFontSymbol.Tremolo3, 12], + [MusicFontSymbol.Tremolo2, 12], + [MusicFontSymbol.Tremolo1, 12], + + [MusicFontSymbol.FlagEighthUp, 0], + [MusicFontSymbol.FlagEighthDown, 0], + [MusicFontSymbol.FlagSixteenthUp, 0], + [MusicFontSymbol.FlagSixteenthDown, 0], + [MusicFontSymbol.FlagThirtySecondUp, 0], + [MusicFontSymbol.FlagThirtySecondDown, 0], + [MusicFontSymbol.FlagSixtyFourthUp, 0], + [MusicFontSymbol.FlagSixtyFourthDown, 0], + [MusicFontSymbol.FlagOneHundredTwentyEighthUp, 0], + [MusicFontSymbol.FlagOneHundredTwentyEighthDown, 0], + [MusicFontSymbol.FlagTwoHundredFiftySixthUp, 0], + [MusicFontSymbol.FlagTwoHundredFiftySixthDown, 0], + + [MusicFontSymbol.AccidentalFlat, 8], + [MusicFontSymbol.AccidentalNatural, 8], + [MusicFontSymbol.AccidentalSharp, 8], + [MusicFontSymbol.AccidentalDoubleSharp, 8], + [MusicFontSymbol.AccidentalDoubleFlat, 18], + [MusicFontSymbol.AccidentalQuarterToneFlatArrowUp, 8], + [MusicFontSymbol.AccidentalQuarterToneSharpArrowUp, 8], + [MusicFontSymbol.AccidentalQuarterToneNaturalArrowUp, 8], + + [MusicFontSymbol.Segno, 0], + [MusicFontSymbol.Coda, 0], + + [MusicFontSymbol.ArticAccentAbove, 9], + [MusicFontSymbol.ArticAccentBelow, 9], + [MusicFontSymbol.ArticStaccatoAbove, 9], + [MusicFontSymbol.ArticStaccatoBelow, 9], + [MusicFontSymbol.ArticTenutoAbove, 9], + [MusicFontSymbol.ArticTenutoBelow, 9], + [MusicFontSymbol.ArticMarcatoAbove, 9], + [MusicFontSymbol.ArticMarcatoBelow, 9], + + [MusicFontSymbol.FermataAbove, 23], + [MusicFontSymbol.FermataShortAbove, 23], + [MusicFontSymbol.FermataLongAbove, 23], + + [MusicFontSymbol.RestLonga, 9], + [MusicFontSymbol.RestDoubleWhole, 9], + [MusicFontSymbol.RestWhole, 9], + [MusicFontSymbol.RestHalf, 9], + [MusicFontSymbol.RestQuarter, 9], + [MusicFontSymbol.RestEighth, 9], + [MusicFontSymbol.RestSixteenth, 9], + [MusicFontSymbol.RestThirtySecond, 12], + [MusicFontSymbol.RestSixtyFourth, 14], + [MusicFontSymbol.RestOneHundredTwentyEighth, 14], + [MusicFontSymbol.RestTwoHundredFiftySixth, 14], + + [MusicFontSymbol.RestHBarLeft, 0], + [MusicFontSymbol.RestHBarMiddle, 0], + [MusicFontSymbol.RestHBarRight, 0], + + [MusicFontSymbol.Repeat1Bar, 0], + [MusicFontSymbol.Repeat2Bars, 0], + + [MusicFontSymbol.Ottava, 0], + [MusicFontSymbol.OttavaAlta, 32], + [MusicFontSymbol.OttavaBassaVb, 29], + [MusicFontSymbol.Quindicesima, 23], + [MusicFontSymbol.QuindicesimaAlta, 46], + + [MusicFontSymbol.DynamicPPPPPP, 0], + [MusicFontSymbol.DynamicPPPPP, 0], + [MusicFontSymbol.DynamicPPPP, 0], + [MusicFontSymbol.DynamicPPP, 0], + [MusicFontSymbol.DynamicPP, 0], + [MusicFontSymbol.DynamicPiano, 0], + [MusicFontSymbol.DynamicMP, 0], + [MusicFontSymbol.DynamicMF, 0], + [MusicFontSymbol.DynamicPF, 0], + [MusicFontSymbol.DynamicForte, 0], + [MusicFontSymbol.DynamicFF, 0], + [MusicFontSymbol.DynamicFFF, 0], + [MusicFontSymbol.DynamicFFFF, 0], + [MusicFontSymbol.DynamicFFFFF, 0], + [MusicFontSymbol.DynamicFFFFFF, 0], + [MusicFontSymbol.DynamicFortePiano, 0], + [MusicFontSymbol.DynamicNiente, 0], + [MusicFontSymbol.DynamicSforzando1, 0], + [MusicFontSymbol.DynamicSforzandoPiano, 0], + [MusicFontSymbol.DynamicSforzandoPianissimo, 0], + [MusicFontSymbol.DynamicSforzato, 0], + [MusicFontSymbol.DynamicSforzatoPiano, 0], + [MusicFontSymbol.DynamicSforzatoFF, 0], + [MusicFontSymbol.DynamicRinforzando1, 0], + [MusicFontSymbol.DynamicRinforzando2, 0], + [MusicFontSymbol.DynamicForzando, 0], + + [MusicFontSymbol.OrnamentTrill, 20], + [MusicFontSymbol.OrnamentTurn, 26], + [MusicFontSymbol.OrnamentTurnInverted, 26], + [MusicFontSymbol.OrnamentShortTrill, 0], + [MusicFontSymbol.OrnamentMordent, 26], + + [MusicFontSymbol.StringsDownBow, 9], + [MusicFontSymbol.StringsUpBow, 9], + + [MusicFontSymbol.KeyboardPedalPed, 35], + [MusicFontSymbol.KeyboardPedalUp, 16], + + [MusicFontSymbol.PictEdgeOfCymbal, 44], + + [MusicFontSymbol.GuitarString0, 20], + [MusicFontSymbol.GuitarString1, 20], + [MusicFontSymbol.GuitarString2, 20], + [MusicFontSymbol.GuitarString3, 20], + [MusicFontSymbol.GuitarString4, 20], + [MusicFontSymbol.GuitarString5, 20], + [MusicFontSymbol.GuitarString6, 20], + [MusicFontSymbol.GuitarString7, 20], + [MusicFontSymbol.GuitarString8, 20], + [MusicFontSymbol.GuitarString9, 20], + + [MusicFontSymbol.GuitarOpenPedal, 11], + [MusicFontSymbol.GuitarClosePedal, 11], + + [MusicFontSymbol.GuitarGolpe, 13.4], + [MusicFontSymbol.GuitarFadeIn, 13], + [MusicFontSymbol.GuitarFadeOut, 13], + [MusicFontSymbol.GuitarVolumeSwell, 26], + + [MusicFontSymbol.FretboardX, 0], + [MusicFontSymbol.FretboardO, 0], + + [MusicFontSymbol.WiggleTrill, 9], + [MusicFontSymbol.WiggleVibratoMediumFast, 10], + + [MusicFontSymbol.OctaveBaselineM, 13], + [MusicFontSymbol.OctaveBaselineB, 9] + ]); + + /** + * The heights of the bounding box for the respective glyphs. + */ + public static readonly Heights: Map = new Map([ + [MusicFontSymbol.Brace, 34], + [MusicFontSymbol.BracketTop, 0], + [MusicFontSymbol.BracketBottom, 0], + [MusicFontSymbol.SystemDivider, 0], + + [MusicFontSymbol.GClef, 0], + [MusicFontSymbol.CClef, 0], + [MusicFontSymbol.FClef, 0], + [MusicFontSymbol.UnpitchedPercussionClef1, 0], + [MusicFontSymbol.SixStringTabClef, 0], + [MusicFontSymbol.FourStringTabClef, 0], + + [MusicFontSymbol.TimeSig0, 0], + [MusicFontSymbol.TimeSig1, 0], + [MusicFontSymbol.TimeSig2, 0], + [MusicFontSymbol.TimeSig3, 0], + [MusicFontSymbol.TimeSig4, 0], + [MusicFontSymbol.TimeSig5, 0], + [MusicFontSymbol.TimeSig6, 0], + [MusicFontSymbol.TimeSig7, 0], + [MusicFontSymbol.TimeSig8, 0], + [MusicFontSymbol.TimeSig9, 0], + [MusicFontSymbol.TimeSigCommon, 0], + [MusicFontSymbol.TimeSigCutCommon, 0], + + [MusicFontSymbol.NoteheadDoubleWholeSquare, 8], + [MusicFontSymbol.NoteheadDoubleWhole, 8], + [MusicFontSymbol.NoteheadWhole, 8], + [MusicFontSymbol.NoteheadHalf, 8], + [MusicFontSymbol.NoteheadBlack, 8], + [MusicFontSymbol.NoteheadNull, 8], + [MusicFontSymbol.NoteheadXOrnate, 8], + + [MusicFontSymbol.NoteheadPlusDoubleWhole, 8], + [MusicFontSymbol.NoteheadPlusWhole, 8], + [MusicFontSymbol.NoteheadPlusHalf, 8], + [MusicFontSymbol.NoteheadPlusBlack, 8], + + [MusicFontSymbol.NoteheadSquareWhite, 8], + [MusicFontSymbol.NoteheadSquareBlack, 8], + + [MusicFontSymbol.NoteheadTriangleUpDoubleWhole, 8], + [MusicFontSymbol.NoteheadTriangleUpWhole, 8], + [MusicFontSymbol.NoteheadTriangleUpHalf, 8], + [MusicFontSymbol.NoteheadTriangleUpBlack, 8], + + [MusicFontSymbol.NoteheadTriangleRightWhite, 8], + [MusicFontSymbol.NoteheadTriangleRightBlack, 8], + + [MusicFontSymbol.NoteheadTriangleDownDoubleWhole, 8], + [MusicFontSymbol.NoteheadTriangleDownWhole, 8], + [MusicFontSymbol.NoteheadTriangleDownHalf, 8], + [MusicFontSymbol.NoteheadTriangleDownBlack, 8], + + [MusicFontSymbol.NoteheadDiamondDoubleWhole, 8], + [MusicFontSymbol.NoteheadDiamondWhole, 8], + [MusicFontSymbol.NoteheadDiamondHalf, 8], + [MusicFontSymbol.NoteheadDiamondBlack, 8], + + [MusicFontSymbol.NoteheadDiamondBlackWide, 9], + [MusicFontSymbol.NoteheadDiamondWhite, 9], + [MusicFontSymbol.NoteheadDiamondWhiteWide, 9], + [MusicFontSymbol.NoteheadCircleXDoubleWhole, 8], + [MusicFontSymbol.NoteheadCircleXWhole, 8], + [MusicFontSymbol.NoteheadCircleXHalf, 8], + [MusicFontSymbol.NoteheadCircleX, 8], + [MusicFontSymbol.NoteheadXDoubleWhole, 8], + [MusicFontSymbol.NoteheadXWhole, 8], + [MusicFontSymbol.NoteheadXHalf, 8], + [MusicFontSymbol.NoteheadXBlack, 8], + [MusicFontSymbol.NoteheadParenthesis, 8], + [MusicFontSymbol.NoteheadSlashedBlack1, 8], + [MusicFontSymbol.NoteheadSlashedBlack2, 8], + [MusicFontSymbol.NoteheadSlashedHalf1, 8], + [MusicFontSymbol.NoteheadSlashedHalf2, 8], + [MusicFontSymbol.NoteheadSlashedWhole1, 8], + [MusicFontSymbol.NoteheadSlashedWhole2, 8], + [MusicFontSymbol.NoteheadSlashedDoubleWhole1, 8], + [MusicFontSymbol.NoteheadSlashedDoubleWhole2, 8], + + [MusicFontSymbol.NoteheadCircledBlack, 8], + [MusicFontSymbol.NoteheadCircledHalf, 8], + [MusicFontSymbol.NoteheadCircledWhole, 8], + [MusicFontSymbol.NoteheadCircledDoubleWhole, 8], + + [MusicFontSymbol.NoteheadCircleSlash, 8], + [MusicFontSymbol.NoteheadHeavyX, 8], + [MusicFontSymbol.NoteheadHeavyXHat, 8], + + [MusicFontSymbol.NoteheadSlashVerticalEnds, 17], + [MusicFontSymbol.NoteheadSlashWhiteWhole, 17], + [MusicFontSymbol.NoteheadSlashWhiteHalf, 17], + + [MusicFontSymbol.NoteheadRoundWhiteWithDot, 8], + + [MusicFontSymbol.NoteheadSquareBlackLarge, 8], + [MusicFontSymbol.NoteheadSquareBlackWhite, 8], + + [MusicFontSymbol.NoteheadClusterDoubleWhole3rd, 8], + [MusicFontSymbol.NoteheadClusterWhole3rd, 8], + [MusicFontSymbol.NoteheadClusterHalf3rd, 8], + [MusicFontSymbol.NoteheadClusterQuarter3rd, 8], + + [MusicFontSymbol.NoteShapeRoundWhite, 8], + [MusicFontSymbol.NoteShapeRoundBlack, 8], + + [MusicFontSymbol.NoteShapeSquareWhite, 8], + [MusicFontSymbol.NoteShapeSquareBlack, 8], + + [MusicFontSymbol.NoteShapeTriangleRightWhite, 8], + [MusicFontSymbol.NoteShapeTriangleRightBlack, 8], + + [MusicFontSymbol.NoteShapeTriangleLeftWhite, 8], + [MusicFontSymbol.NoteShapeTriangleLeftBlack, 8], + + [MusicFontSymbol.NoteShapeDiamondWhite, 8], + [MusicFontSymbol.NoteShapeDiamondBlack, 8], + + [MusicFontSymbol.NoteShapeTriangleUpWhite, 8], + [MusicFontSymbol.NoteShapeTriangleUpBlack, 8], + + [MusicFontSymbol.NoteShapeMoonWhite, 8], + [MusicFontSymbol.NoteShapeMoonBlack, 8], + + [MusicFontSymbol.NoteShapeTriangleRoundWhite, 8], + [MusicFontSymbol.NoteShapeTriangleRoundBlack, 8], + + [MusicFontSymbol.NoteQuarterUp, 8], + [MusicFontSymbol.NoteEighthUp, 8], + + [MusicFontSymbol.TextBlackNoteLongStem, 0], + [MusicFontSymbol.TextBlackNoteFrac8thLongStem, 0], + [MusicFontSymbol.TextBlackNoteFrac16thLongStem, 0], + [MusicFontSymbol.TextBlackNoteFrac32ndLongStem, 0], + + [MusicFontSymbol.TextCont8thBeamLongStem, 0], + [MusicFontSymbol.TextCont16thBeamLongStem, 0], + [MusicFontSymbol.TextCont32ndBeamLongStem, 0], + + [MusicFontSymbol.TextAugmentationDot, 0], + [MusicFontSymbol.TextTupletBracketStartLongStem, 0], + [MusicFontSymbol.TextTuplet3LongStem, 0], + [MusicFontSymbol.TextTupletBracketEndLongStem, 0], + + [MusicFontSymbol.Tremolo3, 0], + [MusicFontSymbol.Tremolo2, 0], + [MusicFontSymbol.Tremolo1, 0], + + [MusicFontSymbol.FlagEighthUp, 0], + [MusicFontSymbol.FlagEighthDown, 0], + [MusicFontSymbol.FlagSixteenthUp, 0], + [MusicFontSymbol.FlagSixteenthDown, 0], + [MusicFontSymbol.FlagThirtySecondUp, 0], + [MusicFontSymbol.FlagThirtySecondDown, 0], + [MusicFontSymbol.FlagSixtyFourthUp, 0], + [MusicFontSymbol.FlagSixtyFourthDown, 0], + [MusicFontSymbol.FlagOneHundredTwentyEighthUp, 0], + [MusicFontSymbol.FlagOneHundredTwentyEighthDown, 0], + [MusicFontSymbol.FlagTwoHundredFiftySixthUp, 0], + [MusicFontSymbol.FlagTwoHundredFiftySixthDown, 0], + + [MusicFontSymbol.AccidentalFlat, 0], + [MusicFontSymbol.AccidentalNatural, 0], + [MusicFontSymbol.AccidentalSharp, 0], + [MusicFontSymbol.AccidentalDoubleSharp, 0], + [MusicFontSymbol.AccidentalDoubleFlat, 0], + [MusicFontSymbol.AccidentalQuarterToneFlatArrowUp, 0], + [MusicFontSymbol.AccidentalQuarterToneSharpArrowUp, 0], + [MusicFontSymbol.AccidentalQuarterToneNaturalArrowUp, 0], + + [MusicFontSymbol.Segno, 0], + [MusicFontSymbol.Coda, 0], + + [MusicFontSymbol.ArticAccentAbove, 9], + [MusicFontSymbol.ArticAccentBelow, 9], + [MusicFontSymbol.ArticStaccatoAbove, 9], + [MusicFontSymbol.ArticStaccatoBelow, 9], + [MusicFontSymbol.ArticTenutoAbove, 9], + [MusicFontSymbol.ArticTenutoBelow, 9], + [MusicFontSymbol.ArticMarcatoAbove, 9], + [MusicFontSymbol.ArticMarcatoBelow, 9], + + [MusicFontSymbol.FermataAbove, 12], + [MusicFontSymbol.FermataShortAbove, 12], + [MusicFontSymbol.FermataLongAbove, 12], + + [MusicFontSymbol.RestLonga, 0], + [MusicFontSymbol.RestDoubleWhole, 0], + [MusicFontSymbol.RestWhole, 0], + [MusicFontSymbol.RestHalf, 0], + [MusicFontSymbol.RestQuarter, 0], + [MusicFontSymbol.RestEighth, 0], + [MusicFontSymbol.RestSixteenth, 0], + [MusicFontSymbol.RestThirtySecond, 0], + [MusicFontSymbol.RestSixtyFourth, 0], + [MusicFontSymbol.RestOneHundredTwentyEighth, 0], + [MusicFontSymbol.RestTwoHundredFiftySixth, 0], + + [MusicFontSymbol.RestHBarLeft, 0], + [MusicFontSymbol.RestHBarMiddle, 0], + [MusicFontSymbol.RestHBarRight, 0], + + [MusicFontSymbol.Repeat1Bar, 0], + [MusicFontSymbol.Repeat2Bars, 0], + + [MusicFontSymbol.Ottava, 13], + [MusicFontSymbol.OttavaAlta, 13], + [MusicFontSymbol.OttavaBassaVb, 13], + [MusicFontSymbol.Quindicesima, 13], + [MusicFontSymbol.QuindicesimaAlta, 13], + + [MusicFontSymbol.DynamicPPPPPP, 28.4], + [MusicFontSymbol.DynamicPPPPP, 28.4], + [MusicFontSymbol.DynamicPPPP, 28.4], + [MusicFontSymbol.DynamicPPP, 28.4], + [MusicFontSymbol.DynamicPP, 28.4], + [MusicFontSymbol.DynamicPiano, 28.4], + [MusicFontSymbol.DynamicMP, 28.4], + [MusicFontSymbol.DynamicMF, 28.4], + [MusicFontSymbol.DynamicPF, 28.4], + [MusicFontSymbol.DynamicForte, 28.4], + [MusicFontSymbol.DynamicFF, 28.4], + [MusicFontSymbol.DynamicFFF, 28.4], + [MusicFontSymbol.DynamicFFFF, 28.4], + [MusicFontSymbol.DynamicFFFFF, 28.4], + [MusicFontSymbol.DynamicFFFFFF, 28.4], + [MusicFontSymbol.DynamicFortePiano, 28.4], + [MusicFontSymbol.DynamicNiente, 28.4], + [MusicFontSymbol.DynamicSforzando1, 28.4], + [MusicFontSymbol.DynamicSforzandoPiano, 28.4], + [MusicFontSymbol.DynamicSforzandoPianissimo, 28.4], + [MusicFontSymbol.DynamicSforzato, 28.4], + [MusicFontSymbol.DynamicSforzatoPiano, 28.4], + [MusicFontSymbol.DynamicSforzatoFF, 28.4], + [MusicFontSymbol.DynamicRinforzando1, 28.4], + [MusicFontSymbol.DynamicRinforzando2, 28.4], + [MusicFontSymbol.DynamicForzando, 28.4], + + [MusicFontSymbol.OrnamentTrill, 24], + [MusicFontSymbol.OrnamentTurn, 18], + [MusicFontSymbol.OrnamentTurnInverted, 18], + [MusicFontSymbol.OrnamentShortTrill, 18], + [MusicFontSymbol.OrnamentMordent, 18], + + [MusicFontSymbol.StringsDownBow, 13 / 0.75], + [MusicFontSymbol.StringsUpBow, 13 / 0.75], + + [MusicFontSymbol.KeyboardPedalPed, 19], + [MusicFontSymbol.KeyboardPedalUp, 16], + + [MusicFontSymbol.PictEdgeOfCymbal, 30], + + [MusicFontSymbol.GuitarString0, 20], + [MusicFontSymbol.GuitarString1, 20], + [MusicFontSymbol.GuitarString2, 20], + [MusicFontSymbol.GuitarString3, 20], + [MusicFontSymbol.GuitarString4, 20], + [MusicFontSymbol.GuitarString5, 20], + [MusicFontSymbol.GuitarString6, 20], + [MusicFontSymbol.GuitarString7, 20], + [MusicFontSymbol.GuitarString8, 20], + [MusicFontSymbol.GuitarString9, 20], + + [MusicFontSymbol.GuitarOpenPedal, 11], + [MusicFontSymbol.GuitarClosePedal, 11], + + [MusicFontSymbol.GuitarGolpe, 13.4], + [MusicFontSymbol.GuitarFadeIn, 13], + [MusicFontSymbol.GuitarFadeOut, 13], + [MusicFontSymbol.GuitarVolumeSwell, 13], + + [MusicFontSymbol.FretboardX, 0], + [MusicFontSymbol.FretboardO, 0], + + [MusicFontSymbol.WiggleTrill, 6], + [MusicFontSymbol.WiggleVibratoMediumFast, 10], + + [MusicFontSymbol.OctaveBaselineM, 0], + [MusicFontSymbol.OctaveBaselineB, 0] + ]); +} diff --git a/src/rendering/utils/NoteBounds.ts b/src/rendering/utils/NoteBounds.ts index 0dc58dac3..f1c884d77 100644 --- a/src/rendering/utils/NoteBounds.ts +++ b/src/rendering/utils/NoteBounds.ts @@ -1,17 +1,16 @@ -import { Note } from '@src/model/Note'; -import { Bounds } from '@src/rendering/utils/Bounds'; -import { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import type { Note } from '@src/model/Note'; +import type { Bounds } from '@src/rendering/utils/Bounds'; +import type { BeatBounds } from '@src/rendering/utils/BeatBounds'; /** * Represents the bounds of a single note */ export class NoteBounds { /** - * Gets or sets the reference to the beat boudns this note relates to. + * Gets or sets the reference to the beat boudns this note relates to. */ public beatBounds!: BeatBounds; - /** * Gets or sets the bounds of the individual note head. */ @@ -22,7 +21,6 @@ export class NoteBounds { */ public note!: Note; - /** * Finishes the lookup object and optimizes itself for fast access. */ diff --git a/src/rendering/utils/StaffSystemBounds.ts b/src/rendering/utils/StaffSystemBounds.ts index b9e06adcc..70aa3bd58 100644 --- a/src/rendering/utils/StaffSystemBounds.ts +++ b/src/rendering/utils/StaffSystemBounds.ts @@ -1,6 +1,6 @@ -import { Bounds } from '@src/rendering/utils/Bounds'; -import { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; -import { MasterBarBounds } from '@src/rendering/utils/MasterBarBounds'; +import type { Bounds } from '@src/rendering/utils/Bounds'; +import type { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; +import type { MasterBarBounds } from '@src/rendering/utils/MasterBarBounds'; /** * Represents the bounds of a staff system. @@ -39,7 +39,7 @@ export class StaffSystemBounds { this.realBounds.scaleWith(scale); this.visualBounds.scaleWith(scale); - for (let t of this.bars) { + for (const t of this.bars) { t.finish(scale); } } @@ -62,7 +62,7 @@ export class StaffSystemBounds { public findBarAtPos(x: number): MasterBarBounds | null { let b: MasterBarBounds | null = null; // move from left to right as long we find bars that start before the clicked position - for (let bar of this.bars) { + for (const bar of this.bars) { if (!b || bar.realBounds.x < x) { b = bar; } else if (x > bar.realBounds.x + bar.realBounds.w) { diff --git a/src/synth/ActiveBeatsChangedEventArgs.ts b/src/synth/ActiveBeatsChangedEventArgs.ts index bfc014238..0f698b2a5 100644 --- a/src/synth/ActiveBeatsChangedEventArgs.ts +++ b/src/synth/ActiveBeatsChangedEventArgs.ts @@ -1,4 +1,4 @@ -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; /** * Represents the information related to the beats actively being played now. diff --git a/src/synth/AlphaSynth.ts b/src/synth/AlphaSynth.ts index cee513784..a20778fe2 100644 --- a/src/synth/AlphaSynth.ts +++ b/src/synth/AlphaSynth.ts @@ -1,22 +1,22 @@ -import { MidiFile } from '@src/midi/MidiFile'; -import { IAlphaSynth } from '@src/synth/IAlphaSynth'; -import { ISynthOutput } from '@src/synth/ISynthOutput'; +import type { MidiFile } from '@src/midi/MidiFile'; +import type { IAlphaSynth } from '@src/synth/IAlphaSynth'; +import type { ISynthOutput } from '@src/synth/ISynthOutput'; import { MidiFileSequencer } from '@src/synth/MidiFileSequencer'; -import { PlaybackRange } from '@src/synth/PlaybackRange'; +import type { PlaybackRange } from '@src/synth/PlaybackRange'; import { PlayerState } from '@src/synth/PlayerState'; import { PlayerStateChangedEventArgs } from '@src/synth/PlayerStateChangedEventArgs'; import { PositionChangedEventArgs } from '@src/synth/PositionChangedEventArgs'; import { Hydra } from '@src/synth/soundfont/Hydra'; import { TinySoundFont } from '@src/synth/synthesis/TinySoundFont'; -import { EventEmitter, IEventEmitter, IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; +import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { Logger } from '@src/Logger'; -import { LogLevel } from '@src/LogLevel'; +import type { LogLevel } from '@src/LogLevel'; import { SynthConstants } from '@src/synth/SynthConstants'; -import { SynthEvent } from '@src/synth/synthesis/SynthEvent'; +import type { SynthEvent } from '@src/synth/synthesis/SynthEvent'; import { Queue } from '@src/synth/ds/Queue'; import { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; -import { MidiEvent, MidiEventType } from '@src/midi/MidiEvent'; +import type { MidiEvent, MidiEventType } from '@src/midi/MidiEvent'; import { PlaybackRangeChangedEventArgs } from '@src/synth/PlaybackRangeChangedEventArgs'; import { ModelUtils } from '@src/model/ModelUtils'; @@ -37,11 +37,11 @@ export class AlphaSynth implements IAlphaSynth { private _midiEventsPlayedFilter: Set = new Set(); private _notPlayedSamples: number = 0; private _synthStopping = false; + private _output: ISynthOutput; - /** - * Gets the {@link ISynthOutput} used for playing the generated samples. - */ - public readonly output: ISynthOutput; + public get output(): ISynthOutput { + return this._output; + } public isReady: boolean = false; @@ -101,7 +101,7 @@ export class AlphaSynth implements IAlphaSynth { public set playbackSpeed(value: number) { value = ModelUtils.clamp(value, SynthConstants.MinPlaybackSpeed, SynthConstants.MaxPlaybackSpeed); - let oldSpeed: number = this._sequencer.playbackSpeed; + const oldSpeed: number = this._sequencer.playbackSpeed; this._sequencer.playbackSpeed = value; this.timePosition = this.timePosition * (oldSpeed / value); } @@ -171,7 +171,7 @@ export class AlphaSynth implements IAlphaSynth { this.state = PlayerState.Paused; Logger.debug('AlphaSynth', 'Creating output'); - this.output = output; + this._output = output; Logger.debug('AlphaSynth', 'Creating synthesizer'); this._synthesizer = new TinySoundFont(this.output.sampleRate); @@ -185,7 +185,7 @@ export class AlphaSynth implements IAlphaSynth { }); this.output.sampleRequest.on(() => { if ( - this.state == PlayerState.Playing && + this.state === PlayerState.Playing && (!this._sequencer.isFinished || this._synthesizer.activeVoiceCount > 0) ) { let samples: Float32Array = new Float32Array( @@ -223,7 +223,7 @@ export class AlphaSynth implements IAlphaSynth { this.output.addSamples(samples); } else { // Tell output that there is no data left for it. - let samples: Float32Array = new Float32Array(0); + const samples: Float32Array = new Float32Array(0); this.output.addSamples(samples); } }); @@ -336,10 +336,10 @@ export class AlphaSynth implements IAlphaSynth { public loadSoundFont(data: Uint8Array, append: boolean): void { this.pause(); - let input: ByteBuffer = ByteBuffer.fromBuffer(data); + const input: ByteBuffer = ByteBuffer.fromBuffer(data); try { Logger.debug('AlphaSynth', 'Loading soundfont from bytes'); - let soundFont: Hydra = new Hydra(); + const soundFont: Hydra = new Hydra(); soundFont.load(input); if (!append) { this._loadedSoundFonts = []; @@ -352,7 +352,7 @@ export class AlphaSynth implements IAlphaSynth { Logger.debug('AlphaSynth', 'soundFont successfully loaded'); this.checkReadyForPlayback(); } catch (e) { - Logger.error('AlphaSynth', 'Could not load soundfont from bytes ' + e); + Logger.error('AlphaSynth', `Could not load soundfont from bytes ${e}`); (this.soundFontLoadFailed as EventEmitterOfT).trigger(e as Error); } } @@ -395,7 +395,7 @@ export class AlphaSynth implements IAlphaSynth { this.checkReadyForPlayback(); this.tickPosition = 0; } catch (e) { - Logger.error('AlphaSynth', 'Could not load midi from model ' + e); + Logger.error('AlphaSynth', `Could not load midi from model ${e}`); (this.midiLoadFailed as EventEmitterOfT).trigger(e as Error); } } @@ -429,7 +429,7 @@ export class AlphaSynth implements IAlphaSynth { if (sampleCount === 0) { return; } - let playedMillis: number = (sampleCount / this._synthesizer.outSampleRate) * 1000; + const playedMillis: number = (sampleCount / this._synthesizer.outSampleRate) * 1000; this._notPlayedSamples -= sampleCount * SynthConstants.AudioChannels; this.updateTimePosition(this._timePosition + playedMillis, false); this.checkForFinish(); @@ -505,8 +505,8 @@ export class AlphaSynth implements IAlphaSynth { const mode = this._sequencer.isPlayingMain ? 'main' : this._sequencer.isPlayingCountIn - ? 'count-in' - : 'one-time'; + ? 'count-in' + : 'one-time'; Logger.debug( 'AlphaSynth', @@ -559,7 +559,6 @@ export class AlphaSynth implements IAlphaSynth { return this._synthesizer.hasSamplesForProgram(program); } - /** * @internal */ diff --git a/src/synth/IAlphaSynth.ts b/src/synth/IAlphaSynth.ts index 9a0c76434..68ed1d3d6 100644 --- a/src/synth/IAlphaSynth.ts +++ b/src/synth/IAlphaSynth.ts @@ -1,13 +1,14 @@ -import { MidiFile } from '@src/midi/MidiFile'; -import { PlaybackRange } from '@src/synth/PlaybackRange'; -import { PlayerState } from '@src/synth/PlayerState'; -import { PlayerStateChangedEventArgs } from '@src/synth/PlayerStateChangedEventArgs'; -import { PlaybackRangeChangedEventArgs } from '@src/synth/PlaybackRangeChangedEventArgs'; -import { PositionChangedEventArgs } from '@src/synth/PositionChangedEventArgs'; -import { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; -import { LogLevel } from '@src/LogLevel'; -import { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; -import { MidiEventType } from '@src/midi/MidiEvent'; +import type { MidiFile } from '@src/midi/MidiFile'; +import type { PlaybackRange } from '@src/synth/PlaybackRange'; +import type { PlayerState } from '@src/synth/PlayerState'; +import type { PlayerStateChangedEventArgs } from '@src/synth/PlayerStateChangedEventArgs'; +import type { PlaybackRangeChangedEventArgs } from '@src/synth/PlaybackRangeChangedEventArgs'; +import type { PositionChangedEventArgs } from '@src/synth/PositionChangedEventArgs'; +import type { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; +import type { LogLevel } from '@src/LogLevel'; +import type { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; +import type { MidiEventType } from '@src/midi/MidiEvent'; +import type { ISynthOutput } from '@src/synth/ISynthOutput'; /** * The public API interface for interacting with the synthesizer. @@ -71,6 +72,7 @@ export interface IAlphaSynth { /** * Gets or sets volume of the metronome during count-in. (range: 0.0-3.0, default 0.0 - no count in) + * @since 1.1.0 */ countInVolume: number; @@ -79,8 +81,14 @@ export interface IAlphaSynth { */ midiEventsPlayedFilter: MidiEventType[]; + /** + * Gets the output used by alphaSynth. + */ + readonly output: ISynthOutput; + /** * Destroys the synthesizer and all related components + * @since 0.9.6 */ destroy(): void; @@ -106,7 +114,7 @@ export interface IAlphaSynth { stop(): void; /** - * Stops any ongoing playback and plays the given midi file instead. + * Stops any ongoing playback and plays the given midi file instead. * @param midi The midi file to play */ playOneTimeMidiFile(midi: MidiFile): void; @@ -136,7 +144,7 @@ export interface IAlphaSynth { applyTranspositionPitches(transpositionPitches: Map): void; /** - * Sets the transposition pitch of a given channel. This pitch is additionally applied beside the + * Sets the transposition pitch of a given channel. This pitch is additionally applied beside the * ones applied already via {@link applyTranspositionPitches}. * @param channel The channel number * @param semitones The number of semitones to apply as pitch offset. @@ -181,46 +189,55 @@ export interface IAlphaSynth { /** * This event is fired when the playback of the whole song finished. + * @eventProperty */ readonly finished: IEventEmitter; /** * This event is fired when the SoundFont needed for playback was loaded. + * @eventProperty */ readonly soundFontLoaded: IEventEmitter; /** * This event is fired when the loading of the SoundFont failed. + * @eventProperty */ readonly soundFontLoadFailed: IEventEmitterOfT; /** * This event is fired when the Midi file needed for playback was loaded. + * @eventProperty */ readonly midiLoaded: IEventEmitterOfT; /** * This event is fired when the loading of the Midi file failed. + * @eventProperty */ - readonly midiLoadFailed: IEventEmitterOfT + readonly midiLoadFailed: IEventEmitterOfT; /** * This event is fired when the playback state changed. + * @eventProperty */ readonly stateChanged: IEventEmitterOfT; /** * This event is fired when the current playback position of/ the song changed. + * @eventProperty */ readonly positionChanged: IEventEmitterOfT; /** * The event is fired when certain midi events were sent to the audio output device for playback. + * @eventProperty */ readonly midiEventsPlayed: IEventEmitterOfT; /** * The event is fired when the playback range within the player was updated. + * @eventProperty */ readonly playbackRangeChanged: IEventEmitterOfT; } diff --git a/src/synth/ISynthOutput.ts b/src/synth/ISynthOutput.ts index 34e9cd31d..430434511 100644 --- a/src/synth/ISynthOutput.ts +++ b/src/synth/ISynthOutput.ts @@ -1,4 +1,24 @@ -import { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; +import type { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; + +/** + * Represents a output device on which the synth can send the audio to. + */ +export interface ISynthOutputDevice { + /** + * The ID to uniquely identify the device. + */ + readonly deviceId: string; + + /** + * A string describing the device. + */ + readonly label: string; + + /** + * Gets a value indicating whether the device is the default output device. + */ + readonly isDefault: boolean; +} /** * This is the base interface for output devices which can @@ -61,4 +81,26 @@ export interface ISynthOutput { * Fired when the output needs more samples to be played. */ readonly sampleRequest: IEventEmitter; + + /** + * Loads and lists the available output devices. Will request permissions if needed. + * @async + */ + enumerateOutputDevices(): Promise; + + /** + * Changes the output device which should be used for playing the audio. + * @async + * @param device The output device to use, or null to switch to the default device. + */ + setOutputDevice(device: ISynthOutputDevice | null): Promise; + + /** + * The currently configured output device if changed via {@link setOutputDevice}. + * @async + * @returns The custom configured output device which was set via {@link setOutputDevice} or `null` + * if the default outputDevice is used. + * The output device might change dynamically if devices are connected/disconnected (e.g. bluetooth headset). + */ + getOutputDevice(): Promise; } diff --git a/src/synth/MidiEventsPlayedEventArgs.ts b/src/synth/MidiEventsPlayedEventArgs.ts index 601f55dce..d6847548f 100644 --- a/src/synth/MidiEventsPlayedEventArgs.ts +++ b/src/synth/MidiEventsPlayedEventArgs.ts @@ -1,7 +1,7 @@ -import { MidiEvent } from "@src/midi/MidiEvent"; +import type { MidiEvent } from '@src/midi/MidiEvent'; /** - * Represents the info when the synthesizer played certain midi events. + * Represents the info when the synthesizer played certain midi events. */ export class MidiEventsPlayedEventArgs { /** @@ -16,4 +16,4 @@ export class MidiEventsPlayedEventArgs { public constructor(events: MidiEvent[]) { this.events = events; } -} \ No newline at end of file +} diff --git a/src/synth/MidiFileSequencer.ts b/src/synth/MidiFileSequencer.ts index 19eb168b8..a53474b16 100644 --- a/src/synth/MidiFileSequencer.ts +++ b/src/synth/MidiFileSequencer.ts @@ -1,14 +1,14 @@ import { MidiEventType, - NoteOnEvent, - ProgramChangeEvent, - TempoChangeEvent, - TimeSignatureEvent + type NoteOnEvent, + type ProgramChangeEvent, + type TempoChangeEvent, + type TimeSignatureEvent } from '@src/midi/MidiEvent'; -import { MidiFile } from '@src/midi/MidiFile'; -import { PlaybackRange } from '@src/synth/PlaybackRange'; +import type { MidiFile } from '@src/midi/MidiFile'; +import type { PlaybackRange } from '@src/synth/PlaybackRange'; import { SynthEvent } from '@src/synth/synthesis/SynthEvent'; -import { TinySoundFont } from '@src/synth/synthesis/TinySoundFont'; +import type { TinySoundFont } from '@src/synth/synthesis/TinySoundFont'; import { Logger } from '@src/Logger'; import { SynthConstants } from '@src/synth/SynthConstants'; import { MidiUtils } from '@src/midi/MidiUtils'; @@ -53,15 +53,15 @@ export class MidiFileSequencer { private _countInState: MidiSequencerState | null = null; public get isPlayingMain(): boolean { - return this._currentState == this._mainState; + return this._currentState === this._mainState; } public get isPlayingOneTimeMidi(): boolean { - return this._currentState == this._oneTimeState; + return this._currentState === this._oneTimeState; } public get isPlayingCountIn(): boolean { - return this._currentState == this._countInState; + return this._currentState === this._countInState; } public constructor(synthesizer: TinySoundFont) { @@ -132,7 +132,7 @@ export class MidiFileSequencer { this._mainState.currentTime = 0; this._mainState.eventIndex = 0; if (this.isPlayingMain) { - let metronomeVolume: number = this._synthesizer.metronomeVolume; + const metronomeVolume: number = this._synthesizer.metronomeVolume; this._synthesizer.noteOffAll(true); this._synthesizer.resetSoft(); this._synthesizer.setupMetronomeChannel(metronomeVolume); @@ -146,8 +146,8 @@ export class MidiFileSequencer { return; } - let start: number = Date.now(); - let finalTime: number = this._mainState.currentTime + milliseconds; + const start: number = Date.now(); + const finalTime: number = this._mainState.currentTime + milliseconds; if (this.isPlayingMain) { while (this._mainState.currentTime < finalTime) { @@ -159,8 +159,8 @@ export class MidiFileSequencer { this._mainState.currentTime = finalTime; - let duration: number = Date.now() - start; - Logger.debug('Sequencer', 'Silent seek finished in ' + duration + 'ms (main)'); + const duration: number = Date.now() - start; + Logger.debug('Sequencer', `Silent seek finished in ${duration}ms (main)`); } public loadOneTimeMidi(midiFile: MidiFile): void { @@ -204,11 +204,11 @@ export class MidiFileSequencer { let metronomeTime: number = 0.0; let previousTick: number = 0; - for (let mEvent of midiFile.events) { - let synthData: SynthEvent = new SynthEvent(state.synthData.length, mEvent); + for (const mEvent of midiFile.events) { + const synthData: SynthEvent = new SynthEvent(state.synthData.length, mEvent); state.synthData.push(synthData); - let deltaTick: number = mEvent.tick - previousTick; + const deltaTick: number = mEvent.tick - previousTick; absTick += deltaTick; absTime += deltaTick * (60000.0 / (bpm * midiFile.division)); synthData.time = absTime; @@ -216,7 +216,7 @@ export class MidiFileSequencer { if (metronomeLengthInTicks > 0) { while (metronomeTick < absTick) { - let metronome: SynthEvent = SynthEvent.newMetronomeEvent( + const metronome: SynthEvent = SynthEvent.newMetronomeEvent( state.synthData.length, metronomeTick, Math.floor(metronomeTick / metronomeLengthInTicks) % metronomeCount, @@ -231,13 +231,13 @@ export class MidiFileSequencer { } if (mEvent.type === MidiEventType.TempoChange) { - let meta: TempoChangeEvent = mEvent as TempoChangeEvent; + const meta: TempoChangeEvent = mEvent as TempoChangeEvent; bpm = 60000000 / meta.microSecondsPerQuarterNote; state.tempoChanges.push(new MidiFileSequencerTempoChange(bpm, absTick, absTime)); metronomeLengthInMillis = metronomeLengthInTicks * (60000.0 / (bpm * midiFile.division)); } else if (mEvent.type === MidiEventType.TimeSignature) { - let meta: TimeSignatureEvent = mEvent as TimeSignatureEvent; - let timeSignatureDenominator: number = Math.pow(2, meta.denominatorIndex); + const meta: TimeSignatureEvent = mEvent as TimeSignatureEvent; + const timeSignatureDenominator: number = Math.pow(2, meta.denominatorIndex); metronomeCount = meta.numerator; metronomeLengthInTicks = (state.division * (4.0 / timeSignatureDenominator)) | 0; metronomeLengthInMillis = metronomeLengthInTicks * (60000.0 / (bpm * midiFile.division)); @@ -247,7 +247,7 @@ export class MidiFileSequencer { } } else if (mEvent.type === MidiEventType.ProgramChange) { const programChange = mEvent as ProgramChangeEvent; - let channel: number = programChange.channel; + const channel: number = programChange.channel; if (!state.firstProgramEventPerChannel.has(channel)) { state.firstProgramEventPerChannel.set(channel, synthData); } @@ -255,7 +255,7 @@ export class MidiFileSequencer { if (!isPercussion) { this.instrumentPrograms.add(programChange.program); } - } else if (mEvent.type == MidiEventType.NoteOn) { + } else if (mEvent.type === MidiEventType.NoteOn) { const noteOn = mEvent as NoteOnEvent; const isPercussion = noteOn.channel === SynthConstants.PercussionChannel; if (isPercussion) { @@ -380,9 +380,8 @@ export class MidiFileSequencer { private get internalEndTime(): number { if (this.isPlayingMain) { return !this.mainPlaybackRange ? this._currentState.endTime : this._currentState.playbackRangeEndTime; - } else { - return this._currentState.endTime; } + return this._currentState.endTime; } public get isFinished(): boolean { @@ -436,13 +435,13 @@ export class MidiFileSequencer { state.tempoChanges.push(new MidiFileSequencerTempoChange(bpm, 0, 0)); - let metronomeLengthInTicks: number = (state.division * (4.0 / timeSignatureDenominator)) | 0; - let metronomeLengthInMillis: number = metronomeLengthInTicks * (60000.0 / (bpm * this._mainState.division)); + const metronomeLengthInTicks: number = (state.division * (4.0 / timeSignatureDenominator)) | 0; + const metronomeLengthInMillis: number = metronomeLengthInTicks * (60000.0 / (bpm * this._mainState.division)); let metronomeTick: number = 0; let metronomeTime: number = 0.0; for (let i = 0; i < timeSignatureNumerator; i++) { - let metronome: SynthEvent = SynthEvent.newMetronomeEvent( + const metronome: SynthEvent = SynthEvent.newMetronomeEvent( state.synthData.length, metronomeTick, i, diff --git a/src/synth/PlaybackRangeChangedEventArgs.ts b/src/synth/PlaybackRangeChangedEventArgs.ts index fd3e58a81..733b810d0 100644 --- a/src/synth/PlaybackRangeChangedEventArgs.ts +++ b/src/synth/PlaybackRangeChangedEventArgs.ts @@ -1,4 +1,4 @@ -import { PlaybackRange } from '@src/synth/PlaybackRange'; +import type { PlaybackRange } from '@src/synth/PlaybackRange'; /** * Represents the info when the playback range changed. diff --git a/src/synth/PlayerState.ts b/src/synth/PlayerState.ts index 002c98630..767619a20 100644 --- a/src/synth/PlayerState.ts +++ b/src/synth/PlayerState.ts @@ -5,9 +5,9 @@ export enum PlayerState { /** * Player is paused */ - Paused, + Paused = 0, /** * Player is playing */ - Playing + Playing = 1 } diff --git a/src/synth/PlayerStateChangedEventArgs.ts b/src/synth/PlayerStateChangedEventArgs.ts index 23c000287..45093ae57 100644 --- a/src/synth/PlayerStateChangedEventArgs.ts +++ b/src/synth/PlayerStateChangedEventArgs.ts @@ -1,4 +1,4 @@ -import { PlayerState } from '@src/synth/PlayerState'; +import type { PlayerState } from '@src/synth/PlayerState'; /** * Represents the info when the player state changes. diff --git a/src/synth/PositionChangedEventArgs.ts b/src/synth/PositionChangedEventArgs.ts index 13cd6ac77..87947c668 100644 --- a/src/synth/PositionChangedEventArgs.ts +++ b/src/synth/PositionChangedEventArgs.ts @@ -3,30 +3,31 @@ */ export class PositionChangedEventArgs { /** - * Gets a value indicating whether the position changed because of time seeking. - */ - public isSeek: boolean; - - /** - * Gets the current time in milliseconds. + * The current time position within the song in milliseconds. */ public readonly currentTime: number; /** - * Gets the length of the played song in milliseconds. + * The total length of the song in milliseconds. */ public readonly endTime: number; /** - * Gets the current time in midi ticks. + * The current time position within the song in midi ticks. */ public readonly currentTick: number; /** - * Gets the length of the played song in midi ticks. + * The total length of the song in midi ticks. */ public readonly endTick: number; + /** + * Whether the position changed because of time seeking. + * @since 1.2.0 + */ + public isSeek: boolean; + /** * Initializes a new instance of the {@link PositionChangedEventArgs} class. * @param currentTime The current time. @@ -35,7 +36,7 @@ export class PositionChangedEventArgs { * @param endTick The end tick. * @param isSeek Whether the time was seeked. */ - public constructor(currentTime: number, endTime: number, currentTick: number, endTick: number, isSeek:boolean) { + public constructor(currentTime: number, endTime: number, currentTick: number, endTick: number, isSeek: boolean) { this.currentTime = currentTime; this.endTime = endTime; this.currentTick = currentTick; diff --git a/src/synth/SynthConstants.ts b/src/synth/SynthConstants.ts index a8b627027..c21de3a59 100644 --- a/src/synth/SynthConstants.ts +++ b/src/synth/SynthConstants.ts @@ -15,7 +15,6 @@ export class SynthConstants { public static readonly PercussionChannel: number = 9; public static readonly PercussionBank: number = 128; - /** * The Midi Pitch bend message is a 15-bit value */ diff --git a/src/synth/index.ts b/src/synth/_barrel.ts similarity index 85% rename from src/synth/index.ts rename to src/synth/_barrel.ts index 5bd0cf0fa..c0c9f8234 100644 --- a/src/synth/index.ts +++ b/src/synth/_barrel.ts @@ -1,8 +1,8 @@ export { AlphaSynth } from '@src/synth/AlphaSynth'; export { CircularSampleBuffer } from '@src/synth/ds/CircularSampleBuffer'; export { PlaybackRange } from '@src/synth/PlaybackRange'; -export { type ISynthOutput } from '@src/synth/ISynthOutput'; -export { type IAlphaSynth } from '@src/synth/IAlphaSynth'; +export type { ISynthOutput, ISynthOutputDevice } from '@src/synth/ISynthOutput'; +export type { IAlphaSynth } from '@src/synth/IAlphaSynth'; export { PlayerState } from '@src/synth/PlayerState'; export { PlayerStateChangedEventArgs } from '@src/synth/PlayerStateChangedEventArgs'; export { PlaybackRangeChangedEventArgs } from '@src/synth/PlaybackRangeChangedEventArgs'; @@ -12,4 +12,4 @@ export { ActiveBeatsChangedEventArgs } from '@src/synth/ActiveBeatsChangedEventA export { AlphaSynthWebWorkerApi } from '@src/platform/javascript/AlphaSynthWebWorkerApi'; export { AlphaSynthWebAudioOutputBase } from '@src/platform/javascript/AlphaSynthWebAudioOutputBase'; export { AlphaSynthScriptProcessorOutput } from '@src/platform/javascript/AlphaSynthScriptProcessorOutput'; -export { AlphaSynthAudioWorkletOutput } from '@src/platform/javascript/AlphaSynthAudioWorkletOutput'; \ No newline at end of file +export { AlphaSynthAudioWorkletOutput } from '@src/platform/javascript/AlphaSynthAudioWorkletOutput'; diff --git a/src/synth/ds/Queue.ts b/src/synth/ds/Queue.ts index 542187614..c582586f3 100644 --- a/src/synth/ds/Queue.ts +++ b/src/synth/ds/Queue.ts @@ -26,7 +26,7 @@ export class Queue { this._items = this._items.slice(this._position); this._position = 0; } - this.isEmpty = this._items.length == 0; + this.isEmpty = this._items.length === 0; return item; } diff --git a/src/synth/soundfont/Hydra.ts b/src/synth/soundfont/Hydra.ts index ce0d7f459..638398826 100644 --- a/src/synth/soundfont/Hydra.ts +++ b/src/synth/soundfont/Hydra.ts @@ -6,11 +6,11 @@ import { RiffChunk } from '@src/synth/soundfont/RiffChunk'; import { IOHelper } from '@src/io/IOHelper'; -import { IReadable } from '@src/io/IReadable'; +import type { IReadable } from '@src/io/IReadable'; import { TypeConversions } from '@src/io/TypeConversions'; import { FormatError } from '@src/FormatError'; -import { VorbisFile } from '../vorbis/VorbisFile'; +import { VorbisFile } from '@src/synth/vorbis/VorbisFile'; import { ByteBuffer } from '@src/io/ByteBuffer'; export class Hydra { @@ -70,7 +70,7 @@ export class Hydra { } while (RiffChunk.load(chunkHead, chunkFastList, readable)) { - let chunk: RiffChunk = new RiffChunk(); + const chunk: RiffChunk = new RiffChunk(); if (chunkFastList.id === 'pdta') { while (RiffChunk.load(chunkFastList, chunk, readable)) { switch (chunk.id) { diff --git a/src/synth/soundfont/RiffChunk.ts b/src/synth/soundfont/RiffChunk.ts index a4162f7e6..2828f89d8 100644 --- a/src/synth/soundfont/RiffChunk.ts +++ b/src/synth/soundfont/RiffChunk.ts @@ -3,7 +3,7 @@ // TypeScript port for alphaTab: (C) 2020 by Daniel Kuschny // Licensed under: MPL-2.0 import { IOHelper } from '@src/io/IOHelper'; -import { IReadable } from '@src/io/IReadable'; +import type { IReadable } from '@src/io/IReadable'; export class RiffChunk { public static readonly HeaderSize = 4 /*FourCC*/ + 4 /*Size*/; @@ -34,8 +34,8 @@ export class RiffChunk { parent.size -= RiffChunk.HeaderSize + chunk.size; } - let isRiff: boolean = chunk.id === 'RIFF'; - let isList: boolean = chunk.id === 'LIST'; + const isRiff: boolean = chunk.id === 'RIFF'; + const isList: boolean = chunk.id === 'LIST'; if (isRiff && parent) { // not allowed return false; @@ -45,7 +45,7 @@ export class RiffChunk { // custom type without sub type return true; } - + // for lists unwrap the list type chunk.id = IOHelper.read8BitStringLength(stream, 4); if (chunk.id.charCodeAt(0) <= 32 || chunk.id.charCodeAt(0) >= 122) { diff --git a/src/synth/synthesis/Channels.ts b/src/synth/synthesis/Channels.ts index ca48227b2..126d2ab3e 100644 --- a/src/synth/synthesis/Channels.ts +++ b/src/synth/synthesis/Channels.ts @@ -3,9 +3,9 @@ // TypeScript port for alphaTab: (C) 2020 by Daniel Kuschny // Licensed under: MPL-2.0 -import { Channel } from '@src/synth/synthesis/Channel'; -import { TinySoundFont } from '@src/synth/synthesis/TinySoundFont'; -import { Voice } from '@src/synth/synthesis/Voice'; +import type { Channel } from '@src/synth/synthesis/Channel'; +import type { TinySoundFont } from '@src/synth/synthesis/TinySoundFont'; +import type { Voice } from '@src/synth/synthesis/Voice'; export class Channels { public activeChannel: number = 0; @@ -19,7 +19,7 @@ export class Channels { voice.noteGainDb += c.gainDb; voice.updatePitchRatio(c, tinySoundFont.outSampleRate); - + if (newpan <= -0.5) { voice.panFactorLeft = 1.0; voice.panFactorRight = 0.0; diff --git a/src/synth/synthesis/Envelope.ts b/src/synth/synthesis/Envelope.ts index 9ecea888c..486cff26f 100644 --- a/src/synth/synthesis/Envelope.ts +++ b/src/synth/synthesis/Envelope.ts @@ -55,7 +55,7 @@ export class Envelope { if (this.keynumToDecay === 0) { this.decay = this.decay < -11950.0 ? 0.0 : SynthHelper.timecents2Secs(this.decay); } - + if (this.sustain < 0.0) { this.sustain = 0.0; } else if (sustainIsGain) { diff --git a/src/synth/synthesis/LoopMode.ts b/src/synth/synthesis/LoopMode.ts index 650421c65..8b79cfb7d 100644 --- a/src/synth/synthesis/LoopMode.ts +++ b/src/synth/synthesis/LoopMode.ts @@ -4,7 +4,7 @@ // Licensed under: MPL-2.0 export enum LoopMode { - None, - Continuous, - Sustain + None = 0, + Continuous = 1, + Sustain = 2 } diff --git a/src/synth/synthesis/OutputMode.ts b/src/synth/synthesis/OutputMode.ts index 8aab4425a..da9a34fa8 100644 --- a/src/synth/synthesis/OutputMode.ts +++ b/src/synth/synthesis/OutputMode.ts @@ -10,13 +10,13 @@ export enum OutputMode { /** * Two channels with single left/right samples one after another */ - StereoInterleaved, + StereoInterleaved = 0, /** * Two channels with all samples for the left channel first then right */ - StereoUnweaved, + StereoUnweaved = 1, /** * A single channel (stereo instruments are mixed into center) */ - Mono + Mono = 2 } diff --git a/src/synth/synthesis/Preset.ts b/src/synth/synthesis/Preset.ts index 71d507608..09ee9a2a6 100644 --- a/src/synth/synthesis/Preset.ts +++ b/src/synth/synthesis/Preset.ts @@ -2,10 +2,10 @@ // developed by Bernhard Schelling (https://github.com/schellingb/TinySoundFont) // TypeScript port for alphaTab: (C) 2020 by Daniel Kuschny // Licensed under: MPL-2.0 -import { Region } from '@src/synth/synthesis/Region'; +import type { Region } from '@src/synth/synthesis/Region'; export class Preset { - public name: string = ""; + public name: string = ''; public presetNumber: number = 0; public bank: number = 0; public regions: Region[] | null = null; diff --git a/src/synth/synthesis/Region.ts b/src/synth/synthesis/Region.ts index aec025a24..a36237b59 100644 --- a/src/synth/synthesis/Region.ts +++ b/src/synth/synthesis/Region.ts @@ -2,73 +2,73 @@ // developed by Bernhard Schelling (https://github.com/schellingb/TinySoundFont) // TypeScript port for alphaTab: (C) 2020 by Daniel Kuschny // Licensed under: MPL-2.0 -import { HydraGenAmount } from '@src/synth/soundfont/Hydra'; +import type { HydraGenAmount } from '@src/synth/soundfont/Hydra'; import { Envelope } from '@src/synth/synthesis/Envelope'; import { LoopMode } from '@src/synth/synthesis/LoopMode'; import { TypeConversions } from '@src/io/TypeConversions'; export enum GenOperators { - StartAddrsOffset, - EndAddrsOffset, - StartloopAddrsOffset, - EndloopAddrsOffset, - StartAddrsCoarseOffset, - ModLfoToPitch, - VibLfoToPitch, - ModEnvToPitch, - InitialFilterFc, - InitialFilterQ, - ModLfoToFilterFc, - ModEnvToFilterFc, - EndAddrsCoarseOffset, - ModLfoToVolume, - Unused1, - ChorusEffectsSend, - ReverbEffectsSend, - Pan, - Unused2, - Unused3, - Unused4, - DelayModLFO, - FreqModLFO, - DelayVibLFO, - FreqVibLFO, - DelayModEnv, - AttackModEnv, - HoldModEnv, - DecayModEnv, - SustainModEnv, - ReleaseModEnv, - KeynumToModEnvHold, - KeynumToModEnvDecay, - DelayVolEnv, - AttackVolEnv, - HoldVolEnv, - DecayVolEnv, - SustainVolEnv, - ReleaseVolEnv, - KeynumToVolEnvHold, - KeynumToVolEnvDecay, - Instrument, - Reserved1, - KeyRange, - VelRange, - StartloopAddrsCoarseOffset, - Keynum, - Velocity, - InitialAttenuation, - Reserved2, - EndloopAddrsCoarseOffset, - CoarseTune, - FineTune, - SampleID, - SampleModes, - Reserved3, - ScaleTuning, - ExclusiveClass, - OverridingRootKey, - Unused5, - EndOper + StartAddrsOffset = 0, + EndAddrsOffset = 1, + StartloopAddrsOffset = 2, + EndloopAddrsOffset = 3, + StartAddrsCoarseOffset = 4, + ModLfoToPitch = 5, + VibLfoToPitch = 6, + ModEnvToPitch = 7, + InitialFilterFc = 8, + InitialFilterQ = 9, + ModLfoToFilterFc = 10, + ModEnvToFilterFc = 11, + EndAddrsCoarseOffset = 12, + ModLfoToVolume = 13, + Unused1 = 14, + ChorusEffectsSend = 15, + ReverbEffectsSend = 16, + Pan = 17, + Unused2 = 18, + Unused3 = 19, + Unused4 = 20, + DelayModLFO = 21, + FreqModLFO = 22, + DelayVibLFO = 23, + FreqVibLFO = 24, + DelayModEnv = 25, + AttackModEnv = 26, + HoldModEnv = 27, + DecayModEnv = 28, + SustainModEnv = 29, + ReleaseModEnv = 30, + KeynumToModEnvHold = 31, + KeynumToModEnvDecay = 32, + DelayVolEnv = 33, + AttackVolEnv = 34, + HoldVolEnv = 35, + DecayVolEnv = 36, + SustainVolEnv = 37, + ReleaseVolEnv = 38, + KeynumToVolEnvHold = 39, + KeynumToVolEnvDecay = 40, + Instrument = 41, + Reserved1 = 42, + KeyRange = 43, + VelRange = 44, + StartloopAddrsCoarseOffset = 45, + Keynum = 46, + Velocity = 47, + InitialAttenuation = 48, + Reserved2 = 49, + EndloopAddrsCoarseOffset = 50, + CoarseTune = 51, + FineTune = 52, + SampleID = 53, + SampleModes = 54, + Reserved3 = 55, + ScaleTuning = 56, + ExclusiveClass = 57, + OverridingRootKey = 58, + Unused5 = 59, + EndOper = 60 } export class Region { @@ -343,8 +343,8 @@ export class Region { (amount.wordAmount & 3) === 3 ? LoopMode.Sustain : (amount.wordAmount & 3) === 1 - ? LoopMode.Continuous - : LoopMode.None; + ? LoopMode.Continuous + : LoopMode.None; break; case GenOperators.ScaleTuning: this.pitchKeyTrack = amount.shortAmount; diff --git a/src/synth/synthesis/SynthEvent.ts b/src/synth/synthesis/SynthEvent.ts index ed8155134..5db332f72 100644 --- a/src/synth/synthesis/SynthEvent.ts +++ b/src/synth/synthesis/SynthEvent.ts @@ -2,7 +2,7 @@ // developed by Bernhard Schelling (https://github.com/schellingb/TinySoundFont) // TypeScript port for alphaTab: (C) 2020 by Daniel Kuschny // Licensed under: MPL-2.0 -import { AlphaTabMetronomeEvent, MidiEvent, MidiEventType } from '@src/midi/MidiEvent'; +import { AlphaTabMetronomeEvent, type MidiEvent, MidiEventType } from '@src/midi/MidiEvent'; export class SynthEvent { public eventIndex: number; @@ -13,16 +13,17 @@ export class SynthEvent { public constructor(eventIndex: number, e: MidiEvent) { this.eventIndex = eventIndex; this.event = e; - this.isMetronome = this.event.type == MidiEventType.AlphaTabMetronome; + this.isMetronome = this.event.type === MidiEventType.AlphaTabMetronome; } - - public static newMetronomeEvent(eventIndex: number, tick: number, counter: number, durationInTicks: number, durationInMillis: number): SynthEvent { - const evt = new AlphaTabMetronomeEvent(0, tick, - counter, - durationInTicks, - durationInMillis - ); + public static newMetronomeEvent( + eventIndex: number, + tick: number, + counter: number, + durationInTicks: number, + durationInMillis: number + ): SynthEvent { + const evt = new AlphaTabMetronomeEvent(0, tick, counter, durationInTicks, durationInMillis); const x: SynthEvent = new SynthEvent(eventIndex, evt); return x; } diff --git a/src/synth/synthesis/TinySoundFont.ts b/src/synth/synthesis/TinySoundFont.ts index 34ec7bbea..198bff7d2 100644 --- a/src/synth/synthesis/TinySoundFont.ts +++ b/src/synth/synthesis/TinySoundFont.ts @@ -3,26 +3,26 @@ // TypeScript port for alphaTab: (C) 2020 by Daniel Kuschny // Licensed under: MPL-2.0 import { - ControlChangeEvent, - MidiEvent, + type ControlChangeEvent, + type MidiEvent, MidiEventType, - NoteBendEvent, - NoteOffEvent, - NoteOnEvent, - PitchBendEvent, - ProgramChangeEvent, - TempoChangeEvent, - TimeSignatureEvent + type NoteBendEvent, + type NoteOffEvent, + type NoteOnEvent, + type PitchBendEvent, + type ProgramChangeEvent, + type TempoChangeEvent, + type TimeSignatureEvent } from '@src/midi/MidiEvent'; import { - Hydra, - HydraIbag, - HydraIgen, - HydraInst, - HydraPbag, + type Hydra, + type HydraIbag, + type HydraIgen, + type HydraInst, + type HydraPbag, HydraPgen, - HydraPhdr, - HydraShdr + type HydraPhdr, + type HydraShdr } from '@src/synth/soundfont/Hydra'; import { Channel } from '@src/synth/synthesis/Channel'; import { Channels } from '@src/synth/synthesis/Channels'; @@ -30,7 +30,7 @@ import { LoopMode } from '@src/synth/synthesis/LoopMode'; import { OutputMode } from '@src/synth/synthesis/OutputMode'; import { Preset } from '@src/synth/synthesis/Preset'; import { Region } from '@src/synth/synthesis/Region'; -import { SynthEvent } from '@src/synth/synthesis/SynthEvent'; +import type { SynthEvent } from '@src/synth/synthesis/SynthEvent'; import { Voice } from '@src/synth/synthesis/Voice'; import { VoiceEnvelopeSegment } from '@src/synth/synthesis/VoiceEnvelope'; import { SynthHelper } from '@src/synth/SynthHelper'; @@ -81,8 +81,8 @@ export class TinySoundFont { } public channelSetMixVolume(channel: number, volume: number): void { - let c: Channel = this.channelInit(channel); - for (let v of this._voices) { + const c: Channel = this.channelInit(channel); + for (const v of this._voices) { if (v.playingChannel === channel && v.playingPreset !== -1) { v.mixVolume = volume; } @@ -184,7 +184,7 @@ export class TinySoundFont { // process in micro-buffers // process events for first microbuffer while (!this._midiEventQueue.isEmpty) { - let m: SynthEvent = this._midiEventQueue.dequeue(); + const m: SynthEvent = this._midiEventQueue.dequeue(); if (m.isMetronome && this.metronomeVolume > 0) { this.channelNoteOff(SynthConstants.MetronomeChannel, SynthConstants.MetronomeKey); this.channelNoteOn(SynthConstants.MetronomeChannel, SynthConstants.MetronomeKey, 95 / 127); @@ -202,7 +202,7 @@ export class TinySoundFont { // exception. metronome is implicitly added in solo const isChannelMuted: boolean = this._mutedChannels.has(channel) || - (anySolo && channel != SynthConstants.MetronomeChannel && !this._soloChannels.has(channel)); + (anySolo && channel !== SynthConstants.MetronomeChannel && !this._soloChannels.has(channel)); if (!buffer) { voice.kill(); @@ -216,7 +216,7 @@ export class TinySoundFont { } private processMidiMessage(e: MidiEvent): void { - Logger.debug('Midi', 'Processing Midi message ' + MidiEventType[e.type] + '/' + e.tick); + Logger.debug('Midi', `Processing Midi message ${MidiEventType[e.type]}/${e.tick}`); const command: MidiEventType = e.type; switch (command) { case MidiEventType.TimeSignature: @@ -279,7 +279,7 @@ export class TinySoundFont { } public set masterVolume(value: number) { - var gainDb = SynthHelper.gainToDecibels(value); + const gainDb = SynthHelper.gainToDecibels(value); const gainDBChange: number = gainDb - this.globalGainDb; if (gainDBChange === 0) { return; @@ -355,7 +355,7 @@ export class TinySoundFont { * Stop all playing notes immediatly and reset all channel parameters */ public reset(): void { - for (let v of this._voices) { + for (const v of this._voices) { if ( v.playingPreset !== -1 && (v.ampEnv.segment < VoiceEnvelopeSegment.Release || v.ampEnv.parameters!.release !== 0) @@ -420,7 +420,7 @@ export class TinySoundFont { } } } else { - for (let v of this._voices) { + for (const v of this._voices) { if (v.playingPreset === -1) { voice = v; } @@ -488,7 +488,7 @@ export class TinySoundFont { * @returns returns false if preset does not exist, otherwise true */ public bankNoteOn(bank: number, presetNumber: number, key: number, vel: number): boolean { - let presetIndex: number = this.getPresetIndex(bank, presetNumber); + const presetIndex: number = this.getPresetIndex(bank, presetNumber); if (presetIndex === -1) { return false; } @@ -502,15 +502,17 @@ export class TinySoundFont { public noteOff(presetIndex: number, key: number): void { let matchFirst: Voice | null = null; let matchLast: Voice | null = null; - let matches: Voice[] = []; - for (let v of this._voices) { + const matches: Voice[] = []; + for (const v of this._voices) { if ( v.playingPreset !== presetIndex || v.playingKey !== key || v.ampEnv.segment >= VoiceEnvelopeSegment.Release ) { continue; - } else if (!matchFirst || v.playIndex < matchFirst.playIndex) { + } + + if (!matchFirst || v.playIndex < matchFirst.playIndex) { matchFirst = v; matchLast = v; matches.push(v); @@ -587,7 +589,7 @@ export class TinySoundFont { } for (let i: number = this._channels.channelList.length; i <= channel; i++) { - let c: Channel = new Channel(); + const c: Channel = new Channel(); c.presetIndex = 0; c.bank = 0; c.pitchWheel = 8192; @@ -617,7 +619,7 @@ export class TinySoundFont { // search reverse (last import wins) for (let i: number = this.presets.length - 1; i >= 0; i--) { - let preset: Preset = this.presets[i]; + const preset: Preset = this.presets[i]; if (preset.presetNumber === presetNumber && preset.bank === bank) { return i; } @@ -703,7 +705,7 @@ export class TinySoundFont { } } - let c: Channel = this.channelInit(channel); + const c: Channel = this.channelInit(channel); c.perNotePitchWheel.delete(key); if (!matchFirst) { @@ -733,7 +735,7 @@ export class TinySoundFont { * @param channel channel number */ public channelNoteOffAll(channel: number): void { - let c: Channel = this.channelInit(channel); + const c: Channel = this.channelInit(channel); c.perNotePitchWheel.clear(); for (const v of this._voices) { @@ -752,10 +754,10 @@ export class TinySoundFont { * @param channel channel number */ public channelSoundsOffAll(channel: number): void { - let c: Channel = this.channelInit(channel); + const c: Channel = this.channelInit(channel); c.perNotePitchWheel.clear(); - for (let v of this._voices) { + for (const v of this._voices) { if ( v.playingPreset !== -1 && v.playingChannel === channel && @@ -835,7 +837,7 @@ export class TinySoundFont { public channelSetPan(channel: number, pan: number): void { for (const v of this._voices) { if (v.playingChannel === channel && v.playingPreset !== -1) { - let newPan: number = v.region!.pan + pan - 0.5; + const newPan: number = v.region!.pan + pan - 0.5; if (newPan <= -0.5) { v.panFactorLeft = 1; v.panFactorRight = 0; @@ -910,7 +912,7 @@ export class TinySoundFont { private channelApplyPitch(channel: number, c: Channel, key: number = -1): void { for (const v of this._voices) { - if (v.playingChannel === channel && v.playingPreset !== -1 && (key == -1 || v.playingKey === key)) { + if (v.playingChannel === channel && v.playingPreset !== -1 && (key === -1 || v.playingKey === key)) { v.updatePitchRatio(c, this.outSampleRate); } } @@ -950,7 +952,7 @@ export class TinySoundFont { * Apply a MIDI control change to the channel (not all controllers are supported!) */ public channelMidiControl(channel: number, controller: ControllerType, controlValue: number): void { - let c: Channel = this.channelInit(channel); + const c: Channel = this.channelInit(channel); switch (controller) { case ControllerType.DataEntryFine: c.midiData = TypeConversions.int32ToUint16((c.midiData & 0x3f80) | controlValue); @@ -1161,7 +1163,7 @@ export class TinySoundFont { let phivel: number = 127; for (let pgenIndex: number = pbag.genNdx; pgenIndex < hydra.pbags[pbagIndex + 1].genNdx; pgenIndex++) { - let pgen: HydraPgen = hydra.pgens[pgenIndex]; + const pgen: HydraPgen = hydra.pgens[pgenIndex]; if (pgen.genOper === HydraPgen.GenKeyRange) { plokey = pgen.genAmount.lowByteAmount; @@ -1183,13 +1185,13 @@ export class TinySoundFont { continue; } - let pinst: HydraInst = hydra.insts[pgen.genAmount.wordAmount]; + const pinst: HydraInst = hydra.insts[pgen.genAmount.wordAmount]; for ( let ibagIndex: number = pinst.instBagNdx; ibagIndex < hydra.insts[pgen.genAmount.wordAmount + 1].instBagNdx; ibagIndex++ ) { - let ibag: HydraIbag = hydra.ibags[ibagIndex]; + const ibag: HydraIbag = hydra.ibags[ibagIndex]; let ilokey: number = 0; let ihikey: number = 127; @@ -1201,7 +1203,7 @@ export class TinySoundFont { igenIndex < hydra.ibags[ibagIndex + 1].instGenNdx; igenIndex++ ) { - let igen: HydraIgen = hydra.igens[igenIndex]; + const igen: HydraIgen = hydra.igens[igenIndex]; if (igen.genOper === HydraPgen.GenKeyRange) { ilokey = igen.genAmount.lowByteAmount; ihikey = igen.genAmount.highByteAmount; @@ -1250,7 +1252,7 @@ export class TinySoundFont { // Instrument. if (pgen.genOper === HydraPgen.GenInstrument) { - let whichInst: number = pgen.genAmount.wordAmount; + const whichInst: number = pgen.genAmount.wordAmount; if (whichInst >= hydra.insts.length) { continue; } @@ -1259,14 +1261,14 @@ export class TinySoundFont { instRegion.clear(false); // Generators - let inst: HydraInst = hydra.insts[whichInst]; + const inst: HydraInst = hydra.insts[whichInst]; for ( let ibagIndex: number = inst.instBagNdx; ibagIndex < hydra.insts[whichInst + 1].instBagNdx; ibagIndex++ ) { - let ibag: HydraIbag = hydra.ibags[ibagIndex]; - let zoneRegion: Region = new Region(instRegion); + const ibag: HydraIbag = hydra.ibags[ibagIndex]; + const zoneRegion: Region = new Region(instRegion); let hadSampleId: boolean = false; for ( @@ -1274,7 +1276,7 @@ export class TinySoundFont { igenIndex < hydra.ibags[ibagIndex + 1].instGenNdx; igenIndex++ ) { - let igen: HydraIgen = hydra.igens[igenIndex]; + const igen: HydraIgen = hydra.igens[igenIndex]; if (igen.genOper === HydraPgen.GenSampleId) { // preset region key and vel ranges are a filter for the zone regions @@ -1368,7 +1370,7 @@ export class TinySoundFont { zoneRegion.initialFilterQ = 0; } - let shdr: HydraShdr = hydra.sHdrs[igen.genAmount.wordAmount]; + const shdr: HydraShdr = hydra.sHdrs[igen.genAmount.wordAmount]; zoneRegion.offset += shdr.start; zoneRegion.end += shdr.end; zoneRegion.loopStart += shdr.startLoop; @@ -1384,7 +1386,7 @@ export class TinySoundFont { zoneRegion.tune += shdr.pitchCorrection; zoneRegion.sampleRate = shdr.sampleRate; - const isPercussion = phdr.bank == SynthConstants.PercussionBank; + const isPercussion = phdr.bank === SynthConstants.PercussionBank; const shouldLoadSamples = (isPercussion && @@ -1411,11 +1413,7 @@ export class TinySoundFont { const decompressVorbis = (shdr.sampleType & 0x10) !== 0; if (decompressVorbis) { // for SF3 the shdr contains the byte offsets within the overall buffer holding the OGG container - zoneRegion.samples = hydra.decodeSamples( - shdr.start, - shdr.end, - true - ); + zoneRegion.samples = hydra.decodeSamples(shdr.start, shdr.end, true); // loop points are already relative within the individual samples. } else { zoneRegion.samples = hydra.decodeSamples( @@ -1434,7 +1432,7 @@ export class TinySoundFont { // The DWORD dwEndloop contains the index, in sample data points, from the beginning of the sample data field to the first // data point following the loop of this sample. Note that this is the data point “equivalent to” the first loop data point, and that // to produce portable artifact free loops, the eight proximal data points surrounding both the Startloop and Endloop points - // should be identical. + // should be identical. // reset offsets relative to sub-buffer if (zoneRegion.loopStart > 0) { @@ -1542,7 +1540,7 @@ export class TinySoundFont { } for (const preset of presets) { - if (preset.bank == SynthConstants.PercussionBank) { + if (preset.bank === SynthConstants.PercussionBank) { for (const region of preset.regions!) { if (region.loKey >= key && region.hiKey <= key && region.samples.length > 0) { return true; diff --git a/src/synth/synthesis/Voice.ts b/src/synth/synthesis/Voice.ts index e363057ba..83f8bd3c3 100644 --- a/src/synth/synthesis/Voice.ts +++ b/src/synth/synthesis/Voice.ts @@ -4,14 +4,14 @@ // Licensed under: MPL-2.0 import { LoopMode } from '@src/synth/synthesis/LoopMode'; import { OutputMode } from '@src/synth/synthesis/OutputMode'; -import { Region } from '@src/synth/synthesis/Region'; -import { TinySoundFont } from '@src/synth/synthesis/TinySoundFont'; +import type { Region } from '@src/synth/synthesis/Region'; +import type { TinySoundFont } from '@src/synth/synthesis/TinySoundFont'; import { VoiceEnvelope, VoiceEnvelopeSegment } from '@src/synth/synthesis/VoiceEnvelope'; import { VoiceLfo } from '@src/synth/synthesis/VoiceLfo'; import { VoiceLowPass } from '@src/synth/synthesis/VoiceLowPass'; import { SynthHelper } from '@src/synth/SynthHelper'; import { SynthConstants } from '@src/synth/SynthConstants'; -import { Channel } from '@src/synth/synthesis/Channel'; +import type { Channel } from '@src/synth/synthesis/Channel'; export class Voice { /** @@ -53,12 +53,11 @@ export class Voice { let pitchWheel = c.pitchWheel; // add additional note pitch if (c.perNotePitchWheel.has(this.playingKey)) { - pitchWheel += (c.perNotePitchWheel.get(this.playingKey)! - 8192); + pitchWheel += c.perNotePitchWheel.get(this.playingKey)! - 8192; } - const pitchShift: number = pitchWheel === 8192 - ? c.tuning - : (pitchWheel / 16383.0 * c.pitchRange * 2) - c.pitchRange + c.tuning; + const pitchShift: number = + pitchWheel === 8192 ? c.tuning : (pitchWheel / 16383.0) * c.pitchRange * 2 - c.pitchRange + c.tuning; this.calcPitchRatio(pitchShift, outSampleRate); } @@ -71,7 +70,9 @@ export class Voice { const note: number = this.playingKey + this.region.transpose + this.region.tune / 100.0; let adjustedPitch: number = this.region.pitchKeyCenter + (note - this.region.pitchKeyCenter) * (this.region.pitchKeyTrack / 100.0); - if (pitchShift !== 0) adjustedPitch += pitchShift; + if (pitchShift !== 0) { + adjustedPitch += pitchShift; + } this.pitchInputTimecents = adjustedPitch * 100.0; this.pitchOutputFactor = this.region.sampleRate / (SynthHelper.timecents2Secs(this.region.pitchKeyCenter * 100.0) * outSampleRate); @@ -108,40 +109,40 @@ export class Voice { return; } - let region: Region = this.region; - let input: Float32Array = region.samples; + const region: Region = this.region; + const input: Float32Array = region.samples; let outL: number = 0; let outR: number = f.outputMode === OutputMode.StereoUnweaved ? numSamples : -1; // Cache some values, to give them at least some chance of ending up in registers. - let updateModEnv: boolean = region.modEnvToPitch !== 0 || region.modEnvToFilterFc !== 0; - let updateModLFO: boolean = + const updateModEnv: boolean = region.modEnvToPitch !== 0 || region.modEnvToFilterFc !== 0; + const updateModLFO: boolean = this.modLfo.delta > 0 && (region.modLfoToPitch !== 0 || region.modLfoToFilterFc !== 0 || region.modLfoToVolume !== 0); - let updateVibLFO: boolean = this.vibLfo.delta > 0 && region.vibLfoToPitch !== 0; - let isLooping: boolean = this.loopStart < this.loopEnd; - let tmpLoopStart: number = this.loopStart; - let tmpLoopEnd: number = this.loopEnd; - let tmpSampleEndDbl: number = region.end; - let tmpLoopEndDbl: number = tmpLoopEnd + 1.0; + const updateVibLFO: boolean = this.vibLfo.delta > 0 && region.vibLfoToPitch !== 0; + const isLooping: boolean = this.loopStart < this.loopEnd; + const tmpLoopStart: number = this.loopStart; + const tmpLoopEnd: number = this.loopEnd; + const tmpSampleEndDbl: number = region.end; + const tmpLoopEndDbl: number = tmpLoopEnd + 1.0; let tmpSourceSamplePosition: number = this.sourceSamplePosition; - let tmpLowpass: VoiceLowPass = new VoiceLowPass(this.lowPass); + const tmpLowpass: VoiceLowPass = new VoiceLowPass(this.lowPass); - let dynamicLowpass: boolean = region.modLfoToFilterFc !== 0 || region.modEnvToFilterFc !== 0; + const dynamicLowpass: boolean = region.modLfoToFilterFc !== 0 || region.modEnvToFilterFc !== 0; let tmpSampleRate: number = 0; let tmpInitialFilterFc: number = 0; let tmpModLfoToFilterFc: number = 0; let tmpModEnvToFilterFc: number = 0; - let dynamicPitchRatio: boolean = + const dynamicPitchRatio: boolean = region.modLfoToPitch !== 0 || region.modEnvToPitch !== 0 || region.vibLfoToPitch !== 0; let pitchRatio: number = 0; let tmpModLfoToPitch: number = 0; let tmpVibLfoToPitch: number = 0; let tmpModEnvToPitch: number = 0; - let dynamicGain: boolean = region.modLfoToVolume !== 0; + const dynamicGain: boolean = region.modLfoToVolume !== 0; let noteGain: number = 0; let tmpModLfoToVolume: number = 0; @@ -180,11 +181,12 @@ export class Voice { let gainMono: number; let gainLeft: number; let gainRight: number = 0; - let blockSamples: number = numSamples > Voice.RenderEffectSampleBlock ? Voice.RenderEffectSampleBlock : numSamples; + let blockSamples: number = + numSamples > Voice.RenderEffectSampleBlock ? Voice.RenderEffectSampleBlock : numSamples; numSamples -= blockSamples; if (dynamicLowpass) { - let fres: number = + const fres: number = tmpInitialFilterFc + this.modLfo.level * tmpModLfoToFilterFc + this.modEnv.level * tmpModEnvToFilterFc; @@ -198,9 +200,9 @@ export class Voice { pitchRatio = SynthHelper.timecents2Secs( this.pitchInputTimecents + - (this.modLfo.level * tmpModLfoToPitch + - this.vibLfo.level * tmpVibLfoToPitch + - this.modEnv.level * tmpModEnvToPitch) + (this.modLfo.level * tmpModLfoToPitch + + this.vibLfo.level * tmpVibLfoToPitch + + this.modEnv.level * tmpModEnvToPitch) ) * this.pitchOutputFactor; } @@ -236,17 +238,19 @@ export class Voice { gainLeft = gainMono * this.panFactorLeft; gainRight = gainMono * this.panFactorRight; while (blockSamples-- > 0 && tmpSourceSamplePosition < tmpSampleEndDbl) { - let pos: number = tmpSourceSamplePosition | 0; - let nextPos: number = pos >= tmpLoopEnd && isLooping ? tmpLoopStart : pos + 1; + const pos: number = tmpSourceSamplePosition | 0; + const nextPos: number = pos >= tmpLoopEnd && isLooping ? tmpLoopStart : pos + 1; // Simple linear interpolation. // TODO: check for interpolation mode on voice - let alpha: number = tmpSourceSamplePosition - pos; + const alpha: number = tmpSourceSamplePosition - pos; let value: number = input[pos] * (1.0 - alpha) + input[nextPos] * alpha; // Low-pass filter. - if (tmpLowpass.active) value = tmpLowpass.process(value); + if (tmpLowpass.active) { + value = tmpLowpass.process(value); + } outputBuffer[offset + outL] += value * gainLeft; outL++; @@ -264,15 +268,17 @@ export class Voice { gainLeft = gainMono * this.panFactorLeft; gainRight = gainMono * this.panFactorRight; while (blockSamples-- > 0 && tmpSourceSamplePosition < tmpSampleEndDbl) { - let pos: number = tmpSourceSamplePosition | 0; - let nextPos: number = pos >= tmpLoopEnd && isLooping ? tmpLoopStart : pos + 1; + const pos: number = tmpSourceSamplePosition | 0; + const nextPos: number = pos >= tmpLoopEnd && isLooping ? tmpLoopStart : pos + 1; // Simple linear interpolation. - let alpha: number = tmpSourceSamplePosition - pos; + const alpha: number = tmpSourceSamplePosition - pos; let value: number = input[pos] * (1.0 - alpha) + input[nextPos] * alpha; // Low-pass filter. - if (tmpLowpass.active) value = tmpLowpass.process(value); + if (tmpLowpass.active) { + value = tmpLowpass.process(value); + } outputBuffer[offset + outL] += value * gainLeft; outL++; @@ -288,15 +294,17 @@ export class Voice { break; case OutputMode.Mono: while (blockSamples-- > 0 && tmpSourceSamplePosition < tmpSampleEndDbl) { - let pos: number = tmpSourceSamplePosition | 0; - let nextPos: number = pos >= tmpLoopEnd && isLooping ? tmpLoopStart : pos + 1; + const pos: number = tmpSourceSamplePosition | 0; + const nextPos: number = pos >= tmpLoopEnd && isLooping ? tmpLoopStart : pos + 1; // Simple linear interpolation. - let alpha: number = tmpSourceSamplePosition - pos; + const alpha: number = tmpSourceSamplePosition - pos; let value: number = input[pos] * (1.0 - alpha) + input[nextPos] * alpha; // Low-pass filter. - if (tmpLowpass.active) value = tmpLowpass.process(value); + if (tmpLowpass.active) { + value = tmpLowpass.process(value); + } outputBuffer[offset + outL] = value * gainMono; outL++; diff --git a/src/synth/synthesis/VoiceEnvelope.ts b/src/synth/synthesis/VoiceEnvelope.ts index e3f4102ed..c42e6cc88 100644 --- a/src/synth/synthesis/VoiceEnvelope.ts +++ b/src/synth/synthesis/VoiceEnvelope.ts @@ -6,14 +6,14 @@ import { Envelope } from '@src/synth/synthesis/Envelope'; import { SynthHelper } from '@src/synth/SynthHelper'; export enum VoiceEnvelopeSegment { - None, - Delay, - Attack, - Hold, - Decay, - Sustain, - Release, - Done + None = 0, + Delay = 1, + Attack = 2, + Hold = 3, + Decay = 4, + Sustain = 5, + Release = 6, + Done = 7 } export class VoiceEnvelope { @@ -57,7 +57,6 @@ export class VoiceEnvelope { this.samplesUntilNextSegment = (this.parameters.attack * outSampleRate) | 0; if (this.samplesUntilNextSegment > 0) { - if (!this.isAmpEnv) { // mod env attack duration scales with velocity (velocity of 1 is full duration, max velocity is 0.125 times duration) this.samplesUntilNextSegment = @@ -95,7 +94,7 @@ export class VoiceEnvelope { if (this.isAmpEnv) { // I don't truly understand this; just following what LinuxSampler does. - let mysterySlope: number = -9.226 / this.samplesUntilNextSegment; + const mysterySlope: number = -9.226 / this.samplesUntilNextSegment; this.slope = Math.exp(mysterySlope); this.segmentIsExponential = true; if (this.parameters.sustain > 0.0) { @@ -130,11 +129,12 @@ export class VoiceEnvelope { this.segment = VoiceEnvelopeSegment.Release; this.samplesUntilNextSegment = ((this.parameters.release <= 0 ? VoiceEnvelope.FastReleaseTime : this.parameters.release) * - outSampleRate) | 0; + outSampleRate) | + 0; if (this.isAmpEnv) { // I don't truly understand this; just following what LinuxSampler does. - let mysterySlope: number = -9.226 / this.samplesUntilNextSegment; + const mysterySlope: number = -9.226 / this.samplesUntilNextSegment; this.slope = Math.exp(mysterySlope); this.segmentIsExponential = true; } else { @@ -188,7 +188,7 @@ export class VoiceEnvelope { this.level += this.slope * numSamples; } } - + this.samplesUntilNextSegment -= numSamples; if (this.samplesUntilNextSegment <= 0) { this.nextSegment(this.segment, outSampleRate); diff --git a/src/synth/synthesis/VoiceLowPass.ts b/src/synth/synthesis/VoiceLowPass.ts index ebf2277d8..0c55f86f9 100644 --- a/src/synth/synthesis/VoiceLowPass.ts +++ b/src/synth/synthesis/VoiceLowPass.ts @@ -27,9 +27,9 @@ export class VoiceLowPass { public setup(fc: number): void { // Lowpass filter from http://www.earlevel.com/main/2012/11/26/biquad-c-source-code/ - let k: number = Math.tan(Math.PI * fc); - let KK: number = k * k; - let norm: number = 1 / (1 + k * this.qInv + KK); + const k: number = Math.tan(Math.PI * fc); + const KK: number = k * k; + const norm: number = 1 / (1 + k * this.qInv + KK); this.a0 = KK * norm; this.a1 = 2 * this.a0; this.b1 = 2 * (KK - 1) * norm; @@ -37,7 +37,7 @@ export class VoiceLowPass { } public process(input: number): number { - let output: number = input * this.a0 + this.z1; + const output: number = input * this.a0 + this.z1; this.z1 = input * this.a1 + this.z2 - this.b1 * output; this.z2 = input * this.a0 - this.b2 * output; return output; diff --git a/src/synth/vorbis/IntBitReader.ts b/src/synth/vorbis/IntBitReader.ts index 1d3277619..82fdba26d 100644 --- a/src/synth/vorbis/IntBitReader.ts +++ b/src/synth/vorbis/IntBitReader.ts @@ -26,7 +26,7 @@ */ import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; -import { IReadable } from '@src/io/IReadable'; +import type { IReadable } from '@src/io/IReadable'; class IntBitReaderReadResult { public value: number = 0; @@ -50,7 +50,7 @@ export class IntBitReader { } public readBit(): boolean { - return this.readBits(1) == 1; + return this.readBits(1) === 1; } public readBytes(count: number): Uint8Array { @@ -63,7 +63,7 @@ export class IntBitReader { public readBits(count: number): number { // short-circuit 0 - if (count == 0) { + if (count === 0) { return 0; } @@ -75,16 +75,17 @@ export class IntBitReader { } public tryPeekBits(count: number): IntBitReaderReadResult { - if (count < 0 || count > 32) + if (count < 0 || count > 32) { throw new AlphaTabError(AlphaTabErrorType.General, 'IO: Cannot read more than 32 bits in one go'); - if (count == 0) { + } + if (count === 0) { return new IntBitReaderReadResult(); } const result = new IntBitReaderReadResult(); while (this._bitCount < count) { const val = BigInt(this._source.readByte()); - if (val == -1n) { + if (val === -1n) { result.bitsRead = Number(this._bitCount); result.value = Number(this._bitBucket); this._bitBucket = 0n; @@ -102,7 +103,7 @@ export class IntBitReader { let bitBucket = this._bitBucket; if (count < 64) { - bitBucket = bitBucket & (1n << BigInt(count)) - 1n; + bitBucket = bitBucket & ((1n << BigInt(count)) - 1n); } result.value = Number(bitBucket); @@ -113,7 +114,7 @@ export class IntBitReader { public skipBits(count: number) { let bigCount = BigInt(count); - if (count == 0) { + if (count === 0) { // no-op } else if (this._bitCount > bigCount) { // we still have bits left over... @@ -133,7 +134,7 @@ export class IntBitReader { } this._bitCount -= bigCount; - } else if (this._bitCount == bigCount) { + } else if (this._bitCount === bigCount) { this._bitBucket = 0n; this._bitCount = 0n; } // _bitCount < count @@ -144,7 +145,7 @@ export class IntBitReader { this._bitBucket = 0n; while (bigCount > 8) { - if (this._source.readByte() == -1) { + if (this._source.readByte() === -1) { bigCount = 0n; break; } @@ -153,7 +154,7 @@ export class IntBitReader { if (bigCount > 0) { const temp = BigInt(this._source.readByte()); - if (temp == -1n) { + if (temp === -1n) { } else { this._bitBucket = temp >> bigCount; this._bitCount = 8n - bigCount; diff --git a/src/synth/vorbis/OggReader.ts b/src/synth/vorbis/OggReader.ts index 5bed2666e..b6e8abfb6 100644 --- a/src/synth/vorbis/OggReader.ts +++ b/src/synth/vorbis/OggReader.ts @@ -1,18 +1,18 @@ import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; import { IOHelper } from '@src/io/IOHelper'; -import { IReadable } from '@src/io/IReadable'; +import type { IReadable } from '@src/io/IReadable'; export class OggPacket { public packetData: Uint8Array; public isBeginningOfStream: boolean; public isEndOfStream: boolean; - public granulePosition: number|null; + public granulePosition: number | null; public constructor( data: Uint8Array, isBeginOfStream: boolean, isEndOfStream: boolean, - granulePosition: number|null + granulePosition: number | null ) { this.packetData = data; this.isBeginningOfStream = isBeginOfStream; @@ -59,7 +59,7 @@ export class OggReader { // search for sync byte (max 64KB) for (let i = 0; i < 65536; i++) { const magic = IOHelper.readInt32LE(this._readable); - if (magic == 0x5367674f) { + if (magic === 0x5367674f) { return true; } this._readable.position -= 3; @@ -81,7 +81,7 @@ export class OggReader { this._readable.skip(4); // Crc const segmentCount = this._readable.readByte(); - if (segmentCount == -1) { + if (segmentCount === -1) { return false; } @@ -91,7 +91,7 @@ export class OggReader { const size = this._readable.readByte(); // ensure packet size exists and add size - if (packetIndex == packetSizes.length) { + if (packetIndex === packetSizes.length) { packetSizes.push(0); } packetSizes[packetIndex] += size; @@ -105,22 +105,23 @@ export class OggReader { for (let i = 0; i < packetSizes.length; i++) { const packetData = new Uint8Array(packetSizes[i]); const c = this._readable.read(packetData, 0, packetData.length); - if (c != packetData.length) { + if (c !== packetData.length) { return false; } - if ((pageFlags & PageFlags.ContinuesPacket) != 0) { - if (packets.length == 0) + if ((pageFlags & PageFlags.ContinuesPacket) !== 0) { + if (packets.length === 0) { throw new AlphaTabError( AlphaTabErrorType.Format, 'OGG: Continuation page without any previous packets' ); + } packets[packets.length - 1].addData(packetData); } else { const packet = new OggPacket( packetData, - (pageFlags & PageFlags.BeginningOfStream) != 0 && i == 0, - (pageFlags & PageFlags.EndOfStream) != 0 && i == packetSizes.length - 1, + (pageFlags & PageFlags.BeginningOfStream) !== 0 && i === 0, + (pageFlags & PageFlags.EndOfStream) !== 0 && i === packetSizes.length - 1, pageGranulePosition ); packets.push(packet); diff --git a/src/synth/vorbis/VorbisFile.ts b/src/synth/vorbis/VorbisFile.ts index 9e14d76be..ab8793a1e 100644 --- a/src/synth/vorbis/VorbisFile.ts +++ b/src/synth/vorbis/VorbisFile.ts @@ -1,24 +1,22 @@ -import { IReadable } from '@src/io/IReadable'; -import { OggReader } from './OggReader'; -import { VorbisStream } from './VorbisStream'; -import { VorbisStreamReader } from './VorbisStreamReader'; +import type { IReadable } from '@src/io/IReadable'; +import { OggReader } from '@src/synth/vorbis/OggReader'; +import type { VorbisStream } from '@src/synth/vorbis/VorbisStream'; +import { VorbisStreamReader } from '@src/synth/vorbis/VorbisStreamReader'; export class VorbisFile { public streams: VorbisStream[] = []; public constructor(readable: IReadable) { - var oggContainer = new OggReader(readable); - var packets = oggContainer.read(); + const oggContainer = new OggReader(readable); + const packets = oggContainer.read(); - var decoder = new VorbisStreamReader(packets); - while(true){ - let stream = decoder.read(); - if(stream == null){ + const decoder = new VorbisStreamReader(packets); + while (true) { + const stream = decoder.read(); + if (stream == null) { break; } - else{ - this.streams.push(stream); - } + this.streams.push(stream); } } } diff --git a/src/synth/vorbis/VorbisStream.ts b/src/synth/vorbis/VorbisStream.ts index 63e57cef2..01b50f5da 100644 --- a/src/synth/vorbis/VorbisStream.ts +++ b/src/synth/vorbis/VorbisStream.ts @@ -7,4 +7,4 @@ export class VorbisStream { public bitrateMinimum: number = 0; public blocksize0: number = 0; public blocksize1: number = 0; -} \ No newline at end of file +} diff --git a/src/synth/vorbis/VorbisStreamDecoder.ts b/src/synth/vorbis/VorbisStreamDecoder.ts index 2b7374e31..14bc3a6df 100644 --- a/src/synth/vorbis/VorbisStreamDecoder.ts +++ b/src/synth/vorbis/VorbisStreamDecoder.ts @@ -26,11 +26,11 @@ */ import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; -import { VorbisStream } from './VorbisStream'; -import { OggPacket } from './OggReader'; +import type { VorbisStream } from '@src/synth/vorbis/VorbisStream'; +import type { OggPacket } from '@src/synth/vorbis/OggReader'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { TypeConversions } from '@src/io/TypeConversions'; -import { IntBitReader } from './IntBitReader'; +import { IntBitReader } from '@src/synth/vorbis/IntBitReader'; export class VorbisSetupHeader { public codebooks: VorbisCodebook[] = []; @@ -112,7 +112,7 @@ export class VorbisCodebook { public constructor(packet: IntBitReader, huffman: Huffman) { // first, check the sync pattern const chkVal = packet.readBits(24); - if (chkVal != 0x564342) { + if (chkVal !== 0x564342) { throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Book header had invalid signature!'); } @@ -133,7 +133,9 @@ export class VorbisCodebook { public decodeScalar(packet: IntBitReader): number { let data = packet.tryPeekBits(this._prefixBitLength); - if (data.bitsRead == 0) return -1; + if (data.bitsRead === 0) { + return -1; + } // try to get the value from the prefix list... let node = this._prefixList![data.value]; @@ -149,7 +151,7 @@ export class VorbisCodebook { for (let i = 0; i < this._overflowList.length; i++) { node = this._overflowList[i]!; const bits = data.value & node.mask; - if (node.bits == bits) { + if (node.bits === bits) { packet.skipBits(node.length); return node.value; } @@ -219,7 +221,7 @@ export class VorbisCodebook { let codewords: Int32Array | null = null; if (!sparse) { codewords = new Int32Array(this.entries); - } else if (sortedCount != 0) { + } else if (sortedCount !== 0) { codewordLengths = new Int32Array(sortedCount); codewords = new Int32Array(sortedCount); values = new Int32Array(sortedCount); @@ -256,7 +258,7 @@ export class VorbisCodebook { break; } } - if (k == n) { + if (k === n) { return true; } @@ -272,18 +274,18 @@ export class VorbisCodebook { continue; } - while (z > 0 && available[z] == 0) { + while (z > 0 && available[z] === 0) { --z; } - if (z == 0) { + if (z === 0) { return false; } - let res = available[z]; + const res = available[z]; available[z] = 0; this.addEntry(sparse, codewords, codewordLengths, VorbisUtils.bitReverse(res), i, m++, len[i], values); - if (z != len[i]) { + if (z !== len[i]) { for (let y = len[i]; y > z; --y) { available[y] = res + (1 << (32 - y)); } @@ -314,7 +316,7 @@ export class VorbisCodebook { private initLookupTable(packet: IntBitReader) { this.mapType = packet.readBits(4); - if (this.mapType == 0) { + if (this.mapType === 0) { return; } @@ -325,7 +327,7 @@ export class VorbisCodebook { let lookupValueCount = this.entries * this.dimensions; const lookupTable = new Float32Array(lookupValueCount); - if (this.mapType == 1) { + if (this.mapType === 1) { lookupValueCount = this.lookup1Values(); } @@ -335,12 +337,12 @@ export class VorbisCodebook { } // now that we have the initial data read in, calculate the entry tree - if (this.mapType == 1) { + if (this.mapType === 1) { for (let idx = 0; idx < this.entries; idx++) { let last = 0.0; let idxDiv = 1; for (let i = 0; i < this.dimensions; i++) { - const moff = (idx / idxDiv) % lookupValueCount | 0; + const moff = ((idx / idxDiv) % lookupValueCount) | 0; const value = multiplicands[moff] * deltaValue + minValue + last; lookupTable[idx * this.dimensions + i] = value; @@ -359,7 +361,9 @@ export class VorbisCodebook { const value = multiplicands[moff] * deltaValue + minValue + last; lookupTable[idx * this.dimensions + i] = value; - if (sequence_p) last = value; + if (sequence_p) { + last = value; + } ++moff; } @@ -507,7 +511,7 @@ export class VorbisFloor0 implements IVorbisFloor { this._ampOfs = packet.readBits(8); this._books = new Array(packet.readBits(4) + 1); - if (this._order < 1 || this._rate < 1 || this._bark_map_size < 1 || this._books.length == 0) { + if (this._order < 1 || this._rate < 1 || this._bark_map_size < 1 || this._books.length === 0) { throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Floor0 Data'); } @@ -521,7 +525,7 @@ export class VorbisFloor0 implements IVorbisFloor { const book = codebooks[num]; - if (book.mapType == 0 || book.dimensions < 1) { + if (book.mapType === 0 || book.dimensions < 1) { throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Floor0 Data'); } @@ -569,7 +573,7 @@ export class VorbisFloor0 implements IVorbisFloor { } public unpack(packet: IntBitReader, blockSize: number, channel: number): IVorbisFloorData { - var data = new VorbisFloorData0(new Float32Array(this._order + 1)); + const data = new VorbisFloorData0(new Float32Array(this._order + 1)); data.amp = packet.readBits(this._ampBits); @@ -587,7 +591,7 @@ export class VorbisFloor0 implements IVorbisFloor { // first, the book decode... for (let i = 0; i < this._order; ) { const entry = book.decodeScalar(packet); - if (entry == -1) { + if (entry === -1) { // we ran out of data or the packet is corrupt... 0 the floor and return data.amp = 0; return data; @@ -612,7 +616,7 @@ export class VorbisFloor0 implements IVorbisFloor { public apply(floorData: IVorbisFloorData, blockSize: number, residue: Float32Array): void { const data = floorData as VorbisFloorData0; - var n = blockSize / 2; + const n = blockSize / 2; if (data.amp > 0) { // this is pretty well stolen directly from libvorbis... BSD license @@ -630,19 +634,24 @@ export class VorbisFloor0 implements IVorbisFloor { const k = barkMap[i]; let p = 0.5; let q = 0.5; - let w = wMap[k]; + const w = wMap[k]; for (j = 1; j < this._order; j += 2) { q *= w - data.coeff[j - 1]; p *= w - data.coeff[j]; } - if (j == this._order) { + if (j === this._order) { // odd order filter; slightly assymetric + q *= w - data.coeff[j - 1]; + // biome-ignore lint/suspicious/noMisrefactoredShorthandAssign: Correct calculation here p *= p * (4 - w * w); q *= q; } else { // even order filter; still symetric + + // biome-ignore lint/suspicious/noMisrefactoredShorthandAssign: Correct calculation here p *= p * (2 - w); + // biome-ignore lint/suspicious/noMisrefactoredShorthandAssign: Correct calculation here q *= q * (2 + w); } @@ -654,7 +663,9 @@ export class VorbisFloor0 implements IVorbisFloor { residue[i] *= q; - while (barkMap[++i] == k) residue[i] *= q; + while (barkMap[++i] === k) { + residue[i] *= q; + } } } else { residue.fill(0, 0, n); @@ -766,9 +777,13 @@ export class VorbisFloor1 implements IVorbisFloor { for (let j = 2; j < i; j++) { const temp = this._xList[j]; if (temp < this._xList[i]) { - if (temp > this._xList[this._lNeigh[i]]) this._lNeigh[i] = j; + if (temp > this._xList[this._lNeigh[i]]) { + this._lNeigh[i] = j; + } } else { - if (temp < this._xList[this._hNeigh[i]]) this._hNeigh[i] = j; + if (temp < this._xList[this._hNeigh[i]]) { + this._hNeigh[i] = j; + } } } } @@ -776,7 +791,7 @@ export class VorbisFloor1 implements IVorbisFloor { // precalc the sort table for (let i = 0; i < this._sortIdx.length - 1; i++) { for (let j = i + 1; j < this._sortIdx.length; j++) { - if (this._xList[i] == this._xList[j]) { + if (this._xList[i] === this._xList[j]) { throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Floor1 Data'); } @@ -791,7 +806,7 @@ export class VorbisFloor1 implements IVorbisFloor { } public unpack(packet: IntBitReader, blockSize: number, channel: number): IVorbisFloorData { - var data = new VorbisFloor1Data(); + const data = new VorbisFloor1Data(); // hoist ReadPosts to here since that's all we're doing... if (packet.readBit()) { @@ -807,7 +822,7 @@ export class VorbisFloor1 implements IVorbisFloor { let cval = 0; if (cbits > 0) { cval = this._classMasterbooks[clsNum].decodeScalar(packet); - if (cval == -1) { + if (cval === -1) { // we read a bad value... bail postCount = 0; break; @@ -818,7 +833,7 @@ export class VorbisFloor1 implements IVorbisFloor { cval = cval >> cbits; if (book != null) { data.posts[postCount] = book.decodeScalar(packet); - if (data.posts[postCount] == -1) { + if (data.posts[postCount] === -1) { // we read a bad value... bail postCount = 0; i = this._partitionClass.length; @@ -847,18 +862,20 @@ export class VorbisFloor1 implements IVorbisFloor { let ly = data.posts[0] * this._multiplier; for (let i = 1; i < data.postCount; i++) { - var idx = this._sortIdx[i]; + const idx = this._sortIdx[i]; if (stepFlags[idx]) { - var hx = this._xList[idx]; - var hy = data.posts[idx] * this._multiplier; + const hx = this._xList[idx]; + const hy = data.posts[idx] * this._multiplier; if (lx < n) { this.renderLineMulti(lx, ly, Math.min(hx, n), hy, residue); } lx = hx; ly = hy; } - if (lx >= n) break; + if (lx >= n) { + break; + } } if (lx < n) { @@ -900,7 +917,7 @@ export class VorbisFloor1 implements IVorbisFloor { } else { room = lowroom * 2; } - if (val != 0) { + if (val !== 0) { stepFlags[lowOfs] = true; stepFlags[highOfs] = true; stepFlags[i] = true; @@ -912,7 +929,7 @@ export class VorbisFloor1 implements IVorbisFloor { finalY[i] = predicted - val + highroom - 1; } } else { - if (val % 2 == 1) { + if (val % 2 === 1) { // odd finalY[i] = predicted - (val + 1) / 2; } else { @@ -934,16 +951,15 @@ export class VorbisFloor1 implements IVorbisFloor { } private renderPoint(x0: number, y0: number, x1: number, y1: number, X: number) { - var dy = y1 - y0; - var adx = x1 - x0; - var ady = Math.abs(dy); - var err = ady * (X - x0); - var off = (err / adx) | 0; + const dy = y1 - y0; + const adx = x1 - x0; + const ady = Math.abs(dy); + const err = ady * (X - x0); + const off = (err / adx) | 0; if (dy < 0) { return y0 - off; - } else { - return y0 + off; } + return y0 + off; } private renderLineMulti(x0: number, y0: number, x1: number, y1: number, v: Float32Array) { @@ -972,70 +988,39 @@ export class VorbisFloor1 implements IVorbisFloor { // prettier-ignore private static readonly inverse_dB_table = new Float32Array([ - 1.0649863e-07, 1.1341951e-07, 1.2079015e-07, 1.2863978e-07, - 1.3699951e-07, 1.4590251e-07, 1.5538408e-07, 1.6548181e-07, - 1.7623575e-07, 1.8768855e-07, 1.9988561e-07, 2.1287530e-07, - 2.2670913e-07, 2.4144197e-07, 2.5713223e-07, 2.7384213e-07, - 2.9163793e-07, 3.1059021e-07, 3.3077411e-07, 3.5226968e-07, - 3.7516214e-07, 3.9954229e-07, 4.2550680e-07, 4.5315863e-07, - 4.8260743e-07, 5.1396998e-07, 5.4737065e-07, 5.8294187e-07, - 6.2082472e-07, 6.6116941e-07, 7.0413592e-07, 7.4989464e-07, - 7.9862701e-07, 8.5052630e-07, 9.0579828e-07, 9.6466216e-07, - 1.0273513e-06, 1.0941144e-06, 1.1652161e-06, 1.2409384e-06, - 1.3215816e-06, 1.4074654e-06, 1.4989305e-06, 1.5963394e-06, - 1.7000785e-06, 1.8105592e-06, 1.9282195e-06, 2.0535261e-06, - 2.1869758e-06, 2.3290978e-06, 2.4804557e-06, 2.6416497e-06, - 2.8133190e-06, 2.9961443e-06, 3.1908506e-06, 3.3982101e-06, - 3.6190449e-06, 3.8542308e-06, 4.1047004e-06, 4.3714470e-06, - 4.6555282e-06, 4.9580707e-06, 5.2802740e-06, 5.6234160e-06, - 5.9888572e-06, 6.3780469e-06, 6.7925283e-06, 7.2339451e-06, - 7.7040476e-06, 8.2047000e-06, 8.7378876e-06, 9.3057248e-06, - 9.9104632e-06, 1.0554501e-05, 1.1240392e-05, 1.1970856e-05, - 1.2748789e-05, 1.3577278e-05, 1.4459606e-05, 1.5399272e-05, - 1.6400004e-05, 1.7465768e-05, 1.8600792e-05, 1.9809576e-05, - 2.1096914e-05, 2.2467911e-05, 2.3928002e-05, 2.5482978e-05, - 2.7139006e-05, 2.8902651e-05, 3.0780908e-05, 3.2781225e-05, - 3.4911534e-05, 3.7180282e-05, 3.9596466e-05, 4.2169667e-05, - 4.4910090e-05, 4.7828601e-05, 5.0936773e-05, 5.4246931e-05, - 5.7772202e-05, 6.1526565e-05, 6.5524908e-05, 6.9783085e-05, - 7.4317983e-05, 7.9147585e-05, 8.4291040e-05, 8.9768747e-05, - 9.5602426e-05, 0.00010181521, 0.00010843174, 0.00011547824, - 0.00012298267, 0.00013097477, 0.00013948625, 0.00014855085, - 0.00015820453, 0.00016848555, 0.00017943469, 0.00019109536, - 0.00020351382, 0.00021673929, 0.00023082423, 0.00024582449, - 0.00026179955, 0.00027881276, 0.00029693158, 0.00031622787, - 0.00033677814, 0.00035866388, 0.00038197188, 0.00040679456, - 0.00043323036, 0.00046138411, 0.00049136745, 0.00052329927, - 0.00055730621, 0.00059352311, 0.00063209358, 0.00067317058, - 0.00071691700, 0.00076350630, 0.00081312324, 0.00086596457, - 0.00092223983, 0.00098217216, 0.0010459992, 0.0011139742, - 0.0011863665, 0.0012634633, 0.0013455702, 0.0014330129, - 0.0015261382, 0.0016253153, 0.0017309374, 0.0018434235, - 0.0019632195, 0.0020908006, 0.0022266726, 0.0023713743, - 0.0025254795, 0.0026895994, 0.0028643847, 0.0030505286, - 0.0032487691, 0.0034598925, 0.0036847358, 0.0039241906, - 0.0041792066, 0.0044507950, 0.0047400328, 0.0050480668, - 0.0053761186, 0.0057254891, 0.0060975636, 0.0064938176, - 0.0069158225, 0.0073652516, 0.0078438871, 0.0083536271, - 0.0088964928, 0.009474637, 0.010090352, 0.010746080, - 0.011444421, 0.012188144, 0.012980198, 0.013823725, - 0.014722068, 0.015678791, 0.016697687, 0.017782797, - 0.018938423, 0.020169149, 0.021479854, 0.022875735, - 0.024362330, 0.025945531, 0.027631618, 0.029427276, - 0.031339626, 0.033376252, 0.035545228, 0.037855157, - 0.040315199, 0.042935108, 0.045725273, 0.048696758, - 0.051861348, 0.055231591, 0.058820850, 0.062643361, - 0.066714279, 0.071049749, 0.075666962, 0.080584227, - 0.085821044, 0.091398179, 0.097337747, 0.10366330, - 0.11039993, 0.11757434, 0.12521498, 0.13335215, - 0.14201813, 0.15124727, 0.16107617, 0.17154380, - 0.18269168, 0.19456402, 0.20720788, 0.22067342, - 0.23501402, 0.25028656, 0.26655159, 0.28387361, - 0.30232132, 0.32196786, 0.34289114, 0.36517414, - 0.38890521, 0.41417847, 0.44109412, 0.46975890, - 0.50028648, 0.53279791, 0.56742212, 0.60429640, - 0.64356699, 0.68538959, 0.72993007, 0.77736504, - 0.82788260, 0.88168307, 0.9389798, 1.0 + 1.0649863e-7, 1.1341951e-7, 1.2079015e-7, 1.2863978e-7, 1.3699951e-7, 1.4590251e-7, 1.5538408e-7, 1.6548181e-7, + 1.7623575e-7, 1.8768855e-7, 1.9988561e-7, 2.128753e-7, 2.2670913e-7, 2.4144197e-7, 2.5713223e-7, 2.7384213e-7, + 2.9163793e-7, 3.1059021e-7, 3.3077411e-7, 3.5226968e-7, 3.7516214e-7, 3.9954229e-7, 4.255068e-7, 4.5315863e-7, + 4.8260743e-7, 5.1396998e-7, 5.4737065e-7, 5.8294187e-7, 6.2082472e-7, 6.6116941e-7, 7.0413592e-7, 7.4989464e-7, + 7.9862701e-7, 8.505263e-7, 9.0579828e-7, 9.6466216e-7, 1.0273513e-6, 1.0941144e-6, 1.1652161e-6, 1.2409384e-6, + 1.3215816e-6, 1.4074654e-6, 1.4989305e-6, 1.5963394e-6, 1.7000785e-6, 1.8105592e-6, 1.9282195e-6, 2.0535261e-6, + 2.1869758e-6, 2.3290978e-6, 2.4804557e-6, 2.6416497e-6, 2.813319e-6, 2.9961443e-6, 3.1908506e-6, 3.3982101e-6, + 3.6190449e-6, 3.8542308e-6, 4.1047004e-6, 4.371447e-6, 4.6555282e-6, 4.9580707e-6, 5.280274e-6, 5.623416e-6, + 5.9888572e-6, 6.3780469e-6, 6.7925283e-6, 7.2339451e-6, 7.7040476e-6, 8.2047e-6, 8.7378876e-6, 9.3057248e-6, + 9.9104632e-6, 1.0554501e-5, 1.1240392e-5, 1.1970856e-5, 1.2748789e-5, 1.3577278e-5, 1.4459606e-5, 1.5399272e-5, + 1.6400004e-5, 1.7465768e-5, 1.8600792e-5, 1.9809576e-5, 2.1096914e-5, 2.2467911e-5, 2.3928002e-5, 2.5482978e-5, + 2.7139006e-5, 2.8902651e-5, 3.0780908e-5, 3.2781225e-5, 3.4911534e-5, 3.7180282e-5, 3.9596466e-5, 4.2169667e-5, + 4.491009e-5, 4.7828601e-5, 5.0936773e-5, 5.4246931e-5, 5.7772202e-5, 6.1526565e-5, 6.5524908e-5, 6.9783085e-5, + 7.4317983e-5, 7.9147585e-5, 8.429104e-5, 8.9768747e-5, 9.5602426e-5, 0.00010181521, 0.00010843174, + 0.00011547824, 0.00012298267, 0.00013097477, 0.00013948625, 0.00014855085, 0.00015820453, 0.00016848555, + 0.00017943469, 0.00019109536, 0.00020351382, 0.00021673929, 0.00023082423, 0.00024582449, 0.00026179955, + 0.00027881276, 0.00029693158, 0.00031622787, 0.00033677814, 0.00035866388, 0.00038197188, 0.00040679456, + 0.00043323036, 0.00046138411, 0.00049136745, 0.00052329927, 0.00055730621, 0.00059352311, 0.00063209358, + 0.00067317058, 0.000716917, 0.0007635063, 0.00081312324, 0.00086596457, 0.00092223983, 0.00098217216, + 0.0010459992, 0.0011139742, 0.0011863665, 0.0012634633, 0.0013455702, 0.0014330129, 0.0015261382, 0.0016253153, + 0.0017309374, 0.0018434235, 0.0019632195, 0.0020908006, 0.0022266726, 0.0023713743, 0.0025254795, 0.0026895994, + 0.0028643847, 0.0030505286, 0.0032487691, 0.0034598925, 0.0036847358, 0.0039241906, 0.0041792066, 0.004450795, + 0.0047400328, 0.0050480668, 0.0053761186, 0.0057254891, 0.0060975636, 0.0064938176, 0.0069158225, 0.0073652516, + 0.0078438871, 0.0083536271, 0.0088964928, 0.009474637, 0.010090352, 0.01074608, 0.011444421, 0.012188144, + 0.012980198, 0.013823725, 0.014722068, 0.015678791, 0.016697687, 0.017782797, 0.018938423, 0.020169149, + 0.021479854, 0.022875735, 0.02436233, 0.025945531, 0.027631618, 0.029427276, 0.031339626, 0.033376252, + 0.035545228, 0.037855157, 0.040315199, 0.042935108, 0.045725273, 0.048696758, 0.051861348, 0.055231591, + 0.05882085, 0.062643361, 0.066714279, 0.071049749, 0.075666962, 0.080584227, 0.085821044, 0.091398179, + 0.097337747, 0.1036633, 0.11039993, 0.11757434, 0.12521498, 0.13335215, 0.14201813, 0.15124727, 0.16107617, + 0.1715438, 0.18269168, 0.19456402, 0.20720788, 0.22067342, 0.23501402, 0.25028656, 0.26655159, 0.28387361, + 0.30232132, 0.32196786, 0.34289114, 0.36517414, 0.38890521, 0.41417847, 0.44109412, 0.4697589, 0.50028648, + 0.53279791, 0.56742212, 0.6042964, 0.64356699, 0.68538959, 0.72993007, 0.77736504, 0.8278826, 0.88168307, + 0.9389798, 1.0 ]); } @@ -1053,8 +1038,7 @@ export class VorbisFloor implements IVorbisFloor { break; default: - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Floor type: ' + type); - break; + throw new AlphaTabError(AlphaTabErrorType.Format, `Vorbis: Invalid Floor type: ${type}`); } } public apply(floorData: IVorbisFloorData, blockSize: number, residue: Float32Array): void { @@ -1106,7 +1090,7 @@ export class VorbisResidue0 implements IVorbisResidue { const bookNums = new Int32Array(acc); for (let i = 0; i < acc; i++) { bookNums[i] = packet.readBits(8); - if (codebooks[bookNums[i]].mapType == 0) { + if (codebooks[bookNums[i]].mapType === 0) { throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Residue 0'); } } @@ -1160,7 +1144,7 @@ export class VorbisResidue0 implements IVorbisResidue { private static icount(v: number): number { let ret = 0; - while (v != 0) { + while (v !== 0) { ret += v & 1; v >>= 1; } @@ -1187,7 +1171,7 @@ export class VorbisResidue0 implements IVorbisResidue { for (let stage = 0; stage < this._maxStages; stage++) { for (let partitionIdx = 0, entryIdx = 0; partitionIdx < partitionCount; entryIdx++) { - if (stage == 0) { + if (stage === 0) { for (let ch = 0; ch < this._channels; ch++) { const idx = this._classBook.decodeScalar(packet); if (idx >= 0 && idx < this._decodeMap.length) { @@ -1207,7 +1191,7 @@ export class VorbisResidue0 implements IVorbisResidue { const offset = this._begin + partitionIdx * this._partitionSize; for (let ch = 0; ch < this._channels; ch++) { const idx = partWordCache[ch][entryIdx][dimensionIdx]; - if ((this._cascade[idx] & (1 << stage)) != 0) { + if ((this._cascade[idx] & (1 << stage)) !== 0) { const book = this._books[idx][stage]; if (book) { if (this.writeVectors(book, packet, buffer, ch, offset, this._partitionSize)) { @@ -1239,7 +1223,7 @@ export class VorbisResidue0 implements IVorbisResidue { for (let i = 0; i < steps; i++) { entryCache[i] = codebook.decodeScalar(packet); - if (entryCache[i] == -1) { + if (entryCache[i] === -1) { return true; } } @@ -1253,10 +1237,6 @@ export class VorbisResidue0 implements IVorbisResidue { } export class VorbisResidue1 extends VorbisResidue0 { - public constructor(packet: IntBitReader, channels: number, codebooks: VorbisCodebook[]) { - super(packet, channels, codebooks); - } - protected override writeVectors( codebook: VorbisCodebook, packet: IntBitReader, @@ -1269,7 +1249,7 @@ export class VorbisResidue1 extends VorbisResidue0 { for (let i = 0; i < partitionSize; ) { const entry = codebook.decodeScalar(packet); - if (entry == -1) { + if (entry === -1) { return true; } for (let j = 0; j < codebook.dimensions; i++, j++) { @@ -1312,12 +1292,12 @@ export class VorbisResidue2 extends VorbisResidue0 { offset /= this._realChannels; for (let c = 0; c < partitionSize; ) { const entry = codebook.decodeScalar(packet); - if (entry == -1) { + if (entry === -1) { return true; } for (let d = 0; d < codebook.dimensions; d++, c++) { residue[chPtr][offset] += codebook.get(entry, d); - if (++chPtr == this._realChannels) { + if (++chPtr === this._realChannels) { chPtr = 0; offset++; } @@ -1345,7 +1325,7 @@ export class VorbisResidue implements IVorbisResidue { break; default: - throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Invalid Residue type: ' + type); + throw new AlphaTabError(AlphaTabErrorType.Format, `Vorbis: Invalid Residue type: ${type}`); } } @@ -1397,7 +1377,7 @@ export class VorbisMapping { for (let j = 0; j < couplingSteps; j++) { const magnitude = packet.readBits(couplingBits); const angle = packet.readBits(couplingBits); - if (magnitude == angle || magnitude > channels - 1 || angle > channels - 1) { + if (magnitude === angle || magnitude > channels - 1 || angle > channels - 1) { throw new AlphaTabError( AlphaTabErrorType.Format, 'Vorbis: Invalid magnitude or angle in mapping header!' @@ -1407,7 +1387,7 @@ export class VorbisMapping { this._couplingMangitude[j] = magnitude; } - if (packet.readBits(2) != 0) { + if (packet.readBits(2) !== 0) { throw new AlphaTabError(AlphaTabErrorType.Format, 'Vorbis: Reserved bits not 0 in mapping header.'); } @@ -1481,8 +1461,8 @@ export class VorbisMapping { for (let i = 0; i < this._submapFloor.length; i++) { for (let j = 0; j < this._channelFloor.length; j++) { if ( - this._submapFloor[i] != this._channelFloor[j] || - this._submapResidue[i] != this._channelResidue[j] + this._submapFloor[i] !== this._channelFloor[j] || + this._submapResidue[i] !== this._channelResidue[j] ) { // the submap doesn't match, so this floor doesn't contribute floorData[j].forceNoEnergy = true; @@ -1502,7 +1482,7 @@ export class VorbisMapping { const angle = buffer[this._couplingAngle[i]]; // we only have to do the first half; MDCT ignores the last half - for (var j = 0; j < halfBlockSize; j++) { + for (let j = 0; j < halfBlockSize; j++) { let newM: number; let newA: number; @@ -1583,7 +1563,7 @@ export class VorbisMode { this._channels = channels; this._blockFlag = packet.readBit(); - if (0 != packet.readBits(32)) { + if (0 !== packet.readBits(32)) { throw new AlphaTabError( AlphaTabErrorType.Format, 'Vorbis: Mode header had invalid window or transform type!' @@ -1699,6 +1679,8 @@ export class VorbisMode { } class MdctImpl { + // biome-ignore lint/correctness/noPrecisionLoss: High precision PI + // biome-ignore lint/suspicious/noApproximativeNumericConstant: High precision PI private static readonly M_PI = 3.14159265358979323846264; private readonly _n: number; @@ -1762,8 +1744,8 @@ class MdctImpl { let d = this._n2 - 2; // buf2 let AA = 0; // A let e = 0; // buffer - let e_stop = this._n2; // buffer - while (e != e_stop) { + const e_stop = this._n2; // buffer + while (e !== e_stop) { buf2[d + 1] = buffer[e] * this._a[AA] - buffer[e + 2] * this._a[AA + 1]; buf2[d] = buffer[e] * this._a[AA + 1] + buffer[e + 2] * this._a[AA]; d -= 2; @@ -1896,9 +1878,9 @@ class MdctImpl { // step 7 { - var c = 0; // C - var d = 0; // v - var e = this._n2 - 4; // v + let c = 0; // C + let d = 0; // v + let e = this._n2 - 4; // v while (d < e) { let a02: number; @@ -2049,9 +2031,9 @@ class MdctImpl { } private step3_iter0_loop(n: number, e: Float32Array, i_off: number, k_off: number) { - var ee0 = i_off; // e - var ee2 = ee0 + k_off; // e - var a = 0; + let ee0 = i_off; // e + let ee2 = ee0 + k_off; // e + let a = 0; for (let i = n >> 2; i > 0; --i) { let k00_20: number; let k01_21: number; @@ -2102,14 +2084,14 @@ class MdctImpl { a_off: number, k0: number ) { - var A0 = this._a[a]; - var A1 = this._a[a + 1]; - var A2 = this._a[a + a_off]; - var A3 = this._a[a + a_off + 1]; - var A4 = this._a[a + a_off * 2]; - var A5 = this._a[a + a_off * 2 + 1]; - var A6 = this._a[a + a_off * 3]; - var A7 = this._a[a + a_off * 3 + 1]; + const A0 = this._a[a]; + const A1 = this._a[a + 1]; + const A2 = this._a[a + a_off]; + const A3 = this._a[a + a_off + 1]; + const A4 = this._a[a + a_off * 2]; + const A5 = this._a[a + a_off * 2 + 1]; + const A6 = this._a[a + a_off * 3]; + const A7 = this._a[a + a_off * 3 + 1]; let k00: number; let k11: number; @@ -2197,32 +2179,22 @@ class MdctImpl { } private iter_54(e: Float32Array, z: number) { - let k00: number; - let k11: number; - let k22: number; - let k33: number; - - let y0: number; - let y1: number; - let y2: number; - let y3: number; - - k00 = e[z] - e[z - 4]; - y0 = e[z] + e[z - 4]; - y2 = e[z - 2] + e[z - 6]; - k22 = e[z - 2] - e[z - 6]; + const k00 = e[z] - e[z - 4]; + const y0 = e[z] + e[z - 4]; + const y2 = e[z - 2] + e[z - 6]; + const k22 = e[z - 2] - e[z - 6]; e[z] = y0 + y2; e[z - 2] = y0 - y2; - k33 = e[z - 3] - e[z - 7]; + const k33 = e[z - 3] - e[z - 7]; e[z - 4] = k00 + k33; e[z - 6] = k00 - k33; - k11 = e[z - 1] - e[z - 5]; - y1 = e[z - 1] + e[z - 5]; - y3 = e[z - 3] + e[z - 7]; + const k11 = e[z - 1] - e[z - 5]; + const y1 = e[z - 1] + e[z - 5]; + const y3 = e[z - 3] + e[z - 7]; e[z - 1] = y1 + y3; e[z - 3] = y1 - y3; @@ -2315,7 +2287,7 @@ export class VorbisStreamDecoder { public read(buffer: Float32Array, offset: number, count: number): number { // if the caller didn't ask for any data, bail early - if (count == 0) { + if (count === 0) { return 0; } @@ -2326,7 +2298,7 @@ export class VorbisStreamDecoder { // try to fill the buffer; drain the last buffer if EOS, resync, bad packet, or parameter change while (idx < tgt) { // if we don't have any more valid data in the current packet, read in the next packet - if (this._prevPacketStart == this._prevPacketEnd) { + if (this._prevPacketStart === this._prevPacketEnd) { if (this._eosFound) { this._nextPacketBuf = null; this._prevPacketBuf = null; diff --git a/src/synth/vorbis/VorbisStreamReader.ts b/src/synth/vorbis/VorbisStreamReader.ts index dee60e83f..2e8678f07 100644 --- a/src/synth/vorbis/VorbisStreamReader.ts +++ b/src/synth/vorbis/VorbisStreamReader.ts @@ -3,8 +3,8 @@ */ import { ByteBuffer } from '@src/io/ByteBuffer'; -import { OggPacket } from './OggReader'; -import { VorbisStream } from './VorbisStream'; +import type { OggPacket } from '@src/synth/vorbis/OggReader'; +import { VorbisStream } from '@src/synth/vorbis/VorbisStream'; import { IOHelper } from '@src/io/IOHelper'; import { Huffman, @@ -17,8 +17,8 @@ import { VorbisSetupHeader, VorbisStreamDecoder, VorbisTimeDomainTransform -} from './VorbisStreamDecoder'; -import { IntBitReader } from './IntBitReader'; +} from '@src/synth/vorbis/VorbisStreamDecoder'; +import { IntBitReader } from '@src/synth/vorbis/IntBitReader'; enum VorbisPacketTypes { IdentificationHeader = 1, @@ -53,7 +53,7 @@ export class VorbisStreamReader { } if (packet.isBeginningOfStream) { - var stream = this.readStream(packet); + const stream = this.readStream(packet); if (stream != null) { return stream; } @@ -164,7 +164,7 @@ export class VorbisStreamReader { const version = IOHelper.readUInt32LE(reader); // [vorbis_version] is to read ’0’ in order to be compatible with this document. - if (version != 0) { + if (version !== 0) { return false; } @@ -180,7 +180,7 @@ export class VorbisStreamReader { stream.bitrateNominal = IOHelper.readInt32LE(reader); stream.bitrateMinimum = IOHelper.readInt32LE(reader); - var blockSize = reader.readByte(); + const blockSize = reader.readByte(); stream.blocksize0 = 1 << (blockSize & 0x0f); stream.blocksize1 = 1 << (blockSize >> 4); if ( @@ -229,7 +229,7 @@ export class VorbisStreamReader { return false; } - var reader = ByteBuffer.fromBuffer(packet.packetData); + const reader = ByteBuffer.fromBuffer(packet.packetData); if (!this.comonHeaderDecode(VorbisPacketTypes.Comment, reader)) { return false; } @@ -237,14 +237,14 @@ export class VorbisStreamReader { const vendorLength = IOHelper.readUInt32LE(reader); reader.skip(vendorLength); // vendor (unused) - var userCommentListLength = IOHelper.readUInt32LE(reader); + const userCommentListLength = IOHelper.readUInt32LE(reader); for (let index = 0; index < userCommentListLength; index++) { const length = IOHelper.readUInt32LE(reader); reader.skip(length); // comment (unused) } - var framing = reader.readByte(); - if (framing == 0) { + const framing = reader.readByte(); + if (framing === 0) { return false; } diff --git a/src/util/FontLoadingChecker.ts b/src/util/FontLoadingChecker.ts index b3ab85e75..403ff49ae 100644 --- a/src/util/FontLoadingChecker.ts +++ b/src/util/FontLoadingChecker.ts @@ -1,4 +1,4 @@ -import { IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; +import { type IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; import { Logger } from '@src/Logger'; import { Environment } from '@src/Environment'; @@ -33,7 +33,7 @@ export class FontLoadingChecker { this._isStarted = true; let failCounter: number = 0; - let failCounterId: number = window.setInterval(() => { + const failCounterId: number = window.setInterval(() => { Logger.warning( 'Rendering', `Could not load font '${this._families[0]}' within ${(failCounter + 1) * 5} seconds`, @@ -51,7 +51,7 @@ export class FontLoadingChecker { Logger.debug('Font', `Start checking for font availablility: ${this._families.join(', ')}`); - let errorHandler = (e: unknown) => { + const errorHandler = (e: unknown) => { if (this._families.length > 1) { Logger.debug('Font', `[${this._families[0]}] Loading Failed, switching to ${this._families[1]}`, e); this._families.shift(); @@ -65,14 +65,14 @@ export class FontLoadingChecker { } }; - let successHandler = (font: string) => { + const successHandler = (font: string) => { Logger.debug('Font', `[${font}] Font API signaled available`); this.isFontLoaded = true; window.clearInterval(failCounterId); (this.fontLoaded as EventEmitterOfT).trigger(this._families[0]); }; - let checkFont = async () => { + const checkFont = async () => { // Fast Path: check if one of the specified fonts is already available. for (const font of this._families) { if (await this.isFontAvailable(font, false)) { @@ -107,7 +107,7 @@ export class FontLoadingChecker { return new Promise(resolve => { // In some very rare occasions Chrome reports false for the font. // in this case we try to force some refresh and reload by creating an element with this font. - const fontString = '1em ' + family; + const fontString = `1em ${family}`; if ((document as any).fonts.check(fontString)) { resolve(true); } else if (advancedCheck) { diff --git a/src/vite/AlphaTabVitePluginOptions.ts b/src/vite/AlphaTabVitePluginOptions.ts index 740e532ba..8859dc979 100644 --- a/src/vite/AlphaTabVitePluginOptions.ts +++ b/src/vite/AlphaTabVitePluginOptions.ts @@ -7,7 +7,7 @@ export interface AlphaTabVitePluginOptions { alphaTabSourceDir?: string; /** - * The location where assets of alphaTab should be placed. + * The location where assets of alphaTab should be placed. * Set it to false to disable the copying of assets like fonts. * (default: compiler.options.output.path) */ @@ -16,7 +16,7 @@ export interface AlphaTabVitePluginOptions { /** * Whether alphaTab should configure the audio worklet support in Vite. * This might break support for audio playback unless audio worklet support is added - * through other means to Vite. + * through other means to Vite. * (default: true) */ audioWorklets?: boolean; @@ -24,8 +24,8 @@ export interface AlphaTabVitePluginOptions { /** * Whether alphaTab should configure the web worklet support in Vite. * This might break support for audio playback and background unless audio worklet support is added - * through other means to Vite. + * through other means to Vite. * (default: true) */ webWorkers?: boolean; -} \ No newline at end of file +} diff --git a/src/vite/alphaTabVitePlugin.ts b/src/vite/alphaTabVitePlugin.ts index 25581a869..0d3f70297 100644 --- a/src/vite/alphaTabVitePlugin.ts +++ b/src/vite/alphaTabVitePlugin.ts @@ -1,10 +1,10 @@ /**@target web */ -import type { Plugin } from './bridge'; -import type { AlphaTabVitePluginOptions } from './AlphaTabVitePluginOptions'; -import { importMetaUrlPlugin } from './importMetaPlugin'; -import { copyAssetsPlugin } from './copyAssetsPlugin'; -import { workerPlugin } from './workerPlugin'; +import type { Plugin } from '@src/vite/bridge'; +import type { AlphaTabVitePluginOptions } from '@src/vite/AlphaTabVitePluginOptions'; +import { importMetaUrlPlugin } from '@src/vite/importMetaPlugin'; +import { copyAssetsPlugin } from '@src/vite/copyAssetsPlugin'; +import { workerPlugin } from '@src/vite/workerPlugin'; export function alphaTab(options?: AlphaTabVitePluginOptions) { const plugins: Plugin[] = []; diff --git a/src/vite/bridge/asset.ts b/src/vite/bridge/asset.ts index 595999889..4b16dd54d 100644 --- a/src/vite/bridge/asset.ts +++ b/src/vite/bridge/asset.ts @@ -2,17 +2,17 @@ // index.ts for more details on contents and license of this file -import { type ResolvedConfig } from './config'; -import * as path from 'path'; -import { joinUrlSegments, removeLeadingSlash, withTrailingSlash } from './utils'; +import type { ResolvedConfig } from '@src/vite/bridge/config'; +import * as path from 'node:path'; +import { joinUrlSegments, removeLeadingSlash, withTrailingSlash } from '@src/vite/bridge/utils'; -export const FS_PREFIX = `/@fs/`; +export const FS_PREFIX = '/@fs/'; export async function fileToUrl(id: string, config: ResolvedConfig): Promise { let rtn: string; if (id.startsWith(withTrailingSlash(config.root))) { // in project root, infer short public path - rtn = '/' + path.posix.relative(config.root, id); + rtn = `/${path.posix.relative(config.root, id)}`; } else { // outside of project root, use absolute fs path // (this is special handled by the serve static middleware diff --git a/src/vite/bridge/build.ts b/src/vite/bridge/build.ts index 67a45a7fe..15949c438 100644 --- a/src/vite/bridge/build.ts +++ b/src/vite/bridge/build.ts @@ -2,21 +2,23 @@ // index.ts for more details on contents and license of this file -import { MinimalPluginContext, PluginContext, type InternalModuleFormat } from 'rollup'; -import { joinUrlSegments, partialEncodeURIPath } from './utils'; -import * as path from 'path'; -import { ResolvedConfig } from './config'; -import { BuildEnvironment, Plugin } from 'vite'; -import { RollupPluginHooks } from './typeUtils'; -import { ROLLUP_HOOKS } from './constants'; -import { getHookHandler } from './plugins'; +import type { MinimalPluginContext, PluginContext, InternalModuleFormat } from 'rollup'; +import { joinUrlSegments, partialEncodeURIPath } from '@src/vite/bridge/utils'; +import * as path from 'node:path'; +import type { ResolvedConfig } from '@src/vite/bridge/config'; +import type { BuildEnvironment, Plugin } from 'vite'; +import type { RollupPluginHooks } from '@src/vite/bridge/typeUtils'; +import { ROLLUP_HOOKS } from '@src/vite/bridge/constants'; +import { getHookHandler } from '@src/vite/bridge/plugins'; const needsEscapeRegEx = /[\n\r'\\\u2028\u2029]/; const quoteNewlineRegEx = /([\n\r'\u2028\u2029])/g; const backSlashRegEx = /\\/g; function escapeId(id: string): string { - if (!needsEscapeRegEx.test(id)) return id; + if (!needsEscapeRegEx.test(id)) { + return id; + } return id.replace(backSlashRegEx, '\\\\').replace(quoteNewlineRegEx, '\\$1'); } @@ -35,7 +37,9 @@ const getFileUrlFromRelativePath = (path: string) => getFileUrlFromFullPath(`__d const relativeUrlMechanisms: Record string> = { amd: relativePath => { - if (relativePath[0] !== '.') relativePath = './' + relativePath; + if (relativePath[0] !== '.') { + relativePath = `./${relativePath}`; + } return getResolveUrl(`require.toUrl('${escapeId(relativePath)}'), document.baseURI`); }, cjs: relativePath => @@ -78,7 +82,7 @@ export function toOutputFilePathInJS( toRelative: (filename: string, hostType: string) => string | { runtime: string } ): string | { runtime: string } { const { renderBuiltUrl } = config.experimental; - let relative = config.base === '' || config.base === './'; + let relative = config.base === '' || config.base === '@src/vite/bridge/'; if (renderBuiltUrl) { const result = renderBuiltUrl(filename, { hostId, @@ -133,7 +137,9 @@ export function injectEnvironmentToHooks(environment: BuildEnvironment, plugin: } function wrapEnvironmentResolveId(environment: BuildEnvironment, hook?: Plugin['resolveId']): Plugin['resolveId'] { - if (!hook) return; + if (!hook) { + return; + } const fn = getHookHandler(hook); const handler: Plugin['resolveId'] = function (id, importer, options) { @@ -150,13 +156,14 @@ function wrapEnvironmentResolveId(environment: BuildEnvironment, hook?: Plugin[' ...hook, handler } as Plugin['resolveId']; - } else { - return handler; } + return handler; } function wrapEnvironmentLoad(environment: BuildEnvironment, hook?: Plugin['load']): Plugin['load'] { - if (!hook) return; + if (!hook) { + return; + } const fn = getHookHandler(hook); const handler: Plugin['load'] = function (id, ...args) { @@ -168,13 +175,14 @@ function wrapEnvironmentLoad(environment: BuildEnvironment, hook?: Plugin['load' ...hook, handler } as Plugin['load']; - } else { - return handler; } + return handler; } function wrapEnvironmentTransform(environment: BuildEnvironment, hook?: Plugin['transform']): Plugin['transform'] { - if (!hook) return; + if (!hook) { + return; + } const fn = getHookHandler(hook); const handler: Plugin['transform'] = function (code, importer, ...args) { @@ -191,19 +199,22 @@ function wrapEnvironmentTransform(environment: BuildEnvironment, hook?: Plugin[' ...hook, handler } as Plugin['transform']; - } else { - return handler; } + return handler; } function wrapEnvironmentHook( environment: BuildEnvironment, hook?: Plugin[HookName] ): Plugin[HookName] { - if (!hook) return; + if (!hook) { + return; + } const fn = getHookHandler(hook); - if (typeof fn !== 'function') return hook; + if (typeof fn !== 'function') { + return hook; + } const handler: Plugin[HookName] = function (this: PluginContext, ...args: any[]) { return fn.call(injectEnvironmentInContext(this, environment), ...args); @@ -214,9 +225,8 @@ function wrapEnvironmentHook( ...hook, handler } as Plugin[HookName]; - } else { - return handler; } + return handler; } function injectEnvironmentInContext( diff --git a/src/vite/bridge/config.ts b/src/vite/bridge/config.ts index 3dfb1ce60..4937b5392 100644 --- a/src/vite/bridge/config.ts +++ b/src/vite/bridge/config.ts @@ -2,7 +2,7 @@ // index.ts for more details on contents and license of this file -import { type ResolvedConfig as ResolvedConfigWithoutInternal } from 'vite'; +import type { ResolvedConfig as ResolvedConfigWithoutInternal } from 'vite'; export type ResolvedConfig = ResolvedConfigWithoutInternal & { // https://github.com/vitejs/vite/blob/b7ddfae5f852c2948fab03e94751ce56f5f31ce0/packages/vite/src/node/config.ts#L376-L378 diff --git a/src/vite/bridge/constants.ts b/src/vite/bridge/constants.ts index ea8ed7d1c..5c87d0e84 100644 --- a/src/vite/bridge/constants.ts +++ b/src/vite/bridge/constants.ts @@ -1,6 +1,6 @@ /**@target web */ -import { RollupPluginHooks } from "./typeUtils"; +import type { RollupPluginHooks } from '@src/vite/bridge/typeUtils'; // index.ts for more details on contents and license of this file @@ -8,7 +8,7 @@ import { RollupPluginHooks } from "./typeUtils"; export const METADATA_FILENAME = '_metadata.json'; // https://github.com/vitejs/vite/blob/b7ddfae5f852c2948fab03e94751ce56f5f31ce0/packages/vite/src/node/constants.ts#L63 -export const ENV_PUBLIC_PATH = `/@vite/env`; +export const ENV_PUBLIC_PATH = '/@vite/env'; // https://github.com/vitejs/vite/blob/v6.1.1/packages/vite/src/node/constants.ts#L10C1-L38C32 export const ROLLUP_HOOKS = [ @@ -38,5 +38,5 @@ export const ROLLUP_HOOKS = [ 'resolveId', 'shouldTransformCachedModule', 'transform', - 'onLog', - ] satisfies RollupPluginHooks[] \ No newline at end of file + 'onLog' +] satisfies RollupPluginHooks[]; diff --git a/src/vite/bridge/fsUtils.ts b/src/vite/bridge/fsUtils.ts index 3fda41844..5af5f791c 100644 --- a/src/vite/bridge/fsUtils.ts +++ b/src/vite/bridge/fsUtils.ts @@ -2,15 +2,16 @@ // index.ts for more details on contents and license of this file -import * as fs from 'fs'; -import { tryStatSync } from './utils'; +import * as fs from 'node:fs'; +import { tryStatSync } from '@src/vite/bridge/utils'; // https://github.com/vitejs/vite/blob/b7ddfae5f852c2948fab03e94751ce56f5f31ce0/packages/vite/src/node/fsUtils.ts#L388 export function tryResolveRealFile(file: string, preserveSymlinks?: boolean): string | undefined { const fileStat = tryStatSync(file); if (fileStat?.isFile()) { return file; - } else if (fileStat?.isSymbolicLink()) { + } + if (fileStat?.isSymbolicLink()) { return preserveSymlinks ? file : fs.realpathSync(file); } diff --git a/src/vite/bridge/index.ts b/src/vite/bridge/index.ts index 04b7e2ad1..bf1939e54 100644 --- a/src/vite/bridge/index.ts +++ b/src/vite/bridge/index.ts @@ -31,12 +31,19 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -export { type Plugin } from 'vite'; -export { fileToUrl } from './asset'; -export { createToImportMetaURLBasedRelativeRuntime, toOutputFilePathInJS } from './build'; -export { type ResolvedConfig } from './config'; -export { ENV_PUBLIC_PATH } from './constants'; -export { evalValue, cleanUrl, injectQuery, encodeURIPath } from './utils'; -export { tryFsResolve } from './resolve'; -export { tryOptimizedDepResolve } from './optimizer'; -export { AlphaTabWorkerTypes, WORKER_ASSET_ID, WORKER_FILE_ID, workerFileToUrl, workerCache, isSameContent } from './worker'; +export type { Plugin } from 'vite'; +export { fileToUrl } from '@src/vite/bridge/asset'; +export { createToImportMetaURLBasedRelativeRuntime, toOutputFilePathInJS } from '@src/vite/bridge/build'; +export type { ResolvedConfig } from '@src/vite/bridge/config'; +export { ENV_PUBLIC_PATH } from '@src/vite/bridge/constants'; +export { evalValue, cleanUrl, injectQuery, encodeURIPath } from '@src/vite/bridge/utils'; +export { tryFsResolve } from '@src/vite/bridge/resolve'; +export { tryOptimizedDepResolve } from '@src/vite/bridge/optimizer'; +export { + AlphaTabWorkerTypes, + WORKER_ASSET_ID, + WORKER_FILE_ID, + workerFileToUrl, + workerCache, + isSameContent +} from '@src/vite/bridge/worker'; diff --git a/src/vite/bridge/optimizer.ts b/src/vite/bridge/optimizer.ts index 24eb55f6d..ea88334aa 100644 --- a/src/vite/bridge/optimizer.ts +++ b/src/vite/bridge/optimizer.ts @@ -2,13 +2,13 @@ // index.ts for more details on contents and license of this file -import { DepOptimizationMetadata, DevEnvironment, OptimizedDepInfo, normalizePath } from 'vite'; -import { ResolvedConfig } from './config'; -import { tryFsResolve } from './resolve'; -import { cleanUrl } from './utils'; -import * as path from 'path'; -import { METADATA_FILENAME } from './constants'; -import * as fs from 'fs'; +import { type DepOptimizationMetadata, type DevEnvironment, type OptimizedDepInfo, normalizePath } from 'vite'; +import type { ResolvedConfig } from '@src/vite/bridge/config'; +import { tryFsResolve } from '@src/vite/bridge/resolve'; +import { cleanUrl } from '@src/vite/bridge/utils'; +import * as path from 'node:path'; +import { METADATA_FILENAME } from '@src/vite/bridge/constants'; +import * as fs from 'node:fs'; // https://github.com/Danielku15/vite/blob/88b7def341f12d07d7d4f83cbe3dc73cc8c6b7be/packages/vite/src/node/optimizer/index.ts#L1356 export function tryOptimizedDepResolve( @@ -33,7 +33,7 @@ export function tryOptimizedDepResolve( return undefined; } -type DepsOptimizer = DevEnvironment['depsOptimizer'] +type DepsOptimizer = DevEnvironment['depsOptimizer']; // https://github.com/Danielku15/vite/blob/88b7def341f12d07d7d4f83cbe3dc73cc8c6b7be/packages/vite/src/node/optimizer/optimizer.ts#L32-L40 const depsOptimizerMap = new WeakMap(); @@ -63,8 +63,7 @@ function createDepsOptimizer(config: ResolvedConfig) { throw new Error('not implemented'); }; const depsOptimizer: DepsOptimizer = { - async init() { - }, + async init() {}, metadata, registerMissingImport: notImplemented, run: notImplemented, diff --git a/src/vite/bridge/plugins.ts b/src/vite/bridge/plugins.ts index 48fb26d3e..19abf03d8 100644 --- a/src/vite/bridge/plugins.ts +++ b/src/vite/bridge/plugins.ts @@ -1,9 +1,10 @@ /**@target web */ -import { ObjectHook } from 'rollup'; -import { HookHandler } from 'vite'; +import type { ObjectHook } from 'rollup'; +import type { HookHandler } from 'vite'; // https://github.com/vitejs/vite/blob/v6.1.1/packages/vite/src/node/plugins/index.ts#L161 +// biome-ignore lint/complexity/noBannedTypes: Function type needed here export function getHookHandler>(hook: T): HookHandler { return (typeof hook === 'object' ? hook.handler : hook) as HookHandler; } diff --git a/src/vite/bridge/resolve.ts b/src/vite/bridge/resolve.ts index 50fa9d785..aa8fdc063 100644 --- a/src/vite/bridge/resolve.ts +++ b/src/vite/bridge/resolve.ts @@ -3,8 +3,8 @@ // index.ts for more details on contents and license of this file import { normalizePath } from 'vite'; -import { tryResolveRealFile } from './fsUtils'; -import { cleanUrl } from './utils'; +import { tryResolveRealFile } from '@src/vite/bridge/fsUtils'; +import { cleanUrl } from '@src/vite/bridge/utils'; // https://github.com/vitejs/vite/blob/b7ddfae5f852c2948fab03e94751ce56f5f31ce0/packages/vite/src/node/plugins/resolve.ts#L534 function splitFileAndPostfix(path: string) { @@ -16,7 +16,9 @@ function splitFileAndPostfix(path: string) { export function tryFsResolve(fsPath: string, preserveSymlinks: boolean): string | undefined { const { file, postfix } = splitFileAndPostfix(fsPath); const res = tryCleanFsResolve(file, preserveSymlinks); - if (res) return res + postfix; + if (res) { + return res + postfix; + } return; } diff --git a/src/vite/bridge/typeUtils.ts b/src/vite/bridge/typeUtils.ts index 7e24ee978..c7e22c296 100644 --- a/src/vite/bridge/typeUtils.ts +++ b/src/vite/bridge/typeUtils.ts @@ -1,6 +1,5 @@ /**@target web */ - // https://github.com/vitejs/vite/blob/v6.1.1/packages/vite/src/node/typeUtils.ts import type { ObjectHook, MinimalPluginContext as RollupMinimalPluginContext, Plugin as RollupPlugin } from 'rollup'; diff --git a/src/vite/bridge/utils.ts b/src/vite/bridge/utils.ts index 5be33a1f6..698d9a9b3 100644 --- a/src/vite/bridge/utils.ts +++ b/src/vite/bridge/utils.ts @@ -2,7 +2,7 @@ // index.ts for more details on contents and license of this file -import * as fs from 'fs'; +import * as fs from 'node:fs'; import { createHash } from 'node:crypto'; // https://github.com/vitejs/vite/blob/b7ddfae5f852c2948fab03e94751ce56f5f31ce0/packages/vite/src/node/utils.ts#L1302 @@ -34,7 +34,9 @@ export function tryStatSync(file: string) { // https://github.com/vitejs/vite/blob/b7ddfae5f852c2948fab03e94751ce56f5f31ce0/packages/vite/src/node/utils.ts#L1030 export function getHash(text: Buffer | string, length = 8): string { const h = createHash('sha256').update(text).digest('hex').substring(0, length); - if (length <= 64) return h; + if (length <= 64) { + return h; + } return h.padEnd(length, '_'); } @@ -55,7 +57,7 @@ export function joinUrlSegments(a: string, b: string): string { a = a.substring(0, a.length - 1); } if (b[0] !== '/') { - b = '/' + b; + b = `/${b}`; } return a + b; } @@ -73,7 +75,9 @@ export function injectQuery(builtUrl: string, query: string): string { // https://github.com/vitejs/vite/blob/b7ddfae5f852c2948fab03e94751ce56f5f31ce0/packages/vite/src/node/utils.ts#L1435 export function partialEncodeURIPath(uri: string): string { - if (uri.startsWith('data:')) return uri; + if (uri.startsWith('data:')) { + return uri; + } const filePath = cleanUrl(uri); const postfix = filePath !== uri ? uri.slice(filePath.length) : ''; return filePath.replaceAll('%', '%25') + postfix; @@ -81,7 +85,9 @@ export function partialEncodeURIPath(uri: string): string { // https://github.com/vitejs/vite/blob/b7ddfae5f852c2948fab03e94751ce56f5f31ce0/packages/vite/src/node/utils.ts#L1424 export function encodeURIPath(uri: string): string { - if (uri.startsWith('data:')) return uri; + if (uri.startsWith('data:')) { + return uri; + } const filePath = cleanUrl(uri); const postfix = filePath !== uri ? uri.slice(filePath.length) : ''; return encodeURI(filePath) + postfix; diff --git a/src/vite/bridge/worker.ts b/src/vite/bridge/worker.ts index 2c68bc584..25b5dfa79 100644 --- a/src/vite/bridge/worker.ts +++ b/src/vite/bridge/worker.ts @@ -2,13 +2,14 @@ // index.ts for more details on contents and license of this file -import { ResolvedConfig } from './config'; -import { cleanUrl, getHash } from './utils'; +import type { ResolvedConfig } from '@src/vite/bridge/config'; +import { cleanUrl, getHash } from '@src/vite/bridge/utils'; import type { OutputChunk } from 'rollup'; -import * as path from 'path'; +import * as path from 'node:path'; import { BuildEnvironment } from 'vite'; -import { injectEnvironmentToHooks } from './build'; +import { injectEnvironmentToHooks } from '@src/vite/bridge/build'; +// biome-ignore lint/suspicious/noConstEnum: Exception where we use them export const enum AlphaTabWorkerTypes { WorkerClassic = 'worker_classic', WorkerModule = 'worker_module', @@ -73,8 +74,7 @@ async function bundleWorkerEntry(config: ResolvedConfig, id: string): Promise ')}` + `Circular worker imports detected. Vite does not support it. Import chain: ${newBundleChain.join(' -> ')}` ); } @@ -111,7 +111,7 @@ async function bundleWorkerEntry(config: ResolvedConfig, id: string): Promise { + for (const outputChunk of outputChunks) { if (outputChunk.type === 'asset') { saveEmitWorkerAsset(config, outputChunk); } else if (outputChunk.type === 'chunk') { @@ -120,7 +120,7 @@ async function bundleWorkerEntry(config: ResolvedConfig, id: string): Promise migrate to monorepo) + /\b(alphaTabWorker|alphaTabWorklet\.addModule)\s*\(\s*(new\s+[^ (]+alphaTabUrl\s*\(\s*(\'[^\']+\'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*\))/dg; + + let match: RegExpExecArray | null = alphaTabWorkerPattern.exec(code); + while (match) { const workerType = getWorkerType(code, match); let typeActive = false; @@ -162,6 +160,7 @@ export function importMetaUrlPlugin(options: AlphaTabVitePluginOptions): Plugin } if (!typeActive) { + match = alphaTabWorkerPattern.exec(code); continue; } @@ -188,6 +187,8 @@ export function importMetaUrlPlugin(options: AlphaTabVitePluginOptions): Plugin // add `'' +` to skip vite:asset-import-meta-url plugin `new URL('' + ${JSON.stringify(builtUrl)}, import.meta.url)` ); + + match = alphaTabWorkerPattern.exec(code); } if (s) { diff --git a/src/vite/workerPlugin.ts b/src/vite/workerPlugin.ts index 0035bfc32..cdb0e7788 100644 --- a/src/vite/workerPlugin.ts +++ b/src/vite/workerPlugin.ts @@ -10,7 +10,6 @@ // With https://github.com/vitejs/vite/pull/16422 integrated this custom code might not be needed anymore // Some adjustment for audio worklet in vite might be needed to treat them as type "module" workers - // Original Sources Licensed under: // MIT License @@ -34,7 +33,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { AlphaTabVitePluginOptions } from './AlphaTabVitePluginOptions'; +import type { AlphaTabVitePluginOptions } from '@src/vite/AlphaTabVitePluginOptions'; import { type ResolvedConfig, type Plugin, @@ -47,9 +46,9 @@ import { AlphaTabWorkerTypes, WORKER_ASSET_ID, WORKER_FILE_ID -} from './bridge'; -import {} from './bridge'; -import * as path from 'path'; +} from '@src/vite/bridge'; +import {} from '@src/vite/bridge'; +import * as path from 'node:path'; import MagicString from 'magic-string'; const workerFileRE = new RegExp(`(?:\\?|&)${WORKER_FILE_ID}&type=(\\w+)(?:&|$)`); @@ -156,7 +155,6 @@ export function workerPlugin(options: AlphaTabVitePluginOptions): Plugin { resolvedConfig.isWorker ); - let match: RegExpExecArray | null; s = new MagicString(code); workerAssetUrlRE.lastIndex = 0; @@ -164,7 +162,8 @@ export function workerPlugin(options: AlphaTabVitePluginOptions): Plugin { const workerMap = workerCache.get(resolvedConfig.mainConfig || resolvedConfig)!; const { fileNameHash } = workerMap; - while ((match = workerAssetUrlRE.exec(code))) { + let match: RegExpExecArray | null = workerAssetUrlRE.exec(code); + while (match) { const [full, hash] = match; const filename = fileNameHash.get(hash)!; const replacement = toOutputFilePathInJS( @@ -180,6 +179,7 @@ export function workerPlugin(options: AlphaTabVitePluginOptions): Plugin { ? JSON.stringify(encodeURIPath(replacement)).slice(1, -1) : `"+${replacement.runtime}+"`; s.update(match.index, match.index + full.length, replacementString); + match = workerAssetUrlRE.exec(code); } } return result(); @@ -190,7 +190,7 @@ export function workerPlugin(options: AlphaTabVitePluginOptions): Plugin { return; } const workerMap = workerCache.get(resolvedConfig)!; - workerMap.assets.forEach(asset => { + for (const asset of workerMap.assets.values()) { const duplicateAsset = bundle[asset.fileName]; if (duplicateAsset) { const content = duplicateAsset.type === 'asset' ? duplicateAsset.source : duplicateAsset.code; @@ -205,7 +205,7 @@ export function workerPlugin(options: AlphaTabVitePluginOptions): Plugin { fileName: asset.fileName, source: asset.source }); - }); + } workerMap.assets.clear(); } }; diff --git a/src/webpack/AlphaTabAudioWorklet.ts b/src/webpack/AlphaTabAudioWorklet.ts index f744aef77..10a8ab542 100644 --- a/src/webpack/AlphaTabAudioWorklet.ts +++ b/src/webpack/AlphaTabAudioWorklet.ts @@ -1,8 +1,8 @@ /**@target web */ -import { type VariableDeclarator, type Identifier, type Expression, type CallExpression } from 'estree'; -import { AlphaTabWebPackPluginOptions } from './AlphaTabWebPackPluginOptions'; -import { getWorkerRuntime, parseModuleUrl, tapJavaScript, webPackWithAlphaTab, webpackTypes } from './Utils'; +import type { VariableDeclarator, Identifier, Expression, CallExpression } from 'estree'; +import type { AlphaTabWebPackPluginOptions } from '@src/webpack/AlphaTabWebPackPluginOptions'; +import { getWorkerRuntime, parseModuleUrl, tapJavaScript, type webPackWithAlphaTab, type webpackTypes } from '@src/webpack/Utils'; const AlphaTabWorkletSpecifierTag = Symbol('alphatab worklet specifier tag'); const workletIndexMap = new WeakMap(); diff --git a/src/webpack/AlphaTabWebPackPlugin.ts b/src/webpack/AlphaTabWebPackPlugin.ts index 5df451b8b..a82d516e4 100644 --- a/src/webpack/AlphaTabWebPackPlugin.ts +++ b/src/webpack/AlphaTabWebPackPlugin.ts @@ -1,27 +1,33 @@ /**@target web */ -import * as fs from 'fs'; -import * as path from 'path'; -import * as url from 'url'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as url from 'node:url'; // webpack doesn't defined properly types for all these internals // needed for the plugin -import { injectWorkerRuntimeModule } from './AlphaTabWorkerRuntimeModule'; -import { configureAudioWorklet } from './AlphaTabAudioWorklet'; -import { AlphaTabWebPackPluginOptions } from './AlphaTabWebPackPluginOptions'; -import { configureWebWorker } from './AlphaTabWebWorker'; -import { webPackWithAlphaTab, webpackTypes } from './Utils'; -import { injectWebWorkerDependency as injectWebWorkerDependency } from './AlphaTabWebWorkerDependency'; -import { injectWorkletRuntimeModule } from './AlphaTabWorkletStartRuntimeModule'; -import { injectWorkletDependency } from './AlphaTabWorkletDependency'; +import { injectWorkerRuntimeModule } from '@src/webpack/AlphaTabWorkerRuntimeModule'; +import { configureAudioWorklet } from '@src/webpack/AlphaTabAudioWorklet'; +import type { AlphaTabWebPackPluginOptions } from '@src/webpack/AlphaTabWebPackPluginOptions'; +import { configureWebWorker } from '@src/webpack/AlphaTabWebWorker'; +import type { webPackWithAlphaTab, webpackTypes } from '@src/webpack/Utils'; +import { injectWebWorkerDependency } from '@src/webpack/AlphaTabWebWorkerDependency'; +import { injectWorkletRuntimeModule } from '@src/webpack/AlphaTabWorkletStartRuntimeModule'; +import { injectWorkletDependency } from '@src/webpack/AlphaTabWorkletDependency'; const WINDOWS_ABS_PATH_REGEXP = /^[a-zA-Z]:[\\/]/; const WINDOWS_PATH_SEPARATOR_REGEXP = /\\/g; const relativePathToRequest = (relativePath: string): string => { - if (relativePath === '') return './.'; - if (relativePath === '..') return '../.'; - if (relativePath.startsWith('../')) return relativePath; + if (relativePath === '') { + return '@src/webpack/.'; + } + if (relativePath === '..') { + return '../.'; + } + if (relativePath.startsWith('../')) { + return relativePath; + } return `./${relativePath}`; }; @@ -64,7 +70,9 @@ const makeCacheableWithContext = (fn: (text: string, request: string) => string) const cache = new WeakMap>>(); const cachedFn = (context: string, identifier: string, associatedObjectForCache?: object): string => { - if (!associatedObjectForCache) return fn(context, identifier); + if (!associatedObjectForCache) { + return fn(context, identifier); + } let innerCache = cache.get(associatedObjectForCache); if (innerCache === undefined) { @@ -72,28 +80,28 @@ const makeCacheableWithContext = (fn: (text: string, request: string) => string) cache.set(associatedObjectForCache, innerCache); } - let cachedResult; + let cachedResult: string | undefined; let innerSubCache = innerCache.get(context); if (innerSubCache === undefined) { - innerCache.set(context, (innerSubCache = new Map())); + innerSubCache = new Map(); + innerCache.set(context, innerSubCache); } else { cachedResult = innerSubCache.get(identifier); } if (cachedResult !== undefined) { return cachedResult; - } else { - const result = fn(context, identifier); - innerSubCache.set(identifier, result); - return result; } + const result = fn(context, identifier); + innerSubCache.set(identifier, result); + return result; }; cachedFn.bindContextCache = ( context: string, associatedObjectForCache?: object ): ((identifier: string) => string) => { - let innerSubCache; + let innerSubCache: Map | undefined; if (associatedObjectForCache) { let innerCache = cache.get(associatedObjectForCache); if (innerCache === undefined) { @@ -103,7 +111,8 @@ const makeCacheableWithContext = (fn: (text: string, request: string) => string) innerSubCache = innerCache.get(context); if (innerSubCache === undefined) { - innerCache.set(context, (innerSubCache = new Map())); + innerSubCache = new Map(); + innerCache.set(context, innerSubCache); } } else { innerSubCache = new Map(); @@ -113,11 +122,10 @@ const makeCacheableWithContext = (fn: (text: string, request: string) => string) const cachedResult = innerSubCache.get(identifier); if (cachedResult !== undefined) { return cachedResult; - } else { - const result = fn(context, identifier); - innerSubCache.set(identifier, result); - return result; } + const result = fn(context, identifier); + innerSubCache.set(identifier, result); + return result; }; return boundFn; @@ -293,7 +301,7 @@ export class AlphaTabWebPackPlugin { // see https://github.com/nodejs/node/pull/50976 const sourceFilename = path.join(file.parentPath ?? file.path, file.name); await fs.promises.copyFile(sourceFilename, path.join(outputPath!, subdir, file.name)); - const assetFileName = subdir + '/' + file.name; + const assetFileName = `${subdir}/${file.name}`; const existingAsset = compilation.getAsset(assetFileName); const data = await fs.promises.readFile(sourceFilename); diff --git a/src/webpack/AlphaTabWebPackPluginOptions.ts b/src/webpack/AlphaTabWebPackPluginOptions.ts index 5929f4061..4b1235359 100644 --- a/src/webpack/AlphaTabWebPackPluginOptions.ts +++ b/src/webpack/AlphaTabWebPackPluginOptions.ts @@ -7,7 +7,7 @@ export interface AlphaTabWebPackPluginOptions { alphaTabSourceDir?: string; /** - * The location where assets of alphaTab should be placed. + * The location where assets of alphaTab should be placed. * Set it to false to disable the copying of assets like fonts. * (default: compiler.options.output.path) */ @@ -16,7 +16,7 @@ export interface AlphaTabWebPackPluginOptions { /** * Whether alphaTab should configure the audio worklet support in WebPack. * This might break support for audio playback unless audio worklet support is added - * through other means to WebPack. + * through other means to WebPack. * (default: true) */ audioWorklets?: boolean; @@ -24,8 +24,8 @@ export interface AlphaTabWebPackPluginOptions { /** * Whether alphaTab should configure the web worklet support in WebPack. * This might break support for audio playback and background unless audio worklet support is added - * through other means to WebPack. + * through other means to WebPack. * (default: true) */ webWorkers?: boolean; -} \ No newline at end of file +} diff --git a/src/webpack/AlphaTabWebWorker.ts b/src/webpack/AlphaTabWebWorker.ts index 07c1db6ad..10a54bc2a 100644 --- a/src/webpack/AlphaTabWebWorker.ts +++ b/src/webpack/AlphaTabWebWorker.ts @@ -1,8 +1,8 @@ /**@target web */ -import { type Expression, type CallExpression } from 'estree'; -import { AlphaTabWebPackPluginOptions } from './AlphaTabWebPackPluginOptions'; -import { getWorkerRuntime, parseModuleUrl, tapJavaScript, webPackWithAlphaTab, webpackTypes } from './Utils'; +import type { Expression, CallExpression, NewExpression } from 'estree'; +import type { AlphaTabWebPackPluginOptions } from '@src/webpack/AlphaTabWebPackPluginOptions'; +import { getWorkerRuntime, parseModuleUrl, tapJavaScript, type webPackWithAlphaTab, type webpackTypes } from '@src/webpack/Utils'; const workerIndexMap = new WeakMap(); @@ -75,6 +75,7 @@ export function configureWebWorker( parser.state.module.addPresentationalDependency(dep1); parser.walkExpression(expr.callee); + parser.walkExpression((arg1 as NewExpression).callee); return true; }; diff --git a/src/webpack/AlphaTabWebWorkerDependency.ts b/src/webpack/AlphaTabWebWorkerDependency.ts index 3f974671b..43c1adbeb 100644 --- a/src/webpack/AlphaTabWebWorkerDependency.ts +++ b/src/webpack/AlphaTabWebWorkerDependency.ts @@ -1,14 +1,14 @@ /**@target web */ import { - Hash, - ObjectSerializerContext, - ObjectDeserializerContext, + type Hash, + type ObjectSerializerContext, + type ObjectDeserializerContext, makeDependencySerializable, - webPackWithAlphaTab, - webpackTypes, - NormalModuleFactory -} from './Utils'; + type webPackWithAlphaTab, + type webpackTypes, + type NormalModuleFactory +} from '@src/webpack/Utils'; export function injectWebWorkerDependency(webPackWithAlphaTab: webPackWithAlphaTab) { class AlphaTabWebWorkerDependency extends webPackWithAlphaTab.webpack.dependencies.ModuleDependency { @@ -71,7 +71,9 @@ export function injectWebWorkerDependency(webPackWithAlphaTab: webPackWithAlphaT const entrypoint = chunkGraph.getBlockChunkGroup(block) as any; const chunk = entrypoint.getEntrypointChunk() as webpackTypes.Chunk; // We use the workerPublicPath option if provided, else we fallback to the RuntimeGlobal publicPath - const workerImportBaseUrl = dep.publicPath ? `"${dep.publicPath}"` : webPackWithAlphaTab.webpack.RuntimeGlobals.publicPath; + const workerImportBaseUrl = dep.publicPath + ? `"${dep.publicPath}"` + : webPackWithAlphaTab.webpack.RuntimeGlobals.publicPath; runtimeRequirements.add(webPackWithAlphaTab.webpack.RuntimeGlobals.publicPath); runtimeRequirements.add(webPackWithAlphaTab.webpack.RuntimeGlobals.baseURI); diff --git a/src/webpack/AlphaTabWorkerRuntimeModule.ts b/src/webpack/AlphaTabWorkerRuntimeModule.ts index 98f606ddb..ade2f8485 100644 --- a/src/webpack/AlphaTabWorkerRuntimeModule.ts +++ b/src/webpack/AlphaTabWorkerRuntimeModule.ts @@ -1,6 +1,6 @@ /**@target web */ -import { isWorkerRuntime, webPackWithAlphaTab, webpackTypes } from './Utils'; +import { isWorkerRuntime, type webPackWithAlphaTab, type webpackTypes } from '@src/webpack/Utils'; export function injectWorkerRuntimeModule(webPackWithAlphaTab: webPackWithAlphaTab) { class AlphaTabWorkerRuntimeModule extends webPackWithAlphaTab.webpack.RuntimeModule { @@ -30,7 +30,7 @@ export function injectWorkerRuntimeModule(webPackWithAlphaTab: webPackWithAlphaT return webPackWithAlphaTab.webpack.Template.asString([ `if ( ! ('AudioWorkletGlobalScope' in ${globalObject}) ) { return; }`, - `const installedChunks = {`, + 'const installedChunks = {', webPackWithAlphaTab.webpack.Template.indent( Array.from(initialChunkIds, id => `${JSON.stringify(id)}: 1`).join(',\n') ), @@ -71,12 +71,12 @@ export function injectWorkerRuntimeModule(webPackWithAlphaTab: webPackWithAlphaT compilation.addRuntimeModule(chunk, new AlphaTabWorkerRuntimeModule()); }); - compilation.hooks.additionalChunkRuntimeRequirements.tap(pluginName, (chunk, runtimeRequirements) =>{ - if(isWorkerRuntime(chunk.runtime)) { - runtimeRequirements.add(webPackWithAlphaTab.webpack.RuntimeGlobals.moduleFactories); - runtimeRequirements.add(webPackWithAlphaTab.alphaTab.WebWorkerRuntimeModuleKey); - } - }); + compilation.hooks.additionalChunkRuntimeRequirements.tap(pluginName, (chunk, runtimeRequirements) => { + if (isWorkerRuntime(chunk.runtime)) { + runtimeRequirements.add(webPackWithAlphaTab.webpack.RuntimeGlobals.moduleFactories); + runtimeRequirements.add(webPackWithAlphaTab.alphaTab.WebWorkerRuntimeModuleKey); + } + }); }; webPackWithAlphaTab.alphaTab.WebWorkerRuntimeModuleKey = AlphaTabWorkerRuntimeModule.Key; } diff --git a/src/webpack/AlphaTabWorkletDependency.ts b/src/webpack/AlphaTabWorkletDependency.ts index 963eb9666..a1a5a3ae6 100644 --- a/src/webpack/AlphaTabWorkletDependency.ts +++ b/src/webpack/AlphaTabWorkletDependency.ts @@ -1,14 +1,14 @@ /**@target web */ import { - Hash, - NormalModuleFactory, - ObjectDeserializerContext, - ObjectSerializerContext, + type Hash, + type NormalModuleFactory, + type ObjectDeserializerContext, + type ObjectSerializerContext, makeDependencySerializable, - webPackWithAlphaTab, - webpackTypes -} from './Utils'; + type webPackWithAlphaTab, + type webpackTypes +} from '@src/webpack/Utils'; export function injectWorkletDependency(webPackWithAlphaTab: webPackWithAlphaTab) { /** @@ -89,7 +89,10 @@ export function injectWorkletDependency(webPackWithAlphaTab: webPackWithAlphaTab chunkGraph.addChunkRuntimeRequirements( chunk, - new Set([webPackWithAlphaTab.webpack.RuntimeGlobals.moduleFactories, webPackWithAlphaTab.alphaTab.WebWorkerRuntimeModuleKey]) + new Set([ + webPackWithAlphaTab.webpack.RuntimeGlobals.moduleFactories, + webPackWithAlphaTab.alphaTab.WebWorkerRuntimeModuleKey + ]) ); runtimeRequirements.add(webPackWithAlphaTab.alphaTab.RuntimeGlobalWorkletGetStartupChunks); @@ -107,7 +110,7 @@ export function injectWorkletDependency(webPackWithAlphaTab: webPackWithAlphaTab ]), '}' ]), - `})(alphaTabWorklet)` + '})(alphaTabWorklet)' ]) ); } @@ -123,7 +126,6 @@ export function injectWorkletDependency(webPackWithAlphaTab: webPackWithAlphaTab compilation.dependencyTemplates.set(AlphaTabWorkletDependency, new AlphaTabWorkletDependency.Template()); }; - webPackWithAlphaTab.alphaTab.createWorkletDependency = ( request: string, range: [number, number], diff --git a/src/webpack/AlphaTabWorkletStartRuntimeModule.ts b/src/webpack/AlphaTabWorkletStartRuntimeModule.ts index 7a7d87efd..c8e904d24 100644 --- a/src/webpack/AlphaTabWorkletStartRuntimeModule.ts +++ b/src/webpack/AlphaTabWorkletStartRuntimeModule.ts @@ -1,5 +1,5 @@ /**@target web */ -import { isWorkerRuntime, webPackWithAlphaTab, webpackTypes } from './Utils'; +import { isWorkerRuntime, type webPackWithAlphaTab, type webpackTypes } from '@src/webpack/Utils'; export function injectWorkletRuntimeModule(webPackWithAlphaTab: webPackWithAlphaTab) { class AlphaTabWorkletStartRuntimeModule extends webPackWithAlphaTab.webpack.RuntimeModule { @@ -40,7 +40,9 @@ export function injectWorkletRuntimeModule(webPackWithAlphaTab: webPackWithAlpha `${AlphaTabWorkletStartRuntimeModule.RuntimeGlobalWorkletGetStartupChunks} = (() => {`, webPackWithAlphaTab.webpack.Template.indent([ 'const lookup = new Map(', - webPackWithAlphaTab.webpack.Template.indent(JSON.stringify(Array.from(workletChunkLookup.entries()))), + webPackWithAlphaTab.webpack.Template.indent( + JSON.stringify(Array.from(workletChunkLookup.entries())) + ), ');', 'return (chunkId) => lookup.get(String(chunkId)) ?? [];' @@ -53,7 +55,10 @@ export function injectWorkletRuntimeModule(webPackWithAlphaTab: webPackWithAlpha webPackWithAlphaTab.alphaTab.RuntimeGlobalWorkletGetStartupChunks = AlphaTabWorkletStartRuntimeModule.RuntimeGlobalWorkletGetStartupChunks; - webPackWithAlphaTab.alphaTab.registerWorkletRuntimeModule = (pluginName: string, compilation: webpackTypes.Compilation) => { + webPackWithAlphaTab.alphaTab.registerWorkletRuntimeModule = ( + pluginName: string, + compilation: webpackTypes.Compilation + ) => { compilation.hooks.runtimeRequirementInTree .for(AlphaTabWorkletStartRuntimeModule.RuntimeGlobalWorkletGetStartupChunks) .tap(pluginName, (chunk: webpackTypes.Chunk) => { diff --git a/src/webpack/Utils.ts b/src/webpack/Utils.ts index 14754feca..56e1c907b 100644 --- a/src/webpack/Utils.ts +++ b/src/webpack/Utils.ts @@ -1,7 +1,7 @@ /**@target web */ import type * as webpackTypes from 'webpack'; -export { webpackTypes }; +export type { webpackTypes }; export type webPackWithAlphaTab = { webpack: webpackTypes.Compiler['webpack']; @@ -90,7 +90,7 @@ export function parseModuleUrl(parser: any, expr: Expression) { const [arg1, arg2] = newExpr.arguments; const callee = parser.evaluateExpression(newExpr.callee); - if (!callee.isIdentifier() || callee.identifier !== 'URL') { + if (!callee.isIdentifier() || !callee.identifier.includes('alphaTabUrl')) { return; } @@ -106,9 +106,9 @@ export function getWorkerRuntime( cachedContextify: (s: string) => string, workerIndexMap: WeakMap ): string { - let i = workerIndexMap.get(parser.state) || 0; + const i = workerIndexMap.get(parser.state) || 0; workerIndexMap.set(parser.state, i + 1); - let name = `${cachedContextify(parser.state.module.identifier())}|${i}`; + const name = `${cachedContextify(parser.state.module.identifier())}|${i}`; const hash = compilation.compiler.webpack.util.createHash(compilation.outputOptions.hashFunction!); hash.update(name); const digest = hash.digest(compilation.outputOptions.hashDigest) as string; diff --git a/src/xml/XmlError.ts b/src/xml/XmlError.ts index fcd4180b9..9a2fe9760 100644 --- a/src/xml/XmlError.ts +++ b/src/xml/XmlError.ts @@ -21,7 +21,7 @@ * DEALINGS IN THE SOFTWARE. */ -import { AlphaTabError, AlphaTabErrorType } from "@src/AlphaTabError"; +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; export class XmlError extends AlphaTabError { public xml: string; diff --git a/src/xml/XmlNode.ts b/src/xml/XmlNode.ts index 14cf46a5e..0fda3d50b 100644 --- a/src/xml/XmlNode.ts +++ b/src/xml/XmlNode.ts @@ -22,12 +22,12 @@ */ export enum XmlNodeType { - None, - Element, - Text, - CDATA, - Document, - DocumentType, + None = 0, + Element = 1, + Text = 2, + CDATA = 3, + Document = 4, + DocumentType = 5 } export class XmlNode { @@ -39,6 +39,14 @@ export class XmlNode { public firstChild: XmlNode | null = null; public firstElement: XmlNode | null = null; + public *childElements() { + for (const c of this.childNodes) { + if (c.nodeType === XmlNodeType.Element) { + yield c; + } + } + } + public addChild(node: XmlNode): void { this.childNodes.push(node); this.firstChild = node; @@ -47,21 +55,21 @@ export class XmlNode { } } - public getAttribute(name: string): string { + public getAttribute(name: string, defaultValue: string = ''): string { if (this.attributes.has(name)) { return this.attributes.get(name)!; } - return ''; + return defaultValue; } public getElementsByTagName(name: string, recursive: boolean = false): XmlNode[] { - let tags: XmlNode[] = []; + const tags: XmlNode[] = []; this.searchElementsByTagName(this.childNodes, tags, name, recursive); return tags; } private searchElementsByTagName(all: XmlNode[], result: XmlNode[], name: string, recursive: boolean = false): void { - for (let c of all) { + for (const c of all) { if (c && c.nodeType === XmlNodeType.Element && c.localName === name) { result.push(c); } @@ -72,7 +80,7 @@ export class XmlNode { } public findChildElement(name: string): XmlNode | null { - for (let c of this.childNodes) { + for (const c of this.childNodes) { if (c && c.nodeType === XmlNodeType.Element && c.localName === name) { return c; } @@ -90,20 +98,19 @@ export class XmlNode { public get innerText(): string { if (this.nodeType === XmlNodeType.Element || this.nodeType === XmlNodeType.Document) { - if(this.firstElement && this.firstElement.nodeType === XmlNodeType.CDATA) { + if (this.firstElement && this.firstElement.nodeType === XmlNodeType.CDATA) { return this.firstElement.innerText; } let txt: string = ''; - for (let c of this.childNodes) { + for (const c of this.childNodes) { txt += c.innerText?.toString(); } - let s: string = txt; + const s: string = txt; return s.trim(); } return this.value ?? ''; } - public set innerText(value: string) { const textNode = new XmlNode(); textNode.nodeType = XmlNodeType.Text; @@ -111,7 +118,7 @@ export class XmlNode { this.childNodes = [textNode]; } - public setCData(s:string) { + public setCData(s: string) { const textNode = new XmlNode(); textNode.nodeType = XmlNodeType.CDATA; textNode.value = s; diff --git a/src/xml/XmlParser.ts b/src/xml/XmlParser.ts index 5d7735352..8e58f28a7 100644 --- a/src/xml/XmlParser.ts +++ b/src/xml/XmlParser.ts @@ -25,25 +25,25 @@ import { XmlError } from '@src/xml/XmlError'; import { XmlNode, XmlNodeType } from '@src/xml/XmlNode'; enum XmlState { - IgnoreSpaces, - Begin, - BeginNode, - TagName, - Body, - AttribName, - Equals, - AttvalBegin, - AttribVal, - Childs, - Close, - WaitEnd, - WaitEndRet, - Pcdata, - Header, - Comment, - Doctype, - Cdata, - Escape + IgnoreSpaces = 0, + Begin = 1, + BeginNode = 2, + TagName = 3, + Body = 4, + AttribName = 5, + Equals = 6, + AttvalBegin = 7, + AttribVal = 8, + Childs = 9, + Close = 10, + WaitEnd = 11, + WaitEndRet = 12, + Pcdata = 13, + Header = 14, + Comment = 15, + Doctype = 16, + Cdata = 17, + Escape = 18 } export class XmlParser { @@ -132,7 +132,7 @@ export class XmlParser { case XmlState.Pcdata: if (c === XmlParser.CharCodeLowerThan) { buf += str.substr(start, p - start); - let child: XmlNode = new XmlNode(); + const child: XmlNode = new XmlNode(); child.nodeType = XmlNodeType.Text; child.value = buf; buf = ''; @@ -154,7 +154,7 @@ export class XmlParser { str.charCodeAt(p + 2) === XmlParser.CharCodeGreaterThan ) { // ]]> - let child: XmlNode = new XmlNode(); + const child: XmlNode = new XmlNode(); child.nodeType = XmlNodeType.CDATA; child.value = str.substr(start, p - start); parent.addChild(child); @@ -249,7 +249,7 @@ export class XmlParser { if (start === p) { throw new XmlError('Expected attribute name', str, p); } - let tmp: string = str.substr(start, p - start); + const tmp: string = str.substr(start, p - start); aname = tmp; if (xml!.attributes.has(aname)) { throw new XmlError(`Duplicate attribute [${aname}]`, str, p); @@ -294,7 +294,7 @@ export class XmlParser { default: if (c === attrValQuote) { buf += str.substr(start, p - start); - let value: string = buf; + const value: string = buf; buf = ''; xml!.attributes.set(aname!, value); state = XmlState.IgnoreSpaces; @@ -333,9 +333,9 @@ export class XmlParser { if (start === p) { throw new XmlError('Expected node name', str, p); } - let v: string = str.substr(start, p - start); + const v: string = str.substr(start, p - start); if (v !== parent.localName) { - throw new XmlError('Expected ', str, p); + throw new XmlError(`Expected `, str, p); } state = XmlState.IgnoreSpaces; next = XmlState.WaitEndRet; @@ -361,7 +361,7 @@ export class XmlParser { nbrackets--; } else if (c === XmlParser.CharCodeGreaterThan && nbrackets === 0) { // > - let node: XmlNode = new XmlNode(); + const node: XmlNode = new XmlNode(); node.nodeType = XmlNodeType.DocumentType; node.value = str.substr(start, p - start); parent.addChild(node); @@ -378,17 +378,17 @@ export class XmlParser { case XmlState.Escape: if (c === XmlParser.CharCodeSemi) { - let s: string = str.substr(start, p - start); + const s: string = str.substr(start, p - start); if (s.charCodeAt(0) === XmlParser.CharCodeSharp) { - let code: number = + const code: number = s.charCodeAt(1) === XmlParser.CharCodeLowerX - ? parseInt('0' + s.substr(1, s.length - 1)) - : parseInt(s.substr(1, s.length - 1)); + ? Number.parseInt(`0${s.substr(1, s.length - 1)}`) + : Number.parseInt(s.substr(1, s.length - 1)); buf += String.fromCharCode(code); } else if (XmlParser.Escapes.has(s)) { buf += XmlParser.Escapes.get(s); } else { - buf += ('&' + s + ';')?.toString(); + buf += `&${s};`?.toString(); } start = p + 1; state = escapeNext; @@ -413,7 +413,7 @@ export class XmlParser { if (state === XmlState.Pcdata) { if (p !== start) { buf += str.substr(start, p - start); - let node: XmlNode = new XmlNode(); + const node: XmlNode = new XmlNode(); node.nodeType = XmlNodeType.Text; node.value = buf; parent.addChild(node); @@ -423,7 +423,7 @@ export class XmlParser { if (state === XmlState.Escape && escapeNext === XmlState.Pcdata) { buf += '&'; buf += str.substr(start, p - start); - let node: XmlNode = new XmlNode(); + const node: XmlNode = new XmlNode(); node.nodeType = XmlNodeType.Text; node.value = buf; parent.addChild(node); diff --git a/src/xml/XmlWriter.ts b/src/xml/XmlWriter.ts index 801368b6f..1c1af1a0c 100644 --- a/src/xml/XmlWriter.ts +++ b/src/xml/XmlWriter.ts @@ -1,4 +1,4 @@ -import { XmlNode, XmlNodeType } from '@src/xml/XmlNode'; +import { type XmlNode, XmlNodeType } from '@src/xml/XmlNode'; export class XmlWriter { public static write(xml: XmlNode, indention: string, xmlHeader: boolean): string { diff --git a/src/zip/Adler32.ts b/src/zip/Adler32.ts index 1659d6c1d..695c98620 100644 --- a/src/zip/Adler32.ts +++ b/src/zip/Adler32.ts @@ -7,10 +7,10 @@ * without restriction, including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons * to whom the Software is furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE @@ -28,13 +28,11 @@ * ZLIB Compressed Data Format Specification version 3.3) */ export class Adler32 { - /** * largest prime smaller than 65536 */ private static readonly Base: number = 65521; - /** * Returns the Adler32 data checksum computed so far. */ @@ -62,7 +60,7 @@ export class Adler32 { */ public update(data: Uint8Array, offset: number, count: number) { //(By Per Bothner) - let s1 = this.value & 0xFFFF; + let s1 = this.value & 0xffff; let s2 = this.value >> 16; while (count > 0) { // We can defer the modulo operation: @@ -82,4 +80,4 @@ export class Adler32 { } this.value = (s2 << 16) | s1; } -} \ No newline at end of file +} diff --git a/src/zip/Crc32.ts b/src/zip/Crc32.ts index ae8164040..2f6e62cbf 100644 --- a/src/zip/Crc32.ts +++ b/src/zip/Crc32.ts @@ -6,7 +6,7 @@ export class Crc32 { private static buildCrc32Lookup(): Uint32Array { const poly = 0xedb88320; const lookup = new Uint32Array(256); - for(let i = 0; i < lookup.length; i++) { + for (let i = 0; i < lookup.length; i++) { let crc = i; for (let bit = 0; bit < 8; bit++) { crc = (crc & 1) === 1 ? (crc >>> 1) ^ poly : crc >>> 1; @@ -17,7 +17,7 @@ export class Crc32 { return lookup; } - private static readonly CrcInit: number = 0xFFFFFFFF; + private static readonly CrcInit: number = 0xffffffff; /** * The CRC data checksum so far. @@ -45,8 +45,9 @@ export class Crc32 { * @param count The number of bytes to checksum starting from offset */ public update(data: Uint8Array, offset: number, count: number) { - for(let i = 0; i < count; i++) { - this._checkValue = Crc32.Crc32Lookup[(this._checkValue ^ data[offset + i]) & 0xff] ^ (this._checkValue >>> 8); + for (let i = 0; i < count; i++) { + this._checkValue = + Crc32.Crc32Lookup[(this._checkValue ^ data[offset + i]) & 0xff] ^ (this._checkValue >>> 8); } } @@ -56,4 +57,4 @@ export class Crc32 { public reset() { this._checkValue = Crc32.CrcInit; } -} \ No newline at end of file +} diff --git a/src/zip/Deflater.ts b/src/zip/Deflater.ts index 12c19db84..99402cab8 100644 --- a/src/zip/Deflater.ts +++ b/src/zip/Deflater.ts @@ -7,10 +7,10 @@ * without restriction, including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons * to whom the Software is furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE @@ -19,18 +19,18 @@ * DEALINGS IN THE SOFTWARE. */ -import { DeflaterConstants } from "./DeflaterConstants"; -import { DeflaterEngine } from "./DeflaterEngine"; -import { PendingBuffer } from "./PendingBuffer"; +import { DeflaterConstants } from '@src/zip/DeflaterConstants'; +import { DeflaterEngine } from '@src/zip/DeflaterEngine'; +import { PendingBuffer } from '@src/zip/PendingBuffer'; /** * This is the Deflater class. The deflater class compresses input * with the deflate algorithm described in RFC 1951. It has several * compression levels and three different strategies described below. - * + * * This class is not thread safe. This is inherent in the API, due * to the split of deflate and setInput. - * + * * author of the original java version : Jochen Hoenicke */ export class Deflater { @@ -112,7 +112,7 @@ export class Deflater { * are available. */ public get isFinished() { - return (this._state == Deflater.FinishedState) && this._pending.isFlushed; + return this._state === Deflater.FinishedState && this._pending.isFlushed; } /** @@ -148,18 +148,23 @@ export class Deflater { * needsInput() or finished() returns true or length is zero. */ public deflate(output: Uint8Array, offset: number, length: number): number { - let origLength = length; + const origLength = length; - while(true) { - let count = this._pending.flush(output, offset, length); + while (true) { + const count = this._pending.flush(output, offset, length); offset += count; length -= count; - if (length == 0 || this._state == Deflater.FinishedState) { + if (length === 0 || this._state === Deflater.FinishedState) { break; } - if (!this._engine.deflate((this._state & Deflater.IsFlushing) != 0, (this._state & Deflater.IsFinishing) != 0)) { + if ( + !this._engine.deflate( + (this._state & Deflater.IsFlushing) !== 0, + (this._state & Deflater.IsFinishing) !== 0 + ) + ) { switch (this._state) { case Deflater.BusyState: // We need more input now @@ -167,14 +172,14 @@ export class Deflater { case Deflater.FlushingState: /* We have to supply some lookahead. 8 bit lookahead - * is needed by the zlib inflater, and we must fill - * the next byte, so that all bits are flushed. - */ - let neededbits = 8 + ((-this._pending.bitCount) & 7); + * is needed by the zlib inflater, and we must fill + * the next byte, so that all bits are flushed. + */ + let neededbits = 8 + (-this._pending.bitCount & 7); while (neededbits > 0) { /* write a static tree block consisting solely of - * an EOF: - */ + * an EOF: + */ this._pending.writeBits(2, 10); neededbits -= 10; } @@ -189,15 +194,14 @@ export class Deflater { } } return origLength - length; - } /** * Finishes the deflater with the current input block. It is an error * to give more input after this method was called. This method must - * be called to force all bytes to be flushed. + * be called to force all bytes to be flushed. */ public finish() { - this._state |= (Deflater.IsFlushing | Deflater.IsFinishing); + this._state |= Deflater.IsFlushing | Deflater.IsFinishing; } } diff --git a/src/zip/DeflaterConstants.ts b/src/zip/DeflaterConstants.ts index 3e2bbf06b..4ff9e724b 100644 --- a/src/zip/DeflaterConstants.ts +++ b/src/zip/DeflaterConstants.ts @@ -7,10 +7,10 @@ * without restriction, including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons * to whom the Software is furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE @@ -35,9 +35,10 @@ export class DeflaterConstants { public static readonly HASH_BITS: number = DeflaterConstants.DEFAULT_MEM_LEVEL + 7; public static readonly HASH_SIZE: number = 1 << DeflaterConstants.HASH_BITS; - public static readonly HASH_SHIFT: number = (DeflaterConstants.HASH_BITS + DeflaterConstants.MIN_MATCH - 1) / DeflaterConstants.MIN_MATCH; + public static readonly HASH_SHIFT: number = + (DeflaterConstants.HASH_BITS + DeflaterConstants.MIN_MATCH - 1) / DeflaterConstants.MIN_MATCH; public static readonly HASH_MASK: number = DeflaterConstants.HASH_SIZE - 1; public static readonly MIN_LOOKAHEAD: number = DeflaterConstants.MAX_MATCH + DeflaterConstants.MIN_MATCH + 1; public static readonly MAX_DIST: number = DeflaterConstants.WSIZE - DeflaterConstants.MIN_LOOKAHEAD; -} \ No newline at end of file +} diff --git a/src/zip/DeflaterEngine.ts b/src/zip/DeflaterEngine.ts index 7b2749cb8..396cc745d 100644 --- a/src/zip/DeflaterEngine.ts +++ b/src/zip/DeflaterEngine.ts @@ -7,10 +7,10 @@ * without restriction, including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons * to whom the Software is furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE @@ -19,10 +19,10 @@ * DEALINGS IN THE SOFTWARE. */ -import { Crc32 } from "./Crc32"; -import { DeflaterConstants } from "./DeflaterConstants"; -import { DeflaterHuffman } from "./DeflaterHuffman"; -import { PendingBuffer } from "./PendingBuffer"; +import { Crc32 } from '@src/zip/Crc32'; +import { DeflaterConstants } from '@src/zip/DeflaterConstants'; +import { DeflaterHuffman } from '@src/zip/DeflaterHuffman'; +import type { PendingBuffer } from '@src/zip/PendingBuffer'; /** * Low level compression engine for deflate algorithm which uses a 32K sliding window @@ -92,7 +92,6 @@ export class DeflaterEngine { */ private inputEnd: number = 0; - /** * Set if previous match exists */ @@ -151,9 +150,9 @@ export class DeflaterEngine { } } - private updateHash() { - this.insertHashIndex = (this.window[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ this.window[this.strstart + 1]; + this.insertHashIndex = + (this.window[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ this.window[this.strstart + 1]; } /** @@ -161,7 +160,7 @@ export class DeflaterEngine { * @returns Return true if input is needed via setInput */ public needsInput(): boolean { - return (this.inputEnd == this.inputOff); + return this.inputEnd === this.inputOff; } /** @@ -172,7 +171,7 @@ export class DeflaterEngine { * @param count The number of bytes of data to use as input. */ public setInput(buffer: Uint8Array, offset: number, count: number) { - let end = offset + count; + const end = offset + count; this.inputBuf = buffer; this.inputOff = offset; this.inputEnd = end; @@ -188,7 +187,7 @@ export class DeflaterEngine { let progress: boolean; do { this.fillWindow(); - let canFlush = flush && (this.inputOff == this.inputEnd); + const canFlush = flush && this.inputOff === this.inputEnd; progress = this.deflateSlow(canFlush, finish); } while (this.pending.isFlushed && progress); // repeat while we have no pending output and progress was made return progress; @@ -200,15 +199,14 @@ export class DeflaterEngine { } while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) { - if (this.lookahead == 0) { + if (this.lookahead === 0) { if (this.prevAvailable) { this.huffman.tallyLit(this.window[this.strstart - 1] & 0xff); } this.prevAvailable = false; // We are flushing everything - this.huffman.flushBlock(this.window, this.blockStart, this.strstart - this.blockStart, - finish); + this.huffman.flushBlock(this.window, this.blockStart, this.strstart - this.blockStart, finish); this.blockStart = this.strstart; return false; } @@ -221,26 +219,30 @@ export class DeflaterEngine { this.slideWindow(); } - let prevMatch = this.matchStart; + const prevMatch = this.matchStart; let prevLen = this.matchLen; if (this.lookahead >= DeflaterConstants.MIN_MATCH) { - let hashHead = this.insertString(); + const hashHead = this.insertString(); - if (hashHead != 0 && + if ( + hashHead !== 0 && this.strstart - hashHead <= DeflaterConstants.MAX_DIST && - this.findLongestMatch(hashHead)) { + this.findLongestMatch(hashHead) + ) { // longestMatch sets matchStart and matchLen // Discard match if too small and too far away - if (this.matchLen == DeflaterConstants.MIN_MATCH && this.strstart - this.matchStart > DeflaterEngine.TooFar) { + if ( + this.matchLen === DeflaterConstants.MIN_MATCH && + this.strstart - this.matchStart > DeflaterEngine.TooFar + ) { this.matchLen = DeflaterConstants.MIN_MATCH - 1; } } } // previous match was better - if ((prevLen >= DeflaterConstants.MIN_MATCH) && (this.matchLen <= prevLen)) { - + if (prevLen >= DeflaterConstants.MIN_MATCH && this.matchLen <= prevLen) { this.huffman.tallyDist(this.strstart - 1 - prevMatch, prevLen); prevLen -= 2; do { @@ -255,8 +257,7 @@ export class DeflaterEngine { this.lookahead--; this.prevAvailable = false; this.matchLen = DeflaterConstants.MIN_MATCH - 1; - } - else { + } else { if (this.prevAvailable) { this.huffman.tallyLit(this.window[this.strstart - 1] & 0xff); } @@ -270,7 +271,7 @@ export class DeflaterEngine { if (this.prevAvailable) { len--; } - let lastBlock = (finish && (this.lookahead == 0) && !this.prevAvailable); + const lastBlock = finish && this.lookahead === 0 && !this.prevAvailable; this.huffman.flushBlock(this.window, this.blockStart, len, lastBlock); this.blockStart += len; return !lastBlock; @@ -279,24 +280,23 @@ export class DeflaterEngine { return true; } - /** * Find the best (longest) string in the window matching the * string starting at strstart. - * @param curMatch + * @param curMatch * @returns True if a match greater than the minimum length is found */ private findLongestMatch(curMatch: number): boolean { let match: number; let scan = this.strstart; // scanMax is the highest position that we can look at - let scanMax = scan + Math.min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1; - let limit = Math.max(scan - DeflaterConstants.MAX_DIST, 0); + const scanMax = scan + Math.min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1; + const limit = Math.max(scan - DeflaterConstants.MAX_DIST, 0); - let window = this.window; - let prev = this.prev; + const window = this.window; + const prev = this.prev; let chainLength = this.maxChain; - let niceLength = Math.min(this.niceLength, this.lookahead); + const niceLength = Math.min(this.niceLength, this.lookahead); this.matchLen = Math.max(this.matchLen, DeflaterConstants.MIN_MATCH - 1); @@ -316,10 +316,12 @@ export class DeflaterEngine { match = curMatch; scan = this.strstart; - if (window[match + this.matchLen] != scan_end - || window[match + this.matchLen - 1] != scan_end1 - || window[match] != window[scan] - || window[++match] != window[++scan]) { + if ( + window[match + this.matchLen] !== scan_end || + window[match + this.matchLen - 1] !== scan_end1 || + window[match] !== window[scan] || + window[++match] !== window[++scan] + ) { continue; } @@ -330,76 +332,100 @@ export class DeflaterEngine { switch ((scanMax - scan) % 8) { case 1: - if (window[++scan] == window[++match]) break; + if (window[++scan] === window[++match]) { + break; + } break; case 2: - if (window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + if (window[++scan] === window[++match] && window[++scan] === window[++match]) { + break; + } break; case 3: - if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + if ( + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] + ) { + break; + } break; case 4: - if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + if ( + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] + ) { + break; + } break; case 5: - if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + if ( + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] + ) { + break; + } break; case 6: - if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + if ( + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] + ) { + break; + } break; case 7: - if (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]) break; + if ( + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] + ) { + break; + } break; } - if (window[scan] == window[match]) { + if (window[scan] === window[match]) { /* We check for insufficient lookahead only every 8th comparison; * the 256th check will be made at strstart + 258 unless lookahead is * exhausted first. */ do { - if (scan == scanMax) { - ++scan; // advance to first position not matched + if (scan === scanMax) { + ++scan; // advance to first position not matched ++match; break; } - } - while (window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match] - && window[++scan] == window[++match]); + } while ( + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] && + window[++scan] === window[++match] + ); } if (scan - this.strstart > this.matchLen) { @@ -413,23 +439,24 @@ export class DeflaterEngine { scan_end1 = window[scan - 1]; scan_end = window[scan]; } - curMatch = (prev[curMatch & DeflaterConstants.WMASK] & 0xffff); + curMatch = prev[curMatch & DeflaterConstants.WMASK] & 0xffff; } while (curMatch > limit && 0 !== --chainLength); return this.matchLen >= DeflaterConstants.MIN_MATCH; } - /** * Inserts the current string in the head hash and returns the previous * value for this hash. * @returns The previous hash value */ private insertString(): number { - let match: number; - let hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.window[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; + const hash = + ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ + this.window[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & + DeflaterConstants.HASH_MASK; - match = this.head[hash]; + const match = this.head[hash]; this.prev[this.strstart & DeflaterConstants.WMASK] = match; this.head[hash] = this.strstart; this.insertHashIndex = hash; @@ -457,7 +484,10 @@ export class DeflaterEngine { more = this.inputEnd - this.inputOff; } - this.window.set(this.inputBuf!.subarray(this.inputOff, this.inputOff + more), this.strstart + this.lookahead); + this.window.set( + this.inputBuf!.subarray(this.inputOff, this.inputOff + more), + this.strstart + this.lookahead + ); this.inputCrc.update(this.inputBuf!, this.inputOff, more); this.inputOff += more; @@ -471,7 +501,10 @@ export class DeflaterEngine { } private slideWindow() { - this.window.set(this.window.subarray(DeflaterConstants.WSIZE, DeflaterConstants.WSIZE + DeflaterConstants.WSIZE), 0); + this.window.set( + this.window.subarray(DeflaterConstants.WSIZE, DeflaterConstants.WSIZE + DeflaterConstants.WSIZE), + 0 + ); this.matchStart -= DeflaterConstants.WSIZE; this.strstart -= DeflaterConstants.WSIZE; this.blockStart -= DeflaterConstants.WSIZE; @@ -479,14 +512,14 @@ export class DeflaterEngine { // Slide the hash table (could be avoided with 32 bit values // at the expense of memory usage). for (let i = 0; i < DeflaterConstants.HASH_SIZE; ++i) { - let m = this.head[i] & 0xffff; - this.head[i] = (m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + const m = this.head[i] & 0xffff; + this.head[i] = m >= DeflaterConstants.WSIZE ? m - DeflaterConstants.WSIZE : 0; } // Slide the prev table. for (let i = 0; i < DeflaterConstants.WSIZE; i++) { - let m = this.prev[i] & 0xffff; - this.prev[i] = (m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + const m = this.prev[i] & 0xffff; + this.prev[i] = m >= DeflaterConstants.WSIZE ? m - DeflaterConstants.WSIZE : 0; } } -} \ No newline at end of file +} diff --git a/src/zip/DeflaterHuffman.ts b/src/zip/DeflaterHuffman.ts index 06c4d2344..f9c67c6b8 100644 --- a/src/zip/DeflaterHuffman.ts +++ b/src/zip/DeflaterHuffman.ts @@ -20,7 +20,7 @@ */ import { DeflaterConstants } from '@src/zip/DeflaterConstants'; -import { PendingBuffer } from '@src/zip/PendingBuffer'; +import type { PendingBuffer } from '@src/zip/PendingBuffer'; class Tree { // repeat previous bit length 3-6 times (2 bits of repeat count) @@ -65,7 +65,7 @@ class Tree { } public buildTree() { - let numSymbols = this.freqs.length; + const numSymbols = this.freqs.length; /* heap is a priority queue, sorted by frequency, least frequent * nodes first. The heap is a binary tree, with the property, that @@ -75,17 +75,17 @@ class Tree { * The binary tree is encoded in an array: 0 is root node and * the nodes 2*n+1, 2*n+2 are the child nodes of node n. */ - let heap = new Int32Array(numSymbols); + const heap = new Int32Array(numSymbols); let heapLen = 0; let maxCode = 0; for (let n = 0; n < numSymbols; n++) { - let freq = this.freqs[n]; + const freq = this.freqs[n]; if (freq !== 0) { // Insert n into heap let pos = heapLen++; while (true) { if (pos > 0) { - let ppos = Math.floor((pos - 1) / 2); + const ppos = Math.floor((pos - 1) / 2); if (this.freqs[heap[ppos]] > freq) { heap[pos] = heap[ppos]; pos = ppos; @@ -108,18 +108,18 @@ class Tree { * this case, both literals get a 1 bit code. */ while (heapLen < 2) { - let node = maxCode < 2 ? ++maxCode : 0; + const node = maxCode < 2 ? ++maxCode : 0; heap[heapLen++] = node; } this.numCodes = Math.max(maxCode + 1, this.minNumCodes); - let numLeafs = heapLen; - let childs = new Int32Array(4 * heapLen - 2); - let values = new Int32Array(2 * heapLen - 1); + const numLeafs = heapLen; + const childs = new Int32Array(4 * heapLen - 2); + const values = new Int32Array(2 * heapLen - 1); let numNodes = numLeafs; for (let i = 0; i < heapLen; i++) { - let node = heap[i]; + const node = heap[i]; childs[2 * i] = node; childs[2 * i + 1] = -1; values[i] = this.freqs[node] << 8; @@ -130,7 +130,7 @@ class Tree { * frequent nodes. */ do { - let first = heap[0]; + const first = heap[0]; let last = heap[--heapLen]; // Propagate the hole to the leafs of the heap @@ -167,13 +167,13 @@ class Tree { heap[path] = last; - let second = heap[0]; + const second = heap[0]; // Create a new node father of first and second last = numNodes++; childs[2 * last] = first; childs[2 * last + 1] = second; - let mindepth = Math.min(values[first] & 0xff, values[second] & 0xff); + const mindepth = Math.min(values[first] & 0xff, values[second] & 0xff); lastVal = values[first] + values[second] - mindepth + 1; values[last] = lastVal; @@ -213,8 +213,8 @@ class Tree { private buildLength(childs: Int32Array) { this.length = new Uint8Array(this.freqs.length); - let numNodes = Math.floor(childs.length / 2); - let numLeafs = Math.floor((numNodes + 1) / 2); + const numNodes = Math.floor(childs.length / 2); + const numLeafs = Math.floor((numNodes + 1) / 2); let overflow = 0; for (let i = 0; i < this.maxLength; i++) { @@ -222,11 +222,11 @@ class Tree { } // First calculate optimal bit lengths - let lengths = new Int32Array(numNodes); + const lengths = new Int32Array(numNodes); lengths[numNodes - 1] = 0; for (let i = numNodes - 1; i >= 0; i--) { - if (childs[2 * i + 1] != -1) { + if (childs[2 * i + 1] !== -1) { let bitLength = lengths[i] + 1; if (bitLength > this.maxLength) { bitLength = this.maxLength; @@ -236,20 +236,20 @@ class Tree { lengths[childs[2 * i + 1]] = bitLength; } else { // A leaf node - let bitLength = lengths[i]; + const bitLength = lengths[i]; this.bitLengthCounts[bitLength - 1]++; this.length[childs[2 * i]] = lengths[i]; } } - if (overflow == 0) { + if (overflow === 0) { return; } let incrBitLen = this.maxLength - 1; do { // Find the first bit length which could increase: - while (this.bitLengthCounts[--incrBitLen] == 0) {} + while (this.bitLengthCounts[--incrBitLen] === 0) {} // Move this node one down and remove a corresponding // number of overflow nodes. @@ -275,11 +275,11 @@ class Tree { * array. */ let nodePtr = 2 * numLeafs; - for (let bits = this.maxLength; bits != 0; bits--) { + for (let bits = this.maxLength; bits !== 0; bits--) { let n = this.bitLengthCounts[bits - 1]; while (n > 0) { - let childPtr = 2 * childs[nodePtr++]; - if (childs[childPtr + 1] == -1) { + const childPtr = 2 * childs[nodePtr++]; + if (childs[childPtr + 1] === -1) { // We found another leaf this.length[childs[childPtr]] = bits; n--; @@ -314,14 +314,14 @@ class Tree { let i = 0; while (i < this.numCodes) { count = 1; - let nextlen = this.length![i]; - if (nextlen == 0) { + const nextlen = this.length![i]; + if (nextlen === 0) { max_count = 138; min_count = 3; } else { max_count = 6; min_count = 3; - if (curlen != nextlen) { + if (curlen !== nextlen) { blTree.freqs[nextlen]++; count = 0; } @@ -329,7 +329,7 @@ class Tree { curlen = nextlen; i++; - while (i < this.numCodes && curlen == this.length![i]) { + while (i < this.numCodes && curlen === this.length![i]) { i++; if (++count >= max_count) { break; @@ -338,7 +338,7 @@ class Tree { if (count < min_count) { blTree.freqs[curlen] += count; - } else if (curlen != 0) { + } else if (curlen !== 0) { blTree.freqs[Tree.Repeat3To6]++; } else if (count <= 10) { blTree.freqs[Tree.Repeat3To10]++; @@ -362,7 +362,7 @@ class Tree { * Build dynamic codes and lengths */ public buildCodes() { - let nextCode = new Int32Array(this.maxLength); + const nextCode = new Int32Array(this.maxLength); let code = 0; this.codes = new Int16Array(this.freqs.length); @@ -373,7 +373,7 @@ class Tree { } for (let i = 0; i < this.numCodes; i++) { - let bits = this.length![i]; + const bits = this.length![i]; if (bits > 0) { this.codes[i] = DeflaterHuffman.bitReverse(nextCode[bits - 1]); nextCode[bits - 1] += 1 << (16 - bits); @@ -394,14 +394,14 @@ class Tree { let i = 0; while (i < this.numCodes) { count = 1; - let nextlen = this.length![i]; - if (nextlen == 0) { + const nextlen = this.length![i]; + if (nextlen === 0) { maxCount = 138; minCount = 3; } else { maxCount = 6; minCount = 3; - if (curlen != nextlen) { + if (curlen !== nextlen) { blTree.writeSymbol(nextlen); count = 0; } @@ -409,7 +409,7 @@ class Tree { curlen = nextlen; i++; - while (i < this.numCodes && curlen == this.length![i]) { + while (i < this.numCodes && curlen === this.length![i]) { i++; if (++count >= maxCount) { break; @@ -420,7 +420,7 @@ class Tree { while (count-- > 0) { blTree.writeSymbol(curlen); } - } else if (curlen != 0) { + } else if (curlen !== 0) { blTree.writeSymbol(Tree.Repeat3To6); this.huffman.pending.writeBits(count - 3, 2); } else if (count <= 10) { @@ -502,22 +502,7 @@ export class DeflaterHuffman { private static readonly BL_ORDER: number[] = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; private static readonly bit4Reverse: Uint8Array = new Uint8Array([ - 0, - 8, - 4, - 12, - 2, - 10, - 6, - 14, - 1, - 9, - 5, - 13, - 3, - 11, - 7, - 15 + 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 ]); /** @@ -630,7 +615,7 @@ export class DeflaterHuffman { if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) { // Store Block this.flushStoredBlock(stored, storedOffset, storedLength, lastBlock); - } else if (opt_len == static_len) { + } else if (opt_len === static_len) { // Encode with static tree this.pending.writeBits((DeflaterHuffman.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); this.literalTree.setStaticCodes(DeflaterHuffman.staticLCodes, DeflaterHuffman.staticLLength); @@ -669,10 +654,10 @@ export class DeflaterHuffman { */ public compressBlock() { for (let i = 0; i < this.last_lit; i++) { - let litlen = this.l_buf[i] & 0xff; + const litlen = this.l_buf[i] & 0xff; let dist = this.d_buf[i]; - if (dist-- != 0) { - let lc = DeflaterHuffman.Lcode(litlen); + if (dist-- !== 0) { + const lc = DeflaterHuffman.Lcode(litlen); this.literalTree.writeSymbol(lc); let bits = Math.floor((lc - 261) / 4); @@ -680,7 +665,7 @@ export class DeflaterHuffman { this.pending.writeBits(litlen & ((1 << bits) - 1), bits); } - let dc = DeflaterHuffman.Dcode(dist); + const dc = DeflaterHuffman.Dcode(dist); this.distTree.writeSymbol(dc); bits = Math.floor(dc / 2) - 1; @@ -705,13 +690,13 @@ export class DeflaterHuffman { this.d_buf[this.last_lit] = distance; this.l_buf[this.last_lit++] = length - 3; - let lc = DeflaterHuffman.Lcode(length - 3); + const lc = DeflaterHuffman.Lcode(length - 3); this.literalTree.freqs[lc]++; if (lc >= 265 && lc < 285) { this.extra_bits += Math.floor((lc - 261) / 4); } - let dc = DeflaterHuffman.Dcode(distance - 1); + const dc = DeflaterHuffman.Dcode(distance - 1); this.distTree.freqs[dc]++; if (dc >= 4) { this.extra_bits += Math.floor(dc / 2) - 1; @@ -732,7 +717,7 @@ export class DeflaterHuffman { } private static Lcode(length: number): number { - if (length == 255) { + if (length === 255) { return 285; } diff --git a/src/zip/HuffTools.ts b/src/zip/HuffTools.ts index cbf091055..45c476c7a 100644 --- a/src/zip/HuffTools.ts +++ b/src/zip/HuffTools.ts @@ -21,13 +21,13 @@ import { FormatError } from '@src/FormatError'; * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ -import { Found, Huffman, NeedBit, NeedBits } from '@src/zip/Huffman'; +import { Found, type Huffman, NeedBit, NeedBits } from '@src/zip/Huffman'; // This Inflater is based on the Zip Reader of the Haxe Standard Library (MIT) export class HuffTools { public static make(lengths: number[], pos: number, nlengths: number, maxbits: number): Huffman { - let counts: number[] = []; - let tmp: number[] = []; + const counts: number[] = []; + const tmp: number[] = []; if (maxbits > 32) { throw new FormatError('Invalid huffman'); } @@ -36,7 +36,7 @@ export class HuffTools { tmp.push(0); } for (let i: number = 0; i < nlengths; i++) { - let p: number = lengths[i + pos]; + const p: number = lengths[i + pos]; if (p >= maxbits) { throw new FormatError('Invalid huffman'); } @@ -47,11 +47,11 @@ export class HuffTools { code = (code + counts[i]) << 1; tmp[i] = code; } - let bits: Map = new Map(); + const bits: Map = new Map(); for (let i: number = 0; i < nlengths; i++) { - let l: number = lengths[i + pos]; + const l: number = lengths[i + pos]; if (l !== 0) { - let n: number = tmp[l - 1]; + const n: number = tmp[l - 1]; tmp[l - 1] = n + 1; bits.set((n << 5) | l, i); } @@ -65,7 +65,7 @@ export class HuffTools { if (len > maxbits) { throw new FormatError('Invalid huffman'); } - let idx: number = (v << 5) | len; + const idx: number = (v << 5) | len; if (bits.has(idx)) { return new Found(bits.get(idx)!); } @@ -75,19 +75,18 @@ export class HuffTools { } private static treeCompress(t: Huffman): Huffman { - let d: number = HuffTools.treeDepth(t); + const d: number = HuffTools.treeDepth(t); if (d === 0) { return t; } if (d === 1) { if (t instanceof NeedBit) { return new NeedBit(HuffTools.treeCompress(t.left), HuffTools.treeCompress(t.right)); - } else { - throw new FormatError('assert'); } + throw new FormatError('assert'); } - let size: number = 1 << d; - let table: Huffman[] = []; + const size: number = 1 << d; + const table: Huffman[] = []; for (let i: number = 0; i < size; i++) { table.push(new Found(-1)); } @@ -116,8 +115,8 @@ export class HuffTools { throw new FormatError('assert'); } if (t instanceof NeedBit) { - let da: number = HuffTools.treeDepth(t.left); - let db: number = HuffTools.treeDepth(t.right); + const da: number = HuffTools.treeDepth(t.left); + const db: number = HuffTools.treeDepth(t.right); return 1 + (da < db ? da : db); } return 0; diff --git a/src/zip/Inflate.ts b/src/zip/Inflate.ts index e5cf59725..4c2199724 100644 --- a/src/zip/Inflate.ts +++ b/src/zip/Inflate.ts @@ -22,24 +22,24 @@ */ import { FormatError } from '@src/FormatError'; import { IOHelper } from '@src/io/IOHelper'; -import { IReadable } from '@src/io/IReadable'; +import type { IReadable } from '@src/io/IReadable'; import { Found as HuffmanFound, - Huffman, + type Huffman, NeedBit as HuffmanNeedBit, NeedBits as HuffmanNeedBits } from '@src/zip/Huffman'; import { HuffTools } from '@src/zip/HuffTools'; enum InflateState { - Head, - Block, - CData, - Flat, - Crc, - Dist, - DistOne, - Done + Head = 0, + Block = 1, + CData = 2, + Flat = 3, + Crc = 4, + Dist = 5, + DistOne = 6, + Done = 7 } class InflateWindow { @@ -50,7 +50,7 @@ class InflateWindow { public pos: number = 0; public slide(): void { - let b: Uint8Array = new Uint8Array(InflateWindow.BufferSize); + const b: Uint8Array = new Uint8Array(InflateWindow.BufferSize); this.pos -= InflateWindow.Size; b.set(this.buffer.subarray(InflateWindow.Size, InflateWindow.Size + this.pos), 0); this.buffer = b; @@ -84,33 +84,29 @@ class InflateWindow { export class Inflate { // prettier-ignore private static LenExtraBitsTbl: number[] = [ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, -1, - -1 + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, -1, -1 ]; // prettier-ignore private static LenBaseValTbl: number[] = [ - 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, - 131, 163, 195, 227, 258 + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, + 258 ]; // prettier-ignore private static DistExtraBitsTbl: number[] = [ - 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, - 13, 13, -1, -1 + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, -1, -1 ]; // prettier-ignore private static DistBaseValTbl: number[] = [ - 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, - 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577 + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, + 6145, 8193, 12289, 16385, 24577 ]; // prettier-ignore - private static CodeLengthsPos: number[] = [ - 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 - ]; + private static CodeLengthsPos: number[] = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; private static _fixedHuffman: Huffman = Inflate.buildFixedHuffman(); private static buildFixedHuffman(): Huffman { - let a: number[] = []; + const a: number[] = []; for (let n: number = 0; n < 288; n++) { a.push(n <= 143 ? 8 : n <= 255 ? 9 : n <= 279 ? 7 : 8); } @@ -154,14 +150,14 @@ export class Inflate { private inflateLoop(): boolean { switch (this._state) { case InflateState.Head: - let cmf: number = this._input.readByte(); - let cm: number = cmf & 15; + const cmf: number = this._input.readByte(); + const cm: number = cmf & 15; if (cm !== 8) { throw new FormatError('Invalid data'); } - let flg: number = this._input.readByte(); + const flg: number = this._input.readByte(); // var fcheck = flg & 31; - let fdict: boolean = (flg & 32) !== 0; + const fdict: boolean = (flg & 32) !== 0; // var flevel = flg >> 6; if (((cmf << 8) + flg) % 31 !== 0) { throw new FormatError('Invalid data'); @@ -182,12 +178,12 @@ export class Inflate { switch (this.getBits(2)) { case 0: this._len = IOHelper.readUInt16LE(this._input); - let nlen: number = IOHelper.readUInt16LE(this._input); + const nlen: number = IOHelper.readUInt16LE(this._input); if (nlen !== 0xffff - this._len) { throw new FormatError('Invalid data'); } this._state = InflateState.Flat; - let r: boolean = this.inflateLoop(); + const r: boolean = this.inflateLoop(); this.resetBits(); return r; case 1: @@ -196,9 +192,9 @@ export class Inflate { this._state = InflateState.CData; return true; case 2: - let hlit: number = this.getBits(5) + 257; - let hdist: number = this.getBits(5) + 1; - let hclen: number = this.getBits(4) + 4; + const hlit: number = this.getBits(5) + 257; + const hdist: number = this.getBits(5) + 1; + const hclen: number = this.getBits(4) + 4; for (let i: number = 0; i < hclen; i++) { this._lengths[Inflate.CodeLengthsPos[i]] = this.getBits(3); } @@ -206,7 +202,7 @@ export class Inflate { this._lengths[Inflate.CodeLengthsPos[i]] = 0; } this._huffman = HuffTools.make(this._lengths, 0, 19, 8); - let xlengths: number[] = []; + const xlengths: number[] = []; for (let i: number = 0; i < hlit + hdist; i++) { xlengths.push(0); } @@ -219,15 +215,17 @@ export class Inflate { throw new FormatError('Invalid data'); } case InflateState.Flat: { - let rlen: number = this._len < this._needed ? this._len : this._needed; - let bytes: Uint8Array = IOHelper.readByteArray(this._input, rlen); + const rlen: number = this._len < this._needed ? this._len : this._needed; + const bytes: Uint8Array = IOHelper.readByteArray(this._input, rlen); this._len -= rlen; this.addBytes(bytes, 0, rlen); - if (this._len === 0) this._state = this._isFinal ? InflateState.Crc : InflateState.Block; + if (this._len === 0) { + this._state = this._isFinal ? InflateState.Crc : InflateState.Block; + } return this._needed > 0; } case InflateState.DistOne: { - let rlen: number = this._len < this._needed ? this._len : this._needed; + const rlen: number = this._len < this._needed ? this._len : this._needed; this.addDistOne(rlen); this._len -= rlen; if (this._len === 0) { @@ -237,8 +235,8 @@ export class Inflate { } case InflateState.Dist: while (this._len > 0 && this._needed > 0) { - let rdist: number = this._len < this._dist ? this._len : this._dist; - let rlen: number = this._needed < rdist ? this._needed : rdist; + const rdist: number = this._len < this._dist ? this._len : this._dist; + const rlen: number = this._needed < rdist ? this._needed : rdist; this.addDist(this._dist, rlen); this._len -= rlen; } @@ -251,35 +249,36 @@ export class Inflate { if (n < 256) { this.addByte(n); return this._needed > 0; - } else if (n === 256) { + } + + if (n === 256) { this._state = this._isFinal ? InflateState.Crc : InflateState.Block; return true; - } else { - n = (n - 257) & 0xff; - let extraBits: number = Inflate.LenExtraBitsTbl[n]; - if (extraBits === -1) { - throw new FormatError('Invalid data'); - } - this._len = Inflate.LenBaseValTbl[n] + this.getBits(extraBits); - let huffdist: Huffman | null = this._huffdist; - let distCode: number = !huffdist ? this.getRevBits(5) : this.applyHuffman(huffdist); - extraBits = Inflate.DistExtraBitsTbl[distCode]; - if (extraBits === -1) { - throw new FormatError('Invalid data'); - } - this._dist = Inflate.DistBaseValTbl[distCode] + this.getBits(extraBits); - if (this._dist > this._window.available()) { - throw new FormatError('Invalid data'); - } - this._state = this._dist === 1 ? InflateState.DistOne : InflateState.Dist; - return true; } + n = (n - 257) & 0xff; + let extraBits: number = Inflate.LenExtraBitsTbl[n]; + if (extraBits === -1) { + throw new FormatError('Invalid data'); + } + this._len = Inflate.LenBaseValTbl[n] + this.getBits(extraBits); + const huffdist: Huffman | null = this._huffdist; + const distCode: number = !huffdist ? this.getRevBits(5) : this.applyHuffman(huffdist); + extraBits = Inflate.DistExtraBitsTbl[distCode]; + if (extraBits === -1) { + throw new FormatError('Invalid data'); + } + this._dist = Inflate.DistBaseValTbl[distCode] + this.getBits(extraBits); + if (this._dist > this._window.available()) { + throw new FormatError('Invalid data'); + } + this._state = this._dist === 1 ? InflateState.DistOne : InflateState.Dist; + return true; } return false; } private addDistOne(n: number): void { - let c: number = this._window.getLastChar(); + const c: number = this._window.getLastChar(); for (let i: number = 0; i < n; i++) { this.addByte(c); } @@ -301,7 +300,7 @@ export class Inflate { this._nbits = 8; this._bits = this._input.readByte(); } - let b: boolean = (this._bits & 1) === 1; + const b: boolean = (this._bits & 1) === 1; this._nbits--; this._bits = this._bits >> 1; return b; @@ -312,7 +311,7 @@ export class Inflate { this._bits = this._bits | (this._input.readByte() << this._nbits); this._nbits += 8; } - let b: number = this._bits & ((1 << n) - 1); + const b: number = this._bits & ((1 << n) - 1); this._nbits -= n; this._bits = this._bits >> n; return b; @@ -338,7 +337,7 @@ export class Inflate { let i: number = 0; let prev: number = 0; while (i < max) { - let n: number = this.applyHuffman(this._huffman); + const n: number = this.applyHuffman(this._huffman); switch (n) { case 0: case 1: @@ -361,7 +360,7 @@ export class Inflate { i++; break; case 16: - let end: number = i + 3 + this.getBits(2); + const end: number = i + 3 + this.getBits(2); if (end > max) { throw new FormatError('Invalid data'); } diff --git a/src/zip/PendingBuffer.ts b/src/zip/PendingBuffer.ts index 52cc55d6c..cac6a4740 100644 --- a/src/zip/PendingBuffer.ts +++ b/src/zip/PendingBuffer.ts @@ -7,10 +7,10 @@ * without restriction, including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons * to whom the Software is furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE @@ -64,8 +64,8 @@ export class PendingBuffer { * @param s value to write */ public writeShortMSB(s: number) { - this._buffer[this._end++] = (s >> 8) & 0xFF; - this._buffer[this._end++] = s & 0xFF; + this._buffer[this._end++] = (s >> 8) & 0xff; + this._buffer[this._end++] = s & 0xff; } /** @@ -74,10 +74,9 @@ export class PendingBuffer { */ public writeShort(value: number) { this._buffer[this._end++] = value; - this._buffer[this._end++] = (value >> 8); + this._buffer[this._end++] = value >> 8; } - /** * Write a block of data to buffer * @param block data to write @@ -99,7 +98,7 @@ export class PendingBuffer { */ public flush(output: Uint8Array, offset: number, length: number) { if (this.bitCount >= 8) { - this._buffer[this._end++] = this._bits & 0xFF; + this._buffer[this._end++] = this._bits & 0xff; this._bits >>= 8; this.bitCount -= 8; } @@ -109,8 +108,7 @@ export class PendingBuffer { output.set(this._buffer.subarray(this._start, this._start + length), offset); this._start = 0; this._end = 0; - } - else { + } else { output.set(this._buffer.subarray(this._start, this._start + length), offset); this._start += length; } @@ -126,8 +124,8 @@ export class PendingBuffer { this._bits |= b << this.bitCount; this.bitCount += count; if (this.bitCount >= 16) { - this._buffer[this._end++] = this._bits & 0xFF; - this._buffer[this._end++] = (this._bits >> 8) & 0xFF; + this._buffer[this._end++] = this._bits & 0xff; + this._buffer[this._end++] = (this._bits >> 8) & 0xff; this._bits >>= 16; this.bitCount -= 16; } @@ -138,9 +136,9 @@ export class PendingBuffer { */ public alignToByte() { if (this.bitCount > 0) { - this._buffer[this._end++] = this._bits & 0xFF; + this._buffer[this._end++] = this._bits & 0xff; if (this.bitCount > 8) { - this._buffer[this._end++] = (this._bits >> 8) & 0xFF; + this._buffer[this._end++] = (this._bits >> 8) & 0xff; } } this._bits = 0; diff --git a/src/zip/ZipEntry.ts b/src/zip/ZipEntry.ts index b5f42d0a8..3b0f5624d 100644 --- a/src/zip/ZipEntry.ts +++ b/src/zip/ZipEntry.ts @@ -1,4 +1,3 @@ - export class ZipEntry { public static readonly OptionalDataDescriptorSignature: number = 0x08074b50; public static readonly CompressionMethodDeflate: number = 8; @@ -6,14 +5,13 @@ export class ZipEntry { public static readonly CentralFileHeaderSignature: number = 0x02014b50; public static readonly EndOfCentralDirSignature: number = 0x06054b50; - public readonly fullName: string; public readonly fileName: string; public readonly data: Uint8Array; public constructor(fullName: string, data: Uint8Array) { this.fullName = fullName; - let i: number = fullName.lastIndexOf('/'); + const i: number = fullName.lastIndexOf('/'); this.fileName = i === -1 || i === fullName.length - 1 ? this.fullName : fullName.substr(i + 1); this.data = data; } diff --git a/src/zip/ZipReader.ts b/src/zip/ZipReader.ts index 2482c8080..ac066d82d 100644 --- a/src/zip/ZipReader.ts +++ b/src/zip/ZipReader.ts @@ -1,11 +1,10 @@ import { ByteBuffer } from '@src/io/ByteBuffer'; import { IOHelper } from '@src/io/IOHelper'; -import { IReadable } from '@src/io/IReadable'; +import type { IReadable } from '@src/io/IReadable'; import { Inflate } from '@src/zip/Inflate'; import { ZipEntry } from '@src/zip/ZipEntry'; export class ZipReader { - private _readable: IReadable; public constructor(readable: IReadable) { @@ -13,9 +12,9 @@ export class ZipReader { } public read(): ZipEntry[] { - let entries: ZipEntry[] = []; + const entries: ZipEntry[] = []; while (true) { - let e: ZipEntry | null = this.readEntry(); + const e: ZipEntry | null = this.readEntry(); if (!e) { break; } @@ -25,17 +24,17 @@ export class ZipReader { } private readEntry(): ZipEntry | null { - let readable: IReadable = this._readable; - let h: number = IOHelper.readInt32LE(readable); + const readable: IReadable = this._readable; + const h: number = IOHelper.readInt32LE(readable); if (h !== ZipEntry.LocalFileHeaderSignature) { return null; } // 4.3.7 local file header IOHelper.readUInt16LE(readable); // version - let flags: number = IOHelper.readUInt16LE(readable); - let compressionMethod: number = IOHelper.readUInt16LE(readable); - let compressed: boolean = compressionMethod !== 0; + const flags: number = IOHelper.readUInt16LE(readable); + const compressionMethod: number = IOHelper.readUInt16LE(readable); + const compressed: boolean = compressionMethod !== 0; if (compressed && compressionMethod !== ZipEntry.CompressionMethodDeflate) { return null; } @@ -45,20 +44,20 @@ export class ZipReader { IOHelper.readInt32LE(readable); // crc-32 IOHelper.readInt32LE(readable); // compressed size - let uncompressedSize: number = IOHelper.readInt32LE(readable); - let fileNameLength: number = IOHelper.readInt16LE(readable); - let extraFieldLength: number = IOHelper.readInt16LE(readable); - let fname: string = IOHelper.toString(IOHelper.readByteArray(readable, fileNameLength), 'utf-8'); + const uncompressedSize: number = IOHelper.readInt32LE(readable); + const fileNameLength: number = IOHelper.readInt16LE(readable); + const extraFieldLength: number = IOHelper.readInt16LE(readable); + const fname: string = IOHelper.toString(IOHelper.readByteArray(readable, fileNameLength), 'utf-8'); readable.skip(extraFieldLength); // 4.3.8 File Data let data: Uint8Array; if (compressed) { - let target: ByteBuffer = ByteBuffer.empty(); - let z: Inflate = new Inflate(this._readable); - let buffer: Uint8Array = new Uint8Array(65536); + const target: ByteBuffer = ByteBuffer.empty(); + const z: Inflate = new Inflate(this._readable); + const buffer: Uint8Array = new Uint8Array(65536); while (true) { - let bytes: number = z.readBytes(buffer, 0, buffer.length); + const bytes: number = z.readBytes(buffer, 0, buffer.length); target.write(buffer, 0, bytes); if (bytes < buffer.length) { break; @@ -72,7 +71,7 @@ export class ZipReader { // 4.3.9 Data Descriptor // 4.3.9.1 if ((flags & 8) !== 0) { - let crc32: number = IOHelper.readInt32LE(this._readable); + const crc32: number = IOHelper.readInt32LE(this._readable); // 4.3.9.3 if (crc32 === ZipEntry.OptionalDataDescriptorSignature) { IOHelper.readInt32LE(this._readable); // real crc diff --git a/src/zip/ZipWriter.ts b/src/zip/ZipWriter.ts index 93a287062..e82200d23 100644 --- a/src/zip/ZipWriter.ts +++ b/src/zip/ZipWriter.ts @@ -1,6 +1,6 @@ import { ByteBuffer } from '@src/io/ByteBuffer'; import { IOHelper } from '@src/io/IOHelper'; -import { IWriteable } from '@src/io/IWriteable'; +import type { IWriteable } from '@src/io/IWriteable'; import { Crc32 } from '@src/zip/Crc32'; import { Deflater } from '@src/zip/Deflater'; import { ZipEntry } from '@src/zip/ZipEntry'; @@ -12,11 +12,13 @@ class ZipCentralDirectoryHeader { public crc32: number; public compressionMode: number; - public constructor(entry: ZipEntry, + public constructor( + entry: ZipEntry, crc32: number, localHeaderOffset: number, compressionMode: number, - compressedSize: number) { + compressedSize: number + ) { this.entry = entry; this.crc32 = crc32; this.localHeaderOffset = localHeaderOffset; @@ -41,7 +43,13 @@ export class ZipWriter { const compressedData = ByteBuffer.empty(); const crc32 = this.compress(compressedData, entry.data, compressionMode); const compressedDataArray = compressedData.toArray(); - const directoryHeader = new ZipCentralDirectoryHeader(entry, crc32, this._data.bytesWritten, compressionMode, compressedData.length); + const directoryHeader = new ZipCentralDirectoryHeader( + entry, + crc32, + this._data.bytesWritten, + compressionMode, + compressedData.length + ); this._centralDirectoryHeaders.push(directoryHeader); // Signature @@ -77,41 +85,40 @@ export class ZipWriter { } private compress(output: IWriteable, data: Uint8Array, compressionMode: number): number { - if (compressionMode != ZipEntry.CompressionMethodDeflate) { + if (compressionMode !== ZipEntry.CompressionMethodDeflate) { const crc = new Crc32(); crc.update(data, 0, data.length); output.write(data, 0, data.length); return crc.value; - } else { - let buffer: Uint8Array = new Uint8Array(512); - - // init deflater - this._deflater.reset(); + } + const buffer: Uint8Array = new Uint8Array(512); - // write data - this._deflater.setInput(data, 0, data.length); - while (!this._deflater.isNeedingInput) { - const len = this._deflater.deflate(buffer, 0, buffer.length); - if (len <= 0) { - break; - } + // init deflater + this._deflater.reset(); - output.write(buffer, 0, len); + // write data + this._deflater.setInput(data, 0, data.length); + while (!this._deflater.isNeedingInput) { + const len = this._deflater.deflate(buffer, 0, buffer.length); + if (len <= 0) { + break; } - // let deflater finish up - this._deflater.finish(); - while (!this._deflater.isFinished) { - const len = this._deflater.deflate(buffer, 0, buffer.length); - if (len <= 0) { - break; - } + output.write(buffer, 0, len); + } - output.write(buffer, 0, len); + // let deflater finish up + this._deflater.finish(); + while (!this._deflater.isFinished) { + const len = this._deflater.deflate(buffer, 0, buffer.length); + if (len <= 0) { + break; } - return this._deflater.inputCrc; + output.write(buffer, 0, len); } + + return this._deflater.inputCrc; } public end() { diff --git a/test-data/guitarpro5/header-footer.gp5 b/test-data/guitarpro5/header-footer.gp5 new file mode 100644 index 000000000..7bd5d93a2 Binary files /dev/null and b/test-data/guitarpro5/header-footer.gp5 differ diff --git a/test-data/guitarpro8/faulty.gp b/test-data/guitarpro8/faulty.gp new file mode 100644 index 000000000..a8719686b Binary files /dev/null and b/test-data/guitarpro8/faulty.gp differ diff --git a/test-data/guitarpro8/header-footer.gp b/test-data/guitarpro8/header-footer.gp new file mode 100644 index 000000000..63a20a320 Binary files /dev/null and b/test-data/guitarpro8/header-footer.gp differ diff --git a/test-data/guitarpro8/multibar-rest.gp b/test-data/guitarpro8/multibar-rest.gp new file mode 100644 index 000000000..5451dc3a1 Binary files /dev/null and b/test-data/guitarpro8/multibar-rest.gp differ diff --git a/test-data/musicxml-samples/BeetAnGeSample.png b/test-data/musicxml-samples/BeetAnGeSample.png new file mode 100644 index 000000000..73da59255 Binary files /dev/null and b/test-data/musicxml-samples/BeetAnGeSample.png differ diff --git a/test-data/musicxml-samples/Binchois.png b/test-data/musicxml-samples/Binchois.png index a1ed7bd25..935d9b4ea 100644 Binary files a/test-data/musicxml-samples/Binchois.png and b/test-data/musicxml-samples/Binchois.png differ diff --git a/test-data/musicxml-samples/BrahWiMeSample.png b/test-data/musicxml-samples/BrahWiMeSample.png new file mode 100644 index 000000000..3ca899a2e Binary files /dev/null and b/test-data/musicxml-samples/BrahWiMeSample.png differ diff --git a/test-data/musicxml-samples/BrookeWestSample.png b/test-data/musicxml-samples/BrookeWestSample.png new file mode 100644 index 000000000..71d3541e3 Binary files /dev/null and b/test-data/musicxml-samples/BrookeWestSample.png differ diff --git a/test-data/musicxml-samples/Chant.png b/test-data/musicxml-samples/Chant.png index 056c2cf00..3d6d6c049 100644 Binary files a/test-data/musicxml-samples/Chant.png and b/test-data/musicxml-samples/Chant.png differ diff --git a/test-data/musicxml-samples/DebuMandSample.png b/test-data/musicxml-samples/DebuMandSample.png new file mode 100644 index 000000000..1f9e82169 Binary files /dev/null and b/test-data/musicxml-samples/DebuMandSample.png differ diff --git a/test-data/musicxml-samples/Dichterliebe01.png b/test-data/musicxml-samples/Dichterliebe01.png new file mode 100644 index 000000000..4e32cf091 Binary files /dev/null and b/test-data/musicxml-samples/Dichterliebe01.png differ diff --git a/test-data/musicxml-samples/Echigo.png b/test-data/musicxml-samples/Echigo.png new file mode 100644 index 000000000..2084bd5a2 Binary files /dev/null and b/test-data/musicxml-samples/Echigo.png differ diff --git a/test-data/musicxml-samples/FaurReveSample.png b/test-data/musicxml-samples/FaurReveSample.png new file mode 100644 index 000000000..500fb8621 Binary files /dev/null and b/test-data/musicxml-samples/FaurReveSample.png differ diff --git a/test-data/musicxml-samples/MahlFaGe4Sample.png b/test-data/musicxml-samples/MahlFaGe4Sample.png new file mode 100644 index 000000000..1c2585eca Binary files /dev/null and b/test-data/musicxml-samples/MahlFaGe4Sample.png differ diff --git a/test-data/musicxml-samples/MozaChloSample.png b/test-data/musicxml-samples/MozaChloSample.png new file mode 100644 index 000000000..32ff3f7d4 Binary files /dev/null and b/test-data/musicxml-samples/MozaChloSample.png differ diff --git a/test-data/musicxml-samples/MozaVeilSample.png b/test-data/musicxml-samples/MozaVeilSample.png new file mode 100644 index 000000000..72bd5723b Binary files /dev/null and b/test-data/musicxml-samples/MozaVeilSample.png differ diff --git a/test-data/musicxml-samples/MozartPianoSonata.png b/test-data/musicxml-samples/MozartPianoSonata.png index 2d8d8485d..c7f4f59ce 100644 Binary files a/test-data/musicxml-samples/MozartPianoSonata.png and b/test-data/musicxml-samples/MozartPianoSonata.png differ diff --git a/test-data/musicxml-samples/MozartTrio.png b/test-data/musicxml-samples/MozartTrio.png index 714e99543..f0443972f 100644 Binary files a/test-data/musicxml-samples/MozartTrio.png and b/test-data/musicxml-samples/MozartTrio.png differ diff --git a/test-data/musicxml-samples/Saltarello.png b/test-data/musicxml-samples/Saltarello.png index 25370ab39..5a20832a9 100644 Binary files a/test-data/musicxml-samples/Saltarello.png and b/test-data/musicxml-samples/Saltarello.png differ diff --git a/test-data/musicxml-samples/SchbAvMaSample.png b/test-data/musicxml-samples/SchbAvMaSample.png new file mode 100644 index 000000000..6aef37f7f Binary files /dev/null and b/test-data/musicxml-samples/SchbAvMaSample.png differ diff --git a/test-data/musicxml-samples/Telemann.png b/test-data/musicxml-samples/Telemann.png index 119262e0c..f84dbd2db 100644 Binary files a/test-data/musicxml-samples/Telemann.png and b/test-data/musicxml-samples/Telemann.png differ diff --git a/test-data/musicxml-testsuite/01a-Pitches-Pitches.png b/test-data/musicxml-testsuite/01a-Pitches-Pitches.png new file mode 100644 index 000000000..bed1a85b0 Binary files /dev/null and b/test-data/musicxml-testsuite/01a-Pitches-Pitches.png differ diff --git a/test-data/musicxml-testsuite/01a-Pitches-Pitches.xml b/test-data/musicxml-testsuite/01a-Pitches-Pitches.xml index 177f2c89d..365866a50 100644 --- a/test-data/musicxml-testsuite/01a-Pitches-Pitches.xml +++ b/test-data/musicxml-testsuite/01a-Pitches-Pitches.xml @@ -1,14 +1,15 @@ - - + Pitches and accidentals - All pitches from G to c'''' in - ascending steps; First without accidentals, then with a sharp and then - with a flat accidental. Double alterations and cautionary accidentals - are tested at the end. + All pitches from G to c’’’’ in + ascending steps. First without accidentals, then with a sharp and + then with a flat accidental, then with explicit natural accidentals. + Double alterations and cautionary accidentals are tested at the + end. @@ -1097,7 +1098,93 @@ - + + + + E + 4 + + 1 + 1 + quarter + natural + + + + F + 4 + + 1 + 1 + quarter + natural + + + + G + 4 + + 1 + 1 + quarter + natural + + + + A + 4 + + 1 + 1 + quarter + natural + + + + + + + B + 4 + + 1 + 1 + quarter + natural + + + + C + 5 + + 1 + 1 + quarter + natural + + + + D + 5 + + 1 + 1 + quarter + natural + + + + E + 5 + + 1 + 1 + quarter + natural + + + + C @@ -1144,7 +1231,7 @@ - + C diff --git a/test-data/musicxml-testsuite/01b-Pitches-Intervals.png b/test-data/musicxml-testsuite/01b-Pitches-Intervals.png new file mode 100644 index 000000000..e629197da Binary files /dev/null and b/test-data/musicxml-testsuite/01b-Pitches-Intervals.png differ diff --git a/test-data/musicxml-testsuite/01b-Pitches-Intervals.xml b/test-data/musicxml-testsuite/01b-Pitches-Intervals.xml index c033498d5..6ee2c003e 100644 --- a/test-data/musicxml-testsuite/01b-Pitches-Intervals.xml +++ b/test-data/musicxml-testsuite/01b-Pitches-Intervals.xml @@ -1,11 +1,11 @@ - - - Various piches and interval sizes + + Various pitches and interval sizes - All pitch intervals in ascending + All pitch intervals in ascending jump size. @@ -50,6 +50,8 @@ 1 quarter + + C @@ -59,6 +61,7 @@ 1 1 quarter + sharp @@ -69,7 +72,10 @@ 1 1 quarter + flat + + D @@ -79,6 +85,7 @@ 1 1 quarter + flat @@ -89,7 +96,10 @@ 1 1 quarter + sharp + + D @@ -108,6 +118,8 @@ 1 quarter + + D @@ -117,6 +129,7 @@ 1 1 quarter + sharp @@ -127,7 +140,10 @@ 1 1 quarter + flat + + E @@ -137,6 +153,7 @@ 1 1 quarter + flat @@ -147,7 +164,10 @@ 1 1 quarter + sharp + + E @@ -166,6 +186,8 @@ 1 quarter + + E @@ -175,6 +197,7 @@ 1 1 quarter + sharp @@ -185,7 +208,10 @@ 1 1 quarter + flat + + F @@ -195,6 +221,7 @@ 1 1 quarter + flat @@ -205,7 +232,10 @@ 1 1 quarter + sharp + + F @@ -224,6 +254,8 @@ 1 quarter + + F @@ -233,6 +265,7 @@ 1 1 quarter + sharp @@ -243,7 +276,10 @@ 1 1 quarter + flat + + G @@ -253,6 +289,7 @@ 1 1 quarter + flat @@ -263,7 +300,10 @@ 1 1 quarter + sharp + + G @@ -282,6 +322,8 @@ 1 quarter + + G @@ -291,6 +333,7 @@ 1 1 quarter + sharp @@ -301,7 +344,10 @@ 1 1 quarter + flat + + A @@ -311,6 +357,7 @@ 1 1 quarter + flat @@ -321,7 +368,10 @@ 1 1 quarter + sharp + + A @@ -340,6 +390,8 @@ 1 quarter + + A @@ -349,6 +401,7 @@ 1 1 quarter + sharp @@ -359,7 +412,10 @@ 1 1 quarter + flat + + B @@ -369,6 +425,7 @@ 1 1 quarter + flat @@ -379,7 +436,10 @@ 1 1 quarter + sharp + + B @@ -398,6 +458,8 @@ 1 quarter + + B @@ -407,6 +469,7 @@ 1 1 quarter + sharp @@ -417,7 +480,10 @@ 1 1 quarter + flat + + C @@ -427,6 +493,7 @@ 1 1 quarter + flat @@ -437,7 +504,10 @@ 1 1 quarter + sharp + + C @@ -456,6 +526,8 @@ 1 quarter + + C @@ -465,6 +537,7 @@ 1 1 quarter + sharp @@ -475,7 +548,10 @@ 1 1 quarter + flat + + D @@ -485,6 +561,7 @@ 1 1 quarter + flat @@ -495,7 +572,10 @@ 1 1 quarter + sharp + + D @@ -514,6 +594,8 @@ 1 quarter + + D @@ -523,6 +605,7 @@ 1 1 quarter + sharp @@ -533,7 +616,10 @@ 1 1 quarter + flat + + E @@ -543,6 +629,7 @@ 1 1 quarter + flat @@ -553,7 +640,10 @@ 1 1 quarter + sharp + + E @@ -572,6 +662,8 @@ 1 quarter + + E @@ -581,6 +673,7 @@ 1 1 quarter + sharp @@ -591,7 +684,10 @@ 1 1 quarter + flat + + F @@ -601,6 +697,7 @@ 1 1 quarter + flat @@ -611,7 +708,10 @@ 1 1 quarter + sharp + + F @@ -630,6 +730,8 @@ 1 quarter + + F @@ -639,6 +741,7 @@ 1 1 quarter + sharp @@ -649,7 +752,10 @@ 1 1 quarter + flat + + G @@ -659,6 +765,7 @@ 1 1 quarter + flat @@ -669,7 +776,10 @@ 1 1 quarter + sharp + + G @@ -688,6 +798,8 @@ 1 quarter + + G @@ -697,6 +809,7 @@ 1 1 quarter + sharp @@ -707,7 +820,10 @@ 1 1 quarter + flat + + A @@ -717,6 +833,7 @@ 1 1 quarter + flat @@ -727,7 +844,10 @@ 1 1 quarter + sharp + + A @@ -746,6 +866,8 @@ 1 quarter + + A @@ -755,6 +877,7 @@ 1 1 quarter + sharp @@ -765,7 +888,10 @@ 1 1 quarter + flat + + B @@ -775,6 +901,7 @@ 1 1 quarter + flat @@ -785,7 +912,10 @@ 1 1 quarter + sharp + + B @@ -804,6 +934,8 @@ 1 quarter + + B @@ -813,6 +945,7 @@ 1 1 quarter + sharp @@ -823,6 +956,7 @@ 1 1 quarter + flat diff --git a/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.png b/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.png new file mode 100644 index 000000000..c6fbe366b Binary files /dev/null and b/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.png differ diff --git a/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.xml b/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.xml index 2f782c13f..a90c9523c 100644 --- a/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.xml +++ b/test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.xml @@ -1,12 +1,13 @@ - - + + - The <voice> element - of notes is optional in MusicXML (although Dolet always writes it out). - Here, there is one note with lyrics, but without a voice assigned. It - should still be correctly converted. + The <voice> element + of notes is optional in MusicXML (although Dolet always writes it out). + Here, there is one note with lyrics, but without a voice assigned. It + should still be correctly converted. diff --git a/test-data/musicxml-testsuite/01d-Pitches-Microtones.png b/test-data/musicxml-testsuite/01d-Pitches-Microtones.png new file mode 100644 index 000000000..759d0c4a3 Binary files /dev/null and b/test-data/musicxml-testsuite/01d-Pitches-Microtones.png differ diff --git a/test-data/musicxml-testsuite/01d-Pitches-Microtones.xml b/test-data/musicxml-testsuite/01d-Pitches-Microtones.xml index 2019abbf5..7b3d67ef8 100644 --- a/test-data/musicxml-testsuite/01d-Pitches-Microtones.xml +++ b/test-data/musicxml-testsuite/01d-Pitches-Microtones.xml @@ -1,12 +1,12 @@ - - + - Some microtones: c - flat-and-a-half, d half-flat, e half-sharp, f sharp-and-a half. - Once in the lower and once in the upper region of the + Some microtones: c + flat-and-a-half, d half-flat, e half-sharp, f sharp-and-a half. + Once in the lower and once in the upper region of the staff. diff --git a/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.png b/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.png new file mode 100644 index 000000000..0563634c7 Binary files /dev/null and b/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.png differ diff --git a/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.xml b/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.xml index d10c45953..ac1ed19d4 100644 --- a/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.xml +++ b/test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.xml @@ -1,12 +1,16 @@ - - + - Accidentals can be cautionary - or editorial. Each measure has a normal accidental, an editorial, - a cautionary and an editioal and cautionary accidental. + Accidentals have the attributes + ‘cautionary’, ‘editorial’, ‘parenthesized’, and ‘bracketed’. The first + two measures each have a cautionary accidental, an editorial, a + cautionary with parentheses off, and an editorial and cautionary + accidental. The next two measures each have a normal accidental, a + bracketed, a parenthesized, and a bracketed and parenthesized + accidental. @@ -41,7 +45,7 @@ 1 1 quarter - flat + flat @@ -63,7 +67,7 @@ 1 1 quarter - flat + flat @@ -88,7 +92,7 @@ 1 1 quarter - sharp + sharp @@ -110,7 +114,7 @@ 1 1 quarter - sharp + sharp @@ -135,7 +139,7 @@ 1 1 quarter - double-flat + flat-flat @@ -146,7 +150,7 @@ 1 1 quarter - double-flat + flat-flat @@ -157,7 +161,7 @@ 1 1 quarter - double-flat + flat-flat @@ -168,7 +172,7 @@ 1 1 quarter - double-flat + flat-flat @@ -193,7 +197,7 @@ 1 1 quarter - double-sharp + double-sharp @@ -204,7 +208,7 @@ 1 1 quarter - double-sharp + double-sharp @@ -215,7 +219,7 @@ 1 1 quarter - double-sharp + double-sharp diff --git a/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png b/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png new file mode 100644 index 000000000..c46864702 Binary files /dev/null and b/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png differ diff --git a/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.xml b/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.xml index 96158eef4..d355f5e96 100644 --- a/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.xml +++ b/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.xml @@ -1,7 +1,7 @@ - - + Microtone accidentals can be diff --git a/test-data/musicxml-testsuite/02a-Rests-Durations.png b/test-data/musicxml-testsuite/02a-Rests-Durations.png new file mode 100644 index 000000000..ba3f17c2d Binary files /dev/null and b/test-data/musicxml-testsuite/02a-Rests-Durations.png differ diff --git a/test-data/musicxml-testsuite/02a-Rests-Durations.xml b/test-data/musicxml-testsuite/02a-Rests-Durations.xml index 139884454..bf6b642eb 100644 --- a/test-data/musicxml-testsuite/02a-Rests-Durations.xml +++ b/test-data/musicxml-testsuite/02a-Rests-Durations.xml @@ -1,13 +1,15 @@ - - + Rest unit test - All different rest lengths: A - two-bar multi-measure rest, a whole rest, a half, etc. until a - 128th-rest; Then the same with dotted durations. + All different rest lengths: A + two-bar multi-measure rest, a whole rest, a half, etc., until a + 1024th-rest, then the same with dotted durations. The last bar is a + full-measure rest with the attribute + ‘measure="no"’. @@ -19,14 +21,14 @@ - 32 + 512 0 major - - 128 + 2048 1 + whole - 128 + 2048 1 + whole - 128 + 2048 1 whole + - 64 + 1024 1 half - 32 + 512 1 quarter - 16 + 256 1 eighth - 8 + 128 1 16th - 4 + 64 1 32nd - 2 + 32 1 64th - 1 + 16 1 128th - 1 + 8 1 - 128th + 256th + + + + 4 + 1 + 512th + + + + 2 + 1 + 1024th + + + + 2 + 1 + 1024th + + + - 96 + 1536 1 half - 32 + 768 1 quarter + + + + + 384 + 1 + eighth + + + + + 192 + 1 + 16th + + + + + 96 + 1 + 32nd + - - - 48 1 - quarter + 64th 24 1 - eighth + 128th 12 1 - 16th + 256th 6 1 - 32nd + 512th 3 1 - 64th + 1024th - 2 + 3 1 - 128th + 1024th + + + + + + + + 3072 + 1 + whole diff --git a/test-data/musicxml-testsuite/02b-Rests-PitchedRests.png b/test-data/musicxml-testsuite/02b-Rests-PitchedRests.png new file mode 100644 index 000000000..e2f0b8c63 Binary files /dev/null and b/test-data/musicxml-testsuite/02b-Rests-PitchedRests.png differ diff --git a/test-data/musicxml-testsuite/02b-Rests-PitchedRests.xml b/test-data/musicxml-testsuite/02b-Rests-PitchedRests.xml index 28fafc39a..20495e5d7 100644 --- a/test-data/musicxml-testsuite/02b-Rests-PitchedRests.xml +++ b/test-data/musicxml-testsuite/02b-Rests-PitchedRests.xml @@ -1,63 +1,101 @@ - - - - - - Rests can have - explicit pitches, where they are displayed. The - first rest uses no explicit position and should use - the default position, all others are explicitly - positioned somewhere else. - - - - MusicXML Part - - - - - 96 - 0 - - 1 - G2 - - - - 96 - 1 - quarter - 1 - - - E4 - 96 - 1 - quarter - 1 - - - F5 - 96 - 1 - quarter - 1 - - - A3 - 96 - 1 - quarter - 1 - - - C6 - 96 - 1 - quarter - 1 - - - + + + + + + Rests can have explicit pitches to + position them vertically. In the first bar, the first rest has no + explicit pitch and should use the default position, while the + remaining rests are explicitly positioned at pitches E4, F5, A3, and + C6. The second bar holds a full-measure rest at pitch G4 (within an + alto clef). + + + + + MusicXML Part + + + + + + 96 + 0 + + 1 + + G + 2 + + + + + 96 + 1 + quarter + 1 + + + + E + 4 + + 96 + 1 + quarter + 1 + + + + F + 5 + + 96 + 1 + quarter + 1 + + + + A + 3 + + 96 + 1 + quarter + 1 + + + + C + 6 + + 96 + 1 + quarter + 1 + + + + + + C + 3 + + + + + G + 4 + + 480 + 1 + 1 + + + diff --git a/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.png b/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.png new file mode 100644 index 000000000..b55b8a0a3 Binary files /dev/null and b/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.png differ diff --git a/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.xml b/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.xml index 346503e36..15478b1f9 100644 --- a/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.xml +++ b/test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.xml @@ -1,11 +1,12 @@ - - + - Four multi-measure rests: 3 - measures, 15 measures, 1 measure, and 12 measures. + Five multi-measure rests: 3 + measures, 15 measures, 1 measure, 12 measures, and 3 measures with + ‘use-symbols’ set. @@ -289,6 +290,35 @@ 4 1 + + + + + + 3 + + + + + 4 + 1 + + + + + + + 4 + 1 + + + + + + + 4 + 1 + light-heavy diff --git a/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.png b/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.png new file mode 100644 index 000000000..c1f65df99 Binary files /dev/null and b/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.png differ diff --git a/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.xml b/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.xml index c357e5bf0..3798371b2 100644 --- a/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.xml +++ b/test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.xml @@ -1,11 +1,11 @@ - - + - Multi-Measure rests should always - be converted into durations that are a multiple of the time + Multi-measure rests should always + be converted into durations that are a multiple of the time signature. diff --git a/test-data/musicxml-testsuite/02e-Rests-NoType.png b/test-data/musicxml-testsuite/02e-Rests-NoType.png new file mode 100644 index 000000000..51c6c9267 Binary files /dev/null and b/test-data/musicxml-testsuite/02e-Rests-NoType.png differ diff --git a/test-data/musicxml-testsuite/02e-Rests-NoType.xml b/test-data/musicxml-testsuite/02e-Rests-NoType.xml index 0b72a4d4e..276de1593 100644 --- a/test-data/musicxml-testsuite/02e-Rests-NoType.xml +++ b/test-data/musicxml-testsuite/02e-Rests-NoType.xml @@ -1,12 +1,12 @@ - - + - In some cases, a rest might + In some cases, a rest might not have its type attribute set (this happens, for example, with - voices in Finale, where you don't manually insert a + voices in Finale, where you don't manually insert a rest). diff --git a/test-data/musicxml-testsuite/03a-Rhythm-Durations.png b/test-data/musicxml-testsuite/03a-Rhythm-Durations.png new file mode 100644 index 000000000..81300db60 Binary files /dev/null and b/test-data/musicxml-testsuite/03a-Rhythm-Durations.png differ diff --git a/test-data/musicxml-testsuite/03a-Rhythm-Durations.xml b/test-data/musicxml-testsuite/03a-Rhythm-Durations.xml index b9fc0b150..986cee909 100644 --- a/test-data/musicxml-testsuite/03a-Rhythm-Durations.xml +++ b/test-data/musicxml-testsuite/03a-Rhythm-Durations.xml @@ -1,12 +1,12 @@ - - + - All note durations, from long, - brevis, whole until 128th; First with their plain values, then dotted - and finally doubly-dotted. + All note durations, from long, + brevis, whole, etc., until 1024th. First with their plain values, + then dotted, and finally double-dotted. @@ -18,7 +18,7 @@ - 64 + 1024 0 major @@ -37,7 +37,7 @@ C 5 - 1024 + 16384 1 long @@ -49,7 +49,7 @@ C 5 - 512 + 8192 1 breve @@ -58,7 +58,7 @@ C 5 - 256 + 4096 1 whole
@@ -67,7 +67,7 @@ C 5 - 128 + 2048 1 half
@@ -76,7 +76,7 @@ C 5 - 64 + 1024 1 quarter
@@ -85,7 +85,7 @@ C 5 - 32 + 512 1 eighth
@@ -94,7 +94,7 @@ C 5 - 16 + 256 1 16th
@@ -103,7 +103,7 @@ C 5 - 8 + 128 1 32nd
@@ -112,7 +112,7 @@ C 5 - 4 + 64 1 64th @@ -121,7 +121,7 @@ C 5 - 2 + 32 1 128th @@ -130,9 +130,36 @@ C 5 - 2 + 16 1 - 128th + 256th + + + + C + 5 + + 8 + 1 + 512th + + + + C + 5 + + 4 + 1 + 1024th + + + + C + 5 + + 4 + 1 + 1024th @@ -148,7 +175,7 @@ C 5 - 1536 + 24576 1 long @@ -161,7 +188,7 @@ C 5 - 768 + 12288 1 breve @@ -171,7 +198,7 @@ C 5 - 384 + 6144 1 whole @@ -181,7 +208,7 @@ C 5 - 192 + 3072 1 half @@ -191,7 +218,7 @@ C 5 - 96 + 1536 1 quarter @@ -201,7 +228,7 @@ C 5 - 48 + 768 1 eighth @@ -211,7 +238,7 @@ C 5 - 24 + 384 1 16th @@ -221,7 +248,7 @@ C 5 - 12 + 192 1 32nd @@ -231,7 +258,7 @@ C 5 - 6 + 96 1 64th @@ -241,7 +268,7 @@ C 5 - 3 + 48 1 128th @@ -251,9 +278,39 @@ C 5 - 3 + 24 1 - 128th + 256th + + + + + C + 5 + + 12 + 1 + 512th + + + + + C + 5 + + 6 + 1 + 1024th + + + + + C + 5 + + 6 + 1 + 1024th @@ -270,7 +327,7 @@ C 5 - 1792 + 28672 1 long @@ -284,7 +341,7 @@ C 5 - 896 + 14336 1 breve @@ -295,7 +352,7 @@ C 5 - 448 + 7168 1 whole @@ -306,7 +363,7 @@ C 5 - 224 + 3584 1 half @@ -317,7 +374,7 @@ C 5 - 112 + 1792 1 quarter @@ -328,7 +385,7 @@ C 5 - 56 + 896 1 eighth @@ -339,7 +396,7 @@ C 5 - 28 + 448 1 16th @@ -350,7 +407,7 @@ C 5 - 14 + 224 1 32nd @@ -361,12 +418,45 @@ C 5 - 7 + 112 1 64th + + + C + 5 + + 56 + 1 + 128th + + + + + + C + 5 + + 28 + 1 + 256th + + + + + + C + 5 + + 14 + 1 + 512th + + + C @@ -374,7 +464,18 @@ 7 1 - 64th + 1024th + + + + + + C + 5 + + 7 + 1 + 1024th diff --git a/test-data/musicxml-testsuite/03b-Rhythm-Backup.png b/test-data/musicxml-testsuite/03b-Rhythm-Backup.png new file mode 100644 index 000000000..109b7e050 Binary files /dev/null and b/test-data/musicxml-testsuite/03b-Rhythm-Backup.png differ diff --git a/test-data/musicxml-testsuite/03b-Rhythm-Backup.xml b/test-data/musicxml-testsuite/03b-Rhythm-Backup.xml index b0e675496..1d31d352b 100644 --- a/test-data/musicxml-testsuite/03b-Rhythm-Backup.xml +++ b/test-data/musicxml-testsuite/03b-Rhythm-Backup.xml @@ -1,13 +1,13 @@ - - - - + + + - Two voices with a backup, that - does not jump to the beginning for the measure for voice 2, but - somewhere in the middle. Voice 2 thus won't have any notes or rests - for the first beat of the measures. + Two voices with a <backup> + element that does not jump to the beginning of the measure for voice + two but somewhere in the middle. Voice two thus won't have any notes + or rests for the first beat of the measure. diff --git a/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.png b/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.png new file mode 100644 index 000000000..abd2ffc44 Binary files /dev/null and b/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.png differ diff --git a/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.xml b/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.xml index 16a54c5f5..7926f5401 100644 --- a/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.xml +++ b/test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.xml @@ -1,7 +1,7 @@ - - + Although uncommon, the divisions diff --git a/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.png b/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.png new file mode 100644 index 000000000..023f7e7fa Binary files /dev/null and b/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.png differ diff --git a/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.xml b/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.xml index 2a53d2c7f..de0cd44f1 100644 --- a/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.xml +++ b/test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.xml @@ -1,12 +1,15 @@ - - + Several durations can be written - with dots. For multimeasure rests, we can also have durations that - cannot be expressed with dotted notes (like 5/8). + with dots. For multi-measure rests it is also possible to have + durations that cannot be expressed with dotted notes (like 5/8). + + In bar 15 there is only a 16th note and no rest for the remaining part + of the bar. diff --git a/test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.png b/test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.png new file mode 100644 index 000000000..d4ff904c2 Binary files /dev/null and b/test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.png differ diff --git a/test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.xml b/test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.xml new file mode 100644 index 000000000..87adfb851 --- /dev/null +++ b/test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.xml @@ -0,0 +1,41 @@ + + + + + + No <divisions> + element. + + + + + MusicXML Part + + + + + + + 0 + + + + G + 2 + + + + + C + 4 + + 4 + whole + + + + diff --git a/test-data/musicxml-testsuite/03f-Rhythm-Forward.png b/test-data/musicxml-testsuite/03f-Rhythm-Forward.png new file mode 100644 index 000000000..47e7dd407 Binary files /dev/null and b/test-data/musicxml-testsuite/03f-Rhythm-Forward.png differ diff --git a/test-data/musicxml-testsuite/03f-Rhythm-Forward.xml b/test-data/musicxml-testsuite/03f-Rhythm-Forward.xml new file mode 100644 index 000000000..61add5152 --- /dev/null +++ b/test-data/musicxml-testsuite/03f-Rhythm-Forward.xml @@ -0,0 +1,58 @@ + + + + + + A voice with two <forward> + elements, putting the first note on the third beat and the second note + on the last 16th of the measure. There are no visible + rests. + + + + + + + + + + + 4 + 0 + + + G + 2 + + + + 8 + + + + C + 4 + + 4 + 1 + quarter + + + 3 + + + + C + 4 + + 1 + 1 + 16th + + + + diff --git a/test-data/musicxml-testsuite/11a-TimeSignatures.png b/test-data/musicxml-testsuite/11a-TimeSignatures.png new file mode 100644 index 000000000..e41d030ce Binary files /dev/null and b/test-data/musicxml-testsuite/11a-TimeSignatures.png differ diff --git a/test-data/musicxml-testsuite/11a-TimeSignatures.xml b/test-data/musicxml-testsuite/11a-TimeSignatures.xml index 23339737b..801cc7647 100644 --- a/test-data/musicxml-testsuite/11a-TimeSignatures.xml +++ b/test-data/musicxml-testsuite/11a-TimeSignatures.xml @@ -1,12 +1,12 @@ - - + - Various time signatures: 2/2 - (alla breve), 4/4 (C), 2/2, 3/2, 2/4, 3/4, 4/4, 5/4, 3/8, 6/8, - 12/8 + Various time signatures: 2/2 + (alla breve), 4/4 (C), 2/2, 3/2, 2/4, 3/4, 4/4, 5/4, 3/8, 6/8, + 12/8. diff --git a/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.png b/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.png new file mode 100644 index 000000000..5fd4f35eb Binary files /dev/null and b/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.png differ diff --git a/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.xml b/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.xml index a70af96c9..70a4f3747 100644 --- a/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.xml +++ b/test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.xml @@ -1,42 +1,99 @@ - - - - - - A score without - a time signature (but with a key and clefs) - - - - - - - - - - - 1 - 0 - 2 - G2 - F4 - - - F4 - 4 - 1 - whole - 1 - - 384 - - B2 - 4 - 2 - whole - 2 - - - + + + + + + A score without + a time signature (but with a key and clefs). The first bar misses a + <time> element altogether, the second bar sets a 2/2 time with + ‘print-object="no"’ for both staves, and the third bar sets a 4/4 time + with ‘print-object="no"’ only for the lower + staff. + + + + + + + + + + + + 1 + 0 + 2 + G2 + F4 + + + F4 + 4 + 1 + whole + 1 + + 384 + + B2 + 4 + 2 + whole + 2 + + + + + + + + + F4 + 4 + 1 + whole + 1 + + 384 + + B2 + 4 + 2 + whole + 2 + + + + + + + + + + F4 + 4 + 1 + whole + 1 + + 384 + + B2 + 4 + 2 + whole + 2 + + + diff --git a/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.png b/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.png new file mode 100644 index 000000000..bb3349af2 Binary files /dev/null and b/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.png differ diff --git a/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.xml b/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.xml index e0c5cb53f..144824999 100644 --- a/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.xml +++ b/test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.xml @@ -1,10 +1,10 @@ - - + - Compound time signatures with + Compound time signatures with same denominator: (3+2)/8 and (5+3+1)/4. diff --git a/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.png b/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.png new file mode 100644 index 000000000..f428b8d2c Binary files /dev/null and b/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.png differ diff --git a/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.xml b/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.xml index 37a5fa3c0..f6686a6b3 100644 --- a/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.xml +++ b/test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.xml @@ -1,10 +1,10 @@ - - + - Compound time signatures with + Compound time signatures with separate fractions displayed: 3/8+2/8+3/4 and 5/2+1/8. diff --git a/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.png b/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.png new file mode 100644 index 000000000..bcda5748a Binary files /dev/null and b/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.png differ diff --git a/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.xml b/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.xml index 8d8cf5f91..e7f484f95 100644 --- a/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.xml +++ b/test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.xml @@ -1,7 +1,7 @@ - - + Compound time signatures of diff --git a/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.png b/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.png new file mode 100644 index 000000000..f99c41306 Binary files /dev/null and b/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.png differ diff --git a/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.xml b/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.xml index 4787a7114..045f86d32 100644 --- a/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.xml +++ b/test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.xml @@ -1,12 +1,12 @@ - - + - A time signature of 3/8 with the - symbol="cut" attribute and two symbol="single-number" attributes with - compound time signatures. Shall the symbol be ignored in this + A time signature of 3/8 with the + ‘symbol="cut"’ attribute and two ‘symbol="single-number"’ attributes + with compound time signatures. Shall the symbol be ignored in this case? @@ -44,7 +44,7 @@ - + - + + + + + + 6 + 1 + + + + + + + B + 4 + + 1 + 1 + eighth + begin + + + + B + 4 + + 1 + 1 + eighth + continue + + + + B + 4 + + 1 + 1 + eighth + continue + + + + B + 4 + + 1 + 1 + eighth + end + light-heavy diff --git a/test-data/musicxml-testsuite/12a-Clefs.png b/test-data/musicxml-testsuite/12a-Clefs.png new file mode 100644 index 000000000..c9bb75edc Binary files /dev/null and b/test-data/musicxml-testsuite/12a-Clefs.png differ diff --git a/test-data/musicxml-testsuite/12a-Clefs.xml b/test-data/musicxml-testsuite/12a-Clefs.xml index 85be49ec1..2b251dc38 100644 --- a/test-data/musicxml-testsuite/12a-Clefs.xml +++ b/test-data/musicxml-testsuite/12a-Clefs.xml @@ -1,15 +1,15 @@ - - + - Various clefs: G, C, F, percussion, - TAB and none; some are also possible with octavation and on other - staff lines than their default (e.g. soprano/alto/tenor/bariton C - clefs); Each measure shows a different clef (measure 17 has the "none" - clef), only measure 18 has the same treble clef as measure - 1. + Various clefs: G, C, F, + percussion, TAB, and ‘none’ (in measure 17). Some clefs are also + shown with transposition and on other staff lines than their default. + + Each measure shows a different clef; only measure 18 has the same + treble clef as the first measure. diff --git a/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.png b/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.png new file mode 100644 index 000000000..3bbabc9c0 Binary files /dev/null and b/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.png differ diff --git a/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.xml b/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.xml index d0ad799e8..97223f55d 100644 --- a/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.xml +++ b/test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.xml @@ -1,47 +1,48 @@ - - - - - - A score without - any key or clef defined. The default (4/4 in treble - clef) should be used. - - - - - - - - - - - 1 - - - - - C - 4 - - 4 - 1 - whole - - - - - - C - 4 - - 4 - 1 - whole - - - + + + + + + A score without a <key> or + <clef> element (but with <time>). The default (4/4 in + treble clef) should be used. + + + + + + + + + + + 1 + + + + + C + 4 + + 4 + 1 + whole + + + + + + C + 4 + + 4 + 1 + whole + + + diff --git a/test-data/musicxml-testsuite/13a-KeySignatures.png b/test-data/musicxml-testsuite/13a-KeySignatures.png new file mode 100644 index 000000000..08b837839 Binary files /dev/null and b/test-data/musicxml-testsuite/13a-KeySignatures.png differ diff --git a/test-data/musicxml-testsuite/13a-KeySignatures.xml b/test-data/musicxml-testsuite/13a-KeySignatures.xml index 1279fd2d1..e6d2c0162 100644 --- a/test-data/musicxml-testsuite/13a-KeySignatures.xml +++ b/test-data/musicxml-testsuite/13a-KeySignatures.xml @@ -1,13 +1,13 @@ - - + Different Key signatures - Various key signature: from 11 - flats to 11 sharps (each one first one measure in major, then one - measure in minor) + Various key signatures: from 11 + flats to 11 sharps. Each signature is shown twice, with one measure + in major and the other measure in minor. diff --git a/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.png b/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.png new file mode 100644 index 000000000..da0e15c62 Binary files /dev/null and b/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.png differ diff --git a/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.xml b/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.xml index 1f4852d51..de53f09d3 100644 --- a/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.xml +++ b/test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.xml @@ -1,12 +1,13 @@ - - + - All different modes: major, - minor, ionian, dorian, phrygian, lydian, mixolydian, aeolian, and - locrian; All modes are given with 2 sharps. + All different modes: ‘major’, + ‘minor’, ‘ionian’, ‘dorian’, ‘phrygian’, ‘lydian’, ‘mixolydian’, + ‘aeolian’, ‘locrian’, and ‘none’. All modes are given with two + sharps. @@ -174,6 +175,22 @@ quarter locrian + + + 2 + none + + + + + G + 4 + + 1 + 1 + quarter + none + diff --git a/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.png b/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.png new file mode 100644 index 000000000..d7262d0c4 Binary files /dev/null and b/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.png differ diff --git a/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.xml b/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.xml index fa7780389..5522fbe57 100644 --- a/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.xml +++ b/test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.xml @@ -1,14 +1,14 @@ - - + Non-traditional key signatures, - where each alteration is separately given. Here we have (f sharp, - a flat, b flat) and (c flatflat, g sharp sharp, d flat, b sharp, f - natural), where in the second case an explicit octave is given for - each alteration. + where each alteration is separately given. The first signature is [f + sharp, a flat, b flat]. The second one is [c flatflat, g sharp sharp, + d flat, b sharp, f natural], with explicitly selected octaves for each + alteration. diff --git a/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.png b/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.png new file mode 100644 index 000000000..2ee533fda Binary files /dev/null and b/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.png differ diff --git a/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.xml b/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.xml index 9505e1555..8f374ff75 100644 --- a/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.xml +++ b/test-data/musicxml-testsuite/13d-KeySignatures-Microtones.xml @@ -1,13 +1,13 @@ - - + Non-traditional key signatures - with microtone alterations: (g flat-and-a-half, - a flat, b half-flat, c natural, d half-sharp, e sharp, f - sharp-and-a-half). + with microtone alterations: [g flat-and-a-half, a flat, b half-flat, c + natural, d half-sharp, e sharp, f + sharp-and-a-half]. diff --git a/test-data/musicxml-testsuite/13e-KeySignatures-Cancel.png b/test-data/musicxml-testsuite/13e-KeySignatures-Cancel.png new file mode 100644 index 000000000..7a4b5bd12 Binary files /dev/null and b/test-data/musicxml-testsuite/13e-KeySignatures-Cancel.png differ diff --git a/test-data/musicxml-testsuite/13e-KeySignatures-Cancel.xml b/test-data/musicxml-testsuite/13e-KeySignatures-Cancel.xml new file mode 100644 index 000000000..147cc412b --- /dev/null +++ b/test-data/musicxml-testsuite/13e-KeySignatures-Cancel.xml @@ -0,0 +1,121 @@ + + + + + + Tests of key signature + cancellation: at default location, at right, and before barline, then + cancelling a key signature that does not exist. + + + + + MusicXML Part + + + + + + + 1 + + 3 + major + + + + G + 2 + + + + + C + 4 + + 2 + 1 + half + + + + + + 3 + -5 + major + + + + + C + 4 + + 2 + 1 + half + + + + + + -5 + -3 + major + + + + + C + 4 + + 2 + 1 + half + + + + + + -3 + 2 + major + + + + + C + 4 + + 2 + 1 + half + + + + + + 4 + -2 + major + + + + + C + 4 + + 2 + 1 + half + + + light-heavy + + + + diff --git a/test-data/musicxml-testsuite/13f-KeySignatures-Visible.png b/test-data/musicxml-testsuite/13f-KeySignatures-Visible.png new file mode 100644 index 000000000..8d009a096 Binary files /dev/null and b/test-data/musicxml-testsuite/13f-KeySignatures-Visible.png differ diff --git a/test-data/musicxml-testsuite/13f-KeySignatures-Visible.xml b/test-data/musicxml-testsuite/13f-KeySignatures-Visible.xml new file mode 100644 index 000000000..203ac640f --- /dev/null +++ b/test-data/musicxml-testsuite/13f-KeySignatures-Visible.xml @@ -0,0 +1,101 @@ + + + + + + Test the ‘print-object’ attribute + of key signatures. The signature at the beginning of the second bar + is a flat major and should be invisible; the following notes d flat, e + flat, a flat, and b flat shouldn't have a flat + accidental. + + + + + MusicXML Part + + + + + + + 1 + + 4 + major + + + + G + 2 + + + + + D + 1 + 4 + + 4 + 1 + whole + + + + + + + -4 + major + + + + + D + -1 + 4 + + 1 + 1 + quarter + + + + E + -1 + 4 + + 1 + 1 + quarter + + + + A + -1 + 4 + + 1 + 1 + quarter + + + + B + -1 + 4 + + 1 + 1 + quarter + + + light-heavy + + + + diff --git a/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.png b/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.png new file mode 100644 index 000000000..3303e88a8 Binary files /dev/null and b/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.png differ diff --git a/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.xml b/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.xml index e297d5c88..1b713bacb 100644 --- a/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.xml +++ b/test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.xml @@ -1,16 +1,19 @@ - - + - The number of staff lines can be - modified by using the staff-lines child of the staff-details attribute. - This can happen globally (the first staff has one line globally) or - during the part at the beginning of a measure and even inside a measure - (the second part has 5 lines initially, 4 at the beginning of the - second measure, and 3 starting in the middle of the third - measure). + Testing staff line configurations + and pitched notes. The number of staff lines can be modified by using + the ‘staff-lines’ child of the ‘staff-details’ attribute. This can + happen globally (the first staff has one line globally) or during the + part at the beginning of a measure and even inside a measure (the + second part has 5 lines initially, 4 at the beginning of the second + measure, and 3 starting in the middle of the third measure). The + fourth measure in the lower staff has five lines again but uses + ‘print-object="no"’ in the ‘line-detail’ element to suppress the + second and fourth staff line. @@ -41,8 +44,8 @@ - D - 5 + G + 4 4 1 @@ -53,8 +56,8 @@ - D - 5 + G + 4 4 1 @@ -65,8 +68,20 @@ - D - 5 + G + 4 + + 4 + 1 + whole + + + + + + + G + 4 4 1 @@ -136,7 +151,7 @@ - 2 + 3 @@ -149,6 +164,25 @@ half + + + + + 5 + + + + + + + G + 4 + + 4 + 1 + whole + + diff --git a/test-data/musicxml-testsuite/21a-Chord-Basic.png b/test-data/musicxml-testsuite/21a-Chord-Basic.png new file mode 100644 index 000000000..95c504724 Binary files /dev/null and b/test-data/musicxml-testsuite/21a-Chord-Basic.png differ diff --git a/test-data/musicxml-testsuite/21a-Chord-Basic.xml b/test-data/musicxml-testsuite/21a-Chord-Basic.xml index 17464b874..90911dc4e 100644 --- a/test-data/musicxml-testsuite/21a-Chord-Basic.xml +++ b/test-data/musicxml-testsuite/21a-Chord-Basic.xml @@ -1,55 +1,56 @@ - - - - - - One simple chord - consisting of two notes. - - - - - MusicXML Part - - - - - - 960 - - - G - 2 - - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - 960 - 1 - quarter - - - + + + + + + One simple chord + consisting of two notes. + + + + + MusicXML Part + + + + + + 960 + + + G + 2 + + + + + A + 4 + + 960 + 1 + quarter + + + + + F + 4 + + 960 + 1 + quarter + + + + 960 + 1 + quarter + + + diff --git a/test-data/musicxml-testsuite/21b-Chords-TwoNotes.png b/test-data/musicxml-testsuite/21b-Chords-TwoNotes.png new file mode 100644 index 000000000..ca6c477cf Binary files /dev/null and b/test-data/musicxml-testsuite/21b-Chords-TwoNotes.png differ diff --git a/test-data/musicxml-testsuite/21b-Chords-TwoNotes.xml b/test-data/musicxml-testsuite/21b-Chords-TwoNotes.xml index 7991e247f..5b182fd21 100644 --- a/test-data/musicxml-testsuite/21b-Chords-TwoNotes.xml +++ b/test-data/musicxml-testsuite/21b-Chords-TwoNotes.xml @@ -1,185 +1,210 @@ - - - - - - Some subsequent - (identical) two-note chords. - - - - - MusicXML Part - - - - - - 960 - - - G - 2 - - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - + + + + + + Some subsequent + (identical) two-note chords. In the second bar, the chords are tied + (top note, bottom note, both notes). + + + + + MusicXML Part + + + + + + + 960 + + + G + 2 + + + + + A + 4 + + 960 + 1 + quarter + + + + + F + 4 + + 960 + 1 + quarter + + + + A + 4 + + 960 + 1 + quarter + + + + + F + 4 + + 960 + 1 + quarter + + + + A + 4 + + 960 + 1 + quarter + + + + + F + 4 + + 960 + 1 + quarter + + + + A + 4 + + 960 + 1 + quarter + + + + + F + 4 + + 960 + 1 + quarter + + + + + + + A + 4 + + 960 + 1 + quarter + + + + + + + + F + 4 + + 960 + 1 + quarter + + + + A + 4 + + 960 + 1 + quarter + + + + + + + + F + 4 + + 960 + 1 + quarter + + + + + + + A + 4 + + 960 + 1 + quarter + + + + + + + + F + 4 + + 960 + 1 + quarter + + + + + + + + A + 4 + + 960 + 1 + quarter + + + + + + + + F + 4 + + 960 + 1 + quarter + + + + + + diff --git a/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.png b/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.png new file mode 100644 index 000000000..b8eb9a588 Binary files /dev/null and b/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.png differ diff --git a/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.xml b/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.xml index f47ef862d..aa3d90a8f 100644 --- a/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.xml +++ b/test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.xml @@ -1,229 +1,240 @@ - - - - - - Some three-note - chords, with various durations. - - - - - MusicXML Part - - - - - - 960 - - - G - 2 - - - - - F - 4 - - 1440 - 1 - quarter - - - - - - A - 4 - - 1440 - 1 - quarter - - - - - - C - 5 - - 1440 - 1 - quarter - - - - - A - 4 - - 480 - 1 - eighth - - - - - G - 5 - - 480 - 1 - eighth - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - - C - 5 - - 960 - 1 - quarter - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - - C - 5 - - 960 - 1 - quarter - - + + + + + + Some three-note + chords, with various durations. + + + + + MusicXML Part + + + + + + 960 + + + G + 2 + + + + + F + 4 + + 1440 + 1 + quarter + + + + + + A + 4 + + 1440 + 1 + quarter + + + + + + C + 5 + + 1440 + 1 + quarter + + + + + A + 4 + + 480 + 1 + eighth + + + + + C + 5 + + 480 + 1 + eighth + + + + + G + 5 + + 480 + 1 + eighth + + + + A + 4 + + 960 + 1 + quarter + + + + + F + 4 + + 960 + 1 + quarter + + + + + C + 5 + + 960 + 1 + quarter + + + + A + 4 + + 960 + 1 + quarter + + + + + F + 4 + + 960 + 1 + quarter + + + + + C + 5 + + 960 + 1 + quarter + + - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - - E - 5 - - 960 - 1 - quarter - - - - A - 4 - - 960 - 1 - quarter - - - - - F - 4 - - 960 - 1 - quarter - - - - - F - 5 - - 960 - 1 - quarter - - - - A - 4 - - 1920 - 1 - half - - - - - F - 4 - - 1920 - 1 - half - - - - - D - 5 - - 1920 - 1 - half - - - + + + + A + 4 + + 960 + 1 + quarter + + + + + F + 4 + + 960 + 1 + quarter + + + + + E + 5 + + 960 + 1 + quarter + + + + A + 4 + + 960 + 1 + quarter + + + + + F + 4 + + 960 + 1 + quarter + + + + + F + 5 + + 960 + 1 + quarter + + + + A + 4 + + 1920 + 1 + half + + + + + F + 4 + + 1920 + 1 + half + + + + + D + 5 + + 1920 + 1 + half + + + diff --git a/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.png b/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.png new file mode 100644 index 000000000..4920c9b79 Binary files /dev/null and b/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.png differ diff --git a/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.xml b/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.xml index a89acac99..c5523777a 100644 --- a/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.xml +++ b/test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.xml @@ -1,156 +1,156 @@ - - - - - - Chords in the - second measure, after several ornaments in the first - measure and a p at the beginning of the second - measure. - - - - - MusicXML Part - - - - - - 8 - - -4 - major - - - - G - 2 - - - - - Largo - - - - - - - - - 3 - - - - F - 4 - - 32 - 1 - whole - - - - - - - - - - - - - -

- - - - - - F - 4 - - 12 - 1 - quarter - - - - - - A - -1 - 4 - - 12 - 1 - quarter - - - - - F - 4 - - 4 - 1 - eighth - - - - - A - -1 - 4 - - 4 - 1 - eighth - - - - G - 4 - - 8 - 1 - quarter - - - - - B - -1 - 4 - - 8 - 1 - quarter - - - - G - 4 - - 8 - 1 - quarter - - - - - B - -1 - 4 - - 8 - 1 - quarter - - - + + + + + + Chords in the second measure, + after several ornaments in the first measure and a ‘p’ at the + beginning of the second measure. + + + + + MusicXML Part + + + + + + 8 + + -4 + major + + + + G + 2 + + + + + Largo + + + + + + + + + 3 + + + + F + 4 + + 32 + 1 + whole + + + + + + + + + + + + + +

+ + + + + + F + 4 + + 12 + 1 + quarter + + + + + + A + -1 + 4 + + 12 + 1 + quarter + + + + + F + 4 + + 4 + 1 + eighth + + + + + A + -1 + 4 + + 4 + 1 + eighth + + + + G + 4 + + 8 + 1 + quarter + + + + + B + -1 + 4 + + 8 + 1 + quarter + + + + G + 4 + + 8 + 1 + quarter + + + + + B + -1 + 4 + + 8 + 1 + quarter + + + diff --git a/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.png b/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.png new file mode 100644 index 000000000..6069a205b Binary files /dev/null and b/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.png differ diff --git a/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.xml b/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.xml index 23e971526..0c7d33b67 100644 --- a/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.xml +++ b/test-data/musicxml-testsuite/21e-Chords-PickupMeasures.xml @@ -1,87 +1,85 @@ - - + + - Check for proper chord detection - after a pickup measure (i.e. the first beat of the measure is not - aligned with multiples of the time signature)! + Check for proper chord detection + after a pickup measure (i.e., the first beat of the measure is not + aligned with a multiple of the time signature)! - - - MusicXML Part - - - - - - - 1 - - - - C - 5 - - 1 - 1 - quarter - - - - - - - C - 5 - - 1 - 1 - quarter - - - - - A - 4 - - 1 - 1 - quarter - - - - - F - 4 - - 1 - 1 - quarter - - - - C - 5 - - 1 - 1 - quarter - - - - - A - 4 - - 1 - 1 - quarter - - - - - + + + MusicXML Part + + + + + + + 1 + + + + C + 5 + + 1 + 1 + quarter + + + + + + + C + 5 + + 1 + 1 + quarter + + + + + A + 4 + + 1 + 1 + quarter + + + + + F + 4 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + + A + 4 + + 1 + 1 + quarter + + + diff --git a/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.png b/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.png new file mode 100644 index 000000000..91ba9c650 Binary files /dev/null and b/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.png differ diff --git a/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.xml b/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.xml index 6a7de1cd2..b15fab7e5 100644 --- a/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.xml +++ b/test-data/musicxml-testsuite/21f-Chord-ElementInBetween.xml @@ -1,84 +1,86 @@ - - - - - - Between the individual notes of - a chord there can be direction or harmony elements, which should be - properly assigned to the chord (or the position of the - chord). - - - - - MusicXML Part - - - - - - 1 - - - G - 2 - - - - - A - 4 - - 1 - 1 - quarter - - - - - - - - - - F - 1 - 4 - - 1 - 1 - quarter - - - -

- - - - - - D - 4 - - 1 - 1 - quarter - - - - 1 - 1 - quarter - - - - 2 - 1 - half - - - + + + + + + Between the individual notes of a + chord there can be <direction> elements, which already belong to + the next <note> element after the current one. The segno and + the piano sign should be attached to the rest after the + chord. + + + + + MusicXML Part + + + + + + 1 + + + G + 2 + + + + + A + 4 + + 1 + 1 + quarter + + + + + + + + + + F + 1 + 4 + + 1 + 1 + quarter + + + +

+ + + + + + D + 4 + + 1 + 1 + quarter + + + + 1 + 1 + quarter + + + + 2 + 1 + half + + + diff --git a/test-data/musicxml-testsuite/21g-Chords-Tremolos.png b/test-data/musicxml-testsuite/21g-Chords-Tremolos.png new file mode 100644 index 000000000..b0c31c227 Binary files /dev/null and b/test-data/musicxml-testsuite/21g-Chords-Tremolos.png differ diff --git a/test-data/musicxml-testsuite/21g-Chords-Tremolos.xml b/test-data/musicxml-testsuite/21g-Chords-Tremolos.xml new file mode 100644 index 000000000..5b876c088 --- /dev/null +++ b/test-data/musicxml-testsuite/21g-Chords-Tremolos.xml @@ -0,0 +1,253 @@ + + + + + Tremolos on chords + + + + Different tremolos on different + chord notes. The tremolo on the last chord is of type + ‘unmeasured’. + + + + + + + + + + + 2 + + 0 + + + G + 2 + + + + + F + 3 + + 2 + 1 + quarter + + + 4 + + + + + + + C + 4 + + 2 + 1 + quarter + + + + + G + 4 + + 2 + 1 + quarter + + + + + D + 5 + + 2 + 1 + quarter + + + + A + 3 + + 1 + 1 + eighth + begin + + + + + D + 4 + + 1 + 1 + eighth + + + 2 + + + + + + + G + 4 + + 1 + 1 + eighth + + + + + C + 5 + + 1 + 1 + eighth + + + + B + 3 + + 1 + 1 + eighth + end + + + + + D + 4 + + 1 + 1 + eighth + + + + + F + 4 + + 1 + 1 + eighth + + + 1 + + + + + + + A + 4 + + 1 + 1 + eighth + + + + F + 5 + + 1 + 1 + eighth + + + + + A + 5 + + 1 + 1 + eighth + + + + + B + 5 + + 1 + 1 + eighth + + + + + C + 6 + + 1 + 1 + eighth + + + 3 + + + + + + G + 4 + + 2 + 1 + quarter + + + 0 + + + + + + + D + 5 + + 2 + 1 + quarter + + + + + A + 5 + + 2 + 1 + quarter + + + + 1 + 1 + eighth + + + + + diff --git a/test-data/musicxml-testsuite/21h-Chord-Accidentals.png b/test-data/musicxml-testsuite/21h-Chord-Accidentals.png new file mode 100644 index 000000000..47b240c60 Binary files /dev/null and b/test-data/musicxml-testsuite/21h-Chord-Accidentals.png differ diff --git a/test-data/musicxml-testsuite/21h-Chord-Accidentals.xml b/test-data/musicxml-testsuite/21h-Chord-Accidentals.xml new file mode 100644 index 000000000..99d769c99 --- /dev/null +++ b/test-data/musicxml-testsuite/21h-Chord-Accidentals.xml @@ -0,0 +1,65 @@ + + + + + + A chord with normal, cautionary, + and editorial accidentals. + + + + + MusicXML Part + + + + + + 1 + + + G + 2 + + + + + D + -1 + 4 + + 1 + 1 + quarter + flat + + + + + F + 1 + 4 + + 1 + 1 + quarter + sharp + + + + + A + 4 + + 1 + 1 + quarter + natural + + + + diff --git a/test-data/musicxml-testsuite/22a-Noteheads.png b/test-data/musicxml-testsuite/22a-Noteheads.png new file mode 100644 index 000000000..0e7030b90 Binary files /dev/null and b/test-data/musicxml-testsuite/22a-Noteheads.png differ diff --git a/test-data/musicxml-testsuite/22a-Noteheads.xml b/test-data/musicxml-testsuite/22a-Noteheads.xml index 287488727..bfa6e18b9 100644 --- a/test-data/musicxml-testsuite/22a-Noteheads.xml +++ b/test-data/musicxml-testsuite/22a-Noteheads.xml @@ -1,15 +1,15 @@ - - + - Different note styles, using the - <notehead> element. First, each note head style is printed - with four quarter notes, two with filled heads, two with unfilled - heads, where first the stem is up and then the stem is down. After - that, each note head style is printed with a half note (should have - an unfilled head by default). Finally, the Aiken note head styles are + Different note styles, using the + <notehead> element. First, each note head style is printed + with four quarter notes, two with filled heads, two with unfilled + heads, where first the stem is up and then the stem is down. After + that, each note head style is printed with a half note (should have + an unfilled head by default). Finally, the Aiken note head styles are tested, once with stem up and once with stem down. @@ -309,7 +309,8 @@ 1 quarter circle-x - circle-x + circle- + x @@ -353,7 +354,8 @@ 1 quarter inverted triangle - inverted triangle + inverted + triangle @@ -397,7 +399,8 @@ 1 quarter arrow down - arrow down + arrow + down @@ -441,7 +444,8 @@ 1 quarter arrow up - arrow up + arrow + up @@ -529,7 +533,8 @@ 1 quarter back slashed - back slashed + back + slashed @@ -693,9 +698,13 @@ quarter none + + light-light + + A @@ -780,7 +789,8 @@ 1 half circle-x - circle-x + circle- + x @@ -791,7 +801,8 @@ 1 half inverted triangle - inverted triangle + inverted + triangle @@ -805,7 +816,8 @@ 1 half arrow down - arrow down + arrow + down @@ -816,7 +828,8 @@ 1 half arrow up - arrow up + arrow + up @@ -841,7 +854,8 @@ 1 half back slashed - back slashed + back + slashed @@ -868,9 +882,13 @@ cluster cluster + + light-light + + A diff --git a/test-data/musicxml-testsuite/22b-Staff-Notestyles.png b/test-data/musicxml-testsuite/22b-Staff-Notestyles.png new file mode 100644 index 000000000..19f45392c Binary files /dev/null and b/test-data/musicxml-testsuite/22b-Staff-Notestyles.png differ diff --git a/test-data/musicxml-testsuite/22b-Staff-Notestyles.xml b/test-data/musicxml-testsuite/22b-Staff-Notestyles.xml index 1a15bc877..bf8c81818 100644 --- a/test-data/musicxml-testsuite/22b-Staff-Notestyles.xml +++ b/test-data/musicxml-testsuite/22b-Staff-Notestyles.xml @@ -1,12 +1,12 @@ - - + - Staff-connected note styles: - slash notation, hidden notes (with and without hidden staff - lines) + Staff-connected note styles: slash + notation, hidden notes (with and without hidden staff + lines). @@ -40,10 +40,43 @@ 1 1 quarter + normal + + + B + 4 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + D + 5 + + 1 + 1 + quarter + + + + - + + quarter + @@ -54,7 +87,17 @@ 1 1 quarter - slash, no stem + slashes, + no stem + + + + B + 4 + + 1 + 1 + quarter @@ -65,12 +108,30 @@ 1 quarter + + + D + 5 + + 1 + 1 + quarter + - + + quarter + + + + + + - + + quarter + @@ -81,11 +142,18 @@ 1 1 quarter - slash, with stem + slashes, + with stem + + + + B + 4 + + 1 + 1 + quarter - - - C @@ -95,11 +163,26 @@ 1 quarter + + + D + 5 + + 1 + 1 + quarter + - + + quarter + + + + + A @@ -108,7 +191,17 @@ 1 1 quarter - hidden notes + hidden + notes + + + + B + 4 + + 1 + 1 + quarter @@ -119,6 +212,18 @@ 1 quarter + + + D + 5 + + 1 + 1 + quarter + + + + 0 @@ -126,25 +231,46 @@ - C + A 4 1 1 quarter - hidden notes, staff lines + hidden notes, + hidden + staff lines - - - D + B 4 - 2 + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + D + 5 + + 1 1 - half + quarter + + + 5 @@ -152,13 +278,42 @@ - G + A 4 - 2 + 1 1 - half - normal settings restored + quarter + normal + settings + restored + + + + B + 4 + + 1 + 1 + quarter + + + + C + 5 + + 1 + 1 + quarter + + + + D + 5 + + 1 + 1 + quarter light-heavy diff --git a/test-data/musicxml-testsuite/22c-Noteheads-Chords.png b/test-data/musicxml-testsuite/22c-Noteheads-Chords.png new file mode 100644 index 000000000..fbfcf4101 Binary files /dev/null and b/test-data/musicxml-testsuite/22c-Noteheads-Chords.png differ diff --git a/test-data/musicxml-testsuite/22c-Noteheads-Chords.xml b/test-data/musicxml-testsuite/22c-Noteheads-Chords.xml index 5196040ae..84ebb0a11 100644 --- a/test-data/musicxml-testsuite/22c-Noteheads-Chords.xml +++ b/test-data/musicxml-testsuite/22c-Noteheads-Chords.xml @@ -1,11 +1,11 @@ - - + - Different note styles for - individual notes inside a chord, using the + Different note styles for + individual notes inside a chord, using the <notehead> element. diff --git a/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.png b/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.png new file mode 100644 index 000000000..6fdc675c1 Binary files /dev/null and b/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.png differ diff --git a/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.xml b/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.xml index 297beed22..b1be0c8d0 100644 --- a/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.xml +++ b/test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.xml @@ -1,13 +1,15 @@ - - + - Parenthesized note heads. First, - a single parenthesized note is tested, once with a normal and then - with a non-standard notehead, then two chords with some/all - parenthesized noteheads and finally a parenthesized rest. + Parenthesized note heads. A + normal parenthesized note, a parenthesized note with an ‘x’ note head, + a three-note chord with the middle note parenthesized, a three-note + chord with all notes parenthesized, a normal quarter rest in + parentheses, and a pitched quarter rest in + parentheses. diff --git a/test-data/musicxml-testsuite/23a-Tuplets.png b/test-data/musicxml-testsuite/23a-Tuplets.png new file mode 100644 index 000000000..b203d8eed Binary files /dev/null and b/test-data/musicxml-testsuite/23a-Tuplets.png differ diff --git a/test-data/musicxml-testsuite/23a-Tuplets.xml b/test-data/musicxml-testsuite/23a-Tuplets.xml index 4fb2a7c4b..3551582f6 100644 --- a/test-data/musicxml-testsuite/23a-Tuplets.xml +++ b/test-data/musicxml-testsuite/23a-Tuplets.xml @@ -1,12 +1,12 @@ - - + - Some tuplets (3:2, 3:2, 3:2, 4:2, - 4:1, 7:3, 6:2) with the default tuplet bracket displaying the number - of actual notes played. The second tuplet does not have a number + Some tuplets (3:2, 3:2, 3:2, 4:2, + 4:1, 7:3, 6:2) with the default tuplet bracket displaying the number + of actual notes played. The second tuplet does not have a number attribute set. diff --git a/test-data/musicxml-testsuite/23b-Tuplets-Styles.png b/test-data/musicxml-testsuite/23b-Tuplets-Styles.png new file mode 100644 index 000000000..64c32346a Binary files /dev/null and b/test-data/musicxml-testsuite/23b-Tuplets-Styles.png differ diff --git a/test-data/musicxml-testsuite/23b-Tuplets-Styles.xml b/test-data/musicxml-testsuite/23b-Tuplets-Styles.xml index c2a7678a8..da108ec41 100644 --- a/test-data/musicxml-testsuite/23b-Tuplets-Styles.xml +++ b/test-data/musicxml-testsuite/23b-Tuplets-Styles.xml @@ -1,12 +1,13 @@ - - + - Different tuplet styles: - default, none, x:y, x:y-note; Each with bracket, slur and none. - Finally, non-standard 4:3 and 17:2 tuplets are given. + Different tuplet styles: default, + none, x:y, x:y-note, and x-note:y-note; each with bracket, slur, and + without bracket. Finally, non-standard 4:3 and 17:2 tuplets are + given. @@ -260,6 +261,7 @@ + C @@ -488,6 +490,7 @@ + C @@ -716,6 +719,7 @@ + C diff --git a/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.png b/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.png new file mode 100644 index 000000000..aeb3ee798 Binary files /dev/null and b/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.png differ diff --git a/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.xml b/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.xml index c0949e4c4..3af9a250b 100644 --- a/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.xml +++ b/test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.xml @@ -1,21 +1,26 @@ - - + - Displaying tuplet note types, - that might not coincide with the displayed note. The first two tuplets - take the type from the note, the second two from the - <time-modification> element, the remaining pair of tuplets from the - <tuplet> notation element. The tuplets in measure 3 specify both - a number of notes and a type inside the <tuplet-actual> and - <tuplet-normal> elements, the ones in measure 4 specify only a - note type (but no number), and the ones in measure 5 specify only a - number of tuplet-notes (but no type, which is deduced from the - note's type). The first tuplet of measures 3-5 uses - 'display-type="actual"', the second one 'display-type="both"'. - FIXME: The tuplet-normal should coincide with the real notes! + Displaying tuplet note types that + might not coincide with the displayed note. The first two tuplets + take the type from the note, the second two from the + <time-modification> element, the remaining pair of tuplets from + the <tuplet> notation element. + + The tuplets in measure 3 specify both a number of notes and a type + inside the <tuplet-actual> and <tuplet-normal> elements, + the ones in measure 4 specify only a note type (but no number), and + the ones in measure 5 specify only a number of tuplet notes (but no + type, which is deduced from the note's type). + + The first tuplet of measures 3 to 5 uses ‘display-type="actual"’, the + second one ‘display-type="both"’. + + FIXME: The tuplet-normal should coincide with the real + notes! @@ -259,7 +264,8 @@ breve - + 7 quarter @@ -321,7 +327,8 @@ - + 7 half @@ -374,6 +381,7 @@ + C @@ -388,7 +396,8 @@ breve - + quarter @@ -448,7 +457,8 @@ - + half @@ -565,7 +575,8 @@ 2 - + 7 eighth diff --git a/test-data/musicxml-testsuite/23d-Tuplets-Nested.png b/test-data/musicxml-testsuite/23d-Tuplets-Nested.png new file mode 100644 index 000000000..208c5b982 Binary files /dev/null and b/test-data/musicxml-testsuite/23d-Tuplets-Nested.png differ diff --git a/test-data/musicxml-testsuite/23d-Tuplets-Nested.xml b/test-data/musicxml-testsuite/23d-Tuplets-Nested.xml index 8daf36345..3df5c4cbe 100644 --- a/test-data/musicxml-testsuite/23d-Tuplets-Nested.xml +++ b/test-data/musicxml-testsuite/23d-Tuplets-Nested.xml @@ -1,12 +1,14 @@ - - + - Tuplets can be nested. Here - there is a 5:2 tuplet inside a 3:2 tuple (all consisting of written - eighth notes). + Tuplets can be nested. The first + bar contains a 5:2 tuplet (with 16th notes) in the middle of a 3:2 + tuple (with eighth notes). The second bar has a 5:2 tuplet at the + beginning and at the end (with the tuplet number forced below) of a + 3:2 tuple (with the bracket forced above). @@ -47,7 +49,7 @@ begin - + @@ -72,21 +74,22 @@ 4 1 - eighth + 16th 15 4 begin + begin - + 5 - eighth + 16th - 2 - eighth + 4 + 16th @@ -98,7 +101,7 @@ 4 1 - eighth + 16th 15 4 @@ -112,7 +115,7 @@ 4 1 - eighth + 16th 15 4 @@ -126,7 +129,7 @@ 4 1 - eighth + 16th 15 4 @@ -140,12 +143,13 @@ 4 1 - eighth + 16th 15 4 end + end @@ -183,6 +187,230 @@ + + + + + + B + 4 + + 4 + 1 + 16th + + 15 + 4 + + begin + begin + + + + 3 + quarter + + + 2 + quarter + + + + + 5 + 16th + + + 4 + 16th + + + + + + + B + 4 + + 4 + 1 + 16th + + 15 + 4 + + continue + continue + + + + B + 4 + + 4 + 1 + 16th + + 15 + 4 + + continue + continue + + + + B + 4 + + 4 + 1 + 16th + + 15 + 4 + + continue + continue + + + + B + 4 + + 4 + 1 + 16th + + 15 + 4 + + end + end + + + + + + + B + 4 + + 10 + 1 + eighth + + 3 + 2 + quarter + + begin + + + + B + 4 + + 10 + 1 + eighth + + 3 + 2 + quarter + + end + + + + B + 4 + + 4 + 1 + 16th + + 15 + 4 + + begin + begin + + + + 5 + 16th + + + 4 + 16th + + + + + + + B + 4 + + 4 + 1 + 16th + + 15 + 4 + + continue + continue + + + + B + 4 + + 4 + 1 + 16th + + 15 + 4 + + continue + continue + + + + B + 4 + + 4 + 1 + 16th + + 15 + 4 + + continue + continue + + + + B + 4 + + 4 + 1 + 16th + + 15 + 4 + + end + end + + + + + light-heavy diff --git a/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.png b/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.png new file mode 100644 index 000000000..9c191ac5c Binary files /dev/null and b/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.png differ diff --git a/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.xml b/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.xml index eee4d0f27..2cd076b8b 100644 --- a/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.xml +++ b/test-data/musicxml-testsuite/23e-Tuplets-Tremolo.xml @@ -1,12 +1,15 @@ - - + - Tremolo tuplets are tuplets on - single notes with a tremolo ornament. The application shall correctly - import these notes with 2/3 or their time... + Tremolo tuplets. The first bar + contains normal eighth triplets with staccato points, the second bar + holds three tremolo tuplets, the third bar holds a sextuple followed + by a triplet, the third bar contains a sextuple (starting on the + second beat) with a ‘fp’ sign, and the fifth bar is identical to the + third bar. diff --git a/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.png b/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.png new file mode 100644 index 000000000..4a280adea Binary files /dev/null and b/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.png differ diff --git a/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.xml b/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.xml index 866d8dc9b..f29df2a3e 100644 --- a/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.xml +++ b/test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.xml @@ -1,204 +1,310 @@ - - - - - - /usr/bin/vi - 2007-02-02 - - - Some "triplets" - on the end of the first and in the second staff, using only - <time-modification>, but not explicit tuplet - bracket. Thus, the duration of the notes in the - second staff should be scaled properly in comparison - to staff 1, but no visual indication about the - tuplets is given. - - - - MusicXML Part - - - - - 96 - 0 - - 2 - G2 - F4 - - - F4 - 96 - 1 - quarter - 1 - - - G4 - 96 - 1 - quarter - 1 - - - A4 - 64 - 1 - quarter - 32 - 1 - - - B4 - 64 - 1 - quarter - 32 - 1 - - - C5 - 64 - 1 - quarter - 32 - 1 - - 384 - - A2 - 48 - 2 - eighth - 2 - begin - - - B2 - 48 - 2 - eighth - 2 - end - - - C3 - 32 - 2 - eighth - 32 - 2 - begin - - - D3 - 32 - 2 - eighth - 32 - 2 - continue - - - E3 - 32 - 2 - eighth - 32 - 2 - end - - - A2 - 24 - 2 - 16th - 2 - begin - - - B2 - 24 - 2 - 16th - 2 - continue - - - C3 - 24 - 2 - 16th - 2 - continue - - - D3 - 24 - 2 - 16th - 2 - end - + + + + + + /usr/bin/vi + 2007-02-02 + + + Tuplets without brackets, using + only <time-modification>. The upper staff contains two quarters + followed by a quarter triplet. The lower staff holds two eighths, an + eighths triplet, four 16th notes, and a 16th + sextuplet. + + + + MusicXML Part + + + + + 96 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + F + 4 + + 96 + 1 + quarter + 1 + + + + G + 4 + + 96 + 1 + quarter + 1 + + + + A + 4 + + 64 + 1 + quarter + + 3 + 2 + + 1 + + + + B + 4 + + 64 + 1 + quarter + + 3 + 2 + + 1 + + + + C + 5 + + 64 + 1 + quarter + + 3 + 2 + + 1 + - - E3 - 16 - 2 - 16th - 32 - 2 - begin - - - F3 - 16 - 2 - 16th - 32 - 2 - continue - - - G3 - 16 - 2 - 16th - 32 - 2 - continue - - - A3 - 16 - 2 - 16th - 32 - 2 - continue - - - B3 - 16 - 2 - 16th - 32 - 2 - continue - - - C4 - 16 - 2 - 16th - 32 - 2 - end - - - + 384 + + + + A + 2 + + 48 + 2 + eighth + 2 + begin + + + + B + 2 + + 48 + 2 + eighth + 2 + end + + + + C + 3 + + 32 + 2 + eighth + + 3 + 2 + + 2 + begin + + + + D + 3 + + 32 + 2 + eighth + + 3 + 2 + + 2 + continue + + + + E + 3 + + 32 + 2 + eighth + + 3 + 2 + + 2 + end + + + + A + 2 + + 24 + 2 + 16th + 2 + begin + + + + B + 2 + + 24 + 2 + 16th + 2 + continue + + + + C + 3 + + 24 + 2 + 16th + 2 + continue + + + + D + 3 + + 24 + 2 + 16th + 2 + end + + + + E + 3 + + 16 + 2 + 16th + + 3 + 2 + + 2 + begin + + + + F + 3 + + 16 + 2 + 16th + + 3 + 2 + + 2 + continue + + + + G + 3 + + 16 + 2 + 16th + + 3 + 2 + + 2 + continue + + + + A + 3 + + 16 + 2 + 16th + + 3 + 2 + + 2 + continue + + + + B + 3 + + 16 + 2 + 16th + + 3 + 2 + + 2 + continue + + + + C + 4 + + 16 + 2 + 16th + + 3 + 2 + + 2 + end + + + diff --git a/test-data/musicxml-testsuite/24a-GraceNotes.png b/test-data/musicxml-testsuite/24a-GraceNotes.png new file mode 100644 index 000000000..bd0925229 Binary files /dev/null and b/test-data/musicxml-testsuite/24a-GraceNotes.png differ diff --git a/test-data/musicxml-testsuite/24a-GraceNotes.xml b/test-data/musicxml-testsuite/24a-GraceNotes.xml index a194cc3b3..58fbefa9a 100644 --- a/test-data/musicxml-testsuite/24a-GraceNotes.xml +++ b/test-data/musicxml-testsuite/24a-GraceNotes.xml @@ -1,12 +1,17 @@ - - + - Different kinds of grace notes: - acciaccatura, appoggiatura; beamed grace notes; grace notes with - accidentals; different durations of the grace notes. + Different kinds of grace notes. + First bar: single 1/16 grace note, beamed 1/16 grace notes, + 1/16 appoggiatura, 1/8 appoggiatura. Second bar: slashed single 1/16 + grace note, beamed 1/16 grace notes (with both notes marked as + slashed), 1/16 acciaccatura, 1/16 grace note (without slash) right + before the measure bar. Third bar: no grace note before chord, 1/4 + grace note with sharp, two 1/4 grace notes with flats, 1/16 slashed + grace note before rest. @@ -33,7 +38,7 @@ - + D 5 @@ -51,7 +56,7 @@ quarter - + E 5 @@ -62,7 +67,7 @@ begin - + D 5 @@ -82,16 +87,15 @@ quarter - + D 5 - 1 16th - + @@ -102,15 +106,21 @@ 4 1 quarter + + + - + D 5 1 eighth + + + @@ -120,6 +130,9 @@ 4 1 quarter + + + @@ -143,7 +156,7 @@ quarter - + E 5 @@ -154,7 +167,7 @@ begin - + D 5 @@ -169,9 +182,9 @@ C 5 - 8 + 4 1 - half + quarter @@ -181,38 +194,33 @@ 1 16th + + + C 5 - 2 - 1 - eighth - begin - - - - - D - 5 - + 4 1 - 16th + quarter + + + C 5 - 2 + 4 1 - eighth - end + quarter - + E 5 @@ -223,15 +231,6 @@ - - - - E - 5 - - 1 - 16th - F @@ -303,10 +302,16 @@ quarter + - C + D 5 + 1 + 16th + + + 4 1 quarter diff --git a/test-data/musicxml-testsuite/24b-ChordAsGraceNote.png b/test-data/musicxml-testsuite/24b-ChordAsGraceNote.png new file mode 100644 index 000000000..bd5b0d59b Binary files /dev/null and b/test-data/musicxml-testsuite/24b-ChordAsGraceNote.png differ diff --git a/test-data/musicxml-testsuite/24b-ChordAsGraceNote.xml b/test-data/musicxml-testsuite/24b-ChordAsGraceNote.xml index 1f00a6f2c..ef2d46d6e 100644 --- a/test-data/musicxml-testsuite/24b-ChordAsGraceNote.xml +++ b/test-data/musicxml-testsuite/24b-ChordAsGraceNote.xml @@ -1,10 +1,13 @@ - - + - Chords as grace notes. + Chords as grace notes. The last + (unslashed and beamed) grace group consists of two chords with one + tie between the two grace chords and another tie between the last + grace chord and the main chord. @@ -105,6 +108,79 @@ 1 quarter + + + + B + 4 + + 1 + 16th + begin + begin + + + + + + D + 5 + + 1 + 16th + + + + + + + + G + 4 + + 1 + 16th + end + end + + + + + + + + + D + 5 + + 1 + 16th + + + + + + + G + 4 + + 2 + 1 + quarter + + + + + + + + C + 5 + + 2 + 1 + quarter + light-heavy diff --git a/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.png b/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.png new file mode 100644 index 000000000..0aabd58ca Binary files /dev/null and b/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.png differ diff --git a/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.xml b/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.xml index ba2c165c4..f1e4ad079 100644 --- a/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.xml +++ b/test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.xml @@ -1,11 +1,11 @@ - - + - A grace note that appears at the - measure end (without any steal-from-* attribute set). Some + A grace note that appears at the + measure end (without any steal-from-* attribute set). Some applications need to convert this into an after-grace. @@ -16,7 +16,7 @@ - + 32 @@ -53,7 +53,7 @@ 1 - + G 5 @@ -65,7 +65,7 @@ begin - + A 5 diff --git a/test-data/musicxml-testsuite/24d-AfterGrace.png b/test-data/musicxml-testsuite/24d-AfterGrace.png new file mode 100644 index 000000000..fb91cd135 Binary files /dev/null and b/test-data/musicxml-testsuite/24d-AfterGrace.png differ diff --git a/test-data/musicxml-testsuite/24d-AfterGrace.xml b/test-data/musicxml-testsuite/24d-AfterGrace.xml index 9e5f3136c..2be74d4d7 100644 --- a/test-data/musicxml-testsuite/24d-AfterGrace.xml +++ b/test-data/musicxml-testsuite/24d-AfterGrace.xml @@ -1,11 +1,13 @@ - - + Some grace notes and after-graces - (indicated by steal-time-previous and steal-time-following). + indicated by ‘steal-time-previous’ (for the first grace note) and + ‘steal-time-following’ (for the second one). The remaining grace + notes have no such attribute. @@ -15,7 +17,7 @@ - + 32 @@ -42,7 +44,7 @@ 1 - + G 5 @@ -52,7 +54,7 @@ 1 - + A 5 @@ -62,7 +64,7 @@ 1 - + A 5 @@ -82,7 +84,7 @@ 1 - + G 5 @@ -94,7 +96,7 @@ begin - + A 5 diff --git a/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.png b/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.png new file mode 100644 index 000000000..4388bd389 Binary files /dev/null and b/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.png differ diff --git a/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.xml b/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.xml index 9470c0cb6..43ba1df4e 100644 --- a/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.xml +++ b/test-data/musicxml-testsuite/24e-GraceNote-StaffChange.xml @@ -1,11 +1,11 @@ - - + - A grace note on a different - staff than the actual note. + Grace notes on a different + staff than the actual notes. @@ -15,7 +15,7 @@ - + 32 @@ -26,11 +26,22 @@ 4 4 + 2 G 2 + + + + G + 5 + + 1 + 16th + 2 + E @@ -42,7 +53,7 @@ 1 - + G 5 @@ -54,7 +65,7 @@ begin - + A 5 @@ -75,6 +86,15 @@ half 1 + + 128 + + + + 128 + 2 + 2 + diff --git a/test-data/musicxml-testsuite/24f-GraceNote-Slur.png b/test-data/musicxml-testsuite/24f-GraceNote-Slur.png new file mode 100644 index 000000000..2081bd427 Binary files /dev/null and b/test-data/musicxml-testsuite/24f-GraceNote-Slur.png differ diff --git a/test-data/musicxml-testsuite/24f-GraceNote-Slur.xml b/test-data/musicxml-testsuite/24f-GraceNote-Slur.xml index 91d085ed3..ab3b42762 100644 --- a/test-data/musicxml-testsuite/24f-GraceNote-Slur.xml +++ b/test-data/musicxml-testsuite/24f-GraceNote-Slur.xml @@ -1,12 +1,14 @@ - - + A grace note with a slur to the - actual note. This can be interpreted as acciaccatura or appoggiatura, - depending on the existence of a slash. + actual note. The <grace> element has no ‘slash’ attribute; + since MusicXML does not provide a default value it is up to the + application to interpret the grace note as an acciaccatura or an + appoggiatura. diff --git a/test-data/musicxml-testsuite/24g-GraceNote-Dynamics.png b/test-data/musicxml-testsuite/24g-GraceNote-Dynamics.png new file mode 100644 index 000000000..ad16def7c Binary files /dev/null and b/test-data/musicxml-testsuite/24g-GraceNote-Dynamics.png differ diff --git a/test-data/musicxml-testsuite/24g-GraceNote-Dynamics.xml b/test-data/musicxml-testsuite/24g-GraceNote-Dynamics.xml new file mode 100644 index 000000000..64bbd1dce --- /dev/null +++ b/test-data/musicxml-testsuite/24g-GraceNote-Dynamics.xml @@ -0,0 +1,124 @@ + + + + + + Grace notes in combination with + dynamics. The ‘f’ sign is located on the first grace note (using a + <direction> element), followed by a diminuendo wedge, and the + ‘p’ sign is on the main beat (again using + <direction>). + + + + + MusicXML Part + + + + + + + 4 + + 0 + major + + + + G + 2 + + + + + + + + + 1 + + + + + + 1 + + + + + G + 5 + + 1 + 16th + begin + begin + begin + + + + + F + 5 + + 1 + 16th + continue + continue + continue + + + + + E + 5 + + 1 + 16th + continue + continue + continue + + + + + D + 5 + + 1 + 16th + end + end + end + + + + + + + + + +

+ + + 1 + + + + C + 5 + + 4 + 1 + quarter + + + + + diff --git a/test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.png b/test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.png new file mode 100644 index 000000000..9bf532ac3 Binary files /dev/null and b/test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.png differ diff --git a/test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.xml b/test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.xml new file mode 100644 index 000000000..a50e0bbef --- /dev/null +++ b/test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.xml @@ -0,0 +1,188 @@ + + + + + + Simultaneous grace notes and grace + note groups of different length starting a part or a voice. + + The topmost three staves are voices one to three of part ‘P1’, with no + grace notes in the third staff and music starting on the second beat. + There is also an ‘fp’ on the main note of the first staff. + + The bottommost two staves are voices one and two of part ‘P2’, with no + grace notes in the lowest staff. + + + + + Part 1 + + + Part 2 + + + + + + + 32 + + 0 + major + + + 3 + + G + 2 + + + G + 2 + + + G + 2 + + + + + + G + 5 + + 1 + 16th + 1 + + + + + + + + + + + E + 5 + + 64 + 1 + half + 1 + + + 64 + + + + + G + 5 + + 2 + 16th + 2 + begin + begin + + + + + A + 5 + + 2 + 16th + 2 + end + end + + + + E + 5 + + 64 + 2 + half + 2 + + + 32 + + + + E + 5 + + 32 + 3 + quarter + 3 + + + + + + + + 32 + + 0 + major + + + 2 + + G + 2 + + + G + 2 + + + + + + G + 5 + + 1 + quarter + 1 + + + + E + 5 + + 64 + 1 + half + 1 + + + 64 + + + + 64 + 2 + half + 2 + + + + + diff --git a/test-data/musicxml-testsuite/31a-Directions.png b/test-data/musicxml-testsuite/31a-Directions.png new file mode 100644 index 000000000..b1d9ffc85 Binary files /dev/null and b/test-data/musicxml-testsuite/31a-Directions.png differ diff --git a/test-data/musicxml-testsuite/31a-Directions.xml b/test-data/musicxml-testsuite/31a-Directions.xml index 4e1412426..6c7582142 100644 --- a/test-data/musicxml-testsuite/31a-Directions.xml +++ b/test-data/musicxml-testsuite/31a-Directions.xml @@ -1,13 +1,15 @@ - - + MusicXML directions (attached to staff) - All <direction> elements - defined in MusicXML. The lyrics for each note describes the direction - element assigned to that note. + All <direction> elements + defined in MusicXML. The lyrics for each note describes the direction + element assigned to that note. Not marked with lyrics is a + <scordatura> element at the very + beginning. @@ -18,13 +20,33 @@ - + rehearsal+ | segno+ | (words | symbol)+ | + coda+ | wedge | dynamics+ | dashes | bracket | pedal | + metronome | octave-shift | harp-pedals | damp | + damp-all | eyeglasses | string-mute | scordatura | image | + principal-voice | percussion+ | accordion-registration | + staff-divide | other-direction --> + + + + + + + C + 3 + + + G + 5 + + + E + 5 + + + + 1 @@ -50,7 +72,8 @@ 1 1 quarter - reh.A (def=sq.) + reh. A↓ + (def=square) @@ -62,11 +85,12 @@ 1 1 quarter - reh.B (none) + reh. B↑ + (none) - Test + Test @@ -74,7 +98,8 @@ 1 1 quarter - reh.Test (sq.) + reh. Test + (rect.) @@ -86,10 +111,11 @@ 1 1 quarter - reh.Crc (crc.) + reh. Crc + (circle) - + @@ -101,7 +127,7 @@ 1 1 quarter - Segno + segno @@ -113,11 +139,11 @@ 1 1 quarter - Coda + coda - + - words + \"words" @@ -125,11 +151,12 @@ 1 1 quarter - Words + words↑ + (oval) - + cClef @@ -137,14 +164,16 @@ 1 1 quarter - Eyegl. + symbol + ("cClef") + f | ff | fff | ffff | fffff | ffffff | + mp | mf | sf | sfp | sfpp | fp | rf | + rfz | sfz | sffz | fz | other-dynamics --> +

@@ -195,7 +224,7 @@ - + @@ -205,9 +234,9 @@ 1 1 quarter - ppppp + ppppp↓ - + @@ -217,7 +246,7 @@ 1 1 quarter - pppppp + pppppp↑ @@ -269,7 +298,7 @@ quarter ffff - + @@ -279,9 +308,9 @@ 1 1 quarter - fffff + fffff↓ - + @@ -291,10 +320,11 @@ 1 1 quarter - ffffff + ffffff↑ + @@ -433,7 +463,7 @@ - abc-ffz + ffz @@ -441,12 +471,14 @@ 1 1 quarter - abc-ffz (oth.) + other dyn. + ('ffz') + @@ -457,7 +489,8 @@ 1 1 quarter - beginhairpin + beginhair + begin(cre @@ -469,9 +502,10 @@ 1 1 quarter - endcresc + endpin + endscendo) - + @@ -493,13 +527,13 @@ 1 1 quarter - endes + endes↓ - + - + @@ -509,9 +543,9 @@ quarter beginbra - + - + @@ -519,7 +553,7 @@ 1 1 quarter - endcket + endcket↓ @@ -532,6 +566,7 @@ 1 quarter beginoct. + (8 @@ -544,12 +579,13 @@ 1 quarter endshift + up) - + @@ -561,7 +597,7 @@ - + @@ -579,7 +615,7 @@ - + @@ -594,7 +630,10 @@ - quarter60 + + quarter + 60 + @@ -602,7 +641,8 @@ 1 1 quarter - Metr. + metro- + nome @@ -643,7 +683,8 @@ 1 1 quarter - Harp ped. + harp + pedal @@ -655,7 +696,7 @@ 1 1 quarter - Damp + damp @@ -667,17 +708,18 @@ 1 1 quarter - Damp all + damp + all - - C3 - G5 - E5 - + + + 2 + + @@ -685,15 +727,12 @@ 1 1 quarter - Scord. + accordion + register - - - 2 - - + @@ -701,85 +740,111 @@ 1 1 quarter - Accordion reg. + string + mute on + + + + + - - 2 + C4 + 1 1 - half + quarter + string + mute off + + + + + + + + C4 + 1 + 1 + quarter + eye- + glasses - - light-light - - - - subito - - -   - + - -

- + + + - 2 C4 1 1 quarter - subp + perc. + (timpani) - - - - + - + - 2 C4 1 1 quarter - beginppp cresc + staff- + divide - + - + - - - - 2 C4 1 1 quarter - endto fff + beginprincipal + begin(Haupt + + + + + - + C4 1 1 quarter - subp + endvoice + endstimme) + + + + + + + + + + + C4 + 4 + 1 + whole + image light-heavy - - diff --git a/test-data/musicxml-testsuite/31b-Directions-Order.png b/test-data/musicxml-testsuite/31b-Directions-Order.png new file mode 100644 index 000000000..272b55332 Binary files /dev/null and b/test-data/musicxml-testsuite/31b-Directions-Order.png differ diff --git a/test-data/musicxml-testsuite/31b-Directions-Order.xml b/test-data/musicxml-testsuite/31b-Directions-Order.xml new file mode 100644 index 000000000..cc3318f87 --- /dev/null +++ b/test-data/musicxml-testsuite/31b-Directions-Order.xml @@ -0,0 +1,102 @@ + + + + + + Using <offset> it is + possible to make successive <direction> elements look like being + concatenated. However, it is a bad idea in general to do that because + it makes the rendering dependent on a program's score formatting. + + In the first bar, the first <direction> element contains a tempo + mark and is directly followed by another <direction> element + containing a metronome mark that is moved to the right by a positive + <offset> element. + + In the second bar, the first <direction> element holds the start + of a diminuendo wedge; it is directly followed by another + <direction> element containing a dynamics mark. Since the + wedge has an <offset> element with a positive value while the + dynamics has no such element at all, the dynamics precedes the + wedge. + + + + + + MusicXML Part + + + + + + + 8 + + 0 + major + + + + G + 2 + + + + + Lento + + + + + + quarter + 56 + + + 4 + + + C4 + 32 + 1 + whole + + + + + + + + + 8 + + + + + + + + + + C4 + 32 + 1 + whole + + + + + + + + light-heavy + + + + diff --git a/test-data/musicxml-testsuite/31c-MetronomeMarks.png b/test-data/musicxml-testsuite/31c-MetronomeMarks.png new file mode 100644 index 000000000..9134835cb Binary files /dev/null and b/test-data/musicxml-testsuite/31c-MetronomeMarks.png differ diff --git a/test-data/musicxml-testsuite/31c-MetronomeMarks.xml b/test-data/musicxml-testsuite/31c-MetronomeMarks.xml index b2b42aae3..e84ea483c 100644 --- a/test-data/musicxml-testsuite/31c-MetronomeMarks.xml +++ b/test-data/musicxml-testsuite/31c-MetronomeMarks.xml @@ -1,11 +1,12 @@ - - + - Tempo Markings: note=bpm, - text (note=bpm), note=note, (note=note), (note=bpm) + Tempo Markings: ‘quarter.=100’, + ‘quarter..=half.’, ‘(quarter.=half..)’, + ‘(quarter.=77)’. @@ -52,37 +53,18 @@ 1 quarter - - - Adagio - - - - long - 100 - - - C5 1 1 quarter - - C5 - 1 - 1 - quarter - - - - quarter + half @@ -94,21 +76,9 @@ 1 quarter - - C5 - 1 - 1 - quarter - - - - - long - 32nd - - - - + + + C5 1 @@ -121,9 +91,6 @@ 1 quarter - - - @@ -131,6 +98,7 @@ half + @@ -146,6 +114,15 @@ 1 quarter + + + + + C5 + 1 + 1 + quarter + @@ -167,6 +144,12 @@ 1 quarter + + C5 + 1 + 1 + quarter + light-heavy diff --git a/test-data/musicxml-testsuite/31d-Directions-Compounds.png b/test-data/musicxml-testsuite/31d-Directions-Compounds.png new file mode 100644 index 000000000..da611e339 Binary files /dev/null and b/test-data/musicxml-testsuite/31d-Directions-Compounds.png differ diff --git a/test-data/musicxml-testsuite/31d-Directions-Compounds.xml b/test-data/musicxml-testsuite/31d-Directions-Compounds.xml new file mode 100644 index 000000000..64b4fb012 --- /dev/null +++ b/test-data/musicxml-testsuite/31d-Directions-Compounds.xml @@ -0,0 +1,266 @@ + + + + MusicXML compound directions + + + This tests various combinations of + <direction> children. The lyrics for each note describe the + compound elements assigned to that note. + + The first ‘molto f’ uses <words>molto <words>, i.e., it + omits the ‘xml:space’ attribute. This makes the rendering of the + space between ‘molto’ and ‘f’ implementation-dependent (and might be + thus omitted). The second ‘molto f’, together with ‘meno f’, uses + ‘xml:space="preserve"’. + + For demonstration purposes, there is no space between words ‘bold’ and + ‘italic’. + + + + + MusicXML Part + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + Adagio + + + + long + 100 + + + + + C4 + 2 + 1 + half + Adagio + long=100 + + + + molto + + + + + + + + + C4 + 2 + 1 + half + molto↓ + f + + + + + + + +

+ + + + subito + + + + C4 + 2 + 1 + half + p↓ + subito + + + + molto + + + + + + + + + C4 + 2 + 1 + half + molto f↑ + (rectangle) + + + + + + + + + + + + + + C4 + 2 + 1 + half + beginppp↓ + begincresc. + + + + + + + + + + + C4 + 2 + 1 + half + endto + endfff↓ + + + + + + + + cresc. + + + + + + + C4 + 2 + 1 + half + begincresc.↑ + begindashes + + + + + + + meno + + + + + + + + + C4 + 2 + 1 + half + endto + endmeno f↑ + + + + + + + 12 + bis + + + + C4 + 2 + 1 + half + reh. 12+bis + (square) + + + + bold + italic + + + + C4 + 2 + 1 + half + bold+ + italic + + + + + + + + + + + C4 + 2 + 1 + half + segno + + + + + + + + + C4 + 2 + 1 + half + coda + ×2 + + + light-heavy + + + + diff --git a/test-data/musicxml-testsuite/32a-Notations.png b/test-data/musicxml-testsuite/32a-Notations.png new file mode 100644 index 000000000..dc0f11d8f Binary files /dev/null and b/test-data/musicxml-testsuite/32a-Notations.png differ diff --git a/test-data/musicxml-testsuite/32a-Notations.xml b/test-data/musicxml-testsuite/32a-Notations.xml index ab92216ff..80b743bde 100644 --- a/test-data/musicxml-testsuite/32a-Notations.xml +++ b/test-data/musicxml-testsuite/32a-Notations.xml @@ -1,13 +1,16 @@ - - + MusicXML notations (attached to note) - All <notation> elements - defined in MusicXML. The lyrics show the notation assigned to each - note. + All <notation> elements + defined in MusicXML. The lyrics show the notation assigned to each + note. + + The third-last bar is a full-measure rest with a + fermata. @@ -41,7 +44,7 @@ - ferm. + fermata @@ -53,7 +56,8 @@ normal - normal ferm. + normal + fermata @@ -65,7 +69,8 @@ angled - angled ferm. + angled + fermata @@ -77,9 +82,11 @@ square - square ferm. + square + fermata + @@ -91,7 +98,8 @@ - inv.ferm. + inverted + fermata @@ -101,7 +109,7 @@ 1 quarter - arp. + arpeggio @@ -131,7 +139,8 @@ 1 quarter - non-arp. + non- + arpeggio @@ -162,14 +171,15 @@ double-sharp - acc.mark + accidental + mark light-light - @@ -235,7 +247,8 @@ - det.-leg. + detached + legato @@ -247,7 +260,8 @@ - stacc.ss + stacca- + tissimo @@ -259,7 +273,7 @@ - spicc. + spiccato @@ -274,6 +288,7 @@ scoop + @@ -322,8 +337,10 @@ breath + mark + @@ -335,7 +352,7 @@ - caes. + caesura @@ -359,7 +376,8 @@ - unstr. + un- + stress @@ -374,11 +392,11 @@ @@ -390,9 +408,12 @@ 1 quarter - + + + - tr. + trill + mark @@ -416,7 +437,8 @@ - del.turn + delayed + turn @@ -428,9 +450,11 @@ - inv.turn + inverted + turn + @@ -456,7 +480,7 @@ - beginwavy + beginwa @@ -467,11 +491,10 @@ quarter - - + - middlewavy + middlevy @@ -488,6 +511,7 @@ endline + @@ -499,7 +523,7 @@ - mord. + mordent @@ -511,7 +535,8 @@ - inv.mord. + inverted + mordent @@ -523,7 +548,7 @@ - schl. + schleifer @@ -535,9 +560,10 @@ 3 - trem. + tremolo + @@ -549,10 +575,11 @@ - natural + natural - turn+acc. + turn + acc. + mark ↑ @@ -566,16 +593,46 @@ sharp - three-quarters-flat + three-quarters-flat - turn+acc.(ab.+bel./rel to turn) + turn + acc. + marks ↑↓ - - 2 + + C + 5 + + 1 1 - half + quarter + + + + sharp + double-sharp + + + turn + acc. + marks ↑[↑] + + + + C5 + + 1 + 1 + quarter + + + + sharp + + + trill + acc. + mark ↑ light-light @@ -599,7 +656,8 @@ - up-b. + up- + bow @@ -611,119 +669,252 @@ - down-b. + down- + bow - C5 + G5 1 1 quarter - + + + - harm. + harmonic - C5 + G5 1 1 quarter - + + + - nat.harm. + natural + harmonic + - C5 + A5 1 1 quarter - + + + - art.harm. + artificial + harmonic - C5 + G3 1 1 quarter - + + + - nat.h./base + natural + harmonic + (base+touch) + - C5 + C4 1 1 quarter - + + + - nat.h./touching + + + + C4 + + 1 + 1 + quarter + + + + + + natural + harmonic + (touch) - C5 + C4 1 1 quarter - + + + + + natural + harmonic + (touch+sound) + + + + + G5 + + 1 + 1 + quarter + + + + - nat.h./sounding + - C5 + G3 1 1 quarter - + + + - open-str. + nat. harm. + (base+touch + +sound) + - C5 + C4 1 1 quarter - + + + - thumb-pos. + - C5 + G5 + + 1 + 1 + quarter + + + + + + + + + A3 + + 1 + 1 + quarter + + + + + + artificial + harmonic + (base+touch) + + + + + D4 + + 1 + 1 + quarter + + + + + + + + + A3 + + 1 + 1 + quarter + + + + + + art. harm. + (base+touch + +sound) + + + + + D4 + + 1 + 1 + quarter + + + + + + + + + + A5 1 1 quarter - + + + - empty fing. @@ -733,11 +924,13 @@ 1 quarter - 1 + - fing.1 + open + string + @@ -747,9 +940,10 @@ 1 quarter - 2 + - fing.2 + thumb- + position @@ -759,9 +953,10 @@ 1 quarter - 3 + 1 - fing.3 + fingering + 1 @@ -771,35 +966,46 @@ 1 quarter - 4 + 2 - fing.4 + fingering + 2,3,4,5 + - C5 + D5 1 1 quarter - 5 + 3 - fing.5 - - + - C5 + E5 1 1 quarter - something + 4 + + + + + + F5 + + 1 + 1 + quarter + + 5 - fing.sth. @@ -809,10 +1015,22 @@ 1 quarter - 532 + + 0 + 1 + 2 + 3 + 4 + 5 + - mult.fing. + fingering + 0,1,2↑ + 3,4,5↓ + + + C5 @@ -821,9 +1039,13 @@ 1 quarter - + + 2 + 3 + - empty pluck + fingering + 2, subst. 3 @@ -833,12 +1055,14 @@ 1 quarter - a + + 2 + 3 + - pluck a + fingering + 2, alt. 3 - - C5 @@ -847,9 +1071,10 @@ 1 quarter - + a - dbl.tng. + pluck + a @@ -859,10 +1084,18 @@ 1 quarter - + + a + m + i + - trpl.tng. + pluck + a,m,i↓ + + + C5 @@ -871,9 +1104,10 @@ 1 quarter - + - stopped + double + tongue @@ -883,12 +1117,11 @@ 1 quarter - + - snp.pizz. + triple + tongue - - C5 @@ -897,9 +1130,9 @@ 1 quarter - + - empty fret + stopped @@ -909,10 +1142,14 @@ 1 quarter - 0 + - fret0 + snap- + pizz. + + + C5 @@ -921,9 +1158,10 @@ 1 quarter - + 0 - empty str. + fret + 0 @@ -937,10 +1175,9 @@ 5 - str. 5 + string + 5 - - C5 @@ -969,6 +1206,9 @@ endon + + + C5 @@ -997,8 +1237,6 @@ endoff - - C5 @@ -1011,7 +1249,7 @@ 4 - bend + bend 4 @@ -1025,8 +1263,12 @@ 3 - b.3 with-bar + bend 3 + with bar + + + C5 @@ -1039,7 +1281,8 @@ -0.5 - pre-b. -0.5 + bend -0.5 + pre-bend @@ -1053,10 +1296,9 @@ 3.5 - b. release 3.5 + bend 3.5 + release - - C5 @@ -1079,8 +1321,12 @@ T - tap T + tap + T + + + C5 @@ -1105,8 +1351,6 @@ toe - - C5 @@ -1117,7 +1361,8 @@ - fingern. + finger- + nails @@ -1125,31 +1370,26 @@ 1 quarter - - - 2 - 1 - half - light-light - - - + + + - - C5 - - 1 + + 4 1 - quarter + whole - + - f + + + C5 @@ -1158,9 +1398,9 @@ 1 quarter - + - ppp + f @@ -1178,16 +1418,17 @@ C5 - 1 + 2 1 - quarter + half sfffz - Oth.dyn. + other + dynamics - + @@ -1204,7 +1445,8 @@ - both above + strong ↑ + stacc. ↑ @@ -1221,7 +1463,9 @@ - ab./bel./bel. + acc. ↓ + ten. ↓ + stacc. ↑ diff --git a/test-data/musicxml-testsuite/32b-Articulations-Texts.png b/test-data/musicxml-testsuite/32b-Articulations-Texts.png new file mode 100644 index 000000000..bdc1fe35a Binary files /dev/null and b/test-data/musicxml-testsuite/32b-Articulations-Texts.png differ diff --git a/test-data/musicxml-testsuite/32b-Articulations-Texts.xml b/test-data/musicxml-testsuite/32b-Articulations-Texts.xml index 15dc26c8c..216c79787 100644 --- a/test-data/musicxml-testsuite/32b-Articulations-Texts.xml +++ b/test-data/musicxml-testsuite/32b-Articulations-Texts.xml @@ -1,96 +1,108 @@ - - - - - - Text markup: - different font sizes, weights and colors. - - - - - - - - - 96 - 0 - - 1 - G2 - - - - - Normal, Medium - - - - - F4 - 384 - 1 - eighth - - - - - Bold, Medium - - - - - - - - - Normal, Large - - - - - G4 - 384 - 1 - whole - - - - - Bold, Large - - - - - - - - - Normal, Small - - - - - F4 - 384 - 1 - whole - - - - - Bold, Small - - - - - - - Normal, Small, Colored, Below - - - - - + + + + + + Text markup with different CSS + font sizes, weights, horizontal positions (using ‘default-x’), and + vertical positions (using ‘default-y’). Strings below the staff are + positioned right before the <measure>; the last one is also + drawn in red. + + + + + + + + + + + 96 + + 0 + + + 1 + + G + 2 + + + + + (0,15) normal, medium ↑ + + + + + F + 4 + + 384 + 1 + whole + + + + (-50,-100) bold, medium ↓ + + + + + + + (50,35) normal, large ↑ + + + + + G + 4 + + 384 + 1 + whole + + + + (0,-120) bold, large ↓ + + + + + + + (0,55) normal, small ↑ + + + + + 384 + 1 + whole + + + + (50,-140) bold, small ↓ + + + + + (100,-160) normal, small, red ↓ + + + + diff --git a/test-data/musicxml-testsuite/32c-MultipleNotationChildren.png b/test-data/musicxml-testsuite/32c-MultipleNotationChildren.png new file mode 100644 index 000000000..edfabe129 Binary files /dev/null and b/test-data/musicxml-testsuite/32c-MultipleNotationChildren.png differ diff --git a/test-data/musicxml-testsuite/32c-MultipleNotationChildren.xml b/test-data/musicxml-testsuite/32c-MultipleNotationChildren.xml index 90a172772..6fe7af0ec 100644 --- a/test-data/musicxml-testsuite/32c-MultipleNotationChildren.xml +++ b/test-data/musicxml-testsuite/32c-MultipleNotationChildren.xml @@ -1,12 +1,12 @@ - - + It should not make any difference whether two articulations are given - inside two different notation elements, inside two different articulations + inside two different notation elements, inside two different articulations children of the same notation element or inside the same articulations element. Thus, all three notes should have a staccato and an accent. @@ -42,7 +42,6 @@ 4 1 quarter - sharp @@ -62,7 +61,6 @@ 4 1 quarter - sharp @@ -80,7 +78,6 @@ 4 1 quarter - sharp diff --git a/test-data/musicxml-testsuite/32d-Arpeggio.png b/test-data/musicxml-testsuite/32d-Arpeggio.png new file mode 100644 index 000000000..762e739a6 Binary files /dev/null and b/test-data/musicxml-testsuite/32d-Arpeggio.png differ diff --git a/test-data/musicxml-testsuite/32d-Arpeggio.xml b/test-data/musicxml-testsuite/32d-Arpeggio.xml index 17cea0b7d..b27bc54aa 100644 --- a/test-data/musicxml-testsuite/32d-Arpeggio.xml +++ b/test-data/musicxml-testsuite/32d-Arpeggio.xml @@ -1,11 +1,11 @@ - - + - Different Arpeggio directions - (normal, up, down, non-arpeggiate) + Different arpeggio kinds and + directions. @@ -148,6 +148,9 @@ quarter + + + C4 @@ -186,7 +189,8 @@ 1 quarter - non-arp. + arpeggio + bracket @@ -237,6 +241,70 @@ quarter + + + C4 + + 1 + 1 + quarter + partial + arpeggio + + + + + E5 + + 1 + 1 + quarter + + + + + + G5 + + 1 + 1 + quarter + + + + + + + + C4 + + 1 + 1 + quarter + partial + arpeggio + bracket + + + + + E5 + + 1 + 1 + quarter + + + + + + G5 + + 1 + 1 + quarter + + light-heavy diff --git a/test-data/musicxml-testsuite/33a-Spanners.png b/test-data/musicxml-testsuite/33a-Spanners.png new file mode 100644 index 000000000..bb10043b9 Binary files /dev/null and b/test-data/musicxml-testsuite/33a-Spanners.png differ diff --git a/test-data/musicxml-testsuite/33a-Spanners.xml b/test-data/musicxml-testsuite/33a-Spanners.xml index dfcd0b2a3..949223470 100644 --- a/test-data/musicxml-testsuite/33a-Spanners.xml +++ b/test-data/musicxml-testsuite/33a-Spanners.xml @@ -1,16 +1,17 @@ - - + Several spanners defined in - MusicXML: tuplet, slur (solid, dashed), tie, wedge (cresc, dim), - tr + wavy-line, single-note trill spanner, octave-shift (8va,15mb), - bracket (solid down/down, dashed down/down, solid none/down, - dashed none/up, solid none/none), dashes, glissando (wavy), - bend-alter, slide (solid), grouping, two-note tremolo, hammer-on, - pull-off, pedal (down, change, up). + MusicXML: tuplet, slur (solid, dashed), wedge (cresc, dim), trill + with accidental mark and wavy-line (with another accidental mark on + the second beat), single-note trill spanner, octave-shift (8va,15mb), + bracket (solid down/down, dashed down/down, solid none/down, dashed + none/up, solid none/none), dashes, glissando (wavy), slide (solid), + grouping, two-note tremolo, hammer-on, pull-off, pedal line (down, + change, up), pedal text (down, up). @@ -99,7 +100,7 @@ 1 quarter - + @@ -202,7 +203,7 @@ - + @@ -253,6 +254,7 @@ + sharp @@ -265,6 +267,12 @@ 3 1 quarter + + + + natural + + @@ -293,6 +301,7 @@ quarter + @@ -427,7 +436,7 @@ - + @@ -450,7 +459,7 @@ 1 quarter - + @@ -507,7 +516,7 @@ - + @@ -530,7 +539,7 @@ 1 quarter - + @@ -587,7 +596,7 @@ - + @@ -610,7 +619,7 @@ 1 quarter - + @@ -660,47 +669,6 @@ - - - B - 4 - - 3 - 1 - quarter - - - - 6 - - - - - - - C - 5 - - 3 - 1 - quarter - - - - 0 - - - - - - - 3 - 1 - quarter - - - - B @@ -733,7 +701,7 @@ - + @@ -765,7 +733,7 @@ - + B @@ -773,9 +741,16 @@ 3 1 - quarter + half + + 2 + 1 + + begin - 2 + + 2 + @@ -785,9 +760,16 @@ 3 1 - quarter + half + + 2 + 1 + + end - 2 + + 2 + @@ -798,7 +780,7 @@ - + B4 @@ -833,7 +815,7 @@ - + B4 @@ -868,10 +850,10 @@ - + - + @@ -882,7 +864,7 @@ - + @@ -892,6 +874,37 @@ quarter + + + + + + B4 + 3 + 1 + quarter + + + + + + + + + + + B4 + 3 + 1 + quarter + + + B4 + 3 + 1 + quarter + + diff --git a/test-data/musicxml-testsuite/33b-Spanners-Tie.png b/test-data/musicxml-testsuite/33b-Spanners-Tie.png new file mode 100644 index 000000000..08ffd4925 Binary files /dev/null and b/test-data/musicxml-testsuite/33b-Spanners-Tie.png differ diff --git a/test-data/musicxml-testsuite/33b-Spanners-Tie.xml b/test-data/musicxml-testsuite/33b-Spanners-Tie.xml index 52e1ce9e2..f46bed1e5 100644 --- a/test-data/musicxml-testsuite/33b-Spanners-Tie.xml +++ b/test-data/musicxml-testsuite/33b-Spanners-Tie.xml @@ -1,42 +1,42 @@ - - - - - - Two simple tied whole notes - - - - - - - - - 1 - 0 - - 1 - G2 - - - F4 - 4 - - 1 - whole - - - - - - F4 - 4 - - 1 - whole - - - - + + + + + + Two simple tied whole notes + + + + + + + + + 1 + 0 + + 1 + G2 + + + F4 + 4 + + 1 + whole + + + + + + F4 + 4 + + 1 + whole + + + + diff --git a/test-data/musicxml-testsuite/33c-Spanners-Slurs.png b/test-data/musicxml-testsuite/33c-Spanners-Slurs.png new file mode 100644 index 000000000..19be0f9c2 Binary files /dev/null and b/test-data/musicxml-testsuite/33c-Spanners-Slurs.png differ diff --git a/test-data/musicxml-testsuite/33c-Spanners-Slurs.xml b/test-data/musicxml-testsuite/33c-Spanners-Slurs.xml index b3d9c7f0a..186a340c6 100644 --- a/test-data/musicxml-testsuite/33c-Spanners-Slurs.xml +++ b/test-data/musicxml-testsuite/33c-Spanners-Slurs.xml @@ -1,13 +1,13 @@ - - + - A note can be the end of one - slur and the start of a new slur. Also, in MusicXML, nested slurs + A note can be the end of one + slur and the start of a new slur. Also, in MusicXML, nested slurs are possible like in the second measure where one slur goes over all - four notes, and another slur goes from the second to the third + four notes, and another slur goes from the second to the third note. diff --git a/test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.png b/test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.png new file mode 100644 index 000000000..77f324ed2 Binary files /dev/null and b/test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.png differ diff --git a/test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.xml b/test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.xml new file mode 100644 index 000000000..59efff5cc --- /dev/null +++ b/test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.xml @@ -0,0 +1,203 @@ + + + + + + Finale + + + All types of octave shifts (15ma + on the third eighth note, 15mb on the fourth and fifth, 8va on the + sixth and seventh, and 8vb on the last two 16th notes). This test + file positions <octave-shift type="stop"/>> before the + associated note, as expected in MusicXML import of Finale, for + example. Consequently, it contains ‘Finale’ as the <software> + tag. + + Note that the end of the last octave shift is anchored at the + following bar line. + + + + + MusicXML Part + + + + + + + 8 + + 0 + major + + + + G + 2 + + + + + A + 4 + + 4 + 1 + eighth + begin + + + + C + 5 + + 4 + 1 + eighth + continue + + + + + + -1 + + + + + + 1 + + + + A + 6 + + 4 + 1 + eighth + continue + + + + + + -1 + + + + C + 3 + + 4 + 1 + eighth + end + + + + + + 1 + + + + B + 2 + + 4 + 1 + eighth + begin + + + + + + -1 + + + + A + 5 + + 4 + 1 + eighth + end + + + + + + 1 + + + + A + 5 + + 4 + 1 + eighth + begin + + + + + + -1 + + + + B + 3 + + 2 + 1 + 16th + continue + begin + + + + C + 4 + + 2 + 1 + 16th + end + end + + + + + + -1 + + + + + + + C + 5 + + 32 + 1 + whole + + + light-heavy + + + + + diff --git a/test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.png b/test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.png new file mode 100644 index 000000000..7042f2ff2 Binary files /dev/null and b/test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.png differ diff --git a/test-data/musicxml-testsuite/33d-Spanners-OctaveShifts.xml b/test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.xml similarity index 78% rename from test-data/musicxml-testsuite/33d-Spanners-OctaveShifts.xml rename to test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.xml index f872d463b..b94802ce8 100644 --- a/test-data/musicxml-testsuite/33d-Spanners-OctaveShifts.xml +++ b/test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.xml @@ -1,11 +1,21 @@ - - + + + MuseScore + - All types of octave shifts (15ma, - 15mb, 8va, 8vb) + All types of octave shifts (15ma + on the third eighth note, 15mb on the fourth and fifth, 8va on the + sixth and seventh, and 8vb on the last two 16th notes). This test + file positions <octave-shift type="stop"/>> after the associated + note, as expected in MusicXML import of MuseScore, for example. + Consequently, it contains ‘MuseScore’ as the <software> tag. + + Note that the end of the last octave shift is anchored at the + following bar line. @@ -55,7 +65,7 @@ - -4 + -1 @@ -71,12 +81,13 @@ - -4 + 1 + -1 @@ -102,12 +113,13 @@ - -4 + 1 + -1 @@ -133,12 +145,13 @@ - -3 + 1 + -1 @@ -166,8 +179,20 @@ - -2 + -1 + + + + + + C + 5 + + 32 + 1 + whole + light-heavy diff --git a/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.png b/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.png new file mode 100644 index 000000000..9ef550d9b Binary files /dev/null and b/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.png differ diff --git a/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.xml b/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.xml index a97088bb9..53d477ea5 100644 --- a/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.xml +++ b/test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.xml @@ -1,7 +1,7 @@ - - + Invalid octave-shifts: 27 down, diff --git a/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.png b/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.png new file mode 100644 index 000000000..238b083d4 Binary files /dev/null and b/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.png differ diff --git a/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.xml b/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.xml index 368085011..f86d9ca37 100644 --- a/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.xml +++ b/test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.xml @@ -1,11 +1,11 @@ - - + - A trill spanner that spans a - grace note and ends on an after-grace note at the end of the + A trill spanner that spans a + grace note and ends on an after-grace note at the end of the measure. diff --git a/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.png b/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.png new file mode 100644 index 000000000..16ea6f8ea Binary files /dev/null and b/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.png differ diff --git a/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.xml b/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.xml index 4fa677342..872e9f736 100644 --- a/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.xml +++ b/test-data/musicxml-testsuite/33g-Slur-ChordedNotes.xml @@ -1,13 +1,15 @@ - - + - Slurs on chorded notes: Only the - first note of the chord should get the slur notation. Some - applications print out the slur for all notes -- these should be - ignored. + Slurs on chorded notes. The upper + slur connects the first and third chord; for both the start and end, + the <slur> element is attached not to the first note of the + chord but to the second one (tagged with <chord>). The lower + slur connects the chord on the second beat and the note on fourth beat + and is attached in the normal way. @@ -41,9 +43,6 @@ 1 1 quarter - - - @@ -54,6 +53,9 @@ 1 1 quarter + + + @@ -83,6 +85,9 @@ 1 1 quarter + + + @@ -92,10 +97,6 @@ 1 1 quarter - - - - @@ -107,7 +108,7 @@ 1 quarter - + @@ -119,7 +120,7 @@ 1 quarter - + diff --git a/test-data/musicxml-testsuite/33h-Spanners-Glissando.png b/test-data/musicxml-testsuite/33h-Spanners-Glissando.png new file mode 100644 index 000000000..21e6600df Binary files /dev/null and b/test-data/musicxml-testsuite/33h-Spanners-Glissando.png differ diff --git a/test-data/musicxml-testsuite/33h-Spanners-Glissando.xml b/test-data/musicxml-testsuite/33h-Spanners-Glissando.xml index ab3c99dca..5325a2141 100644 --- a/test-data/musicxml-testsuite/33h-Spanners-Glissando.xml +++ b/test-data/musicxml-testsuite/33h-Spanners-Glissando.xml @@ -1,10 +1,10 @@ - - + - All different types of + All different types of glissando defined in MusicXML diff --git a/test-data/musicxml-testsuite/33i-Ties-NotEnded.png b/test-data/musicxml-testsuite/33i-Ties-NotEnded.png new file mode 100644 index 000000000..dae1a842c Binary files /dev/null and b/test-data/musicxml-testsuite/33i-Ties-NotEnded.png differ diff --git a/test-data/musicxml-testsuite/33i-Ties-NotEnded.xml b/test-data/musicxml-testsuite/33i-Ties-NotEnded.xml index 7ce312c03..6dad2b85f 100644 --- a/test-data/musicxml-testsuite/33i-Ties-NotEnded.xml +++ b/test-data/musicxml-testsuite/33i-Ties-NotEnded.xml @@ -1,7 +1,7 @@ - - + Several ties that have their end tag missing. diff --git a/test-data/musicxml-testsuite/33j-Beams-Tremolos.png b/test-data/musicxml-testsuite/33j-Beams-Tremolos.png new file mode 100644 index 000000000..05bac1781 Binary files /dev/null and b/test-data/musicxml-testsuite/33j-Beams-Tremolos.png differ diff --git a/test-data/musicxml-testsuite/33j-Beams-Tremolos.xml b/test-data/musicxml-testsuite/33j-Beams-Tremolos.xml new file mode 100644 index 000000000..a059967a1 --- /dev/null +++ b/test-data/musicxml-testsuite/33j-Beams-Tremolos.xml @@ -0,0 +1,373 @@ + + + + + + Tests for double-note tremolo + beams. The first bar shows a half-note tremolo (one beam, two + strokes) followed by a dotted quarter-note tremolo with chords + (three strokes). The second bar shows a half-note triplet with + three tremolos (no beams, three strokes) followed by three beamed + eighths-note chords with a tremolo (two strokes) between the second + and third chord. + + + + + MusicXML Part + + + + + + + 12 + + 0 + major + + + + G + 2 + + + + + A + 4 + + 12 + 1 + half + + 2 + 1 + + begin + + + 2 + + + + + + C + 5 + + 12 + 1 + half + + 2 + 1 + + end + + + 2 + + + + + + F + 4 + + 9 + 1 + quarter + + + 2 + 1 + + begin + begin + begin + + + 0 + + + + + + + A + 4 + + 9 + 1 + quarter + + + 2 + 1 + + + + + C + 5 + + 9 + 1 + quarter + + + 2 + 1 + + end + end + end + + + 0 + + + + + + + E + 5 + + 9 + 1 + quarter + + + 2 + 1 + + + + + + + + A + 4 + + 4 + 1 + quarter + + 3 + 1 + + + + + 3 + + + + + + C + 5 + + 4 + 1 + quarter + + 3 + 1 + + + + 3 + + + + + + G + 4 + + 4 + 1 + quarter + + 3 + 1 + + + + 3 + + + + + + D + 5 + + 4 + 1 + quarter + + 3 + 1 + + + + 3 + + + + + + F + 4 + + 4 + 1 + quarter + + 3 + 1 + + + + 3 + + + + + + E + 5 + + 4 + 1 + quarter + + 3 + 1 + + + + + 3 + + + + + + F + 4 + + 6 + 1 + eighth + begin + + + + + A + 4 + + 6 + 1 + eighth + + + + E + 4 + + 3 + 1 + eighth + + 2 + 1 + + continue + + + 2 + + + + + + + G + 4 + + 3 + 1 + eighth + + 2 + 1 + + + + + D + 5 + + 3 + 1 + eighth + + 2 + 1 + + continue + + + 2 + + + + + + + F + 5 + + 3 + 1 + eighth + + 2 + 1 + + + + + E + 5 + + 6 + 1 + eighth + end + + + + + G + 5 + + 6 + 1 + eighth + + + + + diff --git a/test-data/musicxml-testsuite/34a-Print-Object-Spanners.png b/test-data/musicxml-testsuite/34a-Print-Object-Spanners.png new file mode 100644 index 000000000..0a083d5f9 Binary files /dev/null and b/test-data/musicxml-testsuite/34a-Print-Object-Spanners.png differ diff --git a/test-data/musicxml-testsuite/34a-Print-Object-Spanners.xml b/test-data/musicxml-testsuite/34a-Print-Object-Spanners.xml new file mode 100644 index 000000000..c56d62196 --- /dev/null +++ b/test-data/musicxml-testsuite/34a-Print-Object-Spanners.xml @@ -0,0 +1,618 @@ + + + + + + Test various spanner elements + (mostly from <notations>) starting from a <note> object + with ‘print-object’ set to ‘no’, then test spanners ending with such + a note object: beam, tuplet, slur, trill + wavy-line, glissando + (wavy), slide (solid), two-note tremolo, hammer-on, pull-off. + Spanners starting from an invisible object should be + suppressed. + + + + + MusicXML Part + + + + + + + 6 + + 0 + major + + + + G + 2 + + + + + B + 4 + + 3 + 1 + eighth + begin + + + + B + 4 + + 3 + 1 + eighth + continue + + + + B + 4 + + 3 + 1 + eighth + continue + + + + B + 4 + + 3 + 1 + eighth + end + + + + B + 4 + + 3 + 1 + eighth + begin + + + + B + 4 + + 3 + 1 + eighth + continue + + + + B + 4 + + 3 + 1 + eighth + continue + + + + B + 4 + + 3 + 1 + eighth + end + + + + + + + B + 4 + + 4 + 1 + quarter + + 3 + 2 + + + + + + + + B + 4 + + 4 + 1 + quarter + + 3 + 2 + + + + + B + 4 + + 4 + 1 + quarter + + 3 + 2 + + + + + + + + B + 4 + + 4 + 1 + quarter + + 3 + 2 + + + + + + + + B + 4 + + 4 + 1 + quarter + + 3 + 2 + + + + + B + 4 + + 4 + 1 + quarter + + 3 + 2 + + + + + + + + + + + A + 4 + + 6 + 1 + quarter + + + + + + + C + 5 + + 6 + 1 + quarter + + + + + + + A + 4 + + 6 + 1 + quarter + + + + + + + C + 5 + + 6 + 1 + quarter + + + + + + + + + + B + 4 + + 6 + 1 + quarter + + + + + + + + + + B + 4 + + 6 + 1 + quarter + + + + + + + + + B + 4 + + 6 + 1 + quarter + + + + + + + + + + B + 4 + + 6 + 1 + quarter + + + + + + + + + + + + E + 5 + + 6 + 1 + quarter + + + + + + + C + 4 + + 6 + 1 + quarter + + + + + + + E + 5 + + 6 + 1 + quarter + + + + + + + C + 4 + + 6 + 1 + quarter + + + + + + + + + + E + 5 + + 6 + 1 + quarter + + + + + + + C + 4 + + 6 + 1 + quarter + + + + + + + E + 5 + + 6 + 1 + quarter + + + + + + + C + 4 + + 6 + 1 + quarter + + + + + + + + + + A + 4 + + 6 + 1 + half + + 2 + 1 + + begin + + + 2 + + + + + + C + 5 + + 6 + 1 + half + + 2 + 1 + + end + + + 2 + + + + + + A + 4 + + 6 + 1 + half + + 2 + 1 + + begin + + + 2 + + + + + + C + 5 + + 6 + 1 + half + + 2 + 1 + + end + + + 2 + + + + + + + + + B4 + + 6 + 1 + quarter + + + + + + + + + B4 + + 6 + 1 + quarter + + + + + + + + + B4 + + 6 + 1 + quarter + + + + + + + + + B4 + + 6 + 1 + quarter + + + + + + + + + + + + B4 + + 6 + 1 + quarter + + + + + + + + + B4 + + 6 + 1 + quarter + + + + + + + + + B4 + + 6 + 1 + quarter + + + + + + + + + B4 + + 6 + 1 + quarter + + + + + + + + + + diff --git a/test-data/musicxml-testsuite/34b-Colors.png b/test-data/musicxml-testsuite/34b-Colors.png new file mode 100644 index 000000000..4346b95a2 Binary files /dev/null and b/test-data/musicxml-testsuite/34b-Colors.png differ diff --git a/test-data/musicxml-testsuite/34b-Colors.xml b/test-data/musicxml-testsuite/34b-Colors.xml new file mode 100644 index 000000000..51aa3c777 --- /dev/null +++ b/test-data/musicxml-testsuite/34b-Colors.xml @@ -0,0 +1,1087 @@ + + + + + + Colors. The elements in the first + bar have the ‘color’ attribute set to red for <note>, + <notehead>, <stem>, <dot>, and <accidental>, + respectively. + + The elements in the second bar have the ‘color’ attribute set to red + for <down-bow>, <tremolo>, <accent>, and + <note> again (for the rest), respectively, followed by a red + unpitched note. + + The third and fourth bar consists of a red two-bar rest. + + The fifth bar has a red rehearsal mark on its starting bar line. The + first note has three fingerings, with the middle one in red; also + attached is a red ‘ff’ sign. The second note has three plucks, with + the first one in red. A red ‘Adagio’ tempo indication is on top of + the third beat, which consists of a quadruplet with a red number. The + fourth beat holds a red arpeggio, and the fifth beat demonstrates + lyrics in red. + + The sixth bar holds a red beamlet, a red beam, a red slur, a red pedal + marker that gets continued with a blue one, and a red octave shift. + + Measure seven contains a red trill with a black wavy line, a black + trill with a red wavy line, a red bracket, a red glissando, and a red + wedge. + + Bar eight starts with a red and black tie connecting two chords, + followed by ‘cresc.’ and dashes in red, followed by ‘dim.’ in black + with red dashes. + + The ninth measure demonstrates a red 6/8 time signature, followed by a + red two-stem tremolo, a red breath mark. + + Bar ten begins with a red, non-traditional key change, followed by a + red, traditional one. + + The eleventh bar starts with a red bass clef (actually, still in bar + seven), followed by a blue percussion clef on a red two-line staff + (where the middle line is omitted). + + Measures 12 to 14 holds a repeat structure with two endings, with red + bar lines at the beginning of measures 12 and 14, a blue bar line at + the beginning of measure 13, and a red prima-volta bracket and + number. + + + + + + + + + 8 + + 0 + + + 1 + + G + 2 + + + + + G + 1 + 4 + + 12 + 1 + quarter + + + + + + + 3 + + + + + + + + + G + -1 + 4 + + 12 + 1 + quarter + + normal + + + + + + 3 + + + + + + + + + F + 1 + 4 + + 12 + 1 + quarter + + up + + + + + + 3 + + + + + + + + + F + -1 + 4 + + 12 + 1 + quarter + + + + + + + 3 + + + + + + + + + E + 1 + 4 + + 12 + 1 + quarter + + sharp + + + + + + 3 + + + + + + + + + + + + G + 1 + 4 + + 12 + 1 + quarter + + + + + + + 3 + + + + + + + + + G + -1 + 4 + + 12 + 1 + quarter + + + + + + + 3 + + + + + + + + + F + 1 + 4 + + 12 + 1 + quarter + + + + + + + 3 + + + + + + + + + 12 + 1 + quarter + + + + + 12 + 1 + quarter + + + + + + + + 2 + + + + + 60 + 1 + + + + + + + 60 + 1 + + + + + + + + A + + + + + + + + + + F + 4 + + 12 + 1 + quarter + + + + 0 + 1 + 2 + + + + + + F + 4 + + 12 + 1 + quarter + + + + a + m + i + + + + + + Adagio + + + + + F + 4 + + 3 + 1 + eighth + + 4 + 3 + + begin + + + + 4 + + + + + + + F + 4 + + 3 + 1 + eighth + + 4 + 3 + + continue + + + + F + 4 + + 3 + 1 + eighth + + 4 + 3 + + continue + + + + F + 4 + + 3 + 1 + eighth + + 4 + 3 + + end + + + + + + + F4 + + 12 + 1 + quarter + + + + + + + + + C5 + + 12 + 1 + quarter + + + + + + + + + F5 + + 12 + 1 + quarter + + + + + + + + F4 + + 12 + 1 + quarter + + + lyrics + + + + + + + + F + 4 + + 4 + 1 + eighth + begin + + + + 4 + 1 + eighth + up + continue + + + + F + 4 + + 4 + 1 + eighth + end + + + + F + 4 + + 4 + 1 + eighth + begin + + + + F + 4 + + 4 + 1 + eighth + continue + + + + F + 4 + + 4 + 1 + eighth + end + + + + F + 4 + + 4 + 1 + eighth + begin + + + + + + + F + 4 + + 4 + 1 + eighth + continue + + + + F + 4 + + 4 + 1 + eighth + end + + + + + + + + + + + + F + 4 + + 4 + 1 + eighth + begin + + + + + + + + + F + 4 + + 4 + 1 + eighth + continue + + + + + + + + + F + 4 + + 4 + 1 + eighth + end + + + + + + + + + F + 5 + + 4 + 1 + eighth + begin + + + + F + 5 + + 4 + 1 + eighth + continue + + + + F + 5 + + 4 + 1 + eighth + end + + + + + + + + + + + + + F + 4 + + 12 + 1 + quarter + + + + + + + + + + + + F + 4 + + 12 + 1 + quarter + + + + + + + + + + + + + + + + + F + 4 + + 4 + 1 + eighth + begin + + + + F + 4 + + 4 + 1 + eighth + continue + + + + + + + + + F + 4 + + 4 + 1 + eighth + end + + + + B + 4 + + 8 + 1 + quarter + + + + + + + F + 4 + + 4 + 1 + eighth + + + + + + + + + + + + F + 4 + + 8 + 1 + quarter + + + + F + 4 + + 4 + 1 + eighth + + + + + + + + + + + + F + 4 + + 8 + 1 + quarter + + + + + + + + A + 4 + + 8 + 1 + quarter + + + + + + + F + 4 + + 4 + 1 + eighth + + + + + + + + A + 4 + + 4 + 1 + eighth + + + + + + + cresc. + + + + + + + + F + 4 + + 12 + 1 + quarter + + + + + F + 4 + + 12 + 1 + quarter + + + + + + + + dim. + + + + + + + + F + 4 + + 12 + 1 + quarter + + + + + F + 4 + + 12 + 1 + quarter + + + + + + + + + + + + + + + + + F + 4 + + 12 + 1 + half + + + 2 + 1 + + begin + + + 2 + + + + + + F + 5 + + 12 + 1 + half + + + 2 + 1 + + end + + + 2 + + + + + + + + + + + + D + 1 + A + 1 + C + 1 + + + + + F + 4 + + 12 + 1 + quarter + + + + + -2 + + + + + F + 4 + + 12 + 1 + quarter + + + + + + + + F + 4 + + + + + F + 3 + + 12 + 1 + quarter + + + + + percussion + + + 3 + + + + + + + + 12 + 1 + quarter + + + + + + + + G + 2 + + + 5 + + + + heavy-light + + + + + F + 4 + + 24 + 1 + half + + + + + + + regular + 1. + + + + F + 4 + + 24 + 1 + half + + + + light-heavy + + + + + + + + 2. + + + + F + 4 + + 24 + 1 + half + + + + light-heavy + + + + + diff --git a/test-data/musicxml-testsuite/34c-Font-Size.png b/test-data/musicxml-testsuite/34c-Font-Size.png new file mode 100644 index 000000000..2c5a2e779 Binary files /dev/null and b/test-data/musicxml-testsuite/34c-Font-Size.png differ diff --git a/test-data/musicxml-testsuite/34c-Font-Size.xml b/test-data/musicxml-testsuite/34c-Font-Size.xml new file mode 100644 index 000000000..e9993e347 --- /dev/null +++ b/test-data/musicxml-testsuite/34c-Font-Size.xml @@ -0,0 +1,597 @@ + + + + + + Font sizes. The elements in the + first bar have the ‘font-size’ attribute set to a larger value for + <note>, <notehead>, <trill-mark>, <dot>, and + <accidental>, respectively. + + The elements in the second bar have the ‘font-size’ attribute set to a + larger value for <down-bow>, <accidental-mark>, + <accent>, and <note> again (for the rest), respectively, + followed by a larger percussion clef and a larger unpitched note. + + The third and fourth bar consists of an oversized two-bar rest. + + The fifth bar has an oversized rehearsal mark on its starting bar + line. The first note has three fingerings, with the middle one + oversized; also attached is an oversized ‘ff’ sign. The second note + has three plucks, with the first one oversized. An oversized ‘Adagio’ + tempo indication is on top of the third and fourth beats, which + consist of a quadruplet with an oversized number. The fifth beat + demonstrates oversized lyrics. + + The sixth bar holds an oversized pedal marker with an oversized octave + shift, an oversized trill with a wavy line, and an oversized ‘cresc.’ + with dashes. + + The seventh measure demonstrates an oversized, non-traditional key + change, followed by an oversized 6/8 time signature, an oversized + breath mark, an oversized bass clef, and an oversized, traditional key + change. + + + + + + + + + 8 + + 0 + + + 1 + + G + 2 + + + + + G + 1 + 4 + + 12 + 1 + quarter + + + + + + + + double-sharp + + + + + + + + + G + -1 + 4 + + 12 + 1 + quarter + + normal + + + + + + + double-sharp + + + + + + + + + F + 1 + 4 + + 12 + 1 + quarter + + up + + + + + + + double-sharp + + + + + + + + + F + -1 + 4 + + 12 + 1 + quarter + + + + + + + + double-sharp + + + + + + + + + E + 1 + 4 + + 12 + 1 + quarter + + sharp + + + + + + + double-sharp + + + + + + + + + + + + G + 1 + 4 + + 12 + 1 + quarter + + + + + + + + double-sharp + + + + + + + + + G + -1 + 4 + + 12 + 1 + quarter + + + + + + + + double-sharp + + + + + + + + + F + 1 + 4 + + 12 + 1 + quarter + + + + + + + + double-sharp + + + + + + + + + 12 + 1 + quarter + + + + + percussion + + + + + 12 + 1 + quarter + + + + + + + + + 2 + + + + + 60 + 1 + + + + + + + 60 + 1 + + + + + + + G + 2 + + + + + A + + + + + + + + + + F + 4 + + 12 + 1 + quarter + + + + 0 + 1 + 2 + + + + + + F + 4 + + 12 + 1 + quarter + + + + a + m + i + + + + + + Adagio + + + + + F + 4 + + 6 + 1 + quarter + + 4 + 3 + + + + + 4 + quarter + + + 2 + quarter + + + + + + + + F + 4 + + 6 + 1 + quarter + + 4 + 3 + + + + + F + 4 + + 6 + 1 + quarter + + 4 + 3 + + + + + F + 4 + + 6 + 1 + quarter + + 4 + 3 + + + + + + + + F4 + + 12 + 1 + quarter + + + lyrics + + + + + + + + + + + + + + + + + F + 5 + + 4 + 1 + eighth + begin + + + + F + 5 + + 4 + 1 + eighth + continue + + + + F + 5 + + 4 + 1 + eighth + end + + + + + + + + + + + + F + 4 + + 12 + 1 + quarter + + + + + + + + + + + F + 4 + + 12 + 1 + quarter + + + + + + + + + + cresc. + + + + + + + + F + 4 + + 12 + 1 + quarter + + + + + F + 4 + + 12 + 1 + quarter + + + + + + + + D + 1 + A + 1 + C + 1 + + + + + + + + + + + F + 4 + + 12 + 1 + quarter + + + + + + + + + + -2 + + + F + 4 + + + + + F + 3 + + 12 + 1 + quarter + + + + light-heavy + + + + diff --git a/test-data/musicxml-testsuite/41a-MultiParts-Partorder.png b/test-data/musicxml-testsuite/41a-MultiParts-Partorder.png new file mode 100644 index 000000000..1fff9060c Binary files /dev/null and b/test-data/musicxml-testsuite/41a-MultiParts-Partorder.png differ diff --git a/test-data/musicxml-testsuite/41a-MultiParts-Partorder.xml b/test-data/musicxml-testsuite/41a-MultiParts-Partorder.xml index a41f34780..87f1c0b4e 100644 --- a/test-data/musicxml-testsuite/41a-MultiParts-Partorder.xml +++ b/test-data/musicxml-testsuite/41a-MultiParts-Partorder.xml @@ -1,186 +1,185 @@ - - - - - - A piece with - four parts (P0, P1, P2, P3; different from what - Finale creates!). Are they converted in the correct - order? - - - - - Part 1 - - - Part 2 - - - Part 3 - - - Part 4 - - - - - - 960 - - 1 - major - - - - G - 2 - - - - - C - 4 - - 960 - 1 - quarter - - - - 960 - 1 - quarter - - - - 1920 - 1 - half - - - - - - - 960 - - 1 - major - - - - G - 2 - - - - - E - 4 - - 960 - 1 - quarter - - - - 960 - 1 - quarter - - - - 1920 - 1 - half - - - - - - - 960 - - 1 - major - - - - G - 2 - - - - - G - 4 - - 960 - 1 - quarter - - - - 960 - 1 - quarter - - - - 1920 - 1 - half - - - - - - - 960 - - 1 - major - - - - G - 2 - - - - - B - 4 - - 960 - 1 - quarter - - - - 960 - 1 - quarter - - - - 1920 - 1 - half - - - + + + + + + A piece with four parts named + ‘P0’, ‘P1’, ‘P2’, and ‘P3’ (in that order). + + + + + Part 1 + + + Part 2 + + + Part 3 + + + Part 4 + + + + + + 960 + + 1 + major + + + + G + 2 + + + + + C + 4 + + 960 + 1 + quarter + + + + 960 + 1 + quarter + + + + 1920 + 1 + half + + + + + + + 960 + + 1 + major + + + + G + 2 + + + + + E + 4 + + 960 + 1 + quarter + + + + 960 + 1 + quarter + + + + 1920 + 1 + half + + + + + + + 960 + + 1 + major + + + + G + 2 + + + + + G + 4 + + 960 + 1 + quarter + + + + 960 + 1 + quarter + + + + 1920 + 1 + half + + + + + + + 960 + + 1 + major + + + + G + 2 + + + + + B + 4 + + 960 + 1 + quarter + + + + 960 + 1 + quarter + + + + 1920 + 1 + half + + + diff --git a/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.png b/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.png new file mode 100644 index 000000000..6f49b94d2 Binary files /dev/null and b/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.png differ diff --git a/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.xml b/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.xml index e97bf3d46..803112653 100644 --- a/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.xml +++ b/test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.xml @@ -1,494 +1,501 @@ - - - - - - A piece with - 20 parts to check whether an application supports - that many parts and whether they are - correctly sorted. - - - - - P0 - - - P1 - - - P2 - - - P3 - - - P4 - - - P5 - - - P6 - - - P7 - - - P8 - - - P9 - - - P10 - - - P11 - - - P12 - - - P13 - - - P14 - - - P15 - - - P16 - - - P17 - - - P18 - - - P19 - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - - - - - 960 - - - G - 2 - - - - - 3840 - 1 - whole - - - + + + + + + A piece with 20 parts (called ‘P0’ + to ‘P19’) using a small global font size to check whether an + application supports that many parts and whether they are correctly + sorted. + + + + + 4 + 40 + + + + + P0 + + + P1 + + + P2 + + + P3 + + + P4 + + + P5 + + + P6 + + + P7 + + + P8 + + + P9 + + + P10 + + + P11 + + + P12 + + + P13 + + + P14 + + + P15 + + + P16 + + + P17 + + + P18 + + + P19 + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + + + + + 960 + + + G + 2 + + + + + 3840 + 1 + whole + + + diff --git a/test-data/musicxml-testsuite/41c-StaffGroups.png b/test-data/musicxml-testsuite/41c-StaffGroups.png new file mode 100644 index 000000000..0cfe5a483 Binary files /dev/null and b/test-data/musicxml-testsuite/41c-StaffGroups.png differ diff --git a/test-data/musicxml-testsuite/41c-StaffGroups.xml b/test-data/musicxml-testsuite/41c-StaffGroups.xml index 1a6455174..0bba9e92f 100644 --- a/test-data/musicxml-testsuite/41c-StaffGroups.xml +++ b/test-data/musicxml-testsuite/41c-StaffGroups.xml @@ -1,19 +1,30 @@ - - + - A huge orchestra score with 28 - parts and different kinds of nested bracketed groups. Each part/group - is assigned a name and an abbreviation to be shown before the staff. - Also, most of the groups show unbroken barlines, while the barlines - are broken between the groups. + A huge orchestra score with + 28 parts and different kinds of nested, bracketed groups. Each + part/group is assigned a name and an abbreviation to be shown before + the staff. Also, most of the groups show unbroken bar lines, while + the bar lines are broken between the groups. + + Both the harp and the organ are multi-staff parts; the latter also + uses a <part-symbol> element to set up the + brace delimiter (and the non-contiguous bar line of the three + staves). + + + 3 + 40 + + - - bracket + + bracket yes @@ -23,33 +34,31 @@ Piccolo - - bracket + + + Flute + Fl. + square no - Flute 1 - Fl. 1 + 1 + 1 Flute 1 - Flute 2 - Fl. 2 + 2 + 2 Flute 2 - - - Oboe through Clarinet - O to Cl - bracket - yes - - - line + + + + square yes @@ -66,7 +75,8 @@ English Horn - + + Clarinet in Eb Eb Cl. @@ -74,26 +84,29 @@ Clarinet in Eb - - - bracket + + + Clarinet in Bb + Bb Cl. + square no - Clarinet in Bb 1 - Bb Cl. 1 + 1 + 1 Clarinet in Bb 1 - Clarinet in Bb 2 - Bb Cl. 2 + 2 + 2 Clarinet in Bb 2 - + + Bass Clarinet B. Cl. @@ -101,25 +114,29 @@ Bass Clarinet - - bracket + + + Bassoon + Bsn. + square no - Bassoon 1 - Bsn. 1 + 1 + 1 Bassoon 1 - Bassoon 2 - Bsn. 2 + 2 + 2 Bassoon 2 - + + Contrabassoon C. Bn. @@ -127,68 +144,79 @@ Contrabassoon - + + - bracket - no + bracket + yes + - bracket - yes + Horn in F + F Hn. + square + no - Horn in F 1 - Hn. 1 + 1 + 1 Horn in F 1 - Horn in F 2 - Hn. 2 + 2 + 2 Horn in F 2 - - - bracket + + + + Trumpet in C + C Tpt. + square no - Trumpet in C 1 - C Tpt. 1 + 1 + 1 Trumpet in C 1 - Trumpet in C 2 - C Tpt. 2 + 2 + 2 Trumpet in C 2 - - - bracket + + + + Trombone + Tbn. + square no - Trombone 1 - Tbn. 1 + 1 + 1 Trombone 1 - Trombone 2 - Tbn. 2 + 2 + 2 Trombone 2 - + + Tuba Tuba @@ -196,7 +224,8 @@ Tuba - + + Timpani Timp. @@ -218,31 +247,47 @@ Harp + + + bracket + no + - Piano - Pno. + Organ + Org. - Piano + Organ + + - bracket + bracket yes + + + Violin + Vln. + square + no + - Violin I - Vln. I + I + I Violin I - Violin II - Vln. II + II + II Violin II + + Viola Vla. @@ -1324,7 +1369,10 @@ 4 4 - 2 + 3 + brace G 2 @@ -1333,6 +1381,10 @@ F 4 + + F + 4 + @@ -1385,6 +1437,33 @@ half 2 + + 4 + + + + E + 3 + + 1 + 3 + quarter + 3 + + + + 1 + 3 + quarter + 3 + + + + 2 + 3 + half + 3 + light-heavy diff --git a/test-data/musicxml-testsuite/41d-StaffGroups-Nested.png b/test-data/musicxml-testsuite/41d-StaffGroups-Nested.png new file mode 100644 index 000000000..49c0733d2 Binary files /dev/null and b/test-data/musicxml-testsuite/41d-StaffGroups-Nested.png differ diff --git a/test-data/musicxml-testsuite/41d-StaffGroups-Nested.xml b/test-data/musicxml-testsuite/41d-StaffGroups-Nested.xml index 08f327f6d..92f36c765 100644 --- a/test-data/musicxml-testsuite/41d-StaffGroups-Nested.xml +++ b/test-data/musicxml-testsuite/41d-StaffGroups-Nested.xml @@ -1,39 +1,39 @@ - - + - Two properly nested part groups: - One group (with a square bracket) goes from staff 2 to 4) and another - group (with a curly bracket) goes from staff 3 to 4. + 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. - MusicXML Part + - line + bracket yes - MusicXML Part + - bracket + brace yes - MusicXML Part + - MusicXML Part + - MusicXML Part + diff --git a/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.png b/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.png new file mode 100644 index 000000000..10c1b82ff Binary files /dev/null and b/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.png differ diff --git a/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.xml b/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.xml index 9326768ac..e087b60d3 100644 --- a/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.xml +++ b/test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.xml @@ -1,19 +1,26 @@ - - + - Part names and abbreviations can - contain line breaks. + The <part-name> and + <part-abbreviation> fields don't have an ‘xml:space’ attribute, + making the interpretation of whitespace in the element content + implementation-dependent. + + In this test, there is a line break after each word. It is expected + that some implementations show actual line breaks in the output, while + others do not, replacing the line breaks with + spaces. - Long -Staff + Long +Staff Name - St. + St. Nm. diff --git a/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.png b/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.png new file mode 100644 index 000000000..a4a816ea3 Binary files /dev/null and b/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.png differ diff --git a/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.xml b/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.xml index 9b6d2afe9..1d040ebc6 100644 --- a/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.xml +++ b/test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.xml @@ -1,44 +1,44 @@ - - + - MusicXML allows for overlapping - part-groups, while many applications do not allow overlapping groups, - but require them to be properly nested. In this case, one group - (with a square bracket) goes from staff 2 to 4) and another group - (with a curly bracket) goes from staff 3 to 5. + MusicXML allows for overlapping + part groups, but many applications do not support that, requiring that + they are properly nested instead. In this case, ‘Group 1’ (with a + bracket) goes from staff 1 to 4, and ‘Group 2’ (also with a + bracket) goes from staff 3 to 5. Group 1 Gr1 - bracket + bracket yes - MusicXML Part + - MusicXML Part + Group 2 Grp2 - bracket + bracket yes - MusicXML Part + - MusicXML Part + - MusicXML Part + diff --git a/test-data/musicxml-testsuite/41g-PartNoId.xml b/test-data/musicxml-testsuite/41g-PartNoId.xml deleted file mode 100644 index 337b85160..000000000 --- a/test-data/musicxml-testsuite/41g-PartNoId.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - A part with no id attribute. - Since this piece has only one part, it is clear which part - is described by the one part element. - - - - - MusicXML Part - - - - - - - 4 - 1 - whole - - - - diff --git a/test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.png b/test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.png new file mode 100644 index 000000000..5bf2bcb06 Binary files /dev/null and b/test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.png differ diff --git a/test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.xml b/test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.xml new file mode 100644 index 000000000..58c3d99e5 --- /dev/null +++ b/test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.xml @@ -0,0 +1,371 @@ + + + + + + The horizontal order of nested + group delimiters (brackets, braces, etc.) is unspecified in + MusicXML; however, it can be controlled by the ‘default-x’ attribute + of <group-symbol> in case the application's default positioning + produces unwanted results (and the application actually supports this + attribute). + + In the following, staves 1 to 4 have a four-staves brace on the left + and two two-staves braces to the right. On staves 4 to 8 it's exactly + the opposite (i.e., the four-staves brace is to the right), using the + same ‘number’ attribute values as before but different ‘default-x’ + values. + + Note also that the four-staves brace has the element + <group-barline> set to value ‘no’ while the two-staves braces + use value ‘Mensurstrich’. This means that the Mensurstrich bar lines + should be grouped into two staves each. + + + + + brace + no + + + brace + Mensurstrich + + + + + + + + + + brace + Mensurstrich + + + + + + + + + + + brace + no + + + brace + Mensurstrich + + + + + + + + + + brace + Mensurstrich + + + + + + + + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + diff --git a/test-data/musicxml-testsuite/41h-TooManyParts.png b/test-data/musicxml-testsuite/41h-TooManyParts.png new file mode 100644 index 000000000..4b749ffc1 Binary files /dev/null and b/test-data/musicxml-testsuite/41h-TooManyParts.png differ diff --git a/test-data/musicxml-testsuite/41h-TooManyParts.xml b/test-data/musicxml-testsuite/41h-TooManyParts.xml index 64b7dc90b..31a7a3d20 100644 --- a/test-data/musicxml-testsuite/41h-TooManyParts.xml +++ b/test-data/musicxml-testsuite/41h-TooManyParts.xml @@ -1,12 +1,13 @@ - - + + - This piece has more part elements - than the part-list section gives. One can either convert all - the parts present, but not listed in the part-list, or simply - not import / ignore them. + This piece has two more + <part> elements than the <part-list> section contains. + One can either convert all the parts present but not listed in the + part list, or simply not import or ignore them. diff --git a/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.png b/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.png new file mode 100644 index 000000000..bca240114 Binary files /dev/null and b/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.png differ diff --git a/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.xml b/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.xml index bcc896303..71254f9ab 100644 --- a/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.xml +++ b/test-data/musicxml-testsuite/41i-PartNameDisplay-Override.xml @@ -1,29 +1,42 @@ - - + - MusicXML allows part-name and - part-name-display in the score-part element. If part-name-display - is given, it overrides the part-name for display. + MusicXML allows <part-name> + and <part-name-display> in the <score-part> element. If + <part-name-display> is given, it overrides <part-name> for + display. - The first staff uses only part-name, while the second one (same - part-name) overrides it with a custom text. Similar for the - part-abbreviation used in subsequent staves. - + The first staff uses only <part-name>, while the second one + (with the same <part-name> value) overrides it with a custom + text. + + In a similar vein, <part-abbreviation> can be overridden with + <part-abbreviation-display>, shown in the second system. + + The multi-line entries are generated by using ‘xml:space="preserve"’ + as an attribute within a <display-text> child of the + <xxx-display> element. Part name - abbrv. + Abbrev. Part name - Overridden Part Name - abbrv. - Overr.abbrv. + + Part Name +Override + + Abbrev. + + Abbrv. +overr. + diff --git a/test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.png b/test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.png new file mode 100644 index 000000000..047c2d798 Binary files /dev/null and b/test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.png differ diff --git a/test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.xml b/test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.xml new file mode 100644 index 000000000..deae0db0a --- /dev/null +++ b/test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.xml @@ -0,0 +1,51 @@ + + + + + + This score has multiple + <display-text> elements in its <part-name-display> block: + the word ‘Player’ printed in red with a large font size, the word + ‘One’ printed in blue with a small, italic font + size. + + + + + Music + + Player + One + + + + + + + 1 + + 0 + + + + G + 2 + + + + + C + 4 + + 4 + whole + + + + diff --git a/test-data/musicxml-testsuite/41k-PartName-Print.png b/test-data/musicxml-testsuite/41k-PartName-Print.png new file mode 100644 index 000000000..b956b4c06 Binary files /dev/null and b/test-data/musicxml-testsuite/41k-PartName-Print.png differ diff --git a/test-data/musicxml-testsuite/41k-PartName-Print.xml b/test-data/musicxml-testsuite/41k-PartName-Print.xml new file mode 100644 index 000000000..7b9a91eec --- /dev/null +++ b/test-data/musicxml-testsuite/41k-PartName-Print.xml @@ -0,0 +1,66 @@ + + + + + + The <part-name-display> + and <part-abbreviation-display> elements can also be children of + the <print> element; the former is used at the beginning of the + first bar, and the latter at the beginning of the second bar. + + The multi-line entries are generated by using ‘xml:space="preserve"’ + as an attribute within a <display-text> child of the + <xxx-display> element. There are two empty lines between ‘Part’ + and ‘Name’. + + + + + + + + + + + + + Part + + +Name + + + + 1 + + + + C + 4 + + 4 + 1 + whole + + + + + + + P. +N. + + + + + C + 4 + + 4 + 1 + whole + + + + diff --git a/test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.png b/test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.png new file mode 100644 index 000000000..dad4e69d3 Binary files /dev/null and b/test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.png differ diff --git a/test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.xml b/test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.xml new file mode 100644 index 000000000..b57aee2d9 --- /dev/null +++ b/test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.xml @@ -0,0 +1,174 @@ + + + + + + If <group-name-display> is + given, it overrides <group-name> for display. + + The first two staves use only <group-name>, while the third and + fourth one (with the same <group-name> value) override it with + a custom text. + + In a similar vein, <group-abbreviation> can be overridden with + <group-abbreviation-display>, shown in the second system. + + The multi-line entries are generated by using ‘xml:space="preserve"’ + as an attribute within a <display-text> child of the + <xxx-display> element. + + + + + Group Name + G. N. + bracket + no + + + + + + + + + + Group Name + + Group Name +Override + + G. N. + + G. N. +Overr. + + bracket + no + + + + + + + + + + + + + + 1 + + + + C + 4 + + 4 + 1 + whole + + + + + + + C + 4 + + 4 + 1 + whole + + + + + + + + 1 + + + + C + 4 + + 4 + 1 + whole + + + + + + + C + 4 + + 4 + 1 + whole + + + + + + + + 1 + + + + C + 4 + + 4 + 1 + whole + + + + + + + C + 4 + + 4 + 1 + whole + + + + + + + + 1 + + + + C + 4 + + 4 + 1 + whole + + + + + + + C + 4 + + 4 + 1 + whole + + + + diff --git a/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.png b/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.png new file mode 100644 index 000000000..f2b769203 Binary files /dev/null and b/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.png differ diff --git a/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.xml b/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.xml index 27f4b2aa7..a57ef41ae 100644 --- a/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.xml +++ b/test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.xml @@ -1,11 +1,14 @@ - - + - Two voices share one staff. Each - voice is assigned some lyrics. + Two voices share one staff. To + each voice some lyrics is assigned (with the lyrics of voice one + positioned above the staff). In the last bar, the second voice is + empty, thus the full rest should be positioned on the staff's middle + line. @@ -42,11 +45,11 @@ up - + - + single This @@ -67,7 +70,7 @@ 1 quarter up - + single is @@ -81,7 +84,7 @@ 1 quarter up - + single the @@ -102,7 +105,7 @@ - + single @@ -158,7 +161,7 @@ 1 quarter up - + single lyrics @@ -176,7 +179,7 @@ - + single of @@ -193,7 +196,7 @@ - + single Voice1 @@ -256,7 +259,7 @@ single - Voice1 + Voice2 diff --git a/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.png b/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.png new file mode 100644 index 000000000..411cf5eb4 Binary files /dev/null and b/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.png differ diff --git a/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.xml b/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.xml index b499bbaf5..a86bf9b3a 100644 --- a/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.xml +++ b/test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.xml @@ -1,12 +1,16 @@ - - + - A multi-voice / multi-staff part - with a clef change in the middle of a measure and a <backward> - for voice 2 jumping back beyond that clef change. + A part with three voices; two on + the upper staff and one on the lower staff. There are two clef + changes in the upper staff, one in the middle of measure one and one + at the end. The third voice has a length of only three eights, + starting at the fourth eighth of the first bar. + + No voice contains <stem> elements. @@ -117,6 +121,49 @@ + + 504 + + + + 336 + 2 + quarter + 1 + + + + B + 3 + + 168 + 2 + eighth + 1 + + + + + D + 4 + + 168 + 2 + eighth + 1 + + + + + F + 4 + + 168 + 2 + eighth + natural + 1 + 1008 @@ -304,7 +351,7 @@ - F + E 4 336 @@ -391,7 +438,5 @@ - - diff --git a/test-data/musicxml-testsuite/43a-PianoStaff.png b/test-data/musicxml-testsuite/43a-PianoStaff.png new file mode 100644 index 000000000..4779e852e Binary files /dev/null and b/test-data/musicxml-testsuite/43a-PianoStaff.png differ diff --git a/test-data/musicxml-testsuite/43a-PianoStaff.xml b/test-data/musicxml-testsuite/43a-PianoStaff.xml index d68add6f7..dddb12e33 100644 --- a/test-data/musicxml-testsuite/43a-PianoStaff.xml +++ b/test-data/musicxml-testsuite/43a-PianoStaff.xml @@ -1,42 +1,63 @@ - - - - - - A simple piano staff - - - - - MusicXML Part - - - - - - 96 - 0 - - 2 - G2 - F4 - - - F4 - 384 - 1 - whole - 1 - - 384 - - B2 - 384 - 2 - whole - 2 - - - + + + + + + A simple piano + staff, i.e., two voices, each on a separate + staff. + + + + + MusicXML Part + + + + + + 96 + + 0 + + + 2 + + G + 2 + + + F + 4 + + + + + F + 4 + + 384 + 1 + whole + 1 + + + 384 + + + + B + 2 + + 384 + 2 + whole + 2 + + + diff --git a/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.png b/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.png new file mode 100644 index 000000000..f3313926c Binary files /dev/null and b/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.png differ diff --git a/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.xml b/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.xml index 03a84a914..15863892c 100644 --- a/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.xml +++ b/test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.xml @@ -1,46 +1,66 @@ - - - - - - A piano staff - with different keys and clefs for each of its - staves. The keys and clefs for both staves are given - at the very beginning of the measure. - - - - - MusicXML Part - - - - - - 96 - 0 - 2 - - 2 - G2 - F4 - - - F4 - 384 - 1 - whole - 1 - - 384 - - B2 - 384 - 2 - whole - 2 - - - + + + + + + A piano staff with different keys + and clefs for each of its staves. The keys and clefs for both staves + are given at the very beginning of the measure. + + + + + MusicXML Part + + + + + + 96 + + 0 + + + 2 + + + 2 + + G + 2 + + + F + 4 + + + + + F + 4 + + 384 + 1 + whole + 1 + + + 384 + + + + B + 2 + + 384 + 2 + whole + 2 + + + diff --git a/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.png b/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.png new file mode 100644 index 000000000..48784b34d Binary files /dev/null and b/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.png differ diff --git a/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.xml b/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.xml index 7a20cadce..eb679f487 100644 --- a/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.xml +++ b/test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.xml @@ -1,50 +1,70 @@ - - - - - - A piano staff - with different keys and clefs for each of its - staves. The key and clef for the second staff is - given only after a backward, just before the first - note of the second staff is given, but after the - whole measure for staff 1 has been given. - - - - - MusicXML Part - - - - - - 96 - 0 - - 2 - G2 - - - F4 - 384 - 1 - whole - 1 - - 384 - - 2 - F4 - - - B2 - 384 - 2 - whole - 2 - - - + + + + + + A piano staff with different keys + and clefs for each of its staves. The key and clef for the second + staff is given only after a <backup> element, just before the + first note of the second staff, but after the whole measure for staff + one has been output. + + + + + MusicXML Part + + + + + + 96 + + 0 + + + 2 + + G + 2 + + + + + F + 4 + + 384 + 1 + whole + 1 + + + 384 + + + + 2 + + + F + 4 + + + + + B + 2 + + 384 + 2 + whole + 2 + + + diff --git a/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.png b/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.png new file mode 100644 index 000000000..6b531fc29 Binary files /dev/null and b/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.png differ diff --git a/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.xml b/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.xml index e0071b91f..710a26be7 100644 --- a/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.xml +++ b/test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.xml @@ -1,13 +1,14 @@ - - + - Staff changes in a piano staff. - The voice from the second staff has some notes/chords on the first - staff. The final two chords have some notes on the first, some on - the second staff. + Staff changes in a piano staff. + In the first measure, the voice from the second staff has some notes + on the first staff. In the second measure, the voice from the second + staff has a chord on the second eighth on the first + staff. @@ -34,8 +35,8 @@ 2 - F - 4 + G + 2 @@ -123,12 +124,18 @@ 1 2 eighth - 2 + 1 end + + + F + 4 + + 8 @@ -315,5 +322,4 @@ - diff --git a/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.png b/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.png new file mode 100644 index 000000000..7d468d660 Binary files /dev/null and b/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.png differ diff --git a/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.xml b/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.xml index 9807fb4dc..5a4149229 100644 --- a/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.xml +++ b/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.xml @@ -1,12 +1,15 @@ - - + - A piano staff with dynamics and - clef changes, where each element (ffff, wedge and clef changes) - applies only to one voice or one staff, respectively. + A piano staff with dynamics and + clef changes, where each element (‘ffff’, a wedge, and clef changes) + applies only to one voice or one staff, respectively. + + The key change is given only once, not specifying a particular staff; + it should thus appear on both staves. diff --git a/test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.png b/test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.png new file mode 100644 index 000000000..0143653e9 Binary files /dev/null and b/test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.png differ diff --git a/test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.xml b/test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.xml new file mode 100644 index 000000000..9c26d1061 --- /dev/null +++ b/test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.xml @@ -0,0 +1,281 @@ + + + + + + Two voices of a single part on two + staves, with lyrics. The lyrics of voice one is positioned above the + staff. + + + + + MusicXML Part + + + + + + + 8 + + 0 + major + + + 2 + + G + 2 + + + G + 2 + + + + + E + 5 + + 16 + 1 + half + down + 1 + + single + This + + + + + D + 5 + + 8 + 1 + quarter + down + 1 + + single + is + + + + + B + 4 + + 8 + 1 + quarter + down + 1 + + single + the + + + + 32 + + + + C + 5 + + 16 + 2 + half + down + 2 + + single + This + + + + + B + 4 + + 8 + 2 + quarter + down + 2 + + single + is + + + + + G + 4 + + 8 + 2 + quarter + up + 2 + + single + the + + + + + + + + 8 + 1 + quarter + 1 + + + + D + 5 + + 8 + 1 + quarter + down + 1 + + single + lyrics + + + + + B + 3 + + 12 + 1 + quarter + + up + 1 + + + + + single + of + + + + + C + 5 + + 4 + 1 + eighth + down + 1 + + + + + single + Voice1 + + + + 32 + + + + 8 + 2 + quarter + 2 + + + + B + 4 + + 8 + 2 + quarter + down + 2 + + single + lyrics + + + + + G + 3 + + 12 + 2 + quarter + + up + 2 + + + + + single + of + + + + + A + 4 + + 4 + 2 + eighth + up + 2 + + + + + single + Voice2 + + + + + + + + 32 + 1 + 1 + + + 32 + + + + 32 + 2 + 2 + + + light-heavy + + + + + diff --git a/test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.png b/test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.png new file mode 100644 index 000000000..2f30164eb Binary files /dev/null and b/test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.png differ diff --git a/test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.xml b/test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.xml new file mode 100644 index 000000000..befde6ad8 --- /dev/null +++ b/test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.xml @@ -0,0 +1,121 @@ + + + + + + In this four-staves part, the + <part-symbol> element spans up a ‘square’ staff group delimiter + between staves 2 and 3. + + + + + Test + + + + + + 1 + + 0 + + + 4 + square + + G + 2 + + + G + 2 + + + F + 4 + + + F + 4 + + + + + 4 + 1 + 1 + + + 4 + + + + 4 + 2 + 2 + + + 4 + + + + 4 + 3 + 3 + + + 4 + + + + 4 + 4 + 4 + + + + + + + 4 + 1 + 1 + + + 4 + + + + 4 + 2 + 2 + + + 4 + + + + 4 + 3 + 3 + + + 4 + + + + 4 + 4 + 4 + + + light-heavy + + + + diff --git a/test-data/musicxml-testsuite/45a-SimpleRepeat.png b/test-data/musicxml-testsuite/45a-SimpleRepeat.png new file mode 100644 index 000000000..8358b5785 Binary files /dev/null and b/test-data/musicxml-testsuite/45a-SimpleRepeat.png differ diff --git a/test-data/musicxml-testsuite/45a-SimpleRepeat.xml b/test-data/musicxml-testsuite/45a-SimpleRepeat.xml index 582621a9f..464477f00 100644 --- a/test-data/musicxml-testsuite/45a-SimpleRepeat.xml +++ b/test-data/musicxml-testsuite/45a-SimpleRepeat.xml @@ -1,11 +1,12 @@ - - + - A simple, repeated measure - (repeated 5 times) + A simple, repeated measure (to be + played five times), with an implicit start at the + beginning. diff --git a/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.png b/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.png new file mode 100644 index 000000000..2171380da Binary files /dev/null and b/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.png differ diff --git a/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.xml b/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.xml index 329ac07ab..fcae0ff50 100644 --- a/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.xml +++ b/test-data/musicxml-testsuite/45b-RepeatWithAlternatives.xml @@ -1,10 +1,10 @@ - - + - A simple repeat with two + A simple repeat with two alternative endings (volta brackets). diff --git a/test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.png b/test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.png new file mode 100644 index 000000000..00047a402 Binary files /dev/null and b/test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.png differ diff --git a/test-data/musicxml-testsuite/45c-RepeatMultipleTimes.xml b/test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.xml similarity index 88% rename from test-data/musicxml-testsuite/45c-RepeatMultipleTimes.xml rename to test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.xml index 3a5df794f..dbd486ab0 100644 --- a/test-data/musicxml-testsuite/45c-RepeatMultipleTimes.xml +++ b/test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.xml @@ -1,10 +1,13 @@ - - + - Repeats can also be nested. + Repeats can also be nested. The + inner repeat spans from bar 2 to bar bar 3 (to be played five times). + The outer repeat spans implicitly from the beginning to bar 7 (to be + played one time, i.e., it doesn't get repeated). @@ -93,7 +96,7 @@ light-heavy - + diff --git a/test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.png b/test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.png new file mode 100644 index 000000000..5c431672f Binary files /dev/null and b/test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.png differ diff --git a/test-data/musicxml-testsuite/45d-Repeats-Nested-Alternatives.xml b/test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.xml similarity index 85% rename from test-data/musicxml-testsuite/45d-Repeats-Nested-Alternatives.xml rename to test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.xml index c90afeb59..63c6fb26a 100644 --- a/test-data/musicxml-testsuite/45d-Repeats-Nested-Alternatives.xml +++ b/test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.xml @@ -1,11 +1,13 @@ - - + - Nested repeats, each with - alternative endings. + Multiple alternative endings. The + first alternative starts and ends at bar 2; the second continues until + bar 5, the third until bar 9, the fourth until bar 10, and the fifth + implicitly until the end. @@ -80,7 +82,9 @@ 1 + light-heavy + @@ -93,9 +97,6 @@ 4 1 - - - @@ -120,11 +121,16 @@ 4 1 + + light-heavy + + + - + @@ -132,7 +138,9 @@ 1 - + light-heavy + + @@ -146,9 +154,7 @@ 1 - light-heavy - - + diff --git a/test-data/musicxml-testsuite/45e-Repeats-Combination.png b/test-data/musicxml-testsuite/45e-Repeats-Combination.png new file mode 100644 index 000000000..12e003e93 Binary files /dev/null and b/test-data/musicxml-testsuite/45e-Repeats-Combination.png differ diff --git a/test-data/musicxml-testsuite/45e-Repeats-Nested-Alternatives.xml b/test-data/musicxml-testsuite/45e-Repeats-Combination.xml similarity index 79% rename from test-data/musicxml-testsuite/45e-Repeats-Nested-Alternatives.xml rename to test-data/musicxml-testsuite/45e-Repeats-Combination.xml index b4d324ee3..2a416041e 100644 --- a/test-data/musicxml-testsuite/45e-Repeats-Nested-Alternatives.xml +++ b/test-data/musicxml-testsuite/45e-Repeats-Combination.xml @@ -1,12 +1,19 @@ - - + - Some more nested repeats with - alternatives. The barline between measure 7 and 8 will probably be - messed up! (Should be a repeat on both sides!) + A series of repeat elements. + + The first repeat starts implicitly at the beginning, with the first + alternative starting and ending at bar 2, and the second ending at + bar 3. The next repeat starts and ends at bar 5. Another repeat + starts at bar 6 (causing a back-to-back bar line between bars 5 + and 6), with the first alternative starting and ending at bar 7. Its + second alternative (starting and ending at bar 8) is the start of a + repeat at the same time (causing another back-to-back bar line between + bars 7 and 8), ending after bar 9. @@ -94,6 +101,10 @@ + + heavy-light + + 4 @@ -120,6 +131,7 @@ heavy-light + @@ -127,6 +139,9 @@ 4 1 + + + diff --git a/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.png b/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.png new file mode 100644 index 000000000..d41efdd64 Binary files /dev/null and b/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.png differ diff --git a/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.xml b/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.xml index a84052a0a..7e4b36e3e 100644 --- a/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.xml +++ b/test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.xml @@ -1,13 +1,20 @@ - - + - Some more nested repeats with - alternatives, where the MusicXML file does not make sense in the - first place. How well are applications able to cope with improper - repeats and alternatives? + A stress test with a combination + of <repeat> and <ending> elements that don't make sense. + The displayed result depends on the sanitizing possibilities of the + application that handles the input. + + Bar 2 starts and ends with <ending number="1, 2, 3">. Bar 3 + starts and ends with <ending number="2"> (where the right + element is of type 'discontinue'); there is no <repeat> element + between bars 2 and 3. Finally, at the end of bar 4 there is a + both a <repeat direction="backward"> and a <ending + type="stop"> element. diff --git a/test-data/musicxml-testsuite/45g-Repeats-NotEnded.png b/test-data/musicxml-testsuite/45g-Repeats-NotEnded.png new file mode 100644 index 000000000..eb38085a0 Binary files /dev/null and b/test-data/musicxml-testsuite/45g-Repeats-NotEnded.png differ diff --git a/test-data/musicxml-testsuite/45g-Repeats-NotEnded.xml b/test-data/musicxml-testsuite/45g-Repeats-NotEnded.xml index c29f6cd38..1ac78cf20 100644 --- a/test-data/musicxml-testsuite/45g-Repeats-NotEnded.xml +++ b/test-data/musicxml-testsuite/45g-Repeats-NotEnded.xml @@ -1,10 +1,10 @@ - - + - A forward-repeating bar line + A forward-repeating bar line without an ending repeat bar. diff --git a/test-data/musicxml-testsuite/45h-Repeats-Partial.png b/test-data/musicxml-testsuite/45h-Repeats-Partial.png new file mode 100644 index 000000000..9578d2861 Binary files /dev/null and b/test-data/musicxml-testsuite/45h-Repeats-Partial.png differ diff --git a/test-data/musicxml-testsuite/45h-Repeats-Partial.xml b/test-data/musicxml-testsuite/45h-Repeats-Partial.xml new file mode 100644 index 000000000..4e7da3452 --- /dev/null +++ b/test-data/musicxml-testsuite/45h-Repeats-Partial.xml @@ -0,0 +1,104 @@ + + + + + + A repeat starting and ending at a + partial bar. The style of the back-to-back bar line is 'heavy-heavy' + (using the MusicXML encoding as exported by + Finale). + + + + + MusicXML Part + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + C + 5 + + 1 + 1 + quarter + + + + + + + C + 5 + + 4 + 1 + whole + + + + + + + C + 5 + + 3 + 1 + half + + + + heavy-heavy + + + + + + + + + + + C + 5 + + 1 + 1 + quarter + + + + + + + C + 5 + + 4 + 1 + whole + + + + + diff --git a/test-data/musicxml-testsuite/45i-Repeats-Nested.png b/test-data/musicxml-testsuite/45i-Repeats-Nested.png new file mode 100644 index 000000000..7d9e7acb2 Binary files /dev/null and b/test-data/musicxml-testsuite/45i-Repeats-Nested.png differ diff --git a/test-data/musicxml-testsuite/45i-Repeats-Nested.xml b/test-data/musicxml-testsuite/45i-Repeats-Nested.xml new file mode 100644 index 000000000..0f63eb3fa --- /dev/null +++ b/test-data/musicxml-testsuite/45i-Repeats-Nested.xml @@ -0,0 +1,149 @@ + + + + + + A repeat with two + alternative endings. In the first one (enclosing bar 2 to bar 4), + there is a nested repeat enclosing bar 3. In the second one (from + bar 5 to bar 6), there is another nested repeat starting also at bar 5 + but ending already at the same bar. + + + + + MusicXML Part + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + C + 5 + + 4 + 1 + whole + + + + + + + + + + C + 5 + + 4 + 1 + whole + + + + + + heavy-light + + + + + C + 5 + + 4 + 1 + whole + + + light-heavy + + + + + + + + C + 5 + + 4 + 1 + whole + + + + + + + + + + + + + + + C + 5 + + 4 + 1 + whole + + + + + + + + + + C + 5 + + 4 + 1 + whole + + + + + + + + + + C + 5 + + 4 + 1 + whole + + + light-heavy + + + + + diff --git a/test-data/musicxml-testsuite/46a-Barlines.png b/test-data/musicxml-testsuite/46a-Barlines.png new file mode 100644 index 000000000..f1e85b304 Binary files /dev/null and b/test-data/musicxml-testsuite/46a-Barlines.png differ diff --git a/test-data/musicxml-testsuite/46a-Barlines.xml b/test-data/musicxml-testsuite/46a-Barlines.xml index 814a5b425..617e96093 100644 --- a/test-data/musicxml-testsuite/46a-Barlines.xml +++ b/test-data/musicxml-testsuite/46a-Barlines.xml @@ -1,12 +1,12 @@ - - + - Different types of (non-repeat) + Different types of (non-repeat) barlines: default (no setting), regular, dotted, dashed, heavy, - light-light, light-heavy, heavy-light, heavy-heavy, tick, short, + light-light, light-heavy, heavy-light, heavy-heavy, tick, short, none. diff --git a/test-data/musicxml-testsuite/46b-MidmeasureBarline.png b/test-data/musicxml-testsuite/46b-MidmeasureBarline.png new file mode 100644 index 000000000..2d3cdf0ea Binary files /dev/null and b/test-data/musicxml-testsuite/46b-MidmeasureBarline.png differ diff --git a/test-data/musicxml-testsuite/46b-MidmeasureBarline.xml b/test-data/musicxml-testsuite/46b-MidmeasureBarline.xml index c4517e6cc..15a3608f5 100644 --- a/test-data/musicxml-testsuite/46b-MidmeasureBarline.xml +++ b/test-data/musicxml-testsuite/46b-MidmeasureBarline.xml @@ -1,10 +1,10 @@ - - + + - Barlines can appear at + Barlines can appear at mid-measure positions, without using an implicit measure! @@ -13,9 +13,9 @@ - + - + 1 @@ -60,7 +60,7 @@ quarter - + - + diff --git a/test-data/musicxml-testsuite/46c-Midmeasure-Clef.png b/test-data/musicxml-testsuite/46c-Midmeasure-Clef.png new file mode 100644 index 000000000..010164f6c Binary files /dev/null and b/test-data/musicxml-testsuite/46c-Midmeasure-Clef.png differ diff --git a/test-data/musicxml-testsuite/46c-Midmeasure-Clef.xml b/test-data/musicxml-testsuite/46c-Midmeasure-Clef.xml index 548d105b3..ab47dd7e4 100644 --- a/test-data/musicxml-testsuite/46c-Midmeasure-Clef.xml +++ b/test-data/musicxml-testsuite/46c-Midmeasure-Clef.xml @@ -1,11 +1,11 @@ - - + - A clef change in the middle of a - measure, using either an implicit measure or simply placing + A clef change in the middle of a + measure, using either an implicit measure or simply placing the attributes in the middle of the measure. diff --git a/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.png b/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.png new file mode 100644 index 000000000..34e15b5a4 Binary files /dev/null and b/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.png differ diff --git a/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.xml b/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.xml index 337fa9ed1..827e1c6c2 100644 --- a/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.xml +++ b/test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.xml @@ -1,11 +1,11 @@ - - + - A 3/8 pickup measure, a measure - that is split into one (incomplete, only 2/4) measure and an implicit + A 3/8 pickup measure, a measure + that is split into one (incomplete, only 2/4) measure and an implicit measure, and an incomplete measure (containg 3/4). diff --git a/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.png b/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.png new file mode 100644 index 000000000..8108153a7 Binary files /dev/null and b/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.png differ diff --git a/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.xml b/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.xml index 4dd9631b1..47bad77e5 100644 --- a/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.xml +++ b/test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.xml @@ -1,10 +1,10 @@ - - + + - Voice 2 should start at 2nd + Voice 2 should start at 2nd beat of first full measure. @@ -13,7 +13,7 @@ - + @@ -29,7 +29,7 @@ quarter - + 1 @@ -83,7 +83,7 @@ quarter - + - + diff --git a/test-data/musicxml-testsuite/46f-IncompleteMeasures.png b/test-data/musicxml-testsuite/46f-IncompleteMeasures.png new file mode 100644 index 000000000..1bbc57446 Binary files /dev/null and b/test-data/musicxml-testsuite/46f-IncompleteMeasures.png differ diff --git a/test-data/musicxml-testsuite/46f-IncompleteMeasures.xml b/test-data/musicxml-testsuite/46f-IncompleteMeasures.xml index d16a4638f..eb931c340 100644 --- a/test-data/musicxml-testsuite/46f-IncompleteMeasures.xml +++ b/test-data/musicxml-testsuite/46f-IncompleteMeasures.xml @@ -1,10 +1,11 @@ - - + + Measures can contain less notes - than the time signature says. Here, the first and third measures + than the time signature says. Here, the first and third measures contain only two quarters instead of four. diff --git a/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.png b/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.png new file mode 100644 index 000000000..049db0b0b Binary files /dev/null and b/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.png differ diff --git a/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.xml b/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.xml index 99af4c684..0adf85c66 100644 --- a/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.xml +++ b/test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.xml @@ -1,9 +1,10 @@ - - + + - Pickup measure with chord names + Pickup measure with chord names and figured bass. diff --git a/test-data/musicxml-testsuite/51b-Header-Quotes.png b/test-data/musicxml-testsuite/51b-Header-Quotes.png new file mode 100644 index 000000000..ef527505c Binary files /dev/null and b/test-data/musicxml-testsuite/51b-Header-Quotes.png differ diff --git a/test-data/musicxml-testsuite/51b-Header-Quotes.xml b/test-data/musicxml-testsuite/51b-Header-Quotes.xml index 544bb5af1..ee4abfb29 100644 --- a/test-data/musicxml-testsuite/51b-Header-Quotes.xml +++ b/test-data/musicxml-testsuite/51b-Header-Quotes.xml @@ -1,7 +1,7 @@ - - + "Quotes" in header fields Some "Tester" Name @@ -11,8 +11,8 @@ 2008-02-06 - Several header fields and part - names can contain quotes ("). This test checks whether they are + Several header fields and part + names can contain quotes ("). This test checks whether they are converted/imported without problems (i.e. whether they are correctly escaped when converting). diff --git a/test-data/musicxml-testsuite/51c-MultipleRights.png b/test-data/musicxml-testsuite/51c-MultipleRights.png new file mode 100644 index 000000000..49ce76c41 Binary files /dev/null and b/test-data/musicxml-testsuite/51c-MultipleRights.png differ diff --git a/test-data/musicxml-testsuite/51c-MultipleRights.xml b/test-data/musicxml-testsuite/51c-MultipleRights.xml index 40102cd09..1c66b8294 100644 --- a/test-data/musicxml-testsuite/51c-MultipleRights.xml +++ b/test-data/musicxml-testsuite/51c-MultipleRights.xml @@ -1,14 +1,15 @@ - - - + + + Copyright © XXXX by Y. ZZZZ. Released To The Public Domain. - There can be multiple - <rights> tags in the identification element of the score. The - conversion shall still work, ideally using both of - them. + There can be multiple + <rights> tags in the identification element of the score. The + conversion shall still work, ideally using both of + them. diff --git a/test-data/musicxml-testsuite/51d-EmptyTitle.png b/test-data/musicxml-testsuite/51d-EmptyTitle.png new file mode 100644 index 000000000..13dcacc59 Binary files /dev/null and b/test-data/musicxml-testsuite/51d-EmptyTitle.png differ diff --git a/test-data/musicxml-testsuite/51d-EmptyTitle.xml b/test-data/musicxml-testsuite/51d-EmptyTitle.xml index 49a089bf7..44d9e62ff 100644 --- a/test-data/musicxml-testsuite/51d-EmptyTitle.xml +++ b/test-data/musicxml-testsuite/51d-EmptyTitle.xml @@ -1,7 +1,7 @@ - - + diff --git a/test-data/musicxml-testsuite/52a-PageLayout.png b/test-data/musicxml-testsuite/52a-PageLayout.png new file mode 100644 index 000000000..82c8caa29 Binary files /dev/null and b/test-data/musicxml-testsuite/52a-PageLayout.png differ diff --git a/test-data/musicxml-testsuite/52a-PageLayout.xml b/test-data/musicxml-testsuite/52a-PageLayout.xml index 330b9713b..a439b36f3 100644 --- a/test-data/musicxml-testsuite/52a-PageLayout.xml +++ b/test-data/musicxml-testsuite/52a-PageLayout.xml @@ -1,12 +1,12 @@ - - + Layout options - Several page layout settings: - paper size, margins, system margins and distances, different fonts, + Several page layout settings: + paper size, margins, system margins and distances, different fonts, etc. @@ -78,6 +78,16 @@ + + + Medium size + + + + + Large size + + 4 diff --git a/test-data/musicxml-testsuite/52b-Breaks.png b/test-data/musicxml-testsuite/52b-Breaks.png new file mode 100644 index 000000000..1a978efef Binary files /dev/null and b/test-data/musicxml-testsuite/52b-Breaks.png differ diff --git a/test-data/musicxml-testsuite/52b-Breaks.xml b/test-data/musicxml-testsuite/52b-Breaks.xml index 623261829..1809794be 100644 --- a/test-data/musicxml-testsuite/52b-Breaks.xml +++ b/test-data/musicxml-testsuite/52b-Breaks.xml @@ -1,11 +1,11 @@ - - + - System and page breaks, given in - a <print> element + System and page breaks, given in + a <print> element diff --git a/test-data/musicxml-testsuite/61a-Lyrics.png b/test-data/musicxml-testsuite/61a-Lyrics.png new file mode 100644 index 000000000..d3abed2fb Binary files /dev/null and b/test-data/musicxml-testsuite/61a-Lyrics.png differ diff --git a/test-data/musicxml-testsuite/61a-Lyrics.xml b/test-data/musicxml-testsuite/61a-Lyrics.xml index 69642ea87..a68e1b410 100644 --- a/test-data/musicxml-testsuite/61a-Lyrics.xml +++ b/test-data/musicxml-testsuite/61a-Lyrics.xml @@ -1,12 +1,12 @@ - - + - Some notes with simple lyrics: - Syllables, notes without a syllable, syllable - spanners. + Some notes with simple lyrics: + Syllables, notes without a syllable, syllable + spanners. diff --git a/test-data/musicxml-testsuite/61b-MultipleLyrics.png b/test-data/musicxml-testsuite/61b-MultipleLyrics.png new file mode 100644 index 000000000..4b4e9fd35 Binary files /dev/null and b/test-data/musicxml-testsuite/61b-MultipleLyrics.png differ diff --git a/test-data/musicxml-testsuite/61b-MultipleLyrics.xml b/test-data/musicxml-testsuite/61b-MultipleLyrics.xml index 5f9334d2c..ba92e9605 100644 --- a/test-data/musicxml-testsuite/61b-MultipleLyrics.xml +++ b/test-data/musicxml-testsuite/61b-MultipleLyrics.xml @@ -1,11 +1,11 @@ - - + - Multiple (simple) lyrics. The - order of the exported stanzas is relevant (identified by the number + Multiple (simple) lyrics. The + order of the exported stanzas is relevant (identified by the number attribute in this test case) @@ -46,11 +46,11 @@ begin - 2.tra + 2.-4./5. tra - + begin - 3.TRA + 6., 7.TRA @@ -69,7 +69,7 @@ middle la - + middle LA @@ -90,7 +90,7 @@ end la, - + end LA, @@ -113,7 +113,7 @@ ja! - + single JA! @@ -147,7 +147,7 @@ begin Tra - + begin TRA @@ -177,7 +177,7 @@ end ra. - + end RA... diff --git a/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.png b/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.png new file mode 100644 index 000000000..e7b448251 Binary files /dev/null and b/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.png differ diff --git a/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.xml b/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.xml index ca5b8e6b5..59f7107e5 100644 --- a/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.xml +++ b/test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.xml @@ -1,11 +1,11 @@ - - + - Lyrics assigned to the voices of - a piano staff containing two simple staves. Each staff is assigned + Lyrics assigned to the voices of + a piano staff containing two simple staves. Each staff is assigned exactly one lyrics line. diff --git a/test-data/musicxml-testsuite/61d-Lyrics-Melisma.png b/test-data/musicxml-testsuite/61d-Lyrics-Melisma.png new file mode 100644 index 000000000..67f183271 Binary files /dev/null and b/test-data/musicxml-testsuite/61d-Lyrics-Melisma.png differ diff --git a/test-data/musicxml-testsuite/61d-Lyrics-Melisma.xml b/test-data/musicxml-testsuite/61d-Lyrics-Melisma.xml index 9258058c0..442c77eef 100644 --- a/test-data/musicxml-testsuite/61d-Lyrics-Melisma.xml +++ b/test-data/musicxml-testsuite/61d-Lyrics-Melisma.xml @@ -1,11 +1,11 @@ - - + - How to treat lyrics and slurred - notes. Normally, a slurred group of notes is assigned only one lyrics + How to treat lyrics and slurred + notes. Normally, a slurred group of notes is assigned only one lyrics syllable. diff --git a/test-data/musicxml-testsuite/61e-Lyrics-Chords.png b/test-data/musicxml-testsuite/61e-Lyrics-Chords.png new file mode 100644 index 000000000..b7f385822 Binary files /dev/null and b/test-data/musicxml-testsuite/61e-Lyrics-Chords.png differ diff --git a/test-data/musicxml-testsuite/61e-Lyrics-Chords.xml b/test-data/musicxml-testsuite/61e-Lyrics-Chords.xml index 945a31d43..bf39b989e 100644 --- a/test-data/musicxml-testsuite/61e-Lyrics-Chords.xml +++ b/test-data/musicxml-testsuite/61e-Lyrics-Chords.xml @@ -1,10 +1,10 @@ - - + - Assigning lyrics to chorded + Assigning lyrics to chorded notes. diff --git a/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.png b/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.png new file mode 100644 index 000000000..805154579 Binary files /dev/null and b/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.png differ diff --git a/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.xml b/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.xml index b577b2e0b..4938fc126 100644 --- a/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.xml +++ b/test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.xml @@ -1,11 +1,12 @@ - - + - Grace notes shall not mess up the - lyrics, and they shall not be assigned a syllable. + Grace notes shall not mess up the + lyrics, and they shall not be assigned to a + syllable. @@ -87,12 +88,8 @@ D 5 - 1 eighth - - - @@ -116,12 +113,8 @@ 5 2 - 1 quarter - - - single notes @@ -134,13 +127,9 @@ E 5 - 1 eighth begin - - - @@ -158,12 +147,8 @@ 5 2 - 1 quarter - - - diff --git a/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.png b/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.png new file mode 100644 index 000000000..53f93b8dc Binary files /dev/null and b/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.png differ diff --git a/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.xml b/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.xml index 1999d07db..4425bce9f 100644 --- a/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.xml +++ b/test-data/musicxml-testsuite/61g-Lyrics-NameNumber.xml @@ -1,15 +1,15 @@ - - + - A lyrics syllable can have both - a number and a name attribute. The question is: What should be used - to put syllables of the same voice together. This example uses - different number/name combinations to check how different - applications handle this unspecified case (The advice on the - MusicXML mailing list was "there is no correct way, each + A lyrics syllable can have both + a number and a name attribute. The question is: What should be used + to put syllables of the same voice together. This example uses + different number/name combinations to check how different + applications handle this unspecified case (The advice on the + MusicXML mailing list was "there is no correct way, each application can do what it thinks is best"). @@ -36,20 +36,20 @@ 1 quarter - begin + single Verse1A - begin + single Chorus1A - begin + single AnotherChorus1A - begin - Chorus1A + single + Chorus2A @@ -58,11 +58,11 @@ 1 quarter - begin + single 1B - begin + single 2B @@ -72,11 +72,11 @@ 1 quarter - begin + single Verse1C - begin + single Chorus2C @@ -86,7 +86,7 @@ 1 quarter - begin + single Chorus1D @@ -96,7 +96,7 @@ 1 quarter - begin + single VerseE @@ -106,7 +106,7 @@ 1 quarter - begin + single NoneF diff --git a/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.png b/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.png new file mode 100644 index 000000000..2248c2803 Binary files /dev/null and b/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.png differ diff --git a/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.xml b/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.xml index 0edcc5f2c..8bc69a01b 100644 --- a/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.xml +++ b/test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.xml @@ -1,12 +1,17 @@ - - + - Beaming or slurs can indicate - melismata for lyrics. Also make sure that notes without an explicit - syllable are treated as if they were part of a melisma. + Beaming or slurs can indicate + melismata for lyrics. Also make sure that notes without an explicit + syllable are treated as if they were part of a + melisma. + + In the second bar, ‘start’, ‘continue’, and ‘stop’ types are used for + <extend>, together with a red color for the extender + line. @@ -130,7 +135,7 @@ end ma - + @@ -138,12 +143,18 @@ 1 1 eighth + + + B4 1 1 eighth + + + diff --git a/test-data/musicxml-testsuite/61i-Lyrics-Chords.png b/test-data/musicxml-testsuite/61i-Lyrics-Chords.png new file mode 100644 index 000000000..ff8cac80d Binary files /dev/null and b/test-data/musicxml-testsuite/61i-Lyrics-Chords.png differ diff --git a/test-data/musicxml-testsuite/61i-Lyrics-Chords.xml b/test-data/musicxml-testsuite/61i-Lyrics-Chords.xml index 8f3db1e1a..a26c5a6a8 100644 --- a/test-data/musicxml-testsuite/61i-Lyrics-Chords.xml +++ b/test-data/musicxml-testsuite/61i-Lyrics-Chords.xml @@ -1,14 +1,14 @@ - - + Each note of a chord can have - some lyrics attached. In this case, each note of the chord has lyrics - of the form "Lyrics [123]" attached, where each lyrics has a different - number attribute to distinguish them. These syllables should be - imported into three different stanzas and the timing should be + some lyrics attached. In this case, each note of the chord has lyrics + of the form "Lyrics [123]" attached, where each lyrics has a different + number attribute to distinguish them. These syllables should be + imported into three different stanzas and the timing should be correct. diff --git a/test-data/musicxml-testsuite/61j-Lyrics-Elisions.png b/test-data/musicxml-testsuite/61j-Lyrics-Elisions.png new file mode 100644 index 000000000..fdc77066c Binary files /dev/null and b/test-data/musicxml-testsuite/61j-Lyrics-Elisions.png differ diff --git a/test-data/musicxml-testsuite/61j-Lyrics-Elisions.xml b/test-data/musicxml-testsuite/61j-Lyrics-Elisions.xml index fd76590df..e0cacf03c 100644 --- a/test-data/musicxml-testsuite/61j-Lyrics-Elisions.xml +++ b/test-data/musicxml-testsuite/61j-Lyrics-Elisions.xml @@ -1,16 +1,21 @@ - - + - Multiple lyrics syllables - assigned to a single note are implemented either using a space in - the lyrics or by using the <elision> lyrics element. This - testcase checks both of them. First, a note with on syllable is - given, then a note with two syllables separated by a spcae and finally - a note with two and one with three syllables implemented using - <elision> is given. + Multiple lyrics syllables assigned + to a single note are implemented either using a space in the lyrics' + <text> element or by using <elision>. + + The first note has a single syllable, the second note has two + syllables separated by a space, the third has two syllables with + <elision> set to an undertie, and the fourth has three + syllables (the first and third one in red, and the second one + being in italic and overriding the color with blue), with a green + undertie between the first and second syllable and an empty + <elision> element between the second and third syllable, causing + an application-specific elision glyph. @@ -41,51 +46,60 @@ C 5 - 1 + 4 1 - quarter + whole a + + + C 5 - 1 + 4 1 - quarter + whole b c + + + C 5 - 1 + 4 1 - quarter + whole d - + e + + + C 5 - 1 + 4 1 - quarter - + whole + f - - g + + g h diff --git a/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.png b/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.png new file mode 100644 index 000000000..5509bab7c Binary files /dev/null and b/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.png differ diff --git a/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.xml b/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.xml index 22354d82a..39522be26 100644 --- a/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.xml +++ b/test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.xml @@ -1,13 +1,13 @@ - - + - Lyrics spanners: continued - syllables and extenders, possibly spanning multiple notes. The - intermediate notes do not have any <lyric> - element. + Lyrics spanners: continued + syllables and extenders, possibly spanning multiple notes. The + intermediate notes do not have any <lyric> + element. diff --git a/test-data/musicxml-testsuite/71a-Chordnames.png b/test-data/musicxml-testsuite/71a-Chordnames.png new file mode 100644 index 000000000..5fa6d3e9e Binary files /dev/null and b/test-data/musicxml-testsuite/71a-Chordnames.png differ diff --git a/test-data/musicxml-testsuite/71a-Chordnames.xml b/test-data/musicxml-testsuite/71a-Chordnames.xml index f7339048f..a2daa1e65 100644 --- a/test-data/musicxml-testsuite/71a-Chordnames.xml +++ b/test-data/musicxml-testsuite/71a-Chordnames.xml @@ -1,10 +1,10 @@ - - + - A normal staff with several + A normal staff with several (complex) chord names displayed. diff --git a/test-data/musicxml-testsuite/71c-ChordsFrets.png b/test-data/musicxml-testsuite/71c-ChordsFrets.png new file mode 100644 index 000000000..1d58a970c Binary files /dev/null and b/test-data/musicxml-testsuite/71c-ChordsFrets.png differ diff --git a/test-data/musicxml-testsuite/71c-ChordsFrets.xml b/test-data/musicxml-testsuite/71c-ChordsFrets.xml index cb882c2c6..dde80bded 100644 --- a/test-data/musicxml-testsuite/71c-ChordsFrets.xml +++ b/test-data/musicxml-testsuite/71c-ChordsFrets.xml @@ -1,12 +1,12 @@ - - + - A staff with chord names and some - fretboards shown. The fretboards can have an arbitrary number of - frets/strings, can start at an arbitrary fret and can even contain + A staff with chord names and some + fretboards shown. The fretboards can have an arbitrary number of + frets/strings, can start at an arbitrary fret and can even contain fingering information. diff --git a/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.png b/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.png new file mode 100644 index 000000000..67095c955 Binary files /dev/null and b/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.png differ diff --git a/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.xml b/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.xml index 7b4eab254..5b6c72162 100644 --- a/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.xml +++ b/test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.xml @@ -1,11 +1,11 @@ - - + - Chords and fretboards assigned to - the voices in a multi-voice, multi-staff part. There should be fret + Chords and fretboards assigned to + the voices in a multi-voice, multi-staff part. There should be fret diagrams above each of the two staves. diff --git a/test-data/musicxml-testsuite/71e-TabStaves.png b/test-data/musicxml-testsuite/71e-TabStaves.png new file mode 100644 index 000000000..57b465d37 Binary files /dev/null and b/test-data/musicxml-testsuite/71e-TabStaves.png differ diff --git a/test-data/musicxml-testsuite/71e-TabStaves.xml b/test-data/musicxml-testsuite/71e-TabStaves.xml index 79fc6131e..8a87f515f 100644 --- a/test-data/musicxml-testsuite/71e-TabStaves.xml +++ b/test-data/musicxml-testsuite/71e-TabStaves.xml @@ -1,7 +1,7 @@ - - + Some tablature staves, with diff --git a/test-data/musicxml-testsuite/71f-AllChordTypes.png b/test-data/musicxml-testsuite/71f-AllChordTypes.png new file mode 100644 index 000000000..1f1e8b268 Binary files /dev/null and b/test-data/musicxml-testsuite/71f-AllChordTypes.png differ diff --git a/test-data/musicxml-testsuite/71f-AllChordTypes.xml b/test-data/musicxml-testsuite/71f-AllChordTypes.xml index d6abafbb1..885bc9c17 100644 --- a/test-data/musicxml-testsuite/71f-AllChordTypes.xml +++ b/test-data/musicxml-testsuite/71f-AllChordTypes.xml @@ -1,13 +1,13 @@ - - + All MusicXML chord names/types with <root> - All chord types defined in - MusicXML. The staff will only contain one c' note (NO chord) for - all of them, but the chord names should be properly + All chord types defined in + MusicXML. The staff will only contain one c’ note (NO chord) for + all of them, but the chord names should be properly printed. diff --git a/test-data/musicxml-testsuite/71g-MultipleChordnames.png b/test-data/musicxml-testsuite/71g-MultipleChordnames.png new file mode 100644 index 000000000..01a0e7bf3 Binary files /dev/null and b/test-data/musicxml-testsuite/71g-MultipleChordnames.png differ diff --git a/test-data/musicxml-testsuite/71g-MultipleChordnames.xml b/test-data/musicxml-testsuite/71g-MultipleChordnames.xml index 6c42cdb2c..7c68275b3 100644 --- a/test-data/musicxml-testsuite/71g-MultipleChordnames.xml +++ b/test-data/musicxml-testsuite/71g-MultipleChordnames.xml @@ -1,6 +1,7 @@ - - + + There can be multiple subsequent diff --git a/test-data/musicxml-testsuite/72a-TransposingInstruments.png b/test-data/musicxml-testsuite/72a-TransposingInstruments.png new file mode 100644 index 000000000..a933dc1a2 Binary files /dev/null and b/test-data/musicxml-testsuite/72a-TransposingInstruments.png differ diff --git a/test-data/musicxml-testsuite/72a-TransposingInstruments.xml b/test-data/musicxml-testsuite/72a-TransposingInstruments.xml index bbd3ef8c6..e34add8af 100644 --- a/test-data/musicxml-testsuite/72a-TransposingInstruments.xml +++ b/test-data/musicxml-testsuite/72a-TransposingInstruments.xml @@ -1,11 +1,11 @@ - - + - Transposing instruments: Trumpet - in Bb, Horn in Eb, Piano; All of them show the C major scale (the + Transposing instruments: Trumpet + in Bb, Horn in Eb, Piano; All of them show the C major scale (the trumpet with 2 sharp, the horn with 3 sharp). diff --git a/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png b/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png new file mode 100644 index 000000000..170244ef9 Binary files /dev/null and b/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.png differ diff --git a/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.xml b/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.xml index 6a090be0e..948d992fc 100644 --- a/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.xml +++ b/test-data/musicxml-testsuite/72b-TransposingInstruments-Full.xml @@ -1,13 +1,13 @@ - - + Various transposition. Each - part plays a c'', just displayed in different display pitches. - The second-to-last staff uses a transposition where the displayed c' - is an actual f''' concert pitch. The final staff is an untransposed + part plays a c’’, just displayed in different display pitches. + The second-to-last staff uses a transposition where the displayed c’ + is an actual f’’’ concert pitch. The final staff is an untransposed instrument. @@ -49,7 +49,7 @@ D Tpt. - displayed c'=fis''' + displayed c’=fis’’’ MusicXML Part @@ -169,10 +169,6 @@ 1 - - 2 - major - - Clarinet in Eb - Eb Cl. + + + Clarinet in E + flat + + + Cl. (E + flat + ) + @@ -63,8 +75,15 @@ - Clarinet in Bb - Bb Cl. + + Clarinet in B + flat + + + Cl. (B + flat + ) + @@ -77,7 +96,7 @@ - + C diff --git a/test-data/musicxml-testsuite/73a-Percussion.png b/test-data/musicxml-testsuite/73a-Percussion.png new file mode 100644 index 000000000..77bb5cea4 Binary files /dev/null and b/test-data/musicxml-testsuite/73a-Percussion.png differ diff --git a/test-data/musicxml-testsuite/73a-Percussion.xml b/test-data/musicxml-testsuite/73a-Percussion.xml index 9b8d69ce5..573f4df75 100644 --- a/test-data/musicxml-testsuite/73a-Percussion.xml +++ b/test-data/musicxml-testsuite/73a-Percussion.xml @@ -1,12 +1,12 @@ - - + - Three types of percussion staves: - A five-line staff with bass clef for Timpani, a five-line staff with - percussion clef, and a one-line percussion staff with only unpitched + Three types of percussion staves: + A five-line staff with bass clef for Timpani, a five-line staff with + percussion clef, and a one-line percussion staff with only unpitched notes. diff --git a/test-data/musicxml-testsuite/74a-FiguredBass.png b/test-data/musicxml-testsuite/74a-FiguredBass.png new file mode 100644 index 000000000..3695db016 Binary files /dev/null and b/test-data/musicxml-testsuite/74a-FiguredBass.png differ diff --git a/test-data/musicxml-testsuite/74a-FiguredBass.xml b/test-data/musicxml-testsuite/74a-FiguredBass.xml index 9ffb8125e..ad00950e4 100644 --- a/test-data/musicxml-testsuite/74a-FiguredBass.xml +++ b/test-data/musicxml-testsuite/74a-FiguredBass.xml @@ -1,14 +1,11 @@ - - + - Some figured bass containing - alterated figures, bracketed figures and slashed figures. The last - note contains an empty <figured-bass> element, which is - invalid MusicXML, to check how well applications cope with malformed - files. + Some figured bass containing + alterated figures, bracketed figures and slashed figures. Note that this file does not contain any extenders! @@ -86,15 +83,6 @@ eighth - - - - - G4 - 8 - 1 - quarter - light-heavy diff --git a/test-data/musicxml-testsuite/75a-AccordionRegistrations.png b/test-data/musicxml-testsuite/75a-AccordionRegistrations.png new file mode 100644 index 000000000..38eaab51b Binary files /dev/null and b/test-data/musicxml-testsuite/75a-AccordionRegistrations.png differ diff --git a/test-data/musicxml-testsuite/75a-AccordionRegistrations.xml b/test-data/musicxml-testsuite/75a-AccordionRegistrations.xml index 01893680b..af696a7a4 100644 --- a/test-data/musicxml-testsuite/75a-AccordionRegistrations.xml +++ b/test-data/musicxml-testsuite/75a-AccordionRegistrations.xml @@ -1,10 +1,10 @@ - - + - All possible accordion + All possible accordion registrations. @@ -17,8 +17,8 @@ @@ -267,7 +267,7 @@ quarter 1/3/1 - @@ -282,73 +282,6 @@ quarter empty - - - - - - - - - - - - - C4 - 1 - 1 - quarter - empty M - - - - - - test - - - - - - C4 - 1 - 1 - quarter - inval.M - - - - - - 0 - - - - - - C4 - 1 - 1 - quarter - M=0 - - - - - - 5 - - - - - - C4 - 1 - 1 - quarter - M=5 - - light-heavy diff --git a/test-data/musicxml-testsuite/90a-Compressed-MusicXML.mxl b/test-data/musicxml-testsuite/90a-Compressed-MusicXML.mxl new file mode 100644 index 000000000..d70909448 Binary files /dev/null and b/test-data/musicxml-testsuite/90a-Compressed-MusicXML.mxl differ diff --git a/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png b/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png new file mode 100644 index 000000000..3f844969d Binary files /dev/null and b/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png differ diff --git a/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.png b/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.png new file mode 100644 index 000000000..4e9e445e7 Binary files /dev/null and b/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.png differ diff --git a/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.xml b/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.xml index 1d30c280f..e1b68e41d 100644 --- a/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.xml +++ b/test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.xml @@ -9,9 +9,9 @@ - Dolet 3 for - Sibelius (5.1) did not print out any closing beam - tags, only starting and continuing beam tags. For + Dolet 3 for + Sibelius (5.1) did not print out any closing beam + tags, only starting and continuing beam tags. For such files, one either needs to ignore all beaming information or close all beams @@ -21,7 +21,7 @@ - + @@ -116,7 +116,7 @@ - + - + diff --git a/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.png b/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.png new file mode 100644 index 000000000..2248c2803 Binary files /dev/null and b/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.png differ diff --git a/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.xml b/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.xml index a2626c0f2..d6bdab742 100644 --- a/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.xml +++ b/test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.xml @@ -8,9 +8,9 @@ Dolet 3.4 for Sibelius - If we properly ignore all beaming - information from the Dolet 3 for Sibelius export file, make sure that - the lyrics syllables are still assigned to the correct + If we properly ignore all beaming + information from the Dolet 3 for Sibelius export file, make sure that + the lyrics syllables are still assigned to the correct notes. diff --git a/test-data/musicxml-testsuite/LICENSE b/test-data/musicxml-testsuite/LICENSE index b6cebc252..8d8744c30 100644 --- a/test-data/musicxml-testsuite/LICENSE +++ b/test-data/musicxml-testsuite/LICENSE @@ -2,7 +2,7 @@ An (unofficial) MusicXML Test Suite This test suite of MusicXML unit tests is licenced under the MIT license: -Copyright (c) 2008-2010, Reinhold Kainhofer +Copyright (c) 2008--2023, Reinhold Kainhofer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/test-data/musicxml3/chord-diagram.png b/test-data/musicxml3/chord-diagram.png new file mode 100644 index 000000000..429312441 Binary files /dev/null and b/test-data/musicxml3/chord-diagram.png differ diff --git a/test-data/musicxml3/compressed.png b/test-data/musicxml3/compressed.png new file mode 100644 index 000000000..66b3a040a Binary files /dev/null and b/test-data/musicxml3/compressed.png differ diff --git a/test-data/musicxml3/first-bar-tempo.png b/test-data/musicxml3/first-bar-tempo.png new file mode 100644 index 000000000..01daebac7 Binary files /dev/null and b/test-data/musicxml3/first-bar-tempo.png differ diff --git a/test-data/musicxml3/full-bar-rest.png b/test-data/musicxml3/full-bar-rest.png new file mode 100644 index 000000000..154ca78cd Binary files /dev/null and b/test-data/musicxml3/full-bar-rest.png differ diff --git a/test-data/musicxml3/tie-destination.png b/test-data/musicxml3/tie-destination.png new file mode 100644 index 000000000..3eb0d2f71 Binary files /dev/null and b/test-data/musicxml3/tie-destination.png differ diff --git a/test-data/musicxml3/track-volume-balance.png b/test-data/musicxml3/track-volume-balance.png new file mode 100644 index 000000000..5e34fe628 Binary files /dev/null and b/test-data/musicxml3/track-volume-balance.png differ diff --git a/test-data/musicxml4/barlines.xml b/test-data/musicxml4/barlines.xml new file mode 100644 index 000000000..ea90985e0 --- /dev/null +++ b/test-data/musicxml4/barlines.xml @@ -0,0 +1,140 @@ + + + + + + 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 + + + + + + + diff --git a/test-data/musicxml4/bends.png b/test-data/musicxml4/bends.png new file mode 100644 index 000000000..c50309c75 Binary files /dev/null and b/test-data/musicxml4/bends.png differ diff --git a/test-data/musicxml-testsuite/100a-Guitare-Bends.xml b/test-data/musicxml4/bends.xml similarity index 100% rename from test-data/musicxml-testsuite/100a-Guitare-Bends.xml rename to test-data/musicxml4/bends.xml diff --git a/test-data/musicxml4/partwise-anacrusis.xml b/test-data/musicxml4/partwise-anacrusis.xml new file mode 100644 index 000000000..36bb366f9 --- /dev/null +++ b/test-data/musicxml4/partwise-anacrusis.xml @@ -0,0 +1,72 @@ + + + + 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-data/musicxml4/partwise-basic.xml b/test-data/musicxml4/partwise-basic.xml new file mode 100644 index 000000000..8005df275 --- /dev/null +++ b/test-data/musicxml4/partwise-basic.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/test-data/musicxml4/partwise-complex-measures.xml b/test-data/musicxml4/partwise-complex-measures.xml new file mode 100644 index 000000000..a64157408 --- /dev/null +++ b/test-data/musicxml4/partwise-complex-measures.xml @@ -0,0 +1,283 @@ + + + + + Track 1 + + + Track 2 + + + + + + 480 + + + + + + + C + 4 + + 480 + quarter + + + + + + + D + 4 + + 480 + quarter + + + + + + C + 4 + + 720 + quarter + + + + + 120 + + + + + + C + 4 + + 120 + 16th + + + + + + C + 4 + + 480 + quarter + 2 + + + + + 960 + + + + 120 + + + + 90 + + + + + + C + 5 + + 960 + + 2 + 2 + + + + + 480 + + + + + + + C + 4 + + 480 + quarter + + + + + + + D + 4 + + 480 + quarter + + + + + + C + 4 + + 720 + quarter + + + + + 120 + + + + + + C + 4 + + 120 + 16th + + + + + + C + 4 + + 480 + quarter + 2 + + + + + 960 + + + + 120 + + + + 90 + + + + + + C + 5 + + 960 + + 2 + 2 + + + + + + + 240 + + 4 + + + + + + + 480 + + + + + C + 5 + + 480 + + + + 720 + + + + + C + 5 + + 240 + 2 + + + + 123 + + + + + C + 5 + + 120 + 3 + + + + + + + 480 + + + + + C + 5 + + 480 + + + + 720 + + + + + C + 5 + + 240 + 2 + + + + 123 + + + + + C + 5 + + 120 + 3 + + + + \ No newline at end of file diff --git a/test-data/musicxml4/partwise-staff-change.xml b/test-data/musicxml4/partwise-staff-change.xml new file mode 100644 index 000000000..b063f2666 --- /dev/null +++ b/test-data/musicxml4/partwise-staff-change.xml @@ -0,0 +1,624 @@ + + + + + Track 1 + + + Track 2 + + + + + + 480 + + 4 + + + 2 + + G + 2 + + + F + 4 + + + + + D + 5 + + 120 + 1 + 16th + 1 + + + + G + 1 + 2 + + 120 + 1 + 16th + up + 2 + begin + begin + + + + B + 1 + 2 + + 120 + 1 + 16th + sharp + up + 2 + continue + continue + + + + D + 1 + 3 + + 120 + 1 + 16th + up + 2 + end + end + + + + G + 1 + 3 + + 120 + 1 + 16th + up + 2 + begin + begin + + + + B + 1 + 2 + + 120 + 1 + 16th + up + 2 + continue + continue + + + + D + 1 + 3 + + 120 + 1 + 16th + up + 2 + continue + continue + + + + G + 1 + 3 + + 120 + 1 + 16th + up + 2 + end + end + + + + B + 1 + 3 + + 120 + 1 + 16th + sharp + up + 2 + begin + begin + + + + D + 1 + 3 + + 120 + 1 + 16th + up + 2 + continue + continue + + + + G + 1 + 3 + + 120 + 1 + 16th + up + 2 + continue + continue + + + + B + 1 + 3 + + 120 + 1 + 16th + up + 2 + end + end + + + + D + 1 + 4 + + 120 + 1 + 16th + up + 1 + begin + begin + + + + G + 1 + 3 + + 120 + 1 + 16th + up + 1 + continue + continue + + + + B + 1 + 3 + + 120 + 1 + 16th + sharp + up + 1 + continue + continue + + + + D + 1 + 4 + + 120 + 1 + 16th + up + 1 + end + end + + + 1920 + + + + B + 1 + 1 + + 240 + 5 + eighth + sharp + down + 2 + begin + + + + + + + + + G + 1 + 2 + + 240 + 5 + eighth + down + 2 + continue + + + + + + + + + B + 1 + 1 + + 240 + 5 + eighth + down + 2 + continue + + + + + + + + + G + 1 + 2 + + 240 + 5 + eighth + down + 2 + end + + + + + + + + + B + 1 + 1 + + 240 + 5 + eighth + down + 2 + begin + + + + + + + + + G + 1 + 2 + + 240 + 5 + eighth + down + 2 + continue + + + + + + + + + B + 1 + 1 + + 240 + 5 + eighth + down + 2 + continue + + + + + + + + + G + 1 + 2 + + 240 + 5 + eighth + down + 2 + end + + + + + + + + + + + + + D + 5 + + 120 + 1 + 16th + 1 + + + + G + 1 + 2 + + 120 + 1 + 16th + up + 2 + begin + begin + + + + C + 1 + 3 + + 120 + 1 + 16th + up + 2 + continue + continue + + + + E + 3 + + 120 + 1 + 16th + up + 2 + end + end + + + + G + 1 + 3 + + 120 + 1 + 16th + up + 2 + begin + begin + + + + C + 1 + 3 + + 120 + 1 + 16th + up + 2 + continue + continue + + + + E + 3 + + 120 + 1 + 16th + up + 2 + continue + continue + + + + G + 1 + 3 + + 120 + 1 + 16th + up + 2 + end + end + + + + C + 1 + 4 + + 120 + 1 + 16th + up + 1 + begin + begin + " + + + E + 3 + + 120 + 1 + 16th + up + 2 + continue + continue + + + + G + 1 + 3 + + 120 + 1 + 16th + up + 2 + continue + continue + + + + C + 1 + 4 + + 120 + 1 + 16th + up + 1 + end + end + + + + E + 4 + + 120 + 1 + 16th + up + 1 + begin + begin + + + + G + 1 + 3 + + 120 + 1 + 16th + up + 1 + continue + continue + + + + C + 1 + 4 + + 120 + 1 + 16th + up + 1 + continue + continue + + + + E + 4 + + 120 + 1 + 16th + up + 1 + end + end + + + + \ No newline at end of file diff --git a/test-data/musicxml4/timewise-anacrusis.xml b/test-data/musicxml4/timewise-anacrusis.xml new file mode 100644 index 000000000..0aae257ea --- /dev/null +++ b/test-data/musicxml4/timewise-anacrusis.xml @@ -0,0 +1,68 @@ + + + + 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 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-data/musicxml4/timewise-basic.xml b/test-data/musicxml4/timewise-basic.xml new file mode 100644 index 000000000..c1a610be2 --- /dev/null +++ b/test-data/musicxml4/timewise-basic.xml @@ -0,0 +1,87 @@ + + + + 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-data/test-results.html b/test-data/test-results.html index 6542120d7..359e715a5 100644 --- a/test-data/test-results.html +++ b/test-data/test-results.html @@ -11,6 +11,7 @@ body { padding: 1rem; font-family: 'Noto Sans', sans-serif; + min-height: 100vh; } .comparer { @@ -65,28 +66,72 @@ left: 0; top: 0; } + .accepted .diff, .accepted .expected, .accepted .actual { border-color: green; } + + .card.accepted { + border-color: green; + } + + body.hide-accepted .accepted { + display: none; + } + + .drop-area { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: rgba(255, 255, 255, 0.5); + display: flex; + justify-content: center; + align-items: center; + display: none; + pointer-events: none; + } + + body.drop .drop-area { + display: flex; + } + + .drop-message { + width: 200px; + height: 200px; + font-weight: bold; + border: 5px dashed #426d9d; + color: #426d9d; + display: flex; + text-align: center; + border-radius: 10px; + justify-content: center; + align-items: center; + padding: 1rem; + }

alphaTab - Visual Test Results

- This page contains any failing visual tests for comparison and acceptance. - Run the visualTests via npm run test or using Mocha for VS Code. + This page contains any failing visual tests for comparison and acceptance. + Run the visualTests via npm run test or using Mocha for VS Code. +

+

+ +

- - + + +
+
+ Drop Test-Results Zip to page +
+
\ No newline at end of file diff --git a/test-data/visual-tests/bounds-lookup/onnotes-beat.png b/test-data/visual-tests/bounds-lookup/onnotes-beat.png index 00855f5b4..3486fff42 100644 Binary files a/test-data/visual-tests/bounds-lookup/onnotes-beat.png and b/test-data/visual-tests/bounds-lookup/onnotes-beat.png differ diff --git a/test-data/visual-tests/bounds-lookup/real-bar.png b/test-data/visual-tests/bounds-lookup/real-bar.png index dda335d28..6e143b3b0 100644 Binary files a/test-data/visual-tests/bounds-lookup/real-bar.png and b/test-data/visual-tests/bounds-lookup/real-bar.png differ diff --git a/test-data/visual-tests/bounds-lookup/real-beat.png b/test-data/visual-tests/bounds-lookup/real-beat.png index d27ea90cb..c17998af8 100644 Binary files a/test-data/visual-tests/bounds-lookup/real-beat.png and b/test-data/visual-tests/bounds-lookup/real-beat.png differ diff --git a/test-data/visual-tests/bounds-lookup/real-master.png b/test-data/visual-tests/bounds-lookup/real-master.png index e2718cb56..7dff4a4ad 100644 Binary files a/test-data/visual-tests/bounds-lookup/real-master.png and b/test-data/visual-tests/bounds-lookup/real-master.png differ diff --git a/test-data/visual-tests/bounds-lookup/real-note.png b/test-data/visual-tests/bounds-lookup/real-note.png index 78b17d581..ac4d625e9 100644 Binary files a/test-data/visual-tests/bounds-lookup/real-note.png and b/test-data/visual-tests/bounds-lookup/real-note.png differ diff --git a/test-data/visual-tests/bounds-lookup/real-system.png b/test-data/visual-tests/bounds-lookup/real-system.png index 3f9ab82aa..c89872126 100644 Binary files a/test-data/visual-tests/bounds-lookup/real-system.png and b/test-data/visual-tests/bounds-lookup/real-system.png differ diff --git a/test-data/visual-tests/bounds-lookup/visual-bar.png b/test-data/visual-tests/bounds-lookup/visual-bar.png index 07a3ac916..459456191 100644 Binary files a/test-data/visual-tests/bounds-lookup/visual-bar.png and b/test-data/visual-tests/bounds-lookup/visual-bar.png differ diff --git a/test-data/visual-tests/bounds-lookup/visual-beat.png b/test-data/visual-tests/bounds-lookup/visual-beat.png index 0696684ff..059a1e966 100644 Binary files a/test-data/visual-tests/bounds-lookup/visual-beat.png and b/test-data/visual-tests/bounds-lookup/visual-beat.png differ diff --git a/test-data/visual-tests/bounds-lookup/visual-master.png b/test-data/visual-tests/bounds-lookup/visual-master.png index 4bde7031a..bd925f6b2 100644 Binary files a/test-data/visual-tests/bounds-lookup/visual-master.png and b/test-data/visual-tests/bounds-lookup/visual-master.png differ diff --git a/test-data/visual-tests/bounds-lookup/visual-note.png b/test-data/visual-tests/bounds-lookup/visual-note.png index 78b17d581..ac4d625e9 100644 Binary files a/test-data/visual-tests/bounds-lookup/visual-note.png and b/test-data/visual-tests/bounds-lookup/visual-note.png differ diff --git a/test-data/visual-tests/bounds-lookup/visual-system.png b/test-data/visual-tests/bounds-lookup/visual-system.png index 7888777aa..78f65ef4e 100644 Binary files a/test-data/visual-tests/bounds-lookup/visual-system.png and b/test-data/visual-tests/bounds-lookup/visual-system.png differ diff --git a/test-data/visual-tests/effects-and-annotations/accentuations.png b/test-data/visual-tests/effects-and-annotations/accentuations.png index 28ee2ff06..7430f241f 100644 Binary files a/test-data/visual-tests/effects-and-annotations/accentuations.png and b/test-data/visual-tests/effects-and-annotations/accentuations.png differ diff --git a/test-data/visual-tests/effects-and-annotations/barre.png b/test-data/visual-tests/effects-and-annotations/barre.png index f1dd50beb..3b616e94a 100644 Binary files a/test-data/visual-tests/effects-and-annotations/barre.png and b/test-data/visual-tests/effects-and-annotations/barre.png differ diff --git a/test-data/visual-tests/effects-and-annotations/beat-slash.png b/test-data/visual-tests/effects-and-annotations/beat-slash.png index 9f1bd197b..eab620ea3 100644 Binary files a/test-data/visual-tests/effects-and-annotations/beat-slash.png and b/test-data/visual-tests/effects-and-annotations/beat-slash.png differ diff --git a/test-data/visual-tests/effects-and-annotations/beat-tempo-change.png b/test-data/visual-tests/effects-and-annotations/beat-tempo-change.png index fa42407b3..a92d34ad9 100644 Binary files a/test-data/visual-tests/effects-and-annotations/beat-tempo-change.png and b/test-data/visual-tests/effects-and-annotations/beat-tempo-change.png differ diff --git a/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png b/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png index 2c9def102..62d91f0d9 100644 Binary files a/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png and b/test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png differ diff --git a/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png b/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png index 2b203d1b4..cb6293618 100644 Binary files a/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png and b/test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png differ diff --git a/test-data/visual-tests/effects-and-annotations/bends.png b/test-data/visual-tests/effects-and-annotations/bends.png index 277652015..17be69563 100644 Binary files a/test-data/visual-tests/effects-and-annotations/bends.png and b/test-data/visual-tests/effects-and-annotations/bends.png differ diff --git a/test-data/visual-tests/effects-and-annotations/brush.png b/test-data/visual-tests/effects-and-annotations/brush.png index a3f18374b..e93dbb3b3 100644 Binary files a/test-data/visual-tests/effects-and-annotations/brush.png and b/test-data/visual-tests/effects-and-annotations/brush.png differ diff --git a/test-data/visual-tests/effects-and-annotations/chords-duplicates.png b/test-data/visual-tests/effects-and-annotations/chords-duplicates.png index 4e767115d..3b5f25b37 100644 Binary files a/test-data/visual-tests/effects-and-annotations/chords-duplicates.png and b/test-data/visual-tests/effects-and-annotations/chords-duplicates.png differ diff --git a/test-data/visual-tests/effects-and-annotations/chords.png b/test-data/visual-tests/effects-and-annotations/chords.png index 97ba39ef8..2bacd6c63 100644 Binary files a/test-data/visual-tests/effects-and-annotations/chords.png and b/test-data/visual-tests/effects-and-annotations/chords.png differ diff --git a/test-data/visual-tests/effects-and-annotations/dead-slap.png b/test-data/visual-tests/effects-and-annotations/dead-slap.png index c08840d29..525009270 100644 Binary files a/test-data/visual-tests/effects-and-annotations/dead-slap.png and b/test-data/visual-tests/effects-and-annotations/dead-slap.png differ diff --git a/test-data/visual-tests/effects-and-annotations/directions-simple.png b/test-data/visual-tests/effects-and-annotations/directions-simple.png index 4b4d02c9b..b26054ba4 100644 Binary files a/test-data/visual-tests/effects-and-annotations/directions-simple.png and b/test-data/visual-tests/effects-and-annotations/directions-simple.png differ diff --git a/test-data/visual-tests/effects-and-annotations/directions-symbols.png b/test-data/visual-tests/effects-and-annotations/directions-symbols.png index ba3ea2a7c..e3f924261 100644 Binary files a/test-data/visual-tests/effects-and-annotations/directions-symbols.png and b/test-data/visual-tests/effects-and-annotations/directions-symbols.png differ diff --git a/test-data/visual-tests/effects-and-annotations/dynamics.png b/test-data/visual-tests/effects-and-annotations/dynamics.png index 3739f997c..ac381d19c 100644 Binary files a/test-data/visual-tests/effects-and-annotations/dynamics.png and b/test-data/visual-tests/effects-and-annotations/dynamics.png differ diff --git a/test-data/visual-tests/effects-and-annotations/fade-in.png b/test-data/visual-tests/effects-and-annotations/fade-in.png index 08a7a142d..e4e0604f7 100644 Binary files a/test-data/visual-tests/effects-and-annotations/fade-in.png and b/test-data/visual-tests/effects-and-annotations/fade-in.png differ diff --git a/test-data/visual-tests/effects-and-annotations/fade.png b/test-data/visual-tests/effects-and-annotations/fade.png index 22c3635ff..0018e9277 100644 Binary files a/test-data/visual-tests/effects-and-annotations/fade.png and b/test-data/visual-tests/effects-and-annotations/fade.png differ diff --git a/test-data/visual-tests/effects-and-annotations/fingering-new.png b/test-data/visual-tests/effects-and-annotations/fingering-new.png index bc189b460..8c5f6b911 100644 Binary files a/test-data/visual-tests/effects-and-annotations/fingering-new.png and b/test-data/visual-tests/effects-and-annotations/fingering-new.png differ diff --git a/test-data/visual-tests/effects-and-annotations/fingering.png b/test-data/visual-tests/effects-and-annotations/fingering.png index 084c323e5..3612428c8 100644 Binary files a/test-data/visual-tests/effects-and-annotations/fingering.png and b/test-data/visual-tests/effects-and-annotations/fingering.png differ diff --git a/test-data/visual-tests/effects-and-annotations/free-time.png b/test-data/visual-tests/effects-and-annotations/free-time.png index 7ed5b6a4a..02845015d 100644 Binary files a/test-data/visual-tests/effects-and-annotations/free-time.png and b/test-data/visual-tests/effects-and-annotations/free-time.png differ diff --git a/test-data/visual-tests/effects-and-annotations/golpe-tab.png b/test-data/visual-tests/effects-and-annotations/golpe-tab.png index 7c94a6285..dd7718e0f 100644 Binary files a/test-data/visual-tests/effects-and-annotations/golpe-tab.png and b/test-data/visual-tests/effects-and-annotations/golpe-tab.png differ diff --git a/test-data/visual-tests/effects-and-annotations/golpe.png b/test-data/visual-tests/effects-and-annotations/golpe.png index 0a79955fe..2303c6e5d 100644 Binary files a/test-data/visual-tests/effects-and-annotations/golpe.png and b/test-data/visual-tests/effects-and-annotations/golpe.png differ diff --git a/test-data/visual-tests/effects-and-annotations/legato.png b/test-data/visual-tests/effects-and-annotations/legato.png index bf1cd9582..132e3f856 100644 Binary files a/test-data/visual-tests/effects-and-annotations/legato.png and b/test-data/visual-tests/effects-and-annotations/legato.png differ diff --git a/test-data/visual-tests/effects-and-annotations/let-ring.png b/test-data/visual-tests/effects-and-annotations/let-ring.png index 89895d74a..53957711a 100644 Binary files a/test-data/visual-tests/effects-and-annotations/let-ring.png and b/test-data/visual-tests/effects-and-annotations/let-ring.png differ diff --git a/test-data/visual-tests/effects-and-annotations/markers.png b/test-data/visual-tests/effects-and-annotations/markers.png index cee7fc7c2..58de18591 100644 Binary files a/test-data/visual-tests/effects-and-annotations/markers.png and b/test-data/visual-tests/effects-and-annotations/markers.png differ diff --git a/test-data/visual-tests/effects-and-annotations/ornaments.png b/test-data/visual-tests/effects-and-annotations/ornaments.png index cef915c29..67e71ef7e 100644 Binary files a/test-data/visual-tests/effects-and-annotations/ornaments.png and b/test-data/visual-tests/effects-and-annotations/ornaments.png differ diff --git a/test-data/visual-tests/effects-and-annotations/palm-mute.png b/test-data/visual-tests/effects-and-annotations/palm-mute.png index 4c9c9bdba..6ba160353 100644 Binary files a/test-data/visual-tests/effects-and-annotations/palm-mute.png and b/test-data/visual-tests/effects-and-annotations/palm-mute.png differ diff --git a/test-data/visual-tests/effects-and-annotations/pick-stroke.png b/test-data/visual-tests/effects-and-annotations/pick-stroke.png index 78b1ff903..9fabfa9f0 100644 Binary files a/test-data/visual-tests/effects-and-annotations/pick-stroke.png and b/test-data/visual-tests/effects-and-annotations/pick-stroke.png differ diff --git a/test-data/visual-tests/effects-and-annotations/rasgueado.png b/test-data/visual-tests/effects-and-annotations/rasgueado.png index 101add6c6..0272ee76b 100644 Binary files a/test-data/visual-tests/effects-and-annotations/rasgueado.png and b/test-data/visual-tests/effects-and-annotations/rasgueado.png differ diff --git a/test-data/visual-tests/effects-and-annotations/slides-line-break.png b/test-data/visual-tests/effects-and-annotations/slides-line-break.png index e077a1282..43b6f53a4 100644 Binary files a/test-data/visual-tests/effects-and-annotations/slides-line-break.png and b/test-data/visual-tests/effects-and-annotations/slides-line-break.png differ diff --git a/test-data/visual-tests/effects-and-annotations/slides.png b/test-data/visual-tests/effects-and-annotations/slides.png index 46d8a15d0..d155b65ff 100644 Binary files a/test-data/visual-tests/effects-and-annotations/slides.png and b/test-data/visual-tests/effects-and-annotations/slides.png differ diff --git a/test-data/visual-tests/effects-and-annotations/string-numbers.png b/test-data/visual-tests/effects-and-annotations/string-numbers.png index d5c2a57d6..bf04a020e 100644 Binary files a/test-data/visual-tests/effects-and-annotations/string-numbers.png and b/test-data/visual-tests/effects-and-annotations/string-numbers.png differ diff --git a/test-data/visual-tests/effects-and-annotations/sustain-1200.png b/test-data/visual-tests/effects-and-annotations/sustain-1200.png index 245bb4371..f5e908ddb 100644 Binary files a/test-data/visual-tests/effects-and-annotations/sustain-1200.png and b/test-data/visual-tests/effects-and-annotations/sustain-1200.png differ diff --git a/test-data/visual-tests/effects-and-annotations/sustain-600.png b/test-data/visual-tests/effects-and-annotations/sustain-600.png index 11f2f69b4..cd4f5a659 100644 Binary files a/test-data/visual-tests/effects-and-annotations/sustain-600.png and b/test-data/visual-tests/effects-and-annotations/sustain-600.png differ diff --git a/test-data/visual-tests/effects-and-annotations/sustain-850.png b/test-data/visual-tests/effects-and-annotations/sustain-850.png index 433cd1851..9b30f9245 100644 Binary files a/test-data/visual-tests/effects-and-annotations/sustain-850.png and b/test-data/visual-tests/effects-and-annotations/sustain-850.png differ diff --git a/test-data/visual-tests/effects-and-annotations/tap.png b/test-data/visual-tests/effects-and-annotations/tap.png index 7f9dbb376..2cf4b0331 100644 Binary files a/test-data/visual-tests/effects-and-annotations/tap.png and b/test-data/visual-tests/effects-and-annotations/tap.png differ diff --git a/test-data/visual-tests/effects-and-annotations/tempo-text.png b/test-data/visual-tests/effects-and-annotations/tempo-text.png index e7bd7ed26..e14baff41 100644 Binary files a/test-data/visual-tests/effects-and-annotations/tempo-text.png and b/test-data/visual-tests/effects-and-annotations/tempo-text.png differ diff --git a/test-data/visual-tests/effects-and-annotations/tempo.png b/test-data/visual-tests/effects-and-annotations/tempo.png index cae5bab63..9ba3cf4bc 100644 Binary files a/test-data/visual-tests/effects-and-annotations/tempo.png and b/test-data/visual-tests/effects-and-annotations/tempo.png differ diff --git a/test-data/visual-tests/effects-and-annotations/text.png b/test-data/visual-tests/effects-and-annotations/text.png index 45937f819..074081ff2 100644 Binary files a/test-data/visual-tests/effects-and-annotations/text.png and b/test-data/visual-tests/effects-and-annotations/text.png differ diff --git a/test-data/visual-tests/effects-and-annotations/timer.png b/test-data/visual-tests/effects-and-annotations/timer.png index ed94738aa..736495081 100644 Binary files a/test-data/visual-tests/effects-and-annotations/timer.png and b/test-data/visual-tests/effects-and-annotations/timer.png differ diff --git a/test-data/visual-tests/effects-and-annotations/tremolo-bar.png b/test-data/visual-tests/effects-and-annotations/tremolo-bar.png index 03669bf6e..308da54ad 100644 Binary files a/test-data/visual-tests/effects-and-annotations/tremolo-bar.png and b/test-data/visual-tests/effects-and-annotations/tremolo-bar.png differ diff --git a/test-data/visual-tests/effects-and-annotations/tremolo-picking.png b/test-data/visual-tests/effects-and-annotations/tremolo-picking.png index a3bdba6b2..760a2b158 100644 Binary files a/test-data/visual-tests/effects-and-annotations/tremolo-picking.png and b/test-data/visual-tests/effects-and-annotations/tremolo-picking.png differ diff --git a/test-data/visual-tests/effects-and-annotations/trill.png b/test-data/visual-tests/effects-and-annotations/trill.png index 09ffb9474..7883611ea 100644 Binary files a/test-data/visual-tests/effects-and-annotations/trill.png and b/test-data/visual-tests/effects-and-annotations/trill.png differ diff --git a/test-data/visual-tests/effects-and-annotations/triplet-feel.png b/test-data/visual-tests/effects-and-annotations/triplet-feel.png index 1aed28263..39cc260b4 100644 Binary files a/test-data/visual-tests/effects-and-annotations/triplet-feel.png and b/test-data/visual-tests/effects-and-annotations/triplet-feel.png differ diff --git a/test-data/visual-tests/effects-and-annotations/tuplets-advanced.png b/test-data/visual-tests/effects-and-annotations/tuplets-advanced.png index 1df7ecbf8..1f0039c46 100644 Binary files a/test-data/visual-tests/effects-and-annotations/tuplets-advanced.png and b/test-data/visual-tests/effects-and-annotations/tuplets-advanced.png differ diff --git a/test-data/visual-tests/effects-and-annotations/tuplets.png b/test-data/visual-tests/effects-and-annotations/tuplets.png index 3aaf894bf..088f61196 100644 Binary files a/test-data/visual-tests/effects-and-annotations/tuplets.png and b/test-data/visual-tests/effects-and-annotations/tuplets.png differ diff --git a/test-data/visual-tests/effects-and-annotations/vibrato.png b/test-data/visual-tests/effects-and-annotations/vibrato.png index 5e0c1a95b..ded435297 100644 Binary files a/test-data/visual-tests/effects-and-annotations/vibrato.png and b/test-data/visual-tests/effects-and-annotations/vibrato.png differ diff --git a/test-data/visual-tests/general/alternate-endings.png b/test-data/visual-tests/general/alternate-endings.png index 3b68b9496..d0e038c7b 100644 Binary files a/test-data/visual-tests/general/alternate-endings.png and b/test-data/visual-tests/general/alternate-endings.png differ diff --git a/test-data/visual-tests/general/colors-disabled.png b/test-data/visual-tests/general/colors-disabled.png new file mode 100644 index 000000000..30226c3ff Binary files /dev/null and b/test-data/visual-tests/general/colors-disabled.png differ diff --git a/test-data/visual-tests/general/colors.gp b/test-data/visual-tests/general/colors.gp new file mode 100644 index 000000000..107840c79 Binary files /dev/null and b/test-data/visual-tests/general/colors.gp differ diff --git a/test-data/visual-tests/general/colors.png b/test-data/visual-tests/general/colors.png new file mode 100644 index 000000000..162ac6433 Binary files /dev/null and b/test-data/visual-tests/general/colors.png differ diff --git a/test-data/visual-tests/general/font-fallback.png b/test-data/visual-tests/general/font-fallback.png new file mode 100644 index 000000000..f74f5eeef Binary files /dev/null and b/test-data/visual-tests/general/font-fallback.png differ diff --git a/test-data/visual-tests/general/repeats.png b/test-data/visual-tests/general/repeats.png index 4882ab2e1..9719a4b14 100644 Binary files a/test-data/visual-tests/general/repeats.png and b/test-data/visual-tests/general/repeats.png differ diff --git a/test-data/visual-tests/general/song-details.png b/test-data/visual-tests/general/song-details.png index cefde99ea..443371d64 100644 Binary files a/test-data/visual-tests/general/song-details.png and b/test-data/visual-tests/general/song-details.png differ diff --git a/test-data/visual-tests/general/tuning.png b/test-data/visual-tests/general/tuning.png index c4f6b031c..a056913cb 100644 Binary files a/test-data/visual-tests/general/tuning.png and b/test-data/visual-tests/general/tuning.png differ diff --git a/test-data/visual-tests/guitar-tabs/rhythm-with-beams.png b/test-data/visual-tests/guitar-tabs/rhythm-with-beams.png index aff1b75a5..02e6302d9 100644 Binary files a/test-data/visual-tests/guitar-tabs/rhythm-with-beams.png and b/test-data/visual-tests/guitar-tabs/rhythm-with-beams.png differ diff --git a/test-data/visual-tests/guitar-tabs/rhythm.png b/test-data/visual-tests/guitar-tabs/rhythm.png index a9f83f2a8..841a6c40c 100644 Binary files a/test-data/visual-tests/guitar-tabs/rhythm.png and b/test-data/visual-tests/guitar-tabs/rhythm.png differ diff --git a/test-data/visual-tests/guitar-tabs/string-variations.png b/test-data/visual-tests/guitar-tabs/string-variations.png index a68a1f17e..3806a9e54 100644 Binary files a/test-data/visual-tests/guitar-tabs/string-variations.png and b/test-data/visual-tests/guitar-tabs/string-variations.png differ diff --git a/test-data/visual-tests/issues/let-ring-empty-voice.png b/test-data/visual-tests/issues/let-ring-empty-voice.png index 90be9357a..d7af1e624 100644 Binary files a/test-data/visual-tests/issues/let-ring-empty-voice.png and b/test-data/visual-tests/issues/let-ring-empty-voice.png differ diff --git a/test-data/visual-tests/layout/brackets-braces-none.png b/test-data/visual-tests/layout/brackets-braces-none.png index a7e6b7f2c..531f52adb 100644 Binary files a/test-data/visual-tests/layout/brackets-braces-none.png and b/test-data/visual-tests/layout/brackets-braces-none.png differ diff --git a/test-data/visual-tests/layout/brackets-braces-similar.png b/test-data/visual-tests/layout/brackets-braces-similar.png index 17402626e..e4f56d32b 100644 Binary files a/test-data/visual-tests/layout/brackets-braces-similar.png and b/test-data/visual-tests/layout/brackets-braces-similar.png differ diff --git a/test-data/visual-tests/layout/brackets-braces-staves.png b/test-data/visual-tests/layout/brackets-braces-staves.png index 7152b48c1..3ad701b3d 100644 Binary files a/test-data/visual-tests/layout/brackets-braces-staves.png and b/test-data/visual-tests/layout/brackets-braces-staves.png differ diff --git a/test-data/visual-tests/layout/horizontal-layout-5to8.png b/test-data/visual-tests/layout/horizontal-layout-5to8.png index c2d29cd69..ef6fd5d5a 100644 Binary files a/test-data/visual-tests/layout/horizontal-layout-5to8.png and b/test-data/visual-tests/layout/horizontal-layout-5to8.png differ diff --git a/test-data/visual-tests/layout/horizontal-layout.png b/test-data/visual-tests/layout/horizontal-layout.png index be2b0205e..16496533b 100644 Binary files a/test-data/visual-tests/layout/horizontal-layout.png and b/test-data/visual-tests/layout/horizontal-layout.png differ diff --git a/test-data/visual-tests/layout/multi-track.png b/test-data/visual-tests/layout/multi-track.png index 8c345091f..e9c787402 100644 Binary files a/test-data/visual-tests/layout/multi-track.png and b/test-data/visual-tests/layout/multi-track.png differ diff --git a/test-data/visual-tests/layout/multi-voice.png b/test-data/visual-tests/layout/multi-voice.png index 1f18e1ad9..08f2217ee 100644 Binary files a/test-data/visual-tests/layout/multi-voice.png and b/test-data/visual-tests/layout/multi-voice.png differ diff --git a/test-data/visual-tests/layout/multibar-rest-all-tracks.png b/test-data/visual-tests/layout/multibar-rest-all-tracks.png new file mode 100644 index 000000000..b75fdcf01 Binary files /dev/null and b/test-data/visual-tests/layout/multibar-rest-all-tracks.png differ diff --git a/test-data/visual-tests/layout/multibar-rest-multi-track.png b/test-data/visual-tests/layout/multibar-rest-multi-track.png new file mode 100644 index 000000000..fc39eab1e Binary files /dev/null and b/test-data/visual-tests/layout/multibar-rest-multi-track.png differ diff --git a/test-data/visual-tests/layout/multibar-rest-single-track.png b/test-data/visual-tests/layout/multibar-rest-single-track.png new file mode 100644 index 000000000..8486415c9 Binary files /dev/null and b/test-data/visual-tests/layout/multibar-rest-single-track.png differ diff --git a/test-data/visual-tests/layout/multibar-rest.gp b/test-data/visual-tests/layout/multibar-rest.gp new file mode 100644 index 000000000..7ef5cea7b Binary files /dev/null and b/test-data/visual-tests/layout/multibar-rest.gp differ diff --git a/test-data/visual-tests/layout/page-layout-5barsperrow.png b/test-data/visual-tests/layout/page-layout-5barsperrow.png index 37e97ac3b..b8c1114dd 100644 Binary files a/test-data/visual-tests/layout/page-layout-5barsperrow.png and b/test-data/visual-tests/layout/page-layout-5barsperrow.png differ diff --git a/test-data/visual-tests/layout/page-layout-5to8.png b/test-data/visual-tests/layout/page-layout-5to8.png index efa0c8fce..999eb8f36 100644 Binary files a/test-data/visual-tests/layout/page-layout-5to8.png and b/test-data/visual-tests/layout/page-layout-5to8.png differ diff --git a/test-data/visual-tests/layout/page-layout-justify-last-row.png b/test-data/visual-tests/layout/page-layout-justify-last-row.png index cd47fe5f7..b1aee8717 100644 Binary files a/test-data/visual-tests/layout/page-layout-justify-last-row.png and b/test-data/visual-tests/layout/page-layout-justify-last-row.png differ diff --git a/test-data/visual-tests/layout/page-layout.png b/test-data/visual-tests/layout/page-layout.png index cb5f3945e..f2206c50f 100644 Binary files a/test-data/visual-tests/layout/page-layout.png and b/test-data/visual-tests/layout/page-layout.png differ diff --git a/test-data/visual-tests/layout/system-divider.png b/test-data/visual-tests/layout/system-divider.png index 075c88437..b6f3d5bb1 100644 Binary files a/test-data/visual-tests/layout/system-divider.png and b/test-data/visual-tests/layout/system-divider.png differ diff --git a/test-data/visual-tests/layout/system-layout-tex.png b/test-data/visual-tests/layout/system-layout-tex.png index 9608c7f52..2617ac9dc 100644 Binary files a/test-data/visual-tests/layout/system-layout-tex.png and b/test-data/visual-tests/layout/system-layout-tex.png differ diff --git a/test-data/visual-tests/layout/track-names-all-systems-multi.png b/test-data/visual-tests/layout/track-names-all-systems-multi.png index 79d12d9b5..c4a0a53cf 100644 Binary files a/test-data/visual-tests/layout/track-names-all-systems-multi.png and b/test-data/visual-tests/layout/track-names-all-systems-multi.png differ diff --git a/test-data/visual-tests/layout/track-names-first-system.png b/test-data/visual-tests/layout/track-names-first-system.png index 15d3f8e96..c1973e702 100644 Binary files a/test-data/visual-tests/layout/track-names-first-system.png and b/test-data/visual-tests/layout/track-names-first-system.png differ diff --git a/test-data/visual-tests/layout/track-names-full-name-all.png b/test-data/visual-tests/layout/track-names-full-name-all.png index 4d6dd618c..106b45e97 100644 Binary files a/test-data/visual-tests/layout/track-names-full-name-all.png and b/test-data/visual-tests/layout/track-names-full-name-all.png differ diff --git a/test-data/visual-tests/layout/track-names-full-name-horizontal.png b/test-data/visual-tests/layout/track-names-full-name-horizontal.png index c0c83fc5b..ff77c16e6 100644 Binary files a/test-data/visual-tests/layout/track-names-full-name-horizontal.png and b/test-data/visual-tests/layout/track-names-full-name-horizontal.png differ diff --git a/test-data/visual-tests/layout/track-names-full-name-short-name.png b/test-data/visual-tests/layout/track-names-full-name-short-name.png index 6589e3f8b..1ed6805a2 100644 Binary files a/test-data/visual-tests/layout/track-names-full-name-short-name.png and b/test-data/visual-tests/layout/track-names-full-name-short-name.png differ diff --git a/test-data/visual-tests/music-notation/accidentals-advanced.png b/test-data/visual-tests/music-notation/accidentals-advanced.png index b284cb078..0b16c21e7 100644 Binary files a/test-data/visual-tests/music-notation/accidentals-advanced.png and b/test-data/visual-tests/music-notation/accidentals-advanced.png differ diff --git a/test-data/visual-tests/music-notation/accidentals.png b/test-data/visual-tests/music-notation/accidentals.png index 6691a94e2..a4678f6a6 100644 Binary files a/test-data/visual-tests/music-notation/accidentals.png and b/test-data/visual-tests/music-notation/accidentals.png differ diff --git a/test-data/visual-tests/music-notation/barlines.png b/test-data/visual-tests/music-notation/barlines.png new file mode 100644 index 000000000..23a15c44e Binary files /dev/null and b/test-data/visual-tests/music-notation/barlines.png differ diff --git a/test-data/visual-tests/music-notation/barlines.xml b/test-data/visual-tests/music-notation/barlines.xml new file mode 100644 index 000000000..ea90985e0 --- /dev/null +++ b/test-data/visual-tests/music-notation/barlines.xml @@ -0,0 +1,140 @@ + + + + + + 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 + + + + + + + diff --git a/test-data/visual-tests/music-notation/beams-advanced.png b/test-data/visual-tests/music-notation/beams-advanced.png index 79ebda068..05ca32d3f 100644 Binary files a/test-data/visual-tests/music-notation/beams-advanced.png and b/test-data/visual-tests/music-notation/beams-advanced.png differ diff --git a/test-data/visual-tests/music-notation/brushes-ukulele.png b/test-data/visual-tests/music-notation/brushes-ukulele.png index a625062bf..883cb6102 100644 Binary files a/test-data/visual-tests/music-notation/brushes-ukulele.png and b/test-data/visual-tests/music-notation/brushes-ukulele.png differ diff --git a/test-data/visual-tests/music-notation/brushes.png b/test-data/visual-tests/music-notation/brushes.png index e99fc1425..7d720f891 100644 Binary files a/test-data/visual-tests/music-notation/brushes.png and b/test-data/visual-tests/music-notation/brushes.png differ diff --git a/test-data/visual-tests/music-notation/clefs.png b/test-data/visual-tests/music-notation/clefs.png index 4c45b6995..40a29638f 100644 Binary files a/test-data/visual-tests/music-notation/clefs.png and b/test-data/visual-tests/music-notation/clefs.png differ diff --git a/test-data/visual-tests/music-notation/forced-accidentals.png b/test-data/visual-tests/music-notation/forced-accidentals.png index 823f89747..23ae563db 100644 Binary files a/test-data/visual-tests/music-notation/forced-accidentals.png and b/test-data/visual-tests/music-notation/forced-accidentals.png differ diff --git a/test-data/visual-tests/music-notation/key-signatures-c3.png b/test-data/visual-tests/music-notation/key-signatures-c3.png index 6694ce575..a8e396667 100644 Binary files a/test-data/visual-tests/music-notation/key-signatures-c3.png and b/test-data/visual-tests/music-notation/key-signatures-c3.png differ diff --git a/test-data/visual-tests/music-notation/key-signatures-c4.png b/test-data/visual-tests/music-notation/key-signatures-c4.png index fb0d3c308..dab794c14 100644 Binary files a/test-data/visual-tests/music-notation/key-signatures-c4.png and b/test-data/visual-tests/music-notation/key-signatures-c4.png differ diff --git a/test-data/visual-tests/music-notation/key-signatures-f4.png b/test-data/visual-tests/music-notation/key-signatures-f4.png index 5e28f52fa..00b804c1e 100644 Binary files a/test-data/visual-tests/music-notation/key-signatures-f4.png and b/test-data/visual-tests/music-notation/key-signatures-f4.png differ diff --git a/test-data/visual-tests/music-notation/key-signatures-g2.png b/test-data/visual-tests/music-notation/key-signatures-g2.png index bb6fa81a5..c3c2af7bc 100644 Binary files a/test-data/visual-tests/music-notation/key-signatures-g2.png and b/test-data/visual-tests/music-notation/key-signatures-g2.png differ diff --git a/test-data/visual-tests/music-notation/key-signatures-mixed.png b/test-data/visual-tests/music-notation/key-signatures-mixed.png index cc4ae424c..d7929c2dd 100644 Binary files a/test-data/visual-tests/music-notation/key-signatures-mixed.png and b/test-data/visual-tests/music-notation/key-signatures-mixed.png differ diff --git a/test-data/visual-tests/music-notation/key-signatures.png b/test-data/visual-tests/music-notation/key-signatures.png index bb6fa81a5..c3c2af7bc 100644 Binary files a/test-data/visual-tests/music-notation/key-signatures.png and b/test-data/visual-tests/music-notation/key-signatures.png differ diff --git a/test-data/visual-tests/music-notation/notes-rests-beams.png b/test-data/visual-tests/music-notation/notes-rests-beams.png index 3d2400e32..bfe496f91 100644 Binary files a/test-data/visual-tests/music-notation/notes-rests-beams.png and b/test-data/visual-tests/music-notation/notes-rests-beams.png differ diff --git a/test-data/visual-tests/music-notation/rest-collisions.png b/test-data/visual-tests/music-notation/rest-collisions.png index aeb0c39cd..5cffa07f6 100644 Binary files a/test-data/visual-tests/music-notation/rest-collisions.png and b/test-data/visual-tests/music-notation/rest-collisions.png differ diff --git a/test-data/visual-tests/music-notation/time-signatures.png b/test-data/visual-tests/music-notation/time-signatures.png index dd745621a..709c5f7d0 100644 Binary files a/test-data/visual-tests/music-notation/time-signatures.png and b/test-data/visual-tests/music-notation/time-signatures.png differ diff --git a/test-data/visual-tests/notation-elements/chord-diagrams-off.png b/test-data/visual-tests/notation-elements/chord-diagrams-off.png index 606de4c45..2f6789825 100644 Binary files a/test-data/visual-tests/notation-elements/chord-diagrams-off.png and b/test-data/visual-tests/notation-elements/chord-diagrams-off.png differ diff --git a/test-data/visual-tests/notation-elements/chord-diagrams-on.png b/test-data/visual-tests/notation-elements/chord-diagrams-on.png index 90626de65..65785d752 100644 Binary files a/test-data/visual-tests/notation-elements/chord-diagrams-on.png and b/test-data/visual-tests/notation-elements/chord-diagrams-on.png differ diff --git a/test-data/visual-tests/notation-elements/effects-off.png b/test-data/visual-tests/notation-elements/effects-off.png index 40e51d0ae..d3cd6f72d 100644 Binary files a/test-data/visual-tests/notation-elements/effects-off.png and b/test-data/visual-tests/notation-elements/effects-off.png differ diff --git a/test-data/visual-tests/notation-elements/effects-on.png b/test-data/visual-tests/notation-elements/effects-on.png index fb11fc98c..367c55fcd 100644 Binary files a/test-data/visual-tests/notation-elements/effects-on.png and b/test-data/visual-tests/notation-elements/effects-on.png differ diff --git a/test-data/visual-tests/notation-elements/guitar-tuning-off.png b/test-data/visual-tests/notation-elements/guitar-tuning-off.png index c92480414..1f959f91e 100644 Binary files a/test-data/visual-tests/notation-elements/guitar-tuning-off.png and b/test-data/visual-tests/notation-elements/guitar-tuning-off.png differ diff --git a/test-data/visual-tests/notation-elements/guitar-tuning-on.png b/test-data/visual-tests/notation-elements/guitar-tuning-on.png index e3498c05a..6bb02f428 100644 Binary files a/test-data/visual-tests/notation-elements/guitar-tuning-on.png and b/test-data/visual-tests/notation-elements/guitar-tuning-on.png differ diff --git a/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png b/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png index 182c5674f..3b154667c 100644 Binary files a/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png and b/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png differ diff --git a/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png b/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png index 2100ea450..ae11db2d0 100644 Binary files a/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png and b/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png differ diff --git a/test-data/visual-tests/notation-elements/score-info-album.png b/test-data/visual-tests/notation-elements/score-info-album.png index 13a287d75..3c9b3d835 100644 Binary files a/test-data/visual-tests/notation-elements/score-info-album.png and b/test-data/visual-tests/notation-elements/score-info-album.png differ diff --git a/test-data/visual-tests/notation-elements/score-info-all.png b/test-data/visual-tests/notation-elements/score-info-all.png index c6ea65ec0..37ad41139 100644 Binary files a/test-data/visual-tests/notation-elements/score-info-all.png and b/test-data/visual-tests/notation-elements/score-info-all.png differ diff --git a/test-data/visual-tests/notation-elements/score-info-artist.png b/test-data/visual-tests/notation-elements/score-info-artist.png index d716419c3..b38f29ee1 100644 Binary files a/test-data/visual-tests/notation-elements/score-info-artist.png and b/test-data/visual-tests/notation-elements/score-info-artist.png differ diff --git a/test-data/visual-tests/notation-elements/score-info-copyright.png b/test-data/visual-tests/notation-elements/score-info-copyright.png index 41cb7ee4a..98e821d7e 100644 Binary files a/test-data/visual-tests/notation-elements/score-info-copyright.png and b/test-data/visual-tests/notation-elements/score-info-copyright.png differ diff --git a/test-data/visual-tests/notation-elements/score-info-music.png b/test-data/visual-tests/notation-elements/score-info-music.png index 68f704032..136a9eef7 100644 Binary files a/test-data/visual-tests/notation-elements/score-info-music.png and b/test-data/visual-tests/notation-elements/score-info-music.png differ diff --git a/test-data/visual-tests/notation-elements/score-info-subtitle.png b/test-data/visual-tests/notation-elements/score-info-subtitle.png index b5f2957fb..e9991eda8 100644 Binary files a/test-data/visual-tests/notation-elements/score-info-subtitle.png and b/test-data/visual-tests/notation-elements/score-info-subtitle.png differ diff --git a/test-data/visual-tests/notation-elements/score-info-title.png b/test-data/visual-tests/notation-elements/score-info-title.png index d5ef5eaf0..02d6d6750 100644 Binary files a/test-data/visual-tests/notation-elements/score-info-title.png and b/test-data/visual-tests/notation-elements/score-info-title.png differ diff --git a/test-data/visual-tests/notation-elements/score-info-words-and-music.png b/test-data/visual-tests/notation-elements/score-info-words-and-music.png index 1497a59a9..c08408e2d 100644 Binary files a/test-data/visual-tests/notation-elements/score-info-words-and-music.png and b/test-data/visual-tests/notation-elements/score-info-words-and-music.png differ diff --git a/test-data/visual-tests/notation-elements/score-info-words.png b/test-data/visual-tests/notation-elements/score-info-words.png index c8444ddd5..5a8b8c118 100644 Binary files a/test-data/visual-tests/notation-elements/score-info-words.png and b/test-data/visual-tests/notation-elements/score-info-words.png differ diff --git a/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png b/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png index 94bfa79b8..98e0b0299 100644 Binary files a/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png and b/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png differ diff --git a/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png b/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png index fad09141c..5f4a77540 100644 Binary files a/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png and b/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png differ diff --git a/test-data/visual-tests/notation-elements/track-names-off.png b/test-data/visual-tests/notation-elements/track-names-off.png index 41cb7ee4a..d922f6dcc 100644 Binary files a/test-data/visual-tests/notation-elements/track-names-off.png and b/test-data/visual-tests/notation-elements/track-names-off.png differ diff --git a/test-data/visual-tests/notation-elements/track-names-on.png b/test-data/visual-tests/notation-elements/track-names-on.png index 553d5a9cd..88a86f1c3 100644 Binary files a/test-data/visual-tests/notation-elements/track-names-on.png and b/test-data/visual-tests/notation-elements/track-names-on.png differ diff --git a/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png b/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png index 379bf443a..95b13b815 100644 Binary files a/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png and b/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png differ diff --git a/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png b/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png index 2192ac971..394e8ec84 100644 Binary files a/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png and b/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png differ diff --git a/test-data/visual-tests/notation-legend/accentuations-default.png b/test-data/visual-tests/notation-legend/accentuations-default.png index d1c79e6c7..f37b63c42 100644 Binary files a/test-data/visual-tests/notation-legend/accentuations-default.png and b/test-data/visual-tests/notation-legend/accentuations-default.png differ diff --git a/test-data/visual-tests/notation-legend/accentuations-songbook.png b/test-data/visual-tests/notation-legend/accentuations-songbook.png index b6146e0f8..3509b9abb 100644 Binary files a/test-data/visual-tests/notation-legend/accentuations-songbook.png and b/test-data/visual-tests/notation-legend/accentuations-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/arpeggio-default.png b/test-data/visual-tests/notation-legend/arpeggio-default.png index eb5800d15..6b95a1fc7 100644 Binary files a/test-data/visual-tests/notation-legend/arpeggio-default.png and b/test-data/visual-tests/notation-legend/arpeggio-default.png differ diff --git a/test-data/visual-tests/notation-legend/arpeggio-songbook.png b/test-data/visual-tests/notation-legend/arpeggio-songbook.png index eb5800d15..6b95a1fc7 100644 Binary files a/test-data/visual-tests/notation-legend/arpeggio-songbook.png and b/test-data/visual-tests/notation-legend/arpeggio-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/bends-default.png b/test-data/visual-tests/notation-legend/bends-default.png index ba29301e5..3a1c51cff 100644 Binary files a/test-data/visual-tests/notation-legend/bends-default.png and b/test-data/visual-tests/notation-legend/bends-default.png differ diff --git a/test-data/visual-tests/notation-legend/bends-songbook.png b/test-data/visual-tests/notation-legend/bends-songbook.png index bcefaece7..8b29d5fda 100644 Binary files a/test-data/visual-tests/notation-legend/bends-songbook.png and b/test-data/visual-tests/notation-legend/bends-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/chords-default.png b/test-data/visual-tests/notation-legend/chords-default.png index 6e019f88d..30f078150 100644 Binary files a/test-data/visual-tests/notation-legend/chords-default.png and b/test-data/visual-tests/notation-legend/chords-default.png differ diff --git a/test-data/visual-tests/notation-legend/chords-songbook.png b/test-data/visual-tests/notation-legend/chords-songbook.png index 6e019f88d..30f078150 100644 Binary files a/test-data/visual-tests/notation-legend/chords-songbook.png and b/test-data/visual-tests/notation-legend/chords-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/crescendo-default.png b/test-data/visual-tests/notation-legend/crescendo-default.png index bda5844a9..a9c37b502 100644 Binary files a/test-data/visual-tests/notation-legend/crescendo-default.png and b/test-data/visual-tests/notation-legend/crescendo-default.png differ diff --git a/test-data/visual-tests/notation-legend/crescendo-songbook.png b/test-data/visual-tests/notation-legend/crescendo-songbook.png index bda5844a9..a9c37b502 100644 Binary files a/test-data/visual-tests/notation-legend/crescendo-songbook.png and b/test-data/visual-tests/notation-legend/crescendo-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/dead-default.png b/test-data/visual-tests/notation-legend/dead-default.png index ca8e83fd7..5fa4aafc3 100644 Binary files a/test-data/visual-tests/notation-legend/dead-default.png and b/test-data/visual-tests/notation-legend/dead-default.png differ diff --git a/test-data/visual-tests/notation-legend/dead-songbook.png b/test-data/visual-tests/notation-legend/dead-songbook.png index ca8e83fd7..5fa4aafc3 100644 Binary files a/test-data/visual-tests/notation-legend/dead-songbook.png and b/test-data/visual-tests/notation-legend/dead-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/dynamics-default.png b/test-data/visual-tests/notation-legend/dynamics-default.png index 156e93b52..7421f4873 100644 Binary files a/test-data/visual-tests/notation-legend/dynamics-default.png and b/test-data/visual-tests/notation-legend/dynamics-default.png differ diff --git a/test-data/visual-tests/notation-legend/dynamics-songbook.png b/test-data/visual-tests/notation-legend/dynamics-songbook.png index 156e93b52..7421f4873 100644 Binary files a/test-data/visual-tests/notation-legend/dynamics-songbook.png and b/test-data/visual-tests/notation-legend/dynamics-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/fingering-default.png b/test-data/visual-tests/notation-legend/fingering-default.png index 5362244f6..61c5efa1c 100644 Binary files a/test-data/visual-tests/notation-legend/fingering-default.png and b/test-data/visual-tests/notation-legend/fingering-default.png differ diff --git a/test-data/visual-tests/notation-legend/fingering-songbook.png b/test-data/visual-tests/notation-legend/fingering-songbook.png index 1301af1e3..d3014685d 100644 Binary files a/test-data/visual-tests/notation-legend/fingering-songbook.png and b/test-data/visual-tests/notation-legend/fingering-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/full-default-large.png b/test-data/visual-tests/notation-legend/full-default-large.png index eacae0e5b..e00c9000d 100644 Binary files a/test-data/visual-tests/notation-legend/full-default-large.png and b/test-data/visual-tests/notation-legend/full-default-large.png differ diff --git a/test-data/visual-tests/notation-legend/full-default-small.png b/test-data/visual-tests/notation-legend/full-default-small.png index 8702bf3cf..900a2e464 100644 Binary files a/test-data/visual-tests/notation-legend/full-default-small.png and b/test-data/visual-tests/notation-legend/full-default-small.png differ diff --git a/test-data/visual-tests/notation-legend/full-default.png b/test-data/visual-tests/notation-legend/full-default.png index 1487310db..03e176958 100644 Binary files a/test-data/visual-tests/notation-legend/full-default.png and b/test-data/visual-tests/notation-legend/full-default.png differ diff --git a/test-data/visual-tests/notation-legend/full-songbook.png b/test-data/visual-tests/notation-legend/full-songbook.png index ae2df2a76..c1c3cf77c 100644 Binary files a/test-data/visual-tests/notation-legend/full-songbook.png and b/test-data/visual-tests/notation-legend/full-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/grace-default.png b/test-data/visual-tests/notation-legend/grace-default.png index 3153ec9c3..54c6ae5ea 100644 Binary files a/test-data/visual-tests/notation-legend/grace-default.png and b/test-data/visual-tests/notation-legend/grace-default.png differ diff --git a/test-data/visual-tests/notation-legend/grace-songbook.png b/test-data/visual-tests/notation-legend/grace-songbook.png index 54a8becee..5834d6b21 100644 Binary files a/test-data/visual-tests/notation-legend/grace-songbook.png and b/test-data/visual-tests/notation-legend/grace-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/hammer-default.png b/test-data/visual-tests/notation-legend/hammer-default.png index 5f624b21e..8802163fd 100644 Binary files a/test-data/visual-tests/notation-legend/hammer-default.png and b/test-data/visual-tests/notation-legend/hammer-default.png differ diff --git a/test-data/visual-tests/notation-legend/hammer-songbook.png b/test-data/visual-tests/notation-legend/hammer-songbook.png index 5f624b21e..8802163fd 100644 Binary files a/test-data/visual-tests/notation-legend/hammer-songbook.png and b/test-data/visual-tests/notation-legend/hammer-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/harmonics-default.png b/test-data/visual-tests/notation-legend/harmonics-default.png index 4e1ddf07b..2c09e7820 100644 Binary files a/test-data/visual-tests/notation-legend/harmonics-default.png and b/test-data/visual-tests/notation-legend/harmonics-default.png differ diff --git a/test-data/visual-tests/notation-legend/harmonics-songbook.png b/test-data/visual-tests/notation-legend/harmonics-songbook.png index 4e1ddf07b..2c09e7820 100644 Binary files a/test-data/visual-tests/notation-legend/harmonics-songbook.png and b/test-data/visual-tests/notation-legend/harmonics-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/let-ring-default.png b/test-data/visual-tests/notation-legend/let-ring-default.png index 255112f8f..062ee8940 100644 Binary files a/test-data/visual-tests/notation-legend/let-ring-default.png and b/test-data/visual-tests/notation-legend/let-ring-default.png differ diff --git a/test-data/visual-tests/notation-legend/let-ring-songbook.png b/test-data/visual-tests/notation-legend/let-ring-songbook.png index 89216510f..2e040020c 100644 Binary files a/test-data/visual-tests/notation-legend/let-ring-songbook.png and b/test-data/visual-tests/notation-legend/let-ring-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/mixed-default.png b/test-data/visual-tests/notation-legend/mixed-default.png index 50fde24ce..568f8bb83 100644 Binary files a/test-data/visual-tests/notation-legend/mixed-default.png and b/test-data/visual-tests/notation-legend/mixed-default.png differ diff --git a/test-data/visual-tests/notation-legend/mixed-songbook.png b/test-data/visual-tests/notation-legend/mixed-songbook.png index 3647a02ab..73402ce11 100644 Binary files a/test-data/visual-tests/notation-legend/mixed-songbook.png and b/test-data/visual-tests/notation-legend/mixed-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/multi-grace-default.png b/test-data/visual-tests/notation-legend/multi-grace-default.png index cc1224712..696705250 100644 Binary files a/test-data/visual-tests/notation-legend/multi-grace-default.png and b/test-data/visual-tests/notation-legend/multi-grace-default.png differ diff --git a/test-data/visual-tests/notation-legend/multi-grace-songbook.png b/test-data/visual-tests/notation-legend/multi-grace-songbook.png index 66c4189ad..1e5199280 100644 Binary files a/test-data/visual-tests/notation-legend/multi-grace-songbook.png and b/test-data/visual-tests/notation-legend/multi-grace-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/multi-voice-default.png b/test-data/visual-tests/notation-legend/multi-voice-default.png index 440c17a51..e0efd6e31 100644 Binary files a/test-data/visual-tests/notation-legend/multi-voice-default.png and b/test-data/visual-tests/notation-legend/multi-voice-default.png differ diff --git a/test-data/visual-tests/notation-legend/multi-voice-songbook.png b/test-data/visual-tests/notation-legend/multi-voice-songbook.png index 440c17a51..e0efd6e31 100644 Binary files a/test-data/visual-tests/notation-legend/multi-voice-songbook.png and b/test-data/visual-tests/notation-legend/multi-voice-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/ottavia-default.png b/test-data/visual-tests/notation-legend/ottavia-default.png index 934f2a5a1..2dd4c501a 100644 Binary files a/test-data/visual-tests/notation-legend/ottavia-default.png and b/test-data/visual-tests/notation-legend/ottavia-default.png differ diff --git a/test-data/visual-tests/notation-legend/ottavia-songbook.png b/test-data/visual-tests/notation-legend/ottavia-songbook.png index 934f2a5a1..2dd4c501a 100644 Binary files a/test-data/visual-tests/notation-legend/ottavia-songbook.png and b/test-data/visual-tests/notation-legend/ottavia-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/pick-stroke-default.png b/test-data/visual-tests/notation-legend/pick-stroke-default.png index e3ff9c247..376217667 100644 Binary files a/test-data/visual-tests/notation-legend/pick-stroke-default.png and b/test-data/visual-tests/notation-legend/pick-stroke-default.png differ diff --git a/test-data/visual-tests/notation-legend/pick-stroke-songbook.png b/test-data/visual-tests/notation-legend/pick-stroke-songbook.png index e3ff9c247..376217667 100644 Binary files a/test-data/visual-tests/notation-legend/pick-stroke-songbook.png and b/test-data/visual-tests/notation-legend/pick-stroke-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/resize-sequence-1300.png b/test-data/visual-tests/notation-legend/resize-sequence-1300.png index b9ebaadb6..e9b8818f8 100644 Binary files a/test-data/visual-tests/notation-legend/resize-sequence-1300.png and b/test-data/visual-tests/notation-legend/resize-sequence-1300.png differ diff --git a/test-data/visual-tests/notation-legend/resize-sequence-1500.png b/test-data/visual-tests/notation-legend/resize-sequence-1500.png index dac14f871..db972ebad 100644 Binary files a/test-data/visual-tests/notation-legend/resize-sequence-1500.png and b/test-data/visual-tests/notation-legend/resize-sequence-1500.png differ diff --git a/test-data/visual-tests/notation-legend/resize-sequence-500.png b/test-data/visual-tests/notation-legend/resize-sequence-500.png index 1f278ca2b..9cda4d08e 100644 Binary files a/test-data/visual-tests/notation-legend/resize-sequence-500.png and b/test-data/visual-tests/notation-legend/resize-sequence-500.png differ diff --git a/test-data/visual-tests/notation-legend/resize-sequence-800.png b/test-data/visual-tests/notation-legend/resize-sequence-800.png index 3c3b76bfc..f05a88df3 100644 Binary files a/test-data/visual-tests/notation-legend/resize-sequence-800.png and b/test-data/visual-tests/notation-legend/resize-sequence-800.png differ diff --git a/test-data/visual-tests/notation-legend/slash-default.png b/test-data/visual-tests/notation-legend/slash-default.png index c73baf699..363e3648b 100644 Binary files a/test-data/visual-tests/notation-legend/slash-default.png and b/test-data/visual-tests/notation-legend/slash-default.png differ diff --git a/test-data/visual-tests/notation-legend/slash-songbook.png b/test-data/visual-tests/notation-legend/slash-songbook.png index c73baf699..363e3648b 100644 Binary files a/test-data/visual-tests/notation-legend/slash-songbook.png and b/test-data/visual-tests/notation-legend/slash-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/slides-default.png b/test-data/visual-tests/notation-legend/slides-default.png index 4ead6fcec..e6e29134a 100644 Binary files a/test-data/visual-tests/notation-legend/slides-default.png and b/test-data/visual-tests/notation-legend/slides-default.png differ diff --git a/test-data/visual-tests/notation-legend/slides-songbook.png b/test-data/visual-tests/notation-legend/slides-songbook.png index 4ead6fcec..e6e29134a 100644 Binary files a/test-data/visual-tests/notation-legend/slides-songbook.png and b/test-data/visual-tests/notation-legend/slides-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/staccatissimo-default.png b/test-data/visual-tests/notation-legend/staccatissimo-default.png index 2431b0764..b952f82cd 100644 Binary files a/test-data/visual-tests/notation-legend/staccatissimo-default.png and b/test-data/visual-tests/notation-legend/staccatissimo-default.png differ diff --git a/test-data/visual-tests/notation-legend/staccatissimo-songbook.png b/test-data/visual-tests/notation-legend/staccatissimo-songbook.png index 2431b0764..b952f82cd 100644 Binary files a/test-data/visual-tests/notation-legend/staccatissimo-songbook.png and b/test-data/visual-tests/notation-legend/staccatissimo-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/sweep-default.png b/test-data/visual-tests/notation-legend/sweep-default.png index 909a43bbe..e27f798dd 100644 Binary files a/test-data/visual-tests/notation-legend/sweep-default.png and b/test-data/visual-tests/notation-legend/sweep-default.png differ diff --git a/test-data/visual-tests/notation-legend/sweep-songbook.png b/test-data/visual-tests/notation-legend/sweep-songbook.png index 909a43bbe..e27f798dd 100644 Binary files a/test-data/visual-tests/notation-legend/sweep-songbook.png and b/test-data/visual-tests/notation-legend/sweep-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/tap-riff-default.png b/test-data/visual-tests/notation-legend/tap-riff-default.png index 0522bcb18..28877d461 100644 Binary files a/test-data/visual-tests/notation-legend/tap-riff-default.png and b/test-data/visual-tests/notation-legend/tap-riff-default.png differ diff --git a/test-data/visual-tests/notation-legend/tap-riff-songbook.png b/test-data/visual-tests/notation-legend/tap-riff-songbook.png index 0522bcb18..28877d461 100644 Binary files a/test-data/visual-tests/notation-legend/tap-riff-songbook.png and b/test-data/visual-tests/notation-legend/tap-riff-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/tempo-change-default.png b/test-data/visual-tests/notation-legend/tempo-change-default.png index 090e5013e..be0752263 100644 Binary files a/test-data/visual-tests/notation-legend/tempo-change-default.png and b/test-data/visual-tests/notation-legend/tempo-change-default.png differ diff --git a/test-data/visual-tests/notation-legend/tempo-change-songbook.png b/test-data/visual-tests/notation-legend/tempo-change-songbook.png index 935fe0e75..42772d01d 100644 Binary files a/test-data/visual-tests/notation-legend/tempo-change-songbook.png and b/test-data/visual-tests/notation-legend/tempo-change-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/text-default.png b/test-data/visual-tests/notation-legend/text-default.png index 2f766d881..f862f802a 100644 Binary files a/test-data/visual-tests/notation-legend/text-default.png and b/test-data/visual-tests/notation-legend/text-default.png differ diff --git a/test-data/visual-tests/notation-legend/text-songbook.png b/test-data/visual-tests/notation-legend/text-songbook.png index 2f766d881..f862f802a 100644 Binary files a/test-data/visual-tests/notation-legend/text-songbook.png and b/test-data/visual-tests/notation-legend/text-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/tied-note-accidentals-default.png b/test-data/visual-tests/notation-legend/tied-note-accidentals-default.png index b1b08f502..73eb9e861 100644 Binary files a/test-data/visual-tests/notation-legend/tied-note-accidentals-default.png and b/test-data/visual-tests/notation-legend/tied-note-accidentals-default.png differ diff --git a/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png b/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png index 3999dcb25..0028d12ba 100644 Binary files a/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png and b/test-data/visual-tests/notation-legend/tied-note-accidentals-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/trill-default.png b/test-data/visual-tests/notation-legend/trill-default.png index 50d87a08c..428f36d12 100644 Binary files a/test-data/visual-tests/notation-legend/trill-default.png and b/test-data/visual-tests/notation-legend/trill-default.png differ diff --git a/test-data/visual-tests/notation-legend/trill-songbook.png b/test-data/visual-tests/notation-legend/trill-songbook.png index 50d87a08c..428f36d12 100644 Binary files a/test-data/visual-tests/notation-legend/trill-songbook.png and b/test-data/visual-tests/notation-legend/trill-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/triplet-feel-default.png b/test-data/visual-tests/notation-legend/triplet-feel-default.png index 42499e71f..deb0657ce 100644 Binary files a/test-data/visual-tests/notation-legend/triplet-feel-default.png and b/test-data/visual-tests/notation-legend/triplet-feel-default.png differ diff --git a/test-data/visual-tests/notation-legend/triplet-feel-songbook.png b/test-data/visual-tests/notation-legend/triplet-feel-songbook.png index 42499e71f..deb0657ce 100644 Binary files a/test-data/visual-tests/notation-legend/triplet-feel-songbook.png and b/test-data/visual-tests/notation-legend/triplet-feel-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/vibrato-default.png b/test-data/visual-tests/notation-legend/vibrato-default.png index 5c82cfeaf..bf7001f8f 100644 Binary files a/test-data/visual-tests/notation-legend/vibrato-default.png and b/test-data/visual-tests/notation-legend/vibrato-default.png differ diff --git a/test-data/visual-tests/notation-legend/vibrato-songbook.png b/test-data/visual-tests/notation-legend/vibrato-songbook.png index 5c82cfeaf..bf7001f8f 100644 Binary files a/test-data/visual-tests/notation-legend/vibrato-songbook.png and b/test-data/visual-tests/notation-legend/vibrato-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/wah-default.png b/test-data/visual-tests/notation-legend/wah-default.png index 98a85c3fd..2e8c504e6 100644 Binary files a/test-data/visual-tests/notation-legend/wah-default.png and b/test-data/visual-tests/notation-legend/wah-default.png differ diff --git a/test-data/visual-tests/notation-legend/wah-songbook.png b/test-data/visual-tests/notation-legend/wah-songbook.png index 98a85c3fd..2e8c504e6 100644 Binary files a/test-data/visual-tests/notation-legend/wah-songbook.png and b/test-data/visual-tests/notation-legend/wah-songbook.png differ diff --git a/test-data/visual-tests/notation-legend/whammy-default.png b/test-data/visual-tests/notation-legend/whammy-default.png index 6b27471fa..be0cfc6aa 100644 Binary files a/test-data/visual-tests/notation-legend/whammy-default.png and b/test-data/visual-tests/notation-legend/whammy-default.png differ diff --git a/test-data/visual-tests/notation-legend/whammy-songbook.png b/test-data/visual-tests/notation-legend/whammy-songbook.png index ad27f1ac5..5e236d6c9 100644 Binary files a/test-data/visual-tests/notation-legend/whammy-songbook.png and b/test-data/visual-tests/notation-legend/whammy-songbook.png differ diff --git a/test-data/visual-tests/special-notes/beaming-mode.png b/test-data/visual-tests/special-notes/beaming-mode.png index 6bec2430b..1b019c557 100644 Binary files a/test-data/visual-tests/special-notes/beaming-mode.png and b/test-data/visual-tests/special-notes/beaming-mode.png differ diff --git a/test-data/visual-tests/special-notes/dead-notes.png b/test-data/visual-tests/special-notes/dead-notes.png index 9202a72f0..3584e40c8 100644 Binary files a/test-data/visual-tests/special-notes/dead-notes.png and b/test-data/visual-tests/special-notes/dead-notes.png differ diff --git a/test-data/visual-tests/special-notes/ghost-notes.png b/test-data/visual-tests/special-notes/ghost-notes.png index 8c2d706cc..32b898dc9 100644 Binary files a/test-data/visual-tests/special-notes/ghost-notes.png and b/test-data/visual-tests/special-notes/ghost-notes.png differ diff --git a/test-data/visual-tests/special-notes/grace-notes-advanced-1300-2.png b/test-data/visual-tests/special-notes/grace-notes-advanced-1300-2.png index 5907393db..14b9d13e7 100644 Binary files a/test-data/visual-tests/special-notes/grace-notes-advanced-1300-2.png and b/test-data/visual-tests/special-notes/grace-notes-advanced-1300-2.png differ diff --git a/test-data/visual-tests/special-notes/grace-notes-advanced-1300.png b/test-data/visual-tests/special-notes/grace-notes-advanced-1300.png index 5907393db..14b9d13e7 100644 Binary files a/test-data/visual-tests/special-notes/grace-notes-advanced-1300.png and b/test-data/visual-tests/special-notes/grace-notes-advanced-1300.png differ diff --git a/test-data/visual-tests/special-notes/grace-notes-advanced-800.png b/test-data/visual-tests/special-notes/grace-notes-advanced-800.png index 7a58c2297..439f0ce9d 100644 Binary files a/test-data/visual-tests/special-notes/grace-notes-advanced-800.png and b/test-data/visual-tests/special-notes/grace-notes-advanced-800.png differ diff --git a/test-data/visual-tests/special-notes/grace-notes-advanced.png b/test-data/visual-tests/special-notes/grace-notes-advanced.png index 5907393db..14b9d13e7 100644 Binary files a/test-data/visual-tests/special-notes/grace-notes-advanced.png and b/test-data/visual-tests/special-notes/grace-notes-advanced.png differ diff --git a/test-data/visual-tests/special-notes/grace-notes-alignment.png b/test-data/visual-tests/special-notes/grace-notes-alignment.png index 6f88a0846..82e8674ec 100644 Binary files a/test-data/visual-tests/special-notes/grace-notes-alignment.png and b/test-data/visual-tests/special-notes/grace-notes-alignment.png differ diff --git a/test-data/visual-tests/special-notes/grace-notes.png b/test-data/visual-tests/special-notes/grace-notes.png index 62d69e0b7..7eeb7d013 100644 Binary files a/test-data/visual-tests/special-notes/grace-notes.png and b/test-data/visual-tests/special-notes/grace-notes.png differ diff --git a/test-data/visual-tests/special-notes/tied-notes.png b/test-data/visual-tests/special-notes/tied-notes.png index 7fd9fe6be..28a92723e 100644 Binary files a/test-data/visual-tests/special-notes/tied-notes.png and b/test-data/visual-tests/special-notes/tied-notes.png differ diff --git a/test-data/visual-tests/special-tracks/drum-tabs.png b/test-data/visual-tests/special-tracks/drum-tabs.png index 72b2d39d1..c614915f2 100644 Binary files a/test-data/visual-tests/special-tracks/drum-tabs.png and b/test-data/visual-tests/special-tracks/drum-tabs.png differ diff --git a/test-data/visual-tests/special-tracks/grand-staff.png b/test-data/visual-tests/special-tracks/grand-staff.png index e42ac9e3a..25c04807e 100644 Binary files a/test-data/visual-tests/special-tracks/grand-staff.png and b/test-data/visual-tests/special-tracks/grand-staff.png differ diff --git a/test-data/visual-tests/special-tracks/numbered.png b/test-data/visual-tests/special-tracks/numbered.png index 7190b3806..95bc31aa9 100644 Binary files a/test-data/visual-tests/special-tracks/numbered.png and b/test-data/visual-tests/special-tracks/numbered.png differ diff --git a/test-data/visual-tests/special-tracks/percussion.png b/test-data/visual-tests/special-tracks/percussion.png index f07674e2c..b93fbfff5 100644 Binary files a/test-data/visual-tests/special-tracks/percussion.png and b/test-data/visual-tests/special-tracks/percussion.png differ diff --git a/test-data/visual-tests/special-tracks/slash.png b/test-data/visual-tests/special-tracks/slash.png index cf3ca3909..03b42eb31 100644 Binary files a/test-data/visual-tests/special-tracks/slash.png and b/test-data/visual-tests/special-tracks/slash.png differ diff --git a/test-data/visual-tests/systems-layout/bars-adjusted-automatic.png b/test-data/visual-tests/systems-layout/bars-adjusted-automatic.png index b998cad06..ce415db55 100644 Binary files a/test-data/visual-tests/systems-layout/bars-adjusted-automatic.png and b/test-data/visual-tests/systems-layout/bars-adjusted-automatic.png differ diff --git a/test-data/visual-tests/systems-layout/bars-adjusted-model.png b/test-data/visual-tests/systems-layout/bars-adjusted-model.png index 6880a343d..587290551 100644 Binary files a/test-data/visual-tests/systems-layout/bars-adjusted-model.png and b/test-data/visual-tests/systems-layout/bars-adjusted-model.png differ diff --git a/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-single-track.png b/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-single-track.png index c3e0ff6af..7f21327f9 100644 Binary files a/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-single-track.png and b/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-single-track.png differ diff --git a/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-two-tracks.png b/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-two-tracks.png index d748225cd..54ef69bbf 100644 Binary files a/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-two-tracks.png and b/test-data/visual-tests/systems-layout/horizontal-fixed-sizes-two-tracks.png differ diff --git a/test-data/visual-tests/systems-layout/multi-track-single-track.png b/test-data/visual-tests/systems-layout/multi-track-single-track.png index e610bcafc..9450dc6f3 100644 Binary files a/test-data/visual-tests/systems-layout/multi-track-single-track.png and b/test-data/visual-tests/systems-layout/multi-track-single-track.png differ diff --git a/test-data/visual-tests/systems-layout/multi-track-two-tracks.png b/test-data/visual-tests/systems-layout/multi-track-two-tracks.png index 164ca4b1c..504c8dd73 100644 Binary files a/test-data/visual-tests/systems-layout/multi-track-two-tracks.png and b/test-data/visual-tests/systems-layout/multi-track-two-tracks.png differ diff --git a/test-data/visual-tests/systems-layout/resized.png b/test-data/visual-tests/systems-layout/resized.png index 63cbecb01..1e28e249b 100644 Binary files a/test-data/visual-tests/systems-layout/resized.png and b/test-data/visual-tests/systems-layout/resized.png differ diff --git a/test/PrettyFormat.ts b/test/PrettyFormat.ts new file mode 100644 index 000000000..16b69624c --- /dev/null +++ b/test/PrettyFormat.ts @@ -0,0 +1,624 @@ +// This is a Port of pretty-format to a OOP TypeScript +// for the use in cross-compiled C# and Kotlin code. +// its also stripped down to the needs of alphaTab. + +/* + * MIT License + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * Copyright Contributors to the Jest project. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// https://github.com/jestjs/jest/blob/main/packages/pretty-format + +export class PrettyFormatConfig { + public escapeString: boolean = true; + public indent: string = ' '; + public maxDepth: number = Number.POSITIVE_INFINITY; + public maxWidth: number = Number.POSITIVE_INFINITY; + public min: boolean = false; + public plugins: PrettyFormatNewPlugin[] = []; + public printBasicPrototype: boolean = true; + public printFunctionName: boolean = true; + public spacingInner: string = '\n'; + public spacingOuter: string = '\n'; +} + +export type PrettyFormatPrinter = ( + val: unknown, + config: PrettyFormatConfig, + indentation: string, + depth: number, + refs: unknown[] +) => string; + +interface PrettyFormatNewPlugin { + serialize( + val: unknown, + config: PrettyFormatConfig, + indentation: string, + depth: number, + refs: unknown[], + printer: PrettyFormatPrinter + ): string; + test(arg0: unknown): boolean; +} + +/** + * @partial + */ +export class PrettyFormat { + static findPlugin(plugins: PrettyFormatNewPlugin[], val: unknown) { + for (const plugin of plugins) { + if (plugin.test(val)) { + return plugin; + } + } + return null; + } + + static printer( + val: unknown, + config: PrettyFormatConfig, + indentation: string, + depth: number, + refs: unknown[] + ): string { + const plugin = PrettyFormat.findPlugin(config.plugins, val); + if (plugin !== null) { + return plugin.serialize(val, config, indentation, depth, refs, PrettyFormat.printer); + } + + const basicResult = PrettyFormat.printBasicValue(val, config.escapeString); + if (basicResult !== null) { + return basicResult; + } + + return PrettyFormat.printComplexValue(val, config, indentation, depth, refs); + } + + static printNumber(val: number): string { + return val.toString(); + } + + static printBigInt(val: bigint): string { + return `${val}n`; + } + + static printError(val: Error): string { + return `[${val.toString()}]`; + } + + /** + * The first port of call for printing an object, handles most of the + * data-types in JS. + */ + static printBasicValue(val: unknown, escapeString: boolean): string | null { + switch (typeof val) { + case 'string': + if (escapeString) { + return `"${(val as string).replaceAll(/"|\\/g, '\\$&')}"`; + } + return `"${val}"`; + case 'number': + return PrettyFormat.printNumber(val as number); + case 'bigint': + return PrettyFormat.printBigInt(val as bigint); + case 'boolean': + return `${val}`; + case 'undefined': + return 'undefined'; + case 'object': + if (val === null) { + return 'null'; + } + break; + } + + return null; + } + + static tryGetIterableType(val: unknown): string { + if (val instanceof Float32Array) { + return 'Float32Array'; + } + if (val instanceof Int16Array) { + return 'Int16Array'; + } + if (val instanceof Int32Array) { + return 'Int32Array'; + } + if (val instanceof Uint8Array) { + return 'Uint8Array'; + } + if (val instanceof Uint16Array) { + return 'Uint16Array'; + } + if (val instanceof Uint32Array) { + return 'Uint32Array'; + } + if (Array.isArray(val)) { + return 'Array'; + } + if (val instanceof Set) { + return 'Set'; + } + + return ''; + } + + /** + * Handles more complex objects ( such as objects with circular references. + * maps and sets etc ) + */ + static printComplexValue( + val: unknown, + config: PrettyFormatConfig, + indentation: string, + depth: number, + refs: unknown[] + ): string { + if (refs.includes(val)) { + return '[Circular]'; + } + refs = [...refs]; + refs.push(val); + + const hitMaxDepth = ++depth > config.maxDepth; + const min = config.min; + + const arrayTypeName = PrettyFormat.tryGetIterableType(val); + if (arrayTypeName) { + return hitMaxDepth + ? `[${arrayTypeName}]` + : `${ + min ? '' : `${arrayTypeName} ` + }[${PrettyFormat.printIterableValues(val as Iterable, config, indentation, depth, refs, PrettyFormat.printer)}]`; + } + + if (val instanceof Map) { + return hitMaxDepth + ? '[Map]' + : `Map {${PrettyFormat.printIteratorEntries(PrettyFormat.mapAsUnknownIterable(val), config, indentation, depth, refs, PrettyFormat.printer, ' => ')}}`; + } + + return ''; + } + + /** + * @target web + * @partial + */ + private static mapAsUnknownIterable(map: unknown): Iterable<[unknown, unknown]> { + return (map as Map).entries(); + } + + /** + * Return entries (for example, of a map) + * with spacing, indentation, and comma + * without surrounding punctuation (for example, braces) + */ + static printIteratorEntries( + iterator: Iterable<[unknown, unknown]>, + config: PrettyFormatConfig, + indentation: string, + depth: number, + refs: unknown[], + printer: PrettyFormatPrinter, + // Too bad, so sad that separator for ECMAScript Map has been ' => ' + // What a distracting diff if you change a data structure to/from + // ECMAScript Object or Immutable.Map/OrderedMap which use the default. + separator = ': ' + ): string { + let result = ''; + let width = 0; + + const indentationNext = indentation + config.indent; + for (const current of iterator) { + if (result.length === 0) { + result += config.spacingOuter; + } else { + result += `,${config.spacingInner}`; + } + + result += indentationNext; + + if (width++ === config.maxWidth) { + result += '…'; + break; + } + + const name = printer(current[0], config, indentationNext, depth, refs); + const value = printer(current[1], config, indentationNext, depth, refs); + + result += name + separator + value; + } + + if (result.length > 0) { + result += ','; + result += config.spacingOuter + indentation; + } + + return result; + } + + /** + * Return values (for example, of a set) + * with spacing, indentation, and comma + * without surrounding punctuation (braces or brackets) + */ + static printIterableValues( + iterator: Iterable, + config: PrettyFormatConfig, + indentation: string, + depth: number, + refs: unknown[], + printer: PrettyFormatPrinter + ): string { + let result = ''; + let width = 0; + + const indentationNext = indentation + config.indent; + for (const current of iterator) { + if (result.length === 0) { + result += config.spacingOuter; + result += indentationNext; + } else { + result += `,${config.spacingInner}`; + result += indentationNext; + } + + if (width++ === config.maxWidth) { + result += '…'; + break; + } + + result += printer(current, config, indentationNext, depth, refs); + } + + if (result.length > 0) { + result += ','; + result += config.spacingOuter + indentation; + } + + return result; + } + + /** + * Returns a presentation string of your `val` object + * @param val any potential JavaScript object + * @param options Custom settings + */ + public static format(val: unknown, config?: PrettyFormatConfig): string { + return PrettyFormat.printer(val, config ?? new PrettyFormatConfig(), '', 0, []); + } +} + +import { ScoreSerializer } from '@src/generated/model/ScoreSerializer'; +import { MasterBarSerializer } from '@src/generated/model/MasterBarSerializer'; +import { TrackSerializer } from '@src/generated/model/TrackSerializer'; +import { StaffSerializer } from '@src/generated/model/StaffSerializer'; +import { BarSerializer } from '@src/generated/model/BarSerializer'; +import { VoiceSerializer } from '@src/generated/model/VoiceSerializer'; +import { BeatSerializer } from '@src/generated/model/BeatSerializer'; +import { NoteSerializer } from '@src/generated/model/NoteSerializer'; +import { TestPlatform } from './TestPlatform'; +import { Bar } from '@src/model/Bar'; +import { Beat } from '@src/model/Beat'; +import { JsonConverter } from '@src/model/JsonConverter'; +import { MasterBar } from '@src/model/MasterBar'; +import { Note } from '@src/model/Note'; +import { Score } from '@src/model/Score'; +import { Staff } from '@src/model/Staff'; +import { Track } from '@src/model/Track'; +import { Voice } from '@src/model/Voice'; + +/** + * A serializer plugin for pretty-format for creating simple Score model snapshots + * @partial + */ +export class ScoreSerializerPlugin implements PrettyFormatNewPlugin { + public static readonly instance = new ScoreSerializerPlugin(); + + private defaultScoreJson: Map; + private defaultMasterBarJson: Map; + private defaultTrackJson: Map; + private defaultStaffJson: Map; + private defaultBarJson: Map; + private defaultVoiceJson: Map; + private defaultBeatJson: Map; + private defaultNoteJson: Map; + + private constructor() { + // we create empty basic objects and reset some props we always want in the snapshot + const defaultScore = new Score(); + const defaultMasterBar = new MasterBar(); + defaultMasterBar.index = -1; + + const defaultTrack = new Track(); + defaultTrack.index = -1; + + const defaultStaff = new Staff(); + defaultStaff.index = -1; + + const defaultBar = new Bar(); + defaultBar.id = -1; + defaultBar.index = -1; + + const defaultVoice = new Voice(); + defaultVoice.id = -1; + defaultVoice.index = -1; + + const defaultBeat = new Beat(); + defaultBeat.id = -1; + defaultBeat.index = -1; + + const defaultNote = new Note(); + defaultNote.id = -1; + defaultNote.index = -1; + + this.defaultScoreJson = ScoreSerializer.toJson(defaultScore)!; + this.defaultMasterBarJson = MasterBarSerializer.toJson(defaultMasterBar)!; + this.defaultTrackJson = TrackSerializer.toJson(defaultTrack)!; + this.defaultStaffJson = StaffSerializer.toJson(defaultStaff)!; + this.defaultBarJson = BarSerializer.toJson(defaultBar)!; + this.defaultVoiceJson = VoiceSerializer.toJson(defaultVoice)!; + this.defaultBeatJson = BeatSerializer.toJson(defaultBeat)!; + this.defaultNoteJson = NoteSerializer.toJson(defaultNote)!; + } + + public serialize( + val: unknown, + config: PrettyFormatConfig, + indentation: string, + depth: number, + refs: unknown[], + printer: PrettyFormatPrinter + ): string { + const json = JsonConverter.scoreToJsObject(val as Score); + this.filterOutDefaultValues(json as Map); + return printer(json, config, indentation, depth, refs); + } + + public test(arg0: unknown): boolean { + return arg0 instanceof Score; + } + + private filterOutDefaultValues(scoreJson: Map) { + const masterBars = scoreJson.get('masterbars') as Map[]; + for (const masterBar of masterBars) { + ScoreSerializerPlugin.sanitizeJson(masterBar, this.defaultMasterBarJson, 'MasterBar'); + } + + const tracks = scoreJson.get('tracks') as Map[]; + for (const track of tracks) { + const staves = track.get('staves') as Map[]; + for (const staff of staves) { + const bars = staff.get('bars') as Map[]; + for (const bar of bars) { + const voices = bar.get('voices') as Map[]; + for (const voice of voices) { + const beats = voice.get('beats') as Map[]; + for (const beat of beats) { + const notes = beat.get('notes') as Map[]; + for (const note of notes) { + ScoreSerializerPlugin.sanitizeJson(note, this.defaultNoteJson, 'Note'); + } + ScoreSerializerPlugin.sanitizeJson(beat, this.defaultBeatJson, 'Beat'); + } + ScoreSerializerPlugin.sanitizeJson(voice, this.defaultVoiceJson, 'Voice'); + } + ScoreSerializerPlugin.sanitizeJson(bar, this.defaultBarJson, 'Bar'); + } + ScoreSerializerPlugin.sanitizeJson(staff, this.defaultStaffJson, 'Staff'); + } + ScoreSerializerPlugin.sanitizeJson(track, this.defaultTrackJson, 'Track'); + } + + // walk hierarchy and filter out stuff + ScoreSerializerPlugin.sanitizeJson(scoreJson, this.defaultScoreJson, 'Score'); + } + + private static sanitizeJson( + modelJson: Map, + defaultValueJson: Map, + kind?: string + ) { + const oldMap = new Map(modelJson); + modelJson.clear(); + + // add a __kind property to identify the objects easier in the snap files + if (kind) { + modelJson.set('__kind', kind); + } + + for (const [k, v] of oldMap) { + if (defaultValueJson.has(k)) { + const dv = defaultValueJson.get(k); + + let isEqual = false; + if (typeof v === typeof dv) { + switch (typeof dv) { + case 'string': + isEqual = (v as string) === (dv as string); + break; + case 'number': + isEqual = (v as number) === (dv as number); + break; + case 'bigint': + isEqual = (v as bigint) === (dv as bigint); + break; + case 'boolean': + isEqual = (v as boolean) === (dv as boolean); + break; + case 'undefined': + isEqual = (v as undefined) === (dv as undefined); + break; + case 'object': + if (dv === null && v === null) { + isEqual = true; + } else if (dv instanceof Float32Array && v instanceof Float32Array) { + isEqual = dv.length === 0 && v.length === 0; + } else if (dv instanceof Int16Array && v instanceof Int16Array) { + isEqual = dv.length === 0 && v.length === 0; + } else if (dv instanceof Int32Array && v instanceof Int32Array) { + isEqual = dv.length === 0 && v.length === 0; + } else if (dv instanceof Uint8Array && v instanceof Uint8Array) { + isEqual = dv.length === 0 && v.length === 0; + } else if (dv instanceof Uint16Array && v instanceof Uint16Array) { + isEqual = dv.length === 0 && v.length === 0; + } else if (dv instanceof Uint32Array && v instanceof Uint32Array) { + isEqual = dv.length === 0 && v.length === 0; + } else if (Array.isArray(dv) && Array.isArray(v)) { + isEqual = dv.length === 0 && v.length === 0; + } else if (dv instanceof Map && v instanceof Map) { + ScoreSerializerPlugin.sanitizeJson( + v as Map, + dv as Map + ); + + if ((v as Map).size === 0) { + isEqual = true; + } + } else { + isEqual = ScoreSerializerPlugin.isPlatformTypeEqual(v, dv); + } + break; + } + } + + if (!isEqual) { + modelJson.set(k, v); + } + } else { + modelJson.set(k, v); + } + } + } + + /** + * @target web + * @partial + */ + private static isPlatformTypeEqual(v: unknown, dv: unknown): boolean { + // we should not have any other types in our JSONs, if we extend it, this will catch it + throw new Error(`Unexpected value in serialized json${String(v)}`); + } +} + +// Some helpers for snapshots on C# and Kotlin compilation +export class SnapshotFileRepository { + private static _cache: Map = new Map(); + + public static loadSnapshortFile(path: string): SnapshotFile { + let file = SnapshotFileRepository._cache.get(path); + if (!file) { + file = new SnapshotFile(); + file.loadFrom(path); + SnapshotFileRepository._cache.set(path, file); + } + + return file; + } +} + +export class SnapshotFile { + private static createConfig() { + const c = new PrettyFormatConfig(); + c.plugins.push(ScoreSerializerPlugin.instance); + return c; + } + private static readonly matchOptions: PrettyFormatConfig = SnapshotFile.createConfig(); + + public snapshots: Map = new Map(); + + /** + * Matches the given snapshot with the contained one. + * @param name The name of the snapshot + * @param value The raw value for which to create the snapshot + * @returns An error message if there was a match error, if they match null + */ + public match(name: string, value: unknown): string | null { + const expected = this.snapshots.get(name)?.split('\n'); + if (expected === undefined) { + return `No snapshot '${name}' found`; + } + + const actual = PrettyFormat.format(value, SnapshotFile.matchOptions).split('\n'); + + const lines = Math.min(expected.length, actual.length); + const errors: string[] = []; + + if (expected.length !== actual.length) { + errors.push(`Expected ${expected.length} lines, but only got ${actual.length}`); + } + + for (let i = 0; i < lines; i++) { + if (actual[i].trimEnd() !== expected[i].trimEnd()) { + errors.push(`Error on line ${i + 1}: `); + errors.push(`+ ${actual[i]}`); + errors.push(`- ${expected[i]}`); + } + } + + if (errors.length > 0) { + return errors.join('\n'); + } + return null; + } + + loadFrom(path: string) { + const content = TestPlatform.loadFileAsStringSync(path); + + // plain text parsing for C# and kotlin as we cannot "require/import" them + const lines = content.split('\n'); + + let i = 0; + while (i < lines.length) { + if (lines[i].startsWith('exports[`')) { + // start of snapshot + const endOfName = lines[i].indexOf('`]', 9); + if (endOfName === -1) { + throw new Error(`Failed to parse snapshot file, missing \`] on line ${i + 1}`); + } + + const name = lines[i].substring(9, endOfName); + i++; + + let value = ''; + while (i < lines.length) { + const line = lines[i++]; + + if (line.startsWith('`;')) { + // end of snapshot + break; + } + + value += `${line.trimEnd()}\n`; + } + + this.snapshots.set(name, value.trimEnd()); + } else { + i++; + } + } + } +} diff --git a/test/TestPlatform.ts b/test/TestPlatform.ts index 23721fa7d..d680883a9 100644 --- a/test/TestPlatform.ts +++ b/test/TestPlatform.ts @@ -1,6 +1,6 @@ import { IOHelper } from '@src/io/IOHelper'; -import * as fs from 'fs'; -import * as path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; /** * @partial @@ -12,7 +12,7 @@ export class TestPlatform { */ public static async saveFile(name: string, data: Uint8Array): Promise { const directory = path.dirname(name); - await fs.promises.mkdir(directory, { recursive: true }) + await fs.promises.mkdir(directory, { recursive: true }); await fs.promises.writeFile(name, data); } @@ -21,7 +21,7 @@ export class TestPlatform { * @partial */ public static async deleteFile(name: string): Promise { - await fs.promises.rm(name, { force: true }) + await fs.promises.rm(name, { force: true }); } /** @@ -32,6 +32,14 @@ export class TestPlatform { return fs.promises.readFile(path); } + /** + * @target web + * @partial + */ + public static loadFileSync(path: string): Uint8Array { + return fs.readFileSync(path); + } + /** * @target web * @partial @@ -45,15 +53,19 @@ export class TestPlatform { return IOHelper.toString(data, 'UTF-8'); } + public static loadFileAsStringSync(path: string): string { + const data = TestPlatform.loadFileSync(path); + return IOHelper.toString(data, 'UTF-8'); + } + public static changeExtension(file: string, extension: string): string { - let lastDot: number = file.lastIndexOf('.'); + const lastDot: number = file.lastIndexOf('.'); if (lastDot === -1) { return file + extension; - } else { - return file.substr(0, lastDot) + extension; } + return file.substr(0, lastDot) + extension; } - + /** * @target web * @partial @@ -61,4 +73,12 @@ export class TestPlatform { public static joinPath(...parts: string[]): string { return path.join(...parts); } + + /** + * @target web + * @partial + */ + public static enumValues(enumType: any): T[] { + return Object.values(enumType).filter(k => typeof k === 'number') as T[]; + } } diff --git a/test/audio/AlphaSynth.test.ts b/test/audio/AlphaSynth.test.ts index 6a9291166..9484e75df 100644 --- a/test/audio/AlphaSynth.test.ts +++ b/test/audio/AlphaSynth.test.ts @@ -3,7 +3,7 @@ import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; import { MidiFile } from '@src/midi/MidiFile'; import { AlphaSynth } from '@src/synth/AlphaSynth'; import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { TestOutput } from '@test/audio/TestOutput'; import { TestPlatform } from '@test/TestPlatform'; @@ -15,20 +15,20 @@ import { ByteBuffer } from '@src/io/ByteBuffer'; describe('AlphaSynthTests', () => { it('pcm-generation', async () => { const data = await TestPlatform.loadFile('test-data/audio/default.sf2'); - let tex: string = + const tex: string = '\\tempo 102 \\tuning E4 B3 G3 D3 A2 E2 \\instrument 25 . r.8 (0.4 0.3 ).8 ' + '(-.3 -.4 ).2 {d } | (0.4 0.3 ).8 r.8 (3.3 3.4 ).8 r.8 (5.4 5.3 ).4 r.8 (0.4 0.3 ).8 |' + ' r.8 (3.4 3.3 ).8 r.8 (6.3 6.4 ).8 (5.4 5.3 ).4 {d }r.8 |' + ' (0.4 0.3).8 r.8(3.4 3.3).8 r.8(5.4 5.3).4 r.8(3.4 3.3).8 | ' + 'r.8(0.4 0.3).8(-.3 - .4).2 { d } | '; - let importer: AlphaTexImporter = new AlphaTexImporter(); + const importer: AlphaTexImporter = new AlphaTexImporter(); importer.initFromString(tex, new Settings()); - let score: Score = importer.readScore(); - let midi: MidiFile = new MidiFile(); - let gen: MidiFileGenerator = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); + const score: Score = importer.readScore(); + const midi: MidiFile = new MidiFile(); + const gen: MidiFileGenerator = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); gen.generate(); - let testOutput: TestOutput = new TestOutput(); - let synth: AlphaSynth = new AlphaSynth(testOutput, 500); + const testOutput: TestOutput = new TestOutput(); + const synth: AlphaSynth = new AlphaSynth(testOutput, 500); synth.loadSoundFont(data, false); synth.loadMidiFile(midi); synth.play(); @@ -53,14 +53,14 @@ describe('AlphaSynthTests', () => { \\track "T02" \\instrument 30 4.4.4*4`; - let importer: AlphaTexImporter = new AlphaTexImporter(); + const importer: AlphaTexImporter = new AlphaTexImporter(); importer.initFromString(tex, new Settings()); - let score: Score = importer.readScore(); - let midi: MidiFile = new MidiFile(); - let gen: MidiFileGenerator = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); + const score: Score = importer.readScore(); + const midi: MidiFile = new MidiFile(); + const gen: MidiFileGenerator = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); gen.generate(); - let testOutput: TestOutput = new TestOutput(); - let synth: AlphaSynth = new AlphaSynth(testOutput, 500); + const testOutput: TestOutput = new TestOutput(); + const synth: AlphaSynth = new AlphaSynth(testOutput, 500); synth.loadSoundFont(data, false); synth.loadMidiFile(midi); @@ -85,18 +85,17 @@ describe('AlphaSynthTests', () => { \\track "T02" \\instrument 30 4.4.4*4`; - let importer: AlphaTexImporter = new AlphaTexImporter(); + const importer: AlphaTexImporter = new AlphaTexImporter(); importer.initFromString(tex, new Settings()); - let score: Score = importer.readScore(); - let midi: MidiFile = new MidiFile(); - let gen: MidiFileGenerator = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); + const score: Score = importer.readScore(); + const midi: MidiFile = new MidiFile(); + const gen: MidiFileGenerator = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); gen.generate(); - let testOutput: TestOutput = new TestOutput(); - let synth: AlphaSynth = new AlphaSynth(testOutput, 500); + const testOutput: TestOutput = new TestOutput(); + const synth: AlphaSynth = new AlphaSynth(testOutput, 500); synth.loadSoundFont(data, false); synth.loadMidiFile(midi); - expect(synth.isReadyForPlayback).to.be.true; expect(synth.hasSamplesForProgram(24)).to.be.true; expect(synth.hasSamplesForProgram(30)).to.be.true; @@ -105,7 +104,7 @@ describe('AlphaSynthTests', () => { expect(synth.hasSamplesForPercussion(SynthConstants.MetronomeKey)).to.be.true; }); - async function testVorbisFile(name:string) { + async function testVorbisFile(name: string) { const data = await TestPlatform.loadFile(`test-data/audio/${name}.ogg`); const vorbis = new VorbisFile(ByteBuffer.fromBuffer(data)); diff --git a/test/audio/FlatMidiEventGenerator.ts b/test/audio/FlatMidiEventGenerator.ts index 104b79bbf..ef501386f 100644 --- a/test/audio/FlatMidiEventGenerator.ts +++ b/test/audio/FlatMidiEventGenerator.ts @@ -1,5 +1,5 @@ -import { ControllerType } from '@src/midi/ControllerType'; -import { IMidiFileHandler } from '@src/midi/IMidiFileHandler'; +import type { ControllerType } from '@src/midi/ControllerType'; +import type { IMidiFileHandler } from '@src/midi/IMidiFileHandler'; export class FlatMidiEventGenerator implements IMidiFileHandler { public midiEvents: FlatMidiEvent[]; @@ -9,54 +9,53 @@ export class FlatMidiEventGenerator implements IMidiFileHandler { } public addTimeSignature(tick: number, timeSignatureNumerator: number, timeSignatureDenominator: number): void { - let e = new FlatTimeSignatureEvent(tick, timeSignatureNumerator, timeSignatureDenominator); + const e = new FlatTimeSignatureEvent(tick, timeSignatureNumerator, timeSignatureDenominator); this.midiEvents.push(e); } public addRest(track: number, tick: number, channel: number): void { - let e = new FlatRestEvent(tick, track, channel); + const e = new FlatRestEvent(tick, track, channel); this.midiEvents.push(e); } - public addNote( - track: number, - start: number, - length: number, - key: number, - velocity: number, - channel: number - ): void { - let e = new FlatNoteEvent(start, track, channel, length, key, velocity); + public addNote(track: number, start: number, length: number, key: number, velocity: number, channel: number): void { + const e = new FlatNoteEvent(start, track, channel, length, key, velocity); this.midiEvents.push(e); } - public addControlChange(track: number, tick: number, channel: number, controller: ControllerType, value: number): void { - let e = new FlatControlChangeEvent(tick, track, channel, controller, value); + public addControlChange( + track: number, + tick: number, + channel: number, + controller: ControllerType, + value: number + ): void { + const e = new FlatControlChangeEvent(tick, track, channel, controller, value); this.midiEvents.push(e); } public addProgramChange(track: number, tick: number, channel: number, program: number): void { - let e = new FlatProgramChangeEvent(tick, track, channel, program); + const e = new FlatProgramChangeEvent(tick, track, channel, program); this.midiEvents.push(e); } public addTempo(tick: number, tempo: number): void { - let e = new FlatTempoEvent(tick, tempo); + const e = new FlatTempoEvent(tick, tempo); this.midiEvents.push(e); } public addBend(track: number, tick: number, channel: number, value: number): void { - let e = new FlatBendEvent(tick, track, channel, value); + const e = new FlatBendEvent(tick, track, channel, value); this.midiEvents.push(e); } public addNoteBend(track: number, tick: number, channel: number, key: number, value: number): void { - let e = new FlatNoteBendEvent(tick, track, channel, key, value); + const e = new FlatNoteBendEvent(tick, track, channel, key, value); this.midiEvents.push(e); } public finishTrack(track: number, tick: number): void { - let e = new FlatTrackEndEvent(tick, track); + const e = new FlatTrackEndEvent(tick, track); this.midiEvents.push(e); } } @@ -170,12 +169,8 @@ export class FlatTrackMidiEvent extends FlatMidiEvent { } export class FlatTrackEndEvent extends FlatTrackMidiEvent { - public constructor(tick: number, track: number) { - super(tick, track); - } - public override toString(): string { - return 'End of Track ' + super.toString(); + return `End of Track ${super.toString()}`; } } @@ -232,10 +227,6 @@ export class FlatControlChangeEvent extends FlatChannelMidiEvent { } export class FlatRestEvent extends FlatChannelMidiEvent { - public constructor(tick: number, track: number, channel: number) { - super(tick, track, channel); - } - public override toString(): string { return `Rest: ${super.toString()}`; } @@ -282,14 +273,7 @@ export class FlatNoteEvent extends FlatChannelMidiEvent { public key: number = 0; public velocity: number; - public constructor( - tick: number, - track: number, - channel: number, - length: number, - key: number, - velocity: number - ) { + public constructor(tick: number, track: number, channel: number, length: number, key: number, velocity: number) { super(tick, track, channel); this.length = length; this.key = key; diff --git a/test/audio/MidiFileGenerator.test.ts b/test/audio/MidiFileGenerator.test.ts index 4cf6722a9..832dd6a95 100644 --- a/test/audio/MidiFileGenerator.test.ts +++ b/test/audio/MidiFileGenerator.test.ts @@ -1,5 +1,5 @@ import { ControllerType } from '@src/midi/ControllerType'; -import { MidiEvent, MidiEventType, NoteOnEvent, TimeSignatureEvent } from '@src/midi/MidiEvent'; +import { type MidiEvent, MidiEventType, NoteOnEvent, type TimeSignatureEvent } from '@src/midi/MidiEvent'; import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; import { MidiFile } from '@src/midi/MidiFile'; import { MidiUtils } from '@src/midi/MidiUtils'; @@ -7,19 +7,19 @@ import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; import { Gp3To5Importer } from '@src/importer/Gp3To5Importer'; import { Gp7To8Importer } from '@src/importer/Gp7To8Importer'; import { ByteBuffer } from '@src/io/ByteBuffer'; -import { Beat } from '@src/model/Beat'; +import type { Beat } from '@src/model/Beat'; import { DynamicValue } from '@src/model/DynamicValue'; import { GraceType } from '@src/model/GraceType'; -import { Note } from '@src/model/Note'; -import { PlaybackInformation } from '@src/model/PlaybackInformation'; -import { Score } from '@src/model/Score'; +import type { Note } from '@src/model/Note'; +import type { PlaybackInformation } from '@src/model/PlaybackInformation'; +import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { FlatNoteBendEvent, FlatControlChangeEvent, FlatMidiEventGenerator, - FlatMidiEvent, + type FlatMidiEvent, FlatNoteEvent, FlatProgramChangeEvent, FlatTempoEvent, @@ -29,14 +29,16 @@ import { } from '@test/audio/FlatMidiEventGenerator'; import { TestPlatform } from '@test/TestPlatform'; import { AlphaSynthMidiFileHandler } from '@src/midi/AlphaSynthMidiFileHandler'; -import { AccentuationType, Duration, VibratoType } from '@src/model'; import { expect } from 'chai'; -import { ScoreLoader } from '@src/importer'; -import { MidiTickLookup } from '@src/midi'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; +import type { MidiTickLookup } from '@src/midi/MidiTickLookup'; +import { AccentuationType } from '@src/model/AccentuationType'; +import { Duration } from '@src/model/Duration'; +import { VibratoType } from '@src/model/VibratoType'; describe('MidiFileGeneratorTest', () => { const parseTex: (tex: string) => Score = (tex: string): Score => { - let importer: AlphaTexImporter = new AlphaTexImporter(); + const importer: AlphaTexImporter = new AlphaTexImporter(); importer.initFromString(tex, new Settings()); return importer.readScore(); }; @@ -59,15 +61,15 @@ describe('MidiFileGeneratorTest', () => { it('full-song', async () => { const buffer = await TestPlatform.loadFile('test-data/audio/full-song.gp5'); - let readerBase: Gp3To5Importer = new Gp3To5Importer(); + const readerBase: Gp3To5Importer = new Gp3To5Importer(); readerBase.init(ByteBuffer.fromBuffer(buffer), new Settings()); - let score: Score = readerBase.readScore(); - let generator: MidiFileGenerator = new MidiFileGenerator(score, null, new FlatMidiEventGenerator()); + const score: Score = readerBase.readScore(); + const generator: MidiFileGenerator = new MidiFileGenerator(score, null, new FlatMidiEventGenerator()); generator.generate(); }); it('midi-order', () => { - let midiFile: MidiFile = new MidiFile(); + const midiFile: MidiFile = new MidiFile(); midiFile.addEvent(new NoteOnEvent(0, 0, 0, 0, 0)); midiFile.addEvent(new NoteOnEvent(0, 0, 0, 1, 0)); midiFile.addEvent(new NoteOnEvent(0, 100, 0, 2, 0)); @@ -81,20 +83,20 @@ describe('MidiFileGeneratorTest', () => { }); it('bend', () => { - let tex: string = ':4 15.6{b(0 4)} 15.6'; - let score: Score = parseTex(tex); + const tex: string = ':4 15.6{b(0 4)} 15.6'; + const score: Score = parseTex(tex); expect(score.tracks.length).to.equal(1); expect(score.tracks[0].staves[0].bars.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices.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].notes.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes.length).to.equal(1); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); - let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -140,7 +142,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, MidiUtils.toTicks(note.beat.duration), note.realValue, - MidiUtils.dynamicToVelocity(note.dynamics as number) + MidiUtils.dynamicToVelocity(note.dynamics) ), // reset bend @@ -151,7 +153,7 @@ describe('MidiFileGeneratorTest', () => { info.primaryChannel, MidiUtils.toTicks(note.beat.duration), note.realValue, - MidiUtils.dynamicToVelocity(note.dynamics as number) + MidiUtils.dynamicToVelocity(note.dynamics) ), // end of track @@ -162,17 +164,17 @@ describe('MidiFileGeneratorTest', () => { }); it('grace-beats', async () => { - let reader: Gp7To8Importer = new Gp7To8Importer(); + const reader: Gp7To8Importer = new Gp7To8Importer(); const buffer = await TestPlatform.loadFile('test-data/audio/grace-beats.gp'); - let settings: Settings = Settings.songBook; + const settings: Settings = Settings.songBook; reader.init(ByteBuffer.fromBuffer(buffer), settings); - let score: Score = reader.readScore(); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); - let generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); + const score: Score = reader.readScore(); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); generator.generate(); // on beat let tick: number = 0; - let ticks: number[] = []; + const ticks: number[] = []; expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].absolutePlaybackStart).to.equal(tick); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].playbackDuration).to.equal(3840); ticks.push(tick); @@ -208,9 +210,9 @@ describe('MidiFileGeneratorTest', () => { expect(score.tracks[0].staves[0].bars[4].voices[0].beats[1].playbackDuration).to.equal(1920); ticks.push(tick); tick += score.tracks[0].staves[0].bars[4].voices[0].beats[1].playbackDuration; - let info: PlaybackInformation = score.tracks[0].playbackInfo; - const mfVelocity = MidiUtils.dynamicToVelocity(DynamicValue.MF as number); - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const mfVelocity = MidiUtils.dynamicToVelocity(DynamicValue.MF); + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 96), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -278,20 +280,20 @@ describe('MidiFileGeneratorTest', () => { }); it('bend-multi-point', () => { - let tex: string = ':4 15.6{b(0 4 0)} 15.6'; - let score: Score = parseTex(tex); + const tex: string = ':4 15.6{b(0 4 0)} 15.6'; + const score: Score = parseTex(tex); expect(score.tracks.length).to.equal(1); expect(score.tracks[0].staves[0].bars.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices.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].notes.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes.length).to.equal(1); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); - let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -350,7 +352,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, MidiUtils.toTicks(note.beat.duration), note.realValue, - MidiUtils.dynamicToVelocity(note.dynamics as number) + MidiUtils.dynamicToVelocity(note.dynamics) ), // reset bend @@ -361,7 +363,7 @@ describe('MidiFileGeneratorTest', () => { info.primaryChannel, MidiUtils.toTicks(note.beat.duration), note.realValue, - MidiUtils.dynamicToVelocity(note.dynamics as number) + MidiUtils.dynamicToVelocity(note.dynamics) ), // end of track new FlatTrackEndEvent(3840, 0) // 3840 = end of bar ]; @@ -370,15 +372,15 @@ describe('MidiFileGeneratorTest', () => { }); it('bend-continued', () => { - let tex: string = '7.3{b (0 4)} -.3{b (4 0)}'; - let score: Score = parseTex(tex); + const tex: string = '7.3{b (0 4)} -.3{b (4 0)}'; + const score: Score = parseTex(tex); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); - let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -424,7 +426,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, MidiUtils.toTicks(note.beat.duration) * 2, note.realValue, - MidiUtils.dynamicToVelocity(note.dynamics as number) + MidiUtils.dynamicToVelocity(note.dynamics) ), // release on tied note @@ -450,15 +452,15 @@ describe('MidiFileGeneratorTest', () => { }); it('pre-bend-release-continued', () => { - let tex: string = '7.3{b (4 0)} -.3'; - let score: Score = parseTex(tex); + const tex: string = '7.3{b (4 0)} -.3'; + const score: Score = parseTex(tex); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); - let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -504,7 +506,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, MidiUtils.toTicks(note.beat.duration) * 2, note.realValue, - MidiUtils.dynamicToVelocity(note.dynamics as number) + MidiUtils.dynamicToVelocity(note.dynamics) ), new FlatTrackEndEvent(3840, 0) // 3840 = end of bar @@ -514,17 +516,17 @@ describe('MidiFileGeneratorTest', () => { }); it('pre-bend-release-continued-songbook', () => { - let tex: string = '7.3{b (4 0)} -.3'; - let score: Score = parseTex(tex); + const tex: string = '7.3{b (4 0)} -.3'; + const score: Score = parseTex(tex); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); - let settings = new Settings(); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const settings = new Settings(); settings.setSongBookModeSettings(); - let generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); + const generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -570,7 +572,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, MidiUtils.toTicks(note.beat.duration) * 2, note.realValue, - MidiUtils.dynamicToVelocity(note.dynamics as number) + MidiUtils.dynamicToVelocity(note.dynamics) ), new FlatTrackEndEvent(3840, 0) // 3840 = end of bar @@ -580,29 +582,21 @@ describe('MidiFileGeneratorTest', () => { }); it('triplet-feel', () => { - let tex: string = + const tex: string = '\\ts 2 4 \\tf t8 3.2.8*4 | \\tf t16 3.2.16*8 | \\tf d8 3.2.8*4 | \\tf d16 3.2.16*8 | \\tf s8 3.2.8*4 | \\tf s16 3.2.16*8'; - let score: Score = parseTex(tex); + const score: Score = parseTex(tex); // prettier-ignore - let expectedPlaybackStartTimes: number[] = [ - 0, 480, 960, 1440, - 0, 240, 480, 720, 960, 1200, 1440, 1680, - 0, 480, 960, 1440, - 0, 240, 480, 720, 960, 1200, 1440, 1680, - 0, 480, 960, 1440, - 0, 240, 480, 720, 960, 1200, 1440, 1680 + const expectedPlaybackStartTimes: number[] = [ + 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, 1440, 1680, 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, + 1440, 1680, 0, 480, 960, 1440, 0, 240, 480, 720, 960, 1200, 1440, 1680 ]; // prettier-ignore - let expectedPlaybackDurations: number[] = [ - 480, 480, 480, 480, - 240, 240, 240, 240, 240, 240, 240, 240, - 480, 480, 480, 480, - 240, 240, 240, 240, 240, 240, 240, 240, - 480, 480, 480, 480, - 240, 240, 240, 240, 240, 240, 240, 240 + const expectedPlaybackDurations: number[] = [ + 480, 480, 480, 480, 240, 240, 240, 240, 240, 240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 240, + 240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 240, 240, 240, 240 ]; - let actualPlaybackStartTimes: number[] = []; - let actualPlaybackDurations: number[] = []; + const actualPlaybackStartTimes: number[] = []; + const actualPlaybackDurations: number[] = []; let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; while (beat) { actualPlaybackStartTimes.push(beat.playbackStart); @@ -612,30 +606,22 @@ describe('MidiFileGeneratorTest', () => { expect(actualPlaybackStartTimes.join(',')).to.equal(expectedPlaybackStartTimes.join(',')); expect(actualPlaybackDurations.join(',')).to.equal(expectedPlaybackDurations.join(',')); // prettier-ignore - let expectedMidiStartTimes: number[] = [ - 0, 640, 960, 1600, - 1920, 2240, 2400, 2720, 2880, 3200, 3360, 3680, - 3840, 4560, 4800, 5520, - 5760, 6120, 6240, 6600, 6720, 7080, 7200, 7560, - 7680, 7920, 8640, 8880, - 9600, 9720, 10080, 10200, 10560, 10680, 11040, 11160 + const expectedMidiStartTimes: number[] = [ + 0, 640, 960, 1600, 1920, 2240, 2400, 2720, 2880, 3200, 3360, 3680, 3840, 4560, 4800, 5520, 5760, 6120, 6240, + 6600, 6720, 7080, 7200, 7560, 7680, 7920, 8640, 8880, 9600, 9720, 10080, 10200, 10560, 10680, 11040, 11160 ]; // prettier-ignore - let expectedMidiDurations: number[] = [ - 640, 320, 640, 320, - 320, 160, 320, 160, 320, 160, 320, 160, - 720, 240, 720, 240, - 360, 120, 360, 120, 360, 120, 360, 120, - 240, 720, 240, 720, - 120, 360, 120, 360, 120, 360, 120, 360 + const expectedMidiDurations: number[] = [ + 640, 320, 640, 320, 320, 160, 320, 160, 320, 160, 320, 160, 720, 240, 720, 240, 360, 120, 360, 120, 360, + 120, 360, 120, 240, 720, 240, 720, 120, 360, 120, 360, 120, 360, 120, 360 ]; - let actualMidiStartTimes: number[] = []; - let actualMidiDurations: number[] = []; - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); - let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); + const actualMidiStartTimes: number[] = []; + const actualMidiDurations: number[] = []; + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); generator.generate(); - for (let midiEvent of handler.midiEvents) { + for (const midiEvent of handler.midiEvents) { if (midiEvent instanceof FlatNoteEvent) { actualMidiStartTimes.push(midiEvent.tick); actualMidiDurations.push(midiEvent.length); @@ -646,21 +632,21 @@ describe('MidiFileGeneratorTest', () => { }); it('beat-multi-bend', () => { - let tex: string = ':4 (15.6{b(0 4)} 14.6{b(0 8)}) 15.6'; - let score: Score = parseTex(tex); + const tex: string = ':4 (15.6{b(0 4)} 14.6{b(0 8)}) 15.6'; + const score: Score = parseTex(tex); expect(score.tracks.length).to.equal(1); expect(score.tracks[0].staves[0].bars.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices.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].notes.length).to.equal(2); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes.length).to.equal(1); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); - let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let note2: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[1]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const note2: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[1]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -706,7 +692,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, MidiUtils.toTicks(note1.beat.duration), note1.realValue, - MidiUtils.dynamicToVelocity(note1.dynamics as number) + MidiUtils.dynamicToVelocity(note1.dynamics) ), // bend effect (note 2) @@ -744,7 +730,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, MidiUtils.toTicks(note2.beat.duration), note2.realValue, - MidiUtils.dynamicToVelocity(note2.dynamics as number) + MidiUtils.dynamicToVelocity(note2.dynamics) ), // reset bend @@ -755,7 +741,7 @@ describe('MidiFileGeneratorTest', () => { info.primaryChannel, MidiUtils.toTicks(note1.beat.duration), note1.realValue, - MidiUtils.dynamicToVelocity(note1.dynamics as number) + MidiUtils.dynamicToVelocity(note1.dynamics) ), // end of track @@ -766,20 +752,20 @@ describe('MidiFileGeneratorTest', () => { }); it('tied-vibrato', () => { - let tex: string = '3.3{v}.4 -.3{v}.4'; - let score: Score = parseTex(tex); + const tex: string = '3.3{v}.4 -.3{v}.4'; + const score: Score = parseTex(tex); 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].isTieDestination).to.be.true; score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].vibrato = VibratoType.None; - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); const settings = new Settings(); settings.player.vibrato.noteSlightLength = MidiUtils.QuarterTime / 2; // to reduce the number of vibrato events - let generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); + const generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); generator.vibratoResolution = settings.player.vibrato.noteSlightLength / 4; generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -818,7 +804,7 @@ describe('MidiFileGeneratorTest', () => { info.primaryChannel, 1920, note1.realValue, - MidiUtils.dynamicToVelocity(note1.dynamics as number) + MidiUtils.dynamicToVelocity(note1.dynamics) ), new FlatNoteBendEvent(960, 0, info.primaryChannel, note1.realValue, 8192), // no bend (vibrato start on main note) @@ -839,18 +825,18 @@ describe('MidiFileGeneratorTest', () => { }); it('bend-tied-no-vibrato', () => { - let tex: string = '3.3{b (0 4)}.4 -.3.4'; - let score: Score = parseTex(tex); + const tex: string = '3.3{b (0 4)}.4 -.3.4'; + const score: Score = parseTex(tex); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); const settings = new Settings(); settings.player.vibrato.noteSlightLength = MidiUtils.QuarterTime / 2; // to reduce the number of vibrato events - let generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); + const generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); generator.vibratoResolution = settings.player.vibrato.noteSlightLength / 4; generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -894,7 +880,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, 1920, note1.realValue, - MidiUtils.dynamicToVelocity(note1.dynamics as number) + MidiUtils.dynamicToVelocity(note1.dynamics) ), // end of track @@ -905,18 +891,18 @@ describe('MidiFileGeneratorTest', () => { }); it('tied-bend', () => { - let tex: string = '3.3.4 -.3.8 -.3{b (0 4)}.8'; - let score: Score = parseTex(tex); + const tex: string = '3.3.4 -.3.8 -.3{b (0 4)}.8'; + const score: Score = parseTex(tex); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); const settings = new Settings(); settings.player.vibrato.noteSlightLength = MidiUtils.QuarterTime / 2; // to reduce the number of vibrato events - let generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); + const generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); generator.vibratoResolution = settings.player.vibrato.noteSlightLength / 4; generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -946,7 +932,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, 1920, note1.realValue, - MidiUtils.dynamicToVelocity(note1.dynamics as number) + MidiUtils.dynamicToVelocity(note1.dynamics) ), // tied note (no bend) @@ -975,20 +961,20 @@ describe('MidiFileGeneratorTest', () => { }); it('tied-bend-hammer', async () => { - let score: Score = ScoreLoader.loadScoreFromBytes( + const score: Score = ScoreLoader.loadScoreFromBytes( await TestPlatform.loadFile('test-data/audio/tied-bend-hammer.gp') ); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); const settings = new Settings(); settings.player.vibrato.noteSlightLength = MidiUtils.QuarterTime / 2; // to reduce the number of vibrato events - let generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); + const generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); generator.vibratoResolution = settings.player.vibrato.noteSlightLength / 4; generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let note2: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const note2: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 96), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -1018,7 +1004,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, 2880, note1.realValue, - MidiUtils.dynamicToVelocity(note1.dynamics as number) + MidiUtils.dynamicToVelocity(note1.dynamics) ), // tied note (with bend) @@ -1056,7 +1042,7 @@ describe('MidiFileGeneratorTest', () => { info.primaryChannel, 1920, note2.realValue, - MidiUtils.dynamicToVelocity((note2.dynamics as number) - 1) // -1 on velocity because of pull-off + MidiUtils.dynamicToVelocity(note2.dynamics, -1) // -1 on velocity because of pull-off ), // end of track @@ -1067,18 +1053,18 @@ describe('MidiFileGeneratorTest', () => { }); it('bend-tied-vibrato', () => { - let tex: string = '3.3{b (0 4)}.4 -.3{v}.4'; - let score: Score = parseTex(tex); + const tex: string = '3.3{b (0 4)}.4 -.3{v}.4'; + const score: Score = parseTex(tex); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); const settings = new Settings(); settings.player.vibrato.noteSlightLength = MidiUtils.QuarterTime / 2; // to reduce the number of vibrato events - let generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); + const generator: MidiFileGenerator = new MidiFileGenerator(score, settings, handler); generator.vibratoResolution = settings.player.vibrato.noteSlightLength / 4; generator.generate(); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let expectedEvents: FlatMidiEvent[] = [ + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -1122,7 +1108,7 @@ describe('MidiFileGeneratorTest', () => { info.secondaryChannel, 1920, note1.realValue, - MidiUtils.dynamicToVelocity(note1.dynamics as number) + MidiUtils.dynamicToVelocity(note1.dynamics) ), // vibrato starts on tied note on height of the bend-end @@ -1144,11 +1130,11 @@ describe('MidiFileGeneratorTest', () => { }); it('full-bar-rest', () => { - let tex: string = '\\ts 3 4 3.3.4 3.3.4 3.3.4 | r.1 | 3.3.4 3.3.4 3.3.4'; - let score: Score = parseTex(tex); + const tex: string = '\\ts 3 4 3.3.4 3.3.4 3.3.4 | r.1 | 3.3.4 3.3.4 3.3.4'; + const score: Score = parseTex(tex); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].isFullBarRest).to.be.true; - let expectedNoteOnTimes: number[] = [ + const expectedNoteOnTimes: number[] = [ 0 * MidiUtils.QuarterTime, // note 1 1 * MidiUtils.QuarterTime, // note 2 2 * MidiUtils.QuarterTime, // note 3 @@ -1166,8 +1152,8 @@ describe('MidiFileGeneratorTest', () => { expect(noteOnTimes.join(',')).to.equal(expectedNoteOnTimes.join(',')); - let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); - let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); + const handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); generator.generate(); noteOnTimes = []; for (const evt of handler.midiEvents) { @@ -1181,12 +1167,12 @@ describe('MidiFileGeneratorTest', () => { }); it('time-signature', () => { - let tex: string = '\\ts 3 4 3.3.4 3.3.4 3.3.4'; - let score: Score = parseTex(tex); + const tex: string = '\\ts 3 4 3.3.4 3.3.4 3.3.4'; + const score: Score = parseTex(tex); - let file = new MidiFile(); - let handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(file); - let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); + const file = new MidiFile(); + const handler: AlphaSynthMidiFileHandler = new AlphaSynthMidiFileHandler(file); + const generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); generator.generate(); let timeSignature: MidiEvent | null = null; @@ -1206,8 +1192,8 @@ describe('MidiFileGeneratorTest', () => { }); it('first-bar-tempo', () => { - let tex: string = '\\tempo 120 . \\tempo 60 3.3*4 | \\tempo 80 3.3*4'; - let score: Score = parseTex(tex); + const tex: string = '\\tempo 120 . \\tempo 60 3.3*4 | \\tempo 80 3.3*4'; + const score: Score = parseTex(tex); expect(score.tempo).to.be.equal(120); expect(score.masterBars[0].tempoAutomations).to.have.length(1); @@ -1229,12 +1215,12 @@ describe('MidiFileGeneratorTest', () => { }); it('has-valid-dynamics', () => { - let tex: string = ':2 1.1{dy fff ac} 1.1{dy ppp g}'; - let score: Score = parseTex(tex); + const tex: string = ':2 1.1{dy fff ac} 1.1{dy ppp g}'; + const score: Score = parseTex(tex); - let info: PlaybackInformation = score.tracks[0].playbackInfo; - let note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - let note2: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0]; + const info: PlaybackInformation = score.tracks[0].playbackInfo; + const note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + const note2: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0]; // First note has already highest dynamics which is increased due to accentuation expect(note1.dynamics).to.be.equal(DynamicValue.FFF); expect(note1.accentuated).to.be.equal(AccentuationType.Normal); @@ -1243,7 +1229,7 @@ describe('MidiFileGeneratorTest', () => { expect(note2.dynamics).to.be.equal(DynamicValue.PPP); expect(note2.isGhost).to.be.true; - let expectedEvents: FlatMidiEvent[] = [ + const expectedEvents: FlatMidiEvent[] = [ // channel init new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), new FlatControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), @@ -1273,7 +1259,7 @@ describe('MidiFileGeneratorTest', () => { info.primaryChannel, 1920, note1.realValue, - MidiUtils.dynamicToVelocity((note1.dynamics as number) + 1) + MidiUtils.dynamicToVelocity(note1.dynamics, 1) ), new FlatNoteBendEvent(1920, 0, info.primaryChannel, note2.realValue, 8192), @@ -1283,7 +1269,7 @@ describe('MidiFileGeneratorTest', () => { info.primaryChannel, 1920, note2.realValue, - MidiUtils.dynamicToVelocity((note2.dynamics as number) - 1) + MidiUtils.dynamicToVelocity(note2.dynamics, -1) ), // end of track @@ -1380,7 +1366,7 @@ describe('MidiFileGeneratorTest', () => { const info: PlaybackInformation = score.tracks[0].playbackInfo; const sixtyFourth = MidiUtils.toTicks(Duration.SixtyFourth); - const forte = MidiUtils.dynamicToVelocity(DynamicValue.F as number); + const forte = MidiUtils.dynamicToVelocity(DynamicValue.F); const expectedEvents: FlatMidiEvent[] = [ new FlatNoteEvent(0, 0, info.primaryChannel, sixtyFourth, score.tracks[0].staves[0].tuning[0], forte), new FlatNoteEvent(0, 0, info.primaryChannel, sixtyFourth, score.tracks[0].staves[0].tuning[1], forte), @@ -1546,7 +1532,7 @@ describe('MidiFileGeneratorTest', () => { const note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; const noteKey = note.realValue; - const noteVelocity = MidiUtils.dynamicToVelocity((note.dynamics as number) - 1); + const noteVelocity = MidiUtils.dynamicToVelocity(note.dynamics, -1); const expectedEvents: FlatMidiEvent[] = [ // Upper Mordent (shortened) new FlatNoteEvent(0, 0, 0, 60, noteKey, noteVelocity), @@ -1628,7 +1614,7 @@ describe('MidiFileGeneratorTest', () => { const score = ScoreLoader.loadScoreFromBytes(buffer); const note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; - const noteVelocity = MidiUtils.dynamicToVelocity(note.dynamics as number); + const noteVelocity = MidiUtils.dynamicToVelocity(note.dynamics); const expectedEvents: FlatMidiEvent[] = [ // ii - A string new FlatNoteEvent(0, 0, 0, 480, 48, noteVelocity), // down - no brush offset @@ -1764,7 +1750,7 @@ describe('MidiFileGeneratorTest', () => { expect(actualTimers.join(',')).to.equal(expectedTimers.join(',')); }); - it('transpose', ()=>{ + it('transpose', () => { const score = parseTex(` \\track \\staff \\instrument piano C4.4| r.1 @@ -1805,5 +1791,5 @@ describe('MidiFileGeneratorTest', () => { expect(generator.transpositionPitches.has(5)).to.be.true; expect(generator.transpositionPitches.get(5)!).to.equal(12); - }) + }); }); diff --git a/test/audio/MidiPlaybackController.test.ts b/test/audio/MidiPlaybackController.test.ts index 97952fe06..5c4352097 100644 --- a/test/audio/MidiPlaybackController.test.ts +++ b/test/audio/MidiPlaybackController.test.ts @@ -1,6 +1,6 @@ import { MidiPlaybackController } from '@src/midi/MidiPlaybackController'; import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { GpImporterTestHelper } from '@test/importer/GpImporterTestHelper'; @@ -8,12 +8,12 @@ import { assert, expect } from 'chai'; describe('MidiPlaybackControllerTest', () => { function testRepeat(score: Score, expectedIndexes: number[], maxBars: number): void { - let controller: MidiPlaybackController = new MidiPlaybackController(score); + const controller: MidiPlaybackController = new MidiPlaybackController(score); let i: number = 0; - let errors: number[] = []; - let actual: number[] = []; + const errors: number[] = []; + const actual: number[] = []; while (!controller.finished) { - let index: number = controller.index; + const index: number = controller.index; controller.processCurrent(); if (controller.shouldPlay) { if (i > maxBars) { @@ -40,58 +40,58 @@ describe('MidiPlaybackControllerTest', () => { } async function testGuitarProRepeat(file: string, expectedBars: number[], maxBars: number): Promise { - let reader = await GpImporterTestHelper.prepareImporterWithFile(file); - let score: Score = reader.readScore(); + const reader = await GpImporterTestHelper.prepareImporterWithFile(file); + const score: Score = reader.readScore(); testRepeat(score, expectedBars, maxBars); } function testAlphaTexRepeat(tex: string, expectedBars: number[], maxBars: number): void { - let importer: AlphaTexImporter = new AlphaTexImporter(); + const importer: AlphaTexImporter = new AlphaTexImporter(); importer.initFromString(tex, new Settings()); - let score: Score = importer.readScore(); + const score: Score = importer.readScore(); testRepeat(score, expectedBars, maxBars); } it('repeat-close', async () => { - let file = 'audio/repeat-close.gp5'; - let expectedIndexes = [0, 1, 0, 1, 2]; + const file = 'audio/repeat-close.gp5'; + const expectedIndexes = [0, 1, 0, 1, 2]; await testGuitarProRepeat(file, expectedIndexes, 20); }); it('repeat-close-multi', async () => { - let file = 'audio/repeat-close-multi.gp5'; - let expectedIndexes = [0, 1, 0, 1, 0, 1, 0, 1, 2]; + const file = 'audio/repeat-close-multi.gp5'; + const expectedIndexes = [0, 1, 0, 1, 0, 1, 0, 1, 2]; await testGuitarProRepeat(file, expectedIndexes, 20); }); it('repeat-close-without-start-at-beginning', async () => { - let file = 'audio/repeat-close-without-start-at-beginning.gp5'; - let expectedIndexes = [0, 1, 0, 1]; + const file = 'audio/repeat-close-without-start-at-beginning.gp5'; + const expectedIndexes = [0, 1, 0, 1]; await testGuitarProRepeat(file, expectedIndexes, 20); }); it('repeat-close-alternate-endings', async () => { - let file = 'audio/repeat-close-alternate-endings.gp5'; - let expectedIndexes = [0, 1, 0, 2, 3, 0, 1, 0, 4]; + const file = 'audio/repeat-close-alternate-endings.gp5'; + const expectedIndexes = [0, 1, 0, 2, 3, 0, 1, 0, 4]; await testGuitarProRepeat(file, expectedIndexes, 20); }); it('repeat-with-alphaTex', () => { - let tex: string = + 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'; - let expectedBars: number[] = [0, 1, 2, 0, 1, 2, 3, 3, 3]; + const expectedBars: number[] = [0, 1, 2, 0, 1, 2, 3, 3, 3]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('alternate-endings-with-alphaTex', () => { - let tex: string = ` + 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 `; - let expectedBars: number[] = [ + const expectedBars: number[] = [ 0, 4, 8, // First round: 1st, 5th and 9th bar which have the ending for 1. @@ -110,12 +110,12 @@ describe('MidiPlaybackControllerTest', () => { }); it('multiple-closes', () => { - let tex: string = ` + const tex: string = ` . 4.3.4*4 | \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | 4.3.4*4 `; - let expectedBars: number[] = [ + const expectedBars: number[] = [ 0, // no repeat // First round of outer repeat 1, @@ -147,176 +147,176 @@ describe('MidiPlaybackControllerTest', () => { }); it('nested-repeats', () => { - let tex: string = ` + const tex: string = ` . \\ro 4.3.4*4 | \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | \\rc 2 4.3.4*4 | 3.3.4*4 | \\ro 4.3.4*4 | \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | \\rc 2 4.3.4*4 `; - let expectedBars: number[] = [ + const expectedBars: number[] = [ 0, 1, 2, 1, 2, 3, 4, 0, 1, 2, 1, 2, 3, 4, 5, 6, 7, 8, 7, 8, 9, 10, 6, 7, 8, 7, 8, 9, 10 ]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('alternate-endings-1046', () => { - let tex: string = ` + const tex: string = ` \\tempo 175 . \\ro :1 r | \\ae 1 r | \\ae 2 \\rc 2 r | \\ro r | \\ae 1 r | \\ae (2 3 4) \\rc 4 r | `; - let expectedBars: number[] = [0, 1, 0, 2, 3, 4, 3, 5, 3, 5, 3, 5, 6]; + const expectedBars: number[] = [0, 1, 0, 2, 3, 4, 3, 5, 3, 5, 3, 5, 6]; testAlphaTexRepeat(tex, expectedBars, 50); }); // Da Capo it('da-capo', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | \\jump DaCapo r | r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 0, 1, 2, 3]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 0, 1, 2, 3]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('da-capo-al-fine', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | \\jump fine r | \\jump DaCapoAlFine r | r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 0, 1, 2]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 0, 1, 2]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('da-capo-al-coda', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | \\jump DaCoda r | \\jump DaCapoAlCoda r | \\jump Coda r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 0, 1, 2, 4]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 0, 1, 2, 4]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('da-capo-al-double-coda', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | \\jump DaDoubleCoda r | \\jump DaCapoAlDoubleCoda r | \\jump DoubleCoda r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 0, 1, 2, 4]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 0, 1, 2, 4]; testAlphaTexRepeat(tex, expectedBars, 50); }); // Dal Segno it('dal-segno', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | \\jump Segno r | \\ro \\rc 2 r | \\jump DalSegno r | r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 3, 4, 2, 3, 4, 5]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 3, 4, 2, 3, 4, 5]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('dal-segno-al-coda', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | r | \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | r | r | \\jump DalSegnoAlCoda r | r | \\jump Coda r | r | \\ro \\rc 2 r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('dal-segno-al-coda-missing-target', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | r | \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | r | r | \\jump DalSegnoAlCoda r | r | r | r | \\ro \\rc 2 r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('dal-segno-al-double-coda', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | r | \\jump Segno r | \\ro \\rc 2 r | \\jump DaDoubleCoda r | r | r | \\jump DalSegnoAlDoubleCoda r | r | \\jump DoubleCoda r | r | \\ro \\rc 2 r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('dal-segno-al-fine', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | r | \\jump Segno r | \\ro \\rc 2 r | \\jump Fine | r | r | \\jump DalSegnoAlFine r | r | `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5]; testAlphaTexRepeat(tex, expectedBars, 50); }); // Dal Segno Segno it('dal-segno-segno', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump DalSegnoSegno r | r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 3, 4, 2, 3, 4, 5]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 3, 4, 2, 3, 4, 5]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('dal-segno-segno-missing-target', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | r | \\ro \\rc 2 r | \\jump DalSegnoSegno r | r | \\ro \\rc 2 r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 3, 4, 5, 6, 6]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 3, 4, 5, 6, 6]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('dal-segno-segno-al-coda', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | r | \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump DaCoda r | r | r | \\jump DalSegnoSegnoAlCoda r | r | \\jump Coda r | r | \\ro \\rc 2 r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('dal-segno-segno-al-double-coda', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | r | \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump DaDoubleCoda r | r | r | \\jump DalSegnoSegnoAlDoubleCoda r | r | \\jump DoubleCoda r | r | \\ro \\rc 2 r `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('dal-segno-segno-al-fine', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | r | \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump Fine | r | r | \\jump DalSegnoSegnoAlFine r | r | `; - let expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5]; + const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5]; testAlphaTexRepeat(tex, expectedBars, 50); }); it('multiple-jumps-same-target', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | r | @@ -329,7 +329,7 @@ describe('MidiPlaybackControllerTest', () => { r | r | \\jump DalSegnoAlCoda r | r | \\jump Coda r | r | \\ro \\rc 2 r `; - let expectedBars: number[] = [ + const expectedBars: number[] = [ // first section 0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12, @@ -340,7 +340,7 @@ describe('MidiPlaybackControllerTest', () => { }); it('multiple-jumps-different-target', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\rc 2 r | r | @@ -353,7 +353,7 @@ describe('MidiPlaybackControllerTest', () => { r | r | \\jump DalSegnoSegnoAlDoubleCoda r | r | \\jump DoubleCoda r | r | \\ro \\rc 2 r `; - let expectedBars: number[] = [ + const expectedBars: number[] = [ // first section 0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12, @@ -364,11 +364,11 @@ describe('MidiPlaybackControllerTest', () => { }); it('interleaved-repeat-and-jump', () => { - let tex: string = ` + const tex: string = ` . \\ro :1 r | \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | r | \\jump DalSegnoAlCoda r | r | \\jump Coda r | \\rc 2 r | r | \\ro \\rc 2 r `; - let expectedBars: number[] = [ + const expectedBars: number[] = [ 0, 1, 2, 2, 3, 4, 5, 1, 2, 3, 7, 8, 9, 10, 10 diff --git a/test/audio/MidiTickLookup.test.ts b/test/audio/MidiTickLookup.test.ts index 1cbbb5377..fc42d15b3 100644 --- a/test/audio/MidiTickLookup.test.ts +++ b/test/audio/MidiTickLookup.test.ts @@ -2,10 +2,23 @@ import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; import { ScoreLoader } from '@src/importer/ScoreLoader'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { Logger } from '@src/Logger'; -import { AlphaSynthMidiFileHandler, MasterBarTickLookup, MidiFile, MidiFileGenerator, MidiTickLookup, MidiTickLookupFindBeatResult } from '@src/midi'; -import { MasterBarTickLookupTempoChange } from '@src/midi/MasterBarTickLookup'; +import { AlphaSynthMidiFileHandler } from '@src/midi/AlphaSynthMidiFileHandler'; + +import { MasterBarTickLookup, MasterBarTickLookupTempoChange } from '@src/midi/MasterBarTickLookup'; +import { MidiFile } from '@src/midi/MidiFile'; +import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; +import { + MidiTickLookup, + type MidiTickLookupFindBeatResult, + MidiTickLookupFindBeatResultCursorMode +} from '@src/midi/MidiTickLookup'; import { MidiUtils } from '@src/midi/MidiUtils'; -import { Beat, Duration, MasterBar, Note, Score } from '@src/model'; +import { Beat } from '@src/model/Beat'; +import { Duration } from '@src/model/Duration'; +import { MasterBar } from '@src/model/MasterBar'; +import { ModelUtils } from '@src/model/ModelUtils'; +import { Note } from '@src/model/Note'; +import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { TestPlatform } from '@test/TestPlatform'; import { expect } from 'chai'; @@ -37,7 +50,7 @@ describe('MidiTickLookupTest', () => { expect(masterBarLookup.firstBeat!.end).to.equal(MidiUtils.QuarterTime); expect(masterBarLookup.firstBeat!.highlightedBeats.length).to.equal(1); expect(masterBarLookup.firstBeat!.highlightedBeats[0].beat).to.equal(nb); - }) + }); function prepareVariantTest(): MidiTickLookup { const lookup = new MidiTickLookup(); @@ -86,7 +99,7 @@ describe('MidiTickLookupTest', () => { expect(l1).to.equal(masterBar.firstBeat!); expect(l1.nextBeat).to.equal(l2); expect(l2.nextBeat).to.equal(n1); - }) + }); it('variant-c', () => { const lookup = prepareVariantTest(); @@ -108,8 +121,7 @@ describe('MidiTickLookupTest', () => { expect(l1.nextBeat).to.equal(l2); expect(l2.nextBeat).to.equal(n1); expect(n1).to.equal(masterBar.lastBeat!); - }) - + }); it('variant-d', () => { const lookup = prepareVariantTest(); @@ -131,7 +143,7 @@ describe('MidiTickLookupTest', () => { expect(n1.nextBeat).to.equal(l1); expect(l1.nextBeat).to.equal(l2); expect(l2).to.equal(masterBar.lastBeat!); - }) + }); it('variant-e', () => { const lookup = prepareVariantTest(); @@ -153,7 +165,7 @@ describe('MidiTickLookupTest', () => { expect(n1.nextBeat).to.equal(l1); expect(l1.nextBeat).to.equal(l2); expect(l2).to.equal(masterBar.lastBeat!); - }) + }); it('variant-f', () => { const lookup = prepareVariantTest(); @@ -187,7 +199,7 @@ describe('MidiTickLookupTest', () => { expect(n2.nextBeat).to.equal(l1); expect(l1.nextBeat).to.equal(l2); expect(l2).to.equal(masterBar.lastBeat!); - }) + }); it('variant-g', () => { const lookup = prepareVariantTest(); @@ -214,7 +226,7 @@ describe('MidiTickLookupTest', () => { expect(n1.nextBeat).to.equal(l1); expect(l1.nextBeat).to.equal(l2); expect(l2).to.equal(masterBar.lastBeat!); - }) + }); it('variant-h-variant-m', () => { const lookup = prepareVariantTest(); @@ -253,7 +265,7 @@ describe('MidiTickLookupTest', () => { expect(l1.nextBeat).to.equal(n2); expect(n2.nextBeat).to.equal(l2); expect(l2).to.equal(masterBar.lastBeat!); - }) + }); it('variant-i', () => { const lookup = prepareVariantTest(); @@ -284,8 +296,7 @@ describe('MidiTickLookupTest', () => { expect(n1.nextBeat).to.equal(l1); expect(l1.nextBeat).to.equal(l2); expect(l2).to.equal(masterBar.lastBeat!); - }) - + }); it('variant-j', () => { const lookup = prepareVariantTest(); @@ -323,7 +334,7 @@ describe('MidiTickLookupTest', () => { expect(n2.nextBeat).to.equal(l1); expect(l1.nextBeat).to.equal(l2); expect(l2).to.equal(masterBar.lastBeat!); - }) + }); it('variant-k-variant-m', () => { const lookup = prepareVariantTest(); @@ -361,7 +372,7 @@ describe('MidiTickLookupTest', () => { expect(n2.nextBeat).to.equal(l1); expect(l1.nextBeat).to.equal(l2); expect(l2).to.equal(masterBar.lastBeat!); - }) + }); it('variant-l', () => { const lookup = prepareVariantTest(); @@ -378,7 +389,7 @@ describe('MidiTickLookupTest', () => { expect(l1).to.equal(masterBar.firstBeat!); expect(l1.nextBeat).to.equal(l2); expect(l2).to.equal(masterBar.lastBeat!); - }) + }); it('variant-m', () => { const lookup = prepareVariantTest(); @@ -409,7 +420,7 @@ describe('MidiTickLookupTest', () => { expect(n1.nextBeat).to.equal(l1); expect(l1.nextBeat).to.equal(l2); expect(l2).to.equal(masterBar.lastBeat!); - }) + }); it('variant-h-variant-n-variant-b', () => { const lookup = prepareVariantTest(); @@ -448,9 +459,7 @@ describe('MidiTickLookupTest', () => { expect(l1.nextBeat).to.equal(l2); expect(l2.nextBeat).to.equal(n2); expect(n2).to.equal(masterBar.lastBeat!); - }) - - + }); function beatWithFret(fret: number) { const b = new Beat(); @@ -459,8 +468,8 @@ describe('MidiTickLookupTest', () => { return b; } - function fretOfBeat(beat: Beat | null) { - return beat && beat.notes.length > 0 ? beat.notes[0].fret : -1; + function idOfBeat(beat: Beat | null) { + return beat ? beat.id : -1; } function prepareGraceMultiVoice(graceNoteOverlap: number, graceNoteDuration: number): MidiTickLookup { @@ -497,7 +506,7 @@ describe('MidiTickLookupTest', () => { // grace note lookup.addBeat(beatWithFret(3), -graceNoteOverlap, graceNoteDuration); // normal note - const onNoteSteal = (-graceNoteOverlap) + graceNoteDuration; + const onNoteSteal = -graceNoteOverlap + graceNoteDuration; lookup.addBeat(beatWithFret(4), onNoteSteal, MidiUtils.QuarterTime - onNoteSteal); return lookup; @@ -510,19 +519,19 @@ describe('MidiTickLookupTest', () => { // validate first bar let current = lookup.masterBars[0].firstBeat!; - expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("0,2"); + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('0,2'); expect(current.start).to.equal(0); expect(current.duration).to.equal(1920); current = current.nextBeat!; - expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("1,2"); + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('1,2'); expect(current.start).to.equal(1920); // quarter note ends earlier due to grace note expect(current.duration).to.equal(840); current = current.nextBeat!; // on last slice we have the grace note but not the quarter note - expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("2,3"); + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('2,3'); expect(current.start).to.equal(2760); expect(current.duration).to.equal(120); @@ -531,10 +540,10 @@ describe('MidiTickLookupTest', () => { current = lookup.masterBars[1].firstBeat!; // no grace note, normal quarter note - expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("4"); + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('4'); expect(current.start).to.equal(0); expect(current.duration).to.equal(960); - }) + }); it('grace-multivoice-with-overlap', () => { const lookup = prepareGraceMultiVoice(120, 240); @@ -543,19 +552,19 @@ describe('MidiTickLookupTest', () => { // validate first bar let current = lookup.masterBars[0].firstBeat!; - expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("0,2"); + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('0,2'); expect(current.start).to.equal(0); expect(current.duration).to.equal(1920); current = current.nextBeat!; - expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("1,2"); + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('1,2'); expect(current.start).to.equal(1920); // quarter note ends earlier due to grace note expect(current.duration).to.equal(840); current = current.nextBeat!; // on last slice we have the grace note but not the quarter note - expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("2,3"); + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('2,3'); expect(current.start).to.equal(2760); expect(current.duration).to.equal(120); @@ -564,17 +573,16 @@ describe('MidiTickLookupTest', () => { current = lookup.masterBars[1].firstBeat!; // half the grace note - expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("3"); + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('3'); expect(current.start).to.equal(0); expect(current.duration).to.equal(120); // no grace note, normal quarter note current = current.nextBeat!; - expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("4"); + expect(current.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('4'); expect(current.start).to.equal(120); expect(current.duration).to.equal(840); - }) - + }); it('cursor-snapping', async () => { const buffer = await TestPlatform.loadFile('test-data/audio/cursor-snapping.gp'); @@ -584,7 +592,7 @@ describe('MidiTickLookupTest', () => { const tracks = new Set([0]); - // initial lookup should detect correctly first rest on first voice + // initial lookup should detect correctly first rest on first voice // with the quarter rest on the second voice as next beat const firstBeat = lookup.findBeat(tracks, 0, null); @@ -593,7 +601,7 @@ describe('MidiTickLookupTest', () => { expect(firstBeat!.beat.duration).to.equal(Duration.Whole); expect(firstBeat!.nextBeat!.beat.duration).to.equal(Duration.Quarter); - // Duration must only go to the next rest on the second voice despite the whole note + // Duration must only go to the next rest on the second voice despite the whole note expect(firstBeat!.duration).to.equal(750); expect(firstBeat!.beatLookup.duration).to.equal(960); @@ -616,97 +624,117 @@ describe('MidiTickLookupTest', () => { expect(secondBeat!.beatLookup.duration).to.equal(960); }); - it('before-beat-grace-later-bars', () => { const settings = new Settings(); const importer = new AlphaTexImporter(); - importer.initFromString(`\\ts 2 4 1.1.2 | 2.1.4 3.1 | 4.1{gr} 5.1{gr} 6.1.2 | 7.1.4 8.1`, settings); + importer.initFromString('\\ts 2 4 1.1.2 | 2.1.4 3.1 | 4.1{gr} 5.1{gr} 6.1.2 | 7.1.4 8.1', settings); const score = importer.readScore(); const lookup = buildLookup(score, settings); - // bar 2 contains the grace notes which stole duration from fret 3 beat. + // bar 2 contains the grace notes which stole duration from fret 3 beat. const bar2 = lookup.masterBars[1]; - + let current = bar2.firstBeat; - expect(current!.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("2"); + expect(current!.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('2'); expect(current!.start).to.equal(0); expect(current!.duration).to.equal(960); current = current!.nextBeat; - expect(current!.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("3"); + expect(current!.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('3'); expect(current!.start).to.equal(960); - expect(current!.duration).to.equal(840); // 120 ticks stolen by grace beats - + expect(current!.duration).to.equal(840); // 120 ticks stolen by grace beats + current = current!.nextBeat; - expect(current!.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("4"); + expect(current!.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('4'); expect(current!.start).to.equal(960 + 840); expect(current!.duration).to.equal(60); current = current!.nextBeat; - expect(current!.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal("5"); + expect(current!.highlightedBeats.map(b => b.beat.notes[0].fret).join(',')).to.equal('5'); expect(current!.start).to.equal(960 + 840 + 60); expect(current!.duration).to.equal(60); }); - function lookupTest( tex: string, ticks: number[], trackIndexes: number[], durations: number[], - currentBeatFrets: number[], - nextBeatFrets: number[], + currentBeatIds: number[], + nextBeatIds: number[], + expectedCursorModes: MidiTickLookupFindBeatResultCursorMode[] | null = null, skipClean: boolean = false ) { const buffer = ByteBuffer.fromString(tex); const settings = new Settings(); + Beat.resetIds(); const score = ScoreLoader.loadScoreFromBytes(buffer.getBuffer(), settings); const lookup = buildLookup(score, settings); const tracks = new Set(trackIndexes); + if ( + (trackIndexes.length === 1 && + score.stylesheet.perTrackMultiBarRest && + score.stylesheet.perTrackMultiBarRest!.has(trackIndexes[0])) || + score.stylesheet.multiTrackMultiBarRest + ) { + lookup.multiBarRestInfo = ModelUtils.buildMultiBarRestInfo( + score.tracks.filter(t => tracks.has(t.index)), + 0, + score.masterBars.length - 1 + ); + } + let currentLookup: MidiTickLookupFindBeatResult | null = null; - const actualIncrementalFrets: number[] = []; - const actualIncrementalNextFrets: number[] = []; + const actualIncrementalIds: number[] = []; + const actualIncrementalNextIds: number[] = []; const actualIncrementalTickDurations: number[] = []; - const actualCleanFrets: number[] = []; - const actualCleanNextFrets: number[] = []; + const actualCleanIds: number[] = []; + const actualCleanNextIds: number[] = []; const actualCleanTickDurations: number[] = []; + const actualCursorModes: MidiTickLookupFindBeatResultCursorMode[] = []; for (let i = 0; i < ticks.length; i++) { currentLookup = lookup.findBeat(tracks, ticks[i], currentLookup); - Logger.debug("Test", `Checking index ${i} with tick ${ticks[i]}`) + Logger.info('Test', `Checking index ${i} with tick ${ticks[i]}`); expect(currentLookup).to.be.ok; - actualIncrementalFrets.push(fretOfBeat(currentLookup!.beat)); - actualIncrementalNextFrets.push(fretOfBeat(currentLookup!.nextBeat?.beat ?? null)) - actualIncrementalTickDurations.push(currentLookup!.tickDuration) + actualIncrementalIds.push(idOfBeat(currentLookup!.beat)); + actualIncrementalNextIds.push(idOfBeat(currentLookup!.nextBeat?.beat ?? null)); + actualIncrementalTickDurations.push(currentLookup!.tickDuration); + actualCursorModes.push(currentLookup!.cursorMode); if (!skipClean) { const cleanLookup = lookup.findBeat(tracks, ticks[i], null); - actualCleanFrets.push(fretOfBeat(cleanLookup!.beat)); - actualCleanNextFrets.push(fretOfBeat(cleanLookup!.nextBeat?.beat ?? null)) - actualCleanTickDurations.push(cleanLookup!.tickDuration) + actualCleanIds.push(idOfBeat(cleanLookup?.beat ?? null)); + actualCleanNextIds.push(idOfBeat(cleanLookup?.nextBeat?.beat ?? null)); + actualCleanTickDurations.push(cleanLookup?.tickDuration ?? -1); } } - expect(actualIncrementalFrets.join(',')).to.equal(currentBeatFrets.join(',')); - expect(actualIncrementalNextFrets.join(',')).to.equal(nextBeatFrets.join(',')); - expect(actualIncrementalTickDurations.join(',')).to.equal(durations.join(',')); + expect(actualIncrementalIds.join(',')).to.equal(currentBeatIds.join(','), 'currentBeatIds mismatch'); + expect(actualIncrementalNextIds.join(',')).to.equal(nextBeatIds.join(','), 'nextBeatIds mismatch'); + expect(actualIncrementalTickDurations.join(',')).to.equal(durations.join(','), 'durations mismatch'); + if (expectedCursorModes) { + expect(expectedCursorModes.map(m => MidiTickLookupFindBeatResultCursorMode[m]).join(',')).to.equal( + actualCursorModes.map(m => MidiTickLookupFindBeatResultCursorMode[m]).join(','), + 'cursorModes mismatch' + ); + } if (!skipClean) { - expect(actualCleanFrets.join(',')).to.equal(currentBeatFrets.join(',')); - expect(actualCleanNextFrets.join(',')).to.equal(nextBeatFrets.join(',')); - expect(actualCleanTickDurations.join(',')).to.equal(durations.join(',')); + expect(actualCleanIds.join(',')).to.equal(currentBeatIds.join(','), 'cleanIds mismatch'); + expect(actualCleanNextIds.join(',')).to.equal(nextBeatIds.join(','), 'cleanNextIds mismatch'); + expect(actualCleanTickDurations.join(',')).to.equal(durations.join(','), 'cleanTickDurations mismatch'); } } - - - function nextBeatSearchTest(trackIndexes: number[], + function nextBeatSearchTest( + trackIndexes: number[], durations: number[], currentBeatFrets: number[], nextBeatFrets: number[] @@ -720,100 +748,54 @@ describe('MidiTickLookupTest', () => { \\track "T02" 3.1.16 4.1.16 5.1.8 | 8.1.16 9.1.16 10.1.8 `, - [ - 0, 120, 240, 360, 480, 600, 720, 840, 960, - 1080, 1200, 1320, 1440, 1560, 1680, 1800 - ], + [0, 120, 240, 360, 480, 600, 720, 840, 960, 1080, 1200, 1320, 1440, 1560, 1680, 1800], trackIndexes, durations, currentBeatFrets, nextBeatFrets - ) + ); } - it('next-beat-search-multi-track', () => { nextBeatSearchTest( [0, 1], - [ - 240, 240, 240, 240, 480, 480, 480, 480, - 240, 240, 240, 240, 480, 480, 480, 480 - ], - [ - 1, 1, 4, 4, 2, 2, 2, 2, - 6, 6, 9, 9, 7, 7, 7, 7 - ], - [ - 4, 4, 2, 2, 6, 6, 6, 6, - 9, 9, 7, 7, -1, -1, -1, -1 - ] - ) + [240, 240, 240, 240, 480, 480, 480, 480, 240, 240, 240, 240, 480, 480, 480, 480], + [0, 0, 5, 5, 1, 1, 1, 1, 2, 2, 8, 8, 3, 3, 3, 3], + [5, 5, 1, 1, 2, 2, 2, 2, 8, 8, 3, 3, -1, -1, -1, -1] + ); }); it('next-beat-search-track-1', () => { nextBeatSearchTest( [0], - [ - 480, 480, 480, 480, 480, 480, 480, 480, - 480, 480, 480, 480, 480, 480, 480, 480 - ], - [ - 1, 1, 1, 1, 2, 2, 2, 2, - 6, 6, 6, 6, 7, 7, 7, 7 - ], - [ - 2, 2, 2, 2, 6, 6, 6, 6, - 7, 7, 7, 7, -1, -1, -1, -1 - ] - ) + [480, 480, 480, 480, 480, 480, 480, 480, 480, 480, 480, 480, 480, 480, 480, 480], + [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3], + [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, -1, -1, -1, -1] + ); }); it('lookup-triplet-feel-reference', () => { lookupTest( `\\ts 2 4 1.1.4{tu 3} 2.1.8{tu 3} 3.1.4{tu 3} 4.1.8{tu 3} | 5.1.4{tu 3} 6.1.8{tu 3} 7.1.4{tu 3} 8.1.8{tu 3}`, - [ - 0, 640, 960, 1600, - 1920, 2560, 2880, 3520 - ], + [0, 640, 960, 1600, 1920, 2560, 2880, 3520], [0], - [ - 640, 320, 640, 320, - 640, 320, 640, 320 - ], - [ - 1, 2, 3, 4, - 5, 6, 7, 8 - ], - [ - 2, 3, 4, 5, - 6, 7, 8, -1 - ] - ) + [640, 320, 640, 320, 640, 320, 640, 320], + [0, 1, 2, 3, 4, 5, 6, 7], + [1, 2, 3, 4, 5, 6, 7, -1] + ); }); it('lookup-triplet-feel-test', () => { lookupTest( `\\tf triplet-8th \\ts 2 4 1.1.8 2.1.8 3.1.8 4.1.8 | 5.1.8 6.1.8 7.1.8 8.1.8`, - [ - 0, 640, 960, 1600, - 1920, 2560, 2880, 3520 - ], + [0, 640, 960, 1600, 1920, 2560, 2880, 3520], [0], - [ - 640, 320, 640, 320, - 640, 320, 640, 320 - ], - [ - 1, 2, 3, 4, - 5, 6, 7, 8 - ], - [ - 2, 3, 4, 5, - 6, 7, 8, -1 - ] - ) + [640, 320, 640, 320, 640, 320, 640, 320], + [0, 1, 2, 3, 4, 5, 6, 7], + [1, 2, 3, 4, 5, 6, 7, -1] + ); }); it('incomplete', () => { @@ -833,33 +815,29 @@ describe('MidiTickLookupTest', () => { 5760, 6240, 6720, 7200 ], [0], - [ - 960, 960, 2880, 2880, - 2880, 2880, 2880, 2880, - 960, 960, 2880, 2880, - 2880, 2880, 2880, 2880 - ], + [960, 960, 2880, 2880, 2880, 2880, 2880, 2880, 960, 960, 2880, 2880, 2880, 2880, 2880, 2880], [ // first bar, real playback - 1, 1, 2, 2, + 0, 0, 1, 1, // gap - 2, 2, 2, 2, + 1, 1, 1, 1, // second bar, real playback - 3, 3, 4, 4, + 2, 2, 3, 3, // second gap - 4, 4, 4, 4 + 3, 3, 3, 3 ], [ - 2, 2, -1, -1, + 1, 1, 2, 2, - -1, -1, -1, -1, + 2, 2, 2, 2, - 4, 4, -1, -1, + 3, 3, -1, -1, -1, -1, -1, -1 ], + null, true - ) + ); }); it('empty-bar', () => { @@ -875,20 +853,298 @@ describe('MidiTickLookupTest', () => { 1920, 2400, 2880, 3360 ], [0], - [ - 1920, 1920, 1920, 1920, - 1920, 1920, 1920, 1920 - ], + [1920, 1920, 1920, 1920, 1920, 1920, 1920, 1920], [ // first bar (empty) - -1, -1, -1, -1, + 0, 0, 0, 0, // second bar, real playback 1, 1, 1, 1 ], + [1, 1, 1, 1, -1, -1, -1, -1] + ); + }); + + it('multibar-rest-single-rest', () => { + lookupTest( + ` + \\track { multiBarRest } + \\ts 2 4 + 1.1.4 2.1.4 | + r | r | r | r | + 3.1.4 | + r | r | r | + \\ro r | r | r | \\rc 2 r | + r + `, + // prettier-ignore [ - 1, 1, 1, 1, + // 1st bar (two quarters) + 0, 480, 960, 1440, + // 2nd bar (multirest start) + 1920, 2400, 2880, 3360, + // 3rd bar (multirest with 2nd bar) + 3840, 4320, 4800, 5280, + // 4th bar (multirest with 2nd bar) + 5760, 6240, 6720, 7200, + // 5th bar (multirest with 2nd bar) + 7680, 8160, 8640, 9120, + // 6th bar + 9600, 10080, 10560, 11040, + // 7th bar (multirest start) + 11520, 12000, 12480, 12960, + // 8th bar (multirest with 7th bar) + 13440, 13920, 14400, 14880, + // 9th bar (multirest with 7th bar) + 15360, 15840, 16320, 16800, + + // 10th bar (multirest start - repeat open) + 17280, 17760, 18240, 18720, + // 11th bar (multirest with 10th bar) + 19200, 19680, 20160, 20640, + // 12th bar (multirest with 10th bar) + 21120, 21600, 22080, 22560, + // 13th bar (multirest with 10th bar - repeat close) + 23040, 23520, 24000, 24480, + + // 10th bar repated (multirest start - repeat open) + 24960, 25440, 25920, 26400, + // 11th bar repated (multirest with 10th bar) + 26880, 27360, 27840, 28320, + // 12th bar repated (multirest with 10th bar) + 28800, 29280, 29760, 30240, + // 13th bar repated (multirest with 10th bar - repeat close) + 30720, 31200, 31680, 32160, + + // 14th bar + 32640, 33120, 33600, 34080 + ], + [0], + // prettier-ignore + [ + // 1st bar + 960, + 960, + 960, + 960, + // 2nd bar - 5th bar (4 bars 2/4) + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + // 6th bar + 960 * 2, + 960 * 2, + 960 * 2, + 960 * 2, + // 7th bar - 9th bar + 960 * 2 * 3, + 960 * 2 * 3, + 960 * 2 * 3, + 960 * 2 * 3, + 960 * 2 * 3, + 960 * 2 * 3, + 960 * 2 * 3, + 960 * 2 * 3, + 960 * 2 * 3, + 960 * 2 * 3, + 960 * 2 * 3, + 960 * 2 * 3, + // 10th - 13th bar + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + // 10th - 13th bar (repeated) + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + 960 * 2 * 4, + // 14th bar + 960 * 2, + 960 * 2, + 960 * 2, + 960 * 2 + ], + // prettier-ignore + [ + // first bar (empty) + 0, 0, 1, 1, + // 2nd bar - 5th bar (4 bars 2/4) + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + // 6th bar + 6, 6, 6, 6, + // 7th bar - 9th bar + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + // 10th-13th bar + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + // 10th-13th bar (repeated) + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + // 14th bar + 14, 14, 14, 14 + ], + // prettier-ignore + [ + // first bar (empty) + 1, 1, 2, 2, + // 2nd bar - 5th bar (4 bars 2/4) + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + // 6th bar + 7, 7, 7, 7, + // 7th bar - 9th bar + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + // 10th-13th bar + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + // 10th-13th bar (repeated) + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + // 14th bar -1, -1, -1, -1 - ] - ) + ], + undefined, + true + ); + }); + + it('multibar-rest-repeat', () => { + lookupTest( + ` + \\track { multiBarRest } + \\ts 2 4 + \\ro r | r | \\rc 2 r | + r + `, + // prettier-ignore + [ + // 1st bar + 0, 960, + // 2nd bar + 1920, 2880, + // 3rd bar + 3840, 4800, + // 1st bar (repated) + 5760, 6720, + // 2nd bar (repeated) + 7680, 8640, + // 3rd bar (repeated) + 9600, 10560, + // 4th bar + 11520, 12480 + ], + [0], + // prettier-ignore + [ + // 1st bar + 5760, 5760, + // 2nd bar + 5760, 5760, + // 3rd bar + 5760, 5760, + // 1st bar (repated) + 5760, 5760, + // 2nd bar (repeated) + 5760, 5760, + // 3rd bar (repeated) + 5760, 5760, + // 4th bar + 1920, 1920 + ], + // prettier-ignore + [ + // 1st bar + 0, 0, + // 2nd bar + 0, 0, + // 3rd bar + 0, 0, + // 1st bar (repated) + 0, 0, + // 2nd bar (repeated) + 0, 0, + // 3rd bar (repeated) + 0, 0, + // 4th bar + 3, 3 + ], + // prettier-ignore + [ + // 1st bar + 0, 0, + // 2nd bar + 0, 0, + // 3rd bar + 0, 0, + // 1st bar (repated) + 3, 3, + // 2nd bar (repeated) + 3, 3, + // 3rd bar (repeated) + 3, 3, + // 4th bar + -1, -1 + ], + [ + // 1st bar + MidiTickLookupFindBeatResultCursorMode.ToEndOfBar, + MidiTickLookupFindBeatResultCursorMode.ToEndOfBar, + + // 2nd bar + MidiTickLookupFindBeatResultCursorMode.ToEndOfBar, + MidiTickLookupFindBeatResultCursorMode.ToEndOfBar, + + // 3rd bar + MidiTickLookupFindBeatResultCursorMode.ToEndOfBar, + MidiTickLookupFindBeatResultCursorMode.ToEndOfBar, + + // 1st bar (repated) + MidiTickLookupFindBeatResultCursorMode.ToNextBext, + MidiTickLookupFindBeatResultCursorMode.ToNextBext, + // 2nd bar (repeated) + MidiTickLookupFindBeatResultCursorMode.ToNextBext, + MidiTickLookupFindBeatResultCursorMode.ToNextBext, + // 3rd bar (repeated) + MidiTickLookupFindBeatResultCursorMode.ToNextBext, + MidiTickLookupFindBeatResultCursorMode.ToNextBext, + + // 4th bar + MidiTickLookupFindBeatResultCursorMode.ToEndOfBar, + MidiTickLookupFindBeatResultCursorMode.ToEndOfBar + ], + true + ); }); }); diff --git a/test/audio/TestOutput.ts b/test/audio/TestOutput.ts index c16325b0c..dc64c3a5c 100644 --- a/test/audio/TestOutput.ts +++ b/test/audio/TestOutput.ts @@ -1,5 +1,5 @@ -import { ISynthOutput } from '@src/synth/ISynthOutput'; -import { EventEmitter, IEventEmitter, IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; +import type { ISynthOutput, ISynthOutputDevice } from '@src/synth/ISynthOutput'; +import { EventEmitter, type IEventEmitter, type IEventEmitterOfT, EventEmitterOfT } from '@src/EventEmitter'; export class TestOutput implements ISynthOutput { public samples: number[] = []; @@ -58,4 +58,12 @@ export class TestOutput implements ISynthOutput { * Fired when the output needs more samples to be played. */ readonly sampleRequest: IEventEmitter = new EventEmitter(); + + public async enumerateOutputDevices(): Promise { + return [] as ISynthOutputDevice[]; + } + public async setOutputDevice(device: ISynthOutputDevice | null): Promise {} + public async getOutputDevice(): Promise { + return null; + } } diff --git a/test/bundler/Vite.test.ts b/test/bundler/Vite.test.ts index 141f6b430..273d87966 100644 --- a/test/bundler/Vite.test.ts +++ b/test/bundler/Vite.test.ts @@ -1,7 +1,7 @@ /**@target web */ import { alphaTab } from '../../src/alphaTab.vite'; -import path from 'path'; -import fs from 'fs'; +import path from 'node:path'; +import fs from 'node:fs'; import { expect } from 'chai'; describe('Vite', () => { @@ -17,9 +17,11 @@ describe('Vite', () => { await vite.build( vite.defineConfig({ base: '/test-data/bundler/vite/dist/', - plugins: [alphaTab({ - alphaTabSourceDir: '../../../dist/' - })] + plugins: [ + alphaTab({ + alphaTabSourceDir: '../../../dist/' + }) + ] }) ); } catch (e) { @@ -55,7 +57,7 @@ describe('Vite', () => { if (file.name.startsWith('index-')) { // ensure new worker has worker import - expect(text).to.include('alphaTabWorker(new URL'); + expect(text.match(/new [^ ]+\.alphaTabWorker\(new [^ ]+\.alphaTabUrl/)).to.be.ok; // ensure worker bootstrapping script is references expect(text).to.include('assets/alphaTab.worker-'); // ensure worklet bootstrapper script is references @@ -69,7 +71,7 @@ describe('Vite', () => { expect(text).to.include("font-family: 'alphaTab';"); workerValidated = true; - } else if(file.name.startsWith('alphaTab.worklet-')) { + } else if (file.name.startsWith('alphaTab.worklet-')) { expect(text).to.include('initializeAudioWorklet()'); // without custom chunking the app will bundle alphatab directly expect(text).to.include("font-family: 'alphaTab';"); diff --git a/test/bundler/WebPack.test.ts b/test/bundler/WebPack.test.ts index b66e7779f..3a060b37d 100644 --- a/test/bundler/WebPack.test.ts +++ b/test/bundler/WebPack.test.ts @@ -1,14 +1,15 @@ /**@target web */ import { AlphaTabWebPackPlugin } from '../../src/alphaTab.webpack'; import webpack from 'webpack'; -import path from 'path'; -import fs from 'fs'; +import path from 'node:path'; +import fs from 'node:fs'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import { expect } from 'chai'; describe('WebPack', () => { it('bundle-correctly', async () => { const bundlerProject = './test-data/bundler/webpack'; + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: resolve/reject called accordingly await new Promise(async (resolve, reject) => { const cwd = process.cwd(); process.chdir(bundlerProject); @@ -45,6 +46,10 @@ describe('WebPack', () => { } }, (err, stats) => { + if (stats) { + console.log(stats.toString()); + } + process.chdir(cwd); if (err) { reject(err); @@ -87,7 +92,9 @@ describe('WebPack', () => { if (file.name.startsWith('app-')) { // ensure new worker has worker import - expect(text).to.include('new Environment.alphaTabWorker(new URL(/* worker import */'); + expect(text).to.include( + 'new Environment.alphaTabWorker(new Environment.alphaTabUrl(/* worker import */' + ); // ensure worklet bootstrapper exists expect(text).to.include('/* worklet bootstrap */ async function(__webpack_worklet__) {'); // without custom bundling the app will bundle alphatab directly diff --git a/test/exporter/Gp7Exporter.test.ts b/test/exporter/Gp7Exporter.test.ts index 994458002..34e6036e7 100644 --- a/test/exporter/Gp7Exporter.test.ts +++ b/test/exporter/Gp7Exporter.test.ts @@ -1,6 +1,6 @@ import { Gp7To8Importer } from '@src/importer/Gp7To8Importer'; import { ByteBuffer } from '@src/io/ByteBuffer'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { TestPlatform } from '@test/TestPlatform'; import { Gp7Exporter } from '@src/exporter/Gp7Exporter'; @@ -8,11 +8,12 @@ import { JsonConverter } from '@src/model/JsonConverter'; import { ScoreLoader } from '@src/importer/ScoreLoader'; import { ComparisonHelpers } from '@test/model/ComparisonHelpers'; import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; +import { expect } from 'chai'; describe('Gp7ExporterTest', () => { async function loadScore(name: string): Promise { try { - const data = await TestPlatform.loadFile('test-data/' + name); + const data = await TestPlatform.loadFile(`test-data/${name}`); return ScoreLoader.loadScoreFromBytes(data); } catch (e) { return null; @@ -20,7 +21,7 @@ describe('Gp7ExporterTest', () => { } function prepareImporterWithBytes(buffer: Uint8Array): Gp7To8Importer { - let readerBase: Gp7To8Importer = new Gp7To8Importer(); + const readerBase: Gp7To8Importer = new Gp7To8Importer(); readerBase.init(ByteBuffer.fromBuffer(buffer), new Settings()); return readerBase; } @@ -42,13 +43,16 @@ describe('Gp7ExporterTest', () => { const expectedJson = JsonConverter.scoreToJsObject(expected); const actualJson = JsonConverter.scoreToJsObject(actual); - ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '<' + fileName + '>', ignoreKeys); + ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, `<${fileName}>`, ignoreKeys); } - async function testRoundTripFolderEqual(name: string): Promise { + async function testRoundTripFolderEqual(name: string, ignoredFiles?: string[]): Promise { const files: string[] = await TestPlatform.listDirectory(`test-data/${name}`); + const ignoredFilesLookup = new Set(ignoredFiles); for (const file of files) { - await testRoundTripEqual(`${name}/${file}`, null); + if (!ignoredFilesLookup.has(file)) { + await testRoundTripEqual(`${name}/${file}`, null); + } } } @@ -75,7 +79,7 @@ describe('Gp7ExporterTest', () => { }); it('visual-music-notation', async () => { - await testRoundTripFolderEqual('visual-tests/music-notation'); + await testRoundTripFolderEqual('visual-tests/music-notation', ['barlines.xml']); }); it('visual-notation-legend', async () => { @@ -91,7 +95,7 @@ describe('Gp7ExporterTest', () => { }); it('gp5-to-gp7', async () => { - await testRoundTripEqual(`conversion/full-song.gp5`, [ + await testRoundTripEqual('conversion/full-song.gp5', [ 'accidentalmode', // gets upgraded from default 'percussionarticulations', // gets added 'automations' // volume automations are not yet supported in gpif @@ -99,7 +103,7 @@ describe('Gp7ExporterTest', () => { }); it('gp6-to-gp7', async () => { - await testRoundTripEqual(`conversion/full-song.gpx`, [ + await testRoundTripEqual('conversion/full-song.gpx', [ 'accidentalmode', // gets upgraded from default 'percussionarticulations', // gets added 'percussionarticulation' // gets added @@ -134,6 +138,34 @@ describe('Gp7ExporterTest', () => { ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '', ['accidentalmode']); }); + it('alphatex-drumps-to-gp7', () => { + const tex = `\\track "Drums" + \\instrument percussion + \\clef neutral + \\articulation Kick 36 + \\articulation Unused 46 + Kick.4 42.4 Kick.4 42.4 + `; + + const importer = new AlphaTexImporter(); + importer.initFromString(tex, new Settings()); + const expected = importer.readScore(); + const exported = exportGp7(expected); + + const actual = prepareImporterWithBytes(exported).readScore(); + + const expectedJson = JsonConverter.scoreToJsObject(expected); + const actualJson = JsonConverter.scoreToJsObject(actual); + + ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '', ['accidentalmode']); + + expect(actual.tracks[0].percussionArticulations).to.have.length(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) + expect(actual.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(1); + }); + it('gp7-lyrics-null', async () => { await testRoundTripEqual('guitarpro7/lyrics-null.gp', null); }); diff --git a/test/global-hooks.ts b/test/global-hooks.ts new file mode 100644 index 000000000..45f24d046 --- /dev/null +++ b/test/global-hooks.ts @@ -0,0 +1,20 @@ +/** @target web */ +import * as chai from 'chai'; +import { afterAll, beforeEachTest, initializeJestSnapshot } from './mocha.jest-snapshot'; + +export const mochaHooks = { + async beforeAll() { + chai.config.truncateThreshold = 0; // disable truncating + await initializeJestSnapshot(); + }, + + beforeEach: function (done) { + beforeEachTest(this.currentTest!); + done(); + }, + + afterAll(done) { + afterAll(); + done(); + } +} satisfies Mocha.RootHookObject; diff --git a/test/importer/AlphaTexImporter.test.ts b/test/importer/AlphaTexImporter.test.ts index c5226050c..02a31e99d 100644 --- a/test/importer/AlphaTexImporter.test.ts +++ b/test/importer/AlphaTexImporter.test.ts @@ -1,7 +1,7 @@ import { StaveProfile } from '@src/StaveProfile'; import { AlphaTexError, AlphaTexImporter, AlphaTexSymbols } from '@src/importer/AlphaTexImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; -import { Beat, BeatBeamingMode } from '@src/model/Beat'; +import { type Beat, BeatBeamingMode } from '@src/model/Beat'; import { BrushType } from '@src/model/BrushType'; import { Clef } from '@src/model/Clef'; import { CrescendoType } from '@src/model/CrescendoType'; @@ -10,24 +10,11 @@ import { DynamicValue } from '@src/model/DynamicValue'; import { Fingers } from '@src/model/Fingers'; import { GraceType } from '@src/model/GraceType'; import { HarmonicType } from '@src/model/HarmonicType'; -import { - AutomationType, - BendStyle, - BendType, - FermataType, - KeySignature, - KeySignatureType, - NoteAccidentalMode, - Ottavia, - SimileMark, - VibratoType, - WhammyType -} from '@src/model'; -import { Score } from '@src/model/Score'; +import { type Score, ScoreSubElement } from '@src/model/Score'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; -import { Staff } from '@src/model/Staff'; -import { Track } from '@src/model/Track'; +import type { Staff } from '@src/model/Staff'; +import type { Track } from '@src/model/Track'; import { TripletFeel } from '@src/model/TripletFeel'; import { Tuning } from '@src/model/Tuning'; import { HarmonicsEffectInfo } from '@src/rendering/effects/HarmonicsEffectInfo'; @@ -43,10 +30,22 @@ import { Rasgueado } from '@src/model/Rasgueado'; import { Direction } from '@src/model/Direction'; import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@src/model/RenderStylesheet'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import { AutomationType } from '@src/model/Automation'; +import { BendStyle } from '@src/model/BendStyle'; +import { BendType } from '@src/model/BendType'; +import { FermataType } from '@src/model/Fermata'; +import { KeySignature } from '@src/model/KeySignature'; +import { KeySignatureType } from '@src/model/KeySignatureType'; +import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; +import { Ottavia } from '@src/model/Ottavia'; +import { SimileMark } from '@src/model/SimileMark'; +import { VibratoType } from '@src/model/VibratoType'; +import { WhammyType } from '@src/model/WhammyType'; +import { TextAlign } from '@src/platform/ICanvas'; describe('AlphaTexImporterTest', () => { function parseTex(tex: string): Score { - let importer: AlphaTexImporter = new AlphaTexImporter(); + const importer: AlphaTexImporter = new AlphaTexImporter(); importer.initFromString(tex, new Settings()); return importer.readScore(); } @@ -63,7 +62,7 @@ describe('AlphaTexImporterTest', () => { . 0.5.2 1.5.4 3.4.4 | 5.3.8 5.3.8 5.3.8 5.3.8 r.2`; - let score: Score = parseTex(tex); + const score: Score = parseTex(tex); expect(score.title).to.equal('Test'); expect(score.words).to.equal('test'); expect(score.music).to.equal('alphaTab'); @@ -74,43 +73,43 @@ describe('AlphaTexImporterTest', () => { 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); - } + 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); - } + 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', () => { @@ -118,13 +117,13 @@ describe('AlphaTexImporterTest', () => { . 0.5.1`; - let score: Score = parseTex(tex); + const score: Score = parseTex(tex); expect(score.tracks[0].staves[0].tuning.join(',')).to.equal(Tuning.getDefaultTuningFor(6)!.tunings.join(',')); }); it('dead-notes1-issue79', () => { - let tex: string = ':4 x.3'; - let score: Score = parseTex(tex); + 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); @@ -133,8 +132,8 @@ describe('AlphaTexImporterTest', () => { }); it('dead-notes2-issue79', () => { - let tex: string = ':4 3.3{x}'; - let score: Score = parseTex(tex); + 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); @@ -143,8 +142,8 @@ describe('AlphaTexImporterTest', () => { }); it('trill-issue79', () => { - let tex: string = ':4 3.3{tr 5 16}'; - let score: Score = parseTex(tex); + 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); @@ -155,8 +154,8 @@ describe('AlphaTexImporterTest', () => { }); it('tremolo-issue79', () => { - let tex: string = ':4 3.3{tr 5 16}'; - let score: Score = parseTex(tex); + 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); @@ -167,8 +166,8 @@ describe('AlphaTexImporterTest', () => { }); it('tremolo-picking-issue79', () => { - let tex: string = ':4 3.3{tp 16}'; - let score: Score = parseTex(tex); + 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); @@ -178,12 +177,12 @@ describe('AlphaTexImporterTest', () => { }); it('brushes-arpeggio', () => { - let tex: string = ` + 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 `; - let score: Score = parseTex(tex); + 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); @@ -230,8 +229,8 @@ describe('AlphaTexImporterTest', () => { }); it('hamonics-issue79', () => { - let tex: string = ':8 3.3{nh} 3.3{ah} 3.3{th} 3.3{ph} 3.3{sh}'; - let score: Score = parseTex(tex); + 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); @@ -247,20 +246,20 @@ describe('AlphaTexImporterTest', () => { }); it('hamonics-rendering-text-issue79', () => { - let tex: string = ':8 3.3{nh} 3.3{ah} 3.3{th} 3.3{ph} 3.3{sh}'; - let score: Score = parseTex(tex); - let settings: Settings = new Settings(); + const tex: string = ':8 3.3{nh} 3.3{ah} 3.3{th} 3.3{ph} 3.3{sh}'; + const score: Score = parseTex(tex); + const settings: Settings = new Settings(); settings.core.engine = 'svg'; settings.core.enableLazyLoading = false; settings.display.staveProfile = StaveProfile.ScoreTab; - let renderer: ScoreRenderer = new ScoreRenderer(settings); + const renderer: ScoreRenderer = new ScoreRenderer(settings); renderer.width = 970; let svg: string = ''; renderer.partialRenderFinished.on(r => { svg += r.renderResult; }); renderer.renderScore(score, [0]); - let regexTemplate: string = ']+>\\s*{0}\\s*'; + const regexTemplate: string = ']+>\\s*{0}\\s*'; expect( new RegExp(regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Natural))).exec( svg @@ -283,8 +282,8 @@ describe('AlphaTexImporterTest', () => { }); it('grace-issue79', () => { - let tex: string = ':8 3.3{gr} 3.3{gr ob}'; - let score: Score = parseTex(tex); + 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); @@ -293,8 +292,8 @@ describe('AlphaTexImporterTest', () => { }); it('left-hand-finger-single-note', () => { - let tex: string = ':8 3.3{lf 1} 3.3{lf 2} 3.3{lf 3} 3.3{lf 4} 3.3{lf 5}'; - let score: Score = parseTex(tex); + 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); @@ -314,8 +313,8 @@ describe('AlphaTexImporterTest', () => { }); it('right-hand-finger-single-note', () => { - let tex: string = ':8 3.3{rf 1} 3.3{rf 2} 3.3{rf 3} 3.3{rf 4} 3.3{rf 5}'; - let score: Score = parseTex(tex); + 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); @@ -335,8 +334,8 @@ describe('AlphaTexImporterTest', () => { }); it('left-hand-finger-chord', () => { - let tex: string = ':8 (3.1{lf 1} 3.2{lf 2} 3.3{lf 3} 3.4{lf 4} 3.5{lf 5})'; - let score: Score = parseTex(tex); + 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); @@ -357,8 +356,8 @@ describe('AlphaTexImporterTest', () => { }); it('right-hand-finger-chord', () => { - let tex: string = ':8 (3.1{rf 1} 3.2{rf 2} 3.3{rf 3} 3.4{rf 4} 3.5{rf 5})'; - let score: Score = parseTex(tex); + 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); @@ -379,8 +378,8 @@ describe('AlphaTexImporterTest', () => { }); it('unstringed', () => { - let tex: string = '\\tuning piano . c4 c#4 d4 d#4 | c4 db4 d4 eb4'; - let score: Score = parseTex(tex); + 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); @@ -404,8 +403,8 @@ describe('AlphaTexImporterTest', () => { }); it('multi-staff-default-settings', () => { - let tex: string = '1.1 | 1.1 | \\staff 2.1 | 2.1'; - let score: Score = parseTex(tex); + 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); @@ -419,8 +418,8 @@ describe('AlphaTexImporterTest', () => { }); it('multi-staff-default-settings-braces', () => { - let tex: string = '1.1 | 1.1 | \\staff{} 2.1 | 2.1'; - let score: Score = parseTex(tex); + 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); @@ -434,8 +433,8 @@ describe('AlphaTexImporterTest', () => { }); it('single-staff-with-setting', () => { - let tex: string = '\\staff{score} 1.1 | 1.1'; - let score: Score = parseTex(tex); + 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); @@ -445,8 +444,8 @@ describe('AlphaTexImporterTest', () => { }); it('single-staff-with-slash', () => { - let tex: string = '\\staff{slash} 1.1 | 1.1'; - let score: Score = parseTex(tex); + 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); @@ -457,8 +456,8 @@ describe('AlphaTexImporterTest', () => { }); it('single-staff-with-score-and-slash', () => { - let tex: string = '\\staff{score slash} 1.1 | 1.1'; - let score: Score = parseTex(tex); + 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); @@ -472,7 +471,7 @@ describe('AlphaTexImporterTest', () => { 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`; - let score: Score = parseTex(tex); + 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); @@ -490,8 +489,8 @@ describe('AlphaTexImporterTest', () => { }); it('multi-track', () => { - let tex: string = '\\track "First" 1.1 | 1.1 | \\track "Second" 2.2 | 2.2'; - let score: Score = parseTex(tex); + 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); @@ -511,9 +510,9 @@ describe('AlphaTexImporterTest', () => { }); it('multi-track-names', () => { - let tex: string = + 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 '; - let score: Score = parseTex(tex); + 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); @@ -557,18 +556,18 @@ describe('AlphaTexImporterTest', () => { \\track "Second Guitar" 1.2 3.2 0.1 1.1 `; - let score: Score = parseTex(tex); + const score: Score = parseTex(tex); expect(score.tracks.length).to.equal(3); expect(score.masterBars.length).to.equal(1); { - let track1: Track = score.tracks[0]; + 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); { - let staff1: Staff = track1.staves[0]; + 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); @@ -576,7 +575,7 @@ describe('AlphaTexImporterTest', () => { expect(staff1.bars[0].clef).to.equal(Clef.G2); } { - let staff2: Staff = track1.staves[1]; + 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); @@ -585,14 +584,14 @@ describe('AlphaTexImporterTest', () => { } } { - let track2: Track = score.tracks[1]; + 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); { - let staff1: Staff = track2.staves[0]; + 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); @@ -601,14 +600,14 @@ describe('AlphaTexImporterTest', () => { } } { - let track3: Track = score.tracks[2]; + 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); { - let staff1: Staff = track3.staves[0]; + 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); @@ -619,7 +618,7 @@ describe('AlphaTexImporterTest', () => { }); it('multi-track-multi-staff-inconsistent-bars', () => { - let tex: string = ` + const tex: string = ` \\track "Piano" \\staff{score} \\tuning piano \\instrument acousticgrandpiano c4 d4 e4 f4 | @@ -634,18 +633,18 @@ describe('AlphaTexImporterTest', () => { \\track "Second Guitar" 1.2 3.2 0.1 1.1 `; - let score: Score = parseTex(tex); + const score: Score = parseTex(tex); expect(score.tracks.length).to.equal(3); expect(score.masterBars.length).to.equal(3); { - let track1: Track = score.tracks[0]; + 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); { - let staff1: Staff = track1.staves[0]; + 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); @@ -656,7 +655,7 @@ describe('AlphaTexImporterTest', () => { expect(staff1.bars[0].clef).to.equal(Clef.G2); } { - let staff2: Staff = track1.staves[1]; + 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); @@ -668,14 +667,14 @@ describe('AlphaTexImporterTest', () => { } } { - let track2: Track = score.tracks[1]; + 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); { - let staff1: Staff = track2.staves[0]; + 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); @@ -687,14 +686,14 @@ describe('AlphaTexImporterTest', () => { } } { - let track3: Track = score.tracks[2]; + 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); { - let staff1: Staff = track3.staves[0]; + 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); @@ -708,8 +707,8 @@ describe('AlphaTexImporterTest', () => { }); it('slides', () => { - let 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}'; - let score: Score = parseTex(tex); + 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( @@ -741,8 +740,8 @@ describe('AlphaTexImporterTest', () => { }); it('section', () => { - let tex: string = '\\section Intro 1.1 | 1.1 | \\section "Chorus 01" 1.1 | \\section S Solo'; - let score: Score = parseTex(tex); + 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); @@ -758,70 +757,95 @@ describe('AlphaTexImporterTest', () => { }); it('key-signature', () => { - let tex: string = `:1 3.3 | \\ks C 3.3 | \\ks Cmajor 3.3 | \\ks Aminor 3.3 | + 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`; - let score: Score = parseTex(tex); - expect(score.masterBars[0].keySignature).to.equal(KeySignature.C); - expect(score.masterBars[0].keySignatureType).to.equal(KeySignatureType.Major); - - expect(score.masterBars[1].keySignature).to.equal(KeySignature.C); - expect(score.masterBars[1].keySignatureType).to.equal(KeySignatureType.Major); - - expect(score.masterBars[2].keySignature).to.equal(KeySignature.C); - expect(score.masterBars[2].keySignatureType).to.equal(KeySignatureType.Major); - - expect(score.masterBars[3].keySignature).to.equal(KeySignature.C); - expect(score.masterBars[3].keySignatureType).to.equal(KeySignatureType.Minor); - - expect(score.masterBars[4].keySignature).to.equal(KeySignature.F); - expect(score.masterBars[4].keySignatureType).to.equal(KeySignatureType.Major); - - expect(score.masterBars[5].keySignature).to.equal(KeySignature.Bb); - expect(score.masterBars[5].keySignatureType).to.equal(KeySignatureType.Major); - - expect(score.masterBars[6].keySignature).to.equal(KeySignature.Eb); - expect(score.masterBars[6].keySignatureType).to.equal(KeySignatureType.Minor); - - expect(score.masterBars[7].keySignature).to.equal(KeySignature.Ab); - expect(score.masterBars[7].keySignatureType).to.equal(KeySignatureType.Major); - - expect(score.masterBars[8].keySignature).to.equal(KeySignature.Db); - expect(score.masterBars[8].keySignatureType).to.equal(KeySignatureType.Major); - - expect(score.masterBars[9].keySignature).to.equal(KeySignature.Gb); - expect(score.masterBars[9].keySignatureType).to.equal(KeySignatureType.Minor); - - expect(score.masterBars[10].keySignature).to.equal(KeySignature.G); - expect(score.masterBars[10].keySignatureType).to.equal(KeySignatureType.Major); - - expect(score.masterBars[11].keySignature).to.equal(KeySignature.D); - expect(score.masterBars[11].keySignatureType).to.equal(KeySignatureType.Major); - - expect(score.masterBars[12].keySignature).to.equal(KeySignature.A); - expect(score.masterBars[12].keySignatureType).to.equal(KeySignatureType.Minor); - - expect(score.masterBars[13].keySignature).to.equal(KeySignature.E); - expect(score.masterBars[13].keySignatureType).to.equal(KeySignatureType.Major); + 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]); + } + }); - expect(score.masterBars[14].keySignature).to.equal(KeySignature.B); - expect(score.masterBars[14].keySignatureType).to.equal(KeySignatureType.Major); + 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]); + expect(bars[i].keySignatureType).to.equal(expected[i][1]); + } - expect(score.masterBars[15].keySignature).to.equal(KeySignature.FSharp); - expect(score.masterBars[15].keySignatureType).to.equal(KeySignatureType.Minor); + 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}`); + } }); it('pop-slap-tap', () => { - let tex: string = '3.3{p} 3.3{s} 3.3{tt} r'; - let score: Score = parseTex(tex); + 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); }); it('triplet-feel-numeric', () => { - let tex: string = '\\tf 0 | \\tf 1 | \\tf 2 | \\tf 3 | \\tf 4 | \\tf 5 | \\tf 6'; - let score: Score = parseTex(tex); + 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); @@ -832,9 +856,9 @@ describe('AlphaTexImporterTest', () => { }); it('triplet-feel-long-names', () => { - let tex: string = + const tex: string = '\\tf none | \\tf triplet-16th | \\tf triplet-8th | \\tf dotted-16th | \\tf dotted-8th | \\tf scottish-16th | \\tf scottish-8th'; - let score: Score = parseTex(tex); + 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); @@ -845,8 +869,8 @@ describe('AlphaTexImporterTest', () => { }); it('triplet-feel-short-names', () => { - let tex: string = '\\tf no | \\tf t16 | \\tf t8 | \\tf d16 | \\tf d8 | \\tf s16 | \\tf s8'; - let score: Score = parseTex(tex); + 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); @@ -857,8 +881,8 @@ describe('AlphaTexImporterTest', () => { }); it('triplet-feel-multi-bar', () => { - let tex: string = '\\tf t16 | | | \\tf t8 | | | \\tf no | | '; - let score: Score = parseTex(tex); + const tex: string = '\\tf t16 | | | \\tf t8 | | | \\tf no | | '; + 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); @@ -871,10 +895,10 @@ describe('AlphaTexImporterTest', () => { }); it('tuplet-repeat', () => { - let tex: string = ':8 5.3{tu 3}*3'; - let score: Score = parseTex(tex); - let durations: Duration[] = [Duration.Eighth, Duration.Eighth, Duration.Eighth]; - let tuplets = [3, 3, 3]; + 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) { @@ -891,10 +915,10 @@ describe('AlphaTexImporterTest', () => { }); it('tuplet-custom', () => { - let tex: string = ':8 5.3{tu 5 2}*5'; - let score: Score = parseTex(tex); - let tupletNumerators = [5, 5, 5, 5, 5]; - let tupletDenominators = [2, 2, 2, 2, 2]; + 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]; @@ -907,16 +931,16 @@ describe('AlphaTexImporterTest', () => { }); it('simple-anacrusis', () => { - let tex: string = '\\ac 3.3 3.3 | 1.1 2.1 3.1 4.1'; - let score: Score = parseTex(tex); + 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); }); it('multi-bar-anacrusis', () => { - let tex: string = '\\ac 3.3 3.3 | \\ac 3.3 3.3 | 1.1 2.1 3.1 4.1'; - let score: Score = parseTex(tex); + 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); @@ -925,8 +949,8 @@ describe('AlphaTexImporterTest', () => { }); it('random-anacrusis', () => { - let 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'; - let score: Score = parseTex(tex); + 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); @@ -938,9 +962,9 @@ describe('AlphaTexImporterTest', () => { }); it('repeat', () => { - let tex: string = + 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 |'; - let score: Score = parseTex(tex); + 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); @@ -952,8 +976,8 @@ describe('AlphaTexImporterTest', () => { }); it('alternate-endings', () => { - let 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 |'; - let score: Score = parseTex(tex); + 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); @@ -966,14 +990,14 @@ describe('AlphaTexImporterTest', () => { }); it('random-alternate-endings', () => { - let tex: string = ` + 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 | `; - let score: Score = parseTex(tex); + 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); @@ -995,7 +1019,7 @@ describe('AlphaTexImporterTest', () => { }); it('default-transposition-on-instruments', () => { - let tex: string = ` + const tex: string = ` \\track "Piano with Grand Staff" "pno." \\staff{score} \\tuning piano \\instrument acousticgrandpiano c4 d4 e4 f4 | @@ -1005,7 +1029,7 @@ describe('AlphaTexImporterTest', () => { \\staff{tabs} \\instrument acousticguitarsteel \\capo 5 1.2 3.2 0.1 1.1 `; - let score: Score = parseTex(tex); + 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); @@ -1016,8 +1040,8 @@ describe('AlphaTexImporterTest', () => { }); it('dynamics', () => { - let 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}'; - let score: Score = parseTex(tex); + 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); @@ -1029,8 +1053,8 @@ describe('AlphaTexImporterTest', () => { }); it('dynamics-auto', () => { - let tex: string = '1.1.4{dy ppp} 1.1 1.1{dy mp} 1.1'; - let score: Score = parseTex(tex); + 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); @@ -1038,24 +1062,24 @@ describe('AlphaTexImporterTest', () => { }); it('dynamics-auto-reset-on-track', () => { - let tex: string = '1.1.4{dy ppp} 1.1 \\track "Second" 1.1.4'; - let score: Score = parseTex(tex); + 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); }); it('dynamics-auto-reset-on-staff', () => { - let tex: string = '1.1.4{dy ppp} 1.1 \\staff 1.1.4'; - let score: Score = parseTex(tex); + 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); }); it('crescendo', () => { - let tex: string = '1.1.4{dec} 1.1{dec} 1.1{cre} 1.1{cre}'; - let score: Score = parseTex(tex); + 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); @@ -1063,8 +1087,8 @@ describe('AlphaTexImporterTest', () => { }); it('left-hand-tapping', () => { - let tex: string = ':4 1.1{lht} 1.1 1.1{lht} 1.1'; - let score: Score = parseTex(tex); + 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); @@ -1159,43 +1183,43 @@ describe('AlphaTexImporterTest', () => { 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); - } + 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); - } + 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); } }); @@ -1242,10 +1266,14 @@ describe('AlphaTexImporterTest', () => { `); 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(30); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(31); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(33); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(34); + 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); }); it('percussion-custom-articulation', () => { @@ -1260,10 +1288,14 @@ describe('AlphaTexImporterTest', () => { `); 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(30); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(31); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(33); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(34); + 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); }); it('percussion-default-articulations', () => { @@ -1275,10 +1307,14 @@ describe('AlphaTexImporterTest', () => { `); 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(30); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(31); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(33); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(34); + 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); }); it('percussion-default-articulations-short', () => { @@ -1290,10 +1326,14 @@ describe('AlphaTexImporterTest', () => { `); 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(30); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(31); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(33); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(34); + 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); }); it('beat-tempo-change', () => { @@ -1364,26 +1404,26 @@ describe('AlphaTexImporterTest', () => { }); it('dead-slap', () => { - let score = parseTex('r { ds }'); + 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; }); it('golpe', () => { - let score = parseTex('3.3 { glpf } 3.3 { glpt }'); + 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); }); it('fade', () => { - let score = parseTex('3.3 { f } 3.3 { fo } 3.3 { vs } '); + 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); }); it('barre', () => { - let score = parseTex('3.3 { barre 5 } 3.3 { barre 14 half }'); + 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); @@ -1392,7 +1432,7 @@ describe('AlphaTexImporterTest', () => { }); it('ornaments', () => { - let score = parseTex('3.3 { turn } 3.3 { iturn } 3.3 { umordent } 3.3 { lmordent }'); + 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 @@ -1406,7 +1446,7 @@ describe('AlphaTexImporterTest', () => { }); it('rasgueado', () => { - let score = parseTex('3.3 { rasg mi } 3.3 { rasg pmptriplet } 3.3 { rasg amianapaest }'); + 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); @@ -1416,7 +1456,7 @@ describe('AlphaTexImporterTest', () => { }); it('directions', () => { - let score = parseTex('. \\jump Segno | | \\jump DaCapoAlCoda \\jump Coda \\jump SegnoSegno '); + 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); @@ -1429,7 +1469,7 @@ describe('AlphaTexImporterTest', () => { }); it('multi-voice-full', () => { - let score = parseTex(` + const score = parseTex(` \\track "Piano" \\staff{score} \\tuning piano \\instrument acousticgrandpiano \\voice @@ -1446,7 +1486,7 @@ describe('AlphaTexImporterTest', () => { }); it('multi-voice-simple-all-voices', () => { - let score = parseTex(` + const score = parseTex(` \\voice c4 d4 e4 f4 | c4 d4 e4 f4 \\voice @@ -1461,7 +1501,7 @@ describe('AlphaTexImporterTest', () => { }); it('multi-voice-simple-skip-initial', () => { - let score = parseTex(` + const score = parseTex(` c4 d4 e4 f4 | c4 d4 e4 f4 \\voice c3 d3 e3 f3 | c3 d3 e3 f3 @@ -1475,14 +1515,14 @@ describe('AlphaTexImporterTest', () => { }); it('standard-notation-line-count', () => { - let score = parseTex(` + const score = parseTex(` \\staff { score 3 } `); expect(score.tracks[0].staves[0].standardNotationLineCount).to.equal(3); }); it('song-metadata', () => { - let score = parseTex(` + const score = parseTex(` \\title "Title\\tTitle" \\instructions "Line1\nLine2" . @@ -1492,7 +1532,7 @@ describe('AlphaTexImporterTest', () => { }); it('tempo-label', () => { - let score = parseTex(` + const score = parseTex(` \\tempo 80 "Label" . `); @@ -1501,7 +1541,7 @@ describe('AlphaTexImporterTest', () => { }); it('transpose', () => { - let score = parseTex(` + const score = parseTex(` \\staff \\displaytranspose 12 \\transpose 6 @@ -1512,7 +1552,7 @@ describe('AlphaTexImporterTest', () => { }); it('beat-vibrato', () => { - let score = parseTex(` + 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); @@ -1520,7 +1560,7 @@ describe('AlphaTexImporterTest', () => { }); it('whammy', () => { - let score = parseTex(` + const score = parseTex(` 3.3.4{ tb dive (0 -12.5) } | 3.3.4{ tb dive gradual (0 -12.5) } | `); @@ -1537,7 +1577,7 @@ describe('AlphaTexImporterTest', () => { }); it('beat-ottava', () => { - let score = parseTex(` + 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); @@ -1545,7 +1585,7 @@ describe('AlphaTexImporterTest', () => { }); it('beat-text', () => { - let score = parseTex(` + 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'); @@ -1553,7 +1593,7 @@ describe('AlphaTexImporterTest', () => { }); it('legato-origin', () => { - let score = parseTex(` + 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; @@ -1561,7 +1601,7 @@ describe('AlphaTexImporterTest', () => { }); it('instrument-change', () => { - let score = parseTex(` + const score = parseTex(` \\instrument acousticgrandpiano G4 G4 G4 { instrument brightacousticpiano } `); @@ -1574,7 +1614,7 @@ describe('AlphaTexImporterTest', () => { }); it('beat-fermata', () => { - let score = parseTex(` + 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; @@ -1583,7 +1623,7 @@ describe('AlphaTexImporterTest', () => { }); it('bend-type', () => { - let score = parseTex(` + 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); @@ -1592,7 +1632,7 @@ describe('AlphaTexImporterTest', () => { }); it('harmonic-values', () => { - let score = parseTex(` + 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( @@ -1640,7 +1680,7 @@ describe('AlphaTexImporterTest', () => { }); it('time-signature-commons', () => { - let score = parseTex(` + const score = parseTex(` \\ts common `); expect(score.masterBars[0].timeSignatureNumerator).to.equal(4); @@ -1649,21 +1689,21 @@ describe('AlphaTexImporterTest', () => { }); it('clef-ottava', () => { - let score = parseTex(` + const score = parseTex(` \\ottava 15ma `); expect(score.tracks[0].staves[0].bars[0].clefOttava).to.equal(Ottavia._15ma); }); it('simile-mark', () => { - let score = parseTex(` + const score = parseTex(` \\simile simple `); expect(score.tracks[0].staves[0].bars[0].simileMark).to.equal(SimileMark.Simple); }); it('tempo-automation-text', () => { - let score = parseTex(` + const score = parseTex(` \\tempo 100 T1 . 3.3.4 * 4 | \\tempo 80 T2 4.3.4*4 @@ -1677,13 +1717,13 @@ describe('AlphaTexImporterTest', () => { }); it('double-bar', () => { - let tex: string = '3.3 3.3 3.3 3.3 | \\db 1.1 2.1 3.1 4.1'; - let score: Score = parseTex(tex); + 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); }); it('score-options', () => { - let score = parseTex(` + const score = parseTex(` \\defaultSystemsLayout 5 \\systemsLayout 3 2 3 \\hideDynamics @@ -1715,7 +1755,7 @@ describe('AlphaTexImporterTest', () => { }); it('bar-sizing', () => { - let score = parseTex(` + const score = parseTex(` 3.3.4 | \\scale 0.5 3.3.4 | \\width 300 3.3.4 `); @@ -1724,7 +1764,7 @@ describe('AlphaTexImporterTest', () => { }); it('track-properties', () => { - let score = parseTex(` + const score = parseTex(` \\track "First" { color "#FF0000" defaultSystemsLayout 6 @@ -1749,7 +1789,7 @@ describe('AlphaTexImporterTest', () => { }); it('beat-beam', () => { - let score = parseTex(` + const score = parseTex(` :8 3.3{ beam invert } 3.3 | 3.3{ beam up } 3.3 | 3.3{ beam down } 3.3 | @@ -1773,7 +1813,7 @@ describe('AlphaTexImporterTest', () => { }); it('note-show-string', () => { - let score = parseTex(` + const score = parseTex(` :8 3.3{ string } `); @@ -1781,7 +1821,7 @@ describe('AlphaTexImporterTest', () => { }); it('note-hide', () => { - let score = parseTex(` + const score = parseTex(` :8 3.3{ hide } `); @@ -1789,7 +1829,7 @@ describe('AlphaTexImporterTest', () => { }); it('note-slur', () => { - let score = parseTex(` + const score = parseTex(` :8 (3.3{ slur s1 } 3.4 3.5) (10.3 {slur s1} 17.4 15.5) `); @@ -1812,7 +1852,7 @@ describe('AlphaTexImporterTest', () => { }); it('clefs', () => { - let score = parseTex(` + const score = parseTex(` \\clef C4 \\ottava 15ma C4 | C4 `); expect(score.tracks[0].staves[0].bars[0].clef).to.equal(Clef.C4); @@ -1820,4 +1860,118 @@ describe('AlphaTexImporterTest', () => { expect(score.tracks[0].staves[0].bars[1].clef).to.equal(Clef.C4); expect(score.tracks[0].staves[0].bars[1].clefOttava).to.equal(Ottavia._15ma); }); + + 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; + }); + + 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 + ); + }); + + 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(); + }); }); diff --git a/test/importer/BinaryStylesheet.test.ts b/test/importer/BinaryStylesheet.test.ts index 38c0f2c2d..7fda295e0 100644 --- a/test/importer/BinaryStylesheet.test.ts +++ b/test/importer/BinaryStylesheet.test.ts @@ -1,12 +1,12 @@ import { BinaryStylesheet } from '@src/importer/BinaryStylesheet'; -import { Color } from '@src/model/Color'; +import type { Color } from '@src/model/Color'; import { TestPlatform } from '@test/TestPlatform'; import { expect } from 'chai'; describe('BinaryStylesheetParserTest', () => { it('testRead', async () => { const data = await TestPlatform.loadFile('test-data/guitarpro7/BinaryStylesheet'); - let stylesheet: BinaryStylesheet = new BinaryStylesheet(data); + const stylesheet: BinaryStylesheet = new BinaryStylesheet(data); expect(stylesheet.raw.has('Global/chordNameStyle')).to.be.true; expect(stylesheet.raw.get('Global/chordNameStyle')).to.equal(2); diff --git a/test/importer/Gp3Importer.test.ts b/test/importer/Gp3Importer.test.ts index a78506204..ad0eee4ac 100644 --- a/test/importer/Gp3Importer.test.ts +++ b/test/importer/Gp3Importer.test.ts @@ -1,7 +1,7 @@ import { AutomationType } from '@src/model/Automation'; import { BrushType } from '@src/model/BrushType'; import { DynamicValue } from '@src/model/DynamicValue'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { SlideOutType } from '@src/model/SlideOutType'; import { GpImporterTestHelper } from '@test/importer/GpImporterTestHelper'; import { HarmonicType } from '@src/model/HarmonicType'; @@ -10,7 +10,7 @@ import { expect } from 'chai'; describe('Gp3ImporterTest', () => { it('score-info', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/score-info.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.title).to.equal('Title'); expect(score.subTitle).to.equal('Subtitle'); expect(score.artist).to.equal('Artist'); @@ -29,25 +29,25 @@ describe('Gp3ImporterTest', () => { it('notes', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/notes.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkNotes(score); }); it('time-signatures', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/time-signatures.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTimeSignatures(score); }); it('dead', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/dead.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkDead(score); }); it('accentuations', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/accentuations.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isGhost).to.be.equal(true); // it seems accentuation is handled as Forte Fortissimo expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].dynamics).to.equal(DynamicValue.FFF); @@ -56,29 +56,39 @@ describe('Gp3ImporterTest', () => { it('harmonics', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/harmonics.gp3'); - let score: Score = reader.readScore(); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].harmonicType).to.be.equal(HarmonicType.Natural); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].harmonicType).to.be.equal(HarmonicType.Artificial); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].harmonicType).to.be.equal(HarmonicType.Artificial); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].harmonicType).to.be.equal(HarmonicType.Artificial); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].harmonicType).to.be.equal(HarmonicType.Artificial); + const score: Score = reader.readScore(); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].harmonicType).to.be.equal( + HarmonicType.Natural + ); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].harmonicType).to.be.equal( + HarmonicType.Artificial + ); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].harmonicType).to.be.equal( + HarmonicType.Artificial + ); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].harmonicType).to.be.equal( + HarmonicType.Artificial + ); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].harmonicType).to.be.equal( + HarmonicType.Artificial + ); }); it('hammer', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/hammer.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkHammer(score); }); it('bends', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/bends.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkBend(score); }); it('slides', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/slides.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].getNoteOnString(5)!.slideOutType).to.equal( SlideOutType.Shift ); @@ -90,13 +100,13 @@ describe('Gp3ImporterTest', () => { it('vibrato', async () => { // TODO: Check why this vibrato is not recognized const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/vibrato.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkVibrato(score, false); }); it('other-effects', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/other-effects.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].tap).to.be.equal(true); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].slap).to.be.equal(true); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].pop).to.be.equal(true); @@ -105,12 +115,10 @@ describe('Gp3ImporterTest', () => { expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].chord!.name).to.equal('C'); expect(score.tracks[0].staves[0].bars[3].voices[0].beats[1].text).to.equal('Text'); expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].getAutomation(AutomationType.Tempo)).to.be.ok; - expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].getAutomation(AutomationType.Tempo)!.value).to.equal( - 120 - ); expect( - score.tracks[0].staves[0].bars[4].voices[0].beats[0].getAutomation(AutomationType.Instrument) - ).to.be.ok; + score.tracks[0].staves[0].bars[4].voices[0].beats[0].getAutomation(AutomationType.Tempo)!.value + ).to.equal(120); + expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].getAutomation(AutomationType.Instrument)).to.be.ok; expect( score.tracks[0].staves[0].bars[4].voices[0].beats[0].getAutomation(AutomationType.Instrument)!.value ).to.equal(25); @@ -118,20 +126,20 @@ describe('Gp3ImporterTest', () => { it('strokes', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/strokes.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); 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[1].brushType).to.equal(BrushType.BrushUp); }); it('tuplets', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/tuplets.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTuplets(score); }); it('ranges', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/ranges.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].isLetRing).to.be.equal(true); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].isLetRing).to.be.equal(true); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].isLetRing).to.be.equal(true); @@ -140,13 +148,13 @@ describe('Gp3ImporterTest', () => { it('effects', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/effects.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkEffects(score); }); it('strings', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/strings.gp3'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkStrings(score); }); }); diff --git a/test/importer/Gp4Importer.test.ts b/test/importer/Gp4Importer.test.ts index cfe285caf..91013be27 100644 --- a/test/importer/Gp4Importer.test.ts +++ b/test/importer/Gp4Importer.test.ts @@ -1,11 +1,11 @@ -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { GpImporterTestHelper } from '@test/importer/GpImporterTestHelper'; import { expect } from 'chai'; describe('Gp4ImporterTest', () => { it('score-info', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/score-info.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.title).to.equal('Title'); expect(score.subTitle).to.equal('Subtitle'); expect(score.artist).to.equal('Artist'); @@ -24,121 +24,121 @@ describe('Gp4ImporterTest', () => { it('notes', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/notes.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkNotes(score); }); it('time-signatures', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/time-signatures.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTimeSignatures(score); }); it('dead', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/dead.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkDead(score); }); it('grace', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/grace.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkGrace(score); }); it('accentuations', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/accentuations.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkAccentuations(score, false); }); it('harmonics', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/harmonics.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkHarmonics(score); }); it('hammer', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/hammer.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkHammer(score); }); it('bend', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/bends.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkBend(score); }); it('tremolo', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/tremolo.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTremolo(score); }); it('slides', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/slides.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkSlides(score); }); it('vibrato', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/vibrato.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkVibrato(score, true); }); it('trills', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/trills.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTrills(score); }); it('otherEffects', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/other-effects.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkOtherEffects(score, false); }); it('fingering', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/fingering.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkFingering(score); }); it('stroke', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/strokes.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkStroke(score); }); it('tuplets', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/tuplets.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTuplets(score); }); it('ranges', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/ranges.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkRanges(score); }); it('effects', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/effects.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkEffects(score); }); it('strings', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/strings.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkStrings(score); }); it('colors', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/colors.gp4'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkColors(score); }); }); diff --git a/test/importer/Gp5Importer.test.ts b/test/importer/Gp5Importer.test.ts index 50b5ac7ef..425e534a4 100644 --- a/test/importer/Gp5Importer.test.ts +++ b/test/importer/Gp5Importer.test.ts @@ -1,9 +1,10 @@ import { Settings } from '@src/Settings'; -import { Ottavia } from '@src/model'; -import { Beat, BeatBeamingMode } from '@src/model/Beat'; +import { type Beat, BeatBeamingMode } from '@src/model/Beat'; import { Direction } from '@src/model/Direction'; -import { Score } from '@src/model/Score'; +import { Ottavia } from '@src/model/Ottavia'; +import { type Score, ScoreSubElement } from '@src/model/Score'; import { WahPedal } from '@src/model/WahPedal'; +import { TextAlign } from '@src/platform/ICanvas'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { GpImporterTestHelper } from '@test/importer/GpImporterTestHelper'; import { expect } from 'chai'; @@ -11,7 +12,7 @@ import { expect } from 'chai'; describe('Gp5ImporterTest', () => { it('score-info', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/score-info.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.title).to.equal('Title'); expect(score.subTitle).to.equal('Subtitle'); expect(score.artist).to.equal('Artist'); @@ -30,109 +31,109 @@ describe('Gp5ImporterTest', () => { it('notes', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/notes.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkNotes(score); }); it('time-signatures', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/time-signatures.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTimeSignatures(score); }); it('dead', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/dead.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkDead(score); }); it('grace', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/grace.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkGrace(score); }); it('accentuations', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/accentuations.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkAccentuations(score, true); }); it('harmonics', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/harmonics.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkHarmonics(score); }); it('hammer', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/hammer.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkHammer(score); }); it('bend', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/bends.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkBend(score); }); it('tremolo', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/tremolo.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTremolo(score); }); it('slides', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/slides.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkSlides(score); }); it('vibrato', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/vibrato.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkVibrato(score, true); }); it('trills', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/trills.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTrills(score); }); it('other-effects', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/other-effects.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkOtherEffects(score, false); }); it('fingering', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/fingering.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkFingering(score); }); it('stroke', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/strokes.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkStroke(score); }); it('tuplets', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/tuplets.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTuplets(score); }); it('ranges', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/ranges.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkRanges(score); }); it('effects', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/effects.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkEffects(score); }); @@ -144,25 +145,25 @@ describe('Gp5ImporterTest', () => { it('strings', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/strings.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkStrings(score); }); it('key-signatures', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/key-signatures.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkKeySignatures(score); }); it('chords', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/chords.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkChords(score); }); it('colors', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/colors.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkColors(score); }); @@ -179,7 +180,7 @@ describe('Gp5ImporterTest', () => { it('canon', async () => { const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/canon.gp5'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.title).to.equal('Canon Rock'); expect(score.subTitle).to.equal(''); expect(score.artist).to.equal('JerryC'); @@ -206,7 +207,7 @@ describe('Gp5ImporterTest', () => { const settings = new Settings(); settings.importer.beatTextAsLyrics = true; const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/beat-text-lyrics.gp5', settings); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); const expectedChunks: string[] = [ '', @@ -444,7 +445,9 @@ describe('Gp5ImporterTest', () => { // invert to down expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].beamingMode).to.equal(BeatBeamingMode.Auto); expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].invertBeamDirection).to.be.false; - expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].preferredBeamDirection).to.equal(BeamDirection.Down); + expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].preferredBeamDirection).to.equal( + BeamDirection.Down + ); // invert to up expect(score.tracks[0].staves[0].bars[5].voices[0].beats[0].beamingMode).to.equal(BeatBeamingMode.Auto); @@ -452,7 +455,6 @@ describe('Gp5ImporterTest', () => { expect(score.tracks[0].staves[0].bars[5].voices[0].beats[0].preferredBeamDirection).to.equal(BeamDirection.Up); }); - it('ottavia', async () => { const score = (await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/ottavia.gp5')).readScore(); @@ -462,7 +464,7 @@ describe('Gp5ImporterTest', () => { expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].ottava).to.equal(Ottavia._15mb); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].ottava).to.equal(Ottavia.Regular); }); - + it('wah-wah', async () => { const score = (await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/wah-wah.gp5')).readScore(); @@ -473,4 +475,63 @@ describe('Gp5ImporterTest', () => { expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].wahPedal).to.equal(WahPedal.Closed); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].wahPedal).to.equal(WahPedal.None); }); + + it('header-footer', async () => { + const score = (await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/header-footer.gp5')).readScore(); + + 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.false; + expect(score.style!.headerAndFooter.get(ScoreSubElement.Title)!.textAlign).to.equal(TextAlign.Center); + + 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.false; + expect(score.style!.headerAndFooter.get(ScoreSubElement.Artist)!.textAlign).to.equal(TextAlign.Center); + + 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.Center); + + 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.false; + expect(score.style!.headerAndFooter.get(ScoreSubElement.Words)!.textAlign).to.equal(TextAlign.Left); + + 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: %WORDSMUSIC%' + ); + expect(score.style!.headerAndFooter.get(ScoreSubElement.WordsAndMusic)!.isVisible).to.be.false; + expect(score.style!.headerAndFooter.get(ScoreSubElement.WordsAndMusic)!.textAlign).to.equal(TextAlign.Right); + + expect(score.style!.headerAndFooter.has(ScoreSubElement.Transcriber)).to.be.false; + + 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.Center); + + 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.Center + ); + }); }); diff --git a/test/importer/Gp7Importer.test.ts b/test/importer/Gp7Importer.test.ts index d650faebd..14defa4c2 100644 --- a/test/importer/Gp7Importer.test.ts +++ b/test/importer/Gp7Importer.test.ts @@ -1,14 +1,14 @@ import { MidiUtils } from '@src/midi/MidiUtils'; import { Gp7To8Importer } from '@src/importer/Gp7To8Importer'; import { ByteBuffer } from '@src/io/ByteBuffer'; -import { Beat, BeatBeamingMode } from '@src/model/Beat'; +import { type Beat, BeatBeamingMode } from '@src/model/Beat'; import { BendType } from '@src/model/BendType'; import { FermataType } from '@src/model/Fermata'; import { GraceType } from '@src/model/GraceType'; -import { MasterBar } from '@src/model/MasterBar'; -import { Note } from '@src/model/Note'; +import type { MasterBar } from '@src/model/MasterBar'; +import type { Note } from '@src/model/Note'; import { Ottavia } from '@src/model/Ottavia'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { SimileMark } from '@src/model/SimileMark'; import { SlideOutType } from '@src/model/SlideOutType'; import { VibratoType } from '@src/model/VibratoType'; @@ -22,19 +22,19 @@ import { BeamDirection } from '@src/rendering/utils/BeamDirection'; describe('Gp7ImporterTest', () => { async function prepareImporterWithFile(name: string): Promise { - const data = await TestPlatform.loadFile('test-data/' + name); + const data = await TestPlatform.loadFile(`test-data/${name}`); return prepareImporterWithBytes(data); } function prepareImporterWithBytes(buffer: Uint8Array) { - let readerBase: Gp7To8Importer = new Gp7To8Importer(); + const readerBase: Gp7To8Importer = new Gp7To8Importer(); readerBase.init(ByteBuffer.fromBuffer(buffer), new Settings()); return readerBase; } it('score-info', async () => { const reader = await prepareImporterWithFile('guitarpro7/score-info.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.title).to.equal('Title'); expect(score.subTitle).to.equal('Subtitle'); expect(score.artist).to.equal('Artist'); @@ -53,49 +53,49 @@ describe('Gp7ImporterTest', () => { it('notes', async () => { const reader = await prepareImporterWithFile('guitarpro7/notes.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkNotes(score); }); it('time-signatures', async () => { const reader = await prepareImporterWithFile('guitarpro7/time-signatures.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTimeSignatures(score); }); it('dead', async () => { const reader = await prepareImporterWithFile('guitarpro7/dead.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkDead(score); }); it('grace', async () => { const reader = await prepareImporterWithFile('guitarpro7/grace.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkGrace(score); }); it('accentuations', async () => { const reader = await prepareImporterWithFile('guitarpro7/accentuations.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkAccentuations(score, true); }); it('harmonics', async () => { const reader = await prepareImporterWithFile('guitarpro7/harmonics.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkHarmonics(score); }); it('hammer', async () => { const reader = await prepareImporterWithFile('guitarpro7/hammer.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkHammer(score); }); it('bend', async () => { const reader = await prepareImporterWithFile('guitarpro7/bends.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); 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].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); @@ -122,7 +122,7 @@ describe('Gp7ImporterTest', () => { it('bends-advanced', async () => { const reader = await prepareImporterWithFile('guitarpro7/bends-advanced.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); // Simple Standalone Bends @@ -419,7 +419,7 @@ describe('Gp7ImporterTest', () => { it('whammy-advanced', async () => { const reader = await prepareImporterWithFile('guitarpro7/whammy-advanced.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); // Bar 1 let beat: Beat = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; @@ -577,7 +577,7 @@ describe('Gp7ImporterTest', () => { it('tremolo', async () => { const reader = await prepareImporterWithFile('guitarpro7/tremolo.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints!.length).to.equal(3); @@ -656,55 +656,55 @@ describe('Gp7ImporterTest', () => { it('slides', async () => { const reader = await prepareImporterWithFile('guitarpro7/slides.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkSlides(score); }); it('vibrato', async () => { const reader = await prepareImporterWithFile('guitarpro7/vibrato.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkVibrato(score, true); }); it('trills', async () => { const reader = await prepareImporterWithFile('guitarpro7/trills.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTrills(score); }); it('other-effects', async () => { const reader = await prepareImporterWithFile('guitarpro7/other-effects.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkOtherEffects(score, true); }); it('fingering', async () => { const reader = await prepareImporterWithFile('guitarpro7/fingering.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkFingering(score); }); it('stroke', async () => { const reader = await prepareImporterWithFile('guitarpro7/strokes.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkStroke(score); }); it('tuplets', async () => { const reader = await prepareImporterWithFile('guitarpro7/tuplets.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTuplets(score); }); it('ranges', async () => { const reader = await prepareImporterWithFile('guitarpro7/ranges.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkRanges(score); }); it('effects', async () => { const reader = await prepareImporterWithFile('guitarpro7/effects.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkEffects(score); }); @@ -716,31 +716,31 @@ describe('Gp7ImporterTest', () => { it('strings', async () => { const reader = await prepareImporterWithFile('guitarpro7/strings.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkStrings(score); }); it('key-signatures', async () => { const reader = await prepareImporterWithFile('guitarpro7/key-signatures.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkKeySignatures(score); }); it('chords', async () => { const reader = await prepareImporterWithFile('guitarpro7/chords.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkChords(score); }); it('colors', async () => { const reader = await prepareImporterWithFile('guitarpro7/colors.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkColors(score); }); it('tremolo-vibrato', async () => { const reader = await prepareImporterWithFile('guitarpro7/tremolo-vibrato.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); 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[1].voices[0].beats[0].notes[0].vibrato).to.equal(VibratoType.Wide); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[1].vibrato).to.equal(VibratoType.Slight); @@ -751,7 +751,7 @@ describe('Gp7ImporterTest', () => { it('ottavia', async () => { const reader = await prepareImporterWithFile('guitarpro7/ottavia.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].clefOttava).to.equal(Ottavia._8va); expect(score.tracks[0].staves[0].bars[1].clefOttava).to.equal(Ottavia._8vb); expect(score.tracks[0].staves[0].bars[2].clefOttava).to.equal(Ottavia._15ma); @@ -764,7 +764,7 @@ describe('Gp7ImporterTest', () => { it('simile-mark', async () => { const reader = await prepareImporterWithFile('guitarpro7/simile-mark.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].simileMark).to.equal(SimileMark.None); expect(score.tracks[0].staves[0].bars[1].simileMark).to.equal(SimileMark.Simple); expect(score.tracks[0].staves[0].bars[2].simileMark).to.equal(SimileMark.None); @@ -775,7 +775,7 @@ describe('Gp7ImporterTest', () => { it('anacrusis', async () => { const reader = await prepareImporterWithFile('guitarpro7/anacrusis.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); 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); @@ -783,7 +783,7 @@ describe('Gp7ImporterTest', () => { it('left-hand-tap', async () => { const reader = await prepareImporterWithFile('guitarpro7/left-hand-tap.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].isLeftHandTapped).to.be.equal(true); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].isLeftHandTapped).to.be.equal(true); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[3].notes[0].isLeftHandTapped).to.be.equal(true); @@ -793,30 +793,30 @@ describe('Gp7ImporterTest', () => { it('fermata', async () => { const reader = await prepareImporterWithFile('guitarpro7/fermata.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.masterBars[0].fermata!.size).to.equal(5); expect(score.masterBars[1].fermata!.size).to.equal(5); expect(score.masterBars[2].fermata!.size).to.equal(5); // Short - let offsets = [ + const offsets = [ 0, (MidiUtils.QuarterTime * (1 / 2)) | 0, (MidiUtils.QuarterTime * (1 / 1)) | 0, (MidiUtils.QuarterTime * (2 / 1)) | 0, (MidiUtils.QuarterTime * (3 / 1)) | 0 ]; - let types: FermataType[] = [FermataType.Short, FermataType.Medium, FermataType.Long]; + const types: FermataType[] = [FermataType.Short, FermataType.Medium, FermataType.Long]; for (let i: number = 0; i < 3; i++) { - let masterBar: MasterBar = score.masterBars[i]; + const masterBar: MasterBar = score.masterBars[i]; expect(masterBar.fermata!.size).to.equal(5); - for (let offset of offsets) { - let fermata = masterBar.fermata!.get(offset); + for (const offset of offsets) { + const fermata = masterBar.fermata!.get(offset); expect(fermata).to.be.ok; expect(fermata!.type).to.equal(types[i]); } - let beats: Beat[] = score.tracks[0].staves[0].bars[i].voices[0].beats; - for (let beat of beats) { - let fermata = masterBar.fermata!.get(beat.playbackStart); - let beatFermata = beat.fermata; + const beats: Beat[] = score.tracks[0].staves[0].bars[i].voices[0].beats; + for (const beat of beats) { + const fermata = masterBar.fermata!.get(beat.playbackStart); + const beatFermata = beat.fermata; expect(beatFermata).to.be.ok; expect(fermata).to.be.ok; expect(beatFermata!.type).to.equal(types[i]); @@ -827,7 +827,7 @@ describe('Gp7ImporterTest', () => { it('pick-slide', async () => { const reader = await prepareImporterWithFile('guitarpro7/pick-slide.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].slideOutType).to.equal( SlideOutType.PickSlideUp @@ -889,7 +889,7 @@ describe('Gp7ImporterTest', () => { it('beat-lyrics', async () => { const reader = await prepareImporterWithFile('guitarpro7/beat-lyrics.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].lyrics![0]).to.be.equal('This'); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].lyrics![0]).to.be.equal('is'); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].lyrics![0]).to.be.equal('a'); @@ -902,7 +902,7 @@ describe('Gp7ImporterTest', () => { it('track-volume', async () => { const reader = await prepareImporterWithFile('guitarpro7/track-volume.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].playbackInfo.volume).to.be.equal(16); expect(score.tracks[1].playbackInfo.volume).to.be.equal(14); @@ -915,7 +915,7 @@ describe('Gp7ImporterTest', () => { it('track-balance', async () => { const reader = await prepareImporterWithFile('guitarpro7/track-balance.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].playbackInfo.balance).to.be.equal(0); expect(score.tracks[1].playbackInfo.balance).to.be.equal(4); @@ -926,7 +926,7 @@ describe('Gp7ImporterTest', () => { it('program-change', async () => { const reader = await prepareImporterWithFile('guitarpro7/program-change.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].playbackInfo.program).to.be.equal(25); const automation = score.tracks[0].staves[0].bars[2].voices[0].beats[0].getAutomation( @@ -940,7 +940,7 @@ describe('Gp7ImporterTest', () => { it('chord-no-diagram', async () => { const reader = await prepareImporterWithFile('guitarpro7/chord-no-diagram.gp'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord).to.be.ok; expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.name).to.be.equal('C'); @@ -950,15 +950,14 @@ describe('Gp7ImporterTest', () => { it('layout-configuration', async () => { const track1 = (await prepareImporterWithFile('guitarpro7/layout-configuration-multi-track-1.gp')).readScore(); const track2 = (await prepareImporterWithFile('guitarpro7/layout-configuration-multi-track-2.gp')).readScore(); - const trackAll = (await prepareImporterWithFile('guitarpro7/layout-configuration-multi-track-all.gp')).readScore(); - const track1And3 = (await prepareImporterWithFile('guitarpro7/layout-configuration-multi-track-1-3.gp')).readScore(); - - GpImporterTestHelper.checkMultiTrackLayoutConfiguration( - track1, - track2, - trackAll, - track1And3 - ); + const trackAll = ( + await prepareImporterWithFile('guitarpro7/layout-configuration-multi-track-all.gp') + ).readScore(); + const track1And3 = ( + await prepareImporterWithFile('guitarpro7/layout-configuration-multi-track-1-3.gp') + ).readScore(); + + GpImporterTestHelper.checkMultiTrackLayoutConfiguration(track1, track2, trackAll, track1And3); }); it('slash', async () => { @@ -979,15 +978,21 @@ describe('Gp7ImporterTest', () => { expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].preferredBeamDirection).to.equal(BeamDirection.Up); // force - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].beamingMode).to.equal(BeatBeamingMode.ForceMergeWithNext); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].beamingMode).to.equal( + BeatBeamingMode.ForceMergeWithNext + ); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].invertBeamDirection).to.be.false; expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].preferredBeamDirection).to.equal(BeamDirection.Up); - - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].beamingMode).to.equal(BeatBeamingMode.ForceMergeWithNext); + + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].beamingMode).to.equal( + BeatBeamingMode.ForceMergeWithNext + ); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].invertBeamDirection).to.be.false; expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].preferredBeamDirection).to.equal(BeamDirection.Up); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].beamingMode).to.equal(BeatBeamingMode.ForceMergeWithNext); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].beamingMode).to.equal( + BeatBeamingMode.ForceMergeWithNext + ); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].invertBeamDirection).to.be.false; expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].preferredBeamDirection).to.equal(BeamDirection.Up); @@ -996,15 +1001,21 @@ describe('Gp7ImporterTest', () => { expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].preferredBeamDirection).to.equal(BeamDirection.Up); // break - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].beamingMode).to.equal(BeatBeamingMode.ForceSplitToNext); + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].beamingMode).to.equal( + BeatBeamingMode.ForceSplitToNext + ); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].invertBeamDirection).to.be.false; expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].preferredBeamDirection).to.equal(BeamDirection.Up); - - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].beamingMode).to.equal(BeatBeamingMode.ForceSplitToNext); + + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].beamingMode).to.equal( + BeatBeamingMode.ForceSplitToNext + ); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].invertBeamDirection).to.be.false; expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].preferredBeamDirection).to.equal(BeamDirection.Up); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].beamingMode).to.equal(BeatBeamingMode.ForceSplitToNext); + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].beamingMode).to.equal( + BeatBeamingMode.ForceSplitToNext + ); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].invertBeamDirection).to.be.false; expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].preferredBeamDirection).to.equal(BeamDirection.Up); @@ -1013,10 +1024,12 @@ describe('Gp7ImporterTest', () => { expect(score.tracks[0].staves[0].bars[2].voices[0].beats[3].preferredBeamDirection).to.equal(BeamDirection.Up); // break secondary - expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].beamingMode).to.equal(BeatBeamingMode.ForceSplitOnSecondaryToNext); + expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].beamingMode).to.equal( + BeatBeamingMode.ForceSplitOnSecondaryToNext + ); expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].invertBeamDirection).to.be.false; expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].preferredBeamDirection).to.equal(BeamDirection.Up); - + expect(score.tracks[0].staves[0].bars[3].voices[0].beats[1].beamingMode).to.equal(BeatBeamingMode.Auto); expect(score.tracks[0].staves[0].bars[3].voices[0].beats[1].invertBeamDirection).to.be.false; expect(score.tracks[0].staves[0].bars[3].voices[0].beats[1].preferredBeamDirection).to.equal(BeamDirection.Up); @@ -1028,7 +1041,9 @@ describe('Gp7ImporterTest', () => { // invert to down expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].beamingMode).to.equal(BeatBeamingMode.Auto); expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].invertBeamDirection).to.be.false; - expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].preferredBeamDirection).to.equal(BeamDirection.Down); + expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].preferredBeamDirection).to.equal( + BeamDirection.Down + ); // invert to up expect(score.tracks[0].staves[0].bars[5].voices[0].beats[0].beamingMode).to.equal(BeatBeamingMode.Auto); diff --git a/test/importer/Gp8Importer.test.ts b/test/importer/Gp8Importer.test.ts index 5f5c34bca..c7553471b 100644 --- a/test/importer/Gp8Importer.test.ts +++ b/test/importer/Gp8Importer.test.ts @@ -3,20 +3,23 @@ import { ByteBuffer } from '@src/io/ByteBuffer'; import { BeatBeamingMode } from '@src/model/Beat'; import { Direction } from '@src/model/Direction'; import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@src/model/RenderStylesheet'; +import { ScoreSubElement } from '@src/model/Score'; +import { TextAlign } from '@src/platform/ICanvas'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; import { Settings } from '@src/Settings'; +import { SynthConstants } from '@src/synth/SynthConstants'; import { GpImporterTestHelper } from '@test/importer/GpImporterTestHelper'; import { TestPlatform } from '@test/TestPlatform'; import { expect } from 'chai'; describe('Gp8ImporterTest', () => { async function prepareImporterWithFile(name: string): Promise { - const data = await TestPlatform.loadFile('test-data/' + name); + const data = await TestPlatform.loadFile(`test-data/${name}`); return prepareImporterWithBytes(data); } function prepareImporterWithBytes(buffer: Uint8Array) { - let readerBase: Gp7To8Importer = new Gp7To8Importer(); + const readerBase: Gp7To8Importer = new Gp7To8Importer(); readerBase.init(ByteBuffer.fromBuffer(buffer), new Settings()); return readerBase; } @@ -272,4 +275,115 @@ describe('Gp8ImporterTest', () => { expect(score.tracks[0].staves[0].bars[8].voices[0].beats[0].timer).to.equal(0); expect(score.tracks[0].staves[0].bars[8].voices[0].beats[1].showTimer).to.be.false; }); + + it('multibar-rest', async () => { + const enabled = (await prepareImporterWithFile('guitarpro8/multibar-rest.gp')).readScore(); + const disabled = (await prepareImporterWithFile('guitarpro8/timer.gp')).readScore(); + + expect(disabled.stylesheet.multiTrackMultiBarRest).to.be.false; + expect(disabled.stylesheet.perTrackMultiBarRest).to.equal(null); + expect(enabled.stylesheet.multiTrackMultiBarRest).to.be.true; + expect(enabled.stylesheet.perTrackMultiBarRest).to.be.ok; + expect(enabled.stylesheet.perTrackMultiBarRest!.has(0)).to.be.false; + expect(enabled.stylesheet.perTrackMultiBarRest!.has(1)).to.be.true; + expect(enabled.stylesheet.perTrackMultiBarRest!.has(2)).to.be.true; + }); + + it('header-footer', async () => { + const score = (await prepareImporterWithFile('guitarpro8/header-footer.gp')).readScore(); + + 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.false; + 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.false; + 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.false; + 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.false; + 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.false; + expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.textAlign).to.equal( + TextAlign.Right + ); + }); + + it('faulty', async () => { + // this is a GP8 file from unknown source. + // the score.gpif contents indicate that this file was NOT written by a real Guitar Pro 8 instance but + // by some 3rd party software. there are inconsistencies like: + // * line 752 and 764: Line Breaks in the list of NoteHeads + // * line 67: No line break on close tag, wrong indention + // * line 403: Additional empty line break + // * line 293: Missing line break + // * line 352,353: Missing Midi channels + // * Equal bars/voices/beats are not reused across the file + + // Generally the file looks surprisingly complete for a "non real Guitar Pro" (RSE stuff) but it feels rather like + // a software which has read an original file, and then applied modifications to it before saving again. + + // Maybe its the MacOS version which behaves differently than the Windows Version? + // Or more likely: a non-open source platform like Sound Slice? + + const score = (await prepareImporterWithFile('guitarpro8/faulty.gp')).readScore(); + + const usedChannels = new Set(); + for (const t of score.tracks) { + expect(Number.isNaN(t.playbackInfo.primaryChannel)).to.be.false; + expect(Number.isNaN(t.playbackInfo.secondaryChannel)).to.be.false; + + if (t.playbackInfo.primaryChannel !== SynthConstants.PercussionChannel) { + expect(usedChannels.has(t.playbackInfo.primaryChannel)).to.be.false; + expect(usedChannels.has(t.playbackInfo.secondaryChannel)).to.be.false; + + usedChannels.add(t.playbackInfo.primaryChannel); + usedChannels.add(t.playbackInfo.secondaryChannel); + } + } + }); }); diff --git a/test/importer/GpImporterTestHelper.ts b/test/importer/GpImporterTestHelper.ts index 77c80e45f..2cb706ed0 100644 --- a/test/importer/GpImporterTestHelper.ts +++ b/test/importer/GpImporterTestHelper.ts @@ -11,11 +11,11 @@ import { HarmonicType } from '@src/model/HarmonicType'; import { KeySignature } from '@src/model/KeySignature'; import { KeySignatureType } from '@src/model/KeySignatureType'; import { PickStroke } from '@src/model/PickStroke'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; -import { Staff } from '@src/model/Staff'; -import { Track } from '@src/model/Track'; +import type { Staff } from '@src/model/Staff'; +import type { Track } from '@src/model/Track'; import { VibratoType } from '@src/model/VibratoType'; import { Settings } from '@src/Settings'; import { TestPlatform } from '@test/TestPlatform'; @@ -26,13 +26,13 @@ export class GpImporterTestHelper { name: string, settings: Settings | null = null ): Promise { - let path: string = 'test-data/'; + const path: string = 'test-data/'; const buffer = await TestPlatform.loadFile(path + name); return GpImporterTestHelper.prepareImporterWithBytes(buffer, settings); } public static prepareImporterWithBytes(buffer: Uint8Array, settings: Settings | null = null): Gp3To5Importer { - let readerBase: Gp3To5Importer = new Gp3To5Importer(); + const readerBase: Gp3To5Importer = new Gp3To5Importer(); readerBase.init(ByteBuffer.fromBuffer(buffer), settings ?? new Settings()); return readerBase; } @@ -40,7 +40,7 @@ export class GpImporterTestHelper { public static checkNotes(score: Score): void { // Whole Notes let beat: number = 0; - let durationsInFile: Duration[] = [ + const durationsInFile: Duration[] = [ Duration.Whole, Duration.Half, Duration.Quarter, @@ -49,7 +49,7 @@ export class GpImporterTestHelper { Duration.ThirtySecond, Duration.SixtyFourth ]; - for (let duration of durationsInFile) { + for (const duration of durationsInFile) { expect(score.tracks[0].staves[0].bars[0].voices[0].beats[beat].notes[0].fret).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[beat].notes[0].string).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[beat].duration).to.equal(duration); @@ -382,77 +382,78 @@ export class GpImporterTestHelper { } public static checkKeySignatures(score: Score): void { + const bars = score.tracks[0].staves[0].bars; // major - flats - expect(score.masterBars[0].keySignature).to.equal(KeySignature.C); - expect(score.masterBars[0].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[1].keySignature).to.equal(KeySignature.F); - expect(score.masterBars[1].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[2].keySignature).to.equal(KeySignature.Bb); - expect(score.masterBars[2].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[3].keySignature).to.equal(KeySignature.Eb); - expect(score.masterBars[3].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[4].keySignature).to.equal(KeySignature.Ab); - expect(score.masterBars[4].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[5].keySignature).to.equal(KeySignature.Db); - expect(score.masterBars[5].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[6].keySignature).to.equal(KeySignature.Gb); - expect(score.masterBars[6].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[7].keySignature).to.equal(KeySignature.Cb); - expect(score.masterBars[7].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[0].keySignature).to.equal(KeySignature.C); + expect(bars[0].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[1].keySignature).to.equal(KeySignature.F); + expect(bars[1].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[2].keySignature).to.equal(KeySignature.Bb); + expect(bars[2].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[3].keySignature).to.equal(KeySignature.Eb); + expect(bars[3].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[4].keySignature).to.equal(KeySignature.Ab); + expect(bars[4].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[5].keySignature).to.equal(KeySignature.Db); + expect(bars[5].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[6].keySignature).to.equal(KeySignature.Gb); + expect(bars[6].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[7].keySignature).to.equal(KeySignature.Cb); + expect(bars[7].keySignatureType).to.equal(KeySignatureType.Major); // major - sharps - expect(score.masterBars[8].keySignature).to.equal(KeySignature.C); - expect(score.masterBars[8].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[9].keySignature).to.equal(KeySignature.G); - expect(score.masterBars[9].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[10].keySignature).to.equal(KeySignature.D); - expect(score.masterBars[10].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[11].keySignature).to.equal(KeySignature.A); - expect(score.masterBars[11].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[12].keySignature).to.equal(KeySignature.E); - expect(score.masterBars[12].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[13].keySignature).to.equal(KeySignature.B); - expect(score.masterBars[13].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[14].keySignature).to.equal(KeySignature.FSharp); - expect(score.masterBars[14].keySignatureType).to.equal(KeySignatureType.Major); - expect(score.masterBars[15].keySignature).to.equal(KeySignature.CSharp); - expect(score.masterBars[15].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[8].keySignature).to.equal(KeySignature.C); + expect(bars[8].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[9].keySignature).to.equal(KeySignature.G); + expect(bars[9].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[10].keySignature).to.equal(KeySignature.D); + expect(bars[10].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[11].keySignature).to.equal(KeySignature.A); + expect(bars[11].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[12].keySignature).to.equal(KeySignature.E); + expect(bars[12].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[13].keySignature).to.equal(KeySignature.B); + expect(bars[13].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[14].keySignature).to.equal(KeySignature.FSharp); + expect(bars[14].keySignatureType).to.equal(KeySignatureType.Major); + expect(bars[15].keySignature).to.equal(KeySignature.CSharp); + expect(bars[15].keySignatureType).to.equal(KeySignatureType.Major); // minor flats - expect(score.masterBars[16].keySignature).to.equal(KeySignature.C); - expect(score.masterBars[16].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[17].keySignature).to.equal(KeySignature.F); - expect(score.masterBars[17].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[18].keySignature).to.equal(KeySignature.Bb); - expect(score.masterBars[18].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[19].keySignature).to.equal(KeySignature.Eb); - expect(score.masterBars[19].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[20].keySignature).to.equal(KeySignature.Ab); - expect(score.masterBars[20].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[21].keySignature).to.equal(KeySignature.Db); - expect(score.masterBars[21].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[22].keySignature).to.equal(KeySignature.Gb); - expect(score.masterBars[22].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[23].keySignature).to.equal(KeySignature.Cb); - expect(score.masterBars[23].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[16].keySignature).to.equal(KeySignature.C); + expect(bars[16].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[17].keySignature).to.equal(KeySignature.F); + expect(bars[17].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[18].keySignature).to.equal(KeySignature.Bb); + expect(bars[18].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[19].keySignature).to.equal(KeySignature.Eb); + expect(bars[19].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[20].keySignature).to.equal(KeySignature.Ab); + expect(bars[20].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[21].keySignature).to.equal(KeySignature.Db); + expect(bars[21].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[22].keySignature).to.equal(KeySignature.Gb); + expect(bars[22].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[23].keySignature).to.equal(KeySignature.Cb); + expect(bars[23].keySignatureType).to.equal(KeySignatureType.Minor); // minor sharps - expect(score.masterBars[24].keySignature).to.equal(KeySignature.C); - expect(score.masterBars[24].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[25].keySignature).to.equal(KeySignature.G); - expect(score.masterBars[25].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[26].keySignature).to.equal(KeySignature.D); - expect(score.masterBars[26].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[27].keySignature).to.equal(KeySignature.A); - expect(score.masterBars[27].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[28].keySignature).to.equal(KeySignature.E); - expect(score.masterBars[28].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[29].keySignature).to.equal(KeySignature.B); - expect(score.masterBars[29].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[30].keySignature).to.equal(KeySignature.FSharp); - expect(score.masterBars[30].keySignatureType).to.equal(KeySignatureType.Minor); - expect(score.masterBars[31].keySignature).to.equal(KeySignature.CSharp); - expect(score.masterBars[31].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[24].keySignature).to.equal(KeySignature.C); + expect(bars[24].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[25].keySignature).to.equal(KeySignature.G); + expect(bars[25].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[26].keySignature).to.equal(KeySignature.D); + expect(bars[26].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[27].keySignature).to.equal(KeySignature.A); + expect(bars[27].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[28].keySignature).to.equal(KeySignature.E); + expect(bars[28].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[29].keySignature).to.equal(KeySignature.B); + expect(bars[29].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[30].keySignature).to.equal(KeySignature.FSharp); + expect(bars[30].keySignatureType).to.equal(KeySignatureType.Minor); + expect(bars[31].keySignature).to.equal(KeySignature.CSharp); + expect(bars[31].keySignatureType).to.equal(KeySignatureType.Minor); } public static checkColors(score: Score): void { @@ -478,8 +479,8 @@ export class GpImporterTestHelper { } public static checkChords(score: Score): void { - let track: Track = score.tracks[0]; - let staff: Staff = track.staves[0]; + const track: Track = score.tracks[0]; + const staff: Staff = track.staves[0]; expect(staff.chords!.size).to.equal(8); GpImporterTestHelper.checkChord( @@ -552,7 +553,7 @@ export class GpImporterTestHelper { } public static checkSlash(score: Score): void { expect(score.tracks.length).to.equal(2); - + expect(score.tracks[0].staves.length).to.equal(1); expect(score.tracks[0].staves[0].showSlash).to.equal(true); expect(score.tracks[0].staves[0].showTablature).to.equal(true); diff --git a/test/importer/GpxImporter.test.ts b/test/importer/GpxImporter.test.ts index 7198d19ad..e6f6dbd22 100644 --- a/test/importer/GpxImporter.test.ts +++ b/test/importer/GpxImporter.test.ts @@ -1,7 +1,7 @@ -import { GpxFile, GpxFileSystem } from '@src/importer/GpxFileSystem'; +import { type GpxFile, GpxFileSystem } from '@src/importer/GpxFileSystem'; import { GpxImporter } from '@src/importer/GpxImporter'; import { ByteBuffer } from '@src/io/ByteBuffer'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { GpImporterTestHelper } from '@test/importer/GpImporterTestHelper'; @@ -10,29 +10,30 @@ import { expect } from 'chai'; describe('GpxImporterTest', () => { async function prepareImporterWithFile(name: string): Promise { - const data = await TestPlatform.loadFile('test-data/' + name); - return prepareImporterWithBytes(data);} + const data = await TestPlatform.loadFile(`test-data/${name}`); + return prepareImporterWithBytes(data); + } function prepareImporterWithBytes(buffer: Uint8Array) { - let readerBase: GpxImporter = new GpxImporter(); + const readerBase: GpxImporter = new GpxImporter(); readerBase.init(ByteBuffer.fromBuffer(buffer), new Settings()); return readerBase; } it('file-system-compressed', async () => { const data = await TestPlatform.loadFile('test-data/guitarpro6/file-system-compressed.gpx'); - let fileSystem: GpxFileSystem = new GpxFileSystem(); + const fileSystem: GpxFileSystem = new GpxFileSystem(); fileSystem.load(ByteBuffer.fromBuffer(data)); - let names: string[] = [ + const names: string[] = [ 'score.gpif', 'misc.xml', 'BinaryStylesheet', 'PartConfiguration', 'LayoutConfiguration' ]; - let sizes = [8488, 130, 12204, 20, 12]; + const sizes = [8488, 130, 12204, 20, 12]; for (let i: number = 0; i < fileSystem.files.length; i++) { - let file: GpxFile = fileSystem.files[i]; + const file: GpxFile = fileSystem.files[i]; Logger.info('Test', `${file.fileName} - ${file.fileSize}`); expect(file.fileName).to.equal(names[i]); expect(file.fileSize).to.equal(sizes[i]); @@ -41,7 +42,7 @@ describe('GpxImporterTest', () => { it('score-info', async () => { const reader = await prepareImporterWithFile('guitarpro6/score-info.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.title).to.equal('Title'); expect(score.subTitle).to.equal('Subtitle'); expect(score.artist).to.equal('Artist'); @@ -60,93 +61,123 @@ describe('GpxImporterTest', () => { it('notes', async () => { const reader = await prepareImporterWithFile('guitarpro6/notes.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkNotes(score); }); it('time-signatures', async () => { const reader = await prepareImporterWithFile('guitarpro6/time-signatures.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTimeSignatures(score); }); it('dead', async () => { const reader = await prepareImporterWithFile('guitarpro6/dead.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkDead(score); }); it('grace', async () => { const reader = await prepareImporterWithFile('guitarpro6/grace.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkGrace(score); }); it('accentuations', async () => { const reader = await prepareImporterWithFile('guitarpro6/accentuations.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkAccentuations(score, true); }); it('harmonics', async () => { const reader = await prepareImporterWithFile('guitarpro6/harmonics.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkHarmonics(score); }); it('hammer', async () => { const reader = await prepareImporterWithFile('guitarpro6/hammer.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkHammer(score); }); it('bends', async () => { const reader = await prepareImporterWithFile('guitarpro6/bends.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); 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.be.closeTo(0, 0.001); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints![0].offset).to.be.closeTo( + 0, + 0.001 + ); 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.be.closeTo(60, 0.001); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints![1].offset).to.be.closeTo( + 60, + 0.001 + ); 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[1].notes[0].bendPoints!.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].bendPoints![0].offset).to.be.closeTo(0, 0.001); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].bendPoints![0].offset).to.be.closeTo( + 0, + 0.001 + ); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].bendPoints![0].value).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].bendPoints![1].offset).to.be.closeTo(60, 0.001); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].bendPoints![1].offset).to.be.closeTo( + 60, + 0.001 + ); 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![0].offset).to.be.closeTo(0, 0.001); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![0].offset).to.be.closeTo( + 0, + 0.001 + ); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![0].value).to.equal(0); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![0].offset).to.be.closeTo(0, 0.001); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![0].offset).to.be.closeTo( + 0, + 0.001 + ); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![0].value).to.equal(0); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![1].offset).to.be.closeTo(30, 0.001); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![1].offset).to.be.closeTo( + 30, + 0.001 + ); 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(60, 0.001); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].bendPoints![2].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); }); it('tremolo', async () => { const reader = await prepareImporterWithFile('guitarpro6/tremolo.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints!.length).to.equal(3); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![0].offset).to.be.closeTo(0, 0.001); 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].offset).to.be.closeTo(30, 0.001); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![1].offset).to.be.closeTo( + 30, + 0.001 + ); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![1].value).to.equal(-4); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![2].offset).to.be.closeTo(60, 0.001); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![2].offset).to.be.closeTo( + 60, + 0.001 + ); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![2].value).to.equal(0); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints!.length).to.equal(2); @@ -154,7 +185,10 @@ describe('GpxImporterTest', () => { expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![0].offset).to.be.closeTo(0, 0.001); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![0].value).to.equal(-4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![1].offset).to.be.closeTo(60, 0.001); + expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![1].offset).to.be.closeTo( + 60, + 0.001 + ); expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![1].value).to.equal(0); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].whammyBarPoints!.length).to.equal(3); @@ -162,10 +196,16 @@ describe('GpxImporterTest', () => { expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].whammyBarPoints![0].offset).to.be.closeTo(0, 0.001); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].whammyBarPoints![0].value).to.equal(0); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].whammyBarPoints![1].offset).to.be.closeTo(30, 0.001); + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].whammyBarPoints![1].offset).to.be.closeTo( + 30, + 0.001 + ); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].whammyBarPoints![1].value).to.equal(-4); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].whammyBarPoints![2].offset).to.be.closeTo(60, 0.001); + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].whammyBarPoints![2].offset).to.be.closeTo( + 60, + 0.001 + ); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].whammyBarPoints![2].value).to.equal(-4); expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints!.length).to.equal(4); @@ -173,67 +213,76 @@ describe('GpxImporterTest', () => { expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![0].offset).to.be.closeTo(0, 0.001); expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![0].value).to.equal(-4); - expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![1].offset).to.be.closeTo(15, 0.001); + expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![1].offset).to.be.closeTo( + 15, + 0.001 + ); expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![1].value).to.equal(-12); - expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![2].offset).to.be.closeTo(30.6, 0.001); + expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![2].offset).to.be.closeTo( + 30.6, + 0.001 + ); expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![2].value).to.equal(-12); - expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![3].offset).to.be.closeTo(45, 0.001); + expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![3].offset).to.be.closeTo( + 45, + 0.001 + ); expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].whammyBarPoints![3].value).to.equal(0); }); it('slides', async () => { const reader = await prepareImporterWithFile('guitarpro6/slides.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkSlides(score); }); it('vibrato', async () => { const reader = await prepareImporterWithFile('guitarpro6/vibrato.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkVibrato(score, true); }); it('trills', async () => { const reader = await prepareImporterWithFile('guitarpro6/trills.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTrills(score); }); it('other-effects', async () => { const reader = await prepareImporterWithFile('guitarpro6/other-effects.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkOtherEffects(score, true); }); it('fingering', async () => { const reader = await prepareImporterWithFile('guitarpro6/fingering.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkFingering(score); }); it('stroke', async () => { const reader = await prepareImporterWithFile('guitarpro6/strokes.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkStroke(score); }); it('tuplets', async () => { const reader = await prepareImporterWithFile('guitarpro6/tuplets.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkTuplets(score); }); it('ranges', async () => { const reader = await prepareImporterWithFile('guitarpro6/ranges.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkRanges(score); }); it('effects', async () => { const reader = await prepareImporterWithFile('guitarpro6/effects.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkEffects(score); }); @@ -245,40 +294,39 @@ describe('GpxImporterTest', () => { it('strings', async () => { const reader = await prepareImporterWithFile('guitarpro6/strings.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkStrings(score); }); it('key-signatures', async () => { const reader = await prepareImporterWithFile('guitarpro6/key-signatures.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkKeySignatures(score); }); it('chords', async () => { const reader = await prepareImporterWithFile('guitarpro6/chords.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkChords(score); }); it('colors', async () => { const reader = await prepareImporterWithFile('guitarpro6/colors.gpx'); - let score: Score = reader.readScore(); + const score: Score = reader.readScore(); GpImporterTestHelper.checkColors(score); }); it('layout-configuration', async () => { const track1 = (await prepareImporterWithFile('guitarpro6/layout-configuration-multi-track-1.gpx')).readScore(); const track2 = (await prepareImporterWithFile('guitarpro6/layout-configuration-multi-track-2.gpx')).readScore(); - const trackAll = (await prepareImporterWithFile('guitarpro6/layout-configuration-multi-track-all.gpx')).readScore(); - const track1And3 = (await prepareImporterWithFile('guitarpro6/layout-configuration-multi-track-1-3.gpx')).readScore(); - - GpImporterTestHelper.checkMultiTrackLayoutConfiguration( - track1, - track2, - trackAll, - track1And3 - ); + const trackAll = ( + await prepareImporterWithFile('guitarpro6/layout-configuration-multi-track-all.gpx') + ).readScore(); + const track1And3 = ( + await prepareImporterWithFile('guitarpro6/layout-configuration-multi-track-1-3.gpx') + ).readScore(); + + GpImporterTestHelper.checkMultiTrackLayoutConfiguration(track1, track2, trackAll, track1And3); }); it('slash', async () => { diff --git a/test/importer/MusicXmlImporter.test.ts b/test/importer/MusicXmlImporter.test.ts index 10d8ccb7b..763230b9c 100644 --- a/test/importer/MusicXmlImporter.test.ts +++ b/test/importer/MusicXmlImporter.test.ts @@ -1,12 +1,12 @@ import { MusicXmlImporterTestHelper } from '@test/importer/MusicXmlImporterTestHelper'; -import { Score } from '@src/model/Score'; -import { JsonConverter } from '@src/model'; +import type { Score } from '@src/model/Score'; import { BendType } from '@src/model/BendType'; import { expect } from 'chai'; +import { JsonConverter } from '@src/model/JsonConverter'; describe('MusicXmlImporterTests', () => { it('track-volume', async () => { - let score: Score = await MusicXmlImporterTestHelper.testReferenceFile( + const score: Score = await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml3/track-volume-balance.musicxml' ); @@ -18,7 +18,7 @@ describe('MusicXmlImporterTests', () => { }); it('track-balance', async () => { - let score: Score = await MusicXmlImporterTestHelper.testReferenceFile( + const score: Score = await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml3/track-volume-balance.musicxml' ); @@ -30,7 +30,7 @@ describe('MusicXmlImporterTests', () => { }); it('full-bar-rest', async () => { - let score: Score = await MusicXmlImporterTestHelper.testReferenceFile( + const score: Score = await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml3/full-bar-rest.musicxml' ); @@ -75,7 +75,7 @@ describe('MusicXmlImporterTests', () => { ); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord).to.be.ok; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.name).to.equal("C"); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.name).to.equal('C'); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.strings[0]).to.equal(0); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.strings[1]).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.strings[2]).to.equal(0); @@ -83,11 +83,10 @@ describe('MusicXmlImporterTests', () => { expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.strings[4]).to.equal(3); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.strings[5]).to.equal(-1); - score = JsonConverter.jsObjectToScore(JsonConverter.scoreToJsObject(score)); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord).to.be.ok; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.name).to.equal("C"); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.name).to.equal('C'); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.strings[0]).to.equal(0); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.strings[1]).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.strings[2]).to.equal(0); @@ -96,18 +95,14 @@ describe('MusicXmlImporterTests', () => { expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].chord!.strings[5]).to.equal(-1); }); it('compressed', async () => { - const score: Score = await MusicXmlImporterTestHelper.testReferenceFile( - 'test-data/musicxml3/compressed.mxl' - ); + const score: Score = await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml3/compressed.mxl'); - expect(score.title).to.equal("Title"); + expect(score.title).to.equal('Title'); expect(score.tracks.length).to.equal(1); expect(score.masterBars.length).to.equal(1); - }); + }); it('bend', async () => { - let score: Score = await MusicXmlImporterTestHelper.testReferenceFile( - 'test-data/musicxml-testsuite/100a-Guitare-Bends.xml' - ); + const score: Score = await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml4/bends.xml'); let note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; expect(note.bendType).to.equal(BendType.Bend); expect(note.bendPoints!.length).to.equal(2); @@ -144,7 +139,6 @@ describe('MusicXmlImporterTests', () => { expect(note.bendPoints![1].offset).to.equal(60); expect(note.bendPoints![1].value).to.equal(0); - note = score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0]; expect(note.bendType).to.equal(BendType.PrebendBend); expect(note.bendPoints!.length).to.equal(2); @@ -209,12 +203,12 @@ describe('MusicXmlImporterTests', () => { expect(note.bendPoints![11].value).to.equal(8); note = score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0]; - expect(note.bendType).to.equal(BendType.Release); + expect(note.bendType).to.equal(BendType.PrebendRelease); expect(note.bendPoints!.length).to.equal(2); expect(note.bendPoints![0].offset).to.equal(0); expect(note.bendPoints![0].value).to.equal(8); expect(note.bendPoints![1].offset).to.equal(60); - expect(note.bendPoints![1].value).to.equal(0); + expect(note.bendPoints![1].value).to.equal(0); note = score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0]; expect(note.bendType).to.equal(BendType.Bend); @@ -224,4 +218,39 @@ describe('MusicXmlImporterTests', () => { expect(note.bendPoints![1].offset).to.equal(30); expect(note.bendPoints![1].value).to.equal(2); }); -}); \ No newline at end of file + + it('partwise-basic', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/partwise-basic.xml'); + expect(score).toMatchSnapshot(); + }); + + it('timewise-basic', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/timewise-basic.xml'); + expect(score).toMatchSnapshot(); + }); + + it('partwise-anacrusis', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/partwise-anacrusis.xml'); + expect(score).toMatchSnapshot(); + }); + + it('timewise-anacrusis', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/timewise-anacrusis.xml'); + expect(score).toMatchSnapshot(); + }); + + it('partwise-complex-measures', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/partwise-complex-measures.xml'); + expect(score).toMatchSnapshot(); + }); + + it('partwise-staff-change', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/partwise-staff-change.xml'); + expect(score).toMatchSnapshot(); + }); + + it('barlines', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/barlines.xml'); + expect(score).toMatchSnapshot(); + }); +}); diff --git a/test/importer/MusicXmlImporterTestHelper.ts b/test/importer/MusicXmlImporterTestHelper.ts index bab992658..618080baa 100644 --- a/test/importer/MusicXmlImporterTestHelper.ts +++ b/test/importer/MusicXmlImporterTestHelper.ts @@ -1,16 +1,11 @@ -import { LayoutMode } from '@src/LayoutMode'; import { MusicXmlImporter } from '@src/importer/MusicXmlImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { Bar } from '@src/model/Bar'; import { Beat } from '@src/model/Beat'; -import { BendPoint } from '@src/model/BendPoint'; -import { Chord } from '@src/model/Chord'; import { MasterBar } from '@src/model/MasterBar'; import { Note } from '@src/model/Note'; -import { PlaybackInformation } from '@src/model/PlaybackInformation'; -import { Score } from '@src/model/Score'; -import { Section } from '@src/model/Section'; +import type { Score } from '@src/model/Score'; import { Staff } from '@src/model/Staff'; import { Track } from '@src/model/Track'; import { Voice } from '@src/model/Voice'; @@ -18,24 +13,35 @@ import { Settings } from '@src/Settings'; import { TestPlatform } from '@test/TestPlatform'; import { JsonConverter } from '@src/model/JsonConverter'; import { ComparisonHelpers } from '@test/model/ComparisonHelpers'; -import { assert, expect } from 'chai'; +import { assert } from 'chai'; +import { VisualTestHelper, VisualTestOptions, VisualTestRun } from '@test/visualTests/VisualTestHelper'; +import { SystemsLayoutMode } from '@src/DisplaySettings'; export class MusicXmlImporterTestHelper { - public static prepareImporterWithBytes(buffer: Uint8Array): MusicXmlImporter { - let readerBase: MusicXmlImporter = new MusicXmlImporter(); - readerBase.init(ByteBuffer.fromBuffer(buffer), new Settings()); + public static async loadFile(file: string): Promise { + const fileData = await TestPlatform.loadFile(file); + const reader: MusicXmlImporter = new MusicXmlImporter(); + reader.init(ByteBuffer.fromBuffer(fileData), new Settings()); + return reader.readScore(); + } + + public static prepareImporterWithBytes(buffer: Uint8Array, settings?: Settings): MusicXmlImporter { + const readerBase: MusicXmlImporter = new MusicXmlImporter(); + readerBase.init(ByteBuffer.fromBuffer(buffer), settings ?? new Settings()); return readerBase; } public static async testReferenceFile( file: string, - renderLayout: LayoutMode = LayoutMode.Page, - renderAllTracks: boolean = false + render: boolean = true, + renderAllTracks: boolean = true, + prepare: ((settings: Settings) => void) | null = null ): Promise { const fileData = await TestPlatform.loadFile(file); let score: Score; + const settings = new Settings(); try { - let importer: MusicXmlImporter = MusicXmlImporterTestHelper.prepareImporterWithBytes(fileData); + const importer: MusicXmlImporter = MusicXmlImporterTestHelper.prepareImporterWithBytes(fileData, settings); score = importer.readScore(); } catch (e) { if (e instanceof UnsupportedFormatError) { @@ -48,248 +54,66 @@ export class MusicXmlImporterTestHelper { try { const expectedJson = JsonConverter.scoreToJsObject(score); - const deserialized = JsonConverter.jsObjectToScore(expectedJson); + const deserialized = JsonConverter.jsObjectToScore(expectedJson, settings); const actualJson = JsonConverter.scoreToJsObject(deserialized); - - ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '<' + file + '>', null); - } catch(e) { - assert.fail(String(e)); + + ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, `<${file}>`, null); + } catch (e) { + assert.fail((e as Error).message + (e as Error).stack); + } + + if (render) { + settings.display.justifyLastSystem = score.masterBars.length > 4; + if (score.tracks.some(t => t.systemsLayout.length > 0)) { + settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + } + + prepare?.(settings); + const testOptions = new VisualTestOptions( + score, + [new VisualTestRun(1300, TestPlatform.changeExtension(file, '.png'))], + settings + ); + + if (renderAllTracks) { + testOptions.tracks = score.tracks.map(t => t.index); + } + + await VisualTestHelper.runVisualTestFull(testOptions); } return score; } protected static getHierarchy(node: unknown): string { - let note: Note | null = node instanceof Note ? node : null; + const note: Note | null = node instanceof Note ? node : null; if (note) { return `${MusicXmlImporterTestHelper.getHierarchy(note.beat)}Note[${note.index}]`; } - let beat: Beat | null = node instanceof Beat ? node : null; + const beat: Beat | null = node instanceof Beat ? node : null; if (beat) { return `${MusicXmlImporterTestHelper.getHierarchy(beat.voice)}Beat[${beat.index}]`; } - let voice: Voice | null = node instanceof Voice ? node : null; + const voice: Voice | null = node instanceof Voice ? node : null; if (voice) { return `${MusicXmlImporterTestHelper.getHierarchy(voice.bar)}Voice[${voice.index}]`; } - let bar: Bar | null = node instanceof Bar ? node : null; + const bar: Bar | null = node instanceof Bar ? node : null; if (bar) { return `${MusicXmlImporterTestHelper.getHierarchy(bar.staff)}Bar[${bar.index}]`; } - let staff: Staff | null = node instanceof Staff ? node : null; + const staff: Staff | null = node instanceof Staff ? node : null; if (staff) { return `${MusicXmlImporterTestHelper.getHierarchy(staff.track)}Staff[${staff.index}]`; } - let track: Track | null = node instanceof Track ? node : null; + const track: Track | null = node instanceof Track ? node : null; if (track) { return `Track[${track.index}]`; } - let mb: MasterBar | null = node instanceof MasterBar ? node : null; + const mb: MasterBar | null = node instanceof MasterBar ? node : null; if (mb) { return `MasterBar[${mb.index}]`; } return 'unknown'; } - - protected expectScoreEqual(expected: Score, actual: Score): void { - expect(actual.album).to.equal(expected.album, 'Mismatch on Album'); - expect(actual.artist).to.equal(expected.artist, 'Mismatch on Artist'); - expect(actual.copyright).to.equal(expected.copyright, 'Mismatch on Copyright'); - expect(actual.instructions).to.equal(expected.instructions, 'Mismatch on Instructions'); - expect(actual.music).to.equal(expected.music, 'Mismatch on Music'); - expect(actual.notices).to.equal(expected.notices, 'Mismatch on Notices'); - expect(actual.subTitle).to.equal(expected.subTitle, 'Mismatch on SubTitle'); - expect(actual.title).to.equal(expected.title, 'Mismatch on Title'); - expect(actual.words).to.equal(expected.words, 'Mismatch on Words'); - expect(actual.tab).to.equal(expected.tab, 'Mismatch on Tab'); - expect(actual.tempo).to.equal(expected.tempo, 'Mismatch on Tempo'); - expect(actual.tempoLabel).to.equal(expected.tempoLabel, 'Mismatch on TempoLabel'); - expect(actual.masterBars.length).to.equal(expected.masterBars.length, 'Mismatch on MasterBars.Count'); - for (let i: number = 0; i < expected.masterBars.length; i++) { - this.expectMasterBarEqual(expected.masterBars[i], actual.masterBars[i]); - } - expect(actual.tracks.length).to.equal(expected.tracks.length, 'Mismatch on Tracks.Count'); - for (let i: number = 0; i < expected.tracks.length; i++) { - this.expectTrackEqual(expected.tracks[i], actual.tracks[i]); - } - } - - protected expectTrackEqual(expected: Track, actual: Track): void { - expect(actual.index).to.equal(expected.index, 'Mismatch on Index'); - expect(actual.name).to.equal(expected.name, 'Mismatch on Name'); - this.expectPlaybackInformationEqual(expected.playbackInfo, actual.playbackInfo); - expect(actual.staves.length).to.equal(expected.staves.length, 'Mismatch on Staves.Count'); - for (let i: number = 0; i < expected.staves.length; i++) { - this.expectStaffEqual(expected.staves[i], actual.staves[i]); - } - } - - protected expectStaffEqual(expected: Staff, actual: Staff): void { - expect(actual.capo).to.equal(expected.capo, 'Mismatch on Capo'); - expect(actual.isPercussion).to.equal(expected.isPercussion, 'Mismatch on IsPercussion'); - expect(actual.showStandardNotation).to.equal(expected.showStandardNotation, 'Mismatch on ShowStandardNotation'); - expect(actual.showTablature).to.equal(expected.showTablature, 'Mismatch on ShowTablature'); - expect(actual.tuning.join(',')).to.equal(expected.tuning.join(',')); - expect(actual.tuning.length).to.equal(expected.tuning.length, 'Mismatch on Tuning.Length'); - expect(actual.index).to.equal(expected.index, 'Mismatch on Index'); - expect(actual.bars.length).to.equal(expected.bars.length, 'Mismatch on Bars.Count'); - for (let i: number = 0; i < expected.bars.length; i++) { - this.expectBarEqual(expected.bars[i], actual.bars[i]); - } - } - - protected expectBarEqual(expected: Bar, actual: Bar): void { - expect(actual.index).to.equal(expected.index, 'Mismatch on Index'); - expect(actual.clef).to.equal(expected.clef, 'Mismatch on Clef'); - expect(actual.clefOttava).to.equal(expected.clefOttava, 'Mismatch on ClefOttavia'); - // Assert.AreEqual(expected.Voices.Count, actual.Voices.Count, "Mismatch on Voices.Count"); - for (let i: number = 0; i < Math.min(expected.voices.length, actual.voices.length); i++) { - this.expectVoiceEqual(expected.voices[i], actual.voices[i]); - } - } - - protected expectVoiceEqual(expected: Voice, actual: Voice): void { - expect(actual.index).to.equal(expected.index, 'Mismatch on Index'); - expect(actual.beats.length).to.equal(expected.beats.length, 'Mismatch on Beats.Count'); - for (let i: number = 0; i < expected.beats.length; i++) { - this.expectBeatEqual(expected.beats[i], actual.beats[i]); - } - } - - protected expectBeatEqual(expected: Beat, actual: Beat): void { - expect(actual.index).to.equal(expected.index, 'Mismatch on Index'); - expect(actual.isEmpty).to.equal(expected.isEmpty, 'Mismatch on IsEmpty'); - expect(actual.isRest).to.equal(expected.isRest, 'Mismatch on IsRest'); - expect(actual.dots).to.equal(expected.dots, 'Mismatch on Dots'); - expect(actual.fadeIn).to.equal(expected.fadeIn, 'Mismatch on FadeIn'); - expect(actual.isLegatoOrigin).to.equal(expected.isLegatoOrigin, 'Mismatch on IsLegatoOrigin'); - if (!expected.lyrics) { - expect(actual.lyrics).to.not.be.ok; - } else { - expect(actual.lyrics!.join(',')).to.equal(expected.lyrics.join(',')); - } - expect(actual.pop).to.equal(expected.pop, 'Mismatch on Pop'); - expect(actual.hasChord).to.equal(expected.hasChord, 'Mismatch on HasChord'); - expect(actual.hasRasgueado).to.equal(expected.hasRasgueado, 'Mismatch on HasRasgueado'); - expect(actual.tap).to.equal(expected.tap); - expect(actual.slap).to.equal(expected.slap); - expect(actual.text).to.equal(expected.text, 'Mismatch on Text'); - expect(actual.brushType).to.equal(expected.brushType, 'Mismatch on BrushType'); - expect(actual.brushDuration).to.equal(expected.brushDuration, 'Mismatch on BrushDuration'); - expect(actual.tupletDenominator).to.equal(expected.tupletDenominator, 'Mismatch on TupletDenominator'); - expect(actual.tupletNumerator).to.equal(expected.tupletNumerator, 'Mismatch on TupletNumerator'); - this.expectBendPointsEqual(expected.whammyBarPoints, actual.whammyBarPoints); - expect(actual.vibrato).to.equal(expected.vibrato, 'Mismatch on Vibrato'); - if (expected.hasChord) { - this.expectChordEqual(expected.chord!, actual.chord!); - } - expect(actual.graceType).to.equal(expected.graceType, 'Mismatch on GraceType'); - expect(actual.pickStroke).to.equal(expected.pickStroke, 'Mismatch on PickStroke'); - expect(actual.tremoloSpeed).to.equal(expected.tremoloSpeed, 'Mismatch on TremoloSpeed'); - expect(actual.crescendo).to.equal(expected.crescendo, 'Mismatch on Crescendo'); - expect(actual.playbackStart).to.equal(expected.playbackStart, 'Mismatch on Start'); - expect(actual.displayStart).to.equal(expected.displayStart, 'Mismatch on Start'); - // Assert.AreEqual(expected.Dynamic, actual.Dynamic, "Mismatch on Dynamic"); - expect(actual.invertBeamDirection).to.equal(expected.invertBeamDirection, 'Mismatch on InvertBeamDirection'); - expect(actual.notes.length).to.equal(expected.notes.length, 'Mismatch on Notes.Count'); - for (let i: number = 0; i < expected.notes.length; i++) { - this.expectNoteEqual(expected.notes[i], actual.notes[i]); - } - } - - protected expectNoteEqual(expected: Note, actual: Note): void { - expect(actual.index).to.equal(expected.index, 'Mismatch on Index'); - expect(actual.accentuated).to.equal(expected.accentuated, 'Mismatch on Accentuated'); - this.expectBendPointsEqual(expected.bendPoints, actual.bendPoints); - expect(actual.isStringed).to.equal(expected.isStringed, 'Mismatch on IsStringed'); - if (actual.isStringed) { - expect(actual.fret).to.equal(expected.fret, 'Mismatch on Fret'); - expect(actual.string).to.equal(expected.string, 'Mismatch on String'); - } - expect(actual.isPiano).to.equal(expected.isPiano, 'Mismatch on IsPiano'); - if (actual.isPiano) { - expect(actual.octave).to.equal(expected.octave, 'Mismatch on Octave'); - expect(actual.tone).to.equal(expected.tone, 'Mismatch on Tone'); - } - expect(actual.percussionArticulation).to.equal(expected.percussionArticulation, 'Mismatch on percussionArticulation'); - expect(actual.isHammerPullOrigin).to.equal(expected.isHammerPullOrigin, 'Mismatch on IsHammerPullOrigin'); - expect(actual.harmonicType).to.equal(expected.harmonicType, 'Mismatch on HarmonicType'); - expect(actual.harmonicValue).to.equal(expected.harmonicValue, 'Mismatch on HarmonicValue'); - expect(actual.isGhost).to.equal(expected.isGhost, 'Mismatch on IsGhost'); - expect(actual.isLetRing).to.equal(expected.isLetRing, 'Mismatch on IsLetRing'); - expect(actual.isPalmMute).to.equal(expected.isPalmMute, 'Mismatch on IsPalmMute'); - expect(actual.isDead).to.equal(expected.isDead, 'Mismatch on IsDead'); - expect(actual.isStaccato).to.equal(expected.isStaccato, 'Mismatch on IsStaccato'); - expect(actual.slideInType).to.equal(expected.slideInType, 'Mismatch on SlideInType'); - expect(actual.slideOutType).to.equal(expected.slideOutType, 'Mismatch on SlideOutType'); - expect(actual.vibrato).to.equal(expected.vibrato, 'Mismatch on Vibrato'); - expect(actual.isTieDestination).to.equal(expected.isTieDestination, 'Mismatch on IsTieDestination'); - expect(actual.isTieOrigin).to.equal(expected.isTieOrigin, 'Mismatch on IsTieOrigin'); - expect(actual.leftHandFinger).to.equal(expected.leftHandFinger, 'Mismatch on LeftHandFinger'); - expect(actual.isFingering).to.equal(expected.isFingering, 'Mismatch on IsFingering'); - expect(actual.trillValue).to.equal(expected.trillValue, 'Mismatch on TrillValue'); - expect(actual.trillSpeed).to.equal(expected.trillSpeed, 'Mismatch on TrillSpeed'); - expect(actual.durationPercent).to.equal(expected.durationPercent, 'Mismatch on DurationPercent'); - expect(actual.dynamics).to.equal(expected.dynamics, 'Mismatch on Dynamic'); - expect(actual.realValue).to.equal(expected.realValue, 'Mismatch on RealValue'); - } - - protected expectChordEqual(expected: Chord | null, actual: Chord): void { - expect(!expected).to.equal(!actual); - if (expected) { - // expect(actual.name).to.equal(expected.name, 'Mismatch on Name'); - } - } - - protected expectBendPointsEqual(expected: BendPoint[] | null, actual: BendPoint[] | null): void { - if(expected == null || actual == null) { - expect(actual).to.equal(expected) - return; - } - expect(actual.length).to.equal(expected.length, 'Mismatch on Count'); - for (let i: number = 0; i < expected.length; i++) { - expect(actual[i].value).to.equal(actual[i].value); - expect(actual[i].offset).to.equal(actual[i].offset); - } - } - - protected expectPlaybackInformationEqual(expected: PlaybackInformation, actual: PlaybackInformation): void { - expect(actual.volume).to.equal(expected.volume, 'Mismatch on Volume'); - expect(actual.balance).to.equal(expected.balance, 'Mismatch on Balance'); - // expect(actual.port).to.equal(expected.port, "Mismatch on Port"); - expect(actual.program).to.equal(expected.program, 'Mismatch on Program'); - // expect(actual.primaryChannel).to.equal(expected.primaryChannel, "Mismatch on PrimaryChannel"); - // expect(actual.secondaryChannel).to.equal(expected.secondaryChannel, "Mismatch on SecondaryChannel"); - expect(actual.isMute).to.equal(expected.isMute, 'Mismatch on IsMute'); - expect(actual.isSolo).to.equal(expected.isSolo, 'Mismatch on IsSolo'); - } - - protected expectMasterBarEqual(expected: MasterBar, actual: MasterBar): void { - expect(actual.alternateEndings).to.equal(expected.alternateEndings, 'Mismatch on AlternateEndings'); - expect(actual.index).to.equal(expected.index, 'Mismatch on Index'); - expect(actual.keySignature).to.equal(expected.keySignature, 'Mismatch on KeySignature'); - expect(actual.keySignatureType).to.equal(expected.keySignatureType, 'Mismatch on KeySignatureType'); - expect(actual.isDoubleBar).to.equal(expected.isDoubleBar, 'Mismatch on IsDoubleBar'); - expect(actual.isRepeatStart).to.equal(expected.isRepeatStart, 'Mismatch on IsRepeatStart'); - expect(actual.repeatCount).to.equal(expected.repeatCount, 'Mismatch on RepeatCount'); - expect(actual.timeSignatureNumerator).to.equal( - expected.timeSignatureNumerator, - 'Mismatch on TimeSignatureNumerator' - ); - expect(actual.timeSignatureDenominator).to.equal( - expected.timeSignatureDenominator, - 'Mismatch on TimeSignatureDenominator' - ); - expect(actual.tripletFeel).to.equal(expected.tripletFeel, 'Mismatch on TripletFeel'); - this.expectSectionEqual(expected.section!, actual.section); - expect(actual.start).to.equal(expected.start, 'Mismatch on Start'); - } - - protected expectSectionEqual(expected: Section | null, actual: Section | null): void { - expect(!actual).to.equal(!expected); - if (expected && actual) { - expect(actual.text).to.equal(expected.text, 'Mismatch on Text'); - expect(actual.marker).to.equal(expected.marker, 'Mismatch on Marker'); - } - } } diff --git a/test/importer/MusicXmlImporterTestSuite.test.ts b/test/importer/MusicXmlImporterTestSuite.test.ts index d9abc0cc2..f8a3581ea 100644 --- a/test/importer/MusicXmlImporterTestSuite.test.ts +++ b/test/importer/MusicXmlImporterTestSuite.test.ts @@ -1,617 +1,796 @@ import { MusicXmlImporterTestHelper } from '@test/importer/MusicXmlImporterTestHelper'; describe('MusicXmlImporterTestSuiteTests', () => { - it('01a_Pitches_Pitches', async () => { + it('01a-Pitches-Pitches', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/01a-Pitches-Pitches.xml'); + // Forcing accidentals doesn't exist 100% in alphatab, its rather: "try to adjust the note to this accidental" + // once we have really forced accidentals (even if repeated or not needed) this will slightly change. }); - it('01b_Pitches_Intervals', async () => { + it('01b-Pitches-Intervals', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/01b-Pitches-Intervals.xml'); }); - it('01c_Pitches_NoVoiceElement', async () => { + it('01c-Pitches-NoVoiceElement', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/01c-Pitches-NoVoiceElement.xml' ); }); - it('01d_Pitches_Microtones', async () => { + it('01d-Pitches-Microtones', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/01d-Pitches-Microtones.xml'); + // not supported }); - it('01e_Pitches_ParenthesizedAccidentals', async () => { + it('01e-Pitches-ParenthesizedAccidentals', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/01e-Pitches-ParenthesizedAccidentals.xml' ); + // not supported }); - it('01f_Pitches_ParenthesizedMicrotoneAccidentals', async () => { + it('01f-Pitches-ParenthesizedMicrotoneAccidentals', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.xml' ); + // not supported }); - it('02a_Rests_Durations', async () => { + it('02a-Rests-Durations', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/02a-Rests-Durations.xml'); + // Multibar rests are automatic, no support for individual bars forcing it. }); - it('02b_Rests_PitchedRests', async () => { + it('02b-Rests-PitchedRests', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/02b-Rests-PitchedRests.xml'); + // not supported }); - it('02c_Rests_MultiMeasureRests', async () => { + it('02c-Rests-MultiMeasureRests', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/02c-Rests-MultiMeasureRests.xml' ); + // not supported }); - it('02d_Rests_Multimeasure_TimeSignatures', async () => { + it('02d-Rests-Multimeasure-TimeSignatures', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/02d-Rests-Multimeasure-TimeSignatures.xml' ); + // not supported }); - it('02e_Rests_NoType', async () => { + it('02e-Rests-NoType', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/02e-Rests-NoType.xml'); }); - it('03a_Rhythm_Durations', async () => { + it('03a-Rhythm-Durations', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/03a-Rhythm-Durations.xml'); }); - it('03b_Rhythm_Backup', async () => { + it('03b-Rhythm-Backup', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/03b-Rhythm-Backup.xml'); }); - it('03c_Rhythm_DivisionChange', async () => { - await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.xml'); + it('03c-Rhythm-DivisionChange', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/03c-Rhythm-DivisionChange.xml' + ); }); - it('03d_Rhythm_DottedDurations_Factors', async () => { + it('03d-Rhythm-DottedDurations-Factors', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/03d-Rhythm-DottedDurations-Factors.xml' ); }); - it('11a_TimeSignatures', async () => { + it('03e-Rhythm-No-Divisions', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/03e-Rhythm-No-Divisions.xml'); + }); + + it('03f-Rhythm-Forward', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/03f-Rhythm-Forward.xml'); + }); + + it('11a-TimeSignatures', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/11a-TimeSignatures.xml'); }); - it('11b_TimeSignatures_NoTime', async () => { - await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.xml'); + it('11b-TimeSignatures-NoTime', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/11b-TimeSignatures-NoTime.xml' + ); + // hiding of time signatures not supported }); - it('11c_TimeSignatures_CompoundSimple', async () => { + it('11c-TimeSignatures-CompoundSimple', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/11c-TimeSignatures-CompoundSimple.xml' ); + // compound not supported, but we show the summed values }); - it('11d_TimeSignatures_CompoundMultiple', async () => { + it('11d-TimeSignatures-CompoundMultiple', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/11d-TimeSignatures-CompoundMultiple.xml' ); + // multiple time signatures not supported, initial one is shown as summed valued. }); - it('11e_TimeSignatures_CompoundMixed', async () => { + it('11e-TimeSignatures-CompoundMixed', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/11e-TimeSignatures-CompoundMixed.xml' ); + // multiple time signatures not supported, initial one is shown as summed valued. }); - it('11f_TimeSignatures_SymbolMeaning', async () => { + it('11f-TimeSignatures-SymbolMeaning', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/11f-TimeSignatures-SymbolMeaning.xml' ); + // unclear expectation }); - it('11g_TimeSignatures_SingleNumber', async () => { + it('11g-TimeSignatures-SingleNumber', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/11g-TimeSignatures-SingleNumber.xml' ); + // single number time signature not supported }); - it('11h_TimeSignatures_SenzaMisura', async () => { + it('11h-TimeSignatures-SenzaMisura', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/11h-TimeSignatures-SenzaMisura.xml' ); + // not supported }); - it('12a_Clefs', async () => { + it('12a-Clefs', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/12a-Clefs.xml'); + // mid-bar clef changes not supported + // there are also some clef variations we don't support. }); - it('12b_Clefs_NoKeyOrClef', async () => { + it('12b-Clefs-NoKeyOrClef', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/12b-Clefs-NoKeyOrClef.xml'); }); - it('13a_KeySignatures', async () => { + it('13a-KeySignatures', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/13a-KeySignatures.xml'); + // only classical key signatures (no double flat, double sharp) + // repeating time signatures hidden, no option to force display yet }); - it('13b_KeySignatures_ChurchModes', async () => { + it('13b-KeySignatures-ChurchModes', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/13b-KeySignatures-ChurchModes.xml' ); + // no mid-bar key signature changes. }); - it('13c_KeySignatures_NonTraditional', async () => { + it('13c-KeySignatures-NonTraditional', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/13c-KeySignatures-NonTraditional.xml' ); + // not supported. }); - it('13d_KeySignatures_Microtones', async () => { + it('13d-KeySignatures-Microtones', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/13d-KeySignatures-Microtones.xml' ); + // not supported. }); - it('14a_StaffDetails_LineChanges', async () => { + it('13e-KeySignatures-Cancel', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/13e-KeySignatures-Cancel.xml'); + // not supported to force show cancellation + }); + + it('13f-KeySignatures-Visible', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/13f-KeySignatures-Visible.xml' + ); + // not supported + }); + + it('14a-StaffDetails-LineChanges', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/14a-StaffDetails-LineChanges.xml' ); + // not supported. }); - it('21a_Chord_Basic', async () => { + it('21a-Chord-Basic', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/21a-Chord-Basic.xml'); }); - it('21b_Chords_TwoNotes', async () => { + it('21b-Chords-TwoNotes', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/21b-Chords-TwoNotes.xml'); }); - it('21c_Chords_ThreeNotesDuration', async () => { + it('21c-Chords-ThreeNotesDuration', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/21c-Chords-ThreeNotesDuration.xml' ); }); - it('21d_Chords_SchubertStabatMater', async () => { + it('21d-Chords-SchubertStabatMater', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/21d-Chords-SchubertStabatMater.xml' ); + // fp dynamics not yet supported }); - it('21e_Chords_PickupMeasures', async () => { - await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/21e-Chords-PickupMeasures.xml'); + it('21e-Chords-PickupMeasures', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/21e-Chords-PickupMeasures.xml' + ); }); - it('21f_Chord_ElementInBetween', async () => { + it('21f-Chord-ElementInBetween', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/21f-Chord-ElementInBetween.xml' ); + // mid-bar segno not supported }); - it('22a_Noteheads', async () => { + it('21g-Chords-Tremolos', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/21g-Chords-Tremolos.xml'); + // 4 bar tremolo (not supported) + }); + + it('21h-Chord-Accidentals', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/21h-Chord-Accidentals.xml'); + // natural not shown as not needed (forcing to show not available yet) + // brackets and braces on accidentals not supported + }); + + it('22a-Noteheads', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/22a-Noteheads.xml'); + // some slight stem alignment problems as we do not respect note head position yet + // part of https://github.com/CoderLine/alphaTab/issues/1949 }); - it('22b_Staff_Notestyles', async () => { + it('22b-Staff-Notestyles', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/22b-Staff-Notestyles.xml'); + // hiding stem not supported + // hiding staff line not supported }); - it('22c_Noteheads_Chords', async () => { + it('22c-Noteheads-Chords', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/22c-Noteheads-Chords.xml'); }); - it('22d_Parenthesized_Noteheads', async () => { + it('22d-Parenthesized-Noteheads', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/22d-Parenthesized-Noteheads.xml' ); }); - it('23a_Tuplets', async () => { + it('23a-Tuplets', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/23a-Tuplets.xml'); + // number is sometimes n:m }); - it('23b_Tuplets_Styles', async () => { + it('23b-Tuplets-Styles', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/23b-Tuplets-Styles.xml'); + // not supported }); - it('23c_Tuplet_Display_NonStandard', async () => { + it('23c-Tuplet-Display-NonStandard', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/23c-Tuplet-Display-NonStandard.xml' ); + // customizing display not supported }); - it('23d_Tuplets_Nested', async () => { + it('23d-Tuplets-Nested', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/23d-Tuplets-Nested.xml'); + // not supported }); - it('23e_Tuplets_Tremolo', async () => { + it('23e-Tuplets-Tremolo', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/23e-Tuplets-Tremolo.xml'); }); - it('23f_Tuplets_DurationButNoBracket', async () => { + it('23f-Tuplets-DurationButNoBracket', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/23f-Tuplets-DurationButNoBracket.xml' ); + // not supported }); - it('24a_GraceNotes', async () => { + it('24a-GraceNotes', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/24a-GraceNotes.xml'); + // not all styles are correct }); - it('24b_ChordAsGraceNote', async () => { + it('24b-ChordAsGraceNote', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/24b-ChordAsGraceNote.xml'); }); - it('24c_GraceNote_MeasureEnd', async () => { + it('24c-GraceNote-MeasureEnd', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/24c-GraceNote-MeasureEnd.xml'); }); - it('24d_AfterGrace', async () => { + it('24d-AfterGrace', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/24d-AfterGrace.xml'); }); - it('24e_GraceNote_StaffChange', async () => { - await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/24e-GraceNote-StaffChange.xml'); + it('24e-GraceNote-StaffChange', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/24e-GraceNote-StaffChange.xml' + ); }); - it('24f_GraceNote_Slur', async () => { + it('24f-GraceNote-Slur', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/24f-GraceNote-Slur.xml'); }); - it('31a_Directions', async () => { + it('24g-GraceNote-Dynamics', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/24g-GraceNote-Dynamics.xml'); + }); + + it('24h-GraceNote-Simultaneous', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/24h-GraceNote-Simultaneous.xml' + ); + }); + + it('31a-Directions', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/31a-Directions.xml'); + // many directions not supported yet. + }); + + it('31b-Directions-Order', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/31b-Directions-Order.xml'); + // not supported }); - it('31c_MetronomeMarks', async () => { + it('31c-MetronomeMarks', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/31c-MetronomeMarks.xml'); + // only classical BPM tempo changes supported (no dots, brackets or combined annotations) + }); + + it('31d-Directions-Compounds', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/31d-Directions-Compounds.xml'); }); - it('32a_Notations', async () => { + it('32a-Notations', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/32a-Notations.xml'); + // various annotations not supported, the ones we support seem fine }); - it('32b_Articulations_Texts', async () => { + it('32b-Articulations-Texts', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/32b-Articulations-Texts.xml'); + // no formatted text support }); - it('32c_MultipleNotationChildren', async () => { + it('32c-MultipleNotationChildren', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/32c-MultipleNotationChildren.xml' ); }); - it('32d_Arpeggio', async () => { + it('32d-Arpeggio', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/32d-Arpeggio.xml'); + // no partial or brackets. }); - it('33a_Spanners', async () => { + it('33a-Spanners', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/33a-Spanners.xml'); + // no general spanners + // pedal only with text }); - it('33b_Spanners_Tie', async () => { + it('33b-Spanners-Tie', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/33b-Spanners-Tie.xml'); + // only automatic placement }); - it('33c_Spanners_Slurs', async () => { + it('33c-Spanners-Slurs', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/33c-Spanners-Slurs.xml'); }); - it('33d_Spanners_OctaveShifts', async () => { - await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/33d-Spanners-OctaveShifts.xml'); + it('33da-Spanners-OctaveShifts-before', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/33da-Spanners-OctaveShifts-before.xml' + ); + // not supported + }); + + it('33db-Spanners-OctaveShifts-after', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/33db-Spanners-OctaveShifts-after.xml' + ); + // not supported }); - it('33e_Spanners_OctaveShifts_InvalidSize', async () => { + it('33e-Spanners-OctaveShifts-InvalidSize', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/33e-Spanners-OctaveShifts-InvalidSize.xml' ); }); - it('33f_Trill_EndingOnGraceNote', async () => { + it('33f-Trill-EndingOnGraceNote', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/33f-Trill-EndingOnGraceNote.xml' ); + // not supported }); - it('33g_Slur_ChordedNotes', async () => { + it('33g-Slur-ChordedNotes', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/33g-Slur-ChordedNotes.xml'); + // slur starting on exact note. }); - it('33h_Spanners_Glissando', async () => { + it('33h-Spanners-Glissando', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/33h-Spanners-Glissando.xml'); + // only normal slide }); - it('33i_Ties_NotEnded', async () => { + it('33i-Ties-NotEnded', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/33i-Ties-NotEnded.xml'); }); - it('41a_MultiParts_Partorder', async () => { + it('33j-Beams-Tremolos', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/33j-Beams-Tremolos.xml'); + // not supported + }); + + it('34a-Print-Object-Spanners', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/34a-Print-Object-Spanners.xml' + ); + }); + + it('34b-Colors', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/34b-Colors.xml'); + }); + + it('34c-Font-Size', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/34c-Font-Size.xml'); + }); + + it('41a-MultiParts-Partorder', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/41a-MultiParts-Partorder.xml'); }); - it('41b_MultiParts_MoreThan10', async () => { - await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.xml'); + it('41b-MultiParts-MoreThan10', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/41b-MultiParts-MoreThan10.xml' + ); }); - it('41c_StaffGroups', async () => { + it('41c-StaffGroups', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/41c-StaffGroups.xml'); }); - it('41d_StaffGroups_Nested', async () => { + it('41d-StaffGroups-Nested', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/41d-StaffGroups-Nested.xml'); }); - it('41e_StaffGroups_InstrumentNames_Linebroken', async () => { + it('41e-StaffGroups-InstrumentNames-Linebroken', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/41e-StaffGroups-InstrumentNames-Linebroken.xml' ); }); - it('41f_StaffGroups_Overlapping', async () => { + it('41f-StaffGroups-Overlapping', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/41f-StaffGroups-Overlapping.xml' ); }); - it('41g_PartNoId', async () => { - await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/41g-PartNoId.xml'); + it('41g-StaffGroups-NestingOrder', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/41g-StaffGroups-NestingOrder.xml' + ); }); - it('41h_TooManyParts', async () => { + it('41h-TooManyParts', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/41h-TooManyParts.xml'); }); - it('41i_PartNameDisplay_Override', async () => { + it('41i-PartNameDisplay-Override', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/41i-PartNameDisplay-Override.xml' ); }); - it('42a_MultiVoice_TwoVoicesOnStaff_Lyrics', async () => { + it('41j-PartNameDisplay-Multiple-DisplayText-Children', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/41j-PartNameDisplay-Multiple-DisplayText-Children.xml' + ); + // styling not supported + }); + + it('41k-PartName-Print', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/41k-PartName-Print.xml'); + }); + + it('41l-GroupNameDisplay-Override', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/41l-GroupNameDisplay-Override.xml' + ); + }); + + it('42a-MultiVoice-TwoVoicesOnStaff-Lyrics', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.xml' ); }); - it('42b_MultiVoice_MidMeasureClefChange', async () => { + it('42b-MultiVoice-MidMeasureClefChange', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/42b-MultiVoice-MidMeasureClefChange.xml' ); }); - it('43a_PianoStaff', async () => { + it('43a-PianoStaff', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/43a-PianoStaff.xml'); }); - it('43b_MultiStaff_DifferentKeys', async () => { + it('43b-MultiStaff-DifferentKeys', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/43b-MultiStaff-DifferentKeys.xml' ); + // not supported }); - it('43c_MultiStaff_DifferentKeysAfterBackup', async () => { + it('43c-MultiStaff-DifferentKeysAfterBackup', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/43c-MultiStaff-DifferentKeysAfterBackup.xml' ); + // not supported }); - it('43d_MultiStaff_StaffChange', async () => { + it('43d-MultiStaff-StaffChange', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/43d-MultiStaff-StaffChange.xml' ); + // filling with rests where needed, cross staff beams not supported }); - it('43e_Multistaff_ClefDynamics', async () => { + it('43e-Multistaff-ClefDynamics', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.xml' ); }); - it('45a_SimpleRepeat', async () => { - await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/45a-SimpleRepeat.xml'); + it('43f-MultiStaff-Lyrics', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.xml'); }); - it('45b_RepeatWithAlternatives', async () => { + it('43g-MultiStaff-PartSymbol', async () => { await MusicXmlImporterTestHelper.testReferenceFile( - 'test-data/musicxml-testsuite/45b-RepeatWithAlternatives.xml' + 'test-data/musicxml-testsuite/43g-MultiStaff-PartSymbol.xml' ); }); - it('45c_RepeatMultipleTimes', async () => { - await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/45c-RepeatMultipleTimes.xml'); + it('45a-SimpleRepeat', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/45a-SimpleRepeat.xml'); }); - it('45d_Repeats_Nested_Alternatives', async () => { + it('45b-RepeatWithAlternatives', async () => { await MusicXmlImporterTestHelper.testReferenceFile( - 'test-data/musicxml-testsuite/45d-Repeats-Nested-Alternatives.xml' + 'test-data/musicxml-testsuite/45b-RepeatWithAlternatives.xml' ); }); - it('45e_Repeats_Nested_Alternatives', async () => { + it('45c-SimpleRepeat-Nested', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/45c-SimpleRepeat-Nested.xml'); + }); + + it('45d-Repeats-MultipleEndings', async () => { await MusicXmlImporterTestHelper.testReferenceFile( - 'test-data/musicxml-testsuite/45e-Repeats-Nested-Alternatives.xml' + 'test-data/musicxml-testsuite/45d-Repeats-MultipleEndings.xml' ); }); - it('45f_Repeats_InvalidEndings', async () => { + it('45e-Repeats-Combination', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/45e-Repeats-Combination.xml'); + }); + + it('45f-Repeats-InvalidEndings', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/45f-Repeats-InvalidEndings.xml' ); }); - it('45g_Repeats_NotEnded', async () => { + it('45g-Repeats-NotEnded', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/45g-Repeats-NotEnded.xml'); }); - it('46a_Barlines', async () => { + it('45h-Repeats-Partial', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/45h-Repeats-Partial.xml'); + }); + + it('45i-Repeats-Nested', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/45i-Repeats-Nested.xml'); + }); + + it('46a-Barlines', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/46a-Barlines.xml'); + // only double bar }); - it('46b_MidmeasureBarline', async () => { + it('46b-MidmeasureBarline', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/46b-MidmeasureBarline.xml'); + // not supported }); - it('46c_Midmeasure_Clef', async () => { + it('46c-Midmeasure-Clef', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/46c-Midmeasure-Clef.xml'); + // not supported }); - it('46d_PickupMeasure_ImplicitMeasures', async () => { + it('46d-PickupMeasure-ImplicitMeasures', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/46d-PickupMeasure-ImplicitMeasures.xml' ); }); - it('46e_PickupMeasure_SecondVoiceStartsLater', async () => { + it('46e-PickupMeasure-SecondVoiceStartsLater', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/46e-PickupMeasure-SecondVoiceStartsLater.xml' ); }); - it('46f_IncompleteMeasures', async () => { + it('46f-IncompleteMeasures', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/46f-IncompleteMeasures.xml'); }); - it('46g_PickupMeasure_Chordnames_FiguredBass', async () => { + it('46g-PickupMeasure-Chordnames-FiguredBass', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/46g-PickupMeasure-Chordnames-FiguredBass.xml' ); }); - it('51b_Header_Quotes', async () => { + it('51b-Header-Quotes', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/51b-Header-Quotes.xml'); }); - it('51c_MultipleRights', async () => { + it('51c-MultipleRights', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/51c-MultipleRights.xml'); }); - it('51d_EmptyTitle', async () => { + it('51d-EmptyTitle', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/51d-EmptyTitle.xml'); }); - it('52a_PageLayout', async () => { + it('52a-PageLayout', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/52a-PageLayout.xml'); }); - it('52b_Breaks', async () => { + it('52b-Breaks', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/52b-Breaks.xml'); }); - it('61a_Lyrics', async () => { + it('61a-Lyrics', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/61a-Lyrics.xml'); + // no syllables }); - it('61b_MultipleLyrics', async () => { + it('61b-MultipleLyrics', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/61b-MultipleLyrics.xml'); }); - it('61c_Lyrics_Pianostaff', async () => { + it('61c-Lyrics-Pianostaff', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/61c-Lyrics-Pianostaff.xml'); }); - it('61d_Lyrics_Melisma', async () => { + it('61d-Lyrics-Melisma', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/61d-Lyrics-Melisma.xml'); }); - it('61e_Lyrics_Chords', async () => { + it('61e-Lyrics-Chords', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/61e-Lyrics-Chords.xml'); }); - it('61f_Lyrics_GracedNotes', async () => { + it('61f-Lyrics-GracedNotes', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/61f-Lyrics-GracedNotes.xml'); }); - it('61g_Lyrics_NameNumber', async () => { + it('61g-Lyrics-NameNumber', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/61g-Lyrics-NameNumber.xml'); }); - it('61h_Lyrics_BeamsMelismata', async () => { - await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.xml'); + it('61h-Lyrics-BeamsMelismata', async () => { + await MusicXmlImporterTestHelper.testReferenceFile( + 'test-data/musicxml-testsuite/61h-Lyrics-BeamsMelismata.xml' + ); }); - it('61i_Lyrics_Chords', async () => { + it('61i-Lyrics-Chords', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/61i-Lyrics-Chords.xml'); }); - it('61j_Lyrics_Elisions', async () => { + it('61j-Lyrics-Elisions', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/61j-Lyrics-Elisions.xml'); }); - it('61k_Lyrics_SpannersExtenders', async () => { + it('61k-Lyrics-SpannersExtenders', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/61k-Lyrics-SpannersExtenders.xml' ); }); - it('71a_Chordnames', async () => { + it('71a-Chordnames', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/71a-Chordnames.xml'); }); - it('71c_ChordsFrets', async () => { + it('71c-ChordsFrets', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/71c-ChordsFrets.xml'); }); - it('71d_ChordsFrets_Multistaff', async () => { + it('71d-ChordsFrets-Multistaff', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/71d-ChordsFrets-Multistaff.xml' ); }); - it('71e_TabStaves', async () => { + it('71e-TabStaves', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/71e-TabStaves.xml'); }); - it('71f_AllChordTypes', async () => { + it('71f-AllChordTypes', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/71f-AllChordTypes.xml'); }); - it('71g_MultipleChordnames', async () => { + it('71g-MultipleChordnames', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/71g-MultipleChordnames.xml'); }); - it('72a_TransposingInstruments', async () => { + it('72a-TransposingInstruments', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/72a-TransposingInstruments.xml' ); }); - it('72b_TransposingInstruments_Full', async () => { + it('72b-TransposingInstruments-Full', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/72b-TransposingInstruments-Full.xml' ); }); - it('72c_TransposingInstruments_Change', async () => { + it('72c-TransposingInstruments-Change', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/72c-TransposingInstruments-Change.xml' ); + // broken }); - it('73a_Percussion', async () => { + it('73a-Percussion', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/73a-Percussion.xml'); }); - it('74a_FiguredBass', async () => { + it('74a-FiguredBass', async () => { await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/74a-FiguredBass.xml'); + // not supported }); - it('75a_AccordionRegistrations', async () => { + it('75a-AccordionRegistrations', async () => { await MusicXmlImporterTestHelper.testReferenceFile( 'test-data/musicxml-testsuite/75a-AccordionRegistrations.xml' ); + // not supported }); - it('99a_Sibelius5_IgnoreBeaming', async () => { - await MusicXmlImporterTestHelper.testReferenceFile( - 'test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.xml' - ); + it('90a-Compressed-MusicXML', async () => { + await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml-testsuite/90a-Compressed-MusicXML.mxl'); }); - it('99b_Lyrics_BeamsMelismata_IgnoreBeams', async () => { + it('99a-Sibelius5-IgnoreBeaming', async () => { await MusicXmlImporterTestHelper.testReferenceFile( - 'test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.xml' + 'test-data/musicxml-testsuite/99a-Sibelius5-IgnoreBeaming.xml' ); }); - it('100a_Guitare_Bends', async () => { + it('99b-Lyrics-BeamsMelismata-IgnoreBeams', async () => { await MusicXmlImporterTestHelper.testReferenceFile( - 'test-data/musicxml-testsuite/100a-Guitare-Bends.xml' + 'test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.xml' ); }); - - }); diff --git a/test/importer/__snapshots__/alphateximporter.test.ts.snap b/test/importer/__snapshots__/alphateximporter.test.ts.snap new file mode 100644 index 000000000..23da546df --- /dev/null +++ b/test/importer/__snapshots__/alphateximporter.test.ts.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlphaTexImporterTest barlines 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + ], + "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, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 1, + "barlineright" => 2, + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 4, + "barlineright" => 4, + }, + ], + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 7, + "barlineright" => 6, + }, + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 5, + "barlineright" => 1, + }, + ], + }, + ], + "playbackinfo" => Map { + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; diff --git a/test/importer/__snapshots__/musicxmlimporter.test.ts.snap b/test/importer/__snapshots__/musicxmlimporter.test.ts.snap new file mode 100644 index 000000000..598781b56 --- /dev/null +++ b/test/importer/__snapshots__/musicxmlimporter.test.ts.snap @@ -0,0 +1,3212 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MusicXmlImporterTests barlines 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "timesignaturecommon" => true, + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + Map { + "__kind" => "MasterBar", + "start" => 11520, + }, + Map { + "__kind" => "MasterBar", + "start" => 15360, + }, + Map { + "__kind" => "MasterBar", + "start" => 19200, + }, + Map { + "__kind" => "MasterBar", + "start" => 23040, + }, + Map { + "__kind" => "MasterBar", + "start" => 26880, + }, + Map { + "__kind" => "MasterBar", + "start" => 30720, + }, + Map { + "__kind" => "MasterBar", + "start" => 34560, + }, + Map { + "__kind" => "MasterBar", + "start" => 38400, + }, + Map { + "__kind" => "MasterBar", + "start" => 42240, + }, + Map { + "__kind" => "MasterBar", + "start" => 46080, + }, + Map { + "__kind" => "MasterBar", + "start" => 49920, + }, + Map { + "__kind" => "MasterBar", + "start" => 53760, + }, + Map { + "__kind" => "MasterBar", + "start" => 57600, + }, + Map { + "__kind" => "MasterBar", + "start" => 61440, + }, + Map { + "__kind" => "MasterBar", + "start" => 65280, + }, + ], + "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, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 1, + "barlineright" => 2, + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 1, + "barlineright" => 4, + }, + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 4, + "barlineright" => 4, + }, + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 1, + "barlineright" => 9, + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 1, + "barlineright" => 1, + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 9, + }, + Map { + "__kind" => "Bar", + "id" => 6, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 1, + }, + Map { + "__kind" => "Bar", + "id" => 7, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 2, + }, + Map { + "__kind" => "Bar", + "id" => 8, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 3, + }, + Map { + "__kind" => "Bar", + "id" => 9, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 9, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 9, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 4, + }, + Map { + "__kind" => "Bar", + "id" => 10, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 10, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 10, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 5, + }, + Map { + "__kind" => "Bar", + "id" => 11, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 11, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 11, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 6, + }, + Map { + "__kind" => "Bar", + "id" => 12, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 12, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 12, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 7, + }, + Map { + "__kind" => "Bar", + "id" => 13, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 13, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 13, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 8, + }, + Map { + "__kind" => "Bar", + "id" => 14, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 14, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 14, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 9, + }, + Map { + "__kind" => "Bar", + "id" => 15, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 15, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 15, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 10, + }, + Map { + "__kind" => "Bar", + "id" => 16, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 16, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 16, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineright" => 11, + }, + Map { + "__kind" => "Bar", + "id" => 17, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 17, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 17, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "secondarychannel" => 1, + }, + }, + ], + "stylesheet" => Map { + "hidedynamics" => true, + }, +} +`; + +exports[`MusicXmlImporterTests partwise-anacrusis 1`] = ` +Map { + "__kind" => "Score", + "artist" => "Artist", + "copyright" => "Copyright", + "music" => "Music", + "notices" => "Notices", + "title" => "Title", + "words" => "Words", + "tab" => "Tab", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "isanacrusis" => true, + }, + Map { + "__kind" => "MasterBar", + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + "isanacrusis" => true, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "secondarychannel" => 1, + }, + "name" => "Track 1", + "shortname" => "T1", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 1, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 6, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 7, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 8, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 9, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 9, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 9, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "program" => 1, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "Track 2", + "shortname" => "T2", + }, + ], + "stylesheet" => Map { + "hidedynamics" => true, + }, +} +`; + +exports[`MusicXmlImporterTests partwise-basic 1`] = ` +Map { + "__kind" => "Score", + "artist" => "Artist", + "copyright" => "Copyright", + "music" => "Music", + "notices" => "Notices", + "title" => "Title", + "words" => "Words", + "tab" => "Tab", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "secondarychannel" => 1, + }, + "name" => "Track 1", + "shortname" => "T1", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 1, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "program" => 1, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "Track 2", + "shortname" => "T2", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 6, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 2, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 7, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 8, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "program" => 2, + "primarychannel" => 4, + "secondarychannel" => 5, + }, + "name" => "Track 3", + "shortname" => "T3", + }, + ], + "stylesheet" => Map { + "hidedynamics" => true, + }, +} +`; + +exports[`MusicXmlImporterTests partwise-complex-measures 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "timesignaturecommon" => true, + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + ], + "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, + }, + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 5, + "tone" => 2, + }, + ], + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 2, + "octave" => 5, + "tone" => 0, + }, + ], + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 1440, + "playbackduration" => 1440, + "overridedisplayduration" => 1440, + }, + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "duration" => 256, + "displaystart" => 2400, + "playbackstart" => 2400, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + Map { + "__kind" => "Beat", + "id" => 3, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 3, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 16, + "displaystart" => 2640, + "playbackstart" => 2640, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 6, + "octave" => 5, + "tone" => 0, + }, + Map { + "__kind" => "Note", + "id" => 7, + "octave" => 5, + "tone" => 2, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + Map { + "__kind" => "Beat", + "id" => 9, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 8, + "octave" => 5, + "tone" => 0, + }, + ], + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 1440, + "playbackduration" => 1440, + "overridedisplayduration" => 1440, + }, + Map { + "__kind" => "Beat", + "id" => 10, + "isempty" => true, + "duration" => 256, + "displaystart" => 2400, + "playbackstart" => 2400, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + Map { + "__kind" => "Beat", + "id" => 11, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 9, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 16, + "displaystart" => 2640, + "playbackstart" => 2640, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + ], + }, + ], + }, + ], + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "duration" => 256, + "displayduration" => 2880, + "playbackduration" => 2880, + "overridedisplayduration" => 2880, + }, + Map { + "__kind" => "Beat", + "id" => 5, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 4, + "octave" => 5, + "tone" => 0, + }, + ], + "displaystart" => 2880, + "playbackstart" => 2880, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "duration" => 256, + "displayduration" => 1500, + "playbackduration" => 1500, + "overridedisplayduration" => 1500, + }, + Map { + "__kind" => "Beat", + "id" => 7, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 5, + "octave" => 6, + "tone" => 0, + }, + ], + "duration" => 2, + "displaystart" => 1500, + "playbackstart" => 1500, + "displayduration" => 1920, + "playbackduration" => 1920, + "overridedisplayduration" => 1920, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 12, + "isempty" => true, + "duration" => 256, + "displayduration" => 2880, + "playbackduration" => 2880, + "overridedisplayduration" => 2880, + }, + Map { + "__kind" => "Beat", + "id" => 13, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 10, + "octave" => 5, + "tone" => 0, + }, + ], + "displaystart" => 2880, + "playbackstart" => 2880, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 14, + "isempty" => true, + "duration" => 256, + "displayduration" => 1500, + "playbackduration" => 1500, + "overridedisplayduration" => 1500, + }, + Map { + "__kind" => "Beat", + "id" => 15, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 11, + "octave" => 6, + "tone" => 0, + }, + ], + "duration" => 2, + "displaystart" => 1500, + "playbackstart" => 1500, + "displayduration" => 1920, + "playbackduration" => 1920, + "overridedisplayduration" => 1920, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "secondarychannel" => 1, + }, + "name" => "Track 1", + "shortname" => "Track 1", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 16, + "isempty" => true, + "duration" => 256, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 1920, + "playbackduration" => 1920, + "overridedisplayduration" => 1920, + }, + Map { + "__kind" => "Beat", + "id" => 17, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 12, + "octave" => 6, + "tone" => 0, + }, + ], + "duration" => 2, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 1920, + "playbackduration" => 1920, + "overridedisplayduration" => 1920, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 18, + "isempty" => true, + "duration" => 256, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + Map { + "__kind" => "Beat", + "id" => 19, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 13, + "octave" => 6, + "tone" => 0, + }, + ], + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 20, + "isempty" => true, + "duration" => 256, + "displayduration" => 1428, + "playbackduration" => 1428, + "overridedisplayduration" => 1428, + }, + Map { + "__kind" => "Beat", + "id" => 21, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 14, + "octave" => 6, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 1428, + "playbackstart" => 1428, + "displayduration" => 480, + "playbackduration" => 480, + "overridedisplayduration" => 480, + }, + ], + }, + ], + "keysignature" => 4, + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 9, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 22, + "isempty" => true, + "duration" => 256, + "displayduration" => 1920, + "playbackduration" => 1920, + "overridedisplayduration" => 1920, + }, + Map { + "__kind" => "Beat", + "id" => 23, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 15, + "octave" => 6, + "tone" => 0, + }, + ], + "duration" => 2, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 1920, + "playbackduration" => 1920, + "overridedisplayduration" => 1920, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 10, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 24, + "isempty" => true, + "duration" => 256, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + Map { + "__kind" => "Beat", + "id" => 25, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 16, + "octave" => 6, + "tone" => 0, + }, + ], + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 11, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 26, + "isempty" => true, + "duration" => 256, + "displayduration" => 1428, + "playbackduration" => 1428, + "overridedisplayduration" => 1428, + }, + Map { + "__kind" => "Beat", + "id" => 27, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 17, + "octave" => 6, + "tone" => 0, + }, + ], + "duration" => 8, + "displaystart" => 1428, + "playbackstart" => 1428, + "displayduration" => 480, + "playbackduration" => 480, + "overridedisplayduration" => 480, + }, + ], + }, + ], + "keysignature" => 4, + }, + ], + }, + ], + "playbackinfo" => Map { + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "Track 2", + "shortname" => "Track 2", + }, + ], + "stylesheet" => Map { + "hidedynamics" => true, + }, +} +`; + +exports[`MusicXmlImporterTests partwise-staff-change 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "timesignaturecommon" => true, + }, + ], + "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, + "duration" => 16, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + Map { + "__kind" => "Beat", + "id" => 13, + "isempty" => true, + "duration" => 256, + "displaystart" => 240, + "playbackstart" => 240, + "displayduration" => 2640, + "playbackduration" => 2640, + "overridedisplayduration" => 2640, + }, + Map { + "__kind" => "Beat", + "id" => 14, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 11, + "octave" => 5, + "tone" => 3, + }, + ], + "duration" => 16, + "displaystart" => 2880, + "playbackstart" => 2880, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 15, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 12, + "octave" => 4, + "tone" => 8, + }, + ], + "duration" => 16, + "displaystart" => 3120, + "playbackstart" => 3120, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 16, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 13, + "octave" => 5, + "tone" => 0, + "accidentalmode" => 3, + }, + ], + "duration" => 16, + "displaystart" => 3360, + "playbackstart" => 3360, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 17, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 14, + "octave" => 5, + "tone" => 3, + }, + ], + "duration" => 16, + "displaystart" => 3600, + "playbackstart" => 3600, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 1, + }, + ], + }, + ], + "keysignature" => 4, + }, + ], + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 1, + "clef" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "duration" => 256, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + Map { + "__kind" => "Beat", + "id" => 2, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 0, + "octave" => 3, + "tone" => 8, + }, + ], + "duration" => 16, + "displaystart" => 240, + "playbackstart" => 240, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 3, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 4, + "tone" => 0, + "accidentalmode" => 3, + }, + ], + "duration" => 16, + "displaystart" => 480, + "playbackstart" => 480, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 4, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 2, + "octave" => 4, + "tone" => 3, + }, + ], + "duration" => 16, + "displaystart" => 720, + "playbackstart" => 720, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 1, + }, + Map { + "__kind" => "Beat", + "id" => 5, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 3, + "octave" => 4, + "tone" => 8, + }, + ], + "duration" => 16, + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 6, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 4, + "octave" => 4, + "tone" => 0, + }, + ], + "duration" => 16, + "displaystart" => 1200, + "playbackstart" => 1200, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 7, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 5, + "octave" => 4, + "tone" => 3, + }, + ], + "duration" => 16, + "displaystart" => 1440, + "playbackstart" => 1440, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 8, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 6, + "octave" => 4, + "tone" => 8, + }, + ], + "duration" => 16, + "displaystart" => 1680, + "playbackstart" => 1680, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 1, + }, + Map { + "__kind" => "Beat", + "id" => 9, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 7, + "octave" => 5, + "tone" => 0, + "accidentalmode" => 3, + }, + ], + "duration" => 16, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 10, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 8, + "octave" => 4, + "tone" => 3, + }, + ], + "duration" => 16, + "displaystart" => 2160, + "playbackstart" => 2160, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 11, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 9, + "octave" => 4, + "tone" => 8, + }, + ], + "duration" => 16, + "displaystart" => 2400, + "playbackstart" => 2400, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 12, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 10, + "octave" => 5, + "tone" => 0, + }, + ], + "duration" => 16, + "displaystart" => 2640, + "playbackstart" => 2640, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 1, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 47, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 48, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 49, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 18, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 15, + "octave" => 3, + "tone" => 0, + "isstaccato" => true, + "accidentalmode" => 3, + }, + ], + "duration" => 8, + "displayduration" => 480, + "playbackduration" => 480, + "overridedisplayduration" => 480, + "preferredbeamdirection" => 1, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 19, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 16, + "octave" => 3, + "tone" => 8, + "isstaccato" => true, + }, + ], + "duration" => 8, + "displaystart" => 480, + "playbackstart" => 480, + "displayduration" => 480, + "playbackduration" => 480, + "overridedisplayduration" => 480, + "preferredbeamdirection" => 1, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 20, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 17, + "octave" => 3, + "tone" => 0, + "isstaccato" => true, + }, + ], + "duration" => 8, + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 480, + "playbackduration" => 480, + "overridedisplayduration" => 480, + "preferredbeamdirection" => 1, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 21, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 18, + "octave" => 3, + "tone" => 8, + "isstaccato" => true, + }, + ], + "duration" => 8, + "displaystart" => 1440, + "playbackstart" => 1440, + "displayduration" => 480, + "playbackduration" => 480, + "overridedisplayduration" => 480, + "preferredbeamdirection" => 1, + "beamingmode" => 1, + }, + Map { + "__kind" => "Beat", + "id" => 22, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 19, + "octave" => 3, + "tone" => 0, + "isstaccato" => true, + }, + ], + "duration" => 8, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 480, + "playbackduration" => 480, + "overridedisplayduration" => 480, + "preferredbeamdirection" => 1, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 23, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 20, + "octave" => 3, + "tone" => 8, + "isstaccato" => true, + }, + ], + "duration" => 8, + "displaystart" => 2400, + "playbackstart" => 2400, + "displayduration" => 480, + "playbackduration" => 480, + "overridedisplayduration" => 480, + "preferredbeamdirection" => 1, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 24, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 21, + "octave" => 3, + "tone" => 0, + "isstaccato" => true, + }, + ], + "duration" => 8, + "displaystart" => 2880, + "playbackstart" => 2880, + "displayduration" => 480, + "playbackduration" => 480, + "overridedisplayduration" => 480, + "preferredbeamdirection" => 1, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 25, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 22, + "octave" => 3, + "tone" => 8, + "isstaccato" => true, + }, + ], + "duration" => 8, + "displaystart" => 3360, + "playbackstart" => 3360, + "displayduration" => 480, + "playbackduration" => 480, + "overridedisplayduration" => 480, + "preferredbeamdirection" => 1, + "beamingmode" => 1, + }, + ], + }, + ], + "keysignature" => 4, + }, + ], + }, + ], + "playbackinfo" => Map { + "secondarychannel" => 1, + }, + "name" => "Track 1", + "shortname" => "Track 1", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 26, + "duration" => 16, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + Map { + "__kind" => "Beat", + "id" => 35, + "isempty" => true, + "duration" => 256, + "displaystart" => 240, + "playbackstart" => 240, + "displayduration" => 1680, + "playbackduration" => 1680, + "overridedisplayduration" => 1680, + }, + Map { + "__kind" => "Beat", + "id" => 36, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 30, + "octave" => 5, + "tone" => 1, + }, + ], + "duration" => 16, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 40, + "duration" => 16, + "displaystart" => 2160, + "playbackstart" => 2160, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + Map { + "__kind" => "Beat", + "id" => 41, + "duration" => 16, + "displaystart" => 2400, + "playbackstart" => 2400, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + Map { + "__kind" => "Beat", + "id" => 42, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 33, + "octave" => 5, + "tone" => 1, + }, + ], + "duration" => 16, + "displaystart" => 2640, + "playbackstart" => 2640, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 1, + }, + Map { + "__kind" => "Beat", + "id" => 43, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 34, + "octave" => 5, + "tone" => 4, + }, + ], + "duration" => 16, + "displaystart" => 2880, + "playbackstart" => 2880, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 44, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 35, + "octave" => 4, + "tone" => 8, + }, + ], + "duration" => 16, + "displaystart" => 3120, + "playbackstart" => 3120, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 45, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 36, + "octave" => 5, + "tone" => 1, + }, + ], + "duration" => 16, + "displaystart" => 3360, + "playbackstart" => 3360, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 46, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 37, + "octave" => 5, + "tone" => 4, + }, + ], + "duration" => 16, + "displaystart" => 3600, + "playbackstart" => 3600, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 1, + }, + ], + }, + ], + }, + ], + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 27, + "isempty" => true, + "duration" => 256, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + Map { + "__kind" => "Beat", + "id" => 28, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 23, + "octave" => 3, + "tone" => 8, + }, + ], + "duration" => 16, + "displaystart" => 240, + "playbackstart" => 240, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 29, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 24, + "octave" => 4, + "tone" => 1, + }, + ], + "duration" => 16, + "displaystart" => 480, + "playbackstart" => 480, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 30, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 25, + "octave" => 4, + "tone" => 4, + }, + ], + "duration" => 16, + "displaystart" => 720, + "playbackstart" => 720, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 1, + }, + Map { + "__kind" => "Beat", + "id" => 31, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 26, + "octave" => 4, + "tone" => 8, + }, + ], + "duration" => 16, + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 32, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 27, + "octave" => 4, + "tone" => 1, + }, + ], + "duration" => 16, + "displaystart" => 1200, + "playbackstart" => 1200, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 33, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 28, + "octave" => 4, + "tone" => 4, + }, + ], + "duration" => 16, + "displaystart" => 1440, + "playbackstart" => 1440, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 34, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 29, + "octave" => 4, + "tone" => 8, + }, + ], + "duration" => 16, + "displaystart" => 1680, + "playbackstart" => 1680, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 1, + }, + Map { + "__kind" => "Beat", + "id" => 37, + "isempty" => true, + "duration" => 256, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + }, + Map { + "__kind" => "Beat", + "id" => 38, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 31, + "octave" => 4, + "tone" => 4, + }, + ], + "duration" => 16, + "displaystart" => 2160, + "playbackstart" => 2160, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + Map { + "__kind" => "Beat", + "id" => 39, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 32, + "octave" => 4, + "tone" => 8, + }, + ], + "duration" => 16, + "displaystart" => 2400, + "playbackstart" => 2400, + "displayduration" => 240, + "playbackduration" => 240, + "overridedisplayduration" => 240, + "preferredbeamdirection" => 0, + "beamingmode" => 2, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "Track 2", + "shortname" => "Track 2", + }, + ], + "stylesheet" => Map { + "hidedynamics" => true, + }, +} +`; + +exports[`MusicXmlImporterTests timewise-anacrusis 1`] = ` +Map { + "__kind" => "Score", + "artist" => "Artist", + "copyright" => "Copyright", + "music" => "Music", + "notices" => "Notices", + "title" => "Title", + "words" => "Words", + "tab" => "Tab", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "isanacrusis" => true, + }, + Map { + "__kind" => "MasterBar", + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + "isanacrusis" => true, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 6, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 8, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "secondarychannel" => 1, + }, + "name" => "Track 1", + "shortname" => "T1", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 1, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 7, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 9, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 9, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 9, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "program" => 1, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "Track 2", + "shortname" => "T2", + }, + ], + "stylesheet" => Map { + "hidedynamics" => true, + }, +} +`; + +exports[`MusicXmlImporterTests timewise-basic 1`] = ` +Map { + "__kind" => "Score", + "artist" => "Artist", + "copyright" => "Copyright", + "music" => "Music", + "notices" => "Notices", + "title" => "Title", + "words" => "Words", + "tab" => "Tab", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 6, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "secondarychannel" => 1, + }, + "name" => "Track 1", + "shortname" => "T1", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 1, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 7, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "program" => 1, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "Track 2", + "shortname" => "T2", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 2, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 8, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "program" => 2, + "primarychannel" => 4, + "secondarychannel" => 5, + }, + "name" => "Track 3", + "shortname" => "T3", + }, + ], + "stylesheet" => Map { + "hidedynamics" => true, + }, +} +`; diff --git a/test/index.ts b/test/index.ts index 3007d2412..875bf0755 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1 +1 @@ -import '**/*.test.js'; \ No newline at end of file +import '**/*.test.js'; diff --git a/test/mocha.jest-snapshot.ts b/test/mocha.jest-snapshot.ts new file mode 100644 index 000000000..770cfda85 --- /dev/null +++ b/test/mocha.jest-snapshot.ts @@ -0,0 +1,455 @@ +/** @target web */ +import chalk from 'chalk'; +import * as chai from 'chai'; +import url from 'node:url'; +import path from 'node:path'; +import { + addSerializer, + buildSnapshotResolver, + type SnapshotResolver, + SnapshotState, + toMatchSnapshot +} from 'jest-snapshot'; +import type { Config } from '@jest/types'; +import slash from 'slash'; +import type { SyncExpectationResult } from 'expect'; +import { equals, iterableEquality, subsetEquality } from '@jest/expect-utils'; +import * as matcherUtils from 'jest-matcher-utils'; +import { AssertionError } from 'assertion-error'; +import { type PrettyFormatConfig, type PrettyFormatPrinter, ScoreSerializerPlugin } from './PrettyFormat'; + +// Mocha and Chai integration (called from global-hooks.ts) +declare global { + namespace Chai { + interface Assertion { + toMatchSnapshot(message?: string): Assertion; + } + } +} + +export async function initializeJestSnapshot() { + // Setup jest-snapshot + + addSerializer({ + test(val) { + return ScoreSerializerPlugin.instance.test(val); + }, + serialize(val, config, indentation, depth, refs, printer) { + // + return ScoreSerializerPlugin.instance.serialize( + val, + config as PrettyFormatConfig, + indentation, + depth, + refs, + printer as PrettyFormatPrinter + ); + } + }); + + currentResolver = await buildSnapshotResolver(globalConfig); + + chai.use((chai, utils) => { + utils.addMethod( + chai.Assertion.prototype, + 'toMatchSnapshot', + function (this: Record, message?: string) { + const received = utils.flag(this, 'object'); + const isNot = utils.flag(this, 'negate') as boolean; + + const args = [received]; + if (message !== undefined) { + args.push(message); + } + + const matchResult = toMatchSnapshot.apply( + { + // Context + snapshotState: snapshotState!, + + // MatcherContext -> MatcherState + assertionCalls, + currentConcurrentTestName: undefined, + currentTestName: currentTest!.fullTitle(), + error: undefined, + expand: snapshotOptions.expand, + expectedAssertionsNumber: null, + expectedAssertionsNumberError: undefined, + isExpectingAssertions: false, + isExpectingAssertionsError: undefined, + isNot, + numPassingAsserts: 0, + promise: undefined, + suppressedErrors: [], + testPath: currentTest!.file, + + // MatcherContext -> MatcherUtils + customTesters: [], + dontThrow() {}, + equals, + utils: { + ...matcherUtils, + iterableEquality, + subsetEquality + } + }, + args as any + ) as SyncExpectationResult; + + assertionCalls++; + + if (!matchResult.pass) { + throw new AssertionError(matchResult.message()); + } + } + ); + }); +} + +export function beforeEachTest(newTest: Mocha.Test) { + if (currentTest === undefined || currentTest?.file !== newTest.file) { + if (snapshotState) { + storeSnapshotState(snapshotState); + } + + snapshotState = new SnapshotState(currentResolver!.resolveSnapshotPath(newTest.file!), snapshotOptions); + } + assertionCalls = 0; + currentTest = newTest; + executedTestNames.add(newTest.fullTitle()); +} + +export function afterAll() { + if (snapshotState) { + storeSnapshotState(snapshotState); + } + + writeSummaryReport(); +} + +// +// Adapted code from Jest for the use in Mocha +// MIT License +// Copyright (c) Meta Platforms, Inc. and affiliates. +// Copyright Contributors to the Jest project. +// https://github.com/jestjs/jest/blob/main/LICENSE + +// General Snapshot matching +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + +type SnapshotStateOptions = ConstructorParameters[1]; +type SnapshotSummary = { + added: number; + didUpdate: boolean; + failure: boolean; + filesAdded: number; + filesRemoved: number; + filesRemovedList: Array; + filesUnmatched: number; + filesUpdated: number; + matched: number; + total: number; + unchecked: number; + uncheckedKeysByFile: Array; + unmatched: number; + updated: number; +}; +type UncheckedSnapshot = { + filePath: string; + keys: Array; +}; + +const globalConfig: Config.ProjectConfig = { + rootDir: path.resolve(__dirname, '..'), + snapshotFormat: {}, + // defaults + automock: false, + cache: false, + cacheDirectory: '', + clearMocks: false, + collectCoverageFrom: [], + coverageDirectory: '', + coveragePathIgnorePatterns: [], + cwd: '', + dependencyExtractor: undefined, + detectLeaks: false, + detectOpenHandles: false, + displayName: undefined, + errorOnDeprecated: false, + extensionsToTreatAsEsm: [], + fakeTimers: {}, + filter: undefined, + forceCoverageMatch: [], + globalSetup: undefined, + globalTeardown: undefined, + globals: {}, + haste: {}, + id: '', + injectGlobals: false, + moduleDirectories: [], + moduleFileExtensions: [], + moduleNameMapper: [], + modulePathIgnorePatterns: [], + modulePaths: undefined, + openHandlesTimeout: 0, + preset: undefined, + prettierPath: '', + resetMocks: false, + resetModules: false, + resolver: undefined, + restoreMocks: false, + roots: [], + runner: '', + runtime: undefined, + sandboxInjectedGlobals: [], + setupFiles: [], + setupFilesAfterEnv: [], + skipFilter: false, + skipNodeResolution: undefined, + slowTestThreshold: 0, + snapshotResolver: undefined, + snapshotSerializers: [], + testEnvironment: '', + testEnvironmentOptions: {}, + testMatch: [], + testLocationInResults: false, + testPathIgnorePatterns: [], + testRegex: [], + testRunner: '', + transform: [], + transformIgnorePatterns: [], + watchPathIgnorePatterns: [], + unmockedModulePathPatterns: undefined, + workerIdleMemoryLimit: undefined +}; + +// https://github.com/jestjs/jest/blob/4e7d916ec6a16de5548273c17b5d2c5761b0aebb/packages/jest-config/src/normalize.ts#L1079-L1088 +const argvCi = !!process.env.CI; +const argvUpdateSnapshot = process.argv.includes('--updateSnapshot'); +const snapshotOptions: SnapshotStateOptions = { + updateSnapshot: argvCi && argvUpdateSnapshot ? 'none' : argvUpdateSnapshot ? 'all' : 'new', + rootDir: globalConfig.rootDir, + snapshotFormat: globalConfig.snapshotFormat, + expand: undefined, + prettierPath: undefined +}; + +let currentResolver: SnapshotResolver | undefined; +let currentTest: Mocha.Test | undefined = undefined; +let snapshotState: SnapshotState | undefined = undefined; +let assertionCalls = 0; + +// Snapshot results +const executedTestNames = new Set(); +const aggregatedResults: SnapshotSummary = { + // https://github.com/jestjs/jest/blob/4e7d916ec6a16de5548273c17b5d2c5761b0aebb/packages/jest-test-result/src/helpers.ts#L22 + added: 0, + didUpdate: false, // is set only after the full run + failure: false, + filesAdded: 0, + // combines individual test results + removed files after the full run + filesRemoved: 0, + filesRemovedList: [], + filesUnmatched: 0, + filesUpdated: 0, + matched: 0, + total: 0, + unchecked: 0, + uncheckedKeysByFile: [], + unmatched: 0, + updated: 0 +}; + +function storeSnapshotState(snapshotState: SnapshotState) { + // https://github.com/jestjs/jest/blob/4e7d916ec6a16de5548273c17b5d2c5761b0aebb/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts#L137 + + const uncheckedCount = snapshotState.getUncheckedCount(); + const uncheckedKeys = snapshotState.getUncheckedKeys(); + if (uncheckedCount) { + snapshotState.removeUncheckedKeys(); + } + const status = snapshotState.save(); + + // https://github.com/jestjs/jest/blob/4e7d916ec6a16de5548273c17b5d2c5761b0aebb/packages/jest-test-result/src/helpers.ts#L119C1-L120C1 + // Snapshot data + + if (snapshotState.added) { + aggregatedResults.filesAdded++; + } + if (status.deleted) { + aggregatedResults.filesRemoved++; + } + if (snapshotState.unmatched) { + aggregatedResults.filesUnmatched++; + } + if (snapshotState.updated) { + aggregatedResults.filesUpdated++; + } + + aggregatedResults.unmatched += snapshotState.unmatched; + aggregatedResults.updated += snapshotState.updated; + aggregatedResults.total += + snapshotState.added + snapshotState.matched + snapshotState.unmatched + snapshotState.updated; + + aggregatedResults.added += snapshotState.added; + aggregatedResults.matched += snapshotState.matched; + aggregatedResults.unchecked += status.deleted ? 0 : uncheckedCount; + if (uncheckedKeys.length > 0) { + aggregatedResults.uncheckedKeysByFile.push({ + filePath: currentTest!.file!, + keys: uncheckedKeys + }); + } + + aggregatedResults.filesAdded += snapshotState.added; + aggregatedResults.filesRemoved += uncheckedCount; + aggregatedResults.filesUnmatched += snapshotState.unmatched; +} + +// Result Summary Printing +// https://github.com/jestjs/jest/blob/4e7d916ec6a16de5548273c17b5d2c5761b0aebb/packages/jest-reporters/src/getSnapshotSummary.ts#L25 +const ARROW = ' \u203A '; +const DOWN_ARROW = ' \u21B3 '; +const DOT = ' \u2022 '; +const FAIL_COLOR = chalk.bold.red; +const OBSOLETE_COLOR = chalk.bold.yellow; +const SNAPSHOT_ADDED = chalk.bold.green; +const SNAPSHOT_NOTE = chalk.dim; +const SNAPSHOT_REMOVED = chalk.bold.green; +const SNAPSHOT_SUMMARY = chalk.bold; +const SNAPSHOT_UPDATED = chalk.bold.green; + +function pluralize(word: string, count: number, ending = 's'): string { + return `${count} ${word}${count === 1 ? '' : ending}`; +} + +function relativePath( + config: Config.GlobalConfig | Config.ProjectConfig, + testPath: string +): { basename: string; dirname: string } { + // this function can be called with ProjectConfigs or GlobalConfigs. GlobalConfigs + // do not have config.cwd, only config.rootDir. Try using config.cwd, fallback + // to config.rootDir. (Also, some unit just use config.rootDir, which is ok) + testPath = path.relative((config as Config.ProjectConfig).cwd || config.rootDir, testPath); + const dirname = path.dirname(testPath); + const basename = path.basename(testPath); + return { basename, dirname }; +} + +function formatTestPath(config: Config.GlobalConfig | Config.ProjectConfig, testPath: string): string { + const { dirname, basename } = relativePath(config, testPath); + return slash(chalk.dim(dirname + path.sep) + chalk.bold(basename)); +} + +function writeSummaryReport() { + const snapshots = aggregatedResults; + const updateCommand = '--updateSnapshots'; + + // filter out obsolete keys if we did not execute the related test + for (const uncheckedFile of snapshots.uncheckedKeysByFile) { + const keysToRemove = new Set(); + for (const key of uncheckedFile.keys) { + let removeKey = true; + for (const executed of executedTestNames) { + if (key.startsWith(executed)) { + removeKey = false; + break; + } + } + + if (removeKey) { + keysToRemove.add(key); + snapshots.unchecked--; + snapshots.filesRemoved--; + } + } + + uncheckedFile.keys = uncheckedFile.keys.filter(k => !keysToRemove.has(k)); + } + + snapshots.uncheckedKeysByFile = snapshots.uncheckedKeysByFile.filter(f => f.keys.length > 0); + + const summary: string[] = []; + summary.push(SNAPSHOT_SUMMARY('Snapshot Summary')); + if (snapshots.added) { + summary.push( + `${SNAPSHOT_ADDED( + `${ARROW + pluralize('snapshot', snapshots.added)} written ` + )}from ${pluralize('test suite', snapshots.filesAdded)}.` + ); + } + + if (snapshots.unmatched) { + summary.push( + `${FAIL_COLOR(`${ARROW}${pluralize('snapshot', snapshots.unmatched)} failed`)} from ${pluralize( + 'test suite', + snapshots.filesUnmatched + )}. ${SNAPSHOT_NOTE(`Inspect your code changes or ${updateCommand} to update them.`)}` + ); + } + + if (snapshots.updated) { + summary.push( + `${SNAPSHOT_UPDATED( + `${ARROW + pluralize('snapshot', snapshots.updated)} updated ` + )}from ${pluralize('test suite', snapshots.filesUpdated)}.` + ); + } + + if (snapshots.filesRemoved) { + if (snapshots.didUpdate) { + summary.push( + `${SNAPSHOT_REMOVED( + `${ARROW}${pluralize('snapshot file', snapshots.filesRemoved)} removed ` + )}from ${pluralize('test suite', snapshots.filesRemoved)}.` + ); + } else { + summary.push( + `${OBSOLETE_COLOR( + `${ARROW}${pluralize('snapshot file', snapshots.filesRemoved)} obsolete ` + )}from ${pluralize('test suite', snapshots.filesRemoved)}. ${SNAPSHOT_NOTE( + `To remove ${snapshots.filesRemoved === 1 ? 'it' : 'them all'}, ${updateCommand}.` + )}` + ); + } + } + if (snapshots.filesRemovedList && snapshots.filesRemovedList.length > 0) { + const [head, ...tail] = snapshots.filesRemovedList; + summary.push(` ${DOWN_ARROW} ${DOT}${formatTestPath(globalConfig, head)}`); + + for (const key of tail) { + summary.push(` ${DOT}${formatTestPath(globalConfig, key)}`); + } + } + + if (snapshots.unchecked) { + if (snapshots.didUpdate) { + summary.push( + `${SNAPSHOT_REMOVED(`${ARROW}${pluralize('snapshot', snapshots.unchecked)} removed `)}from ${pluralize( + 'test suite', + snapshots.uncheckedKeysByFile.length + )}.` + ); + } else { + summary.push( + `${OBSOLETE_COLOR(`${ARROW}${pluralize('snapshot', snapshots.unchecked)} obsolete `)}from ${pluralize( + 'test suite', + snapshots.uncheckedKeysByFile.length + )}. ${SNAPSHOT_NOTE(`To remove ${snapshots.unchecked === 1 ? 'it' : 'them all'}, ${updateCommand}.`)}` + ); + } + + for (const uncheckedFile of snapshots.uncheckedKeysByFile) { + summary.push(` ${DOWN_ARROW}${formatTestPath(globalConfig, uncheckedFile.filePath)}`); + + for (const key of uncheckedFile.keys) { + summary.push(` ${DOT}${key}`); + } + } + } + + console.log(); + for (const line of summary) { + console.log(line); + } +} diff --git a/test/model/Beat.test.ts b/test/model/Beat.test.ts index 46ec23f59..5ee7f3ce1 100644 --- a/test/model/Beat.test.ts +++ b/test/model/Beat.test.ts @@ -1,5 +1,5 @@ -import { Note } from '@src/model'; import { Beat } from '@src/model/Beat'; +import { Note } from '@src/model/Note'; import { expect } from 'chai'; describe('BeatTests', () => { @@ -23,4 +23,4 @@ describe('BeatTests', () => { expect(beat.hasNoteOnString(2)).to.be.equal(false); expect(beat.getNoteOnString(2)).to.be.equal(null); }); -}); \ No newline at end of file +}); diff --git a/test/model/ComparisonHelpers.ts b/test/model/ComparisonHelpers.ts index 16eeabc55..c274218b9 100644 --- a/test/model/ComparisonHelpers.ts +++ b/test/model/ComparisonHelpers.ts @@ -18,13 +18,13 @@ export class ComparisonHelpers { const expectedType = typeof expected; const actualType = typeof actual; - if (actualType != expectedType) { + if (actualType !== expectedType) { assert.fail(`Type Mismatch on hierarchy: ${path}, actual<'${actualType}'> != expected<'${expectedType}'>`); } switch (actualType) { case 'boolean': - if ((actual as boolean) != (expected as boolean)) { + if ((actual as boolean) !== (expected as boolean)) { assert.fail( `Boolean mismatch on hierarchy: ${path}, actualy<'${actual}'> != expected<'${expected}'>` ); @@ -70,6 +70,8 @@ export class ComparisonHelpers { 'tiedestinationnoteid', 'sluroriginnoteid', 'slurdestinationnoteid', + 'slidetargetnoteid', + 'slideoriginnoteid', 'systemslayout', 'defaultsystemslayout', 'displayscale', @@ -110,7 +112,9 @@ export class ComparisonHelpers { break; case 'string': if ((actual as string) !== (expected as string)) { - assert.fail(`String mismatch on hierarchy: ${path}, actual<'${actual}'> != expeted<'${expected}'>`); + assert.fail( + `String mismatch on hierarchy: ${path}, actual<'${actual}'> != expected<'${expected}'>` + ); } break; case 'undefined': diff --git a/test/model/Font.test.ts b/test/model/Font.test.ts index c5f9ef9de..38ce35fc8 100644 --- a/test/model/Font.test.ts +++ b/test/model/Font.test.ts @@ -1,4 +1,4 @@ -import { Font, FontStyle, FontWeight } from "@src/model/Font"; +import { Font, FontStyle, FontWeight } from '@src/model/Font'; import { expect } from 'chai'; describe('FontTests', () => { @@ -12,56 +12,95 @@ describe('FontTests', () => { expect(font!.weight).to.equal(expected.weight); } - it('parses-full', function () { - parseText('italic small-caps bold 12px/1.5em "Arial"', Font.withFamilyList(["Arial"], 12, FontStyle.Italic, FontWeight.Bold)) + it('parses-full', () => { + parseText( + 'italic small-caps bold 12px/1.5em "Arial"', + Font.withFamilyList(['Arial'], 12, FontStyle.Italic, FontWeight.Bold) + ); }); - it('parses-partial-options', function () { - parseText('italic bold 12px/1.5em "Arial", sans', Font.withFamilyList(["Arial", "sans"], 12, FontStyle.Italic, FontWeight.Bold)) - parseText('bold italic 12px/1.5em "Arial", sans', Font.withFamilyList(["Arial", "sans"], 12, FontStyle.Italic, FontWeight.Bold)) - parseText('bold 12px/1.5em "Arial", sans', Font.withFamilyList(["Arial", "sans"], 12, FontStyle.Plain, FontWeight.Bold)) - parseText('italic 12px/1.5em "Arial", sans', Font.withFamilyList(["Arial", "sans"], 12, FontStyle.Italic)) + it('parses-partial-options', () => { + parseText( + 'italic bold 12px/1.5em "Arial", sans', + Font.withFamilyList(['Arial', 'sans'], 12, FontStyle.Italic, FontWeight.Bold) + ); + parseText( + 'bold italic 12px/1.5em "Arial", sans', + Font.withFamilyList(['Arial', 'sans'], 12, FontStyle.Italic, FontWeight.Bold) + ); + parseText( + 'bold 12px/1.5em "Arial", sans', + Font.withFamilyList(['Arial', 'sans'], 12, FontStyle.Plain, FontWeight.Bold) + ); + parseText('italic 12px/1.5em "Arial", sans', Font.withFamilyList(['Arial', 'sans'], 12, FontStyle.Italic)); }); - it('parses-no-options', function () { - parseText('12px/1.5em "Arial", sans', Font.withFamilyList(["Arial", "sans"], 12, FontStyle.Plain)) + it('parses-no-options', () => { + parseText('12px/1.5em "Arial", sans', Font.withFamilyList(['Arial', 'sans'], 12, FontStyle.Plain)); }); - it('parses-line-height-spaces', function () { - parseText('12px/1.5em "Arial", sans', Font.withFamilyList(["Arial", "sans"], 12, FontStyle.Plain)) - parseText('12px /1.5em "Arial", sans', Font.withFamilyList(["Arial", "sans"], 12, FontStyle.Plain)) - parseText('12px / 1.5em "Arial", sans', Font.withFamilyList(["Arial", "sans"], 12, FontStyle.Plain)) - parseText('12px / 1.5em "Arial", sans', Font.withFamilyList(["Arial", "sans"], 12, FontStyle.Plain)) + it('parses-line-height-spaces', () => { + parseText('12px/1.5em "Arial", sans', Font.withFamilyList(['Arial', 'sans'], 12, FontStyle.Plain)); + parseText('12px /1.5em "Arial", sans', Font.withFamilyList(['Arial', 'sans'], 12, FontStyle.Plain)); + parseText('12px / 1.5em "Arial", sans', Font.withFamilyList(['Arial', 'sans'], 12, FontStyle.Plain)); + parseText('12px / 1.5em "Arial", sans', Font.withFamilyList(['Arial', 'sans'], 12, FontStyle.Plain)); }); - it('parses-multiple-families', function () { - parseText('12px/1.5em Arial, Verdana, sans', Font.withFamilyList(["Arial", "Verdana", "sans"], 12, FontStyle.Plain)) - parseText("12px/1.5em 'Arial', 'Verdana', 'sans'", Font.withFamilyList(["Arial", "Verdana", "sans"], 12, FontStyle.Plain)) - parseText('12px/1.5em "Arial", "Verdana", "sans"', Font.withFamilyList(["Arial", "Verdana", "sans"], 12, FontStyle.Plain)) - parseText('12px/1.5em Arial, "Verdana", sans', Font.withFamilyList(["Arial", "Verdana", "sans"], 12, FontStyle.Plain)) - parseText('12px/1.5em Arial, \'Verdana\', "sans"', Font.withFamilyList(["Arial", "Verdana", "sans"], 12, FontStyle.Plain)) + it('parses-multiple-families', () => { + parseText( + '12px/1.5em Arial, Verdana, sans', + Font.withFamilyList(['Arial', 'Verdana', 'sans'], 12, FontStyle.Plain) + ); + parseText( + "12px/1.5em 'Arial', 'Verdana', 'sans'", + Font.withFamilyList(['Arial', 'Verdana', 'sans'], 12, FontStyle.Plain) + ); + parseText( + '12px/1.5em "Arial", "Verdana", "sans"', + Font.withFamilyList(['Arial', 'Verdana', 'sans'], 12, FontStyle.Plain) + ); + parseText( + '12px/1.5em Arial, "Verdana", sans', + Font.withFamilyList(['Arial', 'Verdana', 'sans'], 12, FontStyle.Plain) + ); + parseText( + '12px/1.5em Arial, \'Verdana\', "sans"', + Font.withFamilyList(['Arial', 'Verdana', 'sans'], 12, FontStyle.Plain) + ); }); - it('parses-escaped-quotes', function () { - parseText("12px/1.5em \"Ari\\\"al\"", Font.withFamilyList(["Ari\"al"], 12, FontStyle.Plain)) - parseText('12px/1.5em \'Ari\\\'al\'', Font.withFamilyList(["Ari'al"], 12, FontStyle.Plain)) - parseText('12px/1.5em \'Ari\\\'\'', Font.withFamilyList(['Ari\''], 12, FontStyle.Plain)) - parseText("12px/1.5em 'Ari\\al'", Font.withFamilyList(["Ari\\al"], 12, FontStyle.Plain)) + it('parses-escaped-quotes', () => { + parseText('12px/1.5em "Ari\\"al"', Font.withFamilyList(['Ari"al'], 12, FontStyle.Plain)); + parseText("12px/1.5em 'Ari\\'al'", Font.withFamilyList(["Ari'al"], 12, FontStyle.Plain)); + parseText("12px/1.5em 'Ari\\''", Font.withFamilyList(["Ari'"], 12, FontStyle.Plain)); + parseText("12px/1.5em 'Ari\\al'", Font.withFamilyList(['Ari\\al'], 12, FontStyle.Plain)); }); - it('parses-with-spaces-and-quotes', function () { - parseText("12px/1.5em \"Times New Roman\"", Font.withFamilyList(["Times New Roman"], 12, FontStyle.Plain)) - parseText("12px/1.5em \"Times New Roman\", Arial, 'Open Sans'", Font.withFamilyList(["Times New Roman", "Arial", "Open Sans"], 12, FontStyle.Plain)) + it('parses-with-spaces-and-quotes', () => { + parseText('12px/1.5em "Times New Roman"', Font.withFamilyList(['Times New Roman'], 12, FontStyle.Plain)); + parseText( + '12px/1.5em "Times New Roman", Arial, \'Open Sans\'', + Font.withFamilyList(['Times New Roman', 'Arial', 'Open Sans'], 12, FontStyle.Plain) + ); }); - function toCssStringTest(f:Font, expected:string){ - expect(f.toCssString()).to.equal(expected) + function toCssStringTest(f: Font, expected: string) { + expect(f.toCssString()).to.equal(expected); } - it('css-string-tests', function () { - toCssStringTest(Font.withFamilyList(["Arial"], 12, FontStyle.Plain), "12px Arial") - toCssStringTest(Font.withFamilyList(["Arial"], 12, FontStyle.Italic), "italic 12px Arial") - toCssStringTest(Font.withFamilyList(["Arial"], 12, FontStyle.Italic, FontWeight.Bold), "bold italic 12px Arial") - toCssStringTest(Font.withFamilyList(["Times New Roman"], 12, FontStyle.Plain), "12px \"Times New Roman\"") - toCssStringTest(Font.withFamilyList(["Times New Roman", "Arial"], 12, FontStyle.Plain), "12px \"Times New Roman\", Arial") - toCssStringTest(Font.withFamilyList(["With 'SingleQuote'", 'With "DoubleQuote"', "Arial"], 12, FontStyle.Plain), "12px \"With 'SingleQuote'\", \"With \\\"DoubleQuote\\\"\", Arial") + it('css-string-tests', () => { + toCssStringTest(Font.withFamilyList(['Arial'], 12, FontStyle.Plain), '12px Arial'); + toCssStringTest(Font.withFamilyList(['Arial'], 12, FontStyle.Italic), 'italic 12px Arial'); + toCssStringTest( + Font.withFamilyList(['Arial'], 12, FontStyle.Italic, FontWeight.Bold), + 'bold italic 12px Arial' + ); + toCssStringTest(Font.withFamilyList(['Times New Roman'], 12, FontStyle.Plain), '12px "Times New Roman"'); + toCssStringTest( + Font.withFamilyList(['Times New Roman', 'Arial'], 12, FontStyle.Plain), + '12px "Times New Roman", Arial' + ); + toCssStringTest( + Font.withFamilyList(["With 'SingleQuote'", 'With "DoubleQuote"', 'Arial'], 12, FontStyle.Plain), + '12px "With \'SingleQuote\'", "With \\"DoubleQuote\\"", Arial' + ); }); -}); \ No newline at end of file +}); diff --git a/test/model/HeaderFooterStyle.test.ts b/test/model/HeaderFooterStyle.test.ts new file mode 100644 index 000000000..bb59c596a --- /dev/null +++ b/test/model/HeaderFooterStyle.test.ts @@ -0,0 +1,56 @@ +import { Score } from '@src/model/Score'; +import { HeaderFooterStyle } from '@src/model/Score'; +import { expect } from 'chai'; + +describe('HeaderFooterStyleTests', () => { + it('buildTextSimple', () => { + const score = new Score(); + score.title = 'Title'; + const style = new HeaderFooterStyle('%TITLE%'); + expect(style.buildText(score)).to.equal('Title'); + }); + + it('buildTextMultiple', () => { + const score = new Score(); + score.title = 'Title'; + const style = new HeaderFooterStyle('%TITLE% %TITLE%'); + expect(style.buildText(score)).to.equal('Title Title'); + }); + + it('buildTextReuse', () => { + const score = new Score(); + score.title = 'Title'; + const style = new HeaderFooterStyle('%TITLE% %TITLE%'); + expect(style.buildText(score)).to.equal('Title Title'); + expect(style.buildText(score)).to.equal('Title Title'); + }); + + it('buildTextMultipleUnknown', () => { + const score = new Score(); + score.title = 'Title'; + const style = new HeaderFooterStyle('%TITLE% %TITLE% %UNKNOWN% %INVALID%'); + expect(style.buildText(score)).to.equal('Title Title '); + }); + + it('buildTextEmptyIfMissingPlaceholderValue', () => { + const score = new Score(); + score.words = ''; + const style = new HeaderFooterStyle('Words by %WORDS%'); + expect(style.buildText(score)).to.equal(''); + }); + + it('buildTextAll', () => { + const score = new Score(); + score.title = 'Title'; + score.subTitle = 'Subtitle'; + score.artist = 'Artist'; + score.album = 'Album'; + score.words = 'Words'; + score.music = 'Music'; + score.tab = 'Tab'; + score.copyright = 'Copyright'; + + const style = new HeaderFooterStyle('%TITLE% %SUBTITLE% %ARTIST% %ALBUM% %WORDS% %MUSIC% %TABBER% %COPYRIGHT%'); + expect(style.buildText(score)).to.equal('Title Subtitle Artist Album Words Music Tab Copyright'); + }); +}); diff --git a/test/model/JsonConverter.test.ts b/test/model/JsonConverter.test.ts index ea21d52d5..2773266f1 100644 --- a/test/model/JsonConverter.test.ts +++ b/test/model/JsonConverter.test.ts @@ -1,25 +1,24 @@ -import { LayoutMode } from "@src/LayoutMode"; -import { LogLevel } from "@src/LogLevel"; -import { StaveProfile } from "@src/StaveProfile"; -import { Settings } from "@src/Settings"; -import { SettingsSerializer } from "@src/generated/SettingsSerializer"; -import { ScoreLoader } from "@src/importer/ScoreLoader"; -import { Color } from "@src/model/Color"; -import { Font, FontStyle } from "@src/model/Font"; -import { JsonConverter } from "@src/model/JsonConverter"; -import { Score } from "@src/model/Score"; -import { NotationElement, TabRhythmMode, NotationMode, FingeringMode } from "@src/NotationSettings"; -import { TestPlatform } from "@test/TestPlatform"; -import { ComparisonHelpers } from "./ComparisonHelpers"; +import { LayoutMode } from '@src/LayoutMode'; +import { LogLevel } from '@src/LogLevel'; +import { StaveProfile } from '@src/StaveProfile'; +import { Settings } from '@src/Settings'; +import { SettingsSerializer } from '@src/generated/SettingsSerializer'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; +import { Color } from '@src/model/Color'; +import { Font, FontStyle } from '@src/model/Font'; +import { JsonConverter } from '@src/model/JsonConverter'; +import type { Score } from '@src/model/Score'; +import { NotationElement, TabRhythmMode, NotationMode, FingeringMode } from '@src/NotationSettings'; +import { TestPlatform } from '@test/TestPlatform'; +import { ComparisonHelpers } from './ComparisonHelpers'; import { assert, expect } from 'chai'; describe('JsonConverterTest', () => { async function loadScore(name: string): Promise { try { - const data = await TestPlatform.loadFile('test-data/' + name); + const data = await TestPlatform.loadFile(`test-data/${name}`); return ScoreLoader.loadScoreFromBytes(data); - } - catch (e) { + } catch (e) { return null; } } @@ -35,7 +34,12 @@ describe('JsonConverterTest', () => { const actual = JsonConverter.jsObjectToScore(expectedJson); const actualJson = JsonConverter.scoreToJsObject(actual); - ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '<' + name.substr(name.lastIndexOf('/') + 1) + '>', null); + ComparisonHelpers.expectJsonEqual( + expectedJson, + actualJson, + `<${name.substr(name.lastIndexOf('/') + 1)}>`, + null + ); } catch (e) { assert.fail(String(e)); } @@ -84,7 +88,6 @@ describe('JsonConverterTest', () => { await testRoundTripFolderEqual('visual-tests/special-tracks'); }); - it('settings', () => { const expected = new Settings(); // here we modifiy some properties of each level and some special ones additionally @@ -100,7 +103,7 @@ describe('JsonConverterTest', () => { expected.core.tracks = [1, 2, 3]; expected.core.enableLazyLoading = false; - expected.core.engine = "engine"; + expected.core.engine = 'engine'; expected.core.logLevel = LogLevel.Error; expected.core.useWorkers = false; expected.core.includeNoteBounds = true; @@ -128,6 +131,7 @@ describe('JsonConverterTest', () => { expected.importer.encoding = 'enc'; expected.importer.mergePartGroupsInMusicXml = false; + /**@target web*/ expected.player.soundFont = 'soundfont'; /**@target web*/ expected.player.scrollElement = 'scroll'; @@ -148,7 +152,7 @@ describe('JsonConverterTest', () => { // json_on_parent raw.set('enableLazyLoading', false); - // string enum + // string enum raw.set('logLevel', 'error'); raw.set('displayLayoutMode', 1.0 as number); @@ -175,7 +179,6 @@ describe('JsonConverterTest', () => { expect(settings.display.resources.copyrightFont.style).to.equal(FontStyle.Italic); }); - /*@target web*/ it('settings-from-object', () => { const settings = new Settings(); @@ -183,7 +186,7 @@ describe('JsonConverterTest', () => { const raw = { // json_on_parent enableLazyLoading: false, - // string enum + // string enum logLevel: 'error', displayLayoutMode: 1.0, // nested diff --git a/test/model/Lyrics.test.ts b/test/model/Lyrics.test.ts index 3f028494b..b967348fd 100644 --- a/test/model/Lyrics.test.ts +++ b/test/model/Lyrics.test.ts @@ -1,7 +1,7 @@ import { GpxImporter } from '@src/importer/GpxImporter'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { Lyrics } from '@src/model/Lyrics'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { TestPlatform } from '@test/TestPlatform'; import { expect } from 'chai'; @@ -10,14 +10,14 @@ describe('LyricsTests', () => { async function loadLyricsTemplateFile(): Promise { const path: string = 'test-data/lyrics/template.gpx'; const data = await TestPlatform.loadFile(path); - let buffer: ByteBuffer = ByteBuffer.fromBuffer(data); - let importer: GpxImporter = new GpxImporter(); + const buffer: ByteBuffer = ByteBuffer.fromBuffer(data); + const importer: GpxImporter = new GpxImporter(); importer.init(buffer, new Settings()); return importer.readScore(); } function testLyrics(text: string, chunks: string[]): void { - let lyrics: Lyrics = new Lyrics(); + const lyrics: Lyrics = new Lyrics(); lyrics.text = text; lyrics.finish(); expect(lyrics.chunks.join(',')).to.equal(chunks.join(',')); diff --git a/test/model/TuningParser.test.ts b/test/model/TuningParser.test.ts index df330eb95..282ffd8ec 100644 --- a/test/model/TuningParser.test.ts +++ b/test/model/TuningParser.test.ts @@ -4,10 +4,10 @@ import { expect } from 'chai'; describe('TuningParserTest', () => { it('standard', () => { - let standard: Tuning = Tuning.getDefaultTuningFor(6)!; - let tuningText: string[] = ['E4', 'B3', 'G3', 'D3', 'A2', 'E2']; - let tuning = new Array(tuningText.length); - let tuningText2: string[] = new Array(tuningText.length); + const standard: Tuning = Tuning.getDefaultTuningFor(6)!; + const tuningText: string[] = ['E4', 'B3', 'G3', 'D3', 'A2', 'E2']; + const tuning = new Array(tuningText.length); + const tuningText2: string[] = new Array(tuningText.length); for (let i: number = 0; i < tuningText.length; i++) { tuning[i] = ModelUtils.getTuningForText(tuningText[i]); tuningText2[i] = Tuning.getTextForTuning(tuning[i], true); diff --git a/test/visualTests/PixelMatch.ts b/test/visualTests/PixelMatch.ts index 0d4a95b5f..21dbd4698 100644 --- a/test/visualTests/PixelMatch.ts +++ b/test/visualTests/PixelMatch.ts @@ -95,7 +95,9 @@ export class PixelMatch { throw new Error(`Image sizes do not match. ${img1.length} !== ${img2.length}`); } - if (img1.length !== width * height * 4) throw new Error('Image data size does not match width/height.'); + if (img1.length !== width * height * 4) { + throw new Error('Image data size does not match width/height.'); + } options.aaColor = options.aaColor ?? PixelMatch.defaultOptions.aaColor; options.alpha = options.alpha ?? PixelMatch.defaultOptions.alpha; @@ -111,15 +113,15 @@ export class PixelMatch { let transparentPixels = 0; for (let i = 0; i < len; i++) { - const img1r = img1[(i * 4) + 0]; - const img1g = img1[(i * 4) + 1]; - const img1b = img1[(i * 4) + 2]; - const img1a = img1[(i * 4) + 3]; + const img1r = img1[i * 4 + 0]; + const img1g = img1[i * 4 + 1]; + const img1b = img1[i * 4 + 2]; + const img1a = img1[i * 4 + 3]; - const img2r = img2[(i * 4) + 0]; - const img2g = img2[(i * 4) + 1]; - const img2b = img2[(i * 4) + 2]; - const img2a = img2[(i * 4) + 3]; + const img2r = img2[i * 4 + 0]; + const img2g = img2[i * 4 + 1]; + const img2b = img2[i * 4 + 2]; + const img2a = img2[i * 4 + 3]; if (img1r !== img2r || img1g !== img2g || img1b !== img2b || img1a !== img2a) { identical = false; @@ -132,7 +134,9 @@ export class PixelMatch { if (identical) { // fast path if identical if (output && !options.diffMask) { - for (let i = 0; i < len; i++) PixelMatch.drawGrayPixel(img1, 4 * i, options.alpha!, output); + for (let i = 0; i < len; i++) { + PixelMatch.drawGrayPixel(img1, 4 * i, options.alpha!, output); + } } return new PixelMatchResult(len, 0, transparentPixels); } @@ -172,15 +176,21 @@ export class PixelMatch { ) { // one of the pixels is anti-aliasing; draw as yellow and do not count as difference // note that we do not include such pixels in a mask - if (output && !options.diffMask) PixelMatch.drawPixel(output, pos, aaR, aaG, aaB); + if (output && !options.diffMask) { + PixelMatch.drawPixel(output, pos, aaR, aaG, aaB); + } } else { // found substantial difference not caused by anti-aliasing; draw it as red - if (output) PixelMatch.drawPixel(output, pos, diffR, diffG, diffB); + if (output) { + PixelMatch.drawPixel(output, pos, diffR, diffG, diffB); + } diff++; } } else if (output) { // pixels are similar; draw background as grayscale image blended with white - if (!options.diffMask) PixelMatch.drawGrayPixel(img1, pos, options.alpha!, output); + if (!options.diffMask) { + PixelMatch.drawGrayPixel(img1, pos, options.alpha!, output); + } } } } @@ -210,7 +220,9 @@ export class PixelMatch { // go through 8 adjacent pixels for (let x = x0; x <= x2; x++) { for (let y = y0; y <= y2; y++) { - if (x === x1 && y === y1) continue; + if (x === x1 && y === y1) { + continue; + } // brightness delta between the center pixel and adjacent one const delta = PixelMatch.colorDelta(img, img, pos, (y * width + x) * 4, true); @@ -219,7 +231,9 @@ export class PixelMatch { if (delta === 0) { zeroes++; // if found more than 2 equal siblings, it's definitely not anti-aliasing - if (zeroes > 2) return false; + if (zeroes > 2) { + return false; + } // remember the darkest pixel } else if (delta < min) { @@ -237,7 +251,9 @@ export class PixelMatch { } // if there are no both darker and brighter pixels among siblings, it's not anti-aliasing - if (min === 0 || max === 0) return false; + if (min === 0 || max === 0) { + return false; + } // if either the darkest or the brightest pixel has 3+ equal siblings in both images // (definitely not anti-aliased), this pixel is anti-aliased @@ -262,7 +278,9 @@ export class PixelMatch { // go through 8 adjacent pixels for (let x = x0; x <= x2; x++) { for (let y = y0; y <= y2; y++) { - if (x === x1 && y === y1) continue; + if (x === x1 && y === y1) { + continue; + } const pos2 = (y * width + x) * 4; if ( @@ -274,7 +292,9 @@ export class PixelMatch { zeroes++; } - if (zeroes > 2) return true; + if (zeroes > 2) { + return true; + } } } @@ -295,7 +315,9 @@ export class PixelMatch { let b2 = img2[m + 2]; let a2 = img2[m + 3]; - if (a1 === a2 && r1 === r2 && g1 === g2 && b1 === b2) return 0; + if (a1 === a2 && r1 === r2 && g1 === g2 && b1 === b2) { + return 0; + } if (a1 < 255) { a1 /= 255; @@ -313,7 +335,9 @@ export class PixelMatch { const y = PixelMatch.rgb2y(r1, g1, b1) - PixelMatch.rgb2y(r2, g2, b2); - if (yOnly) return y; // brightness difference only + if (yOnly) { + return y; // brightness difference only + } const i = PixelMatch.rgb2i(r1, g1, b1) - PixelMatch.rgb2i(r2, g2, b2); const q = PixelMatch.rgb2q(r1, g1, b1) - PixelMatch.rgb2q(r2, g2, b2); diff --git a/test/visualTests/TestUiFacade.ts b/test/visualTests/TestUiFacade.ts index 922effff0..f1da7331f 100644 --- a/test/visualTests/TestUiFacade.ts +++ b/test/visualTests/TestUiFacade.ts @@ -1,18 +1,19 @@ -import { AlphaTabApiBase } from "@src/AlphaTabApiBase"; -import { EventEmitter, EventEmitterOfT, IEventEmitter, IEventEmitterOfT } from "@src/EventEmitter"; -import { Settings } from "@src/Settings"; -import { ScoreLoader } from "@src/importer"; -import { Score } from "@src/model"; -import { Cursors } from "@src/platform/Cursors"; -import { IContainer } from "@src/platform/IContainer"; -import { IMouseEventArgs } from "@src/platform/IMouseEventArgs"; -import { IUiFacade } from "@src/platform/IUiFacade"; -import { Bounds, IScoreRenderer, RenderFinishedEventArgs } from "@src/rendering"; -import { IAlphaSynth } from "@src/synth"; -import { TestPlatform } from "@test/TestPlatform"; +import type { AlphaTabApiBase } from '@src/AlphaTabApiBase'; +import { EventEmitter, EventEmitterOfT, type IEventEmitter, type IEventEmitterOfT } from '@src/EventEmitter'; +import { Settings } from '@src/Settings'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; +import { Score } from '@src/model/Score'; +import type { Cursors } from '@src/platform/Cursors'; +import type { IContainer } from '@src/platform/IContainer'; +import type { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; +import type { IUiFacade } from '@src/platform/IUiFacade'; +import type { IScoreRenderer } from '@src/rendering/IScoreRenderer'; +import type { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; +import { Bounds } from '@src/rendering/utils/Bounds'; +import type { IAlphaSynth } from '@src/synth/IAlphaSynth'; +import { TestPlatform } from '@test/TestPlatform'; class TestUiContainer implements IContainer { - private _width: number = 0; private _height: number = 0; @@ -28,7 +29,7 @@ class TestUiContainer implements IContainer { } public set width(value: number) { - if (value != this._width) { + if (value !== this._width) { this._width = value; (this.resize as EventEmitter).trigger(); } @@ -39,7 +40,7 @@ class TestUiContainer implements IContainer { } public set height(value: number) { - if (value != this._height) { + if (value !== this._height) { this._height = value; (this.resize as EventEmitter).trigger(); } @@ -58,29 +59,28 @@ class TestUiContainer implements IContainer { } public removeChild(child: TestUiContainer) { - this.childNodes = this.childNodes.filter(i => i != child); + this.childNodes = this.childNodes.filter(i => i !== child); } public appendChild(child: IContainer) { this.childNodes.push(child as TestUiContainer); } - public stopAnimation() { - } + public stopAnimation() {} protected lastBounds: Bounds = new Bounds(); public setBounds(x: number, y: number, w: number, h: number): void { - if (isNaN(x)) { + if (Number.isNaN(x)) { x = this.lastBounds.x; } - if (isNaN(y)) { + if (Number.isNaN(y)) { y = this.lastBounds.y; } - if (isNaN(w)) { + if (Number.isNaN(w)) { w = this.lastBounds.w; } - if (isNaN(h)) { + if (Number.isNaN(h)) { h = this.lastBounds.h; } this.left = x; @@ -90,7 +90,7 @@ class TestUiContainer implements IContainer { } public transitionToX(_duration: number, x: number): void { - this.setBounds(x, NaN, NaN, NaN); + this.setBounds(x, Number.NaN, Number.NaN, Number.NaN); } public clear(): void { @@ -132,14 +132,18 @@ export class TestUiFacade implements IUiFacade { api.settings = settings; } - public destroy(): void { - } + public destroy(): void {} public createCanvasElement(): IContainer { return new TestUiContainer(); } - public triggerEvent(container: IContainer, eventName: string, details: unknown, originalEvent?: IMouseEventArgs): void { + public triggerEvent( + container: IContainer, + eventName: string, + details: unknown, + originalEvent?: IMouseEventArgs + ): void { // nothing to do } @@ -156,7 +160,7 @@ export class TestUiFacade implements IUiFacade { } public beginAppendRenderResults(renderResult: RenderFinishedEventArgs | null): void { - const canvasElement: TestUiContainer = (this._api.canvasElement as TestUiContainer); + const canvasElement: TestUiContainer = this._api.canvasElement as TestUiContainer; // null result indicates that the rendering finished if (!renderResult) { @@ -205,18 +209,15 @@ export class TestUiFacade implements IUiFacade { return null; } - public destroyCursors(): void { - } + public destroyCursors(): void {} public beginInvoke(action: () => void): void { setImmediate(action); } - public removeHighlights(): void { - } + public removeHighlights(): void {} - public highlightElements(groupId: string, masterBarIndex: number): void { - } + public highlightElements(groupId: string, masterBarIndex: number): void {} public createSelectionElement(): IContainer | null { return null; @@ -227,24 +228,23 @@ export class TestUiFacade implements IUiFacade { } public getOffset(scrollContainer: IContainer | null, container: IContainer): Bounds { - let element = container as TestUiContainer; + const element = container as TestUiContainer; let top: number = element.top; let left: number = element.left; if (scrollContainer) { - let scrollElement = scrollContainer as TestUiContainer; - let scrollElementOffset = this.getOffset(null, scrollContainer); + const scrollElement = scrollContainer as TestUiContainer; + const scrollElementOffset = this.getOffset(null, scrollContainer); top = top + scrollElement.scrollTop - scrollElementOffset.y; left = left + scrollElement.scrollLeft - scrollElementOffset.x; } - let b = new Bounds(); + const b = new Bounds(); b.x = left; b.y = top; b.w = element.width; b.h = element.height; return b; - } public scrollToY(scrollElement: IContainer, offset: number, speed: number): void { @@ -261,7 +261,7 @@ export class TestUiFacade implements IUiFacade { return true; } if (data instanceof ArrayBuffer) { - let byteArray: Uint8Array = new Uint8Array(data as ArrayBuffer); + const byteArray: Uint8Array = new Uint8Array(data as ArrayBuffer); success(ScoreLoader.loadScoreFromBytes(byteArray, this._api.settings)); return true; } @@ -272,7 +272,7 @@ export class TestUiFacade implements IUiFacade { if (typeof data === 'string') { TestPlatform.loadFile(data) .then(x => { - success(ScoreLoader.loadScoreFromBytes(x)) + success(ScoreLoader.loadScoreFromBytes(x)); }) .catch(error); return true; @@ -309,4 +309,4 @@ export class TestUiFacade implements IUiFacade { public readonly canRenderChanged: IEventEmitter = new EventEmitter(); public readonly rootContainerBecameVisible: IEventEmitter = new EventEmitter(); -} \ No newline at end of file +} diff --git a/test/visualTests/VisualTestHelper.ts b/test/visualTests/VisualTestHelper.ts index ddae7e740..877919812 100644 --- a/test/visualTests/VisualTestHelper.ts +++ b/test/visualTests/VisualTestHelper.ts @@ -1,9 +1,9 @@ import { ScoreLoader } from '@src/importer/ScoreLoader'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { TestPlatform } from '@test/TestPlatform'; import { Environment } from '@src/Environment'; -import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; +import type { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { PixelMatch, PixelMatchOptions } from '@test/visualTests/PixelMatch'; @@ -71,9 +71,10 @@ export class VisualTestHelper { settings?: Settings, configure?: (o: VisualTestOptions) => void ): Promise { - const inputFileData = await TestPlatform.loadFile(`test-data/visual-tests/${inputFile}`); + inputFile = `test-data/visual-tests/${inputFile}`; + const inputFileData = await TestPlatform.loadFile(inputFile); const referenceFileName = TestPlatform.changeExtension(inputFile, '.png'); - let score: Score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); + const score: Score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); const o = new VisualTestOptions(score, [new VisualTestRun(-1, referenceFileName)], settings); if (configure) { @@ -96,24 +97,24 @@ export class VisualTestHelper { await VisualTestHelper.prepareAlphaSkia(); VisualTestHelper.prepareSettingsForTest(settings!); - let referenceFileData: Uint8Array[] = []; + const referenceFileData: Uint8Array[] = []; for (const run of runs) { try { - referenceFileData.push(await TestPlatform.loadFile(`test-data/visual-tests/${run.referenceFileName}`)); + referenceFileData.push(await TestPlatform.loadFile(run.referenceFileName)); } catch (e) { referenceFileData.push(new Uint8Array(0)); } } - let results: RenderFinishedEventArgs[][] = []; - let totalWidths: number[] = []; - let totalHeights: number[] = []; + const results: RenderFinishedEventArgs[][] = []; + const totalWidths: number[] = []; + const totalHeights: number[] = []; const uiFacade = new TestUiFacade(); uiFacade.rootContainer.width = runs[runIndex++].width; const api = new AlphaTabApiBase(uiFacade, settings); - let render = new Promise((resolve, reject) => { + const render = new Promise((resolve, reject) => { api.renderStarted.on(_ => { results.push([]); totalWidths.push(0); @@ -184,7 +185,8 @@ export class VisualTestHelper { if (errors.length === 1) { throw errors[0]; - } else if (errors.length > 0) { + } + if (errors.length > 0) { const errorMessages = errors.map(e => e.message ?? 'Unknown error').join('\n'); throw new Error(errorMessages); } @@ -200,18 +202,20 @@ export class VisualTestHelper { return; } - const bravura: ArrayBuffer = (await TestPlatform.loadFile('font/bravura/Bravura.ttf')).buffer as ArrayBuffer; + const bravura: ArrayBuffer = (await TestPlatform.loadFile('font/bravura/Bravura.otf')).buffer as ArrayBuffer; VisualTestHelper.enableAlphaSkia(bravura); const fonts = [ - 'font/noto-sans/NotoSans-Regular.ttf', - 'font/noto-sans/NotoSans-Italic.ttf', - 'font/noto-sans/NotoSans-Bold.ttf', - 'font/noto-sans/NotoSans-BoldItalic.ttf', - 'font/noto-serif/NotoSerif-Regular.ttf', - 'font/noto-serif/NotoSerif-Italic.ttf', - 'font/noto-serif/NotoSerif-Bold.ttf', - 'font/noto-serif/NotoSerif-BoldItalic.ttf' + 'font/noto-sans/NotoSans-Regular.otf', + 'font/noto-sans/NotoSans-Italic.otf', + 'font/noto-sans/NotoSans-Bold.otf', + 'font/noto-sans/NotoSans-BoldItalic.otf', + 'font/noto-serif/NotoSerif-Regular.otf', + 'font/noto-serif/NotoSerif-Italic.otf', + 'font/noto-serif/NotoSerif-Bold.otf', + 'font/noto-serif/NotoSerif-BoldItalic.otf', + 'font/noto-music/NotoMusic-Regular.otf', + 'font/noto-color-emoji/NotoColorEmoji_WindowsCompatible.ttf', ]; for (const font of fonts) { @@ -237,22 +241,22 @@ export class VisualTestHelper { Environment.HighDpiFactor = 1; // test data is in scale 1 settings.core.enableLazyLoading = false; - settings.display.resources.copyrightFont.families = ['Noto Sans']; - settings.display.resources.titleFont.families = ['Noto Serif']; - settings.display.resources.subTitleFont.families = ['Noto Serif']; - settings.display.resources.wordsFont.families = ['Noto Serif']; - settings.display.resources.effectFont.families = ['Noto Serif']; - settings.display.resources.timerFont.families = ['Noto Serif']; - settings.display.resources.fretboardNumberFont.families = ['Noto Sans']; - settings.display.resources.tablatureFont.families = ['Noto Sans']; - settings.display.resources.graceFont.families = ['Noto Sans']; - settings.display.resources.barNumberFont.families = ['Noto Sans']; - settings.display.resources.fingeringFont.families = ['Noto Serif']; - settings.display.resources.inlineFingeringFont.families = ['Noto Serif']; - settings.display.resources.markerFont.families = ['Noto Serif']; - settings.display.resources.directionsFont.families = ['Noto Serif']; - settings.display.resources.numberedNotationFont.families = ['Noto Sans']; - settings.display.resources.numberedNotationGraceFont.families = ['Noto Sans']; + 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.fingeringFont.families = ['Noto Serif', 'Noto Music', 'Noto Color Emoji']; + settings.display.resources.inlineFingeringFont.families = ['Noto Serif', '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']; } public static async compareVisualResult( @@ -343,7 +347,7 @@ export class VisualTestHelper { pixelMatchOptions.diffMask = true; pixelMatchOptions.alpha = 1; - let match = PixelMatch.match( + const match = PixelMatch.match( new Uint8Array(expectedImageData), new Uint8Array(actualImageData), new Uint8Array(diffImageData), @@ -353,15 +357,15 @@ export class VisualTestHelper { ); // only pixels that are not transparent are relevant for the diff-ratio - let totalPixels = match.totalPixels - match.transparentPixels; - let percentDifference = (match.differentPixels / totalPixels) * 100; + const totalPixels = match.totalPixels - match.transparentPixels; + const percentDifference = (match.differentPixels / totalPixels) * 100; pass = percentDifference <= tolerancePercent; // result.pass = match.differentPixels === 0; errorMessage = ''; if (!pass) { - let percentDifferenceText = percentDifference.toFixed(2); + const percentDifferenceText = percentDifference.toFixed(2); errorMessage = `Difference between original and new image is too big: ${match.differentPixels}/${totalPixels} (${percentDifferenceText}%)`; using diffPng = AlphaSkiaImage.fromPixels(actual.width, actual.height, diffImageData)!; @@ -379,15 +383,14 @@ export class VisualTestHelper { } } else { pass = false; - errorMessage = 'Missing reference image file' + expectedFileName; + errorMessage = `Missing reference image file${expectedFileName}`; await VisualTestHelper.saveFiles(expectedFileName, oldActual, undefined); } if (!pass) { throw new Error(errorMessage); - } else { - await VisualTestHelper.deleteFiles(expectedFileName); } + await VisualTestHelper.deleteFiles(expectedFileName); } static async saveFiles( @@ -395,7 +398,6 @@ export class VisualTestHelper { actual: AlphaSkiaImage, diff: AlphaSkiaImage | undefined ): Promise { - expectedFilePath = TestPlatform.joinPath('test-data', 'visual-tests', expectedFilePath); if (diff) { const diffData = diff.toPng()!; @@ -409,8 +411,6 @@ export class VisualTestHelper { } static async deleteFiles(expectedFilePath: string): Promise { - expectedFilePath = TestPlatform.joinPath('test-data', 'visual-tests', expectedFilePath); - const diffFileName = TestPlatform.changeExtension(expectedFilePath, '.diff.png'); await TestPlatform.deleteFile(diffFileName); diff --git a/test/visualTests/features/BoundsLookup.test.ts b/test/visualTests/features/BoundsLookup.test.ts index 2684d104c..9c1948c03 100644 --- a/test/visualTests/features/BoundsLookup.test.ts +++ b/test/visualTests/features/BoundsLookup.test.ts @@ -1,7 +1,8 @@ +import { Color } from '@src/model/Color'; import { VisualTestHelper, VisualTestOptions } from '../VisualTestHelper'; import { AlphaSkiaCanvas } from '@coderline/alphaskia'; -import { Color } from '@src/model'; -import { Bounds, BoundsLookup } from '@src/rendering'; +import type { BoundsLookup } from '@src/rendering/utils/BoundsLookup'; +import { Bounds } from '@src/rendering/utils/Bounds'; describe('BoundsLookupRenderingTests', () => { async function runTest(referenceFileName: string, color: Color, collectBounds: (bounds: BoundsLookup) => Bounds[]) { @@ -16,7 +17,7 @@ describe('BoundsLookupRenderingTests', () => { \\track "Guitar 2" 3.3 `, - referenceFileName + `test-data/visual-tests/${referenceFileName}` ); o.tracks = [0, 1]; o.prepareFullImage = (_run, api, img) => { diff --git a/test/visualTests/features/EffectsAndAnnotations.test.ts b/test/visualTests/features/EffectsAndAnnotations.test.ts index c4495c5e2..59e769c2a 100644 --- a/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -1,5 +1,5 @@ import { SystemsLayoutMode } from '@src/DisplaySettings'; -import { ScoreLoader } from '@src/importer'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { BeatBarreEffectInfo } from '@src/rendering/effects/BeatBarreEffectInfo'; @@ -24,7 +24,7 @@ describe('EffectsAndAnnotationsTests', () => { . :4 3.3*4 | 3.3 3.3 {v f tempo 120 "Other" } 3.3 6.3 `, - `effects-and-annotations/tempo-text.png` + 'test-data/visual-tests/effects-and-annotations/tempo-text.png' ); }); @@ -94,18 +94,18 @@ describe('EffectsAndAnnotationsTests', () => { }); it('slides-line-break', async () => { - const tex = `14.1.2 :8 17.2 15.1 14.1{h} 17.2{ss} | 18.2`; + const tex = '14.1.2 :8 17.2 15.1 14.1{h} 17.2{ss} | 18.2'; const settings = new Settings(); settings.display.barsPerRow = 1; const importer = new AlphaTexImporter(); importer.init(ByteBuffer.fromString(tex), settings); - let score = importer.readScore(); + const score = importer.readScore(); await VisualTestHelper.runVisualTestFull( new VisualTestOptions( score, - [new VisualTestRun(400, 'effects-and-annotations/slides-line-break.png')], + [new VisualTestRun(400, 'test-data/visual-tests/effects-and-annotations/slides-line-break.png')], settings ) ); @@ -160,9 +160,9 @@ describe('EffectsAndAnnotationsTests', () => { it('sustain-pedal', async () => { await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file('effects-and-annotations/sustain.gp', [ - new VisualTestRun(1200, 'effects-and-annotations/sustain-1200.png'), - new VisualTestRun(850, 'effects-and-annotations/sustain-850.png'), - new VisualTestRun(600, 'effects-and-annotations/sustain-600.png') + new VisualTestRun(1200, 'test-data/visual-tests/effects-and-annotations/sustain-1200.png'), + new VisualTestRun(850, 'test-data/visual-tests/effects-and-annotations/sustain-850.png'), + new VisualTestRun(600, 'test-data/visual-tests/effects-and-annotations/sustain-600.png') ]) ); }); @@ -190,9 +190,9 @@ describe('EffectsAndAnnotationsTests', () => { new VisualTestOptions( score, [ - new VisualTestRun(1200, 'effects-and-annotations/sustain-1200.png'), - new VisualTestRun(850, 'effects-and-annotations/sustain-850.png'), - new VisualTestRun(600, 'effects-and-annotations/sustain-600.png') + new VisualTestRun(1200, 'test-data/visual-tests/effects-and-annotations/sustain-1200.png'), + new VisualTestRun(850, 'test-data/visual-tests/effects-and-annotations/sustain-850.png'), + new VisualTestRun(600, 'test-data/visual-tests/effects-and-annotations/sustain-600.png') ], settings ) @@ -260,7 +260,7 @@ describe('EffectsAndAnnotationsTests', () => { it('bend-vibrato-default', async () => { const inputFileData = await TestPlatform.loadFile( - `test-data/visual-tests/effects-and-annotations/bend-vibrato.gp` + 'test-data/visual-tests/effects-and-annotations/bend-vibrato.gp' ); const settings = new Settings(); const score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); @@ -268,7 +268,7 @@ describe('EffectsAndAnnotationsTests', () => { await VisualTestHelper.runVisualTestFull( new VisualTestOptions( score, - [new VisualTestRun(-1, 'effects-and-annotations/bend-vibrato-default.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/effects-and-annotations/bend-vibrato-default.png')], settings ) ); @@ -276,7 +276,7 @@ describe('EffectsAndAnnotationsTests', () => { it('bend-vibrato-songbook', async () => { const inputFileData = await TestPlatform.loadFile( - `test-data/visual-tests/effects-and-annotations/bend-vibrato.gp` + 'test-data/visual-tests/effects-and-annotations/bend-vibrato.gp' ); const settings = new Settings(); settings.setSongBookModeSettings(); @@ -285,7 +285,7 @@ describe('EffectsAndAnnotationsTests', () => { await VisualTestHelper.runVisualTestFull( new VisualTestOptions( score, - [new VisualTestRun(-1, 'effects-and-annotations/bend-vibrato-songbook.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/effects-and-annotations/bend-vibrato-songbook.png')], settings ) ); @@ -304,6 +304,9 @@ describe('EffectsAndAnnotationsTests', () => { }); it('legato', async () => { - await VisualTestHelper.runVisualTestTex(`3.3.4{ legatoOrigin } 10.3.4`, 'effects-and-annotations/legato.png'); + await VisualTestHelper.runVisualTestTex( + '3.3.4{ legatoOrigin } 10.3.4', + 'test-data/visual-tests/effects-and-annotations/legato.png' + ); }); }); diff --git a/test/visualTests/features/General.test.ts b/test/visualTests/features/General.test.ts index 4a962a634..3be0e24a5 100644 --- a/test/visualTests/features/General.test.ts +++ b/test/visualTests/features/General.test.ts @@ -1,6 +1,16 @@ import { StaveProfile } from '@src/StaveProfile'; import { Settings } from '@src/Settings'; import { VisualTestHelper } from '@test/visualTests/VisualTestHelper'; +import { TestPlatform } from '@test/TestPlatform'; +import { expect } from 'chai'; +import { Logger } from '@src/Logger'; +import { BarStyle, BarSubElement } from '@src/model/Bar'; +import { BeatStyle, BeatSubElement } from '@src/model/Beat'; +import { Color } from '@src/model/Color'; +import { NoteStyle, NoteSubElement } from '@src/model/Note'; +import { type Score, ScoreStyle, ScoreSubElement } from '@src/model/Score'; +import { TrackStyle, TrackSubElement } from '@src/model/Track'; +import { VoiceStyle, VoiceSubElement } from '@src/model/Voice'; describe('GeneralTests', () => { it('song-details', async () => { @@ -8,13 +18,13 @@ describe('GeneralTests', () => { }); it('repeats', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('general/repeats.gp', settings); }); it('alternate-endings', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('general/alternate-endings.gp', settings); }); @@ -22,4 +32,161 @@ describe('GeneralTests', () => { it('tuning', async () => { await VisualTestHelper.runVisualTest('general/tuning.gp'); }); + + function enableColoring(score: Score) { + const shuffledHues = [ + 0.38, 0.88, 0.04, 0, 0.08, 0.36, 0.48, 0.94, 0.64, 0.72, 0.76, 0.34, 0.44, 0.02, 0.56, 0.1, 0.7, 0.66, 0.96, + 0.68, 0.16, 0.5, 0.46, 0.3, 0.4, 0.26, 0.92, 0.2, 0.24, 0.42, 0.58, 0.74, 0.8, 0.84, 0.22, 0.32, 0.28, 0.12, + 0.9, 0.18, 0.14, 0.54, 0.6, 0.62, 0.86, 0.52, 0.78, 0.82, 0.06, 0.98 + ]; + let hueIndex = 0; + let saturation = 1; + let lightness = 0.5; + + function hueToRgb(p: number, q: number, t: number) { + if (t < 0) { + t += 1; + } + if (t > 1) { + t -= 1; + } + if (t < 1 / 6) { + return p + (q - p) * 6 * t; + } + if (t < 1 / 2) { + return q; + } + if (t < 2 / 3) { + return p + (q - p) * (2 / 3 - t) * 6; + } + return p; + } + + function randomColor() { + hueIndex++; + if (hueIndex >= shuffledHues.length) { + hueIndex = 0; + saturation -= 0.05; + + if (saturation <= 0) { + saturation = 1; + lightness -= 0.05; + + if (lightness < 0) { + lightness = 0.5; + } + } + } + + const h = shuffledHues[hueIndex]; + const q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation; + const p = 2 * lightness - q; + const r = hueToRgb(p, q, h + 1 / 3); + const g = hueToRgb(p, q, h); + const b = hueToRgb(p, q, h - 1 / 3); + + return new Color((r * 255) | 0, (g * 255) | 0, (b * 255) | 0); + } + + score.style = new ScoreStyle(); + for (const k of TestPlatform.enumValues(ScoreSubElement)) { + score.style.colors.set(k, randomColor()); + } + + for (const t of score.tracks) { + t.style = new TrackStyle(); + for (const k of TestPlatform.enumValues(TrackSubElement)) { + t.style.colors.set(k, randomColor()); + } + + for (const s of t.staves) { + for (const bar of s.bars) { + bar.style = new BarStyle(); + for (const k of TestPlatform.enumValues(BarSubElement)) { + if (typeof k === 'number') { + bar.style.colors.set(k, randomColor()); + } + } + + for (const v of bar.voices) { + v.style = new VoiceStyle(); + for (const k of TestPlatform.enumValues(VoiceSubElement)) { + if (typeof k === 'number') { + v.style.colors.set(k, randomColor()); + } + } + + for (const b of v.beats) { + b.style = new BeatStyle(); + for (const k of TestPlatform.enumValues(BeatSubElement)) { + if (typeof k === 'number') { + b.style.colors.set(k, randomColor()); + } + } + + for (const n of b.notes) { + n.style = new NoteStyle(); + for (const k of TestPlatform.enumValues(NoteSubElement)) { + if (typeof k === 'number') { + n.style.colors.set(k, randomColor()); + } + } + } + } + } + } + } + } + } + + it('colors', async () => { + await VisualTestHelper.runVisualTest('general/colors.gp', undefined, o => { + enableColoring(o.score); + }); + }); + + it('color-performance', async () => { + // warm-up + for (let i = 0; i < 5; i++) { + await VisualTestHelper.runVisualTest('general/colors.gp', undefined, o => { + enableColoring(o.score); + }); + } + + for (let i = 0; i < 10; i++) { + let coloredStart: number = 0; + await VisualTestHelper.runVisualTest('general/colors.gp', undefined, o => { + enableColoring(o.score); + coloredStart = performance.now(); + }); + const coloredEnd = performance.now(); + + let defaultStart: number = 0; + + await VisualTestHelper.runVisualTest('general/colors.gp', undefined, o => { + o.runs[0].referenceFileName = 'test-data/visual-tests/general/colors-disabled.png'; + defaultStart = performance.now(); + }); + const defaultEnd = performance.now(); + + const coloredDuration = coloredEnd - coloredStart; + const defaultDuration = defaultEnd - defaultStart; + + expect(coloredDuration - defaultDuration).to.be.lessThan(120); + + Logger.info('Test-color-performance', 'Colored', i, coloredDuration); + Logger.info('Test-color-performance', 'Default', i, defaultDuration); + } + }); + + it('font-fallback', async () => { + await VisualTestHelper.runVisualTestTex( + `\\title "Normal ♮♯ 🎸" + . + \\track "Track 🎸" + \\lyrics "Test Lyrics 🤘" + (1.2 1.1).4 x.2.8 0.1 1.1 | 1.2 3.2 0.1 1.1`, + 'test-data/visual-tests/general/font-fallback.png' + ); + }); }); diff --git a/test/visualTests/features/GuitarTabs.test.ts b/test/visualTests/features/GuitarTabs.test.ts index e84f52af5..ae649d72b 100644 --- a/test/visualTests/features/GuitarTabs.test.ts +++ b/test/visualTests/features/GuitarTabs.test.ts @@ -5,23 +5,22 @@ import { VisualTestHelper } from '@test/visualTests/VisualTestHelper'; describe('GuitarTabsTests', () => { it('rhythm', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Tab; settings.notation.rhythmMode = TabRhythmMode.ShowWithBars; await VisualTestHelper.runVisualTest('guitar-tabs/rhythm.gp', settings); }); it('rhythm-with-beams', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Tab; settings.notation.rhythmMode = TabRhythmMode.ShowWithBeams; await VisualTestHelper.runVisualTest('guitar-tabs/rhythm-with-beams.gp', settings); }); it('string-variations', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Tab; await VisualTestHelper.runVisualTest('guitar-tabs/string-variations.gp', settings); }); - }); diff --git a/test/visualTests/features/Layout.test.ts b/test/visualTests/features/Layout.test.ts index 8fc96cd50..34f189e24 100644 --- a/test/visualTests/features/Layout.test.ts +++ b/test/visualTests/features/Layout.test.ts @@ -15,7 +15,7 @@ describe('LayoutTests', () => { await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( 'layout/page-layout.gp', - [new VisualTestRun(-1, 'layout/page-layout-justify-last-row.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/layout/page-layout-justify-last-row.png')], settings ) ); @@ -32,14 +32,14 @@ describe('LayoutTests', () => { }); it('page-layout-5barsperrow', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.display.barsPerRow = 5; await VisualTestHelper.runVisualTest('layout/page-layout-5barsperrow.gp', settings); }); it('page-layout-bar5to8', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.display.startBar = 5; settings.display.barCount = 4; @@ -47,13 +47,13 @@ describe('LayoutTests', () => { }); it('horizontal-layout', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Horizontal; await VisualTestHelper.runVisualTest('layout/horizontal-layout.gp', settings); }); it('horizontal-layout-bar5to8', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Horizontal; settings.display.startBar = 5; settings.display.barCount = 4; @@ -134,8 +134,29 @@ describe('LayoutTests', () => { \\scale 0.5 c4 | \\scale 2 c4 | \\scale 0.5 c4 | c4 | c4 `, - 'layout/system-layout-tex.png', + 'test-data/visual-tests/layout/system-layout-tex.png', settings ); }); + + it('multibar-rests-single-track', async () => { + await VisualTestHelper.runVisualTest('layout/multibar-rest.gp', undefined, o => { + o.tracks = [0]; + o.runs[0].referenceFileName = 'test-data/visual-tests/layout/multibar-rest-single-track.png'; + }); + }); + + it('multibar-rests-multi-track', async () => { + await VisualTestHelper.runVisualTest('layout/multibar-rest.gp', undefined, o => { + o.tracks = [0, 1]; + o.runs[0].referenceFileName = 'test-data/visual-tests/layout/multibar-rest-multi-track.png'; + }); + }); + + it('multibar-rests-all-tracks', async () => { + await VisualTestHelper.runVisualTest('layout/multibar-rest.gp', undefined, o => { + o.tracks = [0, 1, 2]; + o.runs[0].referenceFileName = 'test-data/visual-tests/layout/multibar-rest-all-tracks.png'; + }); + }); }); diff --git a/test/visualTests/features/MusicNotation.test.ts b/test/visualTests/features/MusicNotation.test.ts index 438cb2028..fa2082a20 100644 --- a/test/visualTests/features/MusicNotation.test.ts +++ b/test/visualTests/features/MusicNotation.test.ts @@ -7,7 +7,7 @@ import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; describe('MusicNotationTests', () => { it('clefs', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.ScoreAlbum, false); @@ -22,7 +22,7 @@ describe('MusicNotationTests', () => { }); it('key-signatures-mixed', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('music-notation/key-signatures-mixed.gp', settings, o => { o.tracks = [0, 1, 2, 3]; @@ -30,55 +30,55 @@ describe('MusicNotationTests', () => { }); it('key-signatures-c3', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('music-notation/key-signatures-c3.gp', settings); }); it('key-signatures-c4', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('music-notation/key-signatures-c4.gp', settings); }); it('key-signatures-f4', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('music-notation/key-signatures-f4.gp', settings); }); it('key-signatures-g2', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('music-notation/key-signatures-g2.gp', settings); }); it('key-signatures', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('music-notation/key-signatures.gp', settings); }); it('time-signatures', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('music-notation/time-signatures.gp', settings); }); it('notes-rests-beams', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('music-notation/notes-rests-beams.gp', settings); }); it('accidentals', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('music-notation/accidentals.gp', settings); }); it('forced-accidentals', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.staveProfile = StaveProfile.Score; await VisualTestHelper.runVisualTest('music-notation/forced-accidentals.gp', settings, o => { o.tracks = [0, 1]; @@ -86,7 +86,7 @@ describe('MusicNotationTests', () => { }); it('beams-advanced', async () => { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.barsPerRow = 4; await VisualTestHelper.runVisualTest('music-notation/beams-advanced.gp', settings); }); @@ -174,7 +174,15 @@ describe('MusicNotationTests', () => { // score.stylesheet.bracketExtendMode = BracketExtendMode.NoBrackets; await VisualTestHelper.runVisualTestFull( - new VisualTestOptions(score, [new VisualTestRun(-1, 'music-notation/accidentals-advanced.png')], settings) + new VisualTestOptions( + score, + [new VisualTestRun(-1, 'test-data/visual-tests/music-notation/accidentals-advanced.png')], + settings + ) ); }); + + it('bar-lines', async () => { + await VisualTestHelper.runVisualTest('music-notation/barlines.xml'); + }); }); diff --git a/test/visualTests/features/NotationElements.test.ts b/test/visualTests/features/NotationElements.test.ts index 64950ba79..e0af91dad 100644 --- a/test/visualTests/features/NotationElements.test.ts +++ b/test/visualTests/features/NotationElements.test.ts @@ -4,8 +4,10 @@ import { VisualTestHelper, VisualTestOptions } from '@test/visualTests/VisualTes import { NotationElement } from '@src/NotationSettings'; describe('NotationElements', () => { - async function testScoreInfo(element: NotationElement|null, referenceName: string, tex?:string) { - tex = tex ?? `\\album "Album" \\artist "Artist" \\copyright "Copyright" \\music "Music" \\subtitle "Subtitle" \\title "Title" \\words "Words" . 3.3*4`; + async function testScoreInfo(element: NotationElement | null, referenceName: string, tex?: string) { + tex = + tex ?? + `\\album "Album" \\artist "Artist" \\copyright "Copyright" \\music "Music" \\subtitle "Subtitle" \\title "Title" \\words "Words" . 3.3*4`; const allKeys = [ NotationElement.ScoreAlbum, @@ -18,21 +20,25 @@ describe('NotationElements', () => { NotationElement.ScoreWordsAndMusic ]; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; - if(element !== null) { + if (element !== null) { for (const k of allKeys) { settings.notation.elements.set(k, false); } - + settings.notation.elements.set(element, true); } else { settings.notation.elements.clear(); } - + await VisualTestHelper.runVisualTestFull( - VisualTestOptions.tex(tex, `notation-elements/score-info-${referenceName}.png`, settings) + VisualTestOptions.tex( + tex, + `test-data/visual-tests/notation-elements/score-info-${referenceName}.png`, + settings + ) ); } @@ -43,176 +49,236 @@ describe('NotationElements', () => { it('score-info-artist', async () => { await testScoreInfo(NotationElement.ScoreArtist, 'artist'); }); - + it('score-info-copyright', async () => { await testScoreInfo(NotationElement.ScoreCopyright, 'copyright'); }); - + it('score-info-music', async () => { await testScoreInfo(NotationElement.ScoreMusic, 'music'); }); - + it('score-info-subtitle', async () => { await testScoreInfo(NotationElement.ScoreSubTitle, 'subtitle'); }); - + it('score-info-title', async () => { await testScoreInfo(NotationElement.ScoreTitle, 'title'); }); - + it('score-info-words', async () => { await testScoreInfo(NotationElement.ScoreWords, 'words'); }); - + it('score-info-words-and-music', async () => { - await testScoreInfo(NotationElement.ScoreWordsAndMusic, 'words-and-music', ` + await testScoreInfo( + NotationElement.ScoreWordsAndMusic, + 'words-and-music', + ` \\album "Album" \\artist "Artist" \\copyright "Copyright" \\music "WordsAndMusic" \\subtitle "Subtitle" \\title "Title" \\words "WordsAndMusic" . 3.3*4 - `); + ` + ); }); - + it('score-info-all', async () => { await testScoreInfo(null, 'all'); }); - + it('guitar-tuning-on', async () => { - const tex = `\\tuning e5 b4 g4 d4 a3 d3 . 3.3*4`; + const tex = '\\tuning e5 b4 g4 d4 a3 d3 . 3.3*4'; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.GuitarTuning, true); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/guitar-tuning-on.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/guitar-tuning-on.png', + settings + ); }); it('guitar-tuning-off', async () => { - const tex = `\\tuning d5 b4 g4 d4 a3 d3 . 3.3*4`; + const tex = '\\tuning d5 b4 g4 d4 a3 d3 . 3.3*4'; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.GuitarTuning, false); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/guitar-tuning-off.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/guitar-tuning-off.png', + settings + ); }); it('track-names-off', async () => { const tex = `\\track "Track Name" 3.3*4`; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.TrackNames, false); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/track-names-off.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/track-names-off.png', + settings + ); }); it('track-names-on', async () => { const tex = `\\track "Track Name" 3.3*4`; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.TrackNames, true); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/track-names-on.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/track-names-on.png', + settings + ); }); it('chord-diagrams-off', async () => { const tex = `\\chord "C" 0 1 0 2 3 x . (0.1 1.2 0.3 2.4 3.5){ch "C"}`; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.ChordDiagrams, false); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/chord-diagrams-off.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/chord-diagrams-off.png', + settings + ); }); it('chord-diagrams-on', async () => { const tex = `\\chord "C" 0 1 0 2 3 x . (0.1 1.2 0.3 2.4 3.5){ch "C"}`; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.ChordDiagrams, true); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/chord-diagrams-on.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/chord-diagrams-on.png', + settings + ); }); it('parenthesis-on-tied-bends-off', async () => { - const tex = `3.3{b (0 4 )} -.3`; + const tex = '3.3{b (0 4 )} -.3'; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.ParenthesisOnTiedBends, false); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/parenthesis-on-tied-bends-off.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png', + settings + ); }); it('parenthesis-on-tied-bends-on', async () => { - const tex = `3.3{b (0 4 )} -.3`; + const tex = '3.3{b (0 4 )} -.3'; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.ParenthesisOnTiedBends, true); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/parenthesis-on-tied-bends-on.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png', + settings + ); }); it('tab-notes-on-tied-bends-off', async () => { - const tex = `3.3{b (0 4 )} -.3{b (4 8)}`; + const tex = '3.3{b (0 4 )} -.3{b (4 8)}'; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.TabNotesOnTiedBends, false); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/tab-notes-on-tied-bends-off.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png', + settings + ); }); it('tab-notes-on-tied-bends-on', async () => { - const tex = `3.3{b (0 4 )} -.3{b (4 8)}`; + const tex = '3.3{b (0 4 )} -.3{b (4 8)}'; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.TabNotesOnTiedBends, true); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/tab-notes-on-tied-bends-on.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png', + settings + ); }); it('zeros-on-dive-whammys-off', async () => { - const tex = `3.3.1{tb (0 -4)}`; + const tex = '3.3.1{tb (0 -4)}'; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.ZerosOnDiveWhammys, false); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/zeros-on-dive-whammys-off.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png', + settings + ); }); it('zeros-on-dive-whammys-on', async () => { - const tex = `3.3.1{tb (0 -4)}`; + const tex = '3.3.1{tb (0 -4)}'; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.ZerosOnDiveWhammys, true); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/zeros-on-dive-whammys-on.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png', + settings + ); }); it('effects-off', async () => { - const tex = `. \\tempo 180 \\tf t16 3.3*4 | \\tempo 60 \\tf d16 3.3*4`; + const tex = '. \\tempo 180 \\tf t16 3.3*4 | \\tempo 60 \\tf d16 3.3*4'; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.EffectTempo, false); settings.notation.elements.set(NotationElement.EffectTripletFeel, false); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/effects-off.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/effects-off.png', + settings + ); }); it('effects-on', async () => { - const tex = `. \\tempo 180 \\tf t16 3.3*4 | \\tempo 60 \\tf d16 3.3*4`; + const tex = '. \\tempo 180 \\tf t16 3.3*4 | \\tempo 60 \\tf d16 3.3*4'; - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Page; settings.notation.elements.set(NotationElement.EffectTempo, true); settings.notation.elements.set(NotationElement.EffectTripletFeel, true); - await VisualTestHelper.runVisualTestTex(tex, `notation-elements/effects-on.png`, settings); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/effects-on.png', + settings + ); }); }); diff --git a/test/visualTests/features/NotationLegend.test.ts b/test/visualTests/features/NotationLegend.test.ts index a3f787098..9ac5d976e 100644 --- a/test/visualTests/features/NotationLegend.test.ts +++ b/test/visualTests/features/NotationLegend.test.ts @@ -3,14 +3,14 @@ import { Settings } from '@src/Settings'; import { VisualTestHelper, VisualTestOptions, VisualTestRun } from '@test/visualTests/VisualTestHelper'; import { TestPlatform } from '@test/TestPlatform'; import { ScoreLoader } from '@src/importer/ScoreLoader'; -import { Score } from '@src/model/Score'; +import type { Score } from '@src/model/Score'; describe('NotationLegend', () => { it('full-default', async () => { - await runNotationLegendTest(`full-default.png`, 1, -1, false); + await runNotationLegendTest('full-default.png', 1, -1, false); }); it('full-songbook', async () => { - await runNotationLegendTest(`full-songbook.png`, 1, -1, true); + await runNotationLegendTest('full-songbook.png', 1, -1, true); }); it('full-default-small', async () => { @@ -19,7 +19,7 @@ describe('NotationLegend', () => { await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( 'notation-legend/notation-legend.gp', - [new VisualTestRun(-1, 'notation-legend/full-default-small.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/notation-legend/full-default-small.png')], settings ) ); @@ -31,229 +31,229 @@ describe('NotationLegend', () => { await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( 'notation-legend/notation-legend.gp', - [new VisualTestRun(-1, 'notation-legend/full-default-large.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/notation-legend/full-default-large.png')], settings ) ); }); it('bends-default', async () => { - await runNotationLegendTest(`bends-default.png`, 1, 29, false); + await runNotationLegendTest('bends-default.png', 1, 29, false); }); it('bends-songbook', async () => { - await runNotationLegendTest(`bends-songbook.png`, 1, 29, true); + await runNotationLegendTest('bends-songbook.png', 1, 29, true); }); it('grace-default', async () => { - await runNotationLegendTest(`grace-default.png`, 30, 2, false); + await runNotationLegendTest('grace-default.png', 30, 2, false); }); it('grace-songbook', async () => { - await runNotationLegendTest(`grace-songbook.png`, 30, 2, true); + await runNotationLegendTest('grace-songbook.png', 30, 2, true); }); it('vibrato-default', async () => { - await runNotationLegendTest(`vibrato-default.png`, 32, 4, false); + await runNotationLegendTest('vibrato-default.png', 32, 4, false); }); it('vibrato-songbook', async () => { - await runNotationLegendTest(`vibrato-songbook.png`, 32, 4, true); + await runNotationLegendTest('vibrato-songbook.png', 32, 4, true); }); it('multi-grace-default', async () => { - await runNotationLegendTest(`multi-grace-default.png`, 36, 4, false); + await runNotationLegendTest('multi-grace-default.png', 36, 4, false); }); it('multi-grace-songbook', async () => { - await runNotationLegendTest(`multi-grace-songbook.png`, 36, 4, true); + await runNotationLegendTest('multi-grace-songbook.png', 36, 4, true); }); it('pick-stroke-default', async () => { - await runNotationLegendTest(`pick-stroke-default.png`, 40, 1, false); + await runNotationLegendTest('pick-stroke-default.png', 40, 1, false); }); it('pick-stroke-songbook', async () => { - await runNotationLegendTest(`pick-stroke-songbook.png`, 40, 1, true); + await runNotationLegendTest('pick-stroke-songbook.png', 40, 1, true); }); it('slides-default', async () => { - await runNotationLegendTest(`slides-default.png`, 41, 8, false); + await runNotationLegendTest('slides-default.png', 41, 8, false); }); it('slides-songbook', async () => { - await runNotationLegendTest(`slides-songbook.png`, 41, 8, true); + await runNotationLegendTest('slides-songbook.png', 41, 8, true); }); it('hammer-default', async () => { - await runNotationLegendTest(`hammer-default.png`, 49, 5, false); + await runNotationLegendTest('hammer-default.png', 49, 5, false); }); it('hammer-songbook', async () => { - await runNotationLegendTest(`hammer-songbook.png`, 49, 5, true); + await runNotationLegendTest('hammer-songbook.png', 49, 5, true); }); it('accentuations-default', async () => { - await runNotationLegendTest(`accentuations-default.png`, 54, 4, false); + await runNotationLegendTest('accentuations-default.png', 54, 4, false); }); it('accentuations-songbook', async () => { - await runNotationLegendTest(`accentuations-songbook.png`, 44, 4, true); + await runNotationLegendTest('accentuations-songbook.png', 44, 4, true); }); it('trill-default', async () => { - await runNotationLegendTest(`trill-default.png`, 58, 2, false); + await runNotationLegendTest('trill-default.png', 58, 2, false); }); it('trill-songbook', async () => { - await runNotationLegendTest(`trill-songbook.png`, 58, 2, true); + await runNotationLegendTest('trill-songbook.png', 58, 2, true); }); it('dead-default', async () => { - await runNotationLegendTest(`dead-default.png`, 60, 2, false); + await runNotationLegendTest('dead-default.png', 60, 2, false); }); it('dead-songbook', async () => { - await runNotationLegendTest(`dead-songbook.png`, 60, 2, true); + await runNotationLegendTest('dead-songbook.png', 60, 2, true); }); it('harmonics-default', async () => { - await runNotationLegendTest(`harmonics-default.png`, 62, 7, false); + await runNotationLegendTest('harmonics-default.png', 62, 7, false); }); it('harmonics-songbook', async () => { - await runNotationLegendTest(`harmonics-songbook.png`, 62, 7, true); + await runNotationLegendTest('harmonics-songbook.png', 62, 7, true); }); it('repeat-bar-default', async () => { - await runNotationLegendTest(`tap-riff-default.png`, 69, 4, false); + await runNotationLegendTest('tap-riff-default.png', 69, 4, false); }); it('repeat-bar-songbook', async () => { - await runNotationLegendTest(`tap-riff-songbook.png`, 69, 4, true); + await runNotationLegendTest('tap-riff-songbook.png', 69, 4, true); }); it('multi-voice-default', async () => { - await runNotationLegendTest(`multi-voice-default.png`, 73, 1, false); + await runNotationLegendTest('multi-voice-default.png', 73, 1, false); }); it('multi-voice-songbook', async () => { - await runNotationLegendTest(`multi-voice-songbook.png`, 73, 1, true); + await runNotationLegendTest('multi-voice-songbook.png', 73, 1, true); }); it('arpeggio-default', async () => { - await runNotationLegendTest(`arpeggio-default.png`, 74, 2, false); + await runNotationLegendTest('arpeggio-default.png', 74, 2, false); }); it('arpeggio-songbook', async () => { - await runNotationLegendTest(`arpeggio-songbook.png`, 74, 2, true); + await runNotationLegendTest('arpeggio-songbook.png', 74, 2, true); }); it('triplet-feel-default', async () => { - await runNotationLegendTest(`triplet-feel-default.png`, 76, 3, false); + await runNotationLegendTest('triplet-feel-default.png', 76, 3, false); }); it('triplet-feel-songbook', async () => { - await runNotationLegendTest(`triplet-feel-songbook.png`, 76, 3, true); + await runNotationLegendTest('triplet-feel-songbook.png', 76, 3, true); }); it('ottavia-default', async () => { - await runNotationLegendTest(`ottavia-default.png`, 79, 2, false); + await runNotationLegendTest('ottavia-default.png', 79, 2, false); }); it('ottavia-songbook', async () => { - await runNotationLegendTest(`ottavia-songbook.png`, 79, 2, true); + await runNotationLegendTest('ottavia-songbook.png', 79, 2, true); }); it('crescendo-default', async () => { - await runNotationLegendTest(`crescendo-default.png`, 81, 1, false); + await runNotationLegendTest('crescendo-default.png', 81, 1, false); }); it('crescendo-songbook', async () => { - await runNotationLegendTest(`crescendo-songbook.png`, 81, 1, true); + await runNotationLegendTest('crescendo-songbook.png', 81, 1, true); }); it('tempo-change-default', async () => { - await runNotationLegendTest(`tempo-change-default.png`, 81, 5, false); + await runNotationLegendTest('tempo-change-default.png', 81, 5, false); }); it('tempo-change-songbook', async () => { - await runNotationLegendTest(`tempo-change-songbook.png`, 81, 5, true); + await runNotationLegendTest('tempo-change-songbook.png', 81, 5, true); }); it('slash-default', async () => { - await runNotationLegendTest(`slash-default.png`, 86, 1, false); + await runNotationLegendTest('slash-default.png', 86, 1, false); }); it('slash-songbook', async () => { - await runNotationLegendTest(`slash-songbook.png`, 86, 1, true); + await runNotationLegendTest('slash-songbook.png', 86, 1, true); }); it('text-default', async () => { - await runNotationLegendTest(`text-default.png`, 87, 1, false); + await runNotationLegendTest('text-default.png', 87, 1, false); }); it('text-songbook', async () => { - await runNotationLegendTest(`text-songbook.png`, 87, 1, true); + await runNotationLegendTest('text-songbook.png', 87, 1, true); }); it('chords-default', async () => { - await runNotationLegendTest(`chords-default.png`, 88, 2, false); + await runNotationLegendTest('chords-default.png', 88, 2, false); }); it('chords-songbook', async () => { - await runNotationLegendTest(`chords-songbook.png`, 88, 2, true); + await runNotationLegendTest('chords-songbook.png', 88, 2, true); }); it('staccatissimo-default', async () => { - await runNotationLegendTest(`staccatissimo-default.png`, 90, 1, false); + await runNotationLegendTest('staccatissimo-default.png', 90, 1, false); }); it('staccatissimo-songbook', async () => { - await runNotationLegendTest(`staccatissimo-songbook.png`, 90, 1, true); + await runNotationLegendTest('staccatissimo-songbook.png', 90, 1, true); }); it('wah-default', async () => { - await runNotationLegendTest(`wah-default.png`, 91, 1, false); + await runNotationLegendTest('wah-default.png', 91, 1, false); }); it('wah-songbook', async () => { - await runNotationLegendTest(`wah-songbook.png`, 91, 1, true); + await runNotationLegendTest('wah-songbook.png', 91, 1, true); }); it('dynamics-default', async () => { - await runNotationLegendTest(`dynamics-default.png`, 92, 1, false); + await runNotationLegendTest('dynamics-default.png', 92, 1, false); }); it('dynamics-songbook', async () => { - await runNotationLegendTest(`dynamics-songbook.png`, 92, 1, true); + await runNotationLegendTest('dynamics-songbook.png', 92, 1, true); }); it('sweep-default', async () => { - await runNotationLegendTest(`sweep-default.png`, 93, 1, false); + await runNotationLegendTest('sweep-default.png', 93, 1, false); }); it('sweep-songbook', async () => { - await runNotationLegendTest(`sweep-songbook.png`, 93, 1, true); + await runNotationLegendTest('sweep-songbook.png', 93, 1, true); }); it('fingering-default', async () => { - await runNotationLegendTest(`fingering-default.png`, 94, 2, false, 'notation-legend.gp'); + await runNotationLegendTest('fingering-default.png', 94, 2, false, 'notation-legend.gp'); }); it('fingering-songbook', async () => { - await runNotationLegendTest(`fingering-songbook.png`, 94, 2, true, 'notation-legend.gp'); + await runNotationLegendTest('fingering-songbook.png', 94, 2, true, 'notation-legend.gp'); }); it('whammy-default', async () => { - await runNotationLegendTest(`whammy-default.png`, 96, 15, false); + await runNotationLegendTest('whammy-default.png', 96, 15, false); }); it('whammy-songbook', async () => { - await runNotationLegendTest(`whammy-songbook.png`, 96, 15, true); + await runNotationLegendTest('whammy-songbook.png', 96, 15, true); }); it('let-ring-default', async () => { - await runNotationLegendTest(`let-ring-default.png`, 111, 10, false); + await runNotationLegendTest('let-ring-default.png', 111, 10, false); }); it('let-ring-songbook', async () => { - await runNotationLegendTest(`let-ring-songbook.png`, 111, 10, true); + await runNotationLegendTest('let-ring-songbook.png', 111, 10, true); }); it('mixed-default', async () => { - await runNotationLegendTest(`mixed-default.png`, 121, 7, false); + await runNotationLegendTest('mixed-default.png', 121, 7, false); }); it('mixed-songbook', async () => { - await runNotationLegendTest(`mixed-songbook.png`, 121, 7, true); + await runNotationLegendTest('mixed-songbook.png', 121, 7, true); }); it('tied-note-accidentals-default', async () => { - await runNotationLegendTest(`tied-note-accidentals-default.png`, 1, -1, false, 'tied-note-accidentals.gp'); + await runNotationLegendTest('tied-note-accidentals-default.png', 1, -1, false, 'tied-note-accidentals.gp'); }); it('tied-note-accidentals-songbook', async () => { - await runNotationLegendTest(`tied-note-accidentals-songbook.png`, 1, -1, true, 'tied-note-accidentals.gp'); + await runNotationLegendTest('tied-note-accidentals-songbook.png', 1, -1, true, 'tied-note-accidentals.gp'); }); it('resize-sequence', async () => { await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file('notation-legend/notation-legend.gp', [ - new VisualTestRun(1300, 'notation-legend/resize-sequence-1300.png'), - new VisualTestRun(800, 'notation-legend/resize-sequence-800.png'), - new VisualTestRun(1500, 'notation-legend/resize-sequence-1500.png'), - new VisualTestRun(500, 'notation-legend/resize-sequence-500.png') + new VisualTestRun(1300, 'test-data/visual-tests/notation-legend/resize-sequence-1300.png'), + new VisualTestRun(800, 'test-data/visual-tests/notation-legend/resize-sequence-800.png'), + new VisualTestRun(1500, 'test-data/visual-tests/notation-legend/resize-sequence-1500.png'), + new VisualTestRun(500, 'test-data/visual-tests/notation-legend/resize-sequence-500.png') ]) ); }); @@ -265,7 +265,7 @@ describe('NotationLegend', () => { songBook: boolean, fileName: string = 'notation-legend.gp' ): Promise { - let settings: Settings = new Settings(); + const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Horizontal; settings.display.startBar = startBar; settings.display.barCount = barCount; @@ -273,9 +273,13 @@ describe('NotationLegend', () => { settings.setSongBookModeSettings(); } const inputFileData = await TestPlatform.loadFile(`test-data/visual-tests/notation-legend/${fileName}`); - let score: Score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); + const score: Score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); - const o = new VisualTestOptions(score, [new VisualTestRun(-1, `notation-legend/${referenceFileName}`)], settings); + const o = new VisualTestOptions( + score, + [new VisualTestRun(-1, `test-data/visual-tests/notation-legend/${referenceFileName}`)], + settings + ); await VisualTestHelper.runVisualTestFull(o); } }); diff --git a/test/visualTests/features/SpecialNotes.test.ts b/test/visualTests/features/SpecialNotes.test.ts index f3299e896..38dc27380 100644 --- a/test/visualTests/features/SpecialNotes.test.ts +++ b/test/visualTests/features/SpecialNotes.test.ts @@ -20,9 +20,9 @@ describe('SpecialNotesTests', () => { // grace notes flick around to wrong positions during resizes // due to wrong size registrations. (#604) const options = await VisualTestOptions.file('special-notes/grace-notes-advanced.gp', [ - new VisualTestRun(1300, 'special-notes/grace-notes-advanced-1300.png'), - new VisualTestRun(1300, 'special-notes/grace-notes-advanced-1300-2.png'), - new VisualTestRun(800, 'special-notes/grace-notes-advanced-800.png') + new VisualTestRun(1300, 'test-data/visual-tests/special-notes/grace-notes-advanced-1300.png'), + new VisualTestRun(1300, 'test-data/visual-tests/special-notes/grace-notes-advanced-1300-2.png'), + new VisualTestRun(800, 'test-data/visual-tests/special-notes/grace-notes-advanced-800.png') ]); options.tracks = [0, 1]; await VisualTestHelper.runVisualTestFull(options); diff --git a/test/visualTests/features/SystemsLayout.test.ts b/test/visualTests/features/SystemsLayout.test.ts index 7e2b62ee7..8bf659a7b 100644 --- a/test/visualTests/features/SystemsLayout.test.ts +++ b/test/visualTests/features/SystemsLayout.test.ts @@ -1,7 +1,7 @@ import { SystemsLayoutMode } from '@src/DisplaySettings'; import { LayoutMode } from '@src/LayoutMode'; import { Settings } from '@src/Settings'; -import { ScoreLoader } from '@src/importer'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; import { TestPlatform } from '@test/TestPlatform'; import { VisualTestHelper, VisualTestOptions, VisualTestRun } from '@test/visualTests/VisualTestHelper'; @@ -13,7 +13,7 @@ describe('SystemsLayoutTests', () => { await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( 'systems-layout/bars-adjusted.gp', - [new VisualTestRun(-1, 'systems-layout/bars-adjusted-automatic.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/systems-layout/bars-adjusted-automatic.png')], settings ) ); @@ -26,7 +26,7 @@ describe('SystemsLayoutTests', () => { await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( 'systems-layout/bars-adjusted.gp', - [new VisualTestRun(-1, 'systems-layout/bars-adjusted-model.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/systems-layout/bars-adjusted-model.png')], settings ) ); @@ -39,7 +39,7 @@ describe('SystemsLayoutTests', () => { await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( 'systems-layout/multi-track-different.gp', - [new VisualTestRun(-1, 'systems-layout/multi-track-single-track.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/systems-layout/multi-track-single-track.png')], settings ) ); @@ -51,7 +51,7 @@ describe('SystemsLayoutTests', () => { const options = await VisualTestOptions.file( 'systems-layout/multi-track-different.gp', - [new VisualTestRun(-1, 'systems-layout/multi-track-two-tracks.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/systems-layout/multi-track-two-tracks.png')], settings ); options.tracks = [0, 1]; @@ -65,7 +65,7 @@ describe('SystemsLayoutTests', () => { await VisualTestHelper.runVisualTestFull( await VisualTestOptions.file( 'systems-layout/resized.gp', - [new VisualTestRun(-1, 'systems-layout/resized.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/systems-layout/resized.png')], settings ) ); @@ -77,7 +77,7 @@ describe('SystemsLayoutTests', () => { settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; const score = ScoreLoader.loadScoreFromBytes( - await TestPlatform.loadFile(`test-data/visual-tests/systems-layout/multi-track-different.gp`) + await TestPlatform.loadFile('test-data/visual-tests/systems-layout/multi-track-different.gp') ); for (const bars of score.tracks[0].staves[0].bars) { @@ -87,7 +87,12 @@ describe('SystemsLayoutTests', () => { await VisualTestHelper.runVisualTestFull( new VisualTestOptions( score, - [new VisualTestRun(-1, 'systems-layout/horizontal-fixed-sizes-single-track.png')], + [ + new VisualTestRun( + -1, + 'test-data/visual-tests/systems-layout/horizontal-fixed-sizes-single-track.png' + ) + ], settings ) ); @@ -98,7 +103,7 @@ describe('SystemsLayoutTests', () => { 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`) + await TestPlatform.loadFile('test-data/visual-tests/systems-layout/multi-track-different.gp') ); for (const masterBar of score.masterBars) { @@ -107,7 +112,7 @@ describe('SystemsLayoutTests', () => { const o = new VisualTestOptions( score, - [new VisualTestRun(-1, 'systems-layout/horizontal-fixed-sizes-two-tracks.png')], + [new VisualTestRun(-1, 'test-data/visual-tests/systems-layout/horizontal-fixed-sizes-two-tracks.png')], settings ); o.tracks = [0, 1]; diff --git a/test/visualTests/issues/BrokenRenders.test.ts b/test/visualTests/issues/BrokenRenders.test.ts index e790c625f..8d6130520 100644 --- a/test/visualTests/issues/BrokenRenders.test.ts +++ b/test/visualTests/issues/BrokenRenders.test.ts @@ -1,10 +1,11 @@ import { TestPlatform } from '@test/TestPlatform'; import { VisualTestHelper } from '../VisualTestHelper'; -import { ScoreLoader } from '@src/importer'; import { Settings } from '@src/Settings'; -import { RenderFinishedEventArgs, ScoreRenderer } from '@src/rendering'; import { XmlDocument } from '@src/xml/XmlDocument'; import { expect } from 'chai'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; +import type { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs'; +import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; describe('BrokenRendersTests', () => { it('let-ring-empty-voice', async () => { @@ -16,7 +17,7 @@ describe('BrokenRendersTests', () => { settings.core.engine = 'svg'; settings.core.enableLazyLoading = false; - const inputFileData = await TestPlatform.loadFile(`test-data/visual-tests/layout/page-layout.gp`); + const inputFileData = await TestPlatform.loadFile('test-data/visual-tests/layout/page-layout.gp'); const score = ScoreLoader.loadScoreFromBytes(inputFileData, settings); const api = new ScoreRenderer(settings); diff --git a/test/xml/XmlParse.test.ts b/test/xml/XmlParse.test.ts index 888213389..0da17bbb4 100644 --- a/test/xml/XmlParse.test.ts +++ b/test/xml/XmlParse.test.ts @@ -5,8 +5,8 @@ import { expect } from 'chai'; describe('XmlParseTest', () => { it('parseSimple', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; expect(xml.firstElement!.localName).to.equal('root'); @@ -14,8 +14,8 @@ describe('XmlParseTest', () => { }); it('parseShorthand', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; expect(xml.firstElement!.localName).to.equal('root'); @@ -23,8 +23,8 @@ describe('XmlParseTest', () => { }); it('parseSingleAttribute', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; expect(xml.firstElement!.localName).to.equal('root'); @@ -33,8 +33,8 @@ describe('XmlParseTest', () => { }); it('parseMultipleAttributes', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; expect(xml.firstElement!.localName).to.equal('root'); @@ -44,8 +44,8 @@ describe('XmlParseTest', () => { }); it('parseSimpleText', () => { - let s: string = 'Text'; - let xml: XmlDocument = new XmlDocument(); + const s: string = 'Text'; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; expect(xml.firstElement!.localName).to.equal('root'); @@ -55,8 +55,8 @@ describe('XmlParseTest', () => { }); it('parseChild', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; expect(xml.firstElement!.localName).to.equal('root'); @@ -66,8 +66,8 @@ describe('XmlParseTest', () => { }); it('parseMultiChild', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; expect(xml.firstElement!.localName).to.equal('root'); @@ -79,8 +79,9 @@ describe('XmlParseTest', () => { }); it('parseComments', () => { - let s: string = 'value'; - let xml: XmlDocument = new XmlDocument(); + const s: string = + 'value'; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; expect(xml.firstElement!.localName).to.equal('test'); @@ -96,8 +97,8 @@ describe('XmlParseTest', () => { }); it('parseDoctype', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; expect(xml.firstElement!.localName).to.equal('test'); @@ -109,8 +110,8 @@ describe('XmlParseTest', () => { }); it('parseXmlHeadTest', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; expect(xml.firstElement!.localName).to.equal('root'); @@ -118,7 +119,7 @@ describe('XmlParseTest', () => { it('parseFull', async () => { const s = await TestPlatform.loadFileAsString('test-data/xml/GPIF.xml'); - let xml: XmlDocument = new XmlDocument(); + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.firstElement).to.be.ok; }); diff --git a/test/xml/XmllWrite.test.ts b/test/xml/XmllWrite.test.ts index 45a9dc85d..428b8d881 100644 --- a/test/xml/XmllWrite.test.ts +++ b/test/xml/XmllWrite.test.ts @@ -3,91 +3,90 @@ import { expect } from 'chai'; describe('XmlWriteTest', () => { it('writeSimple', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.toFormattedString()).to.equal(''); }); it('writeSingleAttribute', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.toFormattedString()).to.equal(''); }); it('writeMultipleAttributes', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.toFormattedString()).to.equal(''); }); it('writeSimpleText', () => { - let s: string = 'Text'; - let xml: XmlDocument = new XmlDocument(); + const s: string = 'Text'; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.toFormattedString()).to.equal(s); }); it('writeSimpleTextFormatted', () => { - let s: string = 'Text'; - let xml: XmlDocument = new XmlDocument(); + const s: string = 'Text'; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); - expect(xml.toFormattedString(" ")).to.equal(s); + expect(xml.toFormattedString(' ')).to.equal(s); }); it('writeChild', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.toFormattedString()).to.equal(''); - expect(xml.toFormattedString(" ")).to.equal('\n \n'); - expect(xml.toFormattedString(" ")).to.equal('\n \n'); + expect(xml.toFormattedString(' ')).to.equal('\n \n'); + expect(xml.toFormattedString(' ')).to.equal('\n \n'); }); it('writeNumber', () => { - let s: string = '0.5'; - let xml: XmlDocument = new XmlDocument(); + const s: string = '0.5'; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.toFormattedString()).to.equal('0.5'); }); it('writeMultiChild', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.toFormattedString()).to.equal(''); - expect(xml.toFormattedString(" ")).to.equal('\n \n \n'); - expect(xml.toFormattedString(" ")).to.equal('\n \n \n'); + expect(xml.toFormattedString(' ')).to.equal('\n \n \n'); + expect(xml.toFormattedString(' ')).to.equal('\n \n \n'); }); it('writeXmlHeadTest', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.toFormattedString('', true)).to.equal(''); - expect(xml.toFormattedString(" ", true)).to.equal('\n'); + expect(xml.toFormattedString(' ', true)).to.equal('\n'); }); it('writeDoctype', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); expect(xml.toFormattedString()).to.equal(s); - expect(xml.toFormattedString(" ")).to.equal('\n'); + expect(xml.toFormattedString(' ')).to.equal('\n'); }); it('writeEscapedAttributeValues', () => { - let s: string = ''; - let xml: XmlDocument = new XmlDocument(); + const s: string = ''; + const xml: XmlDocument = new XmlDocument(); xml.parse(s); - xml.firstElement!.attributes.set("lt", "<"); - xml.firstElement!.attributes.set("gt", ">"); - xml.firstElement!.attributes.set("amp", "&"); - xml.firstElement!.attributes.set("apos", "'"); - xml.firstElement!.attributes.set("quot", "\""); + xml.firstElement!.attributes.set('lt', '<'); + xml.firstElement!.attributes.set('gt', '>'); + xml.firstElement!.attributes.set('amp', '&'); + xml.firstElement!.attributes.set('apos', "'"); + xml.firstElement!.attributes.set('quot', '"'); expect(xml.toFormattedString()).to.equal(''); }); - }); diff --git a/tsconfig.base.json b/tsconfig.base.json deleted file mode 100644 index b640c4b7a..000000000 --- a/tsconfig.base.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "compilerOptions": { - "moduleResolution": "Bundler", - "target": "ES2020", - "module": "ES2020", - "lib": [ - "ES2015", - "ES2016", - "ES2017", - "ES2021", - "es2022.regexp", - "es2022.error", - "dom" - ], - "strict": true, - "allowSyntheticDefaultImports": true, - "strictNullChecks": true, - "strictPropertyInitialization": true, - "emitDecoratorMetadata": false, - "noImplicitAny": true, - "noImplicitThis": true, - "noImplicitReturns": true, - "noUnusedLocals": true, - "noImplicitOverride": true, - "sourceMap": true, - "declaration": true, - "incremental": true, - "declarationDir": "dist/types", - "outDir": "dist", - "baseUrl": ".", - "paths": { - "@src/*": [ - "src/*" - ], - "@test/*": [ - "test/*" - ] - }, - "typeRoots": [ - "./types/", - "./node_modules/@types" - ] - }, - "include": [ - "src" - ], - "exclude": [ - "**/node_modules", - ] -} \ No newline at end of file diff --git a/tsconfig.build.json b/tsconfig.build.json deleted file mode 100644 index b84c26ae2..000000000 --- a/tsconfig.build.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./tsconfig.base.json" -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 85aac0373..f8bdc1fa5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,38 @@ { - "extends": "./tsconfig.base.json", - "compilerOptions": { - "outDir": "dist/lib.test", - "declarationDir": "dist/types.test", - }, - "include": [ - "src", - "test", - "rollup.config.ts", - "rollup.config.cjs.ts", - "rollup.config.esm.ts", - "rollup.plugin.server.ts", - "rollup.plugin.resolve.ts", - ] -} \ No newline at end of file + "compilerOptions": { + "moduleResolution": "Bundler", + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2015", "ES2016", "ES2017", "ES2021", "es2022.regexp", "es2022.error", "dom"], + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "allowSyntheticDefaultImports": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "emitDecoratorMetadata": false, + "noImplicitAny": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noImplicitOverride": true, + "baseUrl": ".", + "paths": { + "@src/*": ["src/*"], + "@test/*": ["test/*"] + }, + "typeRoots": ["./types/", "./node_modules/@types"] + }, + "include": [ + "src", + "test", + "rollup.config.ts", + "rollup.config.cjs.ts", + "rollup.config.esm.ts", + "rollup.plugin.server.ts", + "rollup.plugin.resolve.ts" + ], + "exclude": ["**/node_modules"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 000000000..6e8a206dd --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,345 @@ +import path from 'node:path'; +import url from 'node:url'; +import fs from 'node:fs'; + +import { defineConfig, LibraryOptions, Plugin, UserConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import server from './vite.plugin.server'; +import license from 'rollup-plugin-license'; +import copy from 'rollup-plugin-copy'; +import type { OutputOptions } from 'rollup'; +import typescript, { RollupTypescriptOptions } from '@rollup/plugin-typescript'; +import terser from '@rollup/plugin-terser'; +import ts from 'typescript'; +import generateDts from './vite.plugin.dts'; +import MagicString from 'magic-string'; +import { elementStyleUsingTransformer } from './scripts/element-style-transformer'; + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + +function getGitBranch(): string { + const filepath = '.git/HEAD'; + if (!fs.existsSync(filepath)) { + throw new Error('.git/HEAD does not exist'); + } + const buf = fs.readFileSync(filepath); + const match = /ref: refs\/heads\/([^\n]+)/.exec(buf.toString()); + return match ? match[1] : ''; +} + +function dtsPathsTransformer(mapping: Record) { + const mapPath = (filePath, input: string): string | undefined => { + for (const [k, v] of Object.entries(mapping)) { + if (input.startsWith(k)) { + const absoluteFile = path.resolve(v, input.substring(k.length)); + return './' + path.relative(path.dirname(filePath), absoluteFile).replaceAll('\\', '/'); + } + } + return undefined; + }; + + return (context: ts.TransformationContext) => { + return (source: ts.SourceFile | ts.Bundle) => { + const sourceFilePath = ts.isSourceFile(source) ? source.fileName : source.sourceFiles[0]; + + const visitor = (node: ts.Node) => { + if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) { + const mapped = mapPath(sourceFilePath, node.moduleSpecifier.text); + if (mapped) { + return ts.factory.createExportDeclaration( + node.modifiers, + node.isTypeOnly, + node.exportClause, + ts.factory.createStringLiteral(mapped), + node.attributes + ); + } + return node; + } else if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { + const mapped = mapPath(sourceFilePath, node.moduleSpecifier.text); + if (mapped) { + return ts.factory.createImportDeclaration( + node.modifiers, + node.importClause, + ts.factory.createStringLiteral(mapped), + node.attributes + ); + } + return node; + } + + return ts.visitEachChild(node, visitor, context); + }; + + return ts.visitEachChild(source, visitor, context); + }; + }; +} + +export default defineConfig(({ command, mode }) => { + if (command === 'serve') { + return { + plugins: [ + tsconfigPaths(), + typescript({ + tsconfig: './tsconfig.json', + transformers: { + before: [elementStyleUsingTransformer()] + } + }), + server() + ], + server: { + open: '/playground/control.html' + }, + esbuild: false + // esbuild: { + // supported: { + // using: false + // }, + + // } + }; + } else { + const commonOutput: Partial = { + globals: { + jQuery: 'jQuery' + }, + // ugly workaround for https://github.com/rollup/rollup/issues/5946 + intro(chunk) { + if(chunk.isEntry) { + return "if(typeof Symbol.dispose==='undefined'){Symbol.dispose = Symbol('Symbol.dispose')}" + } + return ''; + } + }; + + const config: UserConfig = { + esbuild: false, + plugins: [ + license({ + banner: { + commentStyle: 'ignored', + content: { + file: 'LICENSE.header', + }, + data() { + let buildNumber = process.env.GITHUB_RUN_NUMBER || 0; + let gitBranch = getGitBranch(); + return { + branch: gitBranch, + build: buildNumber + }; + } + } + }), + copy({ + targets: [ + { src: 'font/bravura/*', dest: 'dist/font' }, + { src: 'font/sonivox/*', dest: 'dist/soundfont' } + ] + }) + ], + build: { + emptyOutDir: false, + lib: { + entry: { + 'alphaTab.core': path.resolve(__dirname, 'src/alphaTab.core.ts'), + alphaTab: path.resolve(__dirname, 'src/alphaTab.main.ts'), + 'alphaTab.worker': path.resolve(__dirname, 'src/alphaTab.worker.ts'), + 'alphaTab.worklet': path.resolve(__dirname, 'src/alphaTab.worklet.ts') + }, + name: 'alphaTab' + }, + minify: false, + rollupOptions: { + external: ['jQuery', 'vite', 'rollup', /node:.*/], + output: [], + onLog(level, log, handler) { + if (log.code === 'CIRCULAR_DEPENDENCY') { + return; // Ignore circular dependency warnings + } + handler(level, log); + } + } + } + }; + + const enableTypeScript = (o: Partial = {}, types: boolean = false) => { + config.plugins!.unshift( + tsconfigPaths(), + typescript({ + tsconfig: './tsconfig.json', + ...(types + ? { + declaration: true, + declarationMap: true, + declarationDir: './dist/types' + } + : {}), + ...o, + transformers: { + before: [elementStyleUsingTransformer()], + afterDeclarations: [ + dtsPathsTransformer({ + '@src/': path.resolve(__dirname, 'src') + }) + ] + } + }) + ); + }; + + const umd = ( + name: string, + entry: string, + tsOptions: RollupTypescriptOptions, + cjs?: boolean, + withMin?: boolean + ) => { + enableTypeScript(tsOptions, false); + + lib.entry = { + [name]: path.resolve(__dirname, entry) + }; + config.plugins!.push({ + name: 'import-meta', + resolveImportMeta() { + return '{}'; // prevent import.meta to be empty in non ES outputs + } + }); + (config.build!.rollupOptions!.output as OutputOptions[]).push({ + ...commonOutput, + dir: 'dist/', + format: cjs ? 'cjs' : 'umd', + name: name, + entryFileNames: '[name].js', + chunkFileNames: '[name].js' + }); + + if (withMin) { + (config.build!.rollupOptions!.output as OutputOptions[]).push({ + ...commonOutput, + dir: 'dist/', + format: cjs ? 'cjs' : 'umd', + name: name, + plugins: [terser()], + entryFileNames: '[name].min.js', + chunkFileNames: '[name].min.js' + }); + } + }; + + const esm = (name: string, entry: string, tsOptions: RollupTypescriptOptions, withMin: boolean = true) => { + enableTypeScript(tsOptions, true); + + lib.entry = { + [name]: path.resolve(__dirname, entry) + }; + + (config.build!.rollupOptions!.external as string[]).push('@src/alphaTab.core'); + + (config.build!.rollupOptions!.output as OutputOptions[]).push({ + ...commonOutput, + dir: 'dist/', + format: 'es', + entryFileNames: '[name].mjs', + chunkFileNames: '[name].mjs', + plugins: [ + { + name: 'dts', + writeBundle(_, bundle) { + const files = Object.keys(bundle); + + for (const file of files) { + const chunk = bundle[file]; + if ( + file.endsWith('.mjs') && + chunk.type === 'chunk' && + chunk.isEntry && + !chunk.facadeModuleId!.endsWith('alphaTab.core.ts') + ) { + this.info(`Creating types for bundle ${file}`); + generateDts(__dirname, chunk.facadeModuleId!, file.replace('.mjs', '.d.ts')); + } + } + } + } + ] + }); + + if (withMin) { + (config.build!.rollupOptions!.output as OutputOptions[]).push({ + ...commonOutput, + dir: 'dist/', + format: 'es', + plugins: [terser()], + entryFileNames: '[name].min.mjs', + chunkFileNames: '[name].min.mjs' + }); + } + }; + + const lib = config.build!.lib! as LibraryOptions; + + // TODO: move to a monorepo style repository and isolate packages + const viteOptions: RollupTypescriptOptions = { + include: ['src/*.vite.ts', 'src/vite/**'] + }; + + const webpackOptions: RollupTypescriptOptions = { + include: ['src/*.webpack.ts', 'src/webpack/**'] + }; + + switch (mode) { + case 'vite-cjs': + umd('alphaTab.vite', 'src/alphaTab.vite.ts', viteOptions, true, false); + break; + case 'vite-esm': + esm('alphaTab.vite', 'src/alphaTab.vite.ts', viteOptions, false); + break; + case 'webpack-cjs': + umd('alphaTab.webpack', 'src/alphaTab.webpack.ts', webpackOptions, true, false); + break; + case 'webpack-esm': + esm('alphaTab.webpack', 'src/alphaTab.webpack.ts', webpackOptions, false); + break; + case 'umd': + umd('alphaTab', 'src/alphaTab.main.ts', {}, false, true); + break; + default: + case 'esm': + // ensure we have file extensions in the URL worker resolve area + const adjustScriptPathsPlugin = (min: boolean) => { + return { + name: 'adjust-script-paths', + renderChunk(code, chunk) { + const modifiedCode = new MagicString(code); + const extension = min ? '.min.mjs' : '.mjs'; + modifiedCode + .replaceAll(/(@src|\.)\/alphaTab\.(core|worker|worklet)(\.ts)?(['"])/g, + `./alphaTab.$2${extension}$4`) + + return { + code: modifiedCode.toString(), + map: modifiedCode.generateMap() + }; + } + } satisfies Plugin; + }; + + esm('alphaTab', 'src/alphaTab.main.ts', {}); + lib.entry['alphaTab.core'] = path.resolve(__dirname, 'src/alphaTab.core.ts'); + lib.entry['alphaTab.worker'] = path.resolve(__dirname, 'src/alphaTab.worker.ts'); + lib.entry['alphaTab.worklet'] = path.resolve(__dirname, 'src/alphaTab.worklet.ts'); + + for (const output of config.build!.rollupOptions!.output as OutputOptions[]) { + const isMin = (output.entryFileNames as string).includes('.min'); + (output.plugins as Plugin[]).push(adjustScriptPathsPlugin(isMin)); + } + break; + } + + return config; + } +}); diff --git a/vite.plugin.dts.ts b/vite.plugin.dts.ts new file mode 100644 index 000000000..c0d9d4964 --- /dev/null +++ b/vite.plugin.dts.ts @@ -0,0 +1,48 @@ +import { Extractor, ExtractorConfig, ExtractorResult } from '@microsoft/api-extractor'; +import path from 'path'; +import url from 'url'; + +export default function generateDts(root: string, entryFile: string, outputFilename: string) { + const relativePath = path.relative(root, entryFile); + const dtsRoot = path.resolve(root, 'dist', 'types'); + const dtsPath = path.resolve(dtsRoot, relativePath).replace('.ts', '.d.ts'); + + const outputFile: string = path.join(root, 'dist', outputFilename); + + const extractorConfig: ExtractorConfig = ExtractorConfig.prepare({ + packageJsonFullPath: path.resolve(root, 'package.json'), + configObjectFullPath: undefined, + configObject: { + projectFolder: root, + mainEntryPointFilePath: dtsPath, + compiler: { + overrideTsconfig: { + compilerOptions: { + paths: { + '@src/*': [dtsRoot] + } + } + } + }, + dtsRollup: { + enabled: true, + publicTrimmedFilePath: outputFile + } + } + }); + + const typescriptPath = path.resolve(url.fileURLToPath(import.meta.resolve('typescript')), '..', '..'); + const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, { + localBuild: process.env.GITHUB_ACTIONS !== 'true', + typescriptCompilerFolder: typescriptPath + }); + + if (extractorResult.succeeded) { + console.log(`DTS bundled`, entryFile, outputFile); + } else { + throw new Error( + `API Extractor completed with ${extractorResult.errorCount} errors` + + ` and ${extractorResult.warningCount} warnings` + ); + } +} diff --git a/vite.plugin.server.ts b/vite.plugin.server.ts new file mode 100644 index 000000000..43c46966f --- /dev/null +++ b/vite.plugin.server.ts @@ -0,0 +1,176 @@ +import fs from 'fs'; +import path from 'path'; +import url from 'url'; +import { acceptOne } from './scripts/accept-new-reference-files.common'; +import { Connect, Plugin } from 'vite'; +import { ServerResponse } from 'http'; + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + +function removeLeadingSlash(str: string): string { + return str[0] === '/' ? str.slice(1) : str; +} + +function readBody(req: Connect.IncomingMessage, length: number): Promise { + return new Promise((resolve, reject) => { + let chunks: string = ''; + let received = 0; + req.on('data', (chunk: Buffer) => { + chunks += chunk.toString('utf-8'); + received += chunk.length; + if (received >= length) { + resolve(chunks); + } + }); + req.on('end', () => { + resolve(chunks); + }); + req.on('error', () => { + reject(); + }); + }); +} + +export default function server(): Plugin { + return { + name: 'at-test-data-server', + configureServer(server) { + const testDataPath = path.join(__dirname, 'test-data'); + + server.middlewares.use('/test-results', (req, res, next) => { + try { + const response: any = []; + + function crawl(d: string, name: string) { + const dir = fs.opendirSync(d); + try { + while (true) { + const entry = dir.readSync(); + if (!entry) { + break; + } else if (entry.isDirectory() && entry.name !== '.' && entry.name !== '..') { + crawl(path.join(d, entry.name), name + '/' + entry.name); + } else if (entry.isFile()) { + if (entry.name.endsWith('.new.png')) { + response.push({ + originalFile: name + '/' + entry.name.replace('.new.png', '.png'), + newFile: name + '/' + entry.name, + diffFile: name + '/' + entry.name.replace('.new.png', '.diff.png') + }); + } + } + } + } finally { + dir.close(); + } + } + + crawl(testDataPath, 'test-data'); + + const json = Buffer.from(JSON.stringify(response), 'utf-8'); + + res.writeHead(200, 'OK', { + 'content-type': 'application/json', + 'content-length': json.length + }); + res.write(json); + res.end(); + } catch (e) { + const json = Buffer.from( + JSON.stringify({ + message: (e as Error).message, + stack: (e as Error).stack + }), + 'utf-8' + ); + res.writeHead(500, 'Internal Server Error', { + 'content-type': 'application/json', + 'content-length': json.length + }); + res.write(json); + res.end(); + } + }); + + async function handleAsync(req: Connect.IncomingMessage, res: ServerResponse) { + if (req.method !== 'POST') { + res.writeHead(405, 'Method not allowed'); + console.log('Wrong HTTP Method'); + return; + } + + if (!req.headers['content-length']) { + res.writeHead(411, 'Length Required'); + console.log('Missing Length'); + return; + } + + const bodyLength = parseInt(req.headers['content-length']!); + if (bodyLength > 10_000 || !isFinite(bodyLength)) { + res.writeHead(413, 'Content Too Large'); + console.log('Too long'); + return; + } + + const bodyText = await readBody(req, bodyLength); + const body = JSON.parse(bodyText); + + // basic validation that nothing bad happens + if (typeof body.originalFile !== 'string') { + res.writeHead(400, 'Bad Request'); + console.log('invalid json'); + return; + } + + let relativePath = removeLeadingSlash(body.newFile); + if (relativePath.startsWith('test-data/')) { + relativePath = relativePath.substring('test-data/'.length); + } + + const newFile = path.normalize(path.resolve(testDataPath, relativePath)); + + if (!newFile.startsWith(testDataPath)) { + res.writeHead(400, 'Bad Request'); + console.log('invalid path', newFile, testDataPath); + res.end(); + return; + } + + await acceptOne(newFile); + const json = Buffer.from( + JSON.stringify({ + message: 'Accepted' + }), + 'utf-8' + ); + res.writeHead(200, 'OK', { + 'content-type': 'application/json', + 'content-length': json.length + }); + res.write(json); + } + + server.middlewares.use('/accept-test-result', (req, res, next) => { + handleAsync(req, res) + .then(() => { + res.end(); + }) + .catch(e => { + const json = Buffer.from( + JSON.stringify({ + message: (e as Error).message, + stack: (e as Error).stack + }), + 'utf-8' + ); + res.writeHead(500, 'Internal Server Error', { + 'content-type': 'application/json', + 'content-length': json.length + }); + res.write(json); + res.end(); + }); + }); + } + }; +}