From eeb3bfc5b6aa033f4a01f2d80f53483c5317680e Mon Sep 17 00:00:00 2001
From: Alessandro Culatti <99362337+looptailG@users.noreply.github.com>
Date: Fri, 29 Nov 2024 23:40:01 +0100
Subject: [PATCH 1/4] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 7bc139a..685801f 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ This plugin remembers which accidental is applied to any given note, and will au
This plugin supports custom key signatures. If the custom key signatures only contain standard accidentals, no extra action is required other than inserting the custom key signaturs into the score.
-If the key signature contains microtonal accidentals, then it is necessary to also add a text (`System Text` or `Staff Text`) to inform the plugin about the accidentals present in the key signature. This text has to be formatted as `X.X.X.X.X.X.X`, where `X` are the accidental applied to each note, arranged according to the circle of fifths: `F.C.G.D.A.E.B`. These accidentals are written using ASCII characters only in the following way:
+If the key signature contains microtonal accidentals, then it is necessary to also add a text (`System Text` for a global key signature or `Staff Text` for a local key signature) to inform the plugin about the accidentals present in the key signature. This text has to be formatted as `X.X.X.X.X.X.X`, where `X` are the accidental applied to each note, arranged according to the circle of fifths: `F.C.G.D.A.E.B`. These accidentals are written using ASCII characters only in the following way:
| Accidental | Text |
| :--------: | :--: |
From 9b66d5b79bb8b2a45ae8cc35d8592c2eafd0a273 Mon Sep 17 00:00:00 2001
From: Alessandro Culatti <99362337+looptailG@users.noreply.github.com>
Date: Fri, 29 Nov 2024 23:42:04 +0100
Subject: [PATCH 2/4] Updated libs
---
source/libs/IterationUtils.js | 163 ++++++++++++++++++++++++++++++++++
source/libs/StringUtils.js | 54 +++++++++--
source/libs/TuningUtils.js | 151 ++++++++++++++++++++++++++++++-
3 files changed, 357 insertions(+), 11 deletions(-)
create mode 100644 source/libs/IterationUtils.js
diff --git a/source/libs/IterationUtils.js b/source/libs/IterationUtils.js
new file mode 100644
index 0000000..392b922
--- /dev/null
+++ b/source/libs/IterationUtils.js
@@ -0,0 +1,163 @@
+/*
+ A collection of functions and constants for iterating over a score.
+ Copyright (C) 2024 Alessandro Culatti
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+const VERSION = "1.0.0";
+
+const ELEMENT_STAFF_TEXT = 48;
+
+function iterate(curScore, actions, logger)
+{
+ let onStaffStart = actions.onStaffStart || null;
+ let onNewMeasure = actions.onNewMeasure || null;
+ let onKeySignatureChange = actions.onKeySignatureChange || null;
+ let onAnnotation = actions.onAnnotation || null;
+ let staffTextOnCurrentStaffOnly = actions.staffTextOnCurrentStaffOnly || true;
+ let onNote = actions.onNote || null;
+
+ curScore.startCmd();
+ let cursor = curScore.newCursor();
+
+ // Calculate the portion of the score to iterate on.
+ let startStaff;
+ let endStaff;
+ let startTick;
+ let endTick;
+ cursor.rewind(Cursor.SELECTION_START);
+ if (!cursor.segment)
+ {
+ logger.log("Tuning the entire score.");
+ startStaff = 0;
+ endStaff = curScore.nstaves - 1;
+ startTick = 0;
+ endTick = curScore.lastSegment.tick;
+ }
+ else
+ {
+ logger.log("Tuning only the current selection.");
+ startStaff = cursor.staffIdx;
+ startTick = cursor.tick;
+ cursor.rewind(Cursor.SELECTION_END);
+ endStaff = cursor.staffIdx;
+ if (cursor.tick == 0)
+ {
+ // If the selection includes the last note of the score, .rewind()
+ // overflows and goes back to tick 0.
+ endTick = curScore.lastSegment.tick;
+ }
+ else
+ {
+ endTick = cursor.tick;
+ }
+ logger.trace("Tuning only ticks: " + startTick + " - " + endTick);
+ logger.trace("Tuning only staffs: " + startStaff + " - " + endStaff);
+ }
+
+ // Iterate on the score.
+ for (let staff = startStaff; staff <= endStaff; staff++)
+ {
+ for (let voice = 0; voice < 4; voice++)
+ {
+ logger.log("Tuning Staff: " + staff + "; Voice: " + voice);
+
+ cursor.voice = voice;
+ cursor.staffIdx = staff;
+ cursor.rewindToTick(startTick);
+
+ let previousKeySignature = cursor.keySignature;
+
+ if (onStaffStart)
+ {
+ onStaffStart();
+ }
+
+ // Loop on the element of the current staff.
+ while (cursor.segment && (cursor.tick <= endTick))
+ {
+ if (cursor.segment.tick == cursor.measure.firstSegment.tick)
+ {
+ if (onNewMeasure)
+ {
+ onNewMeasure();
+ }
+ }
+
+ if (cursor.keySignature != previousKeySignature)
+ {
+ if (onKeySignatureChange)
+ {
+ onKeySignatureChange(cursor.keySignature);
+ }
+ previousKeySignature = cursor.keySignature;
+ }
+
+ for (let i = 0; i < cursor.segment.annotations.length; i++)
+ {
+ let annotation = cursor.segment.annotations[i];
+ let annotationText = annotation.text;
+ if (annotationText)
+ {
+ if (onAnnotation)
+ {
+ if ((annotation.type === ELEMENT_STAFF_TEXT) && staffTextOnCurrentStaffOnly)
+ {
+ let annotationPart = annotation.staff.part;
+ if ((4 * staff >= annotationPart.startTrack) && (4 * staff < annotationPart.endTrack))
+ {
+ onAnnotation(annotation);
+ }
+ }
+ else
+ {
+ onAnnotation(annotation);
+ }
+ }
+ }
+ }
+
+ if (cursor.element && (cursor.element.type == Element.CHORD))
+ {
+ let graceChords = cursor.element.graceNotes;
+ for (let i = 0; i < graceChords.length; i++)
+ {
+ let notes = graceChords[i].notes;
+ for (let j = 0; j < notes.length; j++)
+ {
+ if (onNote)
+ {
+ onNote(notes[j]);
+ }
+ }
+ }
+
+ let notes = cursor.element.notes;
+ for (let i = 0; i < notes.length; i++)
+ {
+ if (onNote)
+ {
+ onNote(notes[i]);
+ }
+ }
+ }
+
+ cursor.next();
+ }
+ }
+ }
+
+ curScore.endCmd();
+}
diff --git a/source/libs/StringUtils.js b/source/libs/StringUtils.js
index 16eb36b..aee99dc 100644
--- a/source/libs/StringUtils.js
+++ b/source/libs/StringUtils.js
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-const VERSION = "1.0.0";
+const VERSION = "1.0.1";
/**
* Split the input string using the tab character, and replace the escaped
@@ -25,12 +25,50 @@ const VERSION = "1.0.0";
function parseTsvRow(s)
{
s = s.split("\t");
- for (var i = 0; i < s.length; i++)
+ // QML does not support lookbehind in regex, which would be necessary to
+ // properly unescape the characters, so we have to manually loop on the
+ // strings and check for escape characters.
+ for (let i = 0; i < s.length; i++)
{
- s[i] = s[i].replace(/\\t/g, "\t");
- s[i] = s[i].replace(/\\\\/g, "\\");
- s[i] = s[i].replace(/\\n/g, "\n");
- s[i] = s[i].replace(/\\r/g, "\r");
+ let unescapedString = "";
+ let escapedString = s[i];
+ let j = 0;
+ while (j < escapedString.length)
+ {
+ let c = escapedString.charAt(j);
+ if (c == "\\")
+ {
+ let nextCharacter = escapedString.charAt(++j);
+ switch (nextCharacter)
+ {
+ case "\\":
+ unescapedString += "\\";
+ break;
+
+ case "n":
+ unescapedString += "\n";
+ break;
+
+ case "r":
+ unescapedString += "\r";
+ break;
+
+ case "t":
+ unescapedString += "\t";
+ break;
+
+ default:
+ throw "Invalid escape sequence: " + c + nextCharacter;
+ }
+ }
+ else
+ {
+ unescapedString += c;
+ }
+
+ j++;
+ }
+ s[i] = unescapedString;
}
return s;
}
@@ -54,7 +92,7 @@ function formatForTsv(s)
function removeEmptyRows(s)
{
s = s.split("\n");
- for (var i = s.length - 1; i >= 0; i--)
+ for (let i = s.length - 1; i >= 0; i--)
{
if (s[i].trim() == "")
{
@@ -75,7 +113,7 @@ function roundToOneDecimalDigit(n)
{
throw "The input is not numeric: " + n;
}
- var roundedNumber = "" + (Math.round(n * 10) / 10);
+ let roundedNumber = "" + (Math.round(n * 10) / 10);
if (Number.isInteger(n))
{
roundedNumber += ".0";
diff --git a/source/libs/TuningUtils.js b/source/libs/TuningUtils.js
index 03ab02b..658a65d 100644
--- a/source/libs/TuningUtils.js
+++ b/source/libs/TuningUtils.js
@@ -16,10 +16,10 @@
along with this program. If not, see .
*/
-const VERSION = "1.1.1";
+const VERSION = "1.3.0";
// Size in cents of a justly tuned perfect fifth.
-const JUST_FIFTH = 1200.0 * Math.log2(3 / 2);
+const JUST_FIFTH = intervalInCents(3 / 2);
// Size in cents of a 12EDO perfect fifth.
const DEFAULT_FIFTH = 700.0;
// Size in cents of the smallest fifth in the diatonic range. It's equal to the
@@ -30,7 +30,7 @@ const SMALLEST_DIATONIC_FIFTH = 1200.0 / 7 * 4;
const LARGEST_DIATONIC_FIFTH = 1200.0 / 5 * 3;
// Size in cents of the syntonic comma.
-const SYNTONIC_COMMA = 1200.0 * Math.log2(81 / 80);
+const SYNTONIC_COMMA = intervalInCents(81 / 80);
// Note distance in the circle of fifths, from the note C.
const CIRCLE_OF_FIFTHS_DISTANCE = {
@@ -80,3 +80,148 @@ function circleOfFifthsDistance(n1, n2, tpcMode = "tpc1")
return n1Tpc - n2Tpc;
}
+
+/**
+ * Return the input frequency ratio in cents.
+ */
+function intervalInCents(frequencyRatio)
+{
+ return 1200 * Math.log2(frequencyRatio);
+}
+
+/**
+ * Return the offset in cents for the input scale degree in the harmonic scale.
+ */
+function harmonicScaleOffset(scaleDegree)
+{
+ var harmonic = harmonicScaleHarmonic(scaleDegree);
+ var interval = intervalInCents(harmonic / 16);
+ var defaultTuning = 100.0 * scaleDegree;
+ return interval - defaultTuning;
+}
+
+/**
+ * Return the harmonic corresponding to the input scale degree in the harmonic
+ * scale.
+ */
+function harmonicScaleHarmonic(scaleDegree)
+{
+ switch (scaleDegree)
+ {
+ case 0:
+ return 16;
+
+ case 1:
+ return 17;
+
+ case 2:
+ return 18;
+
+ case 3:
+ return 19;
+
+ case 4:
+ return 20;
+
+ case 5:
+ return 21;
+
+ case 6:
+ return 22;
+
+ case 7:
+ return 24;
+
+ case 8:
+ return 26;
+
+ case 9:
+ return 27;
+
+ case 10:
+ return 28;
+
+ case 11:
+ return 30;
+
+ default:
+ throw "Invalid scale degree: " + scaleDegree;
+ }
+}
+
+/**
+ * Calculate the tuning offset for an EDO tuning.
+ */
+function edoTuningOffset(
+ note, noteName, accidental, octave, referenceNote,
+ stepSize, fifthDeviation, supportedAccidentals, accidentalData,
+ previousAccidentals, customKeySignature,
+ logger
+) {
+ logger.trace("Tuning note: " + noteName + " " + accidental + " " + octave);
+ let actualAccidental = accidental;
+ let effectiveAccidental = accidental;
+ let fullNoteName = noteName + accidental;
+ let noteNameOctave = noteName + octave;
+
+ let tuningOffset;
+
+ tuningOffset = -circleOfFifthsDistance(note, referenceNote) * fifthDeviation;
+ logger.trace("Base tuning offset: " + tuningOffset);
+
+ // Certain accidentals, like the microtonal accidentals, are not
+ // conveyed by the tpc property, but are instead handled directly via a
+ // tuning offset.
+ // Check which accidental is applied to the note.
+ if (effectiveAccidental == "NONE")
+ {
+ // If the note does not have any accidental applied to it, check if
+ // the same note previously in the measure was modified by a
+ // microtonal accidental.
+ if (previousAccidentals.hasOwnProperty(noteNameOctave))
+ {
+ effectiveAccidental = previousAccidentals[noteNameOctave];
+ logger.trace("Applying the following accidental to the current note from a previous note in the measure: " + effectiveAccidental);
+ }
+ // If the note still does not have an accidental applied to itself,
+ // check if it's modified by a custom key signature.
+ if (effectiveAccidental == "NONE")
+ {
+ if (customKeySignature.hasOwnProperty(noteName))
+ {
+ effectiveAccidental = customKeySignature[noteName];
+ logger.trace("Applying the following accidental from the custom key signature: " + effectiveAccidental);
+ }
+ }
+ }
+ else
+ {
+ // Save the accidental in the previous accidentals map for this
+ // note.
+ previousAccidentals[noteNameOctave] = actualAccidental;
+ }
+
+ // Check if the accidental is handled by a tuning offset.
+ if (!accidentalData[effectiveAccidental]["TPC"])
+ {
+ // Undo the default tuning offset which is applied to certain
+ // accidentals.
+ // The default tuning offset is applied only if an actual microtonal
+ // accidental is applied to the current note.
+ let actualAccidentalOffset = accidentalData[actualAccidental]["DEFAULT_OFFSET"];
+ tuningOffset -= actualAccidentalOffset;
+ logger.trace("Undoing the default tuning offset of: " + actualAccidentalOffset);
+
+ // Apply the tuning offset for this specific accidental.
+ let edoSteps = supportedAccidentals[effectiveAccidental];
+ if (edoSteps === undefined)
+ {
+ throw "Unsupported accidental: " + effectiveAccidental;
+ }
+ tuningOffset += edoSteps * stepSize;
+ logger.trace("Offsetting the tuning by " + edoSteps + " EDO steps.");
+ }
+
+ logger.trace("Final tuning offset: " + tuningOffset);
+ return tuningOffset;
+}
From 13fb572798d478b8b1ea39366db3f5a9bb8f44e2 Mon Sep 17 00:00:00 2001
From: Alessandro Culatti <99362337+looptailG@users.noreply.github.com>
Date: Fri, 29 Nov 2024 23:45:40 +0100
Subject: [PATCH 3/4] Added script for generating the output plugin folder
---
.gitignore | 3 +-
out/Generate22EDOPlugin.py | 61 ++++++++++++++++++++++++++++++++++++++
2 files changed, 63 insertions(+), 1 deletion(-)
create mode 100644 out/Generate22EDOPlugin.py
diff --git a/.gitignore b/.gitignore
index 1fcb152..ab75772 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-out
+out/*
+!out/Generate22EDOPlugin.py
diff --git a/out/Generate22EDOPlugin.py b/out/Generate22EDOPlugin.py
new file mode 100644
index 0000000..caa732a
--- /dev/null
+++ b/out/Generate22EDOPlugin.py
@@ -0,0 +1,61 @@
+import os
+import shutil
+import re
+
+
+PLUGIN_FOLDER_NAME = "22edo_tuner"
+SOURCE_FOLDER = "../source"
+LOGS_FOLDER = f"{PLUGIN_FOLDER_NAME}/logs"
+README_PATH = "../README.md"
+LICENSE_PATH = "../LICENSE"
+THUMBNAIL_PATH = "../thumbnail/22EdoThumbnail.png"
+FILES_TO_COPY = [
+ LICENSE_PATH,
+ THUMBNAIL_PATH,
+]
+
+
+def main():
+ try:
+ shutil.copytree(SOURCE_FOLDER, PLUGIN_FOLDER_NAME, dirs_exist_ok=True)
+ for file_path in FILES_TO_COPY:
+ file_name = file_path[file_path.rindex("/") + 1:]
+ shutil.copyfile(file_path, f"{PLUGIN_FOLDER_NAME}/{file_name}")
+ if not os.path.exists(LOGS_FOLDER):
+ os.makedirs(LOGS_FOLDER)
+
+ version_number = get_version_number()
+ output_folder_name = f"{PLUGIN_FOLDER_NAME}_{version_number}"
+ temporary_folder = "tmp"
+ shutil.copytree(PLUGIN_FOLDER_NAME, f"{temporary_folder}/{PLUGIN_FOLDER_NAME}", dirs_exist_ok=True)
+ shutil.make_archive(output_folder_name, "zip", temporary_folder)
+ shutil.rmtree(temporary_folder)
+
+ except Exception as e:
+ print(e)
+ input()
+
+
+def get_version_number() -> str:
+ version_number_pattern = re.compile(r"version:\s*\"(.+)\";")
+ for root, _, files in os.walk(SOURCE_FOLDER):
+ for file_name in files:
+ if simplify_file_name(PLUGIN_FOLDER_NAME) not in simplify_file_name(file_name):
+ continue
+
+ file_path = os.path.join(root, file_name)
+ with open(file_path, "r") as file:
+ for line in file:
+ match = version_number_pattern.match(line.strip())
+ if match:
+ return match.group(1)
+
+ raise Exception("Could not get the version number.")
+
+
+def simplify_file_name(file_name: str) -> str:
+ return file_name.replace("_", "").lower()
+
+
+if __name__ == "__main__":
+ main()
From a10f200c1569a9e3b3005d9f1d7090dd744f58be Mon Sep 17 00:00:00 2001
From: Alessandro Culatti <99362337+looptailG@users.noreply.github.com>
Date: Sat, 30 Nov 2024 00:07:27 +0100
Subject: [PATCH 4/4] Updated to use iteration utils
---
source/22EdoTuner.qml | 399 +++++++++++++-----------------------------
1 file changed, 117 insertions(+), 282 deletions(-)
diff --git a/source/22EdoTuner.qml b/source/22EdoTuner.qml
index 5d276fe..a4f3c16 100644
--- a/source/22EdoTuner.qml
+++ b/source/22EdoTuner.qml
@@ -21,6 +21,7 @@ import FileIO 3.0
import MuseScore 3.0
import "libs/AccidentalUtils.js" as AccidentalUtils
import "libs/DateUtils.js" as DateUtils
+import "libs/IterationUtils.js" as IterationUtils
import "libs/NoteUtils.js" as NoteUtils
import "libs/StringUtils.js" as StringUtils
import "libs/TuningUtils.js" as TuningUtils
@@ -31,7 +32,7 @@ MuseScore
description: "Retune the selection, or the whole score if nothing is selected, to 22EDO.";
categoryCode: "playback";
thumbnailName: "22EdoThumbnail.png";
- version: "2.0.1";
+ version: "2.1.0";
property variant settings: {};
@@ -190,220 +191,19 @@ MuseScore
logger.log("Log level set to: " + logger.currentLogLevel);
logger.log("Reference note set to: " + referenceNote);
- curScore.startCmd();
-
- // Calculate the portion of the score to tune.
- var cursor = curScore.newCursor();
- var startStaff;
- var endStaff;
- var startTick;
- var endTick;
- cursor.rewind(Cursor.SELECTION_START);
- if (!cursor.segment)
- {
- logger.log("Tuning the entire score.");
- startStaff = 0;
- endStaff = curScore.nstaves - 1;
- startTick = 0;
- endTick = curScore.lastSegment.tick + 1;
- }
- else
- {
- logger.log("Tuning only the current selection.");
- startStaff = cursor.staffIdx;
- startTick = cursor.tick;
- cursor.rewind(Cursor.SELECTION_END);
- endStaff = cursor.staffIdx;
- if (cursor.tick == 0)
+ IterationUtils.iterate(
+ curScore,
{
- // If the selection includes the last measure of the score,
- // .rewind() overflows and goes back to tick 0.
- endTick = curScore.lastSegment.tick + 1;
- }
- else
- {
- endTick = cursor.tick;
- }
- logger.trace("Tuning only ticks: " + startTick + " - " + endTick);
- logger.trace("Tuning only staffs: " + startStaff + " - " + endStaff);
- }
-
- tunedNotes = 0;
- totalNotes = 0;
- // Loop on the portion of the score to tune.
- for (var staff = startStaff; staff <= endStaff; staff++)
- {
- for (var voice = 0; voice < 4; voice++)
- {
- logger.log("Tuning Staff: " + staff + "; Voice: " + voice);
-
- cursor.voice = voice;
- cursor.staffIdx = staff;
- cursor.rewindToTick(startTick);
-
- currentCustomKeySignature = {};
- previousAccidentals = {};
-
- // Loop on elements of a voice.
- while (cursor.segment && (cursor.tick < endTick))
- {
- if (cursor.segment.tick == cursor.measure.firstSegment.tick)
- {
- // New measure, empty the previous accidentals map.
- previousAccidentals = {};
- }
-
- // Check for key signature change.
- // TODO: This implementation is very ineffcient, as this piece of code is called on every element when the key signature is not empty. Find a way to call this only when the key signature actually change.
- if (cursor.keySignature)
- {
- // The key signature has changed, empty the custom
- // key signature map.
- // TODO: This if is necessary only because the previous if is not true only when there is an actual key signature change. This way we check if the mapping was not empty before, and thus actually needs to be emptied now.
- if (Object.keys(currentCustomKeySignature).length != 0)
- {
- logger.log("Key signature change, emptying the custom key signature map.");
- currentCustomKeySignature = {};
- }
- }
- // Check if there is a text indicating a custom key
- // signature change.
- for (var i = 0; i < cursor.segment.annotations.length; i++)
- {
- var annotationText = cursor.segment.annotations[i].text;
- if (annotationText)
- {
- annotationText = annotationText.replace(/\s*/g, "");
- if (customKeySignatureRegex.test(annotationText))
- {
- logger.log("Applying the current custom key signature: " + annotationText);
- currentCustomKeySignature = {};
- try
- {
- var annotationTextSplitted = annotationText.split(".");
- for (var j = 0; j < annotationTextSplitted.length; j++)
- {
- var currentNote = customKeySignatureNoteOrder[j];
- var currentAccidental = annotationTextSplitted[j].trim();
- var accidentalName = "";
- switch (currentAccidental)
- {
- // Non-microtonal accidentals
- // are automatically handled by
- // Musescore even in custom key
- // signatures, so we only have
- // to check for microtonal
- // accidentals.
- case "bb":
- case "b":
- case "":
- case "h":
- case "#":
- case "x":
- break;
-
- case "vbb":
- accidentalName = "FLAT2_ARROW_DOWN";
- break;
-
- case "^bb":
- accidentalName = "FLAT2_ARROW_UP";
- break;
-
- case "vb":
- accidentalName = "FLAT_ARROW_DOWN";
- break;
-
- case "^b":
- accidentalName = "FLAT_ARROW_UP";
- break;
-
- case "vh":
- accidentalName = "NATURAL_ARROW_DOWN";
- break;
-
- case "^h":
- accidentalName = "NATURAL_ARROW_UP";
- break;
-
- case "v#":
- accidentalName = "SHARP_ARROW_DOWN";
- break;
-
- case "^#":
- accidentalName = "SHARP_ARROW_UP";
- break;
-
- case "vx":
- accidentalName = "SHARP2_ARROW_DOWN";
- break;
-
- case "^x":
- accidentalName = "SHARP2_ARROW_UP";
- break;
-
- default:
- throw "Unsupported accidental in the custom key signature: " + currentAccidental;
- }
- if (accidentalName != "")
- {
- currentCustomKeySignature[currentNote] = accidentalName;
- }
- }
- }
- catch (error)
- {
- logger.error(error);
- currentCustomKeySignature = {};
- }
- }
- }
- }
-
- // Tune notes.
- if (cursor.element && (cursor.element.type == Element.CHORD))
- {
- // Iterate through every grace chord.
- var graceChords = cursor.element.graceNotes;
- for (var i = 0; i < graceChords.length; i++)
- {
- var notes = graceChords[i].notes;
- for (var j = 0; j < notes.length; j++)
- {
- try
- {
- notes[j].tuning = calculateTuningOffset(notes[j]);
- }
- catch (error)
- {
- logger.error(error);
- }
- }
- }
-
- // Iterate through every chord note.
- var notes = cursor.element.notes;
- for (var i = 0; i < notes.length; i++)
- {
- try
- {
- notes[i].tuning = calculateTuningOffset(notes[i]);
- }
- catch (error)
- {
- logger.error(error);
- }
- }
- }
-
- cursor.next();
- }
- }
- }
+ "onStaffStart": onStaffStart,
+ "onNewMeasure": onNewMeasure,
+ "onKeySignatureChange": onKeySignatureChange,
+ "onAnnotation": onAnnotation,
+ "onNote": onNote
+ },
+ logger
+ );
logger.log("Notes tuned: " + tunedNotes + " / " + totalNotes);
-
- curScore.endCmd();
}
catch (error)
{
@@ -415,90 +215,125 @@ MuseScore
}
}
- /**
- * Returns the amount of cents necessary to tune the input note to 22EDO.
- */
- function calculateTuningOffset(note)
+ function onStaffStart()
{
- totalNotes += 1;
+ currentCustomKeySignature = {};
+ previousAccidentals = {};
+ }
- var noteLetter = NoteUtils.getNoteLetter(note, "tpc");
- var accidentalName = AccidentalUtils.getAccidentalName(note);
- var noteOctave = NoteUtils.getOctave(note);
- var noteNameOctave = noteLetter + noteOctave;
- var completeNoteName = noteLetter + " " + accidentalName + " " + noteOctave;
- logger.trace("Tuning note: " + completeNoteName);
-
- try
+ function onNewMeasure()
+ {
+ previousAccidentals = {};
+ }
+
+ function onKeySignatureChange(keySignature)
+ {
+ logger.log("Key signature change, emptying the custom key signature map.");
+ currentCustomKeySignature = {};
+ }
+
+ function onAnnotation(annotation)
+ {
+ let annotationText = annotation.text.replace(/\s*/g, "");
+ if (customKeySignatureRegex.test(annotationText))
{
- var tuningOffset = -TuningUtils.circleOfFifthsDistance(note, referenceNote) * fifthDeviation;
- logger.trace("Base tuning offset: " + tuningOffset);
-
- // Certain accidentals, like the microtonal accidentals, are not
- // conveyed by the tpc property, but are instead handled directly
- // via a tuning offset.
- // Check which accidental is applied to the note.
- if (accidentalName == "NONE")
+ logger.log("Applying custom key signature: " + annotationText);
+ currentCustomKeySignature = {};
+ try
{
- // If the note does not have any accidental applied to it, check
- // if the same note previously in the measure was modified by a
- // microtonal accidental.
- if (previousAccidentals.hasOwnProperty(noteNameOctave))
- {
- accidentalName = previousAccidentals[noteNameOctave];
- logger.trace("Applying to the following accidental to the current note from a previous note within the measure: " + accidentalName);
- }
- // If the note still does not have an accidental applied to it,
- // check if it's modified by a custom key signature.
- if (accidentalName == "NONE")
+ let annotationTextSplitted = annotationText.split(".");
+ for (let i = 0; i < annotationTextSplitted.length; i++)
{
- if (currentCustomKeySignature.hasOwnProperty(noteLetter))
+ let currentNote = customKeySignatureNoteOrder[i];
+ let currentAccidental = annotationTextSplitted[i];
+ let accidentalName = "";
+ switch (currentAccidental)
{
- accidentalName = currentCustomKeySignature[noteLetter];
- logger.trace("Applying the following accidental from a custom key signature: " + accidentalName);
+ // Non-microtonal accidentals are automatically handled
+ // by Musescore even in custom key signatures, so we
+ // only have to check for microtonal accidentals.
+ case "bb":
+ case "b":
+ case "":
+ case "h":
+ case "#":
+ case "x":
+ break;
+
+ case "vbb":
+ accidentalName = "FLAT2_ARROW_DOWN";
+ break;
+
+ case "^bb":
+ accidentalName = "FLAT2_ARROW_UP";
+ break;
+
+ case "vb":
+ accidentalName = "FLAT_ARROW_DOWN";
+ break;
+
+ case "^b":
+ accidentalName = "FLAT_ARROW_UP";
+ break;
+
+ case "vh":
+ accidentalName = "NATURAL_ARROW_DOWN";
+ break;
+
+ case "^h":
+ accidentalName = "NATURAL_ARROW_UP";
+ break;
+
+ case "v#":
+ accidentalName = "SHARP_ARROW_DOWN";
+ break;
+
+ case "^#":
+ accidentalName = "SHARP_ARROW_UP";
+ break;
+
+ case "vx":
+ accidentalName = "SHARP2_ARROW_DOWN";
+ break;
+
+ case "^x":
+ accidentalName = "SHARP2_ARROW_UP";
+ break;
+
+ default:
+ throw "Unsupported accidental in the custom key signature: " + currentAccidental;
+ }
+ if (accidentalName != "")
+ {
+ currentCustomKeySignature[currentNote] = accidentalName;
}
}
}
- else
+ catch (error)
{
- // Save the accidental in the previous accidentals map for this
- // note.
- previousAccidentals[noteNameOctave] = accidentalName;
- }
- // Check if the accidental is handled by a tuning offset.
- if (!AccidentalUtils.ACCIDENTAL_DATA[accidentalName]["TPC"])
- {
- // Undo the default tuning offset which is applied to certain
- // accidentals.
- // The default tuning offset is applied only if an actual
- // microtonal accidental is applied to the current note. For
- // this reason, we must check getAccidentalName() on the current
- // note, it is not sufficient to check the value saved in
- // accidentalName.
- var actualAccidentalName = AccidentalUtils.getAccidentalName(note);
- var actualAccidentalOffset = AccidentalUtils.ACCIDENTAL_DATA[actualAccidentalName]["DEFAULT_OFFSET"];
- tuningOffset -= actualAccidentalOffset;
- logger.trace("Undoing the default tuning offset of: " + actualAccidentalOffset);
-
- // Apply the tuning offset for this specific accidental.
- var edoSteps = supportedAccidentals[accidentalName];
- if (edoSteps === undefined)
- {
- throw "Unsupported accidental: " + accidentalName;
- }
- tuningOffset += edoSteps * stepSize;
- logger.trace("Offsetting the tuning by " + edoSteps + " EDO steps.");
+ logger.error(error);
+ currentCustomKeySignature = {};
}
-
- tunedNotes += 1;
- logger.trace("Final tuning offset: " + tuningOffset);
- return tuningOffset;
+ }
+ }
+
+ function onNote(note)
+ {
+ totalNotes++;
+
+ try
+ {
+ note.tuning = TuningUtils.edoTuningOffset(
+ note, NoteUtils.getNoteLetter(note, "tpc"), AccidentalUtils.getAccidentalName(note), NoteUtils.getOctave(note), referenceNote,
+ stepSize, fifthDeviation, supportedAccidentals, AccidentalUtils.ACCIDENTAL_DATA,
+ previousAccidentals, currentCustomKeySignature,
+ logger
+ );
+ tunedNotes++;
}
catch (error)
{
- logger.error("Encontered the following exception while tuning " + completeNoteName + ": " + error);
- // Leave the tuning of the input note unchanged.
- return note.tuning;
+ logger.error(error);
}
}
}