Skip to main content

[sound]

# =============================================================================
# SOUND (FRAMEWORK FILE) VERSION 2026-01-25
# =============================================================================
# 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()
new _sound_channel[](6) -> sound::_sound_channels
sound::_sound_channels.resize(6)

# Suspend Jamdac CPU
0 -> io::jamdac_enable

var queue: ^io_audio_queue

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

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

# This is okay because jamdac is temporarily disabled
queue.write_address -> queue.read_address
0 -> queue.purge_read_address

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

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

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

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

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

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

# Reboot Jamdac CPU
1 -> io::jamdac_enable

# Wait for the queue to be processed;
# If this loops forever then Jamdac is broken
kernel::trace_num(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
sound::_sound_channels[channel_index] -> channel

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

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

# Okay if an instrument is already loaded
# null -> channel.instrument
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
sound::_sound_channels[channel_index] -> channel

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

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

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

queue.read_address -> read_address
queue.write_address -> write_address

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

channel.write_index -> write_index

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

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

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

write_index -> channel.write_index
write_address -> queue.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

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

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

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

sound::stop_channel(i)

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

song -> channel.song
looping -> channel.looping
true -> channel.is_playing

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

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

i + 1 -> i
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

channel.write_index -> write_index
to_address(queue.events[write_index]) -> write_address
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

queue.read_address -> 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
read_address + 500 -> temp # unwrap the ring buffer
else
read_address -> temp
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[]
channel.song.patterns -> 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
0 -> channel.next_pattern_index
else
# Stop playing song
null -> channel.events
drop
end if
end if

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

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

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

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

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

# 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 - 30 -> wait_time_samples
queue.events[write_index] -> new_audio_event
10 -> new_audio_event.kind # 10=RAMPDOWN 30 SAMPLES
0 -> new_audio_event.operand

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

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

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

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

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

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

# 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.
math::bit_or(temp, math::shift_left(track_event.mod_byte, 20)) -> temp

temp -> new_audio_event.operand

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

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

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

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

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

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

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

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

return result
end func

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

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

i + 1 -> i
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