Sample Start/End in Macro Page?

Yes thanks, I see, no problem I just added the two lines I added in the previous script and it works great! Thanks a lot for your help!

1 Like

I have clicks and pops when I loop a grain zone, I tried all possible solutions, crossfading the loop and adding a fade in and fade out to the sample but I continue to have those clicks and pops.
Any idea how I can avoid them? My instrument is completely useless with those clicks and pops!
This is the modified script that Iā€™m using, as you can see when I change the sample start and end points the loop start and end point change accordingly.

But The problem is NOT the script, I tried also building a simple instrument with a grain zone, setting the same values for the sample start/end points and the loop start/end points and I have the same problem. This problem is not happening in padshop.

local zones = this.parent:findZones()

zoneNames = {}
for i = 1, #zones do
    zoneNames[i] = zones[i].name
end

function zoneSelectChanged()
    getSampleLength()
    scope = "@0:"..zones[ZoneSelect].name.."/"
end

defineParameter("ZoneSelect", nil, 1, zoneNames, zoneSelectChanged)
defineParameter("scope", nil, "")
defineParameter("SampleStart", nil, 0, 0, 0x7fffffff, 1)
defineParameter("SampleEnd", nil, 0, 0, 0x7fffffff, 1)
defineParameter("Filename", nil, "", function() onFilenameChanged() end)
defineParameter("PitchDetectionProgress", nil, 0, 0, 100)
defineParameter("PitchDetection", nil, false)

function sampleStartChanged()
    local zone = zones[ZoneSelect]
    zone:setParameter("SampleOsc.SampleStart", SampleStart)
zone:setParameter("SampleOsc.SustainLoopStartA", SampleStart) 
   if SampleEnd < SampleStart then
        SampleEnd = SampleStart
        zone:setParameter("SampleOsc.SampleEnd", SampleEnd)
    end
end

function sampleEndChanged()
    local zone = zones[ZoneSelect]
    zone:setParameter("SampleOsc.SampleEnd", SampleEnd)
zone:setParameter("SampleOsc.SustainLoopEndA", SampleEnd) 
   if SampleStart > SampleEnd then
        SampleStart = SampleEnd
        zone:setParameter("SampleOsc.SampleStart", SampleStart)
    end
end

function setRootkey(sample)
    local zone = zones[ZoneSelect]
    local pitch, voiced = sample:getPitch(0, -1)
    if voiced and voiced then
        local rootkey, detune = math.modf(pitch)
        if detune >= 0.5 then
            rootkey = rootkey + 1
            detune = detune - 1
        end
        detune = math.floor((detune * 100) + 0.5)
        print(pitch, voiced, rootkey, detune)
        zone:setParameter("SampleOsc.Rootkey", rootkey)
        zone:setParameter("SampleOsc.Tune", detune)
    end
end

