Skip to main content

Event coprocessor

To play sounds, your program enqueues (inserts) IO_AUDIO_EVENT command events into six MMIO queues, one for each Jamdac channel. Each queue can hold up to 100 events, which the Jamdac system dequeues (removes) as it processes them.

There are eleven possible commands, determined by the IO_AUDIO_EVENT.KIND field. However, only four of them are essential; the commands labeled as Jamdac Plus provide extra convenience or advanced musical effects.

The IO_AUDIO_EVENT.OPERAND field stores an INT whose meaning depends on the kind of event. The usage is summarized in the IO_AUDIO_EVENT code comments in the I/O definitions below.

Timing characteristics

The design accomplishes several timing goals:

  • Each IO_AUDIO_QUEUE operates independently; excessive activity on one channel will never affect the timeline of another channel.
  • The main program can precisely schedule the exact sample index when a musical note will play, without need for timer interrupts or measuring clock cycles.
  • All six channels can trigger a musical note at the same sample index.

For realtime actions (e.g. pressing a key and hearing a sound), the theoretical worst case latency is:

Latency sourceSamples
wait counter: queue underflow (worst case)30
wait counter: optional RAMPDOWN of previous note30
wait counter: optional LOAD INSTRUMENT30
wait counter: TRIGGER INSTRUMENT30
FIFO buffering30
Total latency150 = ~6.8 ms

* For now, the actual latency will be substantially longer due to important optimizations that are not yet implemented for the Hybrix emulator, as well as overhead imposed by the web browser and operating system.

Sample index

