Table of contents
- Description
- Plugin Name and Description
- Modifying the GUI
- Passing HostEventandMidiFileEventtoPPPthread
- Modifying ITPThread
- Modifying MDLThread
- Loading the Model in PPPThread
Tutorial 5 - Single Thread Implementation
In this tutorial, we will be using the same model as the previous tutorials, so make sure you have completed the previous tutorials.
the source code for this tutorial is available in the tutorials branch of the repository.
Description
If you find the three thread implementation too complicated for a given model you’re using, you can modify the wrapper to use a single thread implementation instead
In this tutorial, we will show you how to do this by modifying the wrapper as follows:
- Modifying ITP: Send incomingMidiFileEventandEventFromHostto the model using theModelInputstruct
- Modifying MDL: Get theModelInputstruct from theITPand send it to the model as is
- Modifying PPP: Get theModelOutputcontaining all necessary events and Implement the processing here
Once finished with this tutorial, you can always use this as a template for your own models.
We’ll use this single thread implementation method for the next tutorial
Plugin Name and Description
As mentioned here, we need to specify the name of the plugin as well as some descriptions for it.
To do this, we will modify the NeuralMidiFXPlugin/NeuralMidiFXPlugin/CMakeLists.txt file as follows:
project(Tutorial5NMFx VERSION 0.0.1)
set (BaseTargetName Tutorial5NMFx)
....
juce_add_plugin("${BaseTargetName}"
        COMPANY_NAME "AIMCTutorials"                
        ... 
        PLUGIN_CODE AZCO                # a unique 4 character code for your plugin                          
        ...
        PRODUCT_NAME "Tutorial5NMFx")           # Replace with your plugin title
Once you re-build the cmake project, and re-build the plugin, you should see the name of the plugin change in the DAW:
Now we are ready to move on to the next step.
Modifying the GUI
For this tutorial, we don’t need the GUI we will make sure there are no components in Configs_GUI.h file.
     // Configs_GUI.h
            
     // rest of the code ...   
            
            namespace Tabs {
        const bool show_grid = false;
        const bool draw_borders_for_components = false;
        const std::vector<tab_tuple> tabList{
            tab_tuple
            {
                "Tab",
                slider_list
                {
                },
                rotary_list
                {
                },
                button_list
                {
                }
            },
        };
    }
    namespace MidiInVisualizer {
        const bool enable = true;
        const bool allowToDragInMidi = true;
        const bool visualizeIncomingMidiFromHost = true;
        const bool deletePreviousIncomingMidiMessagesOnBackwardPlayhead = false;
        const bool deletePreviousIncomingMidiMessagesOnRestart = false;
    }
    namespace GeneratedContentVisualizer
    {
        const bool enable = true;
        const bool allowToDragOutAsMidi = true;
    }
  Passing HostEvent and MidiFileEvent to PPP thread
We will start with modifying the CustomStructs.h file as follows:
First, make sure ITPData and MDLData are empty structs. If not, this won’t cause any issues, but we’ll keep them empty as in this case, we will not be doing any processing in these threads. Rather, we will just be using these threads to pass the data directly from ITP to PPP via MDL thread.
// CustomStructs.h
// Any Extra Variables You need in ITP can be defined here
// An instance called 'ITPdata' will be provided to you in Deploy() method
struct ITPData {
};
// Any Extra Variables You need in MDL can be defined here
// An instance called 'MDLdata' will be provided to you in Deploy() method
struct MDLData {
};
Then, we will be modifying the ModelInput and ModelOutput structs as follows:
// CustomStructs.h
struct ModelInput {
    
    std::optional<MidiFileEvent> new_midi_event_dragdrop;
    std::optional<EventFromHost> new_event_from_host;
    // ==============================================
    // Don't Change Anything in the following section
    // ==============================================
    chrono_timer timer{};
};
struct ModelOutput {
    std::optional<MidiFileEvent> new_midi_event_dragdrop;
    std::optional<EventFromHost> new_event_from_host;
    // ==============================================
    // Don't Change Anything in the following section
    // ==============================================
    chrono_timer timer{};
};
Note that we have added two optional variables to the ModelInput and ModelOutput structs. These are exactly the same as the inputs passed on to the deploy() method of ITP (see ITP_Deploy.cpp).
Now, we are ready to move on to the next step.
  Modifying ITP Thread
In here, we will modify the ITP_Deploy.cpp file.
The objective here is to first make sure if the deploy() method was called because of an Event (rather than a GUI change). If so, then we will place the received events in the ModelInput struct and send it to the MDL thread.
// ITP_Deploy.cpp
bool InputTensorPreparatorThread::deploy(
    std::optional<MidiFileEvent> & new_midi_event_dragdrop,
    std::optional<EventFromHost> & new_event_from_host,
    bool gui_params_changed_since_last_call) {
    // we only pass the data if the method was called because of a new event
    // and not because of a gui parameter change
    if (new_midi_event_dragdrop.has_value() || new_event_from_host.has_value()) {
        model_input.new_event_from_host = new_event_from_host;
        model_input.new_midi_event_dragdrop = new_midi_event_dragdrop;
        return true;
    }
    return false;
}
  Modifying MDL Thread
In here, we will modify the MDL_Deploy.cpp file.
Similar to the ITP thread, we will first make sure if the deploy() method was called because of a new received ModelInput and not because of a GUI change. If so, we will pass the received ModelInput to the PPP thread.
// MDL_Deploy.cpp
bool ModelThread::deploy(bool new_model_input_received,
                         bool did_any_gui_params_change) {
    // we only pass the data if the method was called because of a new model_input
    if (new_model_input_received) {
        model_output.new_event_from_host = model_input.new_event_from_host;
        model_output.new_midi_event_dragdrop = model_input.new_midi_event_dragdrop;
        return true;
    }
    return false;
}
After these modifications are done, we can test whether the required data is correctly passed from ITP to PPP thread. To do this, we’ll modify PPP_Deploy.cpp file as follows:
// PPP_Deploy.cpp
std::pair<bool, bool> PlaybackPreparatorThread::deploy(bool new_model_output_received, bool did_any_gui_params_change) {
    bool newPlaybackPolicyShouldBeSent = false;
    bool newPlaybackSequenceGeneratedAndShouldBeSent = false;
    if (new_model_output_received)
    {
        if (model_output.new_event_from_host) {
            PrintMessage("Received new event from host");
        }
        if (model_output.new_midi_event_dragdrop) {
            PrintMessage("Received new midi event from dragdrop");
        }
    }
    return {newPlaybackPolicyShouldBeSent, newPlaybackSequenceGeneratedAndShouldBeSent};
}

  Loading the Model in PPP Thread
Loading the model in PPP thread is slightly different from the previous tutorials. The reason is that, there is no dedicated loader nor a dedicated place holder for the model in PPP thread. That said, we have the ability to load the model using the PPPData struct in CustomStructs.h. as follows:
// CustomStructs.h
// assuming that the model is called 'drumLoopVAE.pt' and is available somewhere int the TorchScript folder
struct PPPData {
    torch::jit::script::Module model = load_processing_script("drumLoopVAE.pt");
};
To use this model in the PPP_Deploy.cpp file, we can simply use the local instance of PPPData (called PPPdata) as follows:
// PPP_Deploy.cpp
    // some code ...
    
    PPPdata.model.forward(....);
