December Adventure 2023

Last edited: 2024-01-01    34 minute read

December Adventure

I never really gel'd with Advent of Code. I think it's just not really for me. But while I didn't really enjoy the contents event itself, I think the premise is fun, and it's nice to be able to do something with a bunch of people who share similar interests with you. It is perhaps because of that desire that I immediately found myself on board with December Adventure as soon as I heard about it.

The December Adventure is low key. The goal is to write a little bit of code every day in December.

The project towards which I'm going to do my December Adventure development is the CLAP plugin library for ā†¹lature. While I'd love to do a brand new plugin each day (and I probably have enough ideas to get through the month doing that), the premise of the adventure seems to encourage low-key, consistent progress, so I'm going to aim for something a little less manic, and set my end-goal as developing a meaningful feature every day.

Watch my progress here: https://git.sr.ht/~jaxter184/tlature-plugins

Current list of plugins:

  • chord name to note generator
  • simple instruments
    • sampler
  • simple fx
    • filter
    • compressor
  • livestock auctioneer physical model
    • maybe even something as simple as a singing voice synth with a southern drawl
  • tools
    • osc output
    • automation
    • adsr
    • lfo
    • step sequencer
    • https://nimble.itch.io/catjammer

(Not intended to be exhaustive, just to show the general roadmap)

day 01

While this wasn't my initial plan, I spent basically my whole evening reorganizing the ā†¹lature plugin repositories. Today, I:

  • Renamed structs to make the plugins consistent with each other
  • Moved structs to make code easier to navigate
  • Combined the tlature-dev and tlature-std plugin repositories
  • Made a cargo-generate template for creating new plugins

While this is all a lot of really helpful preparatory work (renaming and moving structs was particularly time-consuming), I'm particularly excited about the template. It was surprisingly easy to make, and I'm looking forward to both using it myself and also asking people who expressed interest in the project in the past to try it out to see how difficult it is for them to use.

day 02

Did some organization on the main ā†¹lature repository to make it more sturdy. The goal is to be able to easily point people to a specific commit or tag where everything hopefully "just works".

The most on-track December Adventure thing I did was making an event-offset plugin that offsets incoming events in time and pitch. Well, not every event, just note events. Also it doesn't do anything yet, it just passes events through. That is, I think it does, but I can't tell because I haven't set up a way to test it yet. So I spent the rest of my day working on a ā†¹lature feature to be able to define event output files. It's not done yet, but once it is, it should be helpful for debugging plugins in the future.

But it's December Adventure! So falling off course and working on something completely different and then not finishing it is still a success!

day 03

Trying to create plugins for ā†¹lature has really pointed out all the flaws of its current state. I thought it would be easier to make event plugins instead of audio plugins because it would be easier to debug and test for, but it turns out that not only have I neglected to set up any tests, but I've also completely underdesigned all my event handling. I'm not totally sure what I'm doing going forward, but at the very least, I added a feature to convert CLAP plugin event outputs back into OSC messages, so I can confirm that the event-offset plugin does indeed work as I would expect it to in its current form (i.e. it passes the events through as-is)!

I'm really liking this whole December Adventure thing. The flexibility of being able to explore things at my own pace gives me space to breathe and give this stuff the time it deserves instead of churning out solution after solution.

day 04

ā†¹lature is, by design, an OSC-powered DAW. Back when it was just a sequencer, that worked because all it had to do was generate notes and spit them out somewhere. But as the project grew into a more complete audio workstation, I decided that I wanted the sequencer to be "aware" of the downstream consumers of the messages, and once plugins were added, I also had to add some sort of converter between OSC and various CLAP events. Until now, I was able to get away with that because I was only doing the lossy conversion from OSC, a very flexible, generic message format, to CLAP events, which is more structured. So now that I want to convert them back, I'm not sure how to do it.

