smoosic 1.0.11 → 1.0.14

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.
Files changed (136) hide show
  1. package/README.md +1 -0
  2. package/build/smoosic.js +28 -28
  3. package/package.json +1 -1
  4. package/release/smoosic.js +28 -28
  5. package/src/application/application.ts +14 -2
  6. package/src/application/common.ts +38 -3
  7. package/src/application/configuration.ts +1 -1
  8. package/src/application/eventHandler.ts +27 -28
  9. package/src/application/exports.ts +2 -2
  10. package/src/application/keyCommands.ts +42 -10
  11. package/src/render/audio/musicCursor.ts +3 -1
  12. package/src/render/sui/piano.ts +1 -1
  13. package/src/render/sui/scoreView.ts +15 -11
  14. package/src/render/sui/scoreViewOperations.ts +9 -7
  15. package/src/render/sui/tracker.ts +57 -18
  16. package/src/render/vex/vxMeasure.ts +1 -0
  17. package/src/smo/data/common.ts +1 -0
  18. package/src/smo/data/measure.ts +14 -0
  19. package/src/smo/data/noteModifiers.ts +52 -7
  20. package/src/smo/data/scoreModifiers.ts +11 -3
  21. package/src/smo/data/staffModifiers.ts +10 -4
  22. package/src/smo/data/systemStaff.ts +19 -1
  23. package/src/smo/data/tuplet.ts +17 -0
  24. package/src/smo/mxml/xmlHelpers.ts +8 -1
  25. package/src/smo/xform/beamers.ts +51 -66
  26. package/src/smo/xform/copypaste.ts +8 -0
  27. package/src/smo/xform/selections.ts +13 -1
  28. package/src/smo/xform/tickDuration.ts +58 -27
  29. package/src/ui/dialogs/components/rocker.ts +5 -1
  30. package/src/ui/dialogs/endings.ts +1 -1
  31. package/src/ui/dialogs/preferences.ts +11 -0
  32. package/src/ui/keyBindings/default/editorKeys.ts +0 -7
  33. package/tsconfig.json +1 -1
  34. package/release/library/BachCantataBMV68.json +0 -1
  35. package/release/library/BachCantataBMV682.json +0 -1
  36. package/release/library/Beethoven_AnDieFerneGeliebte.xml +0 -4271
  37. package/release/library/Dichterliebe01.xml +0 -7613
  38. package/release/library/MozartTrio.xml +0 -3498
  39. package/release/library/Mozart_AnChloe.xml +0 -3986
  40. package/release/library/Postillionlied.json +0 -1
  41. package/release/library/ScottJoplin_The_Entertainer.xml +0 -20741
  42. package/release/library/Yama2.json +0 -1
  43. package/release/library/bigband/Bilongo-Mandinga.json +0 -1
  44. package/release/library/bigband/Plena.json +0 -1
  45. package/release/library/bigband/Poinciana.json +0 -1
  46. package/release/library/bigband/child who drums.json +0 -1
  47. package/release/library/cp/Another Hairdo.mxl +0 -0
  48. package/release/library/cp/Anthropology Eb.mxl +0 -0
  49. package/release/library/cp/Au Privave No 1 Eb.mxl +0 -0
  50. package/release/library/cp/Au Privave No 2 Eb.mxl +0 -0
  51. package/release/library/cp/Back Home Blues.mxl +0 -0
  52. package/release/library/cp/Ballade.mxl +0 -0
  53. package/release/library/cp/Billies Bounce.mxl +0 -0
  54. package/release/library/cp/Bloomdido.mxl +0 -0
  55. package/release/library/cp/Blues for Alice.mxl +0 -0
  56. package/release/library/cp/Celerity Alto.mxl +0 -0
  57. package/release/library/cp/Celerity.mxl +0 -0
  58. package/release/library/cp/Cheryl F07.mxl +0 -0
  59. package/release/library/cp/Cheryl.mxl +0 -0
  60. package/release/library/cp/Confirmation Eb 09.mxl +0 -0
  61. package/release/library/cp/Cool Blues Alto1.mxl +0 -0
  62. package/release/library/cp/Dewey Square.mxl +0 -0
  63. package/release/library/cp/Donna Lee Eb.musicxml +0 -9134
  64. package/release/library/cp/Donna Lee Eb.mxl +0 -0
  65. package/release/library/cp/Ko Ko Eb.mxl +0 -0
  66. package/release/library/cp/Little Benny.mxl +0 -0
  67. package/release/library/cp/Moose the Mooche Eb.mxl +0 -0
  68. package/release/library/cp/Night in Tunisia solo.mxl +0 -0
  69. package/release/library/cp/Nows the Time No 1.mxl +0 -0
  70. package/release/library/cp/Nows the Time No 2.mxl +0 -0
  71. package/release/library/cp/Ornithology Eb.mxl +0 -0
  72. package/release/library/cp/Parkers Mood.mxl +0 -0
  73. package/release/library/cp/Passport.mxl +0 -0
  74. package/release/library/cp/Red Cross.mxl +0 -0
  75. package/release/library/cp/Scrapple from the Apple Eb.mxl +0 -0
  76. package/release/library/cp/Shaw Nuff Eb.mxl +0 -0
  77. package/release/library/cp/Shaw Nuff.mxl +0 -0
  78. package/release/library/cp/She Rote No1.mxl +0 -0
  79. package/release/library/cp/She Rote No2.mxl +0 -0
  80. package/release/library/cp/The Bird 07.mxl +0 -0
  81. package/release/library/cp/The Bird.mxl +0 -0
  82. package/release/library/cp/Warming Up A Riff.mxl +0 -0
  83. package/release/library/cp/Yardbird Suite (alto solo).mxl +0 -0
  84. package/release/library/cp/Yardbird Suite Eb Alto+Bass+Chords.mxl +0 -0
  85. package/release/library/cp/Yardbird Suite Eb.mxl +0 -0
  86. package/release/library/cp/cplist.txt +0 -40
  87. package/release/library/hymns/AdesteFideles.json +0 -1
  88. package/release/library/hymns/Heshallfeed.json +0 -1
  89. package/release/library/hymns/Hyfrydol.json +0 -1
  90. package/release/library/hymns/Lakeshore.json +0 -1
  91. package/release/library/hymns/Precious Lord.json +0 -1
  92. package/release/library/hymns/Shade.json +0 -1
  93. package/release/library/hymns/SpiritofLivingGod.json +0 -1
  94. package/release/library/hymns/SweetSpirit.json +0 -1
  95. package/release/library/hymns/We3kings.json +0 -1
  96. package/release/library/hymns/Weisse-Flaggen.json +0 -1
  97. package/release/library/hymns/WorldPeacePrayer.json +0 -1
  98. package/release/library/hymns/balm.json +0 -1
  99. package/release/library/hymns/cplist.txt +0 -9
  100. package/release/library/hymns/nearerMyGod.json +0 -1
  101. package/release/library/links/bigband.json +0 -47
  102. package/release/library/links/cptranscriptions.json +0 -365
  103. package/release/library/links/hymns.json +0 -78
  104. package/release/library/links/messiah.json +0 -33
  105. package/release/library/links/musicxml.json +0 -62
  106. package/release/library/links/piano.json +0 -59
  107. package/release/library/links/samples.json +0 -75
  108. package/release/library/links/smoLibrary.json +0 -68
  109. package/release/library/links/soprano.json +0 -51
  110. package/release/library/links/transcriptions.json +0 -32
  111. package/release/library/links/trumpet.json +0 -76
  112. package/release/library/messiah/Messiah-I-1.json +0 -1
  113. package/release/library/messiah/Messiah-I-2.json +0 -1
  114. package/release/library/miditest/keytime.mid +0 -0
  115. package/release/library/miditest/ties.mid +0 -0
  116. package/release/library/miditest/triplet.mid +0 -0
  117. package/release/library/piano/BachInvention.json +0 -1
  118. package/release/library/piano/BachWTC-3.json +0 -1
  119. package/release/library/piano/Gnossienne3-3.json +0 -1
  120. package/release/library/piano/Gymnopedie No 1 (8).json +0 -1
  121. package/release/library/piano/Joplin-Entertainer.json +0 -1
  122. package/release/library/soprano/Gesu Bambino.json +0 -1
  123. package/release/library/soprano/LaudamusTrumpetSoprano.json +0 -1
  124. package/release/library/soprano/Postillionlied.json +0 -1
  125. package/release/library/soprano/Solovey.json +0 -1
  126. package/release/library/soprano/ThereIsABalm.json +0 -1
  127. package/release/library/trumpet/BachAirOrganTpt.json +0 -1
  128. package/release/library/trumpet/BachCantataBMV68.json +0 -1
  129. package/release/library/trumpet/BeethovenOp21-old.json +0 -1
  130. package/release/library/trumpet/BeethovenOp21.json +0 -1
  131. package/release/library/trumpet/Fireworks - trumpets.json +0 -1
  132. package/release/library/trumpet/HymnFreedom.json +0 -1
  133. package/release/library/trumpet/TelemannSonata3rd.json +0 -1
  134. package/release/library/xml/Gnossienne_No._3.mxl +0 -0
  135. package/release/library/xml/M_Book_1_Part_1_The_Open_Well_Tempered_Clavier.mxl +0 -0
  136. package/release/library/xml/lg-177775783.xml +0 -8674
