Sample Start/End in Macro Page?

I have created my own instrument with a GRAIN zone type.
Is it possible to add a Sample Start/End control similar to what is found in Padshop?

Screen Shot 2022-08-03 at 07.25.19 AM

To start experimenting I have just created two sliders in the Macro Page one connected to the sample start value and the other to the sample end value but It looks like when I move them the values go completely crazy!

Thanks in advance for any help!

Did you connect the sliders to SampleStart and SampleEnd parameters directly?

When you check the max value in parameter list you will find it’s always the same and it doesn’t reflect the length of the currently loaded sample. So it’s likely you get way beyond the actual sample end quite easily.

Do you use a sample display on your macro page? I think it shows the sample between sample start and end markers. So once you start changing those the sample display can become a mess.

But I like the idea of the sliders. You could make them transparent and the same size as the sample display. At least in theory.

Yes, you are right, the max value of the sample end is a lot more than the real length of the sample, that’s why the slider goes completely crazy. I think a script is necessary here. I think the way is to get the sample end value and force the slider not to pass that value, maybe It’s possible to change the “Max Value” to the Sample End Value so they can match? Any example of a script on how to do it if that is possible? Anyway, if Halion programmers let us connect the sample end and start values to the Macro Page there should be a way to make it work… :slightly_smiling_face:

You could try something like this:

function sampleStartChanged()
    SampleStart = math.floor(SampleStart)
    local zone = this.parent:getZone()
    zone:setParameter("SampleOsc.SampleStart", SampleStart)
    if SampleEnd < SampleStart then
        SampleEnd = SampleStart
        zone:setParameter("SampleOsc.SampleEnd", SampleEnd)
    end
end

function sampleEndChanged()
    SampleEnd = math.floor(SampleEnd)
    local zone = this.parent:getZone()
    zone:setParameter("SampleOsc.SampleEnd", SampleEnd)
    if SampleStart > SampleEnd then
        SampleStart = SampleEnd
        zone:setParameter("SampleOsc.SampleStart", SampleStart)
    end
end

function getSampleLength()
    local zone = this.parent:getZone()
    local fn = zone:getParameter("SampleOsc.Filename")
    local sample = AudioFile.open(fn)
    if sample.valid then
        defineParameter("SampleStart", nil, 0, 0, sample.length, sampleStartChanged)
        defineParameter("SampleEnd", nil, 0, 0, sample.length, sampleEndChanged)
        SampleEnd = sample.length
    else
        defineParameter("SampleStart", nil, 0, 0, 0x80000000, sampleStartChanged)
        defineParameter("SampleEnd", nil, 0, 0, 0x80000000, sampleEndChanged)
    end
end

getSampleLength()

Hello, It works but is working only for the first zone, in my instrument I have several sample zones that I select in the GUI with a menu. I tried to add this at the beginning of your code but nothing changed is working only for the first zone even if I select a different zone with the menu.
thanks in advance for any help!

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

That’s a good start. You need to make sure it gets the correct zone. You can use the value of the menu.

local zone = this.parent:getZone(ZoneMenu)

You also need a callback for your menu parameter so when you select different zone you adjust the parameters. You can just call the getSampleLength again. However because you have more zones you should also update the script parameter values so they match the zone parameters. Otherwise they would get reset each time you change zone.

I tried to substitute in your code this:

local zone = this.parent:getZone()

with this:

local zone = this.parent:getZone(ZoneMenu)

but i’m getting this error message
Screen Shot 2022-08-04 at 14.43.47 PM

You need to use the name of your menu parameter. The value of that parameter is a number (integer). That should work as the index.

Here is a variation of the script that could work for multiple zones.

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)

function sampleStartChanged()
    local zone = zones[ZoneSelect]
    zone:setParameter("SampleOsc.SampleStart", 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)
    if SampleStart > SampleEnd then
        SampleStart = SampleEnd
        zone:setParameter("SampleOsc.SampleStart", SampleStart)
    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

SampleStartEnd.vstpreset (12.8 KB)

Great thanks!

I’m trying to adapt the script to my GRAIN instrument.
I have successfully added two lines of code in order to have the loop start and loop end values reflect the sample start and end ones and it works great!

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

Is it possible to have the grain “Position” bar graphically act inside the loop (like in Padshop)? I mean the Grain Position parameter is working but, as you can see in the screenshot attached, is graphically shown on the entire sample and not only inside the loop borders…

Screen Shot 2022-08-05 at 08.02.37 AM

