Skip to content

🎹 Yamaha P-525 + Logic Pro 11: “NoteOn/NoteOff Optimizer” Scripter

OlivierW

🇫🇷 Français

Bonjour Ă  tous đź‘‹

Je partage un script Logic Pro Scripter que j’ai développé et testé avec succès sur un Yamaha P-525, dans Logic Pro 11 sous macOS avec Pianoteq.

👉 Ce script (version 1.11) optimise :
• la vélocité des NoteOn grâce au contrôleur CC19 (Key Acceleration) spécifique Yamaha,
• la vélocité des NoteOff via la pente du Poly Aftertouch (18..32).

🎹 Important : sur le P-525, le CC19 est envoyé de manière polyphonique → une valeur CC19 est transmise juste avant chaque NoteOn, et donc associée individuellement à chaque touche. C’est cette donnée qui permet au script d’adapter dynamiquement la vélocité de chaque note.

Résultat :
• des courbes de vélocité plus réalistes, notamment lors des frappes lentes et de l’échappement,
• un meilleur réalisme dans Pianoteq, qui permet d’ajuster indépendamment les courbes NoteOn et NoteOff (0–127).

Paramètres ajustables :
• Ratio appliqué quand CC19=127 (par défaut ÷2)
• Ratio appliqué quand CC19=0 (par défaut ÷4)
• Facteur exponentiel pour contrôler la rapidité de l’effondrement vers 1 (par défaut 4)
• Vélocité NoteOff min/max, plage de pente, vélocité par défaut

👉 Le script fonctionne parfaitement avec Pianoteq : il suffit de régler dans Pianoteq les courbes de vélocité NoteOn et NoteOff (0–127) selon vos préférences.

⸻

🇬🇧 English

Hi everyone đź‘‹

Here’s a Logic Pro Scripter script I built and tested successfully with a Yamaha P-525, running on Logic Pro 11 (macOS) and Pianoteq.

👉 This script (version 1.11) optimizes:
• NoteOn velocity using Yamaha’s specific CC19 (Key Acceleration) controller,
• NoteOff velocity using the slope of Poly Aftertouch values (18..32).

🎹 Important: on the P-525, CC19 is transmitted polyphonically → a CC19 value is sent right before each NoteOn, and is therefore tied to each note individually. This allows the script to adapt velocity dynamically per key.

Result:
• more realistic velocity curves, especially for slow key strikes and escapement,
• better realism in Pianoteq, since NoteOn/NoteOff curves can be fine-tuned independently (0–127).

Adjustable parameters:
• Ratio when CC19=127 (default ÷2)
• Ratio when CC19=0 (default ÷4)
• Exponent factor to control how fast correction collapses to 1 (default 4)
• NoteOff velocity min/max, slope range, default fallback velocity

👉 Works great with Pianoteq: just adjust the NoteOn and NoteOff velocity curves (0–127) in Pianoteq to match your taste.

⸻

🎹 Script Logic Pro Scripter — Version 1.11

/* 
   Logic Pro Scripter — Yamaha P-525 NoteOn/NoteOff Optimizer v1.11
   ---------------------------------------------------------------
   🇫🇷
   - NoteOn : correction de la vélocité via CC19 (Key Acceleration Yamaha)
       CC19=127 -> vélocité divisée par Ratio_127 (param, défaut ÷2)
       CC19=1   -> pas de correction (ratio = 1)
       127>CC19>1 -> interpolation exponentielle entre Ratio_127 et 1
       CC19=0   -> vélocité divisée par Ratio_0 (param, défaut ÷4)
       Le paramètre "Exponent Factor" contrôle la rapidité de l’effondrement.
   - NoteOff : correction de la vélocité via la pente du dernier PolyAT (18..32).
   - Garantit que la vélocité max (127) reste atteignable.
   
🇬🇧 - NoteOn: velocity correction via CC19 (Yamaha Key Acceleration) CC19=127 -> velocity divided by Ratio_127 (param, default ÷2) CC19=1 -> no correction (ratio = 1) 127>CC19>1 -> exponential interpolation between Ratio_127 and 1 CC19=0 -> velocity divided by Ratio_0 (param, default ÷4) "Exponent Factor" controls how fast correction collapses to 1. - NoteOff: velocity correction via slope of last PolyAT (18..32). - Ensures max velocity (127) can still be reached. */ var PluginParameters = [ { name:"Ratio at CC19=127 (slow key)", type:"lin", minValue:1, maxValue:8, numberOfSteps:7, defaultValue:2 }, { name:"Ratio at CC19=0 (escapement)", type:"lin", minValue:1, maxValue:8, numberOfSteps:7, defaultValue:4 }, { name:"Exponent Factor", type:"lin", minValue:0.5, maxValue:5, numberOfSteps:45, defaultValue:4 },
{ name:"OffVel Min", type:"lin", minValue:1, maxValue:127, numberOfSteps:126, defaultValue:1 }, { name:"OffVel Max", type:"lin", minValue:1, maxValue:127, numberOfSteps:126, defaultValue:50 }, { name:"Slope Min (x1000)", type:"lin", minValue:1, maxValue:200, numberOfSteps:199, defaultValue:10 }, { name:"Slope Max (x1000)", type:"lin", minValue:5, maxValue:400, numberOfSteps:395, defaultValue:80 }, { name:"Default OffVel", type:"lin", minValue:1, maxValue:127, numberOfSteps:126, defaultValue:100 }, { name:"Pass-thru PolyAT", type:"menu", valueStrings:["No","Yes"], defaultValue:0 } ]; var paramValues = []; for (var i=0;i<PluginParameters.length;i++){ paramValues[i] = PluginParameters[i].defaultValue; } function ParameterChanged(index, value){ paramValues[index] = value; } // --- Helpers --- var lastCC19 = {}; var noteState = {}; function keyOf(e){ return (e.channel<<7)+e.pitch; } function nowMs(){ return Date.now(); } function withinWindowAT(v){ return v>=18 && v<=32; } // --- PolyAT slope --- function rememberATSample(e){ var k=keyOf(e), s=noteState[k]; if(!s){s={};noteState[k]=s;} var t=nowMs(), v=e.value|0; if(!withinWindowAT(v)) return false; s.v2=v; s.t2=t; return true; } function computeSlopePerMs(s, noteOffTimeMs){ if(s&&s.v2!==undefined&&s.t2!==undefined){ var dv=Math.abs(s.v2), dt=(noteOffTimeMs-s.t2); if(dt>0) return dv/dt; } return null; } function mapSlopeToVelocity(slope){ var vmin=getParam("OffVel Min")|0, vmax=getParam("OffVel Max")|0; var sMin=(getParam("Slope Min (x1000)")|0)/1000.0; var sMax=(getParam("Slope Max (x1000)")|0)/1000.0; if(slope==null) return getParam("Default OffVel")|0; var denom=(sMax-sMin); if(denom<=0) denom=1e-9; var t=(slope-sMin)/denom; if(t<0) t=0; if(t>1) t=1; var out=Math.round(vmin+t*(vmax-vmin)); if(out<1) out=1; if(out>127) out=127; return out; } function getParam(name){ for(var i=0;i<PluginParameters.length;i++){ if(PluginParameters[i].name===name) return paramValues[i]; } return null; } // --- Main --- function HandleMIDI(e){ if(e instanceof ControlChange && e.number===19){ lastCC19[e.channel]=e.value; return; } if(e instanceof NoteOn){ var vIn=e.velocity, cc=lastCC19[e.channel], ratio=1.0; if(cc!==undefined){ var r127=paramValues[0], r0=paramValues[1], expF=paramValues[2]; if(cc===0){ ratio=1.0/r0; } else if(cc===127){ ratio=1.0/r127; } else if(cc===1){ ratio=1.0; } else{ var t=(cc-1)/(127-1); var curve=Math.pow(t,expF); var rr1=1.0, rr127=1.0/r127; ratio=rr1+curve*(rr127-rr1); } } var vOut=Math.round(vIn*ratio); if(vOut<1) vOut=1; if(vOut>127) vOut=127; if(vIn===127 && ratio<1.0) vOut=127; // ensure max reachable e.velocity=vOut; noteState[keyOf(e)]={}; e.send(); return; } if(e instanceof PolyPressure){ rememberATSample(e); if(getParam("Pass-thru PolyAT")===1){ e.send(); } return; } if(e instanceof NoteOff){ var k=keyOf(e), s=noteState[k]; var slope=computeSlopePerMs(s,nowMs()), vel=mapSlopeToVelocity(slope); e.velocity=vel; e.send(); delete noteState[k]; return; } e.send(); }