[back]

Soundtracker meets DSP

Listen first -- the same ten seconds of the song, taken from the board over Ethernet (48 kHz, 24 bits, stereo):

the DSP chain off soundtracker_dsp_off.wav
the DSP chain on: compressor and reverb soundtracker_dsp_compressor_reverb.wav

A Motorola 68000, Amiga's Paula and a CIA, playing a ProTracker module with the replay routine every Amiga demo used -- on a Digilent Arty A7-100T, or simulated with Verilator. The Arty has no audio output, so the sound leaves over its USB cable: 22 050 stereo frames a second on the serial line at 2 000 000 baud, and ffplay on your PC plays it.

The 68000 is written from the Programmer's Reference Manual and checked against Musashi (every register, every instruction); Paula and the CIA are transcribed from MiSTer's Minimig-AGA and checked against a C model of themselves, bit for bit. ptplayer, the replay routine (Frank Wille, public domain), runs on this 68000 unchanged.

Then the four channels go through five studio effects in hardware -- a compressor, a chorus, a flanger, a delay and a reverb, each the twin of a C++ model and checked against it sample by sample -- while a panel on the PC moves their knobs: the DSP system.

Getting started

Download l8soundtracker-dist.tar.gz, unpack it and start the menu:

tar xzf l8soundtracker-dist.tar.gz
cd l8soundtracker
make

make asks two questions, checks the programs it needs, programs the board (the programmer, openFPGALoader, is in the archive) and plays the song. What it needs on your PC:

program for Debian / Ubuntu
ffplay playing the sound sudo apt install ffmpeg
make, cc, c++ the Makefile; the simulation; the programmer, should the bundled one not run on your system sudo apt install build-essential
python3 reading the board's serial line sudo apt install python3
verilator only for the simulation sudo apt install verilator

If ffplay or the compilers are missing, make says so and prints the line to install them. Your user needs to be allowed to use the serial port (group dialout on Debian / Ubuntu).

The menu

question choices on the command line
1. What should run? dsp (the default) -- the board with the five DSP effects and their panel; arty -- the board without the effects, the sound from its USB cable; verilator -- simulated on this PC TARGET=dsp / TARGET=arty / TARGET=verilator
2. Where should the sound come from? (dsp) usb -- the USB cable, 8 kHz mu-law, nothing to set up; eth -- Ethernet, 48 kHz 24-bit stereo (the Ethernet setup first) OUT=usb / OUT=eth
2. What should happen with the sound? (arty) play -- ffplay plays it; wav -- recorded into a .wav OUT=play / OUT=wav
3. How much music? (a .wav) 20 s, 60 s, the whole song (212 s) SECS=…
make TARGET=dsp OUT=usb                    # the DSP system and its panel, the sound over the USB cable
make TARGET=dsp OUT=eth                    # the same, the sound at full quality over Ethernet
make TARGET=arty OUT=play                  # the plain board, the song in ffplay (Ctrl-C stops; make again starts it again)
make TARGET=arty OUT=wav SECS=60           # the plain board, one minute into soundtracker_arty.wav
make TARGET=verilator SECS=20              # simulated: 20 s into soundtracker_verilator.wav (about 5 s per second)

The Ethernet setup

The USB line carries the sound as 8 kHz mu-law -- a telephone's quality, enough to hear what a knob does. The board also sends the chain's output at full quality over its Ethernet port: 48 000 stereo frames a second, 24 bits, exactly the numbers the effects compute. make TARGET=dsp OUT=eth plays that. It needs a wired connection between the Arty and the PC, set up once.

