|
/* Logic Pro Script to detect and apply velocity corrections to load notes. This is particularly useful for the Yamaha NU1X |
|
* hybrid piano, which can sometimes register a key press with high velocity by accident, usually during rapid trilling or repeating of notes (in this case a regular acoustic |
|
* would simply play a silent or much quieter note). |
|
* |
|
* This script is based on the Kontakt script by herqX located at https://github.com/herqX/YamahaNU1LoudNoteFix. I took the |
|
* liberty to add a few more parameters (see README) |
|
* |
|
* LICENSE: GNU General Public License V3 |
|
* |
|
*/ |
|
|
|
// this script works by comparing velocities of the same note over time; therefore we need timing data |
|
var NeedsTimingInfo = true; // yes, var; Logic Pro won't read this if it's "let" or "const" |
|
|
|
// The original script played a quieter note, but here we have the option to make the note silent if you'd rather |
|
const MODES = { |
|
PLAY: "Play, with gain applied", |
|
MUTE: "Silent note" |
|
}; |
|
|
|
const MODE_MAP = { |
|
PLAY: 0, |
|
MUTE: 1 |
|
}; |
|
|
|
const PARAMS = { |
|
MODE: "Correction Mode", |
|
MAX_DT: "Maximum time interval (ms)", |
|
MIN_DV: "Minimum velocity interval", |
|
MAX_V: "Maximum allowed velocity", |
|
GAIN: "Velocity correction gain (%)", |
|
ENABLED: "Fix NU1(X) Loud Notes", |
|
LIMIT: "Apply velocity limit", |
|
LOG: "Log" |
|
} |
|
|
|
// yes, var; Logic Pro won't read this if it's "let" |
|
var PluginParameters = [ |
|
{ |
|
name: PARAMS.MODE, type: "menu", |
|
valueStrings: Object.values(MODES), defaultValue: 0 |
|
}, |
|
{ |
|
name: PARAMS.MAX_DT, type: "lin", |
|
defaultValue: 120, minValue: 0, maxValue: 2000, numberOfSteps: 500 |
|
}, |
|
{ |
|
name: PARAMS.MIN_DV, type: "lin", |
|
defaultValue: 15, minValue: 0, maxValue: 127, numberOfSteps: 127 |
|
}, |
|
{ |
|
name: PARAMS.MAX_V, type: "lin", |
|
defaultValue: 127, minValue: 0, maxValue: 127, numberOfSteps: 127 |
|
}, |
|
{ |
|
name: PARAMS.GAIN, type: "lin", |
|
defaultValue: 80, minValue: 0, maxValue: 100, numberOfSteps: 100 |
|
}, |
|
{ |
|
name: PARAMS.ENABLED, type: "checkbox", |
|
defaultValue: 1 |
|
}, |
|
{ |
|
name: PARAMS.LIMIT, type: "checkbox", |
|
defaultValue: 0 |
|
}, |
|
{ |
|
name: PARAMS.LOG, type: "checkbox", |
|
defaultValue: 1 |
|
} |
|
] |
|
|
|
let options = {}; |
|
let noteVelocities = Array.from({length: 128}, _ => 0); |
|
let noteIntervals = Array.from({length: 128}, _ => 0); |
|
|
|
// Called whenever a UI parameter is changed. We don't use param or value here; instead I just |
|
// loop over the parameters (PARAMS) and create a new object from them by calling GetParameter. |
|
function ParameterChanged(param, value) { |
|
options = Object.entries(PARAMS).reduce( (acc, cur, idx) => { |
|
let [k, v] = cur; |
|
acc[k] = GetParameter(v); |
|
return acc; |
|
}, {}); |
|
noteVelocities = Array.from({length: 128}, _ => 0); |
|
noteIntervals = Array.from({length: 128}, _ => 0); |
|
} |
|
|
|
// Called for every MIDI event |
|
function HandleMIDI(event) |
|
{ |
|
// beatPos is in seconds, but our parameter is in milliseconds, so multiply it up |
|
const timestamp = Math.floor(event.beatPos * 1000); |
|
|
|
// record the time each note is released (we'll compare this value the next time we see the note played) |
|
if (event instanceof NoteOff) { |
|
noteIntervals[event.pitch] = timestamp; |
|
} |
|
|
|
if (event instanceof NoteOn) { |
|
|
|
let newVelocity = event.velocity; |
|
const originalVelocity = event.velocity; |
|
const previousVelocity = noteVelocities[event.pitch]; |
|
|
|
const deltaT = timestamp - noteIntervals[event.pitch]; |
|
const deltaV = event.velocity - previousVelocity; |
|
|
|
// store the original MIDI velocity data |
|
// TODO: I'm ambivalent on whether this is good (or if we should store the _corrected_ velocity) |
|
noteVelocities[event.pitch] = originalVelocity; |
|
|
|
// If we're applying a limit, cap the velocity to the limit |
|
if (options.LIMIT) { |
|
newVelocity = Math.min(options.MAX_V, originalVelocity); |
|
event.velocity = newVelocity; |
|
} |
|
|
|
// if velocity correction is enabled, compare with the previous velocity. If it's in the |
|
// specified time frame (MAX_DT) _AND_ the change in velocity is greater than our cutoff (MIN_DV) |
|
// we'll apply some correction. (We don't use absolute values here, becase we're not trying to |
|
// smooth quieter notes) |
|
if (options.ENABLED) { |
|
|
|
if (deltaT < options.MAX_DT && deltaV > options.MIN_DV && previousVelocity > 0) { |
|
switch (options.MODE) { |
|
case MODE_MAP.PLAY: |
|
// apply the specified gain to the correction -- this will cause it to be quieter than the |
|
// current MIDI velocity, but it will still be louder than the old velocity -- since the |
|
// player may actually be intending to play louder, unless the correction gain is 100% |
|
newVelocity = Math.min(options.MAX_V, |
|
Math.floor(previousVelocity + (((100 - options.GAIN) / 100) * deltaV)) |
|
); |
|
break; |
|
case MODE_MAP.MUTE: |
|
default: |
|
// silence the note in this mode. |
|
newVelocity = 0; |
|
} |
|
event.velocity = newVelocity; |
|
} |
|
} |
|
|
|
// log out velocity corrections to the console |
|
if (options.LOG && originalVelocity != newVelocity) { |
|
Trace(`${timestamp} [${MIDI.noteName(event.pitch).padEnd(3)} on] ov:${originalVelocity} nv:${newVelocity} ${originalVelocity != newVelocity ? "*" : " "} pv:${previousVelocity}`); |
|
} |
|
|
|
} |
|
|
|
// send the new MIDI information |
|
event.send(); |
|
} |
|
|