Showing posts with label audio. Show all posts
Showing posts with label audio. Show all posts

Monday, November 1, 2021

Setting up Xmonad on a Lenovo ThinkPad X1 Carbon: Power Management and Sound (and Dead Ends)

This is my second post on the topic of setting up Xmonad on a Lenovo ThinkPad X1 Carbon. Previous post is here. This time around, we'll talk power management and screen geometry.

Power Management

Power management is handled by the xfce4-power-manager applet. I launch the applet in the .xsessionrc file, but when I initially launched it, it was invisible. It turns out, there were two issues:

  1. By default, the tray icon doesn't show up. I had to enable it in the xfce-power-manager-settings app, which can be run from the command line.
  2. The default icon in my desktop was black and I had it on a black background. That was easy enough to change out by adjusting the GNOME theme. lxappearance is the tool I used to do this; you can install it with apt-get and it then just runs on the command line. It allows me to tweak various configs, including the theming for icons; "Ubuntu-Mono-Dark" gave me a fine visible icon.


My icon bar, showing two Chrome icons, wifi, power, and sound control
Not fancy, but it works


Once the power manager is in the tray, I can right-click on it and select "Power manager settings..." to configure the manager options. In particular, I make sure "When laptop lid is closed" is set to "Suspend" for both battery and plugged in modes. Note: this didn't work on the first try, and needed a restart to make it work. I can also use systemctl suspend to pause the machine, if needed.

Sound

Sound is notoriously complicated in Linux ecosystems. I tried a couple different options here and didn't find anything I'm completely thrilled with, but I did find some things I hated least. I ended up installing pasystray, which operates at the PulseAudio level. As a result, it's a bit options-heavy; PulseAudio lets you somewhat arbitrarily pipe audio sources to audio sinks, so I have the capability to, for example, send Zoom to my headphones while Chrome goes to my PC speaker (I don't want that, but I have it). My streams are also uhelpfully named "sof-hda-dsp HDMI1/DP1" etc. instead of, I don't know. "Computer speakers" or "Monitor output" or, uh, something sane. But it does work, and I can probably figure out (eventually) how to configure the human-readable device names.

Sound menu, showing way too many options for where to route audio to, none of which are named in a human-readable fashion
"Sound is a solved problem in Linux"


Volume can be increased and decreased (on the default sink) via two-finger scrolling on the speaker icon (on the Carbon X1, Mod-volume-up and Mod-volume-down also work, since I configured them in Xmonad's keybindings themselves).

Final thoughts

I give this part of the configuration three out of five stars. Making the audio layer do anything that isn't space-alien is a real challenge when I step off the beaten path of Canonical's pre-configured tooling. But it is working, and I'm not unhappy with that.

Epilogue: Dead ends

Later update:I should mention some dead-ends I encountered while trying to configure everything. As always, one of the challenges of free software is there are so many options to choose from, and many aren't intended to work together.

Trying to install xmonad-extras from cabal

Directions here led me to try and configure volume control by installing xmonad-extras from cabal, the Haskell package manager. This was a detour; I later discovered the package was available in apt, but was named ligbhc-xmonad-extras-dev. Cabal isn't integrated with the apt packages in several ways (for one, its version of xmonad-extras installs in a different location that the apt install of Xmonad won't find them in; for another, xmonad-extras depends on a pile of C-written libraries that cabal can't install because it's not a universal package manager, it's only intended for Haskell code). Not worth the trouble

fdpowermon

For a brief period, I thought the power manager for xfce4-power-manager was buggy and didn't show an icon. The icon was there, but it was showing as black-on-black because of my icon theme. To work around this, I installed fdpowermon. fdpowermon has a couple of annoying issues: for one, it doesn't have a click-to-configure feature. But perhaps more annoying, the icon disappears when the laptop is plugged in and fully charged. This is a preference by the plugin's author; it can be overridden, but that's the default, to hide the whole thing! More trouble than it's worth; I uninstalled it.

volumeicon-alsa

Before settling on pasystray, I gave volumeicon-alsa a quick tour. This worked okay, but it didn't seem to want to let me switch my audio output devices. Also, I'm still a bit unclear on whether I want to be operating at the PulseAudio or ALSA layer of the audio stack. Rather than debug why internal laptop speakers, HDMI-accessible audio, and headphones weren't all showing up in volumeicon-alsa, I gave up and switched to pasystray... Even though the control it gives is overkill, it gives enough control that I can do what I want.

Thursday, November 5, 2020

Adding music to a NES game

Petris now has a theme song, courtesy of my niece. :)



I asked for some help from my nieces to come up with a small bit of music for the game, since I'm not much of a composer myself. After I described the constraints of the NES to them (two square waves, one triangle, one noise), my oldest niece was able to get it to me as a recording she put together on an iPad; I just had to get it transcribed into a format the NES would accept and a music engine to play it.

Enter FamiStudio.

FamiStudio

