Audio

VIOS has a small synthesizer on the ESP32: three voices, each playing a square, triangle, sawtooth or noise wave, driven by short commands from Forth. Tunes are stored on the ESP as text in a compact music notation and played there with sample-accurate timing, so a melody costs the eZ80 one command to start and nothing while it plays.

Where the sound comes out

The Agon Light 2 has one audio path: the ESP32's 8-bit DAC feeds an amplifier that drives both the 3.5 mm jack and the on-board buzzer. They are the same signal, not two devices. The jack always works; the buzzer sounds only when the BUZ_EN1 jumper is closed.

The engine mixes the three voices at 16384 samples per second inside the existing sound interrupt. It has no task and no timer of its own, which leaves about 13 KB more internal ESP memory free than the original VDP audio system.

The words

Word Stack Effect
TONE ( freq ms -- ) square tone on the first idle voice (voice 0 if all are busy)
TUNE ( addr u slot -- ) store tune text (up to 255 bytes) in slot 0..7; u = 0 frees the slot
PLAY ( slot voice -- ) play the tune in a slot on voice 0..2
STOP ( voice -- ) silence voice 0..2; -1 STOP silences all
VOLUME ( n -- ) master volume 0..127 (default 127)
SOUND? ( -- mask ) bit n set while voice n is sounding

All of them except SOUND? are fire-and-forget: they send a few bytes to the ESP and return at once, so they are cheap to call from tasks. SOUND? waits for the ESP's answer. Ranges are checked on the ESP: TONE limits the frequency to 8191 Hz, VOLUME limits the volume to 127, and PLAY ignores an empty slot or a voice above 2. Only TUNE checks on the eZ80: a text longer than 255 bytes fails with "TUNE text over 255 bytes".

440 300 TONE
660 300 TONE
SOUND? .

The two tones play together on voices 0 and 1, so SOUND? prints 3 on the board. TONE frequencies go up to 8191 Hz; the volume of a tone is the default voice volume (12 of 15).

Tune notation

A tune is a string of letters and numbers, like the PLAY statement of old home computer BASICs. Case does not matter and spaces are ignored, so you can group notes into bars.

Token Meaning Default
c d e f g a b a note in the current octave; a in octave 4 is 440 Hz
# or + after a note sharp (one semitone up)
- after a note flat (one semitone down)
number after a note or rest its length: 1 whole, 2 half, 4 quarter, 8 eighth ... up to 64 the l length
. after the length dotted: one and a half times as long
r or p a rest, with optional length and .
on octave 0..8 4
< and > one octave down / up
ln default note length 1..64 4
tn tempo in quarter notes per minute, 32..255 120
vn voice volume 0..15 12
wn waveform: 0 square, 1 triangle, 2 sawtooth, 3 noise 0
qn how many eighths of each note sound, 1..8 (8 = legato) 7
* jump back to the start of this slot: loop forever
&n continue with the tune in slot n

Details:

  • A note lasts 240 / (tempo x length) seconds: at t120, a quarter note (l4) is half a second.
  • q7 lets each note sound for 7/8 of its length and leaves a short gap, so repeated notes are heard separately. q8 joins notes together.
  • Sharps and flats may cross an octave: b# is the next octave's c, c- the previous octave's b, as long as the result stays within octaves 0..8.
  • Numbers have at most three digits.
  • w3 (noise) is pitched: the noise generator is clocked at the note's frequency, so higher notes sound brighter. It is good for drums and effects.
  • PLAY starts a voice with the defaults (o4 l4 t120 v12 w0 q7); settings then carry over through * and & jumps.
  • A voice stops when its tune ends, or when it follows more than 4 jumps in a row without reaching a note or rest (so * alone cannot hang a voice).
  • A tune longer than 255 bytes is split over several slots chained with &.

When you store a tune, the ESP checks the whole text first. An invalid tune (an unknown letter, o9, t20, l65, v16, w4, q0, &8) is not stored, and voices keep playing what they had. The host harness's audio status (AS) shows the failing slot and the offset of the bad token.

Playing tunes

Store a tune once, then play it as often as you like:

S" t140 l8 cdefg>c" 0 TUNE
0 0 PLAY

Here is a little melody, the start of the Ode to Joy, with a bass line on a second voice. The melody is 16 beats and so is the bass, so they end together:

S" t120 l4 eefg gfed ccde e.d8d2" 1 TUNE
S" t120 o3 l2 c<g>c<g>c<g>c1" 2 TUNE
1 0 PLAY  2 1 PLAY

Since tunes play on the ESP, your program carries on immediately. To wait until the music ends, poll SOUND?:

: WAIT-MUSIC ( -- ) BEGIN 50 MS SOUND? 0= UNTIL ;

Use a loop for background music; it costs the eZ80 nothing while it plays:

S" t160 w1 v10 l8 o5 ceg>c<gec4 *" 3 TUNE
3 2 PLAY
-1 STOP

A drum loop with the noise waveform:

S" w3 v8 l16 o6 c r c r c c r8 *" 4 TUNE

Two slots chained into one longer tune; the second line loops back to the first:

S" t120 l4 eefg gfed ccde e.d8d2 &6" 5 TUNE
S" eefg gfed ccde d.c8c2 &5" 6 TUNE
5 0 PLAY
0 STOP

TUNE on a slot that is playing stops the voices using it. S" " 7 TUNE (length 0) frees slot 7. Tunes live in the ESP's memory: they are lost when the ESP restarts, so store them again at startup (in a module's word, or in /VIOS/USER.FTH).

Volume

Each voice has its own volume (vn in a tune, 12 for tones) and the master VOLUME scales all three:

64 VOLUME
127 VOLUME

BEEP and VDU 7

Two older ways to make a sound still work and share the same three voices:

  • 7 VDU (the ASCII bell) plays a 750 Hz beep for 125 ms on voice 0, if voice 0 is idle.
  • BEEP ( freq ms -- flag ) uses the original VDP sound command on voice 0 and waits for the reply: flag is 1 if the note was accepted, 0 if voice 0 was busy.

MOS programs that use the VDP's VDU 23,0,&85 sound commands on channels 0 to 2 are mapped onto the same voices. Sample playback and envelopes of the original VDP audio system are not available.

Sound from a task

The audio words are allowed in background tasks. A task can play a jingle when something happens without slowing the rest of the program:

: JINGLE ( -- ) S" t200 l16 o5 cegc>c" 0 TUNE  0 2 PLAY ;
' JINGLE SPAWN DROP
100 MS