<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://michaelchadwick.info/feed.xml" rel="self" type="application/atom+xml" /><link href="https://michaelchadwick.info/" rel="alternate" type="text/html" /><updated>2026-08-24T12:02:43-07:00</updated><id>https://michaelchadwick.info/feed.xml</id><title type="html">MC.Info</title><subtitle>code, audio, and life developments by michael chadwick</subtitle><author><name>Michael Chadwick</name></author><entry><title type="html">Dance Dance Evolution</title><link href="https://michaelchadwick.info/blog/2026/08/21/dance-dance-evolution/" rel="alternate" type="text/html" title="Dance Dance Evolution" /><published>2026-08-21T17:45:00-07:00</published><updated>2026-08-21T17:45:00-07:00</updated><id>https://michaelchadwick.info/blog/2026/08/21/dance-dance-evolution</id><content type="html" xml:base="https://michaelchadwick.info/blog/2026/08/21/dance-dance-evolution/"><![CDATA[<p>In my youth I picked up the skill of moving my feet to hit arrows on the ground in time to music, both for fun and for exercise. It has been many years since I did so, but guess what? You are never too old to show your ultimate dance.</p>

<!--more-->

<h2 id="are-you-ready">Are You Ready?</h2>

<p><a href="https://en.wikipedia.org/wiki/Dance_Dance_Revolution">Dance Dance Revolution</a> has had a very positive place in my life. I probably first learned about it back in an arcade somewhere (remember going to arcades to play video games?), watching someone absolutely wreck some really hard song that I still couldn’t beat today. The pulsing beats, the flashing lights…it stood out in a room full of distractions. At first, you’re a noob, and you do easy songs because the whole thing is novel and you’re getting the hang of it. Then, if you keep at it, you start to build muscle memory about how to move, and you can anticipate steps better because you’ve played songs many times. You bump up the difficulty and learn new patterns and techniques. Your stamina increases. You sweat, because it’s not just a game, it’s a workout. It’s actually legitimate exercise.</p>

<p>This “performing” in public as I started doing harder songs more often resulted in more unpleasant physical feelings more of the time, and so I began to want to play not at an arcade, but at home. By that point, the game was so popular that home consoles had versions, and so I spent most of my DDR time on PlayStation instead. Some friends of mine shared my DDR interest, and so we’d sometimes play together, which was super fun. I even accompanied one friend to a legitimate DDR competition once, watching true experts play extremely hard songs (or even freestyle their own dance steps that also satisfied the song’s chart), which was pretty sick.</p>

<p>For the majority of my life now, however, DDR has been a game to play at home, alone, and it’s great that it <em>also</em> counts as exercise. In fact, at one point I was doing it so much that I actually shed considerable pounds. I’m definitely not the only person to use DDR in this way, and I’m not going to be the last to do it again.</p>

<p>Most forms of exercise I’ve tried in the past just never have the staying power of DDR. Racquetball, indoor rock climbing, tennis, walking, running, gym circuit training, even biking…while they all have their pros, none of them match the marriage of music, gaming, and movement that DDR brings to the table, nor can they be easily done in the privacy of one’s residence.</p>

<h2 id="crying-buckets-of-tears">Crying Buckets of Tears</h2>

<p>In order to play DDR at home, you need software (originally PlayStation, then open-source wonder <a href="https://www.stepmania.com/">Stepmania</a>, and now open-source wonder <a href="https://projectoutfox.com/">Project OutFox</a>), the songs themselves (an exercise left up to the reader), an audio/visual output (i.e. TV/monitor and speakers), and, most importantly, the <em>dance pad</em>.</p>

<p>When I was first playing DDR at home, I went through several iterations of my setup. First was the era of cheap, <strong>soft</strong> dance pads. They were superior only in price, because otherwise they sucked. They were barely better than hitting the ground, didn’t always register your steps, and they slid around a bunch, especially on harder songs.</p>

<p>The next step up from a soft pad is the <strong>foam</strong> pad. This is like the soft one, but more expensive, and filled with a small layer of plastic foam. They are definitely an improvement: sliding is minimized, feet get some cushioning, and steps are registered more accurately. However, it’s like going from 70% good to 80% good. It’s still not like the arcade, and as you get better and move faster, it just can’t keep up.</p>

<p><img src="/assets/images/posts/2026/cobalt_flux.png" alt="Cobalt Flux hard metal pad" title="Cobalt Flux hard metal pad" /></p>

<p>The final step, save from buying an actual DDR arcade machine, is the <strong>hard</strong> pad, made of metal and plastic. I went through several soft and foam pads over the years, and dealt with a lot of frustration due to their inaccuracies and drawbacks. I thought that if I just kept buying them as they deteriorated, it was enough. Once you get good at the game and want to do Hard difficulty-level songs all the time, however, you realize they just won’t ever fit the bill. Enter <a href="https://cobaltflux.org/collections/cobalt-flux-pads/products/cobalt-flux-dance-dance-revolution-ddr">Cobalt Flux</a>. It’s sold out now, and there are alternatives I’ve never tried, but the one I got for my birthday in my 20s is <em>still</em> the one I have today and am using in my most recent setup. You can stomp and stomp and stomp on this thing, and it just takes the abuse. It turned my DDR at-home soft and/or foam fragile ecosystem into a solid dance station.</p>

<h2 id="im-no-ordinary-fella">I’m No Ordinary Fella</h2>

<p>In my previous place of residence, there was a downstairs “extra” living room of sorts, and plenty of space to keep a heavy metal dance pad out for stomping. I remember doing DDR often there. In my current place of residence, such a space does not exist, and so I put the DDR stuff away, my skills atrophied, and the whole thing moved to the “archival space” of my brain. But the idea that I would play again never quite left me.</p>

<p>Recently, my usual form of exercise, biking, was temporarily disabled due to (yet another) flat tire. Changing the tire is tedious and unfun, and so I started thinking about an alternative. For some reason, DDR came full-fledged back into my head and I decided that there <em>had</em> to be a way to bring back this favorite form of interactive gaming which had been so beneficial to my life and health.</p>

<p>Through help from my wife (who has also done DDR with me in the past!), we were able to clean up the garage and make some space, and I could finally rebuild my DDR setup! I had an old Windows PC lying around (like you do), and I figured I could use that as a single-purpose DDR machine, but reality hit me quickly: it would not show anything on the new monitor I just got, no matter which video port I used. Rebooting did not help. Trying different cables did not help. The machine turned on, fans fanned, lights went on, but the monitor never got a signal.</p>

<p><strong>Strike 1</strong>.</p>

<p>Wracking my brain for some other old, unused device I could use, I remembered I bought a Raspberry Pi 3b many years ago, hoping to start on a programming adventure with my kid which never materialized (alas!). Would it run OutFox? It only has 1GB memory and graphics hardware that is nothing to write home about, but posts on the Internet from people saying it could be done inspired me. I popped it open, ordered a new Micro-SD card with the Debian 13-flavored RPi operating system, plugged it in, and got some video output!</p>

<p>I excitedly downloaded OutFox and double-clicked the icon. A brief glimpse of a window pops up and then…it crashes. OpenGL has run out of memory. I tinker with config files and try again. Same thing. I tinker some more. I get a different OpenGL error. Tinker. Fail. Tinker. Fail. Maybe I don’t need to be in a windowing system? I try again from the command line. Fail. I try different approaches, using X11 and EGL, but fail. This all occurs over multiple days for multiple hours each time. It’s exhausing and demoralizing. Google and an LLM as my guide both ultimately failed. I finally give up on this path. I almost give up on the project entirely.</p>

<p><strong>Strike 2</strong>.</p>

<p>Despite really initially liking the hacking aspect of a RPi-powered DDR machine, the hacking was not going anywhere, and I had reached my limit. Ordering a newer RPi was an option, but I feared I’d run into similar issues, and so decided to order a mini-PC running Windows 11. I got it up and running, installed OutFox, and…</p>

<p><strong><del>Strike 3</del>WINNER!</strong></p>

<p>It’s not especially fast (setup took seemingly forever just to get to a desktop), nor does it have any whiz-bang like my actual gaming PC, but what it does is take up very little space, not make much noise, and runs DDR. My trusty 20 year-old Cobalt Flux, despite lots of use and some rusting, was plugged in via a PlayStation-&gt;USB converter, and was detected by the OS. I didn’t even have to mess with audio/video sync stuff, which is another bane of rhythm gaming, as it somehow magically Just Worked(tm) and I was A-i-a-i-ai-ing like in the olden days. DDR was, yet again, in my life and let’s hope it stays for a while.</p>

<p><img src="/assets/images/posts/2026/ddr_new_setup.jpg" alt="Current DDR setup in the garage" title="Current DDR setup in the garage" /></p>

<h2 id="dancin-mastery">Dancin’ Mastery</h2>

<p>And that brings us to the present. I started on my at-home DDR journey in my 20s. I was lighter, more agile, and had more stamina. I’m in my 40s now and I’m…not as much any of that. However, once my new setup was complete, the nascent skills came back and the muscle memory kicked in all the same. I just gotta work on getting back the ability to play for 1-2 hours regularly without completely dying.</p>

