Category: Programming

  • Building ‘use English;’ into the Perl core

    Perl has a list of “stuff we really want to add to the language that needs someone to code it in C,” called PPCs. PPC 14 adds English aliases to Perl’s controversial “punctuation” variables (like $", $?, $., etc.), and I’ve decided to try taking this one on.

    I know some of the internals stuff from a long-ago class at a Perl conference, and from Jarkko’s chapter on the internals in Advanced Perl Programming, but this is the first time I’ve actually dived into serious C programming other than a fling with Objective-C back in the day…and I kind of like it.

    C absolutely is just enough tool to get the job done, and I’ve actually kind of missed that. Most of the work I was doing at Zip toward the end of my time there was all Scala, and Scala is a nice language but it’s…heavyweight. Takes an age to build and test. Even with a fairly big recompile of the whole interpreter, an edit-build-test cycle is pretty fast in C.

    The working experience is a lot like Go, just with way fewer guardrails.

    The coding experience, however, is very Zen: a series of enlightenments is necessary to proceed. I have looked at the Perl code a little before, but this project is much more complex than anything I’ve tried before in C. It’s really a process of reading the code, reasoning about it, taking a shot at something, discovering it was more complex that you thought, and continuing until the light dawns.

    First enlightenment

    I started off looking at the XS code in English::Name to see if I could out-and-out steal it. Unfortunately not, but it did start giving me some hints as to what I could do.

    (At this point, I’m going to start talking about Perl internals, and this may become a lot less clear. Sorry about that.)

    Each variable in Perl is represented by a “glob” held in a symbol table hash. A glob is a data structure that can hold all the different types of thing a given name can be — this is why, in Perl, you can have $foo and @foo and %foo all at once, because one glob (also known when working on the internals as a GV – “glob value”, I believe) can hold pointers to each kind of variable.

    I started out wondering if I could just alias the names directly when I saw them in Perl. For some of the read-only special scalar variables, you can do this by overwriting the SV (scalar variable) slot in the GV with a pointer to the aliased variable’s SV.

    The gv.c file contains the code that works with global variables, and the function S_gv_magicalize contains a big switch statement that parses the incoming variable names and then uses the sv_magic function to install hooks that are called when the variable is accessed (read or written). So the easiest, dumbest option is to try just sharing the SV that is created for the variable I want to alias with the new name.

    The code in S_gv_magicalize is essentially one big old switch statement; it uses a function called memEQs to check the incoming name against variable name strings to see if we should process the variables. The new variables I want to add all look like ${^SOMETHING}; this lets us look English-like, but looks different so we remember that this is a special variable. The code that parses the names converts the letter prefixed with a caret into a control character, so (say) ^C becomes \003; ^SOMETHING would be \023OMETHING, so that’s the string we plug in to memEQs:

    if (memEQs(name, len, "\023OMETHING)) ...

    Good, so we have a way to match the variables we’re interested in; now we just need to figure out how to alias the SVs. Poking around, I figured out that if I could find the target variable in the main:: symbol table, I could use a few of the macros that the Perl source provides to find the SV pointer in the old variable, and then assign it to the SV slot in the new GV I was creating. I realized I’d be doing this a lot, so I wrote a preprocessor macro of my own to do this. This was a bit tricky, because I needed to not just substitute a string into the get_sv call, but actually contatenate it into the string. Some Googling found me the C99 # operator that does the trick. Here’s that macro:

    #define SValias(vname) GvSV(gv) = newSVsv(get_sv("main::"#vname, 0))

    This tells Perl to look in the main:: symbol table for the variable whose name I’ve concatenated into the fully-qualified name and extract the contents of the SV slot for that variable. I then call newSVsv (build a new SV I can use out of this SV) and then assign it to the SV slot in the brand new GV that I’m building.

    Easy-peasy, add the aliases for all the variables…and this worked for a certain portion of the variables, but didn’t work at all for others. There also didn’t seem to be rhyme nor reason why this should work for some but not others.

    Second enlightenment

    I dived back into the code, and read it all through again. There were a lot of goto magicalize statements; (almost — of course, almost, why make it easy?) every special variable ends up jumping to this label, which calls

     sv_magic(GvSVn(gv), MUTABLE_SV(gv), PERL_MAGIC_sv, name, len);

    Well. What does that do? Going over to mg.c, where this function is defined, it takes a GV, an SV from that GV, both of which will be modified, and the last three parameters define the kind of magic to add, and name and len define the name being passed. Those are already set when we get here in gv.c, so my understanding at this point (yes, another enlightenment is needed!) was, “okay, we have a GV, and we’re passing a name and length, so this must be keying off the name to assign the right magic. Obviously if I can pass the GV I have but a different name and len, then the Right Thing will happen in mg.c and this will work perfectly.”

    So I tried a couple other variations to try to get remagicking the variable to work.

    1. Adding a block of code right below the sv_magic call to try to reassign the magic. This didn’t work; the call got made, but the variable did not have any magic.
    2. Passing a hardcoded alternate name and length to sv_magic. Also had no detectable effect.
    3. Refactor the code in mg.c so that I could create a new function that would allow me to pass a second name and len, so that I could do the reassignment inside mg.c instead. This also didn’t work, but not because the concept was wrong; I simply could not get the code to compile, because something in the macros was convinced that I should pass one more argument to the call to the refactored code, even though I wasn’t changing the calling sequence at all.

    I spent about a half-hour trying different variations of function calls and naming, and decided that was long enough; I needed to look again and see what was going on deeper down…and maybe find a way that was more compatible with the code already there.

    (Note: I did not want to change the calling sequence for sv_magic, or change its return value, because this would have been a change to the Perl API, potentially breaking lots of XS code, and potentially propagating lots of changes all over the Perl codebase itself.)

    Third enlightenment

    I went back to mg.c again and instead of looking at the code that applied the magic, I went to look at the code that implemented it instead. Reading through all of mg.c, and rereading gv.c, I found that the magic was implemented two different ways.

    • Some variables were set up directly in gv.c, in S_gv_magicalize. These were the variables that I’d been successful in aliasing with the SValias macro; they were read-only, and hard-linked to unchanging data.
    • The rest were set up in mg.c; they were detected as magic in gv.c, in S_gv_magicalize, which then jumped to the sv_magic call to pass the actual assignment of the magic to the SV.

    In mg.c, there are two different functions, Perl_magic_get and Perl_magic_set, which handle the magic for getting and setting the SV. (There are a bunch more Perl_magic functions, and it’s definitely possible I’ll need to learn more about those, but my current knowledge seems to indicate that these two are enough to do the implementation of the English variables.) We do the same kind of matching against names to decide what magic applies to the variable, and then execute the appropriate code to make the magic happen. This made sense based on what I knew already, and confirmed that the attempts to set a different name for the sv_magic call were not wrong; I just didn’t manage to implement something that did it properly.

    Given this, I decided to try implementing the English variations on two different variables: one a simple fixed read-only one implemented only in gv.c, and a second read-write one implemented in the Perl_magic_get and Perl_magic_set functions in mg.c to see if I’d actually understood the code.

    I also chose to go with the paradigm I’d seen throughout these big case statements: do the cases in alphabetical order, and use goto to jump to existing code that already implemented the feature. These gotos are always forward jumps, so they’re not quite so bad, but writing hard branches in code again certainly took me back a ways.

    Magic variable in gv.c alone: $] aliasing to ${^OLD_PERL_VERSION}

    $] provides the older floating-point representation of the Perl interpreter’s version. Looking at gv.c, there’s a block of code that looks like this:

             case ']':               /* $] */
             {
    
                 SV * const sv = GvSV(gv);
                 if (!sv_derived_from(PL_patchlevel, "version"))
                     upg_version(PL_patchlevel, TRUE);
                 GvSV(gv) = vnumify(PL_patchlevel);
                 SvREADONLY_on(GvSV(gv));
                 SvREFCNT_dec(sv);
             }
             break;

    We fetch the SV already in the variable; if it’s not already the version, then we make it the version, turn it into a number, stash it in the GV, make it readonly, and then decrement the refcount of this GV’s SV to prevent multiple frees of this data during global destruction at the end of the program.

    To implement ${^OLD_PERL_VERSION}, we need to catch it, and then do a goto to this code. Here’s the patch:

    | diff --git a/gv.c b/gv.c
    | index 93fc37da63..6c00b050db 100644
    | --- a/gv.c
    | +++ b/gv.c
    | @@ -2231,7 +2231,9 @@ S_gv_magicalize(pTHX_ GV *gv, HV *stash, const char *name, STRLEN len,
    |                      goto storeparen;
    |                  }
    |                  break;
    | -            case '\017':        /* ${^OPEN} */
    | +            case '\017':        /* ${^OPEN}, ${^OLD_PERL_VERSION} */
    | +                if(memEQs(name, len, "\017LD_PERL_VERSION"))
    | +                    goto old_perl_version;
    |                  if (memEQs(name, len, "\017PEN"))
    |                      goto magicalize;
    |                  break;
    | @@ -2430,7 +2432,9 @@ S_gv_magicalize(pTHX_ GV *gv, HV *stash, const char *name, STRLEN len,
    |              sv_setpvs(GvSVn(gv),"\034");
    |              break;
    |          case ']':            /* $] */
    | +          old_perl_version:
    |          {
    | +
    |              SV * const sv = GvSV(gv);
    |              if (!sv_derived_from(PL_patchlevel, "version"))
    |                  upg_version(PL_patchlevel, TRUE);

    It’s very straightforward; just reuse the code we have for $] for ${^OLD_PERL_VERSION} with a goto to that code. Tests show it works as expected:

    And running it:




  • Azuracast metadata redux

    Summary: all for naught, back to the original implementation, but with some guardrails

    Where we last left off, I was trying to get the LDSwiftEventSource library to play nice with iOS, and it just would not. Every way I tried to convince iOS to please let this thing run failed. Even the “cancel and restart” version was a failure.

    So I started looking at the option of a central server that would push the updates using notifications, and being completely honest, it seemed like an awful lot of work that I wasn’t all that interested in doing, and which would push the release date even further out.

    On reflection, I seemed to remember that despite it being fragile as far as staying connected, the websocket implementation was rock-solid (when it was connected). I went back to that version (thank heavens for git!) and relaunched…yeah, it’s fine. It’s fine in the background. All right, how can I make this work?

    Thinking about it for a while, I also remembered that there was a ping parameter in the connect message from Azuracast, which gave the maximum interval between messages (I’ve found in practice that this is what it means; the messages usually arrive every 15 seconds or so with a ping of 25). Since I’d already written the timer code once for force reboots of the SSE code, it seemed reasonable to leverage it like this:

    • When the server connects, we get the initial ping value when we process the first message successfully.
    • I double that value, and set a Timer that will call a method that just executes connect() again if it pops.
    • In the message processing, as soon as I get a new message, I therefore have evidence that I’m connected, so I kill the extant timer, process the message, and then set a new one.

    This loops, so each time I get a message, I tell the timer I’m fine, and then set a new one; if I ever do lose connectivity, then the timer goes off and I try reconnecting.

    This still needs a couple things:

    • The retries should be limited, and do an exponential backoff.
    • I’m of two minds as to whether I throw up an indicator that I can’t reconnect to the metadata server. On one hand, the metadata going out of sync is something I am going to all these lengths to avoid, so if I’m absolutely forced to do without it, I should probably mention that it’s no longer in sync. On the other hand, if we’ve completely lost connectivity, the music will stop, and that’s a pretty significant signal in itself. It strikes me as unlikely that I’ll be able to stream from the server but not contact Azuracast, so for now I’ll just say nothing.

    I’m running it longer-term to see how well it performs. Last night I got 4 hours without a drop on the no-timer version; I think this means that drops will be relatively infrequent, and we’ll mostly just schedule Timers and cancel them.

    Lockscreen follies

    I have also been trying to get the lock screen filled out so it looks nicer. Before I started, I had a generic lockscreen that had the station logo, name and slug line with a play/pause button and two empty “–:–” timestamps. I now have an empty image (boo) but have managed to set the track name and artist name and the play time. So some progress, some regress.

    The lockscreen setup is peculiar: you set as many of the pieces of data that you know in a

  • More adventures in metadata

    Despite the last set of changes, I still had problems with the iOS app losing its connection to the Azuracast websocket with no way for the code to easily see that had happened, so I dove into the code again, looking for alternatives. I think I’ve got a good solution.

    I’ve added Reachability to the websocket monitor; if I detect a network disconnect, then I force the websocket monitor to disconnect as well so that it is in a known state. When Reachability gets a reconnection signal

  • Flutter experiences

    TL;DR: Flutter builds are as much fun as Java and Scala ones, and you spend more time screwing with the tools than you do getting anything done. I don’t think I’m going to switch, at least not now.

    As I’ve mentioned before on the blog, I maintain an iOS application for RadioSpiral’s online radio station. The app has worked well and successfully; the original codebase was Swift- Radio-Pro, which works as an iOS app and a MacOS one as well (I have been doing some infrastructure changes to support Azuracast, as previously documented on the blog.)

    We do have several, very polite, Android users who inquire from time to time if I’ve ported the radio station app to Android yet, and I have had to keep saying no, as the work to duplicate the app on Android looked daunting, and nobody is paying me for this. So I’ve been putting it off, knowing that I would have to learn something that runs on Android sooner or later if I wanted to do it at all.

    Randal Schwartz has been telling me for more than a year that I really should look at Dart and Flutter if I want to maintain something that works the same on both platforms, and I just didn’t have the spare time to learn it.

    Come the end of May 2023, and I found myself laid off, so I really had nothing but time. And I was going to need to update the app for IOS 16 anyway at that point (the last time I recompiled it, Xcode still accepted iOS 8 as a target!) and I figured now was as good a time as any to see if I could get it working multi-platform.

    I started looking around for a sample Flutter radio app, and found RadioSai. From the README, it basically does what I want, but has a bunch of other features that I don’t. I figured an app I could strip down was at least a reasonable place to start, so I checked it out of Github and started to work.

    Gearing up

    Setting up the infrastructure Installing Dart and Flutter was pretty easy: good old Homebrew let me brew install flutter to get those in place, and per instructions, I ran flutter doctor to check my installation. It let me know that I was missing the Android toolchain (no surprise there, since I hadn’t installed
    anything there yet). I downloaded the current Android Studio (Flamingo in my case), opened the .dmg, and copied it into /Applications as directed.

    Rerunning flutter doctor, it now told me that I didn’t have the most recent version of the command-line tools. I then fell into a bit of a rabbit hole. Some quick Googling told me that the command line tools should live inside Android Studio. I ferreted around in the application bundle and they were just Not There. I went back to the Android Studio site and downloaded them, and spent a fair amount of time trying to get sdkmanager into my PATH correctly. When I finally did, it cheerfully informed me that I had no Java SDK. So off to the OpenJDK site, and download JDK 20. (I tried a direct install via brew install, but strangely Java was still /usr/bin/java, and I decided rather than tracking down where the Homebrew Java went, I’d install my own where l could keep an eye on it.

    I downloaded the bin.tar.gz file and followed the installation instructions, adding the specified path to my PATH… and still didn’t have a working Java. Hm. Looking in the OpenJDK directory, the path was Contents, not jdk-18.0.1.jdk/Contents. I created the jdk-18.0.1 directory, moved Contents into it and had a working Java! Hurray! But even with dorking around further with the PATH, I still couldn’t get sdkmanager to update the command-line
    tools properly.

    Not that way, this way

    A little more Googling turned up this Stack Overflow post that told me to forget about installing the command-line tools myself, and to get Android Studio to do it. Following those instructions and checking all the right boxes, flutter doctor told me I had the command-line tools, but that I needed to accept some licenses. I ran the command to do that, and finally I had a working Flutter install!


    Almost.

    When I launched Android Studio and loaded my project, it failed with flutter.sdk not defined. This turned out to mean that I needed to add

    flutter.sdk=/opt/homebrew/Caskroom/flutter/ 3.10.5/flutter

    (the location that Homebrew had used to unpack Flutter — thank you find) to local.properties. After that, Gradle twiddled its fingers a while, and declared that the app was ready. (It did want to upgrade the build, and I let it do that.)

    Build, and…

    The option 'android.enableR8' is deprecated. 
    It was removed in version 7.0 of the
    Android Gradle plugin. 
    Please remove it from 'gradle.properties". 

    Okay, I remove it.

    /Users/joemcmahon/Code/radiosai/.dart_tool/ does not exist.

    More Googling, Stack Overflow says Run Tools > Flutter > Pub Get. Doesn’t exist. Okaaaaaay.

    There’s a command line version:

    flutter clean; flutter pub get

    Deleted dart_tool, then recreated it with package_config.json there. Right!

    Back to Android Studio, still confused about the missing menu entry, and build again. Gradle runs, downloads a ton of POMs and

    Couldn't resolve the package 'radiosai' in 'package:radiosai/audio_service/service_locator.dart'.

    Looking one level up, in :app:compileFlutterBuildDebug, Invalid depfile: /Users/joemcmahon/ Code/radiosai/.dart_tool/flutter_build/bff84666834b820d28a58a702f2c8321/ kernel_snapshot.d.

    Let’s delete those and see if that helps…yes, but still can’t resolve
    radiosai. Okay, time for a break.

    Finally, a build!

    Another Google: I wasn’t able to resolve the package because I needed to pub get again.

    Module was compiled with an incompatible version of Kotlin. 

    The binary version of its metadata is 1.8.0, expected version is 1.6.0. Another Google. One of the build Gradle files is specifying Kotlin 1.6…it’s in /android/ build.gradle. Update that to 1.8.10, build…Kotlin plugin is being loaded, good. Couple
    warnings, still going, good.

    BUILD SUCCESSFUL

    Nice! Now, how do I test this thing? Well, there’s Device Manager over on the right, that looks promising. There’s a “Pixel 3a” entry and a “run” button. What’s the worst that could happen?

    Starts up, I have a “running device” that’s a couple inches tall, on its home screen. Hm. Ah, float AND zoom. Cool. Now I realize I have no idea how to run an Android phone, and I don’t see the app.

    https://developer.android.com/studio/run/emulator…nope. Beginning to remember why I didn’t like working in Scala… Gradle upgrade recommended, okay, and now

    Namespace not specified. Please specify a namespace in the module's build.gradle. 

    Specified, still broken…googling…This is a known issue –
    https://github.com/ionic-team/capacitor/issues/6504

    If you are using Capacitor 4, do not upgrade to Gradle 8.


    Yeah, I remember why I stopped liking Scala. git reset to put everything back…

    Execution failed for task:gallery_saver:compileDebugKotlin'. 
    > compileDebugJavaWithJavac task (current target is 1.8) and 'compileDebugKotlin' task
    (current target is 17) 
    jvm target compatibility should be set to the same Java version.
    Consider using JVM toolchain: https://kotl.in/gradle/jvm/toolchain 

    Fix android/app/build.gradle so everyone thinks we’re using Java 17, which uses a different syntax, ugh.

    Fix it again. Same for the Kotlin target too.

    'compileDebugJavaWithJavac' task (current target is 1.8) and 'compileDebugKotlin' task (current target is 17) jvm target compatibility should be set to the same Java version.

    This is apparently actually Gradle 8 still lying around after the (incorrectly) recommended upgrade. Removing ~/ gradle to nuke from orbit. Also killing android/.gradle.


    [Aside: I am used to using git grep to find things, and it is just not finding them in this repo!]

    Cannot read the array length because "" is null

    WHAT.

    Apparently this means that Gradle 8 is still lurking. Yep, the rm ~/.gradle/* didn’t remove everything because of permissions. Yougoddabefuckingkiddingme. Sudo’ed it, relaunched with the fixes I made above. App runs!


    However it stops working after a bit with no reason indicating why. Let’s stop it and restart. Stop button did not stop it; had to quit Android Studio.

    Well. Okay. This is not promising, but let’s see the benefit of using Flutter and check out if the iOS side works. Seems a lot more straightforward, though I’m not doing much in Xcode. cd iOS, launch the simulator (important!), flutter run…and we get the Flutter demo project. Looks like the IOS version wasn’t brought over from the Android side. Why did you even do this.

    Do we all remember that I wanted something that worked on both platforms? Gah.

    So I’m putting Flutter aside, cleaning up the ton of disk space all this extra infrastructure took up, and will maybe come back to it another time.

    But for right now, the amount of work involved is absolutely not worth it, and I’d have to write the damn thing from scratch anyway.

    Maybe I’ll run this through one of the LLMs and see if it can get me a common codebase as a starting point, but I am not sanguine.

  • Azuracast high-frequency updates, SSE, and iOS background processes

    A big set of learning since the last update.

    I’ve been working on getting the RadioSpiral infrastructure back up to snuff after our Azuracast streaming server upgrade. We really, really did need to do that — it just provides 90% of everything we need to run the station easily. Not having to regenerate the playlists every few weeks is definitely a win, and we’re now able to do stuff like “long-play Sunday”, where all of the tracks are long-players of a half-hour or more.

    But there were some hitches, mostly in my stuff: the iOS app and the now-playing Discord bot. Because of reasons (read: I’m not sure why), the Icecast metadata isn’t available from the streaming server on Azuracast, especially when you’re using TLS. This breaks the display of artist and track on the iOS app, and partially breaks the icecast-monitor Node library I was using to do the now-playing bot in Discord.

    (Side note: this was all my bright idea, and I should have tested the app and bot against Azuracast before I proposed cutting over in production, but I didn’t. I’ll run any new thing in Docker first and test it better next time.)

    Azuracast to the rescue

    Fortunately, Azuracast provides excellent now-playing APIs. There a straight-up GET endpoint that returns the data, and two event-driven ones (websockets and SSE). The GET option depends on you polling the server for updates, and I didn’t like that on principle; the server is quite powerful, but I don’t want multiple copies of the app hammering it frequently to get updates, and it was inherently not going to be close to a real-time update unless I really did hammer the server.

    So that was off the table, leaving websockets and SSE, neither of which I had ever used. Woo, learning experience. I initially tried SSE in Node and didn’t have a lot of success with it, so I decided to go with websockets and see how that went.

    Pretty well actually! I was able to get a websocket client running pretty easily, so I decided to try it that way. After some conferring with ChatGPT, I put together a library that would let me start up a websocket client and run happily, waiting for updates to come in and updating the UI as I went. (I’ll talk about the adventures of parsing Azuracast metadata JSON in another post.)

    I chose to use a technique that I found in the FRadioPlayer source code, of declaring a public static variable containing an instance of the class; this let me do

    import Kingfisher
    import ACWebSocketClient
    
    client = ACWebSocketClient.shared
    ...
    tracklabel.text = client.status.track
    artistlabel.text = client.status.artist
    coverImageView.kf.getImage(with:client.status.artURL)

    (Kingfisher is fantastic! Coupled with Azuracast automatically extracting the artwork from tracks and providing a URL to it, showing the right covers was trivial. FRadioPlayer uses the Apple Music cover art API to get covers, and given the, shall we say, obscure artists we play, some of the cover guesses it made were pretty funny.)

    Right. So we have metadata! Fantastic. Unfortunately, the websocket client uses URLSessionWebSocketTask to manage the connection, and that class has extremely poor error handling. It’s next to impossible to detect that you’ve lost the connection or re-establish it. So It would work for a while, and then a disconnect would happen, and the metadata would stop updating.

    Back to the drawing board. Maybe SSE will work better in Swift? I’ve written one client, maybe I can leverage the code. And yes, I could. After some searching on GitHub and trying a couple of different things, I created a new library that could do Azuracast SSE. (Thank you to LaunchDarkly and LDSwiftEventSource for making the basic implementation dead easy.)

    So close, but so far

    Unfortunately, I now hit iOS architecture issues.

    iOS really, really does not want you to run long-term background tasks, especially with the screen locked. When the screen was unlocked, the metadata updates went okay, but as soon as the screen locked, iOS started a 30-second “and what do you think you’re doing” timer, and killed the metadata monitor process.

    I tried a number of gyrations to keep it running and schedule and reschedule a background thread, but if I let it run continuously, even with all the “please just let this run, I swear I know what I need here” code, iOS would axe it within a minute or so.

    So I’ve fallen back to a solution not a lot better than polling the endpoint: when the audio starts, I start up the SSE client, and then shut it down in 3 seconds, wait 15 seconds, and then run it again. When audio stops, I shut it off and leave it off. This has so far kept iOS from nuking the app, but again, I’m polling. Yuck.

    However, we now do have metadata, and that’s better than none.

    On the other hand…

    On the Discord front, however, I was much more successful. I tried SSE in Node, and found the libraries wanting, so I switched over to Python and was able to use sseclient to do the heavy lifting for the SSE connection. It essentially takes an SSE URL, hooks up to the server, and then calls a callback whenever an event arrives. That was straightforward enough, and I boned up on my Python for traversing arbitrary structures — json.loads() did a nice job for me of turning the complicated JSON into nested Python data structures.

    The only hard bit was persuading Python to turn the JSON struct I needed to send into a proper query parameter. Eventually this worked:

    subs = {
            "subs": {
                f"station:{shortcode}": {"recover": True}
            }
         }
    
    json_subs = json.dumps(subs, separators=(',', ':'))
    json_subs = json_subs.replace("True", "true").replace("False", "false")
    encoded_query = urllib.parse.quote(json_subs)

    I pretty quickly got the events arriving and parsed, and I was able to dump out the metadata in a print. Fab! I must almost be done!

    But no. I did have to learn yet another new thing: nonlocal in Python.

    Once I’d gotten the event and parsed it and stashed the data in an object, I needed to be able to do something with it, and the easiest way to do that was set up another callback mechanism. That looked something like this:

    client = build_sse_client(server, shortcode)
    run(client, send_embed_with_image)

    The send_embed_with_image callback puts together a Discord embed (a fancy message) and posts it to our Discord via a webhook, so I don’t have to write any async code. The SSE client updates every fifteen seconds or so, but I don’t want to just spam the channel with the updates; I want to compare the new update to the last one, and not post if the track hasn’t changed.

    I added a method to the metadata object to compare two objects:

    def __eq__(self, other) -> bool:
        if not isinstance(other, NowPlayingResponse):
            return False
        if other is None:
            return False
        return (self.dj == other.dj and
                self.artist == other.artist and
                self.track == other.track and
                self.album == other.album)

    …but I ran into a difficulty trying to store the old object: the async callback from my sseclient callback couldn’t see the variables in the main script. I knew I’d need a closure to put them in the function’s scope, and I was able to write that fairly easily after a little poking about, but even with them there, the inner function I was returning still couldn’t see the closed-over variables.

    The fix was something I’d never heard of before in Python: nonlocal.

    def wrapper(startup, last_response):
        def sender(response: NowPlayingResponse):
            nonlocal startup, last_response
            if response == last_response:
                return
    
            # Prepare the embed data
            local_tz = get_localzone()
            start = response.start.replace(tzinfo=local_tz)
            embed_data = {
                "title": f"{response.track}",
                "description": f"from _{response.album}_ by {response.artist} ({response.duration})",
                "timestamp": start,
                "thumbnail_url": response.artURL,
            }
    
            # Send to webhook
            send_webhook(embed_data)
    
            startup = False
            last_response = response
    
        return sender

    Normally, all I’d need to do would be have startup and last_response in the outer function’s argument list to have them visible to the inner function’s namespace, but I didn’t want them to just be visible: I wanted them to be mutable. Adding the nonlocal declaration of those variables does that. (If you want to learn more about nonlocal, this is a good tutorial.)

    The Discord monitor main code now looks like this:

    startup = True
    last_response = None
    
    # Build the SSE client
    client = build_sse_client(server, shortcode)
    
    # Create the sender function and start listening
    send_embed_with_image = wrapper(startup, last_response)
    run(client, send_embed_with_image)

    Now send_embed_with_image will successfully be able to check for changes and only send a new embed when there is one.

    One last notable thing here: Discord sets the timestamp of the embed relative to the timezone of the Discord user. If a timezone is supplied, then Discord does the necessary computations to figure out what the local time is for the supplied timestamp. If no zone info is there, then it assumes UTC, which can lead to funny-looking timesstamps. This code finds the timezone where the monitor code is running, and sets the timestamp to that.

    from tzlocal import get_localzone
    
    local_tz = get_localzone()
    start = response.start.replace(tzinfo=local_tz)

    And now we get nice-looking now-playing info in Discord:

    Shows two entries in a Discord channel, listing track title in bold, album name in italics, and artist name, with a start time timestamp and a thumbnail of the album cover.

    Building on this

    Now that we have a working Python monitor, we can now come up with a better solution to (close to) real-time updates for the iOS app.

    Instead of running the monitor itself, the app will register with the Python monitor for silent push updates. This lets us offload the CPU (and battery) intensive operations to the Python code, and only do something when the notification is pushed to the app.

    But that’s code for next week; this week I need to get the iOS stopgap app out, and get the Python server dockerized.

  • Swift Dependency Management Adventures

    I’m in the process of (somewhat belatedly) upgrading the RadioSpiral app to work properly with Azuracast.

    The Apple-recommended way of accessing the stream metadata just does not work with Azuracast’s Icecast server – the stream works fine, but the metadata never updates, so the app streams the music but never updates the UI with anything.

    Because it could still stream (heh, StillStream) the music, we decided to go ahead and deploy. There were so many other things that Azuracast fixed for us that there was no question that decreasing the toil for everyone (especially our admin!) was going to make a huge difference.

    Addressing the problem

    Azuracast supplies an excellent now-playing API in four different flavors:

    • A file on the server that has now-playing data, accessible by simply getting the contents of the URL. This is only updated every 30 seconds or so, which isn’t really good enough resolution, and requires the endpoint be polled.
    • An API that returns the now-playing data as of the time of the request via a plain old GET to the endpoint. This is better but still requires polling to stay up to date, and will still not necessarily catch a track change unless the app polls aggressively, which doesn’t scale well.
    • Real-time push updates, either via SSE over https or websocket connection. The push updates are less load on the server, as we don’t have to go through session establishment every time; we can just use the open connection and write to it. Bonus, the pushes can happen at the time the events occur on the server, so updates are sent exactly when the track change occurs.

    I decided that the websocket API was a little easier to implement. With a little help from ChatGPT to get me an initial chunk of code (and a fair amount of struggling to figure out the proper parameters to send for the connection request),

    I used a super low-rent SwiftUI app to wrap AVAudioSession and start up a websocket client separately to manage the metadata; that basically worked and let me verify that the code to monitor the websocket was working.

    I was able to copy that code inside of FRadioPlayer, the engine that RadioSpiral uses to do the streaming, but then I started running into complications.

    Xcode, Xcode, whatcha gonna do?

    I didn’t want to create an incompatible fork of FRadioPlayer, and I felt that the code was special-purpose enough that it wasn’t a reasonable PR to make. In addition, it was the holidays, and I didn’t want to force folks to have to work just because I was.

    So I decided to go a step further and create a whole new version of the FRadioPlayer library, ACRadioPlayer, that would be specifically designed to be used only with Azuracast stations.

    Initially, this went pretty well. The rename took a little extra effort to get all the FRadio references switched over to ACRadio ones, but it was fairly easy to get to a version of the library that worked just like FRadioPlayer, but renamed.

    Then my troubles began

    I decided that I was going to just include the code directly in ACRadioPlayer and then switch RadioSpiral to the new engine, so I did that, and then started trying to integrate the new code into ACRadioPlayer. Xcode started getting weird. I kept trying to go forward a bit at a time — add the library, start trying to include it into the app, get the fetch working…and every time, I’d get to a certain point (one sample app working, or two) and then I’d start getting strange errors: the class definition I had right there would no longer be found. The build process suddenly couldn’t write to the DerivedData directory anymore. I’d git reset back one commit, another, until I’d undone everything. Sometimes that didn’t work, and I had to throw away the checkout and start over. The capper was “Unexpected error”, with absolutely nothing to go on to fix it.

    Backing off and trying a different path

    So I backed all the way out, and started trying to build up step-by-step. I decided to try building the streaming part of the code as a separate library to be integrated with ACRadioPlayer, so I created a new project, ACWebSocketClient, and pulled the code in. I could easily get that to build, no surprise, it had been building, and I could get the tests of the JSON parse to pass, but when I tried to integrate it into ACRadioPlayer using Swift Package Manager, I was back to the weird errors again. I tried for most of a day to sort that out, and had zero success.

    The next day, I decided that maybe I should follow Fatih’s example for FRadioPlayer and use Cocoapods to handle it. This went much better.

    Because of the way Cocoapods is put together, just building the project skeleton actually gave me some place to put a test app, which was much better, and gave me a stepping stone along the way to building out the library. I added the code, and the process of building the demo showed me that I needed to do a few things: be more explicit about what was public and what was private, and be a little more thoughtful about the public class names.

    A couple hours work got me a working demo app that could connect to the Azuracast test station and monitor the metadata in real time. I elected to just show the URL for the artwork as text because actually fetching the image wasn’t a key part of the API.

    I did then hit the problem that the demo app was iOS only. I could run it on MacOS in emulation mode, but I didn’t have a fully-fledged Mac app to test with. (Nor did I have a tvOS one.) I tried a couple variations on adding a new target to build the Mac app, but mostly I ended up breaking the work I had working, so I eventually abandoned that.

    I then started working step by step to include the library in ACRadioPlayer. FRadioPlayer came with an iOS apps (UIKit and SwiftUI), a native Mac app, and a tvOS app. I carefully worked through getting the required versions of the OS to match in the ACWebSocketClient podspec, the ACRadioPlayer Podfile, and the ACRadioPlayer Xcode project. That was tedious but eventually successful.

    Current status

    I’ve now got the code properly pulled in, compatible with the apps, and visible to each of the apps. I’ll now need to pull in the actual code that uses it from the broken repo (the code was fine, it was just the support structures around it that weren’t) and get all the apps working. At that point I can get both of the libraries out on Cocoapods, and then start integrating with RadioSpiral.

    In general, this has been similar to a lot of projects I’ve worked on in languages complex enough to need an IDE (Java, Scala, and now Swift): the infrastructure involved in just getting the code to build was far more trouble to work with and maintain, and consumed far more time, than writing the code itself.

    Writing code in Perl or Python was perhaps less flashy, but it was a lot simpler: you wrote the code, and ran it, and it ran or it didn’t, and if it didn’t, you ran it under the debugger (or used the tests, or worse case, added print statements) and fixed it. You didn’t have to worry about whether the package management system was working, or if something in the mysterious infrastructure underlying the applications was misconfigured or broken. Either you’d installed it, and told your code to include it, or you hadn’t. Even Go was a bit of a problem in this way; you had to be very careful in how you got all the code in place and that you had gotten it in place.

    Overall, though, I”m pretty happy with Cocoapods and the support it has built in. Because FRadioPlayer was built using Cocoapods as its package management, I’m hoping that the process of integrating it into RadioSpiral won’t be too tough.

  • Re-upping WebWebXNG

    So it’s been a minute since I did any serious work on WebWebXNG.

    Initially, I decided that the easiest way forward was “translate this old CGI code into modern web code”. And up to a point, that was a good way to go. But I got to the point where I was trying to make the rubber meet the road, and the intertwining of templating and code in the old version was making me stall out.

    I’ve had a breather, working on other projects, and the world has moved on and brought me some new things. One in particular is htmx.

    The htmx library works a whole lot more like the old CGI world did, just better. Everything is capable of interacting with the user, all of the HTTP verbs are available, and interaction is by exchanging chunks of HTML. You don’t convert to JSON, then convert back to HTML. This kind of logic definitely fits better with the concept of WebWebX as-it-was.

    Also, Perl project management has definitely changed — and improved. I did like Dist::Zilla, but it’s definitely a heavyweight solution. In the meantime, Minilla has appeared, and it fits incredibly well into the model I want to use to manage the code:

    • Your module is Pure Perl, and files are stored in lib.
    • Your executable file is in script directory, if there is one.
    • Your dist sharedirs are in share, if you have any.
    • Your module is maintained with Git and git ls-files matches with what you will release.
    • Your module has a static list of prerequisites that can be described in a cpanfile.
    • Your module has a Changes file.
    • You want to install via cpanm.

    I do have a working page storage engine, which is good, but the interaction engine is definitely nowhere. I’m coming back to the project with fresh eyes, and I’m going to redesign it top-to-bottom to use htmx for all the backend interaction.

    Looking forward to this, and the next iteration of WebWebXNG starts now.

  • JSON, Codable, and an illustration of ChatGPT’s shortcomings

    A little context: I’m updating the RadioSpiral app to use the (very nice) Radio Station Pro API that gives me access to useful stuff like the station calendar, the current show, etc. Like any modern API, it returns its data in JSON, so to use this in Swift, I need to write the appropriate Codable structs for it — this essentially means that the datatypes are datatypes that Swift either can natively decode, or that they’re Codable structs.

    I spent some time trying to get the structs right (the API delivers something that makes this rough, see below), and after a few tries that weren’t working, I said, “this is dumb, stupid rote work – obviously a job for ChatGPT.”

    So I told it “I have some JSON, and I need the Codable Swift structs to parse it.” The first pass was pretty good; it gave me the structs it thought were right and some code to parse with – and it didn’t work. The structs looked like they matched: the fields were all there, and the types were right, but the parse just failed.

    keyNotFound(CodingKeys(stringValue: "currentShow", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "broadcast", intValue: nil)], debugDescription: "No value associated with key CodingKeys(stringValue: \"currentShow\", intValue: nil) (\"currentShow\").", underlyingError: nil))

    Just so you can be on the same page, here’s how that JSON looks, at least the start of it:

    {
    	"broadcast": {
    		"current_show": {
    			"ID": 30961,
    			"day": "Wednesday",
    			"date": "2023-12-27",
    			"start": "10:00",
    			"end": "12:00",
    			"encore": false,
    			"split": false,
    			"override": false,
    			"id": "11DuWtTE",
    			"show": {...

    I finally figured out that Swift, unlike Go, must have field names that exactly match the keys in the incoming JSON. So if the JSON looks like {broadcast: {current_show... then the struct modeling the contents of the broadcast field had better have a field named current_show, exactly matching the JSON. (Go’s JSON parser uses annotations to map the fields to struct names, so having a field named currentShow is fine, as long as the annotation says its value comes from current_show. That would look something like this:

    type Broadcast struct {
        currentShow  CurrentShow `json:currentShow`
        ...
    }
    
    type CurrentShow struct {
       ... 

    There’s no ambiguity or translation needed, because the code explicitly tells you what field in the struct maps to what field in the JSON. (I suppose you could completely rename everything to arbitrary unrelated names in a Go JSON parse, but from a software engineering POV, that’s just asking for trouble.)

    Fascinatingly, ChatGPT sort of knows what’s wrong, but it can’t use that information to fix the mistake! “I apologize for the oversight. It seems that the actual key in your JSON is “current_show” instead of “currentShow”. Let me provide you with the corrected Swift code:”. It then provides the exact same wrong code again!

    struct Broadcast: Codable {
        let currentShow: BroadcastShow
        let nextShow: BroadcastShow
        let currentPlaylist: Bool
        let nowPlaying: NowPlaying
        let instance: Int
    }

    The right code is

    struct Broadcast: Codable {
        let current_show: BroadcastShow // exact match to the field name
        let next_show: BroadcastShow.   // and so on...
        let current_playlist: Bool
        let now_playing: NowPlaying
        let instance: Int
    }

    When I went through manually and changed all the camel-case names to snake-case, it parsed just fine. (I suppose I could have just asked ChatGPT to make that correction, but after it gets something wrong that it “should” get right, I tend to make the changes myself to be sure I understood it better than the LLM.)

    Yet another illustration that ChatGPT really does not know anything. It’s just spitting out the most likely-looking answer, and a lot of the time it’s close enough. This time it wasn’t.

    On the rough stuff from the API: some fields are either boolean false (“nothing here”) or a struct. Because Swift is a strongly-typed language, this has to be dealt with via an enum and more complex parsing. At the moment, I can get away with failing the parse and using a default value if this happens, but longer-term, the parsing code should use enums for this. If there are multiple fields that do this it may end up being a bit of a combinatorial explosion to try to handle all the cases, but I’ll burn that bridge when I come to it.

  • word break: now where have I seen this before?

    I’m now doing some paired leetcode study sessions with a former ZipRecruiter co-worker, Marcellus Pelcher (LinkedIn). As he says, “I’ve been doing these [leetcode exercises] for a long time!” and it’s been great both to get to spend some time with him and to work together on some problems.

    I’m generally doing OK on easy problems so after getting our video call working, we looked at the word break problem. This essentially boils down to a pattern match: given a list of words, and a string of characters, figure out if the string can be broken into pieces that all match a word in the list of words.

    Examples?

    • leetcode and ["leet", "code"] succeeds (by inspection!)
    • catsandog and ["cat", "cats", "sand", "dog"] fails (no combo lets us match the final og).

    We agreed that this was probably meant to be a dynamic programming problem (way to saddle myself with a hard one right out of the gate!). I put together a recursive greedy algorithm (similar to the good old SNOBOL4 pattern matcher) in ten minutes or so, with a certain amount of flailing at accessing the strings, but it took too long to get to a solution, and it was a huge memory hog. Marcellus did a dynamic programming solution in Java that used a bit array to track the matches, and had it done and passing in about 15-20 minutes. So one for him and zero for me! 🙂

    I let the solution rattle around in my head and after taking a shower, it suddenly dawned on me that this was very much like the coin change problem, but instead of reducing the count, we’re trimming off substrings. I went back to work from there and finally realized that the bit array was tracking the ends of successful matches, which made the dynamic programming recurrence go like this:

    • Start at the beginning of the string (position 0) and find all words that match at that position. Record where they end. That’s our starting configuration.
    • Continue moving forward through the string, comparing all the words at the current position.
    • If the word matches at the current position, it successfully extends a match if and only if a previous match ended right before where this one starts. If this is true, then we record the end of the newly-matched word. Otherwise we just continue to the next word.
    • If a match ends at the end of the string, we’ve successfully found some match or another; for this problem, that’s all that matters.

    Let’s do an example. Let’s say our string is “catsupondog” and our words are “cat”, “cats”, “catsup”, “upon”, “on”, and “dog”. At position zero, “cat”, “cats”, and “catsup” all matched, so we’ll record the ending indexes in a bit array. After our startup matches, things look like this:

    catsupondog
    0123456789A
    00110100000
     AA A
     || |
     || +-- catsup matched up to here
     |+------ cats matched up to here
     +-------- cat matched up to here

    As the index moves up through the string, we keep checking all the words against it. If we match another word, we then look to see if the position just before the start of this new match marks the end of a successful match.

    When we get to position 4, “upon” matches, and position 3 is true, so this extends the match, and we mark position 7 as the end of a successful match.

    catsupondog
    0123456789A
    00110101000
       A   A
       |   |
       |   +--- "upon" extends the "cats" match
       +------- "cats" match ended here

    When we get to position 6, “on” matches, and we mark position 7 as end of a match again. (Note that marking it twice is fine; we just have two different successful matches that end there, and for this problem, which one got us to 7 doesn’t matter.)

    catsupondog
    0123456789A
    00110101000
         A A
         | |
         | +--- "on" extends the "catsup" match
         +----- "catsup" match ended here

    When we finally get to position 8, “dog” matches, and position 7 is true, so this extends the match again. We’re at the end, so we’re done, and successful. We don’t have to check any more past position 8.

    catsupondog
    0123456789A
    00110101001
           A  A
           |  |
           |  +--- "dog" matches, reaches the end, and succeeds
           +------ end of "on" and "upon" matches; which one doesn't matter
            

    Enough chatter, here’s some code.

    func wordBreak(s string, wordDict []string) bool {
        
        // Records where matches ended successfully.
        matched := make([]bool, len(s))
    
        for i := 0; i < len(s); i++ {
            // Check every word at the current position.
            for _,word := range(wordDict) {
                if i + len(word)-1 < len(s) {
                    // Word can fit in remaining space
                    if s[i:i+len(word)] == word {
                        // matches at the current index
                        if i == 0 || matched[i-1] {
                            // If we're at zero, we always succeed.
                            // Otherwise, check if a successful match ended right
                            // before this one. If it did, mark the end of this
                            // match as successful too.
                            matched[i+len(word)-1] = true
                            if i+len(word)-1 == len(s)-1 {
                                // Short-circuit if we just successfully
                                // reached the end with this match
                                break
                            }
                        }
                    }
                }
            }
        }
        return matched[len(s)-1]
    }

    Bottom 7% in performance, but I’m giving myself a win because I actually figured out a dynamic programming problem by getting what the recurrence was. Marcellus told me, I just didn’t follow his explanation! [EDIT: I removed a fmt.Println of the bit array inside the loop and now it’s top 80%! Calling that a pass for an interview too.]

  • Making change via dynamic programming: easy when you see it

    I failed to see this is an induction problem, and did not think of an elegant way to represent “we have no solution” for a given amount.

    The problem description is “given a set of coins of n denominations, figure out the minimum number of coins needed to make change for a given amount. If you can’t make change, return -1.”

    So here’s the induction:

    • Assume it’s impossible to make change for any amount up to the target amount by setting the number of coins to > the max amount. (For this problem it was 2**32, which I’ll refer to as “impossible”).
    • For an amount of 0, it takes 0 coins to make change.
    • For each amount after 0, the number of coins needed to make change for this amount is 1 of the current coin, plus however many coins it took to make change for amount - current coin value.
    • If we couldn’t make change for amount - current coin value (value for that is impossible), then try the next coin.
    • If we run out of coins, we can’t make change for that amount, leave it set to impossible, and we move on to the next amount up.
    • When we reach the final amount, we’ve either found a chain of sub-solutions that reach 0 (change is made with n coins, the sum of all the sub-solutions) or it’s impossible.

    Because this builds a table of solutions from the bottom up, we’re always guaranteed that the solution for any amount < the current amount has already been solved, and we always try all the coins for every amount, so we’re guaranteed to find a solution if there is one, even if the coins are not in sorted order.

    I chose to use int64‘s and set the FAILURE amount to an amount specified in the problem description as definitely larger than any amount that is possible. You could do it with a map[int]int, checking for “no entry”, but using FAILURE allows the lookback to always work with just a less-than test, so it’s probably faster. I’ve updated the variable names to make it clearer how this works.

    
    func coinChange(coins []int, amount int) int {
        const FAILURE int64 = 5000000000
    
        // Assume everything is impossible...
        changeFor := make([]int64, amount+1)
        for i := 0; i < len(changeFor); i++ {
            // effectively Inf
            changeFor[i] = FAILURE
        }
    
        // ...except 0. Change for 0 is 0 (it takes no coins to make change
        // for 0.
        changeFor[0] = 0
    
        // For every amount up to the target amount (i.e., solve every
        // subproblem):
        for currentAmount := 1; currentAmount <= amount; currentAmount++ {
    
            // Try every coin we have to solve the problem.
            for _, coin := range(coins) {
    
                // If this coin might work...
                if coin <= currentAmount {
    
                    // Get the answer for the subproblem.
                    lookBack := changeFor[currentAmount - coin]
                    
                     // This if statement is doing a lot of elegant
                     // heavy lifting.
                     //
                     // If the subproblem had no solution, the lookback is
                     // FAILURE, and FAILURE + 1 is greater than the current
                     // value (FAILURE or less). This guarantees that we don't
                     // overwrite any solution that was already found, and that
                     // we leave it as FAILURE for this coin!
                     //
                     // If the subproblem DID have a solution, and it's better
                     // (less) than what we have (some number of coins or
                     // FAILURE), then we change the current solution for this
                     // amount to the new count.
                     //
                     // This is why the order of the coins in the coin array
                     // doesn't matter: we will always try ALL the coins for
                     // a given amount, and if a different coin has a better
                     // (smaller) solution, we'll change the solution for the
                     // current amount for the better one!
                     //
                     // This means we're guaranteed to have the best solution
                     // for every amount < the current amount (including 0),
                     // so composing a solution for the current amount from
                     // previous solutions always gets the best one for this
                     // amount.
                     //
                     // The fact that it takes a comment THIS LONG to accurately
                     // explain TWO LINES OF CODE...!
                    if lookBack + 1 < changeFor[currentAmount] {
                        // Previous number of coins solving this, plus one more
                        // for the current coin solving this amount better than
                        // we previously did. We never get here if we don't find
                        // a solution for any of the coins, leaving this set to
                        // FAILURE.
                        changeFor[currentAmount] = lookBack + 1
                    }
                }
            }
        }
        // If we never found a solution, the caller wants -1.
        if changeFor[amount] == FAILURE {
            return -1
        }
        // Retrieve the solution from the solution cache. If we were
        // going to use this a lot, we'd save the cache.
        return int(changeFor[amount])
    }
    

    I hope this commented code shows how disgustingly elegant this is. I didn’t find this solution, but I feel like working through why it works has helped me get a better grasp on dynamic programming.

    Lessons learned:

    • What can you solve by inspection?
    • How do you mark things as “not solved” or “insoluble”?
    • Don’t be afraid to solve a lot of seemingly unneeded subproblems if you can use them to break down the problem you have.

    I initially tried a greedy solution for this problem, and that is actually optimal if the set of coins you’re given can always make up any number. (E.g., US coins are 1 cent, 5 cents, 10 cents, 25 cents, and 50 cents; you can use those to make any number of cents from 1 to 100.) If the set of coins doesn’t guarantee this, then you have to use the dynamic programming approach to solve it.

    The second, recursive try might have worked if I’d cached solutions found along the way, effectively turning the iterative lop here into a recursive one, but I didn’t and it DNF’ed.

    Right, stats. 79th percentile on runtime, 80th on memory. Pretty good.