Midi issues when running VST in Waveform

I’m working on a VST3 midi plugin that generates midi data on the fly.

The plugin is able to successfully generate midi in Ableton Live and FLStudio, but in Waveform, I’m having a weird issue. Waveform seems to catch the first midi output at time zero, but nothing after that, so the initial midi on message just continues indefinitely.

I tried to create the simplest example, in hopes that someone can see what I might be doing wrong here.

The code below should create a note on message once per second, each followed by a note off message 1/2 second later.

In Live and FLStudio, I can see/hear the midi and I can see the note messages being output to the log.

In Waveform, only the note on message at “position” 0 is ever realized in the DAW. If hit play from any other transport position, I see/hear no messages at all.

I’m certain I’m missing something simple here, but I’ve been trying for days to figure out what it might be. Any insight is appreciated.

PluginProcessor.h

#include <JuceHeader.h>

class MyVSTAudioProcessor : public juce::AudioProcessor {
public:
    //==============================================================================
    //BOILER PLATE CODE OMITTED
    //==============================================================================

private:
	int bufferSize;
	double sampleRate;
	double halfSampleRate;

	juce::MidiMessage onMsg{ juce::MidiMessage::noteOn(1, 60, (juce::uint8)90) };
	juce::MidiMessage offMsg{ juce::MidiMessage::noteOff(1, 60, (juce::uint8)90) };

	juce::AudioPlayHead* transportHead;
	juce::Optional<juce::AudioPlayHead::PositionInfo> transportInfo{ juce::AudioPlayHead::PositionInfo() };

	bool isPlaying();
	
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MyVSTAudioProcessor);
};

PluginProcessor.cpp

#include "PluginProcessor.h"
#include "PluginEditor.h"

//==============================================================================
//BOILER PLATE CODE OMITTED
//==============================================================================

bool MyVSTAudioProcessor::isPlaying() {
	return transportInfo->getIsPlaying();
}

void MyVSTAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) {
	this->bufferSize = samplesPerBlock;
	this->sampleRate = sampleRate;
	this->halfSampleRate = sampleRate / 2;
	this->transportHead = getPlayHead();
}

void MyVSTAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) {
	// update transport
	transportInfo = transportHead->getPosition();
	// prepare to write to the midi buffer
	midiMessages.clear();

	if (isPlaying()) {
		// set the sample position to the current transport position
		int samplePosition = (int)transportInfo->getTimeInSamples().orFallback(0);

		int start = samplePosition; // start of the buffer
		int end = samplePosition + bufferSize - 1; // end of the buffer

		if (samplePosition % (int)sampleRate < bufferSize) {
			// once per second, send a note on message
			midiMessages.addEvent(onMsg, sampleRate * (int)(samplePosition / sampleRate));
		} else if (samplePosition % (int)(halfSampleRate) < bufferSize) {
			// every 1/2 second after a note on message, send a note off message
			midiMessages.addEvent(offMsg, halfSampleRate * (int)(samplePosition / halfSampleRate));
		}

		// log the midi messages
		for (const juce::MidiMessageMetadata metadata : midiMessages) {
			if (metadata.numBytes == 3) {
				juce::Logger::writeToLog(juce::String(metadata.samplePosition) + ": " + metadata.getMessage().getDescription());
			}
		}
	}
}

For reference, I created a VST3 project with the following relevant settings:

Plugin Formats: VST3
Plugin Characteristics: Plugin MIDI Input, Plugin MIDI Output
Plugin VST3 Category: Fx, Instrument, Generator

I see a few potential issues:

  • The time provided to MidiBuffer::addEvent should a time in samples relative to the start of the current audio block. It looks like you’re currently passing a global time.
  • The buffer size may change between callbacks. The samplesPerBlock passed to prepareToPlay should be treated as a guideline, but the actual block sizes may be smaller (and in rare cases, larger). To find the actual number of samples in a particular block, check the length of the AudioBuffer<float>& parameter.
  • The host is not required to provide position information, so the result of transportHead->getPosition() may be nullopt. You should guard against this case.
  • The result of getPlayHead() shouldn’t be stored in prepareToPlay. This function may only be called in processBlock.
2 Likes

Thank you kindly,

The time provided to MidiBuffer::addEvent should a time in samples relative to the start of the current audio block. It looks like you’re currently passing a global time.

Changing the time in samples to be relative to the start of the block has corrected the issue I was having in Waveform. I’m new at this, and it wasn’t clear to me from the documentation exactly what the “sample number” was supposed to represent (i.e. local to the block or global). In my initial test in Live, it didn’t seem to matter, so I assumed it was supposed to be global.

Your help is greatly appreciated, thanks for taking a look. I will also be taking steps to address the other issues you pointed out. There is so much to learn.