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,104 lines (1,045 loc) 42.9 kB
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. import { Pitch, PitchLetter, Clef, getId } from '../data/common'; import { SmoMusic } from '../data/music'; import { SmoNote } from '../data/note'; import { SmoScore } from '../data/score'; import { SmoMeasureParams, SmoMeasure, SmoVoice } from '../data/measure'; import { SmoSystemStaff, SmoSystemStaffParams } from '../data/systemStaff'; import { SmoArticulation, SmoGraceNote, SmoLyric, SmoMicrotone, SmoOrnament, SmoDynamicText, SmoTabNote} from '../data/noteModifiers'; import { SmoRehearsalMark, SmoMeasureText, SmoVolta, SmoMeasureFormat, SmoTempoText, SmoBarline, TimeSignature, SmoRepeatSymbol } from '../data/measureModifiers'; import { SmoStaffHairpin, SmoSlur, SmoTie, StaffModifierBase, SmoTieParams, SmoInstrument, SmoStaffHairpinParams, SmoSlurParams, SmoInstrumentMeasure, SmoStaffTextBracket, SmoStaffTextBracketParams, SmoTabStave } from '../data/staffModifiers'; import { SmoSystemGroup } from '../data/scoreModifiers'; import { SmoTextGroup } from '../data/scoreText'; import { SmoSelection, SmoSelector, ModifierTab } from './selections'; import { SmoContractNoteActor, SmoStretchNoteActor, SmoMakeTupletActor, SmoUnmakeTupletActor, SmoStretchNoteParams, SmoContractNoteParams, SmoMakeTupletParams} from './tickDuration'; import { SmoBeamer } from './beamers'; import { SmoTupletTree } from '../data/tuplet'; /** * supported operations for {@link SmoOperation.batchSelectionOperation} to change a note's duration */ export type BatchSelectionOperation = 'dotDuration' | 'undotDuration' | 'doubleDuration' | 'halveDuration' | 'doubleGraceNoteDuration' | 'halveGraceNoteDuration'; export interface MakeTupletOperation { numNotes: number, notesOccupied: number, ratioed: boolean, bracketed: boolean, } export type createStaffModifierType<T> = (fromSelection: SmoSelection, toSelection: SmoSelection) => T; /** * SmoOperation is a collection of static methods that operate on/change/transform the music. Most methods * take the score, a selection or selection array, and the parameters of the operation. * @category SmoTransform */ export class SmoOperation { static setMeasureFormat(score: SmoScore, selection: SmoSelection, value: SmoMeasureFormat) { if (!score.formattingManager) { return; } score.staves.forEach((staff: SmoSystemStaff) => { value.formatMeasure(staff.measures[selection.selector.measure]); }); score.formattingManager.updateMeasureFormat(value); } static addKeySignature(score: SmoScore, selection: SmoSelection, keySignature: string) { score.addKeySignature(selection.selector.measure, keySignature); } static addConnectorDown(score: SmoScore, selections: SmoSelection[], parameters: SmoSystemGroup) { const msel = SmoSelection.getMeasureList(selections); const len = msel.length - 1; if (score.staves.length <= msel[len].selector.staff) { return; } const existing = score.getSystemGroupForStaff(msel[0]); if (existing && existing.endSelector.staff < selections[len].selector.staff) { existing.endSelector.staff = msel[len].selector.staff + 1; } else { parameters.startSelector = SmoSelector.default; parameters.startSelector.staff = msel[0].selector.staff; parameters.startSelector.measure = msel[0].selector.measure; parameters.endSelector = SmoSelector.default; parameters.endSelector.staff = msel[len].selector.staff + 1; parameters.endSelector.measure = msel[len].selector.measure; score.addOrReplaceSystemGroup(new SmoSystemGroup(parameters)); } } static setActiveVoice(score: SmoScore, voiceIx: number) { score.staves.forEach((staff) => { staff.measures.forEach((measure) => { measure.setActiveVoice(voiceIx); }); }); } /** * Move a single stave up or down one. If last down, move to first. * If first up, move to last * @param score * @param selection * @param index */ static moveStaffUpDown(score: SmoScore, selection: SmoSelection, index: number) { const index1 = selection.selector.staff; const index2 = selection.selector.staff + index; if (index2 < score.staves.length && index2 >= 0) { score.swapStaves(index1, index2); } else if (index2 === score.staves.length) { for (let i = 0; i < (score.staves.length - 1); ++i) { score.swapStaves((score.staves.length - 1) - i, (score.staves.length - 1) - (i + 1)); } } else if (index2 < 0) { for (let i = 0; i < (score.staves.length - 1); ++i) { score.swapStaves(i, i + 1); } } } static depopulateVoice(selection: SmoSelection, voiceIx: number) { let ix = 0; const voices: SmoVoice[] = []; const measure = selection.measure; measure.voices.forEach((voice) => { if (measure.voices.length < 2 || ix !== voiceIx) { voices.push(voice); } ix += 1; }); measure.voices = voices; if (measure.getActiveVoice() >= measure.voices.length) { measure.setActiveVoice(0); } } static populateVoice(selection: SmoSelection, voiceIx: number) { selection.measure.populateVoice(voiceIx); } static swapVoice(selections: SmoSelection[], voice1: number, voice2: number) { const measures = SmoSelection.getMeasureList(selections); measures.forEach((ss) => { ss.measure.swapVoices(voice1, voice2); }); } static setTabStave(score: SmoScore, tabStave: SmoTabStave) { score.staves[tabStave.startSelector.staff].updateTabStave(tabStave); } static removeTabStave(score: SmoScore, tabStaves: SmoTabStave[]) { if (tabStaves.length > 0) { score.staves[tabStaves[0].startSelector.staff].removeTabStaves(tabStaves); } } static setTimeSignature(score: SmoScore, selections: SmoSelection[], timeSignature: TimeSignature) { const selectors: SmoSelector[] = []; let i = 0; // change the time signature for each stave in the score selections.forEach((selection) => { for (i = 0; i < score.staves.length; ++i) { const measureSel: SmoSelector = SmoSelector.measureSelector(i, selection.selector.measure); selectors.push(measureSel); } }); selectors.forEach((selector: SmoSelector) => { const rowSelection: SmoSelection = (SmoSelection.measureSelection(score, selector.staff, selector.measure) as SmoSelection); rowSelection.measure.timeSignature = new TimeSignature(timeSignature); rowSelection.measure.alignNotesWithTimeSignature(); }); } static batchSelectionOperation(score: SmoScore, selections: SmoSelection[], operation: BatchSelectionOperation) { var measureTicks: { selector: SmoSelector, tickOffset: number }[] = []; selections.forEach((selection) => { const tm = selection.measure.tickmapForVoice(selection.selector.voice); const tickOffset = tm.durationMap[selection.selector.tick]; const selector = JSON.parse(JSON.stringify(selection.selector)); measureTicks.push({ selector, tickOffset }); }); measureTicks.forEach((measureTick) => { const selection = SmoSelection.measureSelection(score, measureTick.selector.staff, measureTick.selector.measure) as SmoSelection; const tickmap = selection.measure.tickmapForVoice(measureTick.selector.voice); const ix = tickmap.durationMap.indexOf(measureTick.tickOffset); if (ix >= 0) { const nsel = SmoSelection.noteSelection(score, measureTick.selector.staff, measureTick.selector.measure, measureTick.selector.voice, ix); (SmoOperation as any)[operation](nsel); } }); } // ## doubleDuration // ## Description // double the duration of a note in a measure, at the expense of the following // note, if possible. Works on tuplets also. static doubleDuration(selection: SmoSelection) { const note = selection.note; const newStemTicks = note!.stemTicks * 2; SmoStretchNoteActor.apply({ startIndex: selection.selector.tick, measure: selection.measure, voice: selection.selector.voice, newStemTicks: newStemTicks }); return true; } // ## halveDuration // ## Description // Replace the note with 2 notes of 1/2 duration, if possible // Works on tuplets also. static halveDuration(selection: SmoSelection) { const note = selection.note as SmoNote; let divisor = 2; const measure = selection.measure; const newStemTicks = note.stemTicks / divisor; SmoContractNoteActor.apply({ startIndex: selection.selector.tick, measure: measure, voice: selection.selector.voice, newStemTicks: newStemTicks }); SmoBeamer.applyBeams(measure); return true; } // ## makeTuplet // ## Description // Makes a non-tuplet into a tuplet of equal value. static makeTuplet(selection: SmoSelection, params: MakeTupletOperation) { SmoMakeTupletActor.apply({ measure: selection.measure, numNotes: params.numNotes, notesOccupied: params.notesOccupied, bracketed: params.bracketed, ratioed: params.ratioed, voice: selection.selector.voice, index: selection.selector.tick }); } static addStaffModifier(selection: SmoSelection, modifier: StaffModifierBase) { selection.staff.addStaffModifier(modifier); } static toggleRest(selection: SmoSelection) { selection.note?.toggleRest(); } static toggleSlash(selection: SmoSelection) { selection.note?.toggleSlash(); } static makeRest(selection: SmoSelection) { selection.note?.makeRest(); } static makeNote(selection: SmoSelection) { selection.note?.makeNote(); } static setNoteHead(selections: SmoSelection[], noteHead: string) { selections.forEach((selection: SmoSelection) => { selection.note?.setNoteHead(noteHead); }); } static addGraceNote(selection: SmoSelection, g: SmoGraceNote, offset: number) { selection.note?.addGraceNote(g, offset); } static removeGraceNote(selection: SmoSelection, offset: number) { selection.note?.removeGraceNote(offset); } static doubleGraceNoteDuration(selection: SmoSelection, modifiers: SmoGraceNote[]) { if (!Array.isArray(modifiers)) { modifiers = [modifiers]; } modifiers.forEach((mm) => { mm.ticks.numerator = mm.ticks.numerator * 2; }); } static halveGraceNoteDuration(selection: SmoSelection, modifiers: SmoGraceNote[]) { if (!Array.isArray(modifiers)) { modifiers = [modifiers]; } modifiers.forEach((mm) => { mm.ticks.numerator = mm.ticks.numerator / 2; }); } static toggleGraceNoteCourtesy(selection: any, modifiers: SmoGraceNote[]) { if (!Array.isArray(modifiers)) { modifiers = [modifiers]; } modifiers.forEach((mm: SmoGraceNote) => { mm.pitches.forEach((pitch: Pitch) => { // eslint-disable-next-line pitch.cautionary = pitch.cautionary ? false : true; }); }); } static toggleGraceNoteEnharmonic(selection: SmoSelection, modifiers: SmoGraceNote[]) { if (!Array.isArray(modifiers)) { modifiers = [modifiers]; } modifiers.forEach((mm) => { mm.pitches.forEach((pitch) => { SmoNote.toggleEnharmonic(pitch); }); }); } static transposeGraceNotes(selection: SmoSelection, modifiers: SmoGraceNote[], offset: number) { if (!Array.isArray(modifiers)) { modifiers = [modifiers]; } modifiers.forEach((mm: SmoGraceNote) => { const par: Pitch[] = []; if (!mm) { console.warn('bad modifier grace note'); return; } mm.pitches.forEach((pitch) => { par.push(SmoMusic.smoIntToPitch(SmoMusic.smoPitchToInt(pitch) + offset)); }); mm.pitches = par; }); } static slashGraceNotes(selections: ModifierTab[] | ModifierTab) { if (!Array.isArray(selections)) { selections = [selections]; } // TODO: modifiers on artifacts should be typed selections.forEach((mm: any) => { if (mm.modifier && mm.modifier.ctor === 'SmoGraceNote') { mm.modifier.slash = !mm.modifier.slash; } }); } // ## unmakeTuplet // ## Description // Makes a tuplet into a single with the duration of the whole tuplet static unmakeTuplet(selection: SmoSelection) { const selector = selection.selector; const measure = selection.measure; const tuplets = SmoTupletTree.getTupletHierarchyForNoteIndex(measure.tupletTrees, selector.voice, selector.tick); if (!tuplets.length) { return; } const tuplet = tuplets[0]; SmoUnmakeTupletActor.apply({ startIndex: tuplet.startIndex, endIndex: tuplet.endIndex, measure: measure, voice: selector.voice }); } // ## dotDuration // ## Description // Add a dot to a note, if possible, and make the note ahead of it shorter // to compensate. static dotDuration(selection: SmoSelection) { const note = selection.note as SmoNote; const measure = selection.measure; const newStemTicks = SmoMusic.getNextDottedLevel(note.stemTicks); if (newStemTicks === note.stemTicks) { return; } // Don't dot if the thing on the right of the . is too small const dotCount = SmoMusic.smoTicksToVexDots(newStemTicks); const multiplier = Math.pow(2, dotCount); const baseDot = SmoMusic.closestDurationTickLtEq(newStemTicks) / (multiplier * 2); if (baseDot <= 128) { return; } // If this is the ultimate note in the measure, we can't increase the length if (selection.selector.tick + 1 === selection.measure.voices[selection.selector.voice].notes.length) { return; } if (selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].stemTicks > note.stemTicks) { console.log('too long'); return; } // is dot too short? if (!SmoMusic.validDurations[selection.measure.voices[selection.selector.voice].notes[selection.selector.tick + 1].stemTicks / 2]) { return; } SmoStretchNoteActor.apply({ startIndex: selection.selector.tick, measure: measure, voice: selection.selector.voice, newStemTicks: newStemTicks }); } // ## undotDuration // ## Description // Add the value of the last dot to the note, increasing length and // reducing the number of dots. static undotDuration(selection: SmoSelection) { const note = selection.note as SmoNote; const measure = selection.measure; const newStemTicks = SmoMusic.getPreviousDottedLevel(note.stemTicks); if (newStemTicks === note.stemTicks) { return; } SmoContractNoteActor.apply({ startIndex: selection.selector.tick, measure: measure, voice: selection.selector.voice, newStemTicks: newStemTicks }); } static transposeScore(score: SmoScore, offset: number) { score.staves.forEach((staff, staffIx) => { staff.measures.forEach((measure, measureIx) => { measure.voices.forEach((voice, voiceIx) => { voice.notes.forEach((note, tickIx) => { const selection = SmoSelection.noteSelection( score,staffIx, measureIx, voiceIx, tickIx); if (selection) { this.transpose(selection, offset); } }); }); }); }); } static updateTabNote(selections: SmoSelection[], tabNote: SmoTabNote) { selections.forEach((sel) => { if (sel.note) { sel.note.setTabNote(tabNote); } }); } static removeTabNote(selections: SmoSelection[]) { selections.forEach((sel) => { if (sel.note) { sel.note.clearTabNote(); } }); } // ## transpose // ## Description // Transpose the selected note, trying to find a key-signature friendly value static transpose(selection: SmoSelection, offset: number) { let trans: Pitch; let transInt: number = 0; let i: number = 0; if (typeof (selection.selector.pitches) === 'undefined') { selection.selector.pitches = []; } const measure = selection.measure; const note = selection.note; if (measure && note) { const pitchar: Pitch[] = []; const tabStave: SmoTabStave | undefined = selection.staff.getTabStaveForMeasure(selection.selector); note.pitches.forEach((opitch, pitchIx) => { // Only translate selected pitches const shouldXpose = selection.selector.pitches.length === 0 || selection.selector.pitches.indexOf(pitchIx) >= 0; // Translate the pitch, ignoring enharmonic trans = shouldXpose ? SmoMusic.getKeyOffset(opitch, offset) : JSON.parse(JSON.stringify(opitch)); if (shouldXpose) { trans = SmoMusic.getEnharmonicInKey(trans, measure.keySignature); if (!trans.accidental) { trans.accidental = 'n'; } transInt = SmoMusic.smoPitchToInt(trans); // Look through the earlier notes in the measure and try // to find an equivalent note, and convert it if it exists. measure.voices.forEach((voice) => { for (i = 0; i < selection.selector.tick && i < voice.notes.length; ++i) { const prevNote = voice.notes[i]; // eslint-disable-next-line prevNote.pitches.forEach((prevPitch: Pitch) => { const prevInt = SmoMusic.smoPitchToInt(prevPitch); if (prevInt === transInt) { trans = JSON.parse(JSON.stringify(prevPitch)); } }); } }); } pitchar.push(trans as Pitch); }); note.pitches = pitchar; // If this note has a tab stave, try to preserve the assigned string. // If not possible, find the default string/fret for the note if (note.tabNote) { note.tabNote.positions.forEach((pp, ix) => { if (pp.fret + offset > 0) { pp.fret = pp.fret + offset; } else if (tabStave && note.pitches.length > ix) { const position = SmoTabStave.getDefaultPositionForStaff(note.pitches[ix], tabStave.stringPitches, offset); pp.fret = position.fret; pp.string = position.string; } else { pp.fret = 0; } }); } return true; } return false; } // ## setPitch // ## Description: // pitches can be either an array, a single pitch, or a letter. In the latter case, // the letter value appropriate for the key signature is used, e.g. c in A major becomes // c# static setPitch(selection: SmoSelection, pitches: Pitch[]) { let i = 0; const measure = selection.measure; const note = selection.note as SmoNote; if (typeof (note) === 'undefined') { console.warn('set Pitch on invalid note'); return; } note.makeNote(); // TODO allow hint for octave const octave = note.pitches[0].octave; note.pitches = []; if (!Array.isArray(pitches)) { pitches = [pitches]; } const earlierAccidental = (pitch: Pitch) => { selection.measure.voices.forEach((voice) => { for (i = 0; i < selection.selector.tick && i < voice.notes.length; ++i) { const prevNote = voice.notes[i]; if (prevNote === null || prevNote.pitches === null) { console.log('this will die null'); } prevNote.pitches.forEach((prevPitch: Pitch) => { if (prevNote.noteType === 'n' && prevPitch.letter === pitch.letter) { pitch.accidental = prevPitch.accidental; } }); } }); }; pitches.forEach((pitch) => { if (typeof (pitch) === 'string') { const letter = SmoMusic.getKeySignatureKey(pitch[0], measure.keySignature); pitch = { letter: letter[0] as PitchLetter, accidental: letter.length > 1 ? letter.substring(1) : '', octave }; } earlierAccidental(pitch); note.pitches.push(pitch); }); } static toggleCourtesyAccidental(selection: SmoSelection) { let toBe: boolean = false; const note = selection.note as SmoNote; if (!selection.selector.pitches || selection.selector.pitches.length === 0) { const ps: Pitch[] = []; note.pitches.forEach((pitch) => { const p = JSON.parse(JSON.stringify(pitch)); ps.push(p); p.cautionary = !(pitch.cautionary); }); note.pitches = ps; } else { toBe = !(note.pitches[selection.selector.pitches[0]].cautionary); } SmoOperation.courtesyAccidental(selection, toBe); } static courtesyAccidental(pitchSelection: SmoSelection, toBe: boolean) { pitchSelection.selector.pitches.forEach((pitchIx) => { (pitchSelection.note as SmoNote).pitches[pitchIx].cautionary = toBe; }); } static toggleEnharmonic(pitchSelection: SmoSelection) { if (pitchSelection.selector.pitches.length === 0) { pitchSelection.selector.pitches.push(0); } const pitch = (pitchSelection.note as SmoNote).pitches[pitchSelection.selector.pitches[0]]; SmoNote.toggleEnharmonic(pitch); } static addDynamic(selection: SmoSelection, dynamic: SmoDynamicText) { (selection.note as SmoNote).addDynamic(dynamic); } static removeDynamic(selection: SmoSelection, dynamic: SmoDynamicText) { (selection.note as SmoNote).removeDynamic(dynamic); } static unbeamSelections(noteSelection: SmoSelection) { if (!noteSelection.note) { return; } noteSelection.note.beamState = SmoNote.beamStates.end; } static beamSelections(score: SmoScore, selections: SmoSelection[]) { const start = selections[0].selector; let cur = selections[0].selector; const beamGroup: SmoNote[] = []; let ticks = 0; selections.forEach((selection) => { const note = selection.note as SmoNote; if (SmoSelector.sameNote(start, selection.selector) || (SmoSelector.sameMeasure(selection.selector, cur) && cur.tick === selection.selector.tick - 1)) { ticks += note.tickCount; cur = selection.selector; beamGroup.push(note); } }); if (beamGroup.length) { beamGroup.forEach((note) => { note.beamBeats = ticks; if (note.beamState !== SmoNote.beamStates.continue) { note.beamState = SmoNote.beamStates.auto; } }); const sn = beamGroup[beamGroup.length - 1]; sn.beamState = SmoNote.beamStates.end; // Make sure the last note of the previous beam is the end of this beam group. if (selections[0].selector.tick > 0) { const ps = JSON.parse(JSON.stringify(selections[0].selector)); ps.tick -= 1; const previous: SmoSelection | null = SmoSelection.noteFromSelector(score, ps); if (previous?.note && previous.note.tickCount < 4096) { previous.note.beamState = SmoNote.beamStates.end; } } } } static clearAllBeamGroups(score: SmoScore) { score.staves.forEach((ss) => { ss.measures.forEach((mm) => { mm.voices.forEach((vv) => { const triple = mm.timeSignature.actualBeats % 3 === 0; vv.notes.forEach((note) => { note.beamBeats = triple ? score.preferences.defaultTripleDuration : score.preferences.defaultDupleDuration; note.endBeam = false; }); }); }); }); } static clearBeamGroups(score: SmoScore, selections: SmoSelection[]) { selections.forEach((ss) => { if (ss.note) { const triple = ss.measure.timeSignature.actualBeats % 3 === 0; const note = ss.note; note.beamBeats = triple ? score.preferences.defaultTripleDuration : score.preferences.defaultDupleDuration; note.beamState = SmoNote.beamStates.end; } }); } static toggleBeamDirection(selections: SmoSelection[]) { const note0 = selections[0].note as SmoNote; note0.toggleFlagState(); selections.forEach((selection) => { const note = selection.note as SmoNote; note.flagState = note0.flagState; }); } static addEnding(score: SmoScore, parameters: SmoVolta) { let m = 0; let s = 0; const startMeasure = parameters.startBar; const endMeasure = parameters.endBar; // Ending ID ties all the instances of an ending across staves parameters.endingId = getId().toString(); score.staves.forEach((staff) => { m = 0; staff.measures.forEach((measure) => { if (m === startMeasure) { const pp = JSON.parse(JSON.stringify(parameters)); pp.startSelector = { staff: s, measure: startMeasure }; pp.endSelector = { staff: s, measure: endMeasure }; const ending = new SmoVolta(pp); measure.addNthEnding(ending); } m += 1; }); s += 1; }); } static removeEnding(score: SmoScore, ending: SmoVolta) { let i = 0; score.staves.forEach((staff) => { // bug // Due to deleted measures, volta might not match up so look through all measures. for (i = 0; i < staff.measures.length; ++i) { staff.measures[i].removeNthEnding(ending); } }); } static addTextGroup(score: SmoScore, textGroup: SmoTextGroup) { score.addTextGroup(textGroup); } static removeTextGroup(score: SmoScore, textGroup: SmoTextGroup) { score.removeTextGroup(textGroup); } static addMeasureText(score: SmoScore, selection: SmoSelection, measureText: SmoMeasureText) { const current = selection.measure.getMeasureText(); // TODO: should we allow multiples per position current.forEach((mod) => { selection.measure.removeMeasureText(mod.attrs.id); }); selection.measure.addMeasureText(measureText); } static removeMeasureText(score: SmoScore, selection: SmoSelection, mt: SmoMeasureText) { selection.measure.removeMeasureText(mt.attrs.id); } static removeRehearsalMark(score: SmoScore, selection: SmoSelection) { score.staves.forEach((staff) => { staff.removeRehearsalMark(selection.selector.measure); }); } static addRehearsalMark(score: SmoScore, selection: SmoSelection, rehearsalMark: SmoRehearsalMark) { score.staves.forEach((staff) => { const mt = new SmoRehearsalMark(rehearsalMark.serialize()); staff.addRehearsalMark(selection.selector.measure, mt); }); } static addTempo(score: SmoScore, selection: SmoSelection, tempo: SmoTempoText) { score.staves.forEach((staff) => { staff.addTempo(tempo, selection.selector.measure); }); } static setMeasureBarline(score: SmoScore, selection: SmoSelection, barline: SmoBarline) { const mm = selection.selector.measure; let ix = 0; score.staves.forEach(() => { const s2: SmoSelection | null = SmoSelection.measureSelection(score, ix, mm); s2?.measure.setBarline(barline); ix += 1; }); } static setRepeatSymbol(score: SmoScore, selection: SmoSelection, sym: SmoRepeatSymbol) { let ix = 0; const mm = selection.selector.measure; score.staves.forEach(() => { const s2 = SmoSelection.measureSelection(score, ix, mm); s2?.measure.setRepeatSymbol(sym); ix += 1; }); } // ## interval // Add a pitch at the specified interval to the chord in the selection. static interval(selection: SmoSelection, interval: number) { const measure = selection.measure; const note = selection.note as SmoNote; let pitch: Pitch = {} as Pitch; // TODO: figure out which pitch is selected pitch = note.pitches[0]; if (interval > 0) { pitch = note.pitches[note.pitches.length - 1]; } pitch = SmoMusic.getIntervalInKey(pitch, measure.keySignature, interval); if (pitch) { note.pitches.push(pitch); note.pitches.sort((x, y) => SmoMusic.smoPitchToInt(x) - SmoMusic.smoPitchToInt(y) ); return true; } return false; } static addOrReplaceBracket(modifier: SmoStaffTextBracket, fromSelection: SmoSelection, toSelection: SmoSelection) { fromSelection.staff.addTextBracket(modifier); } static createRitardBracket(fromSelection: SmoSelection, toSelection: SmoSelection) { const params: SmoStaffTextBracketParams = SmoStaffTextBracket.defaults; params.startSelector = JSON.parse(JSON.stringify(fromSelection.selector)); params.endSelector = JSON.parse(JSON.stringify(toSelection.selector)); params.text = SmoStaffTextBracket.RITARD; const modifier = new SmoStaffTextBracket(params); return modifier; } static createAccelerandoBracket(fromSelection: SmoSelection, toSelection: SmoSelection) { const params: SmoStaffTextBracketParams = SmoStaffTextBracket.defaults; params.startSelector = JSON.parse(JSON.stringify(fromSelection.selector)); params.endSelector = JSON.parse(JSON.stringify(toSelection.selector)); params.text = SmoStaffTextBracket.ACCEL; const modifier = new SmoStaffTextBracket(params); return modifier; } static createCrescendoBracket(fromSelection: SmoSelection, toSelection: SmoSelection) { const params: SmoStaffTextBracketParams = SmoStaffTextBracket.defaults; params.startSelector = JSON.parse(JSON.stringify(fromSelection.selector)); params.endSelector = JSON.parse(JSON.stringify(toSelection.selector)); params.text = SmoStaffTextBracket.CRESCENDO; const modifier = new SmoStaffTextBracket(params); return modifier; } static createDimenuendoBracket(fromSelection: SmoSelection, toSelection: SmoSelection) { const params: SmoStaffTextBracketParams = SmoStaffTextBracket.defaults; params.startSelector = JSON.parse(JSON.stringify(fromSelection.selector)); params.endSelector = JSON.parse(JSON.stringify(toSelection.selector)); params.text = SmoStaffTextBracket.CRESCENDO; const modifier = new SmoStaffTextBracket(params); return modifier; } static createCrescendo(fromSelection: SmoSelection, toSelection: SmoSelection) { const params: SmoStaffHairpinParams = SmoStaffHairpin.defaults; params.startSelector = JSON.parse(JSON.stringify(fromSelection.selector)); params.endSelector = JSON.parse(JSON.stringify(toSelection.selector)); params.hairpinType = SmoStaffHairpin.types.CRESCENDO; const modifier = new SmoStaffHairpin(params); return modifier; } static createDecrescendo(fromSelection: SmoSelection, toSelection: SmoSelection) { const params: SmoStaffHairpinParams = SmoStaffHairpin.defaults; params.startSelector = JSON.parse(JSON.stringify(fromSelection.selector)); params.endSelector = JSON.parse(JSON.stringify(toSelection.selector)); params.hairpinType = SmoStaffHairpin.types.DECRESCENDO; const modifier = new SmoStaffHairpin(params); return modifier; } static createTie(fromSelection: SmoSelection, toSelection: SmoSelection) { // By default, just tie all the pitches to all the other pitches in order const lines = SmoTie.createLines(fromSelection.note as SmoNote, toSelection.note as SmoNote); const params: SmoTieParams = SmoTie.defaults; params.startSelector = fromSelection.selector; params.endSelector = toSelection.selector; params.lines = lines; const modifier = new SmoTie(params); return modifier; } static getSlurDefaultParameters(selections: SmoSelection[]) { const lastIndex = selections.length - 1; const note1 = selections[0].note; const note2 = selections[lastIndex].note; const inners = []; let minLine = -1; let maxLine = 0; if (selections.length > 2) { for (var i = 1; i < selections.length - 1; ++i) { inners.push(selections[i]); } } if (note1 === null || note2 === null) { throw('no note in slur selections'); } } /** * Heuristically determine how a slur should be formatted based on the notes. Determine control points, * offset, and alignment * * ## Note: Vexflow slurs consider `top` to mean the furthest point from the note head, which could be the top * or the bottom of the note. It also considers yoffset to be negative if inverted is set. Head means close to the * note head. * @param score * @param fromSelection * @param toSelection * @returns */ static getDefaultSlurDirection(score: SmoScore, fromSelector: SmoSelector, toSelector: SmoSelector):SmoSlurParams { const params: SmoSlurParams = SmoSlur.defaults; const sels = SmoSelector.order(fromSelector, toSelector); params.startSelector = JSON.parse(JSON.stringify(sels[0])); params.endSelector = JSON.parse(JSON.stringify(sels[1])); const fromSelection = SmoSelection.noteFromSelector(score, fromSelector); if (!fromSelection) { return params; } // Get all selections within the slur const selections = SmoSelection.innerSelections(score, sels[0], sels[1]).filter((ff) => ff.selector.voice === fromSelection.selector.voice); const dirs: Record<number, boolean> = {}; const beamGroups: Record<string, boolean> = {}; let startDir = SmoNote.flagStates.up; let mixed = false; let endDir = SmoNote.flagStates.up; let firstGap = 0; let lastGap = 0; if (selections.length < 1) { return new SmoSlur(params); } selections.forEach((selection, selectionIx) => { const note = selection.note!; if (note.beam_group) { beamGroups[note.beam_group.id] = true; } else { beamGroups[note.attrs.id] = true; } // Find the gap between the first and second note, and also between last 2. If they are far apart, // increase the control points so the slurs don't run into the notes if (selectionIx === 1) { const lastNote = selections[0].note!; firstGap = Math.abs(SmoMusic.pitchToStaffLine(note.clef as Clef, note.pitches[0]) - SmoMusic.pitchToStaffLine(lastNote.clef as Clef, lastNote.pitches[0])); } if (selectionIx === selections.length - 2 && selections.length > 2) { const nextNote = selections[selectionIx + 1].note!; lastGap = Math.abs(SmoMusic.pitchToStaffLine(note.clef as Clef, note.pitches[0]) - SmoMusic.pitchToStaffLine(nextNote.clef as Clef, nextNote.pitches[0])); } const fstate = SmoMusic.flagStateFromNote(note.clef as Clef, note); // Keep track of the number of stem directions, so we can determine if the flags are mixed direction // the rules are a little different for mixed - we always try to put the slur on (the real) top of the staff. dirs[fstate] = true; if (selectionIx === 0) { startDir = fstate; } if (selectionIx === selections.length - 1) { endDir = fstate; } }); params.orientation = SmoSlur.orientations.AUTO; params.position = SmoSlur.positions.AUTO; params.position_end = SmoSlur.positions.AUTO; mixed = Object.keys(dirs).length > 1; // If the notes are beamed together, we assume the beams point in the same direction if (mixed) { // special case: slur 2 notes, note heads close, connect the note heads // to keep a flat arc if (selections.length === 2 && firstGap < 3) { params.xOffset = 5; } else { if (firstGap >= 3 || lastGap >= 3) { params.cp1y = 45; params.cp2y = 45; } } } else { if (firstGap >= 2 || lastGap >= 2) { params.cp1y = 45; params.cp2y = 45; params.yOffset += 10; } else { params.yOffset += 10; } } if (selections.length === 2) { params.xOffset = 0; } return params; } static createSlur(score: SmoScore, fromSelection: SmoSelection, toSelection: SmoSelection): SmoSlur { const params = SmoOperation.getDefaultSlurDirection(score, fromSelection.selector, toSelection.selector); const modifier: SmoSlur = new SmoSlur(params); return modifier; } static addStaff(score: SmoScore, parameters: SmoSystemStaffParams): SmoSystemStaff { return score.addStaff(parameters); } static removeStaff(score: SmoScore, index: number) { score.removeStaff(index); } static transposeChords(smoNote: SmoNote, offset: number, key: string) { const chords = smoNote.getModifiers('SmoLyric'); chords.forEach((ll) => { const lyric = ll as SmoLyric; if (lyric.parser === SmoLyric.parsers.chord) { const tx = lyric.getText(); // Look for something that looks like a key name if (tx.length >= 1 && (tx[0].toUpperCase() >= 'A' && tx[0].toUpperCase() <= 'G')) { // toffset is 2 if the key has b or # in it let toffset = 1; let newText = tx[0]; if (tx.length > 0 && tx[1] === 'b' || tx[1] === '#') { newText += tx[1]; toffset = 2; } // Transpose the key, as if it were a key signature (octave has no meaning) let nkey = SmoMusic.smoIntToPitch(SmoMusic.smoPitchToInt( SmoMusic.pitchKeyToPitch(SmoMusic.vexToSmoKey(newText))) + offset); nkey = JSON.parse(JSON.stringify(SmoMusic.getEnharmonicInKey(nkey, key))); newText = nkey.letter.toUpperCase(); // new key may have different length, e.g. Bb to B natural if (nkey.accidental !== 'n') { newText += nkey.accidental; } newText += tx.substr(toffset, tx.length - toffset); lyric.setText(newText); } } }); } /** * Compute new map based on current instrument selections, adjusting existing instruments as required * @param instrument * @param selections */ static changeInstrument(instrument: SmoInstrument, selections: SmoSelection[]) { selections[0].staff.consolidateInstruments(); const measureSel = SmoSelection.getMeasureList(selections); const measureIndex = measureSel[0].selector.measure; const measureEnd = measureIndex + (measureSel.length - 1); instrument.startSelector = JSON.parse(JSON.stringify(measureSel[0].selector)); instrument.endSelector = JSON.parse(JSON.stringify(measureSel[measureSel.length - 1].selector)); const instMap: Record<number, SmoInstrument> = {}; const staffArray: SmoInstrumentMeasure[] = SmoSystemStaff.getStaffInstrumentArray(measureSel[0].staff.measureInstrumentMap); instMap[measureIndex] = instrument; staffArray.forEach((ar) => { if (ar.instrument.endSelector.measure < measureIndex || ar.instrument.startSelector.measure > measureEnd) { // No overlap, juse use the original instrument instMap[ar.instrument.startSelector.measure] = new SmoInstrument(ar.instrument); } else if (ar.instrument.startSelector.measure < measureIndex) { // overlap on left const split1 = new SmoInstrument(ar.instrument); split1.startSelector.measure = ar.instrument.startSelector.measure; instMap[split1.startSelector.measure] = split1; split1.endSelector.measure = measureIndex - 1; if (ar.instrument.endSelector.measure > measureEnd) { // overlap on left and right const split2 = new SmoInstrument(ar.instrument); split2.startSelector.measure = measureEnd + 1; split2.endSelector.measure = ar.instrument.endSelector.measure; instMap[split2.startSelector.measure] = split2; } instMap[ar.instrument.startSelector.measure] = new SmoInstrument(ar.instrument); } else if (ar.instrument.endSelector.measure > measureEnd) { // overlap on right only const split1 = new SmoInstrument(ar.instrument); split1.startSelector.measure = measureEnd + 1; instMap[split1.startSelector.measure] = split1; } }); selections[0].staff.measureInstrumentMap = instMap; selections[0].staff.updateInstrumentOffsets(); } static computeMultipartRest(score: SmoScore) { let i = 0; let j = 0; const measureRanges: Record<number, number> = {}; const measureCount = score.staves[0].measures.length; if (score.staves[0].partInfo.expandMultimeasureRests === true) { return; } while (i < measureCount) { let forceRest = score.staves[0].measures[i].format.forceRest; if (score.isMultimeasureRest(i, true, forceRest)) { for (j = i + 1; j < measureCount; ++j) { const restBreak = score.staves[0].measures[j].format.restBreak; forceRest = score.staves[0].measures[j].format.forceRest; if (!score.isMultimeasureRest(j, false, forceRest) || restBreak) { break; } } if (j - i >= 2) { measureRanges[i] = j; } i = j; } else { const startMeasure = i; score.staves.forEach((staff) => { staff.measures[startMeasure].svg.hideMultimeasure = false; }); i += 1; } } const multiKeys = Object.keys(measureRanges).map((x) => parseInt(x, 10)); multiKeys.forEach((key) => { const endMeasure = measureRanges[key]; score.staves.forEach((staff) => { const mmLength = endMeasure - key; const svg = staff.measures[key].svg; svg.multimeasureLength = mmLength; if (svg.multimeasureLength > 1) { svg.multimeasureEndBarline = staff.measures[endMeasure - 1].getEndBarline().barline; } staff.measures[key].svg.hideMultimeasure = false; for (i = key + 1; i < endMeasure; ++i) { staff.measures[i].svg.hideMultimeasure = true; } }); }); } }