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.
- package/README.md +1 -0
- package/build/smoosic.js +28 -28
- package/package.json +1 -1
- package/release/smoosic.js +28 -28
- package/src/application/application.ts +14 -2
- package/src/application/common.ts +38 -3
- package/src/application/configuration.ts +1 -1
- package/src/application/eventHandler.ts +27 -28
- package/src/application/exports.ts +2 -2
- package/src/application/keyCommands.ts +42 -10
- package/src/render/audio/musicCursor.ts +3 -1
- package/src/render/sui/piano.ts +1 -1
- package/src/render/sui/scoreView.ts +15 -11
- package/src/render/sui/scoreViewOperations.ts +9 -7
- package/src/render/sui/tracker.ts +57 -18
- package/src/render/vex/vxMeasure.ts +1 -0
- package/src/smo/data/common.ts +1 -0
- package/src/smo/data/measure.ts +14 -0
- package/src/smo/data/noteModifiers.ts +52 -7
- package/src/smo/data/scoreModifiers.ts +11 -3
- package/src/smo/data/staffModifiers.ts +10 -4
- package/src/smo/data/systemStaff.ts +19 -1
- package/src/smo/data/tuplet.ts +17 -0
- package/src/smo/mxml/xmlHelpers.ts +8 -1
- package/src/smo/xform/beamers.ts +51 -66
- package/src/smo/xform/copypaste.ts +8 -0
- package/src/smo/xform/selections.ts +13 -1
- package/src/smo/xform/tickDuration.ts +58 -27
- package/src/ui/dialogs/components/rocker.ts +5 -1
- package/src/ui/dialogs/endings.ts +1 -1
- package/src/ui/dialogs/preferences.ts +11 -0
- package/src/ui/keyBindings/default/editorKeys.ts +0 -7
- package/tsconfig.json +1 -1
- package/release/library/BachCantataBMV68.json +0 -1
- package/release/library/BachCantataBMV682.json +0 -1
- package/release/library/Beethoven_AnDieFerneGeliebte.xml +0 -4271
- package/release/library/Dichterliebe01.xml +0 -7613
- package/release/library/MozartTrio.xml +0 -3498
- package/release/library/Mozart_AnChloe.xml +0 -3986
- package/release/library/Postillionlied.json +0 -1
- package/release/library/ScottJoplin_The_Entertainer.xml +0 -20741
- package/release/library/Yama2.json +0 -1
- package/release/library/bigband/Bilongo-Mandinga.json +0 -1
- package/release/library/bigband/Plena.json +0 -1
- package/release/library/bigband/Poinciana.json +0 -1
- package/release/library/bigband/child who drums.json +0 -1
- package/release/library/cp/Another Hairdo.mxl +0 -0
- package/release/library/cp/Anthropology Eb.mxl +0 -0
- package/release/library/cp/Au Privave No 1 Eb.mxl +0 -0
- package/release/library/cp/Au Privave No 2 Eb.mxl +0 -0
- package/release/library/cp/Back Home Blues.mxl +0 -0
- package/release/library/cp/Ballade.mxl +0 -0
- package/release/library/cp/Billies Bounce.mxl +0 -0
- package/release/library/cp/Bloomdido.mxl +0 -0
- package/release/library/cp/Blues for Alice.mxl +0 -0
- package/release/library/cp/Celerity Alto.mxl +0 -0
- package/release/library/cp/Celerity.mxl +0 -0
- package/release/library/cp/Cheryl F07.mxl +0 -0
- package/release/library/cp/Cheryl.mxl +0 -0
- package/release/library/cp/Confirmation Eb 09.mxl +0 -0
- package/release/library/cp/Cool Blues Alto1.mxl +0 -0
- package/release/library/cp/Dewey Square.mxl +0 -0
- package/release/library/cp/Donna Lee Eb.musicxml +0 -9134
- package/release/library/cp/Donna Lee Eb.mxl +0 -0
- package/release/library/cp/Ko Ko Eb.mxl +0 -0
- package/release/library/cp/Little Benny.mxl +0 -0
- package/release/library/cp/Moose the Mooche Eb.mxl +0 -0
- package/release/library/cp/Night in Tunisia solo.mxl +0 -0
- package/release/library/cp/Nows the Time No 1.mxl +0 -0
- package/release/library/cp/Nows the Time No 2.mxl +0 -0
- package/release/library/cp/Ornithology Eb.mxl +0 -0
- package/release/library/cp/Parkers Mood.mxl +0 -0
- package/release/library/cp/Passport.mxl +0 -0
- package/release/library/cp/Red Cross.mxl +0 -0
- package/release/library/cp/Scrapple from the Apple Eb.mxl +0 -0
- package/release/library/cp/Shaw Nuff Eb.mxl +0 -0
- package/release/library/cp/Shaw Nuff.mxl +0 -0
- package/release/library/cp/She Rote No1.mxl +0 -0
- package/release/library/cp/She Rote No2.mxl +0 -0
- package/release/library/cp/The Bird 07.mxl +0 -0
- package/release/library/cp/The Bird.mxl +0 -0
- package/release/library/cp/Warming Up A Riff.mxl +0 -0
- package/release/library/cp/Yardbird Suite (alto solo).mxl +0 -0
- package/release/library/cp/Yardbird Suite Eb Alto+Bass+Chords.mxl +0 -0
- package/release/library/cp/Yardbird Suite Eb.mxl +0 -0
- package/release/library/cp/cplist.txt +0 -40
- package/release/library/hymns/AdesteFideles.json +0 -1
- package/release/library/hymns/Heshallfeed.json +0 -1
- package/release/library/hymns/Hyfrydol.json +0 -1
- package/release/library/hymns/Lakeshore.json +0 -1
- package/release/library/hymns/Precious Lord.json +0 -1
- package/release/library/hymns/Shade.json +0 -1
- package/release/library/hymns/SpiritofLivingGod.json +0 -1
- package/release/library/hymns/SweetSpirit.json +0 -1
- package/release/library/hymns/We3kings.json +0 -1
- package/release/library/hymns/Weisse-Flaggen.json +0 -1
- package/release/library/hymns/WorldPeacePrayer.json +0 -1
- package/release/library/hymns/balm.json +0 -1
- package/release/library/hymns/cplist.txt +0 -9
- package/release/library/hymns/nearerMyGod.json +0 -1
- package/release/library/links/bigband.json +0 -47
- package/release/library/links/cptranscriptions.json +0 -365
- package/release/library/links/hymns.json +0 -78
- package/release/library/links/messiah.json +0 -33
- package/release/library/links/musicxml.json +0 -62
- package/release/library/links/piano.json +0 -59
- package/release/library/links/samples.json +0 -75
- package/release/library/links/smoLibrary.json +0 -68
- package/release/library/links/soprano.json +0 -51
- package/release/library/links/transcriptions.json +0 -32
- package/release/library/links/trumpet.json +0 -76
- package/release/library/messiah/Messiah-I-1.json +0 -1
- package/release/library/messiah/Messiah-I-2.json +0 -1
- package/release/library/miditest/keytime.mid +0 -0
- package/release/library/miditest/ties.mid +0 -0
- package/release/library/miditest/triplet.mid +0 -0
- package/release/library/piano/BachInvention.json +0 -1
- package/release/library/piano/BachWTC-3.json +0 -1
- package/release/library/piano/Gnossienne3-3.json +0 -1
- package/release/library/piano/Gymnopedie No 1 (8).json +0 -1
- package/release/library/piano/Joplin-Entertainer.json +0 -1
- package/release/library/soprano/Gesu Bambino.json +0 -1
- package/release/library/soprano/LaudamusTrumpetSoprano.json +0 -1
- package/release/library/soprano/Postillionlied.json +0 -1
- package/release/library/soprano/Solovey.json +0 -1
- package/release/library/soprano/ThereIsABalm.json +0 -1
- package/release/library/trumpet/BachAirOrganTpt.json +0 -1
- package/release/library/trumpet/BachCantataBMV68.json +0 -1
- package/release/library/trumpet/BeethovenOp21-old.json +0 -1
- package/release/library/trumpet/BeethovenOp21.json +0 -1
- package/release/library/trumpet/Fireworks - trumpets.json +0 -1
- package/release/library/trumpet/HymnFreedom.json +0 -1
- package/release/library/trumpet/TelemannSonata3rd.json +0 -1
- package/release/library/xml/Gnossienne_No._3.mxl +0 -0
- package/release/library/xml/M_Book_1_Part_1_The_Open_Well_Tempered_Clavier.mxl +0 -0
- package/release/library/xml/lg-177775783.xml +0 -8674
package/src/smo/data/measure.ts
CHANGED
|
@@ -64,6 +64,7 @@ export interface MeasureTick {
|
|
|
64
64
|
*/
|
|
65
65
|
export interface ISmoBeamGroup {
|
|
66
66
|
notes: SmoNote[],
|
|
67
|
+
secondaryBeamBreaks: number[],
|
|
67
68
|
voice: number,
|
|
68
69
|
attrs: SmoAttrs
|
|
69
70
|
}
|
|
@@ -1246,6 +1247,19 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
|
|
|
1246
1247
|
return max;
|
|
1247
1248
|
}
|
|
1248
1249
|
|
|
1250
|
+
/**
|
|
1251
|
+
* For pasting, paste into the target measure if the voice exists, else paste into
|
|
1252
|
+
* voice 0
|
|
1253
|
+
* @param voiceIndex
|
|
1254
|
+
* @returns
|
|
1255
|
+
*/
|
|
1256
|
+
getTicksFromThisOrAnyVoice(voiceIndex: number): number {
|
|
1257
|
+
if (this.voices.length > voiceIndex) {
|
|
1258
|
+
return this.getTicksFromVoice(voiceIndex);
|
|
1259
|
+
} else {
|
|
1260
|
+
return this.getTicksFromVoice(0);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1249
1263
|
/**
|
|
1250
1264
|
* Count the number of ticks in a specific voice
|
|
1251
1265
|
* @param voiceIndex
|
|
@@ -349,7 +349,7 @@ function isSmoMicrotoneParamsSer(params: Partial<SmoMicrotoneParamsSer>): params
|
|
|
349
349
|
* @category SmoObject
|
|
350
350
|
*/
|
|
351
351
|
export class SmoMicrotone extends SmoNoteModifierBase {
|
|
352
|
-
tone: string;
|
|
352
|
+
tone: string = 'flat75sz';
|
|
353
353
|
pitchIndex: number = 0;
|
|
354
354
|
|
|
355
355
|
// This is how VexFlow notates them
|
|
@@ -385,10 +385,12 @@ export class SmoMicrotone extends SmoNoteModifierBase {
|
|
|
385
385
|
get toVex(): string {
|
|
386
386
|
return SmoMicrotone.smoToVex[this.tone];
|
|
387
387
|
}
|
|
388
|
-
static
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
388
|
+
static get defaults(): SmoMicrotoneParams {
|
|
389
|
+
return JSON.parse(JSON.stringify({
|
|
390
|
+
ctor: 'SmoMicrotone',
|
|
391
|
+
tone: 'flat25sz',
|
|
392
|
+
pitch: 0
|
|
393
|
+
}));
|
|
392
394
|
}
|
|
393
395
|
static get parameterArray() {
|
|
394
396
|
const rv: string[] = [];
|
|
@@ -409,8 +411,8 @@ export class SmoMicrotone extends SmoNoteModifierBase {
|
|
|
409
411
|
}
|
|
410
412
|
constructor(parameters: SmoMicrotoneParams) {
|
|
411
413
|
super(parameters.ctor);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
+
smoSerialize.serializedMerge(SmoMicrotone.parameterArray, SmoMicrotone.defaults, this);
|
|
415
|
+
smoSerialize.serializedMerge(SmoMicrotone.parameterArray, parameters, this);
|
|
414
416
|
}
|
|
415
417
|
}
|
|
416
418
|
|
|
@@ -1020,7 +1022,49 @@ export class SmoLyric extends SmoNoteModifierBase {
|
|
|
1020
1022
|
}
|
|
1021
1023
|
}
|
|
1022
1024
|
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Used to create a {@link SmoBarline}
|
|
1027
|
+
* @category SmoObject
|
|
1028
|
+
*/
|
|
1029
|
+
|
|
1030
|
+
export interface SmoNoteBarParams {
|
|
1031
|
+
barline: number
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export interface SmoNoteBarParamsSer extends SmoNoteBarParams {
|
|
1035
|
+
ctor: string,
|
|
1036
|
+
barline: number
|
|
1037
|
+
}
|
|
1023
1038
|
|
|
1039
|
+
export class SmoNoteBar extends SmoNoteModifierBase {
|
|
1040
|
+
barline: number = SmoNoteBar.defaults.barline;
|
|
1041
|
+
static get defaults(): SmoNoteBarParams {
|
|
1042
|
+
return { barline: SmoNoteBar.barlines.noBar }
|
|
1043
|
+
}
|
|
1044
|
+
static get parameterArray() {
|
|
1045
|
+
return ['barline'];
|
|
1046
|
+
}
|
|
1047
|
+
static readonly barlines: Record<string, number> = {
|
|
1048
|
+
singleBar: 0,
|
|
1049
|
+
doubleBar: 1,
|
|
1050
|
+
endBar: 2,
|
|
1051
|
+
startRepeat: 3,
|
|
1052
|
+
endRepeat: 4,
|
|
1053
|
+
noBar: 5
|
|
1054
|
+
}
|
|
1055
|
+
constructor(parameters: SmoNoteBarParams) {
|
|
1056
|
+
super('SmoNoteBar');
|
|
1057
|
+
smoSerialize.serializedMerge(SmoNoteBar.parameterArray, SmoNoteBar.defaults, this);
|
|
1058
|
+
smoSerialize.serializedMerge(SmoLyric.parameterArray, parameters, this);
|
|
1059
|
+
}
|
|
1060
|
+
serialize(): SmoNoteBarParamsSer {
|
|
1061
|
+
const parameters: Partial<SmoNoteBarParamsSer> = {};
|
|
1062
|
+
smoSerialize.serializedMergeNonDefault(SmoNoteBar.defaults,
|
|
1063
|
+
SmoNoteBar.parameterArray, this, parameters);
|
|
1064
|
+
parameters.ctor = 'SmoNoteBar';
|
|
1065
|
+
return parameters as SmoNoteBarParamsSer;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1024
1068
|
/**
|
|
1025
1069
|
* The persisted bits of {@link SmoDynamicTextParams}
|
|
1026
1070
|
* @category serialization
|
|
@@ -1203,6 +1247,7 @@ export const noteModifierDynamicCtorInit = () => {
|
|
|
1203
1247
|
SmoDynamicCtor['SmoGraceNote'] = (params: GraceNoteParams) => new SmoGraceNote(params);
|
|
1204
1248
|
SmoDynamicCtor['SmoArticulation'] = (params: SmoArticulationParameters) => new SmoArticulation(params);
|
|
1205
1249
|
SmoDynamicCtor['SmoLyric'] = (params: SmoLyricParams) => new SmoLyric(params);
|
|
1250
|
+
SmoDynamicCtor['SmoNoteBar'] = (params: SmoNoteBarParams) => new SmoNoteBar(params);
|
|
1206
1251
|
SmoDynamicCtor['SmoDynamicText'] = (params: SmoDynamicTextParams) => new SmoDynamicText(params);
|
|
1207
1252
|
SmoDynamicCtor['SmoTabNote'] = (params: SmoTabNoteParams) => new SmoTabNote(params);
|
|
1208
1253
|
SmoDynamicCtor['SmoClefChange'] = (params: SmoClefChangeParams) => new SmoClefChange(params);
|
|
@@ -124,9 +124,10 @@ export interface SmoScoreInfo {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
export type SmoScorePreferenceBool = 'autoPlay' | 'autoAdvance' | 'showPiano' | 'hideEmptyLines' | 'transposingScore';
|
|
127
|
+
export type SmoScorePreferenceBool = 'autoPlay' | 'autoAdvance' | 'showPiano' | 'hideEmptyLines' | 'transposingScore' | 'autoScrollPlayback';
|
|
128
128
|
export type SmoScorePreferenceNumber = 'defaultDupleDuration' | 'defaultTripleDuration';
|
|
129
|
-
export const SmoScorePreferenceBools: SmoScorePreferenceBool[] = ['autoPlay', 'autoAdvance', 'showPiano', 'hideEmptyLines',
|
|
129
|
+
export const SmoScorePreferenceBools: SmoScorePreferenceBool[] = ['autoPlay', 'autoAdvance', 'showPiano', 'hideEmptyLines',
|
|
130
|
+
'transposingScore', 'autoScrollPlayback'];
|
|
130
131
|
export const SmoScorePreferenceNumbers: SmoScorePreferenceNumber[] = ['defaultDupleDuration', 'defaultTripleDuration'];
|
|
131
132
|
/**
|
|
132
133
|
* Global score/program behavior preferences, see below for parameters
|
|
@@ -135,6 +136,7 @@ export const SmoScorePreferenceNumbers: SmoScorePreferenceNumber[] = ['defaultDu
|
|
|
135
136
|
export interface SmoScorePreferencesParams {
|
|
136
137
|
autoPlay: boolean;
|
|
137
138
|
autoAdvance: boolean;
|
|
139
|
+
autoScrollPlayback: boolean;
|
|
138
140
|
defaultDupleDuration: number;
|
|
139
141
|
defaultTripleDuration: number;
|
|
140
142
|
showPiano: boolean;
|
|
@@ -159,6 +161,7 @@ export class SmoScorePreferences {
|
|
|
159
161
|
defaultTripleDuration: number = 6144;
|
|
160
162
|
showPiano: boolean = false;
|
|
161
163
|
hideEmptyLines: boolean = false;
|
|
164
|
+
autoScrollPlayback: boolean = true;
|
|
162
165
|
transposingScore: boolean = false;
|
|
163
166
|
static get defaults(): SmoScorePreferencesParams {
|
|
164
167
|
return {
|
|
@@ -166,19 +169,24 @@ export class SmoScorePreferences {
|
|
|
166
169
|
autoAdvance: true,
|
|
167
170
|
defaultDupleDuration: 4096,
|
|
168
171
|
defaultTripleDuration: 6144,
|
|
172
|
+
autoScrollPlayback: true,
|
|
169
173
|
showPiano: false,
|
|
170
174
|
hideEmptyLines: false,
|
|
171
175
|
transposingScore: false
|
|
172
176
|
};
|
|
173
177
|
}
|
|
174
178
|
constructor(params: SmoScorePreferencesParams) {
|
|
175
|
-
if (params) {
|
|
179
|
+
if (params) {
|
|
176
180
|
SmoScorePreferenceBools.forEach((bb) => {
|
|
177
181
|
this[bb] = params[bb];
|
|
178
182
|
});
|
|
179
183
|
SmoScorePreferenceNumbers.forEach((nn) => {
|
|
180
184
|
this[nn] = params[nn];
|
|
181
185
|
});
|
|
186
|
+
// legacy, added later
|
|
187
|
+
if (typeof(params.autoScrollPlayback) === 'undefined') {
|
|
188
|
+
this.autoScrollPlayback = true;
|
|
189
|
+
}
|
|
182
190
|
}
|
|
183
191
|
}
|
|
184
192
|
serialize(): SmoScorePreferencesParams {
|
|
@@ -46,6 +46,12 @@ export abstract class StaffModifierBase implements SmoModifierBase {
|
|
|
46
46
|
const rv = SmoDynamicCtor[params.ctor](params);
|
|
47
47
|
return rv;
|
|
48
48
|
}
|
|
49
|
+
static cloneWithId(o: StaffModifierBase) {
|
|
50
|
+
const ser = o.serializeWithId();
|
|
51
|
+
const des = StaffModifierBase.deserialize(ser);
|
|
52
|
+
des.attrs = JSON.parse(JSON.stringify(o.attrs));
|
|
53
|
+
return des;
|
|
54
|
+
}
|
|
49
55
|
serializeWithId() {
|
|
50
56
|
const ser = this.serialize();
|
|
51
57
|
ser.attrs = JSON.parse(JSON.stringify(this.attrs));
|
|
@@ -866,8 +872,8 @@ export class SmoTie extends StaffModifierBase {
|
|
|
866
872
|
invert: boolean = false;
|
|
867
873
|
cp1: number = 8;
|
|
868
874
|
cp2: number = 12;
|
|
869
|
-
first_x_shift: number =
|
|
870
|
-
last_x_shift: number =
|
|
875
|
+
first_x_shift: number = -5;
|
|
876
|
+
last_x_shift: number = 5;
|
|
871
877
|
y_shift: number = 7;
|
|
872
878
|
tie_spacing: number = 0;
|
|
873
879
|
lines: TieLine[] = [];
|
|
@@ -879,8 +885,8 @@ export class SmoTie extends StaffModifierBase {
|
|
|
879
885
|
cp1: 8,
|
|
880
886
|
cp2: 12,
|
|
881
887
|
y_shift: 7,
|
|
882
|
-
first_x_shift:
|
|
883
|
-
last_x_shift:
|
|
888
|
+
first_x_shift: -5,
|
|
889
|
+
last_x_shift: 5,
|
|
884
890
|
lines: [],
|
|
885
891
|
startSelector: SmoSelector.default,
|
|
886
892
|
endSelector: SmoSelector.default
|
|
@@ -777,17 +777,35 @@ export class SmoSystemStaff implements SmoObjectParams {
|
|
|
777
777
|
*/
|
|
778
778
|
syncStaffModifiers(measureIndex: number, ostaff: SmoSystemStaff) {
|
|
779
779
|
const mods: StaffModifierBase[] = [];
|
|
780
|
+
// remove any modifiers in the stored score that aren't in the view score
|
|
780
781
|
this.modifiers.forEach((modifier) => {
|
|
781
782
|
if (modifier.startSelector.measure !== measureIndex) {
|
|
782
783
|
mods.push(modifier);
|
|
783
784
|
} else {
|
|
784
785
|
const omod = ostaff.modifiers.find((mm) => mm.attrs.id === modifier.attrs.id);
|
|
785
786
|
if (omod) {
|
|
786
|
-
mods.push(
|
|
787
|
+
mods.push(omod);
|
|
787
788
|
}
|
|
788
789
|
}
|
|
789
790
|
});
|
|
790
791
|
this.modifiers = mods;
|
|
792
|
+
const measureSelectors = ostaff.modifiers.filter((mm) => mm.startSelector.measure === measureIndex);
|
|
793
|
+
// Add any new modifiers from a copy operation
|
|
794
|
+
measureSelectors.forEach((modifier) => {
|
|
795
|
+
const dup = this.modifiers.find((mm) =>
|
|
796
|
+
mm.startSelector.measure === modifier.startSelector.measure &&
|
|
797
|
+
mm.endSelector.measure === modifier.endSelector.measure &&
|
|
798
|
+
mm.ctor === modifier.ctor);
|
|
799
|
+
if (dup ?? null === null) {
|
|
800
|
+
const ser = StaffModifierBase.cloneWithId(modifier);
|
|
801
|
+
const des = StaffModifierBase.deserialize(ser);
|
|
802
|
+
des.attrs = JSON.parse(JSON.stringify(ser.attrs));
|
|
803
|
+
des.startSelector.staff = this.staffId;
|
|
804
|
+
des.endSelector.staff = this.staffId;
|
|
805
|
+
this.modifiers.push(des);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
791
809
|
}
|
|
792
810
|
// ### deleteMeasure
|
|
793
811
|
// delete the measure, and any staff modifiers that start/end there.
|
package/src/smo/data/tuplet.ts
CHANGED
|
@@ -144,6 +144,23 @@ export class SmoTupletTree {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Determines whether two notes are part of the same tuplet.
|
|
149
|
+
* @param noteOne
|
|
150
|
+
* @param noteTwo
|
|
151
|
+
*/
|
|
152
|
+
static areNotesPartOfTheSameTuplet(noteOne: SmoNote, noteTwo: SmoNote): boolean {
|
|
153
|
+
if (noteOne.tupletId === noteTwo.tupletId) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static areTupletsBothNull(noteOne: SmoNote, noteTwo: SmoNote): boolean {
|
|
161
|
+
return (noteOne.tupletId ?? null) === null && (noteTwo.tupletId ?? null) === null;
|
|
162
|
+
}
|
|
163
|
+
|
|
147
164
|
serialize(): SmoTupletTreeParamsSer {
|
|
148
165
|
const params = {
|
|
149
166
|
ctor: 'SmoTupletTree',
|
|
@@ -114,7 +114,14 @@ export class XmlHelpers {
|
|
|
114
114
|
// smo infers the stem type from the duration, but other applications don't
|
|
115
115
|
static closestStemType(ticks: number) {
|
|
116
116
|
const nticks = SmoMusic.closestDurationTickLtEq(ticks);
|
|
117
|
-
|
|
117
|
+
// closestBeamDuration returns the rounded-up beam length for dotted rhythm and tuplets,
|
|
118
|
+
// we want the actual stem that's used so cut it in 1/2
|
|
119
|
+
const beamDuration = SmoMusic.closestBeamDuration(nticks);
|
|
120
|
+
if (beamDuration.ticks === nticks) {
|
|
121
|
+
return XmlHelpers.ticksToNoteTypeMap[beamDuration.ticks];
|
|
122
|
+
} else {
|
|
123
|
+
return XmlHelpers.ticksToNoteTypeMap[beamDuration.ticks / 2];
|
|
124
|
+
}
|
|
118
125
|
}
|
|
119
126
|
static get beamStates(): Record<string, number> {
|
|
120
127
|
return {
|
package/src/smo/xform/beamers.ts
CHANGED
|
@@ -6,12 +6,14 @@ import { SmoAttrs, getId } from '../data/common';
|
|
|
6
6
|
import { SmoMeasure, ISmoBeamGroup } from '../data/measure';
|
|
7
7
|
import { TickMap } from './tickMap';
|
|
8
8
|
import { smoSerialize } from '../../common/serializationHelpers';
|
|
9
|
+
import {SmoTuplet, SmoTupletTree} from "../data/tuplet";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @category SmoTransform
|
|
12
13
|
*/
|
|
13
14
|
export interface SmoBeamGroupParams {
|
|
14
15
|
notes: SmoNote[],
|
|
16
|
+
secondaryBeamBreaks: number[];
|
|
15
17
|
voice: number
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -21,12 +23,14 @@ export interface SmoBeamGroupParams {
|
|
|
21
23
|
*/
|
|
22
24
|
export class SmoBeamGroup implements ISmoBeamGroup {
|
|
23
25
|
notes: SmoNote[];
|
|
26
|
+
secondaryBeamBreaks: number[];
|
|
24
27
|
attrs: SmoAttrs;
|
|
25
28
|
voice: number = 0;
|
|
26
29
|
constructor(params: SmoBeamGroupParams) {
|
|
27
30
|
let i = 0;
|
|
28
31
|
this.voice = params.voice;
|
|
29
32
|
this.notes = params.notes;
|
|
33
|
+
this.secondaryBeamBreaks = params.secondaryBeamBreaks;
|
|
30
34
|
smoSerialize.vexMerge(this, params);
|
|
31
35
|
|
|
32
36
|
this.attrs = {
|
|
@@ -65,6 +69,7 @@ export class SmoBeamer {
|
|
|
65
69
|
beamBeats: number;
|
|
66
70
|
skipNext: number;
|
|
67
71
|
currentGroup: SmoNote[];
|
|
72
|
+
secondaryBeamBreaks: number[];
|
|
68
73
|
constructor(measure: SmoMeasure, voice: number) {
|
|
69
74
|
this.measure = measure;
|
|
70
75
|
this._removeVoiceBeam(measure, voice);
|
|
@@ -78,6 +83,7 @@ export class SmoBeamer {
|
|
|
78
83
|
}
|
|
79
84
|
this.skipNext = 0;
|
|
80
85
|
this.currentGroup = [];
|
|
86
|
+
this.secondaryBeamBreaks = [];
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
get beamGroups() {
|
|
@@ -101,6 +107,7 @@ export class SmoBeamer {
|
|
|
101
107
|
if (nrCount.length > 1) {
|
|
102
108
|
this.measure.beamGroups.push(new SmoBeamGroup({
|
|
103
109
|
notes: this.currentGroup,
|
|
110
|
+
secondaryBeamBreaks: this.secondaryBeamBreaks,
|
|
104
111
|
voice
|
|
105
112
|
}));
|
|
106
113
|
}
|
|
@@ -108,6 +115,7 @@ export class SmoBeamer {
|
|
|
108
115
|
|
|
109
116
|
_advanceGroup() {
|
|
110
117
|
this.currentGroup = [];
|
|
118
|
+
this.secondaryBeamBreaks = [];
|
|
111
119
|
this.duration = 0;
|
|
112
120
|
}
|
|
113
121
|
|
|
@@ -151,40 +159,33 @@ export class SmoBeamer {
|
|
|
151
159
|
return;
|
|
152
160
|
}
|
|
153
161
|
|
|
162
|
+
if (this.currentGroup.length > 0) {
|
|
163
|
+
const areTupletsBothNull = SmoTupletTree.areTupletsBothNull(tickmap.notes[index - 1], tickmap.notes[index]);
|
|
164
|
+
const areNotesPartOfTheSameTuplet = SmoTupletTree.areNotesPartOfTheSameTuplet(tickmap.notes[index - 1], tickmap.notes[index]);
|
|
165
|
+
|
|
166
|
+
if (!areTupletsBothNull && !areNotesPartOfTheSameTuplet) {
|
|
167
|
+
this.secondaryBeamBreaks.push(this.currentGroup.length - 1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
154
171
|
// beam tuplets
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// if (first.endBeam) {
|
|
173
|
-
// this._advanceGroup();
|
|
174
|
-
// return;
|
|
175
|
-
// }
|
|
176
|
-
|
|
177
|
-
// // is this beamable length-wise
|
|
178
|
-
// if (note.noteType === 'n' && note.stemTicks < 4096) {
|
|
179
|
-
// this.currentGroup.push(note);
|
|
180
|
-
// }
|
|
181
|
-
// // Ultimate note in tuplet
|
|
182
|
-
// if (ult.attrs.id === note.attrs.id && !this._isRemainingTicksBeamable(tickmap, index)) {
|
|
183
|
-
// this._completeGroup(tickmap.voice);
|
|
184
|
-
// this._advanceGroup();
|
|
185
|
-
// }
|
|
186
|
-
// return;
|
|
187
|
-
// }
|
|
172
|
+
if (note.isTuplet) {
|
|
173
|
+
const tupletTree = SmoTupletTree.getTupletTreeForNoteIndex(this.measure.tupletTrees, tickmap.voice, index);
|
|
174
|
+
if (!tupletTree) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// is this beamable length-wise
|
|
179
|
+
if (note.noteType === 'n' && note.stemTicks < 4096) {
|
|
180
|
+
this.currentGroup.push(note);
|
|
181
|
+
}
|
|
182
|
+
// Ultimate note in tuplet
|
|
183
|
+
if (tupletTree.endIndex == index && !this._isRemainingTicksBeamable(tickmap, index)) {
|
|
184
|
+
this._completeGroup(tickmap.voice);
|
|
185
|
+
this._advanceGroup();
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
188
189
|
|
|
189
190
|
// don't beam > 1/4 note in 4/4 time. Don't beam rests.
|
|
190
191
|
if (note.stemTicks >= 4096 || (note.isRest() && this.currentGroup.length === 0)) {
|
|
@@ -193,33 +194,32 @@ export class SmoBeamer {
|
|
|
193
194
|
return;
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (index > 0 && !SmoBeamer.areTupletElementsTheSame(tickmap.notes[index - 1], tickmap.notes[index])) {
|
|
197
|
+
this.currentGroup.push(note);
|
|
198
|
+
|
|
199
|
+
if (note.endBeam) {
|
|
200
200
|
this._completeGroup(tickmap.voice);
|
|
201
201
|
this._advanceGroup();
|
|
202
202
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (note.endBeam) {
|
|
203
|
+
if (index == tickmap.notes.length - 1) {
|
|
204
|
+
//Last note in the voice. We are closing the beam with whatever has been put there
|
|
206
205
|
this._completeGroup(tickmap.voice);
|
|
207
206
|
this._advanceGroup();
|
|
207
|
+
return;
|
|
208
208
|
}
|
|
209
|
+
|
|
209
210
|
if (this.measure.timeSignature.actualBeats % 4 === 0) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
if (this.duration < 8192 && this.allEighth()) {
|
|
212
|
+
return;
|
|
213
|
+
} else if (this.duration === 8192) {
|
|
214
|
+
this._completeGroup(tickmap.voice);
|
|
215
|
+
this._advanceGroup();
|
|
216
|
+
}
|
|
216
217
|
}
|
|
217
218
|
// If we are aligned to a beat on the measure, and we are in common time
|
|
218
|
-
if (this.currentGroup.length > 1 && this.measure.timeSignature.beatDuration === 4 &&
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return;
|
|
219
|
+
if (this.currentGroup.length > 1 && this.measure.timeSignature.beatDuration === 4 && this.measureDuration % 4096 === 0) {
|
|
220
|
+
this._completeGroup(tickmap.voice);
|
|
221
|
+
this._advanceGroup();
|
|
222
|
+
return;
|
|
223
223
|
}
|
|
224
224
|
if (this.duration === this.beamBeats) {
|
|
225
225
|
this._completeGroup(tickmap.voice);
|
|
@@ -232,19 +232,4 @@ export class SmoBeamer {
|
|
|
232
232
|
this._advanceGroup();
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
|
-
|
|
236
|
-
public static areTupletElementsTheSame(noteOne: SmoNote, noteTwo: SmoNote): boolean {
|
|
237
|
-
if (typeof(noteOne.tupletId) === 'undefined' && typeof(noteTwo.tupletId) === 'undefined') {
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
if (noteOne.tupletId === null && noteTwo.tupletId === null) {
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tupletId == noteTwo.tupletId) {
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
235
|
}
|
|
@@ -105,8 +105,16 @@ export class PasteBuffer {
|
|
|
105
105
|
_populateSelectArray(selections: SmoSelection[]) {
|
|
106
106
|
let selector: SmoSelector = SmoSelector.default;
|
|
107
107
|
this.modifiers = [];
|
|
108
|
+
let maxSelector = selections[0].selector;
|
|
109
|
+
let minSelector = selections[0].selector;
|
|
108
110
|
selections.forEach((selection) => {
|
|
109
111
|
selector = JSON.parse(JSON.stringify(selection.selector));
|
|
112
|
+
if (SmoSelector.gt(selector, maxSelector)) {
|
|
113
|
+
maxSelector = selector;
|
|
114
|
+
}
|
|
115
|
+
if (SmoSelector.lt(selector, minSelector)) {
|
|
116
|
+
minSelector = selector;
|
|
117
|
+
}
|
|
110
118
|
const mod: StaffModifierBase[] = selection.staff.getModifiersAt(selector);
|
|
111
119
|
if (mod.length) {
|
|
112
120
|
mod.forEach((modifier: StaffModifierBase) => {
|
|
@@ -73,7 +73,10 @@ export class SmoSelector {
|
|
|
73
73
|
(sel1.measure === sel2.measure && sel1.tick > sel2.tick);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* return true if sel1 > sel2
|
|
79
|
+
*/
|
|
77
80
|
static gt(sel1: SmoSelector, sel2: SmoSelector): boolean {
|
|
78
81
|
// Note: voice is not considered b/c it's more of a vertical component
|
|
79
82
|
// Note further: sometimes we need to consider voice
|
|
@@ -90,13 +93,22 @@ export class SmoSelector {
|
|
|
90
93
|
return !(SmoSelector.eq(sel1, sel2));
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
/**
|
|
97
|
+
* return true if sel1 < sel2
|
|
98
|
+
*/
|
|
93
99
|
static lt(sel1: SmoSelector, sel2: SmoSelector): boolean {
|
|
94
100
|
return SmoSelector.gt(sel2, sel1);
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
/**
|
|
104
|
+
* return true if sel1 >= sel2
|
|
105
|
+
*/
|
|
97
106
|
static gteq(sel1: SmoSelector, sel2: SmoSelector): boolean {
|
|
98
107
|
return SmoSelector.gt(sel1, sel2) || SmoSelector.eq(sel1, sel2);
|
|
99
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* return true if sel1 <= sel2
|
|
111
|
+
*/
|
|
100
112
|
static lteq(sel1: SmoSelector, sel2: SmoSelector): boolean {
|
|
101
113
|
return SmoSelector.lt(sel1, sel2) || SmoSelector.eq(sel1, sel2);
|
|
102
114
|
}
|
|
@@ -220,53 +220,91 @@ export class SmoStretchNoteActor extends TickIteratorBase {
|
|
|
220
220
|
this.notes = this.measure.voices[this.voice].notes;
|
|
221
221
|
|
|
222
222
|
const originalNote: SmoNote = this.notes[this.startIndex];
|
|
223
|
-
let newTicks: Ticks = { numerator: this.newStemTicks, denominator: 1, remainder: 0 };
|
|
224
223
|
const multiplier = originalNote.tickCount / originalNote.stemTicks;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
newTicks = { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1 };
|
|
228
|
-
}
|
|
224
|
+
|
|
225
|
+
const newTicks = this.calculateNewTicks(originalNote, multiplier);
|
|
229
226
|
|
|
230
227
|
const replacingNote = SmoNote.cloneWithDuration(originalNote, newTicks, this.newStemTicks);
|
|
231
228
|
|
|
229
|
+
const {stemTicksUsed, crossedTupletBoundary} = this.determineNotesToDelete(originalNote);
|
|
230
|
+
|
|
231
|
+
// if crossing a tuplet boundary, abort stretching
|
|
232
|
+
if (crossedTupletBoundary) {
|
|
233
|
+
this.numberOfNotesToDelete = 0;
|
|
234
|
+
} else {
|
|
235
|
+
this.prepareNotesToInsert(originalNote, replacingNote, stemTicksUsed, multiplier);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private calculateNewTicks(originalNote: SmoNote, multiplier: number): Ticks {
|
|
240
|
+
if (originalNote.isTuplet) {
|
|
241
|
+
const numerator = this.newStemTicks * multiplier
|
|
242
|
+
return { numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1};
|
|
243
|
+
} else {
|
|
244
|
+
return { numerator: this.newStemTicks, denominator: 1, remainder: 0};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private determineNotesToDelete(originalNote: SmoNote) {
|
|
249
|
+
let crossedTupletBoundary = false;
|
|
232
250
|
let stemTicksUsed = originalNote.stemTicks;
|
|
251
|
+
|
|
233
252
|
for (let i = this.startIndex + 1; i < this.notes.length; ++i) {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
253
|
+
const nextNote = this.notes[i];
|
|
254
|
+
|
|
255
|
+
const areTupletsBothNull = SmoTupletTree.areTupletsBothNull(originalNote, nextNote);
|
|
256
|
+
const areNotesPartOfTheSmeTuplet = SmoTupletTree.areNotesPartOfTheSameTuplet(originalNote, nextNote);
|
|
257
|
+
|
|
258
|
+
if (!areTupletsBothNull && !areNotesPartOfTheSmeTuplet) {
|
|
259
|
+
crossedTupletBoundary = true;
|
|
238
260
|
break;
|
|
239
261
|
}
|
|
240
|
-
|
|
262
|
+
|
|
263
|
+
stemTicksUsed += nextNote.stemTicks;
|
|
241
264
|
++this.numberOfNotesToDelete;
|
|
265
|
+
|
|
242
266
|
if (stemTicksUsed >= this.newStemTicks) {
|
|
243
267
|
break;
|
|
244
268
|
}
|
|
245
269
|
}
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
|
|
271
|
+
return {stemTicksUsed, crossedTupletBoundary};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private prepareNotesToInsert(
|
|
275
|
+
originalNote: SmoNote,
|
|
276
|
+
replacingNote: SmoNote,
|
|
277
|
+
stemTicksUsed: number,
|
|
278
|
+
multiplier: number
|
|
279
|
+
) {
|
|
280
|
+
const remainingTicks = stemTicksUsed - this.newStemTicks;
|
|
281
|
+
|
|
282
|
+
if (remainingTicks >= 0) {
|
|
248
283
|
this.notesToInsert.push(replacingNote);
|
|
249
|
-
|
|
250
|
-
|
|
284
|
+
|
|
285
|
+
const tickMap = SmoMusic.gcdMap(remainingTicks);
|
|
286
|
+
tickMap.forEach((stemTick) => {
|
|
251
287
|
const numerator = stemTick * multiplier;
|
|
252
|
-
const
|
|
253
|
-
this.notesToInsert.push(
|
|
288
|
+
const newNote = SmoNote.cloneWithDuration(originalNote, {numerator: Math.floor(numerator), denominator: 1, remainder: numerator % 1}, stemTick)
|
|
289
|
+
this.notesToInsert.push(newNote);
|
|
254
290
|
});
|
|
291
|
+
|
|
292
|
+
// Adjust tuplet indexes due to note insertion/deletion
|
|
255
293
|
const noteCountDiff = (this.notesToInsert.length - this.numberOfNotesToDelete) - 1;
|
|
256
294
|
SmoTupletTree.adjustTupletIndexes(this.measure.tupletTrees, this.voice, this.startIndex, noteCountDiff);
|
|
257
295
|
|
|
258
296
|
//accumulate all remainders in the first note
|
|
259
|
-
let
|
|
297
|
+
let totalRemainder: number = 0;
|
|
260
298
|
this.notesToInsert.forEach((note: SmoNote) => {
|
|
261
299
|
if (note.ticks.remainder > 0) {
|
|
262
|
-
|
|
300
|
+
totalRemainder += note.ticks.remainder;
|
|
263
301
|
note.ticks.remainder = 0;
|
|
264
302
|
}
|
|
265
303
|
});
|
|
266
|
-
this.notesToInsert[0].ticks.numerator += Math.round(
|
|
267
|
-
|
|
304
|
+
this.notesToInsert[0].ticks.numerator += Math.round(totalRemainder);
|
|
268
305
|
}
|
|
269
306
|
}
|
|
307
|
+
|
|
270
308
|
static apply(params: SmoStretchNoteParams) {
|
|
271
309
|
const actor = new SmoStretchNoteActor(params);
|
|
272
310
|
SmoTickIterator.iterateOverTicks(actor.measure, actor, actor.voice);
|
|
@@ -280,13 +318,6 @@ export class SmoStretchNoteActor extends TickIteratorBase {
|
|
|
280
318
|
}
|
|
281
319
|
return null;
|
|
282
320
|
}
|
|
283
|
-
|
|
284
|
-
private areNotesInSameTuplet(noteOne: SmoNote, noteTwo: SmoNote): boolean {
|
|
285
|
-
if (noteOne.isTuplet && noteTwo.isTuplet && noteOne.tupletId == noteTwo.tupletId) {
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
321
|
}
|
|
291
322
|
|
|
292
323
|
|