SCRIPT_TITLE = "RV Shift Notes & Params" paramTypeNames = { "pitchDelta", "vibratoEnv", "loudness", "tension", "breathiness", "voicing", "gender", "toneShift" } local inputForm = { title = SV:T("Shift Notes & Params"), message = SV:T("Workaround for the \"many notes with params shifting crash\"\nshifts everything between 1st and last note selected"), buttons = "OkCancel", widgets = { { name = "cbDirection", type = "ComboBox", label = "Direction", choices = {"Forward", "Backward"}, default = 0 }, { name = "cbUnit", type = "ComboBox", label = "Unit", choices = {"Measure (at 1st note)", "Quarter", "Time (sec)"}, default = 0 }, { name = "slAmount", type = "Slider", label = "How much to shift", format = "%1.0f", minValue = 0, maxValue = 16, interval = 1, default = 0 }, { name = "tbAmount", type = "TextBox", label = "How much to shift (can use floating point numbers)", default = "0" } } } function getClientInfo() return { name = SV:T(SCRIPT_TITLE), author = "Hataori@protonmail.com", category = "Real Voice", versionNumber = 2, minEditorVersion = 65537 } end function process() local timeAxis = SV:getProject():getTimeAxis() local scope = SV:getMainEditor():getCurrentGroup() local group = scope:getTarget() -- determine start and end time local minTime_b, maxTime_b = math.huge, 0 local noteCnt = group:getNumNotes() if noteCnt == 0 then -- no notes in track return else local selection = SV:getMainEditor():getSelection() local selectedNotes = selection:getSelectedNotes() if #selectedNotes == 0 then SV:showMessageBox(SV:T("Nothing selected"), SV:T("Select notes to shift (only a start and end note is enough)")) return else table.sort(selectedNotes, function(noteA, noteB) return noteA:getOnset() < noteB:getOnset() end) for _, note in ipairs(selectedNotes) do local onset_b, nend_b = note:getOnset(), note:getEnd() if onset_b < minTime_b then minTime_b = onset_b end if nend_b > maxTime_b then maxTime_b = nend_b end end end end assert(maxTime_b > minTime_b) -- list of notes to shift local notes = {} for i = 1, group:getNumNotes() do local note = group:getNote(i) local onset_b, nend_b = note:getOnset(), note:getEnd() if nend_b > minTime_b and onset_b < maxTime_b then table.insert(notes, note) end end inputForm.title = inputForm.title.." ("..#notes.." notes)" -- show dialog local dlgResult = SV:showCustomDialog(inputForm) local amount = tonumber(dlgResult.answers.tbAmount) local amountSlider = tonumber(dlgResult.answers.slAmount) if not dlgResult.status or (amount + amountSlider) == 0 then return end -- cancel pressed or no shift local direction = 1 - 2 * dlgResult.answers.cbDirection -- 1 forward, -1 backward amount = (amount + amountSlider) * direction local timeConvert, shift = false, 0 local unit = dlgResult.answers.cbUnit local shift = 0 if unit == 0 then local measure = timeAxis:getMeasureMarkAtBlick(minTime_b) shift = measure.numerator / measure.denominator * 4 * SV.QUARTER * amount -- in blicks elseif unit == 1 then shift = SV.QUARTER * amount else timeConvert = true end -- do the shift for _, note in ipairs(notes) do local onset_b = note:getOnset() local newOnset_b = onset_b if timeConvert then local onset = timeAxis:getSecondsFromBlick(onset_b) newOnset_b = timeAxis:getBlickFromSeconds(onset + amount) else newOnset_b = onset_b + shift end if newOnset_b < 0 then SV:showMessageBox("Error!", "You are about to move notes before zero time!\nFirst make a room by selecting all notes and shifting them forward.") return end note:setOnset(newOnset_b) end -- correction of short pauses and overlap due to rounding if timeConvert then for _, note in ipairs(notes) do local onset_b = note:getOnset() local ni = note:getIndexInParent() local nextNote = group:getNote(ni + 1) if nextNote then if math.abs(nextNote:getOnset() - note:getEnd()) <= 1 then note:setDuration(nextNote:getOnset() - note:getOnset()) end end end end for _, par in ipairs(paramTypeNames) do local am = group:getParameter(par) -- automation track -- save points to list local points = am:getPoints(minTime_b, maxTime_b) -- add boundary points local startval = am:get(minTime_b) table.insert(points, 1, {minTime_b, startval}) local endval = am:get(maxTime_b) table.insert(points, {maxTime_b, endval}) -- remove from track and add boundaries back am:remove(minTime_b, maxTime_b) am:add(minTime_b, startval) am:add(maxTime_b, endval) -- target times local newStartTime_b, newEndTime_b if timeConvert then local minTime = timeAxis:getSecondsFromBlick(minTime_b) newStartTime_b = timeAxis:getBlickFromSeconds(minTime + amount) local maxTime = timeAxis:getSecondsFromBlick(maxTime_b) newEndTime_b = timeAxis:getBlickFromSeconds(maxTime + amount) else newStartTime_b, newEndTime_b = minTime_b + shift, maxTime_b + shift end startval = am:get(newStartTime_b - 1) endval = am:get(newEndTime_b + 1) am:remove(newStartTime_b, newEndTime_b) if amount < 0 or (amount > 0 and newStartTime_b > maxTime_b) then am:add(newStartTime_b - 1, startval) end if amount > 0 or (amount < 0 and newEndTime_b < minTime_b) then am:add(newEndTime_b + 1, endval) end -- place points for _, pt in ipairs(points) do if timeConvert then local onset = timeAxis:getSecondsFromBlick(pt[1]) am:add(timeAxis:getBlickFromSeconds(onset + amount), pt[2]) else am:add(pt[1] + shift, pt[2]) end end end end function main() process() SV:finish() end