<p>I’m thinking of re-adding the two custom DDR songs I made for two of my original songs, but maybe cleaning them up so they are more danceable (I created them using a computer and tested them with a keyboard, and they’ve never been quite idiomatic when it comes to actually dancing). I have so many more original songs I could turn into DDR songs now, too, so maybe I’ll get back into the custom thing. It’s exciting to re-open this chapter of my life, and I really hope it helps me drop some weight, because that’s the most important thing to me right now, besides just having fun and enjoying “working out” again.</p>]]></content><author><name>Michael Chadwick</name></author><category term="blog" /><category term="ddr" /><category term="exercise" /><category term="mini-pc" /><category term="raspberry-pi" /><summary type="html"><![CDATA[In my youth I picked up the skill of moving my feet to hit arrows on the ground in time to music, both for fun and for exercise. It has been many years since I did so, but guess what? You are never too old to show your ultimate dance.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://michaelchadwick.info/posts/2026/dance_dance_evolution.png" /><media:content medium="image" url="https://michaelchadwick.info/posts/2026/dance_dance_evolution.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Duolingo Disillusionment</title><link href="https://michaelchadwick.info/blog/2026/07/30/duolingo-disillusionment/" rel="alternate" type="text/html" title="Duolingo Disillusionment" /><published>2026-07-30T09:25:00-07:00</published><updated>2026-07-30T09:25:00-07:00</updated><id>https://michaelchadwick.info/blog/2026/07/30/duolingo-disillusionment</id><content type="html" xml:base="https://michaelchadwick.info/blog/2026/07/30/duolingo-disillusionment/"><![CDATA[<p>Duolingo was once a super awesome place for me to experiment with language, and I really enjoyed using it. Nearly 5 and a half years of a daily streak later, I have become disillingoed.</p>

<!--more-->

<h2 id="images">Images</h2>

<p>To illustrate the “Purge” part of the <a href="/blog/2026/01/23/the-binge-purge-tech-cycle/">Binge-Purge Tech Cycle</a>, I submit the following images, as they document my recent deletion of my Duolingo account.</p>

<p><img src="/assets/images/posts/2026/duolingo-01-1999.png" alt="Duolingo: Do your Duolingo, Mike! You are currently at 1999 days in a row." />
<img src="/assets/images/posts/2026/duolingo-02-2000a.png" alt="Duolingo: Way to go, Mike! You're at 2000 days in a row now!" />
<img src="/assets/images/posts/2026/duolingo-02-2000b.png" alt="Duolingo: Seriously, Mike, you are one cool owl that is on fire for your 2000 day streak!" />
<img src="/assets/images/posts/2026/duolingo-03-delete.png" alt="Duolingo: Wait, you want to delete your account?" />
<img src="/assets/images/posts/2026/duolingo-04-delete_confirm.png" alt="Duolingo: Umm, are you sure you want to delete your account?" />
<img src="/assets/images/posts/2026/duolingo-05-delete_confirmed.png" alt="Duolingo: OK, we have confirmed you have confirmed you want to delete your account. However, we're giving you a 7-day buffer to change your mind." />
<img src="/assets/images/posts/2026/duolingo-06-delete_finalized.png" alt="Duolingo: Your account was actually deleted." /></p>

<h2 id="words">Words</h2>

<p>Duolingo is still a really cool app, and is the reason why (besides high school and my wife’s linguistics degree) I got into learning languages other than my native English. I started using it in 2013, but only started trying to keep a mega-streak in the 2020s. Unfortunately, I realized months ago that it really wasn’t fun clicking/tapping through all the post-lesson “you got gems!”, “open this chest!”, “here’s your next friend streak!” nonsense (and I’ve been a paying customer for years! If I had to see ads this whole time, I’d most certainly have checked out long ago). In addition, lessons were not really improving my language aptitude, nor did I enjoy the self-imposed albatross of having to “check in” each day to keep that meaningless streak going.</p>

<p>For me, the way to learn a human language (much like a programming language), is to <strong>use it</strong>, and <strong>use it often</strong>. A few minutes a day reminding myself of some grammar or vocabulary in a particular foreign language isn’t enough to move the needle significantly, and my desire was not stoked by any tactic Duolingo deployed (besides maintaining a streak).</p>

<p>Like a cruise that briefly visits several countries, or a variety platter of food served at a restaurant, Duolingo is at its best as a <em>discovery</em> tool. It allows you to sample a lot of languages, and does it in an engaging way. I’m sure I wouldn’t have learned as much random basic Japanese, Hawaiian, or Arabic as I did without it. But language is a muscle, and all that I’ve learned over the years has largely seeped into the void, and I knew it would, because I don’t use it much anywhere else <em>besides Duolingo</em>.</p>

<p>Introspectively, this may say more about me than Duolingo, honestly. I love to sample things and learn a little about a lot of things. Going super deep on a topic has never been my strong suit, which can often lead to competence, but rarely mastery. That being said, I tested through Spanish to the very last section, but I’d never say I was fluent, which feels like a failure of the system. I might’ve gleaned a new thing or two, but I doubt much will stick.</p>

<p>Duolingo allowed for me to play around with language, and occasionally try to actually <em>learn</em>, but in the end I think the lack of drive on my part plus the increased friction added to each lesson finally put me over the edge of just not caring at all anymore. After ~13 years of different language concentrations, streaks of hundreds of days broken by forgetfulness, and one incredibly long multi-year streak (and sharing all this love with friends), I’m done chasing the owl. If I <em>actually</em> want to learn a language beyond basic reading comprehension, then I will probably look for a different tool.</p>

<p>At least I hit a nice, round number and went out with a bang, right?</p>]]></content><author><name>Michael Chadwick</name></author><category term="blog" /><category term="duolingo" /><category term="language" /><category term="wrapup" /><summary type="html"><![CDATA[Duolingo was once a super awesome place for me to experiment with language, and I really enjoyed using it. Nearly 5 and a half years of a daily streak later, I have become disillingoed.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://michaelchadwick.info/posts/2026/duolingo-disillusionment.jpg" /><media:content medium="image" url="https://michaelchadwick.info/posts/2026/duolingo-disillusionment.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">NebyooAxe is Live</title><link href="https://michaelchadwick.info/blog/2026/06/07/nebyooaxe-is-live/" rel="alternate" type="text/html" title="NebyooAxe is Live" /><published>2026-06-07T17:12:00-07:00</published><updated>2026-06-07T17:12:00-07:00</updated><id>https://michaelchadwick.info/blog/2026/06/07/nebyooaxe-is-live</id><content type="html" xml:base="https://michaelchadwick.info/blog/2026/06/07/nebyooaxe-is-live/"><![CDATA[<p>Occasionally I like to look up guitar chords and their proper names. Why? Cuz it is edifying. The web has many tools that can help with this, but none of them are mine. And now one is.</p>

<!--more-->

<h2 id="what-is-this">WHAT IS THIS?</h2>

<p>It is <a href="https://axe.neb.host">NebyooAxe</a> (or just Axe). And it is a guitar chord identifier and strummer that potentially improves your gitbox knowlege.</p>

<p>As I <a href="/blog/2026/05/09/online-guitar-tool-brainstorming">mentioned</a> not long ago, I wanted to make some kind of online web app that could help me, musically. While I’ve made <a href="https://keebord.neb.host">Keebord</a> and <a href="https://soundlister.neb.host">SoundLister</a>, they are not <strong>guitar</strong>-centric (which is a little wild since that’s my main instrument). Since there are plenty of musicians out there who also happen to be programmers, you can easily find web apps that help guitarists with various things, but most of them are either too bloated or full of ads or <em>both</em>. All of them aren’t made by me, too. Thus, I decided then to make a guitar chord lookup tool…and I have done so.</p>

<h2 id="tech-stack">TECH STACK</h2>

<p>Besides the usual soup of HTML, JS, and CSS that all my (and most others’) web apps tend to have, I decided to once again use <a href="https://vuejs.org">Vue</a>, since I enjoyed using it for Keebord. I like the idea of having these single file components that have all the template, style, and logic, and my work in <a href="https://emberjs.com">EmberJS</a> and <a href="https://glimmerjs.com">GlimmerJS</a> has only cemented that. In order to make it even gooder-er, I opted to use <a href="https://www.typescriptlang.org">TypeScript</a> so that I can have explicit variable types and frustrate myself when my terrible untyped JavaScript skills are constantly tested (but get better about the logic therein!).</p>

<p>Remembering settings is once again done in <code class="language-plaintext highlighter-rouge">localStorage</code> using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API">Web Storage API</a>, because I’ve used it many times before and it works great. I opted for <a href="https://vueuse.org">VueUse</a>’s specific <code class="language-plaintext highlighter-rouge">useStorage</code> method to help things along, though.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">defineStore</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">pinia</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">useStorage</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@vueuse/core</span><span class="dl">'</span>

<span class="nx">type</span> <span class="nx">FretArray</span> <span class="o">=</span> <span class="nx">string</span><span class="p">[]</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">useSettingsStore</span> <span class="o">=</span> <span class="nf">defineStore</span><span class="p">(</span><span class="dl">'</span><span class="s1">nebyooaxe-settings</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">state</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span>
    <span class="na">enableFretSound</span><span class="p">:</span> <span class="nf">useStorage</span><span class="p">(</span><span class="dl">'</span><span class="s1">axe-enable-fretSound</span><span class="dl">'</span><span class="p">,</span> <span class="kc">true</span><span class="p">),</span>
    <span class="na">currentFrets</span><span class="p">:</span> <span class="nf">useStorage</span><span class="p">(</span><span class="dl">'</span><span class="s1">axe-current-frets</span><span class="dl">'</span><span class="p">,</span> <span class="o">&lt;</span><span class="nx">string</span><span class="p">[]</span><span class="o">&gt;</span><span class="p">[]),</span>
  <span class="p">}),</span>
  <span class="na">actions</span><span class="p">:</span> <span class="p">{</span>
    <span class="nf">toggleEnableFretSound</span><span class="p">()</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">enableFretSound</span> <span class="o">=</span> <span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">enableFretSound</span>
    <span class="p">},</span>
    <span class="nf">updateCurrentFrets</span><span class="p">(</span><span class="na">currentFrets</span><span class="p">:</span> <span class="nx">FretArray</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">currentFrets</span> <span class="o">=</span> <span class="nx">currentFrets</span>
    <span class="p">},</span>
  <span class="p">},</span>
<span class="p">})</span>
</code></pre></div></div>