FamiStudio is a nifty combination of tool and framework for constructing NES music and sound effects. It consists of a couple pieces:

  • A desktop application for composing music or sounds against the NES hardware
  • Several audio engines in assembly that can be embedded in an existing NES source code
The system supports both the in-hardware features of the chip and a few software-implemented effects (such as arpeggio). Music is broken down into "patterns" and "instruments," where patterns allow for saving memory by repeating the same musical phrase and instruments combine "voicing" rules with notes to create various timbres the sound circuitry can support. I put together a FamiStudio project and dove in.

My niece gave me a fairly simple piano-and-drum piece, so I only needed two instruments for it. For the drum, I used a bassy frequency and tweaked the volume envelope to give a sharp hit with a quick die-off. The volume is quiet for one frame to give a "squishy" feel, which sounds more bass (soft mallet hitting a big membrane, maybe? I don't know much music physics ;) ).


To get the sound a bit more "drummy," I used the arpeggio effect to tweak the noise frequency higher for two frames, then back down to the selected frequency. It puts a bit of hiss on the front, like a metal rattle that dies off; not quite snare drum, but a bit more color than a flat "boomph" sound.



The square wave carries the two-note melody and only has a few tweaks. Duty cycle selects what percent of time the square wave is "on" and can vary between 12.5%, 25%, 50%, or 75% (which is just 25% again; human ear can't tell the difference). 50% gives me a good old-fashioned "pure" square wave with no flavor, which is very piano. For volume, I went with a sharp attack and a long-tail decay; no deep thought on this I just doodled on the waveform until it "felt" good.



Now that the song exists, I can export it as FamiStudio Music Code using the NESASM format, since Petris is written in nbasic and nbasic includes an escape hatch to NESASM. The result is a data file containing a representation of the music, but not the commands to play it.


Halfway there

The nbasic language doesn't include a "file import" operator, so I copied-and-pasted the content of that file between asm and endasm directives. Now, I just needed some code to play it.

FamiStudio Sound Engine

FamiStudio's GitHub repository includes an implementation of the audio engine in several different NES assembly languages. I grabbed the one for NESASM and pasted it directly into Petris's source file. The audio engine is quite featureful, and quite large; everything I'd written for Petris to that point now composes 19% of the soruce file. So I compiled and..... The assembler choked.

So I'm not sure if I'm using an older version of NESASM (version 3.1) or if FamiStudio was built against a newer version, but NESASM itself has a hard limit of about 32 characters worth of label that the FamiStudio Sound Engine completely ignores. I made and documented 14 search-replaces to trim the label lengths down to something acceptable to NESASM, and we were off to the races.

To connect the audio engine to my game, I had to run some procedures exposed by the engine. The engine documents these pretty well, but a quick summary follows. Since we're operating at assembly level and don't have a standard function-call protocol, each function documents what the a, x, and y registers have to be set to for the function to work.

  • famistudio_init sets everyting up. It accepts a selector of 0 for PAL timing and 1 for NTSC timing (a), and a pointer to the label at the top of the music data spit out by FamiStudio (low-byte x, high-byte y; in my case, that's untitled_music_data.
  • famistudio_music_play begins playing a song. It takes the index of the song to play (a); I only have one, so I just had to load a 0 into the accumulator.
  • famistudio_music_stop kills playback and takes no input params
  • famistudio_update is a "meat and potatoes" function that must be called once per animation frame (i.e. once per non-maskable interrupt cycle) to allow the engine to move the music forward. More on this later.
The nbasic language is very good about bridging to assembly, so calling these was no more complex than throwing down a small wrapper subroutine to make juggling variables easy.

It's almost certainly overkill to save all the registers, but no harm done

I added gosubs to these routines to the relevant locations, through a call to the updater into my nmi interrupt logic, and... Started an earthquake.


Here's what happened. I made the mistake of adding the update function into my code here, after drawing is completed:

You feel like you're gonna have a bad time...

Problem is, that means I'm running the audio update loop inside the vertical blanking period (the NMI fires at the start of vertical blank. This is, generally, setting yourself up for failure; if the audio processing takes so long that the vblank period ends, any more manipulation of the PPU before the next NMI fires is going to fight the PPU itself for control of state, as the PPU uses its memory latches to generate a frame of video output. Specifically in my case, vwait_end does some cleanup and sets the scroll register correctly, so firing it after vblank ends causes offset artifacts as video rendering continues with the wrong scroll value.

Fortunately, the solution is real straightforward: don't do that.


Now we're safely setting up audio while the PPU is doing the heavy-lifting of emitting one frame of video, and life goes on.

Lessons Learned

This was a fun exercise to finish, and I'm honestly very surprised at how good the public tooling is for NES audio. The audio engine available under the MIT license was a huge win, and I want to extend a huge thank you to Mathieu Gauthier (a.k.a. BleuBleu) for making that exist. He doesn't appear to have a Patreon, so drop a note on one of his video tutorials and tell him what a cool guy he is.