Skip to content

Commit c3b20a7

Browse files
authored
Merge pull request #766 from cv-on-hub/cv-musicxml
Update musicxml_massage_export.lua
2 parents 0f36ac9 + dcd209a commit c3b20a7

File tree

1 file changed

+114
-76
lines changed

1 file changed

+114
-76
lines changed

src/musicxml_massage_export.lua

Lines changed: 114 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,34 @@ 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.6"
8+
finaleplugin.Date = "October 2, 2024"
9+
finaleplugin.LoadLuaOSUtils = true
910
finaleplugin.CategoryTags = "Document"
1011
finaleplugin.MinJWLuaVersion = 0.74
12+
finaleplugin.AdditionalMenuOptions = [[
13+
Massage MusicXML Single File...
14+
]]
15+
finaleplugin.AdditionalUndoText = [[
16+
Massage MusicXML Single File
17+
]]
18+
finaleplugin.AdditionalDescriptions = [[
19+
Massage a MusicXML file to improve importing to Dorico and MuseScore
20+
]]
21+
finaleplugin.AdditionalPrefixes = [[
22+
do_single_file = true
23+
]]
24+
finaleplugin.ScriptGroupName = "Staff Explode"
1125
finaleplugin.Notes = [[
12-
This script reads a musicxml file exported from Finale and modifies it to
13-
improve the importing into Dorico or MuseScore. The best process is as follows:
26+
This script reads musicxml files exported from Finale and modifies them to
27+
improve importing into Dorico or MuseScore. The best process is as follows:
1428
1529
1. Export your document as uncompressed MusicXML.
1630
2. Run this plugin on the output *.musicxml document.
1731
3. The massaged file name has " massaged" appended to the file name.
18-
3. Import the massaged *.musicxml into Dorico or MuseScore.
32+
3. Import the massaged *.musicxml file into Dorico or MuseScore.
1933
2034
Here is a list of some of the changes the script makes:
2135
@@ -24,69 +38,44 @@ function plugindef()
2438
Due to a limitation in the xml parser, all xml processing instructions are removed. These are metadata that neither
2539
Dorico nor MuseScore use, so their removal should not affect importing into those programs.
2640
]]
27-
return "Massage MusicXML...", "", "Massages MusicXML to make it easier to import to Dorico and MuseScore."
41+
return "Massage MusicXML Folder...",
42+
"Massage MusicXML Folder",
43+
"Massage a folder of MusicXML files to improve importing to Dorico and MuseScore."
2844
end
2945

