smoosic 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -454,6 +454,14 @@ export class SuiScoreViewOperations extends SuiScoreView {
454
454
  SmoOperation.setActiveVoice(this.score, index);
455
455
  this._renderChangedMeasures(measuresToAdd);
456
456
  await this.renderer.updatePromise();
457
+ this.tracker.selectActiveVoice();
458
+ }
459
+ async swapVoices(voice1: number, voice2: number): Promise<void> {
460
+ const selections = this.tracker.getSelectedMeasures();
461
+ const altSelections = this._getEquivalentSelections(selections);
462
+ SmoOperation.swapVoice(selections, voice1, voice2);
463
+ SmoOperation.swapVoice(altSelections, voice1, voice2);
464
+ this._renderChangedMeasures(selections);
457
465
  }
458
466
  /**
459
467
  * Assign an instrument to a set of measures
@@ -496,7 +496,6 @@ export class SvgPageMap {
496
496
  const x = (box.x - this.containerOffset.x) / cof;
497
497
  const y = (box.y - this.containerOffset.y) / cof;
498
498
  const logicalBox = SvgHelpers.boxPoints(x, y, Math.max(box.width / cof, 1), Math.max(box.height / cof, 1));
499
- logicalBox.y -= Math.round(logicalBox.y / this.layout.pageHeight) / this.layout.svgScale;
500
499
  if (layoutDebug.mask | layoutDebug.values['mouseDebug']) {
501
500
  layoutDebug.updateMouseDebug(box, logicalBox, this.containerOffset);
502
501
  }
@@ -666,6 +666,9 @@ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
666
666
  selection.box = JSON.parse(JSON.stringify(this.suggestion.box));
667
667
  selection.scrollBox = JSON.parse(JSON.stringify(this.suggestion.scrollBox));
668
668
  this.selections = [selection];
669
+ // There is a single selection, make sure the active voice is set to it.
670
+ selection.measure.setActiveVoice(selection.selector.voice);
671
+ this.selectActiveVoice();
669
672
  }
670
673
  }
671
674
  if (preselected && this.modifierSelections.length) {
@@ -219,7 +219,8 @@ export class VxMeasure implements VxMeasureIf {
219
219
  if (smoNote.fillStyle && !this.printing) {
220
220
  vexNote.setStyle({ fillStyle: smoNote.fillStyle });
221
221
  } else if (voiceIx > 0 && !this.printing) {
222
- vexNote.setStyle({ fillStyle: "#115511" });
222
+ const voiceFill = ['#115511','#555511','#883344']
223
+ vexNote.setStyle({ fillStyle: voiceFill[voiceIx - 1] });
223
224
  } else if (smoNote.isHidden() && this.printing) {
224
225
  vexNote.setStyle({ fillStyle: "#ffffff00" });
225
226
  }
@@ -989,8 +989,16 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
989
989
  */
