Skip to content

Instantly share code, notes, and snippets.

@marksweiss
Created November 19, 2023 07:22
Show Gist options
  • Save marksweiss/5a0dfffe072d1e1a8f162bce3d4780cd to your computer and use it in GitHub Desktop.
Save marksweiss/5a0dfffe072d1e1a8f162bce3d4780cd to your computer and use it in GitHub Desktop.
Core APIs, documentation and helper functions for LogicProX MIDI scripting
// Define NeedsTimingInfo as true at the global scope to enable GetTimingInfo()
var NeedsTimingInfo = true;
///////////////////////
// ProcessMIDI Functions
// NOTE: ProcessMIDI() can be used as the main event loop in an application.
// It will be called repeatedly and events will be generated according to the TiminInfo
/*
The ProcessMIDI() function lets you perform periodic (generally
timing-related) tasks. This can be used when scripting a sequencer,
arpeggiator, or other tempo-driven MIDI effect. ProcessMIDI is generally not
required for applications that do not make use of musical timing information
from the host. ProcessMIDI is called once per “process block,” which is
determined by the host’s audio settings (sample rate and buffer size).
ProcessMIDI is called with no arguments.
This function will often be used in combination with the "JavaScript
TimingInfo object" to make use of timing information from the host
application. The use of ProcessMIDI and the TimingInfo object is shown in
the example.
Note: To enable the GetTimingInfo feature, you need to add
var NeedsTimingInfo = true; at the global script level (outside of any
functions).
*/
/*
JavaScript TimingInfo object
The TimingInfo object contains timing information that describes the state of
the host transport and the current musical tempo and meter. A TimingInfo
object can be retrieved by calling GetTimingInfo()
TimingInfo properties:
TimingInfo.playing //uses boolean logic where "true" means the host
//transport is running.
TimingInfo.blockStartBeat //a floating point number indicates the beat
//position at the start of the process block
TimingInfo.blockEndBeat //a floating point number indicates the beat position
//at the end of the process block
TimingInfo.blockSize //a floating point number indicates the length of the
//process block in beats
TimingInfo.tempo //a floating point number indicates the host tempo
TimingInfo.meterNumerator //an integer indicates the host meter numerator
TimingInfo.meterDenominator //an integer number indicates the host meter
//denominator
TimingInfo.cycling //uses boolean logic where "true" means the host transport
//is cycling
TimingInfo.leftCycleBeat //a floating point number indicates the beat position
//at the start of the cycle range
TimingInfo.rightCycleBeat //a floating point number indicates the beat
//position at the end of the cycle range
*note: The length of a beat is determined by the host application time
signature and tempo.
*/
/*
Prints API TimingInfo attributes and calculates additional derived attributes.
Returns an object with the API TimingInfo object and derived attributes, and
sets this return value to a global. Each call after the first call in a run
simply returns the global already set, no-op. This is valid as long as the
event loop doesn't modify meter, sample depth or sampling rate, which the API
does not support.
@return
TimingInfoDump = {
blockLengthBeats: float,
blockLengthSecs: float,
blockLengthMsecs: float,
blocksPerBeat: int,
beatsPerNote: float,
beatsPerMeasure: float,
meter: string
}
*/
// global object holding API TimingInfo and derived attributes, set on first
// call to parent event loop
var INFO = {};
var SHOULD_DUMP = true;
var SECS_PER_MIN = 60;
var ONE_NOTE_PER_BEAT = 4;
function dumpAndGetTimingInfo(info) {
if (SHOULD_DUMP) {
Trace("** TRANSPORT");
Trace("info.playing: " + info.playing);
Trace("info.cycling: " + info.cycling);
Trace("info.leftCycleBeat: " + info.leftCycleBeat);
Trace("info.rightCycleBeat: " + info.rightCycleBeat);
Trace("** TEMPO / BLOCK INFO");
Trace("info.tempo BPM: " + info.tempo);
Trace("info.blockStartBeat: " + info.blockStartBeat);
Trace("info.blockEndBeat: " + info.blockEndBeat);
Trace("info.blockLength beats: " + info.blockLength);
var blocksPerBeat = Math.floor(1.0 / info.blockLength);
Trace("blocksPerBeat: " + blocksPerBeat);
var secsPerBeat = SECS_PER_MIN / info.tempo;
var blockLengthSecs = info.blockLength * secsPerBeat;
Trace("info.blockLength secs: " + blockLengthSecs);
Trace("info.blockLength msecs: " + 1000 * blockLengthSecs);
Trace("** METER / BLOCK INFO");
var meter = info.meterNumerator + "/" + info.meterDenominator;
Trace("meter: " + meter);
var beatsPerNote = ONE_NOTE_PER_BEAT / info.meterDenominator;
var beatsPerMeasure = beatsPerNote * info.meterNumerator;
Trace("beats per note: " + beatsPerNote);
Trace("beats per measure: " + beatsPerMeasure);
INFO = {
"timingInfo": info,
"blockLengthBeats": info.blockLength,
"blockLengthSecs": blockLengthSecs,
"blockLengthMsecs": 1000 * blockLengthSecs,
"blocksPerBeat": blocksPerBeat,
"beatsPerNote": beatsPerNote,
"beatsPerMeasure": beatsPerMeasure,
"meter": meter
};
}
SHOULD_DUMP = false;
return INFO;
}
// LogicProX API Event Handler, modify as needed
function ProcessMIDI() {
dumpAndGetTimingInfo(GetTimingInfo());
}
// /ProcessMIDI Functions
///////////////////////
///////////////////////
// MIDI Event Functions
/*
Original documentation for functions 1 and 2
The HandleMIDI() function lets you process MIDI events that the plug-in
receives.
HandleMIDI is called each time a MIDI event is received by the plug-in and is
required to process incoming MIDI events. If you do not implement the
HandleMIDI function, events pass through the plug-in unaffected.
HandleMIDI is called with one argument which is a JavaScript object that
represents the incoming MIDI event. HandleMIDI and JavaScript Event object
use is shown in the examples.
*/
// Pass MIDI events through the plug-in
function send(event) {
event.send();
}
// Log events to the plug-in console and do not send them anywhere
function trace(event) {
event.trace();
}
// Repeat Note events offset by pitchOffset, pass other event types through
function transposeNote(event, pitchOffset) {
if (event instanceof Note) {
event.pitch += pitchOffset; // transpose
}
event.send(); // send original event
}
// Repeat Note events with N delayMs of delay, pass all other event types through
function delayNote(event, delayMs) {
event.send(); // send original event
// if it's a note
if (event instanceof Note) {
event.sendAfterMilliseconds(100); // send after delay
} else {
event.send(); // otherwise send original event
}
}
// LogicProX API Event Handler
// This event handler modifies an existing event
function HandleMIDI(event) {
send(event);
}
// ----
// This is the meat of the API for musical Events
/*
JavaScript Event Object
When the "HandleMIDI function" is called, an Event object represents one MIDI
event and implements the following methods you can call in your script:
Event.send() //send the event
Event.sendAfterMilliseconds(number ms) //send the event after the specified
//value has elapsed(can be an integer
//or floating point number)
Event.sendAtBeat(number beat) //as above, but uses the beat value as a delay
//in beats from the current position
Event.trace() //print the event to the plug-in console
Event.toString() //returns a String representation of the event
Event.channel(number) //sets MIDI channel 1 to 16. Note: Event.channel is an
//event property, rather than a method
Event.beatPos //event property, represents the beat position of the event
//Event.send() sends at the beat position set by this property
Event.articulationID //property representing the articulation id of the note
The Event object is not instantiated directly, but is a prototype for the
following event-specific object types. All of the following types inherit the
methods described above and the channel property. The event types and their
properties are passed to HandleMIDI as follows:
Note //prototype for NoteOn and NoteOff
NoteOn.pitch(integer number) //pitch from 1-127
NoteOn.velocity(integer number) //velocity from 0-127. A velocity value of 0
//is interpreted as a note off event, not a
//note on.
NoteOff.pitch(integer number) //pitch from 0-127
NoteOff.velocity(integer number) //velocity from 0-127
PolyPressure.pitch(integer number) //pitch from 1-127. Polyphonic aftertouch
//is uncommon on synthesizers
PolyPressure.value(integer number) //pressure value from 0-127
ControlChange.number(integer number) //controller number from 0-127
ControlChange.value(integer number) //controller value from 0-127
//tip: use MIDI.controllerName(number) to
//look up the name of the controller
ProgramChange.number(integer number) //Program change number from 0-127
ChannelPressure.value(integer number) //aftertouch value from 0-127
PitchBend.value(integer number) //14-bit pitch bend value from -8192 to 8191
//a value of 0 is center
TargetEvent.target(string) // Name of target menu entry
// - Target events need a corresponding menu entry,
// see Tutorial script 15
TargetEvent.value(float) // Value of set Target from 0.0 to 1.0
*/
// LogicProX API Event Handler
// This no-arg function creates new Events
function HandleMIDI() {
// Implement as needed
}
// Example from the Tutorial
/*
//Replace every MIDI event with a modulation control change message
//Tip: you can use the JavaScript "new" keyword to generate a new instance of an
//Event object of any type.
function HandleMIDI() {
var cc = new ControlChange; //make a new control change message
cc.number = 1; //set it to controller 1 (modulation)
cc.value = 100; //set the value
cc.send(); //send the event
cc.trace(); //print the event to the console
}
*/
// Example from loop_event Script, good example of creating and sending Notes
/*
function playNote(pitch, beatDur) {
var on = new NoteOn();
on.pitch = pitch;
on.send(on.beatPos);
var off = new NoteOff(on);
off.beatPos = on.beatPos + beatDur;
off.send();
}
*/
// ----
// /MIDI Event Functions
///////////////////////
///////////////////////
// MIDI Global Functions
/*
JavaScript MIDI object
The MIDI object contains a number of convenient and easy to use functions
that can be used when writing your scripts.
Note: the MIDI object is a property of the global object, which means that
you do not instantiate it, but access it's functions much like you
would the JavaScript math object. An example is calling
MIDI.allNotesOff() directly.
MIDI object properties:
noteNumber(string name) //returns the MIDI note number for a given note name.
//for example: 'C3' or 'B#2'
//note: you cannot use flats in your argument. Use
A#3, not Bb3
noteName(number pitch) //returns the name (string) for a given MIDI note
//number
ccName(number controller) //returns the controller name (string) for a given
//controller number
allNotesOff() //sends the all notes off message on all MIDI channels
normalizeStatus(number status) //normalizes a value to the safe range of
//MIDI status bytes (128-239)
normalizeChannel(number channel) //normalizes a value to the safe range of
//MIDI channels (1-16)
normalizeData(number data) //normalizes a value to the safe range of MIDI
//data byes (0-127)
*/
// Example
// MIDI.allNotesOff()
// /MIDI Global Functions
///////////////////////
///////////////////////
// GUI Parameter Controls
/*
The GetParameter() function retrieves information from parameters defined
with var PluginParameters.
The GetParameter name argument must match the defined PluginParameters name
value. Calling GetParameter() returns the current value of the control.
*/
// Example
// create a linear parameter called "Note Velocity" with a range of 1 to 127, and
// a default value of 80
// var PluginParameters = [{name:"Note Velocity", type:"lin", minValue:1,
// maxValue:127, numberOfSteps:126, defaultValue:80}];
/*
Create JavaScriptMIDI controls
The JavaScriptMIDI Script Editor lets you use a simple shorthand to add
standard controllers such as sliders and menus for automated or real time
control of your plug-ins. The only mandatory property to define a new
parameter is a name, which will default to a basic slider. In addition, you
can add the following properties to change the type and behavior of controls.
Optional properties:
type:
//type one of the following strings as the value:
"lin" //creates a linear fader
"log" //creates a logarithmic fader
"menu" //creates a menu
"valueStrings" //the menu type requires an additional property that is
//an array of strings to show in the menu
defaultValue: //type an integer or floating point number to set a default
//value. If not value is typed the default is 0.0
minValue: //type an integer or floating point number to set a minimum value.
//if no value is typed, the default is 0.0
maxValue: //type an integer or floating point number to set a maximum value.
//if no value is typed, the default is 1.0
*/
var SLIDER_TYPE_LINEAR = "lin";
var SLIDER_TYPE_LOG = "log";
var MENU = "menu";
var PluginParameters = [];
// Map of indexes of controls, 'parameters' in LogicProX terminology, in the order they are created,
// mapped to a callback to pass the value of the control when ParameterChanged() is fired. This is
// a built-in LogicProX API call event handler which fires when controls change, and receives the index
// of the control and the current value. This pattern lets our implementation dispatch the value
// received to a callback created for each control.
var PARAM_CALLBACK_MAP = {};
function noOpParamCallback(value) {}
function makeSlider(name, sliderType, minVal, maxVal, numberOfSteps, defaultVal, paramCallback) {
var paramIndex = PluginParameters.length();
PluginParameters.push({
"name": name ,
"type": sliderType,
"minValue": minVal,
"maxValue": 127,
"numberOfSteps": numberOfSteps,
"defaultValue": defaultVal
});
if (paramCallback === null || paramCallback === undefined) {
paramCallback = noOpParamCallback;
}
PARAM_CALLBACK_MAP[paramIndex] = paramCallback;
}
function makeMenu(name, minVal, maxVal, numberOfSteps, defaultVal, menuItems, paramCallback) {
var paramIndex = PluginParameters.length();
PluginParameters.push({
"name": name ,
"type": MENU,
"minValue": minVal,
"maxValue": 127,
"numberOfSteps": numberOfSteps,
"defaultValue": defaultVal,
"valueStrings": valueStrings
});
if (paramCallback === null || paramCallback === undefined) {
paramCallback = noOpParamCallback;
}
PARAM_CALLBACK_MAP[paramIndex] = paramCallback;
}
// Helpers for common event attributes we want to control with controls
function velocitySetAndSend(event, name) {
if (event instanceof Note) {
event.velocity = GetParameter(name);
}
event.send();
}
function pitchSetAndSend(event, name) {
if (event instanceof Note) {
event.pitch = GetParameter(name);
}
event.send();
}
// LogicProX API Event Handler
function ParameterChanged(param, value) {
PARAM_CALLBACK_MAP[param](value);
}
// /GUI Paramter Controls
///////////////////////
///////////////////////
// Event Parameters Controlled by External Conrollers
/*
JavaScript TargetEvent Object
With the TargetEvent object you can create user definable MIDI CC messages
or control plug-in parameters.
The object reads the parameter to be modified from a menu in which the user can select
a destination MIDI CC, or use the Learn Plug-in Parameter command to assign any
parameter of a plug-in inserted after (below) Scripter in the same channel strip.
The chosen destination is saved with the plug-in setting.
TargetEvent properties:
TargetEvent.target(string) // Name of target menu entry
TargetEvent.value(float) // Value of set target from 0.0 to 1.0
Example:
- set a Parameter of type "target" addressabe by it's "name"
- in HandleMIDI(event) if the event is a ControlChange and it's event.number is the CC # we want to bind
to this Parameter, then handle the event
var PluginParameters = [
// parameter 0
{
name:"Modwheel Target",
type:"target"
}
];
// HandleMIDI is called every time the Scripter receives a MIDI event.
function HandleMIDI(incomingEvent)
{
// remap modulation to target selected in menu
// check for incoming CC event with number 1 (Modwheel)
if ((incomingEvent instanceof ControlChange) && (incomingEvent.number == 1))
{
var newEvent = new TargetEvent(); // create new Target Event
newEvent.target = "Modwheel Target"; // set menu entry to be used by this event by its name
newEvent.value = incomingEvent.value / 127; // rescale from 0..127 to 0.0..1.0
newEvent.send(); // send the event
} else {
// send all other events
incomingEvent.send();
};
};
*/
// /Event Parameters Controlled by External Conrollers
///////////////////////
///////////////////////
// General Utils
// Helper to play a single note with a pitch and a duration in beats set by beatDur
function playNote(pitch, beatDur) {
var on = new NoteOn();
on.pitch = pitch;
on.send(on.beatPos);
var off = new NoteOff(on);
off.beatPos = on.beatPos + beatDur;
off.send();
}
// ----
// -----
// pitches
var P0 = {
"C": 24,
"Cs": 25,
"Df": 25,
"D": 26,
"Ds": 27,
"Ef": 27,
"E": 28,
"F": 29,
"Fs": 30,
"Gf": 30,
"G": 31,
"Gs": 32,
"Af": 32,
"A": 33,
"As": 34,
"Bf": 34,
"B": 35
}
/*
Calculates and returns MIDI pitch int value as offset by octave from 0th
octave (i.e. C0 .. B0).
Valid values for octave are 0 .. 7, in that that range
of values will actually create a sound on a MIDI piano (for example).Actual
MIDI pitch values are valid from 0 to 127, though values below 24 map to
"negative octaves" on a piano keyboard, since 60 maps to C3. No validation
is done, to avoid additional computation for every note generation.
@return int value of MIDI pitch
*/
function P(octave, pitch) {
return P0[pitch] + (12 * octave);
}
// ----
var LOOKUP = [
0, 0.01, 0.02,
0.03, 0.04, 0.05,
0.060000000000000005, 0.07, 0.08,
0.09, 0.09999999999999999, 0.10999999999999999,
0.11999999999999998, 0.12999999999999998, 0.13999999999999999,
0.15, 0.16, 0.17,
0.18000000000000002, 0.19000000000000003, 0.20000000000000004,
0.21000000000000005, 0.22000000000000006, 0.23000000000000007,
0.24000000000000007, 0.25000000000000006, 0.26000000000000006,
0.2700000000000001, 0.2800000000000001, 0.2900000000000001,
0.3000000000000001, 0.3100000000000001, 0.3200000000000001,
0.3300000000000001, 0.34000000000000014, 0.35000000000000014,
0.36000000000000015, 0.37000000000000016, 0.38000000000000017,
0.3900000000000002, 0.4000000000000002, 0.4100000000000002,
0.4200000000000002, 0.4300000000000002, 0.4400000000000002,
0.45000000000000023, 0.46000000000000024, 0.47000000000000025,
0.48000000000000026, 0.49000000000000027, 0.5000000000000002,
0.5100000000000002, 0.5200000000000002, 0.5300000000000002,
0.5400000000000003, 0.5500000000000003, 0.5600000000000003,
0.5700000000000003, 0.5800000000000003, 0.5900000000000003,
0.6000000000000003, 0.6100000000000003, 0.6200000000000003,
0.6300000000000003, 0.6400000000000003, 0.6500000000000004,
0.6600000000000004, 0.6700000000000004, 0.6800000000000004,
0.6900000000000004, 0.7000000000000004, 0.7100000000000004,
0.7200000000000004, 0.7300000000000004, 0.7400000000000004,
0.7500000000000004, 0.7600000000000005, 0.7700000000000005,
0.7800000000000005, 0.7900000000000005, 0.8000000000000005,
0.8100000000000005, 0.8200000000000005, 0.8300000000000005,
0.8400000000000005, 0.8500000000000005, 0.8600000000000005,
0.8700000000000006, 0.8800000000000006, 0.8900000000000006,
0.9000000000000006, 0.9100000000000006, 0.9200000000000006,
0.9300000000000006, 0.9400000000000006, 0.9500000000000006,
0.9600000000000006, 0.9700000000000006, 0.9800000000000006,
0.9900000000000007, 1.0000000000000007, 1.0100000000000007,
1.0200000000000007, 1.0300000000000007, 1.0400000000000007,
1.0500000000000007, 1.0600000000000007, 1.0700000000000007,
1.0800000000000007, 1.0900000000000007, 1.1000000000000008,
1.1100000000000008, 1.1200000000000008, 1.1300000000000008,
1.1400000000000008, 1.1500000000000008, 1.1600000000000008,
1.1700000000000008, 1.1800000000000008, 1.1900000000000008,
1.2000000000000008, 1.2100000000000009, 1.2200000000000009,
1.2300000000000009, 1.2400000000000009, 1.2500000000000009,
1.260000000000001, 1.270000000000001, 1.280000000000001,
1.290000000000001, 1.300000000000001, 1.310000000000001,
1.320000000000001, 1.330000000000001, 1.340000000000001,
1.350000000000001, 1.360000000000001, 1.370000000000001,
1.380000000000001, 1.390000000000001, 1.400000000000001,
1.410000000000001, 1.420000000000001, 1.430000000000001,
1.440000000000001, 1.450000000000001, 1.460000000000001,
1.470000000000001, 1.480000000000001, 1.490000000000001,
1.500000000000001, 1.5100000000000011, 1.5200000000000011,
1.5300000000000011, 1.5400000000000011, 1.5500000000000012,
1.5600000000000012, 1.5700000000000012, 1.5800000000000012,
1.5900000000000012, 1.6000000000000012, 1.6100000000000012,
1.6200000000000012, 1.6300000000000012, 1.6400000000000012,
1.6500000000000012, 1.6600000000000013, 1.6700000000000013,
1.6800000000000013, 1.6900000000000013, 1.7000000000000013,
1.7100000000000013, 1.7200000000000013, 1.7300000000000013,
1.7400000000000013, 1.7500000000000013, 1.7600000000000013,
1.7700000000000014, 1.7800000000000014, 1.7900000000000014,
1.8000000000000014, 1.8100000000000014, 1.8200000000000014,
1.8300000000000014, 1.8400000000000014, 1.8500000000000014,
1.8600000000000014, 1.8700000000000014, 1.8800000000000014,
1.8900000000000015, 1.9000000000000015, 1.9100000000000015,
1.9200000000000015, 1.9300000000000015, 1.9400000000000015,
1.9500000000000015, 1.9600000000000015, 1.9700000000000015,
1.9800000000000015, 1.9900000000000015, 2.0000000000000013,
2.010000000000001, 2.020000000000001, 2.0300000000000007,
2.0400000000000005, 2.0500000000000003, 2.06,
2.07, 2.0799999999999996, 2.0899999999999994,
2.099999999999999, 2.109999999999999, 2.1199999999999988,
2.1299999999999986, 2.1399999999999983, 2.149999999999998,
2.159999999999998, 2.1699999999999977, 2.1799999999999975,
2.1899999999999973, 2.199999999999997, 2.209999999999997,
2.2199999999999966, 2.2299999999999964, 2.239999999999996,
2.249999999999996, 2.259999999999996, 2.2699999999999956,
2.2799999999999954, 2.289999999999995, 2.299999999999995,
2.3099999999999947, 2.3199999999999945, 2.3299999999999943,
2.339999999999994, 2.349999999999994, 2.3599999999999937,
2.3699999999999934, 2.3799999999999932, 2.389999999999993,
2.399999999999993, 2.4099999999999926, 2.4199999999999924,
2.429999999999992, 2.439999999999992, 2.4499999999999917,
2.4599999999999915, 2.4699999999999913, 2.479999999999991,
2.489999999999991, 2.4999999999999907, 2.5099999999999905,
2.5199999999999902, 2.52999999999999, 2.53999999999999,
2.5499999999999896, 2.5599999999999894, 2.569999999999989,
2.579999999999989, 2.5899999999999888, 2.5999999999999885,
2.6099999999999883, 2.619999999999988, 2.629999999999988,
2.6399999999999877, 2.6499999999999875, 2.6599999999999873,
2.669999999999987, 2.679999999999987, 2.6899999999999866,
2.6999999999999864, 2.709999999999986, 2.719999999999986,
2.7299999999999858, 2.7399999999999856, 2.7499999999999853,
2.759999999999985, 2.769999999999985, 2.7799999999999847,
2.7899999999999845, 2.7999999999999843, 2.809999999999984,
2.819999999999984, 2.8299999999999836, 2.8399999999999834,
2.849999999999983, 2.859999999999983, 2.869999999999983,
2.8799999999999826, 2.8899999999999824, 2.899999999999982,
2.909999999999982, 2.9199999999999817, 2.9299999999999815,
2.9399999999999813, 2.949999999999981, 2.959999999999981,
2.9699999999999807, 2.9799999999999804, 2.9899999999999802,
2.99999999999998, 3.00999999999998, 3.0199999999999796,
3.0299999999999794, 3.039999999999979, 3.049999999999979,
3.0599999999999787, 3.0699999999999785, 3.0799999999999783,
3.089999999999978, 3.099999999999978, 3.1099999999999777,
3.1199999999999775, 3.1299999999999772, 3.139999999999977,
3.149999999999977
];
var INDEX = 0;
var MAX_INDEX = LOOKUP.length;
function getSineNext() {
if (INDEX == MAX_INDEX) {
INDEX = 0;
}
sineNext = Math.sin(LOOKUP[INDEX]);
INDEX++;
return sineNext;
}
// /General Utils
///////////////////////
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment