Skip to content

Aftertouch for damper release (Yamaha N1X & PTQ or VI Labs Modern D)

OlivierW

Hi everyone,

Yamaha hybrid pianos such as the N1X use Polyphonic Aftertouch to transmit detailed damper release information, depending on the exact speed and position of the key when released.
This results in very precise MIDI data for the damper duration and release behavior.

Here is a short video showing the relation between different release speeds/positions and the resulting MIDI messages:

Pianoteq already includes a Note Off Velocity curve, which is excellent.
It would be fantastic to add an option to drive this curve with Polyphonic Aftertouch instead of Note Off Velocity.

And this addition would also be great (and very easy to implement) for the Modern D by VI Labs: just a few lines of MIDI script could provide a “Release Aftertouch option” with both a default curve and a user-defined curve.

This small feature would unlock the full potential of Yamaha hybrid pianos and greatly enhance the realism of damper release across virtual instruments.

Regards,

Olivier F.


HZPiano

OlivierW

Nice work!

Do note though, that VI Labs does not make use of note-off velocity for any of their virtual pianos.

I asked them about this and they confirmed as such by e-mail sometime in the last few months. I then posted about their reply somewhere on the 'Clack.

Cheers and keep up the good work,

HZ


johanibraaten

OlivierW Great demonstration. I agree that implementing this advanced damper simulation in more VI:s would be great and I believe it shouldn't be that tricky, it's basically the same function as half/partial pedal controlled on a per-note basis.

just a side note: Those "aftertouch"-massages are not really meant as standard midi messages but are instead messages about damper position in the Yamaha Disklavier XP midi dialect and can be understood by other Yamaha hybrids/silent/transacoustic/disklavier instruments and to some degree even Pianoteq. If you change miidi dialect in Pianoteq to "Disklavier XP" those messages will show as "Damper position" instead as "Aftertouch".


OlivierW

Yes, Pianoteq can indeed rename it as “Disklavier dialect” with “Damper position” in the MIDI panel, but this has no effect in practice. Pianoteq 8 does not actually process or respond to these Polyphonic Aftertouch damper data.

I’ve tested it directly: the messages are transmitted, but Pianoteq simply ignores them in the current version.

For clarity: these are standard Polyphonic Aftertouch MIDI messages, with values typically between 32 (damper near the strings) and 20 (damper far from the strings).

On Yamaha hybrids such as the N1X, the behavior is also conditional:
• If the key is released very quickly, no transitional aftertouch is generated.
• The detection is based on both the position and the speed of release.
• In other words, the Polyphonic Aftertouch messages themselves are conditioned by the release velocity and show the exact damper position


johanibraaten

OlivierW Pianoteq 8 does not actually process or respond to these Polyphonic Aftertouch damper data.

I’ve tested it directly: the messages are transmitted, but Pianoteq simply ignores them in the current version.

This I partly true and that's why I wrote that it functions "to some degree". It's true that it doesn't have any effect in Pianoteq regarding slow resp. quick release/dampening but it does function as it should when it comes to silent keypress. As discussed in another thread the first generation Avantgrands doesn't send the standard midi message for silent keypress, but instead it send the Disklavier XP message for the same which is "Damper position 127" which is translated as "aftertouch 127" in standard midi". Therefore, if you press a key slowly without generating a sound on a N1/N2/N3 and then hit keys that correspond to that notes harmonics it will not generate any overtones in standard midi mode. However if you switch dialect to Disklavier XP the silent keypress works perfectly as it should with corresponding harmonics.

OlivierW For clarity: these are standard Polyphonic Aftertouch MIDI messages, with values typically between 32 (damper near the strings) and 20 (damper far from the strings).

I agree but as far as I know the general use of polyphonic aftertouch data is different effects that's generated by pressing the key after the initial keystroke. I personally don't know about any (commercial) applications that uses polyphonic aftertouch for release/damper behavior except for the excellent work you have done with your scripts and apps.


OlivierW

/* Logic Pro Scripter
   - PolyAT 20..32 : convertis en NoteOff selon table
   - PolyAT hors 20..32 : laissés passer tels quels
   - NoteOff original : TOUJOURS transformé en velocity = 127
*/

