#TimFollin

diyelectromusicdiyelectromusic
2026-03-02

I've had another look at the Follin archive and spent a bit of time trying to understand the tester music player for the ZX Spectrum for one of Tim Follin's tracks, using Ste Ruddy's sound driver...

I think I'm getting a bit more of an idea of how neat this all is :)

diyelectromusic.com/2026/03/02

Simple DIY Electronic Music Projectsdiyelectromusic.com@diyelectromusic.com
2026-03-02

Z80 and AY-3-8910 – Part 2

I’ve spent a bit of time looking at the “Tester” part of the AY driver code for Tim Follin’s music archive that I talked about in Z80 and AY-3-8910.

This is documenting what I think I’ve worked out so far for the tester code.

The Sound Tester

As previously mentioned, there are essentially three parts to the code in Follin archive:

  • The tune and effect data.
  • Ste Ruddy’s Sound Driver.
  • A tracker-style (ish) tester UI application.

The first part looked at the sound driver itself, and essentially skipped over the tester part of the code. This post picks up on that tester code.

Reminder, from part one, the main structure is as follows:

Code_Start: EQU 40000
Data_Start: EQU 50000

;-----------------------------
ORG Code_Start

; The UI/tester code
TESTER:
LOOP: Calls the following for each scan:
HALT - Suspends until an interrupt comes in?
CALL UPDATE
CALL REFRESH
CALL CLOCK
CALL KEYSCAN
Repeat as necessary

KEYSCAN: UI scanning
CLOCK: Possibly maintain a 50Hz refresh rate clock?
UPDATE: Loads the internal state of all sound variables from
the driver and displays them in real time via the UI.

; The sound driver
CODE_TOP:
TUNE: Select which tune to play.
TUNE_IN: Init all internal sound state variables for a new tune.
TUNEOFF: Stop a playing tune, eg to change tune or start an FX.

FX: Start playing an FX.
FLOOP: Keep processing FX instructions until complete.

REFRESH: "run" a scan of the sound driver updating and outputting the sound

The Tester Code

Initialisation information and main screen data:

;**************************************

; Z80 AY MUSIC DRIVER

;**************************************

; ORG 40000
; LOAD 0C000H

;======================================
;STACK DEPTHS

SD: EQU 3

;======================================

ASCII: EQU 23560 ; 23560 = $5C08 = System Variable "LAST K"

TESTER: PUSH AF
PUSH BC
PUSH DE
PUSH HL

XOR A ; ASCII = MINS = SECS = 0
LD (ASCII),A
LD (MINS),A
LD (SECS),A

CALL TUNEOFF ; TUNE initialisation
CALL STACKMESS ; Kick off the Tester code!

DB CLS ; The start of the main UI data
DB AT,0,0
DB INK,01010111B
DB "'AY' MUSIC DRIVE"
DB "R V2 BY S.RUDDY"

... Skip ...

DB INK,64+5
DB "VOLUME "
DB " "
DB 255

... Skip ...

AT: EQU 22
INK: EQU 16
CLS: EQU 15

STACKMESS: POP IX
CALL MESS
JP (IX)

There is a whole lot of screen data in DB blocks which includes some “op codes” that are defined later: AT, INK, CLS. These are special codes that are used by the ROM-based print routines (more here), as used by Sinclair BASIC, but in this case they are spelt out directly, later in code. The final 255 signifies the end of the screen data.

So how are these definitions handled? That all comes up in the “MESS” routine I’ll get to in a moment, but first that “STACKMESS” routine needs a bit of explanation.

When a CALL instruction happens, such as the CALL STACKMESS at the start, the current program counter gets pushed onto the stack. In this case the current PC will point to the instruction after the CALL, which happens to be the start of the screen data. So the POP IX will grab the address of the screen data and drop it into IX and then call the “MESS” function to actually get on with it!

But before I get to that, there is some more code after the screen data:

                LD HL,CALC1
PUSH HL
LD A,H
LD DE,4067H ; Output high byte
CALL HEX
POP HL
LD A,L
LD DE,4069H ; Output low byte
CALL HEX

LD HL,(CALC2)
PUSH HL
LD A,H
LD DE,4071H ; Output number of Tunes
CALL HEX
POP HL
LD A,L
LD DE,4073H ; Output number of effects
CALL HEX

LD HL,CALC1
LD DE,(CALC2)
ADD HL,DE
PUSH HL
LD A,H
LD DE,407CH ; Not entirely sure what this is outputting...
CALL HEX
POP HL
LD A,L
LD DE,407EH
CALL HEX

This is writing some basic data out to the display. CALC1 seems to relate to code section size. I believe CALC2 is the start address of the tune data, which is the following:

                ORG Data_Start

TUNES: EQU 5
EFFECTS: EQU 21

All three of these sections are outputting a 16-bit value in two single-byte chunks using the “HEX” routine, which takes a screen address (in the range $4000-$57FF) and outputs a hex number at that screen location.