You need to edit the waveform element. Instead of SampleOsc.PlayPosition connect the Grain.PlayData parameter. Or just type in: @id:100064

Yes I know I have already done that.
What I was meaning is that the grain position white cursor is shown outside the red loop borders, is not possible to have it inside the red loop border in order to show exactly where the grain position is?

Screen Shot 2022-08-05 at 09.38.27 AM

In Padshop is shown correctly, if you select a start/end point the grain position cursor is shown inside those points and can’t go out.

Screen Shot 2022-08-05 at 09.40.56 AM

I see what you mean now. But I don’t think it’s possible the same way.

You could connect the SampleStart and SampleEnd parameters to where they should be… to the sample start and end of the waveform element. The grain position cursor should be correct. However this will cause the waveform to zoom and always show just the part of the sample that’s between sample start and end points. I think that’s the intended use.

I was misusing the release loop so that the whole sample is shown and you could see where the sample start and end are set. But this makes the grain position cursor to be displayed incorrectly.

So the options I can think of are either connect the parameters correctly and make the waveform zoom or don’t use the grain position cursor.

But you are right about not using the grain position it make it work!

I’m trying to add part of the “samplestartend” code to the “DropSample” code in order to have the sample start and sample end points updated when I drop a sample but I tried several options but none of them works. Can you please tell me wich part of the code I have to add to the “Drop sample” one?

defineParameter("Filename", nil, "", function() onFilenameChanged() end)
defineParameter("PitchDetectionProgress", nil, 0, 0, 100)
defineParameter("PitchDetection", nil, false, setRootkey)

function setRootkey(sample)
    local zone = this.parent:getZone(1)
    local pitch, voiced = sample:getPitch(0, -1)
    if pitch 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 = this.parent:getZone(1)
    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)

               -- 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

Is it the same program with multiple zones or a different one with single zone?

You need the bits that create the SampleStart and SampleEnd parameters and their callback functions.

It’s a multi zone instrument where the first zone is for user “Drop samples” and the following zones are for pre-loaded samples zones that i select with a menu.

My understanding is that I have to modify the “Drop Sample” code I posted above.

I tried to add this part of code but is not working:

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

This is how I tried to modify the “Drop Sample” script but is not working:

defineParameter("Filename", nil, "", function() onFilenameChanged() end)
defineParameter("PitchDetectionProgress", nil, 0, 0, 100)
defineParameter("PitchDetection", nil, false, setRootkey)
defineParameter("SampleStart", nil, 0, 0, 0x7fffffff, 1)
defineParameter("SampleEnd", nil, 0, 0, 0x7fffffff, 1)

function setRootkey(sample)
    local zone = this.parent:getZone(1)
    local pitch, voiced = sample:getPitch(0, -1)
    if pitch 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 = this.parent:getZone(1)
    local sample = AudioFile.open(Filename)
    if sample.valid then
        zone:setParameter("SampleOsc.Filename", sample.fileName)

        zone:setParameter("SampleOsc.SampleStart", SampleStart)
        zone:setParameter("SampleOsc.SustainLoopStartA", SampleStart)
        if SampleEnd < SampleStart then
        SampleEnd = SampleStart
        zone:setParameter("SampleOsc.SampleEnd", SampleEnd)
         
    end
        zone:setParameter("SampleOsc.SampleEnd", SampleEnd)
        zone:setParameter("SampleOsc.SustainLoopEndA", SampleEnd)
        if SampleStart > SampleEnd then
        SampleStart = SampleEnd
        zone:setParameter("SampleOsc.SampleStart", SampleStart)
          
    end
               -- 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

OK, I tried to merge these two scripts. You should be able to drop samples when first zone is selected.

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)
    if SampleEnd < SampleStart then
        SampleEnd = SampleStart
        zone:setParameter("SampleOsc.SampleEnd", SampleEnd)
    end
end

function sampleEndChanged()
    local zone = zones[ZoneSelect]
    zone:setParameter("SampleOsc.SampleEnd", 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

SampleStartEnd Drop.vstpreset (12.8 KB)

Thanks a lot, it’s working but it looks like the “loop start” and “loop end” values don’t match when I change the “sample start” and “sample end” points. That was working in the previous script.
It looks like when I move the “sample start” and “sample end” points the loop point don’t change at all like they are not connected.

Thanks in advance for any help!

Do you mean the macro page waveform loop that shows where sample start and end are?

If you mean the actual sample loop start and end markers… script does nothing with those. They are not linked in any way. They don’t change when you change the sample start or end.