AZSlow

General => General Discussion => Topic started by: azslow3 on February 28, 2016, 05:55:32 PM

Title: AZ Lua MFX plug-in
Post by: azslow3 on February 28, 2016, 05:55:32 PM
AZ Lua MFX
Current version can be downloaded here (http://www.azslow.com/index.php?action=downloads;sa=downfile&id=40)
Installation video: https://www.youtube.com/watch?v=oTddpaGxpBA (https://www.youtube.com/watch?v=oTddpaGxpBA)

While the program is still alpha quality, I have tested it under Sonar X2 32bit and Sonar Platinum 64bit. So far I have not observed any problems. Window resizing is still not perfect, but that does not influence the plug-in functionality.

About
AZ Lua is Sonar MIDI Effect (MFX) plug-in in DXi format (the only supported by Sonar for MIDI plug-ins).

To just use this plug-in with existing presets, no programming knowledge required. In this case you can ignore everything written in this thread, except the next post with installations instruction and the last post with existing presets description and the preset file.

It allows to write MFXes in Lua (http://www.lua.org) programming language. While MFX API is published by Cakewalk long time ago, at least average experience in C++ programming is required to create own effects. Programming MFXes is Lua has following advantages:

AZ Lua plug-in also provides the following advantages:

Important differences between CAL and AZ Lua
While both can be used to process MIDI in Sonar, there are more differences then the language used:
CALAZ Lua
Not MIDI functionalityYESNO
Multi-track  functionalityYESNO
"Look back" and "multi-pass" MIDI processingYESNO
Offline MIDI processingYESYES
Live MIDI processingNOYES
Non destructive MIDI processingNOYES


To give you an idea how MFX can look like, the following is a COMPLETE WORKING example for filtering out all "C" Notes:
Code: [Select]
function OnInput(pqIn, pqOut)
  for i = 1, #pqIn do
    e = pqIn[i]
    if e.Key and (e.Key % 12 == 0) then
      e = nil
    end
    pqOut.add(e)
  end
end

OnEvents = function ( From, To, pqIn, pqOut)
 OnInput(pqIn, pqOut)
end
For Lua experts: I know there is better way for looping, "e" should be local and the condition can be reversed to make the code shorter  ;)

If that example looks like a newspaper from the Mars for you, probably you can just use some presets without going into details. If you understand that is "a program" (probably you do not understand WHAT it is doing and HOW it is working, that is ok), I think you can create your own FXes till you finish reading this thread.

Note 1: AZ Lua has different from Lua license. AZ Lua is closed source project (at least for now) but it is free to use (and I have no plans to change that).
Note 2: I do not pretend to be the first in writing such kind of plug-in. I have found several projects which are using Lua is VST(i) plug-ins. But I have not found any in DX(i) format. If you know such project and you think I am violating something or simply "cross the road" for someone, please let me know.

Changelog:
1.3.16 - b2, Different Note event types (API change)
1.3.16 - b3, MfxKey and MfxOffNotes helpers
4.3.16 - b4, User Interface
6.3.16 - b5, UI Resize should work a bit better
19.3.16 - b6, TempoMap functions exposed
14.06.16 - b7, Fixed incorrect CC processing in life mode, could produce rather strange effects...
Title: Re: AZ Lua MFX plug-in
Post by: azslow3 on February 28, 2016, 07:20:41 PM
Installation

Unlike any other MFX plug-ins you have, this plug-in does absolutely nothing till you load Lua program into it. These programs are saved as normal Cakewalk presets and into your project file.
To load preset published by other in SPP format (my examples are attached to the later post in this thread, the file AZLua_Examples_v4.spp):
Title: Re: AZ Lua MFX plug-in
Post by: azslow3 on February 28, 2016, 07:34:42 PM
AZ Lua Interface
AZ Lua interface has 2 modes: "User" and "Developer"

User mode
When some preset is already loaded and valid, AZ Lua is switched to this mode when the interface is opened. What is shown here is defined by the Lua program. By default, there will be no controls. When preset developer has followed simple rules, you should see at least some description of what that preset is doing. In addition, there can be some parameters exposed. These parameters are saves within preset/project, so can be recalled.

You can switch into Developer mode any time by pressing "Modify" button.

Note: in case the preset code is change and applied, all parameters are reset to values specified in the code.

Developer mode has:

Important: While Sonar disable "Maximize" button, you can resize the window using borders. Sometimes that can be required to see all available parameters in the User mode. In the Developer mode, you can change the hight of output section but dragging the line between it and the editor.

Note: note that changing parameters in the User more does not have immediate effect. Sonar "prepare" MIDI data in advance (there is a setting to change the size of the buffer). MFX plug-in can not "undo" what was already prepared.

Tip: in case you have feeling the preset is no longer working, switch to the Developer mode and check output section. Most probably you will see some "error" there. The problem indicated is NOT with plug-in (you will get Sonar crash then) but with the preset! Presets in AZ Lua are programs, and so they can have bugs.
Title: Re: AZ Lua MFX plug-in
Post by: azslow3 on February 28, 2016, 08:05:41 PM
MFX basics
Please note that API and logic of MFX is significantly different from MIDI processing inside VST.

MFX host (Sonar) call 2 different methods (functions) provided by MFX:

There is important different between Events received by these two functions. "OnInput" receives MIDI messages only, while "OnEvents" receives compound "Events" which can be a combination of several MIDI messages. For example, already recorded MIDI clip knows when there was corresponding to "Note pressed" "Note released" message and so instead of 2 distinct MIDI messages, there is one "Event": "Note with duration". On live input it is impossible to predict when the Note ends, and so there is not way to combine them. The same is true for RPN and NRPN (you should get a sequence of  corresponding "Control" events instead).

So, to create an MFX, you need to define 2 functions: one for live processing and one for recorded/offline processing. In both you can:

Original API really deliver just MIDI messages to the OnInput function, without "converting" anything to Events. Since that involves some hex arithmetic, there are only "Events" in AZ Lua. Because of already explained difference, "OnInput" get NoteOn/NoteOff events (without duration) while "OnEvents" get Note (with duration). As you will see, for simple filtering/mapping that is not important. But we should be careful during more advanced processing
Title: Re: AZ Lua MFX plug-in
Post by: azslow3 on February 28, 2016, 09:39:28 PM
AZ Lua API
I recommend at least read some chapters from the book about Lua programming language. You can find the link at www.lua.org (http://www.lua.org). In case you have already installed AZ Lua and want try to reproduce some examples - you have Lua interpreter already! AZ Lua has Lua 5.3.2 interpreter build-in. You do not event forced to leave Sonar  ;)

It is very good idea to start each your preset with Lua multi-line style comment, like:
Code: [Select]
--[[ The is my dummy preset ]]--
The text will be displayed in the User mode, so everyone can get an idea what that preset is doing without looking into the "code".

When Lua program is loaded from preset/project or "Appliy"ed in UI, it is executed once. During that execution it should define 2 global function:

In addition, the program can define 3 additional functions to be called on corresponding transport changes:
The last one can be important in case of dynamic functionality change, for example attempting to restore default/previous state.

Also you program can define (global) table "GUI" (see later).

ANY error (you can see then in the Output section of UI) during initial execution or these functions calls immediately disable your FX. And the plug-in start to work as disabled (everything goes throw unchanged). The same happens in case you have some valid Lua program but you have not defined these functions.

Notice Till you have pressed "Apply" button (so set the program as MFX), your code exists in UI only. If you close UI (or close the project) your changes WILL NOT be saved. Also till "Apply" is pressed, there is no "Project is modified" indication in Sonar. The MFX code currently working is still the same when you have just modified something in the editor, so logically there is no changed is the MIDI processing and so there is no changes in the project.

Till you start to do some "advanced" programming, OnEvents can stay (verbatim) as:
Code: [Select]
OnEvents = function ( From, To, pqIn, pqOut)
  OnInput(pqIn, pqOut)
end
and then you need only one function, OnInput.

Just to finish with "OnEvents": "To" and "From" parameters define the time period in MIDI Ticks for which this function is called.

pqIn and pqOut
There are called such way to mimic original API, with the meaning "the pointer to the Input Queue" and "the pointer to the Output Queue". Pointers make no big sense in Lua, but I have decided to keep the prefix "p". Note that you can rename both, there are just "the first" and "the second" parameters.

Both parameters are Lua Tables. But with a helper method "add". You can create your own queues by calling "MfxQueue.new()" method defined by AZ Lua API. More formal:

There is global object (table) MfxQueue with one single method new which returns an empty table with one defined method add(event,....). This method add a COPY of arguments into the table IF AND ONLY IF they are of type "MfxEvent". So, if you call "pqOut.add(x)" with anything except MfxEvent, nothing will happened (without errors, that is handy for filtering).

pqIn is the Input table. It contains MfxEvent(s) provided by Sonar on input, with numeric indexes (in Lua counting is done from "1").

pqOut is the Output table. You should add (again with numeric indexes started from "1") all MfxEvent(s) you need. Note that in case you do not add anything there, all events will be blocked by your MFX. While you can add events with other methods (for example "table.insert(ev)"), specialized "add" simplify the task (you do not need to bother making the event copy when needed and you do not need the conditional execution when filtering out events).

So, the general structure is:
Code: [Select]
function OnInput(pqIn, pqOut)
  for i = 1, #pqIn do
    local e = pqIn[i]
    -- Do something with "e"
    pqOut.add(e)
  end
end

More "Lua" style (better) definition for the same:
Code: [Select]
function OnInput(pqIn, pqOut)
  for i,e in ipairs(pqIn) do
    -- Do something with "e"
    pqOut.add(e)
  end
end
In that exact form, your MFX will just copy all events from the input table into the output table. Effectively the same "functionality" as not existing or disabled MFX plug-in. As I have already mentioned, defining the function but leaving it empty will BLOCK all MIDI events. So, you already know 2 "extreme" MFX plug-in definitions  :o

Local in "local e = ..." is not required but optimize the speed. We are working close to the Sonar "RealTime engine", so you should care (when that does not severe disturb readability, Lue experts will probable write "must without 'when'").
Title: Re: AZ Lua MFX plug-in
Post by: azslow3 on February 28, 2016, 10:25:10 PM
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 (http://www.midi.org)

All Event types have the following fields:
Event Types and corresponding field names are defined:

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).
Title: Re: AZ Lua MFX plug-in
Post by: azslow3 on February 28, 2016, 10:29:48 PM
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:
The 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:

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: [Select]
--[[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
Title: Re: AZ Lua MFX plug-in
Post by: azslow3 on February 28, 2016, 10:30:09 PM
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: [Select]
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

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).
Title: Re: AZ Lua MFX plug-in
Post by: azslow3 on February 28, 2016, 10:47:00 PM
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: [Select]
--[[ 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


Convert Modulation into Program Change
Code: [Select]
--[[ 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

Transpose everything one octave up during C3 "switch key" engaged
Code: [Select]
--[[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

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: [Select]
--[[
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

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: [Select]
--[[
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

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: [Select]
--[[
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

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: [Select]
--[[
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

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: [Select]
--[[
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

... want something else? Do not hesitate to ask ...
Title: Re: AZ Lua MFX plug-in
Post by: azslow3 on June 08, 2016, 05:35:51 PM
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: [Select]
--[[
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


Harmonyzer
Asked by wolfdancer here:
http://forum.cakewalk.com/Mapping-MIDI-input-to-scale-m3562772.aspx
A kind of Tintinnabuli style

Code: [Select]
--[[
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