tracktion_engine icon indicating copy to clipboard operation
tracktion_engine copied to clipboard

[Bug]: Simple TE v3.0 program hanging on "Creating Default Controllers..."

Open debrisapron opened this issue 1 year ago • 11 comments

Detailed steps on how to reproduce the bug

On macOS, go to https://github.com/debrisapron/tracktion-engine-hello-world/tree/upgrade-tracktion_engine and follow instructions in README. Should build successfully, print the block size & samplerate, then hang when it gets to "Creating Default Controllers...". This is against the v3.0 tag but I've also checked against the latest develop & same error. Note that I don't have any loopbacks or other exotic drivers installed.

What is the expected behaviour?

It should play the demo, as it does with the previous version.

Unit test to reproduce the error?

No response

Operating systems

macOS

What versions of the operating systems?

Sonoma 14.2.1 on M1 Max

Architectures

ARM

Stacktrace

No response

Plug-in formats (if applicable)

No response

Plug-in host applications (DAWs) (if applicable)

No response

Testing on the develop branch

The bug is present on the develop branch

Code of Conduct

  • [X] I agree to follow the Code of Conduct

debrisapron avatar Jul 21 '24 21:07 debrisapron

I just tried this on macOS and got the following:

[100%] Built target TracktionHelloWorld
Finding MIDI I/O
MIDI output: IAC Driver Bus 1 (enabled)
opening MIDI out device:IAC Driver Bus 1
MIDI input: IAC Driver Bus 1 (enabled)
opening MIDI in device: IAC Driver Bus 1
Audio block size: 512  Rate: 44100
Rebuilding Wave Device List...
Wave In: New Label (enabled): 0 (L)
Wave Out: Output 1 + 2 (enabled): 0 (L), 1 (R)
Default Wave Out: Output 1 + 2
Default MIDI Out: IAC Driver Bus 1
Default Wave In: New Label
Default MIDI In: IAC Driver Bus 1
Creating Default Controllers...

but it was playing the audio.

I'm not quite sure what the reported problem is here? It looks like Creating Default Controllers is just the last message to be logged?

But have you run this in debug? You'll get assertions because you've not actually started the JUCE MessageManager. Tracktion Engine requires this to be running and you're not really supposed to block the message thread as there could be all kinds of async messages piling up behind your blocking while loop.

drowaudio avatar Jul 22 '24 11:07 drowaudio

Huh that's interesting, was definitely not producing sound for me but good to know it's not just hanging. I actually thought I was running this in debug but looking at the code I forgot to put it in lol. To the point about the blocking while what else should I do here? I found that without it the program would simply exit before any audio had been emitted, but I'm sure there's a standard construct I should be using here. This is very much a learning process for me!

Anyway thanks for the reply! Will do a bit more investigation & probably close this soon.

debrisapron avatar Jul 23 '24 06:07 debrisapron

It's a bit tricky as you really want to be creating a juce::JUCEApplication rather than a console app.

However, you might get away with adding a ScopedJuceInitialiser_GUI instance at the top of your main function (see our TestRunner.h for an example) and then manually run the message loop afterwards instead of your while loop:

    JUCE_TRY
    {
        // loop until a quit message is received..
        MessageManager::getInstance()->runDispatchLoop();
    }
    JUCE_CATCH_EXCEPTION

drowaudio avatar Jul 23 '24 09:07 drowaudio

Hello again! Sorry, I let this hang for a while, but trying to get it working again with TE V3. So I followed your suggestion and added the lines you mentioned, now my main.cpp looks like this:

#include <tracktion_engine/tracktion_engine.h>
#include <memory>

using namespace std;
namespace te = tracktion;
using namespace std::literals;
using namespace te::literals;

static void addNoteToClip(
    te::MidiClip *midiClip,
    int noteNumber,
    int velocity,
    te::BeatPosition start,
    te::BeatDuration duration)
{
    midiClip->getSequence().addNote(
        noteNumber,
        start,
        duration,
        velocity,
        0,
        nullptr);
}

