General > General Discussion
AZ Lua MFX plug-in
azslow3:
AZ Lua API
MfxEvent
global object has only one method "new(<type>)" which return new Event object of specified <type>.
All types are predefined global variables (so in complex cases it make sense to define local equivalents).
Event object is "userdata" (Lua type) object defined within AZ Lua. It has only one (class) method - copy() (so, use e:copy() and not e.copy()) which produce new equivalent Event (simple assignment in Lua does NOT produce a copy, also note that Event objects can not be compared in usual meaning of this word).
Event of particular type has a fixed set of fields. Accessing (and even assigning) a field which is not part of particular Event is not an error. That returns "nil" in access and has no effect on assignment.
Fields access is organized as "properties" in some languages. So modifying some fields can modify related other fields (for example Status will also modify ShortMsg, Type will reset the whole Event).
You can see all fields/values for particular events by "print"ing it.
All fields except BankSelMethod are numerical. Assigning out of range values silently put the value into the range (for example, if you increase velocity, there is a "limiter" with value 127). "Type" is in fact also numeric.
For MIDI meaning of all fields, check tables/documentation on www.midi.org
All Event types have the following fields:
* Time - the project absolute MIDI time in ticks for this event. Sonar set it to 0 instead of current time (for live events) in some cases when the project is stopped (bug or feature?)
* Port - I do not know either that is relevant for MFX, I think it is safe to assume it is zero
* Chan - MIDI channel for this event. Make no sense for not channel based events which can be received as "ShortMsg".
* Type - the type of this event, see the following table
Event Types and corresponding field names are defined:
* Note - fields Key, Vel (velocity), VelOff (velocity from NoteOff MIDI message) and Duration. Used during OnEvents processing
* NoteOff - fields Key, VelOff (velocity). Used during OnInput processing.
* NoteOn - fields Key, Vel (velocity). Used during OnInput processing.
* KeyAft (Key After-touch) - fields AKey and KAmt
* Control (Control Change) - fields Num (number) and Val (value)
* Patch (Patch Change) - fields Patch (as number), BankSelMothod as string(!) with values "Normal", "Ctrl0", "Ctrl32", "Patch100" (see Sonar documentation for explanation) and Bank (as number, -1 is the default with meaning "no bank")
* ChanAft (Channel After-touch) - field CAmt
* Wheel - field WVal
* RPN - fields RNum and RVal
* NRPN - fields NNum and NVal
* ShortMsg (somehow "all other short messages") - fields ShortMsg (the whole value), Status (first byte), Data1 (second byte) and Data2 (third byte)
* - fields
A tip for live/play/offline independent note filtering: since "Key" property exists in Note,NoteOff and NoteOn, if your code is not sensitive to the particular type (simple mapping or filtering), you can use "if e.Key then" condition instead of checking for each type separately.
Note. SysEx processing is not supported (yet).
Example:
n = MfxEvent.new(NoteOn); n.Chan = 2; n.Key = 64; n.Vel = 127 - "NoteOn" MIDI message , Channel 2, middle "C", max velocity.
Several methods available for MFX plug-ins can be called from Lua directly:
Mfx.GetTimebase()
Return PPQ/Timebase, the number of ticks in one beat
Mfx.TicksToMsecs(Ticks)
Convert absolute ticks (as found in Time property of Events) into absolute time in milliseconds. Note that the argument should be integer (rounded if calculated)
Mfx.MsecsToTicks(Msecs)
Convert absolute time in milliseconds into absolute time in ticks. The argument should be integer (rounded if calculated).
azslow3:
AZ Lua API
GUI
It is possible to define "End user interface" for the preset. That way it is possible to define parameters, which values can be set "on the fly" and are saved within preset/project (till you modify the code...).
Important: if you define some parameters, till you want produce hanging notes in live mode, please use described later MfxOffNotes helper. Just imagine someone modify transpose setting while some key is still pressed...
GUI is defined by (global) array (table) GUI. Each element in that array should also be a table with definition for one parameter. Each parameter should have at least:
* Label (string) to display for what it is good
* the Type of that parameter and
* initial Value for this parameterThe Value is what is going to be changed by the interface. In general (while nothing prevent that) you should not change GUI definition after initial setup.
Currently, only 3 types of parameters are defined:
* Key, which is displayed as a Key in the UI, but it is still just a number within 0-127. You can set Min and Max[/] within that range (when not specified, the default is "standard" 88keys range)
* Int for general integer parameters, with Min and Max (default 0-127)
* Bool for boolean parameter with possible values true and false, presented as a "check box" in the interface
While it is possible to create GUI array by any available in Lua methods, let me propose the following example. What I want to demonstrate is GUI definition part only, the processing part (MfxOffNotes) is explained later.
--- Code: -----[[Transpose, optionally with octave note duplication ]]-
-- GUI definition part --
local Transpose = { Label = "Transpose", Type = "Int", Value = 0, Min = -48, Max = 48 }
local Octave = { Label = "Octave", Type = "Bool", Value = false }
GUI = { Transpose, Octave }
-- Processing part --
local active = MfxOffNotes.new()
function OnInput(pqIn, pqOut)
for i,e in ipairs(pqIn) do
e = active.move(e, pqOut )
if not e then
elseif e.Vel then
base = e:copy()
e.Key = e.Key + Transpose.Value
active.add(base, e)
if Octave.Value then
pqOut.add(e)
e.Key = e.Key + 12
active.add(base, e)
end
end
pqOut.add(e)
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut)
end
--- End code ---
azslow3:
AZ Lua API
While strictly speaking not required and can be done within Lua, there are some task with are faster to achieve inside the plug-in code. I call such functionality "helper".
MfxKey(<number or string>)
Can convert key number to the string form and back. For example, you can write "e.Key = MfxKey("C4")" instead of "e.Key = 48". The result is the same,
MfxOffNotes.new()
If you generate chords or use key switching, you need to remember what was triggered by which key in particular state for live processing.
Let say you use "C4" to transpose all keys from "C5" upwards by one octave when "C4" is pressed and not transpose otherwise. When particular key is released, you should send correct NoteOff to Sonar. For example key "C5" is released. What should we send to Sonar? Unaware approach is to process NoteOff the same way as NoteOn and Note, but you can easily get hanging notes than. C5 pressed (sent as C5), then (without releasing C5) you press C4. If after that you release C5, using naive approach that will send (transposed) "C6 is released", leaving C5 playing! The same can happened in different direction, you press C4, then C5, then release C4 and then C5. You will "hang" C6.
I have found something which looks more or less reasonable. The framework (again it does nothing) is:
--- Code: ---local active = MfxOffNotes.new()
function OnInput(pqIn, pqOut)
for i,e in ipairs(pqIn) do
e = active.move(e, pqOut )
if not e then
elseif e.Vel then
base = e:copy()
-- here you conditionally modify e
active.add(base, e)
--
-- to send more then one note, you can repeat:
-- pqOut.add(e)
-- e.Key = ... (new modification)
-- active.add(base,e)
end
pqOut.add(e)
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut)
end
--- End code ---
First I create an object of MfxOffNotes class. That is Lua table with keys constructed as "Channel*128 + Key" and the list (table) of "NoteOff" events required to send when corresponding key is released. I also modify Time property to the the shift between original key release and desired note release (normally 0, but can be different).
active.add(base, e) does the following. It associate (NoteOn) event "e" with the "origin" event "base", adding NoteOff equivalent of "e" into the list (with Time = e.Time - base.Time). If "base" or "e" are not "NoteOn" events, that method does nothing (without errors)
active.move(e, pqOut) checks "e". If that is "NoteOff" and there is corresponding not empty list associated with it, the list is moved to the pqOut and cleaned inside "active". In such case, that method return "nil" (so, I check for that in the next line). In all other cases ("e" is not "NoteOff" or there is no associated list), the function return "e".
"Wrong" parameters are allowed to make the same code work correctly with other events without checks and modification (Control, Note from OnEvents, etc).
azslow3:
Examples
Tip: "Copy/Paste" an example into AZ Lua editor and press "Apply" (save as Cakewalk preset if required). Alternatively, download attached SPP files and Inport using Cakewalk Plug-in Manager (in Utilities menu)
Split keyboard into 2 parts, left side Channel 1, right side Channel 2
--- Code: -----[[ Split to channel 1 / 2 by key 64 ]]--
function OnInput(pqIn, pqOut)
for i,e in ipairs(pqIn) do
if e.Key then
if e.Key < 64 then
e.Chan = 1
else
e.Chan = 2
end
end
pqOut.add(e)
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut)
end
--- End code ---
Convert Modulation into Program Change
--- Code: -----[[ Convert modulation to the Program Change ]]--
function OnInput(pqIn, pqOut)
for i,e in ipairs(pqIn) do
if e.Type == Control and e.Num == 1 then
local pc = MfxEvent.new(Patch)
pc.Time = e.Time
pc.Chan = e.Chan
pc.Patch = e.Val
e = pc
end
pqOut.add(e)
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut)
end
--- End code ---
Transpose everything one octave up during C3 "switch key" engaged
--- Code: -----[[Use C3 as a Switch Key to transpose one octave ]]--
local switch_key = MfxKey("C3")
--
local active = MfxOffNotes.new()
local skey_live = false
local skey_clip = { from = 0, to = 0 }
function SwitchKey( e )
if not e or not e.Key or (e.Key ~= switch_key) then
return e
end
if e.Type == Note then
skey_clip.from = e.Time
skey_clip.to = e.Time + e.Duration
elseif e.Type == NoteOn then
skey_live = true
else -- NoteOff
skey_live = false
end
return nil
end
function IsSwitchedAt( time )
return skey_live or
( (time >= skey_clip.from) and (time < skey_clip.to) )
end
function OnInput(pqIn, pqOut)
for i,e in ipairs(pqIn) do
e = active.move(e, pqOut )
e = SwitchKey(e)
if not e then
elseif e.Vel then -- process Note/NoteOn (but not NoteOff)
base = e:copy()
if IsSwitchedAt(e.Time) then
e.Key = e.Key + 12 -- transpose
end
active.add(base, e)
end
pqOut.add(e)
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut)
end
--- End code ---
Split keyboard into 3 bands, transfer each band on own MIDI channel, with possible transposition and/or velocity change, provide "Solo" mode
The code is a relatively long. But what it does is at the level of separate MIDI plug-ins! I am almost sure, other plug-ins have a bit more lines ;)
--- Code: -----[[
3 band channel splitter, middle band is between Bass and Voice
Each band has own velocity control and can be transposed.
Bands assigned to MIDI channel 1,2 and 3 on output. AZ, 2016
]]--
local Band = {}
local BassKey = { Label = "Bass under", Type = "Key", Value = MfxKey("C3") }
local VoiceKey = { Label = "Voice over", Type = "Key", Value = MfxKey("C5") }
local Solo = { Label = "Solo band (0 - no solo)", Type = "Int", Value = 0, Min = 0, Max = 3 }
local band_names = { "Bass", "Middle", "Voice"}
GUI = {
BassKey, VoiceKey, Solo
}
for i, name in ipairs( band_names ) do
local b = {}
b.Transpose = { Label = name.." transpose", Type = "Int", Value = 0, Min = -48, Max = 48 }
b.Velocity = { Label = name.." velocity", Type = "Int", Value = 0, Min = -48, Max = 48 }
Band[i] = b
table.insert(GUI, b.Transpose)
table.insert(GUI, b.Velocity)
end
local active = MfxOffNotes.new()
function OnInput(pqIn, pqOut)
for i,e in ipairs(pqIn) do
e = active.move(e, pqOut )
if not e then
elseif e.Vel then
base = e:copy()
local i = 2 -- Middle
if e.Key < BassKey.Value then
i = 1
elseif e.Key > VoiceKey.Value then
i = 3
end
if Solo.Value > 0 and Solo.Value ~= i then
e = nil
else
e.Chan = i
e.Key = e.Key + Band[i].Transpose.Value
e.Vel = e.Vel + Band[i].Velocity.Value
end
active.add(base, e)
end
pqOut.add(e)
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut)
end
--- End code ---
Switch channel for notes using controller (CC)
The controller and how many different channels you need (up to 16) is configurable without code change.
--- Code: -----[[
Use Controller to switch MIDI channel for Notes
AZ 2016
]]--
local Ctl = { Label = "Controller", Type = "Int", Value = 1 }
local ChMax = { Label = "Max channel", Type = "Int", Value = 4, Min = 2, Max = 16 }
GUI = { Ctl, ChMax }
--
local active = MfxOffNotes.new()
local Channel = 0
local floor = math.floor
function SwitchCC( e )
if not e or not e.Num or (e.Num ~= Ctl.Value) then
return e
end
Channel = floor( e.Val * ChMax.Value / 128 )
return nil
end
function OnInput(pqIn, pqOut)
for i,e in ipairs(pqIn) do
e = active.move(e, pqOut )
e = SwitchCC(e)
if not e then
elseif e.Vel then
base = e:copy()
e.Chan = Channel + 1
active.add(base, e)
end
pqOut.add(e)
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut)
end
--- End code ---
Apply tempo map
Let say you have recorded some MIDI without click and then somehow extracted the tempo map from your free performance (for example with Melodyne applied to synth output or using other detection method, may be from other instruments). If you insert original MIDI into the project with applied tempo map, it will be out of sync.
The following preset apply tempo map to the clip. BMP should be set to the original tempo for that MIDI clip which was used during recording. Note that it had to be constant.
Note 1: it should be used form "Process..." menu. It can not be used as a filter since it can generate events "in the past"
Note 2: if resulting clip is longer then original (final length you can see in your audio tracks), extent the clip before processing. The preset can not do this and the result will be truncated otherwise.
Note 3: the correction is done based on absolute time from the project begin
Note 4: that preset can be applied only once since after the first processing "native" clip tempo map is no longer constant
--- Code: -----[[
OFFLINE PROCESSOR ONLY! Does not work as a filter.
Apply tempo map to free played MIDI.
AZ 2016
]]--
local BPM = { Label = "Original BPM", Type = "Int", Value = 100, Min = 50, Max = 240 }
local PPQ = Mfx.GetTimebase()
local T2M = Mfx.TicksToMsecs
local M2T = Mfx.MsecsToTicks
local round = math.floor
GUI = { BPM }
--
function OnInput(pqIn, pqOut)
-- original converter to Msec
local k = 60000/BPM.Value/PPQ
for i,e in ipairs(pqIn) do
local ebegin = e.Time * k
local eend = (e.Time + e.Duration) * k
e.Time = M2T(round(ebegin))
e.Duration = M2T(round(eend)) - e.Time
pqOut.add(e)
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut)
end
--- End code ---
Portamento Fix
After Yamaha MIDI visit Sonar, special way to indicate Portamento is no longer working. This preset, used as MFX on such tracks or as a MIDI processor should solve the problem.
It extends note in question so it is overlapped with the next one, returning MIDI sequence which Yamaha is able to understand.
Note that as a processor it extends less notes than in play mode (live mode should always work precisely).
--- Code: -----[[
Extend some notes to make Portamento works
on Yamaha synth. AZ, 2016
]]--
function OnInput(pqIn, pqOut, To)
local last_on = 0
for i,e in ipairs(pqIn) do
if e.Type == NoteOn then
last_on = e.Time
elseif e.Type == NoteOff then
if e.Time == last_on then
e.Time = e.Time + 1
end
elseif e.Type == Note then
local next_on = To -- we do not know either some note will come after
for ni = i + 1, #pqIn do -- find the first note, when available
ne = pqIn[ni]
if ne.Type == Note then
next_on = ne.Time
break
end
end
if next_on <= e.Time + e.Duration then
e.Duration = e.Duration + 1
end
end
pqOut.add(e)
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut, To)
end
--- End code ---
Send CC1 and CC7 on playback start
Under some conditions, Sonar fail too "look back" for CC values. In case all is needed is constant CC value, the problem can be solved by the following preset
--- Code: -----[[
Send CC1 and CC7 on playback start
Use Controller to switch MIDI channel for Notes
AZ 2016
]]--
local CC1 = { Label = "CC1", Type = "Int", Value = 64 }
local CC7 = { Label = "CC7", Type = "Int", Value = 64 }
GUI = { CC1, CC7 }
--
local cc17sent = false
function OnStart( )
cc17sent = false
end
function OnInput(pqIn, pqOut)
for i,e in ipairs(pqIn) do
pqOut.add(e)
end
end
function OutCC(pqOut, time, num, value)
local e = MfxEvent.new( Control )
e.Time = time
e.Num = num
e.Val = value
pqOut.add(e)
end
OnEvents = function ( From, To, pqIn, pqOut)
if not cc17sent then
OutCC(pqOut, From, 1, CC1.Value)
OutCC(pqOut, From, 7, CC7.Value)
cc17sent = true
end
OnInput(pqIn, pqOut)
end
--- End code ---
... want something else? Do not hesitate to ask ...
azslow3:
Examples (separate SPP file per example)
Drum Map for programmers
There is Drum Map functionality in Sonar, which map one note to another.
The following preset does the same, you can specify the mapping in the preset text ('dm' table). In this example, CM scale is mapped to Cm scale for C4-B4. Not useful but demonstrate how to specify notes.
--- Code: -----[[
Drum Map like functionality,
edit the text to set your mapping
AZ, 2016
]]--
-- This table defines the mapping
local dm = {
["E4"] = "D#4", ["A4"] = "G#4", ["B4"] = "A#4"
}
-- The rest you can keep unchanged
-- Convert note names to numbers
local ndm = { }
for noteIn,noteOut in pairs(dm) do
ndm[MfxKey(noteIn)] = MfxKey(noteOut)
end
function OnInput(pqIn, pqOut)
for i,e in ipairs(pqIn) do
if e.Key then -- Is Note ?
local newkey = ndm[e.Key]
if newkey then -- Mapping exist ?
e.Key = newkey -- map the note
end
end
pqOut.add(e)
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut)
end
--- End code ---
Harmonyzer
Asked by wolfdancer here:
http://forum.cakewalk.com/Mapping-MIDI-input-to-scale-m3562772.aspx
A kind of Tintinnabuli style
--- Code: -----[[
Simple harmonizer
AZ, 2017
]]--
local Scale = { Label = "Scale", Type = "Key", Value = 0, Min = 0, Max = 11 }
local IsMajor = { Label = "Major scale", Type = "Bool", Value = false }
local HarmonyType = {
Label = "T-2, T-1, T+1, T+2", Type = "Int", Value = 0, Min = 0, Max = 3
}
local KeepMelody = { Label = "Keep melody", Type = "Bool", Value = true }
GUI = {
Scale, IsMajor, HarmonyType, KeepMelody
}
local floor = math.floor
-- from absolute note n (0 = c0 ) and root for scale ( 0 = c )
-- calculate octave and shift in that octave, so n = root + octave*12 + shift
-- return base = root + octave*12 and shift
function Normal( n, root )
n = n - root
return root + floor( n / 12 )*12, n % 12
end
-- For major , harmony tones in notes are 0, 4, 7
-- For minor , harmony tones in notes are 0, 3, 7
-- For both functions:
-- integer n : absolute note number
-- return integer harmony tone from basic triad
function NextT( n )
local base, shift
base, shift = Normal( n, Scale.Value )
if IsMajor.Value then
if shift < 4 then return base + 4 end
else
if shift < 3 then return base + 3 end
end
if shift < 7 then return base + 7 end
-- 0 in next octave
return base + 12
end
function PreviousT( n )
local base, shift
base, shift = Normal( n, Scale.Value )
if shift > 7 then return base + 7 end
if IsMajor.Value then
if shift > 4 then return base + 4 end
else
if shift > 3 then return base + 3 end
end
if shift > 0 then return base end
-- 7 in previous octave
return base - 5
end
-- Avoid hanging notes if we change parameters during playing
local active = MfxOffNotes.new()
function OnInput(pqIn, pqOut)
for i,e in ipairs(pqIn) do
e = active.move( e, pqOut ) -- process Note release correctly
if e and e.Vel then -- Process Note and Note On only (Off processed by active)
local t = e:copy()
local tKey
if HarmonyType.Value == 0 then
tKey = PreviousT( t.Key ) -- T-1
tKey = PreviousT( tKey ) -- T-2
elseif HarmonyType.Value == 1 then
tKey = PreviousT( t.Key ) -- T-1
elseif HarmonyType.Value == 2 then
tKey = NextT( t.Key ) -- T+1
else -- HarmonyType.Value == 3
tKey = NextT( t.Key ) -- T+1
tKey = NextT( t.Key ) -- T+2
end
if tKey >= 0 and tKey < 128 then -- is harmony inside note range ?
t.Key = tKey
active.add(t, e)
pqOut.add(t)
end
if KeepMelody.Value then
active.add(e, e)
pqOut.add(e)
end
else
pqOut.add(e) -- all other events
end
end
end
OnEvents = function ( From, To, pqIn, pqOut)
OnInput(pqIn, pqOut)
end
--- End code ---
Navigation
[0] Message Index
[*] Previous page
Go to full version