To illustrate, there are multiple OSC messages that can convert to the same CLAP event. /notes/v0 i 60 and / ii 0 60 both convert to middle C on the 0th voice. So if a CLAP plugin outputs a middle C, which of those OSC messages should it convert to?

The solution that I came up with is that it shouldn't. OSC conversions should only go one way. Maybe in the future it would make sense to set up some sort of conversion, but for now, the plan is that any plugin that wants to output events for ā†¹lature to send to other plugins should output OSC events only.

To facilitate this, I created an OSC message extension that provides OscEvent (that is, I copied the MIDI SysEx event format from regular CLAP and changed some names). It essentially consists of a buffer that is written to and read from using the rosc encode/decode functions.

Behold!:

fn process(
	&mut self,
	_process: Process<'_>,
	_audio: Audio<'_>,
	events: Events<'_>,
) -> Result<ProcessStatus, PluginError> {
	for ea_event in events.input {
		events.output.try_push(ea_event).unwrap();
		let packet = rosc::OscPacket::Message(rosc::OscMessage { addr: "test".to_string(), args: vec![] });
		let buf = rosc::encoder::encode(&packet).unwrap();
		let (ptr, len) = (buf.as_ptr(), buf.len());
		std::mem::forget(buf);
		let osc_event = OscEvent::new(
			EventHeader::new(ea_event.header().time()),
			0,
			unsafe { std::slice::from_raw_parts(ptr, len) }, // SAFETY: leaks are safe
		);
		events.output.try_push(osc_event).unwrap();
	}

	Ok(ProcessStatus::ContinueIfNotQuiet)
}
if let Some(ev) = ea_event.as_event::<OscEvent>() {
	let map = pd.event_map.entry(ctx.clone()).or_default();
	let packet = rosc::decoder::decode_udp(ev.data()).unwrap().1;
	match packet {
		OscPacket::Message(msg) => {
			map.push((ev.header().time().into(), msg));
		}
		OscPacket::Bundle(_) => todo!("handle OSC bundles"),
	}
}

There are a lot of things that aren't great about the code above (memory leaks, allocation on the audio thread, ignoring bundles), but those are problems for future me to solve.

day 05