So while I’m at it then, how is that HEX function working?

;--------------------------------------
HEX: INC DE ; DE contains the screen address to use
PUSH AF ; Start with DE+1
CALL ONEnib ; Write out the LOW 4-bits
POP AF
RRA ; A = A>>4
RRA ; to write out HIGH 4-bits
RRA
RRA
DEC DE ; Back to original DE screen address
ONEnib: AND 15 ; A = A & 0xF
ADD A ; BC = A * 2
LD C,A
LD B,0
LD HL,ROM_TAB ; Read from ROM_TAB[BC]
ADD HL,BC
LD A,(HL)
INC HL
LD H,(HL)
LD L,A ; HL = (uint16_t)ROM_TAB[A]
MIKESbug: LD C,D ; So HL now points to character bitmap in ROM
LD B,8 ; Write out 8 bytes to display memory directly
PRloop: LD A,(HL) ; (DE) = (HL)
LD (DE),A
INC HL ; HL++
INC D ; NB: Layout of display mem means D++ is next line of char
; for same value of E.
DJNZ PRloop ; WHILE (B-- > 0)
LD D,C ; (Restore D before returning, so DE still = screen addr)
RET

ROM_TAB: DW 3D80H ; ROM character set: 3D80 = "0"
DW 3D88H ; Each char = 8 x 8 bits
DW 3D90H
DW 3D98H
DW 3DA0H
DW 3DA8H
DW 3DB0H
DW 3DB8H
DW 3DC0H
DW 3DC8H ; = "9"
DW 3E08H ; = "A"
DW 3E10H
DW 3E18H
DW 3E20H
DW 3E28H
DW 3E30H ; = "F"

This is making use of the character set stored in the Spectrum ROM (more here) which is indexed via a 16-word jump table mapping the characters onto each of the 16 hex characters: 0..9, A..F.

Then each byte, 8 in total, of the character is written directly out to the Spectrum screen memory taking advantage of the odd formatting of the screen memory to easily skip to the next line of the display for each line of the character (more here).

So before I get into the main update loop, how the screen initialised and set up? That happens in the “MESS” and some ancillary functions.

MESS:           LD A,(IX+0)         ; At this point, McursorX, McursorY = (0,0)
INC IX ; So read a byte of screen data
OR A
RET M ; Stop IF A=255 (i.e. negative)
CP 32
JR C,Mcontrol ; IF A<32 process control character then RET back to "MESS"
CALL Mgetchar ; ELSE Process character
CALL Mgetaddr ; Get screen address for next output in DE
CALL MIKESbug ; Output the character
CALL PRattr ; Set the colour attributes
CALL INCcursor ; Update the screen position for the next byte of screen data
JR MESS

Mcontrol: LD HL,MESS ; Stick the address of "MESS" on the stack for the RET
PUSH HL
CP 15 ; IF A == CLS
JR Z,Mcls
CP 22 ; IF A == AT
JP Z,Mat
CP 16 ; IF A == INK
JR Z,Mink
RET ; RETurn to "MESS"

Mcolour: DB 0 ; Working variables for cursor position and colour
McursorX: DB 0
McursorY: DB 0 ; Has to be directly after McursorX (see later)

Mink: LD A,(IX+0) ; Process INK to set colour
INC IX
LD (Mcolour),A
RET

Mcls: LD HL,4000H ; Process CLS to clear screen
LD (HL),L
LD DE,4001H
LD BC,1AFFH
LDIR
LD (McursorX),BC
RET

INCcursor: LD HL,McursorX ; Moves the cursor on one position
LD A,(HL)
INC A
AND 31
LD (HL),A ; X++; X = X % 32
RET NZ ; IF X==0; Y++
INC HL ; Assumes McursorY is McursorX++
INC (HL)
RET

Mgetchar: LD L,A ; HL = A*8 + 3C00
LD H,0 ; Note: A > 32; where 32="Space"
ADD HL,HL ; In ROM, space is address 3D00
ADD HL,HL ; 32 * 8 = 0x100
ADD HL,HL
LD BC,3C00H
ADD HL,BC ; HL = Start address of character map for char in A in ROM
RET

.... skip ....

Mgetaddr: LD A,(McursorY) ; Calculate the screen address for (McursorX, McursorY)
AND 18H
OR 40H
LD D,A
LD A,(McursorY)
RRCA
RRCA
RRCA
AND 0E0H
LD E,A
LD A,(McursorX)
ADD E
LD E,A
RET ; DE = required screen address

Mat: LD A,(IX+0) ; Set cursor to provided X, Y in screen data
LD (McursorX),A
INC IX
LD A,(IX+0)
LD (McursorY),A
INC IX
RET

PRattr: LD A,D ; Get address of ATTRibute memory
RRA
RRA
RRA
AND 3
OR 58H
LD D,A
LD A,(Mcolour)
LD (DE),A ; And set the colour
RET

Basically this loop keeps working on the provided screen data until the value 255 is found, at which point it returns. There are two paths for handling the data:

  • IF the value is < 32 then it is a control value. Only CLS, AT and INK are recognised.
  • ELSE the value is assumed to be an ASCII character and is displayed.

Whatever is happening, happens at the coordinates given by (McursorX, McursorY) which start out as (0,0) and get updated automatically when a character is output, or in response to an AT command. INK will set the required colour in Mcolour, which again starts out as 0. This is applied after the character is written to the screen, using the PRattr function.

There is a fun bit of optimisation going on in Mcontrol. At the start it pushes the address of the MESS function on the stack, which means that the RET will jump back to the start of MESS rather than where the jump happened to Mcontrol itself.

There is another shortcut in the Mcls function: LDIR. From http://z80-heaven.wikidot.com/instructions-set:ldir: “Repeats LDI (LD (DE),(HL), then increments DE, HL, and decrements BC) until BC=0.” By setting the contents of HL (the first byte of the display) to zero, this will tile that same value across the display memory until BC, which starts at $1AFF, is zero. This will zero the whole display – both pixels and attributes – from 0x4000 through to 0x5AFF.

Now finally, we get to the main update loop.

LOOP:           
HALT
CALL UPDATE ; Update the display from the current Sound parameters
LD A,2
OUT (254),A ; Set border to 2
CALL REFRESH ; Update the sound driver parameters
XOR A
OUT (254),A ; Set border to 0
CALL CLOCK ; Run 50Hz clock
CALL KEYSCAN ; Guess what - scans the keyboard 🙂
LD A,07FH
IN A,(254) ; Reads 0x7FFE which is the bottom row of the keyboard
AND 1
JP NZ,LOOP ; Checks bit 0, which is the SPACE key
LD BC,65533 ; AY OUTPUT PORTS (FFFD, BFFD)
LD A,7
OUT (C),A
LD BC,49149
LD A,63 ; Set AY register 7 to 63 - i.e. all channels OFF
OUT (C),A

POP HL
POP DE
POP BC
POP AF
RET

I’m not going through the sub routines of the loop, other than to note the following:

  • UPDATE is a whole series of instructions that basically do the following to output the HEX value of a sound parameter:
LD A, (contents of one of the sound variables)
LD DE, (corresponding screen address for the variable to be displayed)
CALL HEX
  • REFRESH runs the sound driver itself, as described in Z80 and AY-3-8910.
  • CLOCK decrements the FIFTY variable and every time it gets to zero updates SECS and MINS and writes them out to the display. As it also uses the HEX routine, I guess it is storing the time using binary-coded decimal (BCD).
  • KEYSCAN reads the last key pressed from the system variable location stored in ASCII (23560 / 0x5C08).

At some point I might come back and work out what keys do what…

Closing Thoughts

I’d really like to get some of this code running on some of the alternate Z80 platforms I have. Getting the sound output shouldn’t be too much of an issue, but I’d really like to have some kind of display too.

But as can be seen above, the tester UI is pretty well tied into the oddities of the ZX Spectrum display, so porting it won’t be trivial.

I suspect there are already some existing AY/chiptune players that perhaps would be a better starting point, but from what I’ve seen they tend to stream the register data after having sampled it at regular intervals, which isn’t quite what I was after… there would be something really quite interesting about actually running Ste Ruddy’s Sound Driver with a Tim Follin soundtrack programmed in.

Kevin

#ay38910 #TimFollin #zxSpectrum
KEXP 🎶 #NowPlaying BotKEXPMusicBot@mastodonapp.uk
2025-12-04

🇺🇦 #NowPlaying on #KEXP's #AstralPlane

Tim Follin:
🎵 Passage from Genesis

#TimFollin

Simple DIY Electronic Music Projectsdiyelectromusic.com@diyelectromusic.com
2025-07-26

Z80 and AY-3-8910

Finally starting to look at the Arduino and AY-3-8910 was triggered by a couple of things recently. First getting an RC2014 and playing with AY-3-8910 based sound on that.

But also, having visiting RetroFest in Swindon this year, talking to Dean Belfield about the methods he used to develop for the ZX Spectrum and how he was donated a number of archive disks from the Follin brothers related to their producing music for 8-bit games.

The archive can be found here: https://github.com/breakintoprogram/archive-follin and it is a real treasure trove of information.

I wanted to try to understand some of it myself, so this is me poking about in the archives to learn a little about how some of this music was produced at the time.

After having a bit of an initial look and after posting a “this is interesting” post on Mastodon, Steven Tattersall told me about a similar thing they’d done with Follin’s ST driver too. This is for the YM2149 which is the Yamaha equivalent of the AY-3-8910. There is a great description of this one here: http://clarets.org/steve/projects/reverse_engineering_tim_follin.html

