diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 7dc6f5e3..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 7845bbca..f460bf43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,16 +28,6 @@ "xsd-validator": "^1.1.1" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", @@ -49,9 +39,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "peer": true, "engines": { @@ -59,13 +49,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", - "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -208,6 +198,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "peer": true, "dependencies": { @@ -223,6 +214,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true, "peer": true }, @@ -283,9 +275,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==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", @@ -388,6 +380,24 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/eslint": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -399,11 +409,11 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/node": { - "version": "20.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.2.tgz", - "integrity": "sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==", + "version": "22.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz", + "integrity": "sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -770,10 +780,25 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, "engines": { "node": ">=0.4.0" } @@ -823,15 +848,15 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "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": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -900,6 +925,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "delegates": "^1.0.0", @@ -977,9 +1003,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "funding": [ { "type": "opencollective", @@ -995,10 +1021,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -1023,9 +1049,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001603", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001603.tgz", - "integrity": "sha512-iL2iSS0eDILMb9n5yKQoTBim9jMZ0Yrk8g0N9K7UzYyWnfIKzXBZD5ngpM37ZcL/cv0Mli8XtVMRYMQAfFpi5Q==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "funding": [ { "type": "opencollective", @@ -1066,9 +1092,9 @@ } }, "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==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "engines": { "node": ">=6.0" } @@ -1160,9 +1186,9 @@ } }, "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", - "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", @@ -1224,9 +1250,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -1297,9 +1323,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.722", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.722.tgz", - "integrity": "sha512-5nLE0TWFFpZ80Crhtp4pIp8LXCztjYX41yUcV6b+bKR2PqzjskTMOOlBi1VjBHlvHwS+4gar7kNKOrsbsewEZQ==" + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.12.tgz", + "integrity": "sha512-tIhPkdlEoCL1Y+PToq3zRNehUaKp3wBX/sr7aclAWdIWjvqAe/Im/H0SiCM4c1Q8BLPHCdoJTol+ZblflydehA==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -1334,9 +1360,9 @@ } }, "node_modules/envinfo": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", - "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", + "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", "dev": true, "bin": { "envinfo": "dist/cli.js" @@ -1346,9 +1372,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", - "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==" + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==" }, "node_modules/escalade": { "version": "3.1.2", @@ -1557,9 +1583,9 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "peer": true, "dependencies": { @@ -1669,6 +1695,12 @@ "dev": true, "peer": true }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "dev": true + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -1804,6 +1836,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -1824,6 +1857,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -1938,9 +1972,9 @@ } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -1964,9 +1998,9 @@ } }, "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "dependencies": { "pkg-dir": "^4.2.0", @@ -1996,6 +2030,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "dependencies": { "once": "^1.3.0", @@ -2018,12 +2053,15 @@ } }, "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==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2173,9 +2211,9 @@ "peer": true }, "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==" }, "node_modules/keyv": { "version": "4.5.4", @@ -2259,17 +2297,6 @@ "dev": true, "peer": true }, - "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==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -2440,9 +2467,9 @@ "dev": true }, "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", "dev": true }, "node_modules/natural-compare": { @@ -2478,9 +2505,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" }, "node_modules/nopt": { "version": "5.0.0", @@ -2515,6 +2542,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "are-we-there-yet": "^2.0.0", @@ -2542,18 +2570,18 @@ } }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "peer": true, "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -2651,9 +2679,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -2843,6 +2871,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -2916,15 +2945,15 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "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": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2950,12 +2979,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -3175,16 +3201,16 @@ } }, "node_modules/table/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "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, "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -3224,9 +3250,9 @@ } }, "node_modules/terser": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.0.tgz", - "integrity": "sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -3300,9 +3326,9 @@ } }, "node_modules/terser/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "bin": { "acorn": "bin/acorn" }, @@ -3405,9 +3431,9 @@ } }, "node_modules/ts-node/node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3464,9 +3490,9 @@ } }, "node_modules/typedoc": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.12.tgz", - "integrity": "sha512-F+qhkK2VoTweDXd1c42GS/By2DvI2uDF4/EpG424dTexSHdtCH52C6IcAvMA6jR3DzAWZjHpUOW+E02kyPNUNw==", + "version": "0.25.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz", + "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==", "dependencies": { "lunr": "^2.3.9", "marked": "^4.3.0", @@ -3492,9 +3518,9 @@ } }, "node_modules/typedoc/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -3506,9 +3532,9 @@ } }, "node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3518,9 +3544,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unicorn-magic": { "version": "0.1.0", @@ -3535,9 +3561,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "funding": [ { "type": "opencollective", @@ -3553,8 +3579,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -3606,9 +3632,9 @@ "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==" }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -3822,6 +3848,16 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3851,7 +3887,8 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/yn": { "version": "3.1.1", diff --git a/src/application/exports.ts b/src/application/exports.ts index 17b073fa..1dd5c607 100644 --- a/src/application/exports.ts +++ b/src/application/exports.ts @@ -133,7 +133,7 @@ import { SuiSampleMedia } from '../render/audio/samples'; import { SmoScore, engravingFontTypes, isEngravingFont } from '../smo/data/score'; import { UndoBuffer } from '../smo/xform/undo'; import { SmoNote } from '../smo/data/note'; -import { SmoDuration } from '../smo/xform/tickDuration'; +// import { SmoDuration } from '../smo/xform/tickDuration'; import { createLoadTests } from '../../tests/file-load'; import { SmoStaffHairpin, StaffModifierBase, SmoInstrument, SmoSlur, SmoTie, SmoStaffTextBracket, SmoTabStave @@ -243,7 +243,7 @@ export const Smo = { SmoOrnament, SmoArticulation, SmoDynamicText, SmoGraceNote, SmoMicrotone, SmoLyric, SmoArpeggio, SmoClefChange, // Smo Transformers - SmoSelection, SmoSelector, SmoDuration, UndoBuffer, SmoToVex, SmoOperation, + SmoSelection, SmoSelector, /*SmoDuration,*/ UndoBuffer, SmoToVex, SmoOperation, // new score bootstrap // help strings cardKeysHtmlEn, cardNotesLetterHtmlEn, cardNotesChromaticHtmlEn, cardNotesChordsHtmlEn, diff --git a/src/common/vex.ts b/src/common/vex.ts index 36ff24af..83f11e0b 100644 --- a/src/common/vex.ts +++ b/src/common/vex.ts @@ -8,7 +8,7 @@ Font as VexFont, FontInfo as VexFontInfo, FontStyle as VexFontStyle, FontWeight TupletOptions as VexTupletOptions, Curve as VexCurve, StaveTie as VexStaveTie, ClefNote as VexClefNote, Music as VexMusic, ChordSymbol as VexChordSymbol, ChordSymbolBlock as VexChordSymbolBlock, -TabStave as VexTabStave, TabNote as VexTabNote, TabSlide as VexTabSlide, TabNotePosition as VexTabNotePosition, +TabStave as VexTabStave, TabNote as VexTabNote, TabSlide as VexTabSlide, TabNotePosition as VexTabNotePosition, TabNoteStruct as VexTabNoteStruct } from "vexflow_smoosic"; @@ -18,6 +18,7 @@ TabNoteStruct as VexTabNoteStruct * Most of the differences are trivial - e.g. different naming conventions for variables. */ import { smoSerialize } from "./serializationHelpers"; +import { SmoMusic } from "../smo/data/music"; import { SvgBox } from "../smo/data/common"; // export type Vex = SmoVex; export const VexFlow = SmoVex.Flow; @@ -63,8 +64,11 @@ export interface GlyphInfo { // DI interfaces to create vexflow objects export interface CreateVexNoteParams { - isTuplet: boolean, measureIndex: number, clef: string, - closestTicks: string, exactTicks: string, keys: string[], + isTuplet: boolean, + measureIndex: number, + clef: string, + stemTicks: string, + keys: string[], noteType: string }; @@ -200,12 +204,7 @@ export function getVexTuplets(params: SmoVexTupletParams) { return vexTuplet; } export function getVexNoteParameters(params: CreateVexNoteParams): { noteParams: StaveNoteStruct, duration: string } { - // If this is a tuplet, we only get the duration so the appropriate stem - // can be rendered. Vex calculates the actual ticks later when the tuplet is made - var duration = - params.isTuplet ? - params.closestTicks : - params.exactTicks; + var duration: any = params.stemTicks; if (typeof (duration) === 'undefined') { console.warn('bad duration in measure ' + params.measureIndex); duration = '8'; diff --git a/src/render/vex/toVex.ts b/src/render/vex/toVex.ts index ada246cd..6ad62c6b 100644 --- a/src/render/vex/toVex.ts +++ b/src/render/vex/toVex.ts @@ -5,7 +5,7 @@ import { SmoNote } from '../../smo/data/note'; import { SmoMeasure, SmoVoice, MeasureTickmaps } from '../../smo/data/measure'; import { SmoScore } from '../../smo/data/score'; import { SmoArticulation, SmoLyric, SmoOrnament } from '../../smo/data/noteModifiers'; -import { VexFlow, StaveNoteStruct, TupletOptions, vexOrnaments } from '../../common/vex'; +import {VexFlow, StaveNoteStruct, TupletOptions, vexOrnaments, getVexTuplets} from '../../common/vex'; import { SmoBarline, SmoRehearsalMark } from '../../smo/data/measureModifiers'; import { SmoSelection, SmoSelector } from '../../smo/xform/selections'; import { SmoSystemStaff } from '../../smo/data/systemStaff'; @@ -14,6 +14,7 @@ import { SmoSystemGroup } from '../../smo/data/scoreModifiers'; import { StaffModifierBase, SmoStaffHairpin, SmoSlur, SmoTie, SmoStaffTextBracket } from '../../smo/data/staffModifiers'; import { toVexBarlineType, vexBarlineType, vexBarlinePosition, toVexBarlinePosition, leftConnectorVx, rightConnectorVx, toVexVolta, getVexChordBlocks } from '../../render/vex/smoAdapter'; +import {SmoTuplet} from "../../smo/data/tuplet"; @@ -78,10 +79,7 @@ function smoNoteToGraceNotes(smoNote: SmoNote, strs: string[]) { } } function smoNoteToStaveNote(smoNote: SmoNote) { - const duration = - smoNote.isTuplet ? - SmoMusic.closestVexDuration(smoNote.tickCount) : - SmoMusic.ticksToDuration[smoNote.tickCount]; + const duration = SmoMusic.ticksToDuration[smoNote.stemTicks]; const sn: StaveNoteStruct = { clef: smoNote.clef, duration, @@ -427,27 +425,36 @@ function createBeamGroups(smoMeasure: SmoMeasure, strs: string[]) { } function createTuplets(smoMeasure: SmoMeasure, strs: string[]) { smoMeasure.voices.forEach((voice, voiceIx) => { - const tps = smoMeasure.tuplets.filter((tp) => tp.voice === voiceIx); - for (var i = 0; i < tps.length; ++i) { - const tp = tps[i]; - const nar: string[] = []; - for (var j = 0; j < tp.notes.length; ++j) { - const note = tp.notes[j]; - const vexNote = `${note.attrs.id}`; - nar.push(vexNote); + for (let i = 0; i < smoMeasure.tupletTrees.length; ++i) { + const tupletTree = smoMeasure.tupletTrees[i]; + if (tupletTree.voice !== voiceIx) { + continue; } - const direction = tp.getStemDirection(smoMeasure.clef) === SmoNote.flagStates.up ? - VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; - const tpParams: TupletOptions = { - num_notes: tp.num_notes, - notes_occupied: tp.notes_occupied, + const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { + const vexNotes = []; + for (let smoNote of smoMeasure.tupletNotes(parentTuplet)) { + const vexNote = `${smoNote.attrs.id}`; + vexNotes.push(vexNote); + } + const direction = smoMeasure.getStemDirectionForTuplet(parentTuplet) === SmoNote.flagStates.up ? + VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; + const tpParams: TupletOptions = { + num_notes: parentTuplet.numNotes, + notes_occupied: parentTuplet.notesOccupied, ratioed: false, bracketed: true, location: direction - }; - const tpParamString = JSON.stringify(tpParams); - const narString = '[' + nar.join(',') + ']'; - strs.push(`const ${tp.id} = new VF.Tuplet(${narString}, JSON.parse('${tpParamString}'));`); + }; + const tpParamString = JSON.stringify(tpParams); + const vexNotesString = '[' + vexNotes.join(',') + ']'; + strs.push(`const ${parentTuplet.attrs.id} = new VF.Tuplet(${vexNotesString}, JSON.parse('${tpParamString}'));`); + + for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { + const tuplet = parentTuplet.childrenTuplets[i]; + traverseTupletTree(tuplet); + } + } + traverseTupletTree(tupletTree.tuplet); } }); } @@ -493,9 +500,16 @@ function createMeasure(smoMeasure: SmoMeasure, heightOffset: number, strs: strin strs.push(`${bg.attrs.id}.setContext(context);`); strs.push(`${bg.attrs.id}.draw();`) }); - smoMeasure.tuplets.forEach((tp) => { - strs.push(`${tp.id}.setContext(context).draw();`) - }) + smoMeasure.tupletTrees.forEach((tp) => { + const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { + strs.push(`${parentTuplet.attrs.id}.setContext(context).draw();`) + for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { + const tuplet = parentTuplet.childrenTuplets[i]; + traverseTupletTree(tuplet); + } + } + traverseTupletTree(tp.tuplet); + }); } // ## SmoToVex // Simple serialize class that produced VEX note and voice objects diff --git a/src/render/vex/vxMeasure.ts b/src/render/vex/vxMeasure.ts index 88ca17b9..4c91ae19 100644 --- a/src/render/vex/vxMeasure.ts +++ b/src/render/vex/vxMeasure.ts @@ -31,6 +31,7 @@ import { VexFlow, Stave,StemmableNote, Note, Beam, Tuplet, Voice, } from '../../common/vex'; import { VxMeasureIf, VexNoteModifierIf, VxNote } from './vxNote'; +import { SmoTuplet } from '../../smo/data/tuplet'; import { vexGlyph } from './glyphDimensions'; const VF = VexFlow; @@ -155,16 +156,18 @@ export class VxMeasure implements VxMeasureIf { let vexNote: Note | null = null; let smoTabNote: SmoTabNote | null = null; let timestamp = new Date().valueOf(); + const stemTicks = SmoMusic.ticksToDuration[smoNote.stemTicks]; let tabNote: StemmableNote | null = null; - const closestTicks = SmoMusic.closestVexDuration(smoNote.tickCount); - const exactTicks = SmoMusic.ticksToDuration[smoNote.tickCount]; const noteHead = smoNote.isRest() ? 'r' : smoNote.noteHead; const keys = SmoMusic.smoPitchesToVexKeys(smoNote.pitches, 0, noteHead); - const smoNoteParams: CreateVexNoteParams = { - isTuplet: smoNote.isTuplet, measureIndex: this.smoMeasure.measureNumber.measureIndex, + const smoNoteParams = { + isTuplet: smoNote.isTuplet, + measureIndex: this.smoMeasure.measureNumber.measureIndex, clef: smoNote.clef, - closestTicks, exactTicks, keys, - noteType: smoNote.noteType }; + stemTicks, + keys, + noteType: smoNote.noteType + }; const { noteParams, duration } = getVexNoteParameters(smoNoteParams); if (this.tabStave && this.smoTabStave) { smoTabNote = this.smoTabStave.getTabNoteFromNote(smoNote, this.smoMeasure.transposeIndex); @@ -182,7 +185,7 @@ export class VxMeasure implements VxMeasureIf { tabNote = new VF.StaveNote(noteParams); } } - } + } } if (smoNote.noteType === '/') { // vexNote = new VF.GlyphNote('\uE504', { duration }); @@ -294,7 +297,7 @@ export class VxMeasure implements VxMeasureIf { this.voiceNotes = []; const voice = this.smoMeasure.voices[voiceIx]; let clefNoteAdded = false; - + for (i = 0; i < voice.notes.length; ++i) { const smoNote = voice.notes[i]; @@ -366,37 +369,37 @@ export class VxMeasure implements VxMeasureIf { } } - /** - * Create the VF tuplet objects based on the smo tuplet objects - * @param vix - */ - // createVexTuplets(vix: number) { - var j = 0; - var i = 0; this.vexTuplets = []; this.tupletToVexMap = {}; - for (i = 0; i < this.smoMeasure.tuplets.length; ++i) { - const tp = this.smoMeasure.tuplets[i]; - if (tp.voice !== vix) { + for (let i = 0; i < this.smoMeasure.tupletTrees.length; ++i) { + const tupletTree = this.smoMeasure.tupletTrees[i]; + if (tupletTree.voice !== vix) { continue; } - const vexNotes: Note[] = []; - for (j = 0; j < tp.notes.length; ++j) { - const smoNote = tp.notes[j]; - vexNotes.push(this.noteToVexMap[smoNote.attrs.id]); - } - const location = tp.getStemDirection(this.smoMeasure.clef) === SmoNote.flagStates.up ? - VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; - const smoTupletParams = { - vexNotes, - numNotes: tp.numNotes, - notesOccupied: tp.note_ticks_occupied, - location + const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { + const vexNotes = []; + for (let smoNote of this.smoMeasure.tupletNotes(parentTuplet)) { + vexNotes.push(this.noteToVexMap[smoNote.attrs.id]); + } + const location = this.smoMeasure.getStemDirectionForTuplet(parentTuplet) === SmoNote.flagStates.up ? + VF.Tuplet.LOCATION_TOP : VF.Tuplet.LOCATION_BOTTOM; + const smoTupletParams = { + vexNotes, + numNotes: parentTuplet.numNotes, + notesOccupied: parentTuplet.notesOccupied, + location + } + const vexTuplet = getVexTuplets(smoTupletParams); + + this.tupletToVexMap[parentTuplet.attrs.id] = vexTuplet; + this.vexTuplets.push(vexTuplet); + for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { + const tuplet = parentTuplet.childrenTuplets[i]; + traverseTupletTree(tuplet); + } } - const vexTuplet = getVexTuplets(smoTupletParams); - this.tupletToVexMap[tp.id] = vexTuplet; - this.vexTuplets.push(vexTuplet); + traverseTupletTree(tupletTree.tuplet); } } diff --git a/src/smo/data/measure.ts b/src/smo/data/measure.ts index a023d9de..b6414c1c 100644 --- a/src/smo/data/measure.ts +++ b/src/smo/data/measure.ts @@ -17,7 +17,7 @@ import { TimeSignatureParametersSer, SmoMeasureFormatParamsSer, SmoTempoTextParamsSer } from './measureModifiers'; import { SmoNote, NoteType, SmoNoteParamsSer } from './note'; -import { SmoTuplet, SmoTupletParamsSer, SmoTupletParams } from './tuplet'; +import { SmoTuplet, SmoTupletParamsSer, SmoTupletParams, SmoTupletTreeParamsSer, SmoTupletTree } from './tuplet'; import { layoutDebug } from '../../render/sui/layoutDebug'; import { SvgHelpers } from '../../render/sui/svgHelpers'; import { TickMap } from '../xform/tickMap'; @@ -139,7 +139,7 @@ export const SmoMeasureStringParams: SmoMeasureStringParam[] = ['keySignature']; export interface SmoMeasureParams { timeSignature: TimeSignature, keySignature: string, - tuplets: SmoTuplet[], + tupletTrees: SmoTupletTree[], transposeIndex: number, lines: number, // bars: [1, 1], // follows enumeration in VF.Barline @@ -164,11 +164,11 @@ export interface SmoMeasureParamsSer { /** * constructor */ - ctor: string; + ctor: string, /** * a list of tuplets (serialized) */ - tuplets: SmoTupletParamsSer[], + tupletTrees: SmoTupletTreeParamsSer[], /** * transpose the notes up/down. TODO: this should not be serialized * as its part of the instrument parameters @@ -208,7 +208,7 @@ export interface SmoMeasureParamsSer { * tempo at this point */ tempo: SmoTempoTextParamsSer - + } /** @@ -218,7 +218,7 @@ export interface SmoMeasureParamsSer { */ function isSmoMeasureParamsSer(params: Partial):params is SmoMeasureParamsSer { if (!Array.isArray(params.voices) || - !Array.isArray(params.tuplets) || !Array.isArray(params.modifiers) || + !Array.isArray(params.tupletTrees) || !Array.isArray(params.modifiers) || typeof(params?.measureNumber?.measureIndex) !== 'number') { return false; } @@ -242,7 +242,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { static readonly _defaults: SmoMeasureParams = { timeSignature: SmoMeasure.timeSignatureDefault, keySignature: 'C', - tuplets: [], + tupletTrees: [], transposeIndex: 0, modifiers: [], // bars: [1, 1], // follows enumeration in VF.Barline @@ -294,7 +294,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { */ keySignature: string = ''; canceledKeySignature: string = ''; - tuplets: SmoTuplet[] = []; + tupletTrees: SmoTupletTree[] = []; repeatSymbol: boolean = false; repeatCount: number = 0; ctor: string='SmoMeasure'; @@ -393,7 +393,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } } this.voices = params.voices ? params.voices : []; - this.tuplets = params.tuplets ? params.tuplets : []; + this.tupletTrees = params.tupletTrees ? params.tupletTrees : []; this.modifiers = params.modifiers ? params.modifiers : defaults.modifiers; this.setDefaultBarlines(); this.keySignature = SmoMusic.vexKeySigWithOffset(this.keySignature, this.transposeIndex); @@ -531,12 +531,12 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { const fmt = this.format.serialize(); // measure number can't be defaulted b/c tempos etc. can map to default measure params.measureNumber = JSON.parse(JSON.stringify(this.measureNumber)); - params.tuplets = []; + params.tupletTrees = []; params.voices = []; params.modifiers = []; - this.tuplets.forEach((tuplet) => { - params.tuplets!.push(tuplet.serialize()); + this.tupletTrees.forEach((tupletTree) => { + params.tupletTrees!.push(tupletTree.serialize()); }); this.voices.forEach((voice) => { @@ -587,7 +587,6 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { let j = 0; let i = 0; const voices: SmoVoice[] = []; - const noteSum = []; for (j = 0; j < jsonObj.voices.length; ++j) { const voice = jsonObj.voices[j]; const notes: SmoNote[] = []; @@ -598,28 +597,6 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { const noteParams = voice.notes[i]; const smoNote = SmoNote.deserialize(noteParams); notes.push(smoNote); - noteSum.push(smoNote); - } - } - - const tuplets = []; - for (j = 0; j < jsonObj.tuplets.length; ++j) { - const tupJson = jsonObj.tuplets[j]; - const tupParams = SmoTuplet.defaults; - // Legacy schema had attrs.id, now it is just id - if ((tupJson as any).attrs && (tupJson as any).attrs.id) { - tupParams.id = (tupJson as any).attrs.id; - } - smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj.tuplets[j], tupParams); - const noteAr = noteSum.filter((nn: SmoNote) => - nn.isTuplet && nn.tupletId === tupParams.id); - - // Bug fix: A tuplet with no notes may be been overwritten - // in a copy/paste operation - if (noteAr.length > 0) { - tupParams.notes = noteAr; - const tuplet = new SmoTuplet(tupParams); - tuplets.push(tuplet); } } @@ -658,19 +635,43 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { } params.keySignature = jsonObj.keySignature ?? 'C'; params.voices = voices; - params.tuplets = tuplets; + + if ((jsonObj as any).tupletTrees !== undefined) { + for (j = 0; j < jsonObj.tupletTrees.length; ++j) { + const tupletTreeJson = jsonObj.tupletTrees[j]; + const tupletTree = SmoTupletTree.deserialize(tupletTreeJson); + params.tupletTrees.push(tupletTree); + } + } + //deserialization of a legacy tuplets + //legacy schema had measure.tuplets, it is measure.tupletTrees now + if ((jsonObj as any).tuplets !== undefined) { + for (j = 0; j < (jsonObj as any).tuplets.length; ++j) { + const tupJson = (jsonObj as any).tuplets[j]; + const tuplet: SmoTuplet = SmoTuplet.deserialize(tupJson); + const tupletTree: SmoTupletTree = new SmoTupletTree({tuplet: tuplet}); + params.tupletTrees.push(tupletTree); + } + } params.modifiers = modifiers; - const rv = new SmoMeasure(params); + const measure = new SmoMeasure(params); // Handle migration for measure-mapped parameters - rv.modifiers.forEach((mod) => { + measure.modifiers.forEach((mod) => { if (mod.ctor === 'SmoTempoText') { - rv.tempo = (mod as SmoTempoText); + measure.tempo = (mod as SmoTempoText); } }); - if (!rv.tempo) { - rv.tempo = new SmoTempoText(SmoTempoText.defaults); + if (!measure.tempo) { + measure.tempo = new SmoTempoText(SmoTempoText.defaults); } - return rv; + + + + return measure; + } + + static clone(measure: SmoMeasure): SmoMeasure { + return SmoMeasure.deserialize(measure.serialize()); } /** @@ -756,6 +757,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { nextNote.noteType = 'r'; nextNote.clef = clef; nextNote.ticks.numerator = noteTick; + nextNote.stemTicks = noteTick; pnotes.push(new SmoNote(nextNote)); ticks += noteTick; } @@ -915,6 +917,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { const duration = SmoMusic.closestDurationTickLtEq(target); if (duration > 128) { fitNote.ticks = { numerator: duration, denominator: 1, remainder: 0 }; + fitNote.stemTicks = duration; fitNote.pitches = note.pitches; fitNote.noteType = note.noteType; fitNote.clef = note.clef; @@ -931,22 +934,24 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { const note = voice.notes[j]; // if a tuplet, make sure the whole tuplet fits. if (note.isTuplet) { - const tuplet = this.getTupletForNote(note); - if (tuplet) { + const tupletTree = SmoTupletTree.getTupletTreeForNoteIndex(this.tupletTrees, i, j); + if (tupletTree) { // remaining notes of an approved tuplet, just add them - if (tuplet.startIndex !== j) { + if (tupletTree.startIndex !== j) { newNotes.push(note); continue; } - else if (tuplet.tickCount + voiceTicks <= tsTicks) { + else if (tupletTree.totalTicks + voiceTicks <= tsTicks) { // first note of the tuplet, it fits, add it - voiceTicks += tuplet.tickCount; + voiceTicks += tupletTree.totalTicks; newNotes.push(note); - tuplets.push(tuplet); } else { - // tuplet will not fit. Make a note as close to remainder as possible and add it + // tuplet will not fit. Replace tuplet with a note as close to remainder as possible and add it + // remove tuplet + note.tupletId = null replaceNoteWithDuration(tsTicks - voiceTicks, newNotes, note); voiceTicks = tsTicks; + SmoTupletTree.removeTupletForNoteIndex(this, i, j); break; } } else { // missing tuplet, now what? @@ -974,7 +979,6 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { voices.push({ notes: newNotes }); } this.voices = voices; - this.tuplets = tuplets; } get measureNumberDbg(): string { return `${this.measureNumber.measureIndex}/${this.measureNumber.systemIndex}/${this.measureNumber.staffId}`; @@ -1037,6 +1041,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { const pitches: number[] = [...Array(note.pitches.length).keys()]; // when the note is a rest, preserve the rest but match the new clef. if (newClef !== this.clef && note.noteType === 'r') { + // @ts-ignore const defp = JSON.parse(JSON.stringify(SmoMeasure.defaultPitchForClef[newClef])); note.pitches = [defp]; } else { @@ -1197,17 +1202,45 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { return ticks; } - getClosestTickCountIndex(voiceIndex: number, tickCount: number): number { + /** + * Count all the ticks up to the provided tickIndex + * @param voiceIndex + * @param tickIndex + */ + getNotePositionInTicks(voiceIndex: number, tickIndex: number): number { + let rv = 0; + for (let i = 0; i < tickIndex; i++) { + const note = this.voices[voiceIndex].notes[i]; + rv += note.tickCount; + } + return rv; + } + + /** + * Count all the ticks up to the provided tickIndex + * @param voiceIndex + * @param tickIndex + */ + getTickCountForNote(voiceIndex: number, note: SmoNote): number { + let rv = 0; + for (let i = 0; i < this.voices[voiceIndex].notes.length; i++) { + const currentNote = this.voices[voiceIndex].notes[i]; + rv += note.tickCount; + } + return rv; + } + + getClosestIndexFromTickCount(voiceIndex: number, tickCount: number): number { let i = 0; let rv = 0; for (i = 0; i < this.voices[voiceIndex].notes.length; ++i) { const note = this.voices[voiceIndex].notes[i]; - if (note.tickCount + rv > tickCount) { - return rv; + if (note.tickCount + rv >= tickCount) { + return i; } rv += note.tickCount; } - return rv; + return i; } isPickup(): boolean { @@ -1255,59 +1288,32 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { }); } - // ### tuplet methods. - // - // #### tupletNotes - tupletNotes(tuplet: SmoTuplet) { - let j = 0; - let i = 0; - const tnotes = []; - for (j = 0; j < this.voices.length; ++j) { - const vnotes = this.voices[j].notes; - for (i = 0; i < vnotes.length; ++i) { - const note = vnotes[i] as SmoNote; - if (note.tupletId && note.tupletId === tuplet.id) { - tnotes.push(vnotes[i]); - } - } + tupletNotes(smoTuplet: SmoTuplet): SmoNote[] { + let tupletNotes: SmoNote[] = []; + for (let i = smoTuplet.startIndex; i <= smoTuplet.endIndex; i++) { + const note = this.voices[smoTuplet.voice].notes[i]; + tupletNotes.push(note); } - return tnotes; + return tupletNotes; } - // #### tupletIndex - // return the index of the given tuplet - tupletIndex(tuplet: SmoTuplet) { - let j = 0; - let i = 0; - for (j = 0; j < this.voices.length; ++j) { - const notes = this.voices[j].notes; - for (i = 0; i < notes.length; ++i) { - const note = notes[i] as SmoNote; - if (note.tupletId && note.tupletId === tuplet.id) { - return i; - } + getStemDirectionForTuplet(smoTuplet: SmoTuplet) { + let note: SmoNote | null = null; + for (let currentNote of this.tupletNotes(smoTuplet)) { + if (currentNote.noteType === 'n') { + note = currentNote; + break; } } - return -1; - } - // #### getTupletForNote - // Finds the tuplet for a given note, or null if there isn't one. - getTupletForNote(note: SmoNote | null): SmoTuplet | null { - let i = 0; if (!note) { - return null; + return SmoNote.flagStates.down; } - if (!note.isTuplet) { - return null; - } - for (i = 0; i < this.tuplets.length; ++i) { - const tuplet = this.tuplets[i]; - if (typeof(note.tupletId) === 'string' && note.tupletId === tuplet.id) { - return tuplet; - } + if (note.flagState !== SmoNote.flagStates.auto) { + return note.flagState; } - return null; + return SmoMusic.pitchToLedgerLine(this.clef, note.pitches[0]) + >= 2 ? SmoNote.flagStates.up : SmoNote.flagStates.down; } getNoteById(id: string): SmoNote | null { for (var i = 0; i < this.voices.length; ++i) { @@ -1322,17 +1328,11 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable { return null; } - removeTupletForNote(note: SmoNote) { - let i = 0; - const tuplets = []; - for (i = 0; i < this.tuplets.length; ++i) { - const tuplet = this.tuplets[i]; - if (typeof(note.tupletId) === 'string' && note.tupletId !== tuplet.id) { - tuplets.push(tuplet); - } - } - this.tuplets = tuplets; - } + + + + + setClef(clef: Clef) { const oldClef = this.clef; this.clef = clef; diff --git a/src/smo/data/music.ts b/src/smo/data/music.ts index 362c088e..02327835 100644 --- a/src/smo/data/music.ts +++ b/src/smo/data/music.ts @@ -1437,7 +1437,8 @@ export class SmoMusic { SmoMusic.highestDuration / 16, // 8th SmoMusic.highestDuration / 32, // 16th SmoMusic.highestDuration / 64, // 32nd - SmoMusic.highestDuration / 128 // 64th + SmoMusic.highestDuration / 128, // 64th + SmoMusic.highestDuration / 256 // 128th ]; static durationsAscending = [ SmoMusic.highestDuration / 256, // 128th @@ -1525,7 +1526,7 @@ export class SmoMusic { let i = 0; const durations = ['1/2', '1', '2', '4', '8', '16', '32', '64', '128', '256']; const _ticksToDurationsF = () => { - for (i = 0; i < SmoMusic.durationsDescending.length - 1; ++i) { + for (i = 0; i <= SmoMusic.durationsDescending.length - 1; ++i) { let j = 0; let dots = ''; let ticks = 0; @@ -1666,6 +1667,24 @@ export class SmoMusic { return SmoMusic.ticksToDuration[stemTicks]; } + // ## closestBeamDuration + // ## Description: + // return the closest smo duration >= to the actual number of ticks. Used in beaming + // triplets which have fewer ticks then their stem would normally indicate. + static closestBeamDuration(ticks: number): SimpleDuration { + let stemTicks = SmoMusic.highestDuration; + + // The stem value is the type on the non-tuplet note, e.g. 1/8 note + // for a triplet. + while (ticks <= stemTicks) { + stemTicks = stemTicks / 2; + } + stemTicks = stemTicks * 2; + return SmoMusic.validDurations[stemTicks]; + } + + + // ### closestDurationTickLtEq // Price is right style, closest tick value without going over. Used to pad // rests when reading musicXML. @@ -1707,7 +1726,10 @@ export class SmoMusic { static getNextDottedLevel(ticks: number): number { const ticksOrNull = SmoMusic.closestSmoDurationFromTicks(ticks); if (ticksOrNull && ticksOrNull.index > 0) { - return SmoMusic.validDurations[SmoMusic._validDurationKeys[ticksOrNull.index - 1]].ticks; + const newDuration = SmoMusic.validDurations[SmoMusic._validDurationKeys[ticksOrNull.index - 1]]; + if (newDuration.baseTicks === ticksOrNull.baseTicks) { + return newDuration.ticks; + } } return ticks; } @@ -1718,7 +1740,10 @@ export class SmoMusic { static getPreviousDottedLevel(ticks: number): number { const ticksOrNull = SmoMusic.closestSmoDurationFromTicks(ticks); if (ticksOrNull && ticksOrNull.index < SmoMusic._validDurationKeys.length + 1) { - return SmoMusic.validDurations[SmoMusic._validDurationKeys[ticksOrNull.index + 1]].ticks; + const newDuration = SmoMusic.validDurations[SmoMusic._validDurationKeys[ticksOrNull.index + 1]]; + if (newDuration.baseTicks === ticksOrNull.baseTicks) { + return newDuration.ticks; + } } return ticks; } diff --git a/src/smo/data/note.ts b/src/smo/data/note.ts index 68443eb7..a5b61985 100644 --- a/src/smo/data/note.ts +++ b/src/smo/data/note.ts @@ -9,14 +9,18 @@ import { smoSerialize } from '../../common/serializationHelpers'; import { SmoNoteModifierBase, SmoArticulation, SmoLyric, SmoGraceNote, SmoMicrotone, SmoOrnament, SmoDynamicText, SmoArpeggio, SmoArticulationParametersSer, GraceNoteParamsSer, SmoOrnamentParamsSer, SmoMicrotoneParamsSer, SmoClefChangeParamsSer, SmoClefChange, SmoLyricParamsSer, SmoDynamicTextSer, SmoTabNote, - SmoTabNoteParamsSer, + SmoTabNoteParamsSer, SmoTabNoteParams, SmoFretPosition} from './noteModifiers'; import { SmoMusic } from './music'; -import { Ticks, Pitch, SmoAttrs, Transposable, PitchLetter, SvgBox, getId, +import { Ticks, Pitch, SmoAttrs, Transposable, PitchLetter, SvgBox, getId, createXmlAttribute, serializeXmlModifierArray} from './common'; import { FontInfo, vexCanonicalNotes } from '../../common/vex'; +import { SmoTupletParamsSer } from './tuplet'; +export interface TupletInfo { + id: string; +} // @internal export type NoteType = 'n' | 'r' | '/'; // @internal @@ -49,6 +53,7 @@ export const NoteBooleanParams: NoteBooleanParam[] = ['hidden', 'endBeam', 'isCu * @param beamBeats how many ticks to use before beaming a group * @param flagState up down auto * @param ticks duration + * @param stemTicks visible duration (todo update this comment) * @param pitches SmoPitch array * @param isCue tiny notes * @category SmoParameters @@ -120,6 +125,10 @@ export interface SmoNoteParams { * note duration */ ticks: Ticks, + /** + * visible duration + */ + stemTicks: number, /** * pitch for leger lines and sounds */ @@ -212,6 +221,10 @@ export interface SmoNoteParamsSer { * note duration */ ticks: Ticks, + /** + * visible duration (todo: update this comment) + */ + stemTicks: number, /** * pitch for leger lines and sounds */ @@ -225,12 +238,6 @@ export interface SmoNoteParamsSer { */ clefNote? : SmoClefChangeParamsSer } - -export interface SmoTupletNote { - ticks: Ticks, - noteId: string, - tupletId: string -} function isSmoNoteParamsSer(params: Partial): params is SmoNoteParamsSer { if (params.ctor && params.ctor === 'SmoNote') { return true; @@ -270,9 +277,10 @@ export class SmoNote implements Transposable { if (params.tabNote) { this.tabNote = new SmoTabNote(params.tabNote); } - const ticks = params.ticks ? params.ticks : defs.ticks; const pitches = params.pitches ? params.pitches : defs.pitches; + const ticks = params.ticks ? params.ticks : defs.ticks; this.ticks = JSON.parse(JSON.stringify(ticks)); + this.stemTicks = params.stemTicks ? params.stemTicks : defs.stemTicks; this.pitches = JSON.parse(JSON.stringify(pitches)); this.clef = params.clef ? params.clef : defs.clef; this.fillStyle = params.fillStyle ? params.fillStyle : ''; @@ -280,6 +288,7 @@ export class SmoNote implements Transposable { if ((params as any).tuplet) { this.tupletId = (params as any).tuplet.id; } + this.attrs = { id: getId().toString(), type: 'SmoNote' @@ -308,6 +317,7 @@ export class SmoNote implements Transposable { tones: SmoMicrotone[] = []; endBeam: boolean = false; ticks: Ticks = { numerator: 4096, denominator: 1, remainder: 0 }; + stemTicks: number = 4096; beamBeats: number = 4096; beam_group: SmoAttrs | null = null; renderId: string | null = null; @@ -321,9 +331,9 @@ export class SmoNote implements Transposable { * @internal */ static get parameterArray() { - return ['ticks', 'pitches', 'noteType', 'tuplet', 'clef', 'isCue', - 'endBeam', 'beamBeats', 'flagState', 'noteHead', 'fillStyle', 'hidden', 'arpeggio', 'clefNote' - , 'tupletId']; + return ['ticks', 'pitches', 'noteType', 'tuplet', 'clef', 'isCue', 'stemTicks', + 'endBeam', 'beamBeats', 'flagState', 'noteHead', 'fillStyle', 'hidden', 'arpeggio', 'clefNote', + 'tupletId']; } /** * Default constructor parameters. We always return a copy so the caller can modify it @@ -349,6 +359,7 @@ export class SmoNote implements Transposable { denominator: 1, remainder: 0 }, + stemTicks: 4096, pitches: [{ letter: 'b', octave: 4, @@ -363,11 +374,9 @@ export class SmoNote implements Transposable { this.flagState = (this.flagState + 1) % 3; } + //todo: double check this get dots() { - if (this.isTuplet) { - return 0; - } - const vexDuration = SmoMusic.closestSmoDurationFromTicks(this.tickCount); + const vexDuration = SmoMusic.closestSmoDurationFromTicks(this.stemTicks); if (!vexDuration) { return 0; } @@ -838,12 +847,19 @@ export class SmoNote implements Transposable { * @param ticks * @returns A note identical to `note` but with different duration */ - static cloneWithDuration(note: SmoNote, ticks: Ticks | number) { + static cloneWithDuration(note: SmoNote, ticks: Ticks | number, stemTicks: number | null = null) { if (typeof(ticks) === 'number') { ticks = { numerator: ticks, denominator: 1, remainder: 0 }; } const rv = SmoNote.clone(note); rv.ticks = ticks; + + if (stemTicks === null) { + rv.stemTicks = ticks.numerator + ticks.remainder; + } else { + rv.stemTicks = stemTicks; + } + return rv; } static serializeModifier(modifiers: SmoNoteModifierBase[]) : object[] { @@ -879,9 +895,6 @@ export class SmoNote implements Transposable { if (params.ticks) { params.ticks = JSON.parse(JSON.stringify(params.ticks)); } - if (this.tupletId) { - params.tupletId = this.tupletId; - } this._serializeModifiers(params); if (!isSmoNoteParamsSer(params)) { throw 'bad note ' + JSON.stringify(params); @@ -894,6 +907,14 @@ export class SmoNote implements Transposable { * @returns */ static deserialize(jsonObj: any) { + //legacy note + if (jsonObj.ticks && jsonObj.stemTicks === undefined) { + if (jsonObj.tupletId || jsonObj.tuplet) { + jsonObj['stemTicks'] = SmoMusic.closestBeamDuration(jsonObj.ticks.numerator / jsonObj.ticks.denominator + jsonObj.ticks.remainder)!.ticks; + } else { + jsonObj['stemTicks'] = SmoMusic.closestSmoDurationFromTicks(jsonObj.ticks.numerator / jsonObj.ticks.denominator + jsonObj.ticks.remainder)!.ticks; + } + } var note = new SmoNote(jsonObj); if (jsonObj.textModifiers) { jsonObj.textModifiers.forEach((mod: any) => { diff --git a/src/smo/data/tuplet.ts b/src/smo/data/tuplet.ts index 7ff40640..4a7fcb69 100644 --- a/src/smo/data/tuplet.ts +++ b/src/smo/data/tuplet.ts @@ -5,10 +5,153 @@ * @module /smo/data/tuplet */ import { smoSerialize } from '../../common/serializationHelpers'; -import { SmoNote, SmoNoteParamsSer, SmoTupletNote } from './note'; +import { SmoNote, SmoNoteParamsSer, TupletInfo } from './note'; import { SmoMusic } from './music'; import { SmoNoteModifierBase } from './noteModifiers'; import { getId, SmoAttrs, Clef } from './common'; +import { SmoMeasure } from './measure'; +import {tuplets} from "vexflow_smoosic/build/esm/types/tests/formatter/tests"; + + +export interface SmoTupletTreeParams { + tuplet: SmoTuplet +} + +export interface SmoTupletTreeParamsSer { + /** + * constructor + */ + ctor: string, + /** + * root tuplet + */ + tuplet: SmoTupletParamsSer +} + +export class SmoTupletTree { + + /** + * root tuplet + */ + tuplet: SmoTuplet; + + constructor(params: SmoTupletTreeParams) { + this.tuplet = params.tuplet; + } + + static adjustTupletIndexes(tupletTrees: SmoTupletTree[], voice: number, startTick: number, diff: number) { + const traverseTupletTree = (parentTuplet: SmoTuplet): void => { + if (parentTuplet.endIndex >= startTick) { + parentTuplet.endIndex += diff; + if(parentTuplet.startIndex > startTick) { + parentTuplet.startIndex += diff; + } + } + for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { + const tuplet = parentTuplet.childrenTuplets[i]; + traverseTupletTree(tuplet); + } + } + + //traverse tuplet tree + for (let i = 0; i < tupletTrees.length; i++) { + const tupletTree: SmoTupletTree = tupletTrees[i]; + if (tupletTree.endIndex >= startTick && tupletTree.voice == voice) { + traverseTupletTree(tupletTree.tuplet); + } + } + } + + static getTupletForNoteIndex(tupletTrees: SmoTupletTree[], voiceIx: number, noteIx: number): SmoTuplet | null { + const tuplets = SmoTupletTree.getTupletHierarchyForNoteIndex(tupletTrees, voiceIx, noteIx); + if(tuplets.length) { + return tuplets[tuplets.length - 1]; + } + return null; + } + + static getTupletTreeForNoteIndex(tupletTrees: SmoTupletTree[], voiceIx: number, noteIx: number): SmoTupletTree | null { + for (let i = 0; i < tupletTrees.length; i++) { + const tupletTree: SmoTupletTree = tupletTrees[i]; + if (tupletTree.startIndex <= noteIx && tupletTree.endIndex >= noteIx && tupletTree.voice == voiceIx) { + return tupletTree; + } + } + return null; + } + + // Finds the tuplet hierarchy for a given note index. + static getTupletHierarchyForNoteIndex(tupletTrees: SmoTupletTree[], voiceIx: number, noteIx: number): SmoTuplet[] { + let tupletHierarchy: SmoTuplet[] = []; + const traverseTupletTree = ( parentTuplet: SmoTuplet): void => { + tupletHierarchy.push(parentTuplet); + for (let i = 0; i < parentTuplet.childrenTuplets.length; i++) { + const tuplet = parentTuplet.childrenTuplets[i]; + if (tuplet.startIndex <= noteIx && tuplet.endIndex >= noteIx) { + traverseTupletTree(tuplet); + break; + } + } + } + + //find tuplet tree + for (let i = 0; i < tupletTrees.length; i++) { + const tupletTree: SmoTupletTree = tupletTrees[i]; + if (tupletTree.startIndex <= noteIx && tupletTree.endIndex >= noteIx && tupletTree.voice == voiceIx) { + traverseTupletTree(tupletTree.tuplet); + break; + } + } + + return tupletHierarchy; + } + + static removeTupletForNoteIndex(measure: SmoMeasure, voiceIx: number, noteIx: number) { + for (let i = 0; i < measure.tupletTrees.length; i++) { + const tupletTree: SmoTupletTree = measure.tupletTrees[i]; + if (tupletTree.startIndex <= noteIx && tupletTree.endIndex >= noteIx && tupletTree.voice == voiceIx) { + measure.tupletTrees.splice(i, 1); + break; + } + } + } + + serialize(): SmoTupletTreeParamsSer { + const params = { + ctor: 'SmoTupletTree', + tuplet: this.tuplet.serialize() + }; + return params; + } + + static deserialize(jsonObj: SmoTupletTreeParamsSer): SmoTupletTree { + const tuplet = SmoTuplet.deserialize(jsonObj.tuplet); + + return new SmoTupletTree({tuplet: tuplet}); + } + + static clone(tupletTree: SmoTupletTree): SmoTupletTree { + return SmoTupletTree.deserialize(tupletTree.serialize()); + } + + get startIndex() { + return this.tuplet.startIndex; + } + + get endIndex() { + return this.tuplet.endIndex; + } + + get voice() { + return this.tuplet.voice; + } + + get totalTicks() { + return this.tuplet.totalTicks; + } + + +} /** * Parameters for tuplet construction @@ -19,16 +162,15 @@ import { getId, SmoAttrs, Clef } from './common'; * @category SmoParameters */ export interface SmoTupletParams { - notes: SmoNote[], - id?: string, numNotes: number, + notesOccupied: number, stemTicks: number, totalTicks: number, - durationMap: number[], ratioed: boolean, bracketed: boolean, voice: number, - startIndex: number + startIndex: number, + endIndex: number, } /** * serializabl bits of SmoTuplet @@ -42,27 +184,24 @@ export interface SmoTupletParamsSer { /** * attributes for ID */ - id: string, + attrs: SmoAttrs, /** - * info about the serialized notes + * numNotes in the tuplet (not necessarily same as notes array size) */ - notes: SmoTupletNote[], + numNotes: number, /** - * numNotes in the duplet (not necessarily same as notes array size) + * */ - numNotes: number, + notesOccupied: number, /** * used to decide how to beam, 2048 for 1/4 triplet for instance */ stemTicks: number, + /** * total ticks to squeeze numNotes */ totalTicks: number, - /** - * map of notes to ticks - */ - durationMap: number[], /** * whether to use the : */ @@ -75,11 +214,17 @@ export interface SmoTupletParamsSer { * which voice the tuplet applies to */ voice: number, - /** - * the start tick index of the measure - */ - startIndex: number + + startIndex: number, + + endIndex: number, + + parentTuplet: TupletInfo | null, + + childrenTuplets: SmoTupletParamsSer[] + } + /** * tuplets must be serialized with their id attribute, enforce this * @param params a possible-valid SmoTupletParamsSer @@ -89,7 +234,7 @@ function isSmoTupletParamsSer(params: Partial): params is Sm if (!params.ctor || !(params.ctor === 'SmoTuplet')) { return false; } - if (!params.id || !(typeof(params.id) === 'string')) { + if (!params.attrs || !(typeof(params.attrs.id) === 'string')) { return false; } return true; @@ -101,60 +246,82 @@ function isSmoTupletParamsSer(params: Partial): params is Sm export class SmoTuplet { static get defaults(): SmoTupletParams { return JSON.parse(JSON.stringify({ - notes: [], numNotes: 3, + notesOccupied: 2, stemTicks: 2048, + startIndex: 0, + endIndex: 0, totalTicks: 4096, // how many ticks this tuple takes up - durationMap: [1.0, 1.0, 1.0], bracketed: true, voice: 0, - ratioed: false, - startIndex: 0 + ratioed: false })); } - id: string; - notes: SmoNote[]; + attrs: SmoAttrs; numNotes: number = 3; + notesOccupied: number = 2; stemTicks: number = 2048; totalTicks: number = 4096; - durationMap: number[] = [1.0, 1.0, 1.0]; bracketed: boolean = true; voice: number = 0; ratioed: boolean = false; + parentTuplet: TupletInfo | null = null; + childrenTuplets: SmoTuplet[] = []; startIndex: number = 0; + endIndex: number = 0; get clonedParams() { - const paramAr = ['stemTicks', 'ticks', 'totalTicks', 'durationMap', 'numNotes']; + const paramAr = ['stemTicks', 'ticks', 'totalTicks', 'numNotes']; const rv = {}; smoSerialize.serializedMerge(paramAr, this, rv); return rv; } static get parameterArray() { - return ['stemTicks', 'ticks', 'totalTicks', - 'durationMap', 'id', 'ratioed', 'bracketed', 'voice', 'startIndex', 'numNotes']; + return ['stemTicks', 'totalTicks', 'startIndex', 'endIndex', + 'attrs', 'ratioed', 'bracketed', 'voice', 'numNotes']; } serialize(): SmoTupletParamsSer { - const params:Partial = { - notes: [] - }; - this.notes.forEach((nn) => { - if (!nn.tupletId) { - throw 'bad tuplet when serializing'; - } - params.notes!.push({ - noteId: nn.attrs.id, tupletId: nn.tupletId, ticks: nn.ticks - }); - }); + const params: Partial = {}; params.ctor = 'SmoTuplet'; - smoSerialize.serializedMergeNonDefault(SmoTuplet.defaults, - SmoTuplet.parameterArray, this, params); + params.childrenTuplets = []; + + smoSerialize.serializedMergeNonDefault(SmoTuplet.defaults, SmoTuplet.parameterArray, this, params); + + this.childrenTuplets.forEach((tuplet) => { + params.childrenTuplets!.push(tuplet.serialize()); + }); + if (!isSmoTupletParamsSer(params)) { throw 'bad tuplet ' + JSON.stringify(params); } return params; } + + static deserialize(jsonObj: SmoTupletParamsSer): SmoTuplet { + const tupJson = SmoTuplet.defaults; + // We need to calculate the endIndex based on length of notes array + // Legacy schema had notes array, but we now demarcate tuplet with startIndex and endIndex + // Legacy schema did not have notesOccupied, we need to calculate it. + if ((jsonObj as any).notes !== undefined) { + const numberOfNotes = (jsonObj as any).notes.length; + tupJson.endIndex = jsonObj.startIndex + numberOfNotes - 1; + tupJson.notesOccupied = jsonObj.totalTicks / jsonObj.stemTicks; + } + + smoSerialize.serializedMerge(SmoTuplet.parameterArray, jsonObj, tupJson); + const tuplet = new SmoTuplet(tupJson); + tuplet.parentTuplet = jsonObj.parentTuplet ? jsonObj.parentTuplet : null; + if (jsonObj.childrenTuplets !== undefined) { + for (let i = 0; i < jsonObj.childrenTuplets.length; i++) { + const childTuplet = SmoTuplet.deserialize(jsonObj.childrenTuplets[i]); + tuplet.childrenTuplets.push(childTuplet); + } + } + return tuplet; + } + static calculateStemTicks(totalTicks: number, numNotes: number) { const stemValue = totalTicks / numNotes; let stemTicks = SmoTuplet.longestTuplet; @@ -166,202 +333,37 @@ export class SmoTuplet { } return stemTicks * 2; } + constructor(params: SmoTupletParams) { - smoSerialize.vexMerge(this, SmoTuplet.defaults); - smoSerialize.serializedMerge(SmoTuplet.parameterArray, params, this); - this.notes = params.notes; - this.id = getId().toString(); - this._adjustTicks(); + const defs = SmoTuplet.defaults; + this.numNotes = params.numNotes ? params.numNotes : defs.numNotes; + this.notesOccupied = params.notesOccupied ? params.notesOccupied : defs.notesOccupied; + this.stemTicks = params.stemTicks ? params.stemTicks : defs.stemTicks; + this.totalTicks = params.totalTicks ? params.totalTicks : defs.totalTicks; + this.bracketed = params.bracketed ? params.bracketed : defs.bracketed; + this.voice = params.voice ? params.voice : defs.voice; + this.ratioed = params.ratioed ? params.ratioed : defs.ratioed; + this.startIndex = params.startIndex ? params.startIndex : defs.startIndex; + this.endIndex = params.endIndex ? params.endIndex : defs.endIndex; + this.attrs = { + id: getId().toString(), + type: 'SmoTuplet' + }; } + static get longestTuplet() { return 8192; } - static cloneTuplet(tuplet: SmoTuplet, tupletNotes: SmoNote[]): SmoTuplet { - let i = 0; - const noteAr = tupletNotes; - const dupNotes: SmoNote[] = []; - const durationMap = JSON.parse(JSON.stringify(tuplet.durationMap)); // deep copy array - - // Add any remainders for oddlets - const totalTicks = noteAr.map((nn) => nn.ticks.numerator + nn.ticks.remainder) - .reduce((acc, nn) => acc + nn); - - const numNotes: number = tuplet.numNotes; - const stemTicks = SmoTuplet.calculateStemTicks(totalTicks, numNotes); - - noteAr.forEach((note) => { - const textModifiers = note.textModifiers; - // Note preserver remainder - note = SmoNote.cloneWithDuration(note, { - numerator: stemTicks * tuplet.durationMap[i], - denominator: 1, - remainder: note.ticks.remainder - }); - - // Don't clone modifiers, except for first one. - if (i === 0) { - const ntmAr: any = []; - textModifiers.forEach((tm) => { - const ntm = SmoNoteModifierBase.deserialize(tm); - ntmAr.push(ntm); - }); - note.textModifiers = ntmAr; - } - i += 1; - dupNotes.push(note); - }); - const rv = new SmoTuplet({ - numNotes: tuplet.numNotes, - voice: tuplet.voice, - notes: dupNotes, - stemTicks, - totalTicks, - ratioed: false, - bracketed: true, - startIndex: tuplet.startIndex, - durationMap - }); - return rv; - } - - _adjustTicks() { - let i = 0; - const sum = this.durationSum; - for (i = 0; i < this.notes.length; ++i) { - const note = this.notes[i]; - // TODO: notes_occupied needs to consider vex duration - note.ticks.denominator = 1; - note.ticks.numerator = Math.floor((this.totalTicks * this.durationMap[i]) / sum); - note.tupletId = this.id; - } - - // put all the remainder in the first note of the tuplet - const noteTicks = this.notes.map((nn) => nn.tickCount) - .reduce((acc, dd) => acc + dd); - // bug fix: if this is a clones tuplet, remainder is already set - this.notes[0].ticks.remainder = - this.notes[0].ticks.remainder + this.totalTicks - noteTicks; - } - getIndexOfNote(note: SmoNote | null): number { - let rv = -1; - let i = 0; - if (!note) { - return -1; - } - for (i = 0; i < this.notes.length; ++i) { - const tn = this.notes[i]; - if (note.attrs.id === tn.attrs.id) { - rv = i; - } - } - return rv; - } - - split(combineIndex: number) { - let i = 0; - const multiplier = 0.5; - const nnotes: SmoNote[] = []; - const nmap: number[] = []; - for (i = 0; i < this.notes.length; ++i) { - const note = this.notes[i]; - if (i === combineIndex) { - nmap.push(this.durationMap[i] * multiplier); - nmap.push(this.durationMap[i] * multiplier); - note.ticks.numerator *= multiplier; - - const onote = SmoNote.clone(note); - // remainder is for the whole tuplet, so don't duplicate that. - onote.ticks.remainder = 0; - nnotes.push(note); - nnotes.push(onote); - } else { - nmap.push(this.durationMap[i]); - nnotes.push(note); - } - } - this.notes = nnotes; - this.durationMap = nmap; - } - combine(startIndex: number, endIndex: number) { - let i = 0; - let base = 0.0; - let acc = 0.0; - // can't combine in this way, too many notes - if (this.notes.length <= endIndex || startIndex >= endIndex) { - return this; - } - for (i = startIndex; i <= endIndex; ++i) { - acc += this.durationMap[i]; - if (i === startIndex) { - base = this.durationMap[i]; - } else if (this.durationMap[i] !== base) { - // Can't combine non-equal tuplet notes - return this; - } - } - // how much each combined value will be multiplied by - const multiplier = acc / base; - - const nmap = []; - const nnotes = []; - // adjust the duration map - for (i = 0; i < this.notes.length; ++i) { - const note = this.notes[i]; - // notes that don't change are unchanged - if (i < startIndex || i > endIndex) { - nmap.push(this.durationMap[i]); - nnotes.push(note); - } - // changed note with combined duration - if (i === startIndex) { - note.ticks.numerator = note.ticks.numerator * multiplier; - nmap.push(acc); - nnotes.push(note); - } - // other notes after startIndex are removed from the map. - } - this.notes = nnotes; - this.durationMap = nmap; - return this; - } - // ### getStemDirection - // Return the stem direction, so we can bracket the correct place - getStemDirection(clef: Clef) { - const note = this.notes.find((nn) => nn.noteType === 'n'); - if (!note) { - return SmoNote.flagStates.down; - } - if (note.flagState !== SmoNote.flagStates.auto) { - return note.flagState; - } - return SmoMusic.pitchToStaffLine(clef, note.pitches[0]) - >= 3 ? SmoNote.flagStates.down : SmoNote.flagStates.up; - } - get durationSum() { - let acc = 0; - let i = 0; - for (i = 0; i < this.durationMap.length; ++i) { - acc += this.durationMap[i]; - } - return Math.round(acc); - } + //todo: adjust naming get num_notes() { - return this.durationSum; + return this.numNotes; } get notes_occupied() { return Math.floor(this.totalTicks / this.stemTicks); } - get note_ticks_occupied() { - return this.totalTicks / this.stemTicks; - } + get tickCount() { - let rv = 0; - let i = 0; - for (i = 0; i < this.notes.length; ++i) { - const note = this.notes[i]; - rv += (note.ticks.numerator / note.ticks.denominator) + note.ticks.remainder; - } - return rv; + return this.totalTicks; } } diff --git a/src/smo/midi/midiToSmo.ts b/src/smo/midi/midiToSmo.ts index e1401ebb..21d91288 100644 --- a/src/smo/midi/midiToSmo.ts +++ b/src/smo/midi/midiToSmo.ts @@ -14,7 +14,7 @@ import { SmoScore } from "../data/score"; import { SmoLayoutManager } from "../data/scoreModifiers"; import { SmoTie } from "../data/staffModifiers"; import { SmoSystemStaff } from "../data/systemStaff"; -import { SmoTuplet } from "../data/tuplet"; +import {SmoTuplet, SmoTupletTree} from "../data/tuplet"; import { SmoOperation } from "../xform/operations"; export type MidiEventType = 'text' | 'copyrightNotice' | 'trackName' | 'instrumentName' | 'lyrics' | 'marker' | @@ -309,12 +309,15 @@ export class MidiToSmo { const voiceLen = measure.voices[0].notes.length; const tupletNotes = [note, measure.voices[0].notes[voiceLen - 2], measure.voices[0].notes[voiceLen - 3]]; const defs = SmoTuplet.defaults; - defs.notes = tupletNotes; defs.stemTicks = ev.tupletInfo.stemTicks; defs.numNotes = ev.tupletInfo.numNotes; defs.totalTicks = ev.tupletInfo.totalTicks; defs.startIndex = voiceLen - 3; - measure.tuplets.push(new SmoTuplet(defs)); + defs.endIndex = voiceLen - 1; + const tuplet = new SmoTuplet(defs); + this.adjustTupletNotes(tupletNotes, tuplet); + const tupletTree: SmoTupletTree = new SmoTupletTree({tuplet: tuplet}); + measure.tupletTrees.push(tupletTree); } if (ev.isTied) { this.addToTieMap(measureIndex); @@ -326,6 +329,19 @@ export class MidiToSmo { }); return measures; } + + adjustTupletNotes(notes: SmoNote[], tuplet: SmoTuplet) { + const numerator = tuplet.totalTicks / tuplet.numNotes; + for (let i = 0; i < notes.length; ++i) { + const note = notes[i]; + note.ticks = { numerator: Math.floor(numerator), denominator: 1, remainder: 0 } + note.stemTicks = tuplet.stemTicks; + note.tupletId = tuplet.attrs.id; + } + if (numerator % 1) { + notes[0].ticks.numerator += 1; + } + } /** * @param ticks * @returns the length in ticks of a triplet, if this looks like a triplet. Otherwise 0 diff --git a/src/smo/mxml/smoToXml.ts b/src/smo/mxml/smoToXml.ts index adfc8c2a..7e5f7e3f 100644 --- a/src/smo/mxml/smoToXml.ts +++ b/src/smo/mxml/smoToXml.ts @@ -10,7 +10,7 @@ import { SmoBarline, TimeSignature, SmoRehearsalMark, SmoMeasureModifierBase } f import { SmoStaffHairpin, SmoSlur, SmoTie } from '../data/staffModifiers'; import { SmoArticulation, SmoLyric, SmoOrnament } from '../data/noteModifiers'; import { SmoSelector } from '../xform/selections'; -import { SmoTuplet } from '../data/tuplet'; +import { SmoTuplet, SmoTupletTree } from '../data/tuplet'; import { XmlHelpers } from './xmlHelpers'; import { smoSerialize } from '../../common/serializationHelpers'; @@ -49,7 +49,8 @@ export interface SmoState { beamState: number, beamTicks: number, timeSignature?: TimeSignature, - tempo?: SmoTempoText + tempo?: SmoTempoText, + currentTupletLevel: number, // not sure about the name } /** @@ -80,7 +81,8 @@ export class SmoToXml { lyricState: {}, measureTicks: 0, beamState: 0, - beamTicks: 4096 + beamTicks: 4096, + currentTupletLevel: 0, })); } /** @@ -493,32 +495,53 @@ export class SmoToXml { /** * /score-partwise/measure/note/time-modification * /score-partwise/measure/note/tuplet - * @param noteElement - * @param notationsElement - * @param smoState - * @returns */ - static tupletTime(noteElement: Element, tuplet: SmoTuplet, smoState: SmoState) { + static tupletTime(noteElement: Element, note: SmoNote, measure: SmoMeasure, smoState: SmoState) { + const tuplets: SmoTuplet[] = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, smoState.voiceIndex - 1, smoState.voiceTickIndex); + let actualNotes: number = 1; + let normalNotes: number = 1; + for (let i = 0; i < tuplets.length; i++) { + const tuplet = tuplets[i]; + actualNotes *= tuplet.numNotes; + normalNotes *= tuplet.notesOccupied; + } const nn = XmlHelpers.createTextElementChild; const obj = { - actualNotes: tuplet.numNotes, normalNotes: tuplet.notes_occupied + actualNotes: actualNotes, normalNotes: normalNotes }; const timeModification = nn(noteElement, 'time-modification', null, ''); nn(timeModification, 'actual-notes', obj, 'actualNotes'); nn(timeModification, 'normal-notes', obj, 'normalNotes'); } - static tupletNotation(notationsElement: Element, tuplet: SmoTuplet, note: SmoNote) { - const nn = XmlHelpers.createTextElementChild; - if (tuplet.getIndexOfNote(note) === 0) { - const tupletElement = nn(notationsElement, 'tuplet', null, ''); - XmlHelpers.createAttributes(tupletElement, { - number: 1, type: 'start' - }); - } else if (tuplet.getIndexOfNote(note) === tuplet.notes.length - 1) { - const tupletElement = nn(notationsElement, 'tuplet', null, ''); - XmlHelpers.createAttributes(tupletElement, { - number: 1, type: 'stop' - }); + static tupletNotation(notationsElement: Element, note: SmoNote, measure: SmoMeasure, smoState: SmoState) { + const tuplets: SmoTuplet[] = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, smoState.voiceIndex - 1, smoState.voiceTickIndex); + for (let i = 0; i < tuplets.length; i++) { + const tuplet: SmoTuplet = tuplets[i]; + const nn = XmlHelpers.createTextElementChild; + + if (tuplet.startIndex === smoState.voiceTickIndex) {//START + const tupletElement = nn(notationsElement, 'tuplet', null, ''); + smoState.currentTupletLevel++; + XmlHelpers.createAttributes(tupletElement, { + number: smoState.currentTupletLevel, type: 'start' + }); + + const tupletType = XmlHelpers.ticksToNoteTypeMap[tuplet.stemTicks]; + + const tupletActual = nn(tupletElement, 'tuplet-actual', null, ''); + nn(tupletActual, 'tuplet-number', tuplet, 'numNotes'); + nn(tupletActual, 'tuplet-type', tupletType, ''); + + const tupletNormal = nn(tupletElement, 'tuplet-normal', null, ''); + nn(tupletNormal, 'tuplet-number', tuplet, 'notesOccupied'); + nn(tupletNormal, 'tuplet-type', tupletType, ''); + } else if (tuplet.endIndex === smoState.voiceTickIndex) {//STOP + const tupletElement = nn(notationsElement, 'tuplet', null, ''); + XmlHelpers.createAttributes(tupletElement, { + number: smoState.currentTupletLevel, type: 'stop' + }); + smoState.currentTupletLevel--; + } } } @@ -759,14 +782,14 @@ export class SmoToXml { } const duration = note.tickCount; smoState.measureTicks += duration; - const tuplet = measure.getTupletForNote(note); + const tuplet = SmoTupletTree.getTupletForNoteIndex(measure.tupletTrees, smoState.voiceIndex - 1, smoState.voiceTickIndex); nn(noteElement, 'duration', { duration }, 'duration'); SmoToXml.tie(noteElement, smoState); nn(noteElement, 'voice', { voice: smoState.voiceIndex }, 'voice'); - let typeTickCount = note.tickCount; - if (tuplet) { - typeTickCount = tuplet.stemTicks; - } + let typeTickCount = note.stemTicks; + // if (tuplet) { + // typeTickCount = tuplet.stemTicks; + // } nn(noteElement, 'type', { type: XmlHelpers.closestStemType(typeTickCount) }, 'type'); const dots = SmoMusic.smoTicksToVexDots(note.tickCount); @@ -776,7 +799,7 @@ export class SmoToXml { // time modification (tuplet) comes before notations which have tuplet beaming rules // also before stem if (tuplet) { - SmoToXml.tupletTime(noteElement, tuplet, smoState); + SmoToXml.tupletTime(noteElement, note, measure, smoState); } if (note.flagState === SmoNote.flagStates.up) { nn(noteElement, 'stem', { direction: 'up' }, 'direction'); @@ -798,7 +821,7 @@ export class SmoToXml { } SmoToXml.tied(notationsElement, smoState); if (tuplet) { - SmoToXml.tupletNotation(notationsElement, tuplet, note); + SmoToXml.tupletNotation(notationsElement, note, measure, smoState); } const ornaments = note.getOrnaments(); if (ornaments.length) { diff --git a/src/smo/mxml/xmlHelpers.ts b/src/smo/mxml/xmlHelpers.ts index 5dcb0fd7..2893e8bf 100644 --- a/src/smo/mxml/xmlHelpers.ts +++ b/src/smo/mxml/xmlHelpers.ts @@ -7,6 +7,7 @@ import { SmoNote } from '../data/note'; import { Pitch, PitchLetter, createXmlAttributes, createXmlAttribute } from '../data/common'; import { SmoSelector } from '../xform/selections'; import { SmoBarline } from '../data/measureModifiers'; +import { XmlTupletData } from './xmlState'; export interface XmlOrnamentData { ctor: string, @@ -36,9 +37,19 @@ export interface XmlTieType { /** * Store tuplet information when parsing xml */ -export interface XmlTupletData { - number: number, type: string +export interface XmlTupletType { + number: number, + type: string, + data: XmlTupletData | null, } + +export interface XmlTimeModificationType { + actualNotes: number, + normalNotes: number, + normalType: number, + //normalDot, todo: check if just bool or list of dots (probably list of dots) +} + export interface XmlEndingData { numbers: number[], type: string } @@ -341,12 +352,13 @@ export class XmlHelpers { rv.tickCount = XmlHelpers.durationFromType(noteNode, def); rv.duration = (divisions / 4096) * rv.tickCount; } + //todo nenad: seems like this is not needed since we keep stemTicks directly on the note object now // If this is a tuplet, we adjust the note duration back to the graphical type // and SMO will create the tuplet after. We keep track of tuplet data though for beaming - if (timeAlteration) { - rv.tickCount = (rv.tickCount * timeAlteration.noteCount) / timeAlteration.noteDuration; - rv.alteration = timeAlteration; - } + // if (timeAlteration) { + // rv.tickCount = (rv.tickCount * timeAlteration.noteCount) / timeAlteration.noteDuration; + // rv.alteration = timeAlteration; + // } return rv; } @@ -393,19 +405,90 @@ export class XmlHelpers { }); return rv; } - static getTupletData(noteNode: Element): XmlTupletData[] { - const rv: XmlTupletData[] = []; - const nNodes = [...noteNode.getElementsByTagName('notations')]; - nNodes.forEach((nNode) => { - const slurNodes = [...nNode.getElementsByTagName('tuplet')]; - slurNodes.forEach((slurNode) => { - const number = parseInt(slurNode.getAttribute('number') as string, 10); - const type = slurNode.getAttribute('type') as string; - rv.push({ number, type }); + + static getTimeModificationType(noteNode: Element): XmlTimeModificationType | null { + const timeModificationNode = noteNode.querySelector('time-modification'); + let xmlTimeModification: XmlTimeModificationType | null = null; + if (timeModificationNode) { + const actualNotesNode = timeModificationNode.querySelector('actual-notes'); + const normalNotesNode = timeModificationNode.querySelector('normal-notes'); + const normalTypeNode = timeModificationNode.querySelector('normal-type'); + const normalType = normalTypeNode?.textContent ? XmlHelpers.noteTypesToSmoMap[normalTypeNode?.textContent] ?? null : null; + if (actualNotesNode?.textContent && normalNotesNode?.textContent && normalType) { + const actualNotes = parseInt(actualNotesNode.textContent, 10); + const normalNotes = parseInt(normalNotesNode.textContent, 10); + xmlTimeModification = { + actualNotes: actualNotes, + normalNotes: normalNotes, + normalType: normalType + }; + } + } + return xmlTimeModification; + } + + static getTupletData(noteNode: Element): XmlTupletType[] { + const rv: XmlTupletType[] = []; + const timeModification = XmlHelpers.getTimeModificationType(noteNode); + const notationNode = noteNode.querySelector('notations'); + if (notationNode) { + const tupletNodes = [...notationNode.getElementsByTagName('tuplet')]; + tupletNodes.forEach((tupletNode) => { + const number = parseInt(tupletNode.getAttribute('number') as string, 10) as number; + const type = tupletNode.getAttribute('type') as string; + const xmlTupletType: XmlTupletType = { + number: number, + type: type, + data: null + }; + if (type === 'start') { + let tupletActual = null; + let tupletNormal = null; + const tupletActualNode = tupletNode.querySelector('tuplet-actual'); + if (tupletActualNode) { + const tupletNumberNode = tupletActualNode.querySelector('tuplet-number'); + const tupletTypeNode = tupletActualNode.querySelector('tuplet-type'); + const tupletTypeContent = tupletTypeNode?.textContent; + const tupletType = tupletTypeContent ? XmlHelpers.noteTypesToSmoMap[tupletTypeContent] ?? null : null; + if (tupletNumberNode?.textContent && tupletType) { + const tupletNumber = parseInt(tupletNumberNode.textContent, 10); + tupletActual = {tupletNumber: tupletNumber, tupletType: tupletType}; + } + } + const tupletNormalNode = tupletNode.querySelector('tuplet-normal'); + if (tupletNormalNode) { + const tupletNumberNode = tupletNormalNode.querySelector('tuplet-number'); + const tupletTypeNode = tupletNormalNode.querySelector('tuplet-type'); + const tupletTypeContent = tupletTypeNode?.textContent; + const tupletType = tupletTypeContent ? XmlHelpers.noteTypesToSmoMap[tupletTypeContent] ?? null : null; + if (tupletNumberNode?.textContent && tupletType) { + const tupletNumber = parseInt(tupletNumberNode.textContent, 10); + tupletNormal = {tupletNumber: tupletNumber, tupletType: tupletType}; + } + } + if (tupletActual && tupletNormal) { + const xmlTupletData: XmlTupletData = { + stemTicks: tupletActual.tupletType, + numNotes: tupletActual.tupletNumber, + notesOccupied: (tupletActual.tupletType / tupletNormal.tupletType) * tupletNormal.tupletNumber + }; + xmlTupletType.data = xmlTupletData; + } else if (timeModification) { + const xmlTupletData: XmlTupletData = { + stemTicks: timeModification.normalType, + numNotes: timeModification.actualNotes, + notesOccupied: timeModification.normalNotes + }; + xmlTupletType.data = xmlTupletData; + } + } + rv.push(xmlTupletType); }); - }); + } + return rv; } + static articulationsAndOrnaments(noteNode: Element): SmoNoteModifierBase[] { const rv: SmoNoteModifierBase[] = []; const nNodes = [...noteNode.getElementsByTagName('notations')]; diff --git a/src/smo/mxml/xmlState.ts b/src/smo/mxml/xmlState.ts index ac9ed504..971cdcfc 100644 --- a/src/smo/mxml/xmlState.ts +++ b/src/smo/mxml/xmlState.ts @@ -1,19 +1,27 @@ // [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. -import { XmlHelpers, XmlLyricData, XmlDurationAlteration, XmlTieType, XmlSlurType, XmlTupletData } from './xmlHelpers'; -import { SmoScore } from '../data/score'; -import { SmoSystemGroup, SmoFormattingManager } from '../data/scoreModifiers'; -import { SmoSystemStaff } from '../data/systemStaff'; -import { SmoTie, SmoStaffHairpin, SmoSlur, SmoSlurParams, SmoInstrument, SmoInstrumentParams, TieLine } from '../data/staffModifiers'; -import { SmoBarline, SmoMeasureModifierBase, SmoRehearsalMark, SmoTempoText } from '../data/measureModifiers'; -import { SmoPartInfo } from '../data/partInfo'; -import { SmoMeasure } from '../data/measure'; -import { SmoNote } from '../data/note'; -import { SmoLyric, SmoDynamicText, SmoGraceNote } from '../data/noteModifiers'; -import { SmoTuplet } from '../data/tuplet'; -import { Clef } from '../data/common'; -import { SmoMusic } from '../data/music'; -import { SmoSelector, SmoSelection } from '../xform/selections'; +import {XmlDurationAlteration, XmlHelpers, XmlLyricData, XmlSlurType, XmlTieType, XmlTupletType} from './xmlHelpers'; +import {SmoScore} from '../data/score'; +import {SmoFormattingManager, SmoSystemGroup} from '../data/scoreModifiers'; +import {SmoSystemStaff} from '../data/systemStaff'; +import { + SmoInstrument, + SmoInstrumentParams, + SmoSlur, + SmoSlurParams, + SmoStaffHairpin, + SmoTie, + TieLine +} from '../data/staffModifiers'; +import {SmoBarline, SmoMeasureModifierBase, SmoRehearsalMark, SmoTempoText} from '../data/measureModifiers'; +import {SmoPartInfo} from '../data/partInfo'; +import {SmoMeasure} from '../data/measure'; +import {SmoNote} from '../data/note'; +import {SmoDynamicText, SmoGraceNote, SmoLyric} from '../data/noteModifiers'; +import {SmoTuplet, SmoTupletTree} from '../data/tuplet'; +import {Clef} from '../data/common'; +import {SmoMusic} from '../data/music'; +import {SmoSelection, SmoSelector} from '../xform/selections'; export interface XmlClefInfo { clef: string, staffId: number @@ -57,13 +65,37 @@ export interface XmlCompletedTies { fromPitch: number, toPitch: number } + export interface XmlCompletedTuplet { - tuplet: SmoTuplet, staffId: number, voiceId: number + tuplet: SmoTuplet, + staffId: number, + voiceId: number +} +export class XmlTupletStateTreeNode { + tupletState: XmlTupletState; + children: XmlTupletStateTreeNode[]; + constructor(tupletState: XmlTupletState) { + this.tupletState = tupletState; + this.children = []; + } +} + +export interface XmlCompletedTupletState { + tupletState: XmlTupletState, + staffId: number, + voiceId: number } export interface XmlTupletState { - start: SmoSelector, - end: SmoSelector + start: SmoSelector | null, + end: SmoSelector | null, + data: XmlTupletData | null, +} +export interface XmlTupletData { + numNotes: number, + notesOccupied: number, + stemTicks: number, } + export interface XmlEnding { start: number, end: number, @@ -107,7 +139,10 @@ export class XmlState { verseMap: Record = {}; measureNumber: number = 0; formattingManager = new SmoFormattingManager(SmoFormattingManager.defaults); - tuplets: Record = {}; + + completedTupletStates: XmlCompletedTupletState[] = []; + tupletStatesInProgress: Record = {}; + tickCursor: number = 0; tempo: SmoTempoText = new SmoTempoText(SmoTempoText.defaults); staffArray: XmlStaffInfo[] = []; @@ -157,7 +192,7 @@ export class XmlState { if (isNaN(this.measureNumber)) { this.measureNumber = oldMeasure + 1; } - this.tuplets = {}; + this.tupletStatesInProgress = {}; this.tickCursor = 0; this.tempo = SmoMeasureModifierBase.deserialize(this.tempo.serialize()); this.tempo.display = false; @@ -536,66 +571,132 @@ export class XmlState { } }); } - // ### backtrackTuplets - // If we received a tuplet end, go back through the voice - // and construct the SmoTuplet. - backtrackTuplets(voice: XmlVoiceInfo, tupletNumber: number, staffId: number, voiceId: number) { - const tupletState = this.tuplets[tupletNumber]; - let i = tupletState.start.tick; - const notes = []; - const durationMap = []; - while (i < voice.notes.length) { - const note = voice.notes[i]; - notes.push(note); - if (i === tupletState.start.tick) { - durationMap.push(1.0); - } else { - const prev = voice.notes[i - 1]; - durationMap.push(note.ticks.numerator / prev.ticks.numerator); - } - i += 1; - } - const tp = SmoTuplet.defaults; - tp.notes = notes; - tp.durationMap = durationMap; - tp.voice = voiceId; - const tuplet = new SmoTuplet(tp); - // Store the tuplet with the staff ID and voice so we - // can add it to the right measure when it's created. - this.completedTuplets.push({ tuplet, staffId, voiceId }); - } + + + // ### updateTupletStates // react to a tuplet start or stop directive - updateTupletStates(tupletInfos: XmlTupletData[], voice: XmlVoiceInfo, staffIndex: number, voiceIndex: number) { + // we need to handle start and stop directives that appear out of order + updateTupletStates(tupletInfos: XmlTupletType[], voice: XmlVoiceInfo, staffIndex: number, voiceIndex: number) { + // this.tickCursor; const tick = voice.notes.length - 1; tupletInfos.forEach((tupletInfo) => { + let tupletState: XmlTupletState | undefined = this.tupletStatesInProgress[tupletInfo.number]; + if (tupletState == undefined) { + tupletState = { + start: null, + end: null, + data: null, + }; + this.tupletStatesInProgress[tupletInfo.number] = tupletState; + } if (tupletInfo.type === 'start') { - this.tuplets[tupletInfo.number] = { - start: { staff: staffIndex, measure: this.measureNumber, voice: voiceIndex, tick, pitches: [] }, - end: SmoSelector.default + tupletState.start = { + staff: staffIndex, measure: this.measureNumber, voice: voiceIndex, tick, pitches: [] }; + tupletState.data = tupletInfo.data; } else if (tupletInfo.type === 'stop') { - this.tuplets[tupletInfo.number].end = { + tupletState.end = { staff: staffIndex, measure: this.measureNumber, voice: voiceIndex, tick, pitches: [] }; - this.backtrackTuplets(voice, tupletInfo.number, staffIndex, voiceIndex); + } + if (tupletState.start != null && tupletState.end != null) { + this.completedTupletStates.push({ + tupletState: tupletState, + staffId: staffIndex, + voiceId: voiceIndex + }); + delete this.tupletStatesInProgress[tupletInfo.number]; } }); } addTupletsToMeasure(smoMeasure: SmoMeasure, staffId: number, voiceId: number) { - const completed: XmlCompletedTuplet[] = []; - this.completedTuplets.forEach((tuplet) => { - if (tuplet.voiceId === voiceId && tuplet.staffId === staffId) { - smoMeasure.tuplets.push(tuplet.tuplet); - } else { - completed.push(tuplet); + const tupletStates = this.findCompletedTupletStatesByStaffAndVoice(staffId, voiceId); + const xmlTupletStateTrees = this.buildXmlTupletStateTrees(tupletStates); + const notes: SmoNote[] = smoMeasure.voices[voiceId].notes; + smoMeasure.tupletTrees = this.buildSmoTupletTreesFromXmlTupletStateTrees(xmlTupletStateTrees, notes); + } + private findCompletedTupletStatesByStaffAndVoice(staffId: number, voiceId: number): XmlTupletState[] { + const tupletStates: XmlTupletState[] = []; + this.completedTupletStates.forEach((completedTupletState) => { + if (completedTupletState.staffId === staffId && completedTupletState.voiceId === voiceId) { + tupletStates.push(completedTupletState.tupletState); } }); - this.completedTuplets = completed; + return tupletStates; } + private buildXmlTupletStateTrees(tupletStates: XmlTupletState[]): XmlTupletStateTreeNode[] { + let sortedTupletStates = this.sortTupletStates(tupletStates); + let roots: XmlTupletStateTreeNode[] = []; + let activeNodes: XmlTupletStateTreeNode[] = []; + for (let tupletState of sortedTupletStates) { + let node = new XmlTupletStateTreeNode(tupletState); + let placed = false; + while (activeNodes.length > 0) { + let lastNode = activeNodes[activeNodes.length - 1]; + if (tupletState.start!.tick <= lastNode.tupletState.end!.tick) { + lastNode.children.push(node); + placed = true; + break; + } else { + activeNodes.pop(); + } + } + if (!placed) { + roots.push(node); + } + activeNodes.push(node); + } + return roots; + } + private sortTupletStates(tupletStates: XmlTupletState[]): XmlTupletState[] { + return tupletStates.sort((a, b) => { + if (a.start === b.start) { + return a.end!.tick - b.end!.tick; + } + return a.start!.tick - b.start!.tick; + }); + } + /** + * Create SmoTuplets out of completedTupletStates + */ + buildSmoTupletTreesFromXmlTupletStateTrees(xmlTupletStateTrees: XmlTupletStateTreeNode[], notes: SmoNote[]): SmoTupletTree[] { + const smoTupletTrees: SmoTupletTree[] = []; + const traverseXmlTupletStateTree = (xmlTupletStateTreeNode: XmlTupletStateTreeNode): SmoTuplet => { + const smoTupletParams = SmoTuplet.defaults; + const xmlTupletState = xmlTupletStateTreeNode.tupletState; + if (xmlTupletState.data) { + smoTupletParams.numNotes = xmlTupletState.data.numNotes; + smoTupletParams.notesOccupied = xmlTupletState.data.notesOccupied; + smoTupletParams.stemTicks = xmlTupletState.data.stemTicks; + } + smoTupletParams.startIndex = xmlTupletState.start!.tick; + smoTupletParams.endIndex = xmlTupletState.end!.tick; + for (let i = smoTupletParams.startIndex; i <= smoTupletParams.endIndex; i++) { + smoTupletParams.totalTicks += notes[i].tickCount; + } + smoTupletParams.voice = xmlTupletState.start!.voice; + const smoTuplet = new SmoTuplet(smoTupletParams); + for (let i = 0; i < xmlTupletStateTreeNode.children.length; i++) { + const childSmoTuplet = traverseXmlTupletStateTree(xmlTupletStateTreeNode.children[i]); + childSmoTuplet.parentTuplet = {id: smoTuplet.attrs.id}; + smoTuplet.childrenTuplets.push(childSmoTuplet); + } + return smoTuplet; + }; + + for (let i = 0; i < xmlTupletStateTrees.length; i++) { + const xmlTupletStateTreeNode = xmlTupletStateTrees[i]; + const tuplet: SmoTuplet = traverseXmlTupletStateTree(xmlTupletStateTreeNode); + smoTupletTrees.push(new SmoTupletTree({tuplet: tuplet})); + } + return smoTupletTrees; + } + + getSystems(): SmoSystemGroup[] { const rv: SmoSystemGroup[] = []; - this.systems.forEach((system) => { + this.systems.forEach((system: { startSelector: SmoSelector; endSelector: SmoSelector; leftConnector: number; }) => { const params = SmoSystemGroup.defaults; params.startSelector = system.startSelector; params.endSelector = system.endSelector; diff --git a/src/smo/mxml/xmlToSmo.ts b/src/smo/mxml/xmlToSmo.ts index f8fdd407..db54bc68 100644 --- a/src/smo/mxml/xmlToSmo.ts +++ b/src/smo/mxml/xmlToSmo.ts @@ -621,6 +621,8 @@ export class XmlToSmo { const noteType = restNode.length ? 'r' : 'n'; const durationData = XmlHelpers.ticksFromDuration(noteElement, divisions, 4096); const tickCount = durationData.tickCount; + //todo nenad: we probably need to handle dotted durations + const stemTicks = XmlHelpers.durationFromType(noteElement, 4096); if (chordNode.length === 0) { xmlState.staffArray[staffIndex].voices[voiceIndex].ticksUsed += tickCount; } @@ -648,6 +650,7 @@ export class XmlToSmo { // If this is a non-grace note, add any grace notes to the note since SMO // treats them as note modifiers noteData.ticks = { numerator: tickCount, denominator: 1, remainder: 0 }; + noteData.stemTicks = stemTicks; noteData.flagState = flagState; noteData.clef = clefString; xmlState.previousNote = new SmoNote(noteData); @@ -698,6 +701,7 @@ export class XmlToSmo { xmlState.updateSlurStates(slurInfos); xmlState.updateTieStates(tieInfos); voice.notes.push(xmlState.previousNote); + //todo nenad: check if we need to change something with 'alteration' xmlState.updateBeamState(beamState, durationData.alteration, voice, voiceIndex); xmlState.updateTupletStates(tupletInfos, voice, staffIndex, voiceIndex); @@ -792,14 +796,14 @@ export class XmlToSmo { // voices not in array, put them in an array Object.keys(staffData.voices).forEach((voiceKey) => { const voice = staffData.voices[voiceKey]; - xmlState.addTupletsToMeasure(smoMeasure, staffData.clefInfo.staffId, - parseInt(voiceKey, 10)); voice.notes.forEach((note) => { if (!note.clef) { note.clef = smoMeasure.clef; } }); smoMeasure.voices.push(voice); + const voiceId = smoMeasure.voices.length - 1; + xmlState.addTupletsToMeasure(smoMeasure, staffData.clefInfo.staffId, voiceId); }); if (smoMeasure.voices.length === 0) { smoMeasure.voices.push({ notes: SmoMeasure.getDefaultNotes(smoMeasure) }); diff --git a/src/smo/xform/audioTrack.ts b/src/smo/xform/audioTrack.ts index 7f621a43..96a39111 100644 --- a/src/smo/xform/audioTrack.ts +++ b/src/smo/xform/audioTrack.ts @@ -10,6 +10,7 @@ import { SmoNote } from '../data/note'; import { Pitch } from '../data/common'; import { SmoSystemStaff } from '../data/systemStaff'; import { SmoAudioPitch } from '../data/music'; +import { SmoTupletTree } from '../data/tuplet'; export interface SmoAudioRepeat { startRepeat: number, @@ -523,14 +524,14 @@ export class SmoAudioScore { // update staff features of slur/tie/cresc. this.getSlurInfo(track, selection); this.getHairpinInfo(track, selection); - const tuplet = measure.getTupletForNote(note); - if (tuplet && tuplet.getIndexOfNote(note) === 0) { + const tuplet = SmoTupletTree.getTupletForNoteIndex(measure.tupletTrees, voiceIx, noteIx); + if (tuplet && tuplet.startIndex === noteIx) { tupletTicks = tuplet.tickCount / this.timeDiv; } if (tupletTicks) { // tuplet likely won't fit evenly in ticks, so // use remainder in last tuplet note. - if (tuplet && tuplet.getIndexOfNote(note) === tuplet.notes.length - 1) { + if (tuplet && tuplet.endIndex === noteIx) { duration = tupletTicks; tupletTicks = 0; } else { diff --git a/src/smo/xform/beamers.ts b/src/smo/xform/beamers.ts index ee3925fe..c6d8d20a 100644 --- a/src/smo/xform/beamers.ts +++ b/src/smo/xform/beamers.ts @@ -149,40 +149,55 @@ export class SmoBeamer { } // beam tuplets - if (note.isTuplet) { - const tuplet = this.measure.getTupletForNote(note); - // The underlying notes must have been deleted. - if (!tuplet) { - return; - } - const tupletIndex = tuplet.getIndexOfNote(note); - const ult = tuplet.notes[tuplet.notes.length - 1]; - const first = tuplet.notes[0]; + // if (note.isTuplet) { + // const tuplet = this.measure.getTupletForNote(note); + // // The underlying notes must have been deleted. + // if (!tuplet) { + // return; + // } - if (first.endBeam) { - this._advanceGroup(); - return; - } - // is this beamable length-wise - const stemTicks = SmoMusic.closestDurationTickLtEq(note.tickCount) * tuplet.durationMap[tupletIndex]; - if (note.noteType === 'n' && stemTicks < 4096) { - this.currentGroup.push(note); - } - // Ultimate note in tuplet - if (ult.attrs.id === note.attrs.id && !this._isRemainingTicksBeamable(tickmap, index)) { - this._completeGroup(tickmap.voice); - this._advanceGroup(); - } - return; - } + // const first = tuplet.getFirstNote(); + // if (!first) { + // return; + // } + + // const ult = tuplet.getLastNote(); + // if (!ult) { + // return; + // } + + // if (first.endBeam) { + // this._advanceGroup(); + // return; + // } + + // // is this beamable length-wise + // if (note.noteType === 'n' && note.stemTicks < 4096) { + // this.currentGroup.push(note); + // } + // // Ultimate note in tuplet + // if (ult.attrs.id === note.attrs.id && !this._isRemainingTicksBeamable(tickmap, index)) { + // this._completeGroup(tickmap.voice); + // this._advanceGroup(); + // } + // return; + // } // don't beam > 1/4 note in 4/4 time. Don't beam rests. - if (tickmap.deltaMap[index] >= 4096 || (note.isRest() && this.currentGroup.length === 0)) { + if (note.stemTicks >= 4096 || (note.isRest() && this.currentGroup.length === 0)) { this._completeGroup(tickmap.voice); this._advanceGroup(); return; } + //if areTupletElementsDifferent(noteOne, noteTwo) + //this._completeGroup(tickmap.voice); + //this._advanceGroup(); + if (index > 0 && !SmoBeamer.areTupletElementsTheSame(tickmap.notes[index - 1], tickmap.notes[index])) { + this._completeGroup(tickmap.voice); + this._advanceGroup(); + } + this.currentGroup.push(note); if (note.endBeam) { this._completeGroup(tickmap.voice); @@ -193,11 +208,11 @@ export class SmoBeamer { return; } else if (this.duration === 8192) { this._completeGroup(tickmap.voice); - this._advanceGroup(); + this._advanceGroup(); } } // If we are aligned to a beat on the measure, and we are in common time - if (this.currentGroup.length > 1 && this.measure.timeSignature.beatDuration === 4 && + if (this.currentGroup.length > 1 && this.measure.timeSignature.beatDuration === 4 && this.measureDuration % 4096 === 0) { this._completeGroup(tickmap.voice); this._advanceGroup(); @@ -214,4 +229,19 @@ export class SmoBeamer { this._advanceGroup(); } } + + public static areTupletElementsTheSame(noteOne: SmoNote, noteTwo: SmoNote): boolean { + if (typeof(noteOne.tupletId) === 'undefined' && typeof(noteTwo.tupletId) === 'undefined') { + return true; + } + if (noteOne.tupletId === null && noteTwo.tupletId === null) { + return true; + } + if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tupletId == noteTwo.tupletId) { + return true; + } + + return false; + } + } diff --git a/src/smo/xform/copypaste.ts b/src/smo/xform/copypaste.ts index d649f62a..32a4a101 100644 --- a/src/smo/xform/copypaste.ts +++ b/src/smo/xform/copypaste.ts @@ -4,13 +4,14 @@ import { SmoSelection, SmoSelector } from './selections'; import { SmoNote } from '../data/note'; import { SmoMeasure, SmoVoice } from '../data/measure'; import { StaffModifierBase } from '../data/staffModifiers'; -import { SmoTuplet } from '../data/tuplet'; +import {SmoTuplet, SmoTupletTree, SmoTupletTreeParams} from '../data/tuplet'; import { SmoMusic } from '../data/music'; import { SvgHelpers } from '../../render/sui/svgHelpers'; import { SmoScore } from '../data/score'; import { TickMap } from './tickMap'; import { SmoSystemStaff } from '../data/systemStaff'; import { getId } from '../data/common'; +import {SmoUnmakeTupletActor} from "./tickDuration"; /** * Used to calculate the offset and transposition of a note to be pasted @@ -18,8 +19,10 @@ import { getId } from '../data/common'; export interface PasteNote { note: SmoNote, selector: SmoSelector, - originalKey: string + originalKey: string, + tupletStart: SmoTupletTree | null } + /** * Used when pasting staff modifiers like slurs to calculate the * offset @@ -36,19 +39,20 @@ export interface ModifierPlacement { */ export class PasteBuffer { notes: PasteNote[]; + totalDuration: number; noteIndex: number; measures: SmoMeasure[]; measureIndex: number; remainder: number; replacementMeasures: SmoSelection[]; score: SmoScore | null = null; - tupletNoteMap: Record = { }; modifiers: StaffModifierBase[] = []; modifiersToPlace: ModifierPlacement[] = []; destination: SmoSelector = SmoSelector.default; staffSelectors: SmoSelector[] = []; constructor() { this.notes = []; + this.totalDuration = 0; this.noteIndex = 0; this.measures = []; this.measureIndex = -1; @@ -66,23 +70,22 @@ export class PasteBuffer { if (selections.length < 1) { return; } - this.tupletNoteMap = {}; + // this.tupletNoteMap = []; const first = selections[0]; const last = selections[selections.length - 1]; if (!first.note || !last.note) { return; } - - const startTuplet: SmoTuplet | null = first.measure.getTupletForNote(first.note); - if (startTuplet) { - if (startTuplet.getIndexOfNote(first.note) !== 0) { - return; // can't paste from the middle of a tuplet + const startTupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(first.measure.tupletTrees, first.selector.voice, first.selector.tick); + if (startTupletTree) { + if (startTupletTree.startIndex !== first.selector.tick) { + return; // can't copy from the middle of a tuplet } } - const endTuplet: SmoTuplet | null = last.measure.getTupletForNote(last.note); - if (endTuplet) { - if (endTuplet.getIndexOfNote(last.note) !== endTuplet.notes.length - 1) { - return; // can't paste part of a tuplet. + const endTupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(last.measure.tupletTrees, last.selector.voice, last.selector.tick); + if (endTupletTree) { + if (endTupletTree.endIndex !== last.selector.tick) { + return; // can't copy part of a tuplet. } } this._populateSelectArray(selections); @@ -102,31 +105,30 @@ export class PasteBuffer { this.modifiers.push(cp); }); } - const isTuplet: boolean = selection?.note?.isTuplet ?? false; - // We store copy in concert pitch. The originalKey is the original key of the copy. - // the destKey is the originalKey in concert pitch. - const originalKey = selection.measure.keySignature; - const keyOffset = -1 * selection.measure.transposeIndex; - const destKey = SmoMusic.vexKeySignatureTranspose(originalKey, keyOffset).toLocaleLowerCase(); - if (isTuplet) { - const tuplet = (selection.measure.getTupletForNote(selection.note) as SmoTuplet); - const index = tuplet.getIndexOfNote(selection.note); - if (index === 0) { - const tupletNotes = tuplet.notes; - const ntuplet = SmoTuplet.cloneTuplet(tuplet, tupletNotes); - this.tupletNoteMap[ntuplet.id] = ntuplet; - ntuplet.notes.forEach((nnote) => { - const xposeNote = SmoNote.transpose(SmoNote.clone(nnote), - [], -1 * selection.measure.transposeIndex, selection.measure.keySignature, destKey) as SmoNote; - this.notes.push({ selector, note: xposeNote, originalKey: destKey }); - selector = JSON.parse(JSON.stringify(selector)); - selector.tick += 1; - }); + + if (selection.note) { + // We store copy in concert pitch. The originalKey is the original key of the copy. + // the destKey is the originalKey in concert pitch. + const originalKey = selection.measure.keySignature; + const keyOffset = -1 * selection.measure.transposeIndex; + const destKey = SmoMusic.vexKeySignatureTranspose(originalKey, keyOffset).toLocaleLowerCase(); + const note = SmoNote.transpose(SmoNote.clone(selection.note),[], keyOffset, selection.measure.keySignature, destKey) as SmoNote; + const pasteNote: PasteNote = { + selector, + note, + originalKey: destKey, + tupletStart: null + }; + if (selection.note.isTuplet) { + const tupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(selection.measure.tupletTrees, selection.selector.voice, selection.selector.tick); + //const index = tuplet.getIndexOfNote(selection.note); + if (tupletTree && tupletTree.startIndex === selection.selector.tick) { + pasteNote.tupletStart = SmoTupletTree.clone(tupletTree); + } } - } else if (selection.note) { - const note = SmoNote.transpose(SmoNote.clone(selection.note), - [], keyOffset, selection.measure.keySignature, destKey) as SmoNote; - this.notes.push({ selector, note, originalKey: destKey }); + + this.notes.push(pasteNote); + this.totalDuration += note.tickCount; } }); this.notes.sort((a, b) => @@ -147,45 +149,88 @@ export class PasteBuffer { return (typeof(rv) !== 'undefined' && rv.length) ? rv[0] : null; } - // ### _populateMeasureArray + _alignVoices(measure: SmoMeasure, voiceIndex: number) { + while (measure.voices.length <= voiceIndex) { + measure.populateVoice(measure.voices.length); + } + } + // Before pasting, populate an array of existing measures from the paste destination // so we know how to place the notes. - _populateMeasureArray() { - if (!this.score || !this.destination) { - return; - } - let measureSelection = SmoSelection.measureSelection(this.score, this.destination.staff, this.destination.measure); + _populateMeasureArray(selector: SmoSelector) { + let measureSelection = SmoSelection.measureSelection(this.score!, selector.staff, selector.measure); if (!measureSelection) { return; } const measure = measureSelection.measure; - while (measure.voices.length <= this.destination.voice) { - measure.populateVoice(measure.voices.length); - } - const tickmap = measure.tickmapForVoice(this.destination.voice); - let currentDuration = tickmap.durationMap[this.destination.tick]; + this._alignVoices(measure, selector.voice); this.measures = []; this.staffSelectors = []; - this.measures.push(measure); - this.notes.forEach((selection: PasteNote) => { - if (currentDuration + selection.note.tickCount > tickmap.totalDuration && measureSelection !== null) { + const clonedMeasure = SmoMeasure.clone(measureSelection.measure); + clonedMeasure.svg = measureSelection.measure.svg; + this.measures.push(clonedMeasure); + + const firstMeasure = this.measures[0]; + const tickmapForFirstMeasure = firstMeasure.tickmapForVoice(selector.voice); + + let currentDuration = tickmapForFirstMeasure.durationMap[selector.tick]; + const measureTotalDuration = tickmapForFirstMeasure.totalDuration; + for (let i: number = 0; i < this.notes.length; i++) { + const selection: PasteNote = this.notes[i]; + if (selection.tupletStart) { + // const tupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(this.tupletNoteMap, selection.selector.voice, selection.selector.tick); + if (currentDuration + selection.tupletStart.totalTicks > measureTotalDuration && measureSelection !== null) { + //if tuplet does not fit in a measure as a whole we cannot paste it, it is ether the whole thing or nothing + //reset everything that has been changed so far and return + this.measures = []; + this.staffSelectors = []; + return; + } + } + if (currentDuration + selection.note.tickCount > measureTotalDuration && measureSelection !== null) { // If this note will overlap the measure boundary, the note will be split in 2 with the // remainder going to the next measure. If they line up exactly, the remainder is 0. - const remainder = (currentDuration + selection.note.tickCount) - tickmap.totalDuration; + const remainder = (currentDuration + selection.note.tickCount) - measureTotalDuration; currentDuration = remainder; - measureSelection = SmoSelection.measureSelection(this.score as SmoScore, - measureSelection.selector.staff, - measureSelection.selector.measure + 1); + measureSelection = SmoSelection.measureSelection(this.score as SmoScore, measureSelection.selector.staff,measureSelection.selector.measure + 1); // If the paste buffer overlaps the end of the score, we can't paste (TODO: add a measure in this case) if (measureSelection != null) { - this.measures.push(measureSelection.measure); + const clonedMeasure = SmoMeasure.clone(measureSelection.measure); + clonedMeasure.svg = measureSelection.measure.svg; + this.measures.push(clonedMeasure); + // firstMeasureTickmap = measureSelection.measure.tickmapForVoice(selector.voice); } } else if (measureSelection != null) { currentDuration += selection.note.tickCount; } - }); + } + + const lastMeasure = this.measures[this.measures.length - 1]; + + //adjust the beginning of the paste + //adjust this.destination if beginning of the paste is in the middle of a tuplet + //set destination to have a tick index of the first note in the tuplet + this.destination = selector; + const firstTupletTree = SmoTupletTree.getTupletForNoteIndex(firstMeasure.tupletTrees, selector.voice, selector.tick); + if (firstTupletTree) { + this.destination.tick = firstTupletTree.startIndex;//use this as a new selector.tick + } + + if (this.measures.length > 1) { + this._removeOverlappingTuplets(firstMeasure, selector.tick, firstMeasure.voices[selector.voice].notes.length - 1, selector.voice); + this._removeOverlappingTuplets(lastMeasure, 0, lastMeasure.getClosestIndexFromTickCount(selector.voice, currentDuration), selector.voice); + } else { + this._removeOverlappingTuplets(firstMeasure, selector.tick, lastMeasure.getClosestIndexFromTickCount(selector.voice, currentDuration), selector.voice); + } + + //if there are more than 2 measures remove tuplets from all but first and last measure. + if (this.measures.length > 2) { + for(let i = 1; i < this.measures.length - 2; i++) { + this.measures[i].tupletTrees = []; + } + } } // ### _populatePre @@ -194,55 +239,28 @@ export class PasteBuffer { const voice: SmoVoice = { notes: [] }; - let i = 0; - let j = 0; - let ticksToFill = tickmap.durationMap[startTick]; - // TODO: bug here, need to handle tuplets in pre-part, create new tuplet - for (i = 0; i < measure.voices[voiceIndex].notes.length; ++i) { + + for (let i = 0; i < startTick; i++) { const note = measure.voices[voiceIndex].notes[i]; - // If this is a tuplet, clone all the notes at once. - if (note.isTuplet && ticksToFill >= note.tickCount) { - const tuplet = measure.getTupletForNote(note); - if (!tuplet) { - continue; // we remove the tuplet after first iteration - } - const ntuplet: SmoTuplet = SmoTuplet.cloneTuplet(tuplet, tuplet.notes); - voice.notes = voice.notes.concat(ntuplet.notes as SmoNote[]); - measure.removeTupletForNote(note); - measure.tuplets.push(ntuplet); - ticksToFill -= tuplet.tickCount; - } else if (ticksToFill >= note.tickCount) { - ticksToFill -= note.tickCount; - voice.notes.push(SmoNote.clone(note)); - } else { - const duration = note.tickCount - ticksToFill; - const durMap = SmoMusic.gcdMap(duration); - for (j = 0; j < durMap.length; ++j) { - const dd = durMap[j]; - SmoNote.cloneWithDuration(note, { - numerator: dd, - denominator: 1, - remainder: 0 - }); - } - ticksToFill = 0; - } - if (ticksToFill < 1) { - break; - } + voice.notes.push(SmoNote.clone(note)); } + return voice; } + /** + * + * @param voiceIndex + */ // ### _populateVoice // ### Description: // Create a new voice for a new measure in the paste destination - _populateVoice(voiceIndex: number): SmoVoice[] { - this._populateMeasureArray(); + _populateVoice(): SmoVoice[] { + // this._populateMeasureArray(); const measures = this.measures; let measure = measures[0]; let tickmap = measure.tickmapForVoice(this.destination.voice); - let voice = this._populatePre(voiceIndex, measure, this.destination.tick, tickmap); + let voice = this._populatePre(this.destination.voice, measure, this.destination.tick, tickmap); let startSelector = JSON.parse(JSON.stringify(this.destination)); this.measureIndex = 0; const measureVoices = []; @@ -263,7 +281,7 @@ export class PasteBuffer { startSelector = { staff: startSelector.staff, measure: startSelector.measure, - voice: voiceIndex, + voice: this.destination.voice, tick: 0 }; this.measureIndex += 1; @@ -272,7 +290,7 @@ export class PasteBuffer { break; } } - this._populatePost(voice, voiceIndex, measure, tickmap); + this._populatePost(voice, this.destination.voice, measure, tickmap); return measureVoices; } @@ -308,21 +326,39 @@ export class PasteBuffer { }); } } + /** - * Figure out if the tuplet overlaps an existing tuplet in the target measure - * @param t1 - * @param measure - * @returns + * + * @param measure + * @param startIndex + * @param endIndex + * @param voiceIndex + * @private */ - static tupletOverlapIndex(t1: SmoTuplet, measure: SmoMeasure) { - for (var i = 0; i < measure.tuplets.length; ++i) { - const tt = measure.tuplets[i]; - // TODO: what about other kinds of overlap? - if (tt.startIndex === t1.startIndex) { - return i; + private _removeOverlappingTuplets(measure: SmoMeasure, startIndex: number, endIndex: number, voiceIndex: number): void { + const tupletsToDelete: SmoTupletTree[] = []; + for (let i = 0; i < measure.tupletTrees.length; ++i) { + const tupletTree = measure.tupletTrees[i]; + if (startIndex >= tupletTree.startIndex && startIndex <= tupletTree.endIndex) { + tupletsToDelete.push(tupletTree); + break; + } + if (endIndex >= tupletTree.startIndex && endIndex <= tupletTree.endIndex) { + tupletsToDelete.push(tupletTree); + break; } } - return -1; + + //todo: check if we need to remove tuplets in descending order + for (let i: number = 0; i < tupletsToDelete.length; i++) { + const tupletTree: SmoTupletTree = tupletsToDelete[i]; + SmoUnmakeTupletActor.apply({ + startIndex: tupletTree.startIndex, + endIndex: tupletTree.endIndex, + measure: measure, + voice: voiceIndex + }); + } } /** * Start copying the paste buffer into the destination by copying the notes and working out @@ -343,8 +379,8 @@ export class PasteBuffer { if (!this.score) { return; } - const selection = this.notes[this.noteIndex]; - const note = selection.note; + const selection: PasteNote = this.notes[this.noteIndex]; + const note: SmoNote = selection.note; if (note.noteType === 'n') { const pitchAr: number[] = []; note.pitches.forEach((pitch, ix) => { @@ -353,28 +389,20 @@ export class PasteBuffer { SmoNote.transpose(note, pitchAr, measure.transposeIndex, selection.originalKey, measure.keySignature); } this._populateModifier(selection.selector, startSelector, this.score.staves[selection.selector.staff]); - if (note.isTuplet && note.tupletId) { - const tuplet = this.tupletNoteMap[note.tupletId]; - const ntuplet = SmoTuplet.cloneTuplet(tuplet, tuplet.notes); - ntuplet.startIndex = voice.notes.length; - this.noteIndex += ntuplet.notes.length; - startSelector.tick += ntuplet.notes.length; - currentDuration += tuplet.tickCount; - for (i = 0; i < ntuplet.notes.length; ++i) { - const tn = ntuplet.notes[i]; - tn.clef = measure.clef; - voice.notes.push(tn); - } - const tix = PasteBuffer.tupletOverlapIndex(ntuplet, measure); - // If this is overlapping an existing tuplet in the target measure, replace it - if (tix >= 0) { - measure.tuplets[tix] = ntuplet; - } else { - measure.tuplets.push(ntuplet); - } - } else if (currentDuration + note.tickCount <= totalDuration && this.remainder === 0) { + + if (currentDuration + note.tickCount <= totalDuration && this.remainder === 0) { // The whole note fits in the measure, paste it. - const nnote = SmoNote.clone(note); + //If this note is a tuplet, and specifically if it is the beginning of a tuplet, we need to handle it + //NOTE: tuplets never cross measure boundary, we made sure this is handled here: @see this._populateMeasureArray() + if (selection.tupletStart) { + const tupletTree: SmoTupletTree = SmoTupletTree.clone(selection.tupletStart); + const startIndex: number = voice.notes.length; + const diff: number = startIndex - tupletTree.startIndex; + SmoTupletTree.adjustTupletIndexes([tupletTree], selection.selector.voice,-1, diff); + measure.tupletTrees.push(tupletTree); + } + + const nnote: SmoNote = SmoNote.clone(note); nnote.clef = measure.clef; voice.notes.push(nnote); currentDuration += note.tickCount; @@ -418,47 +446,41 @@ export class PasteBuffer { // When we paste, we replace entire measures. Populate the last measure from the end of paste to the // end of the measure with notes in the existing measure. _populatePost(voice: SmoVoice, voiceIndex: number, measure: SmoMeasure, tickmap: TickMap) { - let startTicks = PasteBuffer._countTicks(voice); - let existingIndex = 0; - const totalDuration = tickmap.totalDuration; - while (startTicks < totalDuration) { - // Find the point in the music where the paste area runs out, or as close as we can get. - existingIndex = tickmap.durationMap.indexOf(startTicks); - existingIndex = (existingIndex < 0) ? measure.voices[voiceIndex].notes.length - 1 : existingIndex; + let endOfPasteDuration = PasteBuffer._countTicks(voice); + let existingIndex = measure.getClosestIndexFromTickCount(voiceIndex, endOfPasteDuration); + if (existingIndex > tickmap.durationMap.length - 1) { + return; + } + let existingDuration = tickmap.durationMap[existingIndex]; + let endOfExistingDuration = existingDuration + tickmap.deltaMap[existingIndex]; + + let startIndexToAdjustRemainingTuplets = voice.notes.length; + let diffToAdjustRemainingTuplets: number = startIndexToAdjustRemainingTuplets - existingIndex - 1; + + + if (Math.round(endOfPasteDuration) < Math.round(endOfExistingDuration)) { + //pasted notes ended somewhere in the middle of an existing note + //we need to remove the existing note and fill in the difference between the end of our pasted note and beginning of the next one const note = measure.voices[voiceIndex].notes[existingIndex]; - if (note.isTuplet) { - const tuplet = measure.getTupletForNote(note); - if (!tuplet) { - throw 'bad tuplet in copy paste'; - } - const ntuplet = SmoTuplet.cloneTuplet(tuplet, tuplet.notes); - startTicks += tuplet.tickCount; + const lmap = SmoMusic.gcdMap(endOfExistingDuration - endOfPasteDuration); + lmap.forEach((stemTick) => { + const nnote = SmoNote.cloneWithDuration(note, stemTick); + voice.notes.push(nnote); + }); + diffToAdjustRemainingTuplets += lmap.length; + existingIndex++; + } + SmoTupletTree.adjustTupletIndexes(measure.tupletTrees, voiceIndex, startIndexToAdjustRemainingTuplets, diffToAdjustRemainingTuplets); - voice.notes = voice.notes.concat(ntuplet.notes); - measure.tuplets.push(ntuplet); - measure.removeTupletForNote(note); - } else { - const ticksLeft = totalDuration - startTicks; - if (ticksLeft >= note.tickCount) { - startTicks += note.tickCount; - voice.notes.push(SmoNote.clone(note)); - } else { - const remainder = totalDuration - startTicks; - voice.notes.push(SmoNote.cloneWithDuration(note, { - numerator: remainder, - denominator: 1, - remainder: 0 - })); - startTicks = totalDuration; - } - } + for (let i = existingIndex; i < measure.voices[voiceIndex].notes.length - 1; i++) { + voice.notes.push(SmoNote.clone(measure.voices[voiceIndex].notes[i])); } } - _pasteVoiceSer(ser: any, vobj: any, voiceIx: number) { + _pasteVoiceSer(serializedMeasure: any, vobj: any, voiceIx: number) { const voices: any[] = []; - let ix = 0; - ser.voices.forEach((vc: any) => { + let ix = 0; + serializedMeasure.voices.forEach((vc: any) => { if (ix !== voiceIx) { voices.push(vc); } else { @@ -467,10 +489,10 @@ export class PasteBuffer { ix += 1; }); // If we are pasting into a measure that doesn't contain this voice, add the voice - if (ser.voices.length <= voiceIx) { + if (serializedMeasure.voices.length <= voiceIx) { voices.push(vobj); } - ser.voices = voices; + serializedMeasure.voices = voices; } pasteSelections(selector: SmoSelector) { @@ -478,30 +500,34 @@ export class PasteBuffer { if (this.notes.length < 1) { return; } + if (!this.score) { + return; + } const maxCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); const minCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); const backupNotes: PasteNote[] = []; this.notes.forEach((bb) => { const note = (SmoNote.deserialize(bb.note.serialize())); const selector = JSON.parse(JSON.stringify(bb.selector)); - backupNotes.push({ note, selector, originalKey: bb.originalKey }); + let tupletStart = bb.tupletStart; + if (tupletStart) { + tupletStart = SmoTupletTree.deserialize(bb.tupletStart!.serialize()); + } + backupNotes.push({ note, selector, originalKey: bb.originalKey, tupletStart }); }); - this.destination = selector; - if (minCutVoice === maxCutVoice && minCutVoice > this.destination.voice) { - this.destination.voice = minCutVoice; - + if (minCutVoice === maxCutVoice && minCutVoice > selector.voice) { + selector.voice = minCutVoice; } this.modifiersToPlace = []; - if (this.notes.length < 1) { - return; - } - if (!this.score) { - return; - } this.noteIndex = 0; this.measureIndex = -1; this.remainder = 0; - const voices = this._populateVoice(this.destination.voice); + this._populateMeasureArray(selector); + if (this.measures.length === 0) { + return; + } + + const voices = this._populateVoice(); const measureSel = JSON.parse(JSON.stringify(this.destination)); const selectors: SmoSelector[] = []; for (i = 0; i < this.measures.length && i < voices.length; ++i) { diff --git a/src/smo/xform/operations.ts b/src/smo/xform/operations.ts index 4d04d04e..837197f5 100644 --- a/src/smo/xform/operations.ts +++ b/src/smo/xform/operations.ts @@ -19,11 +19,9 @@ import { SmoStaffHairpin, SmoSlur, SmoTie, StaffModifierBase, SmoTieParams, SmoI import { SmoSystemGroup } from '../data/scoreModifiers'; import { SmoTextGroup } from '../data/scoreText'; import { SmoSelection, SmoSelector, ModifierTab } from './selections'; -import { - SmoDuration, SmoContractNoteActor, SmoStretchNoteActor, SmoMakeTupletActor, - SmoUnmakeTupletActor, SmoContractTupletActor -} from './tickDuration'; +import { SmoContractNoteActor, SmoStretchNoteActor, SmoMakeTupletActor, SmoUnmakeTupletActor, SmoStretchNoteParams, SmoContractNoteParams, SmoMakeTupletParams} from './tickDuration'; import { SmoBeamer } from './beamers'; +import { SmoTupletTree } from '../data/tuplet'; /** * supported operations for {@link SmoOperation.batchSelectionOperation} to change a note's duration */ @@ -178,13 +176,15 @@ export class SmoOperation { // note, if possible. Works on tuplets also. static doubleDuration(selection: SmoSelection) { const note = selection.note; - const measure = selection.measure; - const tuplet = measure.getTupletForNote(note); - if (!tuplet) { - SmoDuration.doubleDurationNonTuplet(selection); - } else { - SmoDuration.doubleDurationTuplet(selection); - } + const newStemTicks = note!.stemTicks * 2; + + SmoStretchNoteActor.apply({ + startIndex: selection.selector.tick, + measure: selection.measure, + voice: selection.selector.voice, + newStemTicks: newStemTicks + }); + return true; } @@ -196,49 +196,27 @@ export class SmoOperation { const note = selection.note as SmoNote; let divisor = 2; const measure = selection.measure; - const tuplet = measure.getTupletForNote(note); - if (measure.timeSignature.actualBeats % 3 === 0 && note.tickCount === 6144) { - // special behavior, if this is dotted 1/4 in 6/8, split to 3 - divisor = 3; - } - if (!tuplet) { - const nticks = note.tickCount / divisor; - if (!SmoMusic.validDurations[nticks]) { - return; - } - SmoContractNoteActor.apply({ - startIndex: selection.selector.tick, - measure: selection.measure, - voice: selection.selector.voice, - newTicks: nticks - }); - SmoBeamer.applyBeams(measure); - } else { - const startIndex = measure.tupletIndex(tuplet) + tuplet.getIndexOfNote(note); - SmoContractTupletActor.apply({ - changeIndex: startIndex, - measure, - voice: selection.selector.voice - }); - } + const newStemTicks = note.stemTicks / divisor; + + SmoContractNoteActor.apply({ + startIndex: selection.selector.tick, + measure: measure, + voice: selection.selector.voice, + newStemTicks: newStemTicks + }); + SmoBeamer.applyBeams(measure); + return true; } // ## makeTuplet // ## Description // Makes a non-tuplet into a tuplet of equal value. static makeTuplet(selection: SmoSelection, numNotes: number) { - const note = selection.note as SmoNote; - const measure = selection.measure; - if (measure.getTupletForNote(note)) { - return; - } - const nticks = note.tickCount; SmoMakeTupletActor.apply({ - index: selection.selector.tick, - totalTicks: nticks, - numNotes, measure: selection.measure, - voice: selection.selector.voice + numNotes: numNotes, + voice: selection.selector.voice, + index: selection.selector.tick }); } static addStaffModifier(selection: SmoSelection, modifier: StaffModifierBase) { @@ -343,23 +321,20 @@ export class SmoOperation { // ## Description // Makes a tuplet into a single with the duration of the whole tuplet static unmakeTuplet(selection: SmoSelection) { - const note = selection.note; + const selector = selection.selector; const measure = selection.measure; - if (!measure.getTupletForNote(note)) { - return; - } - const tuplet = measure.getTupletForNote(note); - if (tuplet === null) { + + const tuplets = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, selector.voice, selector.tick); + if (!tuplets.length) { return; } - const startIndex = measure.tupletIndex(tuplet); - const endIndex = tuplet.notes.length + startIndex - 1; + const tuplet = tuplets[0]; SmoUnmakeTupletActor.apply({ - startIndex, - endIndex, - measure, - voice: selection.selector.voice + startIndex: tuplet.startIndex, + endIndex: tuplet.endIndex, + measure: measure, + voice: selector.voice }); } @@ -370,14 +345,14 @@ export class SmoOperation { static dotDuration(selection: SmoSelection) { const note = selection.note as SmoNote; const measure = selection.measure; - const nticks = SmoMusic.getNextDottedLevel(note.tickCount); - if (nticks === note.tickCount) { + const newStemTicks = SmoMusic.getNextDottedLevel(note.stemTicks); + if (newStemTicks === note.stemTicks) { return; } // Don't dot if the thing on the right of the . is too small - const dotCount = SmoMusic.smoTicksToVexDots(nticks); + const dotCount = SmoMusic.smoTicksToVexDots(newStemTicks); const multiplier = Math.pow(2, dotCount); - const baseDot = SmoMusic.closestDurationTickLtEq(nticks) / (multiplier * 2); + const baseDot = SmoMusic.closestDurationTickLtEq(newStemTicks) / (multiplier * 2); if (baseDot <= 128) { return; } @@ -385,19 +360,20 @@ export class SmoOperation { if (selection.selector.tick + 1 === selection.measure.voices[selection.selector.voice].notes.length) { return; } - if (selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].tickCount > note.tickCount) { + if (selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].stemTicks > note.stemTicks) { console.log('too long'); return; } // is dot too short? - if (!SmoMusic.validDurations[selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].tickCount / 2]) { + if (!SmoMusic.validDurations[selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].stemTicks / 2]) { return; } + SmoStretchNoteActor.apply({ startIndex: selection.selector.tick, - measure, + measure: measure, voice: selection.selector.voice, - newTicks: nticks + newStemTicks: newStemTicks }); } @@ -408,15 +384,16 @@ export class SmoOperation { static undotDuration(selection: SmoSelection) { const note = selection.note as SmoNote; const measure = selection.measure; - const nticks = SmoMusic.getPreviousDottedLevel(note.tickCount); - if (nticks === note.tickCount) { + const newStemTicks = SmoMusic.getPreviousDottedLevel(note.stemTicks); + if (newStemTicks === note.stemTicks) { return; } + SmoContractNoteActor.apply({ startIndex: selection.selector.tick, - measure, + measure: measure, voice: selection.selector.voice, - newTicks: nticks + newStemTicks: newStemTicks }); } diff --git a/src/smo/xform/tickDuration.ts b/src/smo/xform/tickDuration.ts index b5e906ac..344653dc 100644 --- a/src/smo/xform/tickDuration.ts +++ b/src/smo/xform/tickDuration.ts @@ -1,9 +1,8 @@ // [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. -import { SmoNote } from '../data/note'; -import { SmoTuplet } from '../data/tuplet'; +import { SmoNote, TupletInfo } from '../data/note'; +import { SmoTuplet, SmoTupletTree } from '../data/tuplet'; import { SmoMusic } from '../data/music'; -import { SmoSelector, SmoSelection } from './selections'; import { SmoMeasure, SmoVoice } from '../data/measure'; import { Ticks } from '../data/common'; import { TickMap } from './tickMap'; @@ -21,112 +20,7 @@ export abstract class TickIteratorBase { return null; } } -/** - * SmoDuration: change the duration of a note, maybe at the expense of some - * other note. - * @category SmoTransform - */ -export class SmoDuration { - /** - * doubleDurationNonTuplet - * double the duration of the selection, consuming the next note or - * possibly split it in half and consume that. Simple operation so - * do it inline - * @param selection - * @returns - */ - static doubleDurationNonTuplet(selection: SmoSelection) { - const note: SmoNote | null = selection?.note; - const measure: SmoMeasure = selection.measure; - if (note === null) { - return; - } - const selector: SmoSelector = selection.selector; - const voices: SmoVoice[] | undefined = measure?.voices; - const voice: SmoVoice = voices[selector.voice]; - const notes: SmoNote[] = voice?.notes; - let i = 0; - const nticks: Ticks = { numerator: note.tickCount * 2, denominator: 1, remainder: 0 }; - const replNote = SmoNote.cloneWithDuration(note, nticks); - let ticksUsed = note.tickCount; - const newNotes = []; - for (i = 0; i < selector.tick; ++i) { - newNotes.push(notes[i]); - } - for (i = selector.tick + 1; i < notes.length; ++i) { - const nnote = notes[i]; - ticksUsed += nnote.tickCount; - if (ticksUsed >= nticks.numerator) { - break; - } - } - const remainder = ticksUsed - nticks.numerator; - if (remainder < 0) { - return; - } - newNotes.push(replNote); - if (remainder > 0) { - const lmap = SmoMusic.gcdMap(remainder); - lmap.forEach((duration) => { - newNotes.push(SmoNote.cloneWithDuration(note, duration)); - }); - } - for (i = i + 1; i < notes.length; ++i) { - newNotes.push(notes[i]); - } - // If any tuplets got removed while extending the notes, - voice.notes = newNotes; - const measureTuplets: SmoTuplet[] = []; - const allTuplets: SmoTuplet[] | undefined = measure?.tuplets; - allTuplets?.forEach((tuplet: SmoTuplet) => { - const testNotes = measure?.tupletNotes(tuplet); - if (testNotes?.length === tuplet.notes.length) { - measureTuplets.push(tuplet); - } - }); - measure.tuplets = measureTuplets; - } - - /** - * double duration, tuplet form. Increase the first selection and consume the - * following note. Also a simple operation - * @param selection - * @returns - */ - static doubleDurationTuplet(selection: SmoSelection) { - let i: number = 0; - const measure: SmoMeasure = selection.measure; - const note: SmoNote | null = selection?.note; - if (note === null) { - return; - } - const notes = measure.voices[selection.selector.voice].notes; - const tuplet: SmoTuplet | null = measure.getTupletForNote(note); - if (tuplet === null) { - return; - } - const startIndex = selection.selector.tick - tuplet.startIndex; - - const startLength: number = tuplet.notes.length; - tuplet.combine(startIndex, startIndex + 1); - if (tuplet.notes.length >= startLength) { - return; - } - const newNotes = []; - - for (i = 0; i < tuplet.startIndex; ++i) { - newNotes.push(notes[i]); - } - tuplet.notes.forEach((note) => { - newNotes.push(note); - }); - for (i = i + tuplet.notes.length + 1; i < notes.length; ++i) { - newNotes.push(notes[i]); - } - measure.voices[selection.selector.voice].notes = newNotes; - } -} /** * SmoTickIterator * this is a local helper class that follows a pattern of iterating of the notes. Most of the @@ -210,6 +104,7 @@ export class SmoTickIterator { return this.newNotes; } } + /** * used to create a contract/dilate operation on a note via {@link SmoContractNoteActor} * @category SmoTransform @@ -218,7 +113,7 @@ export interface SmoContractNoteParams { startIndex: number, measure: SmoMeasure, voice: number, - newTicks: number + newStemTicks: number } /** * Contract the duration of a note, filling in the space with another note @@ -227,8 +122,7 @@ export interface SmoContractNoteParams { * */ export class SmoContractNoteActor extends TickIteratorBase { startIndex: number; - tickmap: TickMap; - newTicks: number; + newStemTicks: number; measure: SmoMeasure; voice: number; constructor(params: SmoContractNoteParams) { @@ -236,8 +130,7 @@ export class SmoContractNoteActor extends TickIteratorBase { this.startIndex = params.startIndex; this.measure = params.measure; this.voice = params.voice; - this.tickmap = this.measure.tickmapForVoice(this.voice); - this.newTicks = params.newTicks; + this.newStemTicks = params.newStemTicks; } static apply(params: SmoContractNoteParams) { const actor = new SmoContractNoteActor(params); @@ -245,48 +138,45 @@ export class SmoContractNoteActor extends TickIteratorBase { actor, actor.voice); } iterateOverTick(note: SmoNote, tickmap: TickMap, index: number): SmoNote | SmoNote[] | null { - let i = 0; if (index === this.startIndex) { - const notes: SmoNote[] = []; - const noteCount = Math.floor(note.ticks.numerator / this.newTicks); - let remainder = note.ticks.numerator; - /** - * Replace 1 note with noteCOunt notes of newTIcks duration - * old map: - * d . d . . - * new map: - * d d d . . - */ - for (i = 0; i < noteCount; ++i) { - // first note, retain modifiers so clone. Otherwise just - // retain pitches - if (i === 0) { - const nn = SmoNote.clone(note); - nn.ticks = { numerator: this.newTicks, denominator: 1, remainder: 0 }; - notes.push(nn); - } else { - const nnote = new SmoNote(SmoNote.defaults); - nnote.clef = note.clef; - nnote.pitches = JSON.parse(JSON.stringify(note.pitches)); - nnote.ticks = { numerator: this.newTicks, denominator: 1, remainder: 0 }; - nnote.beamBeats = note.beamBeats; - notes.push(nnote); - } - remainder = remainder - this.newTicks; + let newTicks: Ticks = { numerator: this.newStemTicks, denominator: 1, remainder: 0 }; + const multiplier = note.tickCount / note.stemTicks; + + if (note.isTuplet) { + const numerator = this.newStemTicks * multiplier; + newTicks = { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }; } - // make sure remnainder is not too short - if (remainder > 0) { - if (remainder < 128) { + const replacingNote = SmoNote.cloneWithDuration(note, newTicks, this.newStemTicks); + const oldStemTicks = note.stemTicks; + const notes = []; + const remainderStemTicks = oldStemTicks - this.newStemTicks; + + notes.push(replacingNote); + + if (remainderStemTicks > 0) { + if (remainderStemTicks < 128) { return null; } - const nnote = new SmoNote(SmoNote.defaults); - nnote.clef = note.clef; - nnote.pitches = JSON.parse(JSON.stringify(note.pitches)); - nnote.ticks = { numerator: remainder, denominator: 1, remainder: 0 }; - nnote.beamBeats = note.beamBeats; - notes.push(nnote); + const lmap = SmoMusic.gcdMap(remainderStemTicks); + + lmap.forEach((stemTick) => { + const numerator = stemTick * multiplier; + const nnote = SmoNote.cloneWithDuration(note, {numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1}, stemTick); + notes.push(nnote); + }); } + //accumulate all remainders in the first note + let remainder: number = 0; + notes.forEach((note: SmoNote) => { + if (note.ticks.remainder > 0) { + remainder += note.ticks.remainder; + note.ticks.remainder = 0; + } + }); + notes[0].ticks.numerator += Math.round(remainder); + + SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, index, notes.length - 1); return notes; } return null; @@ -294,120 +184,120 @@ export class SmoContractNoteActor extends TickIteratorBase { } /** - * used to create a contract/dilate operation on a note via {@link SmoContractTupletActor} + * Constructor when we want to double or dot the duration of a note (stretch) + * for {@link SmoStretchNoteActor} + * @param startIndex tick index into the measure + * @param measure the container measure + * @param voice the voice index + * @param newTicks the ticks the new note will take up * @category SmoTransform */ -export interface SmoContractTupletParams { - changeIndex: number, +export interface SmoStretchNoteParams { + startIndex: number, measure: SmoMeasure, - voice: number + voice: number, + newStemTicks: number } /** - * Shrink the duration of a note in a tuplet by creating additional notes + * increase the length of a note, removing future notes in the measure as required * @category SmoTransform */ -export class SmoContractTupletActor extends TickIteratorBase { - changeIndex: number; +export class SmoStretchNoteActor extends TickIteratorBase { + startIndex: number; + newStemTicks: number; measure: SmoMeasure; voice: number; - tuplet: SmoTuplet | null; - oldLength: number = 0; - tupletIndex: number = 0; - splitIndex: number = 0; - constructor(params: SmoContractTupletParams) { + notes: SmoNote[]; + notesToInsert: SmoNote[] = []; + numberOfNotesToDelete: number = 0; + constructor(params: SmoStretchNoteParams) { super(); - this.changeIndex = params.changeIndex; + this.startIndex = params.startIndex; this.measure = params.measure; this.voice = params.voice; - this.tuplet = this.measure.getTupletForNote(this.measure.voices[this.voice].notes[this.changeIndex]); - if (this.tuplet === null) { - return; + this.newStemTicks = params.newStemTicks; + this.notes = this.measure.voices[this.voice].notes; + + const originalNote: SmoNote = this.notes[this.startIndex]; + let newTicks: Ticks = { numerator: this.newStemTicks, denominator: 1, remainder: 0 }; + const multiplier = originalNote.tickCount / originalNote.stemTicks; + if (originalNote.isTuplet) { + const numerator = this.newStemTicks * multiplier; + newTicks = { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 }; + } + + const replacingNote = SmoNote.cloneWithDuration(originalNote, newTicks, this.newStemTicks); + + let stemTicksUsed = originalNote.stemTicks; + for (let i = this.startIndex + 1; i < this.notes.length; ++i) { + const nnote = this.notes[i]; + //in case notes are part of the tuplet they need to belong to the same tuplet + //this check is only temporarely here, it should never come to this + if (nnote.isTuplet && !this.areNotesInSameTuplet(originalNote, nnote)) { + break; + } + stemTicksUsed += nnote.stemTicks; + ++this.numberOfNotesToDelete; + if (stemTicksUsed >= this.newStemTicks) { + break; + } + } + const remainingAmount = stemTicksUsed - this.newStemTicks; + if (remainingAmount >= 0) { + this.notesToInsert.push(replacingNote); + const lmap = SmoMusic.gcdMap(remainingAmount); + lmap.forEach((stemTick) => { + const numerator = stemTick * multiplier; + const nnote = SmoNote.cloneWithDuration(originalNote, {numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1}, stemTick) + this.notesToInsert.push(nnote); + }); + const noteCountDiff = (this.notesToInsert.length - this.numberOfNotesToDelete) - 1; + SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, this.startIndex, noteCountDiff); + + //accumulate all remainders in the first note + let remainder: number = 0; + this.notesToInsert.forEach((note: SmoNote) => { + if (note.ticks.remainder > 0) { + remainder += note.ticks.remainder; + note.ticks.remainder = 0; + } + }); + this.notesToInsert[0].ticks.numerator += Math.round(remainder); + } - this.oldLength = this.tuplet.notes.length; - this.tupletIndex = this.measure.tupletIndex(this.tuplet); - this.splitIndex = this.changeIndex - this.tupletIndex; - this.tuplet.split(this.splitIndex); } - static apply(params: SmoContractTupletParams) { - const actor = new SmoContractTupletActor(params); + static apply(params: SmoStretchNoteParams) { + const actor = new SmoStretchNoteActor(params); SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); } iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { - if (this.tuplet === null) { - return null; - } - if (index < this.tupletIndex) { - return note; - } - if (index >= this.tupletIndex + this.oldLength) { - return note; - } - if (index === this.changeIndex) { - return this.tuplet.notes; - } - return []; + if (this.startIndex === index && this.notesToInsert.length) { + return this.notesToInsert; + } else if (index > this.startIndex && this.numberOfNotesToDelete > 0) { + --this.numberOfNotesToDelete; + return []; + } + return null; } -} -/** - * Constructor params for {@link SmoUnmakeTupletActor} - * @category SmoTransform - */ -export interface SmoUnmakeTupletParams { - startIndex: number, - endIndex: number, - measure: SmoMeasure, - voice: number -} -/** - * Convert a tuplet into a single note that takes up the whole duration - * @category SmoTransform - */ -export class SmoUnmakeTupletActor extends TickIteratorBase { - startIndex: number = 0; - endIndex: number = 0; - measure: SmoMeasure; - voice: number; - constructor(parameters: SmoUnmakeTupletParams) { - super(); - this.startIndex = parameters.startIndex; - this.endIndex = parameters.endIndex; - this.measure = parameters.measure; - this.voice = parameters.voice; - } - static apply(params: SmoUnmakeTupletParams) { - const actor = new SmoUnmakeTupletActor(params); - SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); - } - iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { - if (index < this.startIndex || index > this.endIndex) { - return null; - } - if (index === this.startIndex) { - const tuplet = this.measure.getTupletForNote(note); - if (tuplet === null) { - return []; - } - const ticks = tuplet.totalTicks; - const nn: SmoNote = SmoNote.cloneWithDuration(note, { numerator: ticks, denominator: 1, remainder: 0 }); - nn.tupletId = null; - this.measure.removeTupletForNote(note); - return [nn]; + private areNotesInSameTuplet(noteOne: SmoNote, noteTwo: SmoNote): boolean { + if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tupletId == noteTwo.tupletId) { + return true; } - return []; + return false; } } + /** * constructor parameters for {@link SmoMakeTupletActor} * @category SmoTransform */ export interface SmoMakeTupletParams { - index: number, - totalTicks: number, - numNotes: number, measure: SmoMeasure, - voice: number + numNotes: number, + voice: number, + index: number } /** * Turn a tuplet into a non-tuplet of the same length @@ -416,189 +306,129 @@ export interface SmoMakeTupletParams { * */ export class SmoMakeTupletActor extends TickIteratorBase { measure: SmoMeasure; - durationMap: number[]; numNotes: number; - stemTicks: number; - totalTicks: number; - rangeToSkip: number[]; - tuplet: SmoNote[]; voice: number; index: number; + constructor(params: SmoMakeTupletParams) { - let i = 0; super(); this.measure = params.measure; - this.numNotes = params.numNotes; - this.durationMap = []; - this.totalTicks = params.totalTicks; - this.voice = params.voice; this.index = params.index; - for (i = 0; i < this.numNotes; ++i) { - this.durationMap.push(1.0); - } - this.stemTicks = SmoTuplet.calculateStemTicks(this.totalTicks, this.numNotes); - this.rangeToSkip = this._rangeToSkip(); - this.tuplet = []; + this.voice = params.voice; + this.numNotes = params.numNotes; } static apply(params: SmoMakeTupletParams) { const actor = new SmoMakeTupletActor(params); SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); } - _rangeToSkip(): number[] { - let i = 0; - if (this.measure === null) { - return []; - } - const ticks = this.measure.tickmapForVoice(this.voice); - let accum = 0; - const rv = []; - rv.push(this.index); - for (i = 0; i < ticks.deltaMap.length; ++i) { - if (i >= this.index) { - accum += ticks.deltaMap[i]; - } - if (accum >= this.totalTicks) { - rv.push(i); - break; - } - } - return rv; - } + iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { - let i = 0; - // if our tuplet replaces this note, make sure we make it go away. - if (index > this.index && index <= this.rangeToSkip[1]) { - return []; - } if (this.measure === null) { return []; } if (index !== this.index) { return null; } - for (i = 0; i < this.numNotes; ++i) { - note = SmoNote.cloneWithDuration(note, { numerator: this.stemTicks, denominator: 1, remainder: 0 }); - // Don't clone modifiers, except for first one. - note.textModifiers = i === 0 ? note.textModifiers : []; - this.tuplet.push(note); - } + + this.measure.clearBeamGroups(); + const stemTicks = SmoTuplet.calculateStemTicks(note.stemTicks, this.numNotes); + const notesOccupied = note.stemTicks / stemTicks; + const tuplet = new SmoTuplet({ - notes: this.tuplet, - stemTicks: this.stemTicks, - totalTicks: this.totalTicks, + numNotes: this.numNotes, + notesOccupied: notesOccupied, + stemTicks: stemTicks, + totalTicks: note.tickCount, ratioed: false, bracketed: true, - startIndex: index, - durationMap: this.durationMap, - voice: tickmap.voice, - numNotes: this.numNotes + voice: this.voice, + startIndex: this.index, + endIndex: this.index, }); - this.measure.tuplets.push(tuplet); - return this.tuplet; + + const tupletNotes = this._generateNotesForTuplet(tuplet, note, stemTicks); + tuplet.endIndex += tupletNotes.length - 1; + + SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, index, tupletNotes.length - 1); + const parentTuplet: SmoTuplet | null = SmoTupletTree.getTupletForNoteIndex(this.measure.tupletTrees, this.voice, this.index); + if (parentTuplet === null) { + const tupletTree = new SmoTupletTree({tuplet: tuplet}); + this.measure.tupletTrees.push(tupletTree); + } else { + parentTuplet.childrenTuplets.push(tuplet); + } + + return tupletNotes; + } + + private _generateNotesForTuplet(tuplet: SmoTuplet, originalNote: SmoNote, stemTicks: number): SmoNote[] { + const totalTicks = originalNote.tickCount; + const tupletNotes: SmoNote[] = []; + const numerator = totalTicks / this.numNotes; + for (let i = 0; i < this.numNotes; ++i) { + const note: SmoNote = SmoNote.cloneWithDuration(originalNote, { numerator: Math.floor(numerator), denominator: 1, remainder: 0 }, stemTicks); + // Don't clone modifiers, except for first one. + note.textModifiers = i === 0 ? note.textModifiers : []; + note.tupletId = tuplet.attrs.id; + tupletNotes.push(note); + } + if (numerator % 1) { + tupletNotes[0].ticks.numerator += 1; + } + return tupletNotes; } } + /** - * Constructor when we want to double or dot the duration of a note (stretch) - * for {@link SmoStretchNoteActor} - * @param startIndex tick index into the measure - * @param measure the container measure - * @param voice the voice index - * @param newTicks the ticks the new note will take up + * Constructor params for {@link SmoUnmakeTupletActor} * @category SmoTransform */ -export interface SmoStretchNoteParams { +export interface SmoUnmakeTupletParams { startIndex: number, + endIndex: number, measure: SmoMeasure, - voice: number, - newTicks: number + voice: number } /** - * increase the length of a note, removing future notes in the measure as required + * Convert a tuplet into a single note that takes up the whole duration * @category SmoTransform */ -export class SmoStretchNoteActor extends TickIteratorBase { - startIndex: number; - tickmap: TickMap; - newTicks: number; - startTick: number; - divisor: number; - durationMap: number[]; - skipFromStart: number; - skipFromEnd: number; +export class SmoUnmakeTupletActor extends TickIteratorBase { + startIndex: number = 0; + endIndex: number = 0; measure: SmoMeasure; voice: number; - constructor(params: SmoStretchNoteParams) { - let mapIx = 0; - let i = 0; + constructor(parameters: SmoUnmakeTupletParams) { super(); - this.startIndex = params.startIndex; - this.measure = params.measure; - this.voice = params.voice; - this.tickmap = this.measure.tickmapForVoice(this.voice); - this.newTicks = params.newTicks; - this.startTick = this.tickmap.durationMap[this.startIndex]; - const currentTicks = this.tickmap.deltaMap[this.startIndex]; - const endTick = this.tickmap.durationMap[this.startIndex] + this.newTicks; - this.divisor = -1; - this.durationMap = []; - this.skipFromStart = this.startIndex + 1; - this.skipFromEnd = this.startIndex + 1; - this.durationMap.push(this.newTicks); - - mapIx = this.tickmap.durationMap.indexOf(endTick); - - const remaining = this.tickmap.deltaMap.slice(this.startIndex, this.tickmap.durationMap.length).reduce((accum, x) => x + accum); - if (remaining === this.newTicks) { - mapIx = this.tickmap.deltaMap.length; - } - - // If there is no tickable at the end point, try to split the next note - /** - * old map: - * d . d . - * split map: - * d . d d - * new map: - * d . . d - */ - if (mapIx < 0) { - const ndelta = this.tickmap.deltaMap[this.startIndex + 1]; - const needed = this.newTicks - currentTicks; - const exp = ndelta / needed; - // Next tick does not divide evenly into this, or next tick is shorter than this - if (Math.round(ndelta / exp) - ndelta / exp !== 0 || ndelta < 256) { - this.durationMap = []; - } else if (ndelta / exp + this.startTick + this.newTicks <= this.tickmap.totalDuration) { - this.durationMap.push(ndelta - (ndelta / exp)); - } else { - // there is no way to do this... - this.durationMap = []; - } - } else { - // If this note now takes up the space of other notes, remove those notes - for (i = this.startIndex + 1; i < mapIx; ++i) { - this.durationMap.push(0); - } - } + this.startIndex = parameters.startIndex; + this.endIndex = parameters.endIndex; + this.measure = parameters.measure; + this.voice = parameters.voice; } - static apply(params: SmoStretchNoteParams) { - const actor = new SmoStretchNoteActor(params); + static apply(params: SmoUnmakeTupletParams) { + const actor = new SmoUnmakeTupletActor(params); SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice); } iterateOverTick(note: SmoNote, tickmap: TickMap, index: number) { - if (this.durationMap.length === 0) { + if (index < this.startIndex || index > this.endIndex) { return null; } - if (index >= this.startIndex && index < this.startIndex + this.durationMap.length) { - const mapIndex = index - this.startIndex; - const ticks = this.durationMap[mapIndex]; - if (ticks === 0) { + if (index === this.startIndex) { + const tuplet = SmoTupletTree.getTupletForNoteIndex(this.measure.tupletTrees, this.voice, index); + if (tuplet === null) { return []; } - note = SmoNote.cloneWithDuration(note, { numerator: ticks, denominator: 1, remainder: 0 }); - return [note]; + + const ticks = tuplet.totalTicks; + const nn: SmoNote = SmoNote.cloneWithDuration(note, { numerator: ticks, denominator: 1, remainder: 0 }); + nn.tupletId = null; + SmoTupletTree.removeTupletForNoteIndex(this.measure, this.voice, index); + SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, this.startIndex, this.startIndex - this.endIndex); + + return [nn]; } - return null; + return []; } } + diff --git a/tests/file-load.ts b/tests/file-load.ts index 9ede176c..b2b1473a 100644 --- a/tests/file-load.ts +++ b/tests/file-load.ts @@ -39,7 +39,7 @@ export function createLoadTests(): void { midiScore = (new MidiToSmo(parseMidi(midiData.value), 1024)).convert(); await view.changeScore(midiScore); QUnit.test('loadMidi2', assert => { - assert.equal(midiScore.staves[0].measures[0].tuplets.length, 1); + assert.equal(midiScore.staves[0].measures[0].tupletTrees.length, 1); }); midiData = new SuiXhrLoader(midiKeyPath); await midiData.loadAsync();