int main()
{
    juce::ScopedJuceInitialiser_GUI init;

    // Create the engine
    te::Engine engine{"Tracktion Hello World"};

    // Create an edit
    auto edit = std::make_unique<te::Edit>(
        engine,
        te::Edit::forEditing);

    // Create a track
    edit->ensureNumberOfAudioTracks(1);
    auto track = te::getAudioTracks(*edit)[0];

    // Get length of 1 bar
    const tracktion::TimeRange oneBarTimeRange(
        0s,
        edit->tempoSequence.toTime({1, tracktion::BeatDuration()}));

    // Insert a 1 bar long Midi clip
    auto clip = track->insertNewClip(
        te::TrackItem::Type::midi,
        "Midi Clip",
        oneBarTimeRange,
        nullptr);
    auto midiClip = static_cast<te::MidiClip *>(clip);

    // Add a 4-note C-E-G-C sequence to the clip
    // Note the use of Tracktion's beat position/duration literals
    addNoteToClip(midiClip, 60, 100, 0_bp, 0.5_bd);
    addNoteToClip(midiClip, 64, 100, 1_bp, 0.5_bd);
    addNoteToClip(midiClip, 67, 100, 2_bp, 0.5_bd);
    addNoteToClip(midiClip, 72, 100, 3_bp, 0.5_bd);

    // Create a built-in synth plugin instance to play the sequence on
    auto plugin = edit->getPluginCache()
                      .createNewPlugin(te::FourOscPlugin::xmlTypeName, {})
                      .get();
    auto fourOscPlugin = static_cast<te::FourOscPlugin *>(plugin);

    // Insert the plugin to the track
    track->pluginList.insertPlugin(*fourOscPlugin, 0, nullptr);

    // Get the transport & set it to the start of the edit
    auto &transport = edit->getTransport();
    transport.setPosition(0s);

    // Set the transport to loop our clip
    transport.setLoopRange(clip->getEditTimeRange());
    transport.looping = true;

    // Begin playback
    transport.play(false);

    // loop until a quit message is received..
    juce::MessageManager::getInstance()->runDispatchLoop();
}

This builds and runs fine, but all it does is print out the following & then immediately exits:

Audio block size: 512  Rate: 44100
Creating Default Controllers...
Cleaning up temp files...

For the sake of completeness here is my CMakeLists.txt:

cmake_minimum_required(VERSION 3.22)

project(TRACKTION_HELLO_WORLD VERSION 0.0.1)

add_subdirectory(tracktion_engine/modules/juce)
add_subdirectory(tracktion_engine/modules)

juce_add_console_app(TracktionHelloWorld
    PRODUCT_NAME "Tracktion Hello World")

target_sources(TracktionHelloWorld
    PRIVATE
        main.cpp)

target_compile_definitions(TracktionHelloWorld
    PRIVATE
        JUCE_WEB_BROWSER=0
        JUCE_USE_CURL=0)

target_link_libraries(TracktionHelloWorld
    PRIVATE
        tracktion::tracktion_engine
        juce::juce_core
    PUBLIC
        juce::juce_recommended_config_flags
        juce::juce_recommended_warning_flags)

set_property(TARGET TracktionHelloWorld PROPERTY CXX_STANDARD 20)

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
  target_link_libraries(TracktionHelloWorld PRIVATE "-latomic")
endif()

I know I could probably get it working if I just change it to a regular JUCE GUI App, but I'd really like to boil it down to the fewest possible moving parts if I can! Also I have an interest in using TE for a headless audio app and it would be great to know if this is actually possible. Thanks in advance if you have any thoughts!

debrisapron avatar Sep 21 '24 20:09 debrisapron

Is it actually quitting?

If so, is a quit message being posted to the message queue?

You should be able to put a breakpoint in the juce::application/message_manager classes to see where that is coming from.

drowaudio avatar Sep 21 '24 21:09 drowaudio

