diff --git a/.gitignore b/.gitignore index 41deb9e..4ac73f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ -out +out/* +!out/GenerateFifthGeneratedPlugin.py + source/CustomTunings.tsv diff --git a/out/GenerateFifthGeneratedPlugin.py b/out/GenerateFifthGeneratedPlugin.py new file mode 100644 index 0000000..3a3eeed --- /dev/null +++ b/out/GenerateFifthGeneratedPlugin.py @@ -0,0 +1,66 @@ +import os +import shutil +import re + + +PLUGIN_FOLDER_NAME = "fifth_generated_tuner" +SOURCE_FOLDER = "../source" +LOGS_FOLDER = f"{PLUGIN_FOLDER_NAME}/logs" +README_PATH = "../README.md" +LICENSE_PATH = "../LICENSE" +THUMBNAIL_PATH = "../thumbnail/FifthGeneratedTunerThumbnail.png" +FILES_TO_COPY = [ + LICENSE_PATH, + THUMBNAIL_PATH, +] +FILES_TO_CREATE = [ + f"{PLUGIN_FOLDER_NAME}/CustomTunings.tsv" +] + + +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) + for file_path in FILES_TO_CREATE: + open(file_path, "w") + + 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() diff --git a/source/FifthGeneratedTuner.qml b/source/FifthGeneratedTuner.qml index e923b21..abb1209 100644 --- a/source/FifthGeneratedTuner.qml +++ b/source/FifthGeneratedTuner.qml @@ -22,6 +22,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 @@ -32,7 +33,7 @@ MuseScore description: "Retune the selection, or the whole score if nothing is selected, using the specified fifth size."; categoryCode: "playback"; thumbnailName: "FifthGeneratedTunerThumbnail.png"; - version: "1.3.1"; + version: "1.3.2"; pluginType: "dialog"; property var padding: 10; @@ -846,99 +847,15 @@ MuseScore { logger.log("Tuning notes."); - 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); - } - - // 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); - - while (cursor.segment && (cursor.tick < endTick)) - { - // Tune notes. - if (cursor.element && (cursor.element.type == Element.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); - } - } - } - - 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(); - } - } - } + "onNote": onNote + }, + logger + ); logger.log("Notes tuned: " + tunedNotes + " / " + totalNotes); - - curScore.endCmd(); } catch (error) { @@ -950,27 +867,21 @@ MuseScore } } - /** - * Returns the amount of cents necessary to tune the input note to 31EDO. - */ - function calculateTuningOffset(note) + function onNote(note) { totalNotes += 1; - logger.trace("Tuning note: " + NoteUtils.getNoteLetter(note) + " " + AccidentalUtils.getAccidentalName(note) + " " + NoteUtils.getOctave(note)); - try { + logger.trace("Tuning note: " + NoteUtils.getNoteLetter(note) + " " + AccidentalUtils.getAccidentalName(note) + " " + NoteUtils.getOctave(note)); var tuningOffset = -TuningUtils.circleOfFifthsDistance(note, referenceNote) * fifthDeviation; - tunedNotes += 1; logger.trace("Tuning offset: " + tuningOffset); - return tuningOffset; + note.tuning = tuningOffset; + tunedNotes += 1; } catch (error) { logger.error(error); - // Leave the tuning of the input note unchanged. - return note.tuning; } } 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(); +}