question about detecting new notes

This forum is for developers of Rack Extensions to discuss the RE SDK, share code, and offer tips to other developers.
Post Reply
wendallsan
Posts: 45
Joined: 10 Dec 2017

19 Jun 2021

I'm trying to troubleshoot a problem I'm seeing when my rack extension is responding to incoming note data.

I'm checking to see first if I'm doing something horribly wrong and should be approaching this in some other way. If this is not the case, I THINK I have an idea of how to approach the solution, but am curious if this has already been done before or in a better way.

The problem I'm seeing is that if I play ascending notes, my method below detects these notes without problems and my fNoteOutCVJackValuePropertyRef is getting set to the note I expect. But if I play descending notes, lower pitched notes simply repeat the previous note's pitch rather than the correct, lower pitch. This only happens when the note is either immediately following a previous, higher pitched note, or if a higher pitched note is being 'held' and is still playing when the new, lower note is played. If I put a gap (rest) between the notes, the new, lower-pitched note plays fine.

Here is my current HandleNotePressed method, which is called in RenderBatch:

Code: Select all

void cMyPlugin::HandleNotePressed( const TJBox_PropertyDiff iPropertyDiffs[], TJBox_UInt32 iDiffCount ){
	for( TJBox_UInt32 i = 0; i < iDiffCount; ++i ){
        if( iPropertyDiffs[ i ].fPropertyRef.fObject == fNoteStates ){
			TJBox_Float64 velocity = JBox_GetNumber( iPropertyDiffs[ i ].fCurrentValue );
			if( velocity > 0.f ){
				TJBox_NoteEvent noteEvent = JBox_AsNoteEvent( iPropertyDiffs[ i ] );
				JBox_StoreMOMProperty( fNoteOutCVJackValuePropertyRef, JBox_MakeNumber( noteEvent.fNoteNumber / 127.f ) );
				// EXIT THE FOR LOOP SO THIS IS NOT SET BACK TO FALSE ON THE NEXT NOTE STATE INSPECTED IN THE LOOP
				break;
			}
		}
	}
}
My current idea for a solution is to track a list of 'notes that are currently on', ignore any note in that list during my check through the Property Diffs, while adding any new notes to this list once I've handled it so they'll get ignored next time and also responding to velocity = 0 notes to clean up notes from my list once they've received a 'note off' event. Does this approach sound reasonable?

User avatar
orthodox
RE Developer
Posts: 2286
Joined: 22 Jan 2015
Location: 55°09'24.5"N 37°27'41.4"E

19 Jun 2021

wendallsan wrote:
19 Jun 2021
The problem I'm seeing is that if I play ascending notes, my method below detects these notes without problems and my fNoteOutCVJackValuePropertyRef is getting set to the note I expect. But if I play descending notes, lower pitched notes simply repeat the previous note's pitch rather than the correct, lower pitch. This only happens when the note is either immediately following a previous, higher pitched note, or if a higher pitched note is being 'held' and is still playing when the new, lower note is played. If I put a gap (rest) between the notes, the new, lower-pitched note plays fine.
Couldn't it be the feature of the device that you are monitoring the incoming notes with? You are sending the note pitch to some CV output to play it with another device, right?
wendallsan wrote:
19 Jun 2021
My current idea for a solution is to track a list of 'notes that are currently on', ignore any note in that list during my check through the Property Diffs, while adding any new notes to this list once I've handled it so they'll get ignored next time and also responding to velocity = 0 notes to clean up notes from my list once they've received a 'note off' event. Does this approach sound reasonable?
I guess that's what everybody is doing. Even RTC does that with the "/note_states/*" properties. Either a plain array containing all 128 notes or something like a double-linked list based on 128 slots.

User avatar
rcbuse
RE Developer
Posts: 1175
Joined: 16 Jan 2015
Location: SR388
Contact:

19 Jun 2021

Code: Select all