Once I’m a bit more through my own discussion it will be really interesting to compare the two.

For now, I’m just posting this based on my initial thoughts, and will update it as I go.

To be continued…

Warning! I strongly recommend using old or second hand equipment for your experiments.  I am not responsible for any damage to expensive instruments!

If you are new to microcontrollers, see the Getting Started pages.

The Archives

From Dean’s introduction:

“The music and sound effects were hand-coded by Tim and Geoff Follin in assembler as a series of DEFB’s representing note pitch and duration for each channel. This data can also contains commands, for example to loop a sequence, call a subroutine or switch on an effect.”

“Once completed, their music source was included in the intended game along with a small music driver, also written in assembler.”

“This could then be called from within the game for both in-game music and sound effects”

The sound driver effectively implemented a series of instructions and turned these into commands to write out to the chip as part of its main “scan”.

There is an example of the sound driver here: https://github.com/breakintoprogram/archive-follin/blob/main/Examples/AY/Ghouls%20n%20Ghosts/aydrive.z80

And there is a set of instructions for the music here: https://github.com/breakintoprogram/archive-follin/blob/main/Examples/AY/Ghouls%20n%20Ghosts/g%2Bgmusic.z80

Combining these gives a block of assembly that can be built and run directly to play the sound.

Ste Ruddy’s Sound Driver

Ste Ruddy was interviewed about his work with Tim Follin and the sound driver he produced here: https://www.c64.com/interviews/ruddy.html.

“Working with Tim was essential when working on the music driver as basically he told me what he wanted the driver to do and I made it do it.”

As far as I can make out, the aydrive.z80 sound driver in the archive is in two main parts:

  • A manager interface to load the track, run some kind of user-interface to show what is going on, and then call the sound routines.
  • The sound driver itself.

In the assembly, the sound driver starts with the following:

;======================================
; AY MUSIC DRIVER V1.00 BY S.RUDDY

CODE_TOP:

There then follows a series of initialisation statements. These hold the internal state of the various aspects of the sound driver – things like current playing frequency, loop counts, volume levels, ADSR settings and so on. These are the items that implement the “language” that provides the instructions for what to play.

So what is that language? There is a list of commands (“OpCodes”) in the music file:

FOR:            EQU 080H
NEXT: EQU 081H
LENGTH: EQU 082H
STOP: EQU 083H
GOSUB: EQU 084H
RETURN: EQU 085H
TRANS: EQU 086H
DISTORT: EQU 087H
SEND: EQU 088H
ADSR: EQU 089H
ENVON: EQU 08AH
WOBBLE: EQU 08BH
PORT: EQU 08CH
VIB: EQU 08DH
IGNORE: EQU 08EH
EFFECT: EQU 08FH
GOTO: EQU 090H
GATE_CON: EQU 091H
ENDIT: EQU 092H

The tune file contains sets of sound FX and tunes that consist of series of the above instructions to play notes and otherwise manipulate the AY-3-8910.

The basic code structure of the sound driver is as follows (using the assembler labels from aydrive.z80):

  • Working values, variables, and parameters – “CODE_TOP”.
  • Main tune driver setup – “TUNE”.
  • Sound FX driver setup – “FX”.
  • Main driver scanning routine – “REFRESH”.
    • This takes the current state of all the variables, parameters, and so on, performs any updates as per the provided commands, and outputs the latest status to the AY-3-8910.
    • This runs once “per frame”.
    • There are three copies of this routine, processing in turn each of channel A, B and C.
  • Main driver command routine.
    • A “jump off” table for each OpCode command in the sound driver.
    • Assembly routines for each command to update the variables, parameters, and process state of the sound driver.
    • Again there are three copies of the jump table and routines, one for each of channel A, B and C.
  • AY-3-8910 driver routine.
    • Architecture specific code to access the chip.
  • Note frequency table for 102 notes.
  • Close-out code.

The entry points, as far as I can see, are as follows:

  • Main tune driver setup “TUNE”.
  • Sound FX setup “FX”.
  • Scanning “REFRESH” routine.

The combined driver, tunes, and tester UI, when all combined, is structured as follows:

  • UI / Tester.
  • Sound Driver.
  • Tune and FX instructions.

This is expanded upon in pseudo-code below using the labels from the original assembly:

Code_Start: EQU 40000
Data_Start: EQU 50000

;-----------------------------
ORG Code_Start

; The UI/tester code
TESTER:
LOOP: Calls the following for each scan:
HALT - Suspends until an interrupt comes in?
CALL UPDATE
CALL REFRESH
CALL CLOCK
CALL KEYSCAN
Repeat as necessary

KEYSCAN: UI scanning
CLOCK: Possibly maintain a 50Hz refresh rate clock?
UPDATE: Loads the internal state of all sound variables from
the driver and displays them in real time via the UI.

