Skip to content

Chapter 18

Sound and music

Chapter 2 said that two engines drive Paula, one for the sound effects and one for the music; this chapter takes both apart. By its end you will know how the effects reach Paula through two layers of code, why a sound starts two VBlanks after the tick asks for it and how its end is learnt; how the music's waits move a mission; how the music player keeps time and stores its songs, why its notes are flat on a European Amiga, and the one value its tempo rests on; and what the music's key does. The port and its instruments come at the end.

Paula, as far as this chapter needs it

Paula plays four channels, each given a sound sample's address, its length in words, a period and a volume from 0 to 64 (chapter 2). A channel reads its sound sample from chip memory by itself, which the Amiga calls DMA, direct memory access; the register DMACON switches it, its bits 0 to 3 naming the four channels: written with bit 15 set, the named channels go on, without it they go off. The interrupt registers, INTREQ and INTENA, follow the same rule.

Switched on, a channel raises an interrupt request at once, a bit by which a chip asks the processor for an interrupt. It plays its bytes, each held for its period, to the sound sample's end, one cycle; then it takes the address and length again, raises its request again and plays on. The requests are bits 7 to 10 of INTREQ, together 0x0780; one reaches the processor only while its bit and the master bit, 0x4000, are set in INTENA. Paula's channels interrupt at level 4 of the 68000's seven, so the processor runs the routine whose address lies at 0x70, the level-4 vector: so a program learns that a sound sample has played.

Two layers

The game's sound effects engine, the code that plays its eight effects, has two layers. The logic tick knows what ought to sound, but the game hands a channel a new sound sample only in a VBlank server, by a rule of its own, and a channel's end shows only in a request, which only the interrupt's handler sees. So the tick writes what it wants into records of its own, and a second layer starts and stops the channels as the game's rule and the hardware allow.

The upper layer is eight sound slots, two to a channel, each holding a sound sample, its length, period, volume and repeat count, the cycles to play. The lower layer is four channel records, one a channel, holding what the channel was last asked to play, when it last stopped, and the count of cycles the handler takes down. The table's periods and volumes are operands of the instructions (chapter 3):

Sound slot, channel Sound sample Period, volume Cycles Sounds for
0, 0 machinegun 200, 64 for ever the player's guns
1, 0 engine eased for ever the aircraft's engine
2, 1 machinegun 160, 57 for ever an enemy aircraft's guns
3, 1 engine 330, by distance for ever the nearest enemy aircraft
4, 2 boom 500, by distance 1 a burst
5, 2 splash 350, by distance 1 a splash; aboard, the sea at 800, 34, for ever
6, 3 machinegun 320, by distance for ever a ground gun
7, 3 as set the lift, the wheels' screech, a soldier's scream

The first sound slot of a pair that is on gets the channel, as chapter 2 said: the guns drown the engine, a burst a splash. Setting sound slot 7 switches 6 off and the reverse, so on channel 3 the last one set wins. A sound sample already playing on its channel is not started again, so the setter of a one-shot, a sound played once, clears the pair's memory of the sound sample last handed over, and a second burst starts afresh. Loudness falls with distance:

The figures

Loudness by distance
An enemy aircraft, n its distance in steps of 16 pixels 64 − 4n up to n = 5, then 44 − n
A burst, a splash, a scream, n in steps of 32 pixels 64 − n; a scream half of it
Silent from, derived 704 and 2,048 pixels
A ground gun, n its distance in steps of 8 pixels 64 − n, heard within 448 pixels

The distance chapters 7 and 13 saw the pass work out for the sound is this one, sound slot 6's, not the engine's.

The aircraft's engine, sound slot 1, is eased: each tick its volume moves towards a target by 1 up or 2 down, and its period by 20 up or 10 down towards a target of its own. That target is a base plus the nose's target angle divided by 128 and the height divided by 16, so the engine sounds higher with the nose down and lower with height. Ten places in the controls' code set the volume's target and the base, in chapter 14's words:

The aircraft Volume Period's base
full throttle, in the air or on the deck, or a dive 64 335
flying otherwise 49 385
on the deck otherwise, or held by the cable 40 808
crashing 25 960

Paused, or with the music off, nothing moves and the channels stop.

Two VBlanks late

