Skip to content

Commit 4bbdf7d

Browse files
authored
Merge pull request #745 from cv-on-hub/cv_dynamic_levels
Create dynamic_levels.lua
2 parents 2e613fd + 4d1eba9 commit 4bbdf7d

File tree

1 file changed

+293
-0
lines changed

1 file changed

+293
-0
lines changed

src/dynamic_levels.lua

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

0 commit comments

Comments
 (0)