Browse Source

update from osmd 1.4.0: many improvements, see changelog. needs npm install. try deleting node_modules and package-lock.json

try deleting node_modules and package-lock.json before running npm install and npm start again after this update.
sschmidTU 3 years ago
parent
commit
1d153ef688
40 changed files with 3510 additions and 187 deletions
  1. 3 4
      .appveyor.yml
  2. 31 0
      .travis.yml
  3. 21 12
      demo/index.js
  4. 5 1
      karma.conf.js
  5. 59 59
      package.json
  6. 102 0
      src/KarmaWebpackPatch/lib/karma-webpack/preprocessor.js
  7. 34 0
      src/KarmaWebpackPatch/lib/webpack/plugin.js
  8. 3 0
      src/MusicalScore/Graphical/EngravingRules.ts
  9. 1 1
      src/MusicalScore/Graphical/MusicSheetDrawer.ts
  10. 4 4
      src/MusicalScore/Graphical/VexFlow/SvgVexFlowBackend.ts
  11. 2 0
      src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts
  12. 20 0
      src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalNote.ts
  13. 4 1
      src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts
  14. 55 3
      src/MusicalScore/ScoreIO/InstrumentReader.ts
  15. 25 18
      src/MusicalScore/ScoreIO/VoiceGenerator.ts
  16. 4 2
      src/MusicalScore/VoiceData/Tie.ts
  17. 1 1
      src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts
  18. 9 1
      src/VexFlowPatch/readme.txt
  19. 17 3
      src/VexFlowPatch/src/beam.js
  20. 73 48
      src/VexFlowPatch/src/stavenote.js
  21. 175 0
      src/VexFlowPatch/src/stem.js
  22. 692 0
      src/VexFlowPatch/src/svgcontext.js
  23. 1 1
      test/Common/FileIO/Xml_Test.ts
  24. 47 0
      test/MusicalScore/Graphical/VexFlow/VexFlowGraphicalNote_Test.ts
  25. 2 18
      test/MusicalScore/Graphical/VexFlow/VexFlowMeasure_Test.ts
  26. 1 1
      test/MusicalScore/ScoreCalculation/MusicSheetCalculator_Test.ts
  27. 1 1
      test/Util/DiffImages_Test_Experimental.ts
  28. 1 1
      test/Util/generateDiffImagesPuppeteerLocalhost.js
  29. 399 0
      test/Util/generateImages_browserless.mjs
  30. 163 0
      test/data/test_beam_svg_double.musicxml
  31. 704 0
      test/data/test_clef_end_measure.musicxml
  32. 125 0
      test/data/test_note_alignment_long_distance_notes.musicxml
  33. 179 0
      test/data/test_note_overlap_staggering_whole_eighths.musicxml
  34. 140 0
      test/data/test_note_overlap_staggering_whole_half.musicxml
  35. 150 0
      test/data/test_notehead_sharing_eighth_quarter.musicxml
  36. 246 0
      test/data/test_ties_missing_1097.musicxml
  37. 7 3
      webpack.common.js
  38. 1 1
      webpack.dev.js
  39. 2 2
      webpack.prod.js
  40. 1 1
      webpack.sourcemap.js

+ 3 - 4
.appveyor.yml

@@ -1,10 +1,9 @@
 image: Visual Studio 2017
 image: Visual Studio 2017
 environment:
 environment:
-  timeout: 10000
+  timeout: 20000
   matrix:
   matrix:
-    # - nodejs_version: "8" 
-    # - nodejs_version: "10"
-    - nodejs_version: "12"
+    # - nodejs_version: "12"
+    - nodejs_version: "16"
 platform:
 platform:
   # - x86
   # - x86
   - x64
   - x64

+ 31 - 0
.travis.yml

@@ -0,0 +1,31 @@
+sudo: required
+dist: trusty
+language: node_js
+node_js:
+# - '10' # fails on Travis since upgrading to Vexflow 1.2.90 (still passes on AppVeyor), for Mxl_Test. Node 12 works.
+- '12'
+env:
+  - timeout=15000
+notifications:
+  email: false
+  slack:
+    secure: ZLC7oJ5BcdVfLX+R7Bg6NUaFrrOG5heX9t8lpCZAWtHG4Uag/z5hCAtr/pfdaoEZ4AFJ7SS0yubE3EltwoXdx/zeGlF7gV5JxjDtDyNpkqFa38XTSorP/0FYjaahecFnxUYG2oNQWTcnyeE6BMak+RQ4+ciLC1dQrzC84FNE4R28tV5SVwgM+O1JAFg67Z2Xu497tNuLG6aptyRAov6G0mo9e1oLW4apuiV4CnV+p2nMYbLEyHT5TJiQ8/c7ar7jM7Ia8bL6WGHGjOmEmy71DyWWQXBlE+RSS8uBRlF7BvGX7/fleCUa4jE5ieP+IKCENfa+9+SCE6i8YEAc8Wyfqdl/f5A7NqPDNYxWxU1w8iSM4/FJn6hJKJ3vnogAdQUlPtNYssMio2U06bkvtZ+hu961f6qcGaR10fcX8EHi1UwFDHQ+9uha+9U5vF/+EQHXAG5WGSKrpbH3CFypdJ8g3U1eW5qJn76W9Um4COSj26KI+pBTD9gZwaZCmDas0g2bECIClUKK4y1utsYf/KiJcJaIOEE+QvFNyhuXwdAmTFi8OZ784yrbXmpQZqol2ckgfvWNQZnwqY8h3A8RDhXxvbv6UbNOfE8p/BgJCRaSZAkaqU7b9+D0kSaNIWVPbPad3+Plgkg/gvyC07l8GR5+9tMysz669VQXUs2vzIMIzfA=
+deploy:
+  skip_cleanup: true
+  provider: npm
+  email: sebastian@haas.tech
+  api_key:
+    secure: eXeFmjyHP0PRxcydQIqORVobbKY5YT4SAjDsfp70fB0SlFt1brEwPW6Im5cjr32Zky7gSvVqs++mxyultyjxTHT7q3KYXbSsY6CHyRcV0FeFFB8tS0m5HX/bbCcz94//vCuKXPSajn/jcxQUUvsykhwe0abJt+5D8Nl2h4LOQW/5/ItOPvOjVKAaud4I1gVyYJPGFpYvuyRBswYt+JBCoMtRF/nr4qF0Q3PWiifiNpovIm80Zx4yNaALWjf7zJf9GmePBebTboPykXMHEw2ROC805Gao4iX+a9psc340z2LWaWz9Qugb0pa3zL7d2TY0WJLlqXpnZHMWXn+rlDazT5ysrp3mCLdEiDFkVTSbRioBx5QCmDPcWQW9NHxP3XheUBf+cv+1LUUMvYp6tEU9yQWrAl6sk0h8uEi0YSNE27nucXAZyDaavzwIlM//r7oQBpaKXE6NQc0pf2Dsv6fSwwZCokW8YuH42gtr6KTj5hoisaVo/nYRCzdL3iL69sPY6Kgjq47kknobEEgq9jitAEYsedaSADc/s8hzd4gidtfAMj8aTP1ZjDhDMMquUTx+Ksht4dStOZ72ULOIUew8r/j51QPvXVdwJS8bPjz7BEbUqLZxTLRqAUD0HzM7vaM+ynd83mrVruWurm624H2uZm57/yJvpaVnjKEbnLiBG8E=
+  on:
+    tags: true
+    branch: master
+    repo: opensheetmusicdisplay/opensheetmusicdisplay
+after_deploy:
+- openssl aes-256-cbc -K $encrypted_170846311824_key -iv $encrypted_170846311824_iv -in bin/gh_pages_deploy_key.enc -out bin/gh_pages_deploy_key -d
+- eval "$(ssh-agent -s)"
+- chmod 600 bin/gh_pages_deploy_key
+- ssh-add bin/gh_pages_deploy_key
+- git config --global user.email "travis@opensheetmusicdisplay.org"
+- git config --global user.name "travis"
+- chmod +x ./bin/publish_gh_page.sh
+- ./bin/publish_gh_page.sh

+ 21 - 12
demo/index.js

@@ -1,8 +1,8 @@
 import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMusicDisplay';
 import { OpenSheetMusicDisplay } from '../src/OpenSheetMusicDisplay/OpenSheetMusicDisplay';
 import { BackendType } from '../src/OpenSheetMusicDisplay/OSMDOptions';
 import { BackendType } from '../src/OpenSheetMusicDisplay/OSMDOptions';
 import { PlaybackManager, LinearTimingSource, BasicAudioPlayer, ControlPanel } from '../src/Playback';
 import { PlaybackManager, LinearTimingSource, BasicAudioPlayer, ControlPanel } from '../src/Playback';
-import * as jsPDF  from '../node_modules/jspdf-yworks/dist/jspdf.min';
-import * as svg2pdf from '../node_modules/svg2pdf.js/dist/svg2pdf.min';
+import * as jsPDF  from '../node_modules/jspdf/dist/jspdf.es.min';
+import * as svg2pdf from '../node_modules/svg2pdf.js/dist/svg2pdf.umd.min';
 import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculator';
 import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculator';
 
 
 /*jslint browser:true */
 /*jslint browser:true */
@@ -875,7 +875,7 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
      * Creates a Pdf of the currently rendered MusicXML
      * Creates a Pdf of the currently rendered MusicXML
      * @param pdfName if no name is given, the composer and title of the piece will be used
      * @param pdfName if no name is given, the composer and title of the piece will be used
      */
      */