void cMyPlugin::HandleNotePressed( const TJBox_PropertyDiff iPropertyDiffs[], TJBox_UInt32 iDiffCount ){
  for( TJBox_UInt32 i = 0; i < iDiffCount; ++i ){
    if( iPropertyDiffs[ i ].fPropertyRef.fObject == fNoteStates ){
			TJBox_NoteEvent noteEvent = JBox_AsNoteEvent( iPropertyDiffs[ i ] );
			if( noteEvent.fVelocity > 0.f ){
				JBox_StoreMOMProperty( fNoteOutCVJackValuePropertyRef, JBox_MakeNumber( noteEvent.fNoteNumber / 127.f ) );
				break;
			}
		}
	}
}


wendallsan
Posts: 45
Joined: 10 Dec 2017

21 Jun 2021

Thanks again for the response! I'm seeing this behavior when playing note data into my device rather than when providing Note CV data via a jack on the back panel.

I've implemented a prototype solution that tracks each individual note which seems to do what I want it to do in my testing. I've built this out to track just 2 notes as that was the minimum viable product to test things out, but now that this is working so far I'm ready to build it out to track all 128 notes. Here's what I've done so far, in hopes that someone can confirm that this is the 'right' way to do this or can point out a better approach over my planned 128 custom properties and corresponding ridiculous switch statement. Please let me know if this is how you would approach this or if there's a feature of the API that I'm missing that could help this along.

in motheboard_def.lua, I have added the following custom properties:

Code: Select all

