Old 09-30-2019, 03:22 PM   #1
michaeltonight
Human being with feelings
 
michaeltonight's Avatar
 
Join Date: Jun 2010
Location: Texas
Posts: 279
Default JSFX to filter all but highest-velocity duplicates?

Does anyone know of a JSFX that will filter out duplicate notes but keep the highest velocity note? IxIx's excellent "MIDI_DuplicateFilter" is almost doing what I need, but I'm thinking of trying to modify it so that it outputs at the highest velocity of the incoming duplicate notes.

My application is crosstalk-reduction for a hexaphonic guitar pickup; MIDI_DuplicateFilter is successfully giving me one note per pluck, but it seems to sometimes pass through the quiet triggers from adjacent strings while filtering out the stronger notes from the desired string.

If any of you geniuses thinks that'd be an easy mod to do, I'd much appreciate anyone taking a stab, otherwise I'll report back in ... a couple of weeks :P Here's the code to IxIx's original:

Code:
/*******************************************************************************
*  Copyright 2007 - 2011, Philip S. Considine                                  *
*  This program is free software: you can redistribute it and/or modify        *
*  it under the terms of the GNU General Public License as published by        *
*  the Free Software Foundation, either version 3 of the License, or           *
*  (at your option) any later version.                                         *
*                                                                              *
*  This program is distributed in the hope that it will be useful,             *
*  but WITHOUT ANY WARRANTY; without even the implied warranty of              *
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                *
*  GNU General Public License (http://www.gnu.org/licenses/)for more details.  *
*******************************************************************************/

desc: MIDI Duplicate Note Filter
desc: MIDI Duplicate Note Filter [IXix]
//tags: MIDI filter processing
//author: IXix

slider1:0<0,15,1{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}>Input Channel

in_pin:none
out_pin:none

////////////////////////////////////////////////////////////////////////////////
@init

statNoteOn = $x90;
statNoteOff = $x80;

////////////////////////////////////////////////////////////////////////////////
@slider
inChannel = slider1;

////////////////////////////////////////////////////////////////////////////////
@block

trigger & (2 ^ 0) ? // Reset counters and send all notes off on this channel.
(
  i = 0;
  while
  (
    tracker[i] = 0;
    midisend(0, statNoteOff + inChannel, i | 0 * 256);
    (i += 1) < 127;
  );
  trigger -= 2 ^ 0;
);

while
(
  midirecv(offset, msg1, msg23) ?
  (  
    // Reset pass flag
    passThru = 1;
    
    // Extract message type and channel
    status = msg1 & $xF0;
    channel = msg1 & $x0F;
      
    // Is it on our channel?
    channel == inChannel ? 
    (
      // Is it a note event?
      status == statNoteOn || status == statNoteOff ?
      (
        // Get note and velocity
        note = msg23 & $x7F;
        velocity = msg23 >> 8;
        
        // Update counters and flag to pass if necessary. Zero velocity is regarded as note off
        status == statNoteOn && velocity > 0 ?
        (
          // Note on
          tracker[note] += 1;
          tracker[note] == 1 ? midisend(offset, msg1, msg23);
        )
        :
        (
          // Note off
          tracker[note] -= 1;
          tracker[note] == 0 ? midisend(offset, msg1, msg23);
        );
      )
      :
      (
        midisend(offset, msg1, msg23); // Not a note, pass thru
      );
    )
    :
    (
      midisend(offset, msg1, msg23); // Not on our channel, pass thru
    );
    
    1; // Force loop to continue until all messages have been processed
  );
);
michaeltonight is offline   Reply With Quote
Old 09-30-2019, 10:27 PM   #2
mschnell
Human being with feelings
 
mschnell's Avatar
 
Join Date: Jun 2013
Location: Krefeld, Germany
Posts: 7,865
Default

Quote:
Originally Posted by michaeltonight View Post
Does anyone know of a JSFX that will filter out duplicate notes but keep the highest velocity note?
Do you want to drop lower velocity follow ups for already running notes that had previously been triggered with some velocity value and not yet terminated by a note-off ?

Seems rather weird and hence I doubt that such a plugin already exists. But certainly doable.

-Michael
mschnell is offline   Reply With Quote
Old 10-01-2019, 12:47 AM   #3
michaeltonight
Human being with feelings
 
michaeltonight's Avatar
 
