Skip to main content

[sound]

# =============================================================================
# SOUND (FRAMEWORK FILE) VERSION 2026-04-19
# =============================================================================
# This module plays songs and sound effects using the Jamdac synthesizer.
# If your program doesn't need to play sound, you could delete this file.

# -----------------------------------------------------------------------------
class _sound_channel
# The song playing on this channel, or null if there is no song
var song: song

# Index for song_pattern.tracks[]
var track_index: int

# Index for song.patterns[]
var next_pattern_index: int

# The io_audio_event.operand value for the "SYNC" event.
# A value of 0 means no sync.
var sync_data: int

# If this is null, then no sound is playing on the channel
var events: track_event[]
var next_event_index: int

# The instrument already loaded on this channel, or null if none
var instrument: io_instrument

var write_index: byte

var samples_per_tick: pair

# Was the queue non-empty on the last call to _process_channel()?
var is_playing: bool

var looping: bool
end class

# -----------------------------------------------------------------------------
module sound
var _sound_channels: _sound_channel[]
var _note_to_pitch: pair[]

# ---------------------------------------------------------------------------
func init()
sound::_sound_channels <- new _sound_channel[](6)

# Suspend Jamdac CPU
io::jamdac_enable <- 0

var queue: ^io_audio_queue

# Initialize queues and channels
var i: int
i <- 0
loop
sound::_sound_channels[i] <- new _sound_channel()

queue <- io::audio_queues[i]
queue.write_address <- to_address(queue.events)

# This is okay because Jamdac is temporarily disabled
queue.read_address <- queue.write_address
queue.purge_read_address <- 0

i <- i + 1
if i >= 6
do drop
end loop

# Use channel 0 to load art::reverb
queue <- io::audio_queues[0]

# ----------------------------------
# Write a "LOAD GLOBAL REVERB" event
var new_audio_event: ^io_audio_event
new_audio_event <- queue.events[0]
new_audio_event.kind <- 7 # 7=LOAD GLOBAL REVERB
new_audio_event.operand <- to_address(art::reverb)

# ---------------------------------------
# Write a "LOAD GLOBAL ENVELOPE #3" event
new_audio_event <- queue.events[1]
new_audio_event.kind <- 3 # 3=LOAD GLOBAL ENVELOPE #3
new_audio_event.operand <- to_address(art::envelope_3)

# ---------------------------------------
# Write a "TRIGGER GLOBAL ENVELOPE" event
new_audio_event <- queue.events[2]
new_audio_event.kind <- 5 # 5=TRIGGER GLOBAL ENVELOPE
new_audio_event.operand <- 3 # envelope #3

queue.write_address <- to_address(queue.events[3])
sound::_sound_channels[0].write_index <- 3

# Reboot Jamdac CPU
io::jamdac_enable <- 1

# Wait for the queue to be processed;
# If this loops forever then Jamdac is broken
kernel::trace_id(0)
loop
if queue.read_address = queue.write_address
do drop
end loop
end func

# ---------------------------------------------------------------------------
func play_track(track: track, channel_index: int)
sound::_play_track(track, channel_index, false)
end func

# ---------------------------------------------------------------------------
func loop_track(track: track, channel_index: int)
sound::_play_track(track, channel_index, true)
end func

# ---------------------------------------------------------------------------
func _play_track(track: track, channel_index: int, looping: bool)
if sound::_sound_channels = null
do return # sound::init() was not called

sound::stop_channel(channel_index)

var channel: _sound_channel
channel <- sound::_sound_channels[channel_index]

channel.song <- null
channel.looping <- looping
channel.samples_per_tick <- 459 # 120 BPM default
channel.is_playing <- true

channel.track_index <- 0
channel.next_pattern_index <- 0
channel.sync_data <- 0
channel.events <- track.events
channel.next_event_index <- 0

# Okay if an instrument is already loaded
# channel.instrument <- null
end func

# ---------------------------------------------------------------------------
func stop_channel(channel_index: int)
if sound::_sound_channels = null
do return # sound::init() was not called

# 1. clear the channel
var channel: _sound_channel
channel <- sound::_sound_channels[channel_index]

channel.song <- null
channel.is_playing <- false
channel.events <- null
channel.instrument <- null

# 2. reset the audio queue
var queue: ^io_audio_queue
queue <- io::audio_queues[channel_index]

var write_index: byte, write_address: int, read_address: int

read_address <- queue.read_address
write_address <- queue.write_address

