/* MuseScore Plugin: Tab Ring * * 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 * ----------- * Collapse multi-voice stave into a single voice * It's very slow due to the use of cmd-pad but I can't find an alternative * * 26 Oct 2024 | 1.0 | Initial release * 27 Oct 2024 | 1.1 | Handle chords and ties */ import QtQuick 2.2 import QtQuick.Controls 1.5 import QtQuick.Layouts 1.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 import MuseScore 3.0 MuseScore { version: "1.1" description: "Merge voices in first stave of score" menuPath: "Plugins.Merge Voices" requiresScore: true pluginType: "dialog" // Global variables property var zLog : false // view process log only property var zMV : true // = !zLog but used inside inner loop so have a var property var nofSel : 0 // count of selected elements property var nofUpd : 0 // count of notes updated property var tout : "" // text output file property var tlog : "" // text output log line property var tdet : "" // message dialog detail property var nofM : 0 // no. of measures property var nofS : 0 // no. of segments property var nofN : 0 // no. of notes property var nofT : 0 // no. of tiebacks property var tickEnd : 0 // end of score tick property var tickFr : 0 // Tick From property var tickTo : 0 // Tick To // Score Map property var mIX : [] // Map index property var mMez : [] property var mTick : [] property var mFace : [] // Face value length property var mMIDI : [] // MIDI note value for 2nds detection property var mTup : [] // Tuplet factor or 0:Not a tuplet property var mTie : [] // 0:No tie, 1:Tie forward, -1:Tie back property var mVox : [] // Voice 0-3 within each Part property var mPlay : [] // Play flag:

=Play, =Silent property var mTock : [] // Tick + Face (for voice merge) property var mFadj : [] // Face value adjusted for merge // onRun: {} //================================================================================================================================= function runMain(logOnly) // zlog => only log the process, no actual merging { zLog = logOnly; zMV = !zLog; runInfo.text = "Merge Voices: " + version tickEnd = curScore.lastSegment.tick; if (zLog) tout = ""; mapScore(); if (zLog) logMap(); mapMerge(); if (zLog) logMergeMap(); else voxMerge(); tdet = "Voice Merge " + version; tdet += "\nMeasures: " + nofM; tdet += " | Notes: " + nofN; if (zLog) { tout += ""; popInfo.text = tout; tout = ""; infoWin.visible = true; } tdet += " | Updated: " + nofUpd; runInfo.text = tdet; // Clear Maps mIX.length = 0; mMez.length = 0; mTick.length = 0; mFace.length = 0; mMIDI.length = 0; mTup.length = 0; mTie.length = 0; mVox.length = 0; mPlay.length = 0; mTock.length = 0; mFadj.length = 0; } //================================================================================================================================= function voxMerge() { var seg; var elm; var tt; var nn; var note; var ix = 0; var durn = 0; var cursor = curScore.newCursor(); var dur = "0"; var durt = 0; var dot = 0; var tock = 0; var nnc = 0; // number of notes in chord // PRE remove rests from voices 2, 3 and 4 // console.log("voxMerge: tickFr " + tickFr + " > tickTo " + tickTo); nofT = 0; curScore.startCmd(); mez = curScore.firstMeasure; while (mez) { seg = mez.firstSegment; while (seg) { tock = seg.tick; if (tock >= tickFr && tock < tickTo) { // console.log(tock); for (tt = 0; tt < 4; tt++) { elm = seg.elementAt(tt); if (elm) { if (elm.type == Element.REST) removeElement(elm); } } } seg = seg.nextInMeasure; } mez = mez.nextMeasure; } nofUpd = 0; var mez = curScore.firstMeasure; while (mez) { seg = mez.firstSegment; while (seg) { tock = seg.tick; if (tock >= tickFr && tock < tickTo) { for (tt=0; tt < 4; tt++) { // tracks 0..3 are voices 1..4 of first stave elm = seg.elementAt(tt); if (elm) { // false when a voice is empty if (elm.type == Element.CHORD) { ix = mTock.indexOf(tock) // console.log(tt+". "+tock+" : "+ix); if (ix>=0) { durt = mFadj[ix]; if (elm.duration.ticks != durt) { dot = 0; switch (durt) { case 480: dur = "4"; break; case 240: dur = "8"; break; case 120: dur = "16"; break; case 60: dur = "32"; break; case 960: dur = "2"; break; case 1920: dur = "1"; break; case 720: dur = "4"; dot=1; break; case 360: dur = "8"; dot=1; break; case 180: dur = "16"; break; case 90: dur = "32"; dot=1; break; case 1440: dur = "2"; dot=1; break; case 2880: dur = "1"; dot=1; break; default: { console.log("?tock "+tock+" = "+durt); dur = "64"; } } curScore.selection.select(elm.notes[0]); cmd("pad-note-" + dur); if (dot>0) cmd("pad-dot"); nofUpd++; } if (tt>0) { // if not already in voice 1 nnc = elm.notes.length; for (nn=0; nn= tickFr && tock < tickTo) { for (tt = 1; tt < 4; tt++) { elm = seg.elementAt(tt); if (elm) { if (elm.type == Element.REST) removeElement(elm); } } } seg = seg.nextInMeasure; } mez = mez.nextMeasure; } curScore.endCmd(); } //================================================================================================================================= function mapMerge() { var imez = mMez[0]; var tick = mTick[0]; var face = mFace[0]; var fchk = 0; var tchk = 0; var ix=0; while (mMez[ix] > 0) { fchk = mFace[ix]; // next face to check tchk = mTick[ix]; if (tchk == tick) { // while at same tick if (fchk < face) face = fchk; } else { // found next tick mTock.push(tick); if ( (tick+face) > tchk ) face = tchk - tick; mFadj.push(face); tick = tchk; face = fchk; } ix++; } mTock.push(tick); mFadj.push(face); } //================================================================================================================================= function mapScore() // Build score map variables { var el; var elm; var elm0; var elm1 tickFr = 0; tickTo = tickEnd; nofSel = 0;// initialise to no selection nofSel = curScore.selection.elements.length; if (nofSel > 0) { if (curScore.selection.isRange) { tickFr = curScore.selection.startSegment.tick; if (curScore.selection.endSegment) tickTo = curScore.selection.endSegment.tick; if (tickTo == 0) tickTo = tickEnd; // beyond last seg of score } else { // List type selection rather than range elm0 = curScore.selection.elements[0]; elm1 = curScore.selection.elements[nofSel-1]; if (elm0.type == Element.NOTE && elm1.type == Element.NOTE) { tickFr = elm0.parent.parent.tick; tickTo = elm1.parent.parent.tick+1; } } } tout = ""; tlog = ""; nofM = 0; nofS = 0; nofN = 0; var seg; var elm; var note; var tick; var fval; // face value length in ticks var lent; // actual length in ticks var tupl; // tuplet factor var onti; // ontime var mez = curScore.firstMeasure; var nix = 0; var tt = 0; var ii = 0; var mez = curScore.firstMeasure; while (mez) { ++nofM; seg = mez.firstSegment; while (seg) { if (seg.segmentType == 512) { // chordrest ++nofS; tick = seg.tick; for (tt = 0; tt < 4; ++tt) { // only first 4 tracks in stave elm = seg.elementAt(tt); if (elm) { if (elm.type == Element.CHORD) { // chord fval = elm.duration.ticks; if (elm.tuplet == null) tupl = 0; else { tupl = (elm.tuplet.actualNotes / elm.tuplet.normalNotes).toFixed(3); fval = Math.floor(fval / tupl); } for (var nn in elm.notes) { note = elm.notes[nn]; mIX.push(nofN); mMez.push(nofM); mTick.push(tick); mMIDI.push(note.pitch); mFace.push(fval); mTup.push(tupl); if (note.tieForward != null) mTie.push(1); else if ( note.tieBack != null) { if (zMV) { curScore.selection.select(note); removeElement(note); } else mTie.push(-1); } else { mTie.push(0); } mVox.push(tt+1); if (note.play) { mPlay.push("P"); // "Play" } else { mPlay.push("N"); // "No" } onti = note.playEvents[0].ontime; // OnTime‰ lent = note.playEvents[0].len; // Len‰ ++nofN; } } else if (elm.type == Element.REST) { // rest mIX.push(nofN); mMez.push(nofM); mTick.push(tick); mMIDI.push(0); mFace.push(elm.duration.ticks); mTup.push(0); mTie.push(0); mVox.push(tt+1); mPlay.push("R"); ++nofN; } } } } seg = seg.nextInMeasure; } mez = mez.nextMeasure; } mIX.push(nofN); // end of score mMez.push(0); mTick.push(tickEnd); } //================================================================================================================================= function logMap() { var ii = 0; var tick = 0; tout += "

Score Map

"; tout += "

"; tout += ""; for (ii=0; ii < nofN; ++ii) { tick = mTick[ii]; if (tick >= tickFr && tick < tickTo) { tout += ""; } } tout += "
IXMezTickFaceTupTie"; tout += "VoxMIDIPlay
 " + mIX[ii] + "  " + mMez[ii]; tout += " " + tick + "  " + mFace[ii] tout += " " + mTup[ii] + " " + mTie[ii] + " " + mVox[ii]; tout += " " + mMIDI[ii] + " " + mPlay[ii] + "
 " + nofN + "  0  " + tickEnd + " eos

 

"; } //================================================================================================================================= function logMergeMap() { var ii = 0 var tock = mTock[0]; tout += "

Merge Map

"; tout += "

"; tout += ""; while (tock >= 0) { if (tock >= tickFr && tock < tickTo) { tout += ""; } ii++; tock = mTock[ii]; } tout += "
TickFace
 " + tock + " " + mFadj[ii] + "
 -1 0

 

"; } //================================================================================================================================= // USER INTERFACE id: uiMergeVoice width: 275 height: 125 RowLayout { id: uiRowRun x:15; y: 15 Button { id: btnApply Layout.preferredWidth: 120 Layout.preferredHeight: 30 text: "Merge Voices" style: ButtonStyle { background: Rectangle { border.width: 1; border.color: "#999"; color: "#d1e0d1" radius: 15 } } onClicked: runMain(false) } Button { id: btnViewLog Layout.preferredWidth: 120 Layout.preferredHeight: 30 text: "View Process Map" style: ButtonStyle { background: Rectangle { border.width: 1; border.color: "#999"; color: "#c9dbe8" radius: 15 } } onClicked: runMain(true) } } TextArea { id: runInfo x: 15; y: uiRowRun.y+40 width: 245; height: 55 textMargin: 10 textFormat: TextEdit.PlainText readOnly: true wrapMode: TextEdit.Wrap text: "Merge Voices: " + version + "\nMuseScore: " + Math.floor(mscoreVersion/10000) + "." + Math.floor((mscoreVersion % 10000)/100) + "." + (mscoreVersion % 100) } ApplicationWindow { id: infoWin x: 20; y: 70 width: 400; height: 800 title: "Merge Voices Information" visible: false TextArea { id: popInfo width: infoWin.width; height: infoWin.height textMargin: 15 textFormat: TextEdit.RichText readOnly: true wrapMode: TextEdit.Wrap text: "" } } }