Move project to the right

Is there a script that can shift the whole project to the right, including tempo and parameter curves? I need to insert a bar at the start of the project. Alternatively can I set the start bar to 0 or -1?…. I am using version 1 not 2 as I need manual mode.

I have two possible scripts:-

‘shift.lua’ Copyright (c) 2021 hataori@protonmail.com
This moves by fractions or a number of whole bars but may slightly warp the parameter curves and leave remnants that need deleting.

copy_notes_and_parameters.lua Copyright 2022 RigoLigo
This seems to do a better job of copying parameter curves but is probably best used to copy to a new track and the curve start and end points need anchoring (add a pint to each end of the curve) or it will ramp from what ever the new start point is to your first specified point.

Neither copy vocal modes. Try them on a COPY of your project then you have a way to recover if it does go wrong!

Shift.lua

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

copy_notes_and_parameters.lua

--[[
    Synthesizer V Studio Pro Script
    Copy notes with parameters. MIT License.
    The script copies a group of selected notes to the destination along with
    corresponding parameters. It will wait for the user to navigate towards the
    destination, and does not incorporate traditional copy-paste operations.

    Copyright 2022 RigoLigo

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to
    deal in the Software without restriction, including without limitation the
    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    sell copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    IN THE SOFTWARE.
]]

function getClientInfo()
    return {
        name = SV:T("Copy Notes and Parameters"),
	category = "Pitches",
        author = "RigoLigo",
        versionNumber = 2
    }
end

function getTranslations(langCode)
    if langCode == "zh-cn" then
        return {
            {"Copy Notes and Parameters", "带参数复制音符"},
            {"No selection", "没有选择音符"},
            {"You must select the notes to be copied.", "必须先选中要复制的音符。"},
            {"Begin copying", "开始复制"},
            {"Confirm copying", "确认复制"},
            {"Timeout", "等待时间"},
            {" secs", " 秒"},
            {"Script will now wait for you to move your playhead and track selection to the desired destination.\\n"..
             "Select a timeout, and click OK to continue. Click Cancel to abort.\\n\\n"..
             "Note: Timeout of 0 will activte copying immediately.",
             "脚本会等你把播放头和选定的轨道移动到粘贴位置。\\n请选择等待时长,单击“确定”继续。单击“取消”中止。\\n\\n"..
             "注意:等待时间为0时,会立即触发复制操作。"},
            {"Are you now ready for copying? Click Yes to confirm copying.\\n\\n"..
             "Otherwise, select a timeout, and click No to continue waiting.\\n"..
             "Click Cancel to abort.",
             "准备好复制了吗?单击“是”开始复制。否则,可再选择一次等待时间,然后单击“否”继续等待。\\n单击“取消”中止。"}
        }
    end
end

function main()
    if checkHasSelection() then
        useSelectionBounds:exec()
    else
        SV:showMessageBox(SV:T("No selection"), SV:T("You must select the notes to be copied."))
    end
end

function dbgInfo(v)
    local x = "String form: <<" .. tostring(v) .. ">>\n"..
              "Type: <<" .. type(v) .. ">>"
    SV:showMessageBox("Debug", x);
end

function checkHasSelection()
    return SV:getMainEditor():getSelection():hasSelectedContent()
end

function getPlayheadTimeBlick()
    return SV:getProject():getTimeAxis():getBlickFromSeconds(SV:getPlayback():getPlayhead())
end

function safeOp2(a, b, op)
    if a == nil then
        if b == nil then
            return 0
        else
            return b
        end
    elseif b == nil then
        return a
    else
        return op(a, b)
    end
end