const MAP = {
  32: 1, 31: 2, 30: 3, 29: 4, 28: 5, 27: 6, 26: 7,
  25: 8, 24: 9, 23: 10, 22: 11, 21: 11, 20: 60
};

function HandleMIDI(e) {
  if (e instanceof PolyPressure) {
    const v = e.value|0;

    if (v >= 20 && v <= 32) {
      // Conversion selon table
      const mapped = MAP[v];
      if (mapped != null) {
        const off = new NoteOff();
        off.pitch   = e.pitch;
        off.channel = e.channel;
        off.velocity = mapped;
        off.send();
        return; // ne pas transmettre le PolyAT original
      }
    }

    // Hors plage -> laisser passer le PolyAT
    e.send();
    return;
  }

  if (e instanceof NoteOff) {
    // Toujours transformé en velocity 127
    e.velocity = 127;
    e.send();
    return;
  }

  // Tout le reste passe inchangé
  e.send();
}

Example with PIANOTEQ : set the note OFF curve like this for my LOGIC script and you will have smart release with your N1X and PTQ ! 🙂


CyberGene

OlivierW are you sure that's the correct way of deriving note-off velocity from the poly aftertouch?

My understanding is the poly aftertouch values sent by the N1X represent the position of the damper when near the damping point, not the actual velocity value. They are sent with a high frequency when near that point, which is why you need to take at least two of them and derive a velocity value from them by dividing the delta between the two values with the time elapsed between the two events.


OlivierW

I agree with you, but maybe you haven’t noticed that the N1X already calculates internally a velocity between the key bottom and the damper position. If this velocity is slower than usual, the N1X sends information about the damper position using poly aftertouch. But if the key is released normally and quickly, there is no poly aftertouch at all.

So the N1X conditions the poly aftertouch information on the release speed of the key. Damper position aftertouch is therefore not generated systematically.

That’s why this simple script, which is targeted for Pianoteq, already produces results very close to the “smart release” sound of the N1X’s own engine. It’s surprising, I know, but true.

Of course, I could always improve the script by adding timestamp-based calculations, but that would only marginally change the outcome, since aftertouch is only generated when really needed — in case of slow key release.


OlivierW

You ask, i code :

Logic SCRIPT for N1X, tu use NOTE-OFF velocity set like this:

/* Logic Pro Scripter — PolyAT slope -> NoteOff velocity (silent, tuned)
   - Collects 2 PolyAT (20..32) samples + timestamps
   - slope = |Δvalue| / Δtime (values per millisecond)
   - Maps slope -> NoteOff velocity when NoteOff arrives
   - If no relevant PolyAT was received: NoteOff velocity = Default OffVel
   - Option: Pass-thru PolyAT (Yes/No)
*/

