isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

SiriusXM on Squeezebox Boom

This week I've been playing around with an old Squeezebox Boom, and I've been reading through a forum thread where  - within the last few weeks - people have been finding ways of playing SiriusXM channels on it.

Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

Pandacap: Part 6 - Extras

Pandacap: Part 6 - Extras

There are a couple other areas of the Pandacap application that are probably worth drawing attention to.

Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

Pandacap: Part 5 - ActivityPub

Pandacap: Part 5 - ActivityPub

A          screenshot of part of Pandacap's favorites page, showing          gallery-style thumbnails

If I'm thinking about how often I use it, the ActivityPub integration in Pandacap really isn't the most relevant part of the application to me on a regular basis (that would be the inbox, and its integration with DeviantArt and Bluesky). It was, however, my inspiration for building Pandacap in the first place. There were a few reasons I felt this was important:

  • I believe decentralized social media will continue to be useful, regardless of whether it achieves mainstream success - maybe not to replace big social media platforms, but for its own distinct merits. To hopefully facilitate the adoption of such platforms, I wanted to ensure that the ability to view and reply to my work would be available there, and I wanted to know I could follow and reply to their work as well.
  • The ActivityPub server-to-server protocol - though it may be somewhat broad and vague - was within my ability to implement (it only uses HTTP requests and is agnostic to the underlying data model), which gives me a feeling of control over my interaction with the fediverse.
  • Joining an existing server would make me nervous about conforming to unwritten community norms, while using my own Mastodon or Pixelfed instance would increase the cost and saddle me with a tech stack I'm not familiar with. (Pandacap can run on a free web app plan and a database billed only by usage.)
Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)

Pandacap: Part 4 - Inbox

Pandacap: Part 4 - Inbox

Although it's not public-facing, the Inbox is perhaps the most useful part of the Pandacap web app. As a descendant of Artwork Inbox (Pandacap is built on EF Core + Cosmos DB in a very similar manner), the Pandacap inbox pulls in new posts from ActivityPub, Bluesky, DeviantArt, RSS/Atom, and Weasyl, and allows the logged-in user (me) to view and dismiss them, kind of like an email inbox.

Posts from users and feeds you follow are split between four different inboxes:

  • Image posts: DeviantArt and Weasyl art submissions, and any ActivityPub, Bluesky, and RSS/Atom posts that have an image attached. (Unlike the Pandacap gallery, these aren't called "artwork" posts, because Pandacap can't tell whether an image post is "art" or not.)
  • Text posts: DeviantArt journal entries and status updates, and any ActivityPub, Bluesky, and RSS/Atom posts that don't have an image.
  • Shares: If an ActivityPub and Bluesky post is showing up in your feed because it was shared / reposted / boosted by the user you follow, it will be sent here, instead of to the image post or text post sections. Pandacap will group shared posts by the user who shared them, not by the user who originally posted them.
  • Podcasts: RSS/Atom feeds that have an attached audio file will be sent here.

The user experience here is heavily inspired by the Fur Affinity and Weasyl inboxes: posts are shown roughly in chronological order; image posts have thumbnails and text posts only have a title; you have to click through to see the description / body of the post; checkboxes are used to remove posts from your inbox; and a "next page" button is used instead of a dynamic loading of new content.

Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)

Pandacap: Part 3 - Creating Posts

Pandacap: Part 3 - Creating Posts

Ascreenshot of Pandacap's main page, with the user'savatar andname, links to other sites and protocols, a searchbox, 8artwork thumbnails, and 5 status updates (2 with theirownthumbnails)

From a public-facing perspective, Pandacap is essentially just a single person's art gallery (and blog, microblog, and profile, I suppose). One of Pandacap's philosophies is that fundamentally different kinds of content are separated, so there are three types of public posts:

  • Artwork - a single image, with a title and description.
  • Journal entry - essentially a blog post, with a title and text.
  • Status update - text, with an optional attached image.

These are, not coincidentally, three of the four DeviantArt post types. The paradigm of Pandacap - the context in which it assumes you're creating and uploading your posts - is heavily based on art sharing platforms like it, some of which predate the rise of general-purpose microblogging.

Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

Pandacap: Part 2 - Authorization

Pandacap: Part 2 - Authorization

Since Pandacap is a single-user application, I really didn't want to write my own authentication and authorization system. The only goal was to allow myself to log in, and no one else. So instead of using an email/password combo, like the default Identity template, I've limited it to just this:


Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

Pandacap: Part 1

Pandacap

