Need Help with Mobile App Crashes

Why use a timer in an audio application?
The sampling rate is your timer… you already have something that ticks on the clock, the audio clock.
Don’t count milliseconds, count samples instead.

At a 44100 Hz samplerate, 441 samples are 10 milliseconds… You can compute the proportion between Midi tempo, time signature, tempo division and sampling rate, and establish how many samples you should wait between a note event and the next one. Wherever you have fractional samples, you can round to the nearest integer and bring back the fractional part to the next interval.

Check this code, it’s part of a MidiFile player that I wrote… The Play() function must be called at every sample in the audio loop. See how the timing is calculated in setSampleRate() and setTempo()

#pragma once

#include <JuceHeader.h>

class JuceMidiFilePlayer
{
public:
    JuceMidiFilePlayer()
    {
        PlayMidifile = false;
    }

    ~JuceMidiFilePlayer()
    {
        PlayMidifile = false;
    }

    void setMidiFileObject(const juce::MidiFile& newMidifile)
    {
        // Make sure the playback is stopped before loading a new file
        StartStopMidiFile(false);

        midifile = newMidifile;

        FileLoaded = true;
        tick_count = 0;
        sample_count = 0;
        last_tick = midifile.getLastTimestamp();
        TPQ = midifile.getTimeFormat();
        lastSampleOffset = 0.0;
        trackEventStart.clear();
        trackEventStart.insertMultiple(0, 0, midifile.getNumTracks());

        setTempo(tempoBPM);
        if (onLoad != nullptr)
            onLoad();

        DBG("MidiFile loaded.");
        DBG("midifile.getLastTimestamp() = " << midifile.getLastTimestamp()); // last tick
        DBG("midifile.getTimeFormat() = " << midifile.getTimeFormat()); // tpq
        DBG("midifile.getNumTracks() = " << midifile.getNumTracks());
    }

    void LoadMidiFileFromURL(const URL& url)
    {
        jassert(processMidi != nullptr);
        jassert(allNotesOff != nullptr);

        std::unique_ptr<juce::InputStream> inputStream(juce::URLInputSource(url).createInputStream());

        // Make sure the playback is stopped before loading a new file
        StartStopMidiFile(false);

        juce::MidiFile mf;
        if (mf.readFrom(*inputStream.get()))
            setMidiFileObject(mf);
    }

    void Unload()
    {
        StartStopMidiFile(false);
        if (onEOF != nullptr) onEOF();

        midifile.clear();
        FileLoaded = false;
    }

    void Rewind()
    {
        tick_count = 0;
        trackEventStart.fill(0);
    }

    void StartStopMidiFile(bool b)
    {
        // Call AllNotesOff when Stop button is hit
        if (!b) allNotesOff();

        // Don't play if no file is loaded
        if (!FileLoaded)
        {
            if (onEOF != nullptr) onEOF();
            return;
        }

        // No double play
        if (b && PlayMidifile) return;

        // Rewind
        if (!b && !PlayMidifile)
        {
            sample_count = 0;
            lastSampleOffset = 0.0;
            Rewind();
        }

        // Store status
        PlayMidifile = b;
    }

    void setTempo(double _tempoBPM)
    {
        tempoBPM = _tempoBPM;

        // A single tick duration is 60 / BPM / TPQ
        // Assuming tempo is 120 BPM and TPQ is 480, a tick lasts 1,0416666666666666666666666666667 milliseconds = 1041 microseconds

        tick_duration = 60.0 / tempoBPM / (double)TPQ;
        auto d = (lastSampleOffset + tick_duration * sampleRate);
        tick_duration_in_samples = (int)d;
        lastSampleOffset = d - (double)tick_duration_in_samples;
    }

    double getTempo()
    {
        return 60.0 / (double)TPQ / tick_duration;
    }

    float getPosition01()
    {
        return (float)tick_count / (float)last_tick;
    }

    void setPosition01(float pos)
    {
        allNotesOff();
        tick_count = juce::jmap<float>(pos, 0, last_tick);
        trackEventStart.fill(0);
    }

    void setSampleRate(double sr)
    {
        sampleRate = sr;

        // Set a starting tick_length for a tempo of 120 BPM and 480 TPQ
        //tick_duration = 60000.0 / 120.0 / 480.0;
        //tick_duration_in_samples = tick_duration * sampleRate / 1000.f;
        setTempo(tempoBPM);

        sample_count = 0;
        lastSampleOffset = 0.0;
        Rewind();
    }

    // To be called in the audio loop for each single sample
    void Play()
    {
        if (!PlayMidifile) return;

        // Shift to the next tick after the required tick duration
        if (++sample_count >= tick_duration_in_samples)
        {
            sample_count = 0;

            // Play events from all tracks in the Midi File
            for (int t = 0; t < midifile.getNumTracks(); t++)
            {
                // Get single track
                auto Sequence = midifile.getTrack(t);

                // Search events in track having a timestamp that matches the current tick_count
                for (int e = trackEventStart[t]; e < Sequence->getNumEvents(); ++e)
                {
                    auto msg = Sequence->getEventPointer(e)->message;

                    // Play all events matching this timestamp
                    if (msg.getTimeStamp() == (double)tick_count)
                    {
                        // Update tempo if it's a tempo event
                        if (msg.isTempoMetaEvent())
                        {
                            TPQ = midifile.getTimeFormat();
                            tick_duration = msg.getTempoMetaEventTickLength(TPQ);
                            setTempo(getTempo());
                        }

                        // Send Midi
                        processMidi(msg.getRawData(), msg.getRawDataSize());
                    }

                    // If the timestamp goes past the tick_count, skip scanning the rest of the track
                    if (msg.getTimeStamp() > (double)tick_count)
                    {
                        // Store last event index
                        trackEventStart.set(t, e);

                        break;
                    }
                }
            }

            // Advance to the next tick
            tick_count++;

            // Check end of file and stop
            if (tick_count >= last_tick)
            {
				PlayMidifile = false;
				allNotesOff();
				if (onEOF != nullptr) onEOF();
            }
        }
    }

    //==============================================================================

    // These public variables tell whether the file is loaded and is playing
    MidiFile midifile;
    bool PlayMidifile = false;
    bool FileLoaded = false;
    int TPQ = 480;

    // Set a lambda for receiving Midi Data
    std::function<void(const unsigned char* midiData, int chunkLength)> processMidi;

    // Set a lambda for calling the All-notes-off event on stop
    std::function<void(void)> allNotesOff;

    // Set a lambda that, if defined, is called when a new Midi File is loaded
    std::function<void(void)> onLoad = nullptr;

    // Set a lambda that, if defined, is called when the playback reaches the end of the file
    std::function<void(void)> onEOF = nullptr;


private:
    juce::Array<int> trackEventStart;
    double tick_duration = 1.041; // 120 BPM @ 480 TPQ
    long tick_count = 0, last_tick = 1;
    int sample_count = 0, tick_duration_in_samples = 0;
    double sampleRate = 44100.0, lastSampleOffset = 0.0;
    double tempoBPM = 120.0;
};

1 Like