home.social

#tim-follin — Public Fediverse posts

Live and recent posts from across the Fediverse tagged #tim-follin, aggregated by home.social.

fetched live
  1. 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

    #AY38910 #TimFollin #ZXSpectrum

  2. 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
  3. 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

  4. 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

  5. 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

  6. 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

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

    #MakeUpAGuy
    #VGM
    #TimFollin
    #RetroGaming

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

  9. 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

  10. Today we are celebrating "Solstice".

    For anyone not familiar with it: This is the day when #TimFollin finished composing the music for the #NES game of the same name.

    The box even shows an artist's impression of this event.

  11. Thinking about #TimFollin again and how he was only 19 when he composed this for an instrument that, as the great composer David Wise once described it, was "the world's most advanced doorbell"

    youtube.com/watch?v=-J0H5ah1G7

  12. Done some housework, had some lunch and tea and it's 13:37 'o clock.
    Time to listen to tunes from one of my all-time favourite #SNES #racing games: #SiliconAndSynapse ¹ #RockNRollRacing in both their original versions, and their #SPC #chiptune versions composed by #TimFollin: open.spotify.com/user/fixato/p

    #NowPlaying #Music #Nintendo #SuperNintendo

    ¹ (Now better known as #BlizzardEntertainment)