smoosic 1.0.10 → 1.0.12
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/build/smoosic.js +23 -23
- package/package.json +1 -1
- package/release/library/piano/20seconds.json +1 -0
- package/release/library/trumpet/2min.json +1 -0
- package/release/library/trumpet/Fireworks - trumpets.json +1 -1
- package/release/smoosic.js +23 -23
- package/src/application/application.ts +14 -2
- package/src/application/common.ts +38 -3
- package/src/application/eventHandler.ts +27 -28
- package/src/application/keyCommands.ts +42 -10
- 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 +9 -7
- 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 +4 -1
- package/src/ui/keyBindings/default/editorKeys.ts +0 -7
- package/tsconfig.json +1 -1
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -120,12 +120,15 @@ export class SuiRockerComponent extends SuiComponentBase {
|
|
|
120
120
|
() => {
|
|
121
121
|
val = (this as any)[this.parser]();
|
|
122
122
|
if (val !== this.initialValue) {
|
|
123
|
+
this.initialValue = val;
|
|
124
|
+
if (this.dataType === 'percent') {
|
|
125
|
+
val = 100 * val;
|
|
126
|
+
}
|
|
123
127
|
if (this.min != undefined && val < this.min) {
|
|
124
128
|
val = this.min;
|
|
125
129
|
} else if (this.max != undefined && val > this.max) {
|
|
126
130
|
val = this.max;
|
|
127
131
|
}
|
|
128
|
-
this.initialValue = val;
|
|
129
132
|
$(input).val(val);
|
|
130
133
|
this.handleChanged();
|
|
131
134
|
}
|
|
@@ -144,13 +144,6 @@ export class defaultEditorKeys {
|
|
|
144
144
|
altKey: false,
|
|
145
145
|
shiftKey: false,
|
|
146
146
|
action: "makeRest"
|
|
147
|
-
}, {
|
|
148
|
-
event: "keydown",
|
|
149
|
-
key: "r",
|
|
150
|
-
ctrlKey: false,
|
|
151
|
-
altKey: true,
|
|
152
|
-
shiftKey: false,
|
|
153
|
-
action: "rerender"
|
|
154
147
|
}, {
|
|
155
148
|
event: "keydown",
|
|
156
149
|
key: "p",
|
package/tsconfig.json
CHANGED