UNPKG

smoosic

Version:

<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i

1,212 lines (1,195 loc) 84.1 kB
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. import { SuiScoreView, updateStaffModifierFunc } from './scoreView'; import { SmoScore, engravingFontType } from '../../smo/data/score'; import { SmoSystemStaffParams, SmoSystemStaff } from '../../smo/data/systemStaff'; import { SmoPartInfo } from '../../smo/data/partInfo'; import { SmoMeasure } from '../../smo/data/measure'; import { SmoNote } from '../../smo/data/note'; import { KeyEvent, SvgBox, Pitch, PitchLetter, RemoveElementLike, ElementLike } from '../../smo/data/common'; import { SmoRenderConfiguration } from './configuration'; import { SmoSystemGroup, SmoPageLayout, SmoGlobalLayout, SmoLayoutManager, SmoAudioPlayerSettings, SmoScorePreferences, SmoScoreInfo } from '../../smo/data/scoreModifiers'; import { SmoTextGroup } from '../../smo/data/scoreText'; import { SmoDynamicText, SmoNoteModifierBase, SmoGraceNote, SmoArticulation, SmoOrnament, SmoLyric, SmoMicrotone, SmoArpeggio, SmoArpeggioType, SmoClefChange, SmoTabNote} from '../../smo/data/noteModifiers'; import { SmoTempoText, SmoVolta, SmoBarline, SmoRepeatSymbol, SmoRehearsalMark, SmoMeasureFormat, TimeSignature } from '../../smo/data/measureModifiers'; import { UndoBuffer } from '../../smo/xform/undo'; import { SmoOperation, createStaffModifierType, MakeTupletOperation } from '../../smo/xform/operations'; import { BatchSelectionOperation } from '../../smo/xform/operations'; import { smoSerialize } from '../../common/serializationHelpers'; import { FontInfo } from '../../common/vex'; import { SmoMusic } from '../../smo/data/music'; import { SuiOscillator } from '../audio/oscillator'; import { XmlToSmo } from '../../smo/mxml/xmlToSmo'; import { SuiAudioPlayer } from '../audio/player'; import { SuiXhrLoader } from '../../ui/fileio/xhrLoader'; import { SmoSelection, SmoSelector } from '../../smo/xform/selections'; import { StaffModifierBase, SmoInstrument, SmoInstrumentParams, SmoStaffTextBracket, SmoTabStave } from '../../smo/data/staffModifiers'; import { SuiPiano } from './piano'; import { SvgHelpers } from './svgHelpers'; import { PromiseHelpers } from '../../common/promiseHelpers'; import { parseJsonText } from 'typescript'; declare var $: any; declare var SmoConfig: SmoRenderConfiguration; /** * MVVM-like operations on the displayed score. * * All operations that can be performed on a 'live' score go through this * module. It maps the score view to the actual score and makes sure the * model and view stay in sync. * * Because this object operates on the current selections, * all operations return promise so applications can wait for the * operation to complete and update the selection list. * @category SuiRender */ export class SuiScoreViewOperations extends SuiScoreView { /** * Add a new text group to the score * @param textGroup a new text group * @returns */ async addTextGroup(textGroup: SmoTextGroup): Promise<void> { const altNew = SmoTextGroup.deserializePreserveId(textGroup.serialize()); const isPartExposed = this.isPartExposed(); let selector = textGroup.selector ?? SmoSelector.default; const partInfo = this.score.staves[0].partInfo; const bufType = isPartExposed && partInfo.preserveTextGroups ? UndoBuffer.bufferTypes.PART_MODIFIER : UndoBuffer.bufferTypes.SCORE_MODIFIER; if (bufType === UndoBuffer.bufferTypes.PART_MODIFIER) { selector.staff = this.staffMap[0]; } this.storeUndo.addBuffer('remove text group', bufType, selector, textGroup, UndoBuffer.bufferSubtypes.ADD); if (isPartExposed && partInfo.preserveTextGroups) { this.score.staves[0].partInfo.updateTextGroup(textGroup, true); const partInfo = this.storeScore.staves[this._getEquivalentStaff(0)].partInfo; partInfo.updateTextGroup(altNew, true); } else { this.score.updateTextGroup(textGroup, true); this.storeScore.updateTextGroup(altNew, true); } await this.renderer.rerenderTextGroups(); } /** * Remove the text group from the score * @param textGroup * @returns */ async removeTextGroup(textGroup: SmoTextGroup): Promise<void> { let selector = textGroup.selector ?? SmoSelector.default; const partInfo = this.score.staves[0].partInfo; const isPartExposed = this.isPartExposed(); const bufType = isPartExposed && partInfo.preserveTextGroups ? UndoBuffer.bufferTypes.PART_MODIFIER : UndoBuffer.bufferTypes.SCORE_MODIFIER; let ogText = this.storeScore.textGroups.find((tg) => tg.attrs.id === textGroup.attrs.id); if (isPartExposed && partInfo.preserveTextGroups) { ogText = partInfo.textGroups.find((tg) => tg.attrs.id === textGroup.attrs.id); } if (bufType === UndoBuffer.bufferTypes.PART_MODIFIER) { selector.staff = this.staffMap[0]; } else { selector.staff = this.staffMap[selector.staff]; } if (!ogText) { return; } this.storeUndo.addBuffer('remove text group', bufType, selector, ogText, UndoBuffer.bufferSubtypes.REMOVE); const altGroup = SmoTextGroup.deserializePreserveId(textGroup.serialize()); textGroup.elements.forEach((el: ElementLike) => RemoveElementLike(el)); textGroup.elements = []; if (isPartExposed && partInfo.preserveTextGroups) { partInfo.updateTextGroup(textGroup, false); this.storeScore.staves[this._getEquivalentStaff(0)].partInfo.updateTextGroup(altGroup, false); } else { this.score.updateTextGroup(textGroup, false); this.storeScore.updateTextGroup(altGroup, false); } await this.renderer.rerenderTextGroups(); } /** * UPdate an existing text group. The original is passed in, because since TG not tied to a musical * element, we need to find the one we're updating. * @param oldVersion * @param newVersion * @returns */ async updateTextGroup(newVersion: SmoTextGroup): Promise<void> { const selector = newVersion.selector ?? SmoSelector.default; const isPartExposed = this.isPartExposed(); const partInfo = this.score.staves[0].partInfo; // Back up the original score text let ogtg = this.storeScore.textGroups.find((tg) => tg.attrs.id === newVersion.attrs.id); if (isPartExposed && partInfo.preserveTextGroups) { ogtg = partInfo.textGroups.find((tg) => tg.attrs.id === newVersion.attrs.id); } if (!ogtg) { // there is nothing to update, return. return; } if (ogtg) { const bufType = isPartExposed && partInfo.preserveTextGroups ? UndoBuffer.bufferTypes.PART_MODIFIER : UndoBuffer.bufferTypes.SCORE_MODIFIER; // if this is part text, make sure the undo buffer is associated with the part stave // in the full score, so undo works properly if (bufType === UndoBuffer.bufferTypes.PART_MODIFIER) { selector.staff = this.staffMap[0]; } else { selector.staff = this.staffMap[selector.staff]; } this.storeUndo.addBuffer('modify text', bufType, selector, ogtg, UndoBuffer.bufferSubtypes.UPDATE); } const altNew = SmoTextGroup.deserializePreserveId(newVersion.serialize()); this.score.updateTextGroup(newVersion, true); // If this is part text, don't store it in the score text, except for the displayed score if (!isPartExposed) { this.storeScore.updateTextGroup(altNew, true); } else { this.storeScore.staves[this._getEquivalentStaff(0)].partInfo.updateTextGroup(altNew, true); } // TODO: only render the one TG. await this.renderer.rerenderTextGroups(); // return this.renderer.updatePromise(); } /** * load an mxml score remotely, return a promise that * completes when the file is loaded * @param url where to find the xml file * @returns */ async loadRemoteXml(url: string): Promise<any> { const req = new SuiXhrLoader(url); const self = this; // Shouldn't we return promise of actually displaying the score? await req.loadAsync(); const parser = new DOMParser(); const xml = parser.parseFromString(req.value, 'text/xml'); const score = XmlToSmo.convert(xml); score.layoutManager!.zoomToWidth($('body').width()); await self.changeScore(score); } /** * load a remote score in SMO format * @param url url to find the score * @returns */ async loadRemoteJson(url: string) : Promise<any> { const req = new SuiXhrLoader(url); await req.loadAsync(); const score = SmoScore.deserialize(req.value); await this.changeScore(score); } /** * Load a remote score, return promise when it's been loaded * from afar. * @param pref * @returns */ async loadRemoteScore(url: string): Promise<any> { if (url.endsWith('xml') || url.endsWith('mxl')) { return this.loadRemoteXml(url); } else { return this.loadRemoteJson(url); } } async updateAudioSettings(pref: SmoAudioPlayerSettings) { this._undoScorePreferences('Update preferences'); this.score.audioSettings = pref; this.storeScore.audioSettings = new SmoAudioPlayerSettings(pref); // No rendering to be done return this.renderer.updatePromise(); } /** * Global settings that control how the score editor behaves * @param pref * @returns */ async updateScorePreferences(pref: SmoScorePreferences): Promise<void> { this._undoScorePreferences('Update preferences'); const oldXpose = this.score.preferences.transposingScore; const curXpose = pref.transposingScore; this.score.updateScorePreferences(new SmoScorePreferences(pref)); this.storeScore.updateScorePreferences(new SmoScorePreferences(pref)); if (curXpose === false && oldXpose === true) { this.score.setNonTransposing(); } else if (curXpose === true && oldXpose === false) { this.score.setTransposing(); } this.renderer.setDirty(); return this.renderer.updatePromise(); } /** * Update information about the score, composer etc. * @param scoreInfo * @returns */ async updateScoreInfo(scoreInfo: SmoScoreInfo): Promise<void> { this._undoScorePreferences('Update preferences'); this.score.scoreInfo = scoreInfo; this.storeScore.scoreInfo = JSON.parse(JSON.stringify(scoreInfo)); return this.renderer.updatePromise() } async addRemoveArpeggio(code: SmoArpeggioType) { await this.modifyCurrentSelections('add/remove addRemoveArpeggio', (score, selections) => { selections.forEach((sel) => { if (sel.note) { if (code === 'none') { sel.note.arpeggio = undefined; } else { sel.note.arpeggio = new SmoArpeggio({ type: code }); } } }); }); } /** * A clef change mid-measure (clefNote) * @param clef */ async addRemoveClefChange(clef: SmoClefChange) { await this.modifyCurrentSelections('add/remove addRemoveClefChange', (score, selections) => { selections.forEach((sel) => { const measureClef = sel.measure.clef; if (sel.note) { if (measureClef === clef.clef) { sel.note.clefNote = null; } else { sel.note.clefNote = clef; } } }); }); } /** * Modify the dynamics assoicated with the specific selection * @param selection * @param dynamic * @returns */ async addDynamic(selection: SmoSelection, dynamic: SmoDynamicText): Promise<void> { await this.modifySelection('add dynamic', selection, (score, selections) => { SmoOperation.addDynamic(selections[0], dynamic); }); } /** * Remove dynamics from the selection * @param selection * @param dynamic * @returns */ async _removeDynamic(selection: SmoSelection, dynamic: SmoDynamicText): Promise<void> { const equiv = this._getEquivalentSelection(selection); if (equiv !== null && equiv.note !== null) { const altModifiers = equiv.note.getModifiers('SmoDynamicText'); SmoOperation.removeDynamic(selection, dynamic); if (altModifiers.length) { SmoOperation.removeDynamic(equiv, altModifiers[0] as SmoDynamicText); } } await this.renderer.updatePromise(); } /** * Remove dynamics from the current selection * @param dynamic * @returns */ async removeDynamic(dynamic: SmoDynamicText): Promise<void> { const sel = this.tracker.modifierSelections[0]; if (!sel.selection) { return PromiseHelpers.emptyPromise(); } this.tracker.selections = [sel.selection]; this._undoFirstMeasureSelection('remove dynamic'); this._removeDynamic(sel.selection, dynamic); this.renderer.addToReplaceQueue(sel.selection); await this.renderer.updatePromise() } /** * we never really delete a note, but we will convert it into a rest and if it's * already a rest we will try to hide it. * Operates on current selections * */ async deleteNote(): Promise<void> { const measureSelections = this.undoTrackerMeasureSelections('delete note'); this.tracker.selections.forEach((sel) => { if (sel.note) { const altSel = this._getEquivalentSelection(sel); // set the pitch to be a good position for the rest const pitch = JSON.parse(JSON.stringify( SmoMeasure.defaultPitchForClef[sel.measure.clef])); const altPitch = JSON.parse(JSON.stringify( SmoMeasure.defaultPitchForClef[altSel!.measure.clef])); sel.note.pitches = [pitch]; altSel!.note!.pitches = [altPitch]; // If the note is a note, make it into a rest. If the note is a rest already, // make it invisible. If it is invisible already, make it back into a rest. if (sel.note.isRest() && !sel.note.isHidden()) { sel.note.makeHidden(true); altSel!.note!.makeHidden(true); } else { sel.note.clearArticulations(); sel.note.makeRest(); altSel!.note!.makeRest(); altSel!.note!.clearArticulations(); sel.note.makeHidden(false); altSel!.note!.makeHidden(false); } } }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise() } /** * The lyric editor moves around, so we can't depend on the tracker for the * correct selection. We get it directly from the editor. * * @param selector - the selector of the note with the lyric to remove * @param lyric - a copy of the lyric to remove. We use the verse, parser to identify it * @returns render promise */ async removeLyric(selector: SmoSelector, lyric: SmoLyric): Promise<void> { const selection = SmoSelection.noteFromSelector(this.score, selector); if (selection === null) { return PromiseHelpers.emptyPromise(); } this._undoSelection('remove lyric', selection); selection.note!.removeLyric(lyric); const equiv = this._getEquivalentSelection(selection); const storeLyric = equiv!.note!.getLyricForVerse(lyric.verse, lyric.parser); if (typeof (storeLyric) !== 'undefined') { equiv!.note!.removeLyric(lyric); } this.renderer.addToReplaceQueue(selection); lyric.deleted = true; await this.renderer.updatePromise(); } /** * @param selector where to add or update the lyric * @param lyric a copy of the lyric to remove * @returns */ async addOrUpdateLyric(selector: SmoSelector, lyric: SmoLyric): Promise<void> { const selection = SmoSelection.noteFromSelector(this.score, selector); if (selection === null) { return; } this._undoSelection('update lyric', selection); selection.note!.addLyric(lyric); const equiv = this._getEquivalentSelection(selection); const altLyric = SmoNoteModifierBase.deserialize(lyric.serialize() as any) as SmoLyric; equiv!.note!.addLyric(altLyric); this.renderer.addToReplaceQueue(selection); await this.renderer.updatePromise(); } /** * Delete all the notes for the currently selected voice * @returns */ async depopulateVoice(): Promise<void> { const measureSelections = this.undoTrackerMeasureSelections('depopulate voice'); measureSelections.forEach((selection) => { const ix = selection.measure.getActiveVoice(); if (ix !== 0) { SmoOperation.depopulateVoice(selection, ix); const equiv = this._getEquivalentSelection(selection); SmoOperation.depopulateVoice(equiv!, ix); } }); SmoOperation.setActiveVoice(this.score, 0); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * Change the active voice in a multi-voice measure. * @param index * @returns */ _changeActiveVoice(index: number): SmoSelection[] { const measuresToAdd: SmoSelection[] = []; const measureSelections = SmoSelection.getMeasureList(this.tracker.selections); measureSelections.forEach((measureSelection) => { if (index === measureSelection.measure.voices.length) { measuresToAdd.push(measureSelection); } }); return measuresToAdd; } /** * Populate a new voice with default notes * @param index the voice to populate * @returns */ async populateVoice(index: number): Promise<void> { const measuresToAdd = this._changeActiveVoice(index); if (measuresToAdd.length === 0) { SmoOperation.setActiveVoice(this.score, index); this.tracker.selectActiveVoice(); return this.renderer.updatePromise(); } measuresToAdd.forEach((selection) => { this._undoSelection('popualteVoice', selection); SmoOperation.populateVoice(selection, index); const equiv = this._getEquivalentSelection(selection); SmoOperation.populateVoice(equiv!, index); }); SmoOperation.setActiveVoice(this.score, index); this._renderChangedMeasures(measuresToAdd); await this.renderer.updatePromise(); this.tracker.selectActiveVoice(); } async swapVoices(voice1: number, voice2: number): Promise<void> { const selections = this.tracker.getSelectedMeasures(); const altSelections = this._getEquivalentSelections(selections); SmoOperation.swapVoice(selections, voice1, voice2); SmoOperation.swapVoice(altSelections, voice1, voice2); this._renderChangedMeasures(selections); } /** * Assign an instrument to a set of measures * @param instrument the instrument to assign to the selections * @param selections * @returns */ async changeInstrument(instrument: SmoInstrument, selections: SmoSelection[]): Promise<void> { if (typeof (selections) === 'undefined') { selections = this.tracker.selections; } this._undoSelections('change instrument', selections); const altSelections = this._getEquivalentSelections(selections); SmoOperation.changeInstrument(instrument, selections); SmoOperation.changeInstrument(instrument, altSelections); this._renderChangedMeasures(selections); await this.renderer.updatePromise(); } /** * Set the time signature for a selection * @param timeSignature actual time signature */ async setTimeSignature(timeSignature: TimeSignature): Promise<void> { this._undoScore('Set time signature'); const selections = this.tracker.selections; const altSelections = this._getEquivalentSelections(selections); SmoOperation.setTimeSignature(this.score, selections, timeSignature); SmoOperation.setTimeSignature(this.storeScore, altSelections, timeSignature); this._renderChangedMeasures(SmoSelection.getMeasureList(this.tracker.selections)); return this.renderer.updatePromise(); } /** * Move selected staff up or down in the score. * @param index direction to move * @returns */ async moveStaffUpDown(index: number): Promise<void> { this._undoScore('re-order staves'); // Get staff to move const selection = this._getEquivalentSelection(this.tracker.selections[0]); // Make the move in the model, and reset the view so we can see the new // arrangement SmoOperation.moveStaffUpDown(this.storeScore, selection!, index); this.viewAll(); await this.renderer.updatePromise(); } /** * Update the staff group for a score, which determines how the staves * are justified and bracketed * @param staffGroup */ async addOrUpdateStaffGroup(staffGroup: SmoSystemGroup): Promise<void> { this._undoScore('group staves'); // Assume that the view is now set to full score this.score.addOrReplaceSystemGroup(staffGroup); this.storeScore.addOrReplaceSystemGroup(staffGroup); this.renderer.setDirty(); await this.renderer.updatePromise(); } async updateTabStave(tabStave: SmoTabStave) { const selections = SmoSelection.getMeasuresBetween(this.score, tabStave.startSelector, tabStave.endSelector); const altSelections = this._getEquivalentSelections(selections); if (selections.length === 0) { return; } this._undoSelections('updateTabStave', selections); const staff: number = selections[0].selector.staff; const altStaff = altSelections[0].selector.staff; const altTabStave = new SmoTabStave(tabStave.serialize()); altTabStave.startSelector.staff = altStaff; altTabStave.endSelector.staff = altStaff; altTabStave.attrs.id = tabStave.attrs.id; this.score.staves[staff].updateTabStave(tabStave); this.storeScore.staves[altStaff].updateTabStave(altTabStave); this._renderChangedMeasures(SmoSelection.getMeasureList(this.tracker.selections)); await this.renderer.updatePromise(); } async removeTabStave() { const selections = this.tracker.selections; const altSelections = this._getEquivalentSelections(selections); if (selections.length === 0) { return; } this._undoSelections('updateTabStave', selections); const stavesToRemove: SmoTabStave[] = []; const altStavesToRemove: SmoTabStave[] = []; const added: Record<string, SmoTabStave> = {}; selections.forEach((sel, ix) => { const altSel = altSelections[ix]; const tabStave = sel.staff.getTabStaveForMeasure(sel.selector); const altTabStave = altSel.staff.getTabStaveForMeasure(altSel.selector); if (tabStave && altTabStave) { if (!added[tabStave.attrs.id]) { added[tabStave.attrs.id] = tabStave; stavesToRemove.push(tabStave); altStavesToRemove.push(altTabStave); } } }); selections[0].staff.removeTabStaves(stavesToRemove); altSelections[0].staff.removeTabStaves(altStavesToRemove); this._renderChangedMeasures(SmoSelection.getMeasureList(this.tracker.selections)); await this.renderer.updatePromise(); } /** * Update tempo for all or part of the score * @param measure the measure with the tempo. Tempo is measure-wide parameter * @param scoreMode if true, update whole score. Else selections * @returns */ async updateTempoScore(measure: SmoMeasure, tempo: SmoTempoText, scoreMode: boolean, selectionMode: boolean): Promise<void> { let measureIndex = 0; const originalTempo = new SmoTempoText(measure.tempo); this._undoColumn('update tempo', measure.measureNumber.measureIndex); let startMeasure = measure.measureNumber.measureIndex; let endMeasure = this.score.staves[0].measures.length; let displayed = false; if (selectionMode) { const endSel = this.tracker.getExtremeSelection(1); if (endSel.selector.measure > startMeasure) { endMeasure = endSel.selector.measure; } } // If we are only changing the position of the text, it only affects the tempo measure. if (SmoTempoText.eq(originalTempo, tempo) && tempo.yOffset !== originalTempo.yOffset && endMeasure > startMeasure) { endMeasure = startMeasure + 1; } for (measureIndex = startMeasure; measureIndex < endMeasure; ++measureIndex) { if (!scoreMode && !selectionMode) { // If not whole score or selections, change until the tempo doesn't match previous measure's tempo (next tempo change) const compMeasure = this.score.staves[0].measures[measureIndex]; if (SmoTempoText.eq(originalTempo, compMeasure.tempo) || displayed === false) { const sel = SmoSelection.measureSelection(this.score, 0, measureIndex); const altSel = SmoSelection.measureSelection(this.storeScore, 0, measureIndex); if (sel && sel.measure.tempo.display && !displayed) { this.renderer.addToReplaceQueue(sel); displayed = true; } if (sel) { SmoOperation.addTempo(this.score, sel, tempo); } if (altSel) { SmoOperation.addTempo(this.storeScore, altSel, tempo); } } else { break; } } else { const sel = SmoSelection.measureSelection(this.score, 0, measureIndex); const altSel = SmoSelection.measureSelection(this.storeScore, 0, measureIndex); if (sel) { SmoOperation.addTempo(this.score, sel, tempo); if (!displayed) { this.renderer.addToReplaceQueue(sel); displayed = true; } } if (altSel) { SmoOperation.addTempo(this.storeScore, altSel, tempo); } } } await this.renderer.updatePromise(); } async updateTabNote(tabNote: SmoTabNote) { const selections = SmoSelection.getMeasuresBetween(this.score, this.tracker.getExtremeSelection(-1).selector, this.tracker.getExtremeSelection(1).selector); const altSelections = this._getEquivalentSelections(selections); this._undoSelections('updateTabNote', selections); SmoOperation.updateTabNote(selections, tabNote); SmoOperation.updateTabNote(altSelections, tabNote); this.renderer.addToReplaceQueue(selections); await this.renderer.updatePromise(); } async removeTabNote() { const selections = SmoSelection.getMeasuresBetween(this.score, this.tracker.getExtremeSelection(-1).selector, this.tracker.getExtremeSelection(1).selector); const altSelections = this._getEquivalentSelections(selections); this._undoSelections('updateTabNote', selections); SmoOperation.removeTabNote(selections); SmoOperation.removeTabNote(altSelections); this.renderer.addToReplaceQueue(selections); await this.renderer.updatePromise(); } /** * 'remove' tempo, which means either setting the bars to the * default tempo, or the previously-set tempo. * @param scoreMode whether to reset entire score */ async removeTempo(measure: SmoMeasure, tempo: SmoTempoText, scoreMode: boolean, selectionMode: boolean): Promise<void> { const startSelection = this.tracker.selections[0]; if (startSelection.selector.measure > 0) { const measureIx = startSelection.selector.measure - 1; const target = startSelection.staff.measures[measureIx]; const tempo = target.getTempo(); const newTempo = new SmoTempoText(tempo); newTempo.display = false; this.updateTempoScore(measure, newTempo, scoreMode, selectionMode); } else { this.updateTempoScore(measure, new SmoTempoText(SmoTempoText.defaults), scoreMode, selectionMode); } await this.renderer.updatePromise(); } /** * Add a grace note to the selected real notes. */ async addGraceNote(): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('add grace note'); selections.forEach((selection) => { const index = selection.note!.getGraceNotes().length; const pitches = JSON.parse(JSON.stringify(selection.note!.pitches)); const grace = new SmoGraceNote({ pitches, ticks: { numerator: 2048, denominator: 1, remainder: 0 } }); SmoOperation.addGraceNote(selection, grace, index); const altPitches = JSON.parse(JSON.stringify(selection.note!.pitches)); const altGrace = new SmoGraceNote({ pitches: altPitches, ticks: { numerator: 2048, denominator: 1, remainder: 0 } }); altGrace.attrs.id = grace.attrs.id; const altSelection = this._getEquivalentSelection(selection); SmoOperation.addGraceNote(altSelection!, altGrace, index); }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * remove selected grace note * @returns */ async removeGraceNote(): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('remove grace note'); selections.forEach((selection) => { // TODO: get the correct offset SmoOperation.removeGraceNote(selection, 0); const altSel = (this._getEquivalentSelection(selection)); SmoOperation.removeGraceNote(altSel!, 0); }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * Toggle slash in stem of grace note */ async slashGraceNotes(): Promise<void> { const grace = this.tracker.getSelectedGraceNotes(); const measureSelections = this.undoTrackerMeasureSelections('slash grace note toggle'); grace.forEach((gn) => { SmoOperation.slashGraceNotes(gn); if (gn.selection !== null) { const altSelection = this._getEquivalentSelection(gn.selection); const altGn = this._getEquivalentGraceNote(altSelection!, gn.modifier as SmoGraceNote); SmoOperation.slashGraceNotes({ selection: altSelection, modifier: altGn as any, box: SvgBox.default, index: 0 }); } }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } async transposeScore(offset: number): Promise<void> { this._undoScore('transpose score'); SmoOperation.transposeScore(this.score, offset); SmoOperation.transposeScore(this.storeScore, offset); this.renderer.rerenderAll(); await this.renderer.updatePromise(); } /** * transpose selected notes * @param offset 1/2 steps * @returns */ async transposeSelections(offset: number): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('transpose'); const grace = this.tracker.getSelectedGraceNotes(); if (grace.length) { grace.forEach((artifact) => { if (artifact.selection !== null && artifact.selection.note !== null) { const gn1 = artifact.modifier as SmoGraceNote; const index = artifact.selection.note.graceNotes.findIndex((x) => x.attrs.id === gn1.attrs.id); const altSelection = this._getEquivalentSelection(artifact.selection); if (altSelection && altSelection.note !== null) { const gn2 = altSelection.note.graceNotes[index]; SmoOperation.transposeGraceNotes(altSelection!, [gn2], offset); } SmoOperation.transposeGraceNotes(artifact.selection, [gn1], offset); } }); } else { selections.forEach((selected) => { SmoOperation.transpose(selected, offset); const altSel = this._getEquivalentSelection(selected); SmoOperation.transpose(altSel!, offset); }); if (selections.length === 1 && this.score.preferences.autoPlay) { SuiOscillator.playSelectionNow(selections[0], this.score, 1); } } this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * toggle the accidental spelling of the selected notes * @returns */ async toggleEnharmonic(): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('toggle enharmonic'); const grace = this.tracker.getSelectedGraceNotes(); if (grace.length) { grace.forEach((artifact) => { SmoOperation.toggleGraceNoteEnharmonic(artifact.selection!, [artifact.modifier as SmoGraceNote]); const altSelection = this._getEquivalentSelection(artifact.selection!); const altGr = this._getEquivalentGraceNote(altSelection!, artifact.modifier as SmoGraceNote); SmoOperation.toggleGraceNoteEnharmonic(altSelection!, [altGr]); }); } else { selections.forEach((selected) => { if (typeof (selected.selector.pitches) === 'undefined') { selected.selector.pitches = []; } SmoOperation.toggleEnharmonic(selected); const altSel = this._getEquivalentSelection(selected); SmoOperation.toggleEnharmonic(altSel!); }); } this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * Toggle cautionary/courtesy accidentals */ async toggleCourtesyAccidentals(): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('toggle courtesy accidental'); const grace = this.tracker.getSelectedGraceNotes(); if (grace.length) { grace.forEach((artifact) => { const gn1 = [artifact.modifier] as SmoGraceNote[]; SmoOperation.toggleGraceNoteCourtesy(artifact.selection, gn1); const altSel = this._getEquivalentSelection(artifact.selection!); const gn2 = this._getEquivalentGraceNote(altSel!, gn1[0]); SmoOperation.toggleGraceNoteCourtesy(altSel!, [gn2]); }); } else { selections.forEach((selection) => { SmoOperation.toggleCourtesyAccidental(selection); const altSel = this._getEquivalentSelection(selection); SmoOperation.toggleCourtesyAccidental(altSel!); }); } this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * change the duration of notes for selected, creating more * or fewer notes. * After the change, reset the selection so it's as close as possible * to the original length * @param operation * @returns */ async batchDurationOperation(operation: BatchSelectionOperation): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('change duration'); const grace = this.tracker.getSelectedGraceNotes(); const graceMap: Record<string, BatchSelectionOperation> = { doubleDuration: 'doubleGraceNoteDuration', halveDuration: 'halveGraceNoteDuration' }; if (grace.length && typeof (graceMap[operation]) !== 'undefined') { operation = graceMap[operation]; grace.forEach((artifact) => { (SmoOperation as any)[operation](artifact.selection, artifact.modifier); const altSelection = this._getEquivalentSelection(artifact.selection!); const gn2 = this._getEquivalentGraceNote(altSelection!, artifact.modifier as SmoGraceNote); (SmoOperation as any)[operation](altSelection!, gn2); }); } else { const altAr = this._getEquivalentSelections(selections); SmoOperation.batchSelectionOperation(this.score, selections, operation); SmoOperation.batchSelectionOperation(this.storeScore, altAr, operation); } this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * Toggle selected modifier on selected notes * @param modifier * @param ctor parent class constructor (e.g. SmoOrnament) * @returns */ async toggleArticulation(modifier: string, ctor: string): Promise<void> { const measureSelections = this.undoTrackerMeasureSelections('toggle articulation'); this.tracker.selections.forEach((sel) => { if (ctor === 'SmoArticulation') { const aa = new SmoArticulation({ articulation: modifier }); const altAa = new SmoArticulation({ articulation: modifier }); altAa.attrs.id = aa.attrs.id; if (sel.note) { sel.note.toggleArticulation(aa); } const altSelection = this._getEquivalentSelection(sel); if (altSelection && altSelection.note) { altSelection.note.toggleArticulation(altAa); } } else { const aa = new SmoOrnament({ ornament: modifier }); const altAa = new SmoOrnament({ ornament: modifier }); altAa.attrs.id = aa.attrs.id; const altSelection = this._getEquivalentSelection(sel!); if (sel.note) { sel.note.toggleOrnament(aa); } if (altSelection && altSelection.note) { altSelection.note.toggleOrnament(altAa); } } }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } async setArticulation(modifier: SmoArticulation, set: boolean): Promise<void> { const measureSelections = this.undoTrackerMeasureSelections('set articulation'); this.tracker.selections.forEach((sel) => { const altAa = new SmoArticulation(modifier); if (sel.note) { sel.note.setArticulation(modifier, set); } const altSelection = this._getEquivalentSelection(sel); if (altSelection && altSelection.note) { altSelection.note.toggleArticulation(altAa); } }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } async setOrnament(modifier: SmoOrnament, set: boolean): Promise<void> { const measureSelections = this.undoTrackerMeasureSelections('set articulation'); this.tracker.selections.forEach((sel) => { const altAa = new SmoOrnament(modifier); if (sel.note) { sel.note.setOrnament(modifier, set); } const altSelection = this._getEquivalentSelection(sel); if (altSelection && altSelection.note) { altSelection.note.setOrnament(altAa, set); } }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * convert non-tuplet not to a tuplet * @param params */ async makeTuplet(params: MakeTupletOperation): Promise<void> { const selection = this.tracker.selections[0]; const measureSelections = this.undoTrackerMeasureSelections('make tuplet'); SmoOperation.makeTuplet(selection, params); const altSelection = this._getEquivalentSelection(selection!); SmoOperation.makeTuplet(altSelection!, params); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * Convert selected tuplet to a single (if possible) non-tuplet */ async unmakeTuplet(): Promise<void> { const selection = this.tracker.selections[0]; const measureSelections = this.undoTrackerMeasureSelections('unmake tuplet'); SmoOperation.unmakeTuplet(selection); const altSelection = this._getEquivalentSelection(selection); SmoOperation.unmakeTuplet(altSelection!); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * Create a chord by adding an interval to selected note * @param interval 1/2 steps * @returns */ async setInterval(interval: number): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('set interval'); selections.forEach((selected) => { SmoOperation.interval(selected, interval); const altSelection = this._getEquivalentSelection(selected); SmoOperation.interval(altSelection!, interval); }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * change the selected chord into a single note * @returns */ async collapseChord(): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('collapse chord'); selections.forEach((selected) => { const note: SmoNote | null = selected.note; if (note) { const pp = JSON.parse(JSON.stringify(note.pitches[0])); const altpp = JSON.parse(JSON.stringify(note.pitches[0])); // No operation for this? note.pitches = [pp]; const altSelection = this._getEquivalentSelection(selected); altSelection!.note!.pitches = [altpp]; } }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * Toggle chicken-scratches, for jazz improv, comping etc. */ async toggleSlash(): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('make slash'); selections.forEach((selection) => { SmoOperation.toggleSlash(selection); const altSel = this._getEquivalentSelection(selection); SmoOperation.toggleSlash(altSel!); }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * make selected notes into a rest, or visa-versa * @returns */ async makeRest(): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('make rest'); selections.forEach((selection) => { SmoOperation.toggleRest(selection); const altSel = this._getEquivalentSelection(selection); SmoOperation.toggleRest(altSel!); }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } async clearAllBeams(): Promise<void> { this._undoScore('clearAllBeams'); SmoOperation.clearAllBeamGroups(this.score); SmoOperation.clearAllBeamGroups(this.storeScore); await this.awaitRender(); } async clearSelectedBeams() { const selections = this.tracker.selections; const measures = SmoSelection.getMeasureList(selections); const altSelections = this._getEquivalentSelections(selections); SmoOperation.clearBeamGroups(this.score, selections); SmoOperation.clearBeamGroups(this.storeScore, altSelections); this._renderChangedMeasures(measures); await this.renderer.updatePromise(); } /** * toggle the 'end beam' flag for selected notes * @returns */ async unbeamSelections(): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('toggle beam group'); selections.forEach((selection) => { SmoOperation.unbeamSelections(selection); const altSel = this._getEquivalentSelection(selection); SmoOperation.unbeamSelections(altSel!); }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } async toggleCue() { const measureSelections = this.undoTrackerMeasureSelections('toggle note cue'); this.tracker.selections.forEach((selection) => { const altSelection = this._getEquivalentSelection(selection); if (selection.note && selection.note.isRest() === false) { selection.note.isCue = !selection.note.isCue; if (altSelection && altSelection.note) { altSelection.note.isCue = selection.note.isCue; } } }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * up or down * @returns */ async toggleBeamDirection(): Promise<void> { const selections = this.tracker.selections; if (selections.length < 1) { return PromiseHelpers.emptyPromise(); } const measureSelections = this.undoTrackerMeasureSelections('toggle beam direction'); SmoOperation.toggleBeamDirection(selections); SmoOperation.toggleBeamDirection(this._getEquivalentSelections(selections)); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * Add the selected notes to a beam group */ async beamSelections(): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('beam selections'); SmoOperation.beamSelections(this.score, selections); SmoOperation.beamSelections(this.storeScore, this._getEquivalentSelections(selections)); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * change key signature for selected measures * @param keySignature vex key signature */ async addKeySignature(keySignature: string): Promise<void> { const measureSelections = this.undoTrackerMeasureSelections('set key signature ' + keySignature); measureSelections.forEach((sel) => { SmoOperation.addKeySignature(this.score, sel, keySignature); const altSel = this._getEquivalentSelection(sel); SmoOperation.addKeySignature(this.storeScore, altSel!, keySignature); }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * Sets a pitch from the piano widget. * @param pitch {Pitch} * @param chordPedal {boolean} - indicates we are adding to a chord */ async setPitchPiano(pitch: Pitch, chordPedal: boolean): Promise<void> { const measureSelections = this.undoTrackerMeasureSelections( 'setAbsolutePitch ' + pitch.letter + '/' + pitch.accidental); this.tracker.selections.forEach((selected) => { const npitch: Pitch = { letter: pitch.letter, accidental: pitch.accidental, octave: pitch.octave }; const octave = SmoMeasure.defaultPitchForClef[selected.measure.clef].octave; npitch.octave += octave; const altSel = this._getEquivalentSelection(selected); if (chordPedal && selected.note) { selected.note.toggleAddPitch(npitch); altSel!.note!.toggleAddPitch(npitch); } else { SmoOperation.setPitch(selected, [npitch]); SmoOperation.setPitch(altSel!, [npitch]); } }); this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * show or hide the piano widget * @param value to show it */ async showPiano(value: boolean): Promise<void> { this.score.preferences.showPiano = value; this.storeScore.preferences.showPiano = value; if (value) { SuiPiano.showPiano(); } else { SuiPiano.hidePiano(); } await this.renderer.updatePromise(); } /** * Render a pitch for each letter name-pitch in the string, * @param pitches letter names for pitches * @returns promise, resolved when all pitches rendered * @see setPitch */ async setPitchesPromise(pitches: PitchLetter[]): Promise<any> { const self = this; const promise = new Promise((resolve: any) => { const fc = async (index: number) => { if (index >= pitches.length) { resolve(); } else { await self.setPitch(pitches[index]); fc(index + 1); } }; fc(0); }); await promise; } /** * Add a pitch to the score at the cursor. This tries to find the best pitch * to match the letter key (F vs F# for instance) based on key and surrounding notes * @param letter string */ async setPitch(letter: PitchLetter): Promise<void> { const selections = this.tracker.selections; const measureSelections = this.undoTrackerMeasureSelections('set pitch ' + letter); selections.forEach((selected) => { const selector = selected.selector; let hintSel = SmoSelection.lastNoteSelectionNonRest(this.score, selector.staff, selector.measure, selector.voice, selector.tick); if (!hintSel) { hintSel = SmoSelection.nextNoteSelectionNonRest(this.score, selector.staff, selector.measure, selector.voice, selector.tick); } // The selection no longer exists, possibly deleted if (hintSel === null || hintSel.note === null) { return PromiseHelpers.emptyPromise(); } const pitch = SmoMusic.getLetterNotePitch(hintSel.note.pitches[0], letter, hintSel.measure.keySignature); SmoOperation.setPitch(selected, [pitch]); const altSel = this._getEquivalentSelection(selected); SmoOperation.setPitch(altSel!, [pitch]); if (this.score.preferences.autoAdvance) { // Don't play the next note and the added pitch at the same time. this.tracker.deferNextAutoPlay(); this.tracker.moveSelectionRight(); } }); if (selections.length === 1 && this.score.preferences.autoPlay) { SuiOscillator.playSelectionNow(selections[0], this.score, 1); } this._renderChangedMeasures(measureSelections); await this.renderer.updatePromise(); } /** * Generic clipboard copy action */ async copy(): Promise<void> { const altAr: SmoSelection[] = []; this.tracker.selections.forEach((sel) => { const noteSelection = this._getEquivalentSel