Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 161 additions & 109 deletions specs/tiny16-apu.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,114 +5,151 @@ tiny16 APU Specification
OVERVIEW
--------

5-channel audio synthesis with async sample generation.
4-channel unified audio synthesis with async sample generation, plus
memory-driven SFX and music playback.

Sample Rate: 44,100 Hz
Output: 32-bit float, mono
Channels: 2 pulse + 1 triangle + 1 noise + 1 wave
Channels: 4 unified channels (selectable waveform per channel)
Phase: 32-bit accumulator per channel
Frequency: 11-bit divisor (0-2047)
Volume: 4-bit master + 4-bit per channel
Envelope: 4-bit ADSR per channel
Length: 8-bit length (frames @ 60 Hz)
Sweep: Pulse-only frequency sweep


CHANNELS
--------

CH0, CH1 - Pulse Wave
CH0-CH3 - Unified Channels
Frequency: 11-bit (0-2047)
Volume: 4-bit (0-15)
Duty Cycle: 12.5%, 25%, 50%, 75%
Envelope: ADSR (4-bit each)
Sweep: Rate (4-bit), Direction, Shift (3-bit)
Length: 0-255 frames (60 Hz)
Use: Melody, harmony, leads
Waveforms: Selected per channel (see WAVEFORMS below)
Use: Melody, bass, drums, SFX

CH2 - Triangle Wave
Frequency: 11-bit (0-2047)
Volume: 4-bit (0-15)
Envelope: ADSR (4-bit each)
Length: 0-255 frames (60 Hz)
Use: Bass, sub-bass

CH3 - Noise (LFSR)
Period: 4-bit (0-15), lower = higher pitch
Volume: 4-bit (0-15)
Mode: Long (hissy) / Short (metallic)
Envelope: ADSR (4-bit each)
Length: 0-255 frames (60 Hz)
Use: Drums, explosions, SFX

CH4 - Wave (32-sample wavetable)
Frequency: 11-bit (0-2047)
Volume: 4-bit (0-15)
Envelope: ADSR (4-bit each)
Length: 0-255 frames (60 Hz)
Wave RAM: 32 x 4-bit samples
Use: Custom timbres, instruments, SFX


MMIO REGISTERS (0xBF40-0xBF8F)
MMIO REGISTERS (0xBF40-0xBF9F)
------------------------------

0xBF40 APU_CTRL RW bit 0: enable | bits 4-7: master volume
0xBF41 APU_STATUS R bits 0-4: channel active flags
0xBF41 APU_STATUS R bits 0-3: channel active flags

Channel Registers (stride = 6 bytes per channel):
FREQ_LO, FREQ_HI, VOL, CTRL, ENV_AD, ENV_SR

Channel 0 - Pulse 1:
0xBF42 CH0_FREQ_LO W frequency bits 0-7
0xBF43 CH0_FREQ_HI W frequency bits 8-10 (bits 0-2)
0xBF44 CH0_VOL RW bits 0-3: volume | bits 4-5: duty (00/01/10/11)
Channel 0 (0xBF42-0xBF47):
0xBF42 CH0_FREQ_LO RW frequency bits 0-7
0xBF43 CH0_FREQ_HI RW bits 0-2: frequency bits 8-10 | bits 4-6: waveform
0xBF44 CH0_VOL RW bits 0-3: volume
0xBF45 CH0_CTRL RW bit 0: enable | bit 1: trigger
0xBF46 CH0_ENV_AD RW hi nibble: attack | lo nibble: decay
0xBF47 CH0_ENV_SR RW hi nibble: sustain | lo nibble: release

Channel 1 (0xBF48-0xBF4D):
0xBF48 CH1_FREQ_LO RW frequency bits 0-7
0xBF49 CH1_FREQ_HI RW bits 0-2: frequency bits 8-10 | bits 4-6: waveform
0xBF4A CH1_VOL RW bits 0-3: volume
0xBF4B CH1_CTRL RW bit 0: enable | bit 1: trigger
0xBF4C CH1_ENV_AD RW attack/decay
0xBF4D CH1_ENV_SR RW sustain/release