function keyTextFromSampleName(st)
    local notes = {c = 0, d = 2, e = 4, f = 5, g = 7, a = 9, b = 11}
    local mn = {}
    st = string.match(st, "([^\\/]+)%.%a+$")
    print(st)
    for k, s, n in string.gmatch(st, "[_%-%s]([A-Ga-g])(%#?)(%-?%d)") do
        if k and n then
            local midinn = notes[string.lower(k)] + (s == "#" and 1 or 0) + (n + 2) * 12
            table.insert(mn, midinn)
            print(k, s, n, midinn)
        end
    end
    return mn[#mn]
end

function onFilenameChanged()
    local zone = zones[ZoneSelect]
    local sample = AudioFile.open(Filename)
    if sample.valid then
        zone:setParameter("SampleOsc.Filename", sample.fileName)
        zone:setParameter("SampleOsc.SampleStart", 0)
        zone:setParameter("SampleOsc.SampleEnd", sample.length)
        -- sample start and end parameters
        defineParameter("SampleStart", nil, 0, 0, sample.length, 1, sampleStartChanged)
        defineParameter("SampleEnd", nil, 0, 0, sample.length, 1, sampleEndChanged)
        SampleStart = 0
        SampleEnd = sample.length
        -- rootkey and detune
        local rootKey = keyTextFromSampleName(sample.fileName)
        zone:setParameter("SampleOsc.Rootkey", rootKey or 60)
        zone:setParameter("SampleOsc.Tune", sample.detune or 0)
        if not rootKey and PitchDetection then
            sample:analyzePitch(setRootkey)
            while sample:getPitchAnalysisProgress() < 1 do
                PitchDetectionProgress = sample:getPitchAnalysisProgress() * 100
                wait(250)
            end
            PitchDetectionProgress = 0        
        end
    end
end

function getSampleLength()
    local zone = zones[ZoneSelect]
    local fn = zone:getParameter("SampleOsc.Filename")
    local sample = AudioFile.open(fn)
    if sample.valid then
        defineParameter("SampleStart", nil, 0, 0, sample.length, 1, sampleStartChanged)
        defineParameter("SampleEnd", nil, 0, 0, sample.length, 1, sampleEndChanged)
        SampleStart = zone:getParameter("SampleOsc.SampleStart")
        SampleEnd = zone:getParameter("SampleOsc.SampleEnd")    
    end
end

function onLoad()
    zoneSelectChanged()
end

function onNote(e)
    playNote(e.note, e.velocity, -1, zones[ZoneSelect])
end

Itā€™s probably because the loop points are not at zero crossing. If the fades and crossfades donā€™t help this could be a problem.

Iā€™m still experimenting but it looks like disabling ā€œSnapā€ and enabling ā€œSnap to zero crossingā€ improve a lot the clicks and pops problem

Hi, Iā€™m trying to modify this script so that when I move the start and end points of the sample they snap to a 1/32 grid instead of moving freely, any suggestions on how I can do this?

I would try to calculate how many samples is 1/32. Assuming the sampling rate is 48 kHz and tempo is 120bpm it should be 3000. Then round the sample start/end to multiples of whatever the 1/32 is (3000 in this case).

ā€œThanks for the reply! Iā€™ve tried several methods, but none seem to work quite right. The best approach I have so far involves using the plus/minus buttons to snap the start and end points to the grid. This code works perfectly for the StartStepPlus and StartStepMinus buttonsā€”the start point snaps precisely to the grid, even when I change the tempo. However, as soon as I use the EndStepMinus and EndStepPlus buttons, everything gets messed up. The EndStepPlus button also doesnā€™t work at all.
Does anyone know why itā€™s working perfectly for the Start buttons but not for the End buttons?ā€

local layer = this.parent
local zones = this.parent:findZones()

zoneNames = {}
for i = 1, #zones do
    zoneNames[i] = zones[i].name
end

function zoneSelectChanged()
    getSampleLength()
    scope = "@0:Layer 01/@0:"..zones[ZoneSelect].name.."/"
end

defineParameter("ZoneSelect", nil, 1, zoneNames, zoneSelectChanged)
defineParameter("scope", nil, "")
defineParameter("SampleStart", nil, 0, 0, 0x7fffffff, 1)
defineParameter("SampleEnd", nil, 0, 0, 0x7fffffff, 1)
defineParameter("Filename", nil, "", function() onFilenameChanged() end)
-- New parameters for plus/minus switches
defineParameter("StartStepPlus", "Start +1/16", false, nil, function() stepStart(1) end)
defineParameter("StartStepMinus", "Start -1/16", false, nil, function() stepStart(-1) end)
defineParameter("EndStepPlus", "End +1/16", false, nil, function() stepEnd(1) end)
defineParameter("EndStepMinus", "End -1/16", false, nil, function() stepEnd(-1) end)

-- Calculate the size of one 1/16th step
function getGridStep(totalLength)
    return math.max(1, math.floor(totalLength / 16))
end

-- Function to step the start position
function stepStart(direction)
    local zone = zones[ZoneSelect]
    local totalLength = zone:getParameter("SampleOsc.SampleEnd")
    local gridStep = getGridStep(totalLength)
    
    -- Calculate new position
    local newStart = SampleStart + (gridStep * direction)
    
    -- Ensure we stay within bounds
    newStart = math.max(0, newStart)
    newStart = math.min(newStart, SampleEnd)
    
    -- Update parameters
    SampleStart = newStart
    zone:setParameter("SampleOsc.SampleStart", SampleStart)
    zone:setParameter("SampleOsc.SustainLoopStartA", SampleStart)
end

-- Function to step the end position
function stepEnd(direction)
    local zone = zones[ZoneSelect]
    local totalLength = zone:getParameter("SampleOsc.SampleEnd")
    local gridStep = getGridStep(totalLength)
    
    -- Calculate new position for SampleEnd
    local newEnd = SampleEnd + (gridStep * direction)
    
    -- Ensure we stay within bounds
    newEnd = math.max(SampleStart, newEnd) -- End point shouldn't be before start
    newEnd = math.min(newEnd, totalLength)  -- End point shouldn't exceed sample length
    
    -- Update parameters
    SampleEnd = newEnd
    zone:setParameter("SampleOsc.SampleEnd", SampleEnd)
    zone:setParameter("SampleOsc.SustainLoopEndA", SampleEnd)
end

function onFilenameChanged()
    local zone = zones[ZoneSelect]
    local sample = AudioFile.open(Filename)
    if sample.valid then
        zone:setParameter("SampleOsc.Filename", sample.fileName)
        zone:setParameter("SampleOsc.SampleStart", 0)
        zone:setParameter("SampleOsc.SampleEnd", sample.length)
        
        -- Define sample start and end parameters
        defineParameter("SampleStart", nil, 0, 0, sample.length, 1)
        defineParameter("SampleEnd", nil, sample.length, 0, sample.length, 1)
        SampleStart = 0
        SampleEnd = sample.length
        
        -- Set the sustain loop points
        zone:setParameter("SampleOsc.SustainLoopStartA", SampleStart)
        zone:setParameter("SampleOsc.SustainLoopEndA", SampleEnd)
    end
end

function getSampleLength()
    local zone = zones[ZoneSelect]
    local fn = zone:getParameter("SampleOsc.Filename")
    local sample = AudioFile.open(fn)
    if sample.valid then
        defineParameter("SampleStart", nil, 0, 0, sample.length, 1)
        defineParameter("SampleEnd", nil, sample.length, 0, sample.length, 1)
        
        SampleStart = zone:getParameter("SampleOsc.SampleStart")
        SampleEnd = zone:getParameter("SampleOsc.SampleEnd")
    end
end

function onLoad()
    zoneSelectChanged()
end

function onNote(e)
    playNote(e.note, e.velocity, -1, zones[ZoneSelect])
end