DEV Community

gus
gus

Posted on

Jucey Details

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.

Image description

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.

Image description

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.

Image description

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.

Image description

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.

Image description


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);
...
Enter fullscreen mode Exit fullscreen mode

In the DrawModule() function, the Draw() methods of the buttons are called:

void NoteCanvas::DrawModule()
{
...
   mLoadMidiButton->Draw();
   mSaveMidiButton->Draw();
...
Enter fullscreen mode Exit fullscreen mode

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);
   }
}
Enter fullscreen mode Exit fullscreen mode

The FileChooser object is created with:

  1. the message to be displayed at the top of the browser window
  2. the path to look in
  3. the file extensions to look for
  4. flag for whether to use the OS' native dialog box
  5. flag for whether to treat file packages as directories
  6. 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!

Discussion (0)