Join Date: Jun 2010
Location: Texas
Posts: 279
Default

Yeah, it is a little weird. I only want to filter out lower-velocity notes with similar (basically simultaneous) start times only.

I'm sending each string's output of a hexaphonic pickup to a REAPER track with a monophonic instance of MIDI Guitar 2, and all of those go to one instance of Kontakt. Currently, it works OK but needs editing after I play. Because, for example, if I pluck the B string, it sends a MIDI note, but the spillover from the E and G string outputs may also trigger note-ons. The current duplicate filter often outputs at the lower velocity of the spillover notes, so sometimes I need to go in and adjust a bunch of velocities after the fact.

Which is not the worst problem in the world, so that's lucky! Frankly I'm amazed that any of this is even possible.
michaeltonight is offline   Reply With Quote
Old 10-01-2019, 06:38 AM   #4
mschnell
Human being with feelings
 
mschnell's Avatar
 
Join Date: Jun 2013
Location: Krefeld, Germany
Posts: 7,865
Default

Quote:
Originally Posted by michaeltonight View Post
Yeah, it is a little weird. I only want to filter out lower-velocity notes with similar (basically simultaneous) start times only.
I suppose that is much more complex than you estimate ...
-Michael
mschnell is offline   Reply With Quote
Old 10-02-2019, 06:36 AM   #5
snooks
Human being with feelings
 
Join Date: Sep 2015
Posts: 1,623
Default

There's would be a tiny bit of time travel required for this to be zero latency, so you'd need to be happy with a configurable delay on all MIDI to catch all near-instantaneous duplicates. Would that be acceptable?
snooks is offline   Reply With Quote
Old 10-02-2019, 06:50 AM   #6
mschnell
Human being with feelings
 
mschnell's Avatar
 
Join Date: Jun 2013
Location: Krefeld, Germany
Posts: 7,865
Default

No idea.

Of course a Midi delay is doable in a JSFX and there are some examples. So you can try to write such a thingy.

-Michael
mschnell is offline   Reply With Quote
Old 10-02-2019, 07:11 AM   #7
michaeltonight
Human being with feelings
 
michaeltonight's Avatar
 
Join Date: Jun 2010
Location: Texas
Posts: 279
Default

Yeah, as I started to work on making my own just now, I realized that the duplicates wouldn't necessarily trigger during the same block. I'm going to look at a WAV recording I did recently and see how the timing of the audio bleed between strings looks. I imagine it will be well under a millisecond, since all the interference is happening within a few cm.
Thanks for chiming in on this!
michaeltonight is offline   Reply With Quote
Old 10-03-2019, 07:23 AM   #8
michaeltonight
Human being with feelings
 
michaeltonight's Avatar
 
Join Date: Jun 2010
Location: Texas
Posts: 279
Default

I'm making some progress modifying ixix's Duplicate Filter; I currently have it putting up to three duplicates in a table (or nested bracket structure, I don't know what they're called in JSFX :P ) while also keeping track of how many blocks have gone by per note.