the Arty (fixed in the bitstream) the PC
port the RJ45 jack, 100 Mb/s (the board's PHY, MII) any free Ethernet port; a USB-Ethernet dongle does
MAC address 02:00:4c:38:00:01 its own
IPv4 address 192.168.77.2 192.168.77.1, netmask /24 (255.255.255.0)
UDP port 36528 (it listens and sends from there) any: the panel picks one and says "hello" from it
gateway, DNS, DHCP none none: the cable is a network of its own, keep your normal network on the other port
answers ARP and UDP (no ping)
traffic 400 datagrams a second, 1 444 bytes each, 4.6 Mb/s to the PC one "hello" a second until the board answers
 ┌──────────────────────────────┐             ┌──────────────────────────────────────────────┐
 │ Arty A7-100T                 │  Ethernet   │ the PC: a free network port (a USB-Ethernet  │
 │ RJ45, 100 Mb/s               ├─────────────┤ dongle does), address 192.168.77.1/24        │
 │ 192.168.77.2, UDP port 36528 │  a cable    │ the panel: st_gui.py --eth                   │
 └──────────────────────────────┘             └──────────────────────────────────────────────┘
  1. The cable: from the Arty's RJ45 jack to a network port of the PC that is not your normal network (a USB dongle is the easy way). The jack's link LEDs light when both ends are on.

  2. The port's name: ip -br link lists them; the new one appears when the dongle is plugged in (for example enx001122334455 or eth1).

  3. The address (lost at a reboot):

    sudo ip addr add 192.168.77.1/24 dev PORT
    sudo ip link set PORT up

    If NetworkManager runs on the PC, it may take the port back; make it stay with a profile of its own instead:

    nmcli con add type ethernet ifname PORT con-name arty ipv4.method manual ipv4.addresses 192.168.77.1/24 ipv6.method ignore
    nmcli con up arty
  4. Check: with the DSP system on the board, ip neigh show 192.168.77.2 names the Arty's address 02:00:4c:38:00:01 after the panel's first packet. The board answers ARP and UDP only -- ping gets no answer, which is fine.

  5. Play: make TARGET=dsp OUT=eth. The panel sends "hello" to 192.168.77.2, port 36528, once a second until the board answers; the board then sends every datagram to that sender: 400 a second, 4.6 Mb/s, each 120 frames. A firewall that allows answers to the PC's own UDP packets (the usual setting) lets them in.

If make stops with "no network port of this machine has the address 192.168.77.1", step 3 is missing. If the panel runs but stays silent, ip neigh shows whether the board answered, and sudo tcpdump -ni PORT udp port 36528 whether the datagrams arrive. The same datagrams carry the frame that went INTO the chain beside each that came out -- what the repository's probe_check.py uses to compare the board with the C++ models.

The song

"Neon Techno" by Drozerix (3:32 of 4-channel ProTracker, 61 KB), released into the public domain -- its page on The Mod Archive. It is in the archive as mod/drozerix_-_neon_techno.mod and inside the bitstream, in the block RAM beside the program.

The system

┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Digilent Arty A7-100T                                                                              │
│ ┌──────────────────────────────────────────────────────────────────┐   ┌─────────────────────────┐ │
│ │ the system, 25 MHz                     st_soc.l8                 │   │ the board, 100 MHz      │ │
│ │                                                                  │   │                         │ │
│ │ ┌───────────────────┐  bus: 24-bit address, 16-bit data          │   │ ┌─────────────────────┐ │ │
│ │ │ 68000             │  AS R/W UDS LDS ─────┬────────────┐        │   │ │ async FIFO          │ │ │
│ │ │ 68k.l8            ├──────────────────────┤            │        │   │ │ 25 -> 100 MHz       │ │ │
│ │ │ IPL2-0 <── Paula  │                      │            │        │   │ └──────────┬──────────┘ │ │
│ │ └───────────────────┘                      │            │        │   │            │            │ │
│ │ ┌──────────────────────────────────────┐   │            │        │   │ ┌──────────┴──────────┐ │ │
│ │ │ block RAM 128 KB      0x000000       ├───┘            │        │   │ │ UART 2 000 000 baud │ │ │
│ │ │ vectors, boot.s, ptplayer,           │                │        │   │ │ 8N1, S / Q commands │ │ │
│ │ │ the module (Neon Techno, 61 KB)      │                │        │   │ └──────────┬──────────┘ │ │
│ │ └──────────────────┬───────────────────┘                │        │   │            │            │ │
│ │                    │ audio DMA (Agnus's slots)          │        │   └────────────┼────────────┘ │
│ │ ┌──────────────────┴─────────────────────────────────┐  │        │                │              │
│ │ │ custom chips          0xDFF000       st_chips.l8   ├──┘        │   ┌────────────┴────────────┐ │
│ │ │ INTENA INTREQ DMACON  VHPOSR         the beam      │           │   │ FTDI FT2232H, USB       │ │
│ │ │ ┌────────────────────────────────────────────────┐ │           │   │ channel B: /dev/ttyUSB1 │ │
│ │ │ │ Paula: 4 audio channels       st_paula.l8      │ │           │   └────────────┬────────────┘ │
│ │ │ │ AUDxLC LEN PER VOL DAT, states 0 1 2 3 4       │ │           │                │              │
│ │ │ └────────────────────────────────────────────────┘ │           │                │              │
│ │ │ ┌────────────────────────────────────────────────┐ │           │                │              │
│ │ │ │ CIA-B 0xBFD000: timer A (tempo), timer B,      │ │           │                │              │
│ │ │ │ ICR -> INTREQ EXTER, level 6 (ptplayer)        │ │           │                │              │
│ │ │ └────────────────────────────────────────────────┘ │           │                │              │
│ │ │ ┌────────────────────────────────────────────────┐ │           │                │              │
│ │ │ │ mixer: L = 0 + 3, R = 1 + 2; box filter        │ │           │                │              │
│ │ │ │ 3 546 895 Hz -> 22 050 Hz, s16 stereo frames   ├─┼──────────>│                │              │
│ │ │ └────────────────────────────────────────────────┘ │           │                │              │
│ │ └────────────────────────────────────────────────────┘           │                │              │
│ └──────────────────────────────────────────────────────────────────┘                │              │
└─────────────────────────────────────────────────────────────────────────────────────┼──────────────┘
                                                                                      │ USB
                                                                     ┌────────────────┴────────────┐
                                                                     │ the PC                      │
                                                                     │ st_play.py -> ffplay        │
                                                                     └─────────────────────────────┘

How the sound is made

The register interface

The 68000 reaches the sound hardware as memory: word registers at 0xDFF000 + offset (the custom chips) and byte registers on the even addresses of 0xBFD000 (CIA-B). The program's only view of Paula is these writes.

One audio channel -- four of them, x = 0..3, at 0xDFF0A0 + 0x10 * x:

offset register width meaning
+0x0 AUDxLCH 3 bits the sample's address, bits 20..16
+0x2 AUDxLCL 15 bits the sample's address, bits 15..1 (a sample starts on a word)
+0x4 AUDxLEN 16 bits the sample's length in WORDS (two samples a word)
+0x6 AUDxPER 16 bits the period: colour clocks per sample (3 546 895 Hz / period = the sample rate; 124 at the least)
+0x8 AUDxVOL 7 bits the volume, 0..64
+0xA AUDxDAT 16 bits two 8-bit signed samples; written by the DMA, or by the CPU for sound without DMA

Shared, each a set / clear register: bit 15 of a write says whether the ones in bits 14..0 SET or CLEAR their bits.

address register the bits used here
0xDFF096 (write) / 0xDFF002 (read) DMACON / DMACONR 9 DMAEN (the master switch), 0..3 AUD0EN..AUD3EN (a channel's DMA)
0xDFF09A / 0xDFF01C INTENA / INTENAR 14 INTEN (the master switch), 7..10 AUD0..AUD3 (level 4), 13 EXTER (level 6)
0xDFF09C / 0xDFF01E INTREQ / INTREQR the same bits: what is pending; written with bit 15 clear to acknowledge
0xDFF006 VHPOSR the beam's horizontal position (read only)

CIA-B, the timers ptplayer runs on. They count the E clock (709 379 Hz, a fifth of the colour clock):

address register meaning
0xBFD400 / 0xBFD500 TALO / TAHI timer A's start value: the song's tempo (1 773 447 / BPM; 14 187 at 125 BPM = 50 calls a second)
0xBFD600 / 0xBFD700 TBLO / TBHI timer B's start value: 576 E-clocks, the time Paula needs to start a new sample
0xBFDE00 / 0xBFDF00 CRA / CRB bit 0 start, bit 3 one-shot, bit 4 load (0x11: A continuous; 0x19: B once)
0xBFDD00 ICR written: bit 7 set / clear, bits 0 / 1 enable timer A / B; read: what happened, and the read clears it

Playing a note

ptplayer's interrupt runs 50 times a second (timer A). A new note on channel 0, as the board's write trace has it -- the first note of "Neon Techno", 1 595 words of sample at period 113 (31 388 samples a second):

dff096 = 0001       DMACON: channel 0's DMA off (the old sample stops)
dff0a0 = 0000       AUD0LCH \
dff0a2 = a288       AUD0LCL  > the new sample at 0x00a288
dff0a4 = 063b       AUD0LEN: 1 595 words
dff0a6 = 0071       AUD0PER: 113 colour clocks a sample
dff0a8 = 0040       AUD0VOL: 64
bfdf00 = 19         CIA-B CRB: timer B, once, 576 E-clocks (0.8 ms)
   ... level 6, timer B ...
dff096 = 800b       DMACON: channels 0, 1 and 3 on -- Paula fetches and plays
   ... level 6, timer B again ...
dff09c = 2580       INTREQ: acknowledge the audio blocks that started
dff0a0 = 0000       AUD0LC / AUD0LEN: where the sample LOOPS -- Paula takes these
dff0a2 = ab4c         at the end of the block it is playing now
dff0a4 = 01d9

Paula reloads AUDxLC and AUDxLEN at the start of every block, so the pointers the program writes while a block plays are the NEXT block's: the loop of a sample, or a silent word to stop.

One channel

 ┌─────────────────────────────┐        ┌───────────────────────────────────────────────────────────┐
 │ the 68000                   │ writes │ one channel                                st_paula.l8    │
 │ ptplayer, 50 times a second ├───────>│                                                           │
 └─────────────────────────────┘        │ ┌─────────────────────┐      ┌──────────────────────────┐ │
                                        │ │ AUDxLC  AUDxLEN     ├─────>│ DMA: pointer and counter │ │
 ┌─────────────────────────────┐        │ └─────────────────────┘      │ a word a line at most,   │ │
 │ block RAM: the samples      │ a word │                              │ in this channel's slot   │ │
 │ (the module)                ├───────>├─────────────────────────────>│ (hpos 0x0E + 2x)         │ │
 └─────────────────────────────┘        │                              └─────────────┬────────────┘ │
                                        │                                            │ AUDxDAT      │
                                        │ ┌─────────────────────┐      ┌─────────────┴────────────┐ │
                                        │ │ AUDxPER             ├─────>│ period counter: every    │ │
                                        │ └─────────────────────┘      │ PER colour clocks the    │ │
                                        │                              │ next byte: high, then low│ │
                                        │                              └─────────────┬────────────┘ │
                                        │                                            │ 8-bit sample │
                                        │ ┌─────────────────────┐      ┌─────────────┴────────────┐ │
                                        │ │ AUDxVOL             ├─────>│ x volume (64 as 63)      │ │
                                        │ └─────────────────────┘      └─────────────┬────────────┘ │
                                        └────────────────────────────────────────────┼──────────────┘
                                                                                     │ 14 bits, signed
                                                                                     v the mixer

The channel is a state machine of five states, stepped on every colour clock (Minimig's paula_audio_channel.v). States 3 and 4 alternate for as long as the DMA is on; at the end of a block (AUDxLEN words) the pointer and the length are taken again from AUDxLC and AUDxLEN, and the interrupt says that a new block has begun:

 ┌────────────┐  DMA on   ┌──────────────────┐ a word ┌──────────────────┐ a word ┌───────────────────┐
 │ 0 idle     ├──────────>│ 1 started: the   ├───────>│ 2 the first word ├───────>│ 3 the high byte   │
 │            │           │ word is thrown   │        │ of the block;    │        │ plays PER clocks  │
 │            │<──────────┤ away, the        │        │ the interrupt    │        │                   │
 └────────────┘  DMA off  │ pointer reloads  │        │ (AUDx, level 4)  │        │                   │
                          └──────────────────┘        └──────────────────┘        └──┬─────────┬──────┘
                                                                                     │         ^
                                                                          PER clocks │         │ PER clocks:
                                                                                     v         │ the next word
                                                                                  ┌──┴─────────┴──────┐
                                                                                  │ 4 the low byte    │
                                                                                  │ plays PER clocks  │
                                                                                  └───────────────────┘

From four channels to the PC

 ┌──────────────┐
 │ channel 0    ├────┐
 └──────────────┘    │   ┌────────────────┐   ┌─────────────────────────────────┐   ┌─────────────────────┐
 ┌──────────────┐    ├──>│ left = 0 + 3   ├──>│ box filter, 3 546 895 Hz to     ├──>│ a frame: left,      │
 │ channel 3    ├────┘   └────────────────┘   │ 22 050 Hz: each colour clock    │   │ right (s16, x2)     │
 └──────────────┘                             │ weighted 22 050, the one that   │   └──────────┬──────────┘
 ┌──────────────┐                             │ crosses a frame's edge split;   │              │ 4 bytes
 │ channel 1    ├────┐   ┌────────────────┐   │ a frame = the mean of the 160.86│   ┌──────────┴──────────┐
 └──────────────┘    ├──>│ right = 1 + 2  ├──>│ colour clocks it spans          │   │ UART 2 000 000 baud │
 ┌──────────────┐    │   └────────────────┘   └─────────────────────────────────┘   │ -> st_play.py       │
 │ channel 2    ├────┘                                                              │ -> ffplay           │
 └──────────────┘                                                                   └─────────────────────┘

The Amiga's own sound leaves through a DAC and an analogue low-pass filter. Here the box filter is both: it turns the samples, each held for its period, into frames at a fixed rate, and it averages away what is above 11 kHz, where 22 050 frames a second can not follow.

The DSP system

The second system on the same board (arty/st_dsp_arty.l8) sends the four channels through a chain of five audio effects before they leave: a compressor, a chorus, a flanger, a delay and a reverb, each a hardware twin of a C++ model of a studio effect. The PC switches them on and off and moves their knobs while the song plays. The sound leaves twice: over the USB cable as 8 kHz mu-law for listening anywhere, and over Ethernet as the full 48 kHz 24-bit stereo -- together with the frame that went INTO the chain, so the PC can check every frame against the models. make TARGET=dsp programs it and opens the panel.

┌──────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Digilent Arty A7-100T                                                       arty/st_dsp_arty.l8          │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐             │
│ │ the system, 25 MHz                                                                       │             │
│ │ ┌───────────────────┐ 48 000 frames/s ┌─────────────────────────────────────────────────┐│             │
│ │ │ 68000, Paula,     │ s16 stereo      │ st_dsp_sys                        st_dsp.l8     ││             │
│ │ │ CIA-B, the mixer, ├────────────────>│ ┌───────────┐ ┌─────────────────────────────┐   ││             │
│ │ │ box filter 48 kHz │                 │ │ fifo, 16  ├>┤ the chain (Q1.23 stereo)    │   ││             │
│ │ │ st_soc.l8         │                 │ │ frames    │ │ compressor, chorus, flanger,│   ││             │
│ │ └───────────────────┘                 │ └─────┬─────┘ │ delay, reverb               │   ││             │
│ │                                       │       │       └──────────────┬──────────────┘   ││             │
│ │                                       │       │ in                   │ out              ││             │
│ │                                       │ ┌─────┴──────────────────────┴──────────────┐   ││             │
│ │                                       │ │ frame_probe: (in, out) pairs, 96 bits     │   ││             │
│ │                                       │ └─────────────────────┬─────────────────────┘   ││             │
│ │                                       │ ┌─────────────────────┼─────────────────────┐   ││             │
│ │                                       │ │ mono, resample 48 -> 8 kHz, s16, mu-law   │   ││             │
│ │                                       │ └─────────────────────┼─────┬───────────────┘   ││             │
│ │ ┌──────────────────────────────────┐  │ ┌─────────────────────┼─────┼───────────────┐   ││             │
│ │ │ st_cmd: S, Q, the records        ├─>│ │ dsp_param_bus, five dsp_param_reg: the    │   ││             │
│ │ └────────────────┬─────────────────┘  │ │ coefficients, double buffered             │   ││             │
│ │                  │                    │ └─────────────────────┼─────┼───────────────┘   ││             │
│ │                  │                    └───────────────────────┼─────┼───────────────────┘│             │
│ │                  │                    ┌───────────────────────┴───┐ │                    │             │
│ │                  │                    │ probe_pack: 120 pairs a   │ │                    │             │
│ │                  │                    │ datagram; udp4_port       │ │                    │             │
│ │                  │                    └─────────────┬─────────────┘ │                    │             │
│ └──────────────────┼──────────────────────────────────┼───────────────┼────────────────────┘             │
│ ┌──────────────────┴────────┐ ┌───────────────────────┴───┐ ┌─────────┴─────────────────┐                │
│ │ UART rx 115 200 baud      │ │ Ethernet 100 Mb/s (MII)   │ │ UART tx: 8 000 bytes/s    │  100 MHz       │
│ └──────────────────┬────────┘ └───────────────────────┬───┘ └─────────┬─────────────────┘                │
└────────────────────┼──────────────────────────────────┼───────────────┼──────────────────────────────────┘
                     │ USB                              │ RJ45          │ USB
┌────────────────────┴──────────────────────────────────┴───────────────┴──────────────────────────────────┐
│ the PC: st_gui.py -- the knobs (libstdsp packs them), ffplay plays 48 kHz from Ethernet or 8 kHz mu-law  │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────┘

The control

The PC talks to the board over the same USB serial line the sound leaves on, at 115 200 baud. Four commands:

bytes command
S start: the 68000 restarts the song (its reset pin, 255 clocks), the stream starts at a frame's edge
Q stop the stream
W idx lo hi b0 .. b(n-1) write the n = lo + 256 hi bytes of effect idx's record (0 compressor .. 4 reverb)
C mask commit: every record written so far takes effect at once; bit k of mask switches effect k on

A record is not a list of knobs: it is the unit's coefficients, computed on the PC. What a knob means -- a time in ms that becomes a delay in samples, a decay time in seconds that becomes a feedback gain, a cut-off in Hz that becomes five biquad coefficients -- is computed in double precision by the same code the models use (*_ctl.h beside each unit), quantized, and packed bit by bit into the record (libstdsp's stdsp_pack). The hardware only multiplies and adds.

 ┌───────────────────────────┐   ┌───────────────────────────┐   ┌───────────────────────────────────────┐
 │ st_gui.py: a slider moves ├──>│ libstdsp: the effect's    ├──>│ W idx len  record bytes, LSB first    │
 │ (13 knobs of the reverb)  │   │ *_ctl.h -> coefficients,  │   │ C mask                                │
 └───────────────────────────┘   │ quantized, packed         │   └───────────────────┬───────────────────┘
                                 └───────────────────────────┘                       │ UART
 ┌───────────────────────────────────────────────────────────────────────────────────┴───────────────────┐
 │ st_cmd: S and Q only while no record is being received (a record's bytes may be 'S' or 'Q')           │
 └───────────────────────────────────────────────────┬───────────────────────────────────────────────────┘
 ┌───────────────────────────────────────────────────┴───────────────────────────────────────────────────┐
 │ dsp_param_bus: idle -> idx -> len lo -> len hi -> data (n bytes) -> idle;  'C' -> mask -> commit      │
 └───────────┬───────────────────────────────────────────────────────────────────────────────┬───────────┘
             │ a byte for idx k                                                              │ commit
 ┌───────────┴───────────────────────────────────────────────────────────────────────────────┴───────────┐
 │ dsp_param_reg<BITS, k>, one per effect                                                                │
 │ ┌─────────────────────────────────────────┐  commit   ┌─────────────────────────────────────────────┐ │
 │ │ shadow: the bytes shift in              ├──────────>│ live: what the unit uses, cast to its       │ │
 │ │                                         │           │ struct (CompCoef .. ReverbCoef)             │ │
 │ └─────────────────────────────────────────┘           └─────────────────────────────────────────────┘ │
 └───────────────────────────────────────────────────────────────────────────────────────────────────────┘

Double buffered: the record shifts into a shadow while the unit goes on with the old one; C copies all five shadows into their live registers in the same clock, so a unit never runs on a half-written record, and two effects changed together change in the same frame.

effect record knobs
compressor 244 bits input, threshold, ratio, attack, release, knee, make-up, mix, auto, relax, character, mode
chorus 266 bits x (time), y (spread), speed, depth, mix
flanger 2 116 bits character, alternate, stereo, manual, colour, feedback, speed, depth, mix
delay 1 008 bits time, scaler, detune, stereo detune, low cut, high cut, feedback, threshold, level mode, crossfeed, mix
reverb 1 039 bits hall, size, pre-delay, diffusion, buildup, low factor, low split, time, high factor, high split, late mix, width, mix

The panel

make TARGET=dsp programs the Arty with the DSP system (bit/top_dsp.bit), builds the panel's packer the first time (tools/stdsp/ into tools/libstdsp.so: the effects' control code, the same the models use), and starts the panel, tools/st_gui.py -- a terminal page with every effect's switch and knobs. ffplay plays the song behind it; every effect starts switched off, at the knobs' defaults.

key what it does
up / down the next / previous row: an effect's first row is its switch, the rows below are its knobs
left / right move the knob down / up by its step
shift + left / right move it by a fifth of the step
space switch the effect on / off
tab the next effect
r restart the song (and the stream)
q, Esc quit (the stream stops)

The header line shows how many bytes a second come from the USB line (8 000 while the stream runs). Beside an effect's name: the size of the record it last sent, or "refused by the unit (out of its range)" when the hardware cannot do a setting -- a delay longer than its line, a mode the effect does not have; the old record then stays in force.

A moved knob takes effect at once: the panel computes the effect's coefficients from all its knobs, sends the record (W) and a commit (C) -- 30 to 270 bytes, a few to 25 ms on the line, without a gap in the sound.

For the sound in full quality the board and the PC need an Ethernet cable between them: the Ethernet setup.

Soundtracker meets DSP -- the DSP pipeline     8000 bytes/s from the board   (q quit, r restart, tab next effect, space on/off)

  [x] compressor   31 bytes
      input dB               [############------------]        0
      threshold dB           [###############---------]      -12
    > ratio                  [#-----------------------]        2
      attack s               [###---------------------]   0.0244
      release s              [#-----------------------]    0.125
      knee                   [####--------------------]     0.17
      makeup dB              [########----------------]        0
      mix                    [########################]        1
      auto                   [------------------------]        0
      relax                  [------------------------]        0
      character              [------------------------]        0
      mode                   [------------------------]        0

  [ ] chorus   34 bytes
      x (time)               [############------------]      0.5
      y (spread)             [############------------]      0.5
      speed                  [########----------------]   0.3333
      depth                  [############------------]      0.5
      mix                    [############------------]      0.5

  [ ] flanger   265 bytes
    ...

  [ ] delay   126 bytes
    ...

  [ ] reverb   130 bytes
    ...

compressor

knob range step (shift: a fifth) default what it does
input dB -24 .. 24 1 0 gain before the compressor
threshold dB -40 .. 6 1 -12 the level above which the gain is turned down
ratio 1 .. 20 0.5 2 how much: 4 means 4 dB over the threshold come out as 1 dB
attack s 0.001 .. 0.2 0.001 0.0244 how fast the gain goes down when the level rises
release s 0.01 .. 2 0.01 0.125 how fast the gain comes back when the level falls
knee 0 .. 1 0.05 0.17 the soft knee's width: 0 sharp (10 dB), 1 wide (40 dB)
makeup dB -12 .. 24 1 0 gain after the compressor
mix 0 .. 1 0.05 1 dry / compressed: below 1 is parallel compression
auto 0 .. 1 0.05 0 auto timing: attack and release follow the material
relax 0 .. 1 0.05 0 less gain reduction overall
character 0 .. 5 1 0 0 Vanilla, 1 Smooth, 2 Over, 3 Glue, 4 Resist, 5 Smash: the slope and the times scaled
mode 0 .. 1 1 0 0 Standard, 1 Beyond (ratios below 1 expand, past infinity invert)

chorus

knob range step (shift: a fifth) default what it does
x (time) 0 .. 1 0.05 0.5 the voices' base delay: 20 to 60 ms
y (spread) 0 .. 1 0.05 0.5 the width: the two voices' LFO phase apart and panned apart
speed 0 .. 1 0.05 0.3333 the LFO's speed: 0.05 to 1 Hz
depth 0 .. 1 0.05 0.5 how far the delay swings: up to 20.7 ms
mix 0 .. 1 0.05 0.5 dry / wet

flanger

knob range step (shift: a fifth) default what it does
character 0 DP 2 TFX 0 .. 2 2 2 0 DP, 2 TFX: the two flanger characters built (their filters and delay laws)
alternate 0 .. 1 1 0 Alternate: the loop's (and for DP the output's) sign inverted -- the hollow, through-zero sound
stereo 0 .. 1 1 1 1: the right channel's LFO inverted (the comb moves the other way)
manual 0 .. 0 1 0 Manual Override: not in the hardware (stays 0)
color 0 .. 1 0.05 0.5 the comb's centre: where the sweep sits
feedback 0 .. 1 0.05 0.5 feedback: how strongly the comb rings
speed Hz 0 .. 10 0.1 0.5 the LFO's speed
depth 0 .. 1 0.05 0.5 how far the sweep goes
mix 0 .. 1 0.05 0.5 dry / wet

delay

knob range step (shift: a fifth) default what it does
time ms 1 .. 165 1 120 the delay time (up to 165 ms: the hardware's line is 170 ms)
scaler 0 .. 1 0.05 0 the time times 1 + scaler (dotted and longer feels)
detune ms -5 .. 5 0.1 0 the right channel later (or earlier) than the left
stereo detune 0 .. 1 1 0 1: the detune goes the other way on the right
low cut Hz 20 .. 1000 10 69.7 a high-pass in the feedback: the repeats lose their lows
high cut Hz 1000 .. 1.6e4 100 3083 a low-pass in the feedback: the repeats get darker
feedback 0 .. 1.2 0.05 0.6 how much of each repeat comes back: above 1 it grows until the limiter holds it
threshold dB -30 .. 6 1 0 the limiter's level in the loop
level mode 0 .. 1 1 0 the limiter: 0 soft (tanh), 1 hard clip
crossfeed 0 .. 1 1 0 1: each channel's repeats feed the other's line (ping-pong)
mix 0 .. 1 0.05 0.333 dry / wet

reverb

knob range step (shift: a fifth) default what it does
hall 0 .. 1 1 0 0 Room, 1 Hall: the early reflections' pattern and the late diffusion
size 0.5 .. 2 0.05 0.9931 the room's size: all delays scaled 0.5 to 2
pre-delay ms 0 .. 100 1 4 time before the reverb starts
diffusion 0 .. 1 0.05 0.8 how dense the first reflections are smeared
buildup 0 .. 1 0.05 0.64 how fast the late tail builds up
low factor 0.5623 .. 1.778 0.02 0.7295 how much faster (below 1) or slower the lows decay than the rest
low Hz 50 .. 1000 10 298.5 the split between lows and the rest
time s 0.3162 .. 31.62 0.1 1.259 the decay time (RT60) of the tail: 0.32 to 31.6 s
high factor 0.5623 .. 1.778 0.02 0.717 how much faster (below 1) or slower the highs decay
high Hz 1000 .. 1.6e4 100 4003 the split between the rest and highs
late mix 0 .. 1 0.05 0.39 early reflections / late tail
width 0 .. 1 0.05 0.8475 stereo width of the reverb: 0 mono, 1 wide
mix 0 .. 1 0.05 0.585 dry / wet

The chain

       ┌────────────┐   ┌────────────┐   ┌────────────┐   ┌────────────┐   ┌────────────┐
 in ──>│ compressor ├──>│ chorus     ├──>│ flanger    ├──>│ delay      ├──>│ reverb     ├──> out
       │ 8 clocks   │   │ 19 clocks  │   │ 180 clocks │   │ 47 clocks  │   │ 130 clocks │
       └────────────┘   └────────────┘   └────────────┘   └────────────┘   └────────────┘
         each behind dsp_bypass(en[k]): switched off, the frame goes round it; a frame every 520 clocks

Every unit is built the same way, and the way is the reason so much fits on a small FPGA:

The compressor

What it does. It follows the level of the louder channel and turns the gain down when the level is above a threshold, with a ratio, a soft knee, attack and release times -- the model Compressor.hpp:

 x = in * inG                                         the input gain
 a = max(|xl|, |xr|)                                  the detector: peak of both channels
 env = env + (a > env ? cA : cR) * (a - env)          attack / release one-pole
 L = 20 log10(env) + cal                              the level in dB
 x = L - thr                                          above the threshold
 GR = 0                    if x < -W/2                the gain computer: soft knee of width W
      s * (x + W/2)^2 / 2W if |x| <= W/2
      s * x                if x > W/2,   s = 1 - 1/ratio
 out = (1 - mix) * x + mix * x * 10^(-GR/20) * makeup

In the hardware the decibels are replaced by powers of two (1 unit = 6.02 dB), so the logarithm and its inverse become log2 and 2^x, each a 258-entry table and a linear interpolation (logexp.l8, error below 3e-6). The PC folds every constant into the record: the threshold, the knee's width, the slopes, the dry and wet gains.

 ┌──────────┐  ┌───────────┐  ┌────────────────┐  ┌────────────┐  ┌──────────────┐  ┌───────────┐  ┌─────────┐
 │ idle     │  │ env       │  │ lvl            │  │ law        │  │ gain         │  │ gain2     │  │ out     │
 │ x * ing  ├─>│ one-pole, ├─>│ X2 = log2(env) ├─>│ the knee:  ├─>│ e2 = 2^-GR2  ├─>│ gm = dm + ├─>│ x * gm, │
 │ a = max  │  │ ca or cr  │  │ - t2 (table)   │  │ 0, ks X2,  │  │ (table)      │  │ wg * e2   │  │ give    │
 │ |l|, |r| │  │           │  │                │  │ kq(X2+w2)^2│  │              │  │           │  │         │
 └──────────┘  └───────────┘  └────────────────┘  └────────────┘  └──────────────┘  └───────────┘  └─────────┘

Eight states, eight clocks a frame; every product has its own multiplier (the unit is small and short), the states only decide which register takes which result -- so a long chain of logic (table, interpolation, product) is cut into clocks. The envelope is ufx<5,40>: a release of two seconds moves it by 2^-17 of itself a sample, which 40 fraction bits still see. Not built: the model's Dual mode and its gain smoothing (zero in every preset).

The chorus

What it does. Two copies of the input, each read from a delay line at a delay that swings slowly around 20 to 60 ms, panned apart and mixed with the dry signal -- the model Chorus.hpp, measured from the reference:

 x = (l + r) / 2                                      mono into the line
 la = sin(2 pi ph),  lb = sin(2 pi (ph + off))        two LFOs, phase offset = width / 2
 lpa, lpb = one-pole(la, lb), 0.397 Hz                the LFOs smoothed
 va = line[w - (base + amp * lpa)]                    two voices, fractional delays,
 vb = line[w - (base + amp * lpb)]                    4-point Catmull-Rom interpolation
 outL = gd x + g aL va + g bL vb                      the voices panned (aL, aR, bL, bR)
 outR = gd x + g aR va + g bR vb

In the hardware one block RAM of 8 192 words (170 ms at 48 kHz, 18 bits a word) is the line. Both voices share its one read port, one address computation and one interpolator: the state machine reads the four samples around voice A's position, interpolates, then does the same for voice B.

 ┌────────────┐  ┌─────────────┐  ┌─────────────┐  ┌──────────────────────────────────────────────┐  ┌────────┐
 │ idle       │  │ wr          │  │ lfo         │  │ for voice A, then voice B:                   │  │ out    │
 │ x = mean   ├─>│ x into the  ├─>│ both one-   ├─>│ ┌─────────┐  ┌──────────────┐  ┌──────────┐  ├─>│ six    │
 │ of l, r    │  │ line; both  │  │ poles step  │  │ │ pos     ├─>│ rd x 4: i-1, ├─>│ voice:   │  │  │ prod., │
 │            │  │ sines (table│  │             │  │ │ d = base│  │ i, i+1, i+2  │  │ cubic4   │  │  │ give;  │
 │            │  │ + interp.)  │  │             │  │ │ + amp lp│  │ a word/clock │  │ (s0..s3, │  │  │ ph +=  │
 │            │  │             │  │             │  │ └─────────┘  └──────────────┘  │ frac)    │  │  │ inc    │
 │            │  │             │  │             │  │                                └──────────┘  │  │        │
 └────────────┘  └─────────────┘  └─────────────┘  └──────────────────────────────────────────────┘  └────────┘

The flanger

What it does. A short delay (a few ms) swept by an LFO and fed back into itself: the comb filter it makes moves up and down the spectrum -- the model Flanger.hpp (characters TFX and DP), per channel:

 lfo = one-pole(triangle(ph), 38 Hz)                  a triangle LFO, its corners rounded
 dt = ct + bt * lfo,  dl = cl + bl * lfo              two read positions (wet, loop), linear in the LFO
 dry = fir8(highpass(x))                              the dry path: one biquad, an 8-tap FIR
 h   = biquad x 4 (x)                                 the wet input's tone: four biquads
 fbv = biquad x nloop (line[w - dl])                  the loop's tone: one (DP) or three (TFX) biquads
 wet = line[w - dt]                                   16-tap windowed-sinc fractional read
 line[w] = gin * h + gfb * fbv                        into the line: the input and the feedback
 early = one-pole(x)
 out = gd * dry + gw * wet + ge * early

The fractional read is the reference engine's own: a Kaiser-windowed sinc of 16 taps (beta 8.44, cut-off 0.85 of Nyquist), its taps from a table of 257 phases, linearly interpolated between two phases. The table (4 112 entries) is computed when the hardware is elaborated, from the same formula the model uses.

In the hardware everything that repeats is one engine, and the state machine walks both channels through it:

 ┌───────────┐  per channel, left then right:
 │ idle      │  ┌───────────────┐  ┌─────────────┐  ┌───────────────┐  ┌─────────────┐  ┌───────────────┐
 │ LFO step, ├─>│ sec: dry HP   ├─>│ fir, firm:  ├─>│ sec x 4: the  ├─>│ rpos, rd    ├─>│ sec x nloop:  │
 │ ph += inc │  │ (5 clocks)    │  │ 8 taps      │  │ wet input     │  │ x 16: loop  │  │ the loop's    │
 └───────────┘  └───────────────┘  └─────────────┘  └───────────────┘  │ read at dl  │  │ tone          │
                                                                       └─────────────┘  └───────┬───────┘
 ┌───────────┐  ┌───────────────────────────────────────┐  ┌──────────────────────────┐          │
 │ give      │<─┤ wr: line[pos, c] = gin h + gfb fb;    │<─┤ rpos, rd x 16: wet read  │<─────────┘
 │ pos += 1  │  │ early; y = gd dry + gw wet + ge early │  │ at dt                    │
 └───────────┘  └───────────────────────────────────────┘  └──────────────────────────┘

The delay

What it does. Two lines, left and right, read back after a time set in ms, with a filter in the feedback and a soft clipper keeping the loop from running away -- the model DelayPlus.hpp (pattern Stereo):

 raw = line[w - d]                                    16-tap windowed-sinc fractional read (as the flanger)
 y = g(lp(hp(raw)))                                   low cut, high cut, a fixed 11.96 kHz low-pass:
                                                      three bilinear one-poles
 line[w] = level(x + feedback * y)                    level: T tanh(v / T) (soft) or clip to T (hard)
                                                      (crossfeed: left's y into right's line, and back)
 out = dry * x + wet * y

The time, the detune between the channels and the scaler become an integer delay and a 16-tap kernel on the PC (the kernel is computed exactly, not from a table: delay_ctl.h sums the Bessel series), so the hardware only reads and multiplies. The clipper's 1/T comes in the record too, so there is no divider.

 ┌───────────┐  ┌─────────────────────────────────┐  ┌────────────────┐  ┌────────────┐  ┌─────────────┐
 │ idle      │  │ per line, A then B:             │  │ lvl, lvl2      │  │ wra, wrb   │  │ out, give   │
 │ the frame ├─>│ ┌─────────────┐  ┌────────────┐ ├─>│ u = x + fb y,  ├─>│ both lines ├─>│ dry x +     │
 │           │  │ │ taps x 17:  ├─>│ filt x 3:  │ │  │ T tanh(u / T): │  │ written    │  │ wet y       │
 │           │  │ │ the read    │  │ hp, lp, g  │ │  │ a table (514)  │  │            │  │ pos += 1    │
 │           │  │ └─────────────┘  └────────────┘ │  │                │  │            │  │             │
 └───────────┘  └─────────────────────────────────┘  └────────────────┘  └────────────┘  └─────────────┘

The reverb

What it does. The model Reverb.hpp was identified from the reference's impulse responses and reproduces them sample by sample: a pre-delay, four diffusers, two early-reflection loops, a late "ring" of four branches that feed each other, and a short output kernel:

 x0 = (l + r) / 2
 s = diffuser x 4 (predelay(x0))                      all-pass (g + z^-d) / (1 + g z^-d), d from Size
 for c = A, B:                                        two early loops: a line with an all-pass inside
   w = eLine[c].read;  eLine[c].write(allpass(s - fb * w))
   e[c] = direct * s + gain * w
 xin = 0.8503 * lateIn.read(1704)                     the late input, 1 704 samples later
 for k = 0..3:                                        the ring: branch k is fed by branch prev[k]
   u  = xin + y[prev[k]]
   v1 = kLow u + (1 - kLow) highpass(u)               the band split: the lows decay by kLow,
   v2 = v1 + (kHigh - 1)(v1 - lowpass(v1))            the highs by kHigh (Low / High factor)
   w  = allpass(allpass(r * v2))                      r: the decay per round (Time)
   ring[k].write(w);  lA += tap A of ring[k];  lB += tap B of ring[k]
 A = we e[A] + wl lA,  B = we e[B] + wl lB            early / late mix
 L, R = width matrix of A, B -> 15-tap kernel         the reference's output smoothing
 out = gd * x + gw * (L, R)

In the hardware the 22 delay lines -- pre-delay, four diffusers, two early all-passes, two early lines, the late input, eight late all-passes, four ring lines -- live in one block RAM, each exactly as long as its longest delay at Size 200 % (83 084 words of 18 bits, 54 of the chip's 135 block RAMs). Each line has its own write pointer that wraps at its length; a read is the pointer minus the delay, wrapped. The state machine reaches every line through the RAM's one read and one write port:

 ┌────────────┐  ┌──────────────┐  ┌──────────────────┐  ┌──────────────────┐  ┌───────────────────────┐
 │ idle       │  │ pre, preq    │  │ apr apq apw x 4  │  │ er erq ew ewp    │  │ li liq: the late      │
 │ x0         ├─>│ the pre-delay├─>│ the diffusers    ├─>│ x 2: the early   ├─>│ input; rr x 4: the    │
 │            │  │              │  │ (the all-pass)   │  │ loops            │  │ ring lines' words     │
 └────────────┘  └──────────────┘  └──────────────────┘  └──────────────────┘  └───────────┬───────────┘
 ┌───────────────────────────────────────────────────────────────────────────────────────────┴───────────┐
 │ per branch k = 0..3: lu (u) -> bq1 bq2 (high-pass) -> bv1 -> bq1 bq2 (low-pass) -> bv3 bv4 (r v2)     │
 │ -> apr apq apw x 2 (the late all-passes) -> lw lwp (the ring's write) -> ta tb tc (the two taps)      │
 └───────────────────────────────────────────────────┬───────────────────────────────────────────────────┘
 ┌──────────────────────────┐  ┌─────────────────────┴────┐  ┌───────────────────────────────────────────┐
 │ give: the frame          │<─┤ fir x 15, out: the       │<─┤ mx mk: the early / late mix, the width,   │
 │                          │  │ kernel, dry + wet        │  │ into the kernel's line                    │
 └──────────────────────────┘  └──────────────────────────┘  └───────────────────────────────────────────┘

The line out and the Ethernet probe

 ┌──────────────┐   ┌──────────────┐   ┌──────────────────────────────┐   ┌──────────────┐   ┌──────────────┐
 │ chain output ├──>│ frame_mono:  ├──>│ resample<1,6,180,60,24>: a   ├──>│ q23_s16:     ├──>│ mulaw: G.711 │
 │ 48 kHz       │   │ (l + r) / 2  │   │ 180-tap Kaiser FIR, every    │   │ 16 bits      │   │ 8 000 bytes/s│
 │ stereo Q1.23 │   │              │   │ 6th frame: 8 kHz             │   │              │   │ UART 115 200 │
 └──────────────┘   └──────────────┘   └──────────────────────────────┘   └──────────────┘   └──────────────┘

The probe (audio/dsp/line/probe.l8) pairs each frame going into the chain with the frame coming out of it -- the chain is a pipeline of fixed order, so the n-th out belongs to the n-th in -- and probe_pack puts 120 pairs into a UDP datagram for 192.168.77.2, port 36 528:

 ┌────────────┬──────────────────────────────────────────┬──────────────────────────────────────────┬─────┐
 │ seq, 4 B   │ pair 0: in l, in r, out l, out r: 12 B   │ pair 1                                   │ ... │
 │ LE         │ 24-bit words, LE                         │                                          │     │
 └────────────┴──────────────────────────────────────────┴──────────────────────────────────────────┴─────┘
   1 444 bytes, 400 datagrams a second (4.6 Mb/s); a pair the network refuses is counted, never misaligned

The datagrams go to the PC that last sent a packet to the port (the panel sends "hello"). The PC runs the C++ models on the in frames and compares their output with the out frames: the hardware is checked on the music, at any knob setting, while it plays.

The DSP mapping

What each unit takes of the FPGA (an XC7A100T: 240 DSP48E1 slices, 135 block RAMs of 36 Kb, 63 400 LUTs), from the build of the bitstream: the hierarchy's utilization after place and route, and synthesis's DSP Final Report, which lists every DSP48E1 slice with the formula it computes. A slice multiplies 25 x 18 bits (its A port is 30 bits wide, the multiplier takes 25 of them); a wider product takes two slices, the second adding the first's upper half from the cascade (PCIN>>17+A*B, its A then 0 bits when only B carries the upper half). C+A*B and (P+A*B) use the slice's post-adder; a ' marks a pipeline register synthesis moved into the slice.

unit DSP48E1 slices of them the upper half of a wide product (PCIN>>17) with the post-adder (C+, P+) block RAM LUTs FFs
compressor -- the compressor 28 13 0 -- 2288 248
chorus -- the chorus 21 5 5 4 x 36 Kb + 1 x 18 Kb 4383 345
flanger -- the flanger 31 13 1 7 x 36 Kb 8180 2455
delay -- the delay 23 7 5 9 x 36 Kb 1734 547
reverb -- the reverb 76 38 0 54 x 36 Kb 5208 3102
resampler -- 48 kHz to 8 kHz for the serial line 1 0 0 -- 4957 4458
Paula's mixer -- the four channels' volumes, the box filter to 48 kHz 8 0 2 -- 1618 925
68000 -- MULU / MULS 4 1 0 -- 6115 1046
the whole design (the Ethernet and the UART too) 192 of 240 106 x 36 Kb + 4 x 18 Kb of 135 37464 of 63 400 23409

Every slice, as synthesis's DSP Final Report lists it -- the formula the slice computes, its ports' widths in bits (A x B, the adder's C, the result P) and its pipeline registers (A2: two registers on A, and so on):

compressor: 28 DSP48E1
# formula A B C P registers
0 A*B 24 18 - 48 --
1 A*B 24 17 - 48 --
2 PCIN>>17+A*B 24 17 - 48 --
3 A*B 20 17 - 48 --
4 PCIN>>17+A*B 0 18 - 30 --
5 A*B' 30 17 - 48 B1
6 PCIN>>17+A*B' 0 18 - 48 B1
7 A''*B 24 17 - 48 A2
8 PCIN>>17+A''*B 24 18 - 32 A2
9 A''*B 24 17 - 48 A2
10 PCIN>>17+A''*B 24 18 - 32 A2
11 A*B 17 18 - 48 --
12 PCIN>>17+A*B 30 18 - 34 --
13 A*B 17 17 - 48 --
14 PCIN>>17+A*B 17 18 - 48 --
15 A*B 30 17 - 48 --
16 PCIN>>17+A*B 0 18 - 48 --
17 A*B'' 24 17 - 48 B2
18 PCIN>>17+A*B'' 24 18 - 36 B2
19 A'*B' 30 17 - 48 A1 B1
20 PCIN>>17+A*B' 0 18 - 29 B1
21 A'*B' 17 17 - 48 A1 B1
22 PCIN>>17+A*B' 0 18 - 46 B1
23 A'*B' 30 17 - 48 A1 B1
24 PCIN>>17+A*B' 0 18 - 29 B1
25 A'*B' 17 17 - 48 A1 B1
26 PCIN>>17+A*B' 0 18 - 46 B1
27 A*B' 24 12 - 48 B1
chorus: 21 DSP48E1
# formula A B C P registers
0 A''*B 23 17 - 48 A2
1 PCIN>>17+A''*B 23 18 - 48 A2
2 A''*B 17 17 - 48 A2
3 PCIN>>17+A''*B 17 18 - 48 A2
4 A''*B 23 17 - 48 A2
5 PCIN>>17+A''*B 23 18 - 48 A2
6 A''*B 17 17 - 48 A2
7 PCIN>>17+A''*B 17 18 - 48 A2
8 A*B'' 30 17 - 48 B2
9 PCIN>>17+A*B'' 0 18 - 35 B2
10 C+A*B 30 16 39 39 --
11 C+A*B 30 16 40 40 --
12 C'+A*B 30 16 39 39 C1
13 A'*B'' 30 18 - 37 A1 B2
14 A'*B'' 30 18 - 37 A1 B2
15 C+A'*B'' 30 18 44 44 A1 B2
16 A'*B'' 30 18 - 37 A1 B2
17 A'*B'' 30 18 - 37 A1 B2
18 C+A'*B'' 30 18 44 44 A1 B2
19 A*B 30 17 - 48 --
20 A*B 30 17 - 48 --
flanger: 31 DSP48E1
# formula A B C P registers
0 A*B 30 17 - 48 --
1 PCIN>>17+A*B 0 18 - 35 --
2 A*B 30 7 - 48 --
3 A*B 24 17 - 48 --
4 PCIN>>17+A*B 30 17 - 48 --
5 A*B 30 17 - 48 --
6 PCIN>>17+A*B 30 18 - 48 --
7 A*B 30 17 - 48 --
8 PCIN>>17+A*B 30 18 - 48 --
9 C+A*B' 30 12 39 39 B1
10 A'*B 30 17 - 48 A1
11 PCIN>>17+A*B 0 18 - 36 --
12 A*B 30 17 - 48 --
13 PCIN>>17+A*B 30 17 - 43 --
14 A*B 17 17 - 48 --
15 PCIN>>17+A*B 30 17 - 48 --
16 A*B 24 17 - 48 --
17 PCIN>>17+A*B 0 18 - 48 --
18 A*B 30 17 - 48 --
19 PCIN>>17+A*B 0 18 - 48 --
20 A'*B 30 18 - 48 A1
21 A*B' 30 17 - 48 B1
22 PCIN>>17+A'*B 30 17 - 48 A1
23 A*B' 30 17 - 48 B1
24 PCIN>>17+A*B' 0 18 - 48 B1
25 A'*B 30 18 - 48 A1
26 A*B' 30 17 - 48 B1
27 PCIN>>17+A'*B 30 17 - 48 A1
28 A'*B 30 18 - 48 A1
29 A*B' 30 17 - 48 B1
30 PCIN>>17+A'*B 30 17 - 48 A1
delay: 23 DSP48E1
# formula A B C P registers
0 A*B 30 17 - 48 --
1 PCIN>>17+A*B 0 18 - 34 --
2 (P+A*B)' 30 18 - 40 P1
3 A*B 30 17 - 48 --
4 PCIN>>17+A*B 0 18 - 34 --
5 A*B 30 17 - 48 --
6 PCIN>>17+A*B 0 18 - 34 --
7 Dynamic - - - 43 --
8 C+A'*B'' 30 18 45 45 A1 B2
9 Dynamic - - - 43 --
10 C+A'*B'' 30 18 45 45 A1 B2
11 Dynamic - - - 43 --
12 A''*B' 30 17 - 48 A2 B1
13 PCIN>>17+A''*B' 30 18 - 33 A2 B1
14 Dynamic - - - 43 --
15 A''*B' 30 17 - 48 A2 B1
16 PCIN>>17+A''*B' 30 18 - 33 A2 B1
17 C+A*B 30 11 28 32 --
18 PCIN+A:B+C 0 1 - 33 --
19 Dynamic - - - 42 --
20 C+A*B 30 11 28 32 --
21 PCIN+A:B+C 0 1 - 33 --
22 Dynamic - - - 42 --
reverb: 76 DSP48E1
# formula A B C P registers
0 A*B 17 18 - 48 --
1 PCIN>>17+A*B 30 13 - 48 --
2 A'*B'' 30 17 - 48 A1 B2
3 PCIN>>17+A'*B'' 30 18 - 48 A1 B2
4 A*B 17 18 - 48 --
5 PCIN>>17+A*B 30 18 - 48 --
6 A*B 17 17 - 48 --
7 PCIN>>17+A*B 17 18 - 48 --
8 A*B 30 17 - 48 --
9 PCIN>>17+A*B 0 18 - 48 --
10 A*B 30 17 - 48 --
11 PCIN>>17+A*B 0 18 - 48 --
12 A*B 30 17 - 48 --
13 PCIN>>17+A*B 0 18 - 48 --
14 A*B 30 17 - 48 --
15 PCIN>>17+A*B 0 18 - 48 --
16 A*B 30 17 - 48 --
17 PCIN>>17+A*B 0 18 - 48 --
18 A*B'' 30 17 - 48 B2
19 PCIN>>17+A*B'' 0 18 - 48 B2
20 A*B'' 30 17 - 48 B2
21 PCIN>>17+A*B'' 0 18 - 48 B2
22 A*B'' 30 17 - 48 B2
23 PCIN>>17+A*B'' 0 18 - 48 B2
24 A*B'' 30 17 - 48 B2
25 PCIN>>17+A*B'' 0 18 - 40 B2
26 A*B'' 30 17 - 48 B2
27 PCIN>>17+A*B'' 0 18 - 48 B2
28 A*B 30 17 - 48 --
29 PCIN>>17+A*B 0 18 - 48 --
30 A*B'' 30 17 - 48 B2
31 PCIN>>17+A*B'' 0 18 - 48 B2
32 A*B'' 17 18 - 48 B2
33 PCIN>>17+A''*B 30 18 - 48 A2
34 A''*B 17 17 - 48 A2
35 PCIN>>17+A''*B 17 18 - 48 A2
36 A*B'' 30 17 - 48 B2
37 PCIN>>17+A*B'' 0 18 - 48 B2
38 A*B'' 17 18 - 48 B2
39 PCIN>>17+A''*B 30 18 - 48 A2
40 A''*B 17 17 - 48 A2
41 PCIN>>17+A''*B 17 18 - 48 A2
42 A*B'' 17 18 - 48 B2
43 PCIN>>17+A''*B 30 18 - 48 A2
44 A''*B 17 17 - 48 A2
45 PCIN>>17+A''*B 17 18 - 48 A2
46 A*B'' 30 17 - 48 B2
47 PCIN>>17+A*B'' 0 18 - 48 B2
48 A*B'' 17 18 - 48 B2
49 PCIN>>17+A''*B 30 18 - 48 A2
50 A''*B 17 17 - 48 A2
51 PCIN>>17+A''*B 17 18 - 48 A2
52 A*B 30 17 - 48 --
53 PCIN>>17+A*B 0 18 - 48 --
54 A*B'' 17 18 - 48 B2
55 PCIN>>17+A''*B 30 18 - 48 A2
56 A''*B 17 17 - 48 A2
57 PCIN>>17+A''*B 17 18 - 48 A2
58 A'*B'' 30 17 - 48 A1 B2
59 PCIN>>17+A'*B'' 30 18 - 48 A1 B2
60 A*B'' 17 18 - 48 B2
61 PCIN>>17+A''*B 30 18 - 48 A2
62 A''*B 17 17 - 48 A2
63 PCIN>>17+A''*B 17 18 - 48 A2
64 A*B'' 17 18 - 48 B2
65 PCIN>>17+A''*B 30 18 - 48 A2
66 A''*B 17 17 - 48 A2
67 PCIN>>17+A''*B 17 18 - 48 A2
68 A*B 30 17 - 48 --
69 PCIN>>17+A*B 0 18 - 48 --
70 A*B'' 17 18 - 48 B2
71 PCIN>>17+A''*B 30 18 - 48 A2
72 A''*B 17 17 - 48 A2
73 PCIN>>17+A''*B 17 18 - 48 A2
74 A'*B'' 30 17 - 48 A1 B2
75 PCIN>>17+A'*B'' 30 18 - 48 A1 B2
resampler: 1 DSP48E1
# formula A B C P registers
0 A*B 30 17 - 48 --
Paula's mixer: 8 DSP48E1
# formula A B C P registers
0 A*B 16 6 - 22 --
1 C+A*B 16 6 16 16 --
2 A*B 30 18 - 39 --
3 A*B 30 18 - 39 --
4 A*B 16 6 - 22 --
5 C+A*B 16 6 16 16 --
6 A*B 30 18 - 39 --
7 A*B 30 18 - 39 --
68000: 4 DSP48E1
# formula A B C P registers
0 A'*B' 16 16 - 32 A1 B1
1 A'*B' 17 15 - 48 A1 B1
2 A'*B' 17 17 - 48 A1 B1
3 PCIN>>17+A'*B' 17 15 - 48 A1 B1

The reverb takes the most: its coefficients are 32 bits so that a Time of 31.6 s still decays as the model's, and 32 x 25 bits is two DSP48E1. The block RAM is mostly lines: the reverb's 83 K words (54 tiles), the delay's 16 K (9), the flanger's 8 K (7), the chorus's 8 K (4.5); the 68000's memory is 32 tiles.

How the DSP was checked

effect board vs model
compressor -97.8 dB
chorus -65 .. -73 dB (its LFO phase aligned to the board's)
flanger -116.3 dB (LFO aligned; the model's output clipped to full scale, as the hardware's)
delay -90.7 dB
reverb -78.9 / -80.4 dB (left / right)
all five in a row -71.4 dB

An LFO only runs while its unit gets frames, so the board's phase depends on when it was switched on; the check searches the phase that fits (host/probe_phase.py). Where the model's output goes past full scale (the delay and the flanger at high feedback) the hardware saturates and the model does not: those samples are compared clipped.

The 68000

┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ the 68000                                                          68k.l8, m68k_alu.l8       │
│ ┌──────────────────────────────┐  ┌────────────────────────────────────────────────────────┐ │
│ │ M68State: the state struct   │  │ decode, combinational, from the instruction word       │ │
│ │ D0-D7, A0-A6, USP, SSP       │  │ bitslices M68W, M68Move -> m68_decode: the kind        │ │
│ │ SR (T S I2-I0 X N Z V C), PC │  │ m68_plan: source, destination, sizes, ALU operation    │ │
│ └──────────────────────────────┘  └────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ sm { }: one state machine, the steps every instruction is made of                        │ │
│ │                                                                                          │ │
│ │ rst0..3 -> fetch -> disp -> [src EA -> rd] -> [dst EA -> rd] -> exe -> [wr] -> done      │ │
│ │            ^  an interrupt above the mask: exc0..5, the autovector 24 + level            │ │
│ │                                                                                          │ │
│ │ the EA walk   ea_d16 ea_idx ea_absw ea_absl ea_imm: the extension words, then earet      │ │
│ │ the branches  bcc dbcc, jmp jsr rts rtr rte, link unlk, psh / pop                        │ │
│ │ the loops     shf: a shift bit a clock, dv: a divide bit a clock, mvm: MOVEM, mvp: MOVEP │ │
│ │ exceptions    TRAP CHK zero divide illegal privilege line A / F: exc0..5, six bytes      │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────┐  ┌──────────────────────────────────────────┐ │
│ │ m68_alu: one function, the result and the  │  │ the bus: one access at a time            │ │
│ │ flags; add sub, logic, BCD, MUL, bit ops   │  │ AS R/W UDS LDS A23-A0 D15-D0, DTACK (ack)│ │
│ └────────────────────────────────────────────┘  └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

The whole 68000 instruction set, the exceptions and the seven interrupt levels with autovectors. Not yet: the address error and the trace exception (Musashi, the reference, takes neither), and the pipe {} its design rules ask for -- the 68000's own two-word prefetch is where it goes.

The program

boot.s is everything the system runs besides ptplayer: the vectors, a start that hands the module to ptplayer, and an idle loop. Assembled with vasm together with ptplayer and the module (src/boot.s):

; boot.s -- the soundtracker system's whole program: the 68000's vectors, a start that hands the module to ptplayer,
; and an idle loop.  Everything else happens in ptplayer's level-6 interrupt (CIA-B timer A: mt_music at the
; song's tempo; timer B: audio DMA on, then the loop pointers).
;
; The memory is ONE block RAM whose initial contents are this image (`make image`): vectors at 0, this code,
; ptplayer with its variables (it writes them pc-relative, so it must sit in writable memory), the module, and the
; stack at the top.  Assembled with vasm (Motorola syntax), linked at 0, `-Fbin`.
;
;   0x000000  the block RAM (MEMSIZE bytes): vectors, code, ptplayer, the module; the stack grows down from the top
;   0xBFD000  CIA-B (registers on the even byte, a register every 0x100)
;   0xDFF000  the custom chips: INTENA/INTREQ/DMACON and Paula's four audio channels

MEMSIZE equ $20000

MINIMAL     equ 1       ; the player alone: no sound effects, no master volume
ENABLE_SAWRECT  equ 1

    org 0
vectors:
    dc.l    MEMSIZE         ; 0: the initial SSP
    dc.l    start           ; 1: the reset pc
    rept    22          ; 2..23: bus/address error, illegal, zero divide, CHK, TRAPV, privilege, trace,
    dc.l    fault           ;  line A/F, reserved, spurious (24) excluded
    endr
    dc.l    fault           ; 24: spurious interrupt
    rept    7           ; 25..31: the autovectors, levels 1..7; ptplayer sets level 6 ($78) itself
    dc.l    fault
    endr
    rept    32          ; 32..63: TRAP #0..15 and the reserved ones
    dc.l    fault
    endr

start:
    lea $dff000,a6
    move.w  #$7fff,$9a(a6)      ; INTENA: every source off
    move.w  #$7fff,$9c(a6)      ; INTREQ: every request cleared
    move.w  #$7fff,$96(a6)      ; DMACON: every channel off
    move.w  #$8200,$96(a6)      ; DMACON: DMAEN (the master enable); ptplayer turns the channels on
    sub.l   a0,a0           ; the vector base: 0 on a 68000
    moveq   #1,d0           ; PAL timer clock
    bsr _mt_install
    lea module,a0
    sub.l   a1,a1           ; the samples follow the patterns
    moveq   #0,d0           ; from song position 0
    bsr _mt_init
    st  _mt_Enable
    move.w  #$2000,sr       ; supervisor, interrupt mask 0
idle:   bra.s   idle

fault:  move.w  #$2700,sr       ; anything unexpected: stop with every interrupt masked
    move.w  #$000f,$dff096  ; silence
.h: bra.s   .h

    cnop    0,4
module:
    include "module.i"      ; `incbin` of the module (written by the Makefile)
    cnop    0,4

    ; LAST: ptplayer.asm ends with `end`, which stops the assembly
    include "ptplayer.asm"

ptplayer 6.4 -- "Protracker V2.3B Playroutine" by Frank Wille, public domain, built with MINIMAL=1: src/ptplayer.asm (4 000 lines; the whole source below).

src/ptplayer.asm
;**************************************************
;*    ----- Protracker V2.3B Playroutine -----    *
;**************************************************
;
; Version 6.4
; Written by Frank Wille in 2013, 2016-2024.
;
; I, the copyright holder of this work, hereby release it into the
; public domain. This applies worldwide.
;
; The default version (single section, local base register) should
; work with most assemblers. Tested are: Devpac, vasm, PhxAss,
; Barfly-Asm, SNMA, AsmOne, AsmPro.
;
; The small data model can be enabled by defining the symbol SDATA. In
; this case all functions expect a4 to be initialized with the small
; data base register. Interrupt functions restore a4 from _LinkerDB.
; Small data may work with vasm and PhxAss only.
;
; Exported functions and variables:
; (CUSTOM is the custom-chip register set base address $dff000.)
;
; _mt_install(a6=CUSTOM, a0=VectorBase, d0=PALflag.b) [OSCOMPAT=0]
;   Install a CIA-B interrupt for calling mt_music or mt_sfxonly.
;   The music module is replayed via mt_music when _mt_Enable is non-zero.
;   Otherwise the interrupt handler calls mt_sfxonly to play sound
;   effects only. VectorBase is 0 for 68000, otherwise set it to the CPU's
;   VBR register. A non-zero PALflag selects PAL-clock for the CIA timers
;   (NTSC otherwise).
;
; ok(d0) = _mt_install() [OSCOMPAT=1]
;   Register CIA-B interrupts with AmigaOS for calling mt_music or
;   mt_sfxonly. Allocate all Paula audio channels via audio.device.
;   The music module is replayed via mt_music when _mt_Enable is non-zero.
;   Otherwise the interrupt handler calls mt_sfxonly to play sound
;   effects only.
;   Returns true (1) on success, false (0) otherwise. You must not call
;   _mt_remove(), when _mt_install() failed!
;
; _mt_remove(a6=CUSTOM) [OSCOMPAT=0]
;   Remove the CIA-B music interrupt, restore the previous handler and
;   reset the CIA timer registers to their original values.
;
; _mt_remove() [OSCOMPAT=1]
;   Unregister the CIA-B interrupts handlers and deallocate all
;   audio channels.
;
; _mt_init(a6=CUSTOM, a0=TrackerModule, a1=Samples|NULL, d0=InitialSongPos.b)
;   Initialize a new module.
;   Reset speed to 6, tempo to 125 and start at the given song position.
;   Master volume is at 64 (maximum).
;   When a1 is NULL the samples are assumed to be stored after the patterns.
;
; _mt_end(a6=CUSTOM)
;   Stop playing current module and sound effects.
;
; _mt_soundfx(a6=CUSTOM, a0=SamplePointer,
;             d0=SampleLength.w, d1=SamplePeriod.w, d2=SampleVolume.w)
;   Request playing of an external sound effect on the most unused channel.
;   This function is for compatibility with the old API only.
;   You may prefer _mt_playfx instead. MINIMAL=0 only.
;
; channelStatus(d0) = _mt_playfx(a6=CUSTOM, a0=SfxStructurePointer)
;   Request playing of a prioritized external sound effect, either on a
;   fixed channel or on the most unused one.
;   Structure layout of SfxStructure:
;     void *sfx_ptr  (pointer to sample start in Chip RAM, even address)
;     WORD  sfx_len  (sample length in words)
;     WORD  sfx_per  (hardware replay period for sample)
;     WORD  sfx_vol  (volume 0..64, is unaffected by the song's master volume)
;     BYTE  sfx_cha  (0..3 selected replay channel, -1 selects best channel)
;     BYTE  sfx_pri  (priority, must be in the range 1..127)
;   When multiple samples are assigned to the same channel the lower
;   priority sample will be replaced. When priorities are the same, then
;   the older sample is replaced.
;   The chosen channel is blocked for music until the effect has
;   completely been replayed.
;   Returns a pointer to a channel-status structure when the sample
;   is scheduled for playing, and NULL when the request was ignored.
;   MINIMAL=0 only.
;
; _mt_loopfx(a6=CUSTOM, a0=SfxStructurePointer)
;   Request playing of a looped sound effect on a fixed channel, which
;   will be blocked for music until the effect is stopped (_mt_stopfx).
;   It uses the same sfx-structure as _mt_playfx, but the priority is
;   ignored. A looped sound effect has always highest priority and will
;   replace a previous effect on the same channel. No automatic channel
;   selection possible!
;   Also make sure the sample starts with a zero-word, which is used
;   for idling when the effect is stopped. This word is included in the
;   total length calculation, but excluded when actually playing the loop.
;   MINIMAL=0 only.
;
; _mt_stopfx(a6=CUSTOM, d0=Channel.b) [MINIMAL=0]
;   Immediately stop a currently playing sound effect on a channel (0..3)
;   and make it available for music, or other effects, again. This is the
;   only way to stop a looped sound effect (_mt_loopfx) besides stopping
;   replay completely (_mt_end).
;
; _mt_musicmask(a6=CUSTOM, d0=ChannelMask.b) [MINIMAL=0]
;   Bits set in the ChannelMask define which specific channels are reserved
;   for music only. Set bit 0 for channel 0, ..., bit 3 for channel 3.
;   When calling _mt_soundfx or _mt_playfx with automatic channel selection
;   (sfx_cha=-1) then these masked channels will never be picked.
;   The mask defaults to 0.
;
; _mt_mastervol(a6=CUSTOM, d0=MasterVolume.w) [MINIMAL=0]
;   Set a master volume from 0 to 64 for all music channels.
;   Note that the master volume does not affect the volume of external
;   sound effects (which is desired).
;
; _mt_samplevol(d0=SampleNumber.w, d1=Volume.b) [MINIMAL=0]
;   Redefine a sample's volume. May also be done while the song is playing.
;   Warning: Does not check arguments for valid range! You must have done
;   _mt_init before calling this function!
;   The new volume is persistent. Even when the song is restarted.
;
; _mt_channelmask(a6=CUSTOM, d0=ChannelMask.b) [MINIMAL=0]
;  Bits cleared in the ChannelMask define which specific channels are muted
;  for music replay. Clear bit 0 for channel 0, ..., bit 3 for channel 3.
;  Additionally a cleared bit prevents any access to the sample pointers
;  of this channel.
;  The mask defaults to all channels unmuted (bits set) and is reset to
;  this state on _mt_init and _mt_end.
;
; _mt_music(a6=CUSTOM)
;   The replayer routine. Can be called from your own VBlank interrupt
;   handler when VBLANK_MUSIC is set. Is otherwise called automatically
;   by Timer-A interrupts after _mt_install.
;
; _mt_dmaon() [NO_TIMERS=1]
;   MUST be called ca. 550 ticks after calling _mt_music. Enables Audio
;   DMA to play a new note.
;
; _mt_setrep() [NO_TIMERS=1]
;   MUST be called ca. 550 ticks after calling _mt_dmaon. Sets the
;   repetition pointers and lengths for looped samples.
;
; Byte Variables:
;
; _mt_Enable [VBLANK_MUSIC=0]
;   Set this byte to non-zero to play music, zero to pause playing.
;   Note that you can still play sound effects, while music is stopped.
;   It is set to 0 by _mt_install.
;
; _mt_E8Trigger
;   This byte reflects the value of the last E8 command.
;   It is reset to 0 after _mt_init.
;
; _mt_MusicChannels [MINIMAL=0]
;   This byte defines the number of channels which should be dedicated
;   for playing music. So sound effects will never use more
;   than 4 - _mt_MusicChannels channels at once. Defaults to 0.
;

; Set this if you want to build a version which uses AmigaOS to
; register interrupts and reserve audio channels.
    ifnd    OSCOMPAT
OSCOMPAT    equ 0
    endc

; Optionally you can build a minimal version, which includes only
; the basic player. No sound effects insertion, no master volume, no
; sample volume, etc. Define the symbol MINIMAL=1 for it.
    ifnd    MINIMAL
MINIMAL     equ 0
    endc

; You may disable sawtooth and rectangle vibratos/tremolos here, which
; will be replaced by sine-waves. They are rarely used and disabling
; them will free a lot of memory for the tables.
    ifnd    ENABLE_SAWRECT
ENABLE_SAWRECT  equ 1
    endc

; Set this if you can guarantee that the word at $0 is cleared, and if
; you want to use it for idle-looping of samples.
    ifnd    NULL_IS_CLEARED
NULL_IS_CLEARED equ 0
    endc

; Setting NO_TIMERS disables the use of both CIA-B timers completely, which
; means that the player doesn't depend on level-6 EXTER interrupts anymore.
; NO_TIMERS=1 automatically includes VBLANK_MUSIC=1 (see below).
; With NO_TIMERS set, your main program has to call all music subroutines
; itself. The typical procedure in a VERTB-based main loop would be:
; 1. call _mt_music
; 2. wait at least 550 ticks, then call _mt_dmaon
; 3. wait at least 550 ticks again, then call _mt_setrep
; The last two steps could for example be performed by Copper interrupts.
    ifnd    NO_TIMERS
NO_TIMERS   equ 0
    else
    ifne    NO_TIMERS
    ifd VBLANK_MUSIC
    ifeq    VBLANK_MUSIC
    fail    "NO_TIMERS=1 requires VBLANK_MUSIC=1"
    endc
    endc    ; VBLANK_MUSIC
    endc    ; NO_TIMERS=1
    endc    ; NO_TIMERS

; This disables only initialization of CIA Timer-A interrupts for music
; replay, which means you cannot set the tempo with the F-command
; anymore and you have to call _mt_music yourself out of your own
; VBlank interrupt handler. Also sound effects will no longer work,
; when no music is playing (_mt_Enable=0).
    ifnd    VBLANK_MUSIC
VBLANK_MUSIC    equ NO_TIMERS
    endc

; Delay in CIA-ticks, which guarantees that at least one Audio-DMA
; took place, even with the lowest periods.
; 496 should be the correct value. But there are some A1200 which
; need at least 550.
DMADELAY    equ 576     ; was 496


; exec.library (OSCOMPAT)
_LVOAllocSignal equ -330
_LVOFreeSignal  equ -336
_LVOOpenDevice  equ -444
_LVOCloseDevice equ -450
_LVOOpenResource equ    -498

; cia.resource (OSCOMPAT)
_LVOAddICRVector equ    -6
_LVORemICRVector equ    -12
_LVOAbleICR equ -18
CIAICRB_TA  equ 0
CIAICRB_TB  equ 1


; Custom chip registers
CUSTOM      equ $dff000
INTREQR     equ $01e
INTENAR     equ $01c
DMACON      equ $096
INTENA      equ $09a
INTREQ      equ $09c
AUD0LC      equ $0a0
AUD0LEN     equ $0a4
AUD0VOL     equ $0a8
AUD1LC      equ $0b0
AUD1LEN     equ $0b4
AUD1VOL     equ $0b8
AUD2LC      equ $0c0
AUD2LEN     equ $0c4
AUD2VOL     equ $0c8
AUD3LC      equ $0d0
AUD3LEN     equ $0d4
AUD3VOL     equ $0d8

; Audio channel registers
AUDLC       equ 0
AUDLEN      equ 4
AUDPER      equ 6
AUDVOL      equ 8

; CIA registers
CIAA        equ $bfe001
CIAB        equ $bfd000
CIAPRA      equ $000
CIATALO     equ $400
CIATAHI     equ $500
CIATBLO     equ $600
CIATBHI     equ $700
CIAICR      equ $d00
CIACRA      equ $e00
CIACRB      equ $f00


; Sound effects structure, passed into _mt_playfx
        rsreset
sfx_ptr     rs.l    1
sfx_len     rs.w    1
sfx_per     rs.w    1
sfx_vol     rs.w    1
sfx_cha     rs.b    1
sfx_pri     rs.b    1
sfx_sizeof  rs.b    0


; Channel Status
        rsreset
n_note      rs.w    1
n_cmd       rs.b    1
n_cmdlo     rs.b    1
n_index     rs.b    1
n_sfxpri    rs.b    1
n_reserved1 rs.b    2
n_start     rs.l    1
n_loopstart rs.l    1
n_length    rs.w    1
n_replen    rs.w    1
n_period    rs.w    1
n_volume    rs.w    1
n_pertab    rs.l    1
n_dmabit    rs.w    1
n_noteoff   rs.w    1
n_toneportspeed rs.w    1
n_wantedperiod  rs.w    1
n_pattpos   rs.w    1
n_funk      rs.w    1
n_wavestart rs.l    1
n_reallength    rs.w    1
n_intbit    rs.w    1
n_sfxptr    rs.l    1
n_sfxlen    rs.w    1
n_sfxper    rs.w    1
n_sfxvol    rs.w    1
n_looped    rs.b    1
n_minusft   rs.b    1
n_vibratoamp    rs.b    1
n_vibratospd    rs.b    1
n_vibratopos    rs.b    1
n_vibratoctrl   rs.b    1
n_tremoloamp    rs.b    1
n_tremolospd    rs.b    1
n_tremolopos    rs.b    1
n_tremoloctrl   rs.b    1
n_gliss     rs.b    1
n_sampleoffset  rs.b    1
n_loopcount rs.b    1
n_funkoffset    rs.b    1
n_retrigcount   rs.b    1
    ifeq    MINIMAL
n_freecnt   rs.b    1
n_musiconly rs.b    1
n_enable    rs.b    1
    else
n_reserved2 rs.b    3
    endc
n_sizeof    rs.b    0


DISABLE macro
    ifeq    NO_TIMERS
    move.w  #$4000,INTENA(a6)
    endc
    endm

ENABLE  macro
    ifeq    NO_TIMERS
    move.w  #$c000,INTENA(a6)
    endc
    endm


    ifd SDATA
    xref    _LinkerDB       ; small data base from linker
    near    a4
    code
    endc



    ifne    OSCOMPAT
;---------------------------------------------------------------------------
    xdef    _mt_install
_mt_install:
; Register a CIA-B interrupt with AmigaOS for calling mt_music.
; Reserve all audio channels for our use.
; -> d0.l = status (1 for success, 0 for error)

    move.l  a6,-(sp)

    ifnd    SDATA
    move.l  a4,-(sp)
    lea mt_data(pc),a4
    endc

    ; open audio.device and allocate all four audio channels
    move.l  4.w,a6
    moveq   #-1,d0
    jsr _LVOAllocSignal(a6)
    lea mt_ioport+20(pc),a0
    move.b  d0,-5(a0)       ; MP_SIGBIT
    bmi .fail
    move.l  a0,8(a0)        ; NewList MP_MSGLIST
    addq.l  #4,a0
    clr.l   (a0)
    move.l  a0,-(a0)
    move.l  276(a6),-(a0)       ; MP_SIGTASK = ThisTask
    lea mt_audio_name(pc),a0
    lea mt_ioaudio(pc),a1
    moveq   #0,d0
    moveq   #0,d1
    jsr _LVOOpenDevice(a6)
    tst.b   d0
    bne .no_device

    ifeq    NO_TIMERS
    ; attempt to allocate CIA-B timers A and B
    lea mt_ciab_name(pc),a1
    jsr _LVOOpenResource(a6)
    tst.l   d0
    beq .no_rsrc

    move.l  d0,a6

    clr.b   mt_Enable(a4)
    lea TB_toggle(pc),a0
    clr.b   (a0)

    ifeq    VBLANK_MUSIC
    moveq   #CIAICRB_TA,d0
    lea mt_extern_timer_a_is(pc),a1
    jsr _LVOAddICRVector(a6)
    tst.l   d0
    bne .no_timer_a
    endc    ; !VBLANK_MUSIC

    moveq   #CIAICRB_TB,d0
    lea mt_extern_timer_b_is(pc),a1
    jsr _LVOAddICRVector(a6)
    tst.l   d0
    bne .no_timer_b

    ; TimerA and TimerB interrupt disable
    ifne    VBLANK_MUSIC
    moveq   #$2,d0          ; TimerB only for VBLANK_MUSIC
    else
    moveq   #$3,d0
    endc
    jsr _LVOAbleICR(a6)

    lea CIAB,a0

    ; Stop Timer B
    moveq   #0,d0
    move.b  d0,CIACRB(a0)

    ifeq    VBLANK_MUSIC
    ; Stop Timer A
    move.b  d0,CIACRA(a0)

    ; determine if 02 clock for timers is based on PAL or NTSC
    move.l  4.w,a1
    cmp.b   #50,531(a1)     ; PowerSupplyFrequency
    beq .1
    move.l  #1789773,d0             ; NTSC
    bra .2
.1: move.l  #1773447,d0             ; PAL
.2: move.l  d0,mt_timerval(a4)

    ; load TimerA in continuous mode for the default tempo of 125
    divu    #125,d0
    move.b  d0,CIATALO(a0)
    lsr.w   #8,d0
    move.b  d0,CIATAHI(a0)
    move.b  #$11,CIACRA(a0)     ; load timer, start continuous
    endc    ; !VBLANK_MUSIC

    ; load TimerB with DMADELAY ticks for setting DMA and repeat
    move.b  #DMADELAY&255,CIATBLO(a0)
    move.b  #DMADELAY>>8,CIATBHI(a0)

    ; TimerA and TimerB interrupt enable
    ifne    VBLANK_MUSIC
    move.w  #$82,d0         ; TimerB only for VBLANK_MUSIC
    else
    move.w  #$83,d0
    endc
    jsr _LVOAbleICR(a6)

    endc    ; !NO_TIMERS

    ; reset the player
    pea .popout(pc)
    ifnd    SDATA
    move.l  a4,-(sp)
    endc
    lea CUSTOM,a6
    bra mt_reset

    ifeq    NO_TIMERS
.no_timer_b:
    ifeq    VBLANK_MUSIC
    moveq   #CIAICRB_TA,d0
    lea mt_extern_timer_a_is(pc),a1
    jsr _LVORemICRVector(a6)
.no_timer_a:
    endc    ; !VBLANK_MUSIC
    endc    ; !NO_TIMERS

    move.l  4.w,a6
.no_rsrc:
    lea mt_ioaudio(pc),a1
    jsr _LVOCloseDevice(a6)
.no_device:
    moveq   #0,d0
    move.b  mt_ioport+15(pc),d0
    jsr _LVOFreeSignal(a6)
.fail:
    moveq   #0,d0           ; Indicate failure
    bra .out
.popout:
    moveq   #1,d0           ; Indicate success
.out:
    ifnd    SDATA
    move.l  (sp)+,a4
    endc
    move.l  (sp)+,a6
    rts


mt_ioport:
    dc.l    0,0
    dc.b    4,0         ; MT_MSGPORT
    dc.l    0
    dc.b    0,-1            ; PA_SIGNAL
    dc.l    0
.head:  dc.l    .tail
.tail:  dc.l    0
    dc.l    .head

mt_ioaudio:
    dc.l    0,0
    dc.b    7,127           ; NT_REPLYMSG, ADALLOC_MAXPREC
    dc.l    0,mt_ioport
    dc.w    .end-mt_ioaudio
    dc.l    0,0
    dc.w    $20         ; ADCMD_ALLOCATE
    dc.b    $40,0           ; ADIOF_NOWAIT
    dc.w    0           ; ioa_AllocKey
    dc.l    mt_ch_alloc     ; ioa_Data
    dc.l    1           ; ioa_Length
    dc.w    0,0,0
    ds.b    20          ; ioa_WriteMsg
.end:

    ifeq    NO_TIMERS
    ifeq    VBLANK_MUSIC
mt_extern_timer_a_is:
    dc.l    0,0
    dc.b    2           ; NT_INTERRUPT
    dc.b    0           ; priority
    dc.l    mt_timer_int_name
    dc.l    0
    dc.l    mt_cia_timer_a_code
    endc    ; !VBLANK_MUSIC

mt_extern_timer_b_is:
    dc.l    0,0
    dc.b    2           ; NT_INTERRUPT
    dc.b    0           ; priority
    dc.l    mt_timer_int_name
    dc.l    0
    dc.l    mt_cia_timer_b_code

mt_ciab_name:
    dc.b    "ciab.resource",0
mt_timer_int_name:
    dc.b    "ptplayer timer",0
    endc    ; !NO_TIMERS

mt_audio_name:
    dc.b    "audio.device",0
mt_ch_alloc:
    dc.b    %1111           ; allocate all four channels
    even


;---------------------------------------------------------------------------
    xdef    _mt_remove
_mt_remove:
; Unregister our CIA-B interrupt with AmigaOS. Free all audio channels.

    move.l  a6,-(sp)

    ifeq    NO_TIMERS
    move.l  4.w,a6
    lea mt_ciab_name(pc),a1
    jsr _LVOOpenResource(a6)
    tst.l   d0
    beq .nociares

    move.l  d0,a6

    ; TimerA and TimerB interrupt disable
    ifne    VBLANK_MUSIC
    moveq   #$02,d0         ; TimerB only for VBLANK_MUSIC
    else
    moveq   #$03,d0
    endc
    jsr _LVOAbleICR(a6)

    moveq   #CIAICRB_TB,d0
    lea mt_extern_timer_b_is(pc),a1
    jsr _LVORemICRVector(a6)

    ifeq    VBLANK_MUSIC
    moveq   #CIAICRB_TA,d0
    lea mt_extern_timer_a_is(pc),a1
    jsr _LVORemICRVector(a6)
    endc    ; !VBLANK_MUSIC
.nociares:
    endc    ; !NO_TIMERS

    ; close audio.device, free channels
    move.l  4.w,a6
    lea mt_ioaudio(pc),a1
    jsr _LVOCloseDevice(a6)
    moveq   #0,d0
    move.b  mt_ioport+15(pc),d0
    jsr _LVOFreeSignal(a6)

    move.l  (sp)+,a6
    rts


    ifeq    NO_TIMERS
    ifeq    VBLANK_MUSIC
;---------------------------------------------------------------------------
mt_cia_timer_a_code:
; TimerA interrupt calls mt_music at a selectable tempo (Fxx command),
; which defaults to 50 times per second.

    movem.l d2-d7/a2-a6,-(sp)
    lea CUSTOM,a6
    ifd SDATA
    lea _LinkerDB,a4
    else
    lea mt_data(pc),a4
    endc

    ; do music when enabled
    tst.b   mt_Enable(a4)
    ifeq    MINIMAL
    beq .2
    else
    beq .1
    endc

    bsr mt_music        ; music with sfx inserted
.1: movem.l (sp)+,d2-d7/a2-a6
    rts

    ifeq    MINIMAL
.2: bsr mt_sfxonly      ; no music, only sfx
    movem.l (sp)+,d2-d7/a2-a6
    rts
    endc
    endc    ; !VBLANK_MUSIC


;---------------------------------------------------------------------------
mt_cia_timer_b_code:
; Handle one-shot TimerB interrupt.
; TB_toggle-technique suggested by Ross/EAB.

    lea TB_toggle(pc),a0
    not.b   (a0)
    lea CUSTOM+INTREQ,a0
    beq mt_TimerBsetrep

    ; restart timer for repeat, enable audio DMA after DMADELAY ticks
    move.b  #$19,CIAB+CIACRB
    move.w  mt_dmaon(pc),DMACON-INTREQ(a0)
    rts

TB_toggle:
    dc.b    0
    even


mt_TimerBsetrep:
; Oneshot TimerB interrupt to set repeat samples after another DMADELAY ticks.

    move.l  a4,-(sp)

    ifd SDATA
    lea _LinkerDB,a4
    else
    lea mt_data(pc),a4
    endc

    ; clear possible audio interrupt flags
    moveq   #0,d0
    or.b    mt_dmaon+1(pc),d0
    lsl.w   #7,d0
    move.w  d0,(a0)

    ; set repeat sample pointers and lengths
    ifne    MINIMAL
    move.l  mt_chan1+n_loopstart(a4),AUD0LC-INTREQ(a0)
    move.w  mt_chan1+n_replen(a4),AUD0LEN-INTREQ(a0)
    move.l  mt_chan2+n_loopstart(a4),AUD1LC-INTREQ(a0)
    move.w  mt_chan2+n_replen(a4),AUD1LEN-INTREQ(a0)
    move.l  mt_chan3+n_loopstart(a4),AUD2LC-INTREQ(a0)
    move.w  mt_chan3+n_replen(a4),AUD2LEN-INTREQ(a0)
    move.l  mt_chan4+n_loopstart(a4),AUD3LC-INTREQ(a0)
    move.w  mt_chan4+n_replen(a4),AUD3LEN-INTREQ(a0)

    else    ; !MINIMAL
    tst.b   mt_chan1+n_enable(a4)
    beq .1
    move.l  mt_chan1+n_loopstart(a4),AUD0LC-INTREQ(a0)
    move.w  mt_chan1+n_replen(a4),AUD0LEN-INTREQ(a0)
.1: tst.b   mt_chan2+n_enable(a4)
    beq .2
    move.l  mt_chan2+n_loopstart(a4),AUD1LC-INTREQ(a0)
    move.w  mt_chan2+n_replen(a4),AUD1LEN-INTREQ(a0)
.2: tst.b   mt_chan3+n_enable(a4)
    beq .3
    move.l  mt_chan3+n_loopstart(a4),AUD2LC-INTREQ(a0)
    move.w  mt_chan3+n_replen(a4),AUD2LEN-INTREQ(a0)
.3: tst.b   mt_chan4+n_enable(a4)
    beq .4
    move.l  mt_chan4+n_loopstart(a4),AUD3LC-INTREQ(a0)
    move.w  mt_chan4+n_replen(a4),AUD3LEN-INTREQ(a0)
.4:
    endc

    move.l  (sp)+,a4
    rts
    endc    ; !NO_TIMERS


    else    !OSCOMPAT
;---------------------------------------------------------------------------
    xdef    _mt_install
    xdef    _mt_install_cia
_mt_install:
_mt_install_cia:
; Install a CIA-B interrupt for calling mt_music.
; a6 = CUSTOM
; a0 = VectorBase
; d0 = PALflag.b (0 is NTSC)

    ifnd    SDATA
    move.l  a4,-(sp)
    lea mt_data(pc),a4
    endc

    clr.b   mt_Enable(a4)

    ifeq    NO_TIMERS
    ; remember level 6 vector and interrupt enable
    lea $78(a0),a0
    move.l  a0,mt_Lev6Int(a4)
    move.w  #$2000,d1
    and.w   INTENAR(a6),d1
    or.w    #$8000,d1
    move.w  d1,mt_Lev6Ena(a4)

    ; disable level 6 EXTER interrupts, set player interrupt vector
    move.w  #$2000,INTENA(a6)
    move.l  (a0),mt_oldLev6(a4)
    lea mt_TimerAInt(pc),a1
    move.l  a1,(a0)

    ; reset TimerB toggle
    lea TB_toggle(pc),a0
    clr.b   (a0)

    ; disable CIA-B interrupts, stop and save all timers
    lea CIAB,a0
    moveq   #0,d1
    move.b  #$7f,CIAICR(a0)
    move.b  d1,CIACRA(a0)
    move.b  d1,CIACRB(a0)
    lea mt_oldtimers(a4),a1
    move.b  CIATALO(a0),(a1)+
    move.b  CIATAHI(a0),(a1)+
    move.b  CIATBLO(a0),(a1)+
    move.b  CIATBHI(a0),(a1)

    ifeq    VBLANK_MUSIC
    ; determine if 02 clock for timers is based on PAL or NTSC
    tst.b   d0
    bne .1
    move.l  #1789773,d0     ; NTSC
    bra .2
.1: move.l  #1773447,d0     ; PAL
.2: move.l  d0,mt_timerval(a4)

    ; load TimerA in continuous mode for the default tempo of 125
    divu    #125,d0
    move.b  d0,CIATALO(a0)
    lsr.w   #8,d0
    move.b  d0,CIATAHI(a0)
    move.b  #$11,CIACRA(a0)     ; load timer, start continuous
    endc    ; !VBLANK_MUSIC

    ; load TimerB with DMADELAY ticks for setting DMA and repeat
    move.b  #DMADELAY&255,CIATBLO(a0)
    move.b  #DMADELAY>>8,CIATBHI(a0)

    ; Ack. pending interrupts, TimerA and TimerB interrupt enable
    tst.b   CIAICR(a0)
    move.w  #$2000,INTREQ(a6)
    ifne    VBLANK_MUSIC
    move.b  #$82,CIAICR(a0)     ; TimerB only for VBLANK_MUSIC
    else
    move.b  #$83,CIAICR(a0)
    endc

    ; enable level 6 interrupts
    move.w  #$a000,INTENA(a6)
    endc    ; !NO_TIMERS

    bra mt_reset


;---------------------------------------------------------------------------
    xdef    _mt_remove_cia
    xdef    _mt_remove
_mt_remove_cia:
_mt_remove:
; Remove CIA-B music interrupt and restore the old vector.
; a6 = CUSTOM

    ifeq    NO_TIMERS
    ifnd    SDATA
    move.l  a4,-(sp)
    lea mt_data(pc),a4
    endc

    ; disable level 6 and CIA-B interrupts
    lea CIAB,a0
    move.w  #$2000,d0
    move.b  #$7f,CIAICR(a0)
    move.w  d0,INTENA(a6)
    tst.b   CIAICR(a0)
    move.w  d0,INTREQ(a6)

    ; restore old timer values
    lea mt_oldtimers(a4),a1
    move.b  (a1)+,CIATALO(a0)
    move.b  (a1)+,CIATAHI(a0)
    move.b  (a1)+,CIATBLO(a0)
    move.b  (a1),CIATBHI(a0)
    move.b  #$10,CIACRA(a0)
    move.b  #$10,CIACRB(a0)

    ; restore original level 6 interrupt vector
    move.l  mt_Lev6Int(a4),a1
    move.l  mt_oldLev6(a4),(a1)

    ; reenable CIA-B ALRM interrupt, which was set by AmigaOS
    move.b  #$84,CIAICR(a0)

    ; reenable previous level 6 interrupt
    move.w  mt_Lev6Ena(a4),INTENA(a6)

    ifnd    SDATA
    move.l  (sp)+,a4
    endc
    endc    ; !NO_TIMERS
    rts


    ifeq    NO_TIMERS
;---------------------------------------------------------------------------
mt_TimerAInt:
; TimerA interrupt calls mt_music at a selectable tempo (Fxx command),
; which defaults to 50 times per second.

    ; check for TB interrupt and clear CIAB interrupt flags
    btst    #1,CIAB+CIAICR
    bne mt_TimerBInt

    ifne    VBLANK_MUSIC
    move.w  #$2000,CUSTOM+INTREQ    ; ack interrupt and leave
    nop
    rte

    else
    ; Now it should be a TA interrupt.
    ; Other level 6 interrupt sources have to be handled elsewhere.
    movem.l d0-d7/a0-a6,-(sp)
    lea CUSTOM,a6
    ifd SDATA
    lea _LinkerDB,a4
    else
    lea mt_data(pc),a4
    endc

    ; clear EXTER interrupt flag
    move.w  #$2000,INTREQ(a6)

    ; do music when enabled
    tst.b   mt_Enable(a4)
    ifeq    MINIMAL
    beq .2
    else
    beq .1
    endc

    bsr mt_music        ; music with sfx inserted
.1: movem.l (sp)+,d0-d7/a0-a6
    nop
    rte

    ifeq    MINIMAL
.2: bsr mt_sfxonly      ; no music, only sfx
    movem.l (sp)+,d0-d7/a0-a6
    nop
    rte
    endc
    endc    ; !VBLANK_MUSIC


;---------------------------------------------------------------------------
mt_TimerBInt:
; Handle one-shot TimerB interrupt.
; TB_toggle-technique suggested by Ross/EAB.

    move.l  a0,-(sp)
    lea TB_toggle(pc),a0
    not.b   (a0)
    lea CUSTOM+INTREQ,a0
    beq mt_TimerBsetrep

    ; restart timer for repeat, enable audio DMA after DMADELAY ticks
    move.w  #$2000,(a0)     ; clear EXTER interrupt flag
    move.b  #$19,CIAB+CIACRB
    move.w  mt_dmaon(pc),DMACON-INTREQ(a0)

    move.l  (sp)+,a0
    nop
    rte


mt_TimerBsetrep:
; Oneshot TimerB interrupt to set repeat samples after another DMADELAY ticks.

    ; clear EXTER and possible audio interrupt flags
    move.l  a4,-(sp)
    move.l  d0,a4
    moveq   #$2000>>7,d0        ; EXTER-flag
    or.b    mt_dmaon+1(pc),d0
    lsl.w   #7,d0
    move.w  d0,(a0)
    move.l  a4,d0

    ifd SDATA
    lea _LinkerDB,a4
    else
    lea mt_data(pc),a4
    endc

    ; set repeat sample pointers and lengths
    ifne    MINIMAL
    move.l  mt_chan1+n_loopstart(a4),AUD0LC-INTREQ(a0)
    move.w  mt_chan1+n_replen(a4),AUD0LEN-INTREQ(a0)
    move.l  mt_chan2+n_loopstart(a4),AUD1LC-INTREQ(a0)
    move.w  mt_chan2+n_replen(a4),AUD1LEN-INTREQ(a0)
    move.l  mt_chan3+n_loopstart(a4),AUD2LC-INTREQ(a0)
    move.w  mt_chan3+n_replen(a4),AUD2LEN-INTREQ(a0)
    move.l  mt_chan4+n_loopstart(a4),AUD3LC-INTREQ(a0)
    move.w  mt_chan4+n_replen(a4),AUD3LEN-INTREQ(a0)

    else    ; !MINIMAL
    tst.b   mt_chan1+n_enable(a4)
    beq .1
    move.l  mt_chan1+n_loopstart(a4),AUD0LC-INTREQ(a0)
    move.w  mt_chan1+n_replen(a4),AUD0LEN-INTREQ(a0)
.1: tst.b   mt_chan2+n_enable(a4)
    beq .2
    move.l  mt_chan2+n_loopstart(a4),AUD1LC-INTREQ(a0)
    move.w  mt_chan2+n_replen(a4),AUD1LEN-INTREQ(a0)
.2: tst.b   mt_chan3+n_enable(a4)
    beq .3
    move.l  mt_chan3+n_loopstart(a4),AUD2LC-INTREQ(a0)
    move.w  mt_chan3+n_replen(a4),AUD2LEN-INTREQ(a0)
.3: tst.b   mt_chan4+n_enable(a4)
    beq .4
    move.l  mt_chan4+n_loopstart(a4),AUD3LC-INTREQ(a0)
    move.w  mt_chan4+n_replen(a4),AUD3LEN-INTREQ(a0)
.4:
    endc

    move.l  (sp)+,a4
    move.l  (sp)+,a0
    nop
    rte

TB_toggle:
    dc.b    0
    even
    endc    ; !NO_TIMERS
    endc    ; !OSCOMPAT


    ifne    NO_TIMERS
;---------------------------------------------------------------------------
    xdef    _mt_dmaon
_mt_dmaon:
; Enable audio DMA after some delay.
; Called by main program, for example out of a copper-interrupt.

    move.w  mt_dmaon(pc),CUSTOM+DMACON
    rts


    xdef    _mt_setrep
_mt_setrep:
; Set repeat sample pointers and lengths.
; Called by main program, for example out of a copper-interrupt.

    movem.l a0/a4,-(sp)

    ; reset audio interrupt flags
    lea CUSTOM+INTREQ,a0
    move.l  d0,a4
    move.w  mt_dmaon(pc),d0
    lsl.w   #7,d0
    move.w  d0,(a0)
    move.l  a4,d0

    ifd SDATA
    lea _LinkerDB,a4
    else
    lea mt_data(pc),a4
    endc

    ; set repeat sample pointers and lengths
    ifne    MINIMAL
    move.l  mt_chan1+n_loopstart(a4),AUD0LC-INTREQ(a0)
    move.w  mt_chan1+n_replen(a4),AUD0LEN-INTREQ(a0)
    move.l  mt_chan2+n_loopstart(a4),AUD1LC-INTREQ(a0)
    move.w  mt_chan2+n_replen(a4),AUD1LEN-INTREQ(a0)
    move.l  mt_chan3+n_loopstart(a4),AUD2LC-INTREQ(a0)
    move.w  mt_chan3+n_replen(a4),AUD2LEN-INTREQ(a0)
    move.l  mt_chan4+n_loopstart(a4),AUD3LC-INTREQ(a0)
    move.w  mt_chan4+n_replen(a4),AUD3LEN-INTREQ(a0)

    else    ; !MINIMAL
    tst.b   mt_chan1+n_enable(a4)
    beq .1
    move.l  mt_chan1+n_loopstart(a4),AUD0LC-INTREQ(a0)
    move.w  mt_chan1+n_replen(a4),AUD0LEN-INTREQ(a0)
.1: tst.b   mt_chan2+n_enable(a4)
    beq .2
    move.l  mt_chan2+n_loopstart(a4),AUD1LC-INTREQ(a0)
    move.w  mt_chan2+n_replen(a4),AUD1LEN-INTREQ(a0)
.2: tst.b   mt_chan3+n_enable(a4)
    beq .3
    move.l  mt_chan3+n_loopstart(a4),AUD2LC-INTREQ(a0)
    move.w  mt_chan3+n_replen(a4),AUD2LEN-INTREQ(a0)
.3: tst.b   mt_chan4+n_enable(a4)
    beq .4
    move.l  mt_chan4+n_loopstart(a4),AUD3LC-INTREQ(a0)
    move.w  mt_chan4+n_replen(a4),AUD3LEN-INTREQ(a0)
.4:
    endc

    movem.l (sp)+,a0/a4
    rts
    endc    ; NO_TIMERS

mt_dmaon:
    dc.w    $8000


;---------------------------------------------------------------------------
    xdef    _mt_init
_mt_init:
; Initialize new module.
; Reset speed to 6, tempo to 125 and start at given song position.
; Master volume is at 64 (maximum).
; a6 = CUSTOM
; a0 = module pointer
; a1 = sample pointer (NULL means samples are stored within the module)
; d0 = initial song position

    ifnd    SDATA
    move.l  a4,-(sp)
    lea mt_data(pc),a4
    endc

    move.l  a0,mt_mod(a4)
    movem.l d2/a2,-(sp)

    ; set initial song position
    cmp.b   950(a0),d0
    blo .1
    moveq   #0,d0
.1: move.b  d0,mt_SongPos(a4)

    move.l  a1,d0       ; sample data location is given?
    bne .4

    ; get number of highest pattern
    lea 952(a0),a1  ; song arrangement list
    moveq   #127,d0
    moveq   #0,d2
.2: move.b  (a1)+,d1
    cmp.b   d2,d1
    bls .3
    move.b  d1,d2
.3: dbf d0,.2
    addq.b  #1,d2       ; number of patterns

    ; now we can calculate the base address of the sample data
    moveq   #10,d0
    asl.l   d0,d2
    lea (a0,d2.l),a1
    add.w   #1084,a1

    ; save start address of each sample and do some fixes for broken mods
.4: lea mt_SampleStarts(a4),a2
    moveq   #1,d2
    moveq   #31-1,d0
.5: move.l  a1,(a2)+
    moveq   #0,d1
    move.w  42(a0),d1
    cmp.w   d2,d1       ; length 0 and 1 define unused samples
    bls .6
    add.l   d1,d1
    add.l   d1,a1
    bra .7
.6: clr.w   42(a0)      ; length 1 means zero -> no sample
.7: lea 30(a0),a0
    dbf d0,.5

    movem.l (sp)+,d2/a2

    ifeq    VBLANK_MUSIC
    ; reset CIA timer A to default (125)
    move.l  mt_timerval(a4),d0
    divu    #125,d0
    move.b  d0,CIAB+CIATALO
    lsr.w   #8,d0
    move.b  d0,CIAB+CIATAHI
    endc

mt_reset:
; a4 must be initialized with base register

    ; reset speed and counters
    move.b  #6,mt_Speed(a4)
    clr.b   mt_Counter(a4)
    clr.w   mt_PatternPos(a4)
    clr.b   mt_PattDelTime(a4)
    clr.b   mt_PattDelTime2(a4)
    clr.w   mt_PBreakPos(a4)
    clr.b   mt_PBreakFlag(a4)
    clr.b   mt_PosJumpFlag(a4)

    ; disable the filter
    or.b    #2,CIAA+CIAPRA

    ifeq    MINIMAL
    ; set master volume to 64
    lea MasterVolTab64(pc),a0
    move.l  a0,mt_MasterVolTab(a4)
    endc

    ; set channel index
    clr.b   mt_chan1+n_index(a4)
    move.b  #1,mt_chan2+n_index(a4)
    move.b  #2,mt_chan3+n_index(a4)
    move.b  #3,mt_chan4+n_index(a4)

    ; initialize channel DMA and interrupt bits
    move.w  #$0001,mt_chan1+n_dmabit(a4)
    move.w  #$0002,mt_chan2+n_dmabit(a4)
    move.w  #$0004,mt_chan3+n_dmabit(a4)
    move.w  #$0008,mt_chan4+n_dmabit(a4)
    move.w  #$0080,mt_chan1+n_intbit(a4)
    move.w  #$0100,mt_chan2+n_intbit(a4)
    move.w  #$0200,mt_chan3+n_intbit(a4)
    move.w  #$0400,mt_chan4+n_intbit(a4)

    clr.b   mt_E8Trigger(a4)
    ifeq    MINIMAL
    clr.b   mt_SongEnd(a4)
    clr.b   mt_SilCntValid(a4)
    endc

    ifnd    SDATA
    move.l  (sp)+,a4
    endc


;---------------------------------------------------------------------------
    xdef    _mt_end
_mt_end:
; Stop playing current module.
; a6 = CUSTOM

    DISABLE
    ifd SDATA
    clr.b   mt_Enable(a4)
    lea mt_chan1(a4),a0
    bsr resetch
    lea mt_chan2(a4),a0
    bsr resetch
    lea mt_chan3(a4),a0
    bsr resetch
    lea mt_chan4(a4),a0
    bsr resetch
    else
    lea mt_data(pc),a1
    clr.b   mt_Enable(a1)
    lea mt_chan1(a1),a0
    bsr resetch
    lea mt_chan2(a1),a0
    bsr resetch
    lea mt_chan3(a1),a0
    bsr resetch
    lea mt_chan4(a1),a0
    bsr resetch
    endc
    moveq   #0,d0
    move.w  d0,AUD0VOL(a6)
    move.w  d0,AUD1VOL(a6)
    move.w  d0,AUD2VOL(a6)
    move.w  d0,AUD3VOL(a6)
    move.w  #$000f,DMACON(a6)
    ENABLE
    rts


resetch:
; a0 = channel status
; All registers are preserved!

    move.w  #320,n_period(a0)   ; make sure period is not illegal
    clr.w   n_volume(a0)
    clr.w   n_sfxlen(a0)
    clr.w   n_funk(a0)
    clr.b   n_sfxpri(a0)
    clr.b   n_looped(a0)
    clr.b   n_gliss(a0)
    ifeq    MINIMAL
    clr.b   n_musiconly(a0)
    st  n_enable(a0)
    endc
    rts


    ifeq    MINIMAL
;---------------------------------------------------------------------------
    xdef    _mt_soundfx
_mt_soundfx:
; Request playing of an external sound effect on the most unused channel.
; This function is for compatibility with the old API only!
; You should call _mt_playfx instead!
; a6 = CUSTOM
; a0 = sample pointer
; d0.w = sample length in words
; d1.w = sample period
; d2.w = sample volume

    lea -sfx_sizeof(sp),sp
    move.l  a0,sfx_ptr(sp)
    movem.w d0-d2,sfx_len(sp)
    move.w  #$ff01,sfx_cha(sp)  ; any channel, priority=1
    move.l  sp,a0
    bsr _mt_playfx
    lea sfx_sizeof(sp),sp
    rts


;---------------------------------------------------------------------------
    xdef    _mt_playfx
_mt_playfx:
; Request playing of a prioritized external sound effect, either on a
; fixed channel or on the most unused one.
; A negative channel specification means to use the best one.
; The priority is unsigned and should be greater than zero.
; This channel will be blocked for music until the effect has finished.
; a6 = CUSTOM
; a0 = sfx-structure pointer with the following layout:
;      0: ptr, 4: len.w, 6: period.w, 8: vol.w, 10: channel.b, 11: priority.b
; -> d0 = pointer to channel status or NULL when sample was ignored

    ifd SDATA
    movem.l d2-d7/a0-a3/a5,-(sp)
    else
    movem.l d2-d7/a0-a5,-(sp)
    lea mt_data(pc),a4
    endc

    DISABLE
    moveq   #0,d0
    move.b  sfx_cha(a0),d0
    bpl channelsfx      ; use fixed channel for effect

    ; Did we already calculate the n_freecnt values for all channels?
    tst.b   mt_SilCntValid(a4)
    bne freecnt_valid

    ; Look at the next 8 pattern steps to find the longest sequence
    ; of silence (no new note or instrument).
    moveq   #8,d2
    move.l  #$fffff000,d3       ; mask to ignore effects

    ; reset freecnts for all channels
    moveq   #0,d0
    move.b  d0,mt_chan1+n_freecnt(a4)
    move.b  d0,mt_chan2+n_freecnt(a4)
    move.b  d0,mt_chan3+n_freecnt(a4)
    move.b  d0,mt_chan4+n_freecnt(a4)
    moveq   #-1,d4
    moveq   #-1,d5
    moveq   #-1,d6
    moveq   #-1,d7

    ; get pattern pointer
    move.l  mt_mod(a4),a3       ; a3 mod pointer
    lea 952(a3),a5      ; a5 song arrangement list
    move.w  mt_PatternPos(a4),d1
    move.b  mt_SongPos(a4),d0
.1: move.b  (a5,d0.w),d0
    swap    d0
    lea 1084(a3),a1
    lsr.l   #6,d0
    add.l   d0,a1
    lea 1024(a1),a2     ; a2 end of pattern
    add.w   d1,a1           ; a1 current pattern pos

.2: moveq   #0,d0

    move.l  (a1)+,d1
    and.l   d3,d1
    seq d1
    and.b   d1,d4
    sub.b   d4,mt_chan1+n_freecnt(a4)
    add.b   d4,d0

    move.l  (a1)+,d1
    and.l   d3,d1
    seq d1
    and.b   d1,d5
    sub.b   d5,mt_chan2+n_freecnt(a4)
    add.b   d5,d0

    move.l  (a1)+,d1
    and.l   d3,d1
    seq d1
    and.b   d1,d6
    sub.b   d6,mt_chan3+n_freecnt(a4)
    add.b   d6,d0

    move.l  (a1)+,d1
    and.l   d3,d1
    seq d1
    and.b   d1,d7
    sub.b   d7,mt_chan4+n_freecnt(a4)
    add.b   d7,d0

    ; break the loop when no channel has any more free pattern steps
    beq .7

    ; otherwise break after 8 pattern steps
    subq.w  #1,d2
    beq .7

    ; End of pattern reached? Then load next pattern pointer.
    cmp.l   a2,a1
    blo .2
    moveq   #0,d1
    moveq   #1,d0
    add.b   mt_SongPos(a4),d0
    and.w   #$007f,d0
    cmp.b   950(a3),d0      ; end of song reached?
    blo .1
    moveq   #0,d0
    bra .1

.7: st  mt_SilCntValid(a4)

freecnt_valid:
    sub.l   a2,a2
    move.b  sfx_pri(a0),d2

    ; Determine which channels are already allocated for sound
    ; effects and check if the limit was reached. In this case only
    ; replace sound effect channels by higher priority.
    moveq   #3,d0
    sub.b   mt_MusicChannels(a4),d0
    move.b  mt_chan1+n_sfxpri(a4),d4
    or.b    mt_chan1+n_musiconly(a4),d4
    sne d1
    add.b   d1,d0
    move.b  mt_chan2+n_sfxpri(a4),d5
    or.b    mt_chan2+n_musiconly(a4),d5
    sne d1
    add.b   d1,d0
    move.b  mt_chan3+n_sfxpri(a4),d6
    or.b    mt_chan3+n_musiconly(a4),d6
    sne d1
    add.b   d1,d0
    move.b  mt_chan4+n_sfxpri(a4),d7
    or.b    mt_chan4+n_musiconly(a4),d7
    sne d1
    add.b   d1,d0
    bmi .overwrite      ; all channels reserved/playing effects

    ; We will prefer a music channel which had an audio interrupt,
    ; because that means the last instrument sample has been played
    ; completely, and the channel is now in an idle loop.
    ; Also exclude channels which have set a repeat loop.
    ; Try not to break them!
    moveq   #0,d3
    tst.b   mt_chan1+n_looped(a4)
    bne .1
    or.w    #$0080,d3
.1: tst.b   mt_chan2+n_looped(a4)
    bne .2
    or.w    #$0100,d3
.2: tst.b   mt_chan3+n_looped(a4)
    bne .3
    or.w    #$0200,d3
.3: tst.b   mt_chan4+n_looped(a4)
    bne .4
    or.w    #$0400,d3
.4: move.w  INTREQR(a6),d1
    and.w   d3,d1
    bne .6

    ; All channels are busy. Then break the non-looped ones first...
    move.w  d3,d1
    bne .6

    ; ..except there are none. Then it doesn't matter. :|
    move.w  #$0780,d1

    ; first look for the best unused channel
.6: moveq   #0,d3
    btst    #7,d1
    seq d0
    or.b    d4,d0
    bne .7
    lea mt_chan1+n_freecnt(a4),a1
    cmp.b   (a1),d3
    bhi .7
    move.l  a1,a2
    move.b  (a1),d3
.7: btst    #8,d1
    seq d0
    or.b    d5,d0
    bne .8
    lea mt_chan2+n_freecnt(a4),a1
    cmp.b   (a1),d3
    bhi .8
    move.l  a1,a2
    move.b  (a1),d3
.8: btst    #9,d1
    seq d0
    or.b    d6,d0
    bne .9
    lea mt_chan3+n_freecnt(a4),a1
    cmp.b   (a1),d3
    bhi .9
    move.l  a1,a2
    move.b  (a1),d3
.9: btst    #10,d1
    seq d0
    or.b    d7,d0
    bne .10
    lea mt_chan4+n_freecnt(a4),a1
    cmp.b   (a1),d3
    bhi .10
    move.l  a1,a2
    bra found_sfx_ch

.10:    move.l  a2,d3
    bne found_sfx_ch

.overwrite:
    ; finally try to overwrite a sound effect with lower/equal priority
    moveq   #0,d3
    tst.b   d4
    beq .11
    cmp.b   d4,d2
    blo .11
    lea mt_chan1+n_freecnt(a4),a2
    move.b  (a2),d3
.11:    tst.b   d5
    beq .12
    cmp.b   d5,d2
    blo .12
    lea mt_chan2+n_freecnt(a4),a1
    cmp.b   (a1),d3
    bhi .12
    move.l  a1,a2
    move.b  (a1),d3
.12:    tst.b   d6
    beq .13
    cmp.b   d6,d2
    blo .13
    lea mt_chan3+n_freecnt(a4),a1
    cmp.b   (a1),d3
    bhi .13
    move.l  a1,a2
    move.b  (a1),d3
.13:    tst.b   d7
    beq .14
    cmp.b   d7,d2
    blo .14
    lea mt_chan4+n_freecnt(a4),a1
    cmp.b   (a1),d3
    bhi .14
    move.l  a1,a2

.14:    move.l  a2,d3
    beq exit_playfx     ; ignore new sfx due to low priority

found_sfx_ch:
    lea -n_freecnt(a2),a2
    bra set_sfx

channelsfx:
; a0 = sfx structure
; d0 = fixed channel for new sound effect
    add.w   d0,d0
    lea mt_chan1(a4),a2
    add.w   channel_offsets(pc,d0.w),a2

    ; priority high enough to replace a present effect on this channel?
    move.b  sfx_pri(a0),d2
    cmp.b   n_sfxpri(a2),d2
    bhs set_sfx
    sub.l   a2,a2
    bra exit_playfx

set_sfx:
; activate the sound effect on this channel
; a0 = sfx structure
; d2 = sfx priority
; a2 = channel status
    move.l  (a0)+,n_sfxptr(a2)  ; sfx_ptr
    move.w  (a0)+,n_sfxlen(a2)  ; sfx_len
    move.w  (a0)+,n_sfxper(a2)  ; sfx_per
    move.w  (a0),n_sfxvol(a2)   ; sfx_vol
    move.b  d2,n_sfxpri(a2)

exit_playfx:
    ENABLE
    move.l  a2,d0           ; ptr to selected channel or NULL

    ifd SDATA
    movem.l (sp)+,d2-d7/a0-a3/a5
    else
    movem.l (sp)+,d2-d7/a0-a5
    endc
    rts

channel_offsets:
    dc.w    0*n_sizeof,1*n_sizeof,2*n_sizeof,3*n_sizeof


;---------------------------------------------------------------------------
    xdef    _mt_loopfx
_mt_loopfx:
; Request playing of a looped sound effect on a fixed channel, which
; will be blocked for music until the effect is stopped (_mt_stopfx).
; It uses the same sfx-structure as _mt_playfx, but the priority is
; ignored. A looped sound effect has always highest priority and will
; replace a previous effect on the same channel. No automatic channel
; selection possible!
; Also make sure the sample starts with a zero-word, which is used
; for idling when the effect is stopped. This word is included in the
; total length calculation, but excluded when actually playing the loop.
; a6 = CUSTOM
; a0 = sfx-structure pointer with the following layout:
;      0: ptr, 4: len.w, 6: period.w, 8: vol.w, 10: channel.b

    ifnd    SDATA
    lea mt_data+mt_chan1(pc),a1
    else
    lea mt_chan1(a4),a1
    endc

    moveq   #3,d0
    and.b   sfx_cha(a0),d0
    add.w   d0,d0
    add.w   channel_offsets(pc,d0.w),a1

    DISABLE
    move.l  (a0)+,n_sfxptr(a1)  ; sfx_ptr
    move.w  (a0)+,n_sfxlen(a1)  ; sfx_len
    move.w  (a0)+,n_sfxper(a1)  ; sfx_per
    move.w  (a0),n_sfxvol(a1)   ; sfx_vol
    st  n_sfxpri(a1)        ; sfx_pri -1 enables looped mode
    ENABLE
    rts


;---------------------------------------------------------------------------
    xdef    _mt_stopfx
_mt_stopfx:
; Immediately stop a currently playing sound effect on a channel.
; a6 = CUSTOM
; d0.b = channel (0..3)

    ifnd    SDATA
    lea mt_data+mt_chan1(pc),a0
    else
    lea mt_chan1(a4),a0
    endc

    and.w   #3,d0
    add.w   d0,d0
    add.w   channel_offsets(pc,d0.w),a0

    DISABLE
    tst.b   n_sfxpri(a0)
    beq .1          ; no sfx playing anyway
    moveq   #1,d0
    move.b  d0,n_sfxpri(a0)
    move.w  d0,n_sfxlen(a0)     ; idle loop
    move.w  #108,n_sfxper(a0)   ; enter idle as quickly as possible
    clr.w   n_sfxvol(a0)        ; and cut volume
    ifne    NULL_IS_CLEARED
    clr.b   n_looped(a0)
    clr.l   n_sfxptr(a0)        ; use $0 for idle-looping
    else
    tst.b   n_looped(a0)
    beq .1
    clr.b   n_looped(a0)
    subq.l  #2,n_sfxptr(a0)     ; idle loop at sample-start - 2
    endc
.1: ENABLE

    rts


;---------------------------------------------------------------------------
    xdef    _mt_musicmask
_mt_musicmask:
; Set bits in the mask define which specific channels are reserved
; for music only.
; a6 = CUSTOM
; d0.b = channel-mask (bit 0 for channel 0, ..., bit 3 for channel 3)

    ifnd    SDATA
    move.l  a4,-(sp)
    lea mt_data(pc),a4
    endc

    DISABLE

    lsl.b   #5,d0
    scs mt_chan4+n_musiconly(a4)
    add.b   d0,d0
    scs mt_chan3+n_musiconly(a4)
    add.b   d0,d0
    scs mt_chan2+n_musiconly(a4)
    add.b   d0,d0
    scs mt_chan1+n_musiconly(a4)

    ENABLE

    ifnd    SDATA
    move.l  (sp)+,a4
    endc
    rts


;---------------------------------------------------------------------------
    xdef    _mt_mastervol
_mt_mastervol:
; Set a master volume from 0 to 64 for all music channels.
; Note that the master volume does not affect the volume of external
; sound effects (which is desired).
; a6 = CUSTOM
; d0.w = master volume

    ifnd    SDATA
    move.l  a4,-(sp)
    lea mt_data(pc),a4
    endc

    ; stingray, since each volume table has a size of 65 bytes
    ; we simply multiply (optimised of course) by 65 to get the
    ; offset to the correct table
    lea MasterVolTab0(pc),a0
    add.w   d0,a0
    lsl.w   #6,d0
    add.w   d0,a0

    ; set new table and adapt all channel volumes immediately
    DISABLE
    move.l  a0,mt_MasterVolTab(a4)
    bsr set_all_volumes
    ENABLE

    ifnd    SDATA
    move.l  (sp)+,a4
    endc
    rts


;---------------------------------------------------------------------------
    xdef    _mt_channelmask
_mt_channelmask:
; Define which music channels are muted. Doesn't affect sound effects.
; a6 = CUSTOM
; d0.b = channel-mask (bit 0 for channel 0, ..., bit 3 for channel 3)

    ifnd    SDATA
    move.l  a4,-(sp)
    lea mt_data(pc),a4
    endc

    DISABLE

    lsl.b   #5,d0
    scs mt_chan4+n_enable(a4)
    add.b   d0,d0
    scs mt_chan3+n_enable(a4)
    add.b   d0,d0
    scs mt_chan2+n_enable(a4)
    add.b   d0,d0
    scs mt_chan1+n_enable(a4)
    move.l  mt_MasterVolTab(a4),a0
    bsr set_all_volumes

    ENABLE

    ifnd    SDATA
    move.l  (sp)+,a4
    endc
    rts


;---------------------------------------------------------------------------
set_all_volumes:
; a0 = Master volume table

    tst.b   mt_chan1+n_sfxpri(a4)
    bne .1
    move.w  mt_chan1+n_volume(a4),d0
    move.b  (a0,d0.w),d0
    and.b   mt_chan1+n_enable(a4),d0
    move.w  d0,AUD0VOL(a6)
    ifd VOL0_UNLOOPS
    bne .1
    clr.b   mt_chan1+n_looped(a4)
    endc
.1: tst.b   mt_chan2+n_sfxpri(a4)
    bne .2
    move.w  mt_chan2+n_volume(a4),d0
    move.b  (a0,d0.w),d0
    and.b   mt_chan2+n_enable(a4),d0
    move.w  d0,AUD1VOL(a6)
    ifd VOL0_UNLOOPS
    bne .2
    clr.b   mt_chan2+n_looped(a4)
    endc
.2: tst.b   mt_chan3+n_sfxpri(a4)
    bne .3
    move.w  mt_chan3+n_volume(a4),d0
    move.b  (a0,d0.w),d0
    and.b   mt_chan3+n_enable(a4),d0
    move.w  d0,AUD2VOL(a6)
    ifd VOL0_UNLOOPS
    bne .3
    clr.b   mt_chan3+n_looped(a4)
    endc
.3: tst.b   mt_chan4+n_sfxpri(a4)
    bne .4
    move.w  mt_chan4+n_volume(a4),d0
    move.b  (a0,d0.w),d0
    and.b   mt_chan4+n_enable(a4),d0
    move.w  d0,AUD3VOL(a6)
    ifd VOL0_UNLOOPS
    bne .4
    clr.b   mt_chan4+n_looped(a4)
    endc
.4: rts


;---------------------------------------------------------------------------
    xdef    _mt_samplevol
_mt_samplevol:
; Redefine a sample's volume. May also be done while the song is playing.
; Warning: Does not check arguments for valid range! You must have done
; _mt_init before calling this function!
; The new volume is persistent. Even when the song is restarted.
; d0.w = sample number (0-31)
; d1.b = volume (0-64)

    ifd SDATA
    move.l  mt_mod(a4),a0
    else
    move.l  mt_data+mt_mod(pc),a0
    endc

    swap    d1
    move.w  d0,d1
    add.w   d1,d1
    lsl.w   #5,d0
    sub.w   d1,d0           ; table index: sample number * 30
    swap    d1
    move.b  d1,12+3(a0,d0.w)    ; set sample's volume
    rts
    endc    ; !MINIMAL


;---------------------------------------------------------------------------
    xdef    _mt_music
_mt_music:
; Used when called by external interrupt handler.
; Saves/restores all registers and sets up A4 when needed.
; a6 = CUSTOM

    ifd SDATA
    movem.l d2-d7/a2-a3/a5,-(sp)
    bsr mt_music
    movem.l (sp)+,d2-d7/a2-a3/a5
    else    ; !SDATA
    movem.l d2-d7/a2-a5,-(sp)
    lea mt_data(pc),a4
    bsr mt_music
    movem.l (sp)+,d2-d7/a2-a5
    endc
    rts

mt_music:
; Called from interrupt.
; Play next position when Counter equals Speed.
; Effects are always handled.
; a6 = CUSTOM

    moveq   #0,d7           ; d7 is always zero

    lea mt_dmaon+1(pc),a0
    move.b  d7,(a0)

    addq.b  #1,mt_Counter(a4)

    move.b  mt_Counter(a4),d0
    cmp.b   mt_Speed(a4),d0
    blo no_new_note

    ; handle a new note
    move.b  d7,mt_Counter(a4)
    tst.b   mt_PattDelTime2(a4)
    beq get_new_note

    ; we have a pattern delay, check effects then step
    lea AUD0LC(a6),a5
    lea mt_chan1(a4),a2
    bsr mt_checkfx
    lea AUD1LC(a6),a5
    lea mt_chan2(a4),a2
    bsr mt_checkfx
    lea AUD2LC(a6),a5
    lea mt_chan3(a4),a2
    bsr mt_checkfx
    lea AUD3LC(a6),a5
    lea mt_chan4(a4),a2
    bsr mt_checkfx
    bra settb_step

no_new_note:
    ; no new note, just check effects, don't step to next position
    lea AUD0LC(a6),a5
    lea mt_chan1(a4),a2
    bsr mt_checkfx
    lea AUD1LC(a6),a5
    lea mt_chan2(a4),a2
    bsr mt_checkfx
    lea AUD2LC(a6),a5
    lea mt_chan3(a4),a2
    bsr mt_checkfx
    lea AUD3LC(a6),a5
    lea mt_chan4(a4),a2
    bsr mt_checkfx

    ifeq    NO_TIMERS
    ; set one-shot TimerB interrupt for enabling DMA, when needed
    move.b  mt_dmaon+1(pc),d0
    beq same_pattern
    move.b  #$19,CIAB+CIACRB    ; load/start timer B, one-shot
    endc
    bra same_pattern

get_new_note:
    ; determine pointer to current pattern line
    move.l  mt_mod(a4),a0
    lea 12(a0),a3       ; sample info table
    lea 1084(a0),a1     ; pattern data
    lea 952(a0),a0
    moveq   #0,d0
    move.b  mt_SongPos(a4),d0
    move.b  (a0,d0.w),d0        ; current pattern number
    swap    d0
    lsr.l   #6,d0
    add.l   d0,a1           ; pattern base
    add.w   mt_PatternPos(a4),a1    ; a1 pattern line

    ; play new note for each channel, apply some effects
    lea AUD0LC(a6),a5
    lea mt_chan1(a4),a2
    bsr mt_playvoice
    lea AUD1LC(a6),a5
    lea mt_chan2(a4),a2
    bsr mt_playvoice
    lea AUD2LC(a6),a5
    lea mt_chan3(a4),a2
    bsr mt_playvoice
    lea AUD3LC(a6),a5
    lea mt_chan4(a4),a2
    bsr mt_playvoice

settb_step:
    ifeq    NO_TIMERS
    ; set one-shot TimerB interrupt for enabling DMA, when needed
    move.b  mt_dmaon+1(pc),d0
    beq pattern_step
    move.b  #$19,CIAB+CIACRB    ; load/start timer B, one-shot
    endc

pattern_step:
    ; next pattern line, handle delay and break
    ifeq    MINIMAL
    clr.b   mt_SilCntValid(a4)  ; recalculate silence counters
    endc
    moveq   #16,d2          ; offset to next pattern line

    move.b  mt_PattDelTime2(a4),d1
    move.b  mt_PattDelTime(a4),d0
    beq .1
    move.b  d0,d1
    move.b  d7,mt_PattDelTime(a4)
.1: tst.b   d1
    beq .3
    subq.b  #1,d1
    beq .2
    moveq   #0,d2           ; do not advance to next line
.2: move.b  d1,mt_PattDelTime2(a4)

.3: add.w   mt_PatternPos(a4),d2    ; d2 PatternPos

    ; check for break
    bclr    d7,mt_PBreakFlag(a4)
    beq .4
    move.w  mt_PBreakPos(a4),d2
    move.w  d7,mt_PBreakPos(a4)

    ; check whether end of pattern is reached
.4: move.w  d2,mt_PatternPos(a4)
    cmp.w   #1024,d2
    blo same_pattern

song_step:
    move.w  mt_PBreakPos(a4),mt_PatternPos(a4)
    move.w  d7,mt_PBreakPos(a4)
    move.b  d7,mt_PosJumpFlag(a4)

    ; next position in song
    moveq   #1,d0
    add.b   mt_SongPos(a4),d0
    and.w   #$007f,d0
    move.l  mt_mod(a4),a0
    cmp.b   950(a0),d0      ; end of song reached?
    blo .1
    moveq   #0,d0           ; restart the song from the beginning
    ifeq    MINIMAL
    addq.b  #1,mt_SongEnd(a4)
    bne .2
    clr.b   mt_Enable(a4)       ; stop the song when mt_SongEnd was -1
.2: and.b   #$7f,mt_SongEnd(a4)
    endc
.1: move.b  d0,mt_SongPos(a4)

same_pattern:
    tst.b   mt_PosJumpFlag(a4)
    bne song_step

    rts


    ifeq    MINIMAL
;---------------------------------------------------------------------------
mt_sfxonly:
; Called from interrupt.
; Plays sound effects on free channels.
; a6 = CUSTOM

    moveq   #0,d7           ; d7 is always zero

    lea mt_dmaon+1(pc),a0
    move.b  d7,(a0)

    lea AUD0LC(a6),a5
    lea mt_chan1(a4),a2
    bsr chan_sfx_only
    lea AUD1LC(a6),a5
    lea mt_chan2(a4),a2
    bsr chan_sfx_only
    lea AUD2LC(a6),a5
    lea mt_chan3(a4),a2
    bsr chan_sfx_only
    lea AUD3LC(a6),a5
    lea mt_chan4(a4),a2
    bsr chan_sfx_only

    ifeq    NO_TIMERS
    move.b  mt_dmaon+1(pc),d0
    beq .1
    move.b  #$19,CIAB+CIACRB    ; load/start timer B, one-shot
    endc

.1: rts


chan_sfx_only:
; Check for new sound samples. Check if previous ones are finished.
; a2 = channel data
; a5 = audio registers

    tst.b   n_sfxpri(a2)
    beq .1

    move.w  n_sfxlen(a2),d0
    bne start_sfx
    tst.b   n_looped(a2)
    bne .1

    move.w  n_intbit(a2),d0
    and.w   INTREQR(a6),d0
    beq .1
    move.w  n_dmabit(a2),d0
    and.w   mt_dmaon(pc),d0
    bne .1

    ; last sound effect sample has played, so unblock this channel again
    move.b  d7,n_sfxpri(a2)

.1: rts


;---------------------------------------------------------------------------
start_sfx:
; d0 = sfx_len in words
; a2 = channel data
; a5 = audio registers

    ; play new sound effect on this channel
    move.w  n_dmabit(a2),d1
    move.w  d1,DMACON(a6)

    move.l  n_sfxptr(a2),a0
    tst.b   n_sfxpri(a2)
    bpl .1

    ; looped sound effect
    st  n_looped(a2)
    addq.l  #2,a0           ; skip first word, used for idling
    subq.w  #1,d0
    move.l  a0,AUDLC(a5)
    move.w  d0,AUDLEN(a5)
    bra .2

    ; normal sound effect
.1: move.b  d7,n_looped(a2)
    move.l  a0,AUDLC(a5)
    move.w  d0,AUDLEN(a5)
    moveq   #1,d0           ; idles after playing once
    ifne    NULL_IS_CLEARED
    sub.l   a0,a0
    endc

    ; save repeat and period for TimerB interrupt
.2: move.l  a0,n_loopstart(a2)
    move.w  d0,n_replen(a2)
    move.w  n_sfxper(a2),d0
    move.w  d0,AUDPER(a5)
    move.w  d0,n_period(a2)
    move.w  n_sfxvol(a2),AUDVOL(a5)

    move.w  d7,n_sfxlen(a2)     ; don't call start_sfx again

    lea mt_dmaon(pc),a0
    or.w    d1,(a0)         ; DMA-channel to enable on TimerB
    rts
    endc    ; !MINIMAL


;---------------------------------------------------------------------------
mt_checkfx:
; a2 = channel data
; a5 = audio registers

    ifeq    MINIMAL
    tst.b   n_sfxpri(a2)
    beq .3

    move.w  n_sfxlen(a2),d0
    beq .2
    bsr start_sfx

    ; channel is blocked, only check some E-commands
.1: move.w  #$0fff,d4
    and.w   n_cmd(a2),d4
    move.w  d4,d0
    clr.b   d0
    cmp.w   #$0e00,d0
    bne mt_nop
    and.w   #$00ff,d4
    bra blocked_e_cmds

.2: tst.b   n_looped(a2)
    bne .1
    move.w  n_intbit(a2),d0
    and.w   INTREQR(a6),d0
    beq .1
    move.w  n_dmabit(a2),d0
    and.w   mt_dmaon(pc),d0
    bne .1

    ; sound effect sample has played, so unblock this channel again
    move.b  d7,n_sfxpri(a2)
    endc    ; !MINIMAL

    ; do channel effects between notes
.3: move.w  n_funk(a2),d0
    beq .4
    bsr mt_updatefunk

.4: move.w  #$0fff,d4
    and.w   n_cmd(a2),d4
    beq mt_pernop
    and.w   #$00ff,d4

    moveq   #$0f,d0
    and.b   n_cmd(a2),d0
    add.w   d0,d0
    move.w  fx_tab(pc,d0.w),d0
    jmp fx_tab(pc,d0.w)

fx_tab:
    dc.w    mt_arpeggio-fx_tab  ; $0
    dc.w    mt_portaup-fx_tab
    dc.w    mt_portadown-fx_tab
    dc.w    mt_toneporta-fx_tab
    dc.w    mt_vibrato-fx_tab   ; $4
    dc.w    mt_tonevolslide-fx_tab
    dc.w    mt_vibrvolslide-fx_tab
    dc.w    mt_tremolo-fx_tab
    dc.w    mt_nop-fx_tab       ; $8
    dc.w    mt_nop-fx_tab
    dc.w    mt_volumeslide-fx_tab
    dc.w    mt_nop-fx_tab
    dc.w    mt_nop-fx_tab       ; $C
    dc.w    mt_nop-fx_tab
    dc.w    mt_e_cmds-fx_tab
    dc.w    mt_nop-fx_tab


mt_pernop:
; just set the current period

    move.w  n_period(a2),AUDPER(a5)
mt_nop:
    rts


;---------------------------------------------------------------------------
mt_playvoice:
; a1 = pattern ptr
; a2 = channel data
; a3 = sample info table
; a5 = audio registers

    move.l  (a1)+,d6        ; d6 current note/cmd words

    ifeq    MINIMAL
    ; channel blocked by external sound effect?
    tst.b   n_sfxpri(a2)
    beq .2

    move.w  n_sfxlen(a2),d0
    beq .1
    bsr start_sfx
    bra moreblockedfx

    ; do only some limited commands, while sound effect is in progress
.1: tst.b   n_looped(a2)
    bne moreblockedfx
    move.w  n_intbit(a2),d0
    and.w   INTREQR(a6),d0
    beq moreblockedfx
    move.w  n_dmabit(a2),d0
    and.w   mt_dmaon(pc),d0
    bne moreblockedfx

    ; sound effect sample has played, so unblock this channel again
    move.b  d7,n_sfxpri(a2)
    endc    ; !MINIMAL

.2: tst.l   (a2)            ; n_note/cmd: any note or cmd set?
    bne .3
    move.w  n_period(a2),AUDPER(a5)
.3: move.l  d6,(a2)

    moveq   #15,d5
    and.b   n_cmd(a2),d5
    add.w   d5,d5           ; d5 cmd*2

    moveq   #0,d4
    move.b  d6,d4           ; d4 cmd argument (in MSW)
    swap    d4
    move.w  #$0ff0,d4
    and.w   d6,d4           ; d4 for checking E-cmd (in LSW)

    swap    d6
    move.l  d6,d0           ; S...S...
    clr.b   d0
    rol.w   #4,d0
    rol.l   #4,d0           ; ....00SS

    and.w   #$0fff,d6       ; d6 note

    ; get sample start address
    add.w   d0,d0           ; sample number * 2
    beq set_regs
    move.w  mult30tab(pc,d0.w),d1   ; d1 sample info table offset
    lea mt_SampleStarts(a4),a0
    add.w   d0,d0
    move.l  -4(a0,d0.w),d2

    ; read length, volume and repeat from sample info table
    lea (a3,d1.w),a0
    move.w  (a0)+,d0        ; length
    bne .4

    ifne    NULL_IS_CLEARED
    moveq   #0,d2           ; use $0 for empty samples
    else
    ; use the first two bytes from the first sample for empty samples
    move.l  mt_SampleStarts(a4),d2
    endc
    addq.w  #1,d0

.4: move.l  d2,n_start(a2)
    move.w  d0,n_reallength(a2)

    ; determine period table from fine-tune parameter
    moveq   #0,d3
    move.b  (a0)+,d3
    add.w   d3,d3
    move.l  a0,d1
    lea mt_PerFineTune(pc),a0
    add.w   (a0,d3.w),a0
    move.l  a0,n_pertab(a2)
    move.l  d1,a0
    cmp.w   #2*8,d3
    shs n_minusft(a2)

    moveq   #0,d1
    move.b  (a0)+,d1        ; volume
    move.w  d1,n_volume(a2)
    move.w  (a0)+,d3        ; repeat offset
    beq no_offset

    ; set repeat
    add.l   d3,d2
    add.l   d3,d2
    move.w  (a0),d0
    move.w  d0,n_replen(a2)
    exg d0,d3           ; n_replen to d3
    add.w   d3,d0
    bra set_len_start

mult30tab:
    dc.w    0*30,1*30,2*30,3*30,4*30,5*30,6*30,7*30
    dc.w    8*30,9*30,10*30,11*30,12*30,13*30,14*30,15*30
    dc.w    16*30,17*30,18*30,19*30,20*30,21*30,22*30,23*30
    dc.w    24*30,25*30,26*30,27*30,28*30,29*30,30*30,31*30

no_offset:
    move.w  (a0),d3
    ifne    NULL_IS_CLEARED
    cmp.w   #1,d3
    beq .1
    bhi set_replen
    else
    bne set_replen
    endc
    ; repeat length zero means idle-looping
    addq.w  #1,d3
.1: moveq   #0,d2           ; expect two zero bytes at $0
set_replen:
    move.w  d3,n_replen(a2)
set_len_start:
    move.w  d0,n_length(a2)
    move.l  d2,n_loopstart(a2)
    move.l  d2,n_wavestart(a2)

    ifeq    MINIMAL
    move.l  mt_MasterVolTab(a4),a0
    move.b  (a0,d1.w),d1
    and.b   n_enable(a2),d1
    endc
    move.w  d1,AUDVOL(a5)

    ; remember if sample is looped
    ; @@@ FIXME: also need to check if n_loopstart equals n_start
    subq.w  #1,d3
    sne n_looped(a2)

set_regs:
; d4 = cmd argument | masked E-cmd
; d5 = cmd*2
; d6 = cmd.w | note.w

    move.w  d4,d3           ; d3 masked E-cmd
    swap    d4          ; d4 cmd argument into LSW

    tst.w   d6
    beq checkmorefx     ; no new note

    cmp.w   #$0e50,d3
    beq set_finetune

    move.w  prefx_tab(pc,d5.w),d0
    jmp prefx_tab(pc,d0.w)

prefx_tab:
    dc.w    set_period-prefx_tab,set_period-prefx_tab,set_period-prefx_tab
    dc.w    set_toneporta-prefx_tab         ; $3
    dc.w    set_period-prefx_tab
    dc.w    set_toneporta-prefx_tab         ; $5
    dc.w    set_period-prefx_tab,set_period-prefx_tab,set_period-prefx_tab
    dc.w    set_sampleoffset-prefx_tab      ; $9
    dc.w    set_period-prefx_tab,set_period-prefx_tab,set_period-prefx_tab
    dc.w    set_period-prefx_tab,set_period-prefx_tab,set_period-prefx_tab


mt_sampleoffset:
; cmd 9 x y (xy = offset in 256 bytes)
; d4 = xy

    moveq   #0,d0
    move.b  d4,d0
    bne .1
    move.b  n_sampleoffset(a2),d0
    bra .2
.1: move.b  d0,n_sampleoffset(a2)

.2: lsl.w   #7,d0
    cmp.w   n_length(a2),d0
    bhs .3
    sub.w   d0,n_length(a2)
    add.w   d0,d0
    add.l   d0,n_start(a2)
    rts

.3: move.w  #1,n_length(a2)
    rts


set_sampleoffset:
    bsr mt_sampleoffset
    bra set_period

set_finetune:
    lea mt_PerFineTune(pc),a0
    moveq   #$0f,d0
    and.b   d4,d0
    add.w   d0,d0
    add.w   (a0,d0.w),a0
    move.l  a0,n_pertab(a2)
    cmp.w   #2*8,d0
    shs n_minusft(a2)

set_period:
; find nearest period for a note value, then apply finetuning
; d3 = masked E-cmd
; d4 = cmd argument
; d5 = cmd*2
; d6 = note.w

    lea mt_PeriodTable(pc),a0
    moveq   #36-1,d0
    moveq   #-2,d1
.1: addq.w  #2,d1           ; table offset
    cmp.w   (a0)+,d6
    dbhs    d0,.1

    ; apply finetuning, set period and note-offset
    move.l  n_pertab(a2),a0
    move.w  (a0,d1.w),d2
    move.w  d2,n_period(a2)
    move.w  d1,n_noteoff(a2)

    ; check for notedelay
    cmp.w   #$0ed0,d3       ; notedelay
    beq checkmorefx

    ; disable DMA
    move.w  n_dmabit(a2),d0
    move.w  d0,DMACON(a6)

    btst    #2,n_vibratoctrl(a2)
    bne .2
    move.b  d7,n_vibratopos(a2)

.2: btst    #2,n_tremoloctrl(a2)
    bne .3
    move.b  d7,n_tremolopos(a2)

.3: move.l  n_start(a2),AUDLC(a5)
    move.w  n_length(a2),AUDLEN(a5)
    move.w  d2,AUDPER(a5)
    lea mt_dmaon(pc),a0
    or.w    d0,(a0)

checkmorefx:
; d4 = cmd argument
; d5 = cmd*2
; d6 = note.w

    move.w  n_funk(a2),d0
    beq .1
    bsr mt_updatefunk

.1: move.w  morefx_tab(pc,d5.w),d0
    jmp morefx_tab(pc,d0.w)

morefx_tab:
    dc.w    mt_pernop-morefx_tab,mt_pernop-morefx_tab,mt_pernop-morefx_tab
    dc.w    mt_pernop-morefx_tab,mt_pernop-morefx_tab,mt_pernop-morefx_tab
    dc.w    mt_pernop-morefx_tab,mt_pernop-morefx_tab,mt_pernop-morefx_tab
    dc.w    mt_sampleoffset-morefx_tab      ; $9
    dc.w    mt_pernop-morefx_tab
    dc.w    mt_posjump-morefx_tab           ; $B
    dc.w    mt_volchange-morefx_tab
    dc.w    mt_patternbrk-morefx_tab        ; $D
    dc.w    mt_e_cmds-morefx_tab
    dc.w    mt_setspeed-morefx_tab


set_toneporta:
    move.l  n_pertab(a2),a0     ; tuned period table

    ; find first period which is less or equal the note in d6
    moveq   #36-1,d0
    moveq   #-2,d1
.1: addq.w  #2,d1
    cmp.w   (a0)+,d6
    dbhs    d0,.1

    tst.b   n_minusft(a2)       ; negative fine tune?
    beq .2
    tst.w   d1
    beq .2
    subq.l  #2,a0           ; then take previous period
    subq.w  #2,d1

.2: move.w  d1,n_noteoff(a2)    ; note offset in period table
    move.w  n_period(a2),d2
    move.w  -(a0),d1
    cmp.w   d1,d2
    bne .3
    moveq   #0,d1
.3: move.w  d1,n_wantedperiod(a2)

    move.w  n_funk(a2),d0
    beq .4
    bsr mt_updatefunk

.4: move.w  d2,AUDPER(a5)
    rts


    ifeq    MINIMAL
moreblockedfx:
; d6 = note.w | cmd.w

    moveq   #0,d4
    move.b  d6,d4           ; cmd argument
    and.w   #$0f00,d6
    lsr.w   #7,d6
    move.w  blmorefx_tab(pc,d6.w),d0
    jmp blmorefx_tab(pc,d0.w)

blmorefx_tab:
    dc.w    mt_nop-blmorefx_tab,mt_nop-blmorefx_tab
    dc.w    mt_nop-blmorefx_tab,mt_nop-blmorefx_tab
    dc.w    mt_nop-blmorefx_tab,mt_nop-blmorefx_tab
    dc.w    mt_nop-blmorefx_tab,mt_nop-blmorefx_tab
    dc.w    mt_nop-blmorefx_tab,mt_nop-blmorefx_tab
    dc.w    mt_nop-blmorefx_tab
    dc.w    mt_posjump-blmorefx_tab         ; $B
    dc.w    mt_nop-blmorefx_tab
    dc.w    mt_patternbrk-blmorefx_tab      ; $D
    dc.w    blocked_e_cmds-blmorefx_tab
    dc.w    mt_setspeed-blmorefx_tab        ; $F
    endc    ; !MINIMAL


mt_arpeggio:
; cmd 0 x y (x = first arpeggio offset, y = second arpeggio offset)
; d4 = xy

    moveq   #0,d0
    move.b  mt_Counter(a4),d0
    move.b  arptab(pc,d0.w),d0
    beq mt_pernop       ; step 0, just use normal period
    bmi .1

    ; step 1, arpeggio by left nibble
    lsr.b   #4,d4
    bra .2

    ; step 2, arpeggio by right nibble
.1: and.w   #$000f,d4

    ; offset current note
.2: add.w   d4,d4
    add.w   n_noteoff(a2),d4
    cmp.w   #2*36,d4
    bhs .4

    ; set period with arpeggio offset from note table
    move.l  n_pertab(a2),a0
    move.w  (a0,d4.w),AUDPER(a5)
.4: rts

arptab:
    dc.b    0,1,-1,0,1,-1,0,1,-1,0,1,-1,0,1,-1,0
    dc.b    1,-1,0,1,-1,0,1,-1,0,1,-1,0,1,-1,0,1


mt_fineportaup:
; cmd E 1 x (subtract x from period)
; d0 = x

    tst.b   mt_Counter(a4)
    beq do_porta_up
    rts


mt_portaup:
; cmd 1 x x (subtract xx from period)
; d4 = xx

    move.w  d4,d0

do_porta_up:
    move.w  n_period(a2),d1
    sub.w   d0,d1
    cmp.w   #113,d1
    bge .1
    moveq   #113,d1
.1: move.w  d1,n_period(a2)
    move.w  d1,AUDPER(a5)
    rts


mt_fineportadn:
; cmd E 2 x (add x to period)
; d0 = x

    tst.b   mt_Counter(a4)
    beq do_porta_down
    rts


mt_portadown:
; cmd 2 x x (add xx to period)
; d4 = xx

    move.w  d4,d0

do_porta_down:
    move.w  n_period(a2),d1
    add.w   d0,d1
    cmp.w   #856,d1
    bls .1
    move.w  #856,d1
.1: move.w  d1,n_period(a2)
    move.w  d1,AUDPER(a5)
    rts


mt_toneporta:
; cmd 3 x y (xy = tone portamento speed)
; d4 = xy

    tst.b   d4
    beq mt_toneporta_nc
    move.w  d4,n_toneportspeed(a2)
    move.b  d7,n_cmdlo(a2)

mt_toneporta_nc:
    move.w  n_wantedperiod(a2),d1
    beq .6

    move.w  n_toneportspeed(a2),d0
    move.w  n_period(a2),d2
    cmp.w   d1,d2
    blo .2

    ; tone porta up
    sub.w   d0,d2
    cmp.w   d1,d2
    bgt .3
    move.w  d1,d2
    move.w  d7,n_wantedperiod(a2)
    bra .3

    ; tone porta down
.2: add.w   d0,d2
    cmp.w   d1,d2
    blt .3
    move.w  d1,d2
    move.w  d7,n_wantedperiod(a2)

.3: move.w  d2,n_period(a2)

    tst.b   n_gliss(a2)
    beq .5

    ; glissando: find nearest note for new period
    move.l  n_pertab(a2),a0
    moveq   #36-1,d0
    moveq   #-2,d1
.4: addq.w  #2,d1
    cmp.w   (a0)+,d2
    dbhs    d0,.4

    move.w  d1,n_noteoff(a2)    ; @@@ needed?
    move.w  -(a0),d2

.5: move.w  d2,AUDPER(a5)
.6  rts


mt_vibrato:
; cmd 4 x y (x = speed, y = amplitude)
; d4 = xy

    moveq   #$0f,d2
    and.b   d4,d2
    beq .1
    move.b  d2,n_vibratoamp(a2)
    bra .2
.1: move.b  n_vibratoamp(a2),d2

.2: lsr.b   #4,d4
    beq .3
    move.b  d4,n_vibratospd(a2)
    bra mt_vibrato_nc
.3: move.b  n_vibratospd(a2),d4

mt_vibrato_nc:
    ; calculate vibrato table offset: 64 * amplitude + (pos & 63)
    lsl.w   #6,d2
    moveq   #63,d0
    and.b   n_vibratopos(a2),d0
    add.w   d0,d2

    ifne    ENABLE_SAWRECT
    ; select vibrato waveform
    moveq   #3,d1
    and.b   n_vibratoctrl(a2),d1
    beq .6
    subq.b  #1,d1
    beq .5

    ; ctrl 2 & 3 select a rectangle vibrato
    lea mt_VibratoRectTable(pc),a0
    bra .9

    ; ctrl 1 selects a sawtooth vibrato
.5: lea mt_VibratoSawTable(pc),a0
    bra .9
    endc    ; ENABLE_SAWRECT

    ; ctrl 0 selects a sine vibrato
.6: lea mt_VibratoSineTable(pc),a0

    ; add vibrato-offset to period
.9: move.b  (a0,d2.w),d0
    ext.w   d0
    add.w   n_period(a2),d0
    move.w  d0,AUDPER(a5)

    ; increase vibratopos by speed
    add.b   d4,n_vibratopos(a2)
    rts


mt_tonevolslide:
; cmd 5 x y (x = volume-up, y = volume-down)
; d4 = xy

    pea mt_volumeslide(pc)
    bra mt_toneporta_nc


mt_vibrvolslide:
; cmd 6 x y (x = volume-up, y = volume-down)
; d4 = xy

    move.w  d4,d3
    move.b  n_vibratoamp(a2),d2
    move.b  n_vibratospd(a2),d4
    bsr mt_vibrato_nc

    move.w  d3,d4
    bra mt_volumeslide


mt_tremolo:
; cmd 7 x y (x = speed, y = amplitude)
; d4 = xy

    moveq   #$0f,d2
    and.b   d4,d2
    beq .1
    move.b  d2,n_tremoloamp(a2)
    bra .2
.1: move.b  n_tremoloamp(a2),d2

.2: lsr.b   #4,d4
    beq .3
    move.b  d4,n_tremolospd(a2)
    bra .4
.3: move.b  n_tremolospd(a2),d4

    ; calculate tremolo table offset: 64 * amplitude + (pos & 63)
.4: lsl.w   #6,d2
    moveq   #63,d0
    and.b   n_tremolopos(a2),d0
    add.w   d0,d2

    ifne    ENABLE_SAWRECT
    ; select tremolo waveform
    moveq   #3,d1
    and.b   n_tremoloctrl(a2),d1
    beq .6
    subq.b  #1,d1
    beq .5

    ; ctrl 2 & 3 select a rectangle tremolo
    lea mt_VibratoRectTable(pc),a0
    bra .9

    ; ctrl 1 selects a sawtooth tremolo
.5: lea mt_VibratoSawTable(pc),a0
    bra .9
    endc    ; ENABLE_SAWRECT

    ; ctrl 0 selects a sine tremolo
.6: lea mt_VibratoSineTable(pc),a0

    ; add tremolo-offset to volume
.9: move.w  n_volume(a2),d0
    add.b   (a0,d2.w),d0
    bpl .10
    moveq   #0,d0
.10:    cmp.w   #64,d0
    bls .11
    moveq   #64,d0
.11:    move.w  n_period(a2),AUDPER(a5)
    ifeq    MINIMAL
    move.l  mt_MasterVolTab(a4),a0
    move.b  (a0,d0.w),d0
    and.b   n_enable(a2),d0
    endc
    move.w  d0,AUDVOL(a5)
    ifd VOL0_UNLOOPS
    bne .12
    move.b  d7,n_looped(a2)
    endc

    ; increase tremolopos by speed
.12:    add.b   d4,n_tremolopos(a2)
    rts


mt_volumeslide:
; cmd A x y (x = volume-up, y = volume-down)
; d4 = xy

    move.w  n_volume(a2),d0
    moveq   #$0f,d1
    and.b   d4,d1
    lsr.b   #4,d4
    beq vol_slide_down

    ; slide up, until 64
    add.b   d4,d0
vol_slide_up:
    cmp.b   #64,d0
    bls set_pervol
    moveq   #64,d0
    bra set_pervol

    ; slide down, until 0
vol_slide_down:
    sub.b   d1,d0
    bpl set_pervol
    moveq   #0,d0

set_pervol:
    move.w  n_period(a2),AUDPER(a5)
set_vol:
    move.w  d0,n_volume(a2)
    ifeq    MINIMAL
    move.l  mt_MasterVolTab(a4),a0
    move.b  (a0,d0.w),d0
    and.b   n_enable(a2),d0
    endc
    move.w  d0,AUDVOL(a5)
    ifd VOL0_UNLOOPS
    bne .1
    move.b  d7,n_looped(a2)
    endc
.1: rts


mt_posjump:
; cmd B x y (xy = new song position)
; d4 = xy

    move.b  d4,d0
    subq.b  #1,d0
    move.b  d0,mt_SongPos(a4)

jump_pos0:
    move.w  d7,mt_PBreakPos(a4)
    st  mt_PosJumpFlag(a4)
    rts


mt_volchange:
; cmd C x y (xy = new volume)
; d4 = xy

    move.w  d4,d0
    cmp.w   #64,d0
    bls set_vol
    moveq   #64,d0
    bra set_vol


mt_patternbrk:
; cmd D x y (xy = break pos in decimal)
; d4 = xy

    moveq   #$0f,d0
    and.w   d4,d0
    move.w  d4,d1
    lsr.w   #4,d1
    add.b   mult10tab(pc,d1.w),d0
    cmp.b   #63,d0
    bhi jump_pos0

    lsl.w   #4,d0
    move.w  d0,mt_PBreakPos(a4)
    st  mt_PosJumpFlag(a4)
    rts

mult10tab:
    dc.b    0,10,20,30,40,50,60,70,80,90,0,0,0,0,0,0


mt_setspeed:
; cmd F x y (xy<$20 new speed, xy>=$20 new tempo)
; d4 = xy

    ifeq    VBLANK_MUSIC
    cmp.b   #$20,d4
    bhs .1
    endc

    move.b  d4,mt_Speed(a4)
    beq _mt_end
    rts

    ifeq    VBLANK_MUSIC
    ; set tempo (CIA only)
.1: and.w   #$00ff,d4
    move.l  mt_timerval(a4),d0
    divu    d4,d0
    move.b  d0,CIAB+CIATALO
    lsr.w   #8,d0
    move.b  d0,CIAB+CIATAHI
    rts
    endc


mt_e_cmds:
; cmd E x y (x=command, y=argument)
; d4 = xy

    moveq   #$0f,d0
    and.w   d4,d0           ; pass E-cmd argument in d0

    move.w  d4,d1
    lsr.w   #4,d1
    add.w   d1,d1
    move.w  ecmd_tab(pc,d1.w),d1
    jmp ecmd_tab(pc,d1.w)

ecmd_tab:
    dc.w    mt_filter-ecmd_tab
    dc.w    mt_fineportaup-ecmd_tab
    dc.w    mt_fineportadn-ecmd_tab
    dc.w    mt_glissctrl-ecmd_tab
    dc.w    mt_vibratoctrl-ecmd_tab
    dc.w    mt_finetune-ecmd_tab
    dc.w    mt_jumploop-ecmd_tab
    dc.w    mt_tremoctrl-ecmd_tab
    dc.w    mt_e8-ecmd_tab
    dc.w    mt_retrignote-ecmd_tab
    dc.w    mt_volfineup-ecmd_tab
    dc.w    mt_volfinedn-ecmd_tab
    dc.w    mt_notecut-ecmd_tab
    dc.w    mt_notedelay-ecmd_tab
    dc.w    mt_patterndelay-ecmd_tab
    dc.w    mt_funk-ecmd_tab


blocked_e_cmds:
; cmd E x y (x=command, y=argument)
; d4 = xy

    moveq   #$0f,d0
    and.w   d4,d0           ; pass E-cmd argument in d0

    move.w  d4,d1
    lsr.w   #4,d1
    add.w   d1,d1
    move.w  blecmd_tab(pc,d1.w),d1
    jmp blecmd_tab(pc,d1.w)

blecmd_tab:
    dc.w    mt_filter-blecmd_tab
    dc.w    mt_rts-blecmd_tab
    dc.w    mt_rts-blecmd_tab
    dc.w    mt_glissctrl-blecmd_tab
    dc.w    mt_vibratoctrl-blecmd_tab
    dc.w    mt_finetune-blecmd_tab
    dc.w    mt_jumploop-blecmd_tab
    dc.w    mt_tremoctrl-blecmd_tab
    dc.w    mt_e8-blecmd_tab
    dc.w    mt_rts-blecmd_tab
    dc.w    mt_rts-blecmd_tab
    dc.w    mt_rts-blecmd_tab
    dc.w    mt_rts-blecmd_tab
    dc.w    mt_rts-blecmd_tab
    dc.w    mt_patterndelay-blecmd_tab
    dc.w    mt_rts-blecmd_tab


mt_filter:
; cmd E 0 x (x=1 disable, x=0 enable)
; d0 = x

    lsr.b   #1,d0
    bcs .1
    bclr    #1,CIAA+CIAPRA
    rts
.1: bset    #1,CIAA+CIAPRA
mt_rts:
    rts


mt_glissctrl:
; cmd E 3 x (x gliss)
; d0 = x

    move.b  d0,n_gliss(a2)
    rts


mt_vibratoctrl:
; cmd E 4 x (x = vibrato)
; d0 = x

    move.b  d0,n_vibratoctrl(a2)
    rts


mt_finetune:
; cmd E 5 x (x = finetune)
; d0 = x

    lea mt_PerFineTune(pc),a0
    add.w   d0,d0
    add.w   (a0,d0.w),a0
    move.l  a0,n_pertab(a2)
    cmp.w   #2*8,d0
    shs n_minusft(a2)
    rts


mt_jumploop:
; cmd E 6 x (x=0 loop start, else loop count)
; d0 = x

    tst.b   mt_Counter(a4)
    bne .4

.1: tst.b   d0
    beq .3          ; set start

    ; otherwise we are at the end of the loop
    subq.b  #1,n_loopcount(a2)
    beq .4          ; loop finished
    bpl .2

    ; initialize loop counter
    move.b  d0,n_loopcount(a2)

    ; jump back to start of loop
.2: move.w  n_pattpos(a2),mt_PBreakPos(a4)
    st  mt_PBreakFlag(a4)
    rts

    ; remember start of loop position
.3: move.w  mt_PatternPos(a4),n_pattpos(a2)
.4: rts


mt_tremoctrl:
; cmd E 7 x (x = tremolo)
; d0 = x

    move.b  d0,n_tremoloctrl(a2)
    rts


mt_e8:
; cmd E 8 x (x = trigger value)
; d0 = x

    move.b  d0,mt_E8Trigger(a4)
    rts


mt_retrignote:
; cmd E 9 x (x = retrigger count)
; d0 = x

    tst.b   d0
    beq .1

    ; set new retrigger count when Counter=0
    tst.b   mt_Counter(a4)
    bne .2
    move.b  d0,n_retrigcount(a2)

    ; avoid double retrigger, when Counter=0 and a note was set
    move.w  #$0fff,d2
    and.w   (a2),d2
    beq do_retrigger
.1: rts

    ; check if retrigger count is reached
.2: subq.b  #1,n_retrigcount(a2)
    bne .1
    move.b  d0,n_retrigcount(a2)    ; reset

do_retrigger:
    ; DMA off, set sample pointer and length
    move.w  n_dmabit(a2),d0
    move.w  d0,DMACON(a6)
    move.l  n_start(a2),AUDLC(a5)
    move.w  n_length(a2),AUDLEN(a5)
    lea mt_dmaon(pc),a0
    or.w    d0,(a0)
    rts


mt_volfineup:
; cmd E A x (x = volume add)
; d0 = x

    tst.b   mt_Counter(a4)
    beq .1
    rts

.1: add.w   n_volume(a2),d0
    bra vol_slide_up


mt_volfinedn:
; cmd E B x (x = volume sub)
; d0 = x

    tst.b   mt_Counter(a4)
    beq .1
    rts

.1: move.b  d0,d1
    move.w  n_volume(a2),d0
    bra vol_slide_down


mt_notecut:
; cmd E C x (x = counter to cut at)
; d0 = x

    cmp.b   mt_Counter(a4),d0
    bne .1
    move.w  d7,n_volume(a2)
    move.w  d7,AUDVOL(a5)
    ifd VOL0_UNLOOPS
    move.b  d7,n_looped(a2)
    endc
.1: rts


mt_notedelay:
; cmd E D x (x = counter to retrigger at)
; d0 = x

    cmp.b   mt_Counter(a4),d0
    bne .1
    tst.w   (a2)            ; trigger note when given
    bne .2
.1: rts
.2: move.w  n_period(a2),AUDPER(a5)
    bra do_retrigger


mt_patterndelay:
; cmd E E x (x = delay count)
; d0 = x

    tst.b   mt_Counter(a4)
    bne .1
    tst.b   mt_PattDelTime2(a4)
    bne .1
    addq.b  #1,d0
    move.b  d0,mt_PattDelTime(a4)
.1: rts


mt_funk:
; cmd E F x (x = funk speed)
; d0 = x

    tst.b   mt_Counter(a4)
    bne .1
    move.w  d0,n_funk(a2)
    bne mt_updatefunk
.1: rts

mt_updatefunk:
; d0 = funk speed

    move.b  mt_FunkTable(pc,d0.w),d0
    add.b   d0,n_funkoffset(a2)
    bpl .2
    move.b  d7,n_funkoffset(a2)

    move.l  n_loopstart(a2),d0
    moveq   #0,d1
    move.w  n_replen(a2),d1
    add.l   d1,d1
    add.l   d0,d1
    move.l  n_wavestart(a2),a0
    addq.l  #1,a0
    cmp.l   d1,a0
    blo .1
    move.l  d0,a0
.1: move.l  a0,n_wavestart(a2)
    not.b   (a0)

.2: rts


mt_FunkTable:
    dc.b    0,5,6,7,8,10,11,13,16,19,22,26,32,43,64,128

mt_VibratoSineTable:
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1
    dc.b    1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0
    dc.b    0,0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
    dc.b    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,0,0,0
    dc.b    0,0,0,1,1,1,2,2,2,3,3,3,3,3,3,3
    dc.b    3,3,3,3,3,3,3,3,2,2,2,1,1,1,0,0
    dc.b    0,0,0,-1,-1,-1,-2,-2,-2,-3,-3,-3,-3,-3,-3,-3
    dc.b    -3,-3,-3,-3,-3,-3,-3,-3,-2,-2,-2,-1,-1,-1,0,0
    dc.b    0,0,1,1,2,2,3,3,4,4,4,5,5,5,5,5
    dc.b    5,5,5,5,5,5,4,4,4,3,3,2,2,1,1,0
    dc.b    0,0,-1,-1,-2,-2,-3,-3,-4,-4,-4,-5,-5,-5,-5,-5
    dc.b    -5,-5,-5,-5,-5,-5,-4,-4,-4,-3,-3,-2,-2,-1,-1,0
    dc.b    0,0,1,2,3,3,4,5,5,6,6,7,7,7,7,7
    dc.b    7,7,7,7,7,7,6,6,5,5,4,3,3,2,1,0
    dc.b    0,0,-1,-2,-3,-3,-4,-5,-5,-6,-6,-7,-7,-7,-7,-7
    dc.b    -7,-7,-7,-7,-7,-7,-6,-6,-5,-5,-4,-3,-3,-2,-1,0
    dc.b    0,0,1,2,3,4,5,6,7,7,8,8,9,9,9,9
    dc.b    9,9,9,9,9,8,8,7,7,6,5,4,3,2,1,0
    dc.b    0,0,-1,-2,-3,-4,-5,-6,-7,-7,-8,-8,-9,-9,-9,-9
    dc.b    -9,-9,-9,-9,-9,-8,-8,-7,-7,-6,-5,-4,-3,-2,-1,0
    dc.b    0,1,2,3,4,5,6,7,8,9,9,10,11,11,11,11
    dc.b    11,11,11,11,11,10,9,9,8,7,6,5,4,3,2,1
    dc.b    0,-1,-2,-3,-4,-5,-6,-7,-8,-9,-9,-10,-11,-11,-11,-11
    dc.b    -11,-11,-11,-11,-11,-10,-9,-9,-8,-7,-6,-5,-4,-3,-2,-1
    dc.b    0,1,2,4,5,6,7,8,9,10,11,12,12,13,13,13
    dc.b    13,13,13,13,12,12,11,10,9,8,7,6,5,4,2,1
    dc.b    0,-1,-2,-4,-5,-6,-7,-8,-9,-10,-11,-12,-12,-13,-13,-13
    dc.b    -13,-13,-13,-13,-12,-12,-11,-10,-9,-8,-7,-6,-5,-4,-2,-1
    dc.b    0,1,3,4,6,7,8,10,11,12,13,14,14,15,15,15
    dc.b    15,15,15,15,14,14,13,12,11,10,8,7,6,4,3,1
    dc.b    0,-1,-3,-4,-6,-7,-8,-10,-11,-12,-13,-14,-14,-15,-15,-15
    dc.b    -15,-15,-15,-15,-14,-14,-13,-12,-11,-10,-8,-7,-6,-4,-3,-1
    dc.b    0,1,3,5,6,8,9,11,12,13,14,15,16,17,17,17
    dc.b    17,17,17,17,16,15,14,13,12,11,9,8,6,5,3,1
    dc.b    0,-1,-3,-5,-6,-8,-9,-11,-12,-13,-14,-15,-16,-17,-17,-17
    dc.b    -17,-17,-17,-17,-16,-15,-14,-13,-12,-11,-9,-8,-6,-5,-3,-1
    dc.b    0,1,3,5,7,9,11,12,14,15,16,17,18,19,19,19
    dc.b    19,19,19,19,18,17,16,15,14,12,11,9,7,5,3,1
    dc.b    0,-1,-3,-5,-7,-9,-11,-12,-14,-15,-16,-17,-18,-19,-19,-19
    dc.b    -19,-19,-19,-19,-18,-17,-16,-15,-14,-12,-11,-9,-7,-5,-3,-1
    dc.b    0,2,4,6,8,10,12,13,15,16,18,19,20,20,21,21
    dc.b    21,21,21,20,20,19,18,16,15,13,12,10,8,6,4,2
    dc.b    0,-2,-4,-6,-8,-10,-12,-13,-15,-16,-18,-19,-20,-20,-21,-21
    dc.b    -21,-21,-21,-20,-20,-19,-18,-16,-15,-13,-12,-10,-8,-6,-4,-2
    dc.b    0,2,4,6,9,11,13,15,16,18,19,21,22,22,23,23
    dc.b    23,23,23,22,22,21,19,18,16,15,13,11,9,6,4,2
    dc.b    0,-2,-4,-6,-9,-11,-13,-15,-16,-18,-19,-21,-22,-22,-23,-23
    dc.b    -23,-23,-23,-22,-22,-21,-19,-18,-16,-15,-13,-11,-9,-6,-4,-2
    dc.b    0,2,4,7,9,12,14,16,18,20,21,22,23,24,25,25
    dc.b    25,25,25,24,23,22,21,20,18,16,14,12,9,7,4,2
    dc.b    0,-2,-4,-7,-9,-12,-14,-16,-18,-20,-21,-22,-23,-24,-25,-25
    dc.b    -25,-25,-25,-24,-23,-22,-21,-20,-18,-16,-14,-12,-9,-7,-4,-2
    dc.b    0,2,5,8,10,13,15,17,19,21,23,24,25,26,27,27
    dc.b    27,27,27,26,25,24,23,21,19,17,15,13,10,8,5,2
    dc.b    0,-2,-5,-8,-10,-13,-15,-17,-19,-21,-23,-24,-25,-26,-27,-27
    dc.b    -27,-27,-27,-26,-25,-24,-23,-21,-19,-17,-15,-13,-10,-8,-5,-2
    dc.b    0,2,5,8,11,14,16,18,21,23,24,26,27,28,29,29
    dc.b    29,29,29,28,27,26,24,23,21,18,16,14,11,8,5,2
    dc.b    0,-2,-5,-8,-11,-14,-16,-18,-21,-23,-24,-26,-27,-28,-29,-29
    dc.b    -29,-29,-29,-28,-27,-26,-24,-23,-21,-18,-16,-14,-11,-8,-5,-2

    ifne    ENABLE_SAWRECT
mt_VibratoSawTable:
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
    dc.b    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1
    dc.b    2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3
    dc.b    -3,-3,-3,-3,-3,-3,-3,-3,-2,-2,-2,-2,-2,-2,-2,-2
    dc.b    -1,-1,-1,-1,-1,-1,-1,-1,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,1,1,1,1,1,2,2,2,2,2
    dc.b    3,3,3,3,3,3,4,4,4,4,4,5,5,5,5,5
    dc.b    -5,-5,-5,-5,-5,-5,-4,-4,-4,-4,-4,-3,-3,-3,-3,-3
    dc.b    -2,-2,-2,-2,-2,-2,-1,-1,-1,-1,-1,0,0,0,0,0
    dc.b    0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3
    dc.b    4,4,4,4,5,5,5,5,6,6,6,6,7,7,7,7
    dc.b    -7,-7,-7,-7,-6,-6,-6,-6,-5,-5,-5,-5,-4,-4,-4,-4
    dc.b    -3,-3,-3,-3,-2,-2,-2,-2,-1,-1,-1,-1,0,0,0,0
    dc.b    0,0,0,0,1,1,1,2,2,2,3,3,3,4,4,4
    dc.b    5,5,5,5,6,6,6,7,7,7,8,8,8,9,9,9
    dc.b    -9,-9,-9,-9,-8,-8,-8,-7,-7,-7,-6,-6,-6,-5,-5,-5
    dc.b    -4,-4,-4,-4,-3,-3,-3,-2,-2,-2,-1,-1,-1,0,0,0
    dc.b    0,0,0,1,1,1,2,2,3,3,3,4,4,4,5,5
    dc.b    6,6,6,7,7,7,8,8,9,9,9,10,10,10,11,11
    dc.b    -11,-11,-11,-10,-10,-10,-9,-9,-8,-8,-8,-7,-7,-7,-6,-6
    dc.b    -5,-5,-5,-4,-4,-4,-3,-3,-2,-2,-2,-1,-1,-1,0,0
    dc.b    0,0,0,1,1,2,2,3,3,3,4,4,5,5,6,6
    dc.b    7,7,7,8,8,9,9,10,10,10,11,11,12,12,13,13
    dc.b    -13,-13,-13,-12,-12,-11,-11,-10,-10,-10,-9,-9,-8,-8,-7,-7
    dc.b    -6,-6,-6,-5,-5,-4,-4,-3,-3,-3,-2,-2,-1,-1,0,0
    dc.b    0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7
    dc.b    8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15
    dc.b    -15,-15,-14,-14,-13,-13,-12,-12,-11,-11,-10,-10,-9,-9,-8,-8
    dc.b    -7,-7,-6,-6,-5,-5,-4,-4,-3,-3,-2,-2,-1,-1,0,0
    dc.b    0,0,1,1,2,2,3,3,4,5,5,6,6,7,7,8
    dc.b    9,9,10,10,11,11,12,12,13,14,14,15,15,16,16,17
    dc.b    -17,-17,-16,-16,-15,-15,-14,-13,-13,-12,-12,-11,-11,-10,-10,-9
    dc.b    -8,-8,-7,-7,-6,-6,-5,-4,-4,-3,-3,-2,-2,-1,-1,0
    dc.b    0,0,1,1,2,3,3,4,5,5,6,6,7,8,8,9
    dc.b    10,10,11,11,12,13,13,14,15,15,16,16,17,18,18,19
    dc.b    -19,-19,-18,-18,-17,-16,-16,-15,-14,-14,-13,-13,-12,-11,-11,-10
    dc.b    -9,-9,-8,-8,-7,-6,-6,-5,-4,-4,-3,-3,-2,-1,-1,0
    dc.b    0,0,1,2,2,3,4,4,5,6,6,7,8,8,9,10
    dc.b    11,11,12,13,13,14,15,15,16,17,17,18,19,19,20,21
    dc.b    -21,-21,-20,-19,-19,-18,-17,-17,-16,-15,-15,-14,-13,-12,-12,-11
    dc.b    -10,-10,-9,-8,-8,-7,-6,-6,-5,-4,-4,-3,-2,-1,-1,0
    dc.b    0,0,1,2,3,3,4,5,6,6,7,8,9,9,10,11
    dc.b    12,12,13,14,15,15,16,17,18,18,19,20,21,21,22,23
    dc.b    -23,-23,-22,-21,-20,-20,-19,-18,-17,-17,-16,-15,-14,-14,-13,-12
    dc.b    -11,-11,-10,-9,-8,-8,-7,-6,-5,-5,-4,-3,-2,-2,-1,0
    dc.b    0,0,1,2,3,4,4,5,6,7,8,8,9,10,11,12
    dc.b    13,13,14,15,16,17,17,18,19,20,21,21,22,23,24,25
    dc.b    -25,-25,-24,-23,-22,-21,-21,-20,-19,-18,-17,-16,-16,-15,-14,-13
    dc.b    -12,-12,-11,-10,-9,-8,-8,-7,-6,-5,-4,-3,-3,-2,-1,0
    dc.b    0,0,1,2,3,4,5,6,7,7,8,9,10,11,12,13
    dc.b    14,14,15,16,17,18,19,20,21,21,22,23,24,25,26,27
    dc.b    -27,-27,-26,-25,-24,-23,-22,-21,-20,-20,-19,-18,-17,-16,-15,-14
    dc.b    -13,-13,-12,-11,-10,-9,-8,-7,-6,-6,-5,-4,-3,-2,-1,0
    dc.b    0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14
    dc.b    15,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29
    dc.b    -29,-28,-28,-27,-26,-25,-24,-23,-22,-21,-20,-19,-18,-17,-16,-15
    dc.b    -14,-13,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0

mt_VibratoRectTable:
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
    dc.b    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
    dc.b    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
    dc.b    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
    dc.b    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3
    dc.b    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3
    dc.b    -3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3
    dc.b    -3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3
    dc.b    5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5
    dc.b    5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5
    dc.b    -5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5
    dc.b    -5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5
    dc.b    7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7
    dc.b    7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7
    dc.b    -7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7
    dc.b    -7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7
    dc.b    9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9
    dc.b    9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9
    dc.b    -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9
    dc.b    -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9
    dc.b    11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11
    dc.b    11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11
    dc.b    -11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11
    dc.b    -11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11
    dc.b    13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13
    dc.b    13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13
    dc.b    -13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13
    dc.b    -13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13,-13
    dc.b    15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15
    dc.b    15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15
    dc.b    -15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15
    dc.b    -15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15
    dc.b    17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17
    dc.b    17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17
    dc.b    -17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17
    dc.b    -17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17,-17
    dc.b    19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19
    dc.b    19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19
    dc.b    -19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19
    dc.b    -19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19
    dc.b    21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21
    dc.b    21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21
    dc.b    -21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21
    dc.b    -21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21
    dc.b    23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23
    dc.b    23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23
    dc.b    -23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23
    dc.b    -23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23,-23
    dc.b    25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25
    dc.b    25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25
    dc.b    -25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25
    dc.b    -25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25,-25
    dc.b    27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27
    dc.b    27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27
    dc.b    -27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27
    dc.b    -27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27,-27
    dc.b    29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29
    dc.b    29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29
    dc.b    -29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29
    dc.b    -29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29,-29
    endc    ; ENABLE_SAWRECT


mt_PerFineTune:
    dc.w    mt_Tuning0-mt_PerFineTune,mt_Tuning1-mt_PerFineTune
    dc.w    mt_Tuning2-mt_PerFineTune,mt_Tuning3-mt_PerFineTune
    dc.w    mt_Tuning4-mt_PerFineTune,mt_Tuning5-mt_PerFineTune
    dc.w    mt_Tuning6-mt_PerFineTune,mt_Tuning7-mt_PerFineTune
    dc.w    mt_TuningM8-mt_PerFineTune,mt_TuningM7-mt_PerFineTune
    dc.w    mt_TuningM6-mt_PerFineTune,mt_TuningM5-mt_PerFineTune
    dc.w    mt_TuningM4-mt_PerFineTune,mt_TuningM3-mt_PerFineTune
    dc.w    mt_TuningM2-mt_PerFineTune,mt_TuningM1-mt_PerFineTune

mt_PeriodTable:
mt_Tuning0: ; Tuning 0, Normal c-1 - b3
    dc.w    856,808,762,720,678,640,604,570,538,508,480,453
    dc.w    428,404,381,360,339,320,302,285,269,254,240,226
    dc.w    214,202,190,180,170,160,151,143,135,127,120,113
mt_Tuning1:
    dc.w    850,802,757,715,674,637,601,567,535,505,477,450
    dc.w    425,401,379,357,337,318,300,284,268,253,239,225
    dc.w    213,201,189,179,169,159,150,142,134,126,119,113
mt_Tuning2:
    dc.w    844,796,752,709,670,632,597,563,532,502,474,447
    dc.w    422,398,376,355,335,316,298,282,266,251,237,224
    dc.w    211,199,188,177,167,158,149,141,133,125,118,112
mt_Tuning3:
    dc.w    838,791,746,704,665,628,592,559,528,498,470,444
    dc.w    419,395,373,352,332,314,296,280,264,249,235,222
    dc.w    209,198,187,176,166,157,148,140,132,125,118,111
mt_Tuning4:
    dc.w    832,785,741,699,660,623,588,555,524,495,467,441
    dc.w    416,392,370,350,330,312,294,278,262,247,233,220
    dc.w    208,196,185,175,165,156,147,139,131,124,117,110
mt_Tuning5:
    dc.w    826,779,736,694,655,619,584,551,520,491,463,437
    dc.w    413,390,368,347,328,309,292,276,260,245,232,219
    dc.w    206,195,184,174,164,155,146,138,130,123,116,109
mt_Tuning6:
    dc.w    820,774,730,689,651,614,580,547,516,487,460,434
    dc.w    410,387,365,345,325,307,290,274,258,244,230,217
    dc.w    205,193,183,172,163,154,145,137,129,122,115,109
mt_Tuning7:
    dc.w    814,768,725,684,646,610,575,543,513,484,457,431
    dc.w    407,384,363,342,323,305,288,272,256,242,228,216
    dc.w    204,192,181,171,161,152,144,136,128,121,114,108
mt_TuningM8:
    dc.w    907,856,808,762,720,678,640,604,570,538,508,480
    dc.w    453,428,404,381,360,339,320,302,285,269,254,240
    dc.w    226,214,202,190,180,170,160,151,143,135,127,120
mt_TuningM7:
    dc.w    900,850,802,757,715,675,636,601,567,535,505,477
    dc.w    450,425,401,379,357,337,318,300,284,268,253,238
    dc.w    225,212,200,189,179,169,159,150,142,134,126,119
mt_TuningM6:
    dc.w    894,844,796,752,709,670,632,597,563,532,502,474
    dc.w    447,422,398,376,355,335,316,298,282,266,251,237
    dc.w    223,211,199,188,177,167,158,149,141,133,125,118
mt_TuningM5:
    dc.w    887,838,791,746,704,665,628,592,559,528,498,470
    dc.w    444,419,395,373,352,332,314,296,280,264,249,235
    dc.w    222,209,198,187,176,166,157,148,140,132,125,118
mt_TuningM4:
    dc.w    881,832,785,741,699,660,623,588,555,524,494,467
    dc.w    441,416,392,370,350,330,312,294,278,262,247,233
    dc.w    220,208,196,185,175,165,156,147,139,131,123,117
mt_TuningM3:
    dc.w    875,826,779,736,694,655,619,584,551,520,491,463
    dc.w    437,413,390,368,347,328,309,292,276,260,245,232
    dc.w    219,206,195,184,174,164,155,146,138,130,123,116
mt_TuningM2:
    dc.w    868,820,774,730,689,651,614,580,547,516,487,460
    dc.w    434,410,387,365,345,325,307,290,274,258,244,230
    dc.w    217,205,193,183,172,163,154,145,137,129,122,115
mt_TuningM1:
    dc.w    862,814,768,725,684,646,610,575,543,513,484,457
    dc.w    431,407,384,363,342,323,305,288,272,256,242,228
    dc.w    216,203,192,181,171,161,152,144,136,128,121,114

    ifeq    MINIMAL
MasterVolTab0:
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0
MasterVolTab1:
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    1
MasterVolTab2:
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
    dc.b    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
    dc.b    2
MasterVolTab3:
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1
    dc.b    1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2
    dc.b    2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
    dc.b    3
MasterVolTab4:
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    dc.b    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
    dc.b    2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
    dc.b    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3
    dc.b    4
MasterVolTab5:
    dc.b    0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1
    dc.b    1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2
    dc.b    2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3
    dc.b    3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4
    dc.b    5
MasterVolTab6:
    dc.b    0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1
    dc.b    1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2
    dc.b    3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4
    dc.b    4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5
    dc.b    6
MasterVolTab7:
    dc.b    0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1
    dc.b    1,1,1,2,2,2,2,2,2,2,2,2,3,3,3,3
    dc.b    3,3,3,3,3,4,4,4,4,4,4,4,4,4,5,5
    dc.b    5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6
    dc.b    7
MasterVolTab8:
    dc.b    0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1
    dc.b    2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3
    dc.b    4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5
    dc.b    6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7
    dc.b    8
MasterVolTab9:
    dc.b    0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,2
    dc.b    2,2,2,2,2,2,3,3,3,3,3,3,3,4,4,4
    dc.b    4,4,4,4,5,5,5,5,5,5,5,6,6,6,6,6
    dc.b    6,6,7,7,7,7,7,7,7,8,8,8,8,8,8,8
    dc.b    9
MasterVolTab10:
    dc.b    0,0,0,0,0,0,0,1,1,1,1,1,1,2,2,2
    dc.b    2,2,2,2,3,3,3,3,3,3,4,4,4,4,4,4
    dc.b    5,5,5,5,5,5,5,6,6,6,6,6,6,7,7,7
    dc.b    7,7,7,7,8,8,8,8,8,8,9,9,9,9,9,9
    dc.b    10
MasterVolTab11:
    dc.b    0,0,0,0,0,0,1,1,1,1,1,1,2,2,2,2
    dc.b    2,2,3,3,3,3,3,3,4,4,4,4,4,4,5,5
    dc.b    5,5,5,6,6,6,6,6,6,7,7,7,7,7,7,8
    dc.b    8,8,8,8,8,9,9,9,9,9,9,10,10,10,10,10
    dc.b    11
MasterVolTab12:
    dc.b    0,0,0,0,0,0,1,1,1,1,1,2,2,2,2,2
    dc.b    3,3,3,3,3,3,4,4,4,4,4,5,5,5,5,5
    dc.b    6,6,6,6,6,6,7,7,7,7,7,8,8,8,8,8
    dc.b    9,9,9,9,9,9,10,10,10,10,10,11,11,11,11,11
    dc.b    12
MasterVolTab13:
    dc.b    0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,3
    dc.b    3,3,3,3,4,4,4,4,4,5,5,5,5,5,6,6
    dc.b    6,6,6,7,7,7,7,7,8,8,8,8,8,9,9,9
    dc.b    9,9,10,10,10,10,10,11,11,11,11,11,12,12,12,12
    dc.b    13
MasterVolTab14:
    dc.b    0,0,0,0,0,1,1,1,1,1,2,2,2,2,3,3
    dc.b    3,3,3,4,4,4,4,5,5,5,5,5,6,6,6,6
    dc.b    7,7,7,7,7,8,8,8,8,8,9,9,9,9,10,10
    dc.b    10,10,10,11,11,11,11,12,12,12,12,12,13,13,13,13
    dc.b    14
MasterVolTab15:
    dc.b    0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3
    dc.b    3,3,4,4,4,4,5,5,5,5,6,6,6,6,7,7
    dc.b    7,7,7,8,8,8,8,9,9,9,9,10,10,10,10,11
    dc.b    11,11,11,11,12,12,12,12,13,13,13,13,14,14,14,14
    dc.b    15
MasterVolTab16:
    dc.b    0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3
    dc.b    4,4,4,4,5,5,5,5,6,6,6,6,7,7,7,7
    dc.b    8,8,8,8,9,9,9,9,10,10,10,10,11,11,11,11
    dc.b    12,12,12,12,13,13,13,13,14,14,14,14,15,15,15,15
    dc.b    16
MasterVolTab17:
    dc.b    0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3
    dc.b    4,4,4,5,5,5,5,6,6,6,6,7,7,7,7,8
    dc.b    8,8,9,9,9,9,10,10,10,10,11,11,11,11,12,12
    dc.b    12,13,13,13,13,14,14,14,14,15,15,15,15,16,16,16
    dc.b    17
MasterVolTab18:
    dc.b    0,0,0,0,1,1,1,1,2,2,2,3,3,3,3,4
    dc.b    4,4,5,5,5,5,6,6,6,7,7,7,7,8,8,8
    dc.b    9,9,9,9,10,10,10,10,11,11,11,12,12,12,12,13
    dc.b    13,13,14,14,14,14,15,15,15,16,16,16,16,17,17,17
    dc.b    18
MasterVolTab19:
    dc.b    0,0,0,0,1,1,1,2,2,2,2,3,3,3,4,4
    dc.b    4,5,5,5,5,6,6,6,7,7,7,8,8,8,8,9
    dc.b    9,9,10,10,10,10,11,11,11,12,12,12,13,13,13,13
    dc.b    14,14,14,15,15,15,16,16,16,16,17,17,17,18,18,18
    dc.b    19
MasterVolTab20:
    dc.b    0,0,0,0,1,1,1,2,2,2,3,3,3,4,4,4
    dc.b    5,5,5,5,6,6,6,7,7,7,8,8,8,9,9,9
    dc.b    10,10,10,10,11,11,11,12,12,12,13,13,13,14,14,14
    dc.b    15,15,15,15,16,16,16,17,17,17,18,18,18,19,19,19
    dc.b    20
MasterVolTab21:
    dc.b    0,0,0,0,1,1,1,2,2,2,3,3,3,4,4,4
    dc.b    5,5,5,6,6,6,7,7,7,8,8,8,9,9,9,10
    dc.b    10,10,11,11,11,12,12,12,13,13,13,14,14,14,15,15
    dc.b    15,16,16,16,17,17,17,18,18,18,19,19,19,20,20,20
    dc.b    21
MasterVolTab22:
    dc.b    0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5
    dc.b    5,5,6,6,6,7,7,7,8,8,8,9,9,9,10,10
    dc.b    11,11,11,12,12,12,13,13,13,14,14,14,15,15,15,16
    dc.b    16,16,17,17,17,18,18,18,19,19,19,20,20,20,21,21
    dc.b    22
MasterVolTab23:
    dc.b    0,0,0,1,1,1,2,2,2,3,3,3,4,4,5,5
    dc.b    5,6,6,6,7,7,7,8,8,8,9,9,10,10,10,11
    dc.b    11,11,12,12,12,13,13,14,14,14,15,15,15,16,16,16
    dc.b    17,17,17,18,18,19,19,19,20,20,20,21,21,21,22,22
    dc.b    23
MasterVolTab24:
    dc.b    0,0,0,1,1,1,2,2,3,3,3,4,4,4,5,5
    dc.b    6,6,6,7,7,7,8,8,9,9,9,10,10,10,11,11
    dc.b    12,12,12,13,13,13,14,14,15,15,15,16,16,16,17,17
    dc.b    18,18,18,19,19,19,20,20,21,21,21,22,22,22,23,23
    dc.b    24
MasterVolTab25:
    dc.b    0,0,0,1,1,1,2,2,3,3,3,4,4,5,5,5
    dc.b    6,6,7,7,7,8,8,8,9,9,10,10,10,11,11,12
    dc.b    12,12,13,13,14,14,14,15,15,16,16,16,17,17,17,18
    dc.b    18,19,19,19,20,20,21,21,21,22,22,23,23,23,24,24
    dc.b    25
MasterVolTab26:
    dc.b    0,0,0,1,1,2,2,2,3,3,4,4,4,5,5,6
    dc.b    6,6,7,7,8,8,8,9,9,10,10,10,11,11,12,12
    dc.b    13,13,13,14,14,15,15,15,16,16,17,17,17,18,18,19
    dc.b    19,19,20,20,21,21,21,22,22,23,23,23,24,24,25,25
    dc.b    26
MasterVolTab27:
    dc.b    0,0,0,1,1,2,2,2,3,3,4,4,5,5,5,6
    dc.b    6,7,7,8,8,8,9,9,10,10,10,11,11,12,12,13
    dc.b    13,13,14,14,15,15,16,16,16,17,17,18,18,18,19,19
    dc.b    20,20,21,21,21,22,22,23,23,24,24,24,25,25,26,26
    dc.b    27
MasterVolTab28:
    dc.b    0,0,0,1,1,2,2,3,3,3,4,4,5,5,6,6
    dc.b    7,7,7,8,8,9,9,10,10,10,11,11,12,12,13,13
    dc.b    14,14,14,15,15,16,16,17,17,17,18,18,19,19,20,20
    dc.b    21,21,21,22,22,23,23,24,24,24,25,25,26,26,27,27
    dc.b    28
MasterVolTab29:
    dc.b    0,0,0,1,1,2,2,3,3,4,4,4,5,5,6,6
    dc.b    7,7,8,8,9,9,9,10,10,11,11,12,12,13,13,14
    dc.b    14,14,15,15,16,16,17,17,18,18,19,19,19,20,20,21
    dc.b    21,22,22,23,23,24,24,24,25,25,26,26,27,27,28,28
    dc.b    29
MasterVolTab30:
    dc.b    0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7
    dc.b    7,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14
    dc.b    15,15,15,16,16,17,17,18,18,19,19,20,20,21,21,22
    dc.b    22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29
    dc.b    30
MasterVolTab31:
    dc.b    0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7
    dc.b    7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15
    dc.b    15,15,16,16,17,17,18,18,19,19,20,20,21,21,22,22
    dc.b    23,23,24,24,25,25,26,26,27,27,28,28,29,29,30,30
    dc.b    31
MasterVolTab32:
    dc.b    0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7
    dc.b    8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15
    dc.b    16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23
    dc.b    24,24,25,25,26,26,27,27,28,28,29,29,30,30,31,31
    dc.b    32
MasterVolTab33:
    dc.b    0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7
    dc.b    8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15
    dc.b    16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24
    dc.b    24,25,25,26,26,27,27,28,28,29,29,30,30,31,31,32
    dc.b    33
MasterVolTab34:
    dc.b    0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7
    dc.b    8,9,9,10,10,11,11,12,12,13,13,14,14,15,15,16
    dc.b    17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24
    dc.b    25,26,26,27,27,28,28,29,29,30,30,31,31,32,32,33
    dc.b    34
MasterVolTab35:
    dc.b    0,0,1,1,2,2,3,3,4,4,5,6,6,7,7,8
    dc.b    8,9,9,10,10,11,12,12,13,13,14,14,15,15,16,16
    dc.b    17,18,18,19,19,20,20,21,21,22,22,23,24,24,25,25
    dc.b    26,26,27,27,28,28,29,30,30,31,31,32,32,33,33,34
    dc.b    35
MasterVolTab36:
    dc.b    0,0,1,1,2,2,3,3,4,5,5,6,6,7,7,8
    dc.b    9,9,10,10,11,11,12,12,13,14,14,15,15,16,16,17
    dc.b    18,18,19,19,20,20,21,21,22,23,23,24,24,25,25,26
    dc.b    27,27,28,28,29,29,30,30,31,32,32,33,33,34,34,35
    dc.b    36
MasterVolTab37:
    dc.b    0,0,1,1,2,2,3,4,4,5,5,6,6,7,8,8
    dc.b    9,9,10,10,11,12,12,13,13,14,15,15,16,16,17,17
    dc.b    18,19,19,20,20,21,21,22,23,23,24,24,25,26,26,27
    dc.b    27,28,28,29,30,30,31,31,32,32,33,34,34,35,35,36
    dc.b    37
MasterVolTab38:
    dc.b    0,0,1,1,2,2,3,4,4,5,5,6,7,7,8,8
    dc.b    9,10,10,11,11,12,13,13,14,14,15,16,16,17,17,18
    dc.b    19,19,20,20,21,21,22,23,23,24,24,25,26,26,27,27
    dc.b    28,29,29,30,30,31,32,32,33,33,34,35,35,36,36,37
    dc.b    38
MasterVolTab39:
    dc.b    0,0,1,1,2,3,3,4,4,5,6,6,7,7,8,9
    dc.b    9,10,10,11,12,12,13,14,14,15,15,16,17,17,18,18
    dc.b    19,20,20,21,21,22,23,23,24,24,25,26,26,27,28,28
    dc.b    29,29,30,31,31,32,32,33,34,34,35,35,36,37,37,38
    dc.b    39
MasterVolTab40:
    dc.b    0,0,1,1,2,3,3,4,5,5,6,6,7,8,8,9
    dc.b    10,10,11,11,12,13,13,14,15,15,16,16,17,18,18,19
    dc.b    20,20,21,21,22,23,23,24,25,25,26,26,27,28,28,29
    dc.b    30,30,31,31,32,33,33,34,35,35,36,36,37,38,38,39
    dc.b    40
MasterVolTab41:
    dc.b    0,0,1,1,2,3,3,4,5,5,6,7,7,8,8,9
    dc.b    10,10,11,12,12,13,14,14,15,16,16,17,17,18,19,19
    dc.b    20,21,21,22,23,23,24,24,25,26,26,27,28,28,29,30
    dc.b    30,31,32,32,33,33,34,35,35,36,37,37,38,39,39,40
    dc.b    41
MasterVolTab42:
    dc.b    0,0,1,1,2,3,3,4,5,5,6,7,7,8,9,9
    dc.b    10,11,11,12,13,13,14,15,15,16,17,17,18,19,19,20
    dc.b    21,21,22,22,23,24,24,25,26,26,27,28,28,29,30,30
    dc.b    31,32,32,33,34,34,35,36,36,37,38,38,39,40,40,41
    dc.b    42
MasterVolTab43:
    dc.b    0,0,1,2,2,3,4,4,5,6,6,7,8,8,9,10
    dc.b    10,11,12,12,13,14,14,15,16,16,17,18,18,19,20,20
    dc.b    21,22,22,23,24,24,25,26,26,27,28,28,29,30,30,31
    dc.b    32,32,33,34,34,35,36,36,37,38,38,39,40,40,41,42
    dc.b    43
MasterVolTab44:
    dc.b    0,0,1,2,2,3,4,4,5,6,6,7,8,8,9,10
    dc.b    11,11,12,13,13,14,15,15,16,17,17,18,19,19,20,21
    dc.b    22,22,23,24,24,25,26,26,27,28,28,29,30,30,31,32
    dc.b    33,33,34,35,35,36,37,37,38,39,39,40,41,41,42,43
    dc.b    44
MasterVolTab45:
    dc.b    0,0,1,2,2,3,4,4,5,6,7,7,8,9,9,10
    dc.b    11,11,12,13,14,14,15,16,16,17,18,18,19,20,21,21
    dc.b    22,23,23,24,25,26,26,27,28,28,29,30,30,31,32,33
    dc.b    33,34,35,35,36,37,37,38,39,40,40,41,42,42,43,44
    dc.b    45
MasterVolTab46:
    dc.b    0,0,1,2,2,3,4,5,5,6,7,7,8,9,10,10
    dc.b    11,12,12,13,14,15,15,16,17,17,18,19,20,20,21,22
    dc.b    23,23,24,25,25,26,27,28,28,29,30,30,31,32,33,33
    dc.b    34,35,35,36,37,38,38,39,40,40,41,42,43,43,44,45
    dc.b    46
MasterVolTab47:
    dc.b    0,0,1,2,2,3,4,5,5,6,7,8,8,9,10,11
    dc.b    11,12,13,13,14,15,16,16,17,18,19,19,20,21,22,22
    dc.b    23,24,24,25,26,27,27,28,29,30,30,31,32,33,33,34
    dc.b    35,35,36,37,38,38,39,40,41,41,42,43,44,44,45,46
    dc.b    47
MasterVolTab48:
    dc.b    0,0,1,2,3,3,4,5,6,6,7,8,9,9,10,11
    dc.b    12,12,13,14,15,15,16,17,18,18,19,20,21,21,22,23
    dc.b    24,24,25,26,27,27,28,29,30,30,31,32,33,33,34,35
    dc.b    36,36,37,38,39,39,40,41,42,42,43,44,45,45,46,47
    dc.b    48
MasterVolTab49:
    dc.b    0,0,1,2,3,3,4,5,6,6,7,8,9,9,10,11
    dc.b    12,13,13,14,15,16,16,17,18,19,19,20,21,22,22,23
    dc.b    24,25,26,26,27,28,29,29,30,31,32,32,33,34,35,35
    dc.b    36,37,38,39,39,40,41,42,42,43,44,45,45,46,47,48
    dc.b    49
MasterVolTab50:
    dc.b    0,0,1,2,3,3,4,5,6,7,7,8,9,10,10,11
    dc.b    12,13,14,14,15,16,17,17,18,19,20,21,21,22,23,24
    dc.b    25,25,26,27,28,28,29,30,31,32,32,33,34,35,35,36
    dc.b    37,38,39,39,40,41,42,42,43,44,45,46,46,47,48,49
    dc.b    50
MasterVolTab51:
    dc.b    0,0,1,2,3,3,4,5,6,7,7,8,9,10,11,11
    dc.b    12,13,14,15,15,16,17,18,19,19,20,21,22,23,23,24
    dc.b    25,26,27,27,28,29,30,31,31,32,33,34,35,35,36,37
    dc.b    38,39,39,40,41,42,43,43,44,45,46,47,47,48,49,50
    dc.b    51
MasterVolTab52:
    dc.b    0,0,1,2,3,4,4,5,6,7,8,8,9,10,11,12
    dc.b    13,13,14,15,16,17,17,18,19,20,21,21,22,23,24,25
    dc.b    26,26,27,28,29,30,30,31,32,33,34,34,35,36,37,38
    dc.b    39,39,40,41,42,43,43,44,45,46,47,47,48,49,50,51
    dc.b    52
MasterVolTab53:
    dc.b    0,0,1,2,3,4,4,5,6,7,8,9,9,10,11,12
    dc.b    13,14,14,15,16,17,18,19,19,20,21,22,23,24,24,25
    dc.b    26,27,28,28,29,30,31,32,33,33,34,35,36,37,38,38
    dc.b    39,40,41,42,43,43,44,45,46,47,48,48,49,50,51,52
    dc.b    53
MasterVolTab54:
    dc.b    0,0,1,2,3,4,5,5,6,7,8,9,10,10,11,12
    dc.b    13,14,15,16,16,17,18,19,20,21,21,22,23,24,25,26
    dc.b    27,27,28,29,30,31,32,32,33,34,35,36,37,37,38,39
    dc.b    40,41,42,43,43,44,45,46,47,48,48,49,50,51,52,53
    dc.b    54
MasterVolTab55:
    dc.b    0,0,1,2,3,4,5,6,6,7,8,9,10,11,12,12
    dc.b    13,14,15,16,17,18,18,19,20,21,22,23,24,24,25,26
    dc.b    27,28,29,30,30,31,32,33,34,35,36,36,37,38,39,40
    dc.b    41,42,42,43,44,45,46,47,48,48,49,50,51,52,53,54
    dc.b    55
MasterVolTab56:
    dc.b    0,0,1,2,3,4,5,6,7,7,8,9,10,11,12,13
    dc.b    14,14,15,16,17,18,19,20,21,21,22,23,24,25,26,27
    dc.b    28,28,29,30,31,32,33,34,35,35,36,37,38,39,40,41
    dc.b    42,42,43,44,45,46,47,48,49,49,50,51,52,53,54,55
    dc.b    56
MasterVolTab57:
    dc.b    0,0,1,2,3,4,5,6,7,8,8,9,10,11,12,13
    dc.b    14,15,16,16,17,18,19,20,21,22,23,24,24,25,26,27
    dc.b    28,29,30,31,32,32,33,34,35,36,37,38,39,40,40,41
    dc.b    42,43,44,45,46,47,48,48,49,50,51,52,53,54,55,56
    dc.b    57
MasterVolTab58:
    dc.b    0,0,1,2,3,4,5,6,7,8,9,9,10,11,12,13
    dc.b    14,15,16,17,18,19,19,20,21,22,23,24,25,26,27,28
    dc.b    29,29,30,31,32,33,34,35,36,37,38,38,39,40,41,42
    dc.b    43,44,45,46,47,48,48,49,50,51,52,53,54,55,56,57
    dc.b    58
MasterVolTab59:
    dc.b    0,0,1,2,3,4,5,6,7,8,9,10,11,11,12,13
    dc.b    14,15,16,17,18,19,20,21,22,23,23,24,25,26,27,28
    dc.b    29,30,31,32,33,34,35,35,36,37,38,39,40,41,42,43
    dc.b    44,45,46,47,47,48,49,50,51,52,53,54,55,56,57,58
    dc.b    59
MasterVolTab60:
    dc.b    0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14
    dc.b    15,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29
    dc.b    30,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44
    dc.b    45,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59
    dc.b    60
MasterVolTab61:
    dc.b    0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14
    dc.b    15,16,17,18,19,20,20,21,22,23,24,25,26,27,28,29
    dc.b    30,31,32,33,34,35,36,37,38,39,40,40,41,42,43,44
    dc.b    45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60
    dc.b    61
MasterVolTab62:
    dc.b    0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14
    dc.b    15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30
    dc.b    31,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45
    dc.b    46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61
    dc.b    62
MasterVolTab63:
    dc.b    0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14
    dc.b    15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30
    dc.b    31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46
    dc.b    47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62
    dc.b    63
MasterVolTab64:
    dc.b    0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
    dc.b    16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
    dc.b    32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47
    dc.b    48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63
    dc.b    64
    even
    endc    ; !MINIMAL


    ifd SDATA

    section __MERGED,bss

mt_chan1:
    ds.b    n_sizeof
mt_chan2:
    ds.b    n_sizeof
mt_chan3:
    ds.b    n_sizeof
mt_chan4:
    ds.b    n_sizeof

mt_SampleStarts:
    ds.l    31

mt_mod:
    ds.l    1
mt_oldLev6:
    ds.l    1
mt_timerval:
    ds.l    1
mt_oldtimers:
    ds.b    4
mt_Lev6Int:
    ds.l    1
mt_Lev6Ena:
    ds.w    1
mt_PatternPos:
    ds.w    1
mt_PBreakPos:
    ds.w    1
mt_PosJumpFlag:
    ds.b    1
mt_PBreakFlag:
    ds.b    1
mt_Speed:
    ds.b    1
mt_Counter:
    ds.b    1
mt_SongPos:
    ds.b    1
mt_PattDelTime:
    ds.b    1
mt_PattDelTime2:
    ds.b    1

    ifeq    MINIMAL
mt_SilCntValid:
    ds.b    1
mt_MasterVolTab:
    ds.l    1
    endc

    xdef    _mt_Enable
_mt_Enable:
mt_Enable:
    ds.b    1

    xdef    _mt_E8Trigger
_mt_E8Trigger:
mt_E8Trigger:
    ds.b    1

    ifeq    MINIMAL
    xdef    _mt_MusicChannels
_mt_MusicChannels:
mt_MusicChannels:
    ds.b    1

    xdef    _mt_SongEnd
_mt_SongEnd:
mt_SongEnd:
    ds.b    1
    endc    ; !MINIMAL

    else    ; !SDATA : single section with local base register

    rsreset
mt_chan1    rs.b    n_sizeof
mt_chan2    rs.b    n_sizeof
mt_chan3    rs.b    n_sizeof
mt_chan4    rs.b    n_sizeof
mt_SampleStarts rs.l    31
mt_mod      rs.l    1
mt_oldLev6  rs.l    1
mt_timerval rs.l    1
mt_oldtimers    rs.b    4
mt_Lev6Int  rs.l    1
mt_Lev6Ena  rs.w    1
mt_PatternPos   rs.w    1
mt_PBreakPos    rs.w    1
mt_PosJumpFlag  rs.b    1
mt_PBreakFlag   rs.b    1
mt_Speed    rs.b    1
mt_Counter  rs.b    1
mt_SongPos  rs.b    1
mt_PattDelTime  rs.b    1
mt_PattDelTime2 rs.b    1
    ifeq    MINIMAL
mt_SilCntValid  rs.b    1
mt_MasterVolTab rs.l    1
    endc
mt_Enable   rs.b    1       ; exported as _mt_Enable
mt_E8Trigger    rs.b    1       ; exported as _mt_E8Trigger
mt_MusicChannels rs.b   1       ; exported as _mt_MusicChannels
mt_SongEnd  rs.b    1       ; exported as _mt_SongEnd

mt_data:
    ds.b    mt_Enable
    xdef    _mt_Enable
_mt_Enable:
    ds.b    1
    xdef    _mt_E8Trigger
_mt_E8Trigger:
    ds.b    1
    ifeq    MINIMAL
    xdef    _mt_MusicChannels
_mt_MusicChannels:
    ds.b    1
    xdef    _mt_SongEnd
_mt_SongEnd:
    ds.b    1
    endc    ; !MINIMAL

    endc    ; SDATA/!SDATA

    end

Verilog

The whole system as Verilog, without comments:

file what
verilog/top.v the system: the 68000, the block RAM with the program and the module, Paula, CIA-B, the mixer and the filter (module top). This is what TARGET=verilator builds.
verilog/tb.cpp its C++ testbench: the frames into a .wav
verilog/board/top.v, verilog/board/st_arty.v the bitstream's design: the system, the clock divider, the UART, the board's pins
verilog/board_dsp/top.v, verilog/board_dsp/st_dsp_arty.v the DSP system's bitstream: the system, the five effects, the line out, the Ethernet probe (bit/top_dsp.bit)

How it was checked

References

Paula, the custom registers, the 8520 CIA Amiga Hardware Reference Manual: chapter 5, audio, chapter 7 (interrupts, DMA), appendix A (registers), appendix F (the 8520) -- the Amiga Developer CD v2.1
Paula and the CIA in Verilog (transcribed) MiSTer Minimig-AGA: paula_audio_channel.v, agnus_audiodma.v, paula_intcontroller.v, cia_timerb.v, cia_int.v
Paula in an emulator Clock Signal, Machines/Amiga/Audio.cpp; Amiga Machine Code Letter VIII -- Audio
The replay routine ptplayer on Aminet (public domain); the original SoundTracker / NoiseTracker / ProTracker replays: RSE-UTS on Aminet
The module format "Sound Tracker Module File Format", Steven W Green, 1990 (textfiles.com)
The module "Neon Techno" by Drozerix, The Mod Archive (public domain)
The 68000 Motorola M68000 Family Programmer's Reference Manual; the reference model Musashi
The assembler vasm
The programmer openFPGALoader (in the archive, with its source)
The effects' models the C++ models of the compressor, chorus, flanger, delay and reverb (identified from the reference devices' captures and impulse responses), copied unchanged beside each l8 unit (audio/dsp/filters/*/ref)
The DSP48E1 Xilinx UG479, 7 Series DSP48E1 Slice User Guide
mu-law ITU-T G.711; Sun Microsystems' g711.c (linear2ulaw)
The windowed-sinc read, the polyphase resampler J. O. Smith, Digital Audio Resampling (ccrma.stanford.edu/~jos/resample)

[back]