MidiKeyboardComponent: different overlay colours for different MIDI channels?

I’m trying to draw different key colours on a MidiKeyboardComponent depending on which channel the incoming notes are.

What I have so far (simplified):

A CustomMidiKeyboardComponent, derived from MidiKeybOardComponent with overriden drawBlackNote(), drawWhiteNote() and handleNoteOn() methods.

void CustomMidiKeyboardComponent::handleNoteOn(MidiKeyboardState* state, int midiChannel, int midiNoteNumber, float velocity)
{
	curMidiChannel = midiChannel;
	MidiKeyboardComponent::handleNoteOn(state, midiChannel, midiNoteNumber, velocity);
	
}

void CustomMidiKeyboardComponent::drawWhiteNote(int midiNoteNumber, Graphics & g, int x, int y, int w, int h, bool isDown, bool isOver, const Colour & lineColour, const Colour & textColour)
{
	Colour c(Colours::transparentWhite);
	
	if (isDown) {
		if (curMidiChannel == 1) {
			c = c.overlaidWith(chan1Colour);
		}
		if (curMidiChannel == 2) {
			c = c.overlaidWith(chan2Colour);
		}
	}

       // ...
}
void CustomMidiKeyboardComponent::drawBlackNote(int midiNoteNumber, Graphics & g, int x, int y, int w, int h, bool isDown, bool isOver, const Colour & noteFillColour)
{
	 
		Colour c(noteFillColour);

		if (isDown) {
			if (curMidiChannel == 1) {
				c = c.overlaidWith(chan1Colour);
			}
			if (curMidiChannel == 2) {
				c = c.overlaidWith(chan2Colour);
			}
		} 

                // ...
}

This basically kind of works, but it breaks (= sometimes drawn notes have the wrong colour) if there are more than just a few notes incoming on each channel.

I think I kinda get why this happens (setting the channel and notes drawing can overlap ?) but what would be a better (correct) approach to do this ?

Hmm, from the sound of it I think it’s basically a threading issue whereby incoming events are changing the current channel during the draw function.

I’m not sure I understand exactly how you intend it to look, but if you always want all keys to be the same colour, perhaps you could do something as simple as
curDisplayChannel = curMidiChannel;
(i.e. keep a local copy for display purposes) and update it once every graphics update?

Hope that helps. :slight_smile:

Thanks for your reply.
This is what it does currently (draft version):

gif

Below C3 there are only notes incoming on channel 1 (and above C3 only notes on channel 2), so as you can see, some of the notes are drawn in the wrong colour initially, but as soon as I point with the mouse on them to trigger a redraw they are drawn correctly.

Do you think your suggestion could help fix this (tbh not sure currently how I should implement it) or any other ideas how to solve this ?

You’re welcome.

Ah okay, I think I understand this a little more. We’re losing a lot of detail in only updating the colour at certain (unreliable) times from the MIDI and then having this single colour apply to everything.

Perhaps another way of doing this would be to have an array (or similar) of 128 slots, one per key, in which you save/clear the current channel/s for each key, then use these values directly in the display? There shouldn’t be any major problems with sync in that case.

Old thread, but exactly relevant to what I’m doing…
I’ve reproduced the above code, making subtle changes to suit definitions with the latest juce.
I’m getting identical behaviour: the note colour follows the channel colours but when another channel note overlaps, all existing played notes change to the new notes channel colour.

I’m looking at the last post to this thread.
I would have a data structure to represent all channels on each key. Probably just a up/down flag for each channel 1 to 16, in a 128 sized array. When a change to the array happens the affected note would blend the colours of active channels (on that note) and paint the key.

So the data structure would be updated in CustomMidiKeyboardComponent::handleNoteOn(…)
Where would the

be coded?
Thanks for any hints.