var PluginParameters = [
  { name: "OffVel Min", type: "lin", minValue: 1, maxValue: 127, numberOfSteps: 126, defaultValue: 1 },    // min = 1
  { name: "OffVel Max", type: "lin", minValue: 1, maxValue: 127, numberOfSteps: 126, defaultValue: 50 },   // max = 50
  { name: "Slope Min (x1000)", type: "lin", minValue: 1, maxValue: 200, numberOfSteps: 199, defaultValue: 10 },  // 0.010/ms
  { name: "Slope Max (x1000)", type: "lin", minValue: 5, maxValue: 400, numberOfSteps: 395, defaultValue: 80 },  // 0.080/ms
  { name: "Default OffVel", type: "lin", minValue: 1, maxValue: 127, numberOfSteps: 126, defaultValue: 100 },   // fallback
  { 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; }

function getParam(name){
  for (var i=0;i<PluginParameters.length;i++){
    if (PluginParameters[i].name === name) return paramValues[i];
  }
  return null;
}

// state per note: key = (channel<<7)+pitch
var noteState = {};

function keyOf(e){ return (e.channel << 7) + e.pitch; }
function nowMs(){ return Date.now(); }
function withinWindowAT(v){ return v >= 20 && v <= 32; }

function rememberATSample(e){
  var k = keyOf(e);
  var s = noteState[k];
  if (!s){ s = {}; noteState[k] = s; }
  var t = nowMs();
  var v = e.value|0;

  if (!withinWindowAT(v)) return false;

  if (s.v2 !== undefined){ s.v1 = s.v2; s.t1 = s.t2; }
  s.v2 = v; s.t2 = t;
  return true;
}

function computeSlopePerMs(s){
  if (s && s.v1 !== undefined && s.v2 !== undefined && s.t1 !== undefined && s.t2 !== undefined){
    var dv = Math.abs(s.v2 - s.v1);
    var dt = (s.t2 - s.t1);
    if (dt > 0) return dv / dt; // values per ms
  }
  return null;
}

function mapSlopeToVelocity(slope){
  var vmin = getParam("OffVel Min")|0;
  var 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 HandleMIDI(e){
  if (e instanceof NoteOn){
    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);
    var s = noteState[k];
    var slope = computeSlopePerMs(s);
    var vel = mapSlopeToVelocity(slope);

    e.velocity = vel;
    e.send();

    delete noteState[k];
    return;
  }

  e.send();
}

CyberGene

OlivierW I see, that makes sense. I haven’t examined the poly aftertouch behavior and thought it was sent always. Then even your initial approach makes sense 👍🏻


OlivierW

@"CyberGene"#Thanks to you I’ve been able to refine my script for Pianoteq, and honestly Yamaha is just the best with their velocity + position approach. I even have another setup where I combine Pianoteq and the Modern D to simulate damper noise and release depending on position and speed — something that would be totally impossible with the Kawai hybrids 😏. Honestly, I dream of a C3X with the SH3 system.


OlivierW

/* 
  Logic Pro Scripter — PolyAT slope -> NoteOff velocity
  ----------------------------------------------------
  Adapted for Yamaha P-525 (PolyAT fixed at 18)
  
Fonctionnement : - Le script collecte les messages Poly Aftertouch (PolyPressure) compris entre 18 et 32. - Lorsqu'un NoteOff arrive : slope = dernière valeur PolyAT / temps écoulé depuis ce dernier PolyAT (valeurs par milliseconde) - Cette pente est mappée vers une vélocité de NoteOff comprise entre OffVel Min et OffVel Max. - Si aucun PolyAT n’a été reçu : NoteOff velocity = Default OffVel. - Option : passer ou non les messages PolyAT à l’instrument. */ var PluginParameters = [ { 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 }, // 0.010/ms { name: "Slope Max (x1000)", type: "lin", minValue: 5, maxValue: 400, numberOfSteps: 395, defaultValue: 80 }, // 0.080/ms { 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; } function getParam(name) { for (var i = 0; i < PluginParameters.length; i++) { if (PluginParameters[i].name === name) return paramValues[i]; } return null; } // état par note : key = (channel<<7)+pitch var noteState = {}; function keyOf(e) { return (e.channel << 7) + e.pitch; } function nowMs() { return Date.now(); } // compatibilité Yamaha P-525 : accepte valeur fixe 18 function withinWindowAT(v) { return v >= 18 && v <= 32; } function rememberATSample(e) { var k = keyOf(e); var s = noteState[k]; if (!s) { s = {}; noteState[k] = s; } var t = nowMs(); var v = e.value | 0; if (!withinWindowAT(v)) return false; s.v2 = v; // dernière valeur s.t2 = t; // dernier timestamp return true; } // slope = dernière valeur / (temps entre dernier PolyAT et NoteOff) function computeSlopePerMs(s, noteOffTimeMs) { if (s && s.v2 !== undefined && s.t2 !== undefined) { var dv = Math.abs(s.v2); var dt = (noteOffTimeMs - s.t2); if (dt > 0) { return dv / dt; } } return null; } function mapSlopeToVelocity(slope) { var vmin = getParam("OffVel Min") | 0; var 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 HandleMIDI(e) { if (e instanceof NoteOn) { 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); var s = noteState[k]; var slope = computeSlopePerMs(s, nowMs()); var vel = mapSlopeToVelocity(slope); e.velocity = vel; e.send(); delete noteState[k]; return; } e.send(); }