Featured image for Dev Diary 13: Koto - The Resurrection

Dev Diary 13: Koto - The Resurrection

October 31, 2024

taps mic Is this thing on? The year is not 2021, though I would not blame you for thinking otherwise given that was when I wrote the last development diary on Koto (my audio manager). I had half-expected to pick up its development later that year but life had other plans (leaving Solus and founding Buddies of Budgie for Budgie Desktop's future / development, a couple new jobs, moving, western-style horsemanship, you know how it is..). Had I continued, it probably would have been written in EFL/C, as that was originally the plan for it after I had decided to put the GTK/C implementation on hold indefinitely. That never happened and frankly, I could not be happier that it didn't. Don't get me wrong, I'm sure that EFL implementation would have been good. It just would not have served me here in 2024 and my current plans for it.

#But...why?

But...why? meme As a bit of a backstory, I have been writing more C++ recently in the form of the new implementation of the budgie-daemon. The focus has been on output management under Wayland (so leveraging Wayland protocols) and all fancy DBus bits with sdbus-cpp, for the purpose of providing the plumbing for display configuration in Budgie Control Center as part of Buddies of Budgie's move to Wayland for Budgie 10 (and beyond). Meanwhile, Campbell (serebit) on the team has been writing Budgie Desktop's Wayland compositor (leveraging wlroots) in C++ and I would like actually be useful in that area as well at some point. Putting the two together and you end up kinda living C++ a fair bit, so that played a part in the decision-making process. As I have been working on that daemon and alongside the rest of the team engaging with more folks from more projects such as KDE, Cinnamon, and XFCE -- you can't help but get inspired by all the mutual goals on Waylandifying the ecosystem and fostering cross-project collaboration. Koto plays into that a bit with some discussions I had with Clément on XApps before it became a thing (it being similar to the "Modern Desktop Initiative" I had basically only announced on my Patreon back in 2021 with a similar goal), as many of us in the community would like to see more applications that are not adversarial to each other's desktop environment, but rather strive to make those applications feel as native to that environment as possible. Expanding on that, at least a personal vision of mine is building a Budgie Desktop platform that promotes a composable experience (not just for us, but third-parties not even involved in Budgie Desktop). Composable desktop experience + composable application / ala carte experience? Sounds like a win to me. While Koto may not necessarily be an XApp, as at least at the moment there is no real definition on what that even is, in spirit the goal is for it to be. Yet all of the above was still not enough to actually make me say "alright, I think it is time to resurrect Koto and build something cool". I had half-heartedly mentioned in the team standups that I might do it at some point, but in the same vein one would say "yea I should really go dive into that icy lake in the middle of a cold Finnish winter". Never done that despite living here for over 10 years, so I think the comparison is pretty apt. What finally got the wheel turning was Jolla's announcement on the Jolla C2 Community Phone, running Sailfish OS 5.0.
Fuck yes. I had originally used Sailfish OS on my Jolla One back in 2016 and I loved the gesture-based UI, ambiences, the multi-tasking view, and just the design of the applications in general. I had never really gotten into actually developing Sailfish OS applications before I ended up switching back to Android (Jolla One hardware was starting to feel pretty dated), but it was always something that had piqued my interest. At that point though I was neck deep in early evolveOS (now Solus) contributions, so time for anything besides that simply wasn't on the table. The Sailfish X experience passed me by, my interest was really mostly in official hardware but after the troubles with Jolla Tablet I mentally disconnected from it. Well many years had passed since then and there I was, being tempted by a new mid-range Jolla phone (partnered with Reeder) running the "latest and greatest" of their operating system. I had mentally prepared for writing Qt for Koto for a while and I was already writing C++. What if Koto could run not just on the desktop, nor just on my desktop and KDE Plasma Mobile, but those two and Sailfish OS as well? Now we are talking. So the fate was sealed. Now all that I needed was time. Literally, I needed a vacation. So I booked one for the end of September and early October. I wasn't expecting to get anything actually usable done within that timeframe but just wanted to get the ball rolling.

#If you wish bake an apple pie from scratch, you must first invent the universe.

Fortunately I did not have to invent the universe however I did have to figure out what ingredients should go into my apple pie and how to bake it. All while livestreaming like my old Koto streams! The old Koto was built with:
  • GTK as the toolkit (emphasis on the kit part, foreshadowing here)
  • GLib, lots of stuff but most notably GDBus for media keys and MPRIS support
  • gstreamer-1.0 and gstreamer-player-1.0 for the playback
  • sqlite3 (C API) for the database
  • taglib for id3v2 metadata parsing
  • tomlc99 for the configuration (TOML)
This time around I had the luxury of not having to come up with much of my solutions from scratch, but that did not mean that the way I was going to implement it was identical. During the zero day of Koto development, time was dedicated towards:
  1. Sorting out CMake to be used as the build system. As much as I love Meson, the Qt6 module simply is not at feature parity and that is okay. I took the opportunity to look at KDE's Extra CMake Modules (ECM) which provided a ton of useful functions related to QML.
  2. Figuring out at a high level what libraries, APIs, and UI library / libraries I would use. Which, if any, of the libraries I had used were directly translatable over from the C world. What would I need to do from scratch and what work could I leverage from others.
  3. Stretch goal: Some initial QML UI bits. Nothing fancy.
I was very fortunate to have had the KDE Mastodon actually boost my post about Koto, so I had some really awesome people jump in from the KDE community to provide some suggestions, guidance, and probably a carry (to use gaming term) in some respects to be honest. I certainly would not have gotten as far as I did as quickly as I did without them, so big thanks to all that tuned in. Special shout-out to Komma developer Olivier and Haruna developer George for all the assistance (even beyond day 0). For new Koto, obviously I was going with Qt 6.x. That probably did not need explaining, however I really want to drive home that while old Koto used GTK as a toolkit, Qt is more like a toolbelt. If there is something you need your application to do, you should probably look at Qt APIs / modules first, because chances are it exists. Makes sense given how long Qt has been around and how many industries have built solutions on top of it, but it is one thing to know it probably has everything under the Sun and another to actually experience it. So going back to that earlier list, we can break it down a bit differently:
  • Qt6 as the toolbelt
  • Qt6 DBus (including qdbusxml2cpp) or alternatively using sdbus-cpp (including sdbus-c++-xml2cpp) like I do in Budgie Daemon v2 for generating C++ code from the XML interface definitions, as well as the DBus client / server bits
  • Media playback: No need to interface with gstreamer-1.0 / gstreamer-player-1.0 C APIs, there is QtMultimedia and QMediaPlayer
  • sqlite3? Yea I was about to do that by hand but to nobody's surprise, there is an entire the Qt SQL module and its APIs are great. You are not necessarily limited to sqlite3 either as you can just as easily use the MariaDB, PostgresSQL, or other database drivers
As part of my research before writing any code, I looked into KDE Frameworks to see which, if any, would make sense to be utilized in Koto. I think some developers may have the impression that KDE Frameworks are intended for only KDE applications or the Plasma Desktop, when that could not be further from the truth. Of course, KDE applications are bound to use these frameworks, however as a result of how KDE Frameworks has organized their libraries into tiers, in the vast majority of instances you can rely on very specific libraries without pulling in everything. Initially, I was looking at what options I would have for id3v2 metadata parsing in audio files beyond taglib. This is not a special task by any means and there is an abundance of media players, including from the KDE community. Unsurprisingly, KFileMetaData exists for the very purpose of providing common metadata extractors, including ID3 parsing! Looking deeper at the code, it was pretty similar to what I would have needed to write anyways, and it sensibly uses TagLib as well. There are some other KDE Framework libraries I plan on using like KCoreAddons for KJobs and the GUI-equivalent package, but focusing on the direct replacement of TagLib, KFileMetaData was the way to go. I had also considered supporting Baloo as an optional datasource for content, but after seeing that it got dropped from Elisa due to indexing issues, I opted just to stick with my own indexer like old Koto had. I might re-evaluate that in the future, time will tell! For TOML, that one I already had figured out. Budgie Daemon v2 already has TOML file parsing leveraging toml11. For the user interface, I already knew it was going to use QML and Qt Quick Controls with their QML API. For desktop / KDE Plasma Mobile, I was also like 99% certain I was going to leverage KDE's Kirigami, however I did take a look at MauiKit as well just for some due diligence. Both are pretty compelling choices however in the end I decided to still roll with Kirigami since the KDE HIG that Kirigami implements simply resonated with me the most. If you (yes, you!) decide to make a Qt6 application, take a look at both and consider using one of them! Of course, I would be remiss if I did not talk about support for Sailfish OS. After all, it is what really pushed me over the edge for working on Koto. Unfortunately, while there has been a lot of improvements over the years to Sailfish OS despite all the hurdles Jolla has faced, one really important part in my particular case which has not evolved substantially is the Sailfish Silica module. Specifically, it still uses Qt5 and worse it is 5.6.3 from September of 2017. So given that, I have two choices on how to implement Koto for Sailfish OS:
  1. In the monorepo, have a Sailfish OS build variant that is in a completely different toplevel sub-directory to the desktop variant (which I put under ./desktop/ even though yes it could be used for Plasma Mobile) that is written in Qt5.
  2. Leverage some very recent work by Sailfish OS community members piggz and rinigus on Qt6 packaging, which conveniently enough was done so piggz could also implement a music player (though for Subsonic) using Kirigami! Doesn't magically fix the Silica part though.
I think the jury is still out on what I will end up going with. I am leaning more heavily towards a Qt5 implementation because I am so fond of the Silica app experiences, however if it is too technically burdensome to accomplish (or Qt5 just sucks to work with) then the desktop implementation will be used for all three, with some tweaks to what is used for the datasource.

#You smell what I am cooking?

The Rock - You smell what I'm cooking meme Okay, one can only do so much research and planning. No plan survives contact with the ~~enemy~~ IDE. I will skip over all the literal hours of me fighting with different editors (Zed, Visual Studio Code, CLion, Qt Creator, etc.) and just say that the most consistent and enjoyable editor experience for working on Koto has been CLion. Just remember to use the "Reload CMake Project" option if it ever hurts itself in confusion. Great, now we have (checks notes) almost 21 hours of content to get through, so I will try to condense this down a fair bit.

#Day 1

The first proper day of development was spent on a mix of things. I wanted to get some initial UI bootstrapped just to get the gears turning on that before switching gears to the configuration and data lake (build or extract from one or more data sources, transform it into a format for Koto, and load it into the application). For the user interface, I got acquainted with some useful applications suggested by the livestream: Kirigami Gallery (gallery of Kirigami widget examples) and Icon Explorer (self-explanatory). I built some initial primary navigation, mirroring the primary navigation of old Koto ("if it ain't broke, don't fix it"). Before I could start work on building the file indexer, I first needed to know what content I was going to index to begin with. Aside from "sane defaults" like XDG_MUSIC_DIR, that meant implementing the configuration parsing and defining the structure for the config itself. I settled on the below config format, which is essentially identical to old Koto.
["preferences.ui"]
album_info_show_description = true
album_info_show_genre = true
album_info_show_narrator = true
album_info_show_year = true
last_used_volume = 0.5

[[libraries]]
name = "Music"
path = "/home/joshua/Music"
type = "music"
Instead of just using C++ filesystem APIs, I learned about Qt APIs such as: QDir, QFile, QFileInfo, and enums like QStandardPaths::StandardLocation which would prove useful for getting the application location. With configuration reading out of the way, I did some initial work on the data structures for KotoTrack, making sure to carve out a constructor that took in metadata from KFileMetadata in preparation for the next Koto development session, and set up some header definitions for the FileIndexer.

#Day 2

The second day of Koto was spent building out the file indexer and let me tell you, this was considerably easier thanks to QDirIterator compared to what I had to do in GLib land. Of course, it helped that I had done all this before, I used KFileMetaData instead of taglib directly, and I am yet to implement all the cool tricks for manual filename parsing that I still want to do for handling audiobooks and tracks without proper ID3v2 metadata. All that said, things still things went much more smoothly than I was expecting. Within just that day's work, I was able to implement a functional file indexer that showed my tracks, in the correct albums, for the correct artists. I had even built out a similar "Cartographer" class to my old Koto that held all the hash tables, using QHash with QUuids to ease lookups during indexing. 🎉 That day, we cooked.

#Day 3

On the third day, the focus was on taking our indexed files, storing them in a sqlite3 database, and reading them back from the database as part of the application loading. As I mentioned earlier in the blog, my original line of thinking was that I was probably going to use sqlite3 directly. After all, why would I expect to be blown away yet again by Qt having something very usable out-of-the-box? Of course, as you have already learned, I fortunately discovered Qt SQL and they have some really good guides to get started with the basics of connecting to a database, executing queries, and reading with records. With QSqlQuery, I was able to trivially write commit functions for all my relevant classes (KotoArtist, KotoAlbum, and KotoTrack) with binding values. You still have to write the queries of course, as it is not an ORM, but it absolutely beats out the madness I was doing with g_strdup_printf with a function that basically called sqlite3_exec. Was there a better way of doing it back with old Koto? Probably. But I am just comparing old Koto with the new. The fact is that the binding values method of accomplishing it with QSqlQuery is well documented and not hidden away, so the "nice" (in my opinion) way of doing it was the most obvious one. Code comparison of KotoTrack on old versus new:
	gchar * commit_msg = "INSERT INTO tracks(id, artist_id, album_id, name, disc, position, duration, genres)" \
						 "VALUES('%s', '%s', '%s', quote(\"%s\"), %d, %d, %d, '%s')" \
						 "ON CONFLICT(id) DO UPDATE SET album_id=excluded.album_id, artist_id=excluded.artist_id, name=excluded.name, disc=excluded.disc, position=excluded.position, duration=excluded.duration, genres=excluded.genres;";

	// Combine our list items into a semi-colon separated string
	gchar * genres = koto_utils_join_string_list(self->genres, ";"); // Join our GList of strings into a single

	gchar * commit_op = g_strdup_printf(
		commit_msg,
		self->uuid,
		self->artist_uuid,
		koto_utils_string_get_valid(self->album_uuid),
		g_strescape(self->parsed_name, NULL),
		(int) self->cd,
		(int) self->position,
		(int) self->duration,
		genres
	);

	if (new_transaction(commit_op, "Failed to write our file to the database", FALSE) != SQLITE_OK) {
		return;
	}
Source
  QSqlQuery query(KotoDatabase::instance().getDatabase());
  query.prepare(
      "INSERT INTO tracks(id, artist_id, album_id, name, disc, position, duration, genres) "
      "VALUES (:id, :artist_id, :album_id, :name, :disc, :position, :duration, :genres) "
      "ON CONFLICT(id) DO UPDATE SET artist_id = :artist_id, album_id = :album_id, name = :name, disc = :disc, position = :position, duration = :duration, "
      "genres = :genres");
  query.bindValue(":id", this->uuid.toString());
  query.bindValue(":artist_id", !this->artist_uuid.isNull() ? this->artist_uuid.toString() : NULL);
  query.bindValue(":album_id", this->album_uuid.has_value() ? this->album_uuid.value().toString() : NULL);
  query.bindValue(":name", this->title);
  query.bindValue(":disc", this->disc_number);
  query.bindValue(":position", this->track_number);
  query.bindValue(":duration", this->duration);
  query.bindValue(":genres", this->genres.join(", "));

  query.exec();
Source at the time of posting this article. Subject to change. Hot damn. Is that not just so much nicer? Reading various records from the database? I am not going to explain to you the nine circles of hell and the satanic incantations that was the old Koto code for all of this. Trust me, it was bad. Sure, I sucked at writing it then, but I promise it was not all me. With the Qt implementation, you take in a reference to the QSqlQuery and the QSqlRecord. You get the index from the record for the given name like id and you use QSqlRecord functions for casting it directly to the type you need like QString or int. No sorcery needed.
  KotoTrack* track = new KotoTrack();
  track->uuid      = QUuid {query.value(record.indexOf("id")).toString()};
  auto artist_id   = query.value(record.indexOf("artist_id"));
  if (!artist_id.isNull()) { track->artist_uuid = QUuid {artist_id.toString()}; }

  auto album_id = query.value(record.indexOf("album_id"));
  if (!album_id.isNull()) { track->album_uuid = QUuid {album_id.toString()}; }

  track->title        = QString {query.value(record.indexOf("name")).toString()};
  track->disc_number  = query.value(record.indexOf("disc")).toInt();
  track->track_number = query.value(record.indexOf("position")).toInt();
  track->duration     = query.value(record.indexOf("duration")).toInt();
  track->genres       = QList {query.value(record.indexOf("genres")).toString().split(", ")};
Source at the time of posting this article. Subject to change. By the end of my stream, I had Cartographer updated, I was reading content from the database, doing the necessary commits for each class, and skipping file indexing when we didn't need to. Of course it was only at the end of the stream I was told that KDE has something called FutureSQL which provides automatic database migrations, mapping to objects, and more. So there is still room for improvement there. But hey for one day's work, I was pretty happy. My vacation was ending as well, so leaving things on a high note before heading back to work was quite nice!

#Day 4

Anakin / Padma meme Congratulations Josh, you have files in a database, can we show them in the GUI now? No apparently that was the less straight-forward part and probably a part of the Qt6 documentation that needs some work. To facilitate this, you need to ensure your C++ classes inherit from QObject, use various macros like: Q_OBJECT, QML_ELEMENT and optionally QML_SINGLETON; be careful to not accidentally instantiate more than one instance of what you were expecting to be a singleton class, etc. I took the opportunity to change how we store references to KotoArtists from a QHash to a class (KotoArtistModel) inheriting from QAbstractListModel. The benefit of this is direct integration with QML ListView models. That all sounds pretty simple when it is all put that way but the journey extended beyond the stream and fortunately George (the same one mentioned earlier!) was kind enough to file an issue explaining why my ported Cartographer class was not able to show artists in the QML ListView I made...well to list artists. Thank you very much, the screenshot for the featured image would be less exciting otherwise. Some guides if you are interested:

#Summary

To summarize the work itself, I would say I have been very pleased with what has been accomplished over the course of those few days working on Koto and a couple evenings of tinkering. I was able to get to the point of actually listing artists, albeit after the latest session. There is still a mountain of work ahead such as: the artist view (displaying albums, tracks within those albums), playlists, and of course the actual playback of content; however I think thanks to my past experiences with old Koto, Qt being a more feature-rich tool~~kit~~belt, KDE Frameworks being excellent, and some very patient people -- I will be able to get back to feature parity with old Koto sooner than even I probably imagine. Maybe it is a honeymoon phase. Maybe the grass really is greener on the other side. No matter, I am very happy I resurrected Koto and I look forward to all of the new and interesting problems I encounter. Maybe one day you will even be able to get it on the device now resting on my monitor shelf. Picture of a Jolla C2 running Sailfish OS 5.0 So here's to more Dev Diaries! 🍻