Jucey Details
gus
Posted on December 4, 2021
In last week's post I discussed my plans to implement midi import and export in BespokeSynth, a modular synth making app that's built on JUCE, an open source framework for developing VST plugins. So far I've been chipping away on a few fronts: making a demo app to try implementing midi import and export separately from Bespoke, looking at examples of components from other apps that do what I want to, and looking at how Bespoke already uses some components I'll need to implement it.
I have virtually no experience with JUCE so I'm having to learn a lot about a lot of different things simultaneously: how JUCE projects are structured and rendered, how MIDI works, and then the specifics of BespokeSynth's workings on top of that. Juce seems pretty intuitive, with many classes to inherit from and methods to implement to make use of them. BespokeSynth has many custom implementations of classes as well that the developer's set up to inherit from, so once I know how to do some of what I need in vanilla Juce I'll need to see how he does it and adapt my approach accordingly.
To that end, I've now made a demo app in Juce which allows one to load and save a .mid
file.
The structure of the app is as follows - the main.cpp
source file contains an Application
class which inherits from juce::JUCEApplication
. This has basic methods to initialize the app, shut it down, logic to be called when another instance is called, and gets for name and version. There is another class, MainWindow
which inherits from juce::DocumentWindow
and fittingly defines parameters for how the window the app opens in will look and act. I didn't have to change much from the default project Projucer generates, I followed the example in this tutorial but left out the logic for dealing with audio files. The changes in main.cpp
extend mostly to changing the name of the component called, and passing the component to the MainWindow
class.
The other source file, MidiLoader.h
, inherits from juce::Component
and contains the logic in the constructor to add a button, set text and visibility, and assign a function to be triggered when it's clicked. The resized() function sets the position and size of the buttons, and the two functions triggered by the button clicks have calls to juce::FileChooser
methods which is where the logic to open file browsing windows comes from.
With this part now done, the next steps will be figuring out how this should be implemented in BespokeSynth using their preferred methods (I know for one there were custom components set up rather than using the default juce::Component class) and the most difficult part will likely be loading a .mid
and setting up the drum sequencer to use it. From the research I've done it seems like there are a lot of quirks to using MIDI which I've never had to deal with from the user side of it.
After poking around in the Bespoke code for a while I got in touch with the developers on the Bespoke discord to get some guidance, and got some tips on how to proceed. I started by making a mockup of what the added buttons would look like.
The lead developer told me he'd rather it be more of a "back of the module" feature accessible through the triangle menu at the top. Which makes sense, the UI was really cluttered after adding the 2 buttons and I was barely able to make enough room for them.
Unfortunately there isn't a system in place to add arbitrary buttons to the triangle menu, so he'll need to implement that before midi import and export can be accessed there.
He also suggested I add the feature to the note canvas, rather than the drum sequencer, which again was a good call. Rather than worry about midi tracks aligning and note placements/durations lining up with the grid of drum sequencer they could fit more openly on a keyboard layout setting without having to work around all those possibly problematic limitations.
My next steps will be adding placeholder buttons to the note canvas UI, adding a juce::FileChooser
to load and save the files, and then using juce::MidiFile
to get/set the notes from the file.
Here is note canvas with the two placeholder buttons in place and the juce::FileChooser
implemented for the two. The way Bespoke uses FileChooser
is a bit different from the way it was done in the JUCE tutorial I learned from, but it worked out. I opted to copy the existing syntax from the SeaOfGrain module which allows users to load in samples, but changed it to load .mid
files instead. The "midi" you see in the filename field is due to the appropriate folder not being created yet, I'm going to reach out to the other developers to see if they prefer a dedicated midi folder being created in Bespoke's files or a user defined one which can be browsed for from the root.
Breaking down the code added, the buttons were first added in the CreateUIControls()
function as pointers to ClickButton objects, which is a Bespoke-defined class:
void NoteCanvas::CreateUIControls()
{
IDrawableModule::CreateUIControls();
mQuantizeButton = new ClickButton(this,"quantize",160,5);
mLoadMidiButton = new ClickButton(this,"load midi", 224, 5);
mSaveMidiButton = new ClickButton(this,"save midi", 290, 5);
...
In the DrawModule()
function, the Draw()
methods of the buttons are called:
void NoteCanvas::DrawModule()
{
...
mLoadMidiButton->Draw();
mSaveMidiButton->Draw();
...
Finally, I added two methods to load and save midi, which as of right now just contain the FileChooser
portions.
void NoteCanvas::LoadMidi()
{
using namespace juce;
FileChooser chooser("Load midi", File(ofToDataPath("midi")), "*.mid", true, false, TheSynth->GetFileChooserParent());
if (chooser.browseForFileToOpen())
{
auto file = chooser.getResult();
std::vector<std::string> fileArray;
fileArray.push_back(file.getFullPathName().toStdString());
FilesDropped(fileArray, 0, 0);
}
}
The FileChooser
object is created with:
- the message to be displayed at the top of the browser window
- the path to look in
- the file extensions to look for
- flag for whether to use the OS' native dialog box
- flag for whether to treat file packages as directories
- the parent component Once the object is created and opened, the file is loaded into an object. A vector of filenames is then created with the filename pushed into it, I'm not sure if I'll need this functionality yet so I may remove it depending on how the next part goes.
That leaves just the juce::MidiFile
logic to implement, which should be quite a task in and of itself. More on that to come!
Posted on December 4, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.