QuickStartLua

Basics

Here's a quick guide to setting up a VstLua script. Read the Lua guide if you want a quick introduction to the basics of Lua itself.

Setting Up

First of all, you need to get VSTLua into your host, and get it to process MIDI messages. This might be very easy (e.g. in energyXT, just add it and connect up the ports, or add into the FX chain in Reaper), or rather more involved (e.g. in Live). For some idea of how to do it for your host, have a look at the ToneSpace guide .
If you can't use MIDI plugins with your host, you may still be able to use VSTLua by routing MIDI via a loopback device. In this case, the best way to do is to use SaviHost. Copy savihost.exe to vstlua.exe and copy in beside vstlua.dll. Now it vstlua.exe will work like a standalone VSTLua. Make sure you have a MIDI loopback installed (or example MIDI Yoke), and you can route midi from input devices/output from your host, to VSTLua, and then (back) to your host.


Once you've got it running, test that MIDI is being passed through. VSTLua works like a MIDI Thru when no script is loaded. The "Midi In" and "Midi Out" buttons should light up as you send MIDI events (either via a controller like a keyboard, or from the sequencer).

Now, we're ready to get scripting! Use the "Load" button to load a script (try chord.lua, in the scripts/ directory). When you send MIDI now, each note will be replaced by a chord. If you switch to the GUI pane (click on the bar on the left) then you can select the chord from a drop down list.

Scripting

If at any time you get an error, a message will be printed in red in the lower message window. The script will halt (all callbacks will stop), and the "Reset" button will light up. If you correct the error (e.g. by editing the script) and press Reset, the script will be reloaded and reinitialised. You can easily edit the Lua scripts; just open with any text editor (Notepad will work, but Notepad++ is MUCH better and is free) and edit away.

Printing messages

Quick note: it's very useful to be able to print debug messages when working with VstLua. You can use print() to do this:
print("Hello, world!")
will write Hello, world out to the main message window.

Callbacks

VstLua is based around callbacks, functions which get called regularly by the plugin itself. To define a new callback, just define a function with the appropriate name, and it will automatically get called at the right times.

Example: Midi callbacks

Every time VstLua receives a note, a function called midiEventCb is called. If you define a function with that name, the midi will be passed to it. Just copy the following into a file called transpose_notes.lua and you can open it from VSTLua:
--this function transposes notes up by a fifth
function midiEventCb(event)

    --if it's a note event
    if event.type == midi.noteOn or event.type == midi.noteOff    
        --transpose by seven semitones (a fifth)
        event.byte2 = event.byte2 + 7
    end
    
    --send on the note
    sendMidi(event)
end
The "event" argument to midiEventCb has a bunch of fields. The most important are: The sendMidi function just passes a midi event onto the host. You can either modify an incoming message (like above) or create new ones with the functions in lua/midi.lua, such as noteOn(note, velocity, channel) and cc(controller, value, channel).