Channel 2 (0xBF4E-0xBF53):
0xBF4E CH2_FREQ_LO RW frequency bits 0-7
0xBF4F CH2_FREQ_HI RW bits 0-2: frequency bits 8-10 | bits 4-6: waveform
0xBF50 CH2_VOL RW bits 0-3: volume
0xBF51 CH2_CTRL RW bit 0: enable | bit 1: trigger
0xBF52 CH2_ENV_AD RW attack/decay
0xBF53 CH2_ENV_SR RW sustain/release

Channel 3 (0xBF54-0xBF59):
0xBF54 CH3_FREQ_LO RW frequency bits 0-7
0xBF55 CH3_FREQ_HI RW bits 0-2: frequency bits 8-10 | bits 4-6: waveform
0xBF56 CH3_VOL RW bits 0-3: volume
0xBF57 CH3_CTRL RW bit 0: enable | bit 1: trigger
0xBF58 CH3_ENV_AD RW attack/decay
0xBF59 CH3_ENV_SR RW sustain/release

Length Registers (frames @ 60 Hz):
0xBF5A CH0_LEN RW length in frames (60 Hz)
0xBF5B CH1_LEN RW length in frames
0xBF5C CH2_LEN RW length in frames
0xBF5D CH3_LEN RW length in frames
0xBF5E-0xBF5F Reserved

Music System (0xBF60-0xBF74):
Per-channel registers (stride = 5):
ADDR_HI, ADDR_LO, LEN_HI, LEN_LO, CTRL

Channel 0:
0xBF60 MUSIC_CH0_ADDR_HI
0xBF61 MUSIC_CH0_ADDR_LO
0xBF62 MUSIC_CH0_LEN_HI
0xBF63 MUSIC_CH0_LEN_LO
0xBF64 MUSIC_CH0_CTRL bit0=play | bit1=stop | bit2=loop

Channel 1:
0xBF65 MUSIC_CH1_ADDR_HI
0xBF66 MUSIC_CH1_ADDR_LO
0xBF67 MUSIC_CH1_LEN_HI
0xBF68 MUSIC_CH1_LEN_LO
0xBF69 MUSIC_CH1_CTRL bit0=play | bit1=stop | bit2=loop

Channel 2:
0xBF6A MUSIC_CH2_ADDR_HI
0xBF6B MUSIC_CH2_ADDR_LO
0xBF6C MUSIC_CH2_LEN_HI
0xBF6D MUSIC_CH2_LEN_LO
0xBF6E MUSIC_CH2_CTRL bit0=play | bit1=stop | bit2=loop

Channel 3:
0xBF6F MUSIC_CH3_ADDR_HI
0xBF70 MUSIC_CH3_ADDR_LO
0xBF71 MUSIC_CH3_LEN_HI
0xBF72 MUSIC_CH3_LEN_LO
0xBF73 MUSIC_CH3_CTRL bit0=play | bit1=stop | bit2=loop

0xBF74 MUSIC_STATUS R bits 0-3: channel active

SFX System (0xBF90-0xBF95):
0xBF90 SFX_PLAY W trigger SFX by ID
0xBF91 SFX_STOP W stop SFX on channel
0xBF92 SFX_STATUS R bits 0-3: active SFX channels
0xBF93 SFX_TABLE_HI RW SFX table address high byte
0xBF94 SFX_TABLE_LO RW SFX table address low byte
0xBF95 SFX_COUNT RW number of SFX entries


SFX DATA FORMAT
---------------

SFX entries are 6 bytes each, starting at SFX_TABLE (addr_hi/addr_lo).
SFX_ID selects the entry offset: addr + (SFX_ID * 6).

Byte layout:
[0] channel (0-3)
[1] freq_lo
[2] freq_hi (bits 0-2) | waveform (bits 4-6)
[3] volume (bits 0-3)
[4] env_ad (attack << 4 | decay)
[5] duration (frames @ 60 Hz)


Channel 1 - Pulse 2:
0xBF46 CH1_FREQ_LO W frequency bits 0-7
0xBF47 CH1_FREQ_HI W frequency bits 8-10
0xBF48 CH1_VOL RW volume & duty (same as CH0)
0xBF49 CH1_CTRL RW enable & trigger

Channel 2 - Triangle:
0xBF4A CH2_FREQ_LO W frequency bits 0-7
0xBF4B CH2_FREQ_HI W frequency bits 8-10
0xBF4C CH2_VOL RW bits 0-3: volume
0xBF4D CH2_CTRL RW bit 0: enable | bit 1: trigger

