Table of contents
Groove to Drum (Real-Time)
Description
The model provided in the previous tutorials converts a single voice groove into a drum pattern. In that case, we used a midi file to extract the groove. However, in this tutorial, we will create a plugin which allows the user to play the groove in real-time using a midi keyboard.
The code here is very similar to the code in the previous tutorial, so we will only highlight the differences.
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 PluginCode/CMakeLists.txt file as follows:
project(Demo4 VERSION 0.0.1)
set (BaseTargetName Demo4)
add_definitions(-DPROJECT_NAME="${BaseTargetName}")
juce_add_plugin("${BaseTargetName}"
# VERSION ... # Set this if the plugin version is different to the project version
# ICON_BIG ... # ICON_* arguments specify a path to an image file to use as an icon for the Standalone
# ICON_SMALL ...
COMPANY_NAME "DemosMid2Mid" # Replace with a tag identifying your name
IS_SYNTH TRUE # There is no MIDI vst3 plugin format, so we are going to assume a midi instrument plugin
NEEDS_MIDI_INPUT TRUE
NEEDS_MIDI_OUTPUT TRUE
AU_MAIN_TYPE kAudioUnitType_MIDIProcessor
EDITOR_WANTS_KEYBOARD_FOCUS FALSE
COPY_PLUGIN_AFTER_BUILD TRUE # copies the plugin to user plugins folder so as to easily load in DAW
PLUGIN_MANUFACTURER_CODE Juce #
PLUGIN_CODE aaac # MUST BE UNIQUE!! If similar to other plugins, conflicts will occur
FORMATS AU VST3 Standalone
PRODUCT_NAME "Demo4") # Replace with your plugin title
GUI
The GUI is very similar to the previous tutorial. The only difference is that we don’t need the MidiVisualizer widget anymore. So, we will modify the PluginCode/settings.json as follows:
{
"name": "RandomGeneration",
"sliders": [
{
"label": "Density",
"min": 0.0,
"max": 1.0,
"default": 0.5,
"topLeftCorner": "Fi",
"bottomRightCorner": "Tm",
"horizontal": true
}
],
"rotaries": [],
"buttons": [],
"MidiDisplays": []
}
[Optional] We can also add a real-time midi visualizer to the GUI. To do this, we will enable the MidiInVisualizer
widget in the same settings file.
{
"MidiInVisualizer": {
"enable": true,
"allowToDragInMidi": false,
"visualizeIncomingMidiFromHost": true,
"deletePreviousIncomingMidiMessagesOnBackwardPlayhead": true,
"deletePreviousIncomingMidiMessagesOnRestart": true
}
}
Specifying Host Events
As mentioned in the documentation, we will modify the PluginCode/settings.json to specify what events are required from the host.
In this example, all we need is incoming midi notes. That is, we don’t need any frame buffer information as we only will be triggering the deployment process on each incoming midi note
{
"event_communication_settings": {
"SendEventAtBeginningOfNewBuffers_FLAG": false,
"SendEventForNewBufferIfMetadataChanged_FLAG": false,
"SendNewBarEvents_FLAG": false,
"SendTimeShiftEvents_FLAG": false,
"delta_TimeShiftEventRatioOfQuarterNote": 0.5,
"FilterNoteOnEvents_FLAG": false,
"FilterNoteOffEvents_FLAG": true,
"FilterCCEvents_FLAG": true
}
}
In the above, we are filtering out all CC events and NoteOff events. This is because we only need NoteOn events to trigger the deployment process. However, this is completely optional, as we do a further check for NoteOn events in the code.
deploy()
The processing here is very similar to the previous tutorial with the exception that the notes are received one by one in real-time, rather than all at once from a midi file.
As such the only difference to the deploy() function is as follows:
// ...
deploy(...) {
// ...
// check if a new midi file has been dropped on the visualizers
bool shouldEncodeGroove = updateGrooveUsingHostEvent(new_event_from_host);
// ...
}
private:
// ...
// checks if a new host event has been received and
// updates the input tensor accordingly
bool updateGrooveUsingHostEvent(std::optional<EventFromHost> & new_event) {
// return false if no new event is available
if (new_event == std::nullopt) {
return false;
}
if (new_event->isFirstBufferEvent()) {
// clear hits, velocities, offsets
groove_hits = torch::zeros({1, 32, 1}, torch::kFloat32);
groove_velocities = torch::zeros({1, 32, 1}, torch::kFloat32);
groove_offsets = torch::zeros({1, 32, 1}, torch::kFloat32);
}
if (new_event->isNoteOnEvent()) {
auto ppq = new_event->Time().inQuarterNotes(); // time in ppq
auto velocity = new_event->getVelocity(); // velocity
auto div = round(ppq / .25f);
auto offset = (ppq - (div * .25f)) / 0.125 * 0.5 ;
auto grid_index = (long long) fmod(div, 32);
// check if louder if overlapping
if (groove_hits[0][grid_index][0].item<float>() > 0) {
if (groove_velocities[0][grid_index][0].item<float>() < velocity) {
groove_velocities[0][grid_index][0] = velocity;
groove_offsets[0][grid_index][0] = offset;
}
} else {
groove_hits[0][grid_index][0] = 1;
groove_velocities[0][grid_index][0] = velocity;
groove_offsets[0][grid_index][0] = offset;
}
}
// stack up the groove information into a single tensor
groove_hvo = torch::concat(
{
groove_hits,
groove_velocities,
groove_offsets
}, 2);
// ignore the operations (I'm just changing the formation of the tensor)
groove_hvo = torch::zeros({1, 32, 27});
groove_hvo.index_put_(
{torch::indexing::Ellipsis, 2},
groove_hits.index({torch::indexing::Ellipsis, 0}));
groove_hvo.index_put_(
{torch::indexing::Ellipsis, 11},
groove_velocities.index({torch::indexing::Ellipsis, 0}));
groove_hvo.index_put_(
{torch::indexing::Ellipsis, 20},
groove_offsets.index({torch::indexing::Ellipsis, 0}));
return true;
}