Skip to main content

Jamdac audio

The Jamdac audio synthesizer enables Hybrix programs to play music and sound effects. Here we'll summarize its IO interface; for complete details, consult the Jamdac Reference book.

Specifications​

  • 22,050 samples per second stereo with 16-bit samples
  • 6 synthesizer channels, each able to play one instrument at a time
  • Each instrument/channel can play 3 simultaneous oscillators and 3 filters
  • 20 programmable envelopes for controlling dynamics
  • Global reverb

The Jamdac hardware is a separate system, comprised of a DSP unit, event coprocessor, and small dedicated audio memory. The coprocessor is actually a second Chombit CPU, but running a firmware ROM that (currently) cannot be modified.

Audio queues​

In order to avoid glitches or lag, audio synthesis requires very precise realtime timings. The main program cannot provide such guarantees. Instead, it inserts commands into the io_audio_queue.events, and the Jamdac coprocessor will remove (dequeue) the events as they are played. The events represent primitive commands such as loading an instrument or triggering a note. This scheme is conceptually similar to the MIDI standard, except that each Jamdac "channel" corresponds to a physical synthesizer voice, and each of the six channels has its own io_audio_queue.events array.

The queue is a circular buffer whose start and end are tracked by io_audio_queue.read_address and io_audio_queue.write_address.

Timing​

Audio synchronization is controlled by two special timing events:

  • WAIT (io_audio_event.kind = 8): Tells the synthesizer to wait a certain number of samples before proceeding with the next command on that channel.

  • SYNC (io_audio_event.kind = 9): Synchronizes a subset of channels; they each stop when they get to the SYNC event, and then when the last channel has stopped, they all start together.

Framework tracker​

The Hybrix designer implements a complete music tracker and instrument editor, and the sound framework file plays the songs by generating io_audio_event events. Because the designer emits the native Jamdac io_instrument data structures, the player requires relatively little code. The Hybrix designer can also export Jamdac chiptune files.

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 are non-core
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 purge_read_address <- write_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 read_address <- purge_read_address, discarding all events
# 3. assign purge_read_address <- 0
var purge_read_address: int

inset events: io_audio_event[inset 100]
end class
module io
. . .
# 0 = disable Jamdac; 1 = enable Jamdac, rebooting its CPU
var jamdac_enable: byte located at $d0_003e
. . .
inset audio_queues: io_audio_queue[inset 6] located at $d0_4000 # ..$d0_4bff
end module