Move fingering descriptions to separate file

This allows the user to modify the instruments,
 without understanding the code.
Some documentation is included,
 and a lot of names are changed to be closer to the domain.
This commit is contained in:
2026-05-13 10:20:11 +02:00
parent c915c48610
commit 8dc9b87035
2 changed files with 182 additions and 116 deletions

View File

@ -0,0 +1,148 @@
// +--------+
// | COLORS |
// +--------+
// You can change the color of the fingering chart here.
// Colors can be hex codes ("#123456") or names ("green")
function getColor(note) {
const colorMap = {
C: "#ed1c24",
"C#": "#f37021",
D: "#f8931e",
"D#": "#ffc20e",
E: "#fff200",
F: "#bed630",
"F#": "#72bf44",
G: "#00a29d",
"G#": "#007dc5",
A: "#49479d",
"A#": "#8d5ca6",
B: "#c9277d",
}
return colorMap[note] || "gray"
}
//---------------------------------------------------------
// INSTRUMENTS
//---------------------------------------------------------
// Each woodwind has the form:
// {
// name:
// fingering:
// format:
// },
//
// NAME: can be anything descriptive.
//
// FINGERING:
// Each note that can be played, then its holes.
// Holes are listed starting from the mouthpiece.
// You can use these symbols to represent whether the holes are covered:
// 0 = ○
// o = ⦶
// 1 = ●
// | = ◐
// - = ◒
//
// FORMAT:
// Decide how the fingering will be displayed.
// "$note" will be replaced with the name of the note,
// "$1" "$2" etc. will be replaced by the hole symbols, with $1 being closest to the mouthpiece.
const woodwinds = [
{
name: "Soprano Recorder",
fingering: {
"C4": "11111111",
"C#4": "1111111|",
"D4": "11111110",
"D#4": "111111|o",
"E4": "111111oo",
"F4": "11110011",
"F#4": "1111011o",
"G4": "111100oo",
"G#4": "111011|o",
"A4": "111000oo",
"A#4": "110110oo",
"B4": "110000oo",
"C5": "101000oo",
"C#5": "011000oo",
"D5": "001000oo",
"D#5": "0011111o",
"E5": "-11111oo",
"F5": "-111101o",
"F#5": "-11101oo",
"G5": "-11100oo",
"G#5": "-11010oo",
"A5": "-11000oo",
"A#5": "-110011|",
"B5": "-11011oo",
"C6": "-10011oo",
"C#6": "-1011011",
"D6": "-1011011",
"D#6": "-011011o",
},
format:
`$note
$1
$2
$3
$4
$5
$6
$7
$8`
},
{
"name": "Alto Recorder",
"fingering": {
"F4": "11111111",
"F#4":"1111111|",
"G4": "11111110",
"G#4":"111111|o",
"A4": "111111oo",
"A#4":"11110011",
"B4": "1111011o",
"C5": "111100oo",
"C#5":"111011|o",
"D5": "111000oo",
"D#5":"110110oo",
"E5": "110000oo",
"F5": "101000oo",
"F#5":"011000oo",
"G5": "001000oo",
"G#5":"0011111o",
"A5": "-11111oo",
"A#5":"-111101o",
"B5": "-11101oo",
"C6": "-11100oo",
"C#6":"-11010oo",
"D6": "-11000oo",
"D#6":"-110011|",
"E6": "-11011oo",
"F6": "-10011oo",
"F#6": "-1011011",
"G6": "-1011011",
"G#6": "-011011o",
},
format:
`$note
$1
$2
$3
$4
$5
$6
$7
$8`
},
];

View File