I’ve coded an implementation that works nicely the way I envisioned. Midi channels with different colours are rendered on the keys and when notes occur from different channels on the same key, the colours are blended correctly.
The problem is very high CPU while notes are down. If I test for note/channel change in CustomMidiKeyboard::drawWhiteNote (see in code commented below) the CPU goes to negligible but notes flash on the keys then immediately go back to white, and while a key is down, the same key on a different channel is not seen at all.
I cloned juce_MidiKeyboardComponent to dug_MidiKeyboardComponent simply to expose the handleNoteOn(…) and handleNoteOff(…) as public: members. I suspect this is where I have gone wrong (presumably they’re private: for a reason) and there is a different way, I’m not sure, but I would really appreciate suggestions.

class CustomMidiKeyboard :public dug_MidiKeyboardComponent
{  public:
	CustomMidiKeyboard(MidiKeyboardState& state, Orientation orientation) :dug_MidiKeyboardComponent(state, orientation) {
		ChannelColours[0] = Colours::red;
		ChannelColours[1] = Colours::green;
		ChannelColours[2] = Colours::brown;
		ChannelColours[3] = Colours::grey;
		ChannelColours[4] = Colours::yellow;
		ChannelColours[5] = Colours::pink;
		ChannelColours[6] = Colours::cyan;
		ChannelColours[7] = Colours::darkblue;
	
		for (int i = 0; i < 128; i++) KeyChannel[i] = 0;
	};

	void handleNoteOn(MidiKeyboardState* state, int midiChannel, int midiNoteNumber, float velocity)
	{
		SetKeyDown(midiNoteNumber, midiChannel -1 );
		dug_MidiKeyboardComponent::handleNoteOn(state, midiChannel, midiNoteNumber, velocity);
	}
	void handleNoteOff(MidiKeyboardState* state, int midiChannel, int midiNoteNumber, float velocity)
	{
		SetKeyUp(midiNoteNumber, midiChannel - 1);
		dug_MidiKeyboardComponent::handleNoteOff(state, midiChannel, midiNoteNumber, velocity);
	}

	void drawWhiteNote(int midiNoteNumber, Graphics& g, Rectangle<float> area,
		bool isDown, bool isOver, Colour lineColour, Colour textColour)override
	{
		if (IsKeyAnyChannelChange(midiNoteNumber)) //***with this test CPU problem is fixed
                                                  //***but notes get erased very quickly
		{
			Colour c(Colours::transparentWhite);
			c = c.overlaidWith(GetBlendedColour(midiNoteNumber));
			setColour(keyDownOverlayColourId, c);
			dug_MidiKeyboardComponent::drawWhiteNote(midiNoteNumber, g, area, isDown, isOver, c, textColour);

		}
	}

	//void drawBlackNote(..) omitted for brevity
	

private:

	uint16 KeyChannel[128];				//key state, channels in bit positions
	uint16 prev_KeyChannel[128];		//key state, channels in bit positions
	Colour ChannelColours[16];			//each channel its own colour
	
	void SetKeyDown(int k,int ch) {		//set flag
		KeyChannel[k] |= 1 << ch;
	}
	void SetKeyUp(int k, int ch) {		//clear flag
		KeyChannel[k] &= ~(1 << ch);
	}

	bool IsKeyDown(int k, int ch) {
		return(KeyChannel[k] & (1 << ch));
	}

	bool IsKeyAnyChannelChange(int k) {				//test and update
		if (KeyChannel[k] == prev_KeyChannel[k])
			return false;
		prev_KeyChannel[k] = KeyChannel[k];
		return true;
	}

	duglog log1,log2,log3;
	Colour GetBlendedColour(int key) {
		Colour clr;
		int num_clrs = 0;	//number of colours to mix

		for (int ch = 0; ch < 16; ++ch)
			if (IsKeyDown(key, ch))
				if (num_clrs==0) {
					clr = ChannelColours[ch]; //start with "full colour"
					num_clrs = 1;
				}
				else {
					num_clrs++;
					Colour clr_to_add = ChannelColours[ch];
					float prop = 1.0f / (float)num_clrs;		//colour blend proportion
					clr = clr.interpolatedWith(clr_to_add, prop);  //progressively blend other channels
				}
		return clr;
	}
};