990
990
  alignNotesWithTimeSignature() {
991
991
  const tsTicks = SmoMusic.timeSignatureToTicks(this.timeSignature.timeSignature);
992
- if (tsTicks === this.getMaxTicksVoice()) {
993
- return;
992
+ let aligned = true;
993
+ for (let i = 0; i < this.voices.length; ++i) {
994
+ const voice = this.voices[i];
995
+ if (this.getTicksFromVoice(i) !== tsTicks) {
996
+ aligned = false;
997
+ break;
998
+ }
999
+ }
1000
+ if (aligned) {
1001
+ return true;
994
1002
  }
995
1003
  const replaceNoteWithDuration = (target: number, ar: SmoNote[], note: SmoNote) => {
996
1004
  const fitNote = new SmoNote(SmoNote.defaults);
@@ -1193,7 +1201,32 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
1193
1201
  this.activeVoice = vix;
1194
1202
  }
1195
1203
  }
1196
-
1204
+ getSwapVoicePairs() {
1205
+ const rv = [];
1206
+ for (let i = 0; i < this.voices.length; ++i) {
1207
+ for (let j = i + 1; j < this.voices.length; ++j) {
1208
+ rv.push([i, j]);
1209
+ }
1210
+ }
1211
+ return rv;
1212
+ }
1213
+ swapVoices(voice1: number, voice2: number) {
1214
+ if (this.voices.length > voice1 && this.voices.length > voice2) {
1215
+ const v1 = this.voices[voice1];
1216
+ const v2 = this.voices[voice2];
1217
+ const nvoices: SmoVoice[] = [];
1218
+ for (let i = 0; i < this.voices.length; ++i) {
1219
+ if (i === voice1) {
1220
+ nvoices.push(v2);
1221
+ } else if (i === voice2) {
1222
+ nvoices.push(v1);
1223
+ } else {
1224
+ nvoices.push(this.voices[i]);
1225
+ }
1226
+ }
1227
+ this.voices = nvoices;
1228
+ }
1229
+ }
1197
1230
  tickmapForVoice(voiceIx: number) {
1198
1231
  return new TickMap(this, voiceIx);
1199
1232
  }
@@ -1534,7 +1534,7 @@ export class SmoMusic {
1534
1534
  return SmoMusic._validDurations;
1535
1535
  }
1536
1536
  /**
1537
- * Get the closest duration from ticks
1537
+ * Get the closest valid duration for this number of ticks, but not going over
1538
1538
  * @param ticks
1539
1539
  * @returns
1540
1540
  */
@@ -1549,6 +1549,15 @@ export class SmoMusic {
1549
1549
  }
1550
1550
  return null;
1551
1551
  }
1552
+ static closestSimpleDurationFromTicks(ticks: number): number {
1553
+ let closest = SmoMusic.durationsDescending[0];
1554
+ for (let i = 0; i < SmoMusic.durationsDescending.length; ++i) {
1555
+ if (Math.abs(SmoMusic.durationsDescending[i] - ticks) < closest) {
1556
+ closest = i;
1557
+ }
1558
+ }
1559
+ return closest;
1560
+ }
1552
1561
  static _ticksToDuration: Record<string, string> = {};
1553
1562
 
1554
1563
  // ### ticksToDuration
@@ -732,8 +732,11 @@ export class XmlState {
732
732
  smoTupletParams.startIndex = xmlTupletState.start!.tick;
733
733
  smoTupletParams.endIndex = xmlTupletState.end!.tick;
734
734
  for (let i = smoTupletParams.startIndex; i <= smoTupletParams.endIndex; i++) {
735
- smoTupletParams.totalTicks += notes[i].tickCount;
735
+ smoTupletParams.totalTicks += Math.floor(notes[i].tickCount);
736
736
  }
737
+ // Normalize to an allowed note length, because MusicXML durations do not add up
738
+ smoTupletParams.totalTicks = SmoMusic.closestSimpleDurationFromTicks(smoTupletParams.totalTicks);
739
+
737
740
  smoTupletParams.voice = xmlTupletState.start!.voice;
738
741
  const smoTuplet = new SmoTuplet(smoTupletParams);
739
742
  for (let i = 0; i < xmlTupletStateTreeNode.children.length; i++) {
@@ -124,6 +124,12 @@ export class SmoOperation {
124
124
  static populateVoice(selection: SmoSelection, voiceIx: number) {
125
125
  selection.measure.populateVoice(voiceIx);
126
126
  }
127
+ static swapVoice(selections: SmoSelection[], voice1: number, voice2: number) {
128
+ const measures = SmoSelection.getMeasureList(selections);
129
+ measures.forEach((ss) => {
130
+ ss.measure.swapVoices(voice1, voice2);
131
+ });
132
+ }
127
133
 
128
134
  static setTabStave(score: SmoScore, tabStave: SmoTabStave) {
129
135
  score.staves[tabStave.startSelector.staff].updateTabStave(tabStave);
@@ -1,5 +1,5 @@
1
1
  import { SuiMenuBase, SuiMenuParams, MenuDefinition, SuiMenuHandler, SuiMenuShowOption,
2
- SuiConfiguredMenuOption, SuiConfiguredMenu, customizeMenuOptionsFcn } from './menu';
2
+ SuiConfiguredMenuOption, SuiConfiguredMenu, MenuChoiceDefinition } from './menu';
3
3
  import { createAndDisplayDialog } from '../dialogs/dialog';
4
4
 
5
5
  declare var $: any;
@@ -10,15 +10,82 @@ declare var $: any;
10
10
  export class SuiVoiceMenu extends SuiConfiguredMenu {
11
11
  constructor(params: SuiMenuParams) {
12
12
  super(params, 'Voices', SuiVoiceMenuOptions);
13
- }
13
+ }
14
+ }
15
+ class voiceSwapperMenuOption implements SuiConfiguredMenuOption {
16
+ voice1: number;
17
+ voice2: number;
18
+ cmd: string;
19
+ label: string;
20
+ get menuChoice(): MenuChoiceDefinition {
21
+ return {
22
+ icon: '',
23
+ text: this.label,
24
+ value: this.cmd
25
+ }
26
+ }
27
+ constructor(voice1: number, voice2: number) {
28
+ this.voice1 = voice1;
29
+ this.voice2 = voice2;
30
+ this.label = `Swap ${voice1 + 1} and ${voice2 + 1}`;
31
+ this.cmd = `${voice1}To${voice2}`;
32
+ }
33
+ async handler(menu: SuiMenuBase) {
34
+ menu.view.swapVoices(this.voice1, this.voice2);
35
+ }
36
+ display(menu: SuiMenuBase) {
37
+ const measures = menu.view.tracker.getSelectedMeasures();
38
+ if (measures.length > 0) {
39
+ const sel = measures[0];
40
+ const swaps = sel.measure.getSwapVoicePairs();
41
+ for (let i = 0; i < swaps.length; ++i) {
42
+ const pair = swaps[i];
43
+ if (this.voice1 == pair[0] && this.voice2 == pair[1]) {
44
+ return true;
45
+ }
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ }
51
+ class selectVoiceMenuOption implements SuiConfiguredMenuOption {
52
+ voice: number;
53
+ constructor(voice: number) {
54
+ this.voice = voice;
55
+ }
56
+ async handler (menu: SuiMenuBase) {
57
+ await menu.view.populateVoice(this.voice);
58
+ }
59
+ display (menu: SuiMenuBase) {
60
+ for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
61
+ const mm = menu.view.tracker.selections[i].measure;
62
+ if (mm.voices.length === 1) {
63
+ return this.voice === 1;
64
+ }
65
+ // If there are n voices, and I am n+1, show option
66
+ if (mm.voices.length === this.voice) {
67
+ return true;
68
+ }
69
+ if (mm.voices.length > this.voice && mm.getActiveVoice() !== this.voice) {
70
+ return true;
71
+ }
72
+ }
73
+ return false;
74
+ }
75
+ get menuChoice() {
76
+ return {
77
+ icon: '',
78
+ text: `Voice ${this.voice + 1}`,
79
+ value: `voice${this.voice.toString()}`
80
+ };
81
+ }
14
82
  }
15
-
16
83
  /**
17
84
  * @category SuiMenu
18
85
  */
19
86
  const selectVoiceOneMenuOption: SuiConfiguredMenuOption = {
20
87
  handler: async (menu: SuiMenuBase) => {
21
- menu.view.populateVoice(0);
88
+ await menu.view.populateVoice(0);
22
89
  }, display: (menu: SuiMenuBase) => {
23
90
  for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
24
91
  const mm = menu.view.tracker.selections[i].measure;
@@ -39,11 +106,11 @@ const selectVoiceOneMenuOption: SuiConfiguredMenuOption = {
39
106
  */
40
107
  const selectVoiceTwoMenuOption: SuiConfiguredMenuOption = {
41
108
  handler: async (menu: SuiMenuBase) => {
42
- menu.view.populateVoice(1);
109
+ await menu.view.populateVoice(1);
43
110
  }, display: (menu: SuiMenuBase) => {
44
111
  for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
45
112
  const mm = menu.view.tracker.selections[i].measure;
46
- if (mm.voices.length < 4) {
113
+ if (mm.voices.length <= 4 && mm.voices.length > 1) {
47
114
  return true;
48
115
  }
49
116
  }
@@ -60,7 +127,7 @@ const selectVoiceTwoMenuOption: SuiConfiguredMenuOption = {
60
127
  */
61
128
  const selectVoiceThreeMenuOption: SuiConfiguredMenuOption = {
62
129
  handler: async (menu: SuiMenuBase) => {
63
- menu.view.populateVoice(2);
130
+ await menu.view.populateVoice(2);
64
131
  }, display: (menu: SuiMenuBase) => {
65
132
  for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
66
133
  const mm = menu.view.tracker.selections[i].measure;
@@ -81,7 +148,7 @@ const selectVoiceThreeMenuOption: SuiConfiguredMenuOption = {
81
148
  */
82
149
  const selectVoiceFourMenuOption: SuiConfiguredMenuOption = {
83
150
  handler: async (menu: SuiMenuBase) => {
84
- menu.view.populateVoice(3);
151
+ await menu.view.populateVoice(3);
85
152
  }, display: (menu: SuiMenuBase) => {
86
153
  for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
87
154
  const mm = menu.view.tracker.selections[i].measure;
@@ -102,7 +169,7 @@ const selectVoiceFourMenuOption: SuiConfiguredMenuOption = {
102
169
  */
103
170
  const removeVoiceMenuOption: SuiConfiguredMenuOption = {
104
171
  handler: async (menu: SuiMenuBase) => {
105
- menu.view.depopulateVoice();
172
+ await menu.view.depopulateVoice();
106
173
  }, display: (menu: SuiMenuBase) => {
107
174
  for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
108
175
  const mm = menu.view.tracker.selections[i].measure;
@@ -123,6 +190,15 @@ const removeVoiceMenuOption: SuiConfiguredMenuOption = {
123
190
  * @category SuiMenu
124
191
  */
125
192
  const SuiVoiceMenuOptions: SuiConfiguredMenuOption[] = [
126
- selectVoiceOneMenuOption, selectVoiceTwoMenuOption, selectVoiceThreeMenuOption, selectVoiceFourMenuOption,
193
+ new selectVoiceMenuOption(0),
194
+ new selectVoiceMenuOption(1),
195
+ new selectVoiceMenuOption(2),
196
+ new selectVoiceMenuOption(3),
197
+ new voiceSwapperMenuOption(0, 1),
198
+ new voiceSwapperMenuOption(0, 2),
199
+ new voiceSwapperMenuOption(0, 3),
200
+ new voiceSwapperMenuOption(1, 2),
201
+ new voiceSwapperMenuOption(1, 3),
202
+ new voiceSwapperMenuOption(2, 3),
127
203
  removeVoiceMenuOption
128
204
  ];
@@ -1,40 +0,0 @@
1
- <!DOCTYPE HTML>
2
- <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
3
-
4
- <head>
5
- <meta charset="utf-8">
6
- <title>Smoosic Unit Tests</title>
7
- <link href="../styles/fonts.css" rel="stylesheet">
8
- <link href="../styles/media.css" rel="stylesheet">
9
- <link href="../styles/ribbon.css" rel="stylesheet">
10
- <link href="../styles/dialogs.css" rel="stylesheet">
11
- <link href="../styles/menus.css" rel="stylesheet">
12
- <link href="../styles/piano.css" rel="stylesheet">
13
- <link href="../styles/tree.css" rel="stylesheet">
14
- <link href="../qunit.css" rel="stylesheet">
15
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js"
16
- integrity="sha512-uVSVjE7zYsGz4ag0HEzfugJ78oHCI1KhdkivjQro8ABL/PRiEO4ROwvrolYAcZnky0Fl/baWKYilQfWvESliRA=="
17
- crossorigin="anonymous" referrerpolicy="no-referrer"></script>
18
- <!-- script type="text/javascript" src="../../../vex_smoosic/vexflow_smoosic/build/vexflow-debug.js"></script -->
19
- <script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.slim.js"></script>
20
- <script type="text/javascript" src="../smoosic.js"></script>
21
- <script type="text/javascript">
22
- document.addEventListener("DOMContentLoaded", function (event) {
23
- Smo.createLoadTests();
24
- });
25
- </script>
26
- </head>
27
-
28
- <body>
29
- <div id="qunit"></div>
30
- <div id="qunit-fixture"></div>
31
- <!-- audio crossOrigin="anonymous" id="sample" src="https://aarondavidnewman.github.io/Smoosic/build/sound/piano_middle_C.mp3" / -->
32
- <div id="outer-container" style="overflow:auto">
33
- <div id="container1">
34
-
35
- </div>
36
- </div>
37
-
38
- </body>
39
-
40
- </html>
@@ -1,35 +0,0 @@
1
- <!DOCTYPE HTML>
2
- <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
3
- <head>
4
- <meta charset="utf-8">
5
- <title>Smoosic Editor</title>
6
- <link href="https://aarondavidnewman.github.io/Smoosic/build/styles/fonts.css" rel="stylesheet">
7
- <link href="../styles/media.css" rel="stylesheet">
8
- <link href="https://aarondavidnewman.github.io/Smoosic/build/styles/ribbon.css" rel="stylesheet">
9
- <link href="https://aarondavidnewman.github.io/Smoosic/build/styles/dialogs.css" rel="stylesheet">
10
- <link href="https://aarondavidnewman.github.io/Smoosic/build/styles/menus.css" rel="stylesheet">
11
- <link href="https://aarondavidnewman.github.io/Smoosic/build/styles/piano.css" rel="stylesheet">
12
- <link href="https://aarondavidnewman.github.io/Smoosic/build/styles/tree.css" rel="stylesheet">
13
- <script type="text/javascript" src="https://aarondavidnewman.github.io/vexflow_smoosic/releases/vexflow-debug.js"></script>
14
- <!-- script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js" integrity="sha512-uVSVjE7zYsGz4ag0HEzfugJ78oHCI1KhdkivjQro8ABL/PRiEO4ROwvrolYAcZnky0Fl/baWKYilQfWvESliRA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script -->
15
- <!-- script type="text/javascript" src="../../../vex_smoosic/vexflow_smoosic/build/vexflow-debug.js"></script -->
16
- <script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.slim.js" ></script>
17
- <script type="text/javascript" src="../jszip.js"></script>
18
- <script type="text/javascript" src="../smoosic.js" ></script>
19
- <script type="text/javascript">
20
- document.addEventListener("DOMContentLoaded", function (event) {
21
- var config = { smoPath: '..', mode: 'translate', language: 'de', scoreDomContainer: 'smoo' };
22
- Smo.SuiApplication.configure(config);
23
- });
24
- </script>
25
- </head>
26
- <body>
27
- <sub id="link-hdr"><a href="https://github.com/AaronDavidNewman/smoosic">Github site</a> |
28
- <a href="https://aarondavidnewman.github.io/Smoosic/release/docs/modules.html">source documentation</a> |
29
- <a href="https://aarondavidnewman.github.io/Smoosic/changes.html">change notes</a> |
30
- <a href="https://aarondavidnewman.github.io/Smoosic/release/html/smoosic.html">application</a><button class="close-header"><span class="icon icon-cross"></span></button></sub>
31
- <!-- audio crossOrigin="anonymous" id="sample" src="https://aarondavidnewman.github.io/Smoosic/build/sound/piano_middle_C.mp3" / -->
32
- <div id="smoo">
33
- </div>
34
- </body>
35
- </html>