Now, if you want to send a note after some delay, use the scheduleEvent function instead (in fact, it's always safe to use scheduleEvent in place of sendMidi). If you use scheduleEvent, you can add any value to the event.delta value, and the message will be sent at the correct time. So you could delay a note by 5000 samples just by doing:
event.delta = event.delta+5000
scheduleEvent(event)

Timing

To do things with a timing element, such as playing a note repeatedly, you have to send messages on a regular basis. The place to do this is in the onFrameCb() handler. Just define a function called onFrameCb, and it will be called every time the host processes a frame of audio data. Example:
function onFrameCb()

    --send middle C at max velocity every frame
    ev = noteOn(60, 127, 0)
    sendMidi(ev)
    
end
which sends a note every frame. You can always get the current frame length from the global variable VSTFrameLength. However, since frame lengths are variable, timing things like this isn't a great deal of use. If you want to do things which are musically timed (i.e. synced to the host), there are some useful functions for timing defined in lua/timing.lua. The most useful is
beatlen, beatnumber, timetobeat= computeBeatTime(sampletime, tempo, samplerate, fraction)
which takes the tempo, samplerate and sampletime from the host (more on how to get those later), which fraction of a beat you want (e.g. 0.25=1 quarter note) and returns the length of one beat (in samples), the "number" of the beat (i.e. how many complete beats have passed), and the time in samples to the next beat (which is useful for setting event deltas). The tempo, samplerate and sampletime are all part of the hosttime table returned by getHostTime(). So the following sends an event every beat, no matter how long the frame is.
function onFrameCb()
    hosttime = getHostTime()
    --compute the beat properties
    beatlen,number,timetobeat = computeBeatTime(hosttime.samplePos, hosttime.tempo, hosttime.sampleRate, 0.25)
    
    --will this note be played in this frame?
    if timetobeat < VSTFrameLength then
    
        --middle C at max veloctiy 
        ev = midi.noteOn(60, 127, 0)
        
        --set the delta to the exact time to the beat
        ev.delta = timetobeat
        
        sendMidi(ev)  
        
    end
    
end
The problem with the above is that it never sends any note offs. We could add some variables to record the last on time, wait until enough time has passed and send the note off event, but that's rather complicated. The much easier way to do it is to use scheduleEvent(), which will send events at the right time. Example:
function onFrameCb()

    hosttime = getHostTime()
    
    --compute the beat properties
    beatlen,number,timetobeat = computeBeatTime(hosttime.samplePos, hosttime.tempo, hosttime.sampleRate, 0.25)
    
    --will this note be played in this frame?
    if timetobeat < VSTFrameLength then
    
        --middle C at max veloctiy 
        ev = midi.noteOn(60, 127, 0)
        
        --set the delta to the exact time to the beat
        ev.delta = timetobeat
        
        --we could use sendMidi here, but scheduleEvent can be
        --dropped in anywhere sendMidi is used
        scheduleEvent(ev)                     

        --send a note off 1 beat later
        ev_off = midi.noteOff(60, 0)
        ev_off.delta = timetobeat + beatlen
        
        --schedule it
        scheduleEvent(ev_off)
        
    end
    
end
Let's look at a more complex example, a MIDI looper. We want to read in data while the user plays, then continuously play that back until the next recording. The user can control recording using the sustain pedal, CC 64.

The user holds down the pedal and plays notes (and hears them at the same time). The user releases and the loop starts playing, until the next time the pedal goes down. Here's the script, fully commented:


--initialise variables


recording = false --whether or not we're not recording

loop = {}         --this will store the MIDI data

baseTime = 0      --records the exact time since the start of the last frame 
                  --that the pedal went down
                  
curTime = 0       --the current time pointer into the loop; 
                  --i.e. how far we have progressed so far. Measured in samples.


                  
--handle each MIDI event we get                 
function midiEventCb(event)

    
    --if the event is a sustain pedal event
    if event.type==midi.cc and event.byte2==midicc.sustain then
        --record when sustain goes high (>64), and stop when it goes low
        
        if event.byte3>64 then
          startRecording(event)
        else
          stopRecording(event)
          
        end
    else
        --OK, this is a note or some other controller
        --if we're recording, put it in the loop
        if recording==true then
           addToLoop(event)
           sendMidi(event) -- and echo through...
        end
        
        --note that we don't send the note through
        --if we're not recording!
    end
end
   

--every frame, record the advance of one frame    
function onFrameCb()       
  loopAdvance(VSTFrameLength)  
  
                  
--Start recording a sequence; the user has depressed the pedal
function startRecording(event)
    loop = {}              -- clear the loop
    recording = true       -- flag recording mode
    baseTime = event.delta -- store the exact offset of the pedal down
    curTime = 0            -- reset the loop time
end


--Stop recording; the user has released the pedal
function stopRecording(event)
    recording = false      
    curTime = 0            -- reset the time to the start of the loop
end


--This will be called every frame. It advances the loop by one frame, whether
--we're recording or playing
function loopAdvance(samples)
    

    --if we're not recording, we're playing
    if not recording then
           
           --record the last time played
           maxtime = 0
           
            --for each recorded note in the loop
            for i,v in ipairs(loop) do
            
                --v[1] is the time of the event (relative to the start of the loop)
                t = v[1]
                
                --v[2] is the event itself
                m = v[2]
                
                --does this note occur in this frame?
                if t >= curTime and t < curTime+samples then
                    --copy the event (we must do this, because otherwise we'll modify
                    --the original event!)
                    
                    c = copyMidiEvent(m)  
                    
                    --delta is the difference between the play time
                    --and the current position
                    
                    c.delta = t-curTime       
                    
                    --send the event
                    
                    sendMidi(c)
                end
                
                
                --this records the time of the very last note in the sequence
                if t>maxtime then
                    maxtime = t
                end
                
            end
            
            --loop back to zero if we've passed the end of the sequence
            if curTime>maxtime then
                
                --adjust the time because we'll have overshot by a few samples
                curTime = maxtime-curTime
            end
            
        end
    end
                
    
      --advance the current time by one frame
      curTime = curTime + samples    

end

--insert a new event into the buffer
function addToLoop(event)
    --just append a pair to the current loop; time since start of loop, and the event itself
    table.insert(loop, {curTime+event.delta, event})
end




GUIs

VstLua aims to make it trivial to add simple controls to dynamically adjust parameters of your scripts. Here's a very simple example.

button = addControl{x=50, y=50, type=GUITypes.onOffButton, label="Switch", callback = doswitch}

--print out the value every time it changes
function doswitch(value)
    print("Button is now "..value)
end
This adds a simple on/off toggle button. doswitch() will be called every time the value of the control changes (here, when the button is pressed). Try adding this to your script and then pressing the button marked "GUI" at the top-left. See the API docs for the different type of controls you can add.

If you want to read or write its value at any time, just use the value field:
print("Button value is"..button.value)

--turn the button on
--NB this won't call the callback for value changes
button.value = 1

There are many other control types, all of which are used the same way. Try creating a knob by changing the type to "GUITypes.knob". You can set the maximum and minimum values of the knob by setting the "min" and "max" fields when you create the control:

knob = addControl{x=50, y=50, min=100, max=500, type=GUITypes.knob, label="Knob", callback = function(value) print("Knob is "..value) end}