(When reading the following, it may help to know that Location is more or less just a reimplementation of PathBuf that uses String instead of OsString. If that isn't clear, just think of Location as a string that represents a path, like /home/jaxter/music/ninja_tuna.flac)

I think the most difficult thing with ā†¹lature so far has been deciding how routing works. The general philosophy is as follows:

  • routing should be easy to understand if explained properly
  • Locations should be generally analagous to paths on a filesystem, and the routing syntax should reflect that
  • for people editing the project file directly (for example, in a text editor), opportunities for error should be minimized
  • the "royal road" should be encouraged by the syntax, generally by making it short and simple compared to less common alternatives
  • the actual logical code for routing should be relatively simple, with few edge cases

I believe the current method achieves this, though perhaps not to the extent I would like. That being said, having added OSC outputs to plugins has shown me the flaws of my current scheme, so I made a few additions to adjust the routing behavior. Currently, the process data is structured roughly like this:

ProcessData {
	audio_map: BTreeMap<Location, AudioBuffer>,
	event_map: BTreeMap<Location, Vec<rosc::OscMessage>>
}

In this case, there are two places that can represent the "path" of the event: the Location key in event_map, or the rosc::OscMessage, which contains an address field. The changes I want to make are to resolve an ambiguity as to whether the new path should be reflected in the ProcessData event_map key or in the OSC message's address. I've decided for now that the event_map key of any newly routed message will simply the current context (that is, the path of the parent block), and the rest of the path will go in the OscMessage address. I'm a little hesitant to split it up like that because it makes matching message paths difficult later down the pipelite, but chances are, it will need to be refactored again soon anyway, so I'm fine leaving things a little messy and placeholder-esque for now.

All of that planning aside, the only change I made today was to reflect this design in the loop that gets the events of Processor::Plugin.

day 06

After making the changes yesterday in the Processor::Plugin processing, I had to change Processor::Route behavior to be consistent with it, which broke some tests. I think in cases like these when I make some weird change that might affect behavior, I'm really appreciative of my past self for setting up these tests to give me a more clear path to things working as they should. I wish I had the energy today to describe this particular case where I benefitted from a test, but for now, I will just say what has been said many times, and more persuasively by many others: write unit tests for your code!

day 07

Honestly not sure why I thought I needed OSC support in order to make an event offset event. I can just make ā†¹lature pass note events to downstream processors. Maybe I just wanted to not deal with implementing both an OSC channel and a CLAP event channel, and just only dealing with OSC events?

Anyway, today I just added some logic for handling CLAP NoteOnEvents in addition to OSC events.

Things have been slowing down, and I'll probably skip tomorrow to do some non-computer-touching stuff. I'm a little sad that things haven't been as adventurous as I had hoped when starting December Adventure, but I'll be ambiently thinking about how I can shift my progress to be more exploratory going forward.

TODO: Implement CLAP event channel routing

day 08

(rest day)

day 09

As alluded to in day 07, the thing about December Adventure that I like the most is the idea of exploring a little bit of a fun idea every day. The most recent few days have felt like a lot of slogging in place, and I was hoping to get out of that funk, so I've expanded the scope to be anything ā†¹lature-related rather than just plugins, with a priority on fun new features that I don't see in a lot of music software or TUI programs.

First on my list is a file/string picker pop-up. While common in music software, it's not so common in TUIs, so there are a couple interesting things to think about when it comes to the design. But before working on the feature itself, I first need to figure out how my GUI is going to work in general, in particular how various elements are created/activated. Two organization methods that I've run into in the past are stacks and layers. With stacks, you push and pop overlays, and with layers, you activate and deactivate overlays. The main difference is that with stacks, overlay order can be dynamic, while with layers, overlay order is fixed.

When it comes to the interface, it should be easy for the end user to be able to tell how their actions will translate to actions in the software before they actually perform them. A stack throws a wrench in this understandability, because with a given screen, there may be 3 different overlays, plus something going on in the status bar. Rather than using something like color or highlighting to show what is currently focused, I think making the layer order fixed would train people to instinctually know what's focused by assigning a sort of "priority" to each type of overlay. For example, if a help dialog always has higher priority than the numerical argument input, then you know that if you see a help dialog, you can't perform any num arg inputs. Things get a little tougher with the command bar, because it's pretty subtle whether or not its active from just the command input itself, so adding a command info dialog and autocomplete assistance menus will make it clearer that the command entry is actually active.

I did a bit of messing around to see what things would be like in terms of implementing each, and I decided that while both have their flaws, I like layers slightly more.

One thing that makes it difficult is that I want overlay state like the numerical argument and replacement values to be a dynamic "variable" rather than a statically define Rust struct, with the goal of making it more accessible from the hypothetical future scripting language. I implemented rhai support in the past, but removed it because I didn't like how I had to structure the application state to accommodate it, so maybe it's worth considering removing that entirely.

  • TODO: Implement layer UI
  • TODO: Add string picker pop-up

day 10

Spent all day today spinning my wheels trying to figure out how I should refactor layers to work. I think I'm going to abandon this for now and come back to it after December Adventure ends. (Update: I did not abandon this, and it now works pretty ok)

day 11

Not even sure what counts as December Adventure anymore, but spent the evening configuring Zellij. It's a pretty neat piece of software, and while there are a couple things that annoy me about it (mostly visual clutter), I learned a lot of things that seem to be very helpful for higher-level planning for ā†¹lature.

While not intentional, it really helped me solidify a big decision with ā†¹lature: session management. I have two choices when it comes to managing ā†¹lature sessions: I could do it all internally, and have the program itself organize the various panels and windows and tabs, or I could hook into the window/session management of some other program.

Just to give a run down of what exactly I mean by session management, there are various programs that manage different workspaces/windows/sessions:

  • Kakoune has a concept of "buffers", which are basically all the documents you have open, plus a few extra scratch areas where you can do things like peruse the various references of an identifier to goto, or see debug information, and all of the buffers are associated with a session, which can be used to connect a different client to the current set of buffers.
  • Bitwig has tabs, which can each have a project, similar to a web browser.
  • Sway, my window manager, can organize windows in various ways, but the most basic method is tiling windows into various panes, which show up side-by-side.
  • Zellij has pretty much all of these: panes fit into a tab, and tabs are hosted in a session, which a seperate Zellij client can connect to.

I hadn't really given too much consideration, and assumed that I would be implementing this myself in ā†¹lature, but it seems like Zellij has enough functionality (through things like zellij action) for ā†¹lature to be able to integrate pretty seamlessly with it (given an appropriate configuration).

There were also a couple other things that exploring Zellij helped with. Being a TUI Rust program, it runs into many of the same restrictions that ā†¹lature does, but compared to Helix (another TUI Rust program), there were a few aspects of Zellij's approach that I liked more, particularly with regards to keymapping, mode management, and plugins, all of which I'll probably talk about as December Adventure continues. Also, it uses KDL!

Despite not having even looked at any ā†¹lature code today, I made a lot of progress on it! While I'd like to say that it was all part of the plan, and that December Adventure put me in an exploratory, casual mindset that led to me making the decision to take a step back and just check out some other stuff, that's not really what happened. I just happened to check out Zellij on a whim, and it just happened to be really helpful for getting a better idea of what I want ā†¹lature to be. That being said, I'm pretty sure I wouldn't have tried Zellij if I was doing Advent of Code right now.

day 12

Recently, a friend started working on a Discord bot, and it made me jealous, so I decided to go fix up the bot I made for ā†¹lature. My bot was pretty simple: you'd send it a message, and it would reply with a rendered .wav of the output.

The main thing I had to do to fix it was to migrate the rendering code to use the new ProcessData structure, which greatly simplified the setup. Next, I just need to update the part that interfaces with Discord to match. There's also the concern with calculating the tail, but hopefully that just works without too much fuss.

An example of the old message format:

@jaxter184-bot-test
sheet

ticks 6985_0000
"" {
	bass {
		"" (i0)"
			,21 [ _ ] [ ,21 _ ] . ,2D _ ,21 _ ,1F ,21 _ [ ,2D _ ] . ,1F _ [ ,21 _ ]
			. [ ,21 _ ] ,21 . ,2B ,2D ,24 ,25 ,26 . [ ,32 _ ] [ ,24 _ ] . ,26 _ [ ,21 _ ]
			. [ ,21 _ ] ,21 _ [ ,24 _ ] . ,21 . ,27 ,28 _ ,26 _ ,24 _ ,21
			_ . ,21 . ,23 . [ ,2F _ ] ,23 ,24 . [ ,30 _ ] ,26 ,27 ,26 _ ,24
		"
	}

	drums {
		kick  (F)",T ....... ,T ,T ..... ,T ,T . ,T ..... ,T ..... ,T . *6;20 ,T . *E;20 ,T . *8;20"
		snare (F)".... ,T ... *38;8"
		hat  (i0)"*4. ,C0 ,50 *3A;2"
		crash (F)",T *3F."
	}
}

graph

"" {
	"" "mixer" {
		(ai)a01 "../bass/"
		(ai)a02 "../drums/"
	}
	lead "sisqsa" state=(inline)"q"
	bass "sisqsa" state=(inline)"q"
	drums "beep-kit"
}

day 13

The goal was to adapt the rest of the ā†¹lature discord bot to work, and while that did get done, I unfortunately ran into an issue with the tail where it would always return a length of 1. While trying to figure that out, I adjusted the tests so that they would dump the audio output into a file, and in doing so, I uncovered another issue: the test audio output was not the same as the output when opening the test projects with the TUI. I'll hopefully figure it out tomorrow.

day 14

Spoiler: I didn't figure out yesterday's problem. I had a few things going on today, and I didn't do as much work on ā†¹lature as I wanted to. But, in the spirit of December Adventure, I at least took a look. I've got a little TUI tool called twv that I use to inspect audio files, and it seems that there's either a bug in twv or something weird with the output, because I can't see it. It's likely that the signal is entirely outside the -1.0 to 1.0 range, but since I'm writing it to a WAV file as integers, it should clip? I think? I'll figure it out tomorrow.

day 15

We're already halfway through December Adventure! The days have been whizzing by, and I haven't completed a single plugin. But that's OK, because I've been exploring tons!

I've refactored the twv code a bit, and added character markers for zero, NaN, and end of file.

After fixing this and using it on the output, I found out that:

  • Using Ctrl+C to close my recording program results in a malformed WAV file that thinks it has a length of 0 despite being 188kB
  • When I gracefully shutdown the recording program, it seems that the expected sine wave is mostly intact, except for a few discontinuities, which I assume occur at buffer boundaries.

To show this off, I want to make a little asciinema recording where I create a WAV recording from ā†¹lature, and open it in twv. To do so, I need to add a feature to my CLI recording utility to let it connect to ports in the middle of its execution, and not just at startup. While working on that, I ran into a bug. Did you know that a backgrounded program can't read a line from stdio while starting an async JACK client? Neither did I! (Or maybe you did, in which case, why didn't you tell meā€½)

