For last few years I've been following the Godot Engine development with great interest — especially after the infamous Unity Runtime Fee -fiasco (🤦♂️). About a year ago I started evaluating the Godot Engine, first by reading the documentation and then by making a simple game. Just to get a feel for the engine. I also began prototyping a game that might be worth pursuing further (been occasionally working on it for short stints), but that remains to be seen.. 🤔 Nonetheless I've documented my first Godot impressions (Gist): feel free to take a look, if interested.
In any case, an opportunity presented itself to do further development with Godot on my day job. But instead of a game, I made a tool application! 🛠️ In this post I wanted to share my perspective on using Godot as an app framework. (With some performance benchmarks thrown in for good measure!)
Some background: at work we occasionally need to split long audio recordings of various items (words, usually, sometimes longer spoken parts) into separate files, with some processing performed and the output files properly named. All of this automated as much as possible. We had an old custom Python 2.x app for that; it did the job, but was somewhat cumbersome to use: very slow and flickery in rendering the audio waveform, zooming, making selections etc. Especially with huge audio files. I maintained and improved the app over the years, but didn't touch on the core issues (that would've required a rewrite anyway).
Which is exactly what I ended up doing! 😅 With Godot Engine 4.4 adding runtime WAV-loading I thought it'd be a perfect opportunity to try the Godot UI tooling and also do a better version of the audio splitting app for work. Two birds with one stone, as they say.
Enter the (aptly-named, if I may say so myself 😄)...
I started building the tool with GDScript to allow for rapid prototyping — hot-reloading script changes is a huge time-saver. First up was AudioView
, the custom UI component (or Node
, in Godot-terminology) responsible for rendering the audio waveform in efficient manner. With smaller audio files this was fine, but pretty soon I hit a performance ceiling with GDScript. The actual rendering of the lines was not the problem, but "down-sampling" (i.e. for each vertical line computing the relevant range in audio samples1 and finding the min-max values) the audio data for rendering was just too slow to be usable. Now I knew beforehand that GDScript is not the most performant language for raw number crunching like this, but it was still interesting to try. And as I said, for rapid prototyping and less number-crunchy stuff it's very nice!
What to do, then? With frame times of hundreds of milliseconds a smooth user experience would be impossible, even when only rendering as needed! 🤔 What's neat with Godot is that you can mix and match languages. I took my AudioView
and ported it to C# AudioViewCS
. This resulted in a huge speed increase, even better than what I had anticipated! 🔥 Impressed with speed-up I wrote a quick benchmarking rig to properly measure them, just for fun. See my benchmark2 results below:
AudioView | Audio length | Render time avg. (ms) | Total (ms) | Difference |
---|---|---|---|---|
GDScript | ~5 min | 139.87 | 34 687.61 | 1x |
C# | ~5 min | 1.73 | 427.91 | 81x |
GDScript | ~36 min | 292.13 | 72 449.30 | 1x |
C# | ~36 min | 4.26 | 1055.51 | 69x |
AudioView | Audio length | Render time avg. (ms) | Total (ms) | Difference |
---|---|---|---|---|
GDScript | ~5 min | 278.14 | 69 676.25 | 1x |
C# | ~5 min | 3.07 | 762.03 | 91x |
GDScript | ~36 min | 440.07 | 110 215.92 | 1x |
C# | ~36 min | 5.85 | 1450.11 | 75x |
NOTE: Audio view was fully zoomed out with 5 min clip, and only partially on 36 min clip. This explains how in the larger file the performance difference is smaller. Should've rendered the full waveform in hindsight, but hey, a quick benchmark is quick! 😛
Pretty impressive gains, in my opinion! 👍️ Naturally, the bigger the audio view width, the larger the speed difference. Now that the C#-version runs so much better, the GDScript-variant is useless? Nope, I kept it around for a while after that whilst I was improving the rendering, adding the timeline markers etc. Hot-reload FTW, baby! 😬💪 (It is gone in the final product, though.)
And no: I didn't convert any other parts of the app to C#, because I do like GDScript (with as much of static typing as possible!), and it worked fine and fast enough for the audio analysis, other UI code etc. GitLab reports 78.7% of GDScript code, and 18.8% of C#. Rest is shell scripts.
Just wanted to mention: this benchmark isn't meant to diss GDScript at all, but instead highlight the possibility of using other tools for the job (like C#, or even C++). It is great indeed that you can replace individual components with different tech if performance problems emerge! 🙂
* * *
With the big performance issue nicely solved, I had quite a lot of fun implementing the rest of the interface using Godot UI nodes. Overlaid on top of the main
AudioViewCS
-node I have another custom node, that implements the selections manipulation & allows manually slicing the audio. Both of them share one scroll-bar. And that's it for custom components, the rest is built-in nodes. Rendering the custom components (inheriting Control
) was enjoyable, just override _draw()
and draw away!
After spending some time with them I do prefer Godots UI nodes over Unity UI components (disclaimer: I mean UGUI, I have not used the newer UI Elements). Layout nodes are very flexible in Godot and there are lots of useful nodes (spin boxes, text fields / editors were needed in this app) that have no counterpart in Unity UGUI. Not surprising, of course, as Godot editor itself uses the same UI components. For the most part I used the default theme with some tweaking, and also made a semi-shitty icon for the app.
There was one more thing I wanted to try. When AudioSplitter was fully working, I wanted to improve the UX by doing the audio loading, analysis and normalization in a separate background worker thread, to avoid freezing the app during those operations. Godot Engine threading support was a breeze to use: I got my worker thread up and running in no time at all. It uses the basic job queue -pattern: main thread constructs a Job
-object with relevant data + a callback, posts it to the worker thread, which then picks it up, processes the job and posts back the results using the given callback. GDScript Callable
s were useful here.
All in all I was very impressed by how well Godot is suited for application development (despite being, you know, a game engine). Can't stress enough how useful the hot-reloading of scripts and syncing object properties is when building a UI! ✨
Nothing is perfect, though, and I did have some minor niggles: a peculiar bug with text fields + some editor crashes when updating my UI sub-scenes and switching back to main scene, while the app was running. Also the size of an exported .NET build is rather large: to combat that, I wrote a post-build script that removes most of the .NET DLLs and libraries that my sole C#-node doesn't need. Was able to save ~45 MB doing that. More space savings could be achieved by compiling the engine and leaving out unneeded stuff, but I didn't bother with that for an internal tool. Positives far outweigh the negatives and I'm definitely using Godot again for my future tool app needs! And likely games, too.
And that's that, for now. If you made it this far: thanks for reading, and feel free to comment! 🙂
For example: 30 minutes of mono PCM audio at 44.1 kHz sample rate has ~79 million samples.↩
Benchmarked on Ryzen 7 7700X PC in Linux build, with warm-up period. Rendered 250 frames of audio waveform (fully zoomed out with 5 min clip, partially on 36 min clip), discarded 1% slowest frame times.
(Hmm, Linux, you say? Do I sense another blog-post looming in the horizon..? 👀)↩