note48IsPressed = jbox.boolean{
  default = false,
  ui_name = jbox.ui_text( "text_note48IsPressed" ),
  ui_type = jbox.ui_linear( { min = 0, max = 1, units = {{ decimals = 0 }} } )
},
note50IsPressed = jbox.boolean{
  default = false,
  ui_name = jbox.ui_text( "text_note50IsPressed" ),
  ui_type = jbox.ui_linear( { min = 0, max = 1, units = {{ decimals = 0 }} } )

in my cpp:

Code: Select all

void CMyPlugin::RenderBatch( const TJBox_PropertyDiff iPropertyDiffs[], TJBox_UInt32 iDiffCount ){
  HandleNotePressed( iPropertyDiffs, iDiffCount );
  HandleGateOutJack();
}

void CMyPlugin::HandleNotePressed( const TJBox_PropertyDiff iPropertyDiffs[], TJBox_UInt32 iDiffCount ){
  for( TJBox_UInt32 i = 0; i < iDiffCount; ++i ){
    if( iPropertyDiffs[ i ].fPropertyRef.fObject == fNoteStates ){
      TJBox_NoteEvent noteEvent = JBox_AsNoteEvent( iPropertyDiffs[ i ] );
      bool noteIsOn = noteEvent.fVelocity > 0.f;
      if( noteIsOn ) JBox_StoreMOMProperty( fNoteOutCVJackValuePropertyRef, JBox_MakeNumber( noteEvent.fNoteNumber / 127.f ) );
      switch( noteEvent.fNoteNumber ){
        // GO THROUGH EACH NOTE NUMBER AND FIND A MATCHING CASE (ONLY NOTE 48 AND 50 ARE IN PLACE CURRENTLY)
        case 48:
          // STORE WHETHER THIS WAS A NOTE ON OR A NOTE OFF EVENT FOR THE MOM Property FOR THIS NOTE NUMBER
          JBox_StoreMOMProperty( fNote48IsPressedPropertyRef, JBox_MakeBoolean( noteEvent.fVelocity > 0.f ) );
          break;
        case 50:
          JBox_StoreMOMProperty( fNote50IsPressedPropertyRef, JBox_MakeBoolean( noteEvent.fVelocity > 0.f ) );
          break;
      }
    }
  }
}
void CMyPlugin::HandleGateOutJack(){
  bool gateOutJackIsOn = false;
  // LOAD EACH 'NOTE IS PRESSED' PROPERTY REF AND GET ITS VALUE AS A BOOL, THEN IF ANY OF THEM ARE TRUE, SET GATEOUTJACKISON TO TRUE
  if( JBox_GetBoolean( JBox_LoadMOMProperty( fNote48IsPressedPropertyRef ) ) ) gateOutJackIsOn = true;
  if( JBox_GetBoolean( JBox_LoadMOMProperty( fNote50IsPressedPropertyRef ) ) ) gateOutJackIsOn = true;
  JBox_StoreMOMProperty( fGateOutCVJackValuePropertyRef, JBox_MakeNumber( gateOutJackIsOn ) );
}

wendallsan
Posts: 45
Joined: 10 Dec 2017

21 Jun 2021

rcbuse wrote:
19 Jun 2021

Code: Select all

void cMyPlugin::HandleNotePressed( const TJBox_PropertyDiff iPropertyDiffs[], TJBox_UInt32 iDiffCount ){
  for( TJBox_UInt32 i = 0; i < iDiffCount; ++i ){
    if( iPropertyDiffs[ i ].fPropertyRef.fObject == fNoteStates ){
			TJBox_NoteEvent noteEvent = JBox_AsNoteEvent( iPropertyDiffs[ i ] );
			if( noteEvent.fVelocity > 0.f ){
				JBox_StoreMOMProperty( fNoteOutCVJackValuePropertyRef, JBox_MakeNumber( noteEvent.fNoteNumber / 127.f ) );
				break;
			}
		}
	}
}

Thanks Lectric Panda! Much cleaner to just use NoteEvent than get velocities some other way if I'm just going to turn it into a NoteEvent anyways.

wendallsan
Posts: 45
Joined: 10 Dec 2017

22 Jun 2021

I'm still struggling with a fix for this, so am posting more details and updated code, and following up in detail on orthodox's points and questions, as I suspect a solution is there somewhere.

I'm trying to troubleshoot an issue I'm seeing when feeding CV Note data into the MIDI Out Device from my Rack Extension. When I send a new note that is lower than a previously played note and there are no gaps (rests) between the new note and the previous higher note, the higher note is repeating rather than the lower note being played. This is only occurring on descending notes, and applies to series of notes, so if a sequence of G - F - E - D are played and there no rests in the sequence, the G note will just play 4 times in a row.

Here are examples taken from the Reason Sequencer:

Here's an example of 2 note that work fine. Each note has a brief rest between it and the next note.
Image

Here's an example of a lower note playing before a higher note, with no rest between the two notes. This is also working fine.
Image

Here's an example of a higher note playing before a lower note without a rest between the two notes. This example causes the higher note to play twice rather than playing the lower note.
Image

Here's an example that just plays the highest note 4 times in a row.
Image

If I hook a device between my Rack Extension's CV Out and the MIDI Out's CV In to monitor the value of the Note CV signal going into the Midi Out device from my device, I can SEE the CV signal decrease, so it appears that my device is indeed outputting the note change. If I hook a Player such as Evolution directly to a MIDI Out device, I do get these lower notes to play, but if I connect the Evolution Player to my Rack Extension and then connect my device to the MIDI Out device the lower notes don't play. If I hook a virtual instrument up to my device (such as Subtractor), it plays the lower notes without issues. It just seems to be something going on between my device and the MIDI Out device, which is of course what I'm building my Rack Extension specifically to work with.

To address this, so far I've added a TON of code to track each individual note on and off state and also have started tracking the highest note currently played and the lowest note currently played, and tried setting the CV output to the lowest note, but this has not changed the behavior. I also have code in place that makes sure I'm not acting on "note off" events (where the NoteEvent appears in the diffs list but the Velocity is 0), so any note off events that occur at the same time as a new note on event shouldn't be getting acted upon.

Based on this description, can anyone offer a suggestion on why I'm getting these results?

Here's some code if it is helpful:

Code: Select all

void CMyPlugin::RenderBatch( const TJBox_PropertyDiff iPropertyDiffs[], TJBox_UInt32 iDiffCount ){
	HandleNoteStates( iPropertyDiffs, iDiffCount );
	CheckForNotesPressed();
	HandleGateInCVChanges();
	HandleGateOutJack();
}

void CMyPlugin::HandleNoteStates( const TJBox_PropertyDiff iPropertyDiffs[], TJBox_UInt32 iDiffCount ){
  bool newNoteIsPressed = false;
  int lowNote = 128;
  for( TJBox_UInt32 i = 0; i < iDiffCount; ++i ){
    if( iPropertyDiffs[ i ].fPropertyRef.fObject == fNoteStates ){
	  TJBox_NoteEvent noteEvent = JBox_AsNoteEvent( iPropertyDiffs[ i ] );
      bool noteIsOn = noteEvent.fVelocity > 0.f;
      if( noteIsOn && lowNote > noteEvent.fNoteNumber ){
        lowNote = noteEvent.fNoteNumber;
        newNoteIsPressed = true;
      }
      switch( noteEvent.fNoteNumber ){
        case 0:
          JBox_StoreMOMProperty( fNote0IsPressedPropertyRef, JBox_MakeBoolean( noteEvent.fVelocity > 0.f ) );					
          break;
        case 1:
          JBox_StoreMOMProperty( fNote1IsPressedPropertyRef, JBox_MakeBoolean( noteEvent.fVelocity > 0.f ) );
          break;
        ... // YES, THERE ARE 126 MORE CASE STAMENTENTS
      }
    }
  
  // HERE IS WHERE I SET THE NOTE CV OUT JACK VALUE
  if( newNoteIsPressed ) JBox_StoreMOMProperty( fNoteOutCVJackValuePropertyRef, JBox_MakeNumber( lowNote / 127.f ) );
}
void CMyPlugin::CheckForNotesPressed(){
  bool notesArePressed = false;
  if( JBox_GetBoolean( JBox_LoadMOMProperty( fNote0IsPressedPropertyRef ) ) ) notesArePressed = true;
  if( JBox_GetBoolean( JBox_LoadMOMProperty( fNote1IsPressedPropertyRef ) ) ) notesArePressed = true;
  ... // YES, THERE ARE 126 MORE IF STATEMENTS :-|
  JBox_StoreMOMProperty( fNotesArePressedPropertyRef, JBox_MakeBoolean( notesArePressed ) );
}
void CMyPlugin::HandleGateInCVChanges(){
  if( !JBox_GetBoolean( JBox_LoadMOMProperty( fGateInConnectedCVInputRef ) ) ) return;
  TJBox_Value gateCVValue = JBox_LoadMOMProperty( fGateInValuePropertyRef );
  TJBox_Tag gateCV = clamp( JBox_GetNumber( gateCVValue ) * 127.f, 0, 127 );
  if( gateCV != fLastGateCV ){
    JBox_StoreMOMProperty( fGateInputCVWentHighPropertyRef, JBox_MakeBoolean( gateCV > 0 ) );
    fLastGateCV = gateCV;
  } else JBox_StoreMOMProperty( fGateInputCVWentHighPropertyRef, JBox_MakeBoolean( false ) );
}
void CMyPlugin::HandleGateOutJack(){
  bool gateOutJackIsOn = false;
  if( JBox_GetBoolean( JBox_LoadMOMProperty( fGateInputCVWentHighPropertyRef ) ) ) gateOutJackIsOn = true;
  if( !gateOutJackIsOn && JBox_GetBoolean( JBox_LoadMOMProperty( fNotesArePressedPropertyRef ) ) ) gateOutJackIsOn = true;
  JBox_StoreMOMProperty( fGateOutCVJackValuePropertyRef, JBox_MakeNumber( gateOutJackIsOn ) );
}

User avatar
jam-s
Posts: 3035
Joined: 17 Apr 2015
Location: Aachen, Germany
Contact:

22 Jun 2021

Make sure your gate output briefly goes to 0 before you change the note CV and only then back to the desired velocity when you want to (re-)trigger a new note. Your code might be missing this step depending on the order of incoming notes.

User avatar
rcbuse
RE Developer
Posts: 1175
Joined: 16 Jan 2015
Location: SR388
Contact:

22 Jun 2021

It sounds to me like the MIDI OUT device isn't following the CV rules. I don't have a way to check here. Also, what jam-s said is correct. I'm not seeing in your code where you are setting cv gate.

wendallsan
Posts: 45
Joined: 10 Dec 2017

24 Jun 2021

jam-s wrote:
22 Jun 2021
Make sure your gate output briefly goes to 0 before you change the note CV and only then back to the desired velocity when you want to (re-)trigger a new note. Your code might be missing this step depending on the order of incoming notes.
Thanks for the response! It sounds like this might need to occur over a couple RenderBatch calls to happen, is this correct? My understanding of RenderBatch is that values don't actually change within one call but update at the end of RenderBatch. So this might need to set the Gate CV value to 0.0 in one RenderBatch, change the Note CV value in the next RenderBatch, and then set the Gate CV back to the desired velocity on the RenderBatch call after that. Is this correct, or am I over-complicating things?
rcbuse wrote:
22 Jun 2021
It sounds to me like the MIDI OUT device isn't following the CV rules. I don't have a way to check here. Also, what jam-s said is correct. I'm not seeing in your code where you are setting cv gate.


That's what I was thinking as well, but I'm still more likely to blame my own code rather than that provided by the pros :D

I'm not really using velocity data (per say) for my Gate CV since I'm controlling analog equipment with my extension. It's just getting set to 0.0 or 1.0 depending on whether I want it on or off. This is done across a couple methods called in RenderBatch. In HandleGateInCVChanges we set an InputCVWentHigh motherboard property depending on whether the Gate In CV went high during the RenderBatch. In another method named HandleGateOutJack, this bool is checked along with another bool that gets set if any notes are pressed and if either are true, we set our Gate CV to 1.0, otherwise we set it to 0.0. Let me know if something about this process sounds fishy.

User avatar
orthodox
RE Developer
Posts: 2286
Joined: 22 Jan 2015
Location: 55°09'24.5"N 37°27'41.4"E

24 Jun 2021

wendallsan wrote:
24 Jun 2021
It sounds like this might need to occur over a couple RenderBatch calls to happen, is this correct? My understanding of RenderBatch is that values don't actually change within one call but update at the end of RenderBatch. So this might need to set the Gate CV value to 0.0 in one RenderBatch, change the Note CV value in the next RenderBatch, and then set the Gate CV back to the desired velocity on the RenderBatch call after that. Is this correct, or am I over-complicating things?
See JBox_StoreMOMProperty():
The updated value ... will not be propagated to the Motherboard until JBox_Export_RenderRealtime returns.

wendallsan
Posts: 45
Joined: 10 Dec 2017

29 Jun 2021

Thanks again! I'll work on changing my approach so the gate off, note change, and gate on events occur on subsequent RenderBatch calls and see if that fixes the problem I'm seeing.

wendallsan
Posts: 45
Joined: 10 Dec 2017

30 Jun 2021

This approach worked like a charm, and new notes are now being properly output after adding some new logic that stores whether a new note was received in the MOM as a boolean. In the next RenderBatch, I check to see if the newNote value is true, and if so check to see if the gate out is currently high. If it is, I set the Gate Out CV to 0.0 and wait fore another RenderBatch. In the next RenderBatch, the newNote is still true and the gate is now low, so I can check for this state and then change the note CV to the new note value, turn the gate back on, and set newNote to false so it is no longer true for future RenderBatch calls.

Thanks very much for the assistance in figuring out my weird "notes aren't changing" dilemma! It makes much more sense after understanding that MOM properties don't update until next RenderBatch and realizing I need to flip the gate out off and on for every note change for it to work well with the MIDI Out device.

Post Reply
  • Information
  • Who is online

    Users browsing this forum: No registered users and 2 guests