Show Midi Data As It Is Recorded

Can you check if the listener is actually being called?
In MidiInputDevice::timerCallback does this line get called:

midiKeyChangeDispatcher->listeners.call (&MidiKeyChangeDispatcher::Listener::midiKeyStateChanged, t, keys, vels);

And if not, does this function get called:
MidiInputDevice::sendNoteOnToMidiKeyListeners?
That should get called from:

PhysicalMidiInputDevice::handleIncomingMidiMessage

The line;

MidiInputDevice::sendNoteOnToMidiKeyListeners

does get called.

However, the line;

midiKeyChangeDispatcher->listeners.call (&MidiKeyChangeDispatcher::Listener::midiKeyStateChanged, t, keys, vels);

is not called. And it appears to be because the function getDestinationTracks() in MidiInputDevice::timerCallback is empty!

Hmm, it’s probably because you haven’t implemented UIBehaviour::getLastFocusedEdit()

juce::Array<AudioTrack*> MidiInputDevice::getDestinationTracks()
{
    if (auto edit = engine.getUIBehaviour().getLastFocusedEdit())
        if (auto in = edit->getCurrentInstanceForInputDevice (this))
            return in->getTargetTracks();

    return {};
}

You could do that but it highlights perhaps why this isn’t the most appropriate method for drawing thumbnails.

The main use for this method in Waveform is step-entry, when a keyboard key is pressed, we insert that note in to the piano roll and move the insert point forwards. Obviously you only want that to happen in the currently focused Edit hence the above behaviour.

It doesn’t lend itself to MIDI thumbnailing very well though.

Is it enough for UIBehaviour::getLastFocusedEdit() to return the current edit?

Or what would that function look like? I notice it returns an int.

Oops, I think VisualStudio was being overly helpful. UIBehaviour::getLastFocusedEdit() returns tracktion_engine::Edit*.

Yes, that should get the listener callback working.

Just to let you know, this is working as expected! In fact, it is working well enough that you may want to consider this technique for adding the feature yourself.

First you implement UIBehaviour::getLastFocusedEdit() to return the current edit.

Then, below is pseudo code that gives the general idea of what I am using;

class MidiRecordingClip : public Component, public te::MidiInputDevice::MidiKeyChangeDispatcher::Listener
{
public:
	MidiRecordingClip(te::Track::Ptr t, EditViewState& evs) :
		audioTrack(dynamic_cast<te::AudioTrack*> (t.get())),
		editViewState(evs)
	{
		setAlwaysOnTop(true);
		midiKeyChangeDispatcher->listeners.add(this);
	}

	~MidiRecordingClip()
	{
		midiKeyChangeDispatcher->listeners.remove(this);
	}

	void resized() override
	{
		width = getWidth();
		height = getHeight();
		
		// place here any needed calculations here
		// ...
	}

	void paint(Graphics& g) override
	{
		// draw the notes
		for (auto note : midiList.getNotes())
		{
			g.setColour(Colours::white.withAlpha(note->getVelocity() / 127.0f));
			g.fillRect(getNoteBounds(note));
		}
	}

private:
	void midiKeyStateChanged(te::AudioTrack* audTrk, const juce::Array<int>& notes, const juce::Array<int>& vels) override
	{
		if (audTrk == audioTrack)
		{
			midiList.clear(nullptr);
			const auto transport{ &audioTrack->edit.getTransport() };
			const auto leftEdge{ editViewState.timeToX(transport->getCurrentPosition(), width) };

			for (auto i = 0; i <  notes.size(); i++)
				midiList.addNote(notes[i], editViewState.timeToBeats(transport->getCurrentPosition()), 1.0, vels[i], 0, nullptr);
			repaint( leftEdge, 0, (width - leftEdge), height);
		}
	}

	inline Rectangle<int> getNoteBounds(const te::MidiNote* note)
	{
		// calculate note bounds here
		// ...
		return { noteBounds };
	}

	te::AudioTrack* audioTrack;
	EditViewState& editViewState;
	SharedResourcePointer<te::MidiInputDevice::MidiKeyChangeDispatcher> midiKeyChangeDispatcher;
	te::MidiList midiList;

	int width{ getWidth() };
	int height{ getHeight() };
};

The notes appear on-screen with a slight visual delay (as expected) but in surprisingly good agreement with what is being played. And time-stamping the notes with the transport position works quite well, with no need to compensate for any internal latency. The only thing missing is having note length.

My use case is to have a visual guide that midi is being recorded properly, so as to reassure the performer. This is more than good enough!

Thank you for all your help!

3 Likes

@bwall can you explain how to go about implementing the getLastFocusedEdit method? I have a subclass of UIBehavior, do I need to create a member variable for the edit inside this subclass and set this somehow? Or is there some other way to access the edit from within UIBehavior?

UIBehavior has no built-in way to get the edit pointer, so you must provide it yourself by whatever mechanism makes sense in your code. So, you create the pointer to your edit and return it from getLastFocusedEdit.

alright cool I just added an edit member and got things working tthank you!