Jamdac generates audio at a rate of 22,050 samples per second, equal to 22.05 samples per millisecond, or approximately 0.0454 milliseconds per sample. Each sample in the output stream can be thought of as having a sample index. Similar to an array index, the sample index indicates a sample's position in the output stream, as well the time when it will be played. Since we're mainly concerned with measuring time intervals between samples in a continuous audio output, any convenient starting point can be chosen as sample index "0". (Jamdac actually generates two output streams for stereo sound, but we'll ignore this detail for time measurements, since the left and right samples are always played simultaneously.)

Event scheduling

Jamdac allows events to be scheduled precisely at a specific sample index. Like the main CPU, the event coprocessor runs at 6,000,000 cycles/second. That's approximately 272 cycles/sample; however, an event that loads merely one instrument could require more than 1,000 cycles. This problem is solved by limiting how often events are dequeued and by amortizing their processing over a time window of 30 samples (6 channels × five-sample time slices).

Key features of the design:

  • Decoupled input/output timelines: The synthesizer algorithm generates samples at irregular time intervals, and a small FIFO buffer regularizes their timing to 22,050 Hz. The FIFO must hold at least 30 samples, imposing approximately 1.35 ms signal delay.

    The current sample index is defined to be the index of the next sample to be synthesized (irregular timing), not the latest FIFO output sample (regular timing).

  • Decoupled channel timelines: For each channel queue, the synthesizer tracks a wait counter which decreases whenever a sample is synthesized (which increments the current sample index). When a wait counter reaches zero, its channel is ready to dequeue an event.

  • Five-sample time slices: Every 5 output samples (every 0.227 ms of real time), the event coprocessor wakes up and dequeues at most one event. This time slice is equivalent to 1,360 clock cycles, enough time to process the most expensive event kind.

  • Dequeuing costs 30 samples: Regardless of the event kind, dequeuing an event always adds 30 to the wait counter for that channel. If the channel's queue is empty, this underflow condition assigns 30 to the wait counter, introducing a gap in the channel's timeline.

  • Asynchronous DSP unit: Generating output samples using the DSP unit is relatively cheap, due to pipelining optimizations and dedicated audio memory. Up to 30 samples can be synthesized during a single time slice. For example, if six TRIGGER INSTRUMENT commands were triggered at sample index 100, the first five time slices remain at sample index 100; the sample index advances to 130 at the end of the sixth time slice.

Purging queues

Suppose that channels 0 through 3 are playing a four-track song, while channels 4 and 5 are being used to play interactive sound effects. What if the program suddenly needs to stop the song, then play a different song? Several seconds of music from the first song may already be enqueued in channels 0 through 3. To discard that data, one idea might be to use IO::JAMDAC_ENABLE to reset the Jamdac system; however, this unfortunately would stop the sound effects as well.

For a better solution, IO_AUDIO_QUEUE::PURGE_READ_ADDRESS allows the program to immediately reset one specific channel without disturbing the other channels. Any unprocessed queue events are discarded, and the wait counter resets to 0. New events can be safely enqueued after Jamdac confirms the operation by setting IO_AUDIO_QUEUE::PURGE_READ_ADDRESS back to zero.

I/O definitions

CLASS IO_AUDIO_EVENT # SIZE 5
# 0 = LOAD INSTRUMENT OPERAND = ADDRESS OF IO_INSTRUMENT
# OR NULL TO RESET
# 1 = TRIGGER INSTRUMENT OPERAND = MOD_FACTOR IN HIGH PAIR;
# PITCH_FACTOR IN LOW PAIR
# (VALUES ARE S6.10 FIXED POINT)
# * 2 = RELEASE INSTRUMENT
# * 3 = LOAD GLOBAL ENVELOPE #3 OPERAND = ADDRESS OF IO_ENVELOPE
# OR NULL TO RESET
# * 4 = LOAD GLOBAL ENVELOPE #4 OPERAND = ADDRESS OF IO_ENVELOPE
# OR NULL TO RESET
# * 5 = TRIGGER GLOBAL ENVELOPE OPERAND = #3 OR #4
# * 6 = RELEASE GLOBAL ENVELOPE OPERAND = #3 OR #4
# * 7 = LOAD GLOBAL REVERB OPERAND = ADDRESS OF IO_REVERB
# OR NULL TO RESET
#
# 8 = WAIT OPERAND = NUMBER OF ADDITIONAL SAMPLES
# 9 = SYNC OPERAND = SYNC GROUP (0..5) IN BYTE 3;
# CHANNEL COUNT (1..6) IN BYTE 4
# * 10 = RAMPDOWN 30 SAMPLES
#
# * THESE FEATURES REQUIRE JAMDAC PLUS
VAR KIND: BYTE

VAR OPERAND: INT
END CLASS
CLASS IO_AUDIO_QUEUE # SIZE 512
# ADDRESS OF WHERE THE NEXT EVENT WILL BE WRITTEN BY THE APP
VAR WRITE_ADDRESS: INT

# ADDRESS OF THE NEXT EVENT TO BE READ BY THE AUDIO SYSTEM
# QUEUE IS EMPTY WHEN: WRITE_ADDRESS = READ_ADDRESS
# QUEUE IS FULL WHEN: WRAP(WRITE_ADDRESS + SIZEOF(EVENT)) = READ_ADDRESS
VAR READ_ADDRESS: INT

# TO PURGE THE QUEUE, ASSIGN WRITE_ADDRESS -> PURGE_READ_ADDRESS.
# WHEN THE JAMDAC SEES THAT PURGE_READ_ADDRESS IS NONZERO, IT WILL:
# 1. IF A "WAIT" OR "SYNC" EVENT IS ACTIVE, IT WILL BE ENDED EARLY
# 2. ASSIGN PURGE_READ_ADDRESS -> READ_ADDRESS, DISCARDING ALL EVENTS
# 3. ASSIGN 0 -> PURGE_READ_ADDRESS
VAR PURGE_READ_ADDRESS: INT

INSET EVENTS: IO_AUDIO_EVENT[INSET 100]
END CLASS
MODULE IO
# JAMDAC
INSET AUDIO_QUEUES: IO_AUDIO_QUEUE[INSET 6] LOCATED AT $D0_4000 # ..$D0_4BFF
END MODULE