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

665 lines (628 loc) 27.4 kB
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. import { SmoSelection, SmoSelector } from './selections'; import { SmoNote } from '../data/note'; import { SmoMeasure, SmoVoice } from '../data/measure'; import { StaffModifierBase } from '../data/staffModifiers'; import { SmoLyric } from '../data/noteModifiers'; import {SmoTuplet, SmoTupletTree, SmoTupletTreeParams} from '../data/tuplet'; import { SmoMusic } from '../data/music'; import { SvgHelpers } from '../../render/sui/svgHelpers'; import { SmoScore } from '../data/score'; import { TickMap } from './tickMap'; import { SmoSystemStaff } from '../data/systemStaff'; import { getId } from '../data/common'; import {SmoUnmakeTupletActor} from "./tickDuration"; import { SmoTempoText, TimeSignature } from '../data/measureModifiers'; /** * Used to calculate the offset and transposition of a note to be pasted * @category SmoTransform */ export interface PasteNote { note: SmoNote, selector: SmoSelector, originalKey: string, tupletStart: SmoTupletTree | null } /** * Used when pasting staff modifiers like slurs to calculate the * offset * @category SmoTransform */ export interface ModifierPlacement { modifier: StaffModifierBase, ticksToStart: number } /** * PasteBuffer holds copied music, and handles the action of pasting the music to * a different point in the score. It does this by serializing the measure(s) from the source * and then creating handling the overlap with existing music when deserializaing it. * @category SmoTransform */ export class PasteBuffer { notes: PasteNote[]; noteIndex: number; measures: SmoMeasure[]; measureIndex: number; remainder: number; replacementMeasures: SmoSelection[]; score: SmoScore | null = null; modifiers: StaffModifierBase[] = []; modifiersToPlace: ModifierPlacement[] = []; destination: SmoSelector = SmoSelector.default; staffSelectors: SmoSelector[] = []; constructor() { this.notes = []; this.noteIndex = 0; this.measures = []; this.measureIndex = -1; this.remainder = 0; this.replacementMeasures = []; } setScore(score: SmoScore) { this.score = score; } getCopyBufferTickCount() { let rv = 0; this.notes.forEach((note) => { rv += note.note.tickCount; }); return rv; } setSelections(score: SmoScore, selections: SmoSelection[]) { this.notes = []; this.noteIndex = 0; this.score = score; if (selections.length < 1) { return; } // this.tupletNoteMap = []; const first = selections[0]; const last = selections[selections.length - 1]; if (!first.note || !last.note) { return; } const startTupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(first.measure.tupletTrees, first.selector.voice, first.selector.tick); if (startTupletTree) { if (startTupletTree.startIndex !== first.selector.tick) { return; // can't copy from the middle of a tuplet } } const endTupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(last.measure.tupletTrees, last.selector.voice, last.selector.tick); if (endTupletTree) { if (endTupletTree.endIndex !== last.selector.tick) { return; // can't copy part of a tuplet. } } this._populateSelectArray(selections); } // ### _populateSelectArray // copy the selected notes into the paste buffer with their original locations. _populateSelectArray(selections: SmoSelection[]) { let selector: SmoSelector = SmoSelector.default; this.modifiers = []; let maxSelector = selections[0].selector; let minSelector = selections[0].selector; selections.forEach((selection) => { selector = JSON.parse(JSON.stringify(selection.selector)); if (SmoSelector.gt(selector, maxSelector)) { maxSelector = selector; } if (SmoSelector.lt(selector, minSelector)) { minSelector = selector; } const mod: StaffModifierBase[] = selection.staff.getModifiersAt(selector); if (mod.length) { mod.forEach((modifier: StaffModifierBase) => { const cp: StaffModifierBase = StaffModifierBase.deserialize(modifier.serialize()); cp.attrs.id = getId().toString(); this.modifiers.push(cp); }); } if (selection.note) { // We store copy in concert pitch. The originalKey is the original key of the copy. // the destKey is the originalKey in concert pitch. const originalKey = selection.measure.keySignature; const keyOffset = -1 * selection.measure.transposeIndex; const destKey = SmoMusic.vexKeySignatureTranspose(originalKey, keyOffset).toLocaleLowerCase(); const note = SmoNote.transpose(SmoNote.clone(selection.note),[], keyOffset, selection.measure.keySignature, destKey) as SmoNote; const chords: SmoLyric[] = note.getChords(); chords.forEach((chord) => { note.removeLyric(chord); }); chords.forEach((chord) => { const nchord = SmoLyric.transposeChordToKey(chord, keyOffset, selection.measure.keySignature, destKey); note.addLyric(nchord); }); const pasteNote: PasteNote = { selector, note, originalKey: destKey, tupletStart: null }; if (selection.note.isTuplet) { const tupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(selection.measure.tupletTrees, selection.selector.voice, selection.selector.tick); //const index = tuplet.getIndexOfNote(selection.note); if (tupletTree && tupletTree.startIndex === selection.selector.tick) { pasteNote.tupletStart = SmoTupletTree.clone(tupletTree); } } this.notes.push(pasteNote); } }); this.notes.sort((a, b) => SmoSelector.gt(a.selector, b.selector) ? 1 : -1 ); } clearSelections() { this.notes = []; } _findModifier(selector: SmoSelector) { const rv = this.modifiers.filter((mod) => SmoSelector.eq(selector, mod.startSelector)); return (rv && rv.length) ? rv[0] : null; } _findPlacedModifier(selector: SmoSelector) { const rv = this.modifiers.filter((mod) => SmoSelector.eq(selector, mod.endSelector)); return (typeof(rv) !== 'undefined' && rv.length) ? rv[0] : null; } _alignVoices(measure: SmoMeasure, voiceIndex: number) { while (measure.voices.length <= voiceIndex) { measure.populateVoice(measure.voices.length); } } // Before pasting, populate an array of existing measures from the paste destination // so we know how to place the notes. _populateMeasureArray(selector: SmoSelector) { let measureSelection = SmoSelection.measureSelection(this.score!, selector.staff, selector.measure); if (!measureSelection) { return; } const measure = measureSelection.measure; this._alignVoices(measure, selector.voice); this.measures = []; this.staffSelectors = []; const clonedMeasure = SmoMeasure.cloneForPasteOrUndo(measureSelection.measure); clonedMeasure.svg = measureSelection.measure.svg; // Ordinarily, the key/tempo/time is mapped to the stave, but since we are pasting measure-by // measure here, we want to preserve it. clonedMeasure.keySignature = measureSelection.measure.keySignature; clonedMeasure.timeSignature = new TimeSignature(measureSelection.measure.timeSignature); clonedMeasure.tempo = new SmoTempoText(measureSelection.measure.tempo); this.measures.push(clonedMeasure); const firstMeasure = this.measures[0]; const tickmapForFirstMeasure = firstMeasure.tickmapForVoice(selector.voice); let currentDuration = tickmapForFirstMeasure.durationMap[selector.tick]; const measureTotalDuration = tickmapForFirstMeasure.totalDuration; for (let i: number = 0; i < this.notes.length; i++) { const selection: PasteNote = this.notes[i]; if (selection.tupletStart) { // const tupletTree: SmoTupletTree | null = SmoTupletTree.getTupletTreeForNoteIndex(this.tupletNoteMap, selection.selector.voice, selection.selector.tick); if (currentDuration + selection.tupletStart.totalTicks > measureTotalDuration && measureSelection !== null) { //if tuplet does not fit in a measure as a whole we cannot paste it, it is ether the whole thing or nothing //reset everything that has been changed so far and return this.measures = []; this.staffSelectors = []; return; } } if (currentDuration + selection.note.tickCount > measureTotalDuration && measureSelection !== null) { // If this note will overlap the measure boundary, the note will be split in 2 with the // remainder going to the next measure. If they line up exactly, the remainder is 0. const remainder = (currentDuration + selection.note.tickCount) - measureTotalDuration; currentDuration = remainder; measureSelection = SmoSelection.measureSelection(this.score as SmoScore, measureSelection.selector.staff,measureSelection.selector.measure + 1); // If the paste buffer overlaps the end of the score, we can't paste (TODO: add a measure in this case) if (measureSelection != null) { const clonedMeasure = SmoMeasure.cloneForPasteOrUndo(measureSelection.measure); clonedMeasure.svg = measureSelection.measure.svg; this.measures.push(clonedMeasure); // firstMeasureTickmap = measureSelection.measure.tickmapForVoice(selector.voice); } } else if (measureSelection != null) { currentDuration += selection.note.tickCount; } } const lastMeasure = this.measures[this.measures.length - 1]; //adjust the beginning of the paste //adjust this.destination if beginning of the paste is in the middle of a tuplet //set destination to have a tick index of the first note in the tuplet this.destination = selector; const firstTupletTree = SmoTupletTree.getTupletForNoteIndex(firstMeasure.tupletTrees, selector.voice, selector.tick); if (firstTupletTree) { this.destination.tick = firstTupletTree.startIndex;//use this as a new selector.tick } if (this.measures.length > 1) { this._removeOverlappingTuplets(firstMeasure, selector.tick, firstMeasure.voices[selector.voice].notes.length - 1, selector.voice); this._removeOverlappingTuplets(lastMeasure, 0, lastMeasure.getClosestIndexFromTickCount(selector.voice, currentDuration), selector.voice); } else { this._removeOverlappingTuplets(firstMeasure, selector.tick, lastMeasure.getClosestIndexFromTickCount(selector.voice, currentDuration), selector.voice); } //if there are more than 2 measures remove tuplets from all but first and last measure. if (this.measures.length > 2) { for(let i = 1; i < this.measures.length - 2; i++) { this.measures[i].tupletTrees = []; } } } // ### _populatePre // When we paste, we replace entire measures. Populate the first measure up until the start of pasting. _populatePre(voiceIndex: number, measure: SmoMeasure, startTick: number, tickmap: TickMap) { const voice: SmoVoice = { notes: [] }; for (let i = 0; i < startTick; i++) { const note = measure.voices[voiceIndex].notes[i]; voice.notes.push(SmoNote.clone(note)); } return voice; } /** * * @param voiceIndex */ // ### _populateVoice // ### Description: // Create a new voice for a new measure in the paste destination _populateVoice(): SmoVoice[] { // this._populateMeasureArray(); const measures = this.measures; let measure = measures[0]; let tickmap = measure.tickmapForVoice(this.destination.voice); let voice = this._populatePre(this.destination.voice, measure, this.destination.tick, tickmap); let startSelector = JSON.parse(JSON.stringify(this.destination)); this.measureIndex = 0; const measureVoices = []; measureVoices.push(voice); while (this.measureIndex < measures.length) { measure = measures[this.measureIndex]; while (measure.voices.length <= this.destination.voice) { const nvoice = { notes : SmoMeasure.getDefaultNotes(measure) }; measure.voices.push(nvoice); } tickmap = measure.tickmapForVoice(this.destination.voice); this._populateNew(voice, measure, tickmap, startSelector); if (this.noteIndex < this.notes.length && this.measureIndex < measures.length) { voice = { notes: [] }; measureVoices.push(voice); startSelector = { staff: startSelector.staff, measure: startSelector.measure, voice: this.destination.voice, tick: 0 }; this.measureIndex += 1; startSelector.measure += 1; } else { break; } } this._populatePost(voice, this.destination.voice, measure, tickmap); return measureVoices; } static _countTicks(voice: SmoVoice): number { let voiceTicks = 0; voice.notes.forEach((note) => { voiceTicks += note.tickCount; }); return voiceTicks; } /** * If the source contains a staff modifier that ends on the source selection, copy the modifier * @param srcSelector * @param destSelector * @param staff * @returns */ _populateModifier(srcSelector: SmoSelector, destSelector: SmoSelector, staff: SmoSystemStaff) { const mod = this._findPlacedModifier(srcSelector); if (mod && this.score) { // Don't copy modifiers that cross staff boundaries outside the source staff b/c it's not clear what // the dest staff should be if (mod.startSelector.staff !== mod.endSelector.staff && srcSelector.staff !== destSelector.staff) { return; } const repl = StaffModifierBase.deserialize(mod.serialize()); repl.endSelector = JSON.parse(JSON.stringify(destSelector)); const tickOffset = SmoSelection.countTicks(this.score, mod.startSelector, mod.endSelector); this.modifiersToPlace.push({ modifier: repl, ticksToStart: tickOffset }); } } /** * * @param measure * @param startIndex * @param endIndex * @param voiceIndex * @private */ private _removeOverlappingTuplets(measure: SmoMeasure, startIndex: number, endIndex: number, voiceIndex: number): void { const tupletsToDelete: SmoTupletTree[] = []; for (let i = 0; i < measure.tupletTrees.length; ++i) { const tupletTree = measure.tupletTrees[i]; if (startIndex >= tupletTree.startIndex && startIndex <= tupletTree.endIndex) { tupletsToDelete.push(tupletTree); break; } if (endIndex >= tupletTree.startIndex && endIndex <= tupletTree.endIndex) { tupletsToDelete.push(tupletTree); break; } } //todo: check if we need to remove tuplets in descending order for (let i: number = 0; i < tupletsToDelete.length; i++) { const tupletTree: SmoTupletTree = tupletsToDelete[i]; SmoUnmakeTupletActor.apply({ startIndex: tupletTree.startIndex, endIndex: tupletTree.endIndex, measure: measure, voice: voiceIndex }); } } /** * Start copying the paste buffer into the destination by copying the notes and working out * the measure overlap * * @param voice * @param measure * @param tickmap * @param startSelector * @returns */ _populateNew(voice: SmoVoice, measure: SmoMeasure, tickmap: TickMap, startSelector: SmoSelector) { let currentDuration = tickmap.durationMap[startSelector.tick]; let i = 0; let j = 0; const totalDuration = tickmap.totalDuration; while (currentDuration < totalDuration && this.noteIndex < this.notes.length) { if (!this.score) { return; } const selection: PasteNote = this.notes[this.noteIndex]; const note: SmoNote = selection.note; if (note.noteType === 'n') { const pitchAr: number[] = []; note.pitches.forEach((pitch, ix) => { pitchAr.push(ix); }); SmoNote.transpose(note, pitchAr, measure.transposeIndex, selection.originalKey, measure.keySignature); } this._populateModifier(selection.selector, startSelector, this.score.staves[selection.selector.staff]); if (currentDuration + note.tickCount <= totalDuration && this.remainder === 0) { // The whole note fits in the measure, paste it. //If this note is a tuplet, and specifically if it is the beginning of a tuplet, we need to handle it //NOTE: tuplets never cross measure boundary, we made sure this is handled here: @see this._populateMeasureArray() if (selection.tupletStart) { const tupletTree: SmoTupletTree = SmoTupletTree.clone(selection.tupletStart); const startIndex: number = voice.notes.length; const diff: number = startIndex - tupletTree.startIndex; SmoTupletTree.adjustTupletIndexes([tupletTree], selection.selector.voice,-1, diff); measure.tupletTrees.push(tupletTree); } const nnote: SmoNote = SmoNote.clone(note); nnote.clef = measure.clef; voice.notes.push(nnote); currentDuration += note.tickCount; this.noteIndex += 1; startSelector.tick += 1; } else if (this.remainder > 0) { // This is a note that spilled over the last measure const nnote = SmoNote.cloneWithDuration(note, { numerator: this.remainder, denominator: 1, remainder: 0 }); nnote.clef = measure.clef; voice.notes.push(nnote); currentDuration += this.remainder; this.remainder = 0; } else { // The note won't fit, so we split it in 2 and paste the remainder in the next measure. // TODO: tie the last note to this one. const partial = totalDuration - currentDuration; const dar = SmoMusic.gcdMap(partial); for (j = 0; j < dar.length; ++j) { const ddd = dar[j]; const vnote = SmoNote.cloneWithDuration(note, { numerator: ddd, denominator: 1, remainder: 0 }); voice.notes.push(vnote); } currentDuration += partial; // Set the remaining length of the current note, this will be added to the // next measure with the previous note's pitches this.remainder = note.tickCount - partial; } } } // ### _populatePost // When we paste, we replace entire measures. Populate the last measure from the end of paste to the // end of the measure with notes in the existing measure. _populatePost(voice: SmoVoice, voiceIndex: number, measure: SmoMeasure, tickmap: TickMap) { let endOfPasteDuration = PasteBuffer._countTicks(voice); let existingIndex = measure.getClosestIndexFromTickCount(voiceIndex, endOfPasteDuration); if (existingIndex > tickmap.durationMap.length - 1) { return; } let existingDuration = tickmap.durationMap[existingIndex]; let endOfExistingDuration = existingDuration + tickmap.deltaMap[existingIndex]; let startIndexToAdjustRemainingTuplets = voice.notes.length; let diffToAdjustRemainingTuplets: number = startIndexToAdjustRemainingTuplets - existingIndex - 1; if (Math.round(endOfPasteDuration) < Math.round(endOfExistingDuration)) { //pasted notes ended somewhere in the middle of an existing note //we need to remove the existing note and fill in the difference between the end of our pasted note and beginning of the next one const note = measure.voices[voiceIndex].notes[existingIndex]; const lmap = SmoMusic.gcdMap(endOfExistingDuration - endOfPasteDuration); lmap.forEach((stemTick) => { const nnote = SmoNote.cloneWithDuration(note, stemTick); voice.notes.push(nnote); }); diffToAdjustRemainingTuplets += lmap.length; existingIndex++; } SmoTupletTree.adjustTupletIndexes(measure.tupletTrees, voiceIndex, startIndexToAdjustRemainingTuplets, diffToAdjustRemainingTuplets); for (let i = existingIndex + 1; i < measure.voices[voiceIndex].notes.length; i++) { voice.notes.push(SmoNote.clone(measure.voices[voiceIndex].notes[i])); } } _pasteVoiceSer(serializedMeasure: any, vobj: any, voiceIx: number) { const voices: any[] = []; let ix = 0; serializedMeasure.voices.forEach((vc: any) => { if (ix !== voiceIx) { voices.push(vc); } else { voices.push(vobj); } ix += 1; }); // If we are pasting into a measure that doesn't contain this voice, add the voice if (serializedMeasure.voices.length <= voiceIx) { voices.push(vobj); } serializedMeasure.voices = voices; } pasteChords(selector: SmoSelector) { if (this.notes.length < 1) { return; } if (!this.score) { return; } let srcTick = 0; let destTick = 0; let srcIndex = 0; let selection = SmoSelection.noteSelection(this.score!, selector.staff, selector.measure, selector.voice, selector.tick); while (selection && selection.note && srcIndex < this.notes.length) { const srcNote = this.notes[srcIndex].note; const chords = srcNote.getChords(); if (selection && selection.note) { const destNote = selection.note; if (chords.length) { chords.forEach((chord) => { destNote.removeLyric(chord); }); chords.forEach((chord) => { if (selection) { const nchord = SmoLyric.transposeChordToKey( chord, selection.measure.transposeIndex,this.notes[srcIndex].originalKey, selection.measure.keySignature); destNote.addLyric(nchord); } }); } srcTick += srcNote.tickCount; while (selection && selection.note && destTick < srcTick) { destTick += selection.note.tickCount; if (selection && selection.note) { const curSelector = selection.selector; selection = SmoSelection.nextNoteSelection(this.score, curSelector.staff, curSelector.measure, curSelector.voice, curSelector.tick); } } srcIndex += 1; } } } pasteSelections(selector: SmoSelector) { let i = 0; if (this.notes.length < 1) { return; } if (!this.score) { return; } const maxCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); const minCutVoice = this.notes.map((n) => n.selector.voice).reduce((a, b) => a > b ? a : b); const backupNotes: PasteNote[] = []; this.notes.forEach((bb) => { const note = (SmoNote.deserialize(bb.note.serialize())); const selector = JSON.parse(JSON.stringify(bb.selector)); let tupletStart = bb.tupletStart; if (tupletStart) { tupletStart = SmoTupletTree.deserialize(bb.tupletStart!.serialize()); } backupNotes.push({ note, selector, originalKey: bb.originalKey, tupletStart }); }); if (minCutVoice === maxCutVoice && minCutVoice > selector.voice) { selector.voice = minCutVoice; } this.modifiersToPlace = []; this.noteIndex = 0; this.measureIndex = -1; this.remainder = 0; this._populateMeasureArray(selector); if (this.measures.length === 0) { return; } const voices = this._populateVoice(); const measureSel = JSON.parse(JSON.stringify(this.destination)); const selectors: SmoSelector[] = []; for (i = 0; i < this.measures.length && i < voices.length; ++i) { const measure: SmoMeasure = this.measures[i]; const nvoice: SmoVoice = voices[i]; const ser: any = measure.serialize(); ser.transposeIndex = measure.transposeIndex; // default values are undefined, make sure the transpose is valid ser.keySignature = SmoMusic.vexKeySigWithOffset(measure.keySignature, -1 * measure.transposeIndex); ser.timeSignature = measure.timeSignature.serialize(); ser.tempo = measure.tempo.serialize(); const vobj: any = { notes: [] }; nvoice.notes.forEach((note: SmoNote) => { vobj.notes.push(note.serialize()); }); // TODO: figure out how to do this with multiple voices this._pasteVoiceSer(ser, vobj, this.destination.voice); const nmeasure = SmoMeasure.deserialize(ser); // If this is the non-display buffer, don't try to reset the display rectangles. // Q: Is this even required since we are going to re-render? // A: yes, because until we do, the replaced measure needs the formatting info if (measure.svg.logicalBox && measure.svg.logicalBox.width > 0) { nmeasure.setBox(SvgHelpers.smoBox(measure.svg.logicalBox), 'copypaste'); nmeasure.setX(measure.svg.logicalBox.x, 'copyPaste'); nmeasure.setWidth(measure.svg.logicalBox.width, 'copypaste'); nmeasure.setY(measure.svg.logicalBox.y, 'copypaste'); nmeasure.svg.element = measure.svg.element; nmeasure.svg.tabElement = measure.svg.tabElement; } ['forceClef', 'forceKeySignature', 'forceTimeSignature', 'forceTempo'].forEach((flag) => { (nmeasure as any)[flag] = (measure.svg as any)[flag]; }); this.score.replaceMeasure(measureSel, nmeasure); measureSel.measure += 1; selectors.push( { staff: selector.staff, measure: nmeasure.measureNumber.measureIndex, voice: 0, tick: 0, pitches: [] } ); } this.replacementMeasures = []; selectors.forEach((selector: SmoSelector) => { const nsel: SmoSelection | null = SmoSelection.measureSelection(this.score as SmoScore, selector.staff, selector.measure); if (nsel) { this.replacementMeasures.push(nsel); } }); this.modifiersToPlace.forEach((mod) => { let selection = SmoSelection.selectionFromSelector(this.score!, mod.modifier.endSelector); while (selection && mod.ticksToStart !== 0) { if (mod.ticksToStart < 0) { selection = SmoSelection.nextNoteSelectionFromSelector(this.score!, selection.selector); } else { selection = SmoSelection.lastNoteSelectionFromSelector(this.score!, selection.selector); } mod.ticksToStart -= 1 * Math.sign(mod.ticksToStart); } if (selection) { mod.modifier.startSelector = JSON.parse(JSON.stringify(selection.selector)); selection.staff.addStaffModifier(mod.modifier); } }); this.notes = backupNotes; } }