Skip to content

Commit 49aba50

Browse files
authored
Merge pull request #763 from rpatters1/RGP/delint-music-xml
Add musicxml massager script
2 parents 5cbd049 + 62d3d29 commit 49aba50

File tree

2 files changed

+187
-2
lines changed

2 files changed

+187
-2
lines changed

src/articulation_reset_auto_positioning.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
function plugindef()
22
finaleplugin.Author = "Robert Patterson"
33
finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
4-
finaleplugin.Version = "1.1"
5-
finaleplugin.Date = "July 29, 2024"
4+
finaleplugin.Version = "1.1.1"
5+
finaleplugin.Date = "September 22, 2024"
66
finaleplugin.CategoryTags = "Articulation"
77
finaleplugin.MinFinaleVersionRaw = 0x1a000000
88
finaleplugin.MinJWLuaVersion = 0.58

src/musicxml_massage_export.lua

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
function plugindef()
2+
finaleplugin.RequireDocument = false
3+
finaleplugin.RequireSelection = false
4+
finaleplugin.NoStore = true
5+
finaleplugin.Author = "Robert Patterson"
6+
finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
7+
finaleplugin.Version = "1.0"
8+
finaleplugin.Date = "September 24, 2024"
9+
finaleplugin.CategoryTags = "Document"
10+
finaleplugin.MinJWLuaVersion = 0.74
11+
finaleplugin.Notes = [[
12+
This script reads a musicxml file exported from Finale and makes modifies it to
13+
improve the importing into Dorico or MuseScore. The best process is as follows:
14+
15+
1. Export your document as uncompressed MusicXML.
16+
2. Run this plugin on the output *.musicxml document.
17+
3. The massaged file name has " massaged" appended to the file name.
18+
3. Import the massaged *.musicxml into Dorico or MuseScore.
19+
20+
Here is a list of some of the changes the script makes:
21+
22+
- 8va/8vb and 15ma/15mb symbols are extended to include the last note and extended left to include leading grace notes.
23+
24+
Due to a limitation in the xml parser, all xml processing instructions are removed. These are metadata that neither
25+
Dorico nor MuseScore use, so their removal should not affect importing into those programs.
26+
]]
27+
return "Massage MusicXML...", "", "Massages the MusicXML for the current open document."
28+
end
29+
30+
local text_extension = ".musicxml"
31+
32+
local function remove_processing_instructions(file_path)
33+
-- Open the original file for reading
34+
local input_file <close> = io.open(file_path, "r")
35+
if not input_file then
36+
error("Cannot open file: " .. file_path)
37+
end
38+
-- Read the contents of the file
39+
local lines = {}
40+
for line in input_file:lines() do
41+
-- Keep the XML declaration (<?xml ... ?>), remove other processing instructions (<?...?>)
42+
if line:match("^%s*<%?xml") or not line:match("^%s*<%?.*%?>") then
43+
table.insert(lines, line)
44+
end
45+
end
46+
-- Close the input file
47+
input_file:close()
48+
-- Open the file for writing (overwrite the original file)
49+
local output_file <close> = io.open(file_path, "w")
50+
if not output_file then
51+
error("Cannot open file for writing: " .. file_path)
52+
end
53+
-- Write the cleaned lines to the file
54+
for _, line in ipairs(lines) do
55+
output_file:write(line .. "\n")
56+
end
57+
-- Close the output file
58+
output_file:close()
59+
end
60+
61+
function do_open_dialog(document)
62+
local path_name = finale.FCString()
63+
local file_name = finale.FCString()
64+
local file_path = finale.FCString()
65+
if document then
66+
document:GetPath(file_path)
67+
file_path:SplitToPathAndFile(path_name, file_name)
68+
end
69+
local full_file_name = file_name.LuaString
70+
local extension = finale.FCString(file_name.LuaString)
71+
extension:ExtractFileExtension()
72+
if extension.Length > 0 then
73+
file_name:TruncateAt(file_name:FindLast("." .. extension.LuaString))
74+
end
75+
file_name:AppendLuaString(text_extension)
76+
local open_dialog = finale.FCFileOpenDialog(finenv.UI())
77+
open_dialog:SetWindowTitle(finale.FCString("Open MusicXML for " .. full_file_name))
78+
open_dialog:AddFilter(finale.FCString("*" .. text_extension), finale.FCString("MusicXML File"))
79+
open_dialog:SetInitFolder(path_name)
80+
open_dialog:SetFileName(file_name)
81+
open_dialog:AssureFileExtension(text_extension)
82+
if not open_dialog:Execute() then
83+
return nil
84+
end
85+
local selected_file_name = finale.FCString()
86+
open_dialog:GetFileName(selected_file_name)
87+
return selected_file_name.LuaString
88+
end
89+
90+
function fix_octave_shift(xml_measure)
91+
for xml_direction in xmlelements(xml_measure, "direction") do
92+
local xml_direction_type = xml_direction:FirstChildElement("direction-type")
93+
if xml_direction_type then
94+
local octave_shift = xml_direction_type:FirstChildElement("octave-shift")
95+
if octave_shift then
96+
local direction_copy = xml_direction:DeepClone(xml_direction:GetDocument())
97+
local shift_type = octave_shift:Attribute("type")
98+
if shift_type == "stop" then
99+
local next_note = xml_direction:NextSiblingElement("note")
100+
if next_note and not next_note:FirstChildElement("rest") then
101+
xml_measure:DeleteChild(xml_direction)
102+
xml_measure:InsertAfterChild(next_note, direction_copy)
103+
end
104+
elseif shift_type == "up" or shift_type == "down" then
105+
local sign = shift_type == "down" and 1 or -1 -- direction to transpose grace notes
106+
local octaves = (octave_shift:IntAttribute("size", 8) - 1) / 7
107+
local prev_grace_note
108+
local prev_note = xml_direction:PreviousSiblingElement("note")
109+
while prev_note do
110+
if not prev_note:FirstChildElement("rest") and prev_note:FirstChildElement("grace") then
111+
prev_grace_note = prev_note
112+
local pitch = prev_note:FirstChildElement("pitch")
113+
local octave = pitch and pitch:FirstChildElement("octave")
114+
if octave then
115+
octave:SetIntText(octave:IntText() + sign*octaves)
116+
end
117+
else
118+
break
119+
end
120+
prev_note = prev_note:PreviousSiblingElement("note")
121+
end
122+
if prev_grace_note then
123+
xml_measure:DeleteChild(xml_direction)
124+
local prev_element = prev_grace_note:PreviousSiblingElement()
125+
if prev_element then
126+
xml_measure:InsertAfterChild(prev_element, direction_copy)
127+
else
128+
xml_measure:InsertFirstChild(direction_copy)
129+
end
130+
end
131+
end
132+
end
133+
end
134+
end
135+
end
136+
137+
function process_xml(score_partwise)
138+
for xml_part in xmlelements(score_partwise, "part") do
139+
for xml_measure in xmlelements(xml_part, "measure") do
140+
fix_octave_shift(xml_measure)
141+
end
142+
end
143+
end
144+
145+
function append_massaged_to_filename(filepath)
146+
-- Extract the path, filename, and extension
147+
local path, filename, extension = filepath:match("^(.-)([^\\/]-)%.([^\\/%.]+)$")
148+
149+
-- Check if the path extraction was successful
150+
if not path or not filename or not extension then
151+
error("Invalid file path format")
152+
end
153+
154+
-- Construct the new file path with " massaged" appended to the filename
155+
local new_filepath = path .. filename .. " massaged." .. extension
156+
return new_filepath
157+
end
158+
159+
function music_xml_massage_export()
160+
local documents = finale.FCDocuments()
161+
documents:LoadAll()
162+
local document = documents:FindCurrent()
163+
local xml_file = do_open_dialog(document)
164+
if not xml_file then
165+
return
166+
end
167+
-- tinyxml2 can't parse processing instructions, so remove them
168+
remove_processing_instructions(xml_file) -- hopefully not necessary forever
169+
local musicxml = tinyxml2.XMLDocument()
170+
local result = musicxml:LoadFile(xml_file)
171+
if result ~= tinyxml2.XML_SUCCESS then
172+
error("Unable to process " .. xml_file .. ". " .. musicxml:ErrorStr())
173+
return
174+
end
175+
local score_partwise = musicxml:FirstChildElement("score-partwise")
176+
if not score_partwise then
177+
error("File " .. xml_file .. " does not appear to be a Finale-exported MusicXML file.")
178+
end
179+
process_xml(score_partwise)
180+
local output_name = append_massaged_to_filename(xml_file)
181+
musicxml:SaveFile(output_name)
182+
finenv.UI():AlertInfo("Processed to " .. output_name .. ".", "Processed File")
183+
end
184+
185+
music_xml_massage_export()

0 commit comments

Comments
 (0)