; The sound driver as described above
CODE_TOP:
TUNE: Select which tune to play.
TUNE_IN: Init all internal sound state variables for a new tune.
TUNEOFF: Stop a playing tune, eg to change tune or start an FX.

FX: Start playing an FX.
FLOOP: Keep processing FX instructions until complete.

REFRESH:
CHANNEL_A: IF STOP_A jump to CHANNEL_B
START_A: Process channel A state with CALLS to OUT_CA_AY or
OUT_FLESH as reqd and then RET back into this code.
Processing includes: ADSR, vibrato, distortion,
wobble, portamento, note frequency, etc.
DO_A: Entry point for getting next instruction.
COMMAND_A: Process a command for CH A using JUMP_A table.
GOTNOTE_A: Process a note for CH A with CALLS to OUT_CA_AY or
OUT_FLESH as reqd and RET back into this code.
Carry on to CH B

CHANNEL_B: IF STOP_B jump to CHANNEL_C
START_B: As above for Channel B
DO_B:
COMMAND_B:
GOTNOTE_B:
Carry on to CH C

CHANNEL_C: IF STOP_C RET to caller.
START_C: As above for Channel C
DO_C:
COMMAND_C:
GOTNOTE_C:
Finally JUMP to (not CALL) OUT_CA_AY so that the RET
is a RET to caller of the REFRESH code.

OUT_FLESH: Sets CH A,B,C and noise levels then jump to
OUT_CA_AY which includes the RET to caller.

JUMP_A: Jump table and associated instructions for CH A.
All instructions JUMP back to DO_A when complete.

JUMP_B: Jump table and associated instructions for CH B.
All instructions JUMP back to DO_B when complete.

JUMP_C: Jump table and associated instructions for CH C.
All instructions JUMP back to DO_c when complete.

OUT_CA_AY: Send contents of Z80 register C (0 to 255) to AY
register defined by Z80 register A (0 to 15).
RET to caller

NOTES: Frequency definitions for 102 notes.

CODE_BOT: End of Sound Driver

;-----------------------------
ORG Data_Start

; Tune and FX instructions
; 5 tunes and 21 effects
TUNES: EQU 5
EFFECTS: EQU 21

DATA_TOP:
TUNES_A: Jump table for 5 tunes info for CH A.
TUNES_B: Jump table for 5 tunes info for CH B.
TUNES_C: Jump table for 5 tunes info for CH C.
FX_TAB: Jump table for 21 effects info.

Instructions for 21 effects.
Instructions for 5 tunes.

So there is a main logic block – the “REFRESH” code, which defines the current frequency and volume level according to the state of the playing note, ADSR and any added effects. Then there are logic blocks for each of the recognised sound commands to adjust that state as appropriate.

The Sound OpCodes

I was going to document each in turn, but actually Steven Tattersall has already done a really good job of that here: http://clarets.org/steve/projects/reverse_engineering_tim_follin.html

I’ll expand on that as I go in terms of what it seems to mean for the Z80 driver, but for starting point that is a pretty good summary!

State Variables

Before we get into the main block of code, there are a number of state variables within the driver, which are essentially locations reserved in the assembly at the start, with a specific label. In most cases there are three of each, one for each of channel A, B, C.

These are a mixture of internal state variables and stored parameters from the various OpCodes.

There are four DW (word) declarations, all other are DB (byte) locations. I’ve re-ordered them here to put what I believe are related ones together.

  • PC (WORD) – the “program counter” for recording the next instruction to process
  • LOOP (WORD) – for loop “address”
  • REPEAT – the loop count condition
  • COUNT – the loop counter
  • FREQ (WORD) –
  • STOP – Stop paying this channel if > 0
  • TRANS – Transpose in semitones
  • LENGTH – Default note length
  • IGNORE – ignore transpose for following note if > 0
  • PORT – Portamento
  • TARGET – for portamento
  • DISTORT – raw “detune” number to add to playing frequency
  • OLDFREQ
  • VOLUME – channel volume
  • FLIP1
  • OLDNOTE – for portamento
  • W_WAIT
  • W_DEL1
  • W_DEL2
  • W_OFF
  • A_INIT – ADSR parameters (4-bit)
  • A_ATT – ADSR attack(4-bit)
  • A_DEC – ADSR decay (4-bit)
  • A_SUS – ADSR sustain level(4-bit)
  • A_CYC – ADSR parameters (8-bit)
  • A_STAGE – ADSR internal state
  • A_TIME – ADSR internal state
  • A_CONT – ADSR reset action
  • V_DEL
  • V_DEL1
  • V_RATE
  • V_LIM1
  • V_LIM2
  • V_DIR
  • E_FREQ (WORD) – fixed frequency effect
  • E_TIME – Fixed frequency effect
  • E_WAIT – internal variable for effect
  • E_BITS – mixer settings for effect
  • FLESH
  • ENDIT – turn mute off
  • GATE
  • GATERES – mute time?
  • MEMGATE