Channel 3 - Noise:
0xBF4E CH3_PERIOD W bits 0-3: period (0=high, 15=low)
0xBF4F CH3_VOL RW bits 0-3: volume
0xBF50 CH3_CTRL RW bit 0: enable | bit 1: trigger | bit 2: mode

Envelopes (AD = attack/decay, SR = sustain/release):
0xBF51 CH0_ENV_AD RW hi nibble: attack | lo nibble: decay
0xBF52 CH0_ENV_SR RW hi nibble: sustain | lo nibble: release
0xBF53 CH1_ENV_AD RW attack/decay
0xBF54 CH1_ENV_SR RW sustain/release
0xBF55 CH2_ENV_AD RW attack/decay
0xBF56 CH2_ENV_SR RW sustain/release
0xBF57 CH3_ENV_AD RW attack/decay
0xBF58 CH3_ENV_SR RW sustain/release

Sweep + Length:
0xBF59 CH0_SWEEP RW hi nibble: rate | bit 3: dir | bits 0-2: shift
0xBF5A CH1_SWEEP RW sweep (same as CH0)
0xBF5B CH0_LEN RW length in frames (60 Hz)
0xBF5C CH1_LEN RW length in frames
0xBF5D CH2_LEN RW length in frames
0xBF5E CH3_LEN RW length in frames
0xBF5F Reserved

Channel 4 - Wave:
0xBF60 WAVE_FREQ_LO W frequency bits 0-7
0xBF61 WAVE_FREQ_HI W frequency bits 8-10
0xBF62 WAVE_VOL RW bits 0-3: volume
0xBF63 WAVE_CTRL RW bit 0: enable | bit 1: trigger
0xBF64 WAVE_LEN RW length in frames (60 Hz)
0xBF65 WAVE_ENV_AD RW attack/decay
0xBF66 WAVE_ENV_SR RW sustain/release

Wave RAM (32 x 4-bit samples):
0xBF70-0xBF8F WAVE_RAM RW each byte uses low 4 bits
MUSIC DATA FORMAT
-----------------

Music tracks are sequences of fixed-size notes in memory.
Each note is 4 bytes, and LEN_HI/LEN_LO define the number of notes.

Byte layout:
[0] note (0=rest, 1-60=C2-B6)
[1] volume (0-15)
[2] duration (frames @ 60 Hz)
[3] waveform (0-7, same as channel waveform index)

Noise notes use the note value as a period index (0-15). If note > 15,
the period defaults to a mid value.


FREQUENCY
Expand All @@ -123,17 +160,22 @@ Formula:
freq_value = 2048 - (44100 / freq_hz)


NOTE TABLE (A4 = 440 Hz)
------------------------
NOTE TABLE (C2-B6)
------------------

Rounded to nearest integer from the formula above.
Music notes map to a fixed 60-entry table (note = 1..60). Note 0 is a rest.
Values below are rounded to nearest integer from the formula above.

Oct 2: C=1374 C#=1412 D=1447 D#=1481 E=1513 F=1543 F#=1571 G=1598
G#=1623 A=1647 A#=1670 B=1691
Oct 3: C=1711 C#=1730 D=1748 D#=1765 E=1780 F=1795 F#=1810 G=1823
G#=1836 A=1848 A#=1859 B=1869
Oct 4: C=1879 C#=1889 D=1898 D#=1906 E=1914 F=1922 F#=1929 G=1935
G#=1942 A=1948 A#=1953 B=1959
Oct 5: C=1964 C#=1968 D=1973 D#=1977 E=1981 F=1985 F#=1988 G=1992
G#=1995 A=1998 A#=2001 B=2003
Oct 6: C=2006 C#=2008 D=2010 D#=2013 E=2015 F=2016 F#=2018 G=2020
G#=2021 A=2023 A#=2024 B=2026


WAVEFORM GENERATION
Expand All @@ -144,27 +186,46 @@ Phase Accumulator:
phase += phase_inc (per sample, wraps at 32-bit)
t = phase / 4294967296.0 (normalized 0-1)

Pulse:
Waveforms (waveform index in FREQ_HI bits 4-6):
0 = Triangle
1 = Tilted Saw (tsaw)
2 = Saw
3 = Square (50% duty)
4 = Pulse (25% duty)
5 = Organ (triangle + 2nd harmonic blend)
6 = Noise (LFSR)
7 = Phaser (two square waves with phase offset)