30-
local text_extension = ".musicxml"
46+
do_single_file = do_single_file or false
47+
local xml_extension = ".musicxml"
48+
local add_to_filename = " massaged"
3149

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")
50+
local function alert_error(file_list)
51+
local msg = (#file_list > 1 and "These files do not " or "This file does not ")
52+
.. "appear to be MusicXML exported from Finale:\n\n"
53+
.. table.concat(file_list, "\n")
54+
finenv.UI():AlertError(msg, plugindef())
55+
end
56+
57+
local function remove_processing_instructions(input_name, output_name)
58+
local input_file <close> = io.open(input_name, "r")
3559
if not input_file then
36-
error("Cannot open file: " .. file_path)
60+
error("Cannot open file: " .. input_name)
3761
end
38-
-- Read the contents of the file
39-
local lines = {}
62+
local lines = {} -- assemble the output file line by line
4063
for line in input_file:lines() do
41-
-- Keep the XML declaration (<?xml ... ?>), remove other processing instructions (<?...?>)
4264
if line:match("^%s*<%?xml") or not line:match("^%s*<%?.*%?>") then
4365
table.insert(lines, line)
4466
end
4567
end
46-
-- Close the input file
4768
input_file:close()
48-
-- Open the file for writing (overwrite any file already there)
4969
local output_file <close> = io.open(output_name, "w")
5070
if not output_file then
51-
error("Cannot open file for writing: " .. file_path)
71+
error("Cannot open file for writing: " .. output_name)
5272
end
53-
-- Write the cleaned lines to the file
5473
for _, line in ipairs(lines) do
5574
output_file:write(line .. "\n")
5675
end
57-
-- Close the output file
5876
output_file:close()
5977
end
6078

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-
9079
function fix_octave_shift(xml_measure)
9180
for xml_direction in xmlelements(xml_measure, "direction") do
9281
local xml_direction_type = xml_direction:FirstChildElement("direction-type")
@@ -102,7 +91,7 @@ function fix_octave_shift(xml_measure)
10291
xml_measure:InsertAfterChild(next_note, direction_copy)
10392
end
10493
elseif shift_type == "up" or shift_type == "down" then
105-
local sign = shift_type == "down" and 1 or -1 -- direction to transpose grace notes
94+
local sign = shift_type == "down" and 1 or -1
10695
local octaves = (octave_shift:IntAttribute("size", 8) - 1) / 7
10796
local prev_grace_note
10897
local prev_note = xml_direction:PreviousSiblingElement("note")
@@ -142,44 +131,93 @@ function process_xml(score_partwise)
142131
end
143132
end
144133

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
134+
function process_one_file(input_file)
135+
local path, filename, extension = input_file:match("^(.-)([^\\/]-)%.([^\\/%.]+)$")
150136
if not path or not filename or not extension then
151137
error("Invalid file path format")
152138
end
139+
local output_file = path .. filename .. add_to_filename .. xml_extension
140+
141+
remove_processing_instructions(input_file, output_file)
142+
local musicxml = tinyxml2.XMLDocument()
143+
local result = musicxml:LoadFile(output_file)
144+
if result ~= tinyxml2.XML_SUCCESS then
145+
os.remove(output_file) -- delete erroneous file
146+
return input_file -- XML error
147+
end
148+
local score_partwise = musicxml:FirstChildElement("score-partwise")
149+
if not score_partwise then
150+
os.remove(output_file) -- delete erroneous file
151+
return input_file -- massaging failed
152+
end
153+
process_xml(score_partwise)
154+
musicxml:SaveFile(output_file)
155+
return ""
156+
end
157+
158+
function process_directory(path_name)
159+
local folder_dialog = finale.FCFolderBrowseDialog(finenv.UI())
160+
folder_dialog:SetWindowTitle(finale.FCString("Select Folder of MusicXML Files:"))
161+
folder_dialog:SetFolderPath(path_name)
162+
if not folder_dialog:Execute() then
163+
return nil -- user cancelled
164+
end
165+
local selected_directory = finale.FCString()
166+
folder_dialog:GetFolderPath(selected_directory)
167+
local src_dir = selected_directory.LuaString
168+
169+
-- scan the directory, identifying valid candidate files
170+
local error_list = {}
171+
local lfs = require("lfs")
172+
for file in lfs.dir(src_dir) do
173+
if file ~= "." and file ~= ".." and file:sub(-xml_extension:len()) == xml_extension then
174+
local file_error = process_one_file(src_dir .. "/" .. file)
175+
if file_error ~= "" then
176+
table.insert(error_list, file_error)
177+
end
178+
179+
end
180+
end
181+
if #error_list > 0 then
182+
alert_error(error_list)
183+
end
184+
end
153185

154-
-- Construct the new file path with " massaged" appended to the filename
155-
local new_filepath = path .. filename .. " massaged." .. extension
156-
return new_filepath
186+
function do_open_dialog(path_name)
187+
local open_dialog = finale.FCFileOpenDialog(finenv.UI())
188+
open_dialog:SetWindowTitle(finale.FCString("Select a MusicXML File:"))
189+
open_dialog:AddFilter(finale.FCString("*" .. xml_extension), finale.FCString("MusicXML File"))
190+
open_dialog:SetInitFolder(path_name)
191+
open_dialog:AssureFileExtension(xml_extension)
192+
if not open_dialog:Execute() then
193+
return nil
194+
end
195+
local selected_file_name = finale.FCString()
196+
open_dialog:GetFileName(selected_file_name)
197+
return selected_file_name.LuaString
157198
end
158199

159200
function music_xml_massage_export()
160201
local documents = finale.FCDocuments()
161202
documents:LoadAll()
162203
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
170-
local musicxml = tinyxml2.XMLDocument()
171-
local result = musicxml:LoadFile(output_name)
172-
if result ~= tinyxml2.XML_SUCCESS then
173-
error("Unable to process " .. xml_file .. ". " .. musicxml:ErrorStr())
174-
return
204+
local path_name = finale.FCString()
205+
if document then -- extract active pathname
206+
document:GetPath(path_name)
207+
path_name:SplitToPathAndFile(path_name, nil)
175208
end
176-
local score_partwise = musicxml:FirstChildElement("score-partwise")
177-
if not score_partwise then
178-
error("File " .. xml_file .. " does not appear to be a Finale-exported MusicXML file.")
209+
210+
if do_single_file then
211+
local xml_file = do_open_dialog(path_name)
212+
if xml_file then
213+
local file_error = process_one_file(xml_file)
214+
if file_error ~= "" then
215+
alert_error{file_error}
216+
end
217+
end
218+
else
219+
process_directory(path_name)
179220
end
180-
process_xml(score_partwise)
181-
musicxml:SaveFile(output_name)
182-
finenv.UI():AlertInfo("Processed to " .. output_name .. ".", "Processed File")
183221
end
184222

185223
music_xml_massage_export()

0 commit comments

Comments
 (0)