-    function createPdf(pdfName) {
+    async function createPdf(pdfName) {
         if (openSheetMusicDisplay.backendType !== BackendType.SVG) {
         if (openSheetMusicDisplay.backendType !== BackendType.SVG) {
             console.log("[OSMD] createPdf(): Warning: createPDF is only supported for SVG background for now, not for Canvas." +
             console.log("[OSMD] createPdf(): Warning: createPDF is only supported for SVG background for now, not for Canvas." +
                 " Please use osmd.setOptions({backendType: SVG}).");
                 " Please use osmd.setOptions({backendType: SVG}).");
@@ -901,20 +901,29 @@ import { TransposeCalculator } from '../src/Plugins/Transpose/TransposeCalculato
 
 
         const orientation = pageHeight > pageWidth ? "p" : "l";
         const orientation = pageHeight > pageWidth ? "p" : "l";
         // create a new jsPDF instance
         // create a new jsPDF instance
-        const pdf = new jsPDF(orientation, "mm", [pageWidth, pageHeight]);
-        const scale = pageWidth / svgElement.clientWidth;
+        const pdf = new jsPDF.jsPDF({
+            orientation: orientation,
+            unit: "mm",
+            format: [pageWidth, pageHeight]
+        });
+        //const scale = pageWidth / svgElement.clientWidth;
         for (let idx = 0, len = backends.length; idx < len; ++idx) {
         for (let idx = 0, len = backends.length; idx < len; ++idx) {
             if (idx > 0) {
             if (idx > 0) {
                 pdf.addPage();
                 pdf.addPage();
             }
             }
             svgElement = backends[idx].getSvgElement();
             svgElement = backends[idx].getSvgElement();
-
-            // render the svg element
-            svg2pdf(svgElement, pdf, {
-                scale: scale,
-                xOffset: 0,
-                yOffset: 0
-            });
+            
+            if (!pdf.svg && !svg2pdf) { // this line also serves to make the svg2pdf not unused, though it's still necessary
+                // we need svg2pdf to have pdf.svg defined
+                console.log("svg2pdf missing, necessary for jspdf.svg().");
+                return;
+            }
+            await pdf.svg(svgElement, {
+                x: 0,
+                y: 0,
+                width: pageWidth,
+                height: pageHeight,
+            })
         }
         }
 
 
         pdf.save(pdfName); // save/download the created pdf
         pdf.save(pdfName); // save/download the created pdf

+ 5 - 1
karma.conf.js

@@ -25,6 +25,9 @@ module.exports = function (config) {
                 pattern: 'test/data/*.xml',
                 pattern: 'test/data/*.xml',
                 included: true
                 included: true
             }, {
             }, {
+                pattern: 'test/data/*.musicxml',
+                included: true
+            }, {
                 pattern: 'test/data/*.mxl.base64',
                 pattern: 'test/data/*.mxl.base64',
                 included: true
                 included: true
             }, {
             }, {
@@ -39,6 +42,7 @@ module.exports = function (config) {
         // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
         // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
         preprocessors: {
         preprocessors: {
             'test/data/*.xml': ['xml2js'],
             'test/data/*.xml': ['xml2js'],
+            'test/data/*.musicxml': ['xml2js'],
             'test/data/*.mxl.base64': ['base64-to-js'],
             'test/data/*.mxl.base64': ['base64-to-js'],
             // add webpack as preprocessor
             // add webpack as preprocessor
             'test/**/*.ts': ['webpack']
             'test/**/*.ts': ['webpack']
@@ -86,7 +90,7 @@ module.exports = function (config) {
         client: {
         client: {
             captureConsole: true,
             captureConsole: true,
             mocha: {
             mocha: {
-                timeout: process.env.timeout || 6000
+                timeout: process.env.timeout || 20000
             }
             }
         },
         },
 
 

+ 59 - 59
package.json

@@ -1,38 +1,40 @@
 {
 {
   "name": "osmd-extended",
   "name": "osmd-extended",
-  "version": "1.3.1",
+  "version": "1.4.0",
   "description": "Private / sponsor exclusive OSMD mirror/audio player.",
   "description": "Private / sponsor exclusive OSMD mirror/audio player.",
   "main": "build/opensheetmusicdisplay.min.js",
   "main": "build/opensheetmusicdisplay.min.js",
-  "typings": "build/dist/src/",
+  "types": "dist/src/OpenSheetMusicDisplay/index.d.ts",
   "scripts": {
   "scripts": {
     "docs": "typedoc --out ./build/docs --name OpenSheetMusicDisplay --module commonjs --target ES2017 --ignoreCompilerErrors --mode file ./src",
     "docs": "typedoc --out ./build/docs --name OpenSheetMusicDisplay --module commonjs --target ES2017 --ignoreCompilerErrors --mode file ./src",
     "eslint": "eslint -c .eslintrc.js --ext .ts .",
     "eslint": "eslint -c .eslintrc.js --ext .ts .",
     "lint": "npm run eslint",
     "lint": "npm run eslint",
     "test": "karma start --single-run --no-auto-watch",
     "test": "karma start --single-run --no-auto-watch",
     "test:watch": "karma start --no-single-run --auto-watch --browsers ChromeNoSecurity",
     "test:watch": "karma start --no-single-run --auto-watch --browsers ChromeNoSecurity",
-    "prebuild": "ncp src/VexFlowPatch/src/ node_modules/vexflow/src/",
+    "prebuild": "npm-run-all prebuildVexflow prebuildKarma",
+    "prebuildVexflow": "ncp src/VexFlowPatch/src/ node_modules/vexflow/src/",
+    "prebuildKarma": "ncp src/KarmaWebpackPatch/lib/ node_modules/karma-webpack/lib/",
     "prepare": "npm run build",
     "prepare": "npm run build",
     "build": "npm-run-all lint build:webpack",
     "build": "npm-run-all lint build:webpack",
     "build:doc": "cross-env STATIC_FILES_SUBFOLDER=sheets npm run build",
     "build:doc": "cross-env STATIC_FILES_SUBFOLDER=sheets npm run build",
-    "build:webpack": "webpack --progress --colors --config webpack.prod.js",
-    "build:webpack-dev": "webpack --progress --colors --config webpack.dev.js",
-    "build:webpack-sourcemap": "webpack --progress --colors --config webpack.sourcemap.js",
-    "start": "webpack-dev-server --progress --colors --config webpack.dev.js",
-    "start:local": "webpack-dev-server --progress --colors --config webpack.local.js",
-    "generatePNG": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 0 0 allSmall --osmdtesting",
-    "generateSVG": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export/svg svg 0 0 allSmall --osmdtesting",
-    "generatePNG:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 0 0 allSmall --debugosmdtesting",
-    "generatePNG:single": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 0 0 ^Beethoven",
-    "generatePNG:legacyslow": "node ./test/Util/generateDiffImagesPuppeteerLocalhost.js ./test/data ./export 2560",
-    "generatePNG:paged": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 210 297 allSmall",
-    "generatePNG:paged:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 210 297 all --debug 5000",
-    "generatePNG:paged:single": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 0 0 ^Beethoven",
-    "generate:current": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current png 0 0 allSmall --osmdtesting",
-    "generate:current:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current png 0 0 allSmall --debugosmdtesting",
-    "generate:current:singletest": "node test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current png 0 0 ^Beethoven --osmdtestingsingle",
-    "generate:blessed": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/blessed png 0 0 allSmall --osmdtesting",
+    "build:webpack": "webpack --progress --config webpack.prod.js",
+    "build:webpack-dev": "webpack --progress --config webpack.dev.js",
+    "build:webpack-sourcemap": "webpack --progress --config webpack.sourcemap.js",
+    "start": "webpack-dev-server --progress --config webpack.dev.js",
+    "start:local": "webpack-dev-server --progress --config webpack.local.js",
+    "generatePNG": "node ./test/Util/generateImages_browserless.mjs ../../build ./test/data ./export png 0 0 allSmall --osmdtesting",
+    "generateSVG": "node ./test/Util/generateImages_browserless.mjs ../../build ./test/data ./export/svg svg 0 0 allSmall --osmdtesting",
+    "generatePNG:debug": "node ./test/Util/generateImages_browserless.mjs ../../build ./test/data ./export png 0 0 allSmall --debugosmdtesting",
+    "generatePNG:single": "node ./test/Util/generateImages_browserless.mjs ../../build ./test/data ./export png 0 0 ^Beethoven",
+    "generatePNG:legacyslow": "node ./test/Util/generateDiffImagesPuppeteerLocalhost.js ./test/data ./export png 0 0 all",
+    "generatePNG:paged": "node ./test/Util/generateImages_browserless.mjs ../../build ./test/data ./export png 210 297 allSmall",
+    "generatePNG:paged:debug": "node ./test/Util/generateImages_browserless.mjs ../../build ./test/data ./export png 210 297 all --debug 5000",
+    "generatePNG:paged:single": "node ./test/Util/generateImages_browserless.mjs ../../build ./test/data ./export png 0 0 ^Beethoven",
+    "generate:current": "node ./test/Util/generateImages_browserless.mjs ../../build ./test/data ./visual_regression/current png 0 0 allSmall --osmdtesting",
+    "generate:current:debug": "node ./test/Util/generateImages_browserless.mjs ../../build ./test/data ./visual_regression/current png 0 0 allSmall --debugosmdtesting",
+    "generate:current:singletest": "node test/Util/generateImages_browserless.mjs ../../build ./test/data ./visual_regression/current png 0 0 ^Beethoven --osmdtestingsingle",
+    "generate:blessed": "node ./test/Util/generateImages_browserless.mjs ../../build ./test/data ./visual_regression/blessed png 0 0 allSmall --osmdtesting",
     "test:visual": "bash ./test/Util/visual_regression.sh ./visual_regression",
     "test:visual": "bash ./test/Util/visual_regression.sh ./visual_regression",
-    "test:visual:build": "npm run prebuild && npm run build:webpack && npm run generate:current && npm run test:visual",
+    "test:visual:build": "npm-run-all prebuild build:webpack generate:current test:visual",
     "test:visual:singletest": "sh ./test/Util/visual_regression.sh ./visual_regression Beethoven",
     "test:visual:singletest": "sh ./test/Util/visual_regression.sh ./visual_regression Beethoven",
     "fix-memory-limit": "cross-env NODE_OPTIONS=--max_old_space_size=4096"
     "fix-memory-limit": "cross-env NODE_OPTIONS=--max_old_space_size=4096"
   },
   },
@@ -64,62 +66,60 @@
   },
   },
   "homepage": "http://opensheetmusicdisplay.org",
   "homepage": "http://opensheetmusicdisplay.org",
   "dependencies": {
   "dependencies": {
-    "@types/vexflow": "^3.0.0",
+    "@types/vexflow": "^1.2.37",
     "d-path-parser": "^1.0.0",
     "d-path-parser": "^1.0.0",
     "jszip": "3.7.1",
     "jszip": "3.7.1",
-    "loglevel": "^1.6.8",
+    "loglevel": "^1.8.0",
     "soundfont-player": "^0.12.0",
     "soundfont-player": "^0.12.0",
     "standardized-audio-context": "^25.1.5",
     "standardized-audio-context": "^25.1.5",
     "typescript-collections": "^1.3.3",
     "typescript-collections": "^1.3.3",
     "vexflow": "^1.2.93"
     "vexflow": "^1.2.93"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@types/chai": "^4.2.11",
-    "@types/mocha": "^7.0.2",
-    "@types/node": "^14.0.9",
+    "@types/chai": "^4.2.22",
+    "@types/mocha": "^9.0.0",
+    "@types/node": "^16.11.11",
     "@types/resize-observer-browser": "^0.1.5",
     "@types/resize-observer-browser": "^0.1.5",
-    "@typescript-eslint/eslint-plugin": "^4.14.0",
-    "@typescript-eslint/parser": "^4.14.0",
+    "@typescript-eslint/eslint-plugin": "^5.5.0",
+    "@typescript-eslint/parser": "^5.5.0",
     "canvas": "^2.8.0",
     "canvas": "^2.8.0",
-    "chai": "^4.1.0",
-    "clean-webpack-plugin": "^3.0.0",
-    "cross-blob": "^1.2.0",
-    "cross-env": "^7.0.2",
-    "cz-conventional-changelog": "^3.0.0",
-    "eslint": "^6.8.0",
-    "eslint-config-standard": "14.1.1",
-    "eslint-plugin-import": "^2.20.2",
-    "eslint-plugin-jsdoc": "^31.0.8",
+    "chai": "^4.3.4",
+    "clean-webpack-plugin": "^4.0.0",
+    "cross-blob": "^3.0.0",
+    "cross-env": "^7.0.3",
+    "cz-conventional-changelog": "^3.3.0",
+    "eslint": "^8.4.0",
+    "eslint-plugin-import": "^2.25.3",
+    "eslint-plugin-jsdoc": "^37.1.0",
     "eslint-plugin-no-null": "^1.0.2",
     "eslint-plugin-no-null": "^1.0.2",
-    "eslint-plugin-node": "^11.0.0",
-    "eslint-plugin-promise": "^4.2.1",
-    "eslint-plugin-standard": "^4.0.0",
-    "html-webpack-plugin": "^4.3.0",
-    "jquery": "^3.5.1",
-    "jsdom": "^16.2.2",
-    "jspdf-yworks": "^2.1.1",
-    "karma": "^5.0.8",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-standard": "^4.1.0",
+    "html-webpack-plugin": "^5.5.0",
+    "jquery": "^3.6.0",
+    "jsdom": "^19.0.0",
+    "jspdf": "^2.4.0",
+    "karma": "^6.3.9",
     "karma-base64-to-js-preprocessor": "^0.1.0",
     "karma-base64-to-js-preprocessor": "^0.1.0",
     "karma-chai": "^0.1.0",
     "karma-chai": "^0.1.0",
     "karma-chrome-launcher": "^3.1.0",
     "karma-chrome-launcher": "^3.1.0",
-    "karma-firefox-launcher": "^1.0.0",
+    "karma-firefox-launcher": "^2.1.2",
     "karma-mocha": "^2.0.1",
     "karma-mocha": "^2.0.1",
-    "karma-mocha-reporter": "^2.0.4",
-    "karma-webpack": "^4.0.2",
+    "karma-mocha-reporter": "^2.2.5",
+    "karma-webpack": "^5.0.0",
     "karma-xml2js-preprocessor": "^0.1.0",
     "karma-xml2js-preprocessor": "^0.1.0",
-    "mocha": "^7.0.1",
+    "mocha": "^9.1.3",
     "ncp": "^2.0.0",
     "ncp": "^2.0.0",
-    "npm-run-all": "^4.1.2",
+    "npm-run-all": "^4.1.5",
     "pre-commit": "^1.2.2",
     "pre-commit": "^1.2.2",
-    "svg2pdf.js": "^1.5.0",
-    "ts-loader": "^4.1.0",
-    "typedoc": "^0.17.3",
-    "typescript": "^3.9.5",
-    "webpack": "^4.43.0",
-    "webpack-cli": "^3.3.11",
-    "webpack-dev-server": "^3.11.2",
-    "webpack-merge": "^4.1.2",
-    "webpack-visualizer-plugin": "^0.1.11",
+    "svg2pdf.js": "^2.2.0",
+    "ts-loader": "^9.2.6",
+    "typedoc": "^0.22.10",
+    "typescript": "^4.5.2",
+    "webpack": "^5.64.4",
+    "webpack-cli": "^4.9.1",
+    "webpack-dev-server": "^4.7.0",
+    "webpack-merge": "^5.8.0",
+    "webpack-visualizer-plugin2": "^1.0.0",
     "html-loader": "^1.3.0",
     "html-loader": "^1.3.0",
     "json5-loader": "^4.0.0",
     "json5-loader": "^4.0.0",
     "@material/layout-grid": "^7.0.0",
     "@material/layout-grid": "^7.0.0",

+ 102 - 0
src/KarmaWebpackPatch/lib/karma-webpack/preprocessor.js

@@ -0,0 +1,102 @@
+const path = require('path');
+
+const glob = require('glob');
+const minimatch = require('minimatch');
+
+const { ensureWebpackFrameworkSet } = require('../karma/validation');
+const { hash } = require('../utils/hash');
+
+const KW_Controller = require('./controller');
+
+function getPathKey(filePath, withExtension = false) {
+  const pathParts = path.parse(filePath);
+  const key = `${pathParts.name}.${hash(filePath)}`;
+  return withExtension ? `${key}${pathParts.ext}` : key;
+}
+
+function configToWebpackEntries(config) {
+  const filteredPreprocessorsPatterns = [];
+  const { preprocessors } = config;
+
+  let files = [];
+  config.files.forEach((fileEntry, i) => {
+    // forcefully disable karma watch as we use webpack watch only
+    config.files[i].watched = false;
+    files = [...files, ...glob.sync(fileEntry.pattern)];
+  });
+
+  Object.keys(preprocessors).forEach((pattern) => {
+    if (preprocessors[pattern].includes('webpack')) {
+      filteredPreprocessorsPatterns.push(pattern);
+    }
+  });
+
+  const filteredFiles = [];
+  files.forEach((filePath) => {
+    filteredPreprocessorsPatterns.forEach((pattern) => {
+      if (minimatch(filePath, pattern)) {
+        filteredFiles.push(filePath);
+      }
+    });
+  });
+
+  const webpackEntries = {};
+  filteredFiles.forEach((filePath) => {
+    webpackEntries[getPathKey(filePath)] = filePath;
+  });
+
+  return webpackEntries;
+}
+
+function KW_Preprocessor(config, emitter) {
+  const controller = new KW_Controller();
+  config.__karmaWebpackController = controller;
+  ensureWebpackFrameworkSet(config);
+
+  // one time setup
+  if (controller.isActive === false) {
+    controller.updateWebpackOptions({
+      entry: configToWebpackEntries(config),
+      watch: config.autoWatch,
+    });
+
+    if (config.webpack.entry) {
+      console.warn(`
+karma-webpack does not currently support custom entries, if this is something you need,
+consider opening an issue.
+ignoring attempt to set the entry option...
+      `);
+      delete config.webpack.entry;
+    }
+
+    controller.updateWebpackOptions(config.webpack);
+    controller.karmaEmitter = emitter;
+  }
+
+  const normalize = (file) => file.replace(/\\/g, '/');
+
+  const transformPath =
+    config.webpack.transformPath ||
+    ((filepath) => {
+      // force *.js files by default
+      const info = path.parse(filepath);
+      return `${path.join(info.dir, info.name)}.js`;
+    });
+
+  return function processFile(content, file, done) {
+    controller.bundle().then(() => {
+      file.path = normalize(file.path); // eslint-disable-line no-param-reassign
+
+      const transformedFilePath = transformPath(getPathKey(file.path, true));
+      const bundleContent = controller.bundlesContent[transformedFilePath];
+
+      file.path = transformedFilePath;
+
+      done(null, bundleContent);
+    });
+  };
+}
+
+KW_Preprocessor.$inject = ['config', 'emitter'];
+
+module.exports = KW_Preprocessor;

+ 34 - 0
src/KarmaWebpackPatch/lib/webpack/plugin.js

@@ -0,0 +1,34 @@
+const fs = require('fs');
+const path = require('path');
+
+class KW_WebpackPlugin {
+  constructor(options) {
+    this.karmaEmitter = options.karmaEmitter;
+    this.controller = options.controller;
+  }
+
+  apply(compiler) {
+    this.compiler = compiler;
+
+    // webpack bundles are finished
+    compiler.hooks.done.tap('KW_WebpackPlugin', (stats) => {
+      // read generated file content and store for karma preprocessor
+      this.controller.bundlesContent = {};
+      stats.toJson().assets.forEach((webpackFileObj) => {
+        const filePath = path.resolve(
+          compiler.options.output.path,
+          webpackFileObj.name
+        );
+        this.controller.bundlesContent[webpackFileObj.name] = fs.readFileSync(
+          filePath,
+          'utf-8'
+        );
+      });
+
+      // karma refresh
+      this.karmaEmitter.refreshFiles();
+    });
+  }
+}
+
+module.exports = KW_WebpackPlugin;

+ 3 - 0
src/MusicalScore/Graphical/EngravingRules.ts

@@ -238,6 +238,8 @@ export class EngravingRules {
     public MetronomeMarkXShift: number;
     public MetronomeMarkXShift: number;
     public MetronomeMarkYShift: number;
     public MetronomeMarkYShift: number;
     public SoftmaxFactorVexFlow: number;
     public SoftmaxFactorVexFlow: number;
+    /** Stagger (x-shift) whole notes that are the same note, but in different voices (show 2 instead of 1). */
+    public StaggerSameWholeNotes: boolean;
     public MaxInstructionsConstValue: number;
     public MaxInstructionsConstValue: number;
     public NoteDistances: number[] = [1.0, 1.0, 1.3, 1.6, 2.0, 2.5, 3.0, 4.0];
     public NoteDistances: number[] = [1.0, 1.0, 1.3, 1.6, 2.0, 2.5, 3.0, 4.0];
     public NoteDistancesScalingFactors: number[] = [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0];
     public NoteDistancesScalingFactors: number[] = [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0];
@@ -591,6 +593,7 @@ export class EngravingRules {
         this.SoftmaxFactorVexFlow = 15; // only applies to Vexflow 3.x. 15 seems like the sweet spot. Vexflow default is 100.
         this.SoftmaxFactorVexFlow = 15; // only applies to Vexflow 3.x. 15 seems like the sweet spot. Vexflow default is 100.
         // if too high, score gets too big, especially half notes. with half note quarter quarter, the quarters get squeezed.
         // if too high, score gets too big, especially half notes. with half note quarter quarter, the quarters get squeezed.
         // if too low, smaller notes aren't positioned correctly.
         // if too low, smaller notes aren't positioned correctly.
+        this.StaggerSameWholeNotes = true;
 
 
         // Render options (whether to render specific or invisible elements)
         // Render options (whether to render specific or invisible elements)
         this.AlignRests = AlignRestOption.Never; // 0 = false, 1 = true, 2 = auto
         this.AlignRests = AlignRestOption.Never; // 0 = false, 1 = true, 2 = auto

+ 1 - 1
src/MusicalScore/Graphical/MusicSheetDrawer.ts

@@ -50,7 +50,7 @@ export abstract class MusicSheetDrawer {
     public drawingParameters: DrawingParameters;
     public drawingParameters: DrawingParameters;
     public splitScreenLineColor: number;
     public splitScreenLineColor: number;
     public midiPlaybackAvailable: boolean;
     public midiPlaybackAvailable: boolean;
-    public drawableBoundingBoxElement: string = process.env.DRAW_BOUNDING_BOX_ELEMENT;
+    public drawableBoundingBoxElement: string = "None"; // process.env.DRAW_BOUNDING_BOX_ELEMENT;
 
 
     public skyLineVisible: boolean = false;
     public skyLineVisible: boolean = false;
     public bottomLineVisible: boolean = false;
     public bottomLineVisible: boolean = false;

+ 4 - 4
src/MusicalScore/Graphical/VexFlow/SvgVexFlowBackend.ts

@@ -108,7 +108,7 @@ export class SvgVexFlowBackend extends VexFlowBackend {
                       heightInPixel: number, screenPosition: PointF2D,
                       heightInPixel: number, screenPosition: PointF2D,
                       color: string = undefined, fontFamily: string = undefined): Node {
                       color: string = undefined, fontFamily: string = undefined): Node {
         this.ctx.save();
         this.ctx.save();
-        const node: Node = this.ctx.openGroup();
+        const node: Node = this.ctx.openGroup("text");
 
 
         if (color) {
         if (color) {
             this.ctx.attributes.fill = color;
             this.ctx.attributes.fill = color;
@@ -149,7 +149,7 @@ export class SvgVexFlowBackend extends VexFlowBackend {
     }
     }
     public renderRectangle(rectangle: RectangleF2D, styleId: number, colorHex: string, alpha: number = 1): Node {
     public renderRectangle(rectangle: RectangleF2D, styleId: number, colorHex: string, alpha: number = 1): Node {
         this.ctx.save();
         this.ctx.save();
-        const node: Node = this.ctx.openGroup();
+        const node: Node = this.ctx.openGroup("rect");
         if (colorHex) {
         if (colorHex) {
             this.ctx.attributes.fill = colorHex;
             this.ctx.attributes.fill = colorHex;
         } else {
         } else {
@@ -165,7 +165,7 @@ export class SvgVexFlowBackend extends VexFlowBackend {
 
 
     public renderLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number = 2): Node {
     public renderLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number = 2): Node {
         this.ctx.save();
         this.ctx.save();
-        const node: Node = this.ctx.openGroup();
+        const node: Node = this.ctx.openGroup("line");
         this.ctx.beginPath();
         this.ctx.beginPath();
         this.ctx.moveTo(start.x, start.y);
         this.ctx.moveTo(start.x, start.y);
         this.ctx.lineTo(stop.x, stop.y);
         this.ctx.lineTo(stop.x, stop.y);
@@ -184,7 +184,7 @@ export class SvgVexFlowBackend extends VexFlowBackend {
     }
     }
 
 
     public renderCurve(points: PointF2D[]): Node {
     public renderCurve(points: PointF2D[]): Node {
-        const node: Node = this.ctx.openGroup();
+        const node: Node = this.ctx.openGroup("curve");
         this.ctx.beginPath();
         this.ctx.beginPath();
         this.ctx.moveTo(points[0].x, points[0].y);
         this.ctx.moveTo(points[0].x, points[0].y);
         this.ctx.bezierCurveTo(
         this.ctx.bezierCurveTo(

+ 2 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts

@@ -463,6 +463,8 @@ export class VexFlowConverter {
             vfnote = new Vex.Flow.GraceNote(vfnoteStruct);
             vfnote = new Vex.Flow.GraceNote(vfnoteStruct);
         } else {
         } else {
             vfnote = new Vex.Flow.StaveNote(vfnoteStruct);
             vfnote = new Vex.Flow.StaveNote(vfnoteStruct);
+            (vfnote as any).stagger_same_whole_notes = rules.StaggerSameWholeNotes;
+            //   it would be nice to only save this once, not for every note, but has to be accessible in stavenote.js
         }
         }
         const lineShift: number = gve.notes[0].lineShift;
         const lineShift: number = gve.notes[0].lineShift;
         if (lineShift !== 0) {
         if (lineShift !== 0) {

+ 20 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalNote.ts

@@ -122,4 +122,24 @@ export class VexFlowGraphicalNote extends GraphicalNote {
         }
         }
         return this.vfnote[0].getAttribute("el");
         return this.vfnote[0].getAttribute("el");
     }
     }
+
+    /** Gets the SVG path element of the note's stem. */
+    public getStemSVG(): HTMLElement {
+        return document.getElementById("vf-" + this.getSVGId() + "-stem");
+        // more correct, but Vex.Prefix() is not in the definitions:
+        //return document.getElementById((Vex as any).Prefix(this.getSVGId() + "-stem"));
+    }
+
+    /** Gets the SVG path elements of the beams starting on this note. */
+    public getBeamSVGs(): HTMLElement[] {
+        const beamSVGs: HTMLElement[] = [];
+        for (let i: number = 0;; i++) {
+            const newSVG: HTMLElement = document.getElementById(`vf-${this.getSVGId()}-beam${i}`);
+            if (!newSVG) {
+                break;
+            }
+            beamSVGs.push(newSVG);
+        }
+        return beamSVGs;
+    }
 }
 }

+ 4 - 1
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -812,7 +812,10 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     const firstMeasureNumber: number = this.graphicalMusicSheet.MeasureList[0][0].MeasureNumber; // 0 for pickup, 1 otherwise
     const firstMeasureNumber: number = this.graphicalMusicSheet.MeasureList[0][0].MeasureNumber; // 0 for pickup, 1 otherwise
     const measureNumber: number = Math.max(measure.MeasureNumber - firstMeasureNumber, 0);
     const measureNumber: number = Math.max(measure.MeasureNumber - firstMeasureNumber, 0);
     const staffNumber: number = 0;
     const staffNumber: number = 0;
-    const vfStave: Vex.Flow.Stave = (this.graphicalMusicSheet.MeasureList[measureNumber][staffNumber] as VexFlowMeasure).getVFStave();
+    const vfStave: Vex.Flow.Stave = (this.graphicalMusicSheet.MeasureList[measureNumber][staffNumber] as VexFlowMeasure)?.getVFStave();
+    if (!vfStave) { // potentially multi measure rest
+      return;
+    }
     const yOffset: number = -this.rules.RehearsalMarkYOffsetDefault - this.rules.RehearsalMarkYOffset;
     const yOffset: number = -this.rules.RehearsalMarkYOffsetDefault - this.rules.RehearsalMarkYOffset;
     let xOffset: number = this.rules.RehearsalMarkXOffsetDefault + this.rules.RehearsalMarkXOffset;
     let xOffset: number = this.rules.RehearsalMarkXOffsetDefault + this.rules.RehearsalMarkXOffset;
     if (measure.IsSystemStartMeasure) {
     if (measure.IsSystemStartMeasure) {

+ 55 - 3
src/MusicalScore/ScoreIO/InstrumentReader.ts

@@ -911,9 +911,9 @@ export class InstrumentReader {
           }
           }
 
 
         }
         }
-        if (nodeList.hasAttributes && nodeList.attributes()[0].name === "number") {
+        if (nodeList.hasAttributes && nodeList.attribute("number")) {
           try {
           try {
-            staffNumber = parseInt(nodeList.attributes()[0].value, 10);
+            staffNumber = parseInt(nodeList.attribute("number").value, 10);
             if (staffNumber > this.currentClefNumber) {
             if (staffNumber > this.currentClefNumber) {
               staffNumber = this.currentClefNumber;
               staffNumber = this.currentClefNumber;
             }
             }
@@ -1065,7 +1065,7 @@ export class InstrumentReader {
   private saveAbstractInstructionList(numberOfStaves: number, beginOfMeasure: boolean): void {
   private saveAbstractInstructionList(numberOfStaves: number, beginOfMeasure: boolean): void {
     for (let i: number = this.abstractInstructions.length - 1; i >= 0; i--) {
     for (let i: number = this.abstractInstructions.length - 1; i >= 0; i--) {
       const instruction: [number, AbstractNotationInstruction, Fraction] = this.abstractInstructions[i];
       const instruction: [number, AbstractNotationInstruction, Fraction] = this.abstractInstructions[i];
-      const key: number = instruction[0];
+      const key: number = instruction[0]; // staffNumber
       const value: AbstractNotationInstruction = instruction[1];
       const value: AbstractNotationInstruction = instruction[1];
       const instructionTimestamp: Fraction = instruction[2];
       const instructionTimestamp: Fraction = instruction[2];
       if (value instanceof ClefInstruction) {
       if (value instanceof ClefInstruction) {
@@ -1140,6 +1140,58 @@ export class InstrumentReader {
               this.activeClefs[key - 1] = clefInstruction;
               this.activeClefs[key - 1] = clefInstruction;
               this.abstractInstructions.splice(i, 1);
               this.abstractInstructions.splice(i, 1);
             }
             }
+          } else {
+            let lastStaffEntryBefore: SourceStaffEntry;
+            const duration: Fraction = this.activeRhythm.Rhythm;
+            if (duration.RealValue > 0 &&
+              instructionTimestamp.RealValue / duration.RealValue > 0.90) {
+                if (!this.currentMeasure.LastInstructionsStaffEntries[key - 1]) {
+                  this.currentMeasure.LastInstructionsStaffEntries[key - 1] = new SourceStaffEntry(undefined, this.instrument.Staves[key - 1]);
+                }
+                lastStaffEntryBefore = this.currentMeasure.LastInstructionsStaffEntries[key - 1];
+            }
+            // TODO figure out a more elegant way to do this. (see #1120)
+            //   the problem is that not all the staffentries in the measure exist yet,
+            //   so we can't put the clefInstruction before the correct note.
+            //   (if we try that, it's one note too early -> save instruction for later?)
+            //let lastTimestampBefore: Fraction;
+            // for (const vssec of this.currentMeasure.VerticalSourceStaffEntryContainers) {
+            //   for (const sse of vssec.StaffEntries) {
+            //     if (sse?.ParentStaff?.Id !== key) {
+            //       continue;
+            //     }
+            //     // if (!lastTimestampBefore || sse.Timestamp.lte(instructionTimestamp)) {
+            //     //   lastTimestampBefore = sse.Timestamp;
+            //     //   lastStaffEntryBefore = sse;
+            //     // } else {
+            //     //   lastStaffEntryBefore = sse;
+            //     //   break;
+            //     // }
+            //     if (sse.Timestamp.gte(instructionTimestamp)) {
+            //       lastStaffEntryBefore = sse;
+            //       break;
+            //     }
+            //   }
+            // }
+            //const sseIndex: number = this.inSourceMeasureInstrumentIndex + staffNumber - 1;
+            // if (!lastStaffEntryBefore) {
+            //   // this doesn't work for some reason
+            //   const newContainer: VerticalSourceStaffEntryContainer = new VerticalSourceStaffEntryContainer(this.currentMeasure, instructionTimestamp, 1);
+            //   const newStaffEntry: SourceStaffEntry = new SourceStaffEntry(newContainer, this.instrument.Staves[key - 1]);
+            //   newContainer.StaffEntries.push(newStaffEntry);
+            //   this.currentMeasure.VerticalSourceStaffEntryContainers.push(newContainer);
+            //   lastStaffEntryBefore = newStaffEntry;
+            // }
+            // if (!lastStaffEntryBefore) {
+              //   lastStaffEntryBefore = new SourceStaffEntry(undefined, undefined);
+              //   this.currentMeasure.LastInstructionsStaffEntries[sseIndex] = lastStaffEntryBefore;
+              // }
+            if (lastStaffEntryBefore) {
+              clefInstruction.Parent = lastStaffEntryBefore;
+              lastStaffEntryBefore.Instructions.push(clefInstruction);
+              this.activeClefs[key - 1] = clefInstruction;
+              this.abstractInstructions.splice(i, 1);
+            } // else clefinstruction might be processed later (e.g. Haydn Concertante measure 314)
           }
           }
         } else if (key <= this.activeClefs.length && clefInstruction === this.activeClefs[key - 1]) {
         } else if (key <= this.activeClefs.length && clefInstruction === this.activeClefs[key - 1]) {
           this.abstractInstructions.splice(i, 1);
           this.abstractInstructions.splice(i, 1);

+ 25 - 18
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -259,15 +259,16 @@ export class VoiceGenerator {
         }
         }
 
 
         // remove open ties, if there is already a gap between the last tie note and now.
         // remove open ties, if there is already a gap between the last tie note and now.
-        const openTieDict: { [_: number]: Tie } = this.openTieDict;
-        for (const key in openTieDict) {
-          if (openTieDict.hasOwnProperty(key)) {
-            const tie: Tie = openTieDict[key];
-            if (Fraction.plus(tie.StartNote.ParentStaffEntry.Timestamp, tie.Duration).lt(this.currentStaffEntry.Timestamp)) {
-              delete openTieDict[key];
-            }
-          }
-        }
+        // TODO this deletes valid ties, see #1097
+        // const openTieDict: { [_: number]: Tie } = this.openTieDict;
+        // for (const key in openTieDict) {
+        //   if (openTieDict.hasOwnProperty(key)) {
+        //     const tie: Tie = openTieDict[key];
+        //     if (Fraction.plus(tie.StartNote.ParentStaffEntry.Timestamp, tie.Duration).lt(this.currentStaffEntry.Timestamp)) {
+        //       delete openTieDict[key];
+        //     }
+        //   }
+        // }
       }
       }
       // time-modification yields tuplet in currentNote
       // time-modification yields tuplet in currentNote
       // mustn't execute method, if this is the Note where the Tuplet has been created
       // mustn't execute method, if this is the Note where the Tuplet has been created
@@ -988,8 +989,8 @@ export class VoiceGenerator {
 
 
   private addTie(tieNodeList: IXmlElement[], measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction, tieType: TieTypes): void {
   private addTie(tieNodeList: IXmlElement[], measureStartAbsoluteTimestamp: Fraction, maxTieNoteFraction: Fraction, tieType: TieTypes): void {
     if (tieNodeList) {
     if (tieNodeList) {
-      if (tieNodeList.length === 1) {
-        const tieNode: IXmlElement = tieNodeList[0];
+      for (let i: number = 0; i < tieNodeList.length; i++) {
+        const tieNode: IXmlElement = tieNodeList[i];
         if (tieNode !== undefined && tieNode.attributes()) {
         if (tieNode !== undefined && tieNode.attributes()) {
           let tieDirection: PlacementEnum = PlacementEnum.NotYetDefined;
           let tieDirection: PlacementEnum = PlacementEnum.NotYetDefined;
           // read tie direction/placement from XML
           // read tie direction/placement from XML
@@ -1012,6 +1013,11 @@ export class VoiceGenerator {
           }
           }
 
 
           const type: string = tieNode.attribute("type").value;
           const type: string = tieNode.attribute("type").value;
+          if (type === "start" && i === 0) {
+            // handle this after the stop node, so that we don't start a new tie before the old one has ended.
+            tieNodeList.push(tieNode);
+            continue;
+          }
           try {
           try {
             if (type === "start") {
             if (type === "start") {
               const num: number = this.findCurrentNoteInTieDict(this.currentNote);
               const num: number = this.findCurrentNoteInTieDict(this.currentNote);
@@ -1027,7 +1033,7 @@ export class VoiceGenerator {
               const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
               const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
               const tie: Tie = this.openTieDict[tieNumber];
               const tie: Tie = this.openTieDict[tieNumber];
               if (tie) {
               if (tie) {
-                tie.AddNote(this.currentNote);
+                tie.AddNote(this.currentNote, false);
                 delete this.openTieDict[tieNumber];
                 delete this.openTieDict[tieNumber];
               }
               }
             }
             }
@@ -1037,13 +1043,14 @@ export class VoiceGenerator {
           }
           }
 
 
         }
         }
-      } else if (tieNodeList.length === 2) {
-        const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
-        if (tieNumber >= 0) {
-          const tie: Tie = this.openTieDict[tieNumber];
-          tie.AddNote(this.currentNote);
-        }
       }
       }
+      // } else if (tieNodeList.length === 2) {
+      //   const tieNumber: number = this.findCurrentNoteInTieDict(this.currentNote);
+      //   if (tieNumber >= 0) {
+      //     const tie: Tie = this.openTieDict[tieNumber];
+      //     tie.AddNote(this.currentNote);
+      //   }
+      // }
     }
     }
   }
   }
 
 

+ 4 - 2
src/MusicalScore/VoiceData/Tie.ts

@@ -43,8 +43,10 @@ export class Tie {
         return this.StartNote.Pitch;
         return this.StartNote.Pitch;
     }
     }
 
 
-    public AddNote(note: Note): void {
+    public AddNote(note: Note, isStartNote: boolean = true): void {
         this.notes.push(note);
         this.notes.push(note);
-        note.NoteTie = this;
+        if (isStartNote) {
+            note.NoteTie = this; // be careful not to overwrite note.NoteTie wrongly, saves only one tie
+        }
     }
     }
 }
 }

+ 1 - 1
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -35,7 +35,7 @@ import { DynamicsCalculator } from "../MusicalScore/ScoreIO/MusicSymbolModules/D
  * After the constructor, use load() and render() to load and render a MusicXML file.
  * After the constructor, use load() and render() to load and render a MusicXML file.
  */
  */
 export class OpenSheetMusicDisplay {
 export class OpenSheetMusicDisplay {
-    private version: string = "1.3.1-audio-extended"; // getter: this.Version
+    private version: string = "1.4.0-audio-extended"; // getter: this.Version
     // at release, bump version and change to -release, afterwards to -dev again
     // at release, bump version and change to -release, afterwards to -dev again
 
 
     /**
     /**

+ 9 - 1
src/VexFlowPatch/readme.txt

@@ -12,6 +12,7 @@ Each .js has comments like "// VexFlowPatch: [explanation]" to indicate what was
 
 
 beam.js (custom addition):
 beam.js (custom addition):
 add flat_beams, flat_beam_offset, flat_beam_offset_per_beam render_option
 add flat_beams, flat_beam_offset, flat_beam_offset_per_beam render_option
+able to add svg node id+class to beam
 
 
 pedalmarking.js (custom addition):
 pedalmarking.js (custom addition):
 Add rendering options for pedals that break across systems.
 Add rendering options for pedals that break across systems.
@@ -23,7 +24,8 @@ add xOffset, fontSize arguments (see stavesection.js)
 
 
 stavenote.js (custom addition):
 stavenote.js (custom addition):
 Fix stem/flag formatting. Instead of shifting notes by default, update the stem/flag rendering to render different voices aligned.
 Fix stem/flag formatting. Instead of shifting notes by default, update the stem/flag rendering to render different voices aligned.
-Only offset if a note is the same voice, same note.
+  Only offset if a note is the same voice, same note.
+able to add svg node id+class to stem
 
 
 staverepetition.js (custom addition):
 staverepetition.js (custom addition):
 add TO_CODA enum to type() and draw()
 add TO_CODA enum to type() and draw()
@@ -38,9 +40,15 @@ stavevolta.js (merged Vexflow 3.x):
 Fix the length of voltas for first measures in a system
 Fix the length of voltas for first measures in a system
 (whose lengths were wrongly extended by the width of the clef, key signature, etc. (beginInstructions) in Vexflow 1.2.93)
 (whose lengths were wrongly extended by the width of the clef, key signature, etc. (beginInstructions) in Vexflow 1.2.93)
 
 
+stem.js (custom addition):
+able to give an id+class to the stem node in SVG
+
 stemmablenote.js (custom addition):
 stemmablenote.js (custom addition):
 Add manual flag rendering variable so we can choose not to render flags if notes are sharing a stem.
 Add manual flag rendering variable so we can choose not to render flags if notes are sharing a stem.
 
 
+svgcontext.js (custom addition):
+able to add extra attributes (like svg node id) to a stroke (e.g. stem)
+
 tabnote.js (merged Vexflow 3.x):
 tabnote.js (merged Vexflow 3.x):
 Add a context group for each tabnote, so that it can be found in the SVG DOM ("vf-tabnote")
 Add a context group for each tabnote, so that it can be found in the SVG DOM ("vf-tabnote")
 
 

+ 17 - 3
src/VexFlowPatch/src/beam.js

@@ -751,7 +751,7 @@ export class Beam extends Element {
           }
           }
         } else {
         } else {
           // No beam started yet. Start a new one.
           // No beam started yet. Start a new one.
-          current_beam = { start: stem_x, end: null };
+          current_beam = { start: stem_x, end: null, start_note: note };
           beam_started = true;
           beam_started = true;
 
 
           if (beam_alone) {
           if (beam_alone) {
@@ -822,15 +822,24 @@ export class Beam extends Element {
     const firstStemX = firstNote.getStemX();
     const firstStemX = firstNote.getStemX();
     const beamThickness = this.render_options.beam_width * this.stem_direction;
     const beamThickness = this.render_options.beam_width * this.stem_direction;
 
 
+    const beamsPerStartNote = {};
+    for (const note of this.notes) {
+      beamsPerStartNote[note.getAttribute("id")] = 0;
+    }
+    
     // Draw the beams.
     // Draw the beams.
     for (let i = 0; i < valid_beam_durations.length; ++i) {
     for (let i = 0; i < valid_beam_durations.length; ++i) {
       const duration = valid_beam_durations[i];
       const duration = valid_beam_durations[i];
       const beamLines = this.getBeamLines(duration);
       const beamLines = this.getBeamLines(duration);
-
+      
       for (let j = 0; j < beamLines.length; ++j) {
       for (let j = 0; j < beamLines.length; ++j) {
         const beam_line = beamLines[j];
         const beam_line = beamLines[j];
         const startBeamX = beam_line.start;
         const startBeamX = beam_line.start;
 
 
+        const startNoteId = beam_line.start_note.getAttribute("id");
+        const beamNumber = beamsPerStartNote[startNoteId];
+        beamsPerStartNote[startNoteId]++;
+
         const startBeamY = this.getSlopeY(startBeamX, firstStemX, beamY, this.slope);
         const startBeamY = this.getSlopeY(startBeamX, firstStemX, beamY, this.slope);
         const lastBeamX = beam_line.end;
         const lastBeamX = beam_line.end;
         const lastBeamY = this.getSlopeY(lastBeamX, firstStemX, beamY, this.slope);
         const lastBeamY = this.getSlopeY(lastBeamX, firstStemX, beamY, this.slope);
@@ -841,7 +850,12 @@ export class Beam extends Element {
         this.context.lineTo(lastBeamX + 1, lastBeamY + beamThickness);
         this.context.lineTo(lastBeamX + 1, lastBeamY + beamThickness);
         this.context.lineTo(lastBeamX + 1, lastBeamY);
         this.context.lineTo(lastBeamX + 1, lastBeamY);
         this.context.closePath();
         this.context.closePath();
-        this.context.fill();
+        if (this.context.svg) {
+          const noteSVGId = Vex.Prefix(startNoteId);
+          this.context.fill({class: Vex.Prefix("beam"), id: `${noteSVGId}-beam${beamNumber}`});
+        } else {
+          this.context.fill();
+        }
       }
       }
 
 
       beamY += beamThickness * 1.5;
       beamY += beamThickness * 1.5;

+ 73 - 48
src/VexFlowPatch/src/stavenote.js

@@ -69,11 +69,16 @@ export class StaveNote extends StemmableNote {
     //   * 2 voices can be formatted *with or without* a stave being set but
     //   * 2 voices can be formatted *with or without* a stave being set but
     //     the output will be different
     //     the output will be different
     //   * 3 voices can only be formatted *without* a stave
     //   * 3 voices can only be formatted *without* a stave
-    if (notes[0].getStave()) {
-      return StaveNote.formatByY(notes, state);
-    }
+
+    // if this is enabled, notes are sometimes not staggered correctly, see setXShift lines below 
+    // if (notes[0].getStave()) {
+    //   return StaveNote.formatByY(notes, state); // 
+    // }
 
 
     const notesList = [];
     const notesList = [];
+    const stagger_same_whole_notes = notes[0].stagger_same_whole_notes;
+    // whether to stagger whole notes on the same line but different voice (show 2 instead of 1).
+    //   controlled by EngravingRules.StaggerSameWholeNotes
 
 
     for (let i = 0; i < notes.length; i++) {
     for (let i = 0; i < notes.length; i++) {
       const props = notes[i].getKeyProps();
       const props = notes[i].getKeyProps();
@@ -153,57 +158,74 @@ export class StaveNote extends StemmableNote {
           //If we are sharing a line, switch one notes stem direction.
           //If we are sharing a line, switch one notes stem direction.
           //If we are sharing a line and in the same voice, only then offset one note
           //If we are sharing a line and in the same voice, only then offset one note
           const lineDiff = Math.abs(noteU.line - noteL.line);
           const lineDiff = Math.abs(noteU.line - noteL.line);
-          if (noteU.note.glyph.stem && noteL.note.glyph.stem) {
-            //If we have different dot values, must offset
-            //Or If we have a non-filled in mixed with a filled in notehead, must offset
-            if ((noteU.note.duration === "h" && noteL.note.duration !== "h") || 
-                (noteU.note.duration !== "h" && noteL.note.duration === "h") ||
-                 noteU.note.dots !== noteL.note.dots) {
-                noteL.note.setXShift(xShift);
-                if (noteU.note.dots > 0) {
-                  let foundDots = 0;
-                  for (const modifier of noteU.note.modifiers) {
-                    if (modifier instanceof Dot) {
-                      foundDots++;
-                      //offset dot(s) above the shifted note
-                      //lines + 1 to negative pixels
-                      modifier.setYShift(-10 * (noteL.maxLine - noteU.line + 1));
-                      if (foundDots === noteU.note.dots) {
-                        break;
-                      }
-                    }
+          //if (noteU.note.glyph.stem && noteL.note.glyph.stem) { // skip this condition: whole notes also relevant
+          //If we have different dot values, must offset
+          //Or If we have a non-filled in mixed with a filled in notehead, must offset
+          let halfNoteCount = 0;
+          let wholeNoteCount = 0;
+          if (noteU.note.duration === "h") {
+            halfNoteCount++;
+          } else if (noteU.note.duration === "w") {
+            wholeNoteCount++;
+          }
+          if (noteL.note.duration === "h") {
+            halfNoteCount++;
+          } else if (noteL.note.duration === "w") {
+            wholeNoteCount++;
+          }
+          // only stagger/x-shift if one of the notes is whole or half note and the other isn't. (or dots different)
+          let staggerConditions = halfNoteCount === 1 || wholeNoteCount === 1 || noteU.note.dots !== noteL.note.dots;
+          if (stagger_same_whole_notes) { // controlled by EngravingRules.StaggerSameWholeNotes. see declaration above
+            staggerConditions = staggerConditions || wholeNoteCount === 2;
+            // should be ||=, but appveyor says syntax error, doesn't know the operator.
+          }
+          if (lineDiff === 0 && staggerConditions) {
+            noteL.note.setXShift(xShift);
+            if (noteU.note.dots > 0) {
+              let foundDots = 0;
+              for (const modifier of noteU.note.modifiers) {
+                if (modifier instanceof Dot) {
+                  foundDots++;
+                  //offset dot(s) above the shifted note
+                  //lines + 1 to negative pixels
+                  modifier.setYShift(-10 * (noteL.maxLine - noteU.line + 1));
+                  if (foundDots === noteU.note.dots) {
+                    break;
                   }
                   }
                 }
                 }
-            } else if (lineDiff < 1 && lineDiff > 0) {//if the notes are quite close but not on the same line, shift
-              noteL.note.setXShift(xShift);
-            } else if (noteU.note.voice !== noteL.note.voice) {//If we are not in the same voice
-              if (noteU.stemDirection === noteL.stemDirection) {
-                if (noteU.line > noteL.line) {
-                  //noteU is above noteL
-                  if (noteU.stemDirection === 1) {
-                    noteL.note.renderFlag = false;
-                  } else {
-                    noteU.note.renderFlag = false;
-                  }
-                } else if (noteL.line > noteU.line) {
-                  //note L is above noteU
-                  if (noteL.stemDirection === 1) {
-                    noteU.note.renderFlag = false;
-                  } else {
-                    noteL.note.renderFlag = false;
-                  }
+              }
+            }
+          } else if (lineDiff < 1 && lineDiff > 0) {//if the notes are quite close but not on the same line, shift
+            noteL.note.setXShift(xShift);
+          } else if (noteU.note.voice !== noteL.note.voice) {//If we are not in the same voice
+            if (noteU.stemDirection === noteL.stemDirection) {
+              if (noteU.line > noteL.line) {
+                //noteU is above noteL
+                if (noteU.stemDirection === 1) {
+                  noteL.note.renderFlag = false;
                 } else {
                 } else {
-                  //same line, swap stem direction for one note
-                  if (noteL.stemDirection === 1) {
-                    noteL.stemDirection = -1;
-                    noteL.note.setStemDirection(-1);
-                  }
+                  noteU.note.renderFlag = false;
+                }
+              } else if (noteL.line > noteU.line) {
+                //note L is above noteU
+                if (noteL.stemDirection === 1) {
+                  noteU.note.renderFlag = false;
+                } else {
+                  noteL.note.renderFlag = false;
+                }
+              } else {
+                //same line, swap stem direction for one note
+                if (noteL.stemDirection === 1) {
+                  noteL.stemDirection = -1;
+                  noteL.note.setStemDirection(-1);
                 }
                 }
               }
               }
-            } //Very close whole notes
-          } else if ((!noteU.note.glyph.stem && !noteL.note.glyph.stem && lineDiff < 1.5)) {
-            noteL.note.setXShift(xShift);
+            }
           }
           }
+          //Very close whole notes
+          // } else if ((!noteU.note.glyph.stem && !noteL.note.glyph.stem && lineDiff < 1.5)) {
+          //   noteL.note.setXShift(xShift);
+          // }
         }
         }
       }
       }
 
 
@@ -426,6 +448,7 @@ export class StaveNote extends StemmableNote {
   // Builds a `Stem` for the note
   // Builds a `Stem` for the note
   buildStem() {
   buildStem() {
     this.setStem(new Stem({ hide: !!this.isRest(), }));
     this.setStem(new Stem({ hide: !!this.isRest(), }));
+    this.stem.id = Vex.Prefix(`${this.getAttribute("id")}-stem`);
   }
   }
 
 
   // Builds a `NoteHead` for each key in the note
   // Builds a `NoteHead` for each key in the note
@@ -1157,6 +1180,8 @@ export class StaveNote extends StemmableNote {
     if (stemStruct) {
     if (stemStruct) {
       this.setStem(new Stem(stemStruct));
       this.setStem(new Stem(stemStruct));
     }
     }
+    // seems to not get called here, see this.stem.id above
+    this.stem.id = Vex.Prefix(`${this.getAttribute("id")}-stem`);
 
 
     if (this.stem) {
     if (this.stem) {
       this.context.openGroup('stem', null, { pointerBBox: true });
       this.context.openGroup('stem', null, { pointerBBox: true });

+ 175 - 0
src/VexFlowPatch/src/stem.js

@@ -0,0 +1,175 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+//
+// ## Description
+// This file implements the `Stem` object. Generally this object is handled
+// by its parent `StemmableNote`.
+
+import { Vex } from './vex';
+import { Element } from './element';
+import { Flow } from './tables';
+
+// To enable logging for this class. Set `Vex.Flow.Stem.DEBUG` to `true`.
+function L(...args) { if (Stem.DEBUG) Vex.L('Vex.Flow.Stem', args); }
+
+export class Stem extends Element {
+  static get CATEGORY() { return 'stem'; }
+
+  // Stem directions
+  static get UP() {
+    return 1;
+  }
+  static get DOWN() {
+    return -1;
+  }
+
+  // Theme
+  static get WIDTH() {
+    return Flow.STEM_WIDTH;
+  }
+  static get HEIGHT() {
+    return Flow.STEM_HEIGHT;
+  }
+
+  constructor(options = {}) {
+    super();
+    this.setAttribute('type', 'Stem');
+
+    // Default notehead x bounds
+    this.x_begin = options.x_begin || 0;
+    this.x_end = options.x_end || 0;
+
+    // Y bounds for top/bottom most notehead
+    this.y_top = options.y_top || 0;
+    this.y_bottom = options.y_bottom || 0;
+
+    // Stem top extension
+    this.stem_extension = options.stem_extension || 0;
+
+    // Direction of the stem
+    this.stem_direction = options.stem_direction || 0;
+
+    // Flag to override all draw calls
+    this.hide = options.hide || false;
+
+    this.isStemlet = options.isStemlet || false;
+    this.stemletHeight = options.stemletHeight || 0;
+
+    // Use to adjust the rendered height without affecting
+    // the results of `.getExtents()`
+    this.renderHeightAdjustment = 0;
+    this.setOptions(options);
+  }
+
+  setOptions(options) {
+    // Changing where the stem meets the head
+    this.stem_up_y_offset = options.stem_up_y_offset || 0;
+    this.stem_down_y_offset = options.stem_down_y_offset || 0;
+  }
+
+  // Set the x bounds for the default notehead
+  setNoteHeadXBounds(x_begin, x_end) {
+    this.x_begin = x_begin;
+    this.x_end = x_end;
+    return this;
+  }
+
+  // Set the direction of the stem in relation to the noteheads
+  setDirection(direction) { this.stem_direction = direction; }
+
+  // Set the extension for the stem, generally for flags or beams
+  setExtension(ext) { this.stem_extension = ext; }
+  getExtension() { return this.stem_extension; }
+
+  // The the y bounds for the top and bottom noteheads
+  setYBounds(y_top, y_bottom) {
+    this.y_top = y_top;
+    this.y_bottom = y_bottom;
+  }
+
+  // The category of the object
+  getCategory() { return Stem.CATEGORY; }
+
+  // Gets the entire height for the stem
+  getHeight() {
+    const y_offset = (this.stem_direction === Stem.UP) ? this.stem_up_y_offset : this.stem_down_y_offset; // eslint-disable-line max-len
+    return ((this.y_bottom - this.y_top) * this.stem_direction) +
+           ((Stem.HEIGHT - y_offset + this.stem_extension) * this.stem_direction);
+  }
+  getBoundingBox() {
+    throw new Vex.RERR('NotImplemented', 'getBoundingBox() not implemented.');
+  }
+
+  // Get the y coordinates for the very base of the stem to the top of
+  // the extension
+  getExtents() {
+    const isStemUp = this.stem_direction === Stem.UP;
+    const ys = [this.y_top, this.y_bottom];
+    const stemHeight = Stem.HEIGHT + this.stem_extension;
+
+    const innerMostNoteheadY = (isStemUp ? Math.min : Math.max)(...ys);
+    const outerMostNoteheadY = (isStemUp ? Math.max : Math.min)(...ys);
+    const stemTipY = innerMostNoteheadY + (stemHeight * -this.stem_direction);
+
+    return { topY: stemTipY, baseY: outerMostNoteheadY };
+  }
+
+  setVisibility(isVisible) {
+    this.hide = !isVisible;
+    return this;
+  }
+
+  setStemlet(isStemlet, stemletHeight) {
+    this.isStemlet = isStemlet;
+    this.stemletHeight = stemletHeight;
+    return this;
+  }
+
+  // Render the stem onto the canvas
+  draw() {
+    this.setRendered();
+    if (this.hide) return;
+    const ctx = this.checkContext();
+
+    let stem_x;
+    let stem_y;
+    const stem_direction = this.stem_direction;
+
+    if (stem_direction === Stem.DOWN) {
+      // Down stems are rendered to the left of the head.
+      stem_x = this.x_begin;
+      stem_y = this.y_top + this.stem_down_y_offset;
+    } else {
+      // Up stems are rendered to the right of the head.
+      stem_x = this.x_end;
+      stem_y = this.y_bottom - this.stem_up_y_offset;
+    }
+
+    const stemHeight = this.getHeight();
+
+    L('Rendering stem - ', 'Top Y: ', this.y_top, 'Bottom Y: ', this.y_bottom);
+
+    // The offset from the stem's base which is required fo satisfy the stemlet height
+    const stemletYOffset = this.isStemlet
+      ? stemHeight - this.stemletHeight * this.stem_direction
+      : 0;
+
+    // Draw the stem
+    ctx.save();
+    this.applyStyle(ctx);
+    ctx.beginPath();
+    ctx.setLineWidth(Stem.WIDTH);
+    ctx.moveTo(stem_x, stem_y - stemletYOffset);
+    ctx.lineTo(stem_x, stem_y - stemHeight - (this.renderHeightAdjustment * stem_direction));
+    if (ctx.svg) {
+        const strokeAttributes = {class: Vex.Prefix("stem")};
+        if (this.id) {
+            strokeAttributes.id = this.id;
+        }
+        ctx.stroke(strokeAttributes);
+    } else {
+        ctx.stroke();
+    }
+    this.restoreStyle(ctx);
+    ctx.restore();
+  }
+}

+ 692 - 0
src/VexFlowPatch/src/svgcontext.js

@@ -0,0 +1,692 @@
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
+// @author Gregory Ristow (2015)
+
+import { Vex } from './vex';
+
+const attrNamesToIgnoreMap = {
+  path: {
+    x: true,
+    y: true,
+    width: true,
+    height: true,
+  },
+  rect: {
+  },
+  text: {
+    width: true,
+    height: true,
+  },
+};
+
+{
+  const fontAttrNamesToIgnore = {
+    'font-family': true,
+    'font-weight': true,
+    'font-style': true,
+    'font-size': true,
+  };
+
+  Vex.Merge(attrNamesToIgnoreMap.rect, fontAttrNamesToIgnore);
+  Vex.Merge(attrNamesToIgnoreMap.path, fontAttrNamesToIgnore);
+}
+
+export class SVGContext {
+  constructor(element) {
+    // element is the parent DOM object
+    this.element = element;
+    // Create the SVG in the SVG namespace:
+    this.svgNS = 'http://www.w3.org/2000/svg';
+    const svg = this.create('svg');
+    // Add it to the canvas:
+    this.element.appendChild(svg);
+
+    // Point to it:
+    this.svg = svg;
+    this.groups = [this.svg]; // Create the group stack
+    this.parent = this.svg;
+
+    this.path = '';
+    this.pen = { x: NaN, y: NaN };
+    this.lineWidth = 1.0;
+    this.state = {
+      scale: { x: 1, y: 1 },
+      'font-family': 'Arial',
+      'font-size': '8pt',
+      'font-weight': 'normal',
+    };
+
+    this.attributes = {
+      'stroke-width': 0.3,
+      'fill': 'black',
+      'stroke': 'black',
+      'stroke-dasharray': 'none',
+      'font-family': 'Arial',
+      'font-size': '10pt',
+      'font-weight': 'normal',
+      'font-style': 'normal',
+    };
+
+    this.background_attributes = {
+      'stroke-width': 0,
+      'fill': 'white',
+      'stroke': 'white',
+      'stroke-dasharray': 'none',
+      'font-family': 'Arial',
+      'font-size': '10pt',
+      'font-weight': 'normal',
+      'font-style': 'normal',
+    };
+
+    this.shadow_attributes = {
+      width: 0,
+      color: 'black',
+    };
+
+    this.state_stack = [];
+
+    // Test for Internet Explorer
+    this.iePolyfill();
+  }
+
+  create(svgElementType) {
+    return document.createElementNS(this.svgNS, svgElementType);
+  }
+
+  // Allow grouping elements in containers for interactivity.
+  openGroup(cls, id, attrs) {
+    const group = this.create('g');
+    this.groups.push(group);
+    this.parent.appendChild(group);
+    this.parent = group;
+    if (cls) group.setAttribute('class', Vex.Prefix(cls));
+    if (id) group.setAttribute('id', Vex.Prefix(id));
+
+    if (attrs && attrs.pointerBBox) {
+      group.setAttribute('pointer-events', 'bounding-box');
+    }
+    return group;
+  }
+
+  closeGroup() {
+    this.groups.pop();
+    this.parent = this.groups[this.groups.length - 1];
+  }
+
+  add(elem) {
+    this.parent.appendChild(elem);
+  }
+
+  // Tests if the browser is Internet Explorer; if it is,
+  // we do some tricks to improve text layout.  See the
+  // note at ieMeasureTextFix() for details.
+  iePolyfill() {
+    if (typeof (navigator) !== 'undefined') {
+      this.ie = (
+        /MSIE 9/i.test(navigator.userAgent) ||
+        /MSIE 10/i.test(navigator.userAgent) ||
+        /rv:11\.0/i.test(navigator.userAgent) ||
+        /Trident/i.test(navigator.userAgent)
+      );
+    }
+  }
+
+  // ### Styling & State Methods:
+
+  setFont(family, size, weight) {
+    // Unlike canvas, in SVG italic is handled by font-style,
+    // not weight. So: we search the weight argument and
+    // apply bold and italic to weight and style respectively.
+    let bold = false;
+    let italic = false;
+    let style = 'normal';
+    // Weight might also be a number (200, 400, etc...) so we
+    // test its type to be sure we have access to String methods.
+    if (typeof weight === 'string') {
+      // look for "italic" in the weight:
+      if (weight.indexOf('italic') !== -1) {
+        weight = weight.replace(/italic/g, '');
+        italic = true;
+      }
+      // look for "bold" in weight
+      if (weight.indexOf('bold') !== -1) {
+        weight = weight.replace(/bold/g, '');
+        bold = true;
+      }
+      // remove any remaining spaces
+      weight = weight.replace(/ /g, '');
+    }
+    weight = bold ? 'bold' : weight;
+    weight = (typeof weight === 'undefined' || weight === '') ? 'normal' : weight;
+
+    style = italic ? 'italic' : style;
+
+    const fontAttributes = {
+      'font-family': family,
+      'font-size': size + 'pt',
+      'font-weight': weight,
+      'font-style': style,
+    };
+
+    // Store the font size so that if the browser is Internet
+    // Explorer we can fix its calculations of text width.
+    this.fontSize = Number(size);
+
+    Vex.Merge(this.attributes, fontAttributes);
+    Vex.Merge(this.state, fontAttributes);
+
+    return this;
+  }
+
+  setRawFont(font) {
+    font = font.trim();
+    // Assumes size first, splits on space -- which is presently
+    // how all existing modules are calling this.
+    const fontArray = font.split(' ');
+
+    this.attributes['font-family'] = fontArray[1];
+    this.state['font-family'] = fontArray[1];
+
+    this.attributes['font-size'] = fontArray[0];
+    this.state['font-size'] = fontArray[0];
+
+    // Saves fontSize for IE polyfill
+    this.fontSize = Number(fontArray[0].match(/\d+/));
+    return this;
+  }
+
+  setFillStyle(style) {
+    this.attributes.fill = style;
+    return this;
+  }
+
+  setBackgroundFillStyle(style) {
+    this.background_attributes.fill = style;
+    this.background_attributes.stroke = style;
+    return this;
+  }
+
+  setStrokeStyle(style) {
+    this.attributes.stroke = style;
+    return this;
+  }
+
+  setShadowColor(style) {
+    this.shadow_attributes.color = style;
+    return this;
+  }
+
+  setShadowBlur(blur) {
+    this.shadow_attributes.width = blur;
+    return this;
+  }
+
+  setLineWidth(width) {
+    this.attributes['stroke-width'] = width;
+    this.lineWidth = width;
+  }
+
+  // @param array {lineDash} as [dashInt, spaceInt, dashInt, spaceInt, etc...]
+  setLineDash(lineDash) {
+    if (Object.prototype.toString.call(lineDash) === '[object Array]') {
+      lineDash = lineDash.join(', ');
+      this.attributes['stroke-dasharray'] = lineDash;
+      return this;
+    } else {
+      throw new Vex.RERR('ArgumentError', 'lineDash must be an array of integers.');
+    }
+  }
+
+  setLineCap(lineCap) {
+    this.attributes['stroke-linecap'] = lineCap;
+    return this;
+  }
+
+  // ### Sizing & Scaling Methods:
+
+  // TODO (GCR): See note at scale() -- seperate our internal
+  // conception of pixel-based width/height from the style.width
+  // and style.height properties eventually to allow users to
+  // apply responsive sizing attributes to the SVG.
+  resize(width, height) {
+    this.width = width;
+    this.height = height;
+    this.element.style.width = width;
+    const attributes = {
+      width,
+      height,
+    };
+    this.applyAttributes(this.svg, attributes);
+    this.scale(this.state.scale.x, this.state.scale.y);
+    return this;
+  }
+
+  scale(x, y) {
+    // uses viewBox to scale
+    // TODO (GCR): we may at some point want to distinguish the
+    // style.width / style.height properties that are applied to
+    // the SVG object from our internal conception of the SVG
+    // width/height.  This would allow us to create automatically
+    // scaling SVG's that filled their containers, for instance.
+    //
+    // As this isn't implemented in Canvas or Raphael contexts,
+    // I've left as is for now, but in using the viewBox to
+    // handle internal scaling, am trying to make it possible
+    // for us to eventually move in that direction.
+
+    this.state.scale = { x, y };
+    const visibleWidth = this.width / x;
+    const visibleHeight = this.height / y;
+    this.setViewBox(0, 0, visibleWidth, visibleHeight);
+
+    return this;
+  }
+
+  setViewBox(...args) {
+    // Override for "x y w h" style:
+    if (args.length === 1) {
+      const [viewBox] = args;
+      this.svg.setAttribute('viewBox', viewBox);
+    } else {
+      const [xMin, yMin, width, height] = args;
+      const viewBoxString = xMin + ' ' + yMin + ' ' + width + ' ' + height;
+      this.svg.setAttribute('viewBox', viewBoxString);
+    }
+  }
+
+  // ### Drawing helper methods:
+
+  applyAttributes(element, attributes) {
+    const attrNamesToIgnore = attrNamesToIgnoreMap[element.nodeName];
+    Object
+      .keys(attributes)
+      .forEach(propertyName => {
+        if (attrNamesToIgnore && attrNamesToIgnore[propertyName]) {
+          return;
+        }
+        element.setAttributeNS(null, propertyName, attributes[propertyName]);
+      });
+
+    return element;
+  }
+
+  // ### Shape & Path Methods:
+
+  clear() {
+    // Clear the SVG by removing all inner children.
+
+    // (This approach is usually slightly more efficient
+    // than removing the old SVG & adding a new one to
+    // the container element, since it does not cause the
+    // container to resize twice.  Also, the resize
+    // triggered by removing the entire SVG can trigger
+    // a touchcancel event when the element resizes away
+    // from a touch point.)
+
+    while (this.svg.lastChild) {
+      this.svg.removeChild(this.svg.lastChild);
+    }
+
+    // Replace the viewbox attribute we just removed:
+    this.scale(this.state.scale.x, this.state.scale.y);
+  }
+
+  // ## Rectangles:
+
+  rect(x, y, width, height, attributes) {
+    // Avoid invalid negative height attribs by
+    // flipping the rectangle on its head:
+    if (height < 0) {
+      y += height;
+      height *= -1;
+    }
+
+    // Create the rect & style it:
+    const rectangle = this.create('rect');
+    if (typeof attributes === 'undefined') {
+      attributes = {
+        fill: 'none',
+        'stroke-width': this.lineWidth,
+        stroke: 'black',
+      };
+    }
+
+    Vex.Merge(attributes, {
+      x,
+      y,
+      width,
+      height,
+    });
+
+    this.applyAttributes(rectangle, attributes);
+
+    this.add(rectangle);
+    return this;
+  }
+
+  fillRect(x, y, width, height) {
+    if (height < 0) {
+      y += height;
+      height *= -1;
+    }
+
+    this.rect(x, y, width, height, this.attributes);
+    return this;
+  }
+
+  clearRect(x, y, width, height) {
+    // TODO(GCR): Improve implementation of this...
+    // Currently it draws a box of the background color, rather
+    // than creating alpha through lower z-levels.
+    //
+    // See the implementation of this in SVGKit:
+    // http://sourceforge.net/projects/svgkit/
+    // as a starting point.
+    //
+    // Adding a large number of transform paths (as we would
+    // have to do) could be a real performance hit.  Since
+    // tabNote seems to be the only module that makes use of this
+    // it may be worth creating a seperate tabStave that would
+    // draw lines around locations of tablature fingering.
+    //
+
+    this.rect(x, y, width, height, this.background_attributes);
+    return this;
+  }
+
+  // ## Paths:
+
+  beginPath() {
+    this.path = '';
+    this.pen.x = NaN;
+    this.pen.y = NaN;
+
+    return this;
+  }
+
+  moveTo(x, y) {
+    this.path += 'M' + x + ' ' + y;
+    this.pen.x = x;
+    this.pen.y = y;
+    return this;
+  }
+
+  lineTo(x, y) {
+    this.path += 'L' + x + ' ' + y;
+    this.pen.x = x;
+    this.pen.y = y;
+    return this;
+  }
+
+  bezierCurveTo(x1, y1, x2, y2, x, y) {
+    this.path += 'C' +
+      x1 + ' ' +
+      y1 + ',' +
+      x2 + ' ' +
+      y2 + ',' +
+      x + ' ' +
+      y;
+    this.pen.x = x;
+    this.pen.y = y;
+    return this;
+  }
+
+  quadraticCurveTo(x1, y1, x, y) {
+    this.path += 'Q' +
+      x1 + ' ' +
+      y1 + ',' +
+      x + ' ' +
+      y;
+    this.pen.x = x;
+    this.pen.y = y;
+    return this;
+  }
+
+  // This is an attempt (hack) to simulate the HTML5 canvas
+  // arc method.
+  arc(x, y, radius, startAngle, endAngle, antiClockwise) {
+    function normalizeAngle(angle) {
+      while (angle < 0) {
+        angle += Math.PI * 2;
+      }
+
+      while (angle > Math.PI * 2) {
+        angle -= Math.PI * 2;
+      }
+      return angle;
+    }
+
+    startAngle = normalizeAngle(startAngle);
+    endAngle = normalizeAngle(endAngle);
+
+    if (startAngle > endAngle) {
+      const tmp = startAngle;
+      startAngle = endAngle;
+      endAngle = tmp;
+      antiClockwise = !antiClockwise;
+    }
+
+    const delta = endAngle - startAngle;
+
+    if (delta > Math.PI) {
+      this.arcHelper(x, y, radius, startAngle, startAngle + delta / 2, antiClockwise);
+      this.arcHelper(x, y, radius, startAngle + delta / 2, endAngle, antiClockwise);
+    } else {
+      this.arcHelper(x, y, radius, startAngle, endAngle, antiClockwise);
+    }
+    return this;
+  }
+
+  arcHelper(x, y, radius, startAngle, endAngle, antiClockwise) {
+    const x1 = x + radius * Math.cos(startAngle);
+    const y1 = y + radius * Math.sin(startAngle);
+
+    const x2 = x + radius * Math.cos(endAngle);
+    const y2 = y + radius * Math.sin(endAngle);
+
+    let largeArcFlag = 0;
+    let sweepFlag = 0;
+    if (antiClockwise) {
+      sweepFlag = 1;
+      if (endAngle - startAngle < Math.PI) {
+        largeArcFlag = 1;
+      }
+    } else if (endAngle - startAngle > Math.PI) {
+      largeArcFlag = 1;
+    }
+
+    this.path += 'M' + x1 + ' ' + y1 + ' A' +
+      radius + ' ' + radius + ' 0 ' + largeArcFlag + ' ' + sweepFlag + ' ' +
+      x2 + ' ' + y2;
+    if (!isNaN(this.pen.x) && !isNaN(this.pen.y)) {
+      this.peth += 'M' + this.pen.x + ' ' + this.pen.y;
+    }
+  }
+
+  closePath() {
+    this.path += 'Z';
+
+    return this;
+  }
+
+  // Adapted from the source for Raphael's Element.glow
+  glow() {
+    // Calculate the width & paths of the glow:
+    if (this.shadow_attributes.width > 0) {
+      const sa = this.shadow_attributes;
+      const num_paths = sa.width / 2;
+      // Stroke at varying widths to create effect of gaussian blur:
+      for (let i = 1; i <= num_paths; i++) {
+        const attributes = {
+          stroke: sa.color,
+          'stroke-linejoin': 'round',
+          'stroke-linecap': 'round',
+          'stroke-width': +((sa.width * 0.4) / num_paths * i).toFixed(3),
+          opacity: +((sa.opacity || 0.3) / num_paths).toFixed(3),
+        };
+
+        const path = this.create('path');
+        attributes.d = this.path;
+        this.applyAttributes(path, attributes);
+        this.add(path);
+      }
+    }
+    return this;
+  }
+
+  fill(attributes) {
+    // If our current path is set to glow, make it glow
+    this.glow();
+
+    const path = this.create('path');
+    if (typeof attributes === 'undefined') {
+        attributes = {};
+        Vex.Merge(attributes, this.attributes);
+        attributes.stroke = 'none';
+    }
+    
+    attributes.d = this.path;
+    //attributes.class = "testbeam";
+    
+    this.applyAttributes(path, attributes);
+    this.add(path);
+    return this;
+  }
+
+  stroke(extraAttributes = undefined) {
+    // If our current path is set to glow, make it glow.
+    this.glow();
+
+    const path = this.create('path');
+    const attributes = {};
+    Vex.Merge(attributes, this.attributes);
+    if (extraAttributes) {
+        Vex.Merge(attributes, extraAttributes);
+    }
+    attributes.fill = 'none';
+    attributes['stroke-width'] = this.lineWidth;
+    attributes.d = this.path;
+
+    this.applyAttributes(path, attributes);
+    this.add(path);
+    return this;
+  }
+
+  // ## Text Methods:
+  measureText(text) {
+    const txt = this.create('text');
+    if (typeof (txt.getBBox) !== 'function') {
+      return { x: 0, y: 0, width: 0, height: 0 };
+    }
+
+    txt.textContent = text;
+    this.applyAttributes(txt, this.attributes);
+
+    // Temporarily add it to the document for measurement.
+    this.svg.appendChild(txt);
+
+    let bbox = txt.getBBox();
+    if (this.ie && text !== '' && this.attributes['font-style'] === 'italic') {
+      bbox = this.ieMeasureTextFix(bbox, text);
+    }
+
+    this.svg.removeChild(txt);
+    return bbox;
+  }
+
+  ieMeasureTextFix(bbox) {
+    // Internet Explorer over-pads text in italics,
+    // resulting in giant width estimates for measureText.
+    // To fix this, we use this formula, tested against
+    // ie 11:
+    // overestimate (in pixels) = FontSize(in pt) * 1.196 + 1.96
+    // And then subtract the overestimate from calculated width.
+
+    const fontSize = Number(this.fontSize);
+    const m = 1.196;
+    const b = 1.9598;
+    const widthCorrection = (m * fontSize) + b;
+    const width = bbox.width - widthCorrection;
+    const height = bbox.height - 1.5;
+
+    // Get non-protected copy:
+    const box = {
+      x: bbox.x,
+      y: bbox.y,
+      width,
+      height,
+    };
+
+    return box;
+  }
+
+  fillText(text, x, y) {
+    if (!text || text.length <= 0) {
+      return;
+    }
+    const attributes = {};
+    Vex.Merge(attributes, this.attributes);
+    attributes.stroke = 'none';
+    attributes.x = x;
+    attributes.y = y;
+
+    const txt = this.create('text');
+    txt.textContent = text;
+    this.applyAttributes(txt, attributes);
+    this.add(txt);
+  }
+
+  save() {
+    // TODO(mmuthanna): State needs to be deep-copied.
+    this.state_stack.push({
+      state: {
+        'font-family': this.state['font-family'],
+        'font-weight': this.state['font-weight'],
+        'font-style': this.state['font-style'],
+        'font-size': this.state['font-size'],
+        scale: this.state.scale,
+      },
+      attributes: {
+        'font-family': this.attributes['font-family'],
+        'font-weight': this.attributes['font-weight'],
+        'font-style': this.attributes['font-style'],
+        'font-size': this.attributes['font-size'],
+        fill: this.attributes.fill,
+        stroke: this.attributes.stroke,
+        'stroke-width': this.attributes['stroke-width'],
+        'stroke-dasharray': this.attributes['stroke-dasharray'],
+      },
+      shadow_attributes: {
+        width: this.shadow_attributes.width,
+        color: this.shadow_attributes.color,
+      },
+      lineWidth: this.lineWidth,
+    });
+    return this;
+  }
+
+  restore() {
+    // TODO(0xfe): State needs to be deep-restored.
+    const state = this.state_stack.pop();
+    this.state['font-family'] = state.state['font-family'];
+    this.state['font-weight'] = state.state['font-weight'];
+    this.state['font-style'] = state.state['font-style'];
+    this.state['font-size'] = state.state['font-size'];
+    this.state.scale = state.state.scale;
+
+    this.attributes['font-family'] = state.attributes['font-family'];
+    this.attributes['font-weight'] = state.attributes['font-weight'];
+    this.attributes['font-style'] = state.attributes['font-style'];
+    this.attributes['font-size'] = state.attributes['font-size'];
+
+    this.attributes.fill = state.attributes.fill;
+    this.attributes.stroke = state.attributes.stroke;
+    this.attributes['stroke-width'] = state.attributes['stroke-width'];
+    this.attributes['stroke-dasharray'] = state.attributes['stroke-dasharray'];
+
+    this.shadow_attributes.width = state.shadow_attributes.width;
+    this.shadow_attributes.color = state.shadow_attributes.color;
+
+    this.lineWidth = state.lineWidth;
+    return this;
+  }
+}

+ 1 - 1
test/Common/FileIO/Xml_Test.ts

@@ -53,7 +53,7 @@ describe("XML interface", () => {
                 TestUtils.createOpenSheetMusicDisplay(div);
                 TestUtils.createOpenSheetMusicDisplay(div);
             openSheetMusicDisplay.load(score);
             openSheetMusicDisplay.load(score);
             done();
             done();
-        }).timeout(10000);
+        }).timeout(30000);
     }
     }
 
 
     it("test IXmlElement", (done: Mocha.Done) => {
     it("test IXmlElement", (done: Mocha.Done) => {

+ 47 - 0
test/MusicalScore/Graphical/VexFlow/VexFlowGraphicalNote_Test.ts

@@ -0,0 +1,47 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions */
+import { GraphicalMeasure } from "../../../../src/MusicalScore/Graphical/GraphicalMeasure";
+import { VexFlowGraphicalNote } from "../../../../src/MusicalScore/Graphical/VexFlow/VexFlowGraphicalNote";
+import { OpenSheetMusicDisplay } from "../../../../src/OpenSheetMusicDisplay/OpenSheetMusicDisplay";
+import { TestUtils } from "../../../Util/TestUtils";
+
+describe("VexFlow GraphicalNote", () => {
+    it("Can get SVG elements for note, stem and beam", (done: Mocha.Done) => {
+        //const url: string = "base/test/data/test_rest_positioning_8th_quarter.musicxml"; // doesn't work, works for Mozart Clarinet Quintet
+        const score: Document = TestUtils.getScore("test_beam_svg_double.musicxml");
+        // sample should start with a beamed 8th note, and be simple.
+        const div: HTMLElement = TestUtils.getDivElement(document);
+        const osmd: OpenSheetMusicDisplay = TestUtils.createOpenSheetMusicDisplay(div);
+        // we need this way of creating the score to get the SVG elements, doesn't work with creating MusicSheet by hand
+        osmd.load(score).then(
+            (_: {}) => {
+                 osmd.render();
+                 const gm: GraphicalMeasure = osmd.GraphicSheet.findGraphicalMeasure(0, 0);
+                 const note1: VexFlowGraphicalNote = (gm.staffEntries[0].graphicalVoiceEntries[0].notes[0] as VexFlowGraphicalNote);
+                 const noteSVG: SVGGElement = note1.getSVGGElement();
+                 chai.expect(noteSVG).to.not.be.null;
+                 chai.expect(noteSVG).to.not.be.undefined;
+                 // const noteSVGId: string = "vf-" + firstNote.getSVGId();
+                 // const noteSVG: HTMLElement = document.getElementById(noteSVGId);
+                 //const stemSVGId: string = noteSVGId + "-stem";
+                 //const stemSVG: HTMLElement = document.getElementById(stemSVGId);
+                 const stemSVG: HTMLElement = note1.getStemSVG();
+                 chai.expect(stemSVG).to.not.be.null;
+                 chai.expect(stemSVG).to.not.be.undefined;
+                 // const beamSVGId: string = noteSVGId + "-beam";
+                 // const beamSVG: HTMLElement = document.getElementById(beamSVGId);
+                 const beamSVGs: HTMLElement[] = note1.getBeamSVGs();
+                 chai.expect(beamSVGs.length).to.equal(1); // 8th beam start. (16th beam starts on note2)
+                 chai.expect(beamSVGs[0]).to.not.be.null;
+                 chai.expect(beamSVGs[0]).to.not.be.undefined;
+                 const note2: VexFlowGraphicalNote = (gm.staffEntries[1].graphicalVoiceEntries[0].notes[0] as VexFlowGraphicalNote);
+                 chai.expect(note2.getBeamSVGs().length).to.equal(1); // start of 16th beam
+                 const note3: VexFlowGraphicalNote = (gm.staffEntries[2].graphicalVoiceEntries[0].notes[0] as VexFlowGraphicalNote);
+                 chai.expect(note3.getBeamSVGs().length).to.equal(0); // end of 16th beam
+                 const note4: VexFlowGraphicalNote = (gm.staffEntries[3].graphicalVoiceEntries[0].notes[0] as VexFlowGraphicalNote);
+                 chai.expect(note4.getBeamSVGs().length).to.equal(2); // 16th beams start
+                 done();
+            },
+            done
+        );
+     });
+});

+ 2 - 18
test/MusicalScore/Graphical/VexFlow/VexFlowMeasure_Test.ts

@@ -7,13 +7,12 @@ import {VexFlowMusicSheetCalculator} from "../../../../src/MusicalScore/Graphica
 import {TestUtils} from "../../../Util/TestUtils";
 import {TestUtils} from "../../../Util/TestUtils";
 import {SourceMeasure} from "../../../../src/MusicalScore/VoiceData/SourceMeasure";
 import {SourceMeasure} from "../../../../src/MusicalScore/VoiceData/SourceMeasure";
 import {SourceStaffEntry} from "../../../../src/MusicalScore/VoiceData/SourceStaffEntry";
 import {SourceStaffEntry} from "../../../../src/MusicalScore/VoiceData/SourceStaffEntry";
-import {GraphicalMeasure} from "../../../../src/MusicalScore/Graphical/GraphicalMeasure";
 import {MusicSheetCalculator} from "../../../../src/MusicalScore/Graphical/MusicSheetCalculator";
 import {MusicSheetCalculator} from "../../../../src/MusicalScore/Graphical/MusicSheetCalculator";
 import {EngravingRules} from "../../../../src/MusicalScore/Graphical/EngravingRules";
 import {EngravingRules} from "../../../../src/MusicalScore/Graphical/EngravingRules";
 
 
 describe("VexFlow Measure", () => {
 describe("VexFlow Measure", () => {
 
 
-   it("GraphicalMusicSheet", (done: Mocha.Done) => {
+   it("Can create GraphicalMusicSheet", (done: Mocha.Done) => {
       const path: string = "MuzioClementi_SonatinaOpus36No1_Part1.xml";
       const path: string = "MuzioClementi_SonatinaOpus36No1_Part1.xml";
       const score: Document = TestUtils.getScore(path);
       const score: Document = TestUtils.getScore(path);
       chai.expect(score).to.not.be.undefined;
       chai.expect(score).to.not.be.undefined;
@@ -28,22 +27,7 @@ describe("VexFlow Measure", () => {
       done();
       done();
    });
    });
 
 
-   it("Simple Measure", (done: Mocha.Done) => {
-      const sheet: MusicSheet = new MusicSheet();
-      sheet.Rules = new EngravingRules();
-      const measure: SourceMeasure = new SourceMeasure(1, sheet.Rules);
-      sheet.addMeasure(measure);
-      const calc: MusicSheetCalculator = new VexFlowMusicSheetCalculator(sheet.Rules);
-      const gms: GraphicalMusicSheet = new GraphicalMusicSheet(sheet, calc);
-      chai.expect(gms.MeasureList.length).to.equal(1);
-      chai.expect(gms.MeasureList[0].length).to.equal(1);
-      const gm: GraphicalMeasure = gms.MeasureList[0][0];
-      // console.log(gm);
-      chai.expect(gm).to.not.be.undefined; // at least necessary for linter so that variable is not unused
-      done();
-   });
-
-   it("Empty Measure", (done: Mocha.Done) => {
+   it("Can have a single empty Measure", (done: Mocha.Done) => {
       const sheet: MusicSheet = new MusicSheet();
       const sheet: MusicSheet = new MusicSheet();
       sheet.Rules = new EngravingRules();
       sheet.Rules = new EngravingRules();
       const measure: SourceMeasure = new SourceMeasure(1, sheet.Rules);
       const measure: SourceMeasure = new SourceMeasure(1, sheet.Rules);

+ 1 - 1
test/MusicalScore/ScoreCalculation/MusicSheetCalculator_Test.ts

@@ -17,7 +17,7 @@ describe("Music Sheet Calculator", () => {
     let sheet: MusicSheet;
     let sheet: MusicSheet;
 
 
     it("calculates music sheet", (done: Mocha.Done) => {
     it("calculates music sheet", (done: Mocha.Done) => {
-        this.timeout = 10000;
+        // this.timeout = 10000;
         MusicSheetCalculator.TextMeasurer = new VexFlowTextMeasurer(new EngravingRules());
         MusicSheetCalculator.TextMeasurer = new VexFlowTextMeasurer(new EngravingRules());
         // Load the XML file
         // Load the XML file
         const xml: Document = TestUtils.getScore(filename);
         const xml: Document = TestUtils.getScore(filename);

+ 1 - 1
test/Util/DiffImages_Test_Experimental.ts

@@ -58,6 +58,6 @@ describe("GeneratePNGImages", () => {
             //fs.writeFileSync(fileName, imageBuffer, { encoding: "base64" });
             //fs.writeFileSync(fileName, imageBuffer, { encoding: "base64" });
 
 
             done();
             done();
-            }).timeout(10000);
+            }).timeout(30000);
     }
     }
 });
 });

+ 1 - 1
test/Util/generateDiffImagesPuppeteerLocalhost.js

@@ -8,7 +8,7 @@
   npm i puppeteer --save-dev
   npm i puppeteer --save-dev
   (will download ~100MB for Chromium)
   (will download ~100MB for Chromium)
 
 
-  This script is made obsolete by the ~2x faster generateImages_browserless.js,
+  This script is made obsolete by the ~2x faster generateImages_browserless.mjs,
   but may be useful for comparison.
   but may be useful for comparison.
 
 
   inspired by Vexflow's generate_png_images and vexflow-tests.js
   inspired by Vexflow's generate_png_images and vexflow-tests.js

+ 399 - 0
test/Util/generateImages_browserless.mjs

@@ -0,0 +1,399 @@
+import Blob from "cross-blob";
+import FS from "fs";
+import jsdom from "jsdom";
+import OSMD from "../../build/opensheetmusicdisplay.min.js"; // window needs to be available before we can require OSMD
+/*
+  Render each OSMD sample, grab the generated images, and
+  dump them into a local directory as PNG or SVG files.
+
+  inspired by Vexflow's generate_png_images and vexflow-tests.js
+
+  This can be used to generate PNGs or SVGs from OSMD without a browser.
+  It's also used with the visual regression test system (using PNGs) in
+  `tools/visual_regression.sh`
+  (see package.json, used with npm run generate:blessed and generate:current, then test:visual).
+
+  Note: this script needs to "fake" quite a few browser elements, like window, document,
+  and a Canvas HTMLElement (for PNG) or the DOM (for SVG)   ,
+  which otherwise are missing in pure nodejs, causing errors in OSMD.
+  For PNG it needs the canvas package installed.
+  There are also some hacks needed to set the container size (offsetWidth) correctly.
+
+  Otherwise you'd need to run a headless browser, which is way slower,
+  see the semi-obsolete generateDiffImagesPuppeteerLocalhost.js
+*/
+
+function sleep (ms) {
+    return new Promise((resolve) => {
+        setTimeout(resolve, ms);
+    });
+}
+
+// global variables
+//   (without these being global, we'd have to pass many of these values to the generateSampleImage function)
+// eslint-disable-next-line prefer-const
+let [osmdBuildDir, sampleDir, imageDir, imageFormat, pageWidth, pageHeight, filterRegex, mode, debugSleepTimeString] = process.argv.slice(2, 11);
+if (!osmdBuildDir || !sampleDir || !imageDir || (imageFormat !== "png" && imageFormat !== "svg")) {
+    console.log("usage: " +
+        // eslint-disable-next-line max-len
+        "node test/Util/generateImages_browserless.mjs osmdBuildDir sampleDirectory imageDirectory svg|png [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]");
+    console.log("  (use pageWidth and pageHeight 0 to not divide the rendering into pages (endless page))");
+    console.log('  (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)');
+    console.log("example: node test/Util/generateImages_browserless.mjs ../../build ./test/data/ ./export png 210 297 allSmall --debug 5000");
+    console.log("Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.");
+    process.exit(1);
+}
+let pageFormat;
+
+if (!mode) {
+    mode = "";
+}
+if (imageFormat !== "svg") {
+    imageFormat = "png";
+}
+
+// let OSMD; // can only be required once window was simulated
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+
+async function init () {
+    console.log("[OSMD.generateImages] init");
+
+    const osmdTestingMode = mode.includes("osmdtesting"); // can also be --debugosmdtesting
+    const osmdTestingSingleMode = mode.includes("osmdtestingsingle");
+    const DEBUG = mode.startsWith("--debug");
+    // const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
+    if (DEBUG) {
+        // console.log(' (note that --debug slows down the script by about 0.3s per file, through logging)')
+        const debugSleepTimeMs = Number.parseInt(debugSleepTimeString, 10);
+        if (debugSleepTimeMs > 0) {
+            console.log("debug sleep time: " + debugSleepTimeString);
+            await sleep(Number.parseInt(debugSleepTimeMs, 10));
+            // [VSCode] apparently this is necessary for the debugger to attach itself in time before the program closes.
+            // sometimes this is not enough, so you may have to try multiple times or increase the sleep timer. Unfortunately debugging nodejs isn't easy.
+        }
+    }
+    debug("sampleDir: " + sampleDir, DEBUG);
+    debug("imageDir: " + imageDir, DEBUG);
+    debug("imageFormat: " + imageFormat, DEBUG);
+
+    pageFormat = "Endless";
+    pageWidth = Number.parseInt(pageWidth, 10);
+    pageHeight = Number.parseInt(pageHeight, 10);
+    const endlessPage = !(pageHeight > 0 && pageWidth > 0);
+    if (!endlessPage) {
+        pageFormat = `${pageWidth}x${pageHeight}`;
+    }
+
+    // ---- hacks to fake Browser elements OSMD and Vexflow need, like window, document, and a canvas HTMLElement ----
+    // eslint-disable-next-line @typescript-eslint/no-var-requires
+    const dom = new jsdom.JSDOM("<!DOCTYPE html></html>");
+    // eslint-disable-next-line no-global-assign
+    // window = dom.window;
+    // eslint-disable-next-line no-global-assign
+    // document = dom.window.document;
+
+    // eslint-disable-next-line no-global-assign
+    global.window = dom.window;
+    // eslint-disable-next-line no-global-assign
+    global.document = window.document;
+    //window.console = console; // probably does nothing
+    global.HTMLElement = window.HTMLElement;
+    global.HTMLAnchorElement = window.HTMLAnchorElement;
+    global.XMLHttpRequest = window.XMLHttpRequest;
+    global.DOMParser = window.DOMParser;
+    global.Node = window.Node;
+    if (imageFormat === "png") {
+        global.Canvas = window.Canvas;
+    }
+
+    // fix Blob not found (to support external modules like is-blob)
+    global.Blob = Blob;
+
+    const div = document.createElement("div");
+    div.id = "browserlessDiv";
+    document.body.appendChild(div);
+    // const canvas = document.createElement('canvas')
+    // div.canvas = document.createElement('canvas')
+
+    const zoom = 1.0;
+    // width of the div / PNG generated
+    let width = pageWidth * zoom;
+    // TODO sometimes the width is way too small for the score, may need to adjust zoom.
+    if (endlessPage) {
+        width = 1440;
+    }
+    let height = pageHeight;
+    if (endlessPage) {
+        height = 32767;
+    }
+    div.width = width;
+    div.height = height;
+    // div.offsetWidth = width; // doesn't work, offsetWidth is always 0 from this. see below
+    // div.clientWidth = width;
+    // div.clientHeight = height;
+    // div.scrollHeight = height;
+    // div.scrollWidth = width;
+    div.setAttribute("width", width);
+    div.setAttribute("height", height);
+    div.setAttribute("offsetWidth", width);
+    // debug('div.offsetWidth: ' + div.offsetWidth, DEBUG) // 0 here, set correctly later
+    // debug('div.height: ' + div.height, DEBUG)
+
+    // hack: set offsetWidth reliably
+    Object.defineProperties(window.HTMLElement.prototype, {
+        offsetLeft: {
+            get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0; }
+        },
+        offsetTop: {
+            get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0; }
+        },
+        offsetHeight: {
+            get: function () { return height; }
+        },
+        offsetWidth: {
+            get: function () { return width; }
+        }
+    });
+    debug("div.offsetWidth: " + div.offsetWidth, DEBUG);
+    debug("div.height: " + div.height, DEBUG);
+    // ---- end browser hacks (hopefully) ----
+
+    // load globally
+
+    // Create the image directory if it doesn't exist.
+    FS.mkdirSync(imageDir, { recursive: true });
+
+    const sampleDirFilenames = FS.readdirSync(sampleDir);
+    let samplesToProcess = []; // samples we want to process/generate pngs of, excluding the filtered out files/filenames
+    for (const sampleFilename of sampleDirFilenames) {
+        if (osmdTestingMode && filterRegex === "allSmall") {
+            if (sampleFilename.match("^(Actor)|(Gounod)")) { // TODO maybe filter by file size instead
+                debug("filtering big file: " + sampleFilename, DEBUG);
+                continue;
+            }
+        }
+        // eslint-disable-next-line no-useless-escape
+        if (sampleFilename.match("^.*(\.xml)|(\.musicxml)|(\.mxl)$")) {
+            // console.log('found musicxml/mxl: ' + sampleFilename)
+            samplesToProcess.push(sampleFilename);
+        } else {
+            debug("discarded file/directory: " + sampleFilename, DEBUG);
+        }
+    }
+
+    // filter samples to process by regex if given
+    if (filterRegex && filterRegex !== "" && filterRegex !== "all" && !(osmdTestingMode && filterRegex === "allSmall")) {
+        debug("filtering samples for regex: " + filterRegex, DEBUG);
+        samplesToProcess = samplesToProcess.filter((filename) => filename.match(filterRegex));
+        debug(`found ${samplesToProcess.length} matches: `, DEBUG);
+        for (let i = 0; i < samplesToProcess.length; i++) {
+            debug(samplesToProcess[i], DEBUG);
+        }
+    }
+
+    const backend = imageFormat === "png" ? "canvas" : "svg";
+    const osmdInstance = new OSMD.OpenSheetMusicDisplay(div, {
+        autoResize: false,
+        backend: backend,
+        pageBackgroundColor: "#FFFFFF",
+        pageFormat: pageFormat
+        // defaultFontFamily: 'Arial',
+        // drawTitle: false
+    });
+    // for more options check OSMDOptions.ts
+
+    // you can set finer-grained rendering/engraving settings in EngravingRules:
+    // osmdInstance.EngravingRules.TitleTopDistance = 5.0 // 5.0 is default
+    //   (unless in osmdTestingMode, these will be reset with drawingParameters default)
+    // osmdInstance.EngravingRules.PageTopMargin = 5.0 // 5 is default
+    // osmdInstance.EngravingRules.PageBottomMargin = 5.0 // 5 is default. <5 can cut off scores that extend in the last staffline
+    // note that for now the png and canvas will still have the height given in the script argument,
+    //   so even with a margin of 0 the image will be filled to the full height.
+    // osmdInstance.EngravingRules.PageLeftMargin = 5.0 // 5 is default
+    // osmdInstance.EngravingRules.PageRightMargin = 5.0 // 5 is default
+    // osmdInstance.EngravingRules.MetronomeMarkXShift = -8; // -6 is default
+    // osmdInstance.EngravingRules.DistanceBetweenVerticalSystemLines = 0.15; // 0.35 is default
+    // for more options check EngravingRules.ts (though not all of these are meant and fully supported to be changed at will)
+
+    if (DEBUG) {
+        osmdInstance.setLogLevel("debug");
+        // console.log(`osmd PageFormat: ${osmdInstance.EngravingRules.PageFormat.width}x${osmdInstance.EngravingRules.PageFormat.height}`)
+        console.log(`osmd PageFormat idString: ${osmdInstance.EngravingRules.PageFormat.idString}`);
+        console.log("PageHeight: " + osmdInstance.EngravingRules.PageHeight);
+    } else {
+        osmdInstance.setLogLevel("info"); // doesn't seem to work, log.debug still logs
+    }
+
+    debug("[OSMD.generateImages] starting loop over samples, saving images to " + imageDir, DEBUG);
+    for (let i = 0; i < samplesToProcess.length; i++) {
+        const sampleFilename = samplesToProcess[i];
+        debug("sampleFilename: " + sampleFilename, DEBUG);
+
+        await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, {}, DEBUG);
+
+        if (osmdTestingMode && !osmdTestingSingleMode && sampleFilename.startsWith("Beethoven") && sampleFilename.includes("Geliebte")) {
+            // generate one more testing image with skyline and bottomline. (startsWith 'Beethoven' don't catch the function test)
+            await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, {skyBottomLine: true}, DEBUG);
+            // generate one more testing image with GraphicalNote positions
+            await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, {boundingBoxes: "VexFlowGraphicalNote"}, DEBUG);
+        }
+    }
+
+    console.log("[OSMD.generateImages] done, exiting.");
+}
+
+// eslint-disable-next-line
+// let maxRss = 0, maxRssFilename = '' // to log memory usage (debug)
+async function generateSampleImage (sampleFilename, directory, osmdInstance, osmdTestingMode,
+    options = {}, DEBUG = false) {
+    const samplePath = directory + "/" + sampleFilename;
+    let loadParameter = FS.readFileSync(samplePath);
+
+    if (sampleFilename.endsWith(".mxl")) {
+        loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter);
+    } else {
+        loadParameter = loadParameter.toString();
+    }
+    // console.log('loadParameter: ' + loadParameter)
+    // console.log('typeof loadParameter: ' + typeof loadParameter)
+
+    // set sample-specific options for OSMD visual regression testing
+    let includeSkyBottomLine = false;
+    let drawBoundingBoxString;
+    if (osmdTestingMode) {
+        const isFunctionTestAutobeam = sampleFilename.startsWith("OSMD_function_test_autobeam");
+        const isFunctionTestAutoColoring = sampleFilename.startsWith("OSMD_function_test_auto-custom-coloring");
+        const isFunctionTestSystemAndPageBreaks = sampleFilename.startsWith("OSMD_Function_Test_System_and_Page_Breaks");
+        const isFunctionTestDrawingRange = sampleFilename.startsWith("OSMD_function_test_measuresToDraw_");
+        const defaultOrCompactTightMode = sampleFilename.startsWith("OSMD_Function_Test_Container_height") ? "compacttight" : "default";
+        const isTestFlatBeams = sampleFilename.startsWith("test_drum_tuplet_beams");
+        const isTestEndClefStaffEntryBboxes = sampleFilename.startsWith("test_end_measure_clefs_staffentry_bbox");
+        if (isTestEndClefStaffEntryBboxes) {
+            drawBoundingBoxString = "VexFlowStaffEntry";
+        } else {
+            drawBoundingBoxString = options.boundingBoxes; // undefined is also a valid value: no bboxes
+        }
+        osmdInstance.setOptions({
+            autoBeam: isFunctionTestAutobeam, // only set to true for function test autobeam
+            coloringMode: isFunctionTestAutoColoring ? 2 : 0,
+            // eslint-disable-next-line max-len
+            coloringSetCustom: isFunctionTestAutoColoring ? ["#d82c6b", "#F89D15", "#FFE21A", "#4dbd5c", "#009D96", "#43469d", "#76429c", "#ff0000"] : undefined,
+            colorStemsLikeNoteheads: isFunctionTestAutoColoring,
+            drawingParameters: defaultOrCompactTightMode, // note: default resets all EngravingRules. could be solved differently
+            drawFromMeasureNumber: isFunctionTestDrawingRange ? 9 : 1,
+            drawUpToMeasureNumber: isFunctionTestDrawingRange ? 12 : Number.MAX_SAFE_INTEGER,
+            newSystemFromXML: isFunctionTestSystemAndPageBreaks,
+            newPageFromXML: isFunctionTestSystemAndPageBreaks,
+            pageBackgroundColor: "#FFFFFF", // reset by drawingparameters default
+            pageFormat: pageFormat // reset by drawingparameters default
+        });
+        includeSkyBottomLine = options.skyBottomLine ? options.skyBottomLine : false; // apparently es6 doesn't have ?? operator
+        osmdInstance.drawSkyLine = includeSkyBottomLine; // if includeSkyBottomLine, draw skyline and bottomline, else not
+        osmdInstance.drawBottomLine = includeSkyBottomLine;
+        osmdInstance.setDrawBoundingBox(drawBoundingBoxString, false); // false: don't render (now). also (re-)set if undefined!
+        if (isTestFlatBeams) {
+            osmdInstance.EngravingRules.FlatBeams = true;
+            // osmdInstance.EngravingRules.FlatBeamOffset = 30;
+            osmdInstance.EngravingRules.FlatBeamOffset = 10;
+            osmdInstance.EngravingRules.FlatBeamOffsetPerBeam = 10;
+        } else {
+            osmdInstance.EngravingRules.FlatBeams = false;
+        }
+    }
+
+    try {
+        await osmdInstance.load(loadParameter); // if using load.then() without await, memory will not be freed up between renders
+    } catch (ex) {
+        console.log("couldn't load sample " + sampleFilename + ", skipping. Error: \n" + ex);
+        return;
+    }
+    debug("xml loaded", DEBUG);
+    try {
+        osmdInstance.render();
+        // there were reports that await could help here, but render isn't a synchronous function, and it seems to work. see #932
+    } catch (ex) {
+        console.log("renderError: " + ex);
+    }
+    debug("rendered", DEBUG);
+
+    const markupStrings = []; // svg
+    const dataUrls = []; // png
+    let canvasImage;
+
+    for (let pageNumber = 1; pageNumber < Number.POSITIVE_INFINITY; pageNumber++) {
+        if (imageFormat === "png") {
+            canvasImage = document.getElementById("osmdCanvasVexFlowBackendCanvas" + pageNumber);
+            if (!canvasImage) {
+                break;
+            }
+            if (!canvasImage.toDataURL) {
+                console.log(`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`);
+                break;
+            }
+            dataUrls.push(canvasImage.toDataURL());
+        } else if (imageFormat === "svg") {
+            const svgElement = document.getElementById("osmdSvgPage" + pageNumber);
+            if (!svgElement) {
+                break;
+            }
+            // The important xmlns attribute is not serialized unless we set it here
+            svgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
+            markupStrings.push(svgElement.outerHTML);
+        }
+    }
+
+    for (let pageIndex = 0; pageIndex < Math.max(dataUrls.length, markupStrings.length); pageIndex++) {
+        const pageNumberingString = `${pageIndex + 1}`;
+        const skybottomlineString = includeSkyBottomLine ? "skybottomline_" : "";
+        const graphicalNoteBboxesString = drawBoundingBoxString ? "bbox" + drawBoundingBoxString + "_" : "";
+        // pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
+        const pageFilename = `${imageDir}/${sampleFilename}_${skybottomlineString}${graphicalNoteBboxesString}${pageNumberingString}.${imageFormat}`;
+
+        if (imageFormat === "png") {
+            const dataUrl = dataUrls[pageIndex];
+            if (!dataUrl || !dataUrl.split) {
+                console.log(`error: could not get dataUrl (imageData) for page ${pageIndex + 1} of sample: ${sampleFilename}`);
+                continue;
+            }
+            const imageData = dataUrl.split(";base64,").pop();
+            const imageBuffer = Buffer.from(imageData, "base64");
+
+            debug("got image data, saving to: " + pageFilename, DEBUG);
+            FS.writeFileSync(pageFilename, imageBuffer, { encoding: "base64" });
+        } else if (imageFormat === "svg") {
+            const markup = markupStrings[pageIndex];
+            if (!markup) {
+                console.log(`error: could not get markup (SVG data) for page ${pageIndex + 1} of sample: ${sampleFilename}`);
+                continue;
+            }
+
+            debug("got svg markup data, saving to: " + pageFilename, DEBUG);
+            FS.writeFileSync(pageFilename, markup, { encoding: "utf-8" });
+        }
+
+        // debug: log memory usage
+        // const usage = process.memoryUsage()
+        // for (const entry of Object.entries(usage)) {
+        //     if (entry[0] === 'rss') {
+        //         if (entry[1] > maxRss) {
+        //             maxRss = entry[1]
+        //             maxRssFilename = pageFilename
+        //         }
+        //     }
+        //     console.log(entry[0] + ': ' + entry[1] / (1024 * 1024) + 'mb')
+        // }
+        // console.log('maxRss: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
+    }
+    // console.log('maxRss total: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
+
+    // await sleep(5000)
+    // }) // end read file
+}
+
+function debug (msg, debugEnabled) {
+    if (debugEnabled) {
+        console.log(msg);
+    }
+}
+
+init();

+ 163 - 0
test/data/test_beam_svg_double.musicxml

@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test_beam_svg_double</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2022-01-14</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>6.99911</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1696.94</page-height>
+      <page-width>1200.48</page-width>
+      <page-margins type="even">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="600.24" default-y="1611.63" justify="center" valign="top" font-size="22">test_beam_svg_double</credit-words>
+    </credit>
+  <credit page="1">
+    <credit-type>composer</credit-type>
+    <credit-words default-x="1114.75" default-y="1511.63" justify="right" valign="top">Composer</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="469.82">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>509.21</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>4</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>2</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <note default-x="85.98" default-y="-30.00">
+        <pitch>
+          <step>G</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <accidental>sharp</accidental>
+        <stem>up</stem>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="168.91" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <beam number="1">continue</beam>
+        <beam number="2">begin</beam>
+        </note>
+      <note default-x="220.74" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        </note>
+      <note default-x="272.58" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>down</stem>
+        <beam number="1">begin</beam>
+        <beam number="2">begin</beam>
+        </note>
+      <note default-x="324.41" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>down</stem>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 704 - 0
test/data/test_clef_end_measure.musicxml

@@ -0,0 +1,704 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test_clef_end_measure</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2022-01-14</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1697.14</page-height>
+      <page-width>1200</page-width>
+      <page-margins type="even">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>test_clef_end_measure</credit-type>
+    <credit-words default-x="600" default-y="1611.43" justify="center" valign="top" font-size="22">clef end measure test</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="525.53">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>65.90</left-margin>
+            <right-margin>0.00</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        <staff-layout number="2">
+          <staff-distance>90.03</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>4</divisions>
+        <key>
+          <fifths>2</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <staves>2</staves>
+        <clef number="1">
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        <clef number="2">
+          <sign>F</sign>
+          <line>4</line>
+          </clef>
+        </attributes>
+      <note>
+        <rest measure="yes"/>
+        <duration>16</duration>
+        <voice>1</voice>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>16</duration>
+        </backup>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="crescendo" number="1" default-y="78.24"/>
+          </direction-type>
+        <staff>2</staff>
+        </direction>
+      <note default-x="113.27" default-y="-135.03">
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <beam number="2">begin</beam>
+        <notations>
+          <slur type="start" placement="above" number="1"/>
+          </notations>
+        </note>
+      <note default-x="142.58" default-y="-145.03">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="171.88" default-y="-135.03">
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="201.19" default-y="-120.03">
+        <pitch>
+          <step>C</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        <staff>2</staff>
+        </direction>
+      <note default-x="230.50" default-y="-110.03">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        <beam number="2">begin</beam>
+        </note>
+      <note default-x="259.80" default-y="-135.03">
+        <pitch>
+          <step>G</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <note default-x="289.11" default-y="-140.03">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        <beam number="2">continue</beam>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="diminuendo" number="1" default-y="70.10"/>
+          </direction-type>
+        <staff>2</staff>
+        </direction>
+      <note default-x="318.42" default-y="-145.03">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>5</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        <notations>
+          <slur type="stop" number="1"/>
+          </notations>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <wedge type="stop" number="1"/>
+          </direction-type>
+        <staff>2</staff>
+        </direction>
+      <note default-x="334.82" default-y="-145.03">
+        <grace/>
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <voice>5</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <notations>
+          <slur type="start" placement="above" number="1"/>
+          </notations>
+        </note>
+      <note default-x="353.75" default-y="-140.03">
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>3</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>5</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <notations>
+          <slur type="stop" number="1"/>
+          </notations>
+        </note>
+      <direction placement="above">
+        <direction-type>
+          <dynamics default-x="6.50" default-y="8.65" relative-y="15.00">
+            <p/>
+            </dynamics>
+          </direction-type>
+        <staff>2</staff>
+        <sound dynamics="54.44"/>
+        </direction>
+      <note default-x="418.23" default-y="-150.03">
+        <pitch>
+          <step>D</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <notations>
+          <slur type="start" placement="below" number="1"/>
+          <technical>
+            <fingering>1</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="465.12" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <staff>1</staff>
+        <notations>
+          <slur type="stop" number="1"/>
+          <technical>
+            <fingering placement="below">4</fingering>
+            </technical>
+          </notations>
+        </note>
+      <backup>
+        <duration>1</duration>
+        </backup>
+      <attributes>
+        <clef id="666" number="2">
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <forward>
+        <duration>1</duration>
+        </forward>
+      <backup>
+        <duration>16</duration>
+        </backup>
+      <note default-x="113.27" default-y="-145.03">
+        <pitch>
+          <step>E</step>
+          <octave>3</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>6</voice>
+        <type>16th</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        <notations>
+          <articulations>
+            <staccato/>
+            </articulations>
+          </notations>
+        </note>
+      <forward>
+        <duration>3</duration>
+        </forward>
+      <note default-x="230.50" default-y="-165.03">
+        <pitch>
+          <step>A</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>6</voice>
+        <type>16th</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        <notations>
+          <articulations>
+            <staccato/>
+            </articulations>
+          </notations>
+        </note>
+      <forward>
+        <duration>3</duration>
+        </forward>
+      <note default-x="353.75" default-y="-185.03">
+        <pitch>
+          <step>D</step>
+          <octave>2</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>6</voice>
+        <type>quarter</type>
+        <stem>down</stem>
+        <staff>2</staff>
+        <notations>
+          <articulations>
+            <tenuto/>
+            </articulations>
+          </notations>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>6</voice>
+        <type>quarter</type>
+        <staff>2</staff>
+        </note>
+      </measure>
+    <measure number="2" width="437.14">
+      <direction placement="below">
+        <direction-type>
+          <words default-y="-40.00" relative-y="-25.00" font-style="italic">espress.</words>
+          </direction-type>
+        <staff>1</staff>
+        </direction>
+      <note default-x="13.00" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <beam number="1">begin</beam>
+        <beam number="2">begin</beam>
+        <notations>
+          <slur type="start" placement="above" number="1"/>
+          <technical>
+            <fingering>1</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="44.32" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <alter>-1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>1</voice>
+        <type>16th</type>
+        <accidental>flat</accidental>
+        <stem>up</stem>
+        <staff>1</staff>
+        <beam number="1">end</beam>
+        <beam number="2">end</beam>
+        </note>
+      <note default-x="75.65" default-y="-15.00">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <accidental>natural</accidental>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <note default-x="175.89" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <alter>-1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <staff>1</staff>
+        <notations>
+          <slur type="stop" number="1"/>
+          </notations>
+        </note>
+      <note default-x="226.01" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <alter>-1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        <staff>1</staff>
+        <notations>
+          <slur type="start" placement="above" number="1"/>
+          <technical>
+            <fingering>3</fingering>
+            </technical>
+          </notations>
+        </note>
+      <note default-x="276.13" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        <notations>
+          <slur type="stop" number="1"/>
+          </notations>
+        </note>
+      <note default-x="376.37" default-y="-25.00">
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>16</duration>
+        </backup>
+      <note default-x="13.00" default-y="-175.03">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="13.00" default-y="-165.03">
+        <chord/>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="75.65" default-y="-175.03">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="75.65" default-y="-160.03">
+        <chord/>
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="125.77" default-y="-175.03">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="125.77" default-y="-155.03">
+        <chord/>
+        <pitch>
+          <step>A</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="175.89" default-y="-175.03">
+        <pitch>
+          <step>D</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        </note>
+      <note default-x="175.89" default-y="-160.03">
+        <chord/>
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="226.01" default-y="-180.03">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <accidental>natural</accidental>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">begin</beam>
+        </note>
+      <note default-x="226.01" default-y="-160.03">
+        <chord/>
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="276.13" default-y="-180.03">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="276.13" default-y="-160.03">
+        <chord/>
+        <pitch>
+          <step>G</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="326.25" default-y="-180.03">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">continue</beam>
+        </note>
+      <note default-x="326.25" default-y="-165.03">
+        <chord/>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <note default-x="376.37" default-y="-180.03">
+        <pitch>
+          <step>C</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        <beam number="1">end</beam>
+        </note>
+      <note default-x="376.37" default-y="-165.03">
+        <chord/>
+        <pitch>
+          <step>F</step>
+          <alter>1</alter>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>5</voice>
+        <type>eighth</type>
+        <stem>up</stem>
+        <staff>2</staff>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 125 - 0
test/data/test_note_alignment_long_distance_notes.musicxml

@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test note alignment long distance</work-title>
+    </work>
+  <identification>
+    <creator type="composer">Composer</creator>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2022-01-08</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>6.99911</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1696.94</page-height>
+      <page-width>1200.48</page-width>
+      <page-margins type="even">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="600.242" default-y="1611.21" justify="center" valign="top" font-size="22">Title</credit-words>
+    </credit>
+  <credit page="1">
+    <credit-type>composer</credit-type>
+    <credit-words default-x="1114.76" default-y="1511.21" justify="right" valign="bottom">Composer</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="340.16">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>638.55</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <note default-x="80.72" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        </note>
+      <backup>
+        <duration>4</duration>
+        </backup>
+      <note default-x="82.64" default-y="-10.00">
+        <pitch>
+          <step>D</step>
+          <octave>5</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>2</voice>
+        <type>half</type>
+        <stem>up</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>2</voice>
+        <type>half</type>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 179 - 0
test/data/test_note_overlap_staggering_whole_eighths.musicxml

@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding='UTF-8' standalone='no' ?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.0">
+ <work>
+  <work-title>test note overlap whole+eighths</work-title>
+ </work>
+ <identification>
+  <encoding>
+   <encoding-date>2021-11-17</encoding-date>
+   <software>Sibelius 19.5.0</software>
+   <software>Direct export, not from Dolet</software>
+   <encoding-description>Sibelius / MusicXML 3.0</encoding-description>
+   <supports element="print" type="yes" value="yes" attribute="new-system" />
+   <supports element="print" type="yes" value="yes" attribute="new-page" />
+   <supports element="accidental" type="yes" />
+   <supports element="beam" type="yes" />
+   <supports element="stem" type="yes" />
+  </encoding>
+ </identification>
+ <defaults>
+  <scaling>
+   <millimeters>215.9</millimeters>
+   <tenths>1233</tenths>
+  </scaling>
+  <page-layout>
+   <page-height>1596</page-height>
+   <page-width>1233</page-width>
+   <page-margins type="both">
+    <left-margin>85</left-margin>
+    <right-margin>85</right-margin>
+    <top-margin>85</top-margin>
+    <bottom-margin>85</bottom-margin>
+   </page-margins>
+  </page-layout>
+  <system-layout>
+   <system-margins>
+    <left-margin>63</left-margin>
+    <right-margin>0</right-margin>
+   </system-margins>
+   <system-distance>92</system-distance>
+  </system-layout>
+  <appearance>
+   <line-width type="stem">0.9375</line-width>
+   <line-width type="beam">5</line-width>
+   <line-width type="staff">0.9375</line-width>
+   <line-width type="light barline">1.5625</line-width>
+   <line-width type="heavy barline">5</line-width>
+   <line-width type="leger">1.5625</line-width>
+   <line-width type="ending">1.5625</line-width>
+   <line-width type="wedge">1.25</line-width>
+   <line-width type="enclosure">0.9375</line-width>
+   <line-width type="tuplet bracket">1.25</line-width>
+   <line-width type="bracket">5</line-width>
+   <line-width type="dashes">1.5625</line-width>
+   <line-width type="extend">0.9375</line-width>
+   <line-width type="octave shift">1.5625</line-width>
+   <line-width type="pedal">1.5625</line-width>
+   <line-width type="slur middle">1.5625</line-width>
+   <line-width type="slur tip">0.625</line-width>
+   <line-width type="tie middle">1.5625</line-width>
+   <line-width type="tie tip">0.625</line-width>
+   <note-size type="cue">75</note-size>
+   <note-size type="grace">60</note-size>
+  </appearance>
+  <music-font font-family="Opus Std" font-size="19.8425" />
+  <lyric-font font-family="Times New Roman" font-size="11.4715" />
+  <lyric-language xml:lang="en" />
+ </defaults>
+ <part-list>
+  <part-group type="start" number="1">
+   <group-symbol>brace</group-symbol>
+  </part-group>
+  <score-part id="P1">
+   <part-name>Piano</part-name>
+   <part-name-display>
+    <display-text>Piano</display-text>
+   </part-name-display>
+   <part-abbreviation>Pno.</part-abbreviation>
+   <part-abbreviation-display>
+    <display-text>Pno.</display-text>
+   </part-abbreviation-display>
+   <score-instrument id="P1-I1">
+    <instrument-name>Piano (2) (2)</instrument-name>
+    <instrument-sound>keyboard.piano.grand</instrument-sound>
+    <solo />
+    <virtual-instrument>
+     <virtual-library>Sibelius 7 Sounds</virtual-library>
+     <virtual-name>Concert Grand Piano</virtual-name>
+    </virtual-instrument>
+   </score-instrument>
+  </score-part>
+  <part-group type="stop" number="1" />
+ </part-list>
+ <part id="P1">
+  <!--============== Part: P1, Measure: 1 ==============-->
+  <measure number="1" width="986">
+   <attributes>
+    <divisions>256</divisions>
+    <key color="#000000">
+     <fifths>0</fifths>
+     <mode>minor</mode>
+    </key>
+    <time color="#000000">
+     <beats>4</beats>
+     <beat-type>4</beat-type>
+    </time>
+    <staves>1</staves>
+    <clef number="1" color="#000000">
+     <sign>G</sign>
+     <line>2</line>
+    </clef>
+    <staff-details number="1" print-object="yes" />
+   </attributes>
+   <note color="#000000" default-x="96" default-y="9">
+    <pitch>
+     <step>A</step>
+     <octave>4</octave>
+    </pitch>
+    <duration>128</duration>
+    <instrument id="P1-I1" />
+    <voice>1</voice>
+    <type>eighth</type>
+    <stem>up</stem>
+    <staff>1</staff>
+    <beam number="1">begin</beam>
+   </note>
+   <note color="#000000" default-x="249" default-y="9">
+    <pitch>
+     <step>B</step>
+     <octave>4</octave>
+    </pitch>
+    <duration>128</duration>
+    <instrument id="P1-I1" />
+    <voice>1</voice>
+    <type>eighth</type>
+    <stem>up</stem>
+    <staff>1</staff>
+    <beam number="1">end</beam>
+   </note>
+   <note default-x="402">
+    <rest />
+    <duration>256</duration>
+    <instrument id="P1-I1" />
+    <voice>1</voice>
+    <type>quarter</type>
+    <staff>1</staff>
+   </note>
+   <note default-x="613">
+    <rest />
+    <duration>512</duration>
+    <instrument id="P1-I1" />
+    <voice>1</voice>
+    <type>half</type>
+    <staff>1</staff>
+   </note>
+   <barline>
+    <bar-style>light-heavy</bar-style>
+   </barline>
+   <backup>
+    <duration>1024</duration>
+   </backup>
+   <note color="#000000" default-x="96">
+    <pitch>
+     <step>G</step>
+     <octave>4</octave>
+    </pitch>
+    <duration>1024</duration>
+    <tie type="start" />
+    <instrument id="P1-I1" />
+    <voice>2</voice>
+    <type>whole</type>
+    <staff>1</staff>
+    <notations>
+     <tied type="start" orientation="under" />
+    </notations>
+   </note>
+  </measure>
+ </part>
+</score-partwise>

+ 140 - 0
test/data/test_note_overlap_staggering_whole_half.musicxml

@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test note overlap whole+half</work-title>
+    </work>
+  <identification>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2022-01-07</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="no"/>
+      <supports element="print" attribute="new-system" type="no"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>7</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1697.14</page-height>
+      <page-width>1200</page-width>
+      <page-margins type="even">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7143</left-margin>
+        <right-margin>85.7143</right-margin>
+        <top-margin>85.7143</top-margin>
+        <bottom-margin>85.7143</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="600" default-y="1611.43" justify="center" valign="top" font-size="22">test note overlap</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="371.43">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>65.90</left-margin>
+            <right-margin>591.24</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        <staff-layout number="2">
+          <staff-distance>65.00</staff-distance>
+          </staff-layout>
+        </print>
+      <attributes>
+        <divisions>1</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <staves>2</staves>
+        <clef number="1">
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        <clef number="2">
+          <sign>F</sign>
+          <line>4</line>
+          </clef>
+        </attributes>
+      <note default-x="83.49" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>whole</type>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>4</duration>
+        </backup>
+      <note default-x="101.41" default-y="-20.00">
+        <pitch>
+          <step>B</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>2</voice>
+        <type>half</type>
+        <stem>down</stem>
+        <staff>1</staff>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>2</voice>
+        <type>half</type>
+        <staff>1</staff>
+        </note>
+      <backup>
+        <duration>4</duration>
+        </backup>
+      <note>
+        <rest measure="yes"/>
+        <duration>4</duration>
+        <voice>5</voice>
+        <staff>2</staff>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 150 - 0
test/data/test_notehead_sharing_eighth_quarter.musicxml

@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <work>
+    <work-title>test notehead sharing eigth+quarter</work-title>
+    </work>
+  <identification>
+    <creator type="composer">Composer</creator>
+    <encoding>
+      <software>MuseScore 3.6.2</software>
+      <encoding-date>2022-01-08</encoding-date>
+      <supports element="accidental" type="yes"/>
+      <supports element="beam" type="yes"/>
+      <supports element="print" attribute="new-page" type="yes" value="yes"/>
+      <supports element="print" attribute="new-system" type="yes" value="yes"/>
+      <supports element="stem" type="yes"/>
+      </encoding>
+    </identification>
+  <defaults>
+    <scaling>
+      <millimeters>6.99911</millimeters>
+      <tenths>40</tenths>
+      </scaling>
+    <page-layout>
+      <page-height>1696.94</page-height>
+      <page-width>1200.48</page-width>
+      <page-margins type="even">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      <page-margins type="odd">
+        <left-margin>85.7252</left-margin>
+        <right-margin>85.7252</right-margin>
+        <top-margin>85.7252</top-margin>
+        <bottom-margin>85.7252</bottom-margin>
+        </page-margins>
+      </page-layout>
+    <word-font font-family="Edwin" font-size="10"/>
+    <lyric-font font-family="Edwin" font-size="10"/>
+    </defaults>
+  <credit page="1">
+    <credit-type>title</credit-type>
+    <credit-words default-x="600.242" default-y="1611.21" justify="center" valign="top" font-size="22">Title</credit-words>
+    </credit>
+  <credit page="1">
+    <credit-type>composer</credit-type>
+    <credit-words default-x="1114.76" default-y="1511.21" justify="right" valign="bottom">Composer</credit-words>
+    </credit>
+  <part-list>
+    <score-part id="P1">
+      <part-name>Piano</part-name>
+      <part-abbreviation>Pno.</part-abbreviation>
+      <score-instrument id="P1-I1">
+        <instrument-name>Piano</instrument-name>
+        </score-instrument>
+      <midi-device id="P1-I1" port="1"></midi-device>
+      <midi-instrument id="P1-I1">
+        <midi-channel>1</midi-channel>
+        <midi-program>1</midi-program>
+        <volume>78.7402</volume>
+        <pan>0</pan>
+        </midi-instrument>
+      </score-part>
+    </part-list>
+  <part id="P1">
+    <measure number="1" width="407.91">
+      <print>
+        <system-layout>
+          <system-margins>
+            <left-margin>50.00</left-margin>
+            <right-margin>570.79</right-margin>
+            </system-margins>
+          <top-system-distance>170.00</top-system-distance>
+          </system-layout>
+        </print>
+      <attributes>
+        <divisions>2</divisions>
+        <key>
+          <fifths>0</fifths>
+          </key>
+        <time>
+          <beats>4</beats>
+          <beat-type>4</beat-type>
+          </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+          </clef>
+        </attributes>
+      <note default-x="80.72" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        <stem>up</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>quarter</type>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>1</voice>
+        <type>half</type>
+        </note>
+      <backup>
+        <duration>8</duration>
+        </backup>
+      <note default-x="80.72" default-y="-40.00">
+        <pitch>
+          <step>E</step>
+          <octave>4</octave>
+          </pitch>
+        <duration>1</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        <stem>down</stem>
+        </note>
+      <note>
+        <rest/>
+        <duration>1</duration>
+        <voice>2</voice>
+        <type>eighth</type>
+        </note>
+      <note>
+        <rest/>
+        <duration>2</duration>
+        <voice>2</voice>
+        <type>quarter</type>
+        </note>
+      <note>
+        <rest/>
+        <duration>4</duration>
+        <voice>2</voice>
+        <type>half</type>
+        </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+        </barline>
+      </measure>
+    </part>
+  </score-partwise>

+ 246 - 0
test/data/test_ties_missing_1097.musicxml

@@ -0,0 +1,246 @@
+<?xml version="1.0" encoding='UTF-8' standalone='no' ?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.0">
+ <work>
+  <work-title>test_ties_missing_1097</work-title>
+ </work>
+ <identification>
+  <encoding>
+   <encoding-date>2021-11-17</encoding-date>
+   <software>Sibelius 19.5.0</software>
+   <software>Direct export, not from Dolet</software>
+   <encoding-description>Sibelius / MusicXML 3.0</encoding-description>
+   <supports element="print" type="yes" value="yes" attribute="new-system" />
+   <supports element="print" type="yes" value="yes" attribute="new-page" />
+   <supports element="accidental" type="yes" />
+   <supports element="beam" type="yes" />
+   <supports element="stem" type="yes" />
+  </encoding>
+ </identification>
+ <defaults>
+  <scaling>
+   <millimeters>215.9</millimeters>
+   <tenths>1233</tenths>
+  </scaling>
+  <page-layout>
+   <page-height>1596</page-height>
+   <page-width>1233</page-width>
+   <page-margins type="both">
+    <left-margin>85</left-margin>
+    <right-margin>85</right-margin>
+    <top-margin>85</top-margin>
+    <bottom-margin>85</bottom-margin>
+   </page-margins>
+  </page-layout>
+  <system-layout>
+   <system-margins>
+    <left-margin>63</left-margin>
+    <right-margin>0</right-margin>
+   </system-margins>
+   <system-distance>92</system-distance>
+  </system-layout>
+  <appearance>
+   <line-width type="stem">0.9375</line-width>
+   <line-width type="beam">5</line-width>
+   <line-width type="staff">0.9375</line-width>
+   <line-width type="light barline">1.5625</line-width>
+   <line-width type="heavy barline">5</line-width>
+   <line-width type="leger">1.5625</line-width>
+   <line-width type="ending">1.5625</line-width>
+   <line-width type="wedge">1.25</line-width>
+   <line-width type="enclosure">0.9375</line-width>
+   <line-width type="tuplet bracket">1.25</line-width>
+   <line-width type="bracket">5</line-width>
+   <line-width type="dashes">1.5625</line-width>
+   <line-width type="extend">0.9375</line-width>
+   <line-width type="octave shift">1.5625</line-width>
+   <line-width type="pedal">1.5625</line-width>
+   <line-width type="slur middle">1.5625</line-width>
+   <line-width type="slur tip">0.625</line-width>
+   <line-width type="tie middle">1.5625</line-width>
+   <line-width type="tie tip">0.625</line-width>
+   <note-size type="cue">75</note-size>
+   <note-size type="grace">60</note-size>
+  </appearance>
+  <music-font font-family="Opus Std" font-size="19.8425" />
+  <lyric-font font-family="Times New Roman" font-size="11.4715" />
+  <lyric-language xml:lang="en" />
+ </defaults>
+ <part-list>
+  <part-group type="start" number="1">
+   <group-symbol>brace</group-symbol>
+  </part-group>
+  <score-part id="P1">
+   <part-name>Piano</part-name>
+   <part-name-display>
+    <display-text>Piano</display-text>
+   </part-name-display>
+   <part-abbreviation>Pno.</part-abbreviation>
+   <part-abbreviation-display>
+    <display-text>Pno.</display-text>
+   </part-abbreviation-display>
+   <score-instrument id="P1-I1">
+    <instrument-name>Piano (2) (2)</instrument-name>
+    <instrument-sound>keyboard.piano.grand</instrument-sound>
+    <solo />
+    <virtual-instrument>
+     <virtual-library>General MIDI</virtual-library>
+     <virtual-name>Acoustic Piano</virtual-name>
+    </virtual-instrument>
+   </score-instrument>
+  </score-part>
+  <part-group type="stop" number="1" />
+ </part-list>
+ <part id="P1">
+  <!--============== Part: P1, Measure: 1 ==============-->
+  <measure number="1" width="986">
+   <attributes>
+    <divisions>256</divisions>
+    <key color="#000000">
+     <fifths>-3</fifths>
+     <mode>minor</mode>
+    </key>
+    <time color="#000000">
+     <beats>4</beats>
+     <beat-type>4</beat-type>
+    </time>
+    <staves>1</staves>
+    <clef number="1" color="#000000">
+     <sign>F</sign>
+     <line>4</line>
+    </clef>
+    <staff-details number="1" print-object="yes" />
+   </attributes>
+   <note color="#000000" default-x="115" default-y="17">
+    <pitch>
+     <step>A</step>
+     <alter>-1</alter>
+     <octave>2</octave>
+    </pitch>
+    <duration>384</duration>
+    <instrument id="P1-I1" />
+    <voice>1</voice>
+    <type>quarter</type>
+    <dot />
+    <stem>up</stem>
+    <staff>1</staff>
+   </note>
+   <note color="#000000" default-x="115">
+    <chord />
+    <pitch>
+     <step>E</step>
+     <alter>-1</alter>
+     <octave>3</octave>
+    </pitch>
+    <duration>256</duration>
+    <tie type="start" />
+    <instrument id="P1-I1" />
+    <type>quarter</type>
+    <dot />
+    <staff>1</staff>
+    <notations>
+     <tied type="start" orientation="over" />
+    </notations>
+   </note>
+   <note color="#000000" default-x="421" default-y="20">
+    <pitch>
+     <step>G</step>
+     <octave>2</octave>
+    </pitch>
+    <duration>128</duration>
+    <tie type="start" />
+    <instrument id="P1-I1" />
+    <voice>1</voice>
+    <type>eighth</type>
+    <stem>up</stem>
+    <staff>1</staff>
+    <notations>
+     <tied type="start" orientation="under" />
+    </notations>
+   </note>
+   <note color="#000000" default-x="421">
+    <chord />
+    <pitch>
+     <step>E</step>
+     <alter>-1</alter>
+     <octave>3</octave>
+    </pitch>
+    <duration>128</duration>
+    <tie type="stop" />
+    <tie type="start" />
+    <instrument id="P1-I1" />
+    <type>eighth</type>
+    <staff>1</staff>
+    <notations>
+     <tied type="stop" orientation="over" />
+     <tied type="start" orientation="under" />
+    </notations>
+   </note>
+   <note color="#000000" default-x="421">
+    <chord />
+    <pitch>
+     <step>F</step>
+     <octave>3</octave>
+    </pitch>
+    <duration>128</duration>
+    <tie type="start" />
+    <instrument id="P1-I1" />
+    <type>eighth</type>
+    <staff>1</staff>
+    <notations>
+     <tied type="start" orientation="over" />
+    </notations>
+   </note>
+   <note color="#000000" default-x="585" default-y="20">
+    <pitch>
+     <step>G</step>
+     <octave>2</octave>
+    </pitch>
+    <duration>512</duration>
+    <tie type="stop" />
+    <instrument id="P1-I1" />
+    <voice>1</voice>
+    <type>half</type>
+    <stem>up</stem>
+    <staff>1</staff>
+    <notations>
+     <tied type="stop" orientation="under" />
+    </notations>
+   </note>
+   <note color="#000000" default-x="585">
+    <chord />
+    <pitch>
+     <step>E</step>
+     <alter>-1</alter>
+     <octave>3</octave>
+    </pitch>
+    <duration>512</duration>
+    <tie type="stop" />
+    <instrument id="P1-I1" />
+    <type>half</type>
+    <staff>1</staff>
+    <notations>
+     <tied type="stop" orientation="under" />
+    </notations>
+   </note>
+   <note color="#000000" default-x="585">
+    <chord />
+    <pitch>
+     <step>F</step>
+     <octave>3</octave>
+    </pitch>
+    <duration>512</duration>
+    <tie type="stop" />
+    <instrument id="P1-I1" />
+    <type>half</type>
+    <staff>1</staff>
+    <notations>
+     <tied type="stop" orientation="over" />
+    </notations>
+   </note>
+   <barline>
+    <bar-style>light-heavy</bar-style>
+   </barline>
+  </measure>
+ </part>
+</score-partwise>

+ 7 - 3
webpack.common.js

@@ -11,11 +11,15 @@ module.exports = {
         path: path.resolve(__dirname, 'build'),
         path: path.resolve(__dirname, 'build'),
         filename: '[name].js',
         filename: '[name].js',
         library: 'opensheetmusicdisplay',
         library: 'opensheetmusicdisplay',
-        libraryTarget: 'umd'
+        libraryTarget: 'umd',
+        globalObject: 'this'
     },
     },
     resolve: {
     resolve: {
+        alias: {
+            handlebars: 'handlebars/dist/handlebars.min.js'
+        },
         // Add '.ts' and '.tsx' as a resolvable extension.
         // Add '.ts' and '.tsx' as a resolvable extension.
-        extensions: ['.ts', '.tsx', '.js']
+        extensions: ['.ts', '.tsx', '.js'],
     },
     },
     module: {
     module: {
         rules: [
         rules: [
@@ -58,7 +62,7 @@ module.exports = {
         })
         })
     ],
     ],
     devServer: {
     devServer: {
-        contentBase: [
+        static: [
             path.join(__dirname, 'test/data'),
             path.join(__dirname, 'test/data'),
             path.join(__dirname, 'build'),
             path.join(__dirname, 'build'),
             path.join(__dirname, 'demo')
             path.join(__dirname, 'demo')

+ 1 - 1
webpack.dev.js

@@ -1,4 +1,4 @@
-var merge = require('webpack-merge')
+var { merge } = require('webpack-merge')
 var common = require('./webpack.common.js')
 var common = require('./webpack.common.js')
 var webpack = require('webpack')
 var webpack = require('webpack')
 
 

+ 2 - 2
webpack.prod.js

@@ -1,8 +1,8 @@
-var merge = require('webpack-merge')
+const { merge } = require('webpack-merge');
 var webpack = require('webpack')
 var webpack = require('webpack')
 var path = require('path')
 var path = require('path')
 var common = require('./webpack.common.js')
 var common = require('./webpack.common.js')
-var Visualizer = require('webpack-visualizer-plugin')
+var Visualizer = require('webpack-visualizer-plugin2')
 const { CleanWebpackPlugin } = require('clean-webpack-plugin')
 const { CleanWebpackPlugin } = require('clean-webpack-plugin')
 
 
 module.exports = merge(common, {
 module.exports = merge(common, {

+ 1 - 1
webpack.sourcemap.js

@@ -1,4 +1,4 @@
-var merge = require('webpack-merge')
+var { merge } = require('webpack-merge')
 var production = require('./webpack.prod.js')
 var production = require('./webpack.prod.js')
 
 
 // will create a build plus separate .min.js.map source map for debugging
 // will create a build plus separate .min.js.map source map for debugging