I’ll walk through the main logic shortly, but before that, it is worth looking at the part of the code that processes the OpCodes:

                LD BC,(PC_A)    ; On entry, BC = PC_A
DO_A: LD A,(BC) ; Grab the next instruction
INC BC ; Inc the Program Counter
OR A ; If < 0x80 then it's a note
JP P,GOTNOTE_A
COMMAND_A: AND 127 ; Instruction &= 0x7F
ADD A ; x2
LD E,A
LD D,0 ; Now DE = Index into JUMP table
LD HL,JUMP_A ; HL = &JUMP_A
ADD HL,DE ; HL = &JUMP_A[Idx]
LD E,(HL) ; Load first byte into E
INC HL ; Move to 2nd byte to load into D
LD D,(HL) ; DE = OpCode address
EX DE,HL ; Swap DE/HL
JP (HL) ; Jump to OpCode code at addr in HL
GOTNOTE_A: ...

The instruction JUMP table comes later. Each entry is a WORD and is the address of the appropriate code to implement the OpCode, as follows:

JUMP_A:         DW A_FOR
DW A_NEXT
DW A_LENGTH
DW A_STOP
DW A_GOSUB
DW A_RETURN
DW A_TRANS
DW A_DISTORT
DW A_SEND
DW A_ADSR
DW A_ENVON
DW A_WOBBLE
DW A_PORT
DW A_VIBRATO
DW A_IGNORE
DW A_EFFECT
DW A_GOTO
DW A_GATECON
DW A_ENDIT

On entry to each chunk of OpCode assembly, the registers appear to be:

  • HL: Start address of the OpCode now running
  • BC: Program/Stack Counter – now pointing to first location after OpCode
  • DE: OpCode entry in the JUMP table (probably)

The descriptions in the following come from: http://clarets.org/steve/projects/reverse_engineering_tim_follin.html

FOR (repeats)

“0 – start_loop: Start a loop from this point. Loops are not stacked.”

A_FOR:          LD A,(BC)
LD (REPEAT_A),A ; Store repeat counter in REPEAT_A
INC BC ; Loop repeats from next OpCode
LD (LOOP_A),BC ; Store loop location in LOOP_A
JP DO_A ; Next instruction

NEXT ()

“1 – end_loop: Decrement counter and if not zero, go back to loop point.”

A_NEXT:         LD HL,REPEAT_A     ; REPEAT_A--
DEC (HL)
JP Z,DO_A ; IF == 0 THEN next instruction
LD BC,(LOOP_A) ; ELSE return to LOOP_A
JP DO_A

LENGTH (value)

“2 – set_default_note_time: If not zero, all following notes take this value as the duration. If zero, all following notes have an extra byte with the note’s duration.”

A_LENGTH:       LD A,(BC)          ; Store LENGTH_A
LD (LENGTH_A),A
INC BC
JP DO_A ; Next instruction

STOP ()

“3 – stop: Stop playback of the channel.”

A_STOP:         LD HL,STOP_A       ; STOP_A++                INC (HL)                JP CHANNEL_B       ; Move onto channel B

GOSUB (addr(WORD)) / RETURN ()

“4 – gosub: Push the current command address on a stack. Start processing from a new address. Next 2 bytes: offset of the subroutine from the start of the tune data (little-endian).”

“5 – return: Pop the return address off the stack and continue processing from the popped address.”

A_GOSUB:        LD A,(BC)     ; Set HL to required address
LD L,A
INC BC
LD A,(BC)
LD H,A
INC BC
PUSH HL ; Store address on real stack
LD HL,(SP_A) ; Grab OpCode SP (SP_A)
LD (HL),C
INC HL
LD (HL),B
INC HL ; Store OpCode PC in OpCode stack
LD (SP_A),HL ; Store updated OpCode SP
POP BC ; Grab address off real stack and...
JP DO_A ; ... run from there

A_RETURN: LD HL,(SP_A) ; Grab OpCode SP from SP_A
DEC HL ; Grab OpCode PC from OpCode stack
LD B,(HL) ; Stick it back into BC
DEC HL
LD C,(HL)
LD (SP_A),HL ; Update SP_A again
JP DO_A ; then continue processing again

SP_A: DW 0 ; OpCode stack pointer
STACK_A: DS 2*SD,0 ; OpCode stack (SD = 3)

TRANS (value)

“6 – set_transpose: Next byte: number of semitones to transpose all following notes. Signed 8-bit value.”

A_TRANS:        LD A,(BC)       ; Store value in TRANS_A
LD (TRANS_A),A
INC BC
JP DO_A ; Next instruction

DISTORT (value)

“7 – set_raw_detune: Next byte: raw value to add to the final note period in YM register space. Unsigned 8-bit value.”

