Home Artists Posts Import Register

Content

I am beginning jury service tomorrow and they haven't told me yet how long it will last but I assume about a week.

Once I am done with jury service I will likely be doing some unrelated contracting work to help pay the bills and then I will be able to get back to Blockhead.

Since build v0.28.2 I have been doing a bunch of work on performance and stability improvements, and general code clean-up. I was really hoping to finish this before starting jury service but unfortunately I didn't quite make it. It is always tempting to move on implementing new big features like the instrument system I have talked about but I would really like to get a more stable alpha available.

Some of the things I have been working on:

A new undo/redo implementation

Up until now I have been using the undo/redo system provided by Godot. This has a lot of problems and I am glad to finally be rid of it.

The main issue with Godot's system is the requirement for all the undo/redo data to be serialized into essentially a bunch of numbers, strings and arrays. So every time the user performs any undoable action I would have to encode it into a format suitable for Godot's object system and then decode it when the action is played back.

Another big issue with Godot's object system is that, even after encoding everything into a serializable format, nothing is type-safe, so it's easy to accidentally pass the wrong parameters to a function (something that normally is impossible with C++).

At long last I have replaced this mess with my own undo/redo system, which operates pretty similarly to Godot's system but with the advantage that everything is type-safe and I can store any data I want inside the undo/redo actions without having to serialize them.

I even found and fixed multiple type errors in the process of moving to the new system which would probably have been causing some obscure crashes or weirdness while undoing/redoing in certain corner cases. These kind of bugs will be impossible now because going forward all undo/redo actions are statically typed.

Another big benefit of this new system is that I can utilize reference counting to automatically clean up data when it is no longer needed. For example:

Fig.1: The user performs 3 actions to get to a particular state.

Fig. 2: The user then presses undo twice, to rewind back to an earlier state. In Blockhead you can also redo after an undo, so the actions required to redo back to the later states aren't discarded yet, and so any data that is required to perform those redo operations will still exist in memory.

Fig. 3: The user performs a new action which means a new branch of the undo/redo history is created. Those future actions from the old branch are now inaccessible and so the data associated with those actions should be discarded.

Fig. 4: The dead actions from the old branch are discarded, along with the associated data.

When I say "data associated with actions" one good example would be samples, which need to hang around in memory after they are undo'd out of existence, just in case they need to be redo'd back into existence. On the flip side samples also need to hang around in memory after they are deleted by the user, just in case they need to be undo'd back into existence.

With the old Godot system this was a huge pain in the ass because there was no way to tie the lifetime of an object to the set of undo/redo actions which require it to exist. The only thing you can do is store an ID, manually manage the object's lifetime elsewhere and keep track of where the user is in the undo/redo history to figure out when to clean up objects which are no longer accessible.

With the new system I can just store reference-counted objects inside the actions which are automatically cleaned up as soon as their reference count hits zero, which would happen when the old actions are discarded. So far this new system seems to be working wonderfully and has simplified many things.

Another refactor of the drag/drop/stamping system

Another thing I have been working on is the system for dragging blocks around, copy, cloning, stamping new blocks etc. I won't write so much about this because I have already written so much about it in old posts.

This continues to be one of the most deceptively complicated aspects of Blockhead's UI. The system suffers greatly from new features being added and I have already have to refactor the entire thing multiple times as detailed in old dev logs.

These are the different kinds of operation we can do currently, all of which need to work slightly differently and have their own little awkward corner cases:

  • Stamping new blocks from the block browser
  • Cloning existing blocks
  • Copying existing blocks
  • Dragging existing blocks
  • Stamping new sampler blocks from the sample browser
  • Stamping new sampler blocks from sample files
  • Stamping a new macro created from the right-click context menu of an existing selection
  • Stamping a new macro created by dragging a selection of blocks onto the macro creator widget at the bottom of the workspace
  • Left-resizing one or more blocks
  • Right-resizing one or more blocks
  • Left-tempo-resizing a selection of blocks
  • Right-tempo-resizing a selection of blocks

This time I think I have finally found an implementation that I am happy with and so even if I have to add more operations in the future it shouldn't involve any more refactors.

The important thing for me is that I now feel like if there is some bug in block dragging or stamping I should always be able to figure out exactly where I need to look, as long as I know generally what operation the user was doing when the bug occurred.

Multithreaded parameter updates

Using macros and manipulators you can already create some pretty hairy situations for Blockhead to deal with.

Manipulators can look upwards through the macro hierarchy to affect blocks above them and can also compose with each other, so it is easy in v0.28.2 to cause the UI to start lagging out if you push things too much.

Going forward this shouldn't be an issue anymore as I have moved the procedures for updating block parameters into a separate thread, so that after moving or editing a manipulator, all of its affected parameters will gradually resolve in the background.

This means if you have some very deep nested macro hierarchy with manipulators that you are messing with then you might see a slight visual delay before sampler waveforms are updated, or a slight audible delay before the parameters are updated, but you shouldn't experience any input lag in the frontend.

In 99% of use cases where you are just dealing with a few manipulators and only shallowly nested macros then everything will just update instantaneously like it always has done.

Comments

No comments found for this post.