Skip to content

Instantly share code, notes, and snippets.

@jaredly
Created March 27, 2022 16:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jaredly/046fdaff506523789ad9b333b293d24e to your computer and use it in GitHub Desktop.
Save jaredly/046fdaff506523789ad9b333b293d24e to your computer and use it in GitHub Desktop.
Here's a plugin to merge several voices into a single piano part

Look for the EDIT HERE bits to customize it for your use. Developed w/ musescore 3.5

import QtQuick 2.0
import MuseScore 3.0
MuseScore {
menuPath: "Plugins.pluginName"
description: "Description goes here"
version: "1.0"
onRun: {
var loops = 0
function bail(msg) {
if (loops++ > 10000) {
throw new Error('infinite ' + msg)
}
}
function forEach(arr, fn) {
for (var i=0; i<arr.length; i++) {
fn(arr[i], i)
}
}
function map(arr, fn) {
var res = []
forEach(arr, function(v, i) {
res.push(fn(v, i))
})
return res
}
// ##### DETERMINE START & END ######
// Ok first pass, can I just copy.
var cursor = curScore.newCursor()
cursor.rewind(1)
console.log('at start', cursor.tick)
var endTick;
var startTick;
var sel
if (!cursor.segment) {
endTick = curScore.lastSegment.tick + 1
startTick = 0
console.log('no selection')
// return
} else {
startTick = cursor.tick
cursor.rewind(2)
if (cursor.tick === 0) {
endTick = curScore.lastSegment.tick + 1
} else {
endTick = cursor.tick;
}
sel = {start: startTick, end: endTick, sst: curScore.selection.startStaff, est: curScore.selection.endStaff}
curScore.selection.clear()
cursor = curScore.newCursor()
}
function restoreSelection() {
if (sel) {
curScore.selection.selectRange(sel.start, sel.end, sel.sst, sel.est)
}
}
console.log('Stavrs', curScore.nstaves)
cursor.rewindToTick(startTick)
var mticks = []
var m = curScore.firstMeasure
console.log(m.firstSegment.tick, m.nextMeasure)
while (m.firstSegment.tick <= endTick) {
bail('masure')
if (m.firstSegment.tick > startTick) {
mticks.push(m.firstSegment.tick)
}
m = m.nextMeasure
if (!m) {
// TODO: Figure out how to make the last measure work.
// mticks.push(curScore.lastSegment.tick + 1)
break
}
}
console.log(JSON.stringify(mticks))
cursor.rewindToTick(startTick)
function gatherMeasure(sources) {
var state = {
last: mticks[0],
tracks: [],
ticks: []
}
forEach(sources, function(staff) {
for (var voice=0; voice<4; voice++) {
state.tracks.push([])
}
})
var nextTicks = []
function hasTick(tick) {
var has = false
forEach(nextTicks, function(t) {
has = has || t.tick === tick
})
return has
}
while(cursor.tick < mticks[0]) {
bail('inner')
state.ticks.push({tick: cursor.tick, track: cursor.track})
forEach(sources, function(staff, i) {
for (var voice=0; voice<4; voice++) {
var ti = i * 4 + voice;
cursor.track = staff * 4 + voice
if (!cursor.element) {
state.tracks[ti].push(null)
continue
}
if (cursor.element.type === Element.CHORD || cursor.element.type === Element.REST) {
var tick = cursor.element.actualDuration.ticks
if (!hasTick(cursor.tick + tick)) {
nextTicks.push({tick: cursor.tick + tick, track: cursor.track})
}
} else {
console.log(cursor.element.type)
}
if (cursor.element.type === Element.CHORD) {
state.tracks[ti].push(map(cursor.element.notes, function(note) {
return {
num: cursor.element.actualDuration.numerator,
denom: cursor.element.actualDuration.denominator,
pitch: note.pitch,
}
}))
} else {
state.tracks[ti].push([{
num: cursor.element.actualDuration.numerator,
denom: cursor.element.actualDuration.denominator,
pitch: null,
}])
}
}
})
if (!nextTicks.length) {
console.log('nothing at', cursor.tick)
break
}
var mint = null
var mi
forEach(nextTicks, function(nt, i) {
if (mint == null || nt.tick < mint.tick) {
mint = nt
mi = i
}
})
cursor.track = mint.track
cursor.rewindToTick(mint.tick)
nextTicks.splice(mi, 1)
}
return state
}
function pasteMeasure(state, dest) {
// Which tracks are used in this measure?
var used = []
forEach(state.tracks, function(track, i) {
var empty = true
forEach(track, function(t) {
if (t !== null) {
forEach(t, function(n) {
if (n.pitch != null) {
empty = false
}
})
}
})
if (!empty) {
used.push(i)
}
})
forEach(state.ticks, function(tick, i) {
cursor.track = tick.track
cursor.rewindToTick(tick.tick)
forEach(used, function(track, ti) {
cursor.track = dest * 4 + ti
var note = state.tracks[track][i]
if (note != null) {
// console.log('adding', track, dest * 4 + ti, tick, cursor.staffIdx, cursor.voice)
// console.log(JSON.stringify(note))
forEach(note, function(note, i) {
cursor.setDuration(note.num, note.denom)
if (note.pitch === null) {
cursor.addRest()
} else {
cursor.addNote(note.pitch, i > 0)
}
cursor.rewindToTick(tick.tick)
})
}
})
})
}
while(cursor.tick < endTick) {
bail('outer')
var at = cursor.tick
// EDIT HERE: [1, 2] is the list of *source staves* for the top piano staff
var state = gatherMeasure([1, 2])
console.log(JSON.stringify(state))
// EDIT HERE: 6 is the *destination staff*
pasteMeasure(state, 6)
var last = state.last
cursor.rewindToTick(at)
// EDIT HERE: These are the sources for the bottom piano staff
state = gatherMeasure([3, 4, 5])
console.log(JSON.stringify(state))
pasteMeasure(state, 7)
mticks.shift()
if (state.ticks.length === 0) {
console.log('No more ticks')
break
}
cursor.track = 0
if (last < curScore.lastSegment.tick + 1) {
cursor.rewindToTick(last)
} else {
break
}
}
restoreSelection()
Qt.quit()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment