General > General Discussion

AZ Lua MFX plug-in

<< < (2/2)

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