So if it gets three of the same note, then it spits out that note at the highest of the three velocities, and if not, it keeps listening until it either receives more or a certain number of blocks have gone by, at which point it either spits out the higher of two duplicates, or just the single note (which I'm not expecting based on my exact use-case).

I measured the time delay of the crosstalk I'm working with, and it's under 1ms, so I'm hoping 2 or 3 blocks of 256 samples at 48k will be enough. It seems a little clumsy, but for my exact need right now, I'm fine with a little added latency.
michaeltonight is offline   Reply With Quote
Old 10-04-2019, 02:50 PM   #9
michaeltonight
Human being with feelings
 
michaeltonight's Avatar
 
Join Date: Jun 2010
Location: Texas
Posts: 279
Default

So I "finished" modding ixix's plugin to do what I want, and at first it wasn't perfect, but helped. Then I found and fixed a bunch of dumb syntax errors in my code, and now it works hilariously badly! Total MIDI chaos. I'm too embarrassed even to share. When I have a little more free time soon I'll devote more brainpower to it and report back
michaeltonight is offline   Reply With Quote
Old 10-04-2019, 04:07 PM   #10
snooks
Human being with feelings
 
Join Date: Sep 2015
Posts: 1,623
Default

It's a slight PITA dealing with note-ons and note-offs when delaying things with transport changes etc. I've rustled you up something for testing using some old sequencer code. It works from very limited testing, rely on the force using the sample slider and not the actual numbers for now.
Code:
desc:Lower Velocity Duplicate Note Filter

slider1:100<0,2000,1>lookahead (samples)

@init
  NOTE_ON = 0x90;
  NOTE_OFF  = 0x80;
  ignore_noteoffs.mem = 10000; 
  
  /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
   *                           /begin seq                              *
   *                                                                   */
   
  _seq.mem = 0; // event array start position
  _seq.evt_width = 4;
  _seq.len = 0;
  _seq.last_play_pos = 0;
  
  seq.retval0 = seq.retval1 = seq.retval2 = seq.retval3 = 0;
  
  //
  function seq.addEvent(samples_ahead, m1, m2, m3) 
    local( i )
  (
     i = _seq.len;
     _seq.mem[_seq.evt_width * i] = samples_ahead;
     _seq.mem[_seq.evt_width * i + 1] = m1;
     _seq.mem[_seq.evt_width * i + 2] = m2;
     _seq.mem[_seq.evt_width * i + 3] = m3;
     _seq.len += 1;
     _ADD += 1;
  );
  
  //
  function seq.addNote(start, length, channel, note, velocity)
    local ()
  (
    seq.addEvent(start, channel | 0x90, note, velocity);
    seq.addEvent(start + length, channel | 0x80, note, 0);
  );
  
  // searches backwards through event array for match
  function seq.getLastMatchingEvent(m1, m2) 
    local ( evt_pos, evt, pos )
  (
    evt_pos = _seq.len - 1;
    evt = -1;
    while (evt_pos >= 0 && evt == -1) (
      pos = evt_pos * _seq.evt_width;
      _seq.mem[pos + 1] == m1 && _seq.mem[pos + 2] == m2 ? evt = evt_pos;
      evt_pos -= 1;
    );
    evt > -1 ? (
      pos = evt * _seq.evt_width;
      seq.retval0 = _seq.mem[pos];
      seq.retval1 = _seq.mem[pos + 1];
      seq.retval2 = _seq.mem[pos + 2];
      seq.retval3 = _seq.mem[pos + 3];
    );
    evt; // return event position if found, or -1
  );
  
  //
  function seq.removeEvent(evt_pos)
    local ( pos )
  (
    evt_pos < _seq.len ? (
      memcpy(_seq.mem + (_seq.evt_width * evt_pos), 
          _seq.mem + _seq.evt_width * (evt_pos + 1),
          _seq.evt_width * (_seq.len - evt_pos - 1)
      );
      _seq.len -= 1;
      _seq.len; // return new length...
    ) 
    : (
      -1; // ... or failure
    );
  );
  
  //
  function seq.sendEventAndRemove(evt_pos) 
    local ( sp )
  (
    sp = _seq.evt_width * evt_pos;
    midisend(_seq.mem[sp] + samplesblock, _seq.mem[sp + 1], 
        _seq.mem[sp + 2], _seq.mem[sp + 3]
    );
    seq.removeEvent(evt_pos);
  );
  
  //
  function isRelocated()
    local ( diff )
  (
    diff = play_position - _seq.last_play_pos;
    _seq.last_play_pos > 0 && (diff < 0 || diff > samplesblock + 1) ? (
      1;
    )
    : (
      0;
    );
  );
    
  //
  function isHandleRelocation()
    local ( i )
  (
    isRelocated() ? ( 
      // send all notes off, all channels
      i = 0;
      loop(16,
        midisend(0, 0xb0 | i, 123, 0);
        i += 1;
      );
      i = 0;
      loop(128,
        ignore_noteoffs.mem[i] = 0;
        i += 1;
      );
      _seq.len = 0;
      _seq.last_play_pos = play_position;
      1;
    )
    : (
      _seq.last_play_pos = play_position;
      0;
    );
  );
  
  //
  function seq.processBlockEvents() 
    local ( evt, pos )
  (
    (!isHandleRelocation()) ? (
      evt = 0;
      pos = 0;
      while (evt < _seq.len)(
        pos = _seq.evt_width * evt;
        _seq.mem[pos] -= samplesblock;
        _seq.mem[pos] < 0 ? (
          seq.sendEventAndRemove(evt);
        ) 
        : (
          evt += 1;
        );
      );
    );
  );
  
  /*                                                                   *
   *                            /end seq                               *
   * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 

  function sendDelayedEvent(offset, m1, m2, m3)
    local ()
  (
    offset += slider1;
    offset > samplesblock ? (
      seq.addEvent(offset, m1, m2, m3);
    )
    : (
      midisend(offset, m1, m2, m3);
    );
  );
  
  
  // seq.test() // needs seq.sendThisBufferEvents(); in @block

@block
  lookahead = slider1;
  
  while (midirecv(offset, m1, m2, m3)) (
    // process note ons and offs
    m1 & 0xF0 == NOTE_ON || m1 & 0xF0 == NOTE_OFF ?
    (
      m1 & 0xF0 == NOTE_OFF ? (
        // note offs
        ignore_noteoffs.mem[m2] > 0 ? (
          ignore_noteoffs.mem[m2] -= 1;
        )
        : (
          sendDelayedEvent(offset, m1, m2, m3);
        );
      )
      : (
        // note ons
        evt = seq.getLastMatchingEvent(m1, m2);
        evt > -1 ? (
          offset - seq.retval0 - lookahead < lookahead && seq.retval3 < m3 ? (
            seq.removeEvent(evt);
            seq.addEvent(offset + lookahead, m1, m2, m3);
            ignore_noteoffs.mem[m2 + 5] += 1;
          );
        ) 
        : (
          seq.addEvent(offset + lookahead, m1, m2, m3);
        );
      );
    )
    : (
      // not notes - delay by lookahead too to keep stuff in sync
      sendDelayedEvent(offset, m1, m2, m3);
    );
  );
  
  seq.processBlockEvents();

Last edited by snooks; 10-05-2019 at 01:31 PM. Reason: forgot to reset ignore_note_offs; messed up with renaming a couple of things
snooks is offline   Reply With Quote
Old 10-05-2019, 08:35 AM   #11
michaeltonight
Human being with feelings
 
michaeltonight's Avatar
 
Join Date: Jun 2010
Location: Texas
Posts: 279
Default

Whoa! Thank you! Testing momentarily...
michaeltonight is offline   Reply With Quote
Old 10-05-2019, 10:28 AM   #12
michaeltonight
Human being with feelings
 
michaeltonight's Avatar
 
Join Date: Jun 2010
Location: Texas
Posts: 279
Default

Well, it's behaving pretty strangely, and for some reason triggering notes that aren't involved, but thanks so much for sending this! I'm going to take another stab tonight.
michaeltonight is offline   Reply With Quote
Old 10-05-2019, 01:34 PM   #13
snooks
Human being with feelings
 
Join Date: Sep 2015
Posts: 1,623
Default

Sorry, I renamed some stuff b4 posting to tidy it up a bit and missed something which would indeed have completely broken it. The note-off tracking isn't multi-channel aware yet either and I assume your MIDI guitar is firing on 6 channels.

I edited the post with the new code, in case it's still useful.
snooks is offline   Reply With Quote
Old 10-05-2019, 02:44 PM   #14
michaeltonight
Human being with feelings
 
michaeltonight's Avatar
 
Join Date: Jun 2010
Location: Texas
Posts: 279
Default

Cool, I'll try it again. Thanks so much. I'm currently using 6 audio track ins but using one channel of midi. I'm now wondering if most of my issue might be that regardless of crosstalk, sustained note note offs are ending the notes after them. More experimenting now!
michaeltonight is offline   Reply With Quote
Old 10-05-2019, 02:49 PM   #15
michaeltonight
Human being with feelings
 
michaeltonight's Avatar
 
Join Date: Jun 2010
Location: Texas
Posts: 279
Default

Oh wow, that's a big improvement! Thanks!
michaeltonight is offline   Reply With Quote
Old 10-05-2019, 03:17 PM   #16
snooks
Human being with feelings
 
Join Date: Sep 2015
Posts: 1,623
Default

Oh that's a relief!

If the notes were kept multi-channel on the way in to the script it would automagically (once the note-off ignore list is multi-channel) block the correct note off, but with everything on one channel it wouldn't be doable unless the strings note-offs were different and fixed note off velocities.
snooks is offline   Reply With Quote
Reply

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT -7. The time now is 04:13 AM.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2019, vBulletin Solutions Inc.