useSelectionBounds = {
    exec = function()
        local sel = SV:getMainEditor():getSelection()
        selNotes = sel:getSelectedNotes()
        selGroups = sel:getSelectedGroups()
        
        -- Get begin and end time of the section safely
        local beginTime1 = (#selNotes ~= 0) and selNotes[1]:getOnset() or nil
        local beginTime2 = (#selGroups ~= 0)and selGroups[1]:getOnset()or nil
        beginTime = safeOp2(beginTime1, beginTime2, math.min)
        
        local endTime1 = (#selNotes ~= 0) and selNotes[#selNotes]:getEnd() or nil
        local endTime2 = (#selGroups ~= 0)and selGroups[#selGroups]:getEnd()or nil
        endTime = safeOp2(endTime1, endTime2, math.max)
        
        -- Store the group UUID
        fromGroup = SV:getMainEditor():getCurrentGroup():getTarget()
        groupId = fromGroup:getUUID()
        
        useSelectionBounds:beginWaitCopy()
    end,
    
    beginWaitCopy = function()
        local firstWaitForm = {
            title = SV:T("Begin copying"),
            message = SV:T("Script will now wait for you to move your playhead and track selection to the desired destination.\n"..
                      "Select a timeout, and click OK to continue. Click Cancel to abort.\n\n"..
                      "Note: Timeout of 0 will activte copying immediately."),
            buttons = "OkCancel",
            widgets = {
                {
                    name = "timeout", type = "Slider", label = SV:T("Timeout"),
                    format = "%.1f" .. SV:T(" secs"), minValue = 0, maxValue = 40, interval = 0.5, default = 10
                }
            }
        }
        
        local result = SV:showCustomDialog(firstWaitForm)
        if result.status == true then
            if result.answers.timeout == 0 then
                useSelectionBounds:doCopy()
            else
                SV:setTimeout(result.answers.timeout * 1000, useSelectionBounds.confirmCopy)
            end
        else
            SV:finish()
        end
    end,
    
    confirmCopy = function()
        local confirmForm = {
            title = SV:T("Confirm copying"),
            message = SV:T("Are you now ready for copying? Click Yes to confirm copying.\n\n"..
                      "Otherwise, select a timeout, and click No to continue waiting.\n"..
                      "Click Cancel to abort."),
            buttons = "YesNoCancel",
            widgets = {
                {
                    name = "timeout", type = "Slider", label = SV:T("Timeout"),
                    format = "%.1f" .. SV:T(" secs"), minValue = 3, maxValue = 40, interval = 0.5, default = 10
                }
            }
        }
        
        local result = SV:showCustomDialog(confirmForm)
        if result.status == "No" then
            SV:setTimeout(result.answers.timeout * 1000, useSelectionBounds.confirmCopy)
        elseif result.status == "Yes" then
            useSelectionBounds:doCopy();
        else
            SV:finish()
        end
    end,
    
    doCopy = function()
        toGroup = SV:getMainEditor():getCurrentGroup():getTarget()
        -- Snap to nearest
        destTime = SV:getMainEditor():getNavigation():snap(getPlayheadTimeBlick())
        -- Remember to minus the current group onset
        timeOffset = destTime - beginTime - SV:getMainEditor():getCurrentGroup():getOnset()
        
        useSelectionBounds:copyNotes()
        useSelectionBounds:copyGroups()
        
        useSelectionBounds:copyParams()
        
        SV:finish()
    end,
    
    copyNotes = function()
        for _, i in pairs(selNotes) do
            local n = i:clone()
            n:setOnset(n:getOnset() + timeOffset)
            toGroup:addNote(n)
        end
    end, 
    
    copyGroups = function()
    -- FIXME BUG  Synthesizer V doens't provide an NoteGroupReference:setOnset API which is crucial for the operation
    
--         local toGroup = SV:getMainEditor():getCurrentGroup():getTarget()
--         
--         -- Don't copy a group into a group, SynthV editor doesn't support this
--         if #toGroup ~= 0 and not toGroup:isMain() then
--             return
--         end
--         
--         local toTrack = SV:getMainEditor():getCurrentTrack()
--         local timeOffset = getPlayheadTimeBlick() - beginTime
--         for k, v in pairs(selGroups) do
--             local n = v:clone()
--             n:setOnset(n:getOnset() + timeOffset)
--             toTrack:add(n)
--         end
    end,
    
    copyParams = function()
        params = {
            pitchDelta = 0, 
            vibratoEnv = 1, 
            loudness = 0, 
            tension = 0, 
            breathiness = 0, 
            voicing = 1, 
            gender = 0
        }
        for i, def in pairs(params) do
            local p = fromGroup:getParameter(i)
            local pts = p:getPoints(beginTime, endTime) -- Get points in range
            
            -- If no points found?
            if #pts == 0 then
                -- Check value right at the head and tail and add exact points.
                pts[1] = {beginTime, p:get(beginTime)}
                pts[2] = {beginTime, p:get(endTime)}
            end
            
            -- Make the point at beginning and ending the exact values
            pts[1][2] = p:get(pts[1][1])
            pts[#pts][2] = p:get(pts[#pts][1])
            
            -- Clear the destination area, extends 100 blinks for safety
            local q = toGroup:getParameter(i)
            q:remove(destTime - 100, destTime + (endTime - beginTime) + 100)
            -- Add each one into destination
            for _, j in pairs(pts) do
                q:add(j[1] + timeOffset, j[2])
            end
            
            ::NextParam::
        end
    end
}

End of post

I created a script to move n measures to the left or right, while preserving the settings and tempo of the original track. See : MoveTime
This script clones all note groups and recreates them at the offset time position (preserving all vocal parameters). It moves all notes outside of any group (main group) and also shifts all tempo markers.
This version is for the Synthesizer V 1. Another one is also available for V2 in my github repo.

Hi @jfa - wow thanks for this, it’s nearly exactly what I’m looking for. How do I select the whole project, ie all tracks to be shifted? At the moment it is only shifting the selected track, so by the time I shift all 4 tracks, the tempo has shifted 4 times.

It is not possible with this first version. Get the new one!
There : MoveTime V1

1 Like

Wow - yes that works, thank you so much! :folded_hands:

1 Like