Skip to content

Four-Channel Music

A complete eight-row sequencer for four Paula voices, with explicit startup, channel ownership and hold semantics. A small step toward music playback.

Four voices can turn one small waveform into a tune. The useful next step after playing a sample is to give each voice its own sequence of pitches, then advance those sequences on a clock.

This caller is deliberately small: eight rows, four word-sized note indices per row, one shared looping waveform and six updates between row starts. A zero holds the previous note. It does not mean silence. The complete data is included, so the player never wanders past an unfinished pattern.

Run and connect it

Target: A500 OCS PAL with Kickstart 1.3, booted from the sample’s ADF. Press Fire on physical joystick port 2 to start. Reset to leave; this caller does not return to AmigaOS.

Build the sample with Asm198x and Build198x:

Shell
make -C commodore-amiga/patterns/assembly/audio/four-channel-music BUILD198X=/path/to/build198x

The caller owns the custom chips, all four audio channels and their DMA/interrupt requests. It disables audio modulation. Samples must be word-aligned in Chip RAM and stay there while DMA can read them. Before reusing music_init in another caller, turn audio DMA off and allow at least two sample periods for it to settle. A program sharing AmigaOS uses its audio facilities instead of this exclusive caller.

Start, wait and advance

demo.asm
; Boot ADF, A500 OCS PAL/Kickstart1.3. Reset to leave. No OS-return claim.
    section code,code_c
start:
    lea $dff000,a5
    move.w #$7fff,$09a(a5)
    move.w #$7fff,$09c(a5)
    move.w #$7fff,$096(a5)
    bclr #7,$bfe201
    move.w #$cafe,ready
wait_for_fire:
    bsr wait_frame
    btst #7,$bfe001
    bne wait_for_fire
    bsr music_init
main_loop:
    bsr wait_frame
    bsr music_tick
    addq.w #1,updates
    bra main_loop
signature: dc.b "198XMUSICAUDIT!",0
    even
ready: dc.w 0
updates: dc.w 0
    include "music.inc"
    include "../../framework/game-loop/frame.inc"
program_end:

The main loop calls the existing vertical-blank wait, then music_tick. This is a subroutine returning with RTS, not an interrupt handler. An IRQ dispatcher would have to preserve working registers, acknowledge its interrupt and return with RTE itself.

One channel block, four copies

Paula’s channel blocks start at custom offsets $A0, $B0, $C0 and $D0. Within each block, location is at +0, word length at +4, period at +6 and volume at +8. Using one consistent base avoids accidentally writing disk or display registers.

music_init configures every channel before enabling DMA. music_tick changes period and volume on a note; it leaves the looping sample running. A held row leaves that voice alone. There is no attack envelope, instrument retrigger or effect command hidden behind a note number.

Four-voice sequencer and complete pattern

music.inc
; Four continuous waveform voices; a bounded sequencer, not a MOD decoder.
; A500 OCS PAL; caller owns all four Paula channels, their interrupt requests
; and audio-modulation bits. DMA must be off and settled before music_init.
; The instrument and routine state are writable/readable Chip RAM for playback.
; music_init changes A2/A5/D5/CCR; music_tick changes A0/A1/A2/D0/D5/D7/CCR.
; Call tick once per chosen update; six intervals separate row starts.
; 0 means HOLD, 1..24 index periods. The built-in table is complete and bounded.
MUSIC_TEMPO equ 6
MUSIC_ROWS equ 8
music_init:
    lea $dff000,a5
    move.w #$0780,$09a(a5)       ; no audio IRQ consumer in this polling caller
    move.w #$0780,$09c(a5)
    move.w #$00ff,$09e(a5)       ; disable audio period/volume modulation
    clr.w music_row
    move.w #1,music_timer
    lea $0a0(a5),a2
    moveq #3,d5
music_init_voice:
    move.l #instrument,(a2)      ; LC at channel base +0
    move.w #instrument_words,4(a2)
    move.w #856,6(a2)
    clr.w 8(a2)                 ; silent until the first note on this voice
    adda.w #$10,a2
    dbf d5,music_init_voice
    move.w #$820f,$096(a5)       ; start only after EVERY channel is configured
    rts

music_tick:
    subq.w #1,music_timer
    bne music_tick_done
    move.w #MUSIC_TEMPO,music_timer
    move.w music_row,d7
    mulu #8,d7                  ; row = four WORD note indices
    lea music_pattern,a0
    adda.w d7,a0
    lea $0a0(a5),a2
    moveq #3,d5
music_next_voice:
    move.w (a0)+,d0
    beq music_hold_voice
    subq.w #1,d0               ; note1 is the first table entry
    add.w d0,d0
    lea music_periods,a1
    move.w 0(a1,d0.w),6(a2)     ; PERIOD at channel base +6
    move.w #64,8(a2)            ; VOLUME at channel base +8
music_hold_voice:
    adda.w #$10,a2
    dbf d5,music_next_voice
    addq.w #1,music_row
    cmp.w #MUSIC_ROWS,music_row
    blo music_tick_done
    clr.w music_row
music_tick_done:
    rts

; Caller waits two sample periods after this before restarting/reusing data.
; A5 remains CUSTOM; other registers unchanged, CCR changes.
music_stop:
    move.w #$000f,$096(a5)
    rts

music_row: dc.w 0
music_timer: dc.w 0
music_periods:
    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
music_pattern:
    dc.w 13,17,20,1
    dc.w 17,0,0,0
    dc.w 20,13,17,1
    dc.w 17,0,0,0
    dc.w 13,17,20,1
    dc.w 17,0,0,0
    dc.w 20,13,17,1
    dc.w 17,0,0,0
    even
instrument: dc.b 0,49,90,117,127,117,90,49,0,-49,-90,-117,-127,-117,-90,-49
instrument_words equ (*-instrument)/2

music_init changes A2/A5/D5 and condition codes. music_tick changes A0/A1/A2/D0/D5/D7 and condition codes. The wait also changes D6. The pattern and period table are read-only after loading; music_row and music_timer are writable words. Include each routine once and keep competing writers out of that state and those channels.

What its numbers mean

At 50 updates per second, six intervals between rows give about 8.3 rows per second. At 60, the same counter gives 10. A missed update stretches the music; a different clock requires a deliberate timing choice.

The period table uses familiar tracker period values, but they do not give an absolute pitch independently of the sample. Here each waveform cycle has 16 samples: nominal PAL pitch is approximately 3546895 / (period × 16). A recorded instrument with a different cycle length will tune differently. Lower periods play the data faster.

The four hardware channels have fixed stereo routing: 0/3 left and 1/2 right. Another voice needs a different resource plan, such as software mixing; it is not a fifth independent Paula channel.

Verification and limits

Code status: assembled and emulator-executed on the named configuration. The check cold-boots the ADF, starts through real joystick input, observes all four channels over every row and verifies holds, six-update timing and wrap. It also checks actual stereo PCM. A caller with the DMA enable deliberately removed must fail the same audio guard, despite still writing plausible period and volume registers.

Audit execution record · Scope and remaining checks

This proves the bounded waveform sequencer, not a MOD player. Loading a MOD adds a file decoder, sample ownership, loops, instrument rules, effects, order changes and their timing. This example gives us a readable place to begin those discussions without pretending they are already implemented.

Related: sample playback, one-shot samples and the shared sequencer discussion.