# Queue is empty when: write_address = read_address
if read_address <> write_address then
# Purge the queue
queue.purge_read_address <- queue.write_address
end if

write_index <- channel.write_index

# ------------------------------
# Write a "LOAD INSTRUMENT" event with null to reset:
var new_audio_event: ^io_audio_event
new_audio_event <- queue.events[write_index]
new_audio_event.kind <- 0 # 0=LOAD INSTRUMENT
new_audio_event.operand <- 0

# Advance write_index
write_index <- to_byte(write_index + 1)
if write_index >= 100
do write_index <- 0

# Update write_address
write_address <- to_address(queue.events[write_index])

channel.write_index <- write_index
queue.write_address <- write_address
end func

# ---------------------------------------------------------------------------
func play_song(song: song)
sound::_play_song(song, false)
end func

# ---------------------------------------------------------------------------
func loop_song(song: song)
sound::_play_song(song, true)
end func

# ---------------------------------------------------------------------------
func _play_song(song: song, looping: bool)
if sound::_sound_channels = null
do return # sound::init() was not called

var i: int, track_count: int
var pattern: song_pattern
var sync_data: int

pattern <- song.patterns[0]
track_count <- pattern.tracks.size

if track_count > 1 then
sync_data <- track_count
else
sync_data <- 0
end if

i <- 0
loop
if i >= track_count
do drop

sound::stop_channel(i)

var channel: _sound_channel
channel <- sound::_sound_channels[i]

channel.song <- song
channel.looping <- looping
channel.is_playing <- true

channel.track_index <- i
channel.next_pattern_index <- 1
channel.sync_data <- sync_data

channel.events <- pattern.tracks[i].events
channel.samples_per_tick <- pattern.samples_per_tick
channel.next_event_index <- 0
channel.instrument <- null

i <- i + 1
end loop
end func

# ---------------------------------------------------------------------------
func _process_channel(
| channel: _sound_channel,
| queue: ^io_audio_queue)
var write_index: byte, write_address: int, read_address: int
var temp: int
var new_audio_event: ^io_audio_event

if channel.events = null and not channel.is_playing
do return # nothing is playing

write_index <- channel.write_index
write_address <- to_address(queue.events[write_index])
if write_address <> queue.write_address
do kernel::fail("_process_channel() write_address out of sync")

if queue.purge_read_address <> 0
do return # still resetting

read_address <- queue.read_address

if channel.events <> null then
loop
# Does the output buffer have room to write 5 events?
# free_slots_remaining = (read_address - write_address)/5 - 1
if read_address <= write_address then
temp <- read_address + 500 # unwrap the ring buffer
else
temp <- read_address
end if

# If (read_address - write_address)/5 - 1 < 5
if temp - write_address < 30
do drop # not enough room to enqueue up to five events

if channel.next_event_index >= channel.events.size then
# If there is a song, then advance to the next pattern
if channel.song <> null then
var patterns: song_pattern[]
patterns <- channel.song.patterns

if channel.next_pattern_index >= patterns.size then
# Reached the end of the song
if channel.looping then
# Go back to beginning of song
channel.next_pattern_index <- 0
else
# Stop playing song
channel.events <- null
drop
end if
end if

var pattern: song_pattern
pattern <- patterns[channel.next_pattern_index]
channel.samples_per_tick <- pattern.samples_per_tick
channel.events <- pattern.tracks[channel.track_index].events
channel.next_pattern_index <- channel.next_pattern_index + 1
channel.next_event_index <- 0
else
# No song
if channel.looping then
# Go back to beginning of song this track
channel.next_event_index <- 0
else
# Stop playing track
channel.events <- null
drop
end if
end if

# ------------------------------
# Write a "SYNC" event:
if channel.sync_data <> 0 then
new_audio_event <- queue.events[write_index]
new_audio_event.kind <- 9 # 9=SYNC
new_audio_event.operand <- channel.sync_data

# Advance write_index
write_index <- to_byte(write_index + 1)
if write_index >= 100
do write_index <- 0
end if
end if

# Read the next event
var track_event: track_event
track_event <- channel.events[channel.next_event_index]
channel.next_event_index <- channel.next_event_index + 1

var wait_time_samples: int
wait_time_samples <- channel.samples_per_tick * track_event.duration_ticks

# 0..127, 254=release, 255=rest
var track_event_note: byte
track_event_note <- track_event.note

if track_event_note < 254 then # 255=rest
# ------------------------------
# Rampdown the previous note to eliminate clicks
wait_time_samples <- wait_time_samples - 30
new_audio_event <- queue.events[write_index]
new_audio_event.kind <- 10 # 10=RAMPDOWN 30 SAMPLES
new_audio_event.operand <- 0