This has been my hobby project for the better part of the past year, and it's something I've been wanting to make a series of blog posts about for a while. Pandacap is my personal Swiss Army knife web app; it hosts my art gallery and microblog and collects incoming posts and notifications across five different sites and protocols.

The code for Pandacap is open-source (AGPL v3). I don't imagine it will be that useful to many people; trying to ask a non-Microsoft-stack developer to make tweaks to it would be like asking me to contribute to, well, anything in Python. (Plus, the code itself is not very robust, and not at all scalable.) But I've made some very deliberate decisions in the UI of this app, with an eye towards my own psychological well-being. The context collapse of traditional social media kept me away from it for years, and this app (and one of its predecessors, Artwork Inbox) is the reason I can follow artists on Bluesky and Mastodon without giving up in frustration. I'm hoping that someday, these design principles could be useful to other people who find themselves in the same situation.

Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

BIOS quad-boot

I recently expanded my triple-boot PC to boot four operating systems off the same drive: DOS, Windows XP, Windows 7, and Debian.

Generally, in the old BIOS world, you'd handle this by having GRUB (controlled and configured by the Linux installation) as the main bootloader, and let it boot Windows and DOS from there. But I like to take a more unconventional method (detailed here) where I have the newest installed version of Windows control the main bootloader, and let Linux boot from DOS.

There were three big changes I made this time.

First, I kept Windows XP and Windows 7 both installed to separate extended partitions. I believe they both install their bootloaders to the DOS partition - I know Windows 7, by default, won't map it to a drive letter because it assumes it's just a system partition (you can change this from Disk Management).

To limit the number of consecutive menus, after installing Windows 7, I removed FreeDOS from the boot.ini used by Windows XP's bootloader (ntldr) and added it to Windows 7's (bootmgr). The bootsect.dos (extracted and created by Windows XP's installer) can be loaded through bootmgr by adding a new entry:

bcdedit /create /d "FreeDOS 1.3" /application bootsector
bcdedit /set {new-guid} device partition=e:
bcdedit /set {new-guid} path \bootsect.dos
bcdedit /displayorder {new-guid} /addlast

Options like /displayorder or /default can be used to customize its spot in the menu.

But most importantly, I still wanted Debian to boot from DOS so it wouldn't touch the MBR (making it easy to delete or replace from within DOS/Windows without breaking anything). But instead of installing grub-legacy in Debian to generate the menu.lst and using GRUB4DOS to boot it (which I'm sure would have worked fine), I wanted to use a method of booting Debian that would rely only on the partition GUID, and not on the drive number.

A snail character between IDE and SATA cables with a thought bubble reading "(hd0,5)" Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

Nice Little Things in Visual Basic .NET

Being (loosely) based on the original Visual Basic, it's not surprising that Visual Basic .NET has features specifically targeted towards Windows Forms development. When I want to make a quick GUI app to run on my PC, I often find it easiest to build the main code in a C# or F# library, and to build a thin frontend layer in VB.NET, for two reasons: the incredibly aggressive (in a good way) auto-formatting that keeps me from being distracted by code style, and the nice set of quality-of-life helpers the language gives you for this exact use case.

Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

Audio/video desync

When converting recorded ATSC 1.0 (over-the-air) broadcasts to video DVD format using devede, I've noticed the audio/video sync ends up a little off.

Re-encoding the audio seems to fix this:

ffmpeg -i input.mts -c:v copy -c:a aac -ac 2 output.mkv

This keeps the video data intact, and just re-encodes the audio (as stereo, not the original surround sound).

These sorts of broadcasts usually have an alternate audio track as well - you could use some -map arguments to pick the track(s) you want, but I didn't bother with that this time around.

After doing this, I can put the resulting file into devede :)

If that doesn't work, here's a script that will convert the closed captions to subtitles and re-encode the video. Sometimes this can be necessary - an over-the-air data stream is sometimes missing some data that automated scripts can run into problems with.

#!/bin/sh
set -e
for i in $*; do
	o="$(basename $i)"
	m="$o.mp4"
	if ! [ -f "$m" ];then
		chmod +r "$i"
		ffmpeg -i "$i" -c:a copy -c:v copy "$o"
		ccextractor "$o" -in=ts -out=srt
		ffmpeg -i "$o" -c:a aac -ac 2 -c:v libx264 -preset veryfast "$m"
		rm "$o"
	fi
done
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

Breaking podcasts into chunks for burning to CD

Sometimes I want to listen to a podcast on a TV, and sometimes the easiest way to do that is by burning it to a CD (either a CD-ROM with MP3 files, or - if it fits - a normal audio CD). Not every CD player has the greatest seeking features, though. I've got one whose fast-forward is more of a normal-speed-forward, and another that actually goes forward when rewinding an MP3 at the slowest level.