Square/Pulse:
sample = (t < duty_threshold) ? 1.0 : -1.0
duty: 12.5%=0.125, 25%=0.25, 50%=0.5, 75%=0.75
square duty = 50%, pulse duty = 25%

Triangle:
if t < 0.25: sample = t * 4
else if t < 0.75: sample = 2 - t * 4
else: sample = t * 4 - 4

Saw:
sample = t * 2 - 1

Tilted Saw (tsaw):
if t < 0.125: sample = t * 8
else: sample = 1 - (t - 0.125) * (2 / 0.875)

Organ:
sample = (triangle * 0.7) + (triangle at 2x freq * 0.3)

Phaser:
sample = 0.5 * (square(t) + square(t + phase_offset))

Noise (LFSR):
Long mode taps: 0,1,3,12
Short mode taps: 0,6
sample = (lfsr / 32768.0) - 1.0

Noise period table (period 0-15 => divider in samples):
[1, 2, 4, 8, 16, 32, 48, 64, 80, 96, 112, 128, 160, 202, 254, 380]

Wave (CH4):
index = phase >> 27 (32 samples)
v = wave[index] & 0x0F
sample = (v / 7.5) - 1.0
For channel noise, the period index is freq & 0x0F.
For music noise notes, the period index is (freq >> 7) & 0x0F.


MIXING
Expand All @@ -189,16 +250,8 @@ Attack ramps 0 -> 1.0. Decay ramps 1.0 -> sustain. Sustain holds. Release ramps
Enable or trigger starts envelope; disable starts release.


SWEEP (Pulse channels)
----------------------

Rate is in units of (sample_rate / 120). Shift is 0-7.
On each sweep tick: delta = freq >> shift.
If direction=down, freq -= delta; otherwise freq += delta. Clamped to 0-2047.


EXAMPLE - Play A4 on CH0
------------------------
EXAMPLE - Play A4 on CH0 (Square Wave)
--------------------------------------

LOADI R6, 0xBF

Expand All @@ -212,12 +265,12 @@ EXAMPLE - Play A4 on CH0
LOADI R0, 0x9C
STORE R0, [R6:R7]
LOADI R7, 0x43
LOADI R0, 0x07
LOADI R0, 0x37 ; freq_hi=0x07, waveform=3 (square)
STORE R0, [R6:R7]

; Volume=12, duty=50%
; Volume=12
LOADI R7, 0x44
LOADI R0, 0x2C ; duty=10, vol=1100
LOADI R0, 0x0C
STORE R0, [R6:R7]

; Enable channel
Expand All @@ -231,12 +284,11 @@ RESET STATE

All registers = 0x00 (APU disabled, all channels off)
LFSR seed = 0xACE1
Wave RAM default = 0x08 (midpoint)


LIMITATIONS
-----------

- No PCM/samples
- Mono only
- 5 channels max
- 4 channels max
12 changes: 5 additions & 7 deletions specs/tiny16-vm.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Layout:
0x9000–0x91FF oam (512 bytes, 128 sprites x 4 bytes)
0x9200–0x923F palette (64 bytes, 4 palettes x 16 colors)
0xA000–0xBEFF stack (grows down, ~7.7KB)
0xBF00–0xBFFF MMIO control registers (256 bytes)
0xBF00–0xBFFF MMIO control registers (4 KB)
0xC000–0xFFFF framebuffer (16,384 bytes = 128×128)


Expand Down Expand Up @@ -359,13 +359,11 @@ PPU (0xBF30-0xBF3F):
- 0xBF33 : PPU_STATUS (R ) PPU status register
bit 0: VBLANK (set after render, cleared on read)

TODO: Random (0xBF40-0xBF4F):
- 0xBF30 : RNG_VALUE (R ) read random byte
- 0xBF31 : RNG_SEED_LOW (W ) set seed low byte
- 0xBF32 : RNG_SEED_HIGH (W ) set seed high byte
APU (0xBF40-0xBF9F):
- See specs/tiny16-apu.txt for the full APU register map, SFX, and music formats.

TODO: System (0xBFF0-0xBFFF):
- 0xBFF0 : SYS_CONTROL (RW) system control flags
Reserved (0xBFF0-0xBFFF):
- Reserved for future system control registers.

Framebuffer (0xC000-0xFFFF):
- Base: 0xC000
Expand Down
Loading