//============================================================================= // MuseScore // Music Composition & Notation // // Anglo Concertina 30 key Tab Plugin // // Copyright (C) 2023 Michal Gorzynski // // Based on the Tin whistle tabulature plugin // Copyright (C) 2020 Jon Gadsden, Dale Larson // // which is based on the Note Names Plugin which is: // Copyright (C) 2012 Werner Schweer // Copyright (C) 2013 - 2016 Joachim Schmitz // Copyright (C) 2014 Jörn Eichler // // and also based on the Recorder Woodwind Tablature plugin: // Copyright (C)2011 Dario Escobedo, Werner Schweer, Jens Iwanenko and others // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 // as published by the Free Software Foundation and appearing in // the file LICENCE.GPL //============================================================================= import QtQuick 2.15 import QtQuick.Dialogs 1.3 import MuseScore 3.0 MuseScore { version: "4.1" description: "This plugin provides fingering diagrams for the Anglo Concertina 30 key Jeffries layout." title: "Anglo Concertina 30 key C/G Tablature" categoryCode: "composing-arranging-tools" property var tabFontName: "Arial" property var fontSize: 8 /* Naming as follows: 1-5 - C Row, left hand 6-10 - G row, left hand 1a-5a - C row, accidentals, left hand 1.-5. - C Row, left hand 6.-10. - G row, left hand 1-5b - right hand accidentals For 20 key layout replace all accidentals with "x" Hint: notes are as follows (e.g. small c means c, capital C means c#) fingerings : [ "c", "C", "d", "D", "e", "f", "F", "g", "G", "a", "A", "b", "c", "C", "d", "D", "e", "f", "F", "g", "G", "a", "A", "b", "c", "C", "d", "D", "e", "f", "F", "g", "G", "a", "A", "b", "c", "C", "d", "D", "e", "f", "F", "g", "G", "a", "A", "b" ] */ property variant buttonPush : [ "1", "x", "x", "x", "1a", "x", "x", "2", "x", "2a", "x", "2", "3", "3a", "7", "x", "4", "x", "x", "5", "5a", "4a", "x", "9", "1.", "2b", "10", "1b", "2.", "x", "x", "6.", "3b", "5b", "x", "7.", "4.", "4b", "8.", "x", "5.", "10.", "x", "9.", "x", "x", "x", "x" ] property variant buttonPull : [ "x", "x", "x", "x", "x", "1a", "x", "1", "x", "6", "2a", "2", "x", "x", "3", "3a", "x", "4", "6", "4a", "x", "5", "5a", "1.", "9", "1b", "2.", "2b", "10", "3.", "6.", "3b", "x", "7.", "4b", "5.", "8.", "x", "5b", "x", "9.", "x", "10.", "x", "x", "x", "x", "x" ] property var dirPush: "push" property var dirPull: "pull" property var direction: dirPush property var directionCount: 0 property var basePitch: 48 function startDirection(key) { //TODO set start direction based on the key? return dirPush } function changeDirection () { if (direction == dirPush) { direction = dirPull } else { direction = dirPush } } function getKey(idx) { directionCount++ if (direction == dirPush) { return buttonPush[idx] } return buttonPull[idx] } function getPushPullChar() { if (direction == dirPush) { return " " } else { return "_" } } function getKeyAndDirection (pitch, basePitch){ var tabText = "" var index = pitch - basePitch if (index < 0) { console.log("Skipped note as it was too low, pitch : " + pitch) return tabText } if (index > 48) { console.log("Skipped note as it was too high, pitch : " + pitch) return tabText } var oldDirection = direction if (directionCount>4) { changeDirection(); } tabText = getKey(index) if (tabText == "x") { changeDirection() tabText = getKey(index) } if (tabText == "x") { changeDirection() tabText = "x" } if (oldDirection == direction) { directionCount++ } else { directionCount=1 } return tabText } function processChord(chord, cursor){ var hasElementPos = cursor.element.posX !== undefined; var currentDirection = direction var foundCountPush=0; var foundCountPull=0; var pitch = -1; for (var i=0; i= 0 && index <=48) { if (buttonPush[index] != "x") { foundCountPush++ } if (buttonPull[index] != "x") { foundCountPull++ } } } if ( foundCountPush > foundCountPull) { direction = dirPush } else if (foundCountPush < foundCountPull) { direction = dirPull } else if (foundCountPush == foundCountPull && directionCount>4) { changeDirection(); } if (direction == currentDirection) { directionCount++ } else { directionCount = 0 } var tabOffsetY = 6.3 for (var i=0; i= 0 && index <=48) { text.text = getKey(index) if (text.text == "x") { //skip missing buttons - don't display x continue } text2.text = getPushPullChar() cursor.add(text) // Add the staff text at the cursor cursor.add(text2) // Add push/pull character // Set text attributes *after* adding element to the score. setTabCharacterFont(text, fontSize) setTabCharacterFont(text2, fontSize) text.offsetY = tabOffsetY text2.offsetY = tabOffsetY+1.3 // Place the whistle hole pattern to be centered under the note. if (hasElementPos) { text.offsetX = chord.posX + 0.25 text2.offsetX = chord.posX + 0.25 } else { text.offsetX = 0.5 text2.offsetX = 0.5 } // Create new text element for next tab placement tabOffsetY +=1.6 text = newElement(Element.STAFF_TEXT) text2 = newElement(Element.STAFF_TEXT) } } } function setTabCharacterFont (text, tabSize) { text.fontFace = tabFontName text.fontSize = tabSize // Vertical align to top. (0 = top, 1 = center, 2 = bottom, 3 = baseline) text.align = 0 // 'Align.TOP' is available in MuseScore 3.3 // Place text to below the staff. text.placement = Placement.BELOW // Turn off note relative placement text.autoplace = false } // For diagnostic use. function dumpObjectEntries(obj, showUndefinedVals, title) { console.log("VV -------- " + title + " ---------- VV") for (var key in obj) { if (showUndefinedVals || (obj[key] != null)) { console.log(key + "=" + obj[key]); } } console.log("^^ -------- " + title + " ---------- ^^") } function renderTablature() { curScore.startCmd(); // select either the full score or just the selected staves var cursor = curScore.newCursor(); var startStaff; var endStaff; var endTick; var fullScore = false; cursor.rewind(1) if (!cursor.segment) { // no selection fullScore = true startStaff = 0; // start with 1st staff endStaff = curScore.nstaves - 1; // and end with last console.log("Full score staves " + startStaff + " - " + endStaff) } else { startStaff = cursor.staffIdx cursor.rewind(2) if (cursor.tick == 0) { // when the selection includes the last measure of the score: // rewind(2) goes behind the last segment (where there's none) // and sets tick=0. Need to fix up tick endTick = curScore.lastSegment.tick + 1 } else { endTick = cursor.tick } endStaff = cursor.staffIdx console.log("Selected staves " + startStaff + " - " + endStaff + " - " + endTick) } // walk through the score for each (tin whistle) staff var pitch var lastPitch = 0 var tabOffsetY // according to the lowest note for the type of whistle for (var staff = startStaff; staff <= endStaff; staff++) { tabOffsetY = 4.3 lastPitch = 0 if (curScore.hasLyrics) { tabOffsetY += 2.8 // try not to clash with any lyrics } // Musescore supports up to 4 voices, but tin whistle uses only one cursor.voice = 0 cursor.rewind(1) // beginning of selection cursor.staffIdx = staff if (fullScore) // no selection cursor.rewind(0) // beginning of score // Some positioning values used to render grace notes. // Note that these are determined by observation of the results // and appear to be consistent. It would be better to find the actual // location of the grace notes but that capability isn't exposed until // version 3.3 of MuseScore. So for the earlier releases we use these // heuristics. var graceNoteWidth = 1.2 // Assumed grace note width var graceNoteNudgeLeading = 0.0 // Assumed leading note cluster offset from main note var graceNoteNudgeTrailing = 3.0 // Assumed trailing note cluster offset from main note // TinWhistleTab.ttf character image font sizes. var tabFontSizeNormal = fontSize // Size of normal sized whistle tab image var tabFontSizeGrace = fontSize // Size of grace note sized whistle tab image while (cursor.segment && (fullScore || cursor.tick < endTick)) { if (cursor.element && cursor.element.type == Element.CHORD) { var text = newElement(Element.STAFF_TEXT); var text2 = newElement(Element.STAFF_TEXT); // Scan grace notes for existence and add to appropriate lists... var leadingLifo = new Array(); var trailingFifo = new Array(); var graceChords = cursor.element.graceNotes; // Determine if Element.posX and Element.posY is supported. (MuseScore 3.3+) var hasElementPos = cursor.element.posX !== undefined; // Build separate lists of leading and trailing grace note chords. if (graceChords.length > 0) { //TODO later // Determine if Note.noteType is supported. (MuseScore 3.2.1) var hasNoteType = graceChords[0].notes[0] !== undefined; if (hasNoteType) { for (var chordNum = 0; chordNum < graceChords.length; chordNum++) { var noteType = graceChords[chordNum].notes[0].noteType if (noteType == NoteType.GRACE8_AFTER || noteType == NoteType.GRACE16_AFTER || noteType == NoteType.GRACE32_AFTER) { trailingFifo.unshift(graceChords[chordNum]) } else { leadingLifo.push(graceChords[chordNum]) } } } else { // Assume all grace notes are of the leading variety since // the NoteType capability doesn't exist in the running version of // MuseScore. for (var chordNum = 0; chordNum < graceChords.length; chordNum++) { leadingLifo.unshift(graceChords[chordNum]) } } } // First render leading grace notes if any exist... if (leadingLifo.length > 0) { // Compute starting offset to location of the lead leftmost grace note. var graceLocationX = leadingLifo.length * -graceNoteWidth + graceNoteNudgeLeading; for (var chordNum = 0; chordNum < leadingLifo.length; chordNum++) { // there are no chords when playing the tin whistle var chord = leadingLifo[chordNum]; pitch = chord.notes[0].pitch; // keep track of repeated notes lastPitch = pitch text.text = getKeyAndDirection(pitch, basePitch) // Note: Set text attributes *after* adding element to the score. cursor.add(text) // grace notes are shown a bit smaller setTabCharacterFont(text, tabFontSizeGrace) if (hasElementPos) { // X position the tab image under the grace note text.offsetX = chord.posX } else { // there seems to be no way of knowing the exact horizontal pos. // of a grace note, so we have to guess: text.offsetX = graceLocationX // Move to the next note location. graceLocationX += graceNoteWidth; } // (See the note below about behavior of the text.offsetY property.) text.offsetY = tabOffsetY // place the tab below the staff // Create new text element for next tab placement text = newElement(Element.STAFF_TEXT) } } // Next process the parent note... if (cursor.element.notes[0].tieBack != null) { // don't add tab if parent note is tied to previous note console.log("Skipped tied parent note, pitches : " + pitch + ", " + lastPitch) } else { // there are no chords when playing the tin whistle, so use first note var chord = cursor.element; processChord(chord,cursor) } // end if tied back // Finally process trailing grace notes if any exist... if (trailingFifo.length > 0) { // Set the starting offset to location of the first trailing grace note. var graceLocationX = graceNoteNudgeTrailing; for (var chordNum = 0; chordNum < trailingFifo.length; chordNum++) { // there are no chords when playing the tin whistle var chord = trailingFifo[chordNum]; pitch = chord.notes[0].pitch; // keep track of repeated notes lastPitch = pitch text.text = getKeyAndDirection(pitch, basePitch) // Note: Set text attributes *after* adding element to the score. cursor.add(text) // grace notes are shown a bit smaller setTabCharacterFont(text, tabFontSizeGrace) if (hasElementPos) { // X position the tab image under the grace note text.offsetX = chord.posX } else { // there seems to be no way of knowing the exact horizontal pos. // of a grace note, so we have to guess: text.offsetX = graceLocationX // Move to the next note location. graceLocationX += graceNoteWidth; } // (See the note below about behavior of the text.offsetY property.) text.offsetY = tabOffsetY // place the tab below the staff // Create new text element for next tab placement text = newElement(Element.STAFF_TEXT) } } } // end if CHORD cursor.next() } // end while segment } // end for staff curScore.endCmd(); quit() } onRun: { console.log("Hello anglo concertina tablature") if (typeof curScore === 'undefined') quit() renderTablature() quit() } // end onRun }