@@ -32,7 +32,7 @@ import { PromiseHelpers } from '../common/promiseHelpers';
32
32
  import { SuiDom } from './dom';
33
33
  import { SuiKeyCommands } from './keyCommands';
34
34
  import { SuiEventHandler } from './eventHandler';
35
- import { KeyBinding, ModalEventHandlerProxy } from './common';
35
+ import { KeyBinding, ModalEventHandlerProxy, isTrackerKeyAction, isEditorKeyAction } from './common';
36
36
  import { SmoMeasure } from '../smo/data/measure';
37
37
  import { getDomContainer } from '../common/htmlHelpers';
38
38
  import { SuiHelp } from '../ui/help';
@@ -142,13 +142,25 @@ export class SuiApplication {
142
142
  */
143
143
  static get keyBindingDefaults(): KeyBinding[] {
144
144
  var editorKeys = SuiEventHandler.editorKeyBindingDefaults;
145
+ let unknownKeyAction: boolean = false;
145
146
  editorKeys.forEach((key) => {
146
- key.module = 'keyCommands'
147
+ key.module = 'keyCommands';
148
+ if (!isEditorKeyAction(key.action)) {
149
+ console.error(`unknown key action ${key.action} in configuration`);
150
+ unknownKeyAction = true;
151
+ }
147
152
  });
148
153
  var trackerKeys = SuiEventHandler.trackerKeyBindingDefaults;
149
154
  trackerKeys.forEach((key) => {
150
155
  key.module = 'tracker'
156
+ if (!isTrackerKeyAction(key.action)) {
157
+ console.error(`unknown key action ${key.action} in configuration`);
158
+ unknownKeyAction = true;
159
+ }
151
160
  });
161
+ if (unknownKeyAction) {
162
+ throw(`unknown key action in configuration`);
163
+ }
152
164
  return trackerKeys.concat(editorKeys);
153
165
  }
154
166
  /**
@@ -2,8 +2,43 @@ import { SuiScoreViewOperations } from "../render/sui/scoreViewOperations";
2
2
  import { SuiTracker } from "../render/sui/tracker";
3
3
  import { CompleteNotifier } from "../ui/common";
4
4
  import { ModalComponent } from "../ui/common";
5
+ import { KeyEvent } from "../smo/data/common";
5
6
  import { BrowserEventSource, EventHandler } from "../ui/eventSource";
6
7
 
8
+ export type trackerKeyAction = "moveHome" | "moveEnd" | "moveSelectionRight" | "moveSelectionLeft" |
9
+ "moveSelectionUp" | "moveSelectionDown" | "moveSelectionRightMeasure" | "moveSelectionLeftMeasure" |
10
+ "advanceModifierSelection" | "growSelectionRight" | "growSelectionLeft" |
11
+ "growSelectionRightMeasure" | "growSelectionRightMeasure" |
12
+ "moveSelectionPitchUp" | "moveSelectionPitchDown";
13
+ export const trackerKeyActions = ["moveHome" , "moveEnd" , "moveSelectionRight" , "moveSelectionLeft" ,
14
+ "moveSelectionUp" , "moveSelectionDown" , "moveSelectionRightMeasure" , "moveSelectionLeftMeasure" ,
15
+ "advanceModifierSelection" , "growSelectionRight" , "growSelectionLeft" , "growSelectionRightMeasure",
16
+ "moveSelectionPitchUp" , "moveSelectionPitchDown"];
17
+ export type editorKeyAction = "transposeUp" | "transposeDown" | "upOctave" | "downOctave" |
18
+ "toggleCourtesyAccidental" | "toggleEnharmonic" |
19
+ "doubleDuration" | "halveDuration" | "dotDuration" | "undotDuration" | "setPitch" |
20
+ "slashGraceNotes" | "addGraceNote" | "removeGraceNote" |
21
+ "playScore" | "stopPlayer" | "pausePlayer" | "togglePlayer" |
22
+ "undo" | "copy" | "paste" |
23
+ "makeTuplet" | "interval" | "unmakeTuplet" | "addMeasure" | "deleteNote" | "makeRest"|
24
+ "toggleBeamGroup" | "beamSelections" | "toggleBeamDirection" |
25
+ "addRemoveAccent" | "addRemoveTenuto" | "addRemoveStaccato" |
26
+ "addRemovePizzicato" | "addRemoveMarcato";
27
+ export const editorKeyActions = ["transposeUp" , "transposeDown" , "upOctave" , "downOctave" ,
28
+ "toggleCourtesyAccidental" , "toggleEnharmonic",
29
+ "doubleDuration" , "halveDuration" , "dotDuration" , "undotDuration" , "setPitch" ,
30
+ "slashGraceNotes" , "addGraceNote" , "removeGraceNote" ,
31
+ "playScore" , "stopPlayer" , "pausePlayer", "togglePlayer",
32
+ "undo", "copy", "paste",
33
+ "makeTuplet" , "interval" , "unmakeTuplet" , "addMeasure" , "deleteNote" , "makeRest",
34
+ "toggleBeamGroup" , "beamSelections" , "toggleBeamDirection",
35
+ "addRemoveAccent" , "addRemoveTenuto" , "addRemoveStaccato",
36
+ "addRemovePizzicato", "addRemoveMarcato"
37
+ ];
38
+
39
+ export function isEditorKeyAction(action: string) { return editorKeyActions.indexOf(action) >= 0 };
40
+ export function isTrackerKeyAction(action: string) { return trackerKeyActions.indexOf(action) >= 0 };
41
+
7
42
  /**
8
43
  * A binding of a key to some action performed by a module
9
44
  * @category SuiApplication
@@ -43,7 +78,7 @@ export interface KeyCommandParams {
43
78
  export abstract class ModalEventHandler {
44
79
  abstract mouseMove(ev: any): void;
45
80
  abstract mouseClick(ev: any): void;
46
- abstract evKey(evdata: any): void;
81
+ abstract evKey(evdata: any): Promise<void>;
47
82
  abstract keyUp(evdata: any): void;
48
83
  }
49
84
  export type handler = (ev: any) => void;
@@ -69,9 +104,9 @@ export class ModalEventHandlerProxy {
69
104
  this._handler = value;
70
105
  this.unbound = false;
71
106
  }
72
- evKey(ev: any) {
107
+ async evKey(ev: any) {
73
108
  if (this._handler) {
74
- this._handler.evKey(ev);
109
+ await this._handler.evKey(ev);
75
110
  }
76
111
  }
77
112
  keyUp(ev: any) {
@@ -86,7 +86,7 @@ export interface SmoConfigurationParams {
86
86
  leftControls: 'controls-left',
87
87
  topControls: 'controls-top',
88
88
  scoreDomContainer: 'smo-scroll-region',
89
- libraryUrl: 'https://smoosic.github.io/Smoosic/release/library/links/smoLibrary.json',
89
+ libraryUrl: 'https://smoosic.github.io/SmoScores/links/smoLibrary.json',
90
90
  demonPollTime: 50, // how often we poll the score to see if it changed
91
91
  idleRedrawTime: 1000, // maximum time between score modification and render
92
92
  ribbonLayout: defaultRibbonLayout.ribbons,
@@ -165,10 +165,10 @@ export class SuiEventHandler implements ModalEventHandler {
165
165
  return;
166
166
  // this.unbindKeyboardForModal(dialog);
167
167
  } else {
168
- this.view.tracker.advanceModifierSelection(this.view.score, ev);
168
+ this.view.tracker.advanceModifierSelection(ev);
169
169
  }
170
170
  } else {
171
- this.view.tracker.selectSuggestion(this.view.score, ev);
171
+ this.view.tracker.selectSuggestion(ev);
172
172
  }
173
173
  return;
174
174
  }
@@ -254,7 +254,7 @@ export class SuiEventHandler implements ModalEventHandler {
254
254
  }
255
255
  }
256
256
 
257
- evKey(evdata: any) {
257
+ async evKey(evdata: any) {
258
258
  if ($('body').hasClass('translation-mode')) {
259
259
  return;
260
260
  }
@@ -270,34 +270,33 @@ export class SuiEventHandler implements ModalEventHandler {
270
270
  Qwerty.handleKeyEvent(evdata);
271
271
  }
272
272
  const dataCopy = SuiTracker.serializeEvent(evdata);
273
- this.view.renderer.updatePromise().then(() => {
274
- if (dataCopy.key == '?') {
275
- SuiHelp.displayHelp();
276
- }
277
- if (dataCopy.key == 'Enter') {
278
- this.trackerModifierSelect(dataCopy);
279
- }
273
+ await this.view.renderer.updatePromise();
274
+ if (dataCopy.key == '?') {
275
+ SuiHelp.displayHelp();
276
+ }
277
+ if (dataCopy.key == 'Enter') {
278
+ this.trackerModifierSelect(dataCopy);
279
+ }
280
280
 
281
- var binding: KeyBinding | undefined = this.keyBind.find((ev: KeyBinding) =>
282
- ev.event === 'keydown' && ev.key === dataCopy.key &&
283
- ev.ctrlKey === dataCopy.ctrlKey &&
284
- ev.altKey === dataCopy.altKey && dataCopy.shiftKey === ev.shiftKey);
281
+ var binding: KeyBinding | undefined = this.keyBind.find((ev: KeyBinding) =>
282
+ ev.event === 'keydown' && ev.key === dataCopy.key &&
283
+ ev.ctrlKey === dataCopy.ctrlKey &&
284
+ ev.altKey === dataCopy.altKey && dataCopy.shiftKey === ev.shiftKey);
285
285
 
286
- if (binding) {
287
- try {
288
- if (binding.module === 'tracker') {
289
- (this.tracker as any)[binding.action](this.view.score, dataCopy);
290
- } else {
291
- (this.keyCommands as any)[binding.action](dataCopy);
292
- }
293
- } catch (e) {
294
- if (typeof (e) === 'string') {
295
- console.error(e);
296
- }
297
- this.exhandler.exceptionHandler(e);
286
+ if (binding) {
287
+ try {
288
+ if (binding.module === 'tracker') {
289
+ (this.tracker as any)[binding.action](dataCopy);
290
+ } else {
291
+ (this.keyCommands as any)[binding.action](dataCopy);
292
+ }
293
+ } catch (e) {
294
+ if (typeof (e) === 'string') {
295
+ console.error(e);
298
296
  }
297
+ this.exhandler.exceptionHandler(e);
299
298
  }
300
- });
299
+ }
301
300
  }
302
301
 
303
302
  mouseMove(ev: any) {
@@ -310,7 +309,7 @@ export class SuiEventHandler implements ModalEventHandler {
310
309
  mouseClick(ev: any) {
311
310
  const dataCopy = SuiTracker.serializeEvent(ev);
312
311
  this.view.renderer.updatePromise().then(() => {
313
- this.view.tracker.selectSuggestion(this.view.score, dataCopy);
312
+ this.view.tracker.selectSuggestion(dataCopy);
314
313
  var modifier = this.view.tracker.getSelectedModifier();
315
314
  if (modifier) {
316
315
  this.createModifierDialog(modifier);
@@ -40,7 +40,7 @@ import { SuiInstrumentDialog } from '../ui/dialogs/instrument';
40
40
  import { SuiTimeSignatureDialog } from '../ui/dialogs/timeSignature';
41
41
  import { SuiTempoDialog } from '../ui/dialogs/tempo';
42
42
  import { SuiNoteHeadAdapter, SuiNoteHeadDialog, SuiNoteHeadButtonComponent, SuiStemButtonComponent } from '../ui/dialogs/noteHead';
43
- import { SuiEndingsAdapter, SuiEndingsDialog,
43
+ import { SuiEndingsAdapter, SuiEndingsDialog, endingsButtonFactory,
44
44
  SuiEndBarButtonComponent, SuiStartBarButtonComponent, SuiRepeatSymbolButtonComponent } from '../ui/dialogs/endings';
45
45
  import { SuiScoreIdentificationDialog } from '../ui/dialogs/scoreId';
46
46
  import { SuiScorePreferencesDialog } from '../ui/dialogs/preferences';
@@ -365,7 +365,7 @@ export const Smo = {
365
365
  SuiArticulationDialog, SuiArticulationButtonComponent, SuiArticulationAdapter,
366
366
  SuiMicrotoneAdapter, SuiMicrotoneButtonComponent, SuiMicrotoneDialog,
367
367
  SuiNoteHeadAdapter, SuiNoteHeadDialog, SuiStemButtonComponent, SuiNoteHeadButtonComponent,
368
- SuiEndingsAdapter, SuiEndingsDialog,
368
+ SuiEndingsAdapter, SuiEndingsDialog, endingsButtonFactory,
369
369
  SuiEndBarButtonComponent, SuiStartBarButtonComponent, SuiRepeatSymbolButtonComponent,
370
370
  SuiPrintFileDialog, SuiSaveFileDialog, SuiSaveXmlDialog, SuiSaveVexDialog,
371
371
  SuiSaveMidiDialog, SuiDialogBase, createAndDisplayDialog,
@@ -9,14 +9,41 @@ import { BrowserEventSource } from '../ui/eventSource';
9
9
  import { SuiTracker } from '../render/sui/tracker';
10
10
  import { KeyCommandParams } from './common';
11
11
  import { CompleteNotifier } from '../ui/common';
12
- import { PitchLetter, IsPitchLetter, KeyEvent } from '../smo/data/common';
13
-
12
+ import { PitchLetter, IsPitchLetter, KeyEvent, keyHandler, defaultKeyEvent } from '../smo/data/common';
13
+
14
+ export interface EditorKeyHandler {
15
+ transposeUp: keyHandler,
16
+ transposeDown: keyHandler,
17
+ upOctave: keyHandler,
18
+ toggleCourtesyAccidental: keyHandler,
19
+ toggleEnharmonic: keyHandler,
20
+ doubleDuration: keyHandler,
21
+ halveDuration: keyHandler,
22
+ dotDuration: keyHandler,
23
+ undotDuration: keyHandler,
24
+ setPitch: keyHandler,
25
+ slashGraceNotes: keyHandler,
26
+ addGraceNote: keyHandler,
27
+ removeGraceNote: keyHandler,
28
+ playScore: keyHandler,
29
+ stopPlayer: keyHandler,
30
+ makeTuplet: keyHandler,
31
+ interval: keyHandler,
32
+ unmakeTuplet: keyHandler,
33
+ addMeasure: keyHandler,
34
+ deleteNote: keyHandler,
35
+ toggleBeamGroup: keyHandler,
36
+ beamSelections: keyHandler,
37
+ addRemoveAccent: keyHandler,
38
+ addRemoveTenuto: keyHandler,
39
+ addRemoveStaccato: keyHandler
40
+ }
14
41
  /**
15
- * KeyCommands object handles key events and converts them into commands, updating the score and
42
+ * KeyCommands object handles key events and converts them into commands: keyHandler, updating the score and
16
43
  * display
17
44
  * @category SuiApplication
18
45
  * */
19
- export class SuiKeyCommands {
46
+ export class SuiKeyCommands implements EditorKeyHandler {
20
47
  view: SuiScoreViewOperations;
21
48
  slashMode: boolean = false;
22
49
  completeNotifier: CompleteNotifier;
@@ -97,8 +124,9 @@ export class SuiKeyCommands {
97
124
  await this.view.setInterval(direction * interval);
98
125
  }
99
126
 
100
- async interval(keyEvent: KeyEvent) {
127
+ async interval(ev?: KeyEvent) {
101
128
  // code='Digit3'
129
+ const keyEvent = ev ?? defaultKeyEvent();
102
130
  var interval = parseInt(keyEvent.keyCode.toString(), 10) - 49; // 48 === '0', 0 indexed
103
131
  if (isNaN(interval) || interval < 1 || interval > 7) {
104
132
  return;
@@ -129,7 +157,8 @@ export class SuiKeyCommands {
129
157
  await this.view.setPitch(letter);
130
158
  }
131
159
 
132
- async setPitch(keyEvent: KeyEvent) {
160
+ async setPitch(ev?: KeyEvent) {
161
+ const keyEvent = ev ?? defaultKeyEvent();
133
162
  const letter = keyEvent.key.toLowerCase();
134
163
  if (IsPitchLetter(letter)) {
135
164
  await this.setPitchCommand(letter);
@@ -152,8 +181,8 @@ export class SuiKeyCommands {
152
181
  await this.view.batchDurationOperation('halveDuration');
153
182
  }
154
183
 
155
- async addMeasure(keyEvent: KeyEvent) {
156
- await this.view.addMeasure(keyEvent.shiftKey);
184
+ async addMeasure(keyEvent?: KeyEvent) {
185
+ await this.view.addMeasure(false);
157
186
  }
158
187
  async deleteNote() {
159
188
  await this.view.deleteNote();
@@ -167,9 +196,12 @@ export class SuiKeyCommands {
167
196
  }
168
197
 
169
198
  async makeTupletCommand(numNotes: number) {
170
- await this.view.makeTuplet({numNotes: numNotes, notesOccupied: 2, bracketed: true, ratioed: false});
199
+ await this.view.makeTuplet({ numNotes: numNotes, notesOccupied: 2, bracketed: true, ratioed: false });
171
200
  }
172
- async makeTuplet(keyEvent: KeyEvent) {
201
+ async makeTuplet(keyEvent?: KeyEvent) {
202
+ if (!keyEvent) {
203
+ return;
204
+ }
173
205
  const numNotes = parseInt(keyEvent.key, 10);
174
206
  await this.makeTupletCommand(numNotes);
175
207
  }
@@ -81,6 +81,8 @@ export const defaultClearAudioAnimationHandler = (delay: number) => {
81
81
  ctx.closeGroup();
82
82
  ctx.restore();
83
83
  layoutDebug.updatePlayDebug(selector, measure.svg.logicalBox);
84
- scroller.scrollVisibleBox(zmeasureSel.measure.svg.logicalBox);
84
+ if (view.score.preferences.autoScrollPlayback) {
85
+ scroller.scrollVisibleBox(zmeasureSel.measure.svg.logicalBox);
86
+ }
85
87
  }
86
88
  }
@@ -123,7 +123,7 @@ export class SuiPiano {
123
123
  this.view.tracker.moveSelectionLeft();
124
124
  });
125
125
  $('button.jsRight').off('click').on('click', () => {
126
- this.view.tracker.moveSelectionRight(false);
126
+ this.view.tracker.moveSelectionRight();
127
127
  });
128
128
  $('button.jsGrowDuration').off('click').on('click', () => {
129
129
  this.view.batchDurationOperation('doubleDuration');
@@ -241,24 +241,28 @@ export abstract class SuiScoreView {
241
241
  // The length of the paste buffer, in ticks
242
242
  const ticksToPaste = this.storePaste.getCopyBufferTickCount();
243
243
  const selections: SmoSelection[] = SmoSelection.getMeasureList(this.tracker.selections);
244
- const tm = selections[0].measure.tickmapForVoice(selections[0].selector.voice);
245
- // The last measure selected
246
- const lastSelection = this.tracker.selections[this.tracker.selections.length - 1];
244
+ if (!selections.length) {
245
+ return [];
246
+ }
247
+ const destination = selections[0];
248
+ selections.splice(0);
249
+ selections.push(destination);
250
+ const voice = destination.selector.voice;
251
+ const tm = destination.measure.tickmapForVoice(voice);
247
252
  // length of first selected measure, in ticks
248
- const measureTicks = this.tracker.selections[0].measure.getTicksFromVoice(this.tracker.selections[0].selector.voice);
253
+ const measureTicks = destination.measure.getTicksFromVoice(voice);
249
254
  // remaining ticks after first selection. This is our starting point.
250
255
  let startTick = measureTicks - tm.durationMap[this.tracker.selections[0].selector.tick];
251
- // Add ticks for all remaining measures
252
- for (let i = 1; i < selections.length; ++i) {
253
- const sel = selections[i];
254
- startTick += sel.measure.getTicksFromVoice(0);
255
- }
256
+ let currentMeasure = destination.selector.measure + 1;
256
257
  // if we are short, and there are measures left, add them to the selection list
257
- if (startTick < ticksToPaste && lastSelection.selector.measure < (this.score.staves[0].measures.length + 1)) {
258
- const newSel = SmoSelection.measureSelection(this.score, selections[0].selector.staff, lastSelection.selector.measure + 1);
258
+ while (startTick < ticksToPaste && destination.staff.measures.length > currentMeasure) {
259
+ const newSel = SmoSelection.measureSelection(this.score,
260
+ destination.selector.staff, currentMeasure);
259
261
  if (newSel) {
260
262
  selections.push(newSel);
263
+ startTick += newSel.measure.getTicksFromThisOrAnyVoice(voice);
261
264
  }
265
+ currentMeasure += 1;
262
266
  }
263
267
  return selections;
264
268
  }
@@ -1182,7 +1182,9 @@ export class SuiScoreViewOperations extends SuiScoreView {
1182
1182
  const altSel = this._getEquivalentSelection(selected);
1183
1183
  SmoOperation.setPitch(altSel!, [pitch]);
1184
1184
  if (this.score.preferences.autoAdvance) {
1185
- this.tracker.moveSelectionRight(true);
1185
+ // Don't play the next note and the added pitch at the same time.
1186
+ this.tracker.deferNextAutoPlay();
1187
+ this.tracker.moveSelectionRight();
1186
1188
  }
1187
1189
  });
1188
1190
  if (selections.length === 1 && this.score.preferences.autoPlay) {
@@ -1936,7 +1938,7 @@ export class SuiScoreViewOperations extends SuiScoreView {
1936
1938
  * @returns
1937
1939
  */
1938
1940
  async moveHome(ev: KeyEvent): Promise<any> {
1939
- this.tracker.moveHome(this.score, ev);
1941
+ this.tracker.moveHome(ev);
1940
1942
  await this.renderer.updatePromise();
1941
1943
  }
1942
1944
  /**
@@ -1946,7 +1948,7 @@ export class SuiScoreViewOperations extends SuiScoreView {
1946
1948
  * @returns
1947
1949
  */
1948
1950
  async moveEnd(ev: KeyEvent): Promise<any> {
1949
- this.tracker.moveEnd(this.score, ev);
1951
+ this.tracker.moveEnd(ev);
1950
1952
  await this.renderer.updatePromise();
1951
1953
  }
1952
1954
  /**
@@ -1973,7 +1975,7 @@ export class SuiScoreViewOperations extends SuiScoreView {
1973
1975
  * @returns
1974
1976
  */
1975
1977
  async advanceModifierSelection(keyEv: KeyEvent): Promise<any> {
1976
- this.tracker.advanceModifierSelection(this.score, keyEv);
1978
+ this.tracker.advanceModifierSelection(keyEv);
1977
1979
  await this.renderer.updatePromise();
1978
1980
  }
1979
1981
  /**
@@ -1989,8 +1991,8 @@ export class SuiScoreViewOperations extends SuiScoreView {
1989
1991
  * @param ev
1990
1992
  * @returns
1991
1993
  */
1992
- async moveSelectionRight(toPlay: boolean = true): Promise<any> {
1993
- this.tracker.moveSelectionRight(toPlay);
1994
+ async moveSelectionRight(): Promise<any> {
1995
+ this.tracker.moveSelectionRight();
1994
1996
  await this.renderer.updatePromise();
1995
1997
  }
1996
1998
  /**
@@ -2054,7 +2056,7 @@ export class SuiScoreViewOperations extends SuiScoreView {
2054
2056
  * @returns
2055
2057
  */
2056
2058
  async selectSuggestion(evData: KeyEvent): Promise<any> {
2057
- this.tracker.selectSuggestion(this.score, evData);
2059
+ this.tracker.selectSuggestion(evData);
2058
2060
  await this.renderer.updatePromise();
2059
2061
  }
2060
2062
  /**
@@ -6,7 +6,7 @@ import { SmoSelection, SmoSelector, ModifierTab } from '../../smo/xform/selectio
6
6
  import { smoSerialize } from '../../common/serializationHelpers';
7
7
  import { SuiOscillator } from '../audio/oscillator';
8
8
  import { SmoScore } from '../../smo/data/score';
9
- import { SvgBox, KeyEvent } from '../../smo/data/common';
9
+ import { SvgBox, KeyEvent, defaultKeyEvent, keyHandler } from '../../smo/data/common';
10
10
  import { SuiScroller } from './scroller';
11
11
  import { PasteBuffer } from '../../smo/xform/copypaste';
12
12
  import { SmoNote } from '../../smo/data/note';
@@ -14,15 +14,31 @@ import { SmoMeasure } from '../../smo/data/measure';
14
14
  import { layoutDebug } from './layoutDebug';
15
15
  declare var $: any;
16
16
 
17
+ export interface TrackerKeyHandler {
18
+ moveHome : keyHandler,
19
+ moveEnd : keyHandler,
20
+ moveSelectionRight : keyHandler,
21
+ moveSelectionLeft : keyHandler,
22
+ moveSelectionUp : keyHandler,
23
+ moveSelectionDown : keyHandler,
24
+ moveSelectionRightMeasure : keyHandler,
25
+ moveSelectionLeftMeasure : keyHandler,
26
+ advanceModifierSelection : keyHandler,
27
+ growSelectionRight : keyHandler,
28
+ growSelectionLeft : keyHandler,
29
+ moveSelectionPitchUp : keyHandler,
30
+ moveSelectionPitchDown: keyHandler
31
+ }
17
32
  /**
18
33
  * SuiTracker
19
34
  * A tracker maps the UI elements to the logical elements ,and allows the user to
20
35
  * move through the score and make selections, for navigation and editing.
21
36
  * @category SuiRender
22
37
  */
23
- export class SuiTracker extends SuiMapper {
38
+ export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
24
39
  idleTimer: number = Date.now();
25
40
  musicCursorGlyph: SVGSVGElement | null = null;
41
+ deferPlayAdvance: boolean = false;
26
42
  static get strokes(): Record<string, StrokeInfo> {
27
43
  return {
28
44
  suggestion: {
@@ -75,6 +91,21 @@ export class SuiTracker extends SuiMapper {
75
91
  getIdleTime(): number {
76
92
  return this.idleTimer;
77
93
  }
94
+ playSelection(artifact: SmoSelection) {
95
+ if (!this.deferPlayAdvance && this.score) {
96
+ SuiOscillator.playSelectionNow(artifact, this.score, 1);
97
+ } else {
98
+ this.deferPlayAdvance = false;
99
+ }
100
+ }
101
+ deferNextAutoPlay() {
102
+ if (this.score) {
103
+ // don't play on advance if we've just added a note and played it because they overlap
104
+ if (this.score.preferences.autoAdvance && this.score.preferences.autoPlay) {
105
+ this.deferPlayAdvance = true;
106
+ }
107
+ }
108
+ }
78
109
 
79
110
  getSelectedModifier() {
80
111
  if (this.modifierSelections.length) {
@@ -96,7 +127,7 @@ export class SuiTracker extends SuiMapper {
96
127
  return rv;
97
128
  }
98
129
 
99
- advanceModifierSelection(score: SmoScore, keyEv: KeyEvent | null) {
130
+ advanceModifierSelection(keyEv?: KeyEvent) {
100
131
  if (!keyEv) {
101
132
  return;
102
133
  }
@@ -220,14 +251,15 @@ export class SuiTracker extends SuiMapper {
220
251
  return 0;
221
252
  }
222
253
  if (!this.mapping && this.autoPlay && skipPlay === false && this.score) {
223
- SuiOscillator.playSelectionNow(artifact, this.score, 1);
254
+ this.playSelection(artifact);
224
255
  }
225
256
  this.selections.push(artifact);
226
257
  this.deferHighlight();
227
258
  this._createLocalModifiersList();
228
259
  return (artifact.note as SmoNote).tickCount;
229
260
  }
230
- moveHome(score: SmoScore, evKey: KeyEvent) {
261
+ moveHome(keyEvent?: KeyEvent) {
262
+ const evKey = keyEvent ?? defaultKeyEvent();
231
263
  this.idleTimer = Date.now();
232
264
  const ls = this.selections[0].staff;
233
265
  if (evKey.ctrlKey) {
@@ -235,7 +267,7 @@ export class SuiTracker extends SuiMapper {
235
267
  const homeSel = this._getClosestTick({ staff: ls.staffId,
236
268
  measure: 0, voice: mm.getActiveVoice(), tick: 0, pitches: [] });
237
269
  if (evKey.shiftKey) {
238
- this._selectBetweenSelections(score, this.selections[0], homeSel);
270
+ this._selectBetweenSelections(this.selections[0], homeSel);
239
271
  } else {
240
272
  this.selections = [homeSel];
241
273
  this.deferHighlight();
@@ -253,7 +285,7 @@ export class SuiTracker extends SuiMapper {
253
285
  measure: mm.measureNumber.measureIndex, voice: mm.getActiveVoice(),
254
286
  tick: 0, pitches: [] });
255
287
  if (evKey.shiftKey) {
256
- this._selectBetweenSelections(score, this.selections[0], homeSel);
288
+ this._selectBetweenSelections(this.selections[0], homeSel);
257
289
  } else if (homeSel?.measure?.svg?.logicalBox) {
258
290
  this.selections = [homeSel];
259
291
  this.scroller.scrollVisibleBox(homeSel.measure.svg.logicalBox);
@@ -262,9 +294,10 @@ export class SuiTracker extends SuiMapper {
262
294
  }
263
295
  }
264
296
  }
265
- moveEnd(score: SmoScore, evKey: KeyEvent) {
297
+ moveEnd(keyEvent?: KeyEvent) {
266
298
  this.idleTimer = Date.now();
267
299
  const ls = this.selections[0].staff;
300
+ const evKey = keyEvent ?? defaultKeyEvent();
268
301
  if (evKey.ctrlKey) {
269
302
  const lm = ls.measures[ls.measures.length - 1];
270
303
  const voiceIx = lm.getActiveVoice();
@@ -272,7 +305,7 @@ export class SuiTracker extends SuiMapper {
272
305
  const endSel = this._getClosestTick({ staff: ls.staffId,
273
306
  measure: ls.measures.length - 1, voice: voiceIx, tick: voice.notes.length - 1, pitches: [] });
274
307
  if (evKey.shiftKey) {
275
- this._selectBetweenSelections(score, this.selections[0], endSel);
308
+ this._selectBetweenSelections(this.selections[0], endSel);
276
309
  } else {
277
310
  this.selections = [endSel];
278
311
  this.deferHighlight();
@@ -292,7 +325,7 @@ export class SuiTracker extends SuiMapper {
292
325
  const endSel = this._getClosestTick({ staff: ls.staffId,
293
326
  measure: lm.measureNumber.measureIndex, voice: lm.getActiveVoice(), tick: ticks - 1, pitches: [] });
294
327
  if (evKey.shiftKey) {
295
- this._selectBetweenSelections(score, this.selections[0], endSel);
328
+ this._selectBetweenSelections(this.selections[0], endSel);
296
329
  } else {
297
330
  this.selections = [endSel];
298
331
  this.deferHighlight();
@@ -342,7 +375,7 @@ export class SuiTracker extends SuiMapper {
342
375
  artifact.measure.setActiveVoice(nselect.voice);
343
376
  this.selections.push(artifact);
344
377
  if (this.autoPlay && this.score) {
345
- SuiOscillator.playSelectionNow(artifact, this.score, 1);
378
+ this.playSelection(artifact);
346
379
  }
347
380
  this.deferHighlight();
348
381
  this._createLocalModifiersList();
@@ -350,10 +383,11 @@ export class SuiTracker extends SuiMapper {
350
383
  }
351
384
 
352
385
  // if we are being moved right programmatically, avoid playing the selected note.
353
- moveSelectionRight(skipPlay: boolean) {
386
+ moveSelectionRight() {
354
387
  if (this.selections.length === 0 || this.score === null) {
355
388
  return;
356
389
  }
390
+ const skipPlay = !this.score.preferences.autoPlay;
357
391
  // const original = JSON.parse(JSON.stringify(this.getExtremeSelection(-1).selector));
358
392
  const nselect = this._getOffsetSelection(1);
359
393
  // skip any measures that are not displayed due to rest or repetition
@@ -479,7 +513,8 @@ export class SuiTracker extends SuiMapper {
479
513
  artifact = SmoSelection.noteSelection(this.score, 0, 0, 0, 0);
480
514
  }
481
515
  if (!skipPlay && this.autoPlay && artifact) {
482
- SuiOscillator.playSelectionNow(artifact, this.score, 1);
516
+ this.playSelection(artifact);
517
+
483
518
  }
484
519
  if (!artifact) {
485
520
  return;
@@ -538,7 +573,7 @@ export class SuiTracker extends SuiMapper {
538
573
  SmoSelector.neq(sel.selector, selection.selector)
539
574
  );
540
575
  if (this.autoPlay && this.score) {
541
- SuiOscillator.playSelectionNow(selection, this.score, 1);
576
+ this.playSelection(selection);
542
577
  }
543
578
  ar.push(selection);
544
579
  this.selections = ar;
@@ -565,7 +600,11 @@ export class SuiTracker extends SuiMapper {
565
600
  }
566
601
  this.idleTimer = Date.now();
567
602
  }
568
- _selectBetweenSelections(score: SmoScore, s1: SmoSelection, s2: SmoSelection) {
603
+ _selectBetweenSelections(s1: SmoSelection, s2: SmoSelection) {
604
+ const score = this.renderer.score ?? null;
605
+ if (!score) {
606
+ return;
607
+ }
569
608
  const min = SmoSelector.gt(s1.selector, s2.selector) ? s2 : s1;
570
609
  const max = SmoSelector.lt(min.selector, s2.selector) ? s2 : s1;
571
610
  this._selectFromToInStaff(score, min, max);
@@ -573,7 +612,7 @@ export class SuiTracker extends SuiMapper {
573
612
  this.highlightQueue.selectionCount = this.selections.length;
574
613
  this.deferHighlight();
575
614
  }
576
- selectSuggestion(score: SmoScore,ev: KeyEvent) {
615
+ selectSuggestion(ev: KeyEvent) {
577
616
  if (!this.suggestion || !this.suggestion.measure || this.score === null) {
578
617
  return;
579
618
  }
@@ -596,7 +635,7 @@ export class SuiTracker extends SuiMapper {
596
635
  if (ev.shiftKey) {
597
636
  const sel1 = this.getExtremeSelection(-1);
598
637
  if (sel1.selector.staff === this.suggestion.selector.staff) {
599
- this._selectBetweenSelections(score, sel1, this.suggestion);
638
+ this._selectBetweenSelections(sel1, this.suggestion);
600
639
  return;
601
640
  }
602
641
  }
@@ -608,7 +647,7 @@ export class SuiTracker extends SuiMapper {
608
647
  return;
609
648
  }
610
649
  if (this.autoPlay) {
611
- SuiOscillator.playSelectionNow(this.suggestion, this.score, 1);
650
+ this.playSelection(this.suggestion);
612
651
  }
613
652
 
614
653
  const preselected = this.selections[0] ?
@@ -364,6 +364,7 @@ export class VxMeasure implements VxMeasureIf {
364
364
  vexNotes.push(vexNote);
365
365
  }
366
366
  const vexBeam = new VF.Beam(vexNotes);
367
+ vexBeam.breakSecondaryAt(bg.secondaryBeamBreaks);
367
368
  this.beamToVexMap[bg.attrs.id] = vexBeam;
368
369
  this.vexBeamGroups.push(vexBeam);
369
370
  }
@@ -237,6 +237,7 @@ export function keyEventMatch(ev1: KeyEvent, ev2: KeyEvent): boolean {
237
237
  ev1.ctrlKey === ev2.ctrlKey &&
238
238
  ev1.altKey === ev2.altKey && ev1.shiftKey === ev2.shiftKey
239
239
  }
240
+ export type keyHandler = (key?: KeyEvent) => void;
240
241
  /**
241
242
  * @internal
242
243
  */