Sample playback

The game has two samples: "Welcome to Populous" at startup, and a laughing voice when the player loses. The game stores its data as 8-bit unsigned PCM data.

The sample playback code uses three consecutive 256-byte tables starting at $3800 to convert the 8-bit data into SN76489 volume commands. Each sample is used as an index into each table in turn, which then yields the byte written to the SN76489, encoding both the channel selection and volume. By combining channels with different volume levels, it is able to generate 45 different output levels to represent the wave, which corresponds to 5.5 bits of resolution.

The tables seem to be built assuming that the SN76489 response is linear - the combined output volume settings are linear with respect to the PCM data. The tables also invert the waveform.

The sample player function starts at offset $36db. It disables interrupts, then silences the PSG, then waits for a short time (~27ms), then plays the sample. It sets the tone channels to half-wavelength $3f1 (110Hz) but then sets them to $005 (22kHz, effectively silence) immediately afterwards and then before every volume change. It's not clear what the reason for this is.

The sample player supports playing samples that span banks of ROM, although this feature is not used. It invokes a code path that takes some extra time so there would be a small glitch in the output.

Welcome to Populous

The sample data starts at $2c000 and $2da0 samples are played with 672 CPU cycles per sample, giving an effective sampling rate of 5278Hz on a PAL system.

Seemingly due to a mistake when building the game, the sample data includes a VOC file header. This is played as samples, giving some noise in the output at the start of the sample. It also plays one byte too many, giving an extra click at the end of the sample.

The sample data seems to be somewhat processed, but mostly linear. It does not use the full range of output, so the result is rather quiet.


The sample data is in two parts. The first starts at $34000 and $811 samples are played, first with 624 cycles per sample, then 656, 688, 720; this corresponds to 5684Hz, 5407Hz, 5155Hz and 4926Hz respectively. Finally, the second part of the sample (starting at $34bf9 with $e62 samples) is played with 784 cycles per sample (4524Hz).

The sample data seems to have been processed quite differently to the "Welcome to Populous" sample:

Player source code

.define PSG $7f
.define PAGING_REGISTER $ffff
.define BANK_START $8000
.define BANK_END $c000
.define LOOKUP_TABLE_BASE $3800
.define CONTROLLER_1_BUTTON_1 %00010000
.define CONTROLLER_1_BUTTON_2 %00100000

PlaySample: ; $36db
; Arguments:
; hl = data
; d = delay paramter (higher = lower sample rate)
    di ; Stop interrupts
    call SilencePSG

    ; Set tones to 0
    ld a, %10000000 ; Channel 0 tone 0
    out (PSG), a
    xor a
    out (PSG), a
    ld a, %10100000 ; Channel 1 tone 0
    out (PSG), a
    xor a
    out (PSG), a
    ld a, %11000000 ; Channel 2 tone 0
    out (PSG), a
    xor a
    out (PSG), a

    ; Transfer data pointer to hl' and pre-decrement it
    push hl
      pop hl
      dec hl

    ; Delay proportional to d
    ld e, d
--: ld a, $ff
-:  dec a
    jr nz, -
    dec e
    jr nz, --

    ; Set tones to %1111110001 = $3f1 = 110Hz
    ; This seems unnecessary...
    ld e, %00111111
    ld a, %10000001 ; Channel 0
    out (PSG), a
    ld a, e
    out (PSG), a
    ld a, %10100001 ; Channel 1
    out (PSG), a
    ld a, e
    out (PSG), a
    ld a, %11000001 ; Channel 2
    out (PSG), a
    ld a, e
    out (PSG), a

-:  ; Main sample loop
    ; Set tones to %0000000101 = $005
    ld e, %00000000
    ld a, %10000101 ; Channel 0
    out (PSG), a
    ld a, e
    out (PSG), a
    ld a, %10100101 ; Channel 1
    out (PSG), a
    ld a, e
    out (PSG), a
    ld a, %11100101 ; Channel 2
    out (PSG), a
    ld a, e
    out (PSG), a
      inc hl ; Next sample
      ld a, <BANK_END ; Check if we got to the end of the bank
      cp h
      ld a, (hl) ; Get the sample
      jr nz, + ; If not at the end of the bank, we are done
      push af
        ld a, ($d3dc)
        inc a ;
        ld ($d3dc), a
        ld (PAGING_REGISTER), a
        ld hl, BANK_START
        ld a, (hl) ; re-get sample
      pop af
+:  exx

    ; Look up the three PSG data values to use for the sample value
    ld l, a ; Index by sample
    ld a, (hl) ; Get adjustment value
    out (PSG), a
    inc h ; Next section of the lookup
    ld a, (hl)
    out (PSG), a
    inc h ; And again
    ld a, (hl)
    out (PSG), a

    ; Read the cotrollers and OR button 1 or 2 into this RAM location
    ld a, ($d20a)
    ld e, a
    in a, (CONTROLLER_PORT_A) ; Read the controllers
    cpl ; Invert so 1 = pressed
    or e
    ld ($d20a), a

    ld e, d ; Delay
-:  dec e
    jr nz, -

    dec bc ; Decrement byte counter
    ld a, b
    or c
    jr nz, - ; Loop until it reaches 0
    ; Then fall through and return

SilencePSG: ; $376d
    ld a, %10111111 ; Channel 1 volume f
    out (PSG),a
    ld a, %11011111 ; Channel 2 volume f
    out (PSG), a
    ld a, %11111111 ; Channel 3 volume f
    out (PSG), a
    ld a, %10011111 ; Channel 0 volume f
    out (PSG), a

Laugh: ; $377E
    ld d, $14 ; Rate
    ld a, $0d ; Bank ($34000)
-:  ld hl, $8000 ; Sample 1 at $34000
    ld bc, $0811 ; Length
    push de
      call PlaySample
    pop de
    inc d ; Slow down playback
    inc d
    ld a, d
    cp $1c
    jr nz, - ; And repeat slower

    ld hl, $8bf9 ; Sample 2 at $34bf9
    ld bc, $0e62 ; Length
    ld d, $1e ; Rate
    call PlaySample

Return to top