Skip to content

Commit 9934547

Browse files
committed
Update musicxml_massage_export.lua
So I have a folder with 50 MusicXML export files and I thought it would be easier to add folder scanning to this script than to massage them individually. It wasn't, but at least it was more interesting. I tried hard to not break anything in the original.
1 parent 0f36ac9 commit 9934547

File tree

1 file changed

+181
-74
lines changed

1 file changed

+181
-74
lines changed

src/musicxml_massage_export.lua

Lines changed: 181 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ function plugindef()
22
finaleplugin.RequireDocument = false
33
finaleplugin.RequireSelection = false
44
finaleplugin.NoStore = true
5-
finaleplugin.Author = "Robert Patterson"
5+
finaleplugin.Author = "Robert Patterson (folder scanning added by Carl Vine)"
66
finaleplugin.Copyright = "CC0 https://creativecommons.org/publicdomain/zero/1.0/"
7-
finaleplugin.Version = "1.0.1"
8-
finaleplugin.Date = "September 25, 2024"
7+
finaleplugin.Version = "1.0.4"
8+
finaleplugin.Date = "October 1, 2024"
9+
finaleplugin.LoadLuaOSUtils = true
910
finaleplugin.CategoryTags = "Document"
1011
finaleplugin.MinJWLuaVersion = 0.74
1112
finaleplugin.Notes = [[
@@ -24,67 +25,91 @@ function plugindef()
2425
Due to a limitation in the xml parser, all xml processing instructions are removed. These are metadata that neither
2526
Dorico nor MuseScore use, so their removal should not affect importing into those programs.
2627
]]
27-
return "Massage MusicXML...", "", "Massages MusicXML to make it easier to import to Dorico and MuseScore."
28+
return "Massage MusicXML...",
29+
"Massage MusicXML",
30+
"Massages MusicXML to make it easier to import to Dorico and MuseScore."
2831
end
2932

30-
local text_extension = ".musicxml"
33+
local xml_extension = ".musicxml"
34+
local add_to_filename = "massaged"
3135

