-- $NWCUT$CONFIG: FileText $ nwcut.setlevel(2) -- Variables -------------------------- local progname = 'Enharmonic.og' local version = '2.1' local Testing = false local Option local Report = StringBuilder.new() local HelpMsg = [[ With this tool you can replace notes with their enharmonic equivalents. It works on a selection of a staff, or the whole active staff, if you don't select anything. There are several options you can choose from(see below). Independent of the chosen option: - if an enharmonic chance is to be done on a note, all following notes of the same name in the same measure or tied in the next measure are investigated, even when they are not part of your selection: ° before the enharmonic change: a following note with an implicit accidental will receive a forced accidental; ° after the enharmonic change: if a following note has no (implicit or explicit) accidental, it will recieve a natural accidental. - if one or more notes in a measure had to be changed, all unnessary accidentals in that measure are removed. Available options: - 'No sharps': All sharps are replaced. - 'No flats': All flats are replaced. - 'Reverse sharps/flats' - 'No doubles': All double sharps and double flats are replaced. - 'Preferably naturals': If a enharmonic equivalent without accidental is possible, use that. - 'Avoid forced naturals': if a natural accidental is caused by an preceding note with an accidental, this preceding note is replaced by its enharmonic equivalent. - 'Common prefered accidentals': A# becomes Bb, Db becomes C#, D# becomes Eb, Gb becomes F#, Ab becomes G#. - 'Sharps up, flats down': if the next note is a tone or a semitone higher, use a sharp; if it is a tone or a semitone lower, use a flat (not applicable to chords and restchords). - 'Your changes': You can choose one or more items from a list of all possible enharmonic changes. (You need version 2.75a beta 11 or higher to use this option). ]] local ActiveStaff local CurrentStaff = 0 local CurrentIndex local Selection = true local SelectStart = -1 local SelectEnd = -1 local Header = {} Header.Font = {} local Staves = {} local Staff local Clef local Key = {} local MeasureKey = {} local MeasureKeySave = {} local YourChangeList local Ties = {} local MeasureStart local Changed, MeasureChanged local StartingBar, EmptyMeasure, MeasureNumber, ItemNumber local ChangeList = {} ChangeList.Maxni = 0 local EnharmonicsTable = { ['C#'] = {enharmonic='Db',disp=1,acc='b'}, ['D#'] = {enharmonic='Eb',disp=1,acc='b'}, ['E#'] = {enharmonic='F ',disp=1}, ['F#'] = {enharmonic='Gb',disp=1,acc='b'}, ['G#'] = {enharmonic='Ab',disp=1,acc='b'}, ['A#'] = {enharmonic='Bb',disp=1,acc='b'}, ['B#'] = {enharmonic='C ',disp=1}, ['Cb'] = {enharmonic='B ',disp=-1}, ['Db'] = {enharmonic='C#',disp=-1,acc='#'}, ['Eb'] = {enharmonic='D#',disp=-1,acc='#'}, ['Fb'] = {enharmonic='E ',disp=-1}, ['Gb'] = {enharmonic='F#',disp=-1,acc='#'}, ['Ab'] = {enharmonic='G#',disp=-1,acc='#'}, ['Bb'] = {enharmonic='A#',disp=-1,acc='#'}, ['Cx'] = {enharmonic='D ',disp=1}, ['Dx'] = {enharmonic='E ',disp=1}, ['Ex'] = {enharmonic='F#',disp=1,acc='#'}, ['Fx'] = {enharmonic='G ',disp=1}, ['Gx'] = {enharmonic='A ',disp=1}, ['Ax'] = {enharmonic='B ',disp=1}, ['Bx'] = {enharmonic='C#',disp=1,acc='#'}, ['Cv'] = {enharmonic='Bb ',disp=-1,acc='b'}, ['Dv'] = {enharmonic='C ',disp=-1}, ['Ev'] = {enharmonic='D ',disp=-1}, ['Fv'] = {enharmonic='Eb',disp=-1,acc='b'}, ['Gv'] = {enharmonic='F ',disp=-1}, ['Av'] = {enharmonic='G ',disp=-1}, ['Bv'] = {enharmonic='A ',disp=-1} } local Options = { ['No sharps'] = {index=1,notes={'C#','D#','E#','F#','G#','A#','B#'}}, ['No flats'] = {index=2,notes={'Cb','Db','Eb','Fb','Gb','Ab','Bb'}}, ['Reverse sharps/flats'] = {index=3,notes={'C#','D#','E#','F#','G#','A#','B#','Cb','Db','Eb','Fb','Gb','Ab','Bb'}}, ['No doubles'] = {index=4,notes={'Cx','Dx','Ex','Fx','Gx','Ax','Bx','Cv','Dv','Ev','Fv','Gv','Av','Bv'}}, ['Preferably naturals'] = {index=5,notes={'E#','B#','Fb','Cb'}}, ['Avoid forced naturals'] = {index=6, notes={}}, ['Common prefered accidentals'] = {index=7,notes={'A#','Db','D#','Gb', 'Ab'}}, ['Sharps up, flats down'] = {index=8, notes={}}, ['Your changes...'] = {index=9, notes={}}, } -- Options --general functions------------------------------ do --redefine tostring() local Luatostring = tostring tostring = function(t, tab) tab = tab and tab.." " or "" if type(t) ~= 'table' then return Luatostring(t) end--if local s = tab.."{" for k, v in pairs(t) do if s ~= tab.."{" then s = s..", " end--if if string.sub(s, #s-2) == "}, " then s = s.."\n"..tab.." " end--if local s1 = tostring(k) local s2 = tostring(v, tab) s = s..s1.." = "..s2 end--for return "\n"..s.."}" end--tostring end--do function ShowVar(name, var) if Testing then nwcut.warn(name == "" and "" or tostring(name).." ==> ", tostring(var),"\n") end--if end -- ShowVar 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 --general nwcfile related functions-------------------- local function Selected(staff, index) return staff == ActiveStaff and index >= SelectStart and index < SelectEnd end --Selected() local function AtCursor(staff, index) return staff.Active and index > 0 and index == CaretIndex - 1 end --AtCursor() local function IsScoreHeader(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--IsScoreHeader 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 local function CopyNwc(it) return nwcItem.new(it:__tostring()) end-- CopyNwc local function ToActUpon(Staff, index) return not Selection and CurrentStaff == ActiveStaff or Selected(CurrentStaff, index) end -- ToActUpon local function ProcessEditor(item) ActiveStaff = item.Opts.ActiveStaff + 0 local CaretIndex = item.Opts.CaretIndex + 0 local SelectIndex = item.Opts.SelectIndex Selection = (SelectIndex ~= nil) if Selection then SelectIndex = SelectIndex + 0 end--if if Selection then SelectStart = CaretIndex < SelectIndex and CaretIndex or SelectIndex SelectEnd = CaretIndex > SelectIndex and CaretIndex or SelectIndex end --if Header.Editor = item end -- ProcessEditor local function LoadFont(item) Header.Font = Header.Font or {} Header.Font[item.Opts.Style] = item end -- LoadFont(item) local function LoadLyrics(item) local n = string.sub(item.ObjType, 6, 6) if n == "s" then Staff.Lyrics = item else n = n+0 Staff.Lyric[n] = item end--if end--ProcessLyrics local function LoadStaffProperties(item) Staff.Properties[2] = Staff.Properties[1] and item or nil Staff.Properties[1] = Staff.Properties[1] or item end -- ProcessStaffProperties(item) local function LoadAddStaff(item) CurrentStaff = CurrentStaff + 1 Staves[CurrentStaff] = {} Staff = Staves[CurrentStaff] Staff.AddStaff = item Staff.Properties = {} Staff.items = {} Staff.Lyric = {} end -- LoadAddStaff(item) local function LoadScoreHeader(item) Header[item.ObjType] = item end--LoadScoreHeader local function LoadDefault(item) if IsScoreHeader(item) then LoadScoreHeader(item) elseif IsStaffHeader(item) then Staff[item.ObjType] = item else table.insert(Staff.items, item) end -- if IsScoreHeader end -- LoadDefault(item) local function SaveScore() nwcut.writeline(Header.Editor) nwcut.writeline(Header.SongInfo) nwcut.writeline(Header.PgSetup) for k,v in pairs(Header.Font) do nwcut.writeline(v) end--for nwcut.writeline(Header.PgMargins) for i , s in ipairs(Staves) do nwcut.writeline(s.AddStaff) nwcut.writeline(s.Properties[1]) nwcut.writeline(s.Properties[2]) nwcut.writeline(s.StaffInstrument) if s.Lyrics then nwcut.writeline(s.Lyrics) for j, lr in ipairs(s.Lyric) do nwcut.writeline(lr) end--for end--if for _, it in ipairs(s.items) do nwcut.writeline(it) end--for end--for end--SaveScore() -- Tool specific funtions ----------------------------- local function ProcessKey(item) Key = {} for k, v in pairs(item.Opts.Signature) do Key[string.sub(k,1,1)] = string.sub(k,2,2) end --for ShowVar('Key', Key) end -- ProcessClef(item) local function ProcessClef(item) Clef = item.Opts.Type end -- ProcessClef(item) local function NoteName(pos) local NoteChars = {"C", "D", "E", "F", "G", "A", "B"} local ClefVals = {Bass=5,Treble=0,Alto=-1,Tenor=1, Percussion=5} local CVal = ClefVals[Clef] local cpos = pos.Position - CVal local octave = cpos < -6 and -2 or cpos < 1 and -1 or cpos < 8 and 0 or cpos < 15 and 1 local ind = math.fmod((pos.Position - CVal), 7) if ind < 1 then ind = ind + 7 end--if local acc = pos.Accidental or '' return NoteChars[ind]..acc, octave end -- NoteName(pos) local function CopyTable(table) local t = {} for k,v in pairs(table) do t[k] = v end --or k,v in pairs(Key) return t end--CopyTable local function ReportChanges() Report:add('\n') for i = 1, ChangeList.Maxni, 1 do local v = ChangeList[i] if v then for j,w in pairs(v) do Report:add(MeasureNumber,'/', i,': ', w.Old, ' -> ', w.New, ', ') end-- for j,w in pairs(v) end -- if v end -- for i = 1, 1, Maxn ChangeList = {} ChangeList.Maxni = 0 end -- ReportChanges local function AddChangeList(ni, np, name1, name2) if not ChangeList[ni] then ChangeList[ni] = {} ChangeList[ni][np] = {} ChangeList[ni][np].Old = name1 elseif not ChangeList[ni][np] then ChangeList[ni][np] = {} ChangeList[ni][np].Old = name1 end --if not ChangeList[ni] ChangeList[ni][np].New = name2 ChangeList.Maxni = ni > ChangeList.Maxni and ni or ChangeList.Maxni ShowVar('ChangeList', ChangeList) end-- local function CheckAccidentals() MeasureKey = CopyTable(Key) local ind = MeasureStart local ni = 0 repeat ShowVar('MeasureKey',MeasureKey) ind = ind + 1 local it = Staff.items [ind] if it and it:ContainsNotes() then ni = ni + 1 local np = 0 for notepos in it:AllNotePositions() do np = np + 1 local name = NoteName(notepos) local note = string.sub(name,1,1) if notepos.Accidental and not notepos.CourtesyAcc then if notepos.Accidental == 'n' and not MeasureKey[note] or MeasureKey[note] == notepos.Accidental then notepos.Accidental = nil ShowVar('checkaccidentals', ni) AddChangeList(ni, np, name, NoteName(notepos)) else MeasureKey[note] = notepos.Accidental end--if notepos.Accidental end--if notepos.Accidental end -- for notepos in it:AllNotePositions end --if it and it:ContainsNotes() until not it or it:Is('Bar') end --CheckAccidentals local function ProcessBar(item) if MeasureChanged then CheckAccidentals() ReportChanges() MeasureChanged = false end--if MeasureChanged MeasureKey = CopyTable(Key) ShowVar('MeasureKey',MeasureKey) MeasureStart = CurrentIndex if not EmptyMeasure then if not item.Opts.XBarCnt then MeasureNumber= MeasureNumber + 1 EmptyMeasure = true end -- if not item.Opts.XBarCnt end -- if not EmptyMeasure ShowVar('MeasureNumber', MeasureNumber) ItemNumber = 0 end -- ProcessBar(item) local function ProcessRestMultiBar(item) MeasureNumber = MeasureNumber + item.Opts.NumBars - 1 end--ProcessRestMultiBar local function PitchDist(pos1, pos2) local NoteTab = {C = 1, D = 2, E = 3, F = 3.5, G = 4.5, A = 5.5, B = 6.5} local AccTab = {'v','b','n','#','x'} local function Pitch(pos) local name, oct = NoteName(pos) local note = string.sub(name,1,1) local acc = pos.Accidental or MeasureKey[note] local cacc = (acc and acc ~= '') and (IsIn(AccTab, acc) - 3)/2 or 0 local pitch = NoteTab[note] + cacc + 6*oct return pitch end-- Pitch return Pitch(pos2) - Pitch(pos1) end-- Pitch local function beforechange(notepos, npos, mn, ind, np) local name = NoteName(notepos) local note = string.sub(name,1,1) local acc = notepos.Accidental local nname = NoteName(npos) if not npos.Accidental and MeasureKey[note] == acc then if not Ties[npos.Position] then npos.Accidental = acc else npos.Position = npos.Position + EnharmonicsTable[name].disp end --if not Ties[npos.Position] ShowVar('beforechange', ind) AddChangeList(ind, np, nname, NoteName(npos)) elseif npos.Accidental then MeasureKey[note] = npos.Accidental end --if not npos.Accidental... end --beforechange local function afterchange(notepos, npos, mn, ind,np) local nname = NoteName(npos) local nnote = string.sub(nname,1,1) ShowVar('MeasureKey/'..nnote,MeasureKey[nnote]) ShowVar('Key/'..nnote, Key[nnote]) if not npos.Accidental and not MeasureKey[nnote] and not Ties[notepos.Position] then ShowVar('Key/'..nnote, Key[nnote]) if Key[nnote] then npos.Accidental = Key[nnote] else npos.Accidental = 'n' end--if MeasureKey[nnote] = npos.Accidental ShowVar('afterchange', ind) AddChangeList(ind, np, nname, NoteName(npos)) end--if npos.Accidental... if npos.Accidental then MeasureKey[nnote] = npos.Accidental end --if npos.Accidental end --afterchange local function hasforcednatural(notepos, npos) return npos.Accidental == 'n' end-- hasforcednatural local function ProcessNextNotes(notepos, process) MeasureKeySave = CopyTable(MeasureKey) local i= CurrentIndex + 1 local ni = ItemNumber local bar = false local name = string.sub(NoteName(notepos),1,1) Ties[notepos.Position] = notepos.Tied and notepos.Accidental while Staff.items[i] and (not bar or Ties[notepos.Position]) do local nitem = Staff.items[i] if nitem:ContainsNotes() then ni = ni + 1 local np = 1 for npos in nitem:AllNotePositions() do local nname = string.sub(NoteName(npos),1,1) -- Ties[npos.Position] = nil if nname == name then local mn = bar and MeasureNumber + 1 or MeasureNumber local result = process(notepos, npos, mn, ni, np) if Ties[notepos.Position] and math.abs(npos.Position - notepos.Position) < 2 then Ties[notepos.Position] = nil end -- if Ties[npos.Position] and.. if result then return result end--if end --if nname np = np + 1 end--for pos in nitem:AllNotePositions() end --if nitem:ContainsNotes() bar = nitem:Is('Bar') or bar i = i + 1 end --while Staff.items[i]... MeasureKey = CopyTable(MeasureKeySave) end--ProcessNextNotes local function ChangeNote(item) local np = 0 for notepos in item:AllNotePositions() do np = np + 1 local notename = NoteName(notepos) if IsIn(Options[Option].notes,notename) then Changed = true if not MeasureChanged then MeasureChanged = true end -- if not MeasureChanged ProcessNextNotes(notepos, beforechange) local oldnotename = notename notepos.Position = notepos.Position + EnharmonicsTable[notename].disp notepos.Accidental = EnharmonicsTable[notename].acc notename = NoteName(notepos) local note = string.sub(notename,1,1) if not notepos.Accidental and (MeasureKey[note] or Key[note]) then notepos.Accidental = 'n' end--if ShowVar('ChangeNote', ItemNumber) AddChangeList(ItemNumber,np,oldnotename,NoteName(notepos)) ProcessNextNotes(notepos, afterchange) if notepos.Accidental then MeasureKey[note] = notepos.Accidental end -- if notepos.Accidental end --if I end -- for i, notepos in ipairs(item.Opts.Pos) end -- ChangeNote(item) local function AvoidNaturals(item) for pos in item:AllNotePositions() do if pos.Accidental and pos.Accidental ~= 'n' and ProcessNextNotes(pos, hasforcednatural) then local nn = NoteName(pos) table.insert(Options[Option].notes, nn) ChangeNote(item) end-- if pos.Accidental... end --for pos in item:AllNotePositions() end --AvoidNaturals local function sortlistvals(v1,v2) local accs = {'#', 'b', 'x', 'v'} local note1 = string.sub(v1,1,1) local note2 = string.sub(v2,1,1) local acc1 = IsIn(accs,string.sub(v1,2,2)) local acc2 = IsIn(accs,string.sub(v2,2,2)) return (acc1 < acc2) or (acc1 == acc2 and note1 < note2) end--sortlistvals local function YourChanges() local listvals = {} for k,v in pairs(EnharmonicsTable) do local val = k..' -> '..v.enharmonic table.insert(listvals,val) end --for k,v in pairs(EnharmonicsTable) table.sort(listvals, sortlistvals) repeat YourChangeList = nwcut.prompt('Please select the notes you want to change :', '&', listvals) if not YourChangeList[1] then nwcut.msgbox("Please select at least one item or click 'Cancel'") end --if not YourChangeList[1] until YourChangeList[1] for i,v in ipairs(YourChangeList) do table.insert(Options[Option].notes, string.sub(v,1,2)) end -- for i,v in ipairs(YourChangeList) end --YourChanges local function CheckPitch(item) if not item:Is('Note') then return end--if local pos1 = item.Opts.Pos[1] if not pos1.Accidental or pos1.Accidental == 'n' then return end--if local i = CurrentIndex local it repeat i = i + 1 it = Staff.items[i] until not it or it:Is('Note') or it:Is('Chord') or it:Is('RestChord') or it:Is('Rest') if not it or not it:Is('Note') then return end--if local pos2 = it.Opts.Pos[1] if math.abs(pos1.Position - pos2.Position) > 3 then return end--if local dist = PitchDist(pos1, pos2) if pos1.Accidental == 'b' and (dist == 0.5 or dist == 1) or pos1.Accidental == '#' and (dist == -0.5 or dist == -1) then local nn = NoteName(pos1) table.insert(Options[Option].notes, nn) ChangeNote(item) Options[Option].notes= {} end--if end --CheckPitch local function ProcessNotes(item) local OptionProcesses = { ['No sharps'] = ChangeNote, ['No flats'] = ChangeNote, ['Reverse sharps/flats'] = ChangeNote, ['No doubles'] = ChangeNote, ['Preferably naturals'] =ChangeNote, ['Avoid forced naturals'] = AvoidNaturals, ['Common prefered accidentals'] = ChangeNote, ['Sharps up, flats down'] = CheckPitch, ['Your changes...'] = ChangeNote, } -- OptionProcesses for pos in item:AllNotePositions() do if pos.Accidental then MeasureKey[string.sub(NoteName(pos),1,1)] = pos.Accidental ShowVar('MeasureKey/'..ItemNumber,MeasureKey) end--if pos.Accidental end -- for pos in item:AllNotePositions() if ToActUpon(CurrentStaff, CurrentIndex) then local process = OptionProcesses[Option] process(item) end --if ToActUpon(CurrentStaff, CurrentIndex) end --ProcessNotes local function ProcessDefault(item) -- ShowVar('Default/Stage2', item) end -- ProcessDefault local function ProcessItem(item, stage) local ItemProcesses = { Stage1 = { Font = {LoadFont}, Editor = {ProcessEditor}, AddStaff = {LoadAddStaff}, StaffProperties = {LoadStaffProperties}, Lyrics = {LoadLyrics}, Lyric1 = {LoadLyrics}, Lyric2 = {LoadLyrics}, Lyric3 = {LoadLyrics}, Lyric4 = {LoadLyrics}, Lyric5 = {LoadLyrics}, Lyric6 = {LoadLyrics}, Lyric7 = {LoadLyrics}, Lyric8 = {LoadLyrics}, Default = {LoadDefault}, }, Stage2 = { Key = {ProcessKey}, Clef = {ProcessClef}, Bar = {ProcessBar}, RestMultiBar = {ProcessRestMultiBar}, Note = {ProcessNotes}, Chord = {ProcessNotes}, RestChord = {ProcessNotes}, Default = {ProcessDefault}, }, } local Processes = ItemProcesses[stage][item.ObjType] or ItemProcesses[stage].Default for _, process in ipairs(Processes) do process(item) end -- for _, process in ipairs(Processes) end--function ProcessItem local function CreatePromptList(options) local OptionTable = {} for k,v in pairs(options) do OptionTable[v.index] = k end -- for k in pairs(options) local OptionList = StringBuilder.new() for i, v in ipairs(OptionTable) do OptionList:add('|'..v) end--for i, v in ipairs(OptionList) OptionList:add('|Help') return OptionList:__tostring() end --CreatePromptList local function GetOption() local OptionString = CreatePromptList(Options) repeat Option = nwcut.prompt("Option: ", OptionString) if Option == 'Help' then nwcut.msgbox(HelpMsg, progname .. "/"..version.." - Help info") end--if if Option == 'Your changes...' then YourChanges() end-- if Option == 'Your Changes...' until Option ~= 'Help' end--GetOption ---------------- ---------------------- -- 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'") GetOption() for item in nwcut.items() do ProcessItem(item, 'Stage1') end --for item in nwcut.items() for i, staff in ipairs(Staves) do CurrentStaff = i Staff = staff if CurrentStaff == ActiveStaff then ShowVar("CurrentStaff", CurrentStaff) MeasureNumber = Header.PgSetup.Opts.StartingBar + 0 ShowVar('MeasureNumber', MeasureNumber) CurrentIndex = 1 MeasureStart = 1 ItemNumber = 0 EmptyMeasure = true Ties = {} for _, item in ipairs(staff.items) do ShowVar("CurrentIndex", CurrentIndex) if item:HasDuration() then EmptyMeasure = false end --if if item:ContainsNotes() then ItemNumber = ItemNumber + 1 end --if ProcessItem(item, 'Stage2') CurrentIndex = CurrentIndex + 1 end -- for _, item in ipairs(staff.items) if MeasureChanged then CheckAccidentals() ReportChanges() end --if MeasureChanged end --if CurrentStaff == ActiveStaff end -- for _, staff in ipairs(staff) if not Changed then Report:add("No changes") nwcut.msgbox(Report:__tostring(),progname .. " - Change report") else Report:prepend("('m/i' = measure number/note or chord number within measure)\n") Report:add("\nDo you want to make these changes?") if nwcut.askbox(Report:__tostring(), progname .. "/"..version.." - Change report") == 1 then SaveScore() end--if nwcut.askbox end -- if Changed