Midi plugin in Cubase outputting multiple values per step

I have an odd issue with Cubase hosting a midi generating plugin as VST3. This works fine in Ableton Live and in Studio One. I have a feeling it is in int numSamples = buffer.getNumSamples(); but it did work when I was using the deprecated getCurrentPosition(). I updated the code to use getPosition() and it now puts out exactly 11 notes per step. Which is weird. I’m still learning and trying to debug but I am on an M1 and can’t seem to get the values passed back to the debugger in xcode.

The only things that changed in the code is as follows:

    if (playHead != nullptr)
    {
        auto* audioPlayHead = playHead.load();
        if (audioPlayHead != nullptr)
        {
            juce::AudioPlayHead::CurrentPositionInfo posInfo;
            if (audioPlayHead->getCurrentPosition(posInfo))
            {
                currentTempo = posInfo.bpm;
                transportIsPlaying = posInfo.isPlaying;
                ppqPosition = posInfo.ppqPosition;
                if (ppqPosition < 0.01) // if the playhead has returned to the beginning
                {
                    currentStep = 0;
                    currentNote = -1;
                }
            }
        }
    }

And the new code:

void MidiSequencerAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
    buffer.clear();
    juce::MidiBuffer outputMidi;
    int numSamples = buffer.getNumSamples();

    double currentTempo = 120.0;
    bool transportIsPlaying = false;
    double ppqPosition = 0;

    if (playHead != nullptr)
    {
        auto* audioPlayHead = playHead.load();
        if (audioPlayHead != nullptr)
        {
            auto positionInfo = audioPlayHead->getPosition();
            if (positionInfo)  // Check if positionInfo is available
            {
                // Extract bpm if available
                if (auto bpm = positionInfo->getBpm())
                {
                    currentTempo = *bpm;
                }

                transportIsPlaying = positionInfo->getIsPlaying();
                
                // Extract ppqPosition if available
                if (auto ppqPos = positionInfo->getPpqPosition())
                {
                    ppqPosition = *ppqPos;
                }

                if (ppqPosition < 0.01) // if the playhead has returned to the beginning
                {
                    currentStep = 0;
                    currentNote = -1;
                }
            }
        }
    }

    double stepDuration = 60.0 / (currentTempo * stepsPerBeat);
    samplesPerStep = static_cast<int>(getSampleRate() * stepDuration);

    for (int sample = 0; sample < numSamples; ++sample)
    {
        if (transportIsPlaying && sample % samplesPerStep == 0)
        {
            // If there's a note playing, send a note off event
            if (currentNote != -1)
            {
                outputMidi.addEvent(juce::MidiMessage::noteOff(1, currentNote), sample);
                currentNote = -1;
            }

            // If the step is triggered, send a note on event
            if (stepTriggers[currentStep])
            {
                currentNote = stepNotes[currentStep];
                outputMidi.addEvent(juce::MidiMessage::noteOn(1, currentNote, (juce::uint8)127), sample);
                
                
                if (velocityOutputEnabled) {
                    auto velocity = getStepVelocity(currentStep);
                    outputMidi.addEvent(juce::MidiMessage::noteOn(1, currentNote, (juce::uint8)velocity), sample);
                }

                if (cc1OutputEnabled) {
                    auto cc1Value = getStepCC1(currentStep);
                    outputMidi.addEvent(juce::MidiMessage::controllerEvent(1, cc1Channel, cc1Value), sample);                }

                if (cc2OutputEnabled) {
                    auto cc2Value = getStepCC2(currentStep);
                    outputMidi.addEvent(juce::MidiMessage::controllerEvent(1, cc2Channel, cc2Value), sample);                }
                
            }

            sendChangeMessage();
        }
    }

    midiMessages.swapWith(outputMidi);
}

Tested versions are Cubase 12 and 13.

a better way of checking if ppq has returned to 0 is always keeping track of the last ppq and then checking if the abs difference exceeds .5

I changed that thanks!

I have “fixed” the issue by removing the for loop, but for some reason I am getting a weird delay in Ableton when triggering notes and in Cubase the note length isn’t consistent. Sometimes it is too short, others too long, but it always plays at the right time in Cubase.

void MidiSequencerAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
    buffer.clear();
    juce::MidiBuffer outputMidi;
    double currentTempo = 120;
    double ppqPosition = 0;

    if (playHead != nullptr)
    {
        auto positionInfo = *getPlayHead()->getPosition();

        if (auto bpm = positionInfo.getBpm())
        {
            currentTempo = *bpm;
        }
        bool transportIsPlaying = positionInfo.getIsPlaying();
        if (auto ppqPos = positionInfo.getPpqPosition())
        {
            ppqPosition = *ppqPos;
        }
        
        if (std::abs(ppqPosition - lastPpqPosition) > 0.5)
        {
            currentStep = 0;
            currentNote = -1;
        }

        lastPpqPosition = ppqPosition;

        if (transportIsPlaying)
        {
            int newPlayheadStep = static_cast<int>(ppqPosition * stepsPerBeat) % numSteps;
            
            if (newPlayheadStep != previousPlayheadStep)
            {
                previousPlayheadStep = newPlayheadStep;

//switch logic goes here - REDACTED.

                double samplesPerBeat = (getSampleRate() * 60) / currentTempo;
                int noteOnSample = static_cast<int>(std::round(ppqPosition * samplesPerBeat));
                int noteOffSample = noteOnSample + static_cast<int>(std::round(samplesPerBeat / stepsPerBeat));


                // Trigger the note based on the current step
                if (stepTriggers[currentStep])
                {
                    currentNote = stepNotes[currentStep];

                    // Note On event with velocity
                    auto velocity = (velocityOutputEnabled) ? getStepVelocity(currentStep) : 127;
                    outputMidi.addEvent(juce::MidiMessage::noteOn(1, currentNote, (juce::uint8)velocity), noteOnSample);

                    // Additional MIDI events (CC values)
                    if (cc1OutputEnabled) {
                        auto cc1Value = getStepCC1(currentStep);
                        outputMidi.addEvent(juce::MidiMessage::controllerEvent(1, cc1Channel, cc1Value), noteOnSample);
                    }

                    if (cc2OutputEnabled) {
                        auto cc2Value = getStepCC2(currentStep);
                        outputMidi.addEvent(juce::MidiMessage::controllerEvent(1, cc2Channel, cc2Value), noteOnSample);
                    }
                }
                else
                {
                        outputMidi.addEvent(juce::MidiMessage::noteOff(1, currentNote), noteOffSample);
                }
                sendChangeMessage();
            }
        }
    }

    // Swap with the output MIDI buffer to ensure the events are sent out
    midiMessages.swapWith(outputMidi);
}

I think it is either in the midi buffer or the ppqposition and need to revisit that, but I also have removed the other two CCoutput items sending data to the buffer at the same sample time and no difference.

I feel like there has to be a better way to build out a midi sequencer here.

Edit: checked in studio one as well and the timing is late too.

Couple of things that jump out

double samplesPerBeat = (getSampleRate() * 60) / currentTempo;

getSampleRate() * 60 implicitly casts to an int. Make sure to use 60.0 or 60. when multiplying by a floating point literal (and enable warnings).

  double currentTempo = 120;
  // ... 
        if (auto bpm = positionInfo.getBpm())
        {
            currentTempo = *bpm;
        }

What if bpm is null?

I feel like there has to be a better way to build out a midi sequencer here.

VST3 doesn’t really support MIDI output from plugins (or input, for that matter). JUCE is translating MIDI to VST3’s note event data type.

Thanks, I changed those. Sadly didn’t change much, but I did change the following:

            double samplesPerBeat = (getSampleRate() * 60.0) / currentTempo;
            int samplesPerStep = static_cast<int>(samplesPerBeat / stepsPerBeat);
            int noteOnSample = static_cast<int>(ppqPosition * samplesPerStep);
            int noteOffSample = noteOnSample + static_cast<int>(samplesPerBeat / stepsPerBeat);

Where I managed to get the noteOnSample rates from

0
5632
11136
16640

to:

0
5535
11038
16542

which is closer to 5512 from what the samples at 44.1k should be at 16th notes. Looks like I have some math to do.

I’m afraid you confused some things here.

You might have confused it with accidental integer divisions, where people would expect a floating point.

The multiplication indeed promotes one operand to match the other. But the int 60 will be propagated to match the double returned from getSampleRate().

It would have been a problem if you did it in a different order:

double samplesPerBeat = getSampleRate() * (60 / currentTempo);

If currentTempo was an int, this would indeed first do an integer division with data loss and multiply with the double afterwards.

But the advice you give will help: enable warnings!

This is sufficiently easy to mix up that I always make sure my literals match the types I’m using! :smile:

That’s a good habit anyway :slight_smile: