forked from finale-lua/lua-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dynamic_levels.lua
293 lines (279 loc) · 11.9 KB
/
dynamic_levels.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
function plugindef()
finaleplugin.RequireSelection = false
finaleplugin.HandlesUndo = true
finaleplugin.Author = "Carl Vine"
finaleplugin.AuthorURL = "https://carlvine.com/lua"
finaleplugin.Copyright = "https://creativecommons.org/licenses/by/4.0/"
finaleplugin.Version = "0.12"
finaleplugin.Date = "2024/07/20"
finaleplugin.MinJWLuaVersion = 0.70
finaleplugin.Notes = [[
Make dynamic marks in the selection louder or softer by stages.
This functionality is buried within the __JW Change__ plugin
but is useful enough to make it accessible more easily.
This script works similarly but allows jumping up to 9 _levels_ at once.
Dynamics range from __pppppp__ to __ffffff__, though scores using
older (non-__SMuFL__) fonts are restricted to the range __pppp__-__ffff__.
To repeat the previous level shift without a confirmation dialog
hold down [_Shift_] when starting the script.
]]
return "Dynamic Levels...",
"Dynamic Levels",
"Make dynamic marks in the selection louder or softer by stages"
end
local hotkey = { -- customise hotkeys (lowercase only)
direction = "z", -- toggle Louder/Softer
create_new = "e", -- toggle create_new
show_info = "q",
}
local config = {
direction = 0, -- 0 == "Louder", 1 = "Softer"
levels = 1, -- how many "levels" louder or softer
create_new = false, -- don't create new dynamics without permission
window_pos_x = false,
window_pos_y = false,
}
local configuration = require("library.configuration")
local mixin = require("library.mixin")
local expression = require("library.expression")
local utils = require("library.utils")
local library = require("library.general_library")
local script_name = library.calc_script_name()
local name = plugindef():gsub("%.%.%.", "")
local dyn_char = library.is_font_smufl_font() and
{ -- char numbers for SMuFL dynamics (1-14)
0xe527, 0xe528, 0xe529, 0xe52a, 0xe52b, 0xe520, 0xe52c, -- pppppp -> mp
0xe52d, 0xe522, 0xe52f, 0xe530, 0xe531, 0xe532, 0xe533, -- mf -> ffffff
} or
{ -- char numbers for non-SMuFL dynamics (1-10)
175, 184, 185, 112, 80, -- pppp -> mp
70, 102, 196, 236, 235 -- mf -> ffff
}
local function dialog_set_position(dialog)
if config.window_pos_x and config.window_pos_y then
dialog:StorePosition()
dialog:SetRestorePositionOnlyData(config.window_pos_x, config.window_pos_y)
dialog:RestorePosition()
end
end
local function dialog_save_position(dialog)
dialog:StorePosition()
config.window_pos_x = dialog.StoredX
config.window_pos_y = dialog.StoredY
configuration.save_user_settings(script_name, config)
end
local function get_staff_name(staff_num)
local staff = finale.FCStaff()
staff:Load(staff_num)
local str = staff:CreateDisplayAbbreviatedNameString().LuaString
if not str or str == "" then
str = "Staff" .. staff_num
end
return str
end
local function update_selection()
local rgn = finenv.Region()
if rgn:IsEmpty() then
return ""
else
local s = get_staff_name(rgn.StartStaff)
if rgn.EndStaff ~= rgn.StartStaff then
s = s .. "-" .. get_staff_name(rgn.EndStaff)
end
s = s .. " m." .. rgn.StartMeasure
if rgn.StartMeasure ~= rgn.EndMeasure then
s = s .. "-" .. rgn.EndMeasure
end
return s
end
end
local function create_dynamics_alert(dialog)
local msg = "The required replacement dynamic doesn't exist in this file. "
.. "Do you want this script to create "
.. "additional dynamic expressions as required? "
.. "(You can change this decision later in the dialog window.)"
local ui = dialog and dialog:CreateChildUI() or finenv.UI()
return ui:AlertYesNo(msg, name) == finale.YESRETURN
end
local function create_dynamic_def(expression_text, hidden)
local cat_def = finale.FCCategoryDef()
cat_def:Load(1) -- default "DYNAMIC" category
local finfo = finale.FCFontInfo()
cat_def:GetMusicFontInfo(finfo)
finfo.EnigmaStyles = finale["ENIGMASTYLE_" .. (hidden and "HIDDEN" or "PLAIN")]
local str = finale.FCString()
str.LuaString = "^fontMus"
.. finfo:CreateEnigmaString(finale.FCString()).LuaString
.. expression_text
local ted = mixin.FCMTextExpressionDef()
ted:SaveNewTextBlock(str)
:AssignToCategory(cat_def)
:SetUseCategoryPos(true)
:SetUseCategoryFont(true)
:SaveNew()
return ted:GetItemNo() -- save new item number
end
local function is_hidden_exp(exp_def)
local str = exp_def:CreateTextString()
return str:CreateLastFontInfo().Hidden
end
local function change_dynamics(dialog)
local selection = update_selection()
if selection == "" then -- empty region
local ui = dialog and dialog:CreateChildUI() or finenv.UI()
ui:AlertError("Please select some music\nbefore running this script", name)
return
end
local found = { show = {}, hide = {} } -- collate matched dynamic expressions
local match_count = { show = 0, hide = 0 }
local shift = config.levels -- how many dynamic levels to move?
if config.direction == 1 then shift = -shift end -- softer not louder
local dyn_len = library.is_font_smufl_font() and 3 or 2 -- dynamic max string length
-- match all target dynamics within existing dynamic expressions
local function match_dynamics(hidden) -- hidden is true or false
local mode = hidden and "hide" or "show"
local exp_defs = mixin.FCMTextExpressionDefs()
exp_defs:LoadAll()
for exp_def in each(exp_defs) do
if exp_def.CategoryID == 1 and hidden == is_hidden_exp(exp_def) then
local str = exp_def:CreateTextString()
str:TrimEnigmaTags()
if str.LuaString:len() <= dyn_len then -- within max dynamic length
for i, v in ipairs(dyn_char) do -- check all dynamic glyphs
if not found[mode][i] and str.LuaString == utf8.char(v) then
found[mode][i] = exp_def.ItemNo -- matched char
match_count[mode] = match_count[mode] + 1
end
end
end
end
if match_count[mode] >= #dyn_char then break end -- all collected
end
end
match_dynamics(true)
match_dynamics(false)
-- start
finenv.StartNewUndoBlock(string.format("Dynamics %s%d %s",
(config.direction == 0 and "+" or "-"), config.levels, selection)
)
-- scan the selection for dynamics and change them
for e in loadallforregion(mixin.FCMExpressions(), finenv.Region()) do
if expression.is_dynamic(e) then
local exp_def = e:CreateTextExpressionDef()
if exp_def then
local hidden = is_hidden_exp(exp_def)
local mode = hidden and "hide" or "show"
local str = exp_def:CreateTextString()
str:TrimEnigmaTags()
if str.LuaString:len() <= dyn_len then -- dynamic length
for i, v in ipairs(dyn_char) do -- look for matching dynamic
local target = math.min(math.max(1, i + shift), #dyn_char)
if str.LuaString == utf8.char(v) then -- dynamic match
if found[mode][target] then -- replacement exists
e:SetID(found[mode][target]):Save()
else -- create new dynamic
if not config.create_new then -- ask permission
config.create_new = create_dynamics_alert(dialog)
end
if config.create_new then -- create missing dynamic exp_def
if dialog then -- update checkbox condition
dialog:GetControl("create_new"):SetCheck(1)
end
local t = utf8.char(dyn_char[target]) -- dynamic text char
found[mode][target] = create_dynamic_def(t, hidden)
e:SetID(found[mode][target]):Save()
end
end
break -- all done for this target dynamic
end
end
end
end
end
end
finenv.EndUndoBlock(true)
finenv.Region():Redraw()
end
local function run_the_dialog()
local y, m_offset = 0, finenv.UI():IsOnMac() and 3 or 0
local save = config.levels
local ctl = {}
local dialog = mixin.FCXCustomLuaWindow():SetTitle("Dynamics")
-- local functions
local function yd(diff) y = y + (diff or 20) end
local function show_info()
utils.show_notes_dialog(dialog, "About " .. name, 330, 160)
end
local function cstat(horiz, vert, wide, str) -- dialog static text
return dialog:CreateStatic(horiz, vert):SetWidth(wide):SetText(str)
end
local function key_subs()
local s = ctl.levels:GetText():lower()
if s:find("[^1-9]") then
if s:find(hotkey.show_info) then show_info()
elseif s:find(hotkey.direction) then
local n = ctl.direction:GetSelectedItem()
ctl.direction:SetSelectedItem((n + 1) % 2)
elseif s:find(hotkey.create_new) then
local n = ctl.create_new:GetCheck()
ctl.create_new:SetCheck((n + 1) % 2)
end
else
save = s:sub(-1) -- save last entered char only
end
ctl.levels:SetText(save)
end
ctl.title = cstat(10, y, 120, name:upper())
yd()
-- RadioButtonGroup
local labels = finale.FCStrings()
labels:CopyFromStringTable{ "Louder", "Softer" }
ctl.direction = dialog:CreateRadioButtonGroup(0, y + 1, 2)
:SetText(labels):SetWidth(55):SetSelectedItem(config.direction)
local softer = ctl.direction:GetItemAt(1) -- 2nd button
softer:SetTop(y + 24)
cstat(23, y + 11, 25, "(" .. hotkey.direction .. ")")
-- levels
cstat(65, y, 55, "Levels:")
ctl.levels = dialog:CreateEdit(110, y - m_offset):SetText(config.levels):SetWidth(20)
:AddHandleCommand(function() key_subs() end)
yd()
ctl.q = dialog:CreateButton(110, y):SetText("?"):SetWidth(20)
:AddHandleCommand(function() show_info() end)
yd(23)
ctl.create_new = dialog:CreateCheckbox(0, y, "create_new")
:SetWidth(145):SetCheck(config.create_new and 1 or 0)
:SetText("Enable Creation of New")
yd(13)
cstat(13, y, 135, "Dynamic Expressions (" .. hotkey.create_new .. ")")
-- wrap it up
dialog:CreateOkButton() :SetText("Apply")
dialog:CreateCancelButton():SetText("Close")
dialog:RegisterInitWindow(function()
local bold = ctl.q:CreateFontInfo():SetBold(true)
ctl.q:SetFont(bold)
ctl.title:SetFont(bold)
end)
dialog_set_position(dialog)
dialog:RegisterHandleOkButtonPressed(function()
config.direction = ctl.direction:GetSelectedItem()
config.levels = ctl.levels:GetInteger()
config.create_new = (ctl.create_new:GetCheck() == 1)
change_dynamics(dialog)
end)
dialog:RegisterCloseWindow(function(self)
dialog_save_position(self)
end)
dialog:RunModeless()
end
local function dynamic_levels()
configuration.get_user_settings(script_name, config, true)
local qim = finenv.QueryInvokedModifierKeys
local mod_key = qim and (qim(finale.CMDMODKEY_ALT) or qim(finale.CMDMODKEY_SHIFT))
if mod_key then
change_dynamics(nil)
else
run_the_dialog()
end
end
dynamic_levels()