QSPI saves interrupting MIDI

I’ve been having some issues with late and missing MIDI events and tracked it down to slow QSPI storage saves.

As I understand it both the MIDI handling and the PersistantStorage saves should be in the main loop, but while the storage is saving it’s blocking the MIDI listen so that events are either missed or delayed.

I created a simple reproduction that saves every two seconds and you can really clearly hear the long or missed notes (all the notes should have been about the same length and rhythm):

Am I doing something wrong? How are others handling MIDI so they don’t have this issue?

Here’s my reproduction, using a Patch Submodule:

#include "daisy_patch_sm.h"
#include "daisysp.h"

using namespace daisy;
using namespace patch_sm;
using namespace daisysp;

struct Settings
{
    uint32_t something;

    bool operator==(const Settings &rhs) { return something == rhs.something; }
    bool operator!=(const Settings &rhs) { return !operator==(rhs); }
};

DaisyPatchSM hw;
Adsr env;
MidiUartHandler midi;
Oscillator osc;
PersistentStorage<Settings> storage(hw.qspi);
uint32_t last_save;
bool gate;

void AudioCallback(AudioHandle::InputBuffer in,
                   AudioHandle::OutputBuffer out,
                   size_t size)
{
    float sig;
    for (size_t i = 0; i < size; i++)
    {
        sig = osc.Process() * env.Process(gate);
        OUT_L[i] = sig;
        OUT_R[i] = sig;
    }
}

// Very simple MIDI event handling.
void HandleMidiMessage(MidiEvent m)
{
    switch (m.type)
    {
    case NoteOn:
    {
        NoteOnEvent p = m.AsNoteOn();
        if (m.data[1] != 0)
        {
            p = m.AsNoteOn();
            osc.SetFreq(mtof(p.note));
            osc.SetAmp((p.velocity / 127.0f));
            gate = true;
        }
    }
    break;
    case NoteOff:
    {
        gate = false;
    }
    break;
    default:
        break;
    }
}

int main(void)
{
    float samplerate;
    hw.Init();
    samplerate = hw.AudioSampleRate();
    storage.Init({0});

    // Synthesis
    osc.Init(samplerate);
    osc.SetWaveform(Oscillator::WAVE_POLYBLEP_SAW);

    env.Init(samplerate);
    env.SetSustainLevel(0.5f);
    env.SetAttackTime(0.005f);
    env.SetDecayTime(0.005f);
    env.SetReleaseTime(0.2f);

    // I think this MIDI setup would not be not needed if trying this on Daisy
    // hardware with inbuilt MIDI.
    MidiUartHandler::Config midi_config;
    midi_config.transport_config.periph = UartHandler::Config::Peripheral::USART_1;
    midi_config.transport_config.rx = hw.A9;
    midi_config.transport_config.tx = hw.A8;
    midi.Init(midi_config);

    midi.StartReceive();
    hw.StartAudio(AudioCallback);
    while (1)
    {
        midi.Listen();
        while (midi.HasEvents())
        {
            HandleMidiMessage(midi.PopEvent());
        }
        Settings &settings = storage.GetSettings();
        // Change the storage so that it will actually save:
        settings.something = System::GetNow();
        // Throttle the saves to once every 2 seconds:
        if (System::GetNow() - last_save >= 2000)
        {
            storage.Save();
            last_save = System::GetNow();
        }
    }
}


I’m sorry for the delay in the response.
I’ll look into this with the team and get back to you once I have a definitive answer! Thank you.

I’ve managed to make the situation slightly better by updating a timer whenever there is a MIDI event (just note on/off events for now, don’t want a MIDI clock blocking saves completely) and waiting for 10 seconds after the last MIDI event before saving.

This still means it can collide with MIDI events or result in data loss, but it is an improvement from a musical sense.

1 Like

ANY update from the Team on this ?? I want to be able to use MIDI clock in the project I am building and am worried it will mean saves will be off the table

1 Like

Hi cricketbee! And I’ll also tag @whatnot!

I just checked with the team about this and here’s the response:

The QSPI writing is a blocking transaction, so anything that shouldn’t be interrupted will need to be done in some sort of interrupt.

libDaisy has the ability to generate a low-priority interrupt (essentially can be interrupted by everything that isn’t the main loop). So you can move your MIDI and any UI/LED stuff into that, and it will allow those to operate while the QSPI save is taking place in the main while loop.

The Aurora-SDK has an example of how to initialize a timer to do this.

We hope this helps!

thanks-this is very helpful

1 Like

Thanks a lot @Takumi_Ogata! I’ve found some time to try this out and the result is vastly improved. I do still get the occasional missed message and I’m wondering if maybe my period is not set correctly.

I used the calculation from the Aurora example and passed the audio rate (48k) as target_freq which from memory resulted in a period of 2500, but I don’t actually know what period range I should be aiming for. Any suggestions?

Here’s my updated example:

#include "daisy_patch_sm.h"
#include "daisysp.h"

using namespace daisy;
using namespace patch_sm;
using namespace daisysp;

struct Settings
{
    uint32_t something;

    bool operator==(const Settings &rhs) { return something == rhs.something; }
    bool operator!=(const Settings &rhs) { return !operator==(rhs); }
};

DaisyPatchSM hw;
Adsr env;
MidiUartHandler midi;
Oscillator osc;
PersistentStorage<Settings> storage(hw.qspi);
uint32_t last_save;
bool gate;
TimerHandle tim5_handle;

void AudioCallback(AudioHandle::InputBuffer in,
                   AudioHandle::OutputBuffer out,
                   size_t size)
{
    float sig;
    for (size_t i = 0; i < size; i++)
    {
        sig = osc.Process() * env.Process(gate);
        OUT_L[i] = sig;
        OUT_R[i] = sig;
    }
}

// Very simple MIDI event handling.
void HandleMidiMessage(MidiEvent m)
{
    switch (m.type)
    {
    case NoteOn:
    {
        NoteOnEvent p = m.AsNoteOn();
        if (m.data[1] != 0)
        {
            p = m.AsNoteOn();
            osc.SetFreq(mtof(p.note));
            osc.SetAmp((p.velocity / 127.0f));
            gate = true;
        }
    }
    break;
    case NoteOff:
    {
        gate = false;
    }
    break;
    default:
        break;
    }
}

void StartLowPriorityCallback(TimerHandle::PeriodElapsedCallback cb,
                              uint32_t target_freq,
                              void *data = nullptr)
{
    TimerHandle::Config timcfg;
    timcfg.periph = TimerHandle::Config::Peripheral::TIM_5;
    timcfg.dir = TimerHandle::Config::CounterDir::UP;
    timcfg.period = System::GetPClk2Freq() / target_freq;
    timcfg.enable_irq = true;
    tim5_handle.Init(timcfg);
    tim5_handle.SetCallback(cb, data);
    tim5_handle.Start();
}

void MidiCallback(void *data)
{
    midi.Listen();
    while (midi.HasEvents())
    {
        HandleMidiMessage(midi.PopEvent());
    }
}

int main(void)
{
    float samplerate;
    hw.Init();
    samplerate = hw.AudioSampleRate();
    storage.Init({0});

    // Synthesis
    osc.Init(samplerate);
    osc.SetWaveform(Oscillator::WAVE_POLYBLEP_SAW);

    env.Init(samplerate);
    env.SetSustainLevel(0.5f);
    env.SetAttackTime(0.005f);
    env.SetDecayTime(0.005f);
    env.SetReleaseTime(0.2f);

    // I think this MIDI setup would not be needed if trying this on Daisy
    // hardware with inbuilt MIDI.
    MidiUartHandler::Config midi_config;
    midi_config.transport_config.periph = UartHandler::Config::Peripheral::USART_1;
    midi_config.transport_config.rx = hw.A9;
    midi_config.transport_config.tx = hw.A8;
    midi.Init(midi_config);

    midi.StartReceive();
    hw.StartAudio(AudioCallback);
    StartLowPriorityCallback(MidiCallback, samplerate);
    while (1)
    {
        Settings &settings = storage.GetSettings();
        // Change the storage so that it will actually save:
        settings.something = System::GetNow();
        // Throttle the saves to once every 2 seconds:
        if (System::GetNow() - last_save >= 2000)
        {
            storage.Save();
            last_save = System::GetNow();
        }
    }
}
2 Likes