Need Help with Mobile App Crashes

Hello everyone,

I have had an iOS and Android application available on the app stores (iOS and Android) for over a year now. While I’m not a C++ expert, I am quite satisfied with the overall result of my application. It’s a comprehensive application, but unfortunately, around 3% of users are experiencing crashes (ANR or app crashes). I use Firebase Crashlytics to track issues. This percentage is too high. The crashes occur either within the JUCE library or, occasionally (0.4% of the time), during the display of an Admob ad. I’ve attempted to share the details of these issues on the forum in the past, but no one has been able to assist me.

I’m curious if there are any JUCE (Full, including the JUCE interface) users on iOS and Android who have encountered very few bugs. Regrettably, I lack the expertise to understand all the intricacies of JUCE’s code, and I can’t reproduce these bugs on my own devices.

In general, it appears that these crashes may be related to overlaying pages, displaying a web page on top, showing an ad on top, returning to my app, cleanly closing my app, and managing threads, among other things.

My JUCE application interface (JUCEApplication) is standard, but there are instances when I need to display a web page (e.g., Terms Of Use or Privacy Policy) or a store page (Appstore Connect, pay or PlayStore Rate And Review), share pages (audio or midi), or show an ad (Google Admob), and my crashes occur during these moments. Therefore, it’s possible that the issue lies in how my application launches (main.cpp) or in managing the suspended() or resumed() states. I don’t know.

Understanding the Firebase Crashlytics reports is challenging because these issues seem to originate outside of my code.

If anyone can provide guidance, advice, or any help that can help me improve and finally have a stable application, it would be greatly appreciated. I would also be grateful for the opportunity to connect with someone who has a comprehensive application running smoothly on both iOS and Android.

I understand that many JUCE users primarily focus on creating plugins rather than smartphone applications, but I’m hopeful that I can find some assistance. I’m also aware that there are individuals who use JUCE on smartphones, primarily for audio processing, and they may utilize native iOS and Android interfaces for visual components.

I’m more than willing to share whatever is necessary, be it code, Crashlytics data, or anything else that could be of help.

Thank you very much,
ToBy

Or perhaps my issue lies in the foundation of my sequencer with its ‘waitForMillisecondCounter’ calls.

My main sequencer is a ‘HighResolutionTimer.’ In the ‘hiResTimerCallback,’ I send MIDI note events to my ‘AudioSource’ components (using the JUCE ‘Synthesiser’ in my ‘AudioSource’). In this process, I use ‘waitForMillisecondCounter’ to introduce groove or prevent clicking during note overlaps.

It’s possible that when a user interacts with my app, such as clicking a button to display a web page, store, ad, etc., during a ‘waitForMillisecondCounter’ call, it may lead to crashes. I’m not entirely certain. Or perhaps it’s essential to ensure that everything is properly finalized, waits, music, etc., before transitioning my app to the background?

Problems like these are sometimes (often!) caused by memory corruption or threading issues, or a combination of both. If possible, you can build and test your app with the Clang address sanitizer or thread sanitizer. In Xcode it should be as simple as enabling one of the corresponding checkboxes on the “Diagnostics” tab of the “Scheme Editor”. There are serious performance impacts of enabling these so you should only enable them for your test builds.

As a side note, when sending midi into an audio stream, you don’t want to use HighResolutionTimer
but compute the time using the buffer from the audio thread

@otristan
I’m not entirely sure I understand what ‘compute the time using the buffer from the audio thread’ means. Does it imply that I should use ‘getNextAudioBlock()’ to replace my timer? Would I count the number of times the ‘getNextAudioBlock()’ function is called (based on the sample rate), and from that, deduce a BPM, etc? I might not have grasped the concept completely.
I’ve been searching for a JUCE-based sequencer on GitHub that uses this concept of ‘compute the time using the buffer from the audio thread,’ but unfortunately, I haven’t found one yet.

Thank you very much for your help!

Yes this is how it should be done
You can check the arpeggiator example
https://docs.juce.com/master/tutorial_plugin_examples.html#tutorial_plugin_examples_arpeggiator
or this

Google’s Ad SDKs have always been very buggy, so some of the crashes might simply be due to that. (In that case, the stacktrace would show a crash in the code for the Admob SDK.)

@kerfuffle
Yes, there are indeed some issues with the Admob SDK, and I’ve encountered a few of them:

  • firebase::gma::GmaInternal::CompleteLoadAdFutureFailure Resulting in ‘EXC_BAD_ACCESS (KERN_INVALID_ADDRESS)’
  • firebase::gma::internal::InterstitialAdInternalIOS::InterstitialDidReceiveAd(GADInterstitialAd*) Also leading to ‘EXC_BAD_ACCESS (KERN_INVALID_ADDRESS)’
  • firebase::FutureHandle::FutureHandle(firebase::FutureHandle const&) Which triggers an ‘EXC_BAD_ACCESS (KERN_INVALID_ADDRESS)’ error."

But are these issues a result of my usage of the Admob SDK, or is there nothing I can do to prevent them?

Thank you very much for your response

@otristan
Thank you so much for these examples; I understand better now. Unfortunately, I wish I had come across this tutorial before building my app. I should have done more research. I have a lot of changes to make in my application to implement this properly, but it makes much more sense than what I’ve done.

Do you think there’s a chance that these changes will reduce the number of bugs in my app or bring about other improvements?

I’m delighted to receive a response from someone from Paris (like me), who works with JUCE. Congratulations on your apps!
ToBy

@jdv
Thank you; I’ll give it a try. I’ve used similar tools in the past to address issues within my code, though not on my iOS device. I’ll see if I can pinpoint these threading issues using my iOS device.

bug I don’t know, improvment in timing, that’s for sure.

1 Like

I haven’t used the latest version of the Admob SDK (can’t update for reasons) but for the past 9 years it’s been the only source of crashes in my app (which does not use JUCE).

I’d focus on the crashes that are unrelated to Admob first.

1 Like

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

Thank you so much for pointing that out. It’s my first app, and I’m a beginner, so I didn’t think about it. It makes sense now, and I appreciate your guidance.
Thanks a lot for the code snippet; it’ll be really helpful.
Have a great day!

1 Like