Author Topic: AZ Lua MFX plug-in  (Read 7984 times)

Offline azslow3

  • Administrator
  • Hero Member
  • *****
  • Posts: 1236
AZ Lua MFX plug-in
« on: February 28, 2016, 05:55:32 PM »
AZ Lua MFX
Current version can be downloaded here
Installation video: 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 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:
  • Lua is a simple language, easy to learn: www.lua.org
  • tiny footprint. The plug-in size is way under 1MB, no external dependencies (except MS Windows Runtime, included in all currently deployed Windows versions)
  • scripting language, no compilation step
  • reasonably fast

AZ Lua plug-in also provides the following advantages:
  • Modified toward simplicity API.  Everyone with basic programming experience should be able to create own MIDI filters. Everyone with basic calculations experience (for example calculations in spreadsheet) should be able to modify filters created by other
  • The API created for unexperienced audience, several protections are implemented to avoid your "first program" crash/lock Sonar. The plug-in disallow dangerous Lua functionality, detect (big) memory leaks and too slow for MIDI processing code (including endless loops). Like with normal plug-ins, users should not worry about Sonar integrity when creating programs/presets
  • You can program without leaving Sonar, AZ Lua User Interface has simple text editor and can (test) run your Lua code. All errors and "print" output are displayed. Unlike most MFXes, the interface can be resized
  • Several instances of the plug-in run completely independently, the code is multi-thread and RealTime nature of MIDI processing inside DAW aware and seamless give the same properties to the Lua code

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...
« Last Edit: June 14, 2016, 08:38:41 PM by azslow3 »

Offline azslow3

  • Administrator
  • Hero Member
  • *****
  • Posts: 1236
Re: AZ Lua MFX plug-in
« Reply #1 on: February 28, 2016, 07:20:41 PM »
Installation
  • Exit Sonar
  • Download the installer
  • Run installation, modern OS will warn you it is not signed. If you trust me, do not worry. The only "safety" which signed executable gives you is some information about the author and prevention some virus which already exist on your computer to modify the executable. You can find more information about me then you can find in any certificate using DNS of my site. If I start to sign my installators, I will be forced to collect money just for the certificate. And if you already have a virus on your computer, preventing it is too late (also you can compare the size of your local file with the information in the Download section, most modern "viruses" are bigger then my whole package...)
  • Several files will be installed and registered for Sonar use. The path is not configurable, but that will not fill your disk. You can uninstall any time, from my knowledge uninstallation is restless
  • Start Sonar. "AZ Lua" should appear as possible MFX plug-in (in MIDI tracks FX Bin and Processing menu)

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):
  • Open Cakewalk Plug-in Manager from Sonar Utilities menu
  • Select "MIDI Effects", "AZ Lua".
  • Press "Import..." buttons, select SPP file and the preset(s) is it, press "Ok"
  • Normally imported presets appear is the plug-in presets list without Sonar restart. But sometimes the restart is required
« Last Edit: April 16, 2016, 04:00:10 PM by azslow3 »

Offline azslow3

  • Administrator
  • Hero Member
  • *****
  • Posts: 1236
Re: AZ Lua MFX plug-in
« Reply #2 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:
  • Lower "Output" section. It display error messages (which something went wrong with Lua program) and the results of Lua "print" function
  • The editor. Current cursor position is displayed above it, next to the buttons (useful when error message includes line information). When you modify Lua code it is not automatically applied.
  • "GUI" button to switch into User mode
  • "Apply" button. Set Lua program in the editor as FX processor.
  • "Test" button. Execute Lua program in the editor. The program is run independent from currently active FX Lua program and so it does not effect MIDI processing. Useful to check you have no bugs in Lua syntax or as general Lua interpreter
  • "Revert"/"Clear" button. If you have modified Lua code, you can revert it to currently running one, even in case you have "Test"ed it.

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.
« Last Edit: March 04, 2016, 03:04:28 PM by azslow3 »

Offline azslow3

  • Administrator
  • Hero Member
  • *****
  • Posts: 1236
Re: AZ Lua MFX plug-in
« Reply #3 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:
  • "OnInput" is called when live MIDI message is received from the track MIDI input. There can be several simultaneous messages (for example chords played with sufficient precision). Please note that in "Record" mode these events are recording BEFORE any MFX processing (the same as with Audio recording)
  • "OnEvents" is called in two cases. When you "Play" the track and when you "Process" MIDI clip using the plug-in. In the first case, the processing is not destructive (till you "Apply effects"), it is just sent to the next MFX or the track output. In the second case the result of processing replace original clip. Sonar call "OnEvents" once for "MIDI buffer" specified time period (in advance during playback). Note that that buffer is usually "big" compare to the Audio buffer size, it has order of 0.5 second (audio buffer is normally several milliseconds)

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:
  • forward incoming MIDI Event "as is"
  • modify incoming MIDI Event before forwarding
  • generate new MIDI Events

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
« Last Edit: March 01, 2016, 10:45:04 AM by azslow3 »

Offline azslow3

  • Administrator
  • Hero Member
  • *****
  • Posts: 1236
Re: AZ Lua MFX plug-in
« Reply #4 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. 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:
  • OnInput
  • OnEvents

In addition, the program can define 3 additional functions to be called on corresponding transport changes:
  • OnStart
  • OnStop
  • OnLoop
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'").
« Last Edit: March 04, 2016, 03:10:08 PM by azslow3 »

Offline azslow3

  • Administrator
  • Hero Member
  • *****
  • Posts: 1236
Re: AZ Lua MFX plug-in
« Reply #5 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

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).
« Last Edit: March 19, 2016, 08:01:34 PM by azslow3 »

Offline azslow3

  • Administrator
  • Hero Member
  • *****
  • Posts: 1236
Re: AZ Lua MFX plug-in
« Reply #6 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:
  • Label (string) to display for what it is good
  • the Type of that parameter and
  • initial Value for this parameter
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:
  • 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: [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
« Last Edit: April 24, 2016, 10:04:01 AM by azslow3 »

Offline azslow3

  • Administrator
  • Hero Member
  • *****
  • Posts: 1236
Re: AZ Lua MFX plug-in
« Reply #7 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).
« Last Edit: June 08, 2016, 04:54:24 PM by azslow3 »

Offline azslow3

  • Administrator
  • Hero Member
  • *****
  • Posts: 1236
Re: AZ Lua MFX plug-in
« Reply #8 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 ...
« Last Edit: April 24, 2016, 10:18:51 AM by azslow3 »

Offline azslow3

  • Administrator
  • Hero Member
  • *****
  • Posts: 1236
Re: AZ Lua MFX plug-in
« Reply #9 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
« Last Edit: March 06, 2017, 03:58:27 PM by azslow3 »