A_DISTORT:      LD A,(BC)       ; Store value in DISTORT_A
LD (DISTORT_A),A
INC BC
JP DO_A ; Next instruction

SEND (reg, value)

“8 – direct_write: Write a value directly to the YM registers. This is often used to write noise pitch.
The “mixer” register, register 7, is treated differently. This register combines settings for all 3 channels A,B,C to determine whether they use the square or noise channel, so the driver ensures that only the bits relevant to the active channel are set and cleared.”

A_SEND:         LD A,(BC)
LD L,A ; L = register to write
INC BC
LD A,(BC)
INC BC
PUSH BC ; Store OpCode PC for later
LD C,A ; C = value to write
LD A,L
CP 7 ; IF reg != 7 JP A_NOT_IO
JP NZ,A_NOT_IO

LD A,C
LD (FLESH_A),A
LD (MEMGATE_A),A
CALL OUT_FLESH ; OUT_FLESH handles R7 (mixer)
POP BC ; Grab OpCode PC back
JP DO_A ; Next instruction

A_NOT_IO: CALL OUT_CA_AY ; A=reg, C=value
POP BC ; Grab OpCode PC back
JP DO_A ; Next instruction

ADSR (INIT|SUS, ATT|DEC, CYC)

“9 – set_adsr: Sets the note envelope. This takes 3 bytes and contains the attack and decay speeds, the minimum and maximum volume levels after attack or decay, and which stage to start in (attack, decay, or hold).”

A_ADSR:         LD A,(BC)
RRA
RRA
RRA
RRA
AND 15
LD (A_INIT_A),A ; A_INIT_A = param[0] >> 4
LD A,(BC)
AND 15
LD (A_SUS_A),A ; A_SUS_A = param[0] & 0x0F
INC BC
LD A,(BC)
RRA
RRA
RRA
RRA
AND 15
LD (A_ATT_A),A ; A_ATT_A = param[1] >> 4
LD A,(BC)
AND 15
LD (A_DEC_A),A ; A_DEC_A = param[1] & 0x0F
INC BC
LD A,(BC)
LD (A_CYC_A),A ; A_CYC_A = param[2]
INC BC
JP DO_A ; Next instruction

ENVON (value)

“10 – set_adsr_reset: Next byte: if zero, moving to a new note does not reset ADSR, otherwise ADSR is reset. (A zero value is usually used to define complex arpeggio sequences.)”

A_ENVON:        LD A,(BC)        ; Store value in A_CONT_A
LD (A_CONT_A),A
INC BC
JP DO_A ; Next instruction

WOBBLE (offset, del1, del2)

“11 – set_arpeggio: Sets the semitone note offset of the arpeggio and the times they are held for.”

A_WOBBLE:       LD A,(BC)     
LD (W_OFF_A),A ; W_OFF_A = param[0]
INC BC
LD A,(BC)
LD (W_DEL1_A),A ; W_DEL1_A = param[1]
INC BC
LD A,(BC)
LD (W_DEL2_A),A ; W_DEL2_A = param[2]
INC BC
JP DO_A ; Next instruction

PORT (value)

“12 – set_slide: Set the number of semitones to jump per update when applying glissando between notes. Usually set to 1.”

A_PORT:         LD A,(BC)
LD (PORT_A),A ; PORT_A = value
INC BC
JP DO_A ; Next instruction

VIB (del, rate, lim, dir)

“13 – set_vibrato: Set the delay, size, speed and starting direction of the vibrato effect.”

A_VIBRATO:      LD A,(BC)
LD (V_DEL_A),A ; V_DEL_A = param[0]
INC BC
LD A,(BC)
LD (V_RATE_A),A ; V_RATE_A = param[1]
INC BC
LD A,(BC)
LD (V_LIM2_A),A ; V_LIM2_A = param[2]
INC BC
LD A,(BC)
LD (V_DIR_A),A ; V_DIR_A = param[3]
INC BC
JP DO_A ; Next instruction

IGNORE()

“14 – skip_transpose: For the next note only, don’t apply transpose. Usually used for drums mixed in with bassline notes.”

A_IGNORE:       LD A,255
LD (IGNORE_A),A ; IGNORE_A = 255 (on)
JP DO_A ; Next instruction

EFFECT (time, bits, freq)

“15 – set_fixfreq: Force using a fixed frequency, defined in the next 3 bytes (mixer and period low/high), or turn it off by using a single zero-byte.”

A_EFFECT:       LD A,(BC)
LD (E_TIME_A),A ; E_TIME_A = param[0]
INC BC

; (skipping some commented out code)

A_SETEFF: LD A,(BC)
LD (E_BITS_A),A ; E_BITS_A = param[1]
INC BC
LD A,(BC)
LD (E_FREQ_A),A ; E_FREQ_A = param[2]
INC BC
LD A,(BC)
LD (E_FREQ_A+1),A ; E_FREQ_A = param[3]
INC BC
JP DO_A

