Skip to content

Instantly share code, notes, and snippets.

@leo6104
Last active February 3, 2021 16:14
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leo6104/85869c82485da716d7274a685d9cb5dc to your computer and use it in GitHub Desktop.
Save leo6104/85869c82485da716d7274a685d9cb5dc to your computer and use it in GitHub Desktop.
MusicXML Transpose (Typescript)
const main = () => {
const transposer = new MusicXmlTransposeService();
transposer.load(this.xmlPath).then(() => {
transposer.transpose('E');
// use transposer.xml variable (In my case, call `osmd.load(transposer.xml);`)
});
};
// Thanks to @ice6 @AlbertHart
// Inspired from https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/commit/467e0a600d168e59c6376bc9c75f553801c98961#diff-19b11c7ef440f126313c8af1217075c7L122
export class MusicXmlTransposeService {
xml: Document;
private oldKey: string;
private transposeKey: string;
private transposeDirection: 'closest' | 'down' | 'up' = 'closest';
constructor() {
}
transpose(key: string, direction: 'closest' | 'down' | 'up' = 'closest') {
this.transposeKey = key;
this.transposeDirection = direction;
let divisions = 0;
let mesauresCnt = 0;
let lastNoteDuration = 0;
let lastStemDirection = '';
let currentAccidentals = {};
const stack: Element[] = [this.xml.firstElementChild];
let target: Element;
// tslint:disable-next-line:no-conditional-assignment
while (target = stack.pop()) {
// 인접 태그는 항상 탐색
if (target.nextElementSibling) {
stack.push(target.nextElementSibling);
}
switch (target.tagName.toUpperCase()) {
case 'DIVISIONS':
divisions = +target.innerHTML;
break;
case 'MEASURES':
mesauresCnt++;
lastNoteDuration = 0;
lastStemDirection = '';
currentAccidentals = { ...TransposeConstant.accidentals_in_key[this.transposeKey] };
break;
case 'FIFTHS':
const fifths = +target.innerHTML;
const lineOfFifthsInC = TransposeConstant.line_of_fifths_numbers.C;
const oldKeyNumber = fifths + lineOfFifthsInC;
this.oldKey = TransposeConstant.line_of_fifths[oldKeyNumber];
const newLineOfFifthsNumber = TransposeConstant.line_of_fifths_numbers[this.transposeKey] - lineOfFifthsInC;
target.innerHTML = `${newLineOfFifthsNumber}`;
currentAccidentals = { ...TransposeConstant.accidentals_in_key[this.transposeKey] };
break;
case 'NOTE':
const durationElem = target.querySelector('duration');
const duration = +durationElem.innerHTML;
const restElem = target.querySelector('rest');
if (restElem) {
const stepElem = restElem.querySelector('display-step');
const octaveElem = restElem.querySelector('display-octave');
const transposedRest = this.transposePitch(stepElem.innerHTML, 0, +octaveElem.innerHTML);
stepElem.innerHTML = transposedRest.step;
octaveElem.innerHTML = `${transposedRest.octave}`;
}
const pitchElem = target.querySelector('pitch');
if (pitchElem) {
const stepElem = pitchElem.querySelector('step');
const octaveElem = pitchElem.querySelector('octave');
let alterElem = pitchElem.querySelector('alter');
const accidentalElem = target.querySelector('accidental');
const originalNote = { step: stepElem.innerHTML, alter: +alterElem?.innerHTML || 0, octave: +octaveElem.innerHTML };
const transposedNote = this.transposePitch(originalNote.step, originalNote.alter, originalNote.octave);
octaveElem.innerHTML = `${transposedNote.octave}`;
stepElem.innerHTML = transposedNote.step;
let stepAlteredNote = transposedNote.step;
let newAccidental = '';
let currentAccidental;
if (transposedNote.alter === '1') {
if (!alterElem) {
alterElem = document.createElement('alter');
pitchElem.appendChild(alterElem);
}
alterElem.innerHTML = transposedNote.alter;
stepAlteredNote = transposedNote.step + '#';
newAccidental = 'sharp';
} else if (transposedNote.alter === '-1') {
if (!alterElem) {
alterElem = document.createElement('alter');
pitchElem.appendChild(alterElem);
}
alterElem.innerHTML = transposedNote.alter;
stepAlteredNote = transposedNote.step + 'b';
newAccidental = 'flat';
} else {
if (alterElem) {
alterElem.remove();
}
}
currentAccidental = currentAccidentals[stepAlteredNote];
currentAccidentals[stepAlteredNote] = newAccidental;
if (accidentalElem) {
if (currentAccidental === newAccidental) { // no change from key or last note
accidentalElem?.remove();
} else if (newAccidental === '') {
accidentalElem.innerHTML = 'natural';
} else {
accidentalElem.innerHTML = newAccidental;
}
}
const stemElem = target.querySelector('stem');
let stemDirection;
if (duration < divisions && lastNoteDuration > 0 && lastNoteDuration < divisions) {
stemDirection = lastStemDirection;
} else if (originalNote.octave > 4) {
stemDirection = `down`;
} else if (originalNote.octave < 4) {
stemDirection = `up`;
} else if (originalNote.step === 'B') {
stemDirection = `down`;
} else {
stemDirection = `up`;
}
if (stemElem) {
stemElem.innerHTML = stemDirection;
lastStemDirection = stemDirection;
}
lastNoteDuration = duration;
}
continue; // 자식 태그는 더이상 탐색하지 않아도 됩니다.
case 'ROOT':
const rootStepElem = target.querySelector('root-step');
const rootAlterElem = target.querySelector('root-alter');
const transposedRoot = this.transposePitch(rootStepElem.innerHTML, +rootAlterElem.innerHTML, 0);
rootStepElem.innerHTML = transposedRoot.step;
if (transposedRoot.alter !== '0') {
rootAlterElem.innerHTML = transposedRoot.alter;
} else {
rootAlterElem.remove();
}
continue; // 자식 태그는 더이상 탐색하지 않아도 됩니다.
case 'BASS':
const bassStepElem = target.querySelector('bass-step');
const bassAlter = target.querySelector('bass-alter');
const transposedBass = this.transposePitch(bassStepElem.innerHTML, +bassAlter.innerHTML, 0);
bassStepElem.innerHTML = transposedBass.step;
if (transposedBass.alter !== '0') {
bassAlter.innerHTML = transposedBass.alter;
} else {
bassAlter.remove();
}
continue; // 자식 태그는 더이상 탐색하지 않아도 됩니다.
}
// 자식 태그 탐색
if (target.firstElementChild) {
stack.push(target.firstElementChild);
}
}
}
public load(str: string) {
// Warning! This function is asynchronous! No error handling is done here.
// console.log("typeof content: " + typeof content);
const self = this;
// console.log("substring: " + str.substr(0, 5));
if (str.substr(0, 4) === '\x50\x4b\x03\x04') {
// This is a zip file, unpack it first
return MXLHelper.MXLtoXMLstring(str).then(
(x: string) => {
return self.load(x);
}
);
}
// Javascript loads strings as utf-16, which is wonderful BS if you want to parse UTF-8 :S
else if (str.substr(0, 3) === '\uf7ef\uf7bb\uf7bf') {
// UTF with BOM detected, truncate first three bytes and pass along
return this.load(str.substr(3));
}
// first character is sometimes null, making first five characters '<?xm'.
else if (str.substr(0, 6).includes('<?xml')) {
// Parse the string representing an xml file
const parser: DOMParser = new DOMParser();
const contentDocument = parser.parseFromString(str, 'application/xml');
this.xml = contentDocument;
return Promise.resolve(contentDocument);
} else if (str.length < 2083) {
// Assume now "str" is a URL
// Retrieve the file at the given URL
return AJAX.ajax(str).then(
(s: string) => self.load(s),
(exc: Error) => {
throw exc;
}
);
} else {
throw new Error('[MusicXMLTranspose.load(string)] Could not process string. Missing else branch?');
}
}
private transposePitch(oldStep: string, oldAlter: number, oldOctave: number) {
let oldNote = oldStep;
if (oldAlter === 1) {
oldNote += '#';
} else if (oldAlter === -1) {
oldNote += 'b';
}
// move to local variables
const { oldKey, transposeKey: newKey } = this;
const oldKeyNumber = TransposeConstant.note_numbers[oldKey];
const newKeyNumber = TransposeConstant.note_numbers[newKey];
let keyOffset = newKeyNumber - oldKeyNumber;
const upOffset = (keyOffset + 12) % 12; // move up
const downOffset = (keyOffset - 12) % 12; // move down
switch (this.transposeDirection) {
case 'up':
keyOffset = upOffset;
break;
case 'down':
keyOffset = downOffset;
break;
default: // get closest offset
if (Math.abs(upOffset) <= Math.abs(downOffset)) {
keyOffset = upOffset;
} else {
keyOffset = downOffset;
}
}
const kpos1 = TransposeConstant.line_of_fifths_numbers[oldKey];
const kpos2 = TransposeConstant.line_of_fifths_numbers[newKey];
const fifthsOffset = kpos2 - kpos1;
const npos1 = TransposeConstant.line_of_fifths_numbers[oldNote];
const npos2 = npos1 + fifthsOffset;
const newNote = TransposeConstant.line_of_fifths[npos2];
const newStep = newNote.substr(0, 1);
let newAlter = '';
if (newNote.substr(1, 1) === '#') {
newAlter = '1';
} else if (newNote.substr(1, 1) === 'b') {
newAlter = '-1';
}
// offset octave
const oldStepNumber = TransposeConstant.step_number[oldStep];
const newStepNumber = TransposeConstant.step_number[newStep];
let newOctave = +oldOctave; // ADH - calculate change of octave
if (keyOffset > 0 && newStepNumber < oldStepNumber) {
newOctave += 1;
} else if (keyOffset < 0 && newStepNumber > oldStepNumber) {
newOctave -= 1;
}
return {
note: newNote,
step: newStep,
alter: newAlter,
octave: newOctave,
};
}
}
@leo6104
Copy link
Author