Once a tick, sound_channels gives each channel the winner of its pair: a sound sample already playing gets only a changed period or volume, a new one goes to channel_play, three times with the same arguments. No note says why; the port calls it three times too. Look at the three calls at the end, and the three of channel_stop above:

; re/Wings.lst 0x0120A4-0x01211E: sound_channels [asm], a part of 0x012066-0x012131
loc_0120a4:
0120a4  4aaa0010             tst.l      $10(a2)
0120a8  67000078             beq.w      $12122
0120ac  3006                 move.w     d6, d0
0120ae  4eac8260             jsr        -$7da0(a4)                             ; -> channel_stop
0120b2  3006                 move.w     d6, d0
0120b4  4eac8260             jsr        -$7da0(a4)                             ; -> channel_stop
0120b8  3006                 move.w     d6, d0
0120ba  4eac8260             jsr        -$7da0(a4)                             ; -> channel_stop
0120be  42aa0010             clr.l      $10(a2)
0120c2  6000005e             bra.w      $12122
loc_0120c6:
0120c6  206b0002             movea.l    $2(a3), a0
0120ca  b1ea0010             cmpa.l     $10(a2), a0
0120ce  66000020             bne.w      $120f0
0120d2  242b000a             move.l     $a(a3), d2
0120d6  b4aa0014             cmp.l      $14(a2), d2
0120da  67000046             beq.w      $12122
0120de  25420014             move.l     d2, $14(a2)
0120e2  322b000a             move.w     $a(a3), d1
0120e6  3006                 move.w     d6, d0
0120e8  4eac826c             jsr        -$7d94(a4)                             ; -> channel_adjust
0120ec  60000034             bra.w      $12122
loc_0120f0:
0120f0  206b0002             movea.l    $2(a3), a0
0120f4  202b0006             move.l     $6(a3), d0
0120f8  3206                 move.w     d6, d1
0120fa  4cab001c000a         movem.w    $a(a3), d2-d4
012100  25480010             move.l     a0, $10(a2)
012104  256b000a0014         move.l     $a(a3), $14(a2)
01210a  48e7f880             movem.l    d0-d4/a0, -(a7)
01210e  4eac8254             jsr        -$7dac(a4)                             ; -> channel_play
012112  4cd7011f             movem.l    (a7), d0-d4/a0
012116  4eac8254             jsr        -$7dac(a4)                             ; -> channel_play
01211a  4cdf011f             movem.l    (a7)+, d0-d4/a0
01211e  4eac8254             jsr        -$7dac(a4)                             ; -> channel_play
/* src/sound.c, lines 86-108, a part of wof_sound_channels (lines 70-110) */
if (!a3) {                                                /* 0x0120A4 */
    if (a2->playing != 0) {
        channel_stop(c);
        channel_stop(c);
        channel_stop(c);
        a2->playing = 0;
    }
    continue;
}
if (a3->sample == a2->playing) {                          /* 0x0120C6 */
    uint32_t pv = ((uint32_t)a3->period << 16) | a3->volume;

    if (pv == a2->playing_pv)
        continue;
    a2->playing_pv = pv;
    channel_adjust(c, a3->period, a3->volume);
    continue;
}
a2->playing    = a3->sample;                              /* 0x0120F0 */
a2->playing_pv = ((uint32_t)a3->period << 16) | a3->volume;
for (int k = 0; k < 3; k++)
    channel_play(a3->sample, a3->length, (uint16_t)c, a3->period, a3->volume,
                 (uint16_t)a3->repeat);

channel_play stops a busy channel before setting the new sound sample up, and every stop writes the effects engine's count of VBlanks into the channel record. So the second call undoes the first and sets it up again, as does the third, and the record waits, stamped with the current count.

The effects engine's VBlank server, soundfx_vblank, runs before the game's own and starts a waiting channel only two VBlanks or more after its stamp: cmp.l #$2, d0 and blt.w. Then come the four registers, and or.w d1 collects the channel's bit for the one write of DMACON after the loop, which switches the channels on together.