I figured one way of working around these issues - which could come into play for podcast episodes that are, like, an hour long - would be to split the podcast into segments of five minutes each. You'd need to make a normal audio CD (which means a limit of 75 or 80 minutes or so), but you could have gapless playback, while also being able to use track selection to go forward and back in chunks.

Yesterday I put together a small Windows application to help with this. It's called Cue Sheet Generator, and it takes in one or more audio files and converts them to either a set of .wav files (to burn with Windows Media Player Legacy or another app with gapless burning support) or a .wav/.cue pair (which ImgBurn and other such apps can handle).

The main program logic is small enough to fit into this post. I wrote it in VB.NET (there's nothing here C# couldn't do, I'm just tired of looking at curly brackets), and I thought it might be helpful to annotate it.

Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

2023 USB-C hub on a 2004 PC

Recently I bought a travel hub for my laptop (up 'til now, I'd been using my work laptop's Thunderbolt dock, but I thought it would be nice to have something I could bring with me.) It has a USB-C plug and a small collection of ports:

  • Left:
    • HDMI out
    • SD and microSD card reader
    • USB-C (power input)
  • Right:
    • USB-C (not Thunderbolt)
    • USB-A (2x)

These sorts of hubs have become widespread with the rise of laptop computers (and tablets/smartphones) with nothing but USB-C ports. USB-C is still seen as something of a new, shiny technology (after all, it didn't even exist ten years ago!) So out of curiosity, and a sense of playfulness, I decided to try plugging the hub into my Dell Dimension 4700, a computer designed almost twenty years ago.

The HDMI port didn't do anything, of course, but everything else worked almost perfectly.

In 2004, it was the early days of PCI Express, and the Dimension 4700 came with two PCI Express slots: a long slot for the graphics card, and a spare one-lane slot. And since PCI Express is, of course, still a thing, it's not hard to get your hands on a PCIe card that gives you a couple of USB 3 slots... and then add a front panel with USB-A and USB-C ports. (After all, USB-C at its core - if you're not using any alternate modes - is still USB, maybe with a bit of extra circuitry for detecting plug rotation.) My PCIe card uses a VL805 chip and provides a header for the front panel to connect to, along with a power input - otherwise, the 4700 doesn't give enough power over PCIe to run the card, the front panel, and a spinning disk hard drive. (I bought this card after learning that - but the old card found a home in a relative's PC, to add a couple extra ports to the back.)

The USB-C connector normally provides pins for a USB 2.0 link and two USB 3.x links, but if DisplayPort Alternate Mode is used, one or both of the USB 3.x links can be repurposed for a DisplayPort connection. This explains why you see a similar port selection on so many of these hubs, and why you can't just plug another HDMI or DisplayPort adapter into the USB-C data port on them. I imagine the only real logic the hub has to handle is power delivery (whether from an external source or from the host device) - everything else can be built from just a hub (using the USB 3 link and the USB 2 link), a card reader (attached over USB), and a DisplayPort to HDMI adapter, all of which are commonplace.

Thunderbolt docks work differently. There's a lot more active circuitry in those - it lets you have a higher bandwidth connection (and the flexibility to daisy-chain another Thunderbolt or USB-C device), but it's really working at a higher / more abstract level in order to make that happen. Not surprising that the Thunderbolt dock does nothing when plugged into the 4700!

isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

Using F# collections in C# records for immutability and structural equality

When working in .NET, it can be awkward to figure out whether two objects are "equal". With reference types, two variables might refer to the same object on the heap, or to two different objects with identical properties. In the picture below, changing Snail C's shirt would also change Snail B's, because they are the same snail, while the otherwise identical Snail A is unchanged.

A points to one drawing, B and C both point to another        identical drawing; A is not equal to B, but B is equal to C

Value types are more like C structs. So giving Snail C a jacket and hat does not affect Snail B. It wouldn't make sense to do a "reference comparison" on a value type; the objects are always stored in different places (setting aside explicit pointers to objects in memory, of course - something C# also allows for).

A      and B point to separate but identical drawings, and are equal; C      points to a different drawing, where the snail has a hat, and is      not equal to B

Immutable types with built-in structural equality checks are a great way to work around this issue, but there's still the question of what to do for collection types - the standard .NET HashSet, List and Array types aren't immutable, and even ImmutableList and ImmutableSet don't consider two lists or two sets with the same items to be "equal". But there's a very easy way to handle this problem: you can use the collection types in the F# standard library (FSharp.Core), even without using F#.

Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

Give me a yield break

Working in .NET, I've been in a situation where I need to implement a function in an interface that's supposed to return an IEnumerable<T>. (In some cases, I'd argue that IReadOnlyList<T> might make more sense - this way, the caller knows it's not going to be a lazily-evaluated sequence, and you can still return a .NET list, F# list, or array - but I digress).

Maybe the most obvious way to do this is by calling a function that explicitly returns an empty enumerable for you:

IEnumerable<string> SampleInterface.getAll() => Enumerable.Empty<string>();

In F#, it would be even shorter, because of the aggressive type resolution:

interface SampleInterface with
    member _.getAll() = Seq.empty

There are other clever ways to do the same thing, though, and it might just depend on what you think is the clearest or most readable - which might just mean keeping it consistent with the code around it. First, you can always expand out the function (because clearer isn't always shorter - I think it really does depend on the context of what's around it):

IEnumerable<string> SampleInterface.getAll() {
    return Enumerable.Empty<string>();
}

But there's also something clever you can do here, if you want to think of your code in a different way - where instead of resulting in "an empty list", it results in "no elements". C# lets you build iterator functions, where your code defines an IEnumerable<T> (and runs every time the resulting object is enumerated). Any function with a yield return or a yield break is treated in this way by the compiler. This means you can implement a function that returns "no elements" just by doing this:

IEnumerable<string> SampleInterface.getAll() {
    yield break;
}

It's a bit different in VB.NET, where iterator functions are denoted explicitly - so the yield break isn't needed:

Public Iterator Function getAll() As IEnumerable(Of String) Implements SampleInterface.getAll

End Function

That is, in a very literal sense, a function that returns no elements!

Funny thing is that there's no real equivalent to an empty iterator function in F# (not that you'd need it); the compiler won't allow a seq { } workflow without any elements in it, and suggests you use Seq.empty or the empty list [] instead.

isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

goombasav

The Game Boy Advance (GBA) provided backwards compatibility with Game Boy and Game Boy Color games by including a second CPU (and a clever voltage switch inside the cartridge slot), but the GBA was easily been powerful enough to emulate the GBC in software. I don't think any commercial releases ever did anything like this, but on the homebrew front, the Goomba family of emulators proved plenty useful for people with unofficial GBA flashcards (as the system can't simply switch into GBC mode in software, or anything like that), and later came in handy on the Micro and the DS.