I’ve simplified the code considerably. I’m using MidiKeyboardState to track the note on/off across channels (the way its intended). This means I don’t need to access private members, so no more custom juce_MidiKeyboardComponent required.
CPU activity is unreasonably high when any notes are down. There is constant redrawing going on when there obviously shouldn’t be. I can see a whole stack of calls repainting the keys even though no new events are coming in.
Thanks for any help with this.


class CustomMidiKeyboard :public MidiKeyboardComponent
{public:

	void drawWhiteNote(int midiNoteNumber, Graphics& g, Rectangle<float> area,
		bool isDown, bool isOver, Colour lineColour, Colour textColour)override
	{
		bool change = IsKeyAnyChannelChange(midiNoteNumber);	//acquire channel states
		if (isDown)
		{
			Colour c(Colours::transparentWhite);
			c = c.overlaidWith(GetBlendedColour(midiNoteNumber));
			setColour(keyDownOverlayColourId, c);		
		}
		MidiKeyboardComponent::drawWhiteNote(midiNoteNumber, g, area, isDown, isOver, lineColour,  textColour);
	}

	void drawBlackNote(int midiNoteNumber, Graphics& g, Rectangle<float> area,
		bool isDown, bool isOver, Colour noteFillColour) override
	{
		Colour c(noteFillColour);
		bool change = IsKeyAnyChannelChange(midiNoteNumber); //acquire channel states
		c = c.overlaidWith(GetBlendedColour(midiNoteNumber));
		setColour(keyDownOverlayColourId, c);
		
		MidiKeyboardComponent::drawBlackNote(midiNoteNumber, g, area, isDown, isOver, c);
	}
CustomMidiKeyboard(MidiKeyboardState& state, Orientation orientation) :MidiKeyboardComponent(state, orientation) {
		state_ptr = &state;
		ChannelColours[0] = Colours::red;
		ChannelColours[1] = Colours::green;
		ChannelColours[2] = Colours::brown;
		ChannelColours[3] = Colours::grey;
		ChannelColours[4] = Colours::yellow;
		ChannelColours[5] = Colours::pink;
		ChannelColours[6] = Colours::cyan;
		ChannelColours[7] = Colours::darkblue;
	
		for (int i = 0; i < 128; i++) KeyChannel[i] = prev_KeyChannel[i] = 0;
		setColour(keySeparatorLineColourId, Colours::grey);
		setColour(textLabelColourId, Colours::black);

	};
	
private:
	MidiKeyboardState* state_ptr;
	uint16 KeyChannel[128];				//key state, channels in bit positions
	uint16 prev_KeyChannel[128];		//key state, channels in bit positions
	Colour ChannelColours[16];			//each channel its own colour
	
	void SetKeyDown(int k,int ch) {		//set flag
		KeyChannel[k] |= 1 << ch;
	}
	void SetKeyUp(int k, int ch) {		//clear flag
		KeyChannel[k] &= ~(1 << ch);
	}

	bool IsKeyDown(int k, int ch) {
		return(KeyChannel[k] & (1 << ch));
	}

	bool IsKeyAnyChannelChange(int k) {				//test and update
		for (int x = 1; x <= 16; x++) {
			if (state_ptr->isNoteOn(x, k)) 
				SetKeyDown(k,x-1);					//update from MidiKeyboardState 
			else
				SetKeyUp(k, x - 1);
			}
	if (KeyChannel[k]!= prev_KeyChannel[k]){
		prev_KeyChannel[k] = KeyChannel[k];
		return true;
	}
	else
		return false;
	}

