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