@ -29,6 +29,8 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import MuseScore 3.0 import MuseScore 3.0
import "Fingering.js" as Fingering
MuseScore { MuseScore {
id: mscore id: mscore
version: "4.4" version: "4.4"
@ -68,7 +70,7 @@ MuseScore {
property int userJustification: 1 // 0=left,1=center,2=right property int userJustification: 1 // 0=left,1=center,2=right
property real userOffsetY: 1.7 property real userOffsetY: 1.7
property real userLineSpacing: 0.5 property real userLineSpacing: 0.5
property string userFormatString: "" // mirrors current profile's formatString property string userFormatString: "" // mirrors current profile's format
property string userFontFamily: "Menlo, Consolas, Liberation Mono, Courier New, monospace" property string userFontFamily: "Menlo, Consolas, Liberation Mono, Courier New, monospace"
property bool userTransposed: false property bool userTransposed: false
@ -83,73 +85,6 @@ MuseScore {
property var history: 0 property var history: 0
property var modified: false property var modified: false
//---------------------------------------------------------
// FINGERING DICTIONARY
//---------------------------------------------------------
//Dictionary for your woodwind's fingerings, specifying the note and the fingering pattern (for whistles: left-> right, starting from the fipple end).
//NOTE: Note specification convention here uses sharps, so e.g. a Bb whistle needs to be defined as A#
//
// ○ = 0
// ● = 1
// ◐ = |
// ◐ = -
// ○ = o
//Example fingering specification for G#5 on a high D whistle
// |---------------------------------------------
//windway -> | | | ● ● ◐ ○ ○ ○ |
// |---------------------------------------------
// "G#5": " 1 1 | 0 0 0
// | | | | | |
// variables in format string: $1 $2 $3 $4 $5 $6 and then $note would be replaced with 'G#5'
// These variables are replaced by the corresponding symbol or text when run.
//Can have any number of holes in the specification
//NOTE: This plugin was generated from a maker's perspective, and thus the note names correspond with the sounding pitch, and not the
// typical octave shift down presented in notations. The dictionary can easily be edited to reflect the transposition, however, if desired.
//Whistle definitions
property var defaultProfiles: [
{
"name": "Soprano Recorder",
"fingeringDict": {
"C4": "11111111",
"C#4": "1111111|",
"D4": "11111110",
"D#4": "111111|o",
"E4": "111111oo",
"F4": "11110011",
"F#4": "1111011o",
"G4": "111100oo",
"G#4": "111011|o",
"A4": "111000oo",
"A#4": "110110oo",
"B4": "110000oo",
"C5": "101000oo",
"C#5": "011000oo",
"D5": "001000oo",
"D#5": "0011111o",
"E5": "-11111oo",
"F5": "-111101o",
"F#5": "-11101oo",
"G5": "-11100oo",
"G#5": "-11010oo",
"A5": "-11000oo",
"A#5": "-110011|",
"B5": "-11011oo",
"C6": "-10011oo",
"C#6": "-1011011",
"D6": "-1011011",
"D#6": "-011011o",
},
formatString: "$note\n\n$1\n—\n$2\n$3\n$4\n—\n$5\n$6\n$7\n$8"
},
]
// Runtime copy of profiles (loaded/saved via preferences) // Runtime copy of profiles (loaded/saved via preferences)
property var profiles: [] property var profiles: []
property int currentProfileIndex: 0 property int currentProfileIndex: 0
@ -160,16 +95,16 @@ MuseScore {
function getCurrentFingeringDict() { function getCurrentFingeringDict() {
return profiles.length > 0 ? profiles[currentProfileIndex].fingeringDict : {} return profiles.length > 0 ? profiles[currentProfileIndex].fingering : {}
} }
function getCurrentFormatString() { function getCurrentFormatString() {
return profiles.length > 0 ? profiles[currentProfileIndex].formatString : "" return profiles.length > 0 ? profiles[currentProfileIndex].format : ""
} }
function setCurrentFormatString(newFormat) { function setCurrentFormatString(newFormat) {
if (profiles.length > 0) { if (profiles.length > 0) {
profiles[currentProfileIndex].formatString = newFormat profiles[currentProfileIndex].format = newFormat
userFormatString = newFormat userFormatString = newFormat
} }
} }
@ -234,8 +169,8 @@ MuseScore {
// Save profiles as JSON string // Save profiles as JSON string
var profilesForStorage = profiles.map(p => ({ var profilesForStorage = profiles.map(p => ({
name: p.name, name: p.name,
fingeringDict: p.fingeringDict, fingering: p.fingering,
formatString: p.formatString format: p.format
})) }))
pluginSettings.storedProfiles = JSON.stringify(profilesForStorage) pluginSettings.storedProfiles = JSON.stringify(profilesForStorage)
pluginSettings.storedCurrentProfile = currentProfileIndex pluginSettings.storedCurrentProfile = currentProfileIndex
@ -257,24 +192,24 @@ MuseScore {
var loaded = JSON.parse(profilesStr) var loaded = JSON.parse(profilesStr)
profiles = loaded.map(p => ({ profiles = loaded.map(p => ({
name: p.name, name: p.name,
fingeringDict: p.fingeringDict, fingering: p.fingering,
formatString: p.formatString format: p.format
})) }))
} catch (e) { } catch (e) {
console.log("Failed to parse saved profiles, using defaults") console.log("Failed to parse saved profiles, using defaults")
error("Failed to parse saved profiles, using defaults") error("Failed to parse saved profiles, using defaults")
profiles = defaultProfiles.map(p => ({ profiles = Fingering.woodwinds.map(p => ({
name: p.name, name: p.name,
fingeringDict: JSON.parse(JSON.stringify(p.fingeringDict)), fingering: JSON.parse(JSON.stringify(p.fingering)),
formatString: p.formatString format: p.format
})) }))
} }
} else { } else {
profiles = defaultProfiles.map(p => ({ profiles = Fingering.woodwinds.map(p => ({
name: p.name, name: p.name,
fingeringDict: JSON.parse(JSON.stringify(p.fingeringDict)), fingering: JSON.parse(JSON.stringify(p.fingering)),
formatString: p.formatString format: p.format
})) }))
} }
@ -304,11 +239,11 @@ MuseScore {
function resetToDefaults() { function resetToDefaults() {
getHistory().begin() // if you have undo/redo getHistory().begin() // if you have undo/redo
// 1. Restore profiles from defaultProfiles // 1. Restore profiles from woodwinds
profiles = defaultProfiles.map(p => ({ profiles = Fingering.woodwinds.map(p => ({
name: p.name, name: p.name,
fingeringDict: JSON.parse(JSON.stringify(p.fingeringDict)), fingering: JSON.parse(JSON.stringify(p.fingering)),
formatString: p.formatString format: p.format
})) }))
currentProfileIndex = 0 currentProfileIndex = 0
@ -335,8 +270,8 @@ MuseScore {
// 4. Persist all changes to Settings // 4. Persist all changes to Settings
pluginSettings.storedProfiles = JSON.stringify(profiles.map(p => ({ pluginSettings.storedProfiles = JSON.stringify(profiles.map(p => ({
name: p.name, name: p.name,
fingeringDict: p.fingeringDict, fingering: p.fingering,
formatString: p.formatString format: p.format
}))) })))
pluginSettings.storedCurrentProfile = currentProfileIndex pluginSettings.storedCurrentProfile = currentProfileIndex
pluginSettings.storedFontSize = userFontSize pluginSettings.storedFontSize = userFontSize
@ -405,7 +340,7 @@ MuseScore {
//--------------------------------------------------------- //---------------------------------------------------------
//Build fingering text using binary representation of the fingering for a note. //Build fingering text using binary representation of the fingering for a note.
function buildFingeringText(binaryString, formatString, noteName) { function buildFingeringText(binaryString, format, noteName) {
const open = "0" const open = "0"
const closed = "1" const closed = "1"
const half_ver = "|" const half_ver = "|"
@ -414,7 +349,7 @@ MuseScore {
if (!binaryString || binaryString.length < 1) if (!binaryString || binaryString.length < 1)
return "Invalid fingering pattern. Length is wrong" return "Invalid fingering pattern. Length is wrong"
if (!formatString || typeof formatString !== "string") if (!format || typeof format !== "string")
return "ERR" return "ERR"
var holeCount = binaryString.length var holeCount = binaryString.length
@ -436,7 +371,7 @@ MuseScore {
// Clone format string // Clone format string
//------------------------------------------------- //-------------------------------------------------
var output = formatString var output = format
//------------------------------------------------- //-------------------------------------------------
// Replace note placeholder ($note) with note name // Replace note placeholder ($note) with note name
@ -487,24 +422,6 @@ MuseScore {
// APPLY TEXT FORMATTING // APPLY TEXT FORMATTING
//--------------------------------------------------------- //---------------------------------------------------------
function getColor(noteName) {
const note = noteName.slice(0, -1)
const colorMap = {
C: "#ed1c24",
"C#": "#f37021",
D: "#f8931e",
"D#": "#ffc20e",
E: "#fff200",
F: "#bed630",
"F#": "#72bf44",
G: "#00a29d",
"G#": "#007dc5",
A: "#49479d",
"A#": "#8d5ca6",
B: "#c9277d",
}
return colorMap[note] || "gray"
}
function formatText(text) { function formatText(text) {
console.log("Formatting text - setting fontSize to:", userFontSize) console.log("Formatting text - setting fontSize to:", userFontSize)
@ -671,7 +588,8 @@ MuseScore {
cursor.add(text) cursor.add(text)
formatText(text) formatText(text)
text.color = getColor(noteName) const note = noteName.slice(0, -1)
text.color = Fingering.getColor(note)
// Verify the text object has the properties set // Verify the text object has the properties set
console.log("Text fontSize after format:", text.fontSize) console.log("Text fontSize after format:", text.fontSize)
@ -839,11 +757,11 @@ MuseScore {
var oldProfileFormat = getCurrentFormatString() var oldProfileFormat = getCurrentFormatString()
getHistory().add( getHistory().add(
function() { function() {
// Undo: restore both userFormatString and the profile's formatString // Undo: restore both userFormatString and the profile's format
userFormatString = oldFormat userFormatString = oldFormat
formatInput.text = oldFormat formatInput.text = oldFormat
if (profiles.length > 0) { if (profiles.length > 0) {
profiles[currentProfileIndex].formatString = oldProfileFormat profiles[currentProfileIndex].format = oldProfileFormat
} }
}, },
function() { function() {
@ -851,7 +769,7 @@ MuseScore {
userFormatString = format userFormatString = format
formatInput.text = format formatInput.text = format
if (profiles.length > 0) { if (profiles.length > 0) {
profiles[currentProfileIndex].formatString = format profiles[currentProfileIndex].format = format
} }
}, },
"format string" "format string"
@ -871,10 +789,10 @@ MuseScore {
// Event handlers (call setters and save) // Event handlers (call setters and save)
//--------------------------------------------------------- //---------------------------------------------------------
function formatStringChanged(formatString) { function formatChanged(format) {
getHistory().begin() getHistory().begin()
setModified(true) setModified(true)
setUserFormatString(formatString) setUserFormatString(format)
getHistory().end() getHistory().end()
saveSettings() saveSettings()
} }
@ -928,7 +846,7 @@ MuseScore {
function profileChanged(index) { function profileChanged(index) {
if (index < 0 || index >= profiles.length) return if (index < 0 || index >= profiles.length) return
currentProfileIndex = index currentProfileIndex = index
userFormatString = profiles[index].formatString userFormatString = profiles[index].format
formatInput.text = userFormatString formatInput.text = userFormatString
updatePreview() updatePreview()
saveSettings() // immediately save profile change saveSettings() // immediately save profile change
@ -1358,7 +1276,7 @@ MuseScore {
} }
property var previousText: userFormatString property var previousText: userFormatString
onEditingFinished: { onEditingFinished: {
formatStringChanged(formatInput.text) formatChanged(formatInput.text)
updatePreview() updatePreview()
} }
onTextChanged: { onTextChanged: {