	Colour GetBlendedColour(int key) {
		Colour clr(Colours::transparentWhite);
		int num_clrs = 0;	//number of colours to mix

		for (int ch = 0; ch < 16; ++ch)
			if (IsKeyDown(key, ch))
				if (num_clrs==0) {
					clr = ChannelColours[ch]; //start with "full colour"
					num_clrs = 1;
				}
				else {
					num_clrs++;
					Colour clr_to_add = ChannelColours[ch];
					float prop = 1.0f / (float)num_clrs;		//colour blend proportion

					clr = clr.interpolatedWith(clr_to_add, prop);  //progressively blend other channels
				}
		return clr;
	}
};

Every time you call “setColour” the whole keyboard component gets repainted.

You have to inherit from the keyboard component and overwrite the drawWhiteKey and drawBlackKey functions. There you then draw with your desired colors.

Thanks for the tip, very helpful.
What I’ve noticed is that drawWhiteKey and drawBlackKey are private methods in the base class. However they do nothing more than call drawWhiteNote and drawBlackNote, respectively.
These I have overridden as coded above except I no longer call setColour() instead I pass the calculated colour to drawWhiteNote code (pasted from the base class).
here it is:

void drawWhiteNote(int midiNoteNumber, Graphics& g, Rectangle<float> area,
		bool isDown, bool isOver, Colour lineColour, Colour textColour)override
	{
		bool change = IsKeyAnyChannelChange(midiNoteNumber);	//acquire channel states
		if(change)
		{
			Colour c(Colours::transparentWhite);
			c = c.overlaidWith(GetBlendedColour(midiNoteNumber));  //calculate colour
// the rest is MidiKeyboardComponent::drawWhiteNote() from 2nd line...
			if (isOver)  c = c.overlaidWith(findColour(mouseOverKeyOverlayColourId));

			g.setColour(c);
			g.fillRect(area);

			const auto currentOrientation = getOrientation();

			auto text = getWhiteNoteText(midiNoteNumber);

			if (text.isNotEmpty())
			{
				auto fontHeight = jmin(12.0f, getKeyWidth() * 0.9f);

				g.setColour(textColour);
				g.setFont(Font(fontHeight).withHorizontalScale(0.8f));

				switch (currentOrientation)
				{
				case horizontalKeyboard:            g.drawText(text, area.withTrimmedLeft(1.0f).withTrimmedBottom(2.0f), Justification::centredBottom, false); break;
				case verticalKeyboardFacingLeft:    g.drawText(text, area.reduced(2.0f), Justification::centredLeft, false); break;
				case verticalKeyboardFacingRight:   g.drawText(text, area.reduced(2.0f), Justification::centredRight, false); break;
				default: break;
				}
			}

			if (!lineColour.isTransparent())
			{
				g.setColour(lineColour);

				switch (currentOrientation)
				{
				case horizontalKeyboard:            g.fillRect(area.withWidth(1.0f)); break;
				case verticalKeyboardFacingLeft:    g.fillRect(area.withHeight(1.0f)); break;
				case verticalKeyboardFacingRight:   g.fillRect(area.removeFromBottom(1.0f)); break;
				default: break;
				}

				if (midiNoteNumber == getRangeEnd())
				{
					switch (currentOrientation)
					{
					case horizontalKeyboard:            g.fillRect(area.expanded(1.0f, 0).removeFromRight(1.0f)); break;
					case verticalKeyboardFacingLeft:    g.fillRect(area.expanded(0, 1.0f).removeFromBottom(1.0f)); break;
					case verticalKeyboardFacingRight:   g.fillRect(area.expanded(0, 1.0f).removeFromTop(1.0f)); break;
					default: break;
					}
				}
			}
		}
	}

The CPU problem is fixed. Colours on channels are drawn ok except…
I’m finding that MidiKeyboardComponent::drawWhiteKey(…) is not being called when say C3 is already down on Ch1, and then C3 is pressed on Ch2.
If the two keydown events are passed to keyboardState.processNextMidiEvent(msg) in the same call then the keyboard displays the combined colour.
It’s not clear to me looking at the call stack where the call to MidiKeyboardComponent::drawWhiteKey(…) is made from. It seems that whatever does, is only single (omni) channel.