; re/Wings.lst 0x01EC9C-0x01ECEC: soundfx_vblank [asm], a part of 0x01EC64-0x01ED79
loc_01ec9c:
01ec9c  4a680008             tst.w      $8(a0)
01eca0  6700004e             beq.w      $1ecf0
01eca4  202cce6c             move.l     -$3194(a4), d0                         ; sound_vblanks
01eca8  90a80004             sub.l      $4(a0), d0
01ecac  b0bc00000002         cmp.l      #$2, d0
01ecb2  6d00003c             blt.w      $1ecf0
01ecb6  4a2ccf17             tst.b      -$30e9(a4)                             ; g_027f15
01ecba  67000012             beq.w      $1ecce
01ecbe  b47c0001             cmp.w      #$1, d2
01ecc2  6600000a             bne.w      $1ecce
01ecc6  50eccf16             st.b       -$30ea(a4)                             ; sound_flags
01ecca  50eccf18             st.b       -$30e8(a4)                             ; g_027f16
loc_01ecce:
01ecce  2290                 move.l     (a0), (a1)
01ecd0  3368000a0004         move.w     $a(a0), $4(a1)
01ecd6  3368000c0006         move.w     $c(a0), $6(a1)
01ecdc  3368000e0008         move.w     $e(a0), $8(a1)
01ece2  316800120014         move.w     $12(a0), $14(a0)
01ece8  836ccf22             or.w       d1, -$30de(a4)                         ; sound_dmacon
01ecec  42680008             clr.w      $8(a0)
/* src/sound.c, lines 475-486, a part of wof_soundfx_vblank (lines 461-516) */
if (r->pending != 0 && (int32_t)(wof_g.sound_vblanks - r->stamp) >= 2) {
    if (wof_g.sound_flags[1] != 0 && c == 2) {
        wof_g.sound_flags[0] = 0xFF;
        wof_g.sound_flags[2] = 0xFF;
    }
    wof_paula_lc(c, r->sample);
    wof_paula_write(AUD(c, AUDLEN), r->words);
    wof_paula_write(AUD(c, AUDPER), r->period);
    wof_paula_write(AUD(c, AUDVOL), (uint16_t)(r->volume >> 16));
    r->count = r->repeat;
    wof_g.sound_dmacon = (uint16_t)(wof_g.sound_dmacon | (1u << c));
    r->pending = 0;

So a sound sample the tick asks for starts at the second VBlank after the tick, which keeps a channel silent for at least a whole VBlank between two sound samples. No note says why the game waits; the port waits as it does.

A ruler of VBlanks k to k + 3 and the cycle's end; four rows: the tick, the VBlank server, Paula's channel 3 and the handler, with what each does when.

The lift's clang through the two layers: the tick after VBlank k fills sound slot 7 and stamps the channel record; the VBlank server waits a VBlank and starts the channel at the second; Paula plays the one cycle, a request at its start and at its end; the handler stops it at the second.

The handler at the level-4 vector, audio_irq, takes a channel's count down at each request and stops the channel when the count goes below zero. A request comes at the start and at every cycle's end, so a repeat count of N plays N cycles: the clang, with 1, is stopped by its second request. A count of −1 is never counted, and the engine, the sea and the guns loop until the tick stops them. At a stop the handler's write of INTENA also silences, until the next VBlank, the interrupt of any channel it looked at earlier in the call and left playing; no script ever met that.

; re/Wings.lst 0x01EBF0-0x01EC52: audio_irq [asm], a part of 0x01EBAA-0x01EC63
loc_01ebf0:
01ebf0  0300                 btst.l     d1, d0
01ebf2  67000048             beq.w      $1ec3c
01ebf6  4a90                 tst.l      (a0)
01ebf8  67000012             beq.w      $1ec0c
01ebfc  4a680014             tst.w      $14(a0)
01ec00  6d000038             blt.w      $1ec3a
01ec04  53680014             subq.w     #$1, $14(a0)
01ec08  6c000030             bge.w      $1ec3a
loc_01ec0c:
01ec0c  b67c0001             cmp.w      #$1, d3
01ec10  66000006             bne.w      $1ec18
01ec14  422ccf18             clr.b      -$30e8(a4)                             ; g_027f16
loc_01ec18:
01ec18  03c4                 bset.b     d1, d4
01ec1a  3d44009a             move.w     d4, $9a(a6)
01ec1e  3d420096             move.w     d2, $96(a6)
01ec22  217cffffffff0016     move.l     #$ffffffff, $16(a0)
01ec2a  42690008             clr.w      $8(a1)
01ec2e  42a8000e             clr.l      $e(a0)
01ec32  216cce6c0004         move.l     -$3194(a4), $4(a0)                     ; sound_vblanks
01ec38  4290                 clr.l      (a0)
loc_01ec3a:
01ec3a  03c4                 bset.b     d1, d4
loc_01ec3c:
01ec3c  5241                 addq.w     #$1, d1
01ec3e  e34a                 lsl.w      #$1, d2
01ec40  d0fc001e             adda.w     #$1e, a0
01ec44  d2fc0010             adda.w     #$10, a1
01ec48  51cbffa6             dbra       d3, $1ebf0
01ec4c  4a44                 tst.w      d4
01ec4e  67000006             beq.w      $1ec56
01ec52  3d44009c             move.w     d4, $9c(a6)
/* src/sound.c, lines 431-457, a part of wof_audio_irq (lines 415-459) */
    for (int c = 0; c < 4; c++) {
        uint16_t bit = (uint16_t)(1u << (c + 7));

        if (!(d0 & bit))
            continue;
        if (CHAN(c).sample != 0) {
            if (CHAN(c).count < 0)
                goto keep;
            CHAN(c).count--;
            if (CHAN(c).count >= 0)
                goto keep;
        }
        if (c == 2)                                               /* 0x01EC0C */
            wof_g.sound_flags[2] = 0;
        d4 = (uint16_t)(d4 | bit);
        wof_paula_write(WOF_INTENA, d4);
        wof_paula_write(WOF_DMACON, (uint16_t)(1u << c));
        CHAN(c).target = -1;
        wof_paula_write(AUD(c, AUDVOL), 0);
        CHAN(c).volume = 0;
        CHAN(c).stamp  = wof_g.sound_vblanks;
        CHAN(c).sample = 0;
keep:
        d4 = (uint16_t)(d4 | bit);
    }
    if (d4)
        wof_paula_write(WOF_INTREQ, d4);

Eight routines of the effects engine are never called in play, and the port leaves them out; the one path of channel_play that would lead to them, for a channel 6 no caller asks for, is the stand-in chapter 8 showed.

The sound samples

The eight effects are files of signed bytes (chapter 3), loaded into chip memory at a mission's setup unless already loaded. Between missions all but the engine's are freed, for no reason a note gives, and the dialog for saving and loading frees that one too and loads it again as it closes. When the next mission begins, sound_slots_clear empties the eight sound slots and sound_channels stops the channels. The manual warns that a 512K machine may lose some effects at the higher levels (page 3); the port plays them all from its page.

What the scripts play

The headless original and the port keep the same event log, chapter 1's sound event log: every start of a sound sample and every restart at a cycle's end, with its channel, period, volume and instant. Over the 75 runs of the first three mission milestones, chapter 8's mission scripts and runs of the keys, the effects start 1,129 times and restart 10,219 times, the sea in every run while the aircraft waits in the hold. No request became ready for the processor while the main program ran between VBlanks, so delivering at VBlanks, as chapter 6's model does, loses nothing the original would see.

The sound moves even missions that never hear it. A target lets its soldiers out one by one, each after the first at a wait read from the count of VBlanks since the program's start (chapter 15), a count that includes the 312 VBlanks the front end waits between screens for the music to die away. Three scripts made before the headless original played the music (chapter 6) went another way with it: one let its second soldier out at tick 625 instead of 637, another no longer won. Each takes a poke setting the count back to its value without those waits.

The music player

The music player, songplay, is a hunk file of 5,148 bytes; the songs and their sound samples are in wofsongs, 41,328 bytes, whose code returns the address of its data, beside an overlay loader nothing calls. music_start loads both with LoadSeg and opens the player's timer; music_stop closes it and lets both go, called too by the dialog for saving and loading before it loads a game. The player has one entry, a command in D0: thirteen commands, of which the game gives six: open the timer, read a song's instruments, play, close the timer, report the song's state, and the song's fade.

The player's beat is the timer chapter 2 named, CIA-A's timer A. It counts down at the E clock, the colour clock divided by five; run out, it reloads from the timer's latch, a 16-bit value written a byte at a time, and raises an interrupt: a timer's tick, at which the player runs SongInt once. A timer's latch of L gives a timer's tick every L + 1 counts.

The player takes the timer through ciaa.resource, the system's way of sharing a CIA's timers. Command 0, _OpenTimerInt, hands it an interrupt node, the system's record of a routine to call, here SongInt, and starts the timer without loading it from the timer's latch, so that it counts down from the counter's power-up value. Then it keeps the level-4 vector and puts its own handler there, the move.l from $70.w and the one into it; in the port the vector is a value of the state.

; re/songplay.lst 0x09BC-0x0A4E: _OpenTimerInt, the music player
_OpenTimerInt:
09bc  2f0e                 move.l     a6, -(a7)
09be  42790000127c         clr.w      $127c.l                           ; PlayState
09c4  42b90000127e         clr.l      $127e.l                           ; TrackState
09ca  43f90000100c         lea.l      $100c.l, a1                       ; ciaa_name
09d0  2c780004             movea.l    $4.w, a6
09d4  4eaefe0e             jsr        -$1f2(a6)
09d8  23c000001008         move.l     d0, $1008.l                       ; ciaa_base
09de  41f90000101a         lea.l      $101a.l, a0                       ; timer_node
09e4  117c00020008         move.b     #$2, $8(a0)
09ea  117c00000009         move.b     #$0, $9(a0)
09f0  43f900001030         lea.l      $1030.l, a1                       ; timer_name
09f6  2149000a             move.l     a1, $a(a0)
09fa  217c00000000000e     move.l     #$0, $e(a0)
0a02  43faf8a2             lea.l      $2a6(pc), a1
0a06  21490012             move.l     a1, $12(a0)
0a0a  2c7900001008         movea.l    $1008.l, a6                       ; ciaa_base
0a10  7000                 moveq      #$0, d0
0a12  43f90000101a         lea.l      $101a.l, a1                       ; timer_node
0a18  4eaefffa             jsr        -$6(a6)
0a1c  13fc000100bfee01     move.b     #$1, $bfee01.l
0a24  33fc078000dff09a     move.w     #$780, $dff09a.l
0a2c  23f8007000001040     move.l     $70.w, $1040.l                    ; saved_level4_vector
0a34  43fafe12             lea.l      $848(pc), a1
0a38  21c90070             move.l     a1, $70.w
0a3c  33fc078000dff01e     move.w     #$780, $dff01e.l
0a44  33fc878000dff09a     move.w     #$8780, $dff09a.l
0a4c  2c5f                 movea.l    (a7)+, a6
0a4e  4e75                 rts
/* src/music.c, lines 522-544 */
/* orig songplay+0x09BC _OpenTimerInt - command 0: the song idle and no track started;
 * ciaa.resource opened and its timer A vector given an Interrupt node with SongInt; timer A
 * started, counting on from whatever it holds; SongIntHandler at 0x70 in place of the
 * vector found there, which is kept; the audio interrupts on.  Its write to INTREQR does
 * nothing. */
static void open_timer_int(void)
{
    VARS.play_state  = 0;
    VARS.track_state = 0;
    HEAD.ciaa_base   = 1;                              /* OpenResource("ciaa.resource") */
    HEAD.node_type   = NT_INTERRUPT;
    HEAD.node_pri    = 0;
    HEAD.node_name   = 1;                              /* timer_name */
    HEAD.node_data   = 0;
    HEAD.node_code   = 1;                              /* SongInt */
    wof_s.cia.vector = 1;                              /* AddICRVector(0, timer_node) */
    wof_cia_write(WOF_CIAA_CRA, 0x01);
    wof_paula_write(WOF_INTENA, 0x0780);
    HEAD.saved_level4_vector = wof_s.cia.level4;
    wof_s.cia.level4 = WOF_L4_SONGINT;
    wof_paula_write(INTREQR, 0x0780);
    wof_paula_write(WOF_INTENA, 0x8780);
}

While the music is loaded, the effects engine's audio_irq never runs. Command 4, close the timer, puts it back, and since the rank selection ends with music_stop, every mission begins with it in place.

A song's command writes the timer's latch's high byte and restarts the timer: 56 for songs 1 to 4, 60 for song 0. Nothing writes the low byte. It and the counter are taken to hold 0xFF and 0xFFFF from power-up, the CIA's state at reset by its documentation: the one assumption chapter 1 named, which nothing in the game writes over and no film of a real machine checked. With it the songs keep chapter 2's tempo; with 0 in the low byte every song would be 1.8 percent faster.

The figures

The times, on PAL, derived
A timer's tick, songs 1 to 4: the timer's latch 0x38FF 14,592 counts, 20.57 ms, 1.0285 VBlanks
With the low byte 0: 0x3800 14,337 counts, 1.8 percent faster
The first timer's tick, from the counter's 0xFFFF 65,536 counts, 92 ms
A quarter note, 24 timer's ticks 494 ms, 121.5 beats a minute; song 0, 528 ms, 113.6
The song's fade, 99 timer's ticks 2.04 seconds; song 0, 2.18

What a timer's tick does

At each timer's tick SongInt looks at the play state: idle, a song asked for, playing, a stop asked for, or fading. A song asked for starts its four tracks, each a line of music on its own channel, at volume 32, which goes as it stands to Paula's volume register. Playing, a note counts its time down and at its release switches its channel off; when its time has run out, the track reads on to the next note.

A note's length and the timer's tick of its release come from the player's table of durations. Its sound is the track's voice, the instrument the track has chosen: the player points the channel at the voice's sound sample, its length covering the one-shot part and the repeat part, sets the period and volume, and switches channel and interrupt on. At the first request, at the start, its own handler points the channel at the repeat part, which Paula takes at the cycle's end: the one-shot part plays once, the repeat part loops while the note sounds. A sound sample without a repeat part, as the two drums', goes off at its next request.

A waveform in two inks, the one-shot part gold and the repeat part blue, a line at the boundary; below, a hundred bytes around the boundary as steps.

The voice omlead at 427, the period of track 0's first note of song 2: its one-shot part, played once, and its repeat part, which loops from the line, as the maker reads them.

The period comes from a table of 132 numbers, a semitone apart, each the colour clocks of one wave at that note, divided by the voice's bytes a wave and shifted by its octave; the notes' path never reads the rate a sound sample's header names, so that rate changes nothing. The table holds an NTSC machine's colour clocks: on an NTSC Amiga its notes are in tune with A at 440 hertz within a third of a cent on average, a cent being a hundredth of a semitone; on a PAL Amiga, whose colour clock is slower, every note is 15.9 cents flat, about a sixth of a semitone, and the timer's ticks, counting the same clock, are slower by the same ratio. The port plays the table as it stands: in its PAL setting, the default, as a PAL Amiga does; in its NTSC setting in tune and 0.9 percent faster.

Changing songs

The song's fade is the player's fade-out, not the display's fade: every speed + 1 timer's ticks each track's volume goes one lower, until none is left and the song stops. The game asks for speed 2, a step every three timer's ticks, so from volume 32 the song's fade takes 33 steps, the last finding nothing left. A note keeps the volume it started with, so the long held notes of songs 2 and 4 sound at full volume through the song's fade and stop at its end.

With the music loaded, music_start first starts the song's fade of the song playing, then asks for the song's state until the answer is 0: a call, a test, a branch back, nothing that waits. On a real machine the timer's interrupts end the song's fade while it spins, and the screen stands still for about two seconds. The port's coroutine waits four VBlanks after each call of the state command, as the headless original does (chapter 6); each of the front end's changes of song waits 104 VBlanks.

; re/Wings.lst 0x0123DC-0x01240A: music_start [asm], a part of 0x0123DC-0x01246F
music_start:
0123dc  206f0004             movea.l    $4(a7), a0
0123e0  202f0008             move.l     $8(a7), d0
0123e4  2940a5f4             move.l     d0, -$5a0c(a4)                         ; song_number
0123e8  4aacc42a             tst.l      -$3bd6(a4)                             ; songs_seglist
0123ec  67000020             beq.w      $1240e
0123f0  2c6ca5f0             movea.l    -$5a10(a4), a6                         ; player_entry
0123f4  7006                 moveq      #$6, d0
0123f6  7202                 moveq      #$2, d1
0123f8  4e96                 jsr        (a6)
0123fa  4a2ca4f9             tst.b      -$5b07(a4)                             ; opt_music_off
0123fe  6600000e             bne.w      $1240e
loc_012402:
012402  7005                 moveq      #$5, d0
012404  4e96                 jsr        (a6)
012406  4a40                 tst.w      d0
012408  66f8                 bne.b      $12402
01240a  6000003c             bra.w      $12448
/* src/music.c, lines 681-707, a part of wof_music_start (lines 671-728) */
CO_BEGIN(c);
wof_g.song_number = song;
if (wof_m.songs_seglist[0].set) {
    wof_player_call(6, wof_tbl_music_fade_speed[0], 0);
    if (wof_g.opt_music_off) {
        /* 0x0123FE: with opt_music_off set the original branches to the LoadSeg path at
         * 0x01240E and loads wofsongs and songplay a second time, the first copies
         * leaking.  The new player's command 0, _OpenTimerInt (songplay+0x09BC), ignores
         * what AddICRVector answers, and timer A stays with the first player's timer_node,
         * so every underflow goes on running the first player's SongInt on its DATA hunk,
         * fading under command 6; it saves the vector at 0x70, the first player's
         * SongIntHandler, and puts its own there, so the channels' interrupts reach the
         * second player's DATA hunk.  Two players' DATA are then live, which the port's
         * state, one player's (src/mission.def), cannot hold.  A later music_stop gives
         * command 4 to the second player: RemICRVector takes the first player's node off
         * timer A, and 0x70 gets back the first player's handler, in a segment that
         * leaked but whose code is still there; audio_irq would never be at 0x70 again.
         * Nothing reaches it (re/notes/music.md, "The game's calls"): opt_music_off is set
         * only in flight (0x01CD5A), when the music is unloaded, and cleared at 0x01006A
         * before every rank selection.  The port goes on as with the music off: command 1
         * and no command 2. */
        WOF_STANDIN("M8 STAND-IN: music_start at 0x0123FE, the music loaded and "
                    "opt_music_off set: a second player, which the port's state cannot hold");
    } else {
        while (wof_player_call(5, 0, 0) != 0) {
            for (wof_f.music_spin = SPIN_VBLANKS; wof_f.music_spin; wof_f.music_spin--)
                CO_WAIT(c);

Control-S, which the manual's keys leave out (page 12) and the port puts on M (chapter 1), toggles a flag that switches the music off. The game reads the key only in flight, where no song plays; the flag silences the effects, and a song started while it is set is never played. The outer loop clears it before every rank selection, so it silences at most the high-score screen's song; and since it is the last byte of the raw part, a game loaded from a file saved with it set comes back silent too. With the music loaded and the flag set, music_start would load a second player beside the first: nothing reaches that branch, and the port marks it as a stand-in.

The songs

In the song data as the file stands, before the loader relocates it, every pointer is an offset into it; tools/song_decode.py reads it as the player does:

Part What it holds
a song for each track a sequence, and a table of voices
a sequence a pattern and a transpose for each entry, played in turn
a pattern events of 2 bytes: a note and its length's index, or a command
a command the next entry, the sequence again, a voice, the timer's latch, a volume
a voice 0x2E bytes: its sound sample, an IFF 8SVX form, and a vibrato and arpeggio, off

A note with its top bit set is tied: a run of tied notes sounds as one. Voice 0 is the rest voice, without a sound sample, whose notes take their time and start nothing. A note's length is one of twenty in the player's table, each released after about nine tenths of it.

Song Played Sequence entries Voices
0 on the high-score screen 17 SyntheBass, omlead, RoomBrass2, BassDrum3, sdrum1
1 behind the title pictures 13 the same five
2 behind the story scroller 29 the five and Mechanic1
3 never: song 1's data again 13 as song 1
4 in the rank selection 15 the five and Mechanic1

Four lanes of coloured bars over four bars of music: a brass melody with gaps, a brass line of long notes, a bass in short notes, and drums.

Song 0's first four bars as the player plays them; each lane names its track's voices and range. Songs 2 and 4 open with tied notes of one voice; song 0 shows the rest voice's half bars, tied notes held to the next start, a bass and two drums.

What the port made of it

src/sound.c is the effects engine routine by routine in the original's order and arithmetic, the triple calls and the INTENA write among it, with the registers the burst and the scream leave for their callers (chapter 9's family). A pointer to a sound sample becomes a sound handle, the port's number for a file and an offset, as the files lie in the page. The sound slots, channel records and pointers are registered state, compared under their original addresses; Paula, the timer and the level-4 vector are in the core's saved state (chapter 22).

The model is the headless original's, so that the two event logs can be held to each other: time in units so small that a VBlank and a byte at any period are whole numbers of them; every write at its VBlank's instant; what happens between two VBlanks delivered at the second, in time order. So the port's VBlank runs the events of the VBlank that passed, soundfx_vblank, its requests, then the game's server. In the same walk the core mixes each audio frame from the byte each channel plays at its instant into a queue outside the state, so a replay renders the same sound whatever the shell asks; the shell takes it by emulated time (chapter 23).

music_start and music_stop are coroutines, since each waits for a song's fade, four VBlanks a round, which keeps the phase of the input samples taken every fourth VBlank. Of what LoadSeg would load, the port keeps the player's changing data and the seven voice records, the rest voice's among them, which the player writes to, and reads the rest where the file lies. The player follows re/songplay.lst with the 68000's arithmetic. Seventeen stand-ins of the sound remain, chapter 8's count: channel 6, and sixteen of the music for the commands the game never gives, the second player and values no song holds.

How it is held

Every closed and open loop of the mission milestones compares the event log and the model's state after every pass and tick, chapter 8's comparison, the level-4 vector with it. The model's own tests hold a one-shot stopped by its second interrupt, a loop's restarts, the port's output at 960 audio frames a VBlank at 48 kHz, and the instrument's tempo: in the headless original's run, the first timer's tick comes at the counter's power-up value, and every steady one 0x3900, the times box's 14,592 counts, after the last:

# tests/test_music.py, lines 141-149
def test_the_timer_ticks_at_the_songs_tempo(idle_front):
    """The songs set only the latch's high byte, 56 here; the low byte keeps its power-up
    0xFF, so a tick comes every 0x38FF + 1 E cycles, 14,592 x 5 x 50 units on PAL; the
    first after the timer opens at the power-up latch, 65,536 cycles after it."""
    ticks = idle_front.paula.timer.calls
    tick = headless_paula.E_CLOCK_CC * 50
    assert ticks[0][2] == 0xFFFF and ticks[0][1] == headless_paula.PAL_CLOCK + 0x10000 * tick
    steady = [b[1] - a[1] for a, b in zip(ticks, ticks[1:]) if a[2] == b[2] == 0x38FF]
    assert len(steady) > 5000 and set(steady) == {0x3900 * tick}

The oracle holds every routine of the effects engine, and of the player those the game's commands reach, on hundreds to thousands of random states, the loudness helpers over every distance, Paula and the timer register by register. The front end's event log, the music after every VBlank through the outer loop with Control-S, and the page's sound are compared too (chapter 24). The effects were heard from the files tests/m8_renders.py writes, and the owner heard the music and found it right: chapter 10's ears. Controls, chapter 8's breaks of the port on purpose, were each caught:

What was changed in the port Where it first showed
a period one too high kills_a, pass 2: the sea's start
the two sound slots of a pair exchanged guns_a, pass 1120: the guns' start missing
a cycle's end delivered a VBlank late kills_a, pass 40: the sea's restart
the timer's latch's high byte one too high the idle front end, event 186: a start on channel 3 missing
a timer's tick delivered a VBlank late the idle front end, event 0: song 2's first starts

Left out of the model: Paula raises a cycle's end request four bytes before the model does; a real PAL video frame is about 0.16 percent longer than a fiftieth of a second; there is no filter and no analogue mixing; the wait for the song's fade ends up to three VBlanks late; and the timer's latch's low byte is an assumption.

For the developer

music_start's spin is at 0x012402, the branch to a second player at 0x0123FE: both files loaded again, the first copies never freed, and the new player, ignoring what the system answers, leaves the timer calling the first while it takes the level-4 vector. Of the eight routines never called, a shutdown at exit and a hand-over of channel 2 to the music hold chapter 7's two Delay waits; no note says what the hand-over was for. audio_irq collects the bits with bset.b d1, d4. The player writes INTREQR, $dff01e, three times, which does nothing on the machine, whatever it was for. Tools: tools/song_decode.py --events N, tools/sound_observe.py --runs NAME, tools/headless_paula.py.

What comes next

The chapter in one sentence: the tick asks, a VBlank server starts the sound two VBlanks later and an interrupt stops it, and the music is a player of its own on a CIA timer, whose one unwritten byte sets the tempo. Chapter 19 takes up the screens the songs play behind and the keys, Control-S among them.

Further reading