GOTO (addr)

“16 – jump: Jump to a new offset. Used for the infinite looping of tunes.”

A_GOTO:         LD A,(BC)  ; L = addr[0]
LD L,A
INC BC
LD A,(BC) ; H = addr[1]
LD H,A
PUSH HL
POP BC ; Set BC = addr then
JP DO_A ; run the next instruction

GATECON (value)

“17 – set_mute_time: Turn the channel off after N more updates.”

A_GATECON:      LD A,(BC)
LD (GATERES_A),A ; GATERES_A = value
INC BC
JP DO_A ; Next instruction

ENDIF (value)

“18 – set_nomute: If set to non-zero suppress the automatic muting. The starting commands of a channel usually set this to 0xff.”

A_ENDIT:        LD A,(BC)
LD (ENDIT_A),A ; ENDIT_A = value
INC BC
JP DO_A ; Next instruction

The Main Logic Function

I’ll work through the assembly for the main REFRESH function in time. Watch this space!

Closing Thoughts

I’ll keep coming back to this and posting an update as I get into more of it. When I think I’m done, I’ll post a proper conclusion.

For now, its just interesting to note that even though I’m sure many people have been through this before me already, it is a really interesting activity to try to figure it out myself.

As I say – to be continued…

Kevin

#ay38910 #chiptunes #TimFollin

2025-05-12

Did you know … ?

That #TimFollin today is on the »Did You Know…«-section on the english Wikipedia and ›... that the FMV game Contradiction was made on such a low budget that its creator learned how to code instead of hiring a programmer?‹

en.wikipedia.org/wiki/Contradi

#C64 #Amiga

2025-04-30

So, what was the first videogame music "joke?" i.e. a bit of musical theme that is doing something funny on purpose?

I think it's a toss-up between

  • youtube.com/watch?v=4_gObHt1uZA <-- Tim Follin opening with a down-instrumented version of the kind of fantasy theme that Dragon Warrior and Final Fantasy had, and then stepping all the way on the gas

  • youtube.com/watch?v=V2k01qmi99k Comparing the NES version of the Metroid escape theme to the Famicom version of the theme (youtube.com/watch?v=WLpz_yNIT_o), the original Famicom version has an A-B-B loop structure while the NES version introduces a C part (A-B-B-C). The C part sounds very reminiscent of the themes from Kid Icarus; Hip Tanaka scored both games and the version of the escape theme for the NES originally appeared in public as a B-side track on a game music compilation released in Japan. I think it's either a nod to the game's shared lineage (same composer, same "engine," same dev team), or a nod-and-wink that the end of Metroid is just an enemy-free Kid Icarus level climb.

Are there older examples?

#metroid #solstice #HipTanaka #TimFollin #MusicHistory

2024-09-10

I've just published the final translated article from my blog to that blog. It's more fun than informative, but if you like computer game music, you will find a link to my favourite computer game song ever.

vintrospektiv.de/index.php/202

#TimFollin #ZXSpectrum #C64 #SNES

2024-08-05

Guy who plays Tim Follin composed games because he likes the gameplay but not the music.

#MakeUpAGuy
#VGM
#TimFollin
#RetroGaming

2024-04-12

Les commentaires sous cette vidéo contenant la bande-son du jeu Pictionary sur NES 🤓 #TimFollin youtube.com/watch?v=SJwh3erQly

Stewart Russellscruss@xoxo.zone
2024-03-16

via b3ta: Tim Follin's utterly banging soundtrack for Pictionary on the NES — youtube.com/watch?v=DzTDAgG4EX

#ChipTune #NES #TimFollin

2024-02-11

Every now and then, whenever I want to have a good time, I go and read the YouTube comments on a Tim Follin tune.

Here we have Solstice, Pictionary, Silver Surfer, and Target: Renegade.

(And I listen to the music too, of course.)

youtube.com/watch?v=4_gObHt1uZ

youtube.com/watch?v=tBGuInVojX

youtube.com/watch?v=SJwh3erQly

youtube.com/watch?v=ZQlLl2j5TH

#TimFollin #chiptune #NES

Screenshot of YouTube commentsScreenshot of YouTube commentsScreenshot of YouTube commentsScreenshot of YouTube comments
Jessie Nabein (Old Account)jasonnab@hachyderm.io
2023-10-15

youtube.com/watch?v=9tc2J6Z--L

Tim Follin is one of if not THE chiptune/retro gaming music god. Still one of my all-time favorite tracks by him.

#TimFollin #Chiptune #retrogaming #TimeTrax

2023-08-12

[SNES ]Rock N' Roll Racing
Music by: Tim Follin and Geoff Follin

#console #SNES #Nintendo #cartridge #TimFollin #music #retrogaming #videogames ##80s #90s #Geek

Client Info

Server: https://mastodon.social
Version: 2025.07
Repository: https://github.com/cyevgeniy/lmst