<p>Playing the notes when pressed and strumming the chords are all done manually through the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API">Web Audio API</a>, creating oscillators and fading the volume in and out.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">playNote</span><span class="p">(</span><span class="nx">fretId</span><span class="p">:</span> <span class="nx">string</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">masterGainNode</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">createGain</span><span class="p">()</span>
  <span class="kd">const</span> <span class="nx">startGain</span> <span class="o">=</span> <span class="mf">0.0001</span>
  <span class="kd">const</span> <span class="nx">endGain</span> <span class="o">=</span> <span class="mf">0.1</span>
  <span class="nx">masterGainNode</span><span class="p">.</span><span class="nx">gain</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="nx">startGain</span>

  <span class="c1">// main osc note</span>
  <span class="kd">const</span> <span class="nx">osc</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">createOscillator</span><span class="p">()</span>
  <span class="nx">osc</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">square</span><span class="dl">'</span>
  <span class="kd">const</span> <span class="nx">noteFreq</span> <span class="o">=</span> <span class="nx">MUSICAL_NOTES</span><span class="p">.</span><span class="nf">filter</span><span class="p">(</span>
    <span class="p">(</span><span class="nx">n</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">n</span><span class="p">.</span><span class="nx">fretIds</span><span class="p">.</span><span class="nf">includes</span><span class="p">(</span><span class="nx">fretId</span><span class="p">)</span>
  <span class="p">)[</span><span class="mi">0</span><span class="p">]?.</span><span class="nx">frequency</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">noteFreq</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">osc</span><span class="p">.</span><span class="nx">frequency</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="nx">noteFreq</span>
  <span class="p">}</span>

  <span class="c1">// vibrato lfo</span>
  <span class="kd">const</span> <span class="nx">lfo</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">createOscillator</span><span class="p">()</span>
  <span class="nx">lfo</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">sine</span><span class="dl">'</span>
  <span class="nx">lfo</span><span class="p">.</span><span class="nx">frequency</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="mi">8</span>

  <span class="c1">// gain that sets the depth of the freq mod</span>
  <span class="kd">const</span> <span class="nx">lfoGain</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">createGain</span><span class="p">()</span>
  <span class="nx">lfoGain</span><span class="p">.</span><span class="nx">gain</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="mf">0.35</span>
  <span class="kd">const</span> <span class="nx">lfoDetune</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">createGain</span><span class="p">()</span>
  <span class="nx">lfoDetune</span><span class="p">.</span><span class="nx">gain</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="mi">50</span>

  <span class="c1">// connect lfo to its gain and its detune and then start it</span>
  <span class="nx">lfo</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="nx">lfoGain</span><span class="p">)</span>
  <span class="nx">lfoGain</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="nx">osc</span><span class="p">.</span><span class="nx">detune</span><span class="p">)</span>
  <span class="nx">lfo</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span>

  <span class="c1">// connect main osc to main gain and then to destination</span>
  <span class="nx">osc</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="nx">masterGainNode</span><span class="p">)</span>
  <span class="nx">masterGainNode</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">destination</span><span class="p">)</span>

  <span class="c1">// start the main osc, fade in volume, fade out volume, stop main osc</span>
  <span class="nx">osc</span><span class="p">.</span><span class="nf">start</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">currentTime</span><span class="p">)</span>
  <span class="nx">masterGainNode</span><span class="p">.</span><span class="nx">gain</span><span class="p">.</span><span class="nf">exponentialRampToValueAtTime</span><span class="p">(</span>
    <span class="nx">endGain</span><span class="p">,</span>
    <span class="nx">ctx</span><span class="p">.</span><span class="nx">currentTime</span> <span class="o">+</span> <span class="mf">0.2</span>
  <span class="p">)</span>
  <span class="nx">masterGainNode</span><span class="p">.</span><span class="nx">gain</span><span class="p">.</span><span class="nf">exponentialRampToValueAtTime</span><span class="p">(</span>
    <span class="nx">startGain</span><span class="p">,</span>
    <span class="nx">ctx</span><span class="p">.</span><span class="nx">currentTime</span> <span class="o">+</span> <span class="mf">2.1</span>
  <span class="p">)</span>
  <span class="nx">osc</span><span class="p">.</span><span class="nf">stop</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">currentTime</span> <span class="o">+</span> <span class="mf">2.1</span><span class="p">)</span>
  <span class="nx">lfo</span><span class="p">.</span><span class="nf">stop</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">currentTime</span> <span class="o">+</span> <span class="mf">2.1</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Speaking of which: I should really write some more tests. I’m using <a href="https://playwright.dev/">Playwright</a> for E2E tests and <a href="https://vitest.dev">Vitest</a> for unit tests…because that’s what Vue defaults to. I’ve had some minor experience with both of them due to using <a href="https://astro.build">Astro</a> recently, but I’m still mainly used to either <a href="https://qunitjs.com">QUnit</a> or <a href="https://www.cypress.io">Cypress</a>, so it’ll take a little getting used to.</p>

<h2 id="final-note">FINAL NOTE</h2>

<p>Of course, as a web app, NebyooAxe is not <strong>done</strong> <em>done</em> just yet. The algorithm to figure out chords could be improved with more intelligence. It only works for standard tuning and six strings, despite variations existing. It also only works for right-handedly strung guitars. I know, I know. It is not yet as featureful or accessible as it could be. My mantra, however, is that the web is art, and art is never done. However, it is currently <strong>useful</strong> to me, and possibly others, so it’s up and ready to strum.</p>

<h2 id="for-the-road">FOR THE ROAD</h2>

<p>I never took formal lessons to learn to play the guitar. I was inspired by someone in high school playing to finally pick one up and start plucking. Then I turned to tablature on the Internet to learn to play my favorite songs. Somewhere along the way I just got curious about what all the theory behind it was and that became a lifelong interest. Knowing the chord names (and how they’re derived) has been fascinating to me for a long time. Now that I have my very own configurable way to look them up, I feel like a bucket list item got checked off. Hopefully <a href="https://axe.neb.host">NebyooAxe</a> is helpful to someone else, too!</p>]]></content><author><name>Michael Chadwick</name></author><category term="blog" /><category term="astro" /><category term="audio" /><category term="chords" /><category term="cypress" /><category term="emberjs" /><category term="guitar" /><category term="music" /><category term="nebapp" /><category term="playwright" /><category term="qunit" /><category term="testing" /><category term="tool" /><category term="typescript" /><category term="vitest" /><category term="vuejs" /><category term="web" /><category term="web-app" /><category term="web-audio-api" /><category term="web-storage-api" /><summary type="html"><![CDATA[Occasionally I like to look up guitar chords and their proper names. Why? Cuz it is edifying. The web has many tools that can help with this, but none of them are mine. And now one is.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://michaelchadwick.info/posts/2026/nebyooaxe-2026-06-07.png" /><media:content medium="image" url="https://michaelchadwick.info/posts/2026/nebyooaxe-2026-06-07.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Online Guitar Tools Brainstorming</title><link href="https://michaelchadwick.info/blog/2026/05/09/online-guitar-tool-brainstorming/" rel="alternate" type="text/html" title="Online Guitar Tools Brainstorming" /><published>2026-05-09T12:00:00-07:00</published><updated>2026-05-09T12:00:00-07:00</updated><id>https://michaelchadwick.info/blog/2026/05/09/online-guitar-tool-brainstorming</id><content type="html" xml:base="https://michaelchadwick.info/blog/2026/05/09/online-guitar-tool-brainstorming/"><![CDATA[<p>As a guitarist, I sometimes need to reach for tools to facilitate the proper plucking of strings on my axe. While plenty of existing options exist, they never seem to perfectly fit what I want, and I start thinking about making my own versions. But what and how?</p>

<!--more-->

<h2 id="pre-amble">PRE-AMBLE</h2>

<p>I’ve been a guitar player since the mid-1990s, so…it’s been a minute. I grew up when the Internet was becoming popular, and one of the best things about it was the easy access to <em>tablature</em>. If you’re not familiar, imagine standard sheet music, with the bars and lines and notes, but instead of a 6-line, 5-space area, with circles denoting pitch AND duration, each note was a number, denoting the specific fret on a string instrument. Obviously, this meant non-string players did not use it, which limited its use. And each note had no intrinsic duration, so only careful spacing of text would infer that information.</p>

<div class="vextab-auto" title="The C major scale" width="570">
tabstave<br />
notes 3-5/6 2-3-5/5 2-4-5/4
</div>
<div class="vex-tabdiv-caption">The C major scale</div>

<p>Regardless of its failings, guitar tablature was easier to write and read, being closer to the metal, mimicking the 6-string fretboard of your average guitar, so I was able to grok it immediately, unlike standard sheet music. That, and all my favorite pop and rock songs were often in tablature format and accessible from OLGA (rip) or Ultimate Guitar, so I devoured that in my first few years of learning the guitbox. I even printed out a massive book of them at one point!</p>

<p>While tablature is amazing, if you can’t tune your guitar it’s not going to be much help. Also, some of the “tablature” you find may just be chord charts, and if you don’t know how to play a <a class="musical-tone musical-chord" href="#" title="click for a C#sus4" data-notes="69.296,103.83,138.59,155.56,207.65">C#sus2</a>, then you’d be out of luck. Thus, that’s kind of the three tools I first think of when I ponder a new web app to help guitarists (myself and anyone else).</p>

<ul>
  <li>Tablature</li>
  <li>Tuner</li>
  <li>Chord lookup</li>
</ul>

<h2 id="amble">AMBLE</h2>

<p>There are plenty of web apps available that do one or multiple of these things. Some of them are very good! Some of them are less good. A lot of them are full of ads. I’ve never made a web app and put ads on it. I live a privileged life where I don’t need to make money off of the random things I make for the web. Thus, I will continue to do that as long as I can.</p>

<p>Of the three tools that I identified, I think that a chord lookup app appeals to me the most. Tablature (and the editing of it) is a big job, with lots of decisions to make and edge cases to handle. A tuner would be even easier than a chord lookup tool, but I use it less, in general, since I have an app on my phone and even a couple hardware tuners (or I can just hit a note on a piano).</p>

<p>Chord lookup appeals not only to my intrinsic interest in cataloguing and identifying and theorizing about music, but it also sounds like a fun challenge. My online keyboard app, <a href="https://keebord.neb.host">Keebord</a>, already does some decent chord identification on a piano, but this would allow me to expand that, and center it around my main instrument. In fact, maybe I could even intermingle the tuning thing to potentially identify a strummed chord. I’m sure that’s a lot harder than using an algorithm with manual notes or intervals, but…it is also intriguing.</p>

<h2 id="post-amble">POST-AMBLE</h2>

<p>Decision made! A chord lookup tool it is going to be, with a potential audio input identification thingy maybe later.</p>

<p>Now…what to call it?</p>]]></content><author><name>Michael Chadwick</name></author><category term="blog" /><category term="chords" /><category term="guitar" /><category term="keebord" /><category term="tabdiv" /><category term="tablature" /><category term="tone" /><category term="tuner" /><category term="vexflow" /><category term="vextab" /><category term="vuejs" /><category term="webdev" /><summary type="html"><![CDATA[As a guitarist, I sometimes need to reach for tools to facilitate the proper plucking of strings on my axe. While plenty of existing options exist, they never seem to perfectly fit what I want, and I start thinking about making my own versions. But what and how?]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://michaelchadwick.info/posts/2026/guitar-playing.jpg" /><media:content medium="image" url="https://michaelchadwick.info/posts/2026/guitar-playing.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Making SoundLister Work Offline</title><link href="https://michaelchadwick.info/blog/2026/04/03/making-soundlister-work-offline/" rel="alternate" type="text/html" title="Making SoundLister Work Offline" /><published>2026-04-03T17:01:00-07:00</published><updated>2026-04-03T17:01:00-07:00</updated><id>https://michaelchadwick.info/blog/2026/04/03/making-soundlister-work-offline</id><content type="html" xml:base="https://michaelchadwick.info/blog/2026/04/03/making-soundlister-work-offline/"><![CDATA[<p>SoundLister lets you listen to arbitrary audio using a web-based player and playlist, but if you lose network then it fails to be useful. That is, until now.</p>

<!--more-->

<h2 id="intro-easing-you-into-the-topic">INTRO: Easing You Into the Topic</h2>

<p>While <a href="https://deckdle.neb.host">Deckdle</a> is probably my most successful and continuously used web app, I think my most useful one is <a href="https://soundlister.neb.host">SoundLister</a>, mainly because whenever I’m working on new music it gets used heavily. Adding temp tracks to something like iTunes or even iCloud Files is just not as simple or efficient, and have drawbacks I don’t want (they are not finished so they don’t yet belong in my music collection, but I still need a playlist of them to listen and scrub and loop over while I’m doing listening tests). Thus, many years ago I decided to make a web app so I could drop audio files in a directory on a server and listen to them on any Internet-enabled device. It works like a charm and more details can be found at <a href="/blog/2023/01/09/soundlister-is-live/">my initial unveiling blog post</a> (note: despite what it says then, iOS eventually fixed the ‘screen off, so next track does not play bug’!).</p>

<h2 id="verse-telling-you-a-story">VERSE: Telling You a Story</h2>

<p>The only real problem with SoundLister is a problem any web app that is delivered over a network has: if the network disappears, you can’t get to the web app. This could happen if you were on a flight or driving through a low signal area or because all wireless towers and satellites blew up instantaneously. I don’t know, man, crazy stuff happens sometimes!</p>

<p>However, there is a way to somewhat get around this: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers">Service Workers</a>.</p>

<aside class="right">
  I'd lightly used <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">Web Workers</a> on my other project <a href="https://neb.host/apps/gemwarrior">Gem Warrior</a>, mainly to see if I could. The site <a href="https://github.com/michaelchadwick/gemwarrior-web/blob/master/assets/js/app/worker.js">caches some text files</a> representing your avatar's status (sitting, standing, etc.) and uses the Web Worker to do the grunt work. Of course, loading text files is <strong>not</strong> CPU-intensive at all, so a Web Worker was overkill, but it got my mind in the door.
</aside>

<p>Service Workers act as a proxy, essentially doing a full cache of whatever you tell it to cache, and so in the future if your app requests files, like CSS or image or MP3, then it checks the cache first before going to the network. Thus, if you install and register one, you go from the initial “Web App Live From the Network” to a subsequent “Web App From the Cache”, negating the need to re-download anything, and making your “web app” a “local app” for all intents and purposes. It’s kind of magical!</p>

<p>The basic code needed is the <code class="language-plaintext highlighter-rouge">sw.js</code> file itself, which handles the event life cycle of the Service Worker itself, and a <code class="language-plaintext highlighter-rouge">navigator.serviceWorker.register</code> method that registers the Service Worker to do its thing.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app.js</span>
<span class="kd">const</span> <span class="nx">SL_SERVICE_WORKER_PATH</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">/sw.js</span><span class="dl">'</span>

<span class="nx">SoundLister</span><span class="p">.</span><span class="nx">registerServiceWorker</span> <span class="o">=</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="dl">'</span><span class="s1">serviceWorker</span><span class="dl">'</span> <span class="k">in</span> <span class="nb">navigator</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">registration</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">navigator</span><span class="p">.</span><span class="nx">serviceWorker</span><span class="p">.</span><span class="nf">register</span><span class="p">(</span>
        <span class="nx">SL_SERVICE_WORKER_PATH</span><span class="p">,</span> <span class="p">{</span>
          <span class="na">scope</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">,</span>
        <span class="p">}</span>
      <span class="p">)</span>

      <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">_logStatus</span><span class="p">(</span><span class="dl">'</span><span class="s1">Service Worker registered</span><span class="dl">'</span><span class="p">,</span> <span class="nx">registration</span><span class="p">)</span>

      <span class="k">if </span><span class="p">(</span><span class="nx">registration</span><span class="p">.</span><span class="nx">installing</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">_logStatus</span><span class="p">(</span><span class="dl">'</span><span class="s1">Service worker installing</span><span class="dl">'</span><span class="p">)</span>
      <span class="p">}</span> <span class="k">else</span> <span class="k">if </span><span class="p">(</span><span class="nx">registration</span><span class="p">.</span><span class="nx">waiting</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">_logStatus</span><span class="p">(</span><span class="dl">'</span><span class="s1">Service worker installed</span><span class="dl">'</span><span class="p">)</span>
      <span class="p">}</span> <span class="k">else</span> <span class="k">if </span><span class="p">(</span><span class="nx">registration</span><span class="p">.</span><span class="nx">active</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">_logStatus</span><span class="p">(</span><span class="dl">'</span><span class="s1">Service worker active</span><span class="dl">'</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Service Worker failed to register</span><span class="dl">'</span><span class="p">,</span> <span class="nx">error</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// sw.js</span>
<span class="kd">const</span> <span class="nx">SL_CACHE_STATIC_KEY</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">soundlist-cache-static-v1</span><span class="dl">'</span>

<span class="nb">self</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">install</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">event</span><span class="p">.</span><span class="nf">waitUntil</span><span class="p">(</span>
    <span class="nx">caches</span>
      <span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="nx">SL_CACHE_STATIC_KEY</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">cache</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">staticAssets</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">/index.html</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">/assets/css/app.css</span><span class="dl">'</span><span class="p">]</span>
        <span class="k">return</span> <span class="nx">cache</span><span class="p">.</span><span class="nf">addAll</span><span class="p">(</span><span class="nx">staticAssets</span><span class="p">)</span>
      <span class="p">})</span>

      <span class="c1">// ...we will come back to this part...</span>

  <span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>

<p>What a Service Worker is <em>not</em> is something that provides an intuitive way to cache not only static files <strong>BUT ALSO</strong> a dynamic list of audio files that can change depending on the instance or day.</p>

<h2 id="chorus-the-part-you-will-remember">CHORUS: The Part You Will Remember</h2>

<p>PHP to the rescue!</p>

<p>That is to say, of course, that I was already using a PHP script to scan a directory and build a nice array of all the subdirs and their files so that I could use it build the playlist interface.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>
<span class="c1">// to echo out and return to JS for further processing</span>
<span class="nv">$files</span> <span class="o">=</span> <span class="k">array</span><span class="p">();</span>

<span class="nv">$iterator</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RecursiveIteratorIterator</span><span class="p">(</span>
  <span class="k">new</span> <span class="nc">RecursiveDirectoryIterator</span><span class="p">(</span>
    <span class="s1">'../audio/'</span><span class="p">,</span>
    <span class="nc">FilesystemIterator</span><span class="o">::</span><span class="no">SKIP_DOTS</span>
  <span class="p">)</span>
<span class="p">);</span>

<span class="nv">$curDir</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span>

<span class="k">foreach</span> <span class="p">(</span><span class="nv">$iterator</span> <span class="k">as</span> <span class="nv">$file</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nb">in_array</span><span class="p">(</span><span class="nb">strtolower</span><span class="p">(</span><span class="nv">$file</span><span class="o">-&gt;</span><span class="nf">getExtension</span><span class="p">()),</span> <span class="nv">$exts</span><span class="p">))</span> <span class="p">{</span>
    <span class="nv">$path</span><span class="p">[</span><span class="s1">'basename'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$file</span><span class="o">-&gt;</span><span class="nf">getBasename</span><span class="p">();</span>
    <span class="nv">$path</span><span class="p">[</span><span class="s1">'filename'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$file</span><span class="o">-&gt;</span><span class="n">pathname</span><span class="p">;</span>

    <span class="c1">// etc.</span>

    <span class="nv">$files</span><span class="p">[</span><span class="nv">$dirIndex</span><span class="p">][]</span> <span class="o">=</span> <span class="nv">$path</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">echo</span> <span class="nb">json_encode</span><span class="p">(</span><span class="nv">$files</span><span class="p">);</span>
<span class="cp">?&gt;</span>
</code></pre></div></div>

<p>Once the script returns its value Javascript, one is left with a <code class="language-plaintext highlighter-rouge">const response = fetch(SL_PHP_SCRIPT)</code> call that then feeds into a <code class="language-plaintext highlighter-rouge">const data = response.json()</code> call that is then processed into the UI.</p>

<p>However, the Service Worker <code class="language-plaintext highlighter-rouge">install</code> method cannot have any arguments, so in order to load things dynamically into its cache when it is installed you need to have a manifest of sorts to tell it what to cache once you <em>do</em> know. Thus, I had to do an extra step in my PHP script, like so:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>
<span class="c1">// to echo out and return to JS for further processing</span>
<span class="nv">$files</span> <span class="o">=</span> <span class="k">array</span><span class="p">();</span>
<span class="c1">// to save to JSON for service worker manifest</span>
<span class="c1">// [</span>
<span class="c1">//   '/audio/subdir1/song1.mp3',</span>
<span class="c1">//   '/audio/subdir1/song2.mp3',</span>
<span class="c1">//   '/audio/subdir2/song1.mp3'</span>
<span class="c1">// ]</span>
<span class="nv">$filePaths</span> <span class="o">=</span> <span class="k">array</span><span class="p">();</span>

<span class="nv">$iterator</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RecursiveIteratorIterator</span><span class="p">(</span>
  <span class="k">new</span> <span class="nc">RecursiveDirectoryIterator</span><span class="p">(</span>
    <span class="s1">'../audio'</span><span class="p">,</span>
    <span class="nc">FilesystemIterator</span><span class="o">::</span><span class="no">SKIP_DOTS</span>
  <span class="p">)</span>
<span class="p">);</span>

<span class="nv">$curDir</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span>

<span class="k">foreach</span> <span class="p">(</span><span class="nv">$iterator</span> <span class="k">as</span> <span class="nv">$file</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nb">in_array</span><span class="p">(</span><span class="nb">strtolower</span><span class="p">(</span><span class="nv">$file</span><span class="o">-&gt;</span><span class="nf">getExtension</span><span class="p">()),</span> <span class="nv">$exts</span><span class="p">))</span> <span class="p">{</span>
    <span class="nv">$path</span><span class="p">[</span><span class="s1">'basename'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$file</span><span class="o">-&gt;</span><span class="nf">getBasename</span><span class="p">();</span>
    <span class="nv">$path</span><span class="p">[</span><span class="s1">'filename'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$file</span><span class="o">-&gt;</span><span class="n">pathname</span><span class="p">;</span>

    <span class="c1">// etc.</span>

    <span class="nv">$files</span><span class="p">[</span><span class="nv">$dirIndex</span><span class="p">][]</span> <span class="o">=</span> <span class="nv">$path</span><span class="p">;</span>
    <span class="nv">$filePaths</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'../audio'</span> <span class="mf">.</span> <span class="s1">'/'</span> <span class="mf">.</span> <span class="nv">$path</span><span class="p">[</span><span class="s1">'subdirPath'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'/'</span> <span class="mf">.</span> <span class="nv">$path</span><span class="p">[</span><span class="s1">'basename'</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// for Service Worker</span>
<span class="nv">$jsonString</span> <span class="o">=</span> <span class="nb">json_encode</span><span class="p">(</span>
  <span class="nv">$filePaths</span><span class="p">,</span>
  <span class="no">JSON_UNESCAPED_SLASHES</span> <span class="o">|</span> <span class="no">JSON_PRETTY_PRINT</span>
<span class="p">);</span>
<span class="c1">// make sure web server user has write access to the directory this is in!</span>
<span class="nv">$fp</span> <span class="o">=</span> <span class="nb">fopen</span><span class="p">(</span><span class="s1">'../json/audio_manifest.json'</span><span class="p">,</span> <span class="s1">'w'</span><span class="p">);</span>
<span class="nb">fwrite</span><span class="p">(</span><span class="nv">$fp</span><span class="p">,</span> <span class="nv">$jsonString</span><span class="p">);</span>
<span class="nb">fclose</span><span class="p">(</span><span class="nv">$fp</span><span class="p">);</span>

<span class="c1">// for immediate return to JS</span>
<span class="k">echo</span> <span class="nb">json_encode</span><span class="p">(</span><span class="nv">$files</span><span class="p">);</span>
<span class="cp">?&gt;</span>
</code></pre></div></div>

<p>Two notes:</p>

<ol>
  <li>The <code class="language-plaintext highlighter-rouge">JSON_UNESCAPED_SLASHES</code> flag was necessary so PHP wouldn’t write <code class="language-plaintext highlighter-rouge">\/audio\/subdir1\/song1.mp3</code> in its output, and the <code class="language-plaintext highlighter-rouge">JSON_PRETTY_PRINT</code> flag makes its output more human-readable (not necessary, but nice).</li>
  <li>The comment about web server user access was important because I was just getting 500 errors from my script until I realized the group that owned it did not have the ability to create the <code class="language-plaintext highlighter-rouge">json</code> file I was asking it to create.</li>
</ol>

<h2 id="bridge-slight-tangent-for-variety">BRIDGE: Slight Tangent for Variety</h2>

<p>As much as I want anyone reading this to listen to <em>my</em> music and fall in love with <strong>all of it</strong> and listen to it <strong><em>all the time</em></strong> and reach out to me about it, there are other people making music that is equally good (but really probably way more good), and you should check it out.</p>

<p>While writing this post I’ve been jamming out to Angine de Poitrine <a href="https://anginedepoitrine.bandcamp.com/album/vol-1">Vol. 1</a> and <a href="https://anginedepoitrine.bandcamp.com/album/vol-ii">Vol. II</a> and this stuff RULES SO HARD.</p>

<h2 id="chorus-the-part-you-remembered">CHORUS: The Part You Remembered</h2>

<p>PHP to the rescue!</p>

<p>Not that saying ‘PHP is cool’ has ever been, well, <em>cool</em>, I’d still like to shout it out because when I need to drop to the <em>backend</em> in order to do things frontend Javascript won’t do I still default to PHP. Python is really popular, too, and it seems to be getting more popular as LLM and AI agent programming becomes the norm, but it’s not in my gut like PHP.</p>

<p>My first ever real web app was your typical *AMP (Mac/Win/Linux, Apache, MySQL, PHP) type of project, and it really laid the groundwork for how I think to make a web application to this day. They released <strong>8.5</strong> back in November 2025 and <strong>8.6</strong> is already in development, so support remains strong even if it’s not hip to use.</p>

<h2 id="outro-transition-out-of-here">OUTRO: Transition Out of Here</h2>

<p>As a real test of this new Service Worker, I took a drive and loaded up SoundLister on my phone (with network on, obvs). Once it was done, I turned on Airplane Mode and refreshed the site. Lo and behold, it loaded and played audio like it was a local app with no need for network activity! It felt like real magic, honestly. There was a hiccup at one point where a track stopped playing out of nowhere, but a refresh of the site fixed things.</p>

<p>Since this blog post is so musically-themed, I will leave you with the outro of an actual song in the works:</p>
<audio controls="">
  <source src="/assets/audio/posts/undu-outro.mp3" type="audio/mpeg" />
No &lt;audio&gt; support in your browser found.
</audio>]]></content><author><name>Michael Chadwick</name></author><category term="blog" /><category term="offline" /><category term="service-worker" /><category term="soundlister" /><category term="webdev" /><summary type="html"><![CDATA[SoundLister lets you listen to arbitrary audio using a web-based player and playlist, but if you lose network then it fails to be useful. That is, until now.]]></summary></entry><entry><title type="html">YouTube Video ID Allowance</title><link href="https://michaelchadwick.info/blog/2026/02/02/youtube-video-id-allowance/" rel="alternate" type="text/html" title="YouTube Video ID Allowance" /><published>2026-02-02T01:22:00-08:00</published><updated>2026-02-02T01:22:00-08:00</updated><id>https://michaelchadwick.info/blog/2026/02/02/youtube-video-id-allowance</id><content type="html" xml:base="https://michaelchadwick.info/blog/2026/02/02/youtube-video-id-allowance/"><![CDATA[<p>All current existing YouTube videos can be filtered by a single 11-character id. Does that really seem like enough?</p>

<!--more-->

<p>Have you ever wondered how the many, many, MANY YouTube videos that apparently exist can really be filtered by such a small id? It doesnt seem nearly enough, especially when we live in a world with systems that use alphanumeric ids of 16, 20, 24, and more. Of course, my grasp of big numbers is limited and deplorable, so I did a little more digging.</p>

<p>According to some brief research it seems like YouTube (as of sometime last year) has about ~15 <em>billion</em> videos, with upload rates each day in the <em>millions</em>. The <code class="language-plaintext highlighter-rouge">id</code> that each video uses is an 11-character combination of three character sets: <code class="language-plaintext highlighter-rouge">A-Z (26)</code>, <code class="language-plaintext highlighter-rouge">a-z (26)</code>, and <code class="language-plaintext highlighter-rouge">[0-9] (10)</code>, which makes the number of choices for each character <code class="language-plaintext highlighter-rouge">62</code>. Remembering my high school math lead me to a permutation to figure out:</p>

<p><sub>n</sub>P<sub>r</sub> = n! / (n - r)! -&gt; <sub>62</sub>P<sub>11</sub> = 62! / (62 - 11)!</p>

<p>This results in about <em>20 quintillion</em> (i.e. 20 with 18 zeroes) possibilities for a YouTube video ID. The current data set is nowhere near hitting that number. Thus, an 11-character ID really can easily filter all those videos.</p>

<p>This thought experiment has now concluded, but go read this <a href="https://www.bbc.com/future/article/20250213-youtube-at-20-a-computer-that-drunk-dials-online-videos-reveals-statistics-that-google-doesnt-want-you-to-know">BBC article</a> which gets into it more.</p>]]></content><author><name>Michael Chadwick</name></author><category term="blog" /><category term="numbers" /><category term="thought" /><category term="youtube" /><summary type="html"><![CDATA[All current existing YouTube videos can be filtered by a single 11-character id. Does that really seem like enough?]]></summary></entry><entry><title type="html">Anti-Social Mediation</title><link href="https://michaelchadwick.info/blog/2026/01/31/antisocial-mediation/" rel="alternate" type="text/html" title="Anti-Social Mediation" /><published>2026-01-31T00:00:00-08:00</published><updated>2026-01-31T00:00:00-08:00</updated><id>https://michaelchadwick.info/blog/2026/01/31/antisocial-mediation</id><content type="html" xml:base="https://michaelchadwick.info/blog/2026/01/31/antisocial-mediation/"><![CDATA[<p>After many years of sharing my life in a public broadcast kind of way, a server upgrade kills my love for even the last bastion of social media I still use consistently.</p>

<!--more-->

<p>I just killed my single-user, hosted instance of <a href="https://joinmastodon.org">Mastodon</a>.</p>

<p>Perhaps a victim of the <a href="/blog/2026/01/23/the-binge-purge-tech-cycle/">binge-purge cycle</a>?</p>

<h2 id="context">CONTEXT</h2>

<p>When I first heard about Mastodon, and that it could be self-hosted, I was very interested. I’ve had my own Linux VPS (Virtual Private Server) for many years now, and I love to try out new self-hosted software. It started with web and file serving. Then, slowly but surely, I’ve been moving services that used to be on a server I had no control over to my own server which I do. I was also excited to try a social media/microblogging platform that didn’t use an algorithm, nor that was owned by a tech billionaire.</p>

<p>My server started on Debian 9(?), and moving to 10 didn’t bring any major issues, but I was also running a lot less on it. Moving to 11 also didn’t bring any major issues that I can remember. Even a recent move from 11 to 12 was no big deal, but going to 13 borked several things: my mail server, a web site using Phusion Passenger, and my self-hosted version of Mastodon. The other two are going to need more research and work at some point, but I attempted to ameliorate the latter one today. Before I even started, I was tired. Knowing how something works and being able to fix it are things I value, but there’s a cost to the knowledge and the application of it. I have a limit on how much I want to endure, and I guess I finally hit it today.</p>

<p>I’ve been using Mastodon for several years now, occasionally posting to it, and using it to keep up on other creative people who did fun stuff like make games, music, animations, and other art. I wish it were used by more people I know, but I understand why it is not. Regardless, I believe it truly is the best way to be social online today, in a public way, to a wide audience, without being subject to tech giant algorithm nonsense.</p>

<p>Unfortunately, like every other microblogging platform that has ever existed, if you don’t already have an audience, it’s not some guarantee to getting one. Thus, a lot of the time it felt like I was posting into a void, much like when I make a blog like this one. The blog is just a <em>part</em> of my personal website, however, and so failing to write new blog posts often doesn’t mean I can’t tinker with the rest of it. Mastodon, on the other hand, is just the blog, and nothing else.</p>

<p>Not being a stranger to putting things online that no one looks at, this didn’t bother me, and so I used Mastodon, anyway. Running a Mastodon <em>server</em>, however, can be frustrating like running any other kind of server, and the difficulty of upgrading a Linux OS coupled with some confusion about the proper <code class="language-plaintext highlighter-rouge">postgresql</code> version to use resulted in losing my years-old account and friends list and post history. With more effort expended, I could go to a backup, extract said data, and put more hours into trying to fix things. But over the years I’ve grown almost as much weary at the concept of this particular brand of devops as I am curious. In other words, I’m getting too old for this shit.</p>

<p>So, I made a decision: just blow away the whole thing. The server maintenance and occasional bugfixing and Google sleuthing that comes with the whole thing, espcially when I barely use it, just doesn’t add up to enough value for it to feel worth it to me right now. It means, however, that what little “social media” I actively contribute to just took a dive. My time spent on Facebook or Instagram or X/Twitter or whatever can be measured in “barely” or “non-existent”, so removing Mastodon means I’m down to just my <a href="https://michaelchadwick.info">website</a>. Just like the old days, I guess. Except in the old days, I had LiveJournal. I didn’t control it, but I posted to it often, as did my friends, and we had a community. That particular community died years ago, however, and I feel like social on the web has never been the same since.</p>

<h2 id="for-the-road">FOR THE ROAD</h2>

<p>I’ll miss the people I found on Mastodon, and maybe I’ll join some non-self-hosted instance someday, but for now…I feel a burden lifted. Mastodon was kind of a behemoth to host, and while I probably made it more difficult on myself not using Docker (it would probably have made the dependency management much less of a concern), the overall utility of it as a platform plus the exhausted frustration made it something I could see myself going without. Tootles for now.</p>]]></content><author><name>Michael Chadwick</name></author><category term="blog" /><category term="debian" /><category term="facebook" /><category term="instagram" /><category term="livejournal" /><category term="mastodon" /><category term="social-media" /><category term="twitter" /><summary type="html"><![CDATA[After many years of sharing my life in a public broadcast kind of way, a server upgrade kills my love for even the last bastion of social media I still use consistently.]]></summary></entry><entry><title type="html">The Binge-Purge Tech Cycle</title><link href="https://michaelchadwick.info/blog/2026/01/23/the-binge-purge-tech-cycle/" rel="alternate" type="text/html" title="The Binge-Purge Tech Cycle" /><published>2026-01-23T00:00:00-08:00</published><updated>2026-01-23T00:00:00-08:00</updated><id>https://michaelchadwick.info/blog/2026/01/23/the-binge-purge-tech-cycle</id><content type="html" xml:base="https://michaelchadwick.info/blog/2026/01/23/the-binge-purge-tech-cycle/"><![CDATA[<p>You begin life simple and slowly become more complex. Within that life there are often multiple periods of growth and reduction. This is evident quite clearly in one’s interests. My life is no different.</p>

<!--more-->

<p>You are born. You no longer get all nutrients from another life form (well, ok, you still will for a while, but the process of stopping this begins). It’s a lot brighter, a lot noisier, and a lot weirder than the small, warm, comfortable space you’ve been in for months. However, while you were gestating, you grew from a simple organism to a complex one. And now that you’re on The Outside, you’re only going to get a lot more complex.</p>

<p>…</p>

<p>I’m now in my mid-40s. I’ve experienced things. I’ve learned things. My interests have broadened. Regardless, I go through periods where I’m seriously all about adding more stuff to my life, and then periods where I want to just excise any and everything non-essential.</p>

<p>As a kid, I was into collecting comic books and trading cards of the sports and comic varieties. These collections now sit in boxes in my garage, never looked at for decades. I did some research on whether they’re worth anything, and they’re not. I would like to purge them from my life as they don’t bring any value anymore, but it’s hard to just throw them away. So…they sit.</p>

<p>…</p>

<p>Since “getting into computers” in the mid-90s, being able to learn and toy with new things, and thusly <em>binge</em> them, has become quite easy. Bingeing on something when it’s new or once-new-and-now-refreshed is often followed by a purge once the newness wears off, or the administration to handle it becomes mentally taxing.</p>

<div class="binge-purge">
  <span class="binge">BINGE</span>: I can <strong>run applications</strong> on this thing! I will try this application and that application and this application, because it's so easy and fun! Now I have a LOT of apps on my computer. App stores made the process easier, and now Github has become the place I look first for new tools and technology since I can get them and see how they're made and modify them.<br />
  <span class="purge">PURGE</span>: My hard drive space is filling up and I realize I haven't used this or that application in a long time, so let's get rid of it.<br /><br />

  <span class="binge">BINGE</span>: I can <strong>collect music</strong> to listen to! During college I "acquired" a lot of things, and then later when I was gainfully employed I bought a lot of things. Between the two, I've got a music library that's tens of thousands of songs large, good enough for ~2 months of constant listening without repeats.<br />
  <span class="purge">PURGE</span>: Despite lots of space, looking at 20,000+ (not even mentioning Spotify, Apple Music, et. al.) tracks makes choosing any one thing to listen to near-impossible, so I just keep occasionally getting new stuff, listening to it, and then repeating that cycle, and ignore the years of music I already have. Like photos, there's no real need to purge them since they don't lose value and I can always listen later.
</div>

<p>You can see where this has gone and will continue to go. New things are fun and as cheap storage space ballooned over the decades, acquiring and storing it all got easier and easier. The Internet has allowed more people access to more things and so much of it can be digitized so it’s accessible.</p>

<div class="binge-purge">
  <span class="binge">BINGE</span>: I can <strong>browse and save favorite websites</strong> in a browser on this thing! I have many different folders of bookmarks now! I also have a Read-It-Later web service I self-host that has thousands of entries, so that I can largely not look at or refer to after I save them.<br />
  <span class="purge">PURGE</span>: Why did I save this website I haven't visited in forever as a bookmark? Time to remove. Also, this directory of sites all pertain to some activity I don't do anymore. Gone.<br /><br />

  <span class="binge">BINGE</span>: I can <strong>MAKE websites</strong> and <strong>buy domains</strong> with this thing! Since I <em>know how to</em> make a website for any old idea that comes across my brain, I <em>should</em> probably do that. Oh, wow, I have a <a href="https://omni.neb.host">LOT</a> of websites now.<br />
  <span class="purge">PURGE</span>: I don't need so many domains. No one goes to these sites, and I haven't worked on them in a while, so axe 'em. They can be kept as subdomains of fewer domains! `/time-passes` Wait, no one visits these sub-domains, not even me, so...make them sub-directories so I don't have to see so many items when I do a <code>ls -la /var/www</code>.<br /><br />

  <span class="binge">BINGE</span>: I can <strong>take and hoard pictures</strong> to look at! Modern mobile devices with excellent cameras make taking photos easy and so I will do that. Now I have years and years of photos that I probably won't look at often, but I guess I'm glad I have them?<br />
  <span class="purge">PURGE</span>: The cloud storages like iCloud/Google/OneDrive/etc. and a large backup drive make this a thing I can just ignore, so no purging ever really happens, but I should probably cull them? I'm going to take more pictures, so the collection is only going to grow...
</div>

<p>This is not a new thing because of digital life, as people have hoarded material things for centuries, buying bigger homes to store more <em>stuff</em>. As I’ve gone through life, my place of residence and the potential amount of storage I have increased, and I’ve definitely spent some time filling it. However, the digital avenue allows for <em>immense scale</em> and <em>speed</em> in harboring binge-dom. If you keep on this road and never stop, then you might become a “hoarder” and, at the most extreme, become fodder for a show. If you course-correct and go the other way without stopping, you might join the “minimalist” cult. As I’ve learned over the years, however, is that long-term extremism is not healthy.</p>

<div class="binge-purge">
  <span class="binge">BINGE</span>: I can <strong>play video games</strong> on this thing! Steam and GOG and all the other marketplaces that have seemingly limitless games to offer make it very easy to build a monumental collection of games. I've got plenty and I know I haven't played them all. Looking at the possibilities to choose from brings that <a href="https://en.wikipedia.org/wiki/Resource_curse">paradox of abundance</a> into sharp relief.<br />
  <span class="purge">PURGE</span>: Like other media, I don't purge my collection, but I ignore existing games and focus on a few new things every once in a while until I beat them or grow tired of them. Then the cycle continues.<br /><br />

  <span class="binge">BINGE</span>: I can <strong>learn programming languages</strong> on this thing! Now I have a <code>~/Code</code> folder full of various projects big and small in lots of languages, just because I <em>can</em>.<br />
  <span class="purge">PURGE</span>: New projects will fill up my local machine as I work on then. Once they're done-ish and enter maintenance mode I will tend to move them off to my backup storage. I like having lots of projects to go between, so this will build up again until I get overwhelmed or lose interest, and then they will get purged to backup again.
</div>

<p>Even outside of the digital world, I go through periods of binge-purge.</p>

<div class="binge-purge">
  <span class="binge">BINGE</span>: <strong>E-bikes are amazing</strong>! A few years ago I test-rode one for the first time, and was instantly sold on the idea. I like riding a bike, but the landscape of my city is quite hilly, and I'm not very physically fit. This is the answer! I bought one, and then a while later I bought two more for the rest of my family. We will all go on bike rides together!<br />
  <span class="purge">PURGE</span>: And we did...once or twice. Then the reality sets in that I'm the most into this thing, and the other two bikes largely languish in the garage. Time to put two of three bikes on the market, and probably only go on sporadic rides.<br /><br />

  <span class="binge">BINGE</span>: <strong>3D printers are rad</strong>! The family got one for the most recent Christmas, and we spent most of the holiday listening to the <em>brrrrring</em> of the print heads moving quickly back and forth, creating lots of fun (and sometimes useful!) widgets and doohickeys.<br />
  <span class="purge">PURGE</span>: While we will print more stuff in the future, the initial buzz of it has worn off and it currently sits silent. We got a laser printer a few months ago and it had its initial hey-day, too, but I barely register it being there now.
</div>

<p>I could do an AskReddit to see if I’m the only one who has this issue, but without even trying I’m 100% sure that is false. Perhaps this binge-purge cycle is on a spectrum, like with many character traits. We all do it to some degree, and we all probably move around on the spectrum over time.</p>

<p>As a final meta-note, I once did blogging on the Internet SO VERY MUCH, sometimes blogging multiple times a day. Now, it has taken me literally days to even finish this one. And the last one I did was months ago.</p>

<p>No great conclusion to any of this, but glad I finally got it out, as it’s something that’s rolled around in my head for years. To wit: moderation in all things, including moderation.</p>]]></content><author><name>Michael Chadwick</name></author><category term="blog" /><category term="reflection" /><summary type="html"><![CDATA[You begin life simple and slowly become more complex. Within that life there are often multiple periods of growth and reduction. This is evident quite clearly in one's interests. My life is no different.]]></summary></entry><entry><title type="html">Defolding My Love For 2D Gamedev</title><link href="https://michaelchadwick.info/blog/2025/10/27/defolding-my-love-for-2d-gamedev/" rel="alternate" type="text/html" title="Defolding My Love For 2D Gamedev" /><published>2025-10-27T00:00:00-07:00</published><updated>2025-10-27T00:00:00-07:00</updated><id>https://michaelchadwick.info/blog/2025/10/27/defolding-my-love-for-2d-gamedev</id><content type="html" xml:base="https://michaelchadwick.info/blog/2025/10/27/defolding-my-love-for-2d-gamedev/"><![CDATA[<p>Season two of Hacking the Grepson is all about making a game. After some consideration, the decision to use Love2D and/or Defold, two different game engines, was made and a concentrated effort to tinker with both of them has begun.</p>

<!--more-->

<h2 id="context">CONTEXT</h2>

<p>For the first 100 episodes of <a href="https://hackingthegrepson.com">Hacking the Grepson</a>, the podcast about programming and related topics that my friend Matt and I do every other week, we did various topics. These topics include the following: programming languages, Wordle, software development life cycle, documentation, Factorio, technical debt, data analytics, Slay the Spire, version control, and many more.</p>

<p>You may have noticed a few proper nouns in that list, and those proper nouns are all what? GAMES. Many people become programmers to make games, and then end up working on spreadsheets or websites to pay the bills because those are generally easier and more applicable to more people. However, that itch to make a super cool game that’s fun and addictive never quite goes away.</p>

<h2 id="action">ACTION</h2>

<p>Thus! For season 2 (episode 101+) we decided to try something different and just have one topic for as many episodes as it takes: Make a Game (Together). That means we have had to decide what kind of game we are going to make, what to make it in, and then…make it and deploy it to the world.</p>

<p>After a couple of episodes (and some offline chatter), we have both gravitated to using <a href="https://love2d.org">Love2D</a> (for prototyping) and <a href="https://defold.com">Defold</a> (for the final product). I’d played with the former back in the day (<a href="https://playbalatro.com">Balatro</a> was made with it!), but never even heard of Defold until Matt brought it up (apparently it was created by an offshoot of <a href="https://avalanchestudios.com/">Avalanche Studios</a>, the makers of Just Cause). They both use <a href="https://lua.org">Lua</a>, a very popular scripting language that seems to be embedded in a lot of other things (I remember first experiencing it when investigating World of Warcraft mods waaaaay back in the day).</p>

<p>As far as which kind of game we are going to make, or which platform, or which control scheme, or which theme, or which mechanics, or <code class="language-plaintext highlighter-rouge">[insert-other-aspect-of-game]</code>…that is still being actively decided. At least we know which tool(s) we are going to use, and are both getting the hang of both. I’ve been making prototypes, going through tutorials, and testing the online documentation and wikis of both tools.</p>

<h3 id="love2d">LOVE2D</h3>

<p>Love (or LÖVE, as the creators stylize it, and as Love2D, which is how I will refer to it to make it distinct from the emotion/concept) is a <a href="https://github.com/love2d/love">free, open-source framework</a> built on Lua, which means it takes the base Lua scripting language and adds a bunch of <code class="language-plaintext highlighter-rouge">love</code> (objects/classes) to it, and allows you to compile and run it (or export it to various things). Love2D has no IDE, so you write your code in a text editor of your choice, and build whatever pre-existing assets you want to use in whichever tool you choose, referencing them in code, but with no Love2D-specific GUI tooling to manipulate or organize them.</p>

<p>Here is some example code from a 2-player game’s drawing routine:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="nc">love</span><span class="p">.</span><span class="nf">draw</span><span class="p">()</span>
  <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="n">setBackgroundColor</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">3</span><span class="p">)</span>

  <span class="c1">-- draw player1</span>
  <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="n">setCanvas</span><span class="p">(</span><span class="n">screenCanvas</span><span class="p">)</span>
    <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="n">clear</span><span class="p">()</span>
    <span class="n">drawEntities</span><span class="p">(</span><span class="n">player1</span><span class="p">)</span>
  <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="n">setCanvas</span><span class="p">()</span>
  <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="n">draw</span><span class="p">(</span><span class="n">screenCanvas</span><span class="p">)</span>

  <span class="c1">-- draw player2</span>
  <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="n">setCanvas</span><span class="p">(</span><span class="n">screenCanvas</span><span class="p">)</span>
    <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="n">clear</span><span class="p">()</span>
    <span class="n">drawEntities</span><span class="p">(</span><span class="n">player2</span><span class="p">)</span>
  <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="n">setCanvas</span><span class="p">()</span>
  <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="n">draw</span><span class="p">(</span><span class="n">screenCanvas</span><span class="p">,</span> <span class="mi">400</span><span class="p">)</span>

  <span class="c1">-- Add a line to separate the screens</span>
  <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="n">line</span><span class="p">(</span><span class="n">WinWidthCenter</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">WinWidthCenter</span><span class="p">,</span> <span class="n">WinHeight</span><span class="p">)</span>

  <span class="c1">-- draw scores</span>
  <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="nb">print</span><span class="p">(</span><span class="s2">"$1: "</span> <span class="o">..</span> <span class="n">player1</span><span class="p">.</span><span class="n">score</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>
  <span class="n">love</span><span class="p">.</span><span class="n">graphics</span><span class="p">.</span><span class="nb">print</span><span class="p">(</span><span class="s2">"$2: "</span> <span class="o">..</span> <span class="n">player2</span><span class="p">.</span><span class="n">score</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">30</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="defold">DEFOLD</h3>

<p>Defold is a fully-fledged game engine and IDE (write your code and build projects from the application) also built on Lua. You spend most of your time in the application, writing code, creating tilesets and animations, and organizing things inside Game Objects and Collections. You still build your pre-existing assets in whichever tool you choose, but once inside Defold there are more GUI affordances for manipulating and integrating them.</p>

<p>Here is an excerpt from an avatar’s update routine:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="nf">update</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">dt</span><span class="p">)</span>
  <span class="o">...</span>

  <span class="kd">local</span> <span class="n">anim</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">current_anim</span>
  <span class="kd">local</span> <span class="n">dx</span><span class="p">,</span> <span class="n">dy</span> <span class="o">=</span> <span class="n">axis_to_vec</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">input</span><span class="p">)</span>
  <span class="kd">local</span> <span class="n">moving</span> <span class="o">=</span> <span class="p">(</span><span class="n">dx</span> <span class="o">~=</span> <span class="mi">0</span> <span class="ow">or</span> <span class="n">dy</span> <span class="o">~=</span> <span class="mi">0</span><span class="p">)</span>

  <span class="c1">-- Update facing direction and animation when moving</span>
  <span class="k">if</span> <span class="n">dx</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="k">then</span>
    <span class="n">anim</span> <span class="o">=</span> <span class="n">hash</span><span class="p">(</span><span class="s2">"left"</span><span class="p">)</span>
    <span class="n">self</span><span class="p">.</span><span class="n">facing_direction</span> <span class="o">=</span> <span class="n">vmath</span><span class="p">.</span><span class="n">vector3</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
  <span class="k">elseif</span> <span class="n">dx</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">then</span>
    <span class="n">anim</span> <span class="o">=</span> <span class="n">hash</span><span class="p">(</span><span class="s2">"right"</span><span class="p">)</span>
    <span class="n">self</span><span class="p">.</span><span class="n">facing_direction</span> <span class="o">=</span> <span class="n">vmath</span><span class="p">.</span><span class="n">vector3</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
  <span class="k">elseif</span> <span class="n">dy</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">then</span>
    <span class="n">anim</span> <span class="o">=</span> <span class="n">hash</span><span class="p">(</span><span class="s2">"back"</span><span class="p">)</span>
    <span class="n">self</span><span class="p">.</span><span class="n">facing_direction</span> <span class="o">=</span> <span class="n">vmath</span><span class="p">.</span><span class="n">vector3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
  <span class="k">elseif</span> <span class="n">dy</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="k">then</span>
    <span class="n">anim</span> <span class="o">=</span> <span class="n">hash</span><span class="p">(</span><span class="s2">"front"</span><span class="p">)</span>
    <span class="n">self</span><span class="p">.</span><span class="n">facing_direction</span> <span class="o">=</span> <span class="n">vmath</span><span class="p">.</span><span class="n">vector3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
  <span class="k">else</span>
    <span class="c1">-- When stopped, switch to idle only if facing front, otherwise keep current animation</span>
    <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">facing_direction</span> <span class="o">==</span> <span class="n">vmath</span><span class="p">.</span><span class="n">vector3</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">then</span>
      <span class="n">anim</span> <span class="o">=</span> <span class="n">hash</span><span class="p">(</span><span class="s2">"idle"</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="n">anim</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">current_anim</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="o">...</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="progress-thus-far">PROGRESS THUS FAR</h2>

<p>After almost two months of this experiment, and many hours put into both tools, it’s hard to say one is a clear winner. They both use Lua, so there’s no real difference there. Love2D is more of a “write code and run compiler on command line” flow, whereas Defold is an “open project, tinker, (re)-build” flow. They both run quickly and allow for quick iteration. Defold having built-in tilesource/tilemap support so you can easily build sprite-based levels is nice. However, I’m still getting tripped up on its collection/game object organization scheme. Love2D feels closer to Pico-8, which I’ve used a lot, but it’s missing the latter’s sprite/music editors (as is Defold).</p>

<p>Basically, this point in the project is still mainly learning tooling and trying to form muscle memory for the basic foundational things, so progress consists largely of going through tutorials, tweaking them, and spending a lot of time in Google/Wikis/Ollama. It’s not all fun, and often frustrating, but that’s part of the journey.</p>

<p>For now, I’m enjoying spending time messing around with various prototypes here and there and then doing a bi-weekly checkin with Matt on the podcast. It’s almost like recording a development team’s standup. I also started a “blog” of sorts which just consists of notes about each checkin, so we have a record of what we did each meeting, and can look back on progress.</p>

<h2 id="for-the-road">FOR THE ROAD</h2>

<p>Hopefully all of this coalesces into a single project that we share and iterate on, but for now it’s just the wild west of gamedev, baby.</p>]]></content><author><name>Michael Chadwick</name></author><category term="blog" /><category term="defold" /><category term="htg" /><category term="gamedev" /><category term="love2d" /><category term="lua" /><category term="podcast" /><summary type="html"><![CDATA[Season two of Hacking the Grepson is all about making a game. After some consideration, the decision to use Love2D and/or Defold, two different game engines, was made and a concentrated effort to tinker with both of them has begun.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://michaelchadwick.info/posts/2025/defold-love2d-gamedev.jpg" /><media:content medium="image" url="https://michaelchadwick.info/posts/2025/defold-love2d-gamedev.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Event Listener Ephiphany</title><link href="https://michaelchadwick.info/blog/2025/08/27/event-listener-epiphany/" rel="alternate" type="text/html" title="Event Listener Ephiphany" /><published>2025-08-27T00:00:00-07:00</published><updated>2025-08-27T00:00:00-07:00</updated><id>https://michaelchadwick.info/blog/2025/08/27/event-listener-epiphany</id><content type="html" xml:base="https://michaelchadwick.info/blog/2025/08/27/event-listener-epiphany/"><![CDATA[<p>Being able to use Javascript event listeners to check when certain things happen, and then trigger useful tasks when they do, is not always as straightforward as I’d think.</p>

<!--more-->

<p>Basically, I wanted <a href="https://soundlister.neb.host">SoundLister</a> to honor a keyboard function key (like the one I have on my Mac) that toggles play/pause, while also honoring the UI play/pause button. Since SL uses a regular <code class="language-plaintext highlighter-rouge">&lt;audio&gt;</code> element, pressing that key would play/pause the audio like expected, but the UI would NOT toggle like you’d expect, since the existing event listener was only checking for clicking on the main player’s icon.</p>

<p>At first I thought I needed to find an EventListener for <code class="language-plaintext highlighter-rouge">keypress</code>, as the event in question was a ‘key’ being pressed. Seems intuitive, right? That’s how I added some simple keyboard shortcuts for play/pause and skip forward/back:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">keydown</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">code</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">Space</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// fix issue with double-triggering</span>
    <span class="c1">// if space bar is activeElement</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">.</span><span class="nf">blur</span><span class="p">();</span>
    <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">_updatePlayState</span><span class="p">(</span><span class="dl">'</span><span class="s1">key</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="c1">// Next Track: Shift+Cmd/Win+Right</span>
    <span class="c1">// Prev Track: Shift+Cmd/Win+Left</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">metaKey</span> <span class="o">&amp;&amp;</span> <span class="nx">event</span><span class="p">.</span><span class="nx">shiftKey</span> <span class="o">&amp;&amp;</span> <span class="nx">event</span><span class="p">.</span><span class="nx">code</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">ArrowRight</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">goForward</span><span class="p">();</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if </span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">metaKey</span> <span class="o">&amp;&amp;</span> <span class="nx">event</span><span class="p">.</span><span class="nx">shiftKey</span> <span class="o">&amp;&amp;</span> <span class="nx">event</span><span class="p">.</span><span class="nx">code</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">ArrowLeft</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">goBack</span><span class="p">();</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<p>However, despite being aided by this wonderful <a href="https://www.toptal.com/developers/keycode">JavaScript Key Codes</a> site, JS doesn’t seem to <em>hear</em> function keys (F1-F19). Why? Probably some OS-specific low-level reason. I dunno. Regardless, I was looking in the wrong place: I needed to listen for when audio began to play or was paused. I was already doing this, but I needed to augment it and figure out the proper logic to keep from accidentally getting stuck in loops. This was achieved by properly seperating the various entrypoints into toggling the play/pause state of the <code class="language-plaintext highlighter-rouge">&lt;audio&gt;</code> element:</p>

<ul>
  <li>Clicking on the playlist itself</li>
  <li>Clicking on the collection dropdown and choosing a new subset of tracks</li>
  <li>Clicking on the UI play/pause icon (or using keyboard shortcuts)</li>
  <li><code class="language-plaintext highlighter-rouge">&lt;audio&gt;</code> events that the other methods would trigger</li>
</ul>

<p>I ended up with the following:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// excerpt from events.js</span>
<span class="c1">// ...</span>
<span class="c1">// audio player UI play/pause icon</span>
<span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButton</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">_updatePlayState</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
<span class="c1">// &lt;audio&gt; element has started playing</span>
<span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">audio</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">play</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">_updatePlayState</span><span class="p">();</span>
  <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">_setTitle</span><span class="p">();</span> <span class="c1">// set &lt;title&gt; with current track name</span>
<span class="p">});</span>
<span class="c1">// &lt;audio&gt; element has been paused</span>
<span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">audio</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">pause</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">_updatePlayState</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// excerpt from main.js</span>
<span class="c1">// ...</span>
<span class="c1">// change play/pause icon and audio element depending on context</span>
<span class="nx">SoundLister</span><span class="p">.</span><span class="nx">_updatePlayState</span> <span class="o">=</span> <span class="p">(</span><span class="nx">source</span> <span class="o">=</span> <span class="kc">null</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">switch </span><span class="p">(</span><span class="nx">source</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// clicking in the audio playlist auto-starts track</span>
    <span class="c1">// and sets play/pause icon to 'pause'</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">playlist</span><span class="dl">'</span><span class="p">:</span>
      <span class="c1">// start the audio scrubbing bar updating so refreshes every second</span>
      <span class="nf">requestAnimationFrame</span><span class="p">(</span><span class="nx">SoundLister</span><span class="p">.</span><span class="nx">_whilePlaying</span><span class="p">);</span>

      <span class="c1">// change play/pause icon to 'pause'</span>
      <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-play</span><span class="dl">'</span><span class="p">);</span>
      <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-pause</span><span class="dl">'</span><span class="p">);</span>

      <span class="k">break</span><span class="p">;</span>

    <span class="c1">// clicking on the collection dropdown auto-stops track</span>
    <span class="c1">// and sets play/pause icon to 'play'</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">collection</span><span class="dl">'</span><span class="p">:</span>
      <span class="c1">// stop updating the audio scrubber bar refresh</span>
      <span class="nf">cancelAnimationFrame</span><span class="p">(</span><span class="nx">SoundLister</span><span class="p">.</span><span class="nx">raf</span><span class="p">);</span>

      <span class="c1">// load first track in collection</span>
      <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">audio</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">SoundLister</span><span class="p">.</span><span class="nf">tracks</span><span class="p">()[</span><span class="mi">0</span><span class="p">].</span><span class="nx">href</span><span class="p">;</span>

      <span class="c1">// change play/pause icon to 'play'</span>
      <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-pause</span><span class="dl">'</span><span class="p">);</span>
      <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-play</span><span class="dl">'</span><span class="p">);</span>

      <span class="k">break</span><span class="p">;</span>

    <span class="c1">// clicking directly on the play/pause icon</span>
    <span class="c1">// or using the space or next/prev keys</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">:</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">key</span><span class="dl">'</span><span class="p">:</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">audio</span><span class="p">.</span><span class="nx">paused</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">audio</span><span class="p">.</span><span class="nf">play</span><span class="p">();</span>

        <span class="c1">// start the audio scrubbing bar updating so refreshes every second</span>
        <span class="nf">requestAnimationFrame</span><span class="p">(</span><span class="nx">SoundLister</span><span class="p">.</span><span class="nx">_whilePlaying</span><span class="p">);</span>

        <span class="c1">// change play/pause icon to 'play'</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-pause</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-play</span><span class="dl">'</span><span class="p">);</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">audio</span><span class="p">.</span><span class="nf">pause</span><span class="p">();</span>

        <span class="c1">// stop updating the audio scrubber bar refresh</span>
        <span class="nf">cancelAnimationFrame</span><span class="p">(</span><span class="nx">SoundLister</span><span class="p">.</span><span class="nx">raf</span><span class="p">);</span>

        <span class="c1">// change play/pause icon to 'pause'</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-play</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-pause</span><span class="dl">'</span><span class="p">);</span>
      <span class="p">}</span>

      <span class="k">break</span><span class="p">;</span>

    <span class="c1">// audio play/pause events</span>
    <span class="nl">default</span><span class="p">:</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">audio</span><span class="p">.</span><span class="nx">paused</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// change play/pause icon to 'play'</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-pause</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-play</span><span class="dl">'</span><span class="p">);</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="c1">// change play/pause icon to 'pause'</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-play</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">SoundLister</span><span class="p">.</span><span class="nx">dom</span><span class="p">.</span><span class="nx">playButtonIcon</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">fa-pause</span><span class="dl">'</span><span class="p">);</span>
      <span class="p">}</span>

      <span class="k">break</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The real key was making sure that activating the play/pause UI icon didn’t create an infinite loop of audio start/stop events, causing the icon to toggle endlessly and the audio to take on a nice glitchy tremelo feature.</p>

<p>Trial and error, complication followed by simplification, and lots of refreshing of the browser finally got me to the finished, working state. Now I can hit the function key on my Mac and the status of everything syncs up nicely.</p>]]></content><author><name>Michael Chadwick</name></author><category term="blog" /><category term="audio" /><category term="event-listeners" /><category term="keyboard" /><category term="javascript" /><category term="js" /><category term="soundlister" /><category term="webdev" /><summary type="html"><![CDATA[Being able to use Javascript event listeners to check when certain things happen, and then trigger useful tasks when they do, is not always as straightforward as I'd think.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://michaelchadwick.info/posts/2025/event-listener-epiphany.jpg" /><media:content medium="image" url="https://michaelchadwick.info/posts/2025/event-listener-epiphany.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>