After several hours, I think I've fixed everything I needed to make the recording that demonstrates the problem I had yesterday. While I could probably make the recording at this point, it's getting late, so I'll leave learning autocast and making the recording for tomorrow.

day 16

There were a couple extra things I had to do to get my utilities to work with autocast. The main incompatibility was that the point in the recording when ā†¹lature starts is not fixed. Sometimes it takes longer to start up, so I couldn't repeatably zoom in on the exact point of the discontinuity. To fix this, I added the [ keybind to twv, which pans the waveform such that the first nonzero sample is at the very left of the screen. From the first nonzero point, the buffer discontinuity is always exactly the buffer length.

But after sorting through all that, I present the first December Adventure demo! (Though it seems there's an issue with how box-drawing characters are drawn in the asciinema player, so you may need to download the asciicast and use the asciinema CLI to play it back)

(link to demo)

Commands used:

$ cat tlature-engine/projects/load/main.tlgr.kdl
$ jaxrec -p 1 tlature:output_1 -e &> jaxrec.log & tlature tlature-engine/projects/load
$ du out.wav
$ twv out.wav

To break down the chunky command in the middle:

  • jaxrec: my recording utility. The recording ends automatically when all the input ports have been disconnected.
  • -p 1: open one port in the jack client, and write a single-channel (mono) WAV file.
  • tlature:output_1: the positional arguments to jaxrec match output ports to each input using a regex, or simply the first port(s) that start with this string.
  • -e: ignores the stdin. Usually, jaxrec ends when enter is pressed, but as described yesterday, this prevents the JACK client from starting, so we use this flag to disable that feature.
  • &> jaxrec.log: pipe all stdout and stderr to the file jaxrec.log.
  • &: run the previous command (jaxrec) in the background, and start the next command immediately. Contrast with ;, which waits until the previous command finishes before running the next command.
  • tlature tlature-engine/projects/load: open the project file in ā†¹lature.

While I was getting this working, I realized why the output was not consistent between the TUI and the tests: the tests have extremely large buffers, such that the entire test runs in one buffer, so there are no buffer edges for the test tone to be discontinuous across. After fixing that, the tests seem to be pretty much in line with the behavior of the TUI, so I think I can move forward with debugging why the events aren't getting read in the tests, after which I can move on to fixing the Discord bot.

day 17

Started a bit of work on a KDL highlighter for kakoune. It's just a basic implementation that colors numeric values and quoted strings, and sets the indentation on each newline. Not directly relevant to ā†¹lature, but it'll at least make the files a bit easier to read and write.

TODO: Features that I plan to add are properly highlighting the types, node name, and comments.

day 18

Did a little bit more diagnosis on why my tests are behaving strangely. I made two findings:

  • The OSC messages are not stored in chronological order. My assumption was that clack sorts the order when it creates the event buffer, but that could very well not be the case. In fact, it may be better for performance for that to be done on ā†¹lature's side rather than on clack's.
  • The test-tone plugin sees exactly one parameter change that sets the frequency to 440 Hz. It is likely that this is from the first event. Potentially related to the above bullet point.
  • When adding an abridged version of the ave-maria example project as a test, only the arpeggio voice is output in the test, and the reverb plugin (michaelwillis' Dragonfly Reverb) raises some sort of DPF error. Possibly also due to out-of-order events. (Update: this was not due to the out-of-order events, and is still an issue)

TODO: Tomorrow I'll try actually putting the events in the right order before they're added to clack's event buffer.

day 19

Did a couple of minor fixes for event processing. As alluded to yesterday, I fixed the event order, as well as an additional bug where the root block sends a blank OSC message on note off.

day 20

Missed one spot where transport events are sent after all of the other messages instead of being interleaved with them. The solution was calling sort on the event buffer (which I should have been doing in the first place), so events being sent out of order should no longer be an issue.

TODO: The plan tomorrow is to take a shorter day by organizing commits and checking the behavior of all my tests and examples.

day 21

Organized the commits as planned, and fixed the Discord bot, so now everything works as it did before I made all the ā†¹lature changes.

I updated to the latest version of serenity (a Rust crate interface to the Discord API), but I think what I should've done is use poise, which is a higher-level framework that makes things like context menu bot commands easier. I'm not gonna worry about that for now, and go back to ā†¹lature work tomorrow.

This weekend is also the start to my time off work, so I'm hoping to spend the time doing some particularly fun stuff with ā†¹lature, and maybe get another demo out. I don't want to put too much pressure on myself to get stuff done, but it's the final stretch of December Adventure, and I want to continue hiking along as I've been doing up until now. I think the theme of "do a little bit every day, with an exploratory mindset" has been very beneficial so far, not only for the project itself, but also my attitude towards it.

day 22

Made some plans for how the UI layers are going to be implemented. And that's not a euphemism for not having done anything, I did actually write some code today! Comments count as code, right?

Anyway, I'm chalking today off as a sort of wind-up to a future follow-through. I deserve it. In fact, I always deserve to take things slow.

day 23

I added a bunch of stuff today, and pretty much all of it is stuff that I discussed on day 09:

  • Refactored to a layer/mode hybrid UI system
  • Refactored the CommandTemplate struct to be (hopefully) a bit easier to work with in the future when I add documentation for individual arguments
  • Added a command popup interface, which is the main use case for the string input dialog that I mentioned on day 09
  • Added commands for adding, inserting, and removing a processor

But maybe it's easier to describe with a demo, showing the newly added processor commands:

(link to demo)

TODO: I forgot to also demo the new command popup (which is probably the more fun and interesting addition), but it's late, so I'll do it tomorrow.

day 24

  • Added a structure to communicate information about the currently selected item, so that you can use commands like :insert processor delay without specifying the block that the processor is going to be added to (before, you had to use :add processor --processor 0:this-block/3 delay)
  • Improved the command popup. Hints are now filtered based on whether they start with the current typed value, and you can use Tab to navigate hints.
  • TODO: Ran into a bug where processors are added even if they don't exist, which has proven to be a bit tough to fix because of the way that ā†¹lature is structured
  • TODO: There seems to be a hitch in the audio thread, so I'll probably do some profiling after December Adventure ends, and maybe look more into the real-time safety

I'll be honest, these log entries are much more for myself than they are for other people. I think something that would help with that is drawing a clearer narrative. For example, starting with how I want the user flow to work, some bad approaches to accomplishing that, and then my final approach.

But until then, another demo! (The one I said I'd make yesterday):

(unfortunately, the box drawing characters are still a bit broken)

day 25

  • Fixed the bug where adding a plugin that doesn't exist crashes the program

I started some work on refactoring the structure I made yesterday. As it is now, it just passes in arguments (like --block) automatically, but really, I want the argument parsers to understand the value, so I can use relative paths.

day 26

Finished refactoring the environment information passing. I'm really happy with how usable commands like rm processor are now. You can now use a relative path to add and remove processors and blocks, which also lets me use . as a default value for the --block and --processor variables, which gets interpreted as the currently selected item.

day 27

Started some work on refactoring the OSC channels to more general CLAP event channels. Right now, I'm creating OSC events by leaking memory which is definitely not sustainable, and I'm approaching the limit to the hacks I can get away with by just leaking, so my plan for tomorrow is to get a better idea of what CLAP needs from a lifetime perspective, and adjust my implementation to better align with that.

day 28

Finished changing OSC channels to CLAP event channels. Did a lot of strange stuff that is definitely not real-time safe (I will leave that for future me to figure out), but it passes the events as needed, and didn't really require too much thinking. I just changed one of the types from Vec<(u64, OscMessage)> to clack::EventBuffer, and followed the Rust compiler's instructions from there. The most involved part was writing a function to filter the events in the buffer and produce a Vec<(u64, OscMessage)> for the OSC file writer.

Hooray for Rust and its ability to facilitate fearless[citationĀ needed] refactoring!

A remix of
the 'no fear - one fear' meme by Branson Reese. First pane: Someone
standing in a T-pose with a shirt emblazoned with the words 'NO FEAR'.
A second person approaches from out of frame. Second panel: A close up
of the second person's shirt, which reads 'what about real-time safety'.
Third pane: The first person, still in T-pose, but their shirt now says
'ONE FEAR'.

Just yesterday, ā†¹lature received the first ticket submitted by someone other than me! There are a few requests that folks have pointed out to me over private channels because I asked them, but this is the first one from someone I've had no prior contact with, and I didn't have to solicit them for feedback. It might seem pretty insignificant from an outside perspective, but personally, it's really cool to see that someone cares enough about this project to let me know that something is broken, even if it's just because they want to check it out as research for their own project. In fact, I'm pretty flattered that they looked at it and thought it would be a good reference in the first place.

day 29

Fixing windows support, as requested. I actually should have been working on it for a while because someone else had also requested it (via private channels, and after I solicited them to try it out), but the high of receiving a ticket really motivated me to actually work on this.

There are three main things that are *nix-specific:

  • The conversion OS paths to C strings in the resource directory extension
  • The stdio redirect shenanigans to print plugin stdout to a file while still printing the TUI to the terminal
  • JACK (technically cross-platform, but I dare you to find me a Windows or MacOS user who regularly uses JACK)

Today, I worked on the first two. The resource directory extension was pretty easy, I just had to follow the compiler's instructions. With the stdio stuff, my solution was to ignore it, which will result in issues when plugins print errors, but that also generally shouldn't happen, and if it does, it means there's probably something in either ā†¹lature or the plugin that needs to be fixed.

There may be other things that I'm forgetting, but I suppose I will find out when I actually try to use ā†¹lature on Windows.

day 30

I had forgotten how awful of an experience Windows is. I had to install a bunch of stuff (Rust/cargo, git, ASIO4ALL, JACK), and for the most part, it went fine, other than the part where I had to agree to a license (that I didn't read) to use the MSVC compiler (šŸ¤¢). It was a bit annoying to have to go to each website and download the installer, but that's easy peasy compared to the weird display/USB issues I was having the whole time. For some reason, my USB hub kept disconnecting every couple seconds, and Windows really didn't like my second monitor. This last one is probably on me, but the peak disk speed was around 50MB/s, probably because I'm connecting my NVME SSD to my computer via USB type C.

Anyway, after I got everything installed and got past the hiccups of running JACK on a Windows machine, the TUI showed up and I was able to navigate around the UI! While I will almost certainly never use this, it's nice to see that it at least works, and didn't take too much extra effort to adjust things for Windows compatibility.

After checking that, I added some help windows to make it easier to know what the available hotkeys are in any given mode. I'd like to eventually do this in a more structured way such as implementing default keybinds in a KDL file rather than manually written out in Rust, but to implement it how I'm imagining would likely require me to add a scripting language (and not roll it back this time), which would also let me implement the keybinds as a file.

I think I'll explore Lua support eventually (since rhai was a bust), but since tomorrow is the last day of December Adventure, I think I'll try to top it off neatly by finishing the event offset plugin, bringing this adventure full-circle to what I started the month with.

day 31

It's the last day of December Adventure! Today, I fixed event buffers so that CLAP events are properly passed down. I assume there are still a bunch of problems with it (especially with regards to non-serial signal paths and nested groups), but for the most part, it seems to work as expected. Most importantly, the event-offset plugin works as intended! If I put it between the tracker processor and the physical/marimba plugin, it shifts the pitch up by the designated amount. Right now, that amount is just a default value of 20 because parameters are kind of a hassle that I don't want to deal with at the moment. But it works! The first task (at least, the first "actual" task) that I set out to complete for December Adventure is now finished!

retrospective

The main thing I was surprised about when it came to December Adventure is how consistent I was. There were a few days where I did less than 10 minutes of work, and one day that I didn't do anything at all (though in my defence, I had a lot going on that day, and knew a few days in advance that I wouldn't be doing anything), but other than that, I made meaningful contributions towards my project every day this month! Looking back at my original plan, it's laughable that I ever thought I would be capable of "a brand new plugin each day", or the "meaningful feature every day" that I fell back on, but even with "just touch the code at least once every day", I'm really happy with all the progress I made on this project.

As for why I think I was able to do it, I think there are a few key benefits to December Adventure:

  • Choose your own adventure - Whatever you decide to work on is something that you get to pick, so you're naturally more inclined to do it compared to, for example, the coding interview-esque challenges of Advent of Code.
  • Relaxed - There isn't any competitive pressure involved, and you're just working on stuff because you want to.
  • Minimal - There's a sort of sentiment that I picked up on that any amount of progress is valid progress. For me, the first minute of working on something every day is the hardest, usually because of decision paralysis, but by weighing 10 minutes of work against failing the task in the first week, or breaking a 2 week streak, I found it much easier to just get started on literally anything, which usually led to multiple hours spent happily chugging away.

There are a few things that I think could have been better:

  • Community - While I did keep up with a few other peoples' December Adventures, mostly through the #DecemberAdventure Fediverse hashtag, there wasn't much of a dialogue. I would read other people's logs, other people would read mine (hopefully, though I don't blame them if they didn't; my log is over 6000 words at this point), but I didn't feel like I had anything meaningful to add to their progress, and therefore didn't find myself commenting on their progress.
  • Refactoring - I personally found myself doing a lot of refactoring and bug fixing rather than exploring new things. I think to some extent, this was inevitable, but it would have been nice to somehow have been aware of this in advance so I could have more realistic expectations about how I was going to spend the month. Which isn't to say that I regret it! I'm really happy with everything I did during December Adventure.

Anyway, overall, it was a great month and I'm really glad I did it. Thanks to eli and all the other folks who did December Adventure! Here's hoping 2024 is the year of the tracker on the Linux desktop!