/* Copyright © 2024 yonah_ag
*
* This program is free software; you can redistribute it or modify it under
* the terms of the GNU General Public License version 3 as published by the
* Free Software Foundation and appearing in the accompanying LICENSE file.
*
* Description
* -----------
*
*/
import MuseScore 3.0
import QtQuick 2.2
import QtQuick.Controls 1.5
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import FileIO 3.0
// import QtQuick.Controls.Styles 1.3
// import Qt.labs.settings 1.0
MuseScore
{
description: "Bow Direction";
requiresScore: true;
version: "0.5.8";
menuPath: "Plugins.Bow Direction";
pluginType: "dialog"; // "dock";
property var noteName : [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
property var bowText : "xdup45678r"
property var mTick : []
property var mBow : []
property var mSlut : []
property var mSlur : []
property var tinfo : ""
property var mxPath : "";
FileIO { id: fid; source: mxPath }
// =======================================================================================================================
function mapAll()
{
infoWin.visible = true;
var ix=0; var tick; var info;
var elm; var etyp
var zunk; // unknown element
var zsys; // parent is system
tinfo = "
";
tinfo += "All Elements
";
tinfo += "
";
tinfo += " index | Track | Element Name | SegTick "
tinfo += " | Details | ^Parent^ | ^^Parent^^ |
";
cmd('select-all')
var selected = curScore.selection.elements;
for (var ee in selected) {
elm = selected[ee];
zunk = false;
zsys = false;
etyp = elm.type;
switch (etyp) {
case Element.NOTE:
if (elm.noteType==0) {
tick = elm.parent.parent.tick;
info = noteName[(elm.pitch % 12)];
if (elm.tieForward) info += " ~f";
if (elm.tieBack) info += " ~b";
}
else {
tick = elm.parent.parent.parent.tick + " *"; // grace note
info = noteName[(elm.pitch % 12)] + " *";
}
break;
case Element.STEM:
case Element.HOOK:
case Element.ACCIDENTAL:
case Element.ARPEGGIO:
tick = "";
info = "";
break;
case Element.REST:
case Element.BAR_LINE:
case Element.DYNAMIC:
case Element.HARMONY:
case Element.TEMPO_TEXT:
case Element.CLEF:
tick = elm.parent.tick;
switch (etyp) {
case Element.REST:
info = elm.duration.str;
break;
case Element.DYNAMIC:
info = "v = " + elm.velocity;
break;
case Element.HARMONY:
info = elm.text;
break;
default:
info = "";
}
break;
case Element.ARTICULATION:
case Element.LYRICS:
if (etyp==Element.ARTICULATION) {
info = elm.symbol;
if (info==SymId.stringsDownBow) {
info = "Down bow";
}
else if (info==SymId.stringsUpBow) {
info = "Up bow";
}
}
else {
info = elm.text;
}
tick = elm.parent.parent.tick;
break;
case Element.STAFF_TEXT:
tick = elm.parent.tick;
info = elm.text.substring(0,20);
break;
case Element.NOTEDOT:
tick = elm.parent.parent.parent.tick;
info = "";
break;
case Element.BREATH:
tick = elm.parent.tick;
info = elm.symbol;
break;
case Element.BEAM:
case Element.TIE:
case Element.SLUR:
case Element.TEXTLINE:
case Element.VIBRATO:
case Element.PEDAL:
zsys = true;
tick = "";
info = "";
break;
default:
tick = "";
info = "";
zunk = true;
}
++ix;
tinfo += "" + ix + " ";
tinfo += " | " + elm.track;
tinfo += " | " + elm.type + " - " + elm.name + " ";
tinfo += " | " + tick + " ";
tinfo += " | " + info + " ";
if (zunk)
tinfo += " | | ";
else if (zsys)
tinfo += " | System | ";
else {
tinfo += " | " + elm.parent.name + " ";
tinfo += " | " + elm.parent.parent.name + " ";
}
tinfo += " |
";
}
curScore.selection.clear();
tinfo += "
";
popInfo.text = tinfo;
}
// =======================================================================================================================
function mapBow()
{
infoWin.visible = true;
var ix=0; var ee;
var tick;
var tock = -1; // previous note tick (for chord detection)
var elm; var etyp; var zelm;
var zunk; // unknown element
var zsys; // parent is system
var nslur = 0; // count of slurs
var info;
tinfo = "";
tinfo += "Bowing Elements
";
tinfo += "
";
tinfo += " index | Track | Element Name | SegTick "
tinfo += " | Details | ^Parent^ | ^^Parent^^ |
";
cmd('select-all')
var selected = curScore.selection.elements;
for (ee in selected) {
elm = selected[ee];
if (elm.track > 0) continue; // only process first track
zunk = false;
zsys = false;
etyp = elm.type;
switch (etyp) {
case Element.NOTE:
if (elm.noteType==0) {
tick = elm.parent.parent.tick;
if (tick > tock) { // no bow change on chords
tock = tick;
zelm = true;
info = noteName[(elm.pitch % 12)];
if (elm.tieForward) info += " ~f";
if (elm.tieBack) info += " ~b";
}
else
zelm = false;
}
break;
case Element.REST:
zelm = true;
tick = elm.parent.tick;
tock = tick;
info = elm.duration.str;
break;
case Element.ARTICULATION:
info = elm.symbol;
zelm = false;
if (info==SymId.stringsDownBow) {
info = "Down bow";
tick = elm.parent.parent.tick;
zelm = true;
}
else if (info==SymId.stringsUpBow) {
info = "Up bow";
tick = elm.parent.parent.tick;
zelm = true;
}
break;
case Element.STAFF_TEXT:
info = elm.text;
zelm = (info == "pizz." || info == "arco");
if (zelm) tick = elm.parent.tick;
break;
case Element.BREATH:
zelm = true;
tick = elm.parent.tick;
info = elm.symbol;
break;
case Element.TIE:
zelm = true;
zsys = true;
tick = "";
info = "";
break;
case Element.SLUR:
++nslur;
zelm = true;
zsys = true;
tick = "";
info = "";
break;
default:
zelm = false;
}
if (zelm) {
++ix;
tinfo += "" + ix + " ";
tinfo += " | " + elm.track;
tinfo += " | " + elm.name + " ";
tinfo += " | " + tick + " ";
tinfo += " | " + info + " ";
if (zsys)
tinfo += " | System | ";
else {
tinfo += " | " + elm.parent.name + " ";
tinfo += " | " + elm.parent.parent.name + " ";
}
tinfo += " |
";
}
}
curScore.selection.clear();
tinfo += "
";
tinfo += "Slur count = " + nslur + "
";
if (nslur > 0 ) {
tinfo += "
Tick | Slur |
";
var mxFile; // musicxml file contents
var dmul; // divisions multiplier
var tock; // slur tick
mxPath = curScore.path.substring(0, curScore.path.lastIndexOf("/")+1) + curScore.scoreName+".musicxml";
writeScore(curScore, mxPath, "musicxml");
mxFile = fid.read();
mxFile = mxFile.replace(/[^]+?([^]+?)<\/divisions>/, '$1');
ix = mxFile.search("\n");
dmul = 480 / mxFile.substring(0,ix);
mxFile = mxFile.replace(/[^]+/,'');
mxFile = mxFile.replace(/[^]+?<\/note>/g,'');
mxFile = mxFile.replace(/[^]+?(|)/g,'$2\n');
mxFile = mxFile.replace(/<\/duration>/g,'');
mxFile = mxFile.replace(/\n\n/g,'\nz\n');
// fid.write(mxFile);
tick = 0;
ix = mxFile.search('\n');
while (ix >= 0) {
elm = mxFile.substring(0,ix);
if (isNaN(elm)) {
if (elm == "z") break;
tinfo += "" + tock + " | " + elm.substr(8,1) + " |
";
}
else {
tock = tick;
tick += dmul * elm;
}
mxFile = mxFile.substring(ix+1);
ix = mxFile.search('\n');
}
mxFile = '';
}
tinfo += "";
popInfo.text = tinfo;
}
// =======================================================================================================================
function mapTick()
{
var ix; var ox; var ibow;
var tick; var info;
var elm; var etyp; var ee;
var tock = -1; // previous note tick (for chord detection)
var nslur = 0; // count slurs
if (inpMapType.currentIndex==2) infoBow.visible = true;
cmd('select-all')
var selected = curScore.selection.elements;
mTick.length = 0;
mBow.length = 0;
ix = -1;
ibow = 1;
for (var ee in selected) {
elm = selected[ee];
if (elm.track > 0) continue; // only process first track
etyp = elm.type;
switch (etyp) {
case Element.NOTE:
tick = elm.parent.parent.tick;
if (elm.noteType==0 && tick > tock) { // no bow change on chords
tock = tick;
if (elm.tieBack) ibow = 3-ibow;
ix++;
mTick.push(tick);
mBow.push(ibow);
if (ibow<3) ibow = 3-ibow;
}
break;
case Element.REST:
tick = elm.parent.tick;
tock = tick;
ix++;
mTick.push(tick);
mBow.push(9);
if (ibow != 3) ibow = 1;
break;
case Element.ARTICULATION:
info = elm.symbol;
if (info==SymId.stringsDownBow) {
mBow[ix]=1;
ibow = 2;
}
else if (info==SymId.stringsUpBow) {
mBow[ix]=2;
ibow = 1;
}
break;
case Element.STAFF_TEXT:
info = elm.text;
if (info=="pizz.") {
ibow = 3;
}
else if (info=="arco") {
ibow = 1;
}
break;
case Element.BREATH:
tick = elm.parent.tick;
ibow =1;
break;
case Element.SLUR:
++nslur;
break;
// case Element.TIE: // handled via note tieBack property
// break;
}
}
curScore.selection.clear();
if (nslur > 0 ) {
tinfo += "Tick | Slur |
";
var mxFile; // musicxml file contents
var dmul; // divisions multiplier
var tock; // slur tick
mxPath = curScore.path.substring(0, curScore.path.lastIndexOf("/")+1) + curScore.scoreName+".musicxml";
writeScore(curScore, mxPath, "musicxml");
mxFile = fid.read();
mxFile = mxFile.replace(/[^]+?([^]+?)<\/divisions>/, '$1');
ix = mxFile.search("\n");
dmul = 480 / mxFile.substring(0,ix);
mxFile = mxFile.replace(/[^]+/,'');
mxFile = mxFile.replace(/[^]+?<\/note>/g,'');
mxFile = mxFile.replace(/[^]+?(|)/g,'$2\n');
mxFile = mxFile.replace(/<\/duration>/g,'');
mxFile = mxFile.replace(/\n\n/g,'\nz\n');
fid.write(mxFile);
tick = 0;
ix = mxFile.search('\n');
while (ix >= 0) {
elm = mxFile.substring(0,ix);
if (isNaN(elm)) {
if (elm == "z") break;
mSlut.push(tock);
mSlur.push(elm.substr(8,1))
}
else {
tock = tick;
tick += dmul * elm;
}
mxFile = mxFile.substring(ix+1);
ix = mxFile.search('\n');
}
mxFile = '';
ix=0; ox=0;
for (ee=0; ee<=nslur; ee++) { // Now patch the slurs into the main map
tock = mSlut[ox]; // slur start
tick = mTick[ix];
while (tick < tock) {
ix++;
tick = mTick[ix]
}
ibow = mBow[ix]; // bow code at start of slur
ox++;
tock = mSlut[ox]; // slur stop
ix++;
tick = mTick[ix];
while (tick <= tock) {
mBow[ix] = ibow;
ix++;
tick = mTick[ix];
}
ox++;
}
}
if (inpMapType.currentIndex==2) {
tinfo = "";
tinfo += "Bow @ Tick
";
tinfo += "
";
tinfo += " SegTick | Bow |
";
for (ee = 0; ee < mTick.length; ee++) {
tinfo += ""+mTick[ee]+" ";
tinfo += " | "+bowText.substring(mBow[ee],mBow[ee]+1)+" |
";
}
if (nslur > 0 ){
tinfo += "Slurs | |
";
for (ee = 0; ee <= nslur; ee++) {
tinfo += ""+mSlut[ee]+" | "+mSlur[ee]+" |
";
}
}
tinfo += "
";
popBow.text = tinfo;
}
else {
curScore.startCmd();
var cur = curScore.newCursor();
var tob; // text object
for (ee = 0; ee < mTick.length; ee++)
{
cur.rewindToTick(mTick[ee]);
tob = newElement(Element.STAFF_TEXT);
tob.text = bowText.substring(mBow[ee],mBow[ee]+1);
tob.color = "#0000D0";
tob.placement = Placement.ABOVE;
tob.align = 8;
tob.fontFace = "FreeSans";
// tob.offsetX = 0.5;
tob.offsetY = -5;
tob.fontSize = 8;
tob.fontStyle = 0; // 0:Normal, 1:Bold
tob.autoplace = true;
cur.add(tob);
}
curScore.endCmd();
}
}
// =======================================================================================================================
function mapScore()
{
console.time("Process");
switch (inpMapType.currentIndex) {
case 0:
mapAll();
break;
case 1:
mapBow();
break;
case 2:
case 3:
mapTick();
break;
default:
tinfo = "Unknown processing option!";
}
tinfo = "";
console.timeEnd("Process");
}
// =======================================================================================================================
onRun: {}
width: 300;
height: 50;
RowLayout { id: uiRow1
x: 15; y:15
Label { id: lblRow2
Layout.preferredWidth: 50
color: "#000000"
text: "Process"
}
ComboBox {
id: inpMapType
model: ["Map all elements", "Map bowing elements", "Map bow @ tick", "Add bow text to score"]
Layout.leftMargin: 0
Layout.preferredWidth: 175
currentIndex: 0
}
Button { id: btnMapIt
Layout.leftMargin: 0
Layout.preferredWidth: 40
Layout.preferredHeight: 24
text: "Go"
onClicked: mapScore();
}
}
ApplicationWindow {
id: infoWin
x: 20; y: 50
width: 550; height: 900
title: "Score Map"
visible: false
TextArea {
id: popInfo
width: infoWin.width; height: infoWin.height
textMargin: 15
textFormat: TextEdit.RichText
readOnly: true
wrapMode: TextEdit.Wrap
text: "Processing..."
}
}
ApplicationWindow {
id: infoBow
x: 20; y: 50
width: 200; height: 900
title: "Bowing"
visible: false
TextArea {
id: popBow
width: infoBow.width; height: infoBow.height
textMargin: 15
textFormat: TextEdit.RichText
readOnly: true
wrapMode: TextEdit.Wrap
text: "Processing..."
}
}
}