# Advance write_index
write_index <- to_byte(write_index + 1)
if write_index >= 100
do write_index <- 0

# Do we need to load the instrument?
var instrument: io_instrument
instrument <- art::instruments[track_event.instrument]
if channel.instrument <> instrument then
channel.instrument <- instrument

# ------------------------------
# Write a "LOAD INSTRUMENT" event:
wait_time_samples <- wait_time_samples - 30
new_audio_event <- queue.events[write_index]
new_audio_event.kind <- 0 # 0=LOAD INSTRUMENT
new_audio_event.operand <- to_address(instrument)

# Advance write_index
write_index <- to_byte(write_index + 1)
if write_index >= 100
do write_index <- 0
end if

# ------------------------------
# Trigger the note
wait_time_samples <- wait_time_samples - 30
new_audio_event <- queue.events[write_index]
new_audio_event.kind <- 1 # 1=TRIGGER INSTRUMENT

# Translate the note into a pitch_factor
temp <- sound::_note_to_pitch[track_event_note]

# Add the mod_factor in the high pair, which requires shifting by 16 bits.
# But we also need to convert u2.6 -> s6.10 fixed point, which requires shifting by 4 bits.
temp <- math::bit_or(temp, math::shift_left(track_event.mod_byte, 20))

new_audio_event.operand <- temp

# Advance write_index
write_index <- to_byte(write_index + 1)
if write_index >= 100
do write_index <- 0
end if

# ------------------------------
# Skip the appropriate amount of time
wait_time_samples <- wait_time_samples - 30
new_audio_event <- queue.events[write_index]
new_audio_event.kind <- 8 # 8=WAIT
new_audio_event.operand <- wait_time_samples

# Advance write_index
write_index <- to_byte(write_index + 1)
if write_index >= 100
do write_index <- 0

# Update write_address
write_address <- to_address(queue.events[write_index])
end loop

channel.write_index <- write_index
queue.write_address <- write_address
end if

# Queue is empty when: write_address = read_address
if write_address = read_address then
channel.is_playing <- false
else
channel.is_playing <- true
end if
end func

# ---------------------------------------------------------------------------
func is_playing(channel_index: int): bool
var channel: _sound_channel

if sound::_sound_channels = null then
# sound::init() was not called
return false
else
channel <- sound::_sound_channels[channel_index]
return channel.is_playing
end if
end func

# ---------------------------------------------------------------------------
func think()
if sound::_sound_channels = null
do return # sound::init() was not called

var i: int
i <- 0
loop
sound::_process_channel(sound::_sound_channels[i],
| io::audio_queues[i])

i <- i + 1
if i >= 6
do drop
end loop
end func
end module

# -----------------------------------------------------------------------------
# Maps from: MIDI note number --> s6.10 fixed point pitch_factor
# piano A4 = MIDI #69 = $45 --> 1.0 = 1024fp
# a true "A4" pitch occurs when io_wave::oscillator_hz = 440 Hz
# formula: to_pair(2^(n/12)*2^10 + 0.5)
data sound::_note_to_pitch
[
# 0 1 2 3 4 5 6 7 8 9
19, 20, 21, 23, 24, 25, 27, 29, 30, 32, # _
34, 36, 38, 40, 43, 45, 48, 51, 54, 57, # 1_
60, 64, 68, 72, 76, 81, 85, 91, 96, 102, # 2_
108, 114, 121, 128, 136, 144, 152, 161, 171, 181, # 3_
192, 203, 215, 228, 242, 256, 271, 287, 304, 323, # 4_
342, 362, 384, 406, 431, 456, 483, 512, 542, 575, # 5_
609, 645, 683, 724, 767, 813, 861, 912, 967, 1024, # 6_
1085, 1149, 1218, 1290, 1367, 1448, 1534, 1625, 1722, 1825, # 7_
1933, 2048, 2170, 2299, 2435, 2580, 2734, 2896, 3069, 3251, # 8_
3444, 3649, 3866, 4096, 4340, 4598, 4871, 5161, 5468, 5793, # 9_
6137, 6502, 6889, 7298, 7732, 8192, 8679, 9195, 9742, 10321, # 10_
10935, 11585, 12274, 13004, 13777, 14596, 15464, 16384, 17358, 18390, # 11_
19484, 20643, 21870, 23170, 24548, 26008, 27554, 29193 # 12_
]
end data