May 4th Musical offering: FORTI Decompiled
FORTI was a sound card for the TI-99/4A PEB, with software written in FORTH. It featured 4 of the TI sound chips, giving 12 programmable voices. Each chip's tone#3 generator was paired with a noise generator, for bass or percussion.
The FORTI software was best known in the form of its 3-voice demo of Bach's FWV 578 "Little" Fugue in G Minor (G-Moll), distributed with the TI FORTH Demo disk in 1983-84. The Bach demo used only 3 voices, on the sound chip in the 4A.
However, other compositions were made for 12 voices. I know of Chariots of Fire (Vangelis), Ricercar a 6 (Bach, Musical Offering, aka Prussian Fugue), and a newer Bach "Litle" Fugue (first 1K lost). On the plain 4A, these sound terrible but tantalizing, as 11 voices compete skilfully but hopelessly for 3 hardware sound channels.
What I did
Starting from a compiled FORTI demo of Chariots of Fire, I decompiled the FORTH words, read out data structures, and disassembled the code for the MUSIC word, which is in the ISR sound driver.
I benefitted from a small amount of leftover original source (from the corrupted Bach demo) and the FORTI user manual, but most of FORTI, especially the music compiling words, is lost to me and has to be reverse engineered.
From internal evidence, the Bach demo is older than Chariots of Fire. The FORTI user manual indicates features not supported in the Chariots of Fire MUSIC driver, which I may create again. I classify features into three versions of the FORTI software:
- B. Bach Demo (TI FORTH graphics demo)
- C. Chariots of Fire
- D. Distribution with FORTI manual
2. Capabilities of FORTI:
2.1 The Data Structures
- Voiceline - a list of note numbers (byte) and duration in ticks (byte).
- Envelope - a list of attenuation bytes, with looping index (high bit set).
- Workspace - set of 16 TMS9900 workspace registers holding the context for one sound channel.
- Pitchtable - arrays of frequency cmds for each note number.
For each of 12 voices, there is a separate voiceline, envelope, and pitchtable, assigned to a hardware sound channel.
The differential tempo counter:
Note durations in ticks (1/60th second) are multiplied by 32 and added into a "Next Note" countdown timer (per voice).
On each interrupt tick (1/60th second), the value DEL is subtracted from the countdown timer. DEL has an initial value of 32. When DEL is modified by the RIT, ACC, and TEMPO words, the effect is that the tempo is increased or decreased. The duration timer is never cleared so no time is lost. All voices move ahead at the same tempo.
When the duration timer reaches 0 or less (it is initialized to 0 to start music) a new note and duration are taken from the voiceline. If the duration is 0, the channel is silenced, and the ISR returns -1 on the stack (bug: and the ISR is removed?)
Looping inside the envelope: in terms of an ADSR model, the envelope will begin with an Attack-Decay section, a Sustain (looped) section, and an optional Release (tail) section which can apply during the last portion of each note. The Release feature is used in the Bach Demo but not Chariots of Fire (buggy).
Envelope bytes are processed every tick (1/60th second), not modified by tempo.
The 16 attenuation values are remapped through a dynamic offset DYN and a 48-byte dynamics table (DYT). This allows words like +VOLUME and -VOLUME to take effect
A pitchtable entry is 2 data bytes to send to the sound chip for a note number (the channel number will be added later).
Note number 0 is used as a rest, but a rest is actually defined by having 0 for the sound chip period.
PT0 is the regular pitchtable. Note number 37 corresponds to low A or 110 Hz, which is the lowest musical note a tone generator can produce. Because of this limitation, PT0 fills the bottom 3 octaves (notes 1-36) with a copy of the octave 37-48. If a voice using PT0 plays notes in the range 1-36, they will still be heard, but in a higher octave.
PT1 notes load white noise for percussion.
PT4 is for channel 3 to drive the periodic noise channel and begins with note 0 corresponding to 13.75 Hz.
0 VARIABLE VOICE1 QU C D E F EI G A B C 0 ,compiles a list of note-duration pairs into VOICE1 relying on definitions such as these:
49 VARIABLE XPOSE : &* XPOSE @ + SWPB DURATION C@ + , ; : A 0 &* ; : A# 1 &* ; : C 3 &* ; : AA 12 &* ; : CC 15 &* ; : EI 24 DURATION C! ; : QU 48 DURATION C! ; : RE DURATION C@ , ; : OCTAVE 12 * 49 + XPOSE ! ; ( n -- )
The FORTI manual says to compile a voiceline like so:
START VOICE: VOICE1 ( 1 ) QU C D E F ( 2 ) QU G A B C FINISMy example based on C/ uses a lower level way:
( Verdi Requiem Mv2 trumpet ) 0 OCTAVE 0 VARIABLE VOICE2 ( 131 ) 3E RE C E G E G RE C E G E G ( 132 ) Q CC RE RE 3E C E$ A$ ( 133 ) Q CC H RE 3E C E$ G ( 134 ) Q C RE H RE 0 ,
4.1 Workspace registers
FORTI takes advantage of the TMS9900 workspace pointer context. There is a block of 12 workspaces allocated in the dictionary, one for each voice. In MUSIC version C with dancing sprites, they must be contiguous so that a sprite table address can be found from (STWP-WS1)/8. In version B they are not contiguous.
In the Bach demo, (no sprites) Workspace WS1 might be set up like this:
VOICE2 VARIABLE WS1 PT0 , TRUMPET1 , 0 , 0 , 8000 , 9000 , 12 ALLOTBut with dancing sprites, all WS must be contiguous, in order to calculate the sprite#.
0 VARIABLE WS1 17E ALLOT WS1 20 + WS2 CONSTANT WS1 40 + WS3 CONSTANT WS1 60 + WS4 CONSTANT WS1 80 + WS5 CONSTANT WS1 A0 + WS6 CONSTANT WS1 C0 + WS7 CONSTANT WS1 E0 + WS8 CONSTANT WS1 100 + WS9 CONSTANT WS1 120 + WSA CONSTANT WS1 140 + WSB CONSTANT WS1 160 + WSC CONSTANTThe word MUSIC is the core of the ISR and is written in assembler. The first instructions of MUSIC pulls the WP off the stack;
ED10 0201 LI R1,>ED18 ED12 ED18 ED14 C019 MOV *SP,R0 fetch WSP ( wsp -- f ) ED16 0400 BLWP R0 effect is R0->WP R1->PC. no LWP on 9900The full code is in Listing 1 at the end of this article.
The workspace registers are initialized before MUSIC first runs, and have the following meaning:
FORTI WS usage. WS is a register workspace ------------------------ R0 voice or cascade addr. List of byte pairs (note#,dur) R1 pitch-table: e.g. PT0 or PT1 R2 envelope. e.g. PLUCK3 or ORGAN2 R3 index into envelope R4 countdown timer to next note. init to 0 to begin. R5 const pitch cmd byte R6 const attn cmd byte (for a noise, pitch is tone#3 and attn is tone#4/noise) R7 pitch cmd word for current note or 0 for rest R8 temp. compute vdp address (for sprite) or envelope address R9 vdp addr of sppat# R10 copy of SGCA, sound card address (copied from SGCA at each ISR entry, sic) R11 not used R12 temp. compute vdp data byte (for sprite) R13 ... R15 after blwp, not used ( Register numbers for use in assembling MUSIC - EO ) 0 CONSTANT NTP ( Note table / voice pointer ) 1 CONSTANT PTS ( pitch table start ) 2 CONSTANT ATS ( attenuation table start / envelope ) 3 CONSTANT ATP ( attenuation table offset ) 4 CONSTANT DUR ( countdown timer duration ) 5 CONSTANT PVID ( pitch voice ID ) 6 CONSTANT VVID ( volume voice ID ) A CONSTANT SG ( sound card address ) C CONSTANT PNDX ( pattern number vdp address ) ( D E F are clobbered on entry to MUSIC but could be temps )4.2 Player words
Words PLA1, PLA3, PLA12 begin playing 1, 3, or 12 voices, resp.
: PLA12 ' BMUSIC XMUS ; ( play : PLA3 ' CMUSIC XMUS ; ( play only WS1-3 on internal snd only ) : PLA1 ' DMUSIC XMUS ; ( play only WS1 on internal snd only )where
: MUSIC0 85EE SGCA ! MUSIC ; ( 1110 1110 only chip #0 with enable low ) : MUSIC1 85F6 SGCA ! MUSIC ; ( 1111 0110 only chip #1 ) : MUSIC2 85FA SGCA ! MUSIC ; ( 1111 1010 only chip #2 ) : MUSIC3 85FC SGCA ! MUSIC ; ( 1111 1100 only chip #3 ) : MUSIC4 84FE SGCA ! MUSIC ; ( 1111 1110 internal only ) : MUSIC5 857E SGCA ! MUSIC ; ( 0111 1110 say chip 4 ) : MUSIC6 85BE SGCA ! MUSIC ; ( 1011 1110 say chip 5 ) : MUSIC7 85DE SGCA ! MUSIC ; ( 1101 1110 say chip 6 ) 3424 VARIABLE PSTART : XMUSIC WS1 MUSIC0 OR IF 83C4 @ PSTART ! 0 83C4 ! OFF ENDIF ; : DMUSIC 0 XMUSIC ; : YMUSIC WS2 MUSIC0 WS3 MUSIC0 OR OR XMUSIC ; : BMUSIC WS4 MUSIC1 WS5 MUSIC1 WS6 MUSIC1 WS7 MUSIC2 WS8 MUSIC2 WSA MUSIC3 WSB MUSIC3 WSC MUSIC3 OR OR OR OR OR OR OR YMUSIC ; : CMUSIC 0 YMUSIC ; ( for PLA3 ) : XMUS CFA ISR ! INTLNK @ PSTART ! ;When playing just one voice:
: MUSIC0 85EE SGCA ! MUSIC ; ( 1110 1110 only chip #1 with enable low ) : XMUSIC WS1 MUSIC0 OR IF 83C4 @ PSTART ! 0 83C4 ! OFF ENDIF ; ( f/finished? -- ) : DMUSIC 0 XMUSIC ; : PLA1 ' DMUSIC XMUS ; ( play only WS1 on chip#1 or internal snd )In PLA1, DMUSIC is the ISR which calls MUSIC on one workspace pointer. (PLA12 and BMUSIC make a player that calls each of 12 voices.)
The version of MUSIC in Chariots of Fire draws dancing sprites on the screen (Bach demo does not have this.) For each of 12 voices, there is a sprite. The note number translates to a column on the screen, while the attenuation is shown by a colored bar where height indicates loudness.
Chariots of Fire utilizes 11 sound channels across 4 chips. Interestingly, it defines addresses for MUSIC5,6,7 as if the author imagined having more chips than 4.
It's quite likely that I'm not analyzing the final version of the MUSIC word. That said, I see the following optimizations that could be made:
* SGCA is placed on the stack and copied to the workspace R10 on each tick. This should be configured before music starts playing, just like other registers that don't change.
* The envelope release (or tail) segment looks buggy in Chariots of Fire (whose envelopes don't rely on it.)
* Byte swapping and SRL by 8.
movb *r8,r8 ab @DYN+1,r8 srl r8,8 movb @DYT(r8),r8 movb r8,*r10could be
clr r11 movb *r8,r11 swpb r11 a @DYN,r11 movb @DYT(r11),*r106. Missing Words
Parts of the music entry system were not present in the compiled demo:
- Defining words like VOICE: FINIS and <ENV: ENV> were not recovered. I speculate that the demos B and C were created in a lower level way, and that these defining words were invented for the final distributon.
- Envelope word =REPEAT to encode the sustain loop
- 3 SHARPS established a key signature, modifying the behavior of note words like F. In the demo, F pushes a constant note number 8 (plus octave). This implies
- T for Tie or slur is not present. I don't see that MUSIC implements any means to vary frequency.
- R: and :R to repeat a section in a voiceline. These save memory, but would require runtime implementation in MUSIC.
- convenience word =TEMPO
- counting words =MEAS and +MEAS
- Envelope defining words <ENV: ENV> produce code that copies the envelope PFA into a WS. This requires a <BUILDS DOES> construct. In the demo, envelope words are just arrays.
- The words +VOLUME and -VOLUME and dynamic markings from PPP to FFF.
- CONDUCTOR is the top defining word and is not present
- SILENT is not present
- +FIFTH and words that transpose voices at runtime, not compile time, will require support in MUSIC.
- <ALBUM and ALBUM> words
- =GR and GR that subtracts time (for a grace note) from the previous note duration. In the demo, GR sets DURATION to 1 tick.
- =FERMATA and FERMATA
- =DRUM to set a WS to play noise
- <PHRASE: PHRASE>
I was disappointed to find nothing about tremolo in this demo. Envelopes are just attenuation changes, which is how an organ tremolo operates, but on other instruments, tremolo means to vary the frequency, or play two frequencies for the overtones. (T or Tie/Slur effect on frequency is unknown; perhaps it just skips the tail.) An envelope for pitch would allow many more effects.
The FORTI manual lacks a dictionary.
The pitch table is flexible enough to allow different temperament. The pitch table we take for granted is the equal tempered system in which music can be transposed to another key without changing its character.
David Olson retrieved the Chariots of Fire demo disk.
Rene LeBlanc for his FORTH disassembler, which I obtained in 1985 and never used until now.
Gene Hitz and Owen Brand provided the MATIUG disk library, which I converted to DSK format, finding the TI FORTH source.
The BERG/WERNECKE FORTH decompiler found in the MATIUG disk library.
Lee Stewart's fbFORTH and TI FORTH manuals which I used as a reference throughout, especially on the dictionary entry structure.
Tursi for Classic99 in which I did most of the work.
Kryoflux hardware and software was used to efficiently acquire hundreds of legacy disks.
CANTRELL, the TI engineer named on the FORTI "stereo card" schematic. Who is CANTRELL?
FORTI Music Card Users Manual and Schematic at WHTech
* code field of MUSIC * Load workspace from stack arg ED10 0201 LI R1,>ED18 ED12 ED18 ED14 C019 mov *SP,R0 fetch WSP ( wsp -- f ) ED16 0400 blwp R0 effect is R0->WP R1->PC. no LWP on 9900 ED18 C2A0 mov @SGCA,R10 ECCE is pfa of variable SGCA. typ 85EE(#1 chip) ECCE ED1C 04CC clr R12 ED1E C104 mov R4,R4 duration timer. clear to start ED20 1531 jgt LABEL2 * next note ED22 D330 movb *R0+,R12 get note number in R12 ED24 098C srl R12,8 ED26 C1CC mov R12,R7 get note number in R7 ED28 0A9C sla R12,9 sprite col = note# * 2 ED2A 0209 li R9,>d844 WS1 D844 ED2E 02A8 stwp R8 ED30 6209 s R9,R8 (WSx - WS1) is a multiple of 32 ED32 0938 srl R8,3 divide by 8 ED34 0228 ai R8,>4301 address to write, in SAT 4301 ED38 06C8 swpb R8 ED3A D808 movb R8,@>8c02 set up VDP address 8C02 ED3E 06C8 swpb R8 ED40 D808 movb R8,@>8c02 8C02 ED44 D80C movb R12,@>8c00 sprite col = note# * 2 8C00 ED48 0588 inc R8 ED4A C248 mov R8,R9 vdp addr of sppat# ED4C D230 movb *R0+,R8 duration ED4E 0988 srl R8,8 ED50 0A58 sla R8,5 times 32 . 32 is the neutral value of DEL ED52 A108 a R8,R4 add, because need to pay time debt ED54 1509 jgt LABEL1 * silence * bug: if R4 is at worst 1-DEL, R8 is at worst 32, if R8<DEL-1 then music may stall FINIS ED56 0640 dect R0 leave R0 pointing to the last (valid) note ED58 C206 mov R6,R8 attn cmd byte ED5A 0228 ai R8,>0f00 silence 0F00 ED5E D688 movb R8,*R10 write to sound chip ED60 02E0 lwpi >8300 8300 ED64 0719 seto *SP put -1 on the stack ( wsp -- f ) ED66 045F b *NEXT LABEL1 ED68 A1C7 a R7,R7 note# * 2 ED6A A1C1 a R1,R7 pitch table address ED6C C1D7 mov *R7,R7 ED6E 1605 jne NOTE else, turn off voice: REST ED70 C206 mov R6,R8 attn cmd byte ED72 0228 ai R8,>0f00 silence 0F00 ED76 D688 movb R8,*R10 write to sound chip ED78 1005 jmp LABEL2 why not LABEL6 NOTE ED7A A1C5 a R5,R7 pitch cmd byte ED7C D687 movb R7,*R10 write to sound chip ED7E 06C7 swpb R7 ED80 D687 movb R7,*R10 write to sound chip ED82 04C3 clr R3 * note in progress LABEL2 ED84 C1C7 mov R7,R7 pitch cmd or 0 if none ED86 132E jeq LABEL6 nothing to do ED88 D212 movb *R2,R8 envelope first byte (N in sustain) ED8A 0938 srl R8,3 ED8C 8108 c R8,R4 R4 is counting down from duration*32 ED8E 1501 jgt SSTAIN ED90 1003 jmp LABEL3 SSTAIN ED92 0203 li R3,>003f start envelope at 3fh-R8*32 003F ED96 60C8 s R8,R3 LABEL3 ED98 0283 ci R3,>003f 003F ED9C 1102 jlt LABEL4 ED9E 0203 li R3,>003f R3 max 3F 003F LABEL4 EDA2 0583 inc R3 at least 1 EDA4 C203 mov R3,R8 EDA6 A202 a R2,R8 envelope pointer in R8 EDA8 D218 movb *R8,R8 EDAA 0988 srl R8,8 EDAC 0288 ci R8,>0080 0-F attentuation, 8x cmd byte 0080 EDB0 1501 jgt ENVJMP EDB2 1006 jmp LABEL5 * envelope cmd byte ENVJMP EDB4 0228 ai R8,->80 FF80 EDB8 C0C8 mov R8,R3 new envelope index in R3 EDBA A202 a R2,R8 EDBC D218 movb *R8,R8 they seem to have run out of registers EDBE 1001 jmp LABEL6 * R8 lsb is attn LABEL5 EDC0 06C8 swpb R8 * adjust attn by dynamics table LABEL6 EDC2 B220 ab @DYN+1,R8 ECD9 EDC6 0988 srl R8,8 ugh EDC8 D228 movb @DYT(R8),R8 D1BA EDCC 06C9 swpb R9 vdp address of sppat# EDCE D809 movb R9,@>8c02 8C02 EDD2 06C9 swpb R9 EDD4 D809 movb R9,@>8c02 8C02 EDD8 0A28 sla R8,2 mpy by 4 EDDA D808 movb R8,@>8c00 set sppat# to attn 8C00 EDDE 0928 srl R8,2 EDE0 A206 a R6,R8 add attn cmd byte EDE2 D688 movb R8,*R10 write to sound chip LABEL6 EDE4 6120 s @DEL,R4 Subtract DEL from timer CC60 EDE8 02E0 lwpi >8300 8300 EDEC 04D9 clr *SP put 0 on the stack EDEE 045F b *NEXT
Edited by FarmerPotato, Fri May 4, 2018 7:42 PM.