Goomba runs on the GBA as its host platform, which means anything it needs to save, it needs to save in its own SRAM. It's designed to work with 32 KiB of save memory - within which it has to store the emulated games' SRAM and savestates (Goomba can have more than one Game Boy ROM appended to its own ROM), as well as its configuration settings. Here's a thread that explains how the saving system works. Goomba's save data is stored sequentially (with headers) starting at the beginning of SRAM; each chunk of data is either emulator config, a Game Boy savestate, or Game Boy SRAM (the latter two compressed with LZO). Meanwhile, the area at the end of GBA SRAM (56 KiB - 64 KiB) is reserved for an uncompressed working copy of the current GBC game's SRAM, if any (although I don't think Goomba Color uses this).

The goombasav code I put together is designed to extract and replace the GBC SRAM stored inside the GBA SRAM - it's for use on platforms other than the GBA, so you can transfer the save data to other emulators.

"Why not just use the GameCube and a flashcard?""'Cause I'm gonna need a topic for a blog post about adecade from now"

Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

This whole blog post is also a .NET UTF-32 string library

// UTF-32 is a Unicode encoding that uses exactly four bytes for each
// codepoint. Unlike UTF-8 (used widely on the Web) or UTF-16 (used in Java
// and .NET), each codepoint / character takes up the same number of bytes,
// making it much easier to do string processing (counting, substrings) based
// on character count.

// One common application is applying styling or custom behavior to Twitter
// posts based on the data provided by the API:
// https://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/object-model/entities
Read more... )
isaacschemm: A cartoon of myself as a snail (snail8)
Entry tags:

Silly little hybrid disc: making super-low-quality DVDs with HD video in the free space

I stopped by Free Geek Twin Cities recently and picked up (among other things) a Blu-ray player. It's the first time I've had one, and I was pleasantly surprised that it was perfectly happy to play standard audio and video files right off whatever media you put in. It's incredibly useful with the USB port on the front, but it can also read files off a CD or DVD if you happen to have one (not that uncommon; I've certainly put podcasts on a CD-RW before - a lot of car radios will play them!)

I also have a DVR that records shows from U.S. over-the-air digital TV (ATSC 1.0) to a USB device, and the Blu-ray player will play these raw transport stream files perfectly as well.

This gave me an idea: what if you wanted to permanently save something you recorded off the antenna, in its original quality, onto permanent physical media?

Of course, this is a very specific use case, one that I don't even have a need for - it's definitely another case of me putting the cart before the snail, if you will. But the fun part is solving for a specific situation, and making something really unique and cool in the process. Here were my requirements:

  • The content must be a half-hour TV show from U.S. broadcast television, with commercials removed.
  • It must be placed onto write-once / read-only DVD media.
  • The media must contain a copy of the content, in its original quality, that can be played on a PC (or another device with a USB slot and the appropriate codecs).
  • The media must also contain a copy of the content that can be played on a DVD player, with good quality audio, but the video quality of this copy is not important. We're talking about sitcom reruns here - you might as well be watching them on a potato.
  • Finally, the DVD used must be one of those 8-centimeter ones.
    • Because they're cute.

Obviously, it's pretty unusual to dedicate most of the space of a video DVD to data that isn't actually part of the DVD video content.

Could I get, like, a one-twelfth pounder with five tomatoslices and a whole raw onion?

But the important thing here is: your hard copy has the original media (not re-encoded in any way), in case you want to transfer it to another format; and it can be played on a standard consumer device still found in many households, as long as you're only half paying attention. Because although video data takes up most of the bandwidth of any recording, it's almost always the audio that's conveying the most important information, so that's what you want to focus on.

Read more... )
isaacschemm: Drawing of myself as a snail (snail)
Entry tags:

Artwork Inbox

Ascreenshot of the Artwork Inbox home page

Artwork Inbox is a web app I developed for my own use a few years ago. The idea behind Artwork Inbox is to let the user (a.k.a. me) view a social media feed in a way that separates the presentation of visual art (such as drawings or photographs), from other types of posts (like journal or status updates). Instead of simply sorting all posts by date and time, the app splits the feed into pages of 200 items each, and groups those items - first by type (visual or text) and then by author/artist. The current version is built on ASP.NET Core 7, with EF Core, Identity, and Cosmos DB.

Read more... )
isaacschemm: Drawing of myself as a snail (snail)
Entry tags:

How Looping Audio Converter (hopefully) works

I started working on Looping Audio Converter back in 2015. Looping Audio Converter is designed to handle music files with seamless loops, and maintain those loops when converting from one format to another - usually when extracting music from one video game, with the intention of using it in another (this is why the default output format is the Wii's .brstm format and ADPCM codec). It's always been a little bit of a kludge, built from pieces that were floating around elsewhere: almost all input and output formats the program supports are handled by calling out to either a .NET library or a Windows executable to convert the input to 16-bit PCM, then again to convert that PCM data to the output format.

One important component of Looping Audio Converter is the included FFmpeg binary. FFmpeg is used to encode and decode certain formats (including FLAC, Ogg Vorbis, and AAC), but it's also used for most "effects" (like sample rate conversion and tempo and volume adjustment).

Read more... )
isaacschemm: Drawing of myself as a snail (snail)
Entry tags:

Finding the duration of a video (whether MP4, HLS, or YouTube) with DurationFinder

Let's say you want to embed some videos on your website, and you want to put them in a list so people can click through and watch them.

Aset of three YouTube thumbnails. 1: "Summer CampIsland". 2: "Connecting a Bluetooth tape adapter to aBluetooth adapter for tape players". 3: "Are QuantumLeap and Gilmore Girls CONNECTED?"

The title, description, and thumbnail you give to the video are largely subjective decisions, but the duration - in minutes and seconds - is an objective property of the media itself, which means you should be able to extract it if you know the video's URL. But how exactly do you do that?

Well, the first thing you'll need to do is figure out what exactly you're dealing with: a raw video file (or stream) that plays in the browser's <video> tag, or a link to a page on a site like YouTube or Vimeo that hosts embeddable content. Technologically speaking, it's an entirely different beast. A YouTube page gives you the code for a player, and wraps all of it up with copy protection and a variety of other features specific to the platform. In other words, it's not handing the end user something to play; it's playing it for them. It's kind of like the difference between having a record of a song, and having a band come over with their own instruments to play it on.

SoI've got "Forever Your Girl" on CD, and also PaulaAbdul is in my kitchen. Not sure why.

That doesn't mean they can't serve the same purpose for the end user of your site, though, and in both cases it should be possible to programmatically determine the duration of the media. I've written a .NET library (ISchemm.DurationFinder) that handles this for you for a variety of common video types with just a URL; I'll walk through how it works overall, and how it finds the duration for each type of media that it supports.

Read more... )