32-
local function remove_processing_instructions(file_path, output_name)
33-
-- Open the original file for reading
34-
local input_file <close> = io.open(file_path, "r")
36+
local function alert_error(file_list)
37+
local msg = (#file_list > 1 and "These files do not " or "This file does not ")
38+
.. "appear to be MusicXML exported from Finale:\n\n"
39+
.. table.concat(file_list, "\n")
40+
finenv.UI():AlertError(msg, plugindef())
41+
end
42+
43+
local function remove_processing_instructions(input_name, output_name)
44+
local input_file <close> = io.open(input_name, "r")
3545
if not input_file then
36-
error("Cannot open file: " .. file_path)
46+
error("Cannot open file: " .. input_name)
3747
end
38-
-- Read the contents of the file
39-
local lines = {}
48+
local lines = {} -- assemble the output file line by line
4049
for line in input_file:lines() do
41-
-- Keep the XML declaration (<?xml ... ?>), remove other processing instructions (<?...?>)
4250
if line:match("^%s*<%?xml") or not line:match("^%s*<%?.*%?>") then
4351
table.insert(lines, line)
4452
end
4553
end
46-
-- Close the input file
4754
input_file:close()
48-
-- Open the file for writing (overwrite any file already there)
4955
local output_file <close> = io.open(output_name, "w")
5056
if not output_file then
51-
error("Cannot open file for writing: " .. file_path)
57+
error("Cannot open file for writing: " .. output_name)
5258
end
53-
-- Write the cleaned lines to the file
5459
for _, line in ipairs(lines) do
5560
output_file:write(line .. "\n")
5661
end
57-
-- Close the output file
5862
output_file:close()
5963
end
6064

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
65+
local function choose_extraction_method()
66+
local fs = finale.FCString
67+
local dialog = finale.FCCustomLuaWindow()
68+
dialog:SetTitle(fs(plugindef()))
69+
local stat = dialog:CreateStatic(0, 0)
70+
stat:SetText(fs("Massage the MusicXML for:"))
71+
stat:SetWidth(150)
72+
local labels = finale.FCStrings()
73+
labels:CopyFromStringTable{ "one MusicXML file", "a folder of MusicXML files" }
74+
local method = dialog:CreateRadioButtonGroup(0, 20, 2)
75+
method:SetText(labels)
76+
method:SetWidth(160)
77+
method:SetSelectedItem(1) -- assume "folder"
78+
dialog:CreateOkButton()
79+
dialog:CreateCancelButton()
80+
local ok = (dialog:ExecuteModal(nil) == finale.EXECMODAL_OK)
81+
return ok, (method:GetSelectedItem() == 1)
82+
end
83+
84+
local function choose_new_folder_dialog()
85+
local fs = finale.FCString
86+
local dialog = finale.FCCustomLuaWindow()
87+
dialog:SetTitle(fs(plugindef()))
88+
local stat = dialog:CreateStatic(0, 0)
89+
stat:SetText(fs("Select a Different Folder"))
90+
stat:SetWidth(150)
91+
stat = dialog:CreateStatic(0, 15)
92+
stat:SetText(fs("for the Massaged Files:"))
93+
stat:SetWidth(150)
94+
local labels = finale.FCStrings()
95+
labels:CopyFromStringTable{ "YES", "NO" }
96+
local new_folder = dialog:CreateRadioButtonGroup(0, 35, 2)
97+
new_folder:SetText(labels)
98+
new_folder:SetWidth(80)
99+
new_folder:SetSelectedItem(0)
100+
local add = dialog:CreateCheckbox(0, 85)
101+
add:SetText(fs("Don't Add \"" .. add_to_filename .. "\" to Filenames"))
102+
add:SetWidth(210)
103+
add:SetCheck(1)
104+
stat = dialog:CreateStatic(15, 100)
105+
stat:SetText(fs("When Using a Different Folder"))
106+
stat:SetWidth(180)
107+
dialog:CreateOkButton()
108+
dialog:CreateCancelButton()
109+
local ok = (dialog:ExecuteModal(nil) == finale.EXECMODAL_OK)
110+
local do_change_filename = (add:GetCheck() == 0)
111+
local select_new_folder = (new_folder:GetSelectedItem() == 0)
112+
return ok, select_new_folder, do_change_filename
88113
end
89114

90115
function fix_octave_shift(xml_measure)
@@ -102,7 +127,7 @@ function fix_octave_shift(xml_measure)
102127
xml_measure:InsertAfterChild(next_note, direction_copy)
103128
end
104129
elseif shift_type == "up" or shift_type == "down" then
105-
local sign = shift_type == "down" and 1 or -1 -- direction to transpose grace notes
130+
local sign = shift_type == "down" and 1 or -1
106131
local octaves = (octave_shift:IntAttribute("size", 8) - 1) / 7
107132
local prev_grace_note
108133
local prev_note = xml_direction:PreviousSiblingElement("note")
@@ -142,44 +167,126 @@ function process_xml(score_partwise)
142167
end
143168
end
144169

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")
170+
function process_one_file(input_file, output_file, do_change_filename)
171+
if do_change_filename then -- add "massaged" to output_file
172+
local path, filename, extension = output_file:match("^(.-)([^\\/]-)%.([^\\/%.]+)$")
173+
if not path or not filename or not extension then
174+
error("Invalid file path format")
175+
end
176+
output_file = path .. filename .. " " .. add_to_filename .. "." .. extension
152177
end
153178

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-
local output_name = append_massaged_to_filename(xml_file)
168-
-- tinyxml2 can't parse processing instructions, so remove them
169-
remove_processing_instructions(xml_file, output_name) -- hopefully not necessary forever
179+
remove_processing_instructions(input_file, output_file)
170180
local musicxml = tinyxml2.XMLDocument()
171-
local result = musicxml:LoadFile(output_name)
181+
local result = musicxml:LoadFile(output_file)
172182
if result ~= tinyxml2.XML_SUCCESS then
173-
error("Unable to process " .. xml_file .. ". " .. musicxml:ErrorStr())
174-
return
183+
os.remove(output_file) -- delete erroneous file
184+
return input_file -- XML error
175185
end
176186
local score_partwise = musicxml:FirstChildElement("score-partwise")
177187
if not score_partwise then
178-
error("File " .. xml_file .. " does not appear to be a Finale-exported MusicXML file.")
188+
os.remove(output_file) -- delete erroneous file
189+
return input_file -- massaging failed
179190
end
180191
process_xml(score_partwise)
181-
musicxml:SaveFile(output_name)
182-
finenv.UI():AlertInfo("Processed to " .. output_name .. ".", "Processed File")
192+
musicxml:SaveFile(output_file)
193+
return ""
194+
end
195+
196+
function do_open_directory()
197+
local src_dialog = finale.FCFolderBrowseDialog(finenv.UI())
198+
src_dialog:SetWindowTitle(finale.FCString("Open Folder of MusicXML Files:"))
199+
if not src_dialog:Execute() then
200+
return nil -- user cancelled
201+
end
202+
local selected_directory = finale.FCString()
203+
src_dialog:GetFolderPath(selected_directory)
204+
local src_dir = selected_directory.LuaString
205+
local out_dir = src_dir -- duplicate source to output (for now)
206+
207+
local ok, select_new_folder, do_change_filename = choose_new_folder_dialog()
208+
if not ok then return end -- cancelled
209+
if select_new_folder then -- choose alternate destination dir
210+
local out_dialog = finale.FCFolderBrowseDialog(finenv.UI())
211+
out_dialog:SetWindowTitle(finale.FCString("Choose Folder for Massaged Files:"))
212+
if not out_dialog:Execute() then return end -- user cancelled
213+
214+
out_dialog:GetFolderPath(selected_directory)
215+
out_dir = selected_directory.LuaString
216+
end
217+
if out_dir == src_dir then -- user might "choose" same folder as original
218+
do_change_filename = true -- always change filenames in same directory
219+
end
220+
local osutils = finenv.EmbeddedLuaOSUtils and require("luaosutils")
221+
if not osutils then return end -- can't get a directory listing
222+
local options = finenv.UI():IsOnWindows() and "/b /ad" or "-1"
223+
local file_list = osutils.process.list_dir(src_dir, options)
224+
if file_list == "" then return end -- empty directory
225+
226+
-- run through the file list, identifying valid candidates
227+
local error_list = {}
228+
for x_file in file_list:gmatch("([^\r\n]*)[\r\n]?") do
229+
if x_file:sub(-xml_extension:len()) == xml_extension then
230+
local src_file = src_dir .. "/" .. x_file
231+
local dest_file = out_dir .. "/" .. x_file
232+
local file_error = process_one_file(src_file, dest_file, do_change_filename)
233+
if file_error ~= "" then
234+
table.insert(error_list, file_error)
235+
end
236+
end
237+
end
238+
if #error_list > 0 then
239+
alert_error(error_list)
240+
end
241+
end
242+
243+
function do_open_dialog(document)
244+
local path_name = finale.FCString()
245+
local file_name = finale.FCString()
246+
local file_path = finale.FCString()
247+
if document then
248+
document:GetPath(file_path)
249+
file_path:SplitToPathAndFile(path_name, file_name)
250+
end
251+
local full_file_name = file_name.LuaString
252+
local extension = finale.FCString(file_name.LuaString)
253+
extension:ExtractFileExtension()
254+
if extension.Length > 0 then
255+
file_name:TruncateAt(file_name:FindLast("." .. extension.LuaString))
256+
end
257+
file_name:AppendLuaString(xml_extension)
258+
local open_dialog = finale.FCFileOpenDialog(finenv.UI())
259+
open_dialog:SetWindowTitle(finale.FCString("Open MusicXML for " .. full_file_name))
260+
open_dialog:AddFilter(finale.FCString("*" .. xml_extension), finale.FCString("MusicXML File"))
261+
open_dialog:SetInitFolder(path_name)
262+
open_dialog:SetFileName(file_name)
263+
open_dialog:AssureFileExtension(xml_extension)
264+
if not open_dialog:Execute() then
265+
return nil
266+
end
267+
local selected_file_name = finale.FCString()
268+
open_dialog:GetFileName(selected_file_name)
269+
return selected_file_name.LuaString
270+
end
271+
272+
function music_xml_massage_export()
273+
local ok, full_directory = choose_extraction_method()
274+
if not ok then return end -- user cancelled
275+
276+
if full_directory then
277+
do_open_directory()
278+
else -- only one file
279+
local documents = finale.FCDocuments()
280+
documents:LoadAll()
281+
local document = documents:FindCurrent()
282+
local xml_file = do_open_dialog(document)
283+
if xml_file then
284+
local file_error = process_one_file(xml_file, xml_file, true)
285+
if file_error ~= "" then
286+
alert_error{file_error}
287+
end
288+
end
289+
end
183290
end
184291

185292
music_xml_massage_export()

0 commit comments

Comments
 (0)