-- $NWCUT$CONFIG: FileText $ nwcut.setlevel(2) -- update 20/11/2017 -- * New Parameters: -- - Foreground Staves : one or more staff names (instead of active staff + staff named 'All')) -- - Foreground Color (instead of fixed 'Highlight 2') -- - What to Color :key signature & clef or whole staff -- * You can add arguments to the command line to set the value or default for one or more parameters (see 'Help on command line parameters'). -- Variables -------------------------- local progname = 'MakeRehearsalFile.og' local Testing = false local Action local HelpMsg = [[ This tool is intended to make seperate rehearsal scores for the different voices of multi voice songs. This will be done by setting different Stereo Pan, Channel Volumes, Dynamic Volumes and Velocities and Color of Clef and Key Signature for the Foreground Staves versus the Background Staves. Before running this tool, make a group "Piano" for your instrumental staves. You can add arguments to the command line to set the value or default for one or more parameters (see 'Help on command line parameters'). - Dynamics: The Staff Velocities (DynVel) from the first staff will be copied to the next staves and will be used, along with the Back- or Foreground Channel Volumes to calculate the Velocities and Volumes of the dynamics(*). (The dynamics in the 'Piano' group will have default volume and velocity.) - Stereo Pan: The Stereo Pan of the foreground staves will be set to 0 (left), that of the Piano Group to 64 (center) and that of the background staves to 127 (right). - Color: According to your choise, either the Staff Color or the color of the Clef and Key Signatures in the Foreground Staves will be set to the Foreground Color, that of the other staves to the Default Color. If you choose the action 'Undo', you have to provide a value for the 'Channel Volume'. Then all Staff Volumes will be set to this Channel Volume, the dynamics are recalculated (*), Stereo Pans are set to 64 and Staff Colors to Default. (*) Calculation of the Velocities and Volumes for the dynamics: - The Velocity of each dynamic is set equal to the highest Staff Velocity ('fff'). - The Volume is set using Tina Billet's formula : Volume = Velocity * Staff Volume) / highest Staff Velocity. For more information: see document 'Dynamics.pdf' in . ]] local HelpArg = [[ You can add arguments to the command line to set the value or default for one or more parameters. Syntax: =['?']. The arguments pairs must be seperated by 1 or more spaces. There must not be a space within an argument. If an argument ends with a '?', the value will be used as default in the dialog, otherwise the value will be assiged to the parameter, which will be skipped in the dialog. Parameters (meaning) : Values - Action : Make or Undo - FStaves (Foreground Staves) : one or more staff names, seperated by a comma. A space in a staff name need to be replaced by a '_'. - FVolume (Foreground volume) : 0-127 - BVolume (Background volume) : 0-127 - CVolume (Channel volume for action 'Undo') : 0-127 - FColor (Foreground color) : Default, Highlight_1, Highlight_2, Highlight_3, Highlight_4, Highlight_5, Highlight_6 or Highlight_7 - WColor (What to color) : Key&Clef or Staff ]] local ArgTab, ArgDefaults local ParmTab = { --listprompts: list of valid prompt answers --listargs: list of valid command arguments (if not present: = listprompts) Action = {msg = "Action: ", Type = "|", listprompts = {"Make rehearsal file","Undo","Help","Help on command line parameters"}, listargs = {'Make', 'Undo', 'Help','HCommand'}}, FStaves = {msg = "Select the foreground staves", Type = '&', blank='_'}, FVolume = {msg = "Foreground Volume: ", Type = "#", min = 0, max = 127, default = 120}, BVolume = { msg = "Background Volume: ", Type = "#", min = 0, max = 127, default = 60}, FColor = { msg = "Foreground Color: ", Type = "|", listprompts = {"Default","Highlight 1","Highlight 2","Highlight 3","Highlight 4","Highlight 5","Highlight 6","Highlight 7"}, default = 'Highlight 2', blank='_'}, WColor = {msg="What to Color: ", Type = "|",listprompts = {"Key Signature & Clef","Staff"}, listargs = {'Key&Clef', 'Staff'}}, CVolume = {msg = "Channel Volume: ", Type = "#", min = 0, max = 127, default = 120}, } local ActiveStaff = 0 local CurrentStaff = 0 local ppp, pp, p, mp, mf, f, f, ff = 1, 2, 3, 4, 5, 6, 7 local NumProp, StartingBar local FileHeader = {} local Staffs = {} local StaffName, Staff local ActOrigName local EmptyMeasure = true local FVolume, BVolume, CVolume = 127, 127, 127 local FVolumes = {} local BVolumes = {} local CVolumes = {} local FColor, WColor local VoiceChannels = {} local Voices = {} local FStaves = {} -- general functions ------------------------ local function v2s(v) local s if v == true then s = "true" elseif v == false then s = "false" elseif v == nil then s = "nil" elseif type(v) == "table" then s = "{" for val in pairs(v) do if s ~= "{" then s = s..", " end--if local s1 = v2s(val) local s2 = v2s(v[s1]) if tonumber(s1) then s = s..s2 else s = s..s1.." = "..s2 end--if end--for s = s.."}" else s = v end--if return s end --v2s local function ShowVar(name, var) if Testing then nwcut.warn(name == "" and "" or name.." ==> ", v2s(var),"\n") end -- if end -- ShowVar local function split(str, sep) local tab = {} if str and str ~= '' then local i = 1 local f = 1 local s if string.sub(str,1,1) == '"' then str = string.sub(str,2,-2) end--if repeat s = string.find(str, sep, f) or 0 tab[i] = string.sub(str, f, s - 1) f = s + 1 i = i + 1 until s == 0 end return tab end local LuaAssert = assert do -- redefine assert assert = function(cond, msg) if cond then return end--if nwcut.msgbox(msg,'ERROR') nwcut.status = nwcut.const.rc_Error if Testing then error(msg,2) else error("",0) end--if end--assert end -- redefine assert local function unquote(str) return split(split (str, "%'")[1], '%"')[1] end--unquote local function IsIn (tab, val) -- looks for value 'val' in the table 'tab' and returns the index of the first match S(or nil) -- if 'tab' is a nested table, a multiple index is returned (so i.j denotes tab[i][j]) for k,v in pairs(tab) do if type(v) == 'table' then local kk = IsIn(v, val) if kk then return k..'.'..kk end--if elseif val == v then return k elseif not tonumber(k) then if val == k then return k end--if end--if end--for end--function IsIn function ProcessParmDatatypes() for k,p in pairs(ParmTab) do if p.Type == '*' then elseif p.Type == '#' then p.datatype = not p.min and '#' or '#['..p.min..'-'..p.max..']' elseif p.Type == '|' then p.datatype = '|'..table.concat(p.listprompts, '|') p.listargs = p.listargs or p.listprompts p.strvals = table.concat(p.listargs, ',') elseif p.Type == '&' then p.datatype = '&' p.listargs = p.listargs or p.listprompts p.strvals = table.concat(p.listargs, ',') end--if if p.blank then p.strvals = string.gsub(p.strvals, " ", p.blank) end--if end--for end--function ProcessDatatypes function GetArgs() local argtab = {} local defaults = {} if not arg[1] then return argtab end--if ProcessParmDatatypes() for i,a in ipairs(arg) do local tab = split(a, '=') ShowVar('tab',tab) assert(#tab == 2, "Invalid argument '"..a.."'.") val = tab[2] if string.find(val, '?') == #val then val = string.sub(val, 1, #val - 1) defaults[tab[1]] = val end--if argtab[tab[1]] = val end--for for k, v in pairs(argtab) do local p = ParmTab[k] assert(p, "Invalid parameter '"..k.."'.") local vk = "value '"..v.."' of parameter '"..k.."'" if p.Type == '#' then nv = tonumber(v) assert(type(nv) == 'number', "Invalid type for "..vk) if p.Min then assert(nv >= p.Min, vk.. " smaller then minimum "..p.Min..".") end--if if p.Max then assert(nv <= p.Max, vk.. " greater then maximum "..p.Max..".") end--if elseif p.Type == '|' then if p.blank then v = string.gsub(v, p.blank, ' ') end--if assert (IsIn(p.listargs,v), "Invalid "..vk..".\nValid values are: '"..p.strvals.."'.") argtab[k] = v elseif p.Type == '&' then argtab[k] = split(v, ',') if p.blank then defaults[k]= string.gsub(defaults[k], p.blank, ' ') end--if defaults[k] = defaults[k] and split(defaults[k], ',') for l, w in ipairs(argtab[k]) do local wk = "value '"..w.."' of parameter '"..k.."'" if p.blank then w = string.gsub(w, p.blank, ' ') end--if assert (IsIn(p.listargs,w), "Invalid "..wk..".\nValid values are: '"..p.strvals.."'.") argtab[k][l] = w end--for end--if end--for return argtab, defaults end--GetArgs function GetParameters(...) local function GetP(p) local pp = ParmTab[p] if ArgTab[p] then if pp.Type == '|' then ArgTab[p] = pp.listprompts[IsIn(pp.listargs, ArgTab[p])] or ArgTab[p] end--if if not ArgDefaults[p] then return ArgTab[p] end--if end--if local default = ArgDefaults[p] or pp.default ShowVar('default',default) local pp3 = pp.Type == '&' and pp.listprompts or default local pp4 = pp.Type == '&' and default or nil return nwcut.prompt(pp.msg, pp.datatype, pp3, pp4) end--GetP local argtab = {...} local parmlist = {} if argtab[1] then for _,p in ipairs(argtab) do table.insert(parmlist,GetP(p)) end--for else for p,_ in pairs(ParmTab) do table.insert(parmlist,GetP(p)) end--for end--if return table.unpack(parmlist) end--GetParameters --general nwcfile related functions-------------------- local function Selected() return CurrentStaff == ActiveStaff and CurrentIndex >= SelectStart and CurrentIndex < SelectEnd end --Selected() local function AtCursor() return StaffActive and CurrentIndex > 0 and CurrentIndex == CaretIndex - 1 end --AtCursor() local function IsFileHeader(item) return item:Is("Locale") or item:Is("Editor") or item:Is("SongInfo") or item:Is("PgSetup") or item:Is("Font") or item:Is("PgMargins") end--IsFileHeader local function IsStaffHeader(item) return item:Is("AddStaff") or item:Is("StaffProperties") or item:Is("StaffInstrument") or string.sub(item.ObjType,1,5) == "Lyric" end--IsStaffHeader -- tool related functions---------------------------------------------------- function IsForegrStaff(s) local OK = false for _,v in ipairs(FStaves) do if v == s.Name then OK = true break end--if v = string.gsub(v, '_', ' ') if v == s.Name then OK = true break end--if end--for return OK end-- IsForegrStaff local function CalcVolumes() local dynvel = Staffs[1].DynVel for i, vel in ipairs(dynvel) do FVolumes[i] = math.floor(vel * FVolume / dynvel[8] + 0.5) BVolumes[i] = math.floor(vel * BVolume / dynvel[8] + 0.5) CVolumes[i] = math.floor(vel * CVolume / dynvel[8] + 0.5) end--for end-- local function ProcessEditor(item) ActiveStaff = item.Opts.ActiveStaff + 0 assert(item.Opts.SelectIndex == nil,"\nThis tool doesn't work on a selection.") end -- ProcessEditor local function ProcessPgSetup(item) StartingBar = item.Opts.StartingBar end -- ProcessPgSetup(item) local function ProcessAddStaff(item) CurrentStaff = CurrentStaff + 1 Staffs[CurrentStaff] = {} Staff = Staffs[CurrentStaff] Staff.Name = item.Opts.Name.Text Staff.Label= item.Opts.Label and item.Opts.Label.Text Staff.Group= item.Opts.Group.Text Staff.LabelAbbr= item.Opts.LabelAbbr Staff.Group = item.Opts.Group.Text Staff.Active = CurrentStaff == ActiveStaff if Staff.Active then local i = string.find(Staff.Name, '/Layered') or string.find(Staff.Name, '/Muted') ActOrigName = i and string.sub(Staff.Name, 1, i - 1) ShowVar('ActOrigName', ActOrigName) end--if if Staff.Group ~= 'Piano' then table.insert(Voices, Staff.Name) end--if Staff.MeasureNumber = StartingBar CurrentIndex = 0 NumProp = 0 return end -- ProcessAddStaff(item) local function ProcessStaffProperties(item) NumProp = NumProp + 1 if NumProp == 1 then Staff.Visible = item.Opts.Visible == 'Y' -- Staff.Color = item.Opts.Color else Staff.Volume = item.Opts.Volume Staff.StereoPan = item.Opts.StereoPan Staff.Muted = item.Opts.Muted == 'Y' Staff.Channel = item.Opts.Channel end--if end-- ProcessStaffProperties local function ProcessStaffInstrument(item) Staff.DynVel = item.Opts.DynVel end-- ProcessStaffProperties local function ProcessBar(s,item) if not EmptyMeasure then if not item.Opts.XBarCnt then s.MeasureNumber = s.MeasureNumber + 1 EmptyMeasure = true end -- if end -- if end--ProcessBar local function SetDynVol(s,item) if item.Opts.Opts and item.Opts.Opts.Volume and item.Opts.Opts.Volume == '0' then return end--if local dynnum = {["ppp"]=1, ["pp"]=2,["p"]=3,["mp"]=4,["mf"]=5,["f"]=6,["ff"]=7,["fff"]=8} local style = item.Opts.Style local i = dynnum[style] local opts = item.Opts.Opts local velocity, volume velocity = opts and opts.velocity or nil volume = opts and opts.Volume or nil item:Provide("Opts") if Action == 'Make' then if s.Group == 'Piano' then if item.Opts.Opts then item.Opts.Opts.Velocity = nil item.Opts.Opts.Volume = nil end--if else item.Opts.Opts.Velocity = Staffs[1].DynVel[8] -- item.Opts.Opts.Volume = (s.Active or s.Name == ActOrigName) and FVolumes[i] or BVolumes[i] item.Opts.Opts.Volume = IsForegrStaff(s) and FVolumes[i] or BVolumes[i] end--if elseif Action == 'Undo' then if s.Group ~= 'Piano' then item.Opts.Opts.Velocity = Staffs[1].DynVel[8] item.Opts.Opts.Volume = CVolumes[i] end--if end--if end-- SetDynVol local function ProcessColor(s,item) if WColor == 'Key' and IsForegrStaff(s) then item.Opts.Color = FColor else item.Opts.Color = 'Default' end--if end--ProcessColor local function Undostaffprops(s) if s.Group == 'Piano' then s[3].Opts.Volume = CVolume else s[2].Opts.Color = 'Default' s[3].Opts.Volume = CVolume s[3].Opts.StereoPan = 64 end--if end-- Undostaffprops local function Changestaffprops(s) if s.Group ~= 'Piano' and not s.Muted then -- assert(VoiceChannels[s.Channel] == nil, "Duplicate channel found in non Piano group.") VoiceChannels[s.Channel] = true end--if if s.Group == 'Piano' then s[3].Opts.Volume = BVolume s[3].Opts.StereoPan = 64 elseif IsForegrStaff(s) then ShowVar('WColor',WColor) ShowVar('FColor',FColor) if WColor == 'Sta' then s[2].Opts.Color = FColor else s[2].Opts.Color = 'Default' end--if s[3].Opts.Volume = FVolume s[3].Opts.StereoPan = 0 else s[2].Opts.Color = 'Default' s[3].Opts.Volume = BVolume s[3].Opts.StereoPan = 127 end--if s[4].Opts.DynVel = Staffs[1].DynVel local indr = 0 local sitem repeat indr = indr + 1 sitem = s[indr] until not IsStaffHeader(sitem) and not(sitem:Is('Boundary') and sitem.Opts.Style == 'Collapse') end-- function Changestaffprops local function AllocateItem(item) if IsFileHeader(item)then if item:Is("Editor") then ProcessEditor(item) elseif item:Is('PgSetup') then ProcessPgSetup(item) end--if table.insert(FileHeader, item) elseif IsStaffHeader(item)then if item:Is("AddStaff") then ProcessAddStaff(item) elseif item:Is("StaffProperties") then ProcessStaffProperties(item) elseif item:Is("StaffInstrument") then ProcessStaffInstrument(item) end--if table.insert(Staff, item) else table.insert(Staff, item) end--if end -- AllocateItem -------------------------------------- -- Main processing ------------------- -------------------------------------- nwcut.status = nwcut.const.rc_Succes assert(nwcut.getprop('Mode') == nwcut.const.mode_FileText, "Input type must be 'File Text'") assert(nwcut.getprop('ReturnMode') == nwcut.const.mode_FileText, "Under 'Options', check 'Returns File Text'") for item in nwcut.items() do AllocateItem(item) end --for item ParmTab.FStaves.listprompts = Voices ShowVar('Voices',Voices) ArgTab, ArgDefaults = GetArgs() repeat Action = GetParameters('Action') if Action == 'Help' then nwcut.msgbox(HelpMsg, progname .. " - Help info") elseif string.sub(Action,1,7) == 'Help on' then nwcut.msgbox(HelpArg, progname .. " - Help info") end--if ShowVar('Action', Action) until string.sub(Action,1,4) ~= 'Help' Action = string.sub(Action, 1,4) if Action == 'Make' then FStaves, FVolume, BVolume, FColor = GetParameters('FStaves', 'FVolume', 'BVolume', 'FColor') if FColor ~= 'Default' then WColor = string.sub(GetParameters('WColor'),1,3) if WColor == 'Key' then FColor= string.sub(FColor,11) -- else -- FColor = string.gsub(FColor,'-', ' ') end--if end--if assert(FVolume + 0 >= BVolume + 0, "Background Channel Volume should be <= Foreground Volume.") elseif Action == 'Undo' then CVolume = GetParameters('CVolume') ShowVar('CVolume',CVolume) end--if CalcVolumes() for _, item in ipairs(FileHeader) do nwcut.writeline(item) end--for for _, s in ipairs(Staffs) do local ifs = IsForegrStaff(s) if Action == 'Make' then Changestaffprops(s) elseif Action == 'Undo' then Undostaffprops(s) end--if for _,item in ipairs(s) do if item:HasDuration() then EmptyMeasure = false end -- if if item:Is('Clef') or item:Is('Key') then ProcessColor(s,item) elseif item:Is('Bar') then ProcessBar(s,item) elseif item:Is('Dynamic') then SetDynVol(s, item) end--if nwcut.writeline(item) end--for end--for