|
| 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