Skip to main content

[SOUND]

# =============================================================================
# SOUND (FRAMEWORK FILE) VERSION 2025-11-29
# =============================================================================
# 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

# 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
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
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

PATTERNS[CHANNEL.NEXT_PATTERN_INDEX].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
459 * 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