Home Artists Posts Import Register

Content

Blockhead is a mixture of C++ and a language called GDScript, Godot's built-in scripting language. All of the audio/performance critical code is C++ whereas the majority of the UI is written in GDScript.

GDScript makes it really easy to prototype new UI ideas and get things working very quickly. It's no good for professional audio code because it's so dog slow but performance wise it's perfectly good for UI/event processing which is really what it's meant for. However one of the problems I've found with it is that if the problem at hand is complex enough then the code involved can become really hard to maintain.

One of the most complex parts of Blockhead right now is the code for handling block stamping, cloning, and drag+drop.

In Blockhead you can select a bunch of blocks and drag them around and as long as the "root" block under the cursor is being dragged to an empty space then Blockhead will attempt to find valid spaces for all the other blocks in the selection, and maintain their respective start positions relative to each other. If a target space is blocked by something already on the workspace then each block will search up and down through the lanes until it finds an empty space. If it can't find space on any lane then a new lane will be automatically inserted into the track.

This system has a lot of different problems it needs to deal with:

  • Two different input modes, dragging and stamping, which should feel consistent with one another
  • Different behaviours depending on whether new samples are being added, or blocks are being cloned, or existing blocks are being dragged
  • When stamping a single sample, the user can hover it over an existing block to preview it
  • New lanes can be added by dragging blocks to the edge of a track. Sometimes multiple new lanes need to be added and there should be a visual indication of this
  • Blocks will automatically crop themselves if they collide with a block to the right, but only down to a minimum pixel threshold, dependent on the current zoom level
  • Blocks will also nudge themselves a little bit to the right if they collide with a block to the left, but only up to a maximum pixel threshold
  • While searching for an empty space, blocks might collide with another block in the selection that was processed before it
  • When dragging a group of blocks that span multiple tracks, the blocks should attempt to stay in their original lanes if possible, but only if the cursor did not move to another track
  • When cloning an existing group of blocks that spans multiple tracks, the user can hold down CTRL to tell the blocks to try to stay in the original lanes of their respective "source" blocks
  • When cloning an existing group of blocks, the system has to figure out which block it should treat as the "root" block, and where it should be placed on the screen relative to the cursor position
  • Blocks can snap to other blocks or tempo guides while dragging

This all adds up to something very complex, and all of it is currently written in around 1000 lines of very messy GDScript. There are a few unaddressed bugs and things that don't work the way I want them to, and things I will want to add or change in the future. I dread having to make changes to this code because it's such a mess.

I'm not the kind of person who knows or thinks a lot about programming language design but put simply I find it difficult to structure and organize things in a clean way in GDScript. The language doesn't offer very many options for how to encapsulate things, how data should be stored, etc. That's not usually a problem with simple tasks but becomes more of a problem when implementing complex algorithms like this.

Godot allows you to write UI code in C++ too and I have done this for a few things. The catch buffer is almost entirely C++. I have about 15 years of experience with C++ which I think makes me a "beginner". It's not an easy language but it provides many more tools for effectively encapsulating concepts and structuring things. The downside of using C++ for UI is it can be a bit slower to quickly prototype and try out new ideas, which is where GDScript really excels.

Going forward I will probably be spending some time rewriting some of the more complex parts of the UI in C++ as I find it much easier to write things in a maintainable way which will be important in the long run. Some parts of the UI that are still in a kind of prototyping stage (such as the effects rack) will probably benefit from staying in GDScript for a while longer.

Comments

Anonymous

C++ is so ubiquitous / reliable. Better to change things now and achieve greater scalability and future ease, than to hit roadblocks or functionality dead-ends later down the road. All the features and developments thus far have been very exiting and I (along with many others) look forward to where Blockhead will go from here! In a world full of DAWs, Blockhead is unique / truly offers its own perspective and approach, which we have not seen in years

Anonymous

Really nice drag and drop is a pain to do in any language I think - maybe it's partly because it's messing with the view hierarchy, by pulling things in and out of it, working with your long list of exceptional cases etc. Last time I had a bug with drag and drop, basically had to rewrite it to understand what I wrote the first time round :-S Using GDScript in blockhead must help you enforce a decent amount of code separation which is awesome especially with audio software, where there are threads flying all over the place, so I guess it's important to maintain that aspect if doing more UI in C++ - it's so tempting to mix it all up and end up in a rats nest (that's basically my life these days).

colugomusic

Writing C++ UI elements in Godot always involves creating a class that inherits from godot::Control or something like that so that can sort of double as a way to help enforce that audio/GUI separation. e.g. For the catch buffer I ended up with a CatchBuffer class and a CatchBufferView class

Anonymous

Ah, very sensible. Probably a bit of a rabbit hole, but hot reloading is nice way to prototype quickly in C++ - I have a thing I made here: https://github.com/elf-audio/cppsketch - but it's mac/linux only for now :( - this project does it on windows but it's tied into a whole graphics engine: https://github.com/tgfrerer/island