leo6104 commented May 16, 2020

Oh, i miss TransposeConstant variables.

export const TransposeConstant = {
  // this has to mave room for offsets of -12 to 12
  line_of_fifths: [
    // 0
    'Db', 'Ab', 'Eb', 'Bb', 'F', 'C', 'G',
    'D', 'A', 'E', 'B', 'Gb', 'Db', 'Ab',
    'Eb', 'Bb', 'F', 'C', 'G', 'D', 'A',

    'E', 'B',
    // 23 start here
    'Gb', 'Db', 'Ab', 'Eb', 'Bb',
    'F', 'C', 'G', 'D', 'A', 'E', 'B',
    'F#', 'C#', 'G#', 'D#', 'A#',
    // 40
    'F', 'C',
    'G', 'D', 'A', 'E', 'B', 'F#', 'C#',
    'G#', 'D#', 'A#', 'F', 'C', 'G', 'D',
  ],

  line_of_fifths_numbers: {
    Gb: 23,
    Db: 24,
    Ab: 25,
    Eb: 26,
    Bb: 27,
    F: 28, 'E#': 28,
    C: 29, 'B#': 29,
    G: 30,
    D: 31,
    A: 32,
    E: 33, Fb: 33,
    B: 34, Cb: 34,
    'F#': 35,
    'C#': 36,
    'G#': 37,
    'D#': 38,
    'A#': 39,
  },

  // ## and bb do not work yet
  accidentals_in_key: {
    C: { C: '', D: '', E: '', F: '', G: '', A: '', B: '' },
    F: { C: '', D: '', E: '', F: '', G: '', A: '', B: 'flat' },
    Bb: { C: '', D: '', E: 'flat', F: '', G: '', A: '', B: 'flat' },
    Eb: { C: '', D: '', E: 'flat', F: '', G: '', A: 'flat', B: 'flat' },
    Ab: { C: '', D: 'flat', E: 'flat', F: '', G: '', A: 'flat', B: 'flat' },
    Db: { C: '', D: 'flat', E: 'flat', F: '', G: 'flat', A: 'flat', B: 'flat' },
    Gb: { C: '', D: 'flat', E: 'flat', F: 'flat', G: 'flat', A: 'flat', B: 'flat' },
    Cb: { C: 'flat', D: 'flat', E: 'flat', F: 'flat', G: 'flat', A: 'flat', B: 'flat' },


    G: { C: '', D: '', E: '', F: 'sharp', G: '', A: '', B: '' },
    D: { C: 'sharp', D: '', E: '', F: 'sharp', G: '', A: '', B: '' },
    A: { C: 'sharp', D: '', E: '', F: 'sharp', G: 'sharp', A: '', B: '' },
    E: { C: 'sharp', D: 'sharp', E: '', F: 'sharp', G: 'sharp', A: '', B: '' },
    B: { C: 'sharp', D: 'sharp', E: '', F: 'sharp', G: 'sharp', A: 'sharp', B: '' },
    'F#': { C: 'sharp', D: '', E: 'sharp', F: 'sharp', G: 'sharp', A: 'sharp', B: '' },
    'C#': { C: 'sharp', D: '', E: 'sharp', F: 'sharp', G: 'sharp', A: 'sharp', B: 'sharp' },
    'G#': { C: 'sharp', D: '', E: 'sharp', F: '##', G: 'sharp', A: 'sharp', B: 'sharp' },
    'D#': { C: '##', D: 'sharp', E: 'sharp', F: '##', G: 'sharp', A: 'sharp', B: 'sharp' },
  },

  note_letters_flat: ['', 'C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'],
  note_letters_sharp: ['', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'],

  sharp_flat_from_note: {
    C: 'b',
    'C#': '#',
    Db: 'b',
    D: '#',
    'D#': '#',
    Eb: 'b',
    E: '#',
    'E#': '#',
    Fb: 'b',
    F: 'b',
    'F#': '#',
    Gb: 'b',
    G: '#',
    'G#': '#',
    Ab: 'b',
    A: '#',
    'A#': '#',
    B: '#',
    'B#': '#',
    Cb: 'b',
  },

  note_numbers: {

    'B#': 1,
    C: 1,
    'C#': 2,
    Db: 2,
    D: 3,
    'D#': 4,
    Eb: 4,
    E: 5,

    Fb: 5,
    'E#': 6,

    F: 6,
    'F#': 7,
    Gb: 7,
    G: 8,
    'G#': 9,
    Ab: 9,
    A: 10,
    'A#': 11,
    Bb: 11,
    B: 12,
    Cb: 12,
  },

  // when to bump the octave
  step_number: {
    C: 0,
    D: 1,
    E: 2,
    F: 3,
    G: 4,
    A: 5,
    B: 6,
  },
};

@ice6
Copy link

ice6 commented May 21, 2020

so sool, @AlbertHart had made some progress on the essential code. maybe you have to translate again. :P

@davidecampello
Copy link

Hi @leo6104 ! I'm trying your code but I'm having an error:
ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'innerHTML' of null

This is the stack trace:
core.js:6141 ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'innerHTML' of null TypeError: Cannot read property 'innerHTML' of null at MusicXmlTransposeService.transpose (music-xml-transpose.ts:191) at FolderPage.<anonymous> (folder.page.ts:74) at Generator.next (<anonymous>) at fulfilled (tslib.es6.js:73) at ZoneDelegate.invoke (zone-evergreen.js:368) at Object.onInvoke (core.js:28512) at ZoneDelegate.invoke (zone-evergreen.js:367) at Zone.run (zone-evergreen.js:130) at zone-evergreen.js:1272 at ZoneDelegate.invokeTask (zone-evergreen.js:402) at resolvePromise (zone-evergreen.js:1209) at zone-evergreen.js:1116 at fulfilled (tslib.es6.js:73) at ZoneDelegate.invoke (zone-evergreen.js:368) at Object.onInvoke (core.js:28512) at ZoneDelegate.invoke (zone-evergreen.js:367) at Zone.run (zone-evergreen.js:130) at zone-evergreen.js:1272 at ZoneDelegate.invokeTask (zone-evergreen.js:402) at Object.onInvokeTask (core.js:28499)

I'm trying to transpose this musicxml file:
https://gist.github.com/davidecampello/a90f963b84a8125183e983273f6e00b0

I'm loading it as a string into a ionic app.
If i load directly the string into OSMD I can see the error without problems but transposing is not working.
Can you help me? I can provide you with a complete example..
Thank you
Davide

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment