Skip to content

Commit 3646aaa

Browse files
vertical formatting, score transpose feature
1 parent a19a744 commit 3646aaa

14 files changed

Lines changed: 213 additions & 81 deletions

File tree

src/application/exports.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { cardKeysHtmlEn, cardNotesLetterHtmlEn, cardNotesChromaticHtmlEn, cardNo
4242
// Dialogs
4343
import { SuiDialogBase } from '../ui/dialogs/dialog';
4444
import { SuiModifierDialogFactory } from '../ui/dialogs/factory';
45+
import { SuiTransposeScoreDialog, SuiTransposeScoreAdapter } from '../ui/dialogs/transposeScore';
4546
import { SuiMeasureDialog } from '../ui/dialogs/measureFormat';
4647
import { SuiInsertMeasures } from '../ui/dialogs/addMeasure';
4748
import { SuiInstrumentDialog } from '../ui/dialogs/instrument';
@@ -175,7 +176,8 @@ export const Smo = {
175176
// Dialogs
176177
SuiTempoDialog, SuiInstrumentDialog, SuiModifierDialogFactory, SuiLibraryDialog,
177178
SuiTextBracketDialog,
178-
SuiScoreViewDialog, SuiGlobalLayoutDialog, SuiScoreIdentificationDialog,
179+
SuiScoreViewDialog, SuiGlobalLayoutDialog, SuiScoreIdentificationDialog, SuiTransposeScoreAdapter,
180+
SuiTransposeScoreDialog,
179181
SuiScoreFontDialog, SuiPageLayoutDialog, SuiMeasureDialog, SuiInsertMeasures,
180182
SuiTimeSignatureDialog,SuiTextBlockDialog, SuiLyricDialog, SuiChordChangeDialog,
181183
SuiSlurAttributesDialog, SuiTieAttributesDialog, SuiVoltaAttributeDialog,

src/render/sui/formatter.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -643,17 +643,23 @@ export class SuiLayoutFormatter {
643643
}
644644
}
645645
}
646-
// ### _highestLowestHead
647-
// highest value is actually the one lowest on the page
646+
/**
647+
* highest value is actually the one lowest on the page
648+
* @param measure
649+
* @param note
650+
* @returns
651+
*/
648652
static _highestLowestHead(measure: SmoMeasure, note: SmoNote) {
649-
const hilo = { hi: 0, lo: 9999999 };
650-
note.pitches.forEach((pitch) => {
651-
// 10 pixels per line
652-
const ledger = SmoMusic.pitchToLedgerLine(measure.clef, pitch);
653-
const noteHeight = ledger > 0 ? 10 : -10;
654-
const px = (10 * ledger) + noteHeight;
655-
hilo.lo = Math.min(hilo.lo, px);
656-
hilo.hi = Math.max(hilo.hi, px);
653+
// note...er warning: Notes always have at least 1 pitch, even a rest
654+
// or glyph has a pitch to indicate the placement
655+
const hilo = { hi: 0, lo: 99999999 };
656+
note.pitches.forEach((pitch) => {
657+
const line = 5 - SmoMusic.pitchToStaffLine(measure.clef, pitch);
658+
// TODO: use actual note head/rest/glyph. 10 px is space between staff lines
659+
const noteHeight = 10;
660+
const px = (noteHeight * line);
661+
hilo.lo = Math.min(hilo.lo, px - noteHeight / 2);
662+
hilo.hi = Math.max(hilo.hi, px + noteHeight / 2);
657663
});
658664
return hilo;
659665
}
@@ -752,7 +758,9 @@ export class SuiLayoutFormatter {
752758
if (measure.svg.forceTempo) {
753759
yTop = Math.min(-1 * vexGlyph.tempo.yTop, yTop);
754760
}
755-
measure.voices.forEach((voice) => {
761+
let yBottomOffset = 0;
762+
let yBottomVoiceZero = 0;
763+
measure.voices.forEach((voice, voiceIx) => {
756764
voice.notes.forEach((note) => {
757765
const bg = SuiLayoutFormatter._beamGroupForNote(measure, note);
758766
flag = SmoNote.flagStates.auto;
@@ -766,11 +774,16 @@ export class SuiLayoutFormatter {
766774
}
767775
} else {
768776
flag = note.flagState;
777+
// odd-numbered voices flip default up/down
778+
const voiceMod = voiceIx % 2;
769779
// an auto-flag note is up if the 1st note is middle line
770780
if (flag === SmoNote.flagStates.auto) {
771781
const pitch = note.pitches[0];
772782
flag = SmoMusic.pitchToStaffLine(measure.clef, pitch)
773-
>= 3 ? SmoNote.flagStates.up : SmoNote.flagStates.down;
783+
>= 3 ? SmoNote.flagStates.down : SmoNote.flagStates.up;
784+
if (voiceMod === 1) {
785+
flag = (flag === SmoNote.flagStates.down) ? SmoNote.flagStates.up : SmoNote.flagStates.down;
786+
}
774787
}
775788
}
776789
const hiloHead = SuiLayoutFormatter._highestLowestHead(measure, note);
@@ -784,7 +797,21 @@ export class SuiLayoutFormatter {
784797
// Lyrics will be rendered below the lowest thing on the staff, so add to
785798
// belowBaseline value based on the max number of verses and font size
786799
// it will extend
800+
});
801+
// Vex won't adjust for music in voices > 0 when placing lyrics.
802+
// So we need to adjust here, if voices > 0 have music below lyrics.
803+
if (voiceIx > 0 && yBottomVoiceZero < yBottom) {
804+
yBottomOffset = yBottom - yBottomVoiceZero;
805+
} else {
806+
yBottomVoiceZero = yBottom;
807+
}
808+
});
809+
let lyricsToAdjust: SmoLyric[] = [];
810+
// get the lowest music part, then consider the lyrics
811+
measure.voices.forEach((voice, voiceIx) => {
812+
voice.notes.forEach((note) => {
787813
const lyrics = note.getTrueLyrics();
814+
lyricsToAdjust = lyricsToAdjust.concat(lyrics);
788815
if (lyrics.length) {
789816
const maxLyric = lyrics.reduce((a, b) => a.verse > b.verse ? a : b);
790817
const fontInfo = SuiLayoutFormatter.textFont(maxLyric);
@@ -812,6 +839,11 @@ export class SuiLayoutFormatter {
812839
});
813840
});
814841
yBottom += lyricOffset;
842+
if (lyricsToAdjust.length > 0) {
843+
lyricsToAdjust.forEach((lyric: SmoLyric) => {
844+
lyric.musicYOffset = yBottomOffset;
845+
});
846+
}
815847
return { belowBaseline: yBottom, aboveBaseline: yTop };
816848
}
817849
}

src/render/sui/scoreViewOperations.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,13 @@ export class SuiScoreViewOperations extends SuiScoreView {
614614
this._renderChangedMeasures(measureSelections);
615615
return this.renderer.updatePromise();
616616
}
617-
617+
transposeScore(offset: number): Promise<void> {
618+
this._undoScore('transpose score');
619+
SmoOperation.transposeScore(this.score, offset);
620+
SmoOperation.transposeScore(this.storeScore, offset);
621+
this.renderer.rerenderAll();
622+
return this.renderer.updatePromise();
623+
}
618624
/**
619625
* transpose selected notes
620626
* @param offset 1/2 steps

src/render/vex/glyphDimensions.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import { SmoBarline } from '../../smo/data/measureModifiers';
44
import { SmoMusic } from '../../smo/data/music';
55
import { VexFlow, GlyphInfo, getGlyphWidth } from '../../common/vex';
6-
const VF = VexFlow;
7-
86

97
export class vexGlyph {
108
static width(smoGlyph: GlyphInfo) {

src/render/vex/vxSystem.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,14 @@ export class VxSystem {
118118
verseLyrics.forEach((lyric: SmoLyric) => {
119119
if (lyric.logicalBox) {
120120
// 'lowest' Y on screen is Y with largest value...
121-
lowestY = Math.max(lyric.logicalBox.y, lowestY);
121+
lowestY = Math.max(lyric.logicalBox.y + lyric.musicYOffset, lowestY);
122122
}
123123
});
124124
// second offset all to that point
125125
verseLyrics.forEach((lyric: SmoLyric) => {
126126
if (lyric.logicalBox) {
127127
const offset = Math.max(0, lowestY - lyric.logicalBox.y);
128-
lyric.adjY = offset + lyric.translateY;
128+
lyric.adjY = offset + lyric.translateY;
129129
}
130130
});
131131
} else {
@@ -141,7 +141,7 @@ export class VxSystem {
141141
verseLyrics.forEach((lyric: SmoLyric)=> {
142142
if (lyric.logicalBox) {
143143
const offset = Math.max(0, lowestY - lyric.logicalBox.y);
144-
lyric.adjY = offset + lyric.translateY;
144+
lyric.adjY = offset + lyric.translateY;
145145
}
146146
});
147147
}

src/smo/data/music.ts

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { Pitch, PitchKey, Clef, PitchLetter, TickAccidental,
1111
import { SmoMicrotone } from './noteModifiers';
1212
import { VexFlow, pitchToLedgerLine, vexCanonicalNotes } from '../../common/vex';
1313

14-
const VF = VexFlow;
1514
/**
1615
* Used for xml clef conversion
1716
*/
@@ -97,6 +96,8 @@ export class SmoAudioPitch {
9796
* base note lengths + dots)
9897
*/
9998
export interface SimpleDuration {
99+
index: number,
100+
ticks: number,
100101
baseTicks: number,
101102
dots: number
102103
}
@@ -268,7 +269,14 @@ export class SmoMusic {
268269
}
269270
];
270271
}
271-
static getLineFromSmoPitch(clef: string, smoPitch: Pitch): number {
272+
/**
273+
* Return the number of lines above first ledger line below the staff.
274+
* e.g. middle c in treble clef returns 0. Top line f in treble returns 5.
275+
* @param clef
276+
* @param pitch
277+
* @returns number where 0 is the first ledger line below
278+
*/
279+
static pitchToStaffLine(clef: string, smoPitch: Pitch): number {
272280
const octave = smoPitch.octave * 7 - 4 * 7;
273281
const keyTable = smoPitch.letter + smoPitch.accidental;
274282
let line = (SmoMusic.noteValues[keyTable].root_index + octave) / 2;
@@ -390,7 +398,7 @@ export class SmoMusic {
390398
*/
391399
static pitchToLedgerLine(clef: Clef, pitch: Pitch): number {
392400
// return the distance from the top ledger line, as 0.5 per line/space
393-
const line = SmoMusic.getLineFromSmoPitch(clef, pitch);
401+
const line = SmoMusic.pitchToStaffLine(clef, pitch);
394402
if (line > 5) {
395403
return -1 * (line - 5);
396404
}
@@ -399,16 +407,7 @@ export class SmoMusic {
399407
}
400408
return 0;
401409
}
402-
/**
403-
* Return the number of ledger lines based on the pitch and clef
404-
* @param clef
405-
* @param pitch
406-
* @returns number where 0 is the top staff line
407-
*/
408-
static pitchToStaffLine(clef: Clef, pitch: Pitch): number {
409-
// return the distance from the top ledger line, as 0.5 per line/space
410-
return SmoMusic.getLineFromSmoPitch(clef, pitch);
411-
}
410+
412411
/**
413412
* return flag state (up === 1 or down === 2) based on pitch and clef if auto
414413
* */
@@ -455,6 +454,17 @@ export class SmoMusic {
455454
'subbass': 7,
456455
'french': -1
457456
}
457+
static scaleTones: string[] = ['tonic', '2', '3', '4', '5', '6', '7'];
458+
static getScaleTonesForKey(keySignature: string): Record<string, string> {
459+
const key = SmoMusic.enharmonicRoles[keySignature];
460+
const rv: Record<string, string> = {};
461+
key.forEach((role) => {
462+
if (SmoMusic.scaleTones.indexOf(role.role) >= 0) {
463+
rv[role.letter] = role.letter + role.accidental;
464+
}
465+
});
466+
return rv;
467+
}
458468
/**
459469
* The purpose of this table is to keep consistent enharmonic spelling when transposing
460470
* instruments in different keys. It is not theoritically complete, e.g.
@@ -1115,7 +1125,8 @@ export class SmoMusic {
11151125
const flatKey = keySignature.indexOf('b') >= 0;
11161126
const ar = SmoMusic.getEnharmonics(SmoMusic.pitchToVexKey(smoPitch));
11171127
rv = SmoMusic.stripVexOctave(SmoMusic.pitchToVexKey(smoPitch));
1118-
const scaleMap: Record<string, string> = new VF.Music().createScaleMap(keySignature);
1128+
const scaleMap: Record<string, string> = SmoMusic.getScaleTonesForKey(keySignature);
1129+
// new VF.Music().createScaleMap(keySignature);
11191130
ar.forEach((vexKey) => {
11201131
if (vexKey.length === 1) {
11211132
vexKey += 'n';
@@ -1159,8 +1170,7 @@ export class SmoMusic {
11591170
static getKeyFriendlyEnharmonic(letter: string, keySignature: string): string {
11601171
let rv: string = letter;
11611172
let i = 0;
1162-
const muse = new VF.Music();
1163-
const scale: string[] = Object.values(muse.createScaleMap(keySignature));
1173+
const scale: string[] = Object.values(SmoMusic.getScaleTonesForKey(keySignature));
11641174
let prop: string = SmoMusic.getEnharmonic(letter.toLowerCase());
11651175
while (prop.toLowerCase() !== letter.toLowerCase()) {
11661176
for (i = 0; i < scale.length; ++i) {
@@ -1185,8 +1195,8 @@ export class SmoMusic {
11851195
* @returns
11861196
*/
11871197
static getKeySignatureKey(letter: PitchLetter, keySignature: string): string {
1188-
const km = new VF.KeyManager(keySignature);
1189-
return (km as any).scaleMap[letter];
1198+
const scaleMap = SmoMusic.getScaleTonesForKey(keySignature);
1199+
return scaleMap[letter];
11901200
}
11911201

11921202
static getAccidentalForKeySignature(smoPitch: Pitch, keySignature: string): string {
@@ -1465,23 +1475,28 @@ export class SmoMusic {
14651475
}
14661476
if (SmoMusic._validDurations === null) {
14671477
SmoMusic._validDurations = {};
1478+
let index = 0;
14681479
for (var i = 0; i < SmoMusic.durationsDescending.length; ++i) {
14691480
const baseTicks = SmoMusic.durationsDescending[i];
14701481
for (var j = 3; j >= 1; --j) {
14711482
const { dottedValue, minDot } = computeDots(baseTicks, j);
14721483
if (dottedValue < SmoMusic.highestDuration && minDot > SmoMusic.lowestDuration) {
14731484
SmoMusic._validDurations[dottedValue] = {
1485+
index: SmoMusic._validDurationKeys.length,
1486+
ticks: dottedValue,
14741487
baseTicks,
14751488
dots: j
14761489
}
14771490
SmoMusic._validDurationKeys.push(dottedValue);
14781491
}
14791492
}
1480-
SmoMusic._validDurationKeys.push(baseTicks);
14811493
SmoMusic._validDurations[baseTicks] = {
1494+
index: SmoMusic._validDurationKeys.length,
1495+
ticks: baseTicks,
14821496
baseTicks,
14831497
dots: 0
14841498
};
1499+
SmoMusic._validDurationKeys.push(baseTicks);
14851500
}
14861501
}
14871502
return SmoMusic._validDurations;
@@ -1690,11 +1705,9 @@ export class SmoMusic {
16901705
// Get ticks for this note with an added dot. Return
16911706
// identity if that is not a supported value.
16921707
static getNextDottedLevel(ticks: number): number {
1693-
const ttd = SmoMusic.ticksToDuration;
1694-
const vals = Object.values(ttd);
1695-
const ix = vals.indexOf(ttd[ticks]);
1696-
if (ix >= 0 && ix < vals.length && vals[ix][0] === vals[ix + 1][0]) {
1697-
return SmoMusic.durationToTicks(vals[ix + 1]);
1708+
const ticksOrNull = SmoMusic.closestSmoDurationFromTicks(ticks);
1709+
if (ticksOrNull && ticksOrNull.index > 0) {
1710+
return SmoMusic.validDurations[SmoMusic._validDurationKeys[ticksOrNull.index - 1]].ticks;
16981711
}
16991712
return ticks;
17001713
}
@@ -1703,36 +1716,12 @@ export class SmoMusic {
17031716
// Get ticks for this note with one fewer dot. Return
17041717
// identity if that is not a supported value.
17051718
static getPreviousDottedLevel(ticks: number): number {
1706-
const ttd = SmoMusic.ticksToDuration;
1707-
const vals = Object.values(ttd);
1708-
const ix = vals.indexOf(ttd[ticks]);
1709-
if (ix > 0 && vals[ix][0] === vals[ix - 1][0]) {
1710-
return SmoMusic.durationToTicks(vals[ix - 1]);
1719+
const ticksOrNull = SmoMusic.closestSmoDurationFromTicks(ticks);
1720+
if (ticksOrNull && ticksOrNull.index < SmoMusic._validDurationKeys.length + 1) {
1721+
return SmoMusic.validDurations[SmoMusic._validDurationKeys[ticksOrNull.index + 1]].ticks;
17111722
}
17121723
return ticks;
17131724
}
1714-
1715-
1716-
// ### durationToTicks
1717-
// Uses VF.durationToTicks, but handles dots.
1718-
static durationToTicks(duration: string): number {
1719-
let split = 0;
1720-
let i = 0;
1721-
let vfDuration = 0;
1722-
let dots = duration.indexOf('d');
1723-
if (dots < 0) {
1724-
return VF.durationToTicks(duration) as number;
1725-
} else {
1726-
vfDuration = VF.durationToTicks(duration.substring(0, dots)) as number;
1727-
dots = duration.length - dots; // number of dots
1728-
split = vfDuration / 2;
1729-
for (i = 0; i < dots; ++i) {
1730-
vfDuration += split;
1731-
split = split / 2;
1732-
}
1733-
return vfDuration;
1734-
}
1735-
}
17361725

17371726
/**
17381727
* break the duration up into an array of durations, to split a long

src/smo/data/noteModifiers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,8 +802,11 @@ export class SmoLyric extends SmoNoteModifierBase {
802802
translateX: number = 0;
803803
translateY: number = 0;
804804
classes: string = '';
805+
// used by the renderer to calculate offsets for aligning lyrics
805806
adjX: number = 0;
806807
adjY: number = 0;
808+
// used by the renderer to calculate the y offset for music that goes below the staff
809+
musicYOffset: number = 0;
807810
hyphenX: number = 0;
808811
deleted: boolean = false;
809812

src/smo/xform/operations.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,21 @@ export class SmoOperation {
414414
});
415415
}
416416

417+
static transposeScore(score: SmoScore, offset: number) {
418+
score.staves.forEach((staff, staffIx) => {
419+
staff.measures.forEach((measure, measureIx) => {
420+
measure.voices.forEach((voice, voiceIx) => {
421+
voice.notes.forEach((note, tickIx) => {
422+
const selection = SmoSelection.noteSelection(
423+
score,staffIx, measureIx, voiceIx, tickIx);
424+
if (selection) {
425+
this.transpose(selection, offset);
426+
}
427+
});
428+
});
429+
});
430+
});
431+
}
417432
// ## transpose
418433
// ## Description
419434
// Transpose the selected note, trying to find a key-signature friendly value

0 commit comments

Comments
 (0)