OK this is good stuff! So I registered an ActionListener that just prints any received messages to stdout and it never receives anything! Which is super weird because runDispatchLoop should block forever if it doesn't get a quit message right?

debrisapron avatar Sep 22 '24 19:09 debrisapron

I added a bunch of logging and it's not silently crapping out early or anything. It runs all the way through to the end and returns. So runDispatchLoop isn't blocking at all.

debrisapron avatar Sep 22 '24 19:09 debrisapron

So it's working as you hoped?

drowaudio avatar Sep 23 '24 09:09 drowaudio

No, because it doesn't play audio and exits immediately. With TE 2 it plays audio endlessly until I kill it, now it just exits without playing anything.

debrisapron avatar Sep 25 '24 03:09 debrisapron

Right, but you must be able to debug why it's quitting? But a breakpoint in MessageManager::runDispatchLoop() and step through it. Is the quitMessageReceived equal to 1?

If so, look at the line quitMessageReceived = true, is that being hit at all? Or MessageManager::stopDispatchLoop() being hit?

This isn't really a supported use case so I can't spend a bunch of time debugging it myself I'm afraid but I'm happy to help if you can debug it and find out what's actually happening. But you'll need to be able to use the debugger and breakpoints etc. to step in to the juce code.

drowaudio avatar Sep 25 '24 13:09 drowaudio

OK will try breakpointing it, thanks!

debrisapron avatar Sep 26 '24 04:09 debrisapron

I bet you thought this would be clogging up your issues forever? Nope, I'm back for another round!

So I decided to rework my "Hello World" as a simple GUI app, since it seems like TE isn't really intended for headless use. I've created a dead-simple Juce GUI app & put my TE code in the MainComponent. Here's what I've got now:

#include <juce_gui_extra/juce_gui_extra.h>


// ---- INTERESTING STUFF STARTS HERE ----

#include <tracktion_engine/tracktion_engine.h>
#include <memory>

using namespace std;
namespace te = tracktion;
using namespace std::literals;
using namespace te::literals;

static void addNoteToClip(
    te::MidiClip *midiClip,
    int noteNumber,
    int velocity,
    te::BeatPosition start,
    te::BeatDuration duration)
{
    midiClip->getSequence().addNote(
        noteNumber,
        start,
        duration,
        velocity,
        0,
        nullptr);
}

//==============================================================================
class MainComponent final : public juce::Component
{
public:
    MainComponent(te::Engine &e) : engine(e)
    {
        DBG("* Initialize MainComponent");
        setSize(400, 200);

        DBG("* Create an edit");
        edit = std::make_unique<te::Edit>(
            engine,
            te::Edit::forEditing);

        DBG("* Create a track");
        edit->ensureNumberOfAudioTracks(1);
        auto track = te::getAudioTracks(*edit)[0];

        DBG("* Get length of 1 bar");
        const tracktion::TimeRange oneBarTimeRange(
            0s,
            edit->tempoSequence.toTime({1, tracktion::BeatDuration()}));

        DBG("* Insert a 1 bar long Midi clip");
        auto clip = track->insertNewClip(
            te::TrackItem::Type::midi,
            "Midi Clip",
            oneBarTimeRange,
            nullptr);
        auto midiClip = static_cast<te::MidiClip *>(clip);

        DBG("* Add a 4-note C-E-G-C sequence to the clip");
        // Note the use of Tracktion's beat position/duration literals
        addNoteToClip(midiClip, 60, 100, 0_bp, 0.5_bd);
        addNoteToClip(midiClip, 64, 100, 1_bp, 0.5_bd);
        addNoteToClip(midiClip, 67, 100, 2_bp, 0.5_bd);
        addNoteToClip(midiClip, 72, 100, 3_bp, 0.5_bd);

        DBG("* Create a built-in synth plugin instance to play the sequence on");
        auto plugin = edit->getPluginCache()
                          .createNewPlugin(te::FourOscPlugin::xmlTypeName, {})
                          .get();
        auto fourOscPlugin = static_cast<te::FourOscPlugin *>(plugin);

        DBG("* Insert the plugin to the track");
        track->pluginList.insertPlugin(*fourOscPlugin, 0, nullptr);

        DBG("* Get the transport & set it to the start of the edit");
        auto &transport = edit->getTransport();
        transport.setPosition(0s);

        DBG("* Set the transport to loop our clip");
        transport.setLoopRange(clip->getEditTimeRange());
        transport.looping = true;

        DBG("* Begin playback");
        transport.play(false);
    }

