You are viewing ahefner

GIMP-Python script for dithering pixel art

Here's a Python script for GIMP that takes an indexed color image and replaces every other color with a checkerboard dithering of its neighbors. The intended use was to create grayscale 4 color images with this style of dithering, by first creating a hand-tuned 7 color image, then applying the script. After it runs, you can delete the in-between tones from the palette. It should work for any odd number of colors, n>=3.

Having scripted Gimp in both Python and Scheme, I definitely found Python more pleasant.

This is the kind of silly thing I used to do, to pass the time:




#!/usr/bin/env python

# Pixel art dither script for GIMP
# Author: ahefner@gmail.com

import math
from gimpfu import *
from array import array

def python_8bit_dither(img, srclayer):

    try:
        pdb.gimp_message("Running...")
        pdb.gimp_image_undo_group_start(img)

        layer = srclayer.copy()
        img.add_layer(layer, 0) 
        width = layer.width
        height = layer.height

        rgn = layer.get_pixel_rgn(0, 0, width, height, TRUE, FALSE)
        src_pixels = array("B", rgn[0:width, 0:height])

#        cmbytes, cmdata = pdb.gimp_image_get_colormap(img)
#        colors = cmbytes / 3
#        pdb.gimp_message(str(cmdata))

        i = 0
        ph = 0
        for y in range(height):
            ph = y & 1
            for x in range(width):
                c = src_pixels[i]
                if (c & 1):
                    c = c-1 if (ph == 0) else c+1
                src_pixels[i] = c
                ph = ph ^ 1
                i = i + 1

        rgn[0:width, 0:height] = src_pixels.tostring()

        layer.flush()
#        layer.merge_shadow()
        layer.update(0,0,width,height)
        pdb.gimp_image_undo_group_end(img)
        pdb.gimp_message("Finished!")

    except Exception, err:
        pdb.gimp_message("ERR: " + str(err))
        pdb.gimp_image_undo_group_end(img)

register(
        "python_fu_dither",
        "Dither odd colors in an indexed-color image",
        "Dither odd colors in an indexed-color image",
        "Andy Hefner",
        "Andy Hefner",
        "2010",
        "/Filters/Mine/8-Bit Dither Helper",
        "INDEXED",
        [],
        [],
        python_8bit_dither)

main()

Fun with Lisp: Programming the NES

"Low-level programming is good for the programmer's soul." -John Carmack

When the complexity of modern computing gets fatiguing, I look back to the simpler machines of my childhood. I grew up surrounded by various 8-bit and 16-bit machines (most of them already a few years outdated at the time), but my favorite of the bunch is the Nintendo Entertainment System. It's a small machine, even by '80s standards:

  • 6502 CPU at 1.79 MHz
  • 2 Kilobytes of RAM for the CPU
  • 2 Kilobytes of RAM for the PPU (video)

This tiny RAM was augmented by relatively massive amounts of ROM (and sometimes additional RAM) in each cartridge. Typical software ranged in size from 40KB (Super Mario Bros.) to 384 KB (Super Mario Bros. 3), with a few outliers on each end.

When the mood strikes, I'll variously elect to work on my emulator or tinker with EPROM carts and code. The siren call of 6502 assembly language beckoned, so I dusted off the assembler I'd written in Lisp a few years prior. After a little time spent polishing the code, I wanted to write something cool to show it off; so far, I'd mostly written fairly boring experiments and tests such as the following:

boring!

I can do better than that.

The 6502 is a great processor to write an assembler for, with a compact and regularly-encoded instruction set. Simple hardware and no external constraints (as might be imposed by operating systems, library code, compilers, coworkers, etc.) make an ideal playground to do your own thing. My assembler is minimalistic, but not minimal: 570 lines at the core, split evenly between infrastructure and definitions of the 6502 architecture. It's embedded in CL, rather than operating as a standalone language processor, so typical assembly source code is really a series of lisp function calls, each emitting an instruction. I'll say a little more about the design.

The job of an assembler is simple enough: to translate a description of data and machine instructions to an output file (an executable, object file, or raw binary data) which can be loaded in memory on the target machine. Machine code is just another kind of data, for which mnemonic assembly language is syntactic sugar, and so knowledge of a particular instruction set can be considered a layer of drudgery built on top of a very simple foundation, with three essential tasks:

  1. Accumulate an output vector.

  2. Remember the current assembly position, in terms of the target's address space.

  3. Maintain a mapping of symbolic names to locations so that self-referential structures can be assembled, including forward references.

My assembler bundles these responsibilities into an object I'll call the assembly context, implementing the following protocol:

(defgeneric context-emit (context vector)
  (:documentation "Emit a vector of bytes into the assembly context"))

(defgeneric context-address (context)
  (:documentation "Returns current virtual address of the context"))

(defgeneric (setf context-address) (address context)
  (:documentation "Set the current virtual address of the context"))

(defgeneric context-find-label (context symbol)
  (:documentation "Returns the address of a label, or nil."))

(defgeneric context-set-label (context symbol &optional address)
  (:documentation "Set the address of a label. If not supplied, the current address is used."))

(defgeneric context-emit-instruction (context vector)
  (:documentation "Emit an instruction into the assembly context. This
  is a hint, for contexts which want to handle instructions
  specially (e.g. cycle counting).")
  (:method (context vector) (context-emit context vector)))

(defgeneric link (context)
  (:documentation "Prepare and return final, assembled output."))

Handling forward references is the only tricky bit. Assemblers take various approaches to this. Using delayed evaluation (in the fashion of force / delay) seemed the simplest way to me - if an expression (such as a call or branch target) involves a label that is not yet defined, a promise object is emitted into the output vector. When LINK is called on the context at the end of assembly, promises are forced to evaluate. If they still can't be resolved, the problems are collected and presented as an error.

For brevity, the context is dynamically bound to the variable *context*. Instruction emitters use this implicitly, as do a number of functions which provide a friendlier user interface to the assembler:

;;;; User interface:

(defvar *context* nil "Current assembly context")
(define-symbol-macro *origin* (context-address *context*))

(defun emit (bytes) (context-emit *context* bytes))

(defun db (&rest bytes)
  (dolist (byte bytes) (context-emit *context* (encode-byte byte))))

(defun dw (&rest words)
  (dolist (word words) (context-emit *context* (encode-word word))))

(defun advance-to (offset &optional (fill-byte #xFF))
  (let ((delta (- offset (context-address *context*))))
    (when (< offset 0)
      (error "Cannot advance to ~X, it is less than the current assembly address (~X)"
             offset (context-address *context*)))
    (context-emit *context* (make-array delta :initial-element fill-byte))))

(defun align (alignment &optional (fill-byte #xFF))
  (advance-to (* alignment (ceiling (context-address *context*) alignment)) fill-byte))

(defun label (name &key (offset 0) (context *context*))
  (assert (not (null context)))
  (delay name (offset)
    (+ offset
       (or (context-find-label context name)
           (error 'resolvable-condition
                  :path (format nil "Label ~A is undefined" name))))))

(defun set-label (name &optional (context *context*))
  (context-set-label context name)
  name)

You could define the simplest instruction emitters as follows:

(defun nop () (db #xEA))
(defun rti () (db #x40))
(defun rts () (db #x60))
...

This leaves open the question of how to handle the more complicated instructions which take an operand and support multiple addressing modes. One possibility would be to indicate the addressing mode in the function name, yielding lda.imm, lda.zp, lda.mem, etc. I opted for a different approach, where each addressing mode corresponds to a class, and the instruction encoder can select the opcode according to operand type. Worst case, you could define a generic function per instruction mnemonic, with a method for each addressing mode. This would be easy enough, but there's a better way.

The 6502/65C02/65C816 Instruction Set Decoded describes how the 6502 instruction set can be partitioned into three groups. Within each group, addressing modes are expressed consistently by certain patterns of bits within the opcode. With a handful of supporting definitions, the assembler can exploit this knowledge to minimize the manual labor required. For instance, my assembler defines the first group of instructions as follows, with an extra defmethod to handle the lone exception to the pattern:

;;; Group 1:
;;;        ORA     AND     EOR     ADC     STA     LDA     CMP     SBC
;;; (zp,X)  01      21      41      61      81      A1      C1      E1
;;; zp      05      25      45      65      85      A5      C5      E5
;;; #       09      29      49      69              A9      C9      E9
;;; abs     0D      2D      4D      6D      8D      AD      CD      ED
;;; (zp),Y  11      31      51      71      91      B1      D1      F1
;;; zp,X    15      35      55      75      95      B5      D5      F5
;;; abs,Y   19      39      59      79      99      B9      D9      F9
;;; abs,X   1D      3D      5D      7D      9D      BD      DD      FD

(defun group-1-addr-code (x)
  (typecase x
    (idxi #b000)  ;   (zero page,X)
    (zp   #b001)  ;   zero page
    (imm  #b010)  ;   #immediate
    (mem  #b011)  ;   absolute
    (indi #b100)  ;   (zero page),Y
    (zpx  #b101)  ;   zero page,X
    (aby  #b110)  ;   absolute,Y
    (abx  #b111)  ;   absolute,X
    (t (invalid-operand-error nil x))))

(defun group-1-asm (parameter opcode)
  (join-masks
   (join-masks (ash opcode 5)
               (ash (group-1-addr-code parameter) 2))
   #b01))

(def6502 ORA  group-1-asm #b000)
(def6502 ANDA group-1-asm #b001)
(def6502 EOR  group-1-asm #b010)
(def6502 ADC  group-1-asm #b011)
(def6502 STA  group-1-asm #b100)
(def6502 LDA  group-1-asm #b101)
(def6502 CMP  group-1-asm #b110)
(def6502 SBC  group-1-asm #b111)

(defmethod choose-opcode ((instruction (eql 'sta)) (operand imm))
  ;; One exception: STA with immediate destination makes no sense.
  (invalid-operand-error instruction operand))

With an assembler in Lisp, it's easy to define higher level control structures. Here I define a conditional ASIF and a simple looping macro, AS/UNTIL (the AS prefix is chosen to distinguish them from Lisp control structures, just as the AND and OR instructions are renamed to ANDA and ORA above to avoid collision with the CL operators of the same name):

(defmethod condition-to-branch ((condition symbol))
  (or
   (cdr
    (assoc condition
           '((:positive    . bmi)
             (:negative    . bpl)
             (:carry       . bcc)
             (:no-carry    . bcs)
             (:zero        . bne)
             (:not-zero    . beq)
             (:equal       . bne)
             (:not-equal   . beq)
             (:overflow    . bvc)
             (:no-overflow . bvs))))
   (error "Unknown condition ~A" condition)))

(defun assemble-if (branch-compiler then-compiler &optional else-compiler)
  (let ((else-sym    (gensym "ELSE"))
        (finally-sym (gensym "FINALLY")))
    (funcall branch-compiler (rel else-sym))
    (funcall then-compiler)
    (when else-compiler (jmp (mem (label finally-sym))))
    (set-label else-sym)
    (when else-compiler (funcall else-compiler))
    (set-label finally-sym)))

(defmacro asif (condition &body statements)
  (let ((then statements)
        (else nil)
        (part (position :else statements)))
    (when part
      (setf then (subseq statements 0 part)
            else (subseq statements (1+ part) nil)))
    `(assemble-if
      ',(condition-to-branch condition)
      (lambda () ,@then)
      ,(and else `(lambda () ,@else)))))

(defmacro as/until (condition &body body)
  (let ((sym (gensym)))
    `(with-label ,sym
       ,@body
       (funcall (condition-to-branch ',condition) (rel ',sym)))))

Another way to extend the assembler is by defining new types of contexts. One obvious thing you'd want is a local-context with its own symbol table, for defining nested scopes. Using this, you can define a cute procedure macro, defining its name in the surrounding context but enclosing the body in a local context:

(defmacro procedure (name &body body)
  `(progn
     (set-label ',name)
     (let ((*context* (make-instance 'local-context :parent *context*)))
       ,@body)))

A cooler trick is to define a cycle-counting-context: by specializing a method on context-emit-instruction, we can peek at each assembled instruction and tally up the number of cycles used in a block of straight-line code:

(defclass cycle-counting-context (delegate-code-vector
                                  delegate-symbol-lookup)
  ((cycle-count :initform 0 :accessor cycle-count :initarg :cycle-count)
   (precise-p   :initform t :accessor precise-p   :initarg :precise-p)))

(defmethod context-note-cycles ((context cycle-counting-context) num-cycles)
  (incf (cycle-count context) num-cycles))

(defmethod context-emit-instruction ((context cycle-counting-context) vector)
  (multiple-value-bind (cycles variable) (opcode-cycles (aref vector 0))
    (when variable (setf (precise-p context) nil))
    (if cycles
        (context-note-cycles context cycles)
        (warn "Don't know number of cycles for opcode ~X" (aref vector 0)))
    (call-next-method)))

(defmacro counting-cycles (&body body)
  `(let ((*context* (make-instance 'cycle-counting-context :parent *context*)))
     ,@body
     (cycle-count *context*)))

Coupled with a utility function emit-delay, this enables a macro called timed-section which can time a block of code and pad it out to consume a specific number of cycles, for precise cycle-timed loops. Here's a simple example from my test cart, using timed-section to produce a sawtooth wave through the 7-bit DAC, timing the loop by dividing the clock rate of the system by the target frequency (220 Hz) and the number of steps in the waveform (128):

(subprogram (sawtooth-220 "Sawtooth 220")
  (poke 0 +ppu-cr1+)                ; Disable NMI
  (poke 0 +ppu-cr2+)                ; Disable display
  (poke 0 +papu-control+)           ; Silence audio
  (ldy (imm 0))
  (timed-section ((round (/ +ntsc-clock-rate+ 220 128)) :loop t)
    (sty (mem +dmc-dac+))
    (iny)))

I'll confess that emit-delay and/or counting-cycles aren't 100% accurate, as I discovered when trying to wrap raster effect kernels with them, but they work well enough for calibrating the pitch of various audio hacks - most notably music-demo.lisp, which streams a surprisingly high quality loop of music. I should improve these.

Most of my test programs were using character ROMs from commercial games (appropriately, as my first MMC3 devcart still had the original CHR ROM soldered to the board). Before uploading those to github, I wanted to strip the graphics out. Since virtually all of the tools for editing NES graphics run on Windows, I threw together some basic functions for converting between GIF files (using the SKIPPY library) and 2-bit NES characters. This initiated a sequence of events culminating in a nifty little graphics demo, which you can watch on Youtube.

With my newfound ability to convert graphics and display them on the NES, I searched out a graphic online I could turn into another simple example to put under the assembler's hacks directory. Typing the name of the first artist I could think of into Google, I settled on Mark Ryden's "Fur Girl" - I hope he doesn't mind. I wanted to run this on my real NES using the EPROM cart I made from an old Gyromite board a few weeks ago, which constrained me to 32 KB for program data - more than enough - but only 8 KB graphics.

early screenshot

8 KB is enough for half a screen of unique graphics, but the NES divides the ROM into two 4 KB pages, typically one for background and another for sprites. I elected to fill the height of the screen and tile the image horizontally, alternating the coloring. To use both pages of ROM for background graphics, I have to hit one of the PPU control registers ($2000) at the right time mid-frame to switch pages. One glaring design flaw in the NES is the lack of a dedicated scanline counter or horizontal blank interrupt. Some cartridges rectify this with additional hardware that cleverly monitors the PPU address lines, but the basic NROM board I used had no such hardware. There are other techniques you might try, but the most basic and broadly applicable solution is a delay loop after some reference point (such as the beginning of the vertical blank) that spins until the necessary time has elapsed.

On top of the scrolling background image, I called the 64 sprites of the NES into play. I believe the demoscene refers to these as "sinebobs". Each sprite has a separate X and Y angle, indexing into a sine table to determine its X/Y screen coordinates. These are initialized to form a circle, and different methods of incrementing the angle variables produce different patterns of motion on screen. For instance, the effect of the circle splitting into four pieces and recombining is achieved by the following code, which increments the X angle only for every other group of 16 sprites:

(procedure split-by-4
  (jsr 'framestep)
  (ldx (imm 63))
  (as/until :negative
    (inc (abx table-y))
    (txa)
    (anda (imm 16))
    (lsr)
    (lsr)
    (lsr)
    (clc)
    (adc (abx table-x))
    (sta (abx table-x))
    (dex))
  (rts))

Looking at the girl's hair in the image, I thought it would be cool to try a wavy raster effect. This is done by reprogramming the scroll/address registers during the horizontal blanking period between scanlines, and is subject to the same timing constraints as changing the character ROM bank mid-frame. In fact, the timing for this must be tighter, because the visual glitch if you miss the timing window here is much more visible. I don't manage to get it 100% right, but in practice it looks pretty good. I do have a good excuse: nearly all of the processing each frame occurs before the mid-frame split point and must be written to execute in a constant number of CPU cycles. Even so, there are a number of factors that complicate CPU/PPU sychronization. Reading Consistent frame synchronization on the Nesdev wiki might make your head explode.

Timing all this caused a big problem for doing music: before coding the wavy effect, I'd brought in some music I'd written several years ago using the MML/MCK toolchain, calling the playback routine each frame after the screen split. At the time, there was nothing left to do after the split but wait for the vertical blank interrupt, so it was a perfect place to put the music and anything else that might take a variable number of CPU cycles. After adding the wavy effect, there was zero time left before the vertical blank. I ended up turning the screen off a few scanlines early (necessary, otherwise you'd notice the wave effect stop before reaching the bottom) and using that time to update the sprite positions and run the code controlling the overall sequence of patterns. There was no room there for the music.

There's a neat trick you can do on the NES to eyeball (literally) the amount of time a piece of code takes during the frame, by toggling the color emphasis bits in register $2001 before and after. Before removing the music, I timed the player in this way, illustrating its wildly variable execution time - from virtually none upward to perhaps 20 scanlines per frame. If it had taken only a few scanlines to execute, I'd have been okay turning the display off a little earlier to make time, but reserving enough time to accomodate the longest spikes would've seriously cut into the image.

There were two mid-frame delay loops (before and after the screen split) which could be shortened to provide plenty of time for music, if the player routine executes in a fixed number of machine cycles. It took a couple days of head scratching and feet dragging before I reduced my ambitions to nearly the simplest design imaginable - compile the music all the way down to a sequence of sound register writes. I defined a music 'frame' as sixteen address/value pairs (a reasonable upper bound), and with the assumption that particular frames would recur frequently, merged the duplicates and stored the song as an array of pointers, one per 60 Hz tick, to the sixteen memory writes performed for each frame.

I realize now that I could've written the music using any of the existing tools, easily converting it by modifying my emulator to log all the sound register writes during playback. I'd spent enough time initially considering more complicated player designs that the idea of having to invent my own tools had already stuck. No matter, I'd have probably done it this way regardless.

I built a miniature embedded language for composing the music based on the same ideas I used in my previous post playing with audio synthesis: the ability to repeat and combine audio sequentially and in parallel. This time around, rather than mixing audio samples, the basic building blocks combine lists of sound register writes:

(defun register (address value) (list value address))
(defun nop-write ()  (register #x0D 0)) ; Dummy write to unused register.

(defun pad-list (list padding desired-length)
  (assert (<= (length list) desired-length))
  (append list (loop repeat (- desired-length (length list)) collect padding)))

(defun pad-frame (frame)
  (pad-list frame (nop-write) 16))

(defun para (&rest args)
  (apply #'mapcar #'append
         (mapcar (lambda (x)
                   (pad-list x nil (reduce #'max args :key #'length)))
                 args)))

(defun seq (&rest args)
  (apply #'concatenate 'list args))

(defun repeat (n &rest args)
  (apply #'seq (mapcan #'copy-list (loop repeat n collect args))))

(defun segment (length list)
  (if (< (length list) length)
      (pad-list list nil length)
      (subseq list 0 length)))

(defun measure (&rest args)
  (segment 128 (apply 'para args)))

(defun rst (length) (segment length nil))    ; "Rest"

I then defined functions to control each sound channel. For instance, the NOISE function encodes its arguments as a set of writes to the noise channel registers, upon which I've defined several percussive sounds:

(defun noise (length duration period &key short loop (env t) (vol 15))
  (check-type duration (integer 0 31))
  (check-type vol (integer 0 15))
  (check-type period (integer 0 15))
  (segment length
    (list
     (list
      (register #xC (logior (if loop #x20 0)
                            (if env 0 #x10)
                            vol))
      (register #xE (logior (if short #x80 0)
                            period))
      (register #xF (ash (translate-length duration) 3))))))

(defun kick (length)
  (noise length 8 15 :vol 1))

(defun snare (length &optional (variation 0))
  (noise length 8 (+ 10 variation) :vol 1))

(defun hat (length &optional (variation 0))
  (noise length 4 (+ variation 1) :vol 1))

To make certain it's clear, I'll give an example. Here I evaluate (snare 8), meaning I want a snare sound that consumes 8 frames (or 8/60 = 0.13s) of time:

CL-USER> (write (dollhouse-demo::snare 8) :base 16)
(((1 C) (A E) (48 F)) NIL NIL NIL NIL NIL NIL NIL)

The outermost list is of length 8: each element is a list of memory writes to be performed on a particular frame, in the form (value address). Here, writes occur on the first frame, and the subsequent frames (empty lists) pad the sequence out to the desired length. In this way, rhythms can be built by concatenation. The address is taken relative to $4000, where the NES sound registers begin. Comparing with the list of noise channel registers, the first frame of writes can be interpreted as follows:

  • Store $1 to $400C: Set envelope length.
  • Store $A to $400E: Set shift register period.
  • Store $48 to $400F: Load note length counter and play.

I began building the song up as a series of local function definitions. The seq function combines its inputs sequentially. Here are three half-bar phrases, swagger, stagger, and jagger, which I combine in different orders to form all the drum patterns:

(swagger ()
  (seq
    (kick 16)
    (hat 8)
    (hat 8)
    (snare 16)
    (hat 8)
    (hat 8 4)))

(stagger ()
  (seq
    (hat 8)
    (kick 8)
    (hat 8)
    (hat 8)
    (snare 16)
    (rst 16)))

(jagger ()
  (seq
    (shaker 8 15)
    (shaker 8 4)
    (shaker 8 8)
    (shaker 8 12)
    (shaker 8 15)
    (shaker 8 5)
    (shaker 8 11)
    (shaker 8 14)))

Along with a thump achieved by sweeping the pitch of the triangle channel, specific drum patterns are built up by combining pieces in series and parallel. The measure function combines its arguments in parallel, ensuring the resulting length is one measure (128 frames) exactly. The drum patterns used in the first half of the music are defined as follows:

(four-on-the-floor ()
  (repeat 4 (thump 32 (et -24))))

(intro-beat ()
  (measure
    (four-on-the-floor)
    (seq
      (swagger)
      (swagger))))

(intro-fill-1 ()
  (measure
    (four-on-the-floor)
    (seq (swagger)
         (stagger))))

(intro-fill-2 ()
  (measure
    (four-on-the-floor)
    (seq (jagger)
         (jagger))))

Finally, I've built up to defining the music. Here, the para (parallel) operator combines its two inputs: a four measure drum pattern, constructed in an AAAB pattern from intro-beat and intro-fill-1, and a sequence of four arpeggiated chords, each one measure long. This defines the first four measures of the music:

(para
  (phrase-aaab
    (intro-beat)
    (intro-fill-1))
  (seq
    (measure (fat-arp 128 '(0.00   0  3  7 11) :rate 4 :volume (volramp  8 -1/22)))
    (measure (fat-arp 128 '(0.00   0  2  5  8) :rate 4 :volume (volramp  8 -1/22)))
    (measure (fat-arp 128 '(0.00  -2  7  8 12) :rate 4 :volume (volramp  9 -1/20)))
    (measure (fat-arp 128 '(0.00  -1  2  3  7) :rate 4 :volume (volramp 10 -1/18)))))

My arpeggiator functions burn through a lot of space in the ROM, so I was only able to fit a little over 30 seconds of music (which is just fine with me, because I was getting impatient to finish this). You could compress the music substantially, but I wasn't willing to complicate the playback routine (including doing anything requiring conditionals, for which I didn't feel like balancing the timing of both branches). The short music loop is appropriate to the repetitive visuals. Everything came together quickly, and I'm pleased with the result.

One idea I'm disappointed didn't work out was to gradually shift the tuning of the music downward, so that at the end it could appear to modulate upward to a higher key, but actually return to where it started. I'd need a longer loop of music for this to work well. I found I could only shift the tuning by one semitone over a thirty second loop without it being distracting. I'm particularly disappointed because the implementation was cute. Since the music is constructed and concatenated piece by piece, the individual units have no knowledge of where in the final timeline they will appear. This is a problem, because if I'm shifting the tuning continually, pitch becomes an implicit function of time. Since the pitches get encoded into bitfields written to the hardware, it wasn't reasonable to just do a final pass over the fully assembled music, shifting the pitches down.

I solved this by abusing the assembler's delayed evaluation mechanism, modifying the function translating equal tempered pitch to frequency, returning a promise object rather than an actual number. This promise will only resolve if the *tuning-root* is bound in the dynamic environment. After assembling the music, a final pass walks each frame of music, binding the *tuning-root* and forcing the promises to resolve (with a hack to the delayed evaluation framework to stop it from memoizing results of promises, in case a passage is repeated). This was relatively unobtrusive - since CL doesn't transparently support delayed evaluation, you do have to thread support through code that could operate on a promise value - but that only touched the pitch translation and a few sound register functions. I'd like to revisit this idea. It's the sort of neat trick that's usually too much hassle to bother with when you're using conventional MIDI and DAW software. The remains of the idea still exist in the music-test.lisp sandbox, but I stripped it out of the final demo source code.

Anyway, this stuff is a lot of fun.

Links:

Piddling Plugins

The Shuffletron music player, in various branches, has accumulated some neat features (particularly last.fm scrobbling in Brit Butler's branch) that deserve merging, and ought to be cleanly separated from the core of the program. Leslie Polzer sent me a novel implementation early on which used generic functions for extensibility, adding/removing methods via the MOP as plugins load/unload. Clever as that was (and I'm impressed how little code is required, rereading the patch now), I wasn't comfortable with it, and the lack of a pressing need for a plugin interface let me put it off for a good long while.

Building extensibility around generic functions seemed the right thing to do though, and a slightly different idea, of writing plugins in the style of mixins and calling CHANGE-CLASS to enable them at runtime, stuck in the back of my head until (with some prodding) I was motivated to try it out. It's hardly a new idea (both Gsharp and McCLIM contain implementations of similar ideas, as does the AMOP book, just to name a few examples), and a minimal implementation doesn't take much code at all:

(defvar *configurations* (make-hash-table :test 'equal))

(defun configuration (plugins)
  (or (gethash plugins *configurations*)
      (setf (gethash plugins *configurations*)
            (make-instance 'standard-class
                           :name (format nil "MY-APPLICATION~{/~A~}" plugins)
                           :direct-superclasses (cons (find-class 'my-application)
                                                      (mapcar #'find-class plugins))))))

(defun reconfigure (application plugins &rest initargs)
  (apply #'change-class application (configuration plugins) initargs))

(defun active-plugins (instance)
  (mapcar #'class-name (rest (sb-mop:class-direct-superclasses (class-of instance)))))

(defun enable-plugin (application plugin &rest initargs)
  (apply #'reconfigure application (adjoin plugin (active-plugins application)) initargs))

(defun disable-plugin (application plugin)
  (reconfigure application (remove plugin (active-plugins application))))

(defun make-application (&rest initargs)
  (apply 'make-instance (configuration '()) initargs))

This isn't an ideal implementation, and there's a limit to how good it's going to get when CLOS doesn't fully support anonymous classes. However, a more serious attempt should work on arbitrary classes, provide a place to hang init/shutdown code for plugins, and the ability to list which plugins are enabled within an instance.

Piddling Plugins

Piddling-plugins adds these features using only slightly more code than above, along with some superfluous macro magic for writing defun-style definitions that are extensible by plugins. I've made light use of it in a branch of Shuffletron, confirming to myself that it's a good fit.

The code is tiny and self-explanatory, so I'll just post the examples from the README file for fun.

Examples

Imagine we have written a music player, looking something like this deliberately simplified code:

(defclass music-player () ())

(defun run-music-player ()
  ;; You need to set or bind *application* to your application instance
  ;; if you use defun-extensible. It's a good idea even if you don't.
  (let ((*application* (make-instance 'music-player)))
    (init-audio)
    (init-library *application*)
    (loop (execute-command (read-line)))))

Functions extensible by plugins can be defined using DEFUN-EXTENSIBLE. This is just syntactic sugar for defining a generic function specialized on the application object, with a wrapper that passes in the value of *APPLICATION*.

(defun-extensible execute-command (command)
  ...)

(defun-extensible play-song (song)
  ...)

(defun-extensible song-finished (song)
  ...)

Plugins extend the behavior of the application by defining methods on the extensible functions (or rather the generic functions defined behind the scenes, which are prefixed by "EXTENDING-"):

(defclass scrobbler ()
  ((auth-token :accessor auth-token)))

(defmethod plugin-enabled (app (plugin-name (eql 'scrobbler)) &key &allow-other-keys)
  (setf (auth-token app) (get-auth-token))
  (format t "~&Scrobbler enabled.~%"))

(defmethod plugin-disabled (app (plugin-name (eql 'scrobbler)))
  (format t "~&Scrobbler disabled.~%"))

(defmethod extending-song-finished :after ((plugin scrobbler) song)
  (scrobble song (auth-token plugin)))

(defclass status-bar () ())

(defmethod extending-play-song :after ((plugin status-bar) song)
  (redraw-status-bar))

(defmethod extending-execute-command :after ((plugin status-bar) command-line)
  (declare (ignore command-line))
  (redraw-status-bar))

To enable a plugin:

(enable-plugin *application* NAME [INITARGS...])

To disable a plugin:

(disable-plugin *application* NAME)

To set precisely which plugins are enabled:

(reconfigure *application* LIST-OF-PLUGINS [INITARGS...])

References

If you're interested in Lisp, audio/music hacking, just intonation, or microtonality, then this is the sort of thing you're interested in.

First, we'll need a way to play audio. In the past, I dumped the raw audio out to a file in /tmp and played it by shelling out to SoX. These days I can just play it out of an array in memory using my Mixalot library (of which this code requires the latest version, having only recently added support for playback of floating point vectors).

Audio playback through Mixalot is straightforward:

(defparameter *mixer* (mixalot:create-mixer))

(defgeneric play (this))

(defun normalize (vector)
  (let ((rescale (/ (reduce #'max vector :key #'abs :initial-value 0.0d0))))
    (map-into vector (lambda (x) (* x rescale 0.8d0)) vector)))

(defmethod play ((this vector))
  (mixalot:mixer-add-streamer
   *mixer*
   (mixalot:make-vector-streamer-mono-double-float
    (normalize this))))

Next, synthesis - we'll need some audio to play. I'll define a function of frequency called TONE that produces a buffer of audio:

(defparameter *len* 1 "Note length")

(deftype buffer () '(simple-array double-float 1))
(defun make-buffer (size) (make-array size :element-type 'double-float :adjustable nil :fill-pointer nil))

;;; Generate an audible tone.
(defun tone (freq &key (duration 40000) (len *len*))
  (declare (optimize (speed 3)))
  (loop with nsamples = (round (* duration len))
        with output = (the buffer (make-buffer nsamples))
        with decay-rate = (expt 0.07 1/65000)
        with omega = (float (/ (* freq 2.0d0 pi) 44100.0d0) 0.0d0)
        for amp of-type double-float = 1.0d0 then (* amp decay-rate)
        for phase of-type double-float = 0.0d0 then (+ phase omega)
        for n from 0 below nsamples
        do (setf (aref output n)
                 (* amp
                    ;; A simple FM (PM) oscillator. Tweaking the magic
                    ;; numbers produces a variety of mostly chime-like
                    ;; timbres.
                    (sin (+ phase (* 1.5 (expt amp 2) (sin (* phase 5)))))))
        finally (return output)))

Next I'll define a simple language for constructing musical phrases from the output of this function. There are two fundamental building blocks:

1. Sequencing of events serially in time:

(defun seq (&rest args) (apply #'concatenate 'buffer args))

..of which repetition is a special case:

(defun repeat (n &rest args) (apply #'seq (mapcan #'copy-list (loop repeat n collect args))))

2. Events in parallel:

(defun para (&rest args)
  (reduce (lambda (out in) (declare (type buffer out in)) (map-into out #'+ out in))
          args :initial-value (make-buffer (reduce #'max args :key #'length))))

Originally I wrote a simpler definition for this:

(defun para (&rest args) (apply #'map '(simple-array double-float 1) #'+ args))

This defintion has the disadvantage of truncating the output length to that of the shortest component. Also, using SBCL, it's much slower. (apply #'map ...) is a hairy expression that defied optimization by the compiler, whereas it can inline the map-into operation. Combined with the type declaration, it expands to a nice fast addition loop.

Here's some syntactic sugar to make the pieces fit together more nicely, and print some useful information to the REPL:

(defmacro chord (properties &body body) `(let ,properties (print :chord) (para ,@body)))

(defparameter *tonic* 261.0d0)          ; Middle C.

(defmacro just (numerators denominators &rest args)
  `(progn
     ;; It's useful to see the both reduced fraction and its decimal representation:
     (print (list ',numerators ',denominators
                  (* ,@numerators (/ 1 ,@denominators))
                  (float (* ,@numerators (/ 1 ,@denominators)))))
     (tone (* *tonic* ,@numerators (/ 1 ,@denominators)) ,@args)))

Now, to combine these tools to some musical end. First test: start with a major chord, invert the intervals each way we can, then resolve down to an inversion of the original chord. Note the two different senses of "invert".

(play
 (seq
  (chord ()
   (just (1) (1))                       ; Root
   (just (5) (4))                       ; Major 3rd - 5:4
   (just (3) (2)))                      ; Fifth - 3:2
  (chord ()
   (just (1) (1))
   (just (4) (5))                       ; 5:4 becomes 4:5 - The major 3rd is reflected about the octave.
   (just (3) (2)))
  (chord ()
   (just (1) (1))
   (just (5) (4))
   (just (2) (3)))                      ; This time, the fifth.
  (chord ()
   (just (1) (1))
   (just (4) (5))                       ; - Now both are reflected.
   (just (2) (3)))                      ; -
  (chord ()                             ; Resolve down..
   (just (1) (1))
   (just (5) (4 2))
   (just (3) (2 2)))))

Here's a pair of chords used by Harry Partch (see http://en.wikipedia.org/wiki/Tonality_flux):

(let ((*tonic* 196.0)
      (*len* 3))
 (play
  (repeat 2
   (seq
    (chord ()
     (just (8)  (7))
     (just (10) (7))
     (just (12) (7)))
    (chord ()
     (just (7) (6))
     (just (7) (5))
     (just (7) (4)))))))

See what he did there? The major chord, built starting 8/7 above the tonic, is reflected around the octave. I find it clearer to consider the minor chord first, built upon an interval of 7/6 above the tonic, then interpret the major as as built symmetrically downward from the next octave.

It's clearer after rewriting the fractions without simplification to separate the 8:7 and octave components from the intervals of the chord:

(let ((*tonic* 196.0)
      (*len* 3))
 (play
  (repeat 2
   (seq
    (chord ()
     (just   (8 1) (1 7))               ;  8/7
     (just   (8 5) (4 7))               ; 10/7
     (just   (8 3) (2 7)))              ; 12/7
    (chord ()
     (just (2 7 1) (1 8 1))             ; 7/4, i.e. (2*7*1)/(1*8*1), if it isn't clear.
     (just (2 7 4) (5 8 1))             ; 7/5
     (just (2 7 2) (3 8 1)))))))        ; 7/6

Applying that construction under equal temperament, the middle notes of the chords would be the same. Since this is just-intoned, the frequencies differ by about a third of a semitone, and we find ourselves in the uncanny valley of microtonal voice leading.

I've never developed an ear for microtonal music, so I'll run with the idea of this pivoting around the octave and try something conventional. Take the same two chords, add a minor chord on the tonic (without the 8/7 offset) before, and its reflection around the next octave after. Then tack on a little ending to make a more satisfying musical snippet: move down by a fourth, then back, with a couple added notes for color.

(let ((*tonic* 196.0)
      (*len* 2))
  (play
   (seq
    (chord ()                           ; 1
      (just (1) (1))
      (just (6) (5))
      (just (3) (2)))
    (chord ()                           ; 2
      (just (8  ) (  7))
      (just (8 5) (4 7))
      (just (8 3) (2 7)))
    (chord ()                           ; 3: Built downward from next octave, symmetric with 2.
      (just (2 7 2) (3 8))
      (just (2 7 4) (5 8))
      (just (2 7 1) (1 8)))
    (chord ()                           ; 4: Built downward from next octave, symmetric with 1.
      (just (2  ) (1))
      (just (2 5) (6))
      (just (2 2) (3)))

    ;; Build and release tension.
    (chord ((*len* 3))
      (just (2 3  ) (  4))
      (just (2 3 4) (3 4))
      (just (2 3 3) (2 4)))
    (chord ((*len* 1))
      (just (2 3  )   (    4))
      (just (2 3 4)   (  3 4))
      (just (2 3 3)   (  2 4))
      (just (2 3 4 4) (3 3 4)))         ; 4/3*4/3, let's call it a dominant seventh.
    (chord ((*len* 4))
      (just (1) (1))
      (just (6) (5))
      (just (3) (2))
      (just (2) (1))
      (just (3) (1))))))                ; ..and that's a ninth. The 7th resolved here.

Stacking fourths:

(let ((*tonic* 262.0)
      (*len* 2))
  (play
   (seq
    (chord ()
      (just (1) (1))
      (just (4) (3)))
    (chord ()
      (just (   ) (   ))                ; Did I mention the ones are optional?
      (just (4  ) (  3))
      (just (4 4) (3 3)))
    (chord ()
      (just (     ) (     ))
      (just (4    ) (    3))
      (just (4 4  ) (  3 3))
      (just (4 4 4) (3 3 3)))
    (chord ()
      (just (       ) (       ))
      (just (4      ) (      3))
      (just (4 4    ) (    3 3))
      (just (4 4 4  ) (  3 3 3))
      (just (4 4 4 4) (3 3 3 3)))
    (chord ()
      (just (         ) (         ))
      (just (4        ) (        3))
      (just (4 4      ) (      3 3))
      (just (4 4 4    ) (    3 3 3))
      (just (4 4 4 4  ) (  3 3 3 3))
      (just (4 4 4 4 4) (3 3 3 3 3))))))

Stacking fifths and fourths:

(let ((*tonic* 262.0)
      (*len* 1))
  (play
   (seq
    (chord ()
      (just (   ) (   ))
      (just (3  ) (  2))
      (just (3 4) (3 2)))
    (chord ()
      (just (   ) (   ))
      (just (4  ) (  3))
      (just (4 4) (3 3)))
    (chord ((*len* 2))
      (just (     ) (     ))
      (just (3    ) (    2))
      (just (3 3  ) (  4 2))
      (just (3 3 4) (3 4 2)))
    (chord ()
      (just (     ) (     ))
      (just (4    ) (    3))
      (just (4 4  ) (  3 3))
      (just (4 4 4) (3 3 3)))
    (chord ()
      (just (     ) (     ))
      (just (3    ) (    2))
      (just (3 4  ) (  3 2))
      (just (3 4 4) (3 3 2)))
    (chord ((*len* 4))
      (just (     ) (     ))
      (just (4    ) (    3))
      (just (4 3  ) (  2 3))
      (just (4 3 4) (3 2 3))))))

Various ways to express the a major triad:

(play
 (seq
  (chord ()                             ; Three separate notes.
    (just (1) (1))
    (just (5) (4))
    (just (3) (2)))
  (chord ()
    (just (1    ) (    1))
    (just (1 5  ) (  4 1))              ; A major 3rd..
    (just (1 5 6) (5 4 1)))             ; ..and a minor 3rd on top of that.
  (chord ()
    (just (1    ) (    1))
    (just (1 3  ) (  2 1))              ; Or, you could nest the third inside
    (just (1 3 5) (6 2 1)))))           ; the fifth by a downward interval.

The Tristan Chord (in equal temperament)

(play
 (chord ((*tonic* 349.0)                ; F
         (*len* 3))
   (tone (* *tonic* (print (expt 2.0  0/12))))
   (tone (* *tonic* (print (expt 2.0  6/12))))
   (tone (* *tonic* (print (expt 2.0 10/12))))
   (tone (* *tonic* (print (expt 2.0 15/12))))))

Assume we're in A minor, and the chord is inverted such that the root is B. The tritone between B and F is problematic. It's an unnatural interval in just-intonation, and there are various ways to interpret it relative to the other notes.

(play
 (chord ((*tonic* 440.0)
         (*len* 3))
   (just (9    ) (    8))               ; B
   (just (9 5  ) (  4 8))               ; B * 5/4 = D#
   (just (9 5 4) (3 4 8))               ; D * 4/3 = G#

   ;; Now we need that F. Three ways to get there spring to mind:

   ;; 1. Two intervals of 3/4 downward from D#. Yields intervals of
   ;; 45/64, 9/16, and 27/64 versus the other notes in the chord, and
   ;; an ungainly 405/512 versus the tonic. The ratios are ugly, but
   ;; the sound is quite close.
   ;; (just (9 5 9) (16 4 8))

   ;; 2. An intervals of 5/9 downward from D# yields intervals of
   ;; 25/36, 5/9, and 5/12 versus the rest of the chord, and 25/32
   ;; against the tonic. The pitch ratios within the chord are mostly
   ;; nice and simple, but the F sounds oddly flat.
   ;; (just (9 5 5) (9 4 8))

   ;; 3. An interval of 7/10 downward from the root, yielding
   ;; intervals of 7/10, 14/25, and 21/50 versus the rest of the
   ;; chord, and 63/80 against the tonic. Constructing the F relative
   ;; to the root of the chord seems preferable, even if it introduces
   ;; a new factor of 7 into the ratios, which make the intervals
   ;; against D# and G# odd. Overall, I prefer the sound of this
   ;; one. The F is slightly flat compared to the equal-tempered
   ;; chord, but not unpleasantly so.
   (just (9 7) (10 8))))

When you uncomment more than one of the above versions of 'F', they're slightly detuned and beat against each other. This could be a cool compositional device to highlight shifts in the tonality. I'll try it with the two Partch chords:

(let ((*tonic* 196.0)
      (*len* 3))
 (play
  (repeat 2
    (seq
     (chord ()
       (just   (8 1) (1 7))
       (just   (8 5) (4 7))
       (just   (8 3) (2 7)))
     (chord ()
       (just   (8 1) (1 7))
       (just   (8 5) (4 7))
       (just (2 7 4) (5 8 1))           ; Presages the following chord..
       (just   (8 3) (2 7)))
     (chord ()
       (just (2 7 1) (1 8 1))
       (just (2 7 4) (5 8 1))
       (just (2 7 2) (3 8 1)))
     (chord ()
       (just (2 7 1) (1 8 1))
       (just (2 7 4) (5 8 1))
       (just   (8 5) (4 7))             ; And again..
       (just (2 7 2) (3 8 1)))))))

That suggests a more subtle trick. Rather than playing both tones to mark the shift, replace one with the similar tone from the next chord.

(let ((*tonic* 196.0)
      (*len* 3))
 (play
  (repeat 2
    (seq
     (chord ()
       (just   (8 1) (1 7))
       (just   (8 5) (4 7))
       (just   (8 3) (2 7)))
     (chord ()
       (just   (8 1) (1 7))
       (just (2 7 4) (5 8 1))           ; Replaced (8 5) (4 7) to lead into the next chord.
       (just   (8 3) (2 7)))
     (chord ()
       (just (2 7 1) (1 8 1))
       (just (2 7 4) (5 8 1))
       (just (2 7 2) (3 8 1)))
     (chord ()
       (just (2 7 1) (1 8 1))
       (just   (8 5) (4 7))             ; Likewise, replaced (2 7 4) (5 8 1)
       (just (2 7 2) (3 8 1)))))))

That's all for now. I've published the code on Github here.

Incidentally, I've concluded my world tour (my Thailand visa having expired, and an additional season of leisure and international intrigue being financially unwise), so if you're looking for a versatile young Lisp/C/C++ hacker with a background in computer/network security and an equal penchant for spiffy user interfaces and gritty low-level trawling around in debuggers and disassemblers, you could do worse than to get in touch.

Lispm archaeology: Compiler Protocols

Skimming through the Genera source just now, I enjoyed this description of the compiler structure (from sys2/compiler_protocol.lisp), which precedes a very modern-feeling definition of the interface in terms of classes (or rather Flavors) and generic functions. I wonder who wrote it.

;;; This file defines the base flavors for compiler objects and their protocols.
;;;
;;; The function of a compiler object is fairly simple: it must translate some
;;; program source into an appropriate object representation, and put that representation
;;; somewhere.
;;;
;;; A compiler object is made up of several components, which may be inter-related:
;;; - The Language Parser, which differentiates between various dialects of Lisp
;;; - The Target Architecture on which the object representation should run
;;; - The Compilation Environment
;;; - The Intermediate-Representation Optimizer
;;; - The Compilation Target, which actually disposes of the object representation
;;; (e.g. load it into virtual memory, or store it in a file)
;;;
;;; A Language Parser is responsible for all of the steps required to translate
;;; the source into an intermediate representation, including all source-to-source
;;; and source-to-pseudo-source transformations. [By pseudo-source we mean constructs
;;; which are similar in syntax to those of the source language, but which may not be
;;; understood correctly by an interpreter of the source language - "compiler-only forms",
;;; to put it another way.] The Language Parser may make use of the Target Architecture,
;;; and will certainly make use of the Compilation Environment.
;;;
;;; The task of the Target Architecture is to translate the program from its intermediate
;;; representation into an object representation that corresponds to the Instruction-Set
;;; Architecture of the machine on which the target is to be run. [Note that a "machine"
;;; can be a software emulation as well as a physical computer.] As previously stated, the
;;; Target Architecture may also be used by the Language Parser in the source-to-intermediate
;;; translation step. The Target Architecture may also interact with the
;;; Intermediate-Representation Optimizer, as described below.
;;;
;;; The Compilation Environment contains an evaluator and definitions that are needed at
;;; compile-time. Its main function is to support macroexpansion and compile-time
;;; error-checking.
;;;
;;; The Intermediate-Representation Optimizer does just what its name implies. It bases its
;;; decisions on advise from the user (e.g. the CL OPTIMIZE declaration). It may interact
;;; with the Target Architecture when the optimization is more easily implemented or disabled
;;; at this level, rather than during the intermediate-to-object translation.
;;;
;;; The Compilation Target's reponsibility is to put the object representation somewhere.
;;; This could either be the local Lisp virtual memory, a remote Lisp, either running on
;;; another piece of hardware or in a software emulation, or a file of some kind.

Emacs: Rebinding the numeric keypad.

Lately I'm trying out various emacs hacks (the awesome cycle-buffer.el, and today highlight-symbol). I'm running low on free Function keys and wanted to start binding things on the numeric keypad, which I rarely use. I was initially foiled by built-in translations which map the numeric keypad keys to their non-keypad equivalents (arrows, home, end, page up/down, etc.). A few minutes of googling didn't turn up a ready made formula for disabling this, so I kludged something up for my .emacs:
;; Disable keypad key translations, so I can rebind them.
;; (There Has To Be A Better Way..)
(setq function-key-map
       (cons
        (first function-key-map)
        (remove-if
         (lambda (pair)
           (member (first pair) '(kp-home kp-up kp-prior kp-left
                   kp-begin kp-right kp-end kp-down kp-next
                   kp-insert kp-delete kp-divide kp-multiply
                   kp-subtract kp-add kp-enter)))
                   (rest function-key-map))))

Linux to Mac transition notes


Random notes taken during my transition to a MacBook Pro, away from a Thinkpad running Debian Linux (and occasionally Windows XP) as my former primary computer. I don't really have anything better to do, since I wanted to play Portal, but Steam will probably spend the next half hour downloading updates (now I remember why I stopped playing Valve's games back in 2004), so I'll transfer my notes into this blog.



Essentials
  • Emacs: emacsformacosx.com is a reasonable build. Repaint is annoyingly slow, though, and it shows when you're holding down C-s to search through a document - on OS X, and with my keyboard repeat rate turned up a bit, it doesn't seem to repaint quickly enough to keep up and so stops redrawing entirely until I release the key, whereas Emacs under Linux/X11 (on a machine half the speed, even) had no such problem. It supports standard Mac keys via the Command modifier, which is a nice touch.
  • KeyRemap4Macbook provides numerous options for remapping the keyboard. I use it to remap the right Command and Alt keys to Alt and Control, otherwise Emacs would be painful to use. I've confined this remapping to Emacs, X11, and Terminal.
  • MacPorts is handy to get software on the machine. It strikes me as a silly approach. If it's going to build everything from source, it should keep source code around and make it easy for me to modify it. No, its approach of letting me dump patch files in some directory for it to apply as it rebuilds the software doesn't count as easy. As it is, MacPorts combines the restrictiveness of a Debian-style package system with the inconvenience of building everything from source. I'm tempted to suggest truly Debian-style binary packages would be a better solution, but on the other hand, the Mac OS is something of a mess now that there's three (perhaps four, if anyone takes 64-bit PPC seriously) different machine architectures kicking around. I still have no idea how to control which architecture MacPorts targets. Now I've got a bunch of useful libraries built, but I'm just going to have to recompile them all myself to get 32-bit versions the first time I try distributing any software. Meh.
The good
  • Dashboard remembers where the keyboard focus was when close and re-enter it. This is a nice touch.
  • I'm impressed how Spaces and Expose can work together, and more impressed that via screen-corner shortcuts, you can use them even while dragging a file to copy it. (Unfortunately, that I know this at all is a consequence of moving files via the Finder often being a huge hassle involving fussing with multiple windows.)
  • Dictionary.app is cool (and the dashboard widget version is handy too).
The bad
  • File open/save dialogs are infuriating. There's always too small. Almost every time I open one I have to move the resize it and move it around. Opening/saving documents is a modal activity. You might as well make the damn thing full screen.
  • Too much friction on the trackpad. I think whoever designed or signed off on this must have very dry skin. The slightest bit of natural oil on your fingertip will create enough friction to bounce and skip against the surface.
  • Wireless reception on this machine is appallingly bad. Also, airport signal strength indicator is a big fat liar. I'll see two or three bars before I connect to my access point. Once I'm connected, it reads four bars all time, even when I'm experiencing 100% packet loss. Only under the very worst conditions will it drop to 3 of 4 bars. At all times I keep a terminal pinging my access point on the screen. Periodically, for no discernible reason, pings spike from a reasonable couple milliseconds to hundreds, thousands, or tens of thousands of milliseconds. Sometimes tilting the Macbook slightly by propping the front or back up fixes it. Sometimes moving it one inch to the left fixes it. Sometimes touching my finger to the black plastic hinge-thing magically makes the packets flow. Other times, it simply wants to be held.
  • Screen DPI on the 15" MacBook Pro is comically low. In addition to dry skin, the designers must have failing eyesight as well. You could remedy this by upgrading to the higher resolution screen, but good luck finding that configuration in a retail store (which, unfortunately, was a necessity for me). How do build a laptop this large and expensive yet put a comically small 1440x900 display in it?
  • Subpixel AA looks ridiculous when the pixels are so visible. Text everywhere is surrounded by colorful fringes.
  • Glossy screens are fucking retarded. I'll be trying to watch a video and find myself continually distracted by reflections of myself and the backlit keyboard. They offer an anti-glare screen, but again, good luck finding that retail.
  • Scrolling with the mouse wheel feels wrong (that is, different than Windows or Linux, and their way is better). Turing the wheel one click only scrolls the screen by three or four pixels. Mac OS applies acceleration to click wheel scrolling, but this makes it hard to guess where scrolling is going to stop, and ruins the feel of direct manipulation.
  • Terminal malfunctions when you have ANSI colors in your bash prompt. Moving through the readline history gets garbled. Very bad. I need to find a terminal that doesn't suck. I'll cry a little if I end up using xterm inside X11.
  • OS X isn't the most stable desktop I've ever used. My system has locked up hard several times. Compare with WinXP, which is (somewhat to my astonishment) probably the most stable desktop system I've ever used. Likewise my Linux machines will trundle along for months and months until a power outage or battery incident knocks them out, even if X11 comes crashing down like a house of cards and has to be restarted every couple weeks.
  • Fullscreen applications are quirky. They're quirky on Windows, they're quirky on Linux, but I'm disappointed that OS X doesn't manage to do it right either. I'm sick to death of fullscreen apps leaving my screen in the wrong color depth and resolution, half the time with the desktop having no idea anything is amiss. At least nothing yet has crashed and left the mouse grabbed, as routinely happens under Linux whenever I'm brave enough to risk toggling DOSBOX into fullscreen mode.
  • One time, upon rebooting after an OS crash, I found I could no longer create files in my home directory. Fixing required removing some "uappnd" flag from the directory via command line. I'll wildly extrapolate from this and predict that OS X will eventually crumble under the weight of all the bizarre extensions Apple grafts on to its lackluster Unix foundations.
The Dock
  • Is very pretty.
  • Takes up too much space on the screen.
  • Is marginally more effective at its job than the Windows start bar, except that it also takes up three times as much precious vertical space.
  • You can auto-hide it, but the delay to unhide it is too long. So long (half a second or so) that I always use the Expose gesture to reveal it, rather than waiting for it to appear.
  • Apple needs to rethink workflows and how application starting/switching should work. The dock is not the answer.
  • As an exception to setting the dock to auto-hide, it ought to show itself when you're in an empty space. It isn't like there's much else you might want to do in that situation except to start an app from the Dock, and it can't get in the way when there's nothing else on the screen.
Safari
  • URL completion, history navigation, and the "Top Sites" view are done better than the equivalents in any browser I've tried.
  • Bookmark interface is the worst of any browser I've ever used (I'll claim that the last browser I've known to have a decent bookmark interface was Netscape Navigator, and it's been steadily downhill from there)
  • Likes to open links in new windows instead of new tabs. No one wants this. Reaching for the Command key almost every time I open a link gets old.
  • Unwieldy once you have more tabs than fit horizontally. Sane browsers scroll the tab list so your current tab is always visible. Safari doesn't, so you have no idea where navigating to the next or previous tab might take you.
  • Safari never remembers my passwords for Google or Twitter. Because it hates me.
Multi-touch
  • My favorite thing about the Mac.
  • Two finger scrolling is great.
  • Three finger gesture to go forward and back are cool when they work, but they aren't implemented consistently across the OS. In fact, they don't work in most places where you might expect them to.
  • The four finger left/right swipes to open the application switcher aren't thought through very well, and could be put to much better use. My experience is that after the app switcher appears on the screen you still have to point and click at the one you want, because the gestures don't navigate it effectively. Why not just use the dock, then?
  • Decision not to enable tap-to-click by default is questionable. I left it off for a week or two before turning it on. If the whole pad were really usable as a button I'd live without it, but the upper third of my trackpad doesn't press down at all. 
  • Double-tap to drag is annoying and best left off (the default). It introduces a noticeable (and annoying) delay when tapping to do single clicks where presumably the OS is waiting to see if I'm going to tap again to start a drag. The killer problem isn't this, but rather than once you drag something and release your finger, there's a too-long period afterward where touching the pad again will continue the drag. This is extremely annoying. Somehow Windows and Linux manage to implement this without any such problems. Mac OS X is full of these annoying little delays, actually (think of the delay in Xcode between when you click a file and when it opens in the editor, in case it turns out you wanted to double click to open it in a new window, nevermind that no one sane would ever want to do that).
  • Why can't I open Spaces with a multi-touch gesture? That would be a lot more useful than the one to reveal the desktop.
  • The pinch to zoom gesture is cool in applications that can smoothly zoom in and out, but ABSOLUTELY SHOULD NOT be used for discrete switching of things like text size in Safari. This is a nuisance, as there's no feedback during the gesture, just a sudden transition. It would maybe be okay if some kind of overlay appeared indicated how far you'd "pinched" versus where the next step begins. Fundamentally, I don't believe you can quantize a continuous control without providing some feedback to the user, because it's too easy to end up off by one if you nudge the control just slightly while releasing it. If you could see where you were, you'd aim for the center of the region. Apple makes this mistake repeatedly - the iPod click wheel and scrolling in Cover Flow views are two examples.
Spaces
  • Exquisitely polished implementation of brain-damaged semantics.
  • The spaces view (Activate Spaces, whatever you call it) where you can see and drag windows around is very cool, particularly if you assign a screen corner to make it appear.
  • When leaving and re-entering a space, THE TOPMOST APPLICATION SHOULD NEVER CHANGE. WTF.
  • When leaving and re-entering a space, THE ORDER OF APPLICATIONS IN THE COMMAND-TAB LIST SHOULD NOT CHANGE. WTF!
  • The whole point of a virtual desktop is to group together the windows you're using for a specific activity. For me, this usually means an editor and either a terminal or a web browser, which I will Command-tab between constantly. If I switch away briefly, I don't want this arrangement disrupted when I return.
  • The animated scrolling (with acceleration/deceleration) when switching between spaces is a nice touch, except...
  • Switching between non-adjacent spaces from the keyboard sucks. It lurches from space to space, stopping at each one along the way, instead of smoothly scrolling from the destination.
  • Sometimes I get the impression that the MacOS is optimized for two scenarios: 1) drooling over how pretty the hardware and software are, and 2) browsing apple.com to buy your next Apple product. 
Xcode
  • Awkward. Wants to open dozens of windows, but OS X's lousy window management makes that untenable.
  • I haven't figured out a good way to navigate warnings and errors from the keyboard yet. They keys I've tried so far only navigate between errors in one file.
  • Maybe I can customize Xcode to be less irritating, but it seems like they missed the chance to have sane defaults.
  • Coding workflow: 1) Edit code. 2) Compile. If errors present, go to 1. 3) Run program. 4) Go to 1. Iterations of steps 1 and 2 should not involve opening new windows, reaching for the mouse, or any other fancy GUI tomfoolery. My hands should be on the keyboard at all times, and it shouldn't take extensive customization or obscure keyboard shortcuts to make this happen. Xcode doesn't seem to grasp this. 
One thing I wanted to do was make a virtual machine out of my old Linux and (ideally) Windows installs, so I spent some time playing with VirtualBox and the demos of VMware Fusion and Parallels. Here are some observations:
VMware Fusion
  • Doesn't go out of its way to let you boot a VM from an external disk. I managed it by booting from this CD image and then asking it to boot from USB.
  • My Linux system worked immediately - sound, networking, video. Nice.
  • Unacceptably high audio latency (an extra 250ms or worse).
  • Resizing the window seems to scale instead of adjusting the desktop resolution (as I'm accustomed to when I've used VMware on Windows and Linux), even with tools installed. Weird. I can't imagine anyone wanting this behavior, except in the case of the guest not fitting on the host's screen. 
VirtualBox
  • Also doesn't make any attempt to accommodate the seemingly obvious desire to boot from an external disk. Again, using the PLoP bootmanager worked. Sort of...
  • My USB disk was always grayed out in the devices menu, even after unmounting it from the host OS, so I could not connect it. People on the web suggested some complicated device filter nonsense to make it work. I found a simpler workaround - attach the disk to a VM in Parallels, then quit Parallels. As if by magic, the menu item in VirtualBox becomes available. Failing to quit Parallels first may or may not work - I seem to recall Parallels popping up continually, wanting to steal the device back.
  • IO from my external disk was excruciatingly slow. It took over an hour to copy my 6 GB root filesystem from the external disk to a new virtual disk. When I did the same thing in Fusion, it did not take nearly as long.
  • I don't like its design of wanting to steal a modifier key for itself. The VM software isn't so important as to deserve a whole modifier key to itself - all I need is precisely one key combination to ungrab the keyboard/mouse and let me out of the VM. We emacs users are extremely protective of modifier keys. I hate this enough that I'd consider hacking the source code to change it, if I were to attempt any serious work inside VirtualBox.
  • My Linux environment came up straight away with networking and X11 using the VESA driver, with no effort on my part. This is good.
  • Deleting a snapshot takes forever.
  • Audio sample rate was all screwed up. Music played back at a very wrong pitch and tempo. This is apparently to do with some sheer insanity the Linux driver for whatever audio chip VirtualBox emulates, where attempts to measure the audio clock by timing it. I found a solution here: http://www.virtualbox.org/ticket/4605
  • I'm disappointed that running VMware Player inside my Linux VM didn't work (It didn't work with VMware Fusion either). You'd think that in this age of hardware virtualization extensions, they could make this work.
  • The big draw of VirtualBox versus the alternatives was the promise of accelerated OpenGL in my Linux guest. I couldn't make it work. I don't know why, it just never seemed to be accelerated. There isn't much to provide feedback except running a few OpenGL apps and guessing whether it works based on them being conspicuously non-slow or not. There wasn't much documentation on how to make this work. I tried with Debian 5.0. Perhaps it would work with a different distribution of Linux - maybe one of the tourist distros like Ubuntu, where people want their Compiz or whatever other newfangled unstable bullshit they're pushing now, so they can marvel at the spinning cube and wobbly windows for fifteen minutes before they boot back into Windows.
Parallels
  • Again, no direct support for booting from an external disk. W T F.
  • Unlike Fusion and VirtualBox, the PLoP boot manager wasn't able to manage booting from USB in Parallels.
  • Has a cute tool for migrating a Windows install into a VM, but was completely useless to me, as it expects a running Windows machine you can run some software on. All that remains of my Windows machine is its hard disk. I think VMware Fusion has something similarly useless. WTF.
  • 12 day demo period wasn't long enough to try it out, particularly as there was no way to boot my existing Windows or Linux systems. It expired before I got around to really trying to exercise the program.

 

New MacBook Pro: Wireless Sucks

I just bought a shiny new 15" MacBook Pro. I'm not a Mac person; I dabbled in the platform when the original Mac Mini came out, but found the hardware underpowered and the software awkward and unlikely to displace Linux as my preferred desktop OS. I still think OS X is clumsy and inefficient to use, but so well executed that I find some joy in that inefficiency every time I flick the multitouch gesture for Expose and watch the pretty animation. I'm not convinced I can adapt to this keyboard, at least without some remapping, as one day of very light hacking has already induced a case of emacs pinky due to the lack of a Control key on the right side of the keyboard, a sensation I had not experienced in years since adjusting to rely on the right control modifier when hacking with desktop keyboards and Thinkpads. On the other hand, I continually resolve to give up emacs "sometime soon" due its terrible ergonomics, so I'll look at this more as an opportunity to finally do something about that.

I like the machine, or really want to. It's gorgeous, it feels very solid (contrast with a brand new Thinkpad T410 I tried at a store the other day, which audibly creaked when I rested the weight of my hand on it to type), and is plenty powerful. Part of me wants to complain that glossy displays are brain-damaged and 16:9 aspect ratios are the work of the devil, but that seems like a lost cause at this point. It's a bit frustrating that this 15" machine, which feels ridiculously huge compared to the 14.1" laptop it replaced, actually has less pixels in the display, and even fewer usable pixels when working due to the widescreen format (versus my Thinkpad's old 1400x1050 4:3 display). I fail to see what's "Pro" about a machine with a glossy widescreen display and a keyboard that omits basics like "Delete" (forward delete, that is), "Page Up" and "Page Down", but I shouldn't complain. I won't go in depth into my disdain for PC laptops. I'm not sure anyone makes one worth buying anymore. Older Thinkpads, up through the T60-series, are great. The newer models rub me the wrong way. Switching to the Mac seemed like the rational choice for a previously diehard Linux user with no patience for playing russian roulette with hardware purchases. It's nice knowing things like wireless, 3D acceleration, and suspend/resume will Just Work.

There's lots of things to love about his machine:
  • Multitouch is awesome
  • Gorgeous screen with great viewing angle
  • Speakers sound good
  • Fast CPU,  great graphics, plenty of RAM
  • Doesn't run Windows
Unfortunately, I have one minor gripe, and one huge show-stopper:
  • (minor gripe) Too much friction on the touch pad. Swiping a finger vertically upward causes my finger to grind and skip against the surface. This is excusable in cheap laptops where they want to make the touchpad surface out of the same material as the surrounding enclosure and can't be bothered to press some texture into the material, but on a laptop in this price range there's no excuse for screwing it up. Imagine Mercedes Benz making a car where the steering wheel was uncomfortable to hold.
  • (major WTF) The wireless reception is terrible. Horribly, laptop-returningly terrible. I'm less than twenty feet from the access point in the hallway, with one wall between me and it, and the reception is spotty. The MacBook Pro will drop in and out while the piece of crap Lenovo netbook sitting next to it, at 1/6 the cost, happily continues browsing with no interruption. The whole time, The Macbook will claim four bars of reception, even as it reports that I'm "not connected to the internet". When a website takes suspiciously long to display, I've observed that moving the laptop a few feet to the left or turning it 90 degrees will cause it to instantly kick in and begin loading. I've had to rearrange my furniture just to access the web with this machine, because moving ten feet further into the room where I used to sit, the wireless becomes completely unuseable, struggling to load anything from the web even as it reports nominal signal levels, while the crappy netbook and my old Thinkpad worked perfectly fine.
The wireless reception on this machine is so appallingly poor that I don't understand how they could release it as a product. I'm operating on what I think is a reasonable assumption that this is typical of these machines (rather than mine being defective), which is even more distressing, because in any sane world these machines would've uniformly condemned by reviewers on account of the wireless being unusable in most situations that don't involve sitting in the same room as the access point. I can't imagine this machine being useful in hotels, universities, or any other place where you aren't in control of the AP location. For a laptop, this is fundamentally broken and inexcusable.
The wireless issue is bad enough that I'm tempted to return the machine, and the only reason I don't consider doing so more seriously is that I can't imagine what I'd buy to replace it. A vanilla MacBook? An overpriced plastic Thinkpad? A dowdy Dell with a useless Fn key strategically placed to sabotage Emacs users? Some glimmering HP full of stupid gimmicks that will probably break in six months? The mind boggles. I'm fond of abusing the term "lemon market" in reference to computers (laptops in particular), but I had held out faith that paying the premium to enter the Apple club was a safe way out. Apparently not. Maybe Mac owners really are brainwashed.

While I'm ranting, why the hell does Backspace (sorry, "Delete") in Safari unpredictably navigate me backward, away from this page, instead of performing its expected text editing function? Very frustrating.

Tags:

McPixel, a frivolous lisp hack.

Yesterday, I had the idea that it would be easy and entertaining to hack together a toy application for drawing and animating pixel art. Some hours later, McPixel was born. It provides an editable grid of pixels, a color palette, and allows you to string sequences of images together into animations. It's a toy, but someone might find it amusing. Obligatory screenshot:

McPixel

Features:
  • 80's retro McCLIM user interface.
  • No documentation or online help.
  • Maximally frustrating color sliders, using IHS colorspace.
  • Intuitive modeless drawing UI, achieved by including only a pencil tool.
  • Saves/loads files only in its own format, based on unportable s-expressions.
  • Requires X11 and SBCL with threads, to keep out the riffraff.
  • User-editable brush shape.
  • Remap Color and Silhouette commands, for tracing previous frames.
  • Realtime animated preview plays while you work.
I'm not sure I'll ever put this to productive use, if a productive use for such a thing even exists, but there you have it.

Update: I've added the ability to export an animated GIF file, using Xach Beane's Skippy library. Here's the "turtle.anim" example file, exported as a GIF:
McTurtle

Tags:

Romhack idea

Super Mario Bros. is a classic game. It has held up amazingly well over the years, and is still as fun today as it was in the 80s. Unfortunately, I've played the first few levels of the game so many times that I wouldn't mind if I never see them again. The serial nature of the game made sense in an 80s arcade mindset, but today, most people playing Super Mario Bros. probably aren't out to finish the game, and likely have already finished the game in the past, instead just wanting to play for a little while before moving on to other things.

My idea, then, is to hack the SMB1 cart to make it better suited to the casual player. I take inspiration from, of all things, Quake 3: Arena. First, add a level select screen. I'm sick of endlessly replaying World 1. Second, some stat keeping to encourage replay. It should at least record the time to completion and score accumulated during the level. It might be worthwhile to give the player only one life to use during a run through a level, to make things more interesting, and stop them from gaming the scoring system. Naturally, add save ram and a battery so this data is preserved. Finally, more levels. You could integrate the whole of SMB2j, and possibly fan-made levels from the internet.

On the technical side, move it over to an MMC1/3/6 board with a battery, and you'll have plenty of space to cram in various hacked versions of the games. Write the main menu, make some minor hacks to the games to integrate them with the scoreboard, and you're set. Shouldn't be too difficult, although it isn't something I want to spend time on right now. Perhaps something like this already exists, among the hundreds of SMB romhacks that no one has time to dig through.

Tags: