|Keep It Simple, Stupid|
I have a side hobby that involves resurrecting ancient electronics, and retrofitting them for modern purposes. One such tinkering project was to make an old early 60's oscilloscope and turn it into a device for displaying bitmap images.
To do this, I needed something that would convert a bitmapped image into audio. The resulting audio can then be played back with the Left and Right audio channels hooked up to the Horizontal and Vertical inputs on the oscilloscope.
Given the constraints of the hardware (an always-on single beam of electrons being redirected and shot against the backend of phosphor tube) this poses a bit of a challenge. First of all, youre constrained to 48 KHz of bandwidth, the upper limit of what most audio cards are capable of. Secondly, there's a limit to how quickly the oscilloscope can move the beam around. It's very easy to tell the oscilloscope to draw something too intricate, which results in terrible image flicker. Comprimises must be made to ensure that the image is both legible and as close to devoid of flicker as possible.
Simply put, Squarepusher takes a 1-bit PBM image, and converts it into a 48KHz 2-channel (stereo) .WAV audio file. Why 1-bit? Because oscilloscopes are naturally monochrome display devices, read: no color. Secondly, the brightness of any given spot on the phosphor is a function of how long the beam stays in that location. There simply isn't enough time to modulate the beam at the microsecond level to emulate different greyscale values. You're effectively constrained to a small resolution (256x256) and the equivalent of 1-bit depth... a pixel is either on, or off. PBM happens to be a good format for such purposes, since ASCII PBM is nothing but a file of zeroes and ones. As a side note, to convert an image to 1-bit PBM, you can use pnmtools, GIMP, or Photoshop. The resulting image must be 256x256, and only 256x256.
Why 256x256? Think of it like this. There's an upper limit to the number of points you can continually re-plot on an oscilloscope display before it becomes insanely flickery. There simply isn't enough time. At 256x256 resolution, in a worst case scenario, all 65536 pixels will need to be plotted, but it will usually be less. Even when the pixel count is far less than 65536, you'll still see noticable flicker..So, even at 256x256, that's already kind of above the functional threshold of the hardware. 256x256 just happens to be the best comprimise between image resolution and the draw speed of the beam. In theory, my code can be modded to do much higher resolutions, but you're going to need a wicked fast oscilloscope that might require violating every known law of physics to make it happen. :) I suppose you could change the resolution to 512x128, and it would look just as good, but, I figure most people would prefer having a nice square to work within, versus a rectangle. 256x256 is the best comprimise between image resolution the need to move the beam quickly. You're not likely to ever see a script such as this operate at any larger resolution.
Squarepusher can render an image using any one of three different modes; "Standard", "turbo", and "plow".
In Standard mode, the beam is directed across the display in a fashion similar to how you're reading this text; from left to right, and repositioning to a zero location once the end of a line is reached. This makes for a nice drawing, but some time is wasted in the horizontal sync (the time required to move the beam back to begin painting the next line.)
In Turbo mode, Squarepusher will only bother drawing an outline of the image you provided, skipping any region that contails a solid-filled area of pixels. This helps reduce flicker at the cost of image clarity, but it can also produce some interesting effects.
In Plow mode, Squarepusher will paint the image on the phosphor boustrophedonically. In English, like a farmer with an ox plows a field. The first scanline will be drawn left-to-right, and the following will be drawn right-to-left, in order to minimize the amount of time wasted respositioning the beam for the next pixel.
Each mode takes three values. Frames, Lossyness, and Skip.
Frames: Squarepusher will render multiple frames, which you will probably want to do most of the time. Multiple frames are also a must-have for images that require some persistence-of-vision trickery to look good. More on that in a moment.
Lossyness: Often times, it simply takes too long for the beam to paint every single pixel of an image. The results will look terribly flickery. To get around this, we can specify a "lossyness" value. With this value, the painting algorithm can be instructed to randomly jump ahead, painting only a portion of the pixels on a given scanline. This is fine, provided you are rendering multiple frames, because eventually, the entire scanline will be painted, and thus the entire image. Specifying a lossyness value of "10", will tell the algorithm to randomly jump anywhere from 1 to 10 pixels ahead, checking to see if that pixel needs to be drawn, jumping another 1-10 pixels ahead, seeing if that pixel needs to be drawn, and so on. The net result will be a legible image via persistence-of-vision that seems a little bit glittery or grainy, but complete, and mostly flicker-free. Again, comprimise is the name of the game--We only have 48 KHz to work with, compared to something like NTSC television, which has something in the neighborhood of 4 MHz.
Skip: Additional time can be saved by specifying a skip value. This value dictates the painting algorithm to skip a number of scanlines, sort of like the horizontal equivalent of the lossyness value. For example, specifying a skip value of 5 will tell the painting algorithm to paint a scanline, randomly skip ahead 1-5 scanlines, and continue painting. Used in conjunction with the lossyness value, the best possible combination of image clarity and refresh rate can be obtained.
Squarepusher dumps its output to stdout, so, be sure to redirect it into a file. You'll see the command template, usage, and example usage below.