    void paint(juce::Graphics &g) override
    {
        g.fillAll(juce::Colours::darkgrey);
    }

private:
    te::Engine &engine;
    std::unique_ptr<te::Edit> edit;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent)
};


// ---- BORING JUCE BOILERPLATE FOLLOWS ----

//==============================================================================
class TEHelloWorldApplication : public juce::JUCEApplication
{
public:
    TEHelloWorldApplication() {}

    const juce::String getApplicationName() override { return JUCE_APPLICATION_NAME_STRING; }
    const juce::String getApplicationVersion() override { return JUCE_APPLICATION_VERSION_STRING; }

    void initialise(const juce::String &commandLine) override
    {
        mainWindow.reset(new MainWindow(getApplicationName()));
    }

    void shutdown() override
    {
        mainWindow = nullptr;
    }

    class MainWindow : public juce::DocumentWindow
    {
    public:
        MainWindow(juce::String name)
            : DocumentWindow(name,
                             juce::Desktop::getInstance().getDefaultLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId),
                             DocumentWindow::allButtons)
        {
            setUsingNativeTitleBar(true);
            te::Engine engine{JUCE_APPLICATION_NAME_STRING};
            setContentOwned(new MainComponent(engine), true);
            setVisible(true);
        }

        void closeButtonPressed() override
        {
            JUCEApplication::getInstance()->systemRequestedQuit();
        }

    private:
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow)
    };

private:
    std::unique_ptr<MainWindow> mainWindow;
};

//==============================================================================
START_JUCE_APPLICATION(TEHelloWorldApplication)

This builds & runs successfully and prints the following output:

[boring build log omitted]
JUCE v8.0.6
Settings file: /Users/ms/Library/TEHelloWorld/Settings.xml
Audio block size: 512  Rate: 44100
Creating Default Controllers...
* Initialize MainComponent
* Create an edit
Edit loaded in: 2 ms
* Create a track
* Get length of 1 bar
* Insert a 1 bar long Midi clip
* Add a 4-note C-E-G-C sequence to the clip
* Create a built-in synth plugin instance to play the sequence on
* Insert the plugin to the track
* Get the transport & set it to the start of the edit
* Set the transport to loop our clip
* Begin playback
JUCE Assertion failure in tracktion_DeviceManager.cpp:432
Cleaning up temp files..
JUCE Assertion failure in tracktion_Engine.cpp:152
./build-and-run.sh: line 3: 41355 Segmentation fault: 11  ./cmake-build/TEHelloWorld_artefacts/Debug/TEHelloWorld.app/Contents/MacOS/TEHelloWorld

As you can see there are two assertion failures in TE here. To save you looking them up the first one is:

jassert (activeContexts.isEmpty());

And the second one is:

jassert (deviceManager != nullptr);

This is as far as I've got for now! Can you point me in a direction of investigation, ideally one that doesn't involve attaching a debugger, because I've already lost a lot of time going down that rabbit hole. Thank you!

debrisapron avatar Jun 03 '25 20:06 debrisapron

FWIW a big project I've been working on recently uses the engine headlessly, so it's well tested like that. Yes, you need an event loop, but you don't need a GUI.

If you're hitting assertions like that, then it's probably because you're shutting down the engine before deleting things that are using it.

But github issues are for bugs, not coding advice, so I'm going to close this. If you do find an actual bug, please open a specific ticket for it.

For general "how do I do this" advice, a better place would be the discord: https://discord.gg/cVQhcrxX

julianstorer avatar Jun 04 '25 09:06 julianstorer