mirror of
https://github.com/mattrberry/crab.git
synced 2025-01-30 20:34:45 +01:00
basic apu support (copy from CryBoy w/ slight modification), scheduler short-circuit, scheduler clear
This commit is contained in:
parent
70913a4ee8
commit
ac8319b19c
10 changed files with 776 additions and 11 deletions
182
src/crab/apu.cr
Normal file
182
src/crab/apu.cr
Normal file
|
@ -0,0 +1,182 @@
|
|||
require "./apu/abstract_channels" # so that channels don't need to all import
|
||||
require "./apu/*"
|
||||
|
||||
lib LibSDL
|
||||
fun queue_audio = SDL_QueueAudio(dev : AudioDeviceID, data : Void*, len : UInt32) : Int
|
||||
fun get_queued_audio_size = SDL_GetQueuedAudioSize(dev : AudioDeviceID) : UInt32
|
||||
fun clear_queued_audio = SDL_ClearQueuedAudio(dev : AudioDeviceID)
|
||||
fun delay = SDL_Delay(ms : UInt32) : Nil
|
||||
end
|
||||
|
||||
class APU
|
||||
CHANNELS = 2 # Left / Right
|
||||
BUFFER_SIZE = 1024
|
||||
SAMPLE_RATE = 32768 # Hz
|
||||
SAMPLE_PERIOD = CPU::CLOCK_SPEED // SAMPLE_RATE
|
||||
|
||||
FRAME_SEQUENCER_RATE = 512 # Hz
|
||||
FRAME_SEQUENCER_PERIOD = CPU::CLOCK_SPEED // FRAME_SEQUENCER_RATE
|
||||
|
||||
@sound_enabled : Bool = false
|
||||
|
||||
@buffer = Slice(Float32).new BUFFER_SIZE
|
||||
@buffer_pos = 0
|
||||
@frame_sequencer_stage = 0
|
||||
getter first_half_of_length_period = false
|
||||
|
||||
# @left_enable = false
|
||||
# @left_volume = 0_u8
|
||||
# @right_enable = false
|
||||
# @right_volume = 0_u8
|
||||
@left_enable = true
|
||||
@left_volume = 7_u8
|
||||
@right_enable = true
|
||||
@right_volume = 7_u8
|
||||
|
||||
# @nr51 : UInt8 = 0x00
|
||||
@nr51 : UInt8 = 0xFF
|
||||
|
||||
@audiospec : LibSDL::AudioSpec
|
||||
@obtained_spec : LibSDL::AudioSpec
|
||||
|
||||
def initialize(@gba : GBA)
|
||||
@audiospec = LibSDL::AudioSpec.new
|
||||
@audiospec.freq = SAMPLE_RATE
|
||||
@audiospec.format = LibSDL::AUDIO_F32SYS
|
||||
@audiospec.channels = CHANNELS
|
||||
@audiospec.samples = BUFFER_SIZE
|
||||
@audiospec.callback = nil
|
||||
@audiospec.userdata = nil
|
||||
|
||||
@obtained_spec = LibSDL::AudioSpec.new
|
||||
|
||||
@channel1 = Channel1.new @gba
|
||||
@channel2 = Channel2.new @gba
|
||||
@channel3 = Channel3.new @gba
|
||||
@channel4 = Channel4.new @gba
|
||||
|
||||
tick_frame_sequencer
|
||||
get_sample
|
||||
|
||||
raise "Failed to open audio" if LibSDL.open_audio(pointerof(@audiospec), pointerof(@obtained_spec)) > 0
|
||||
|
||||
LibSDL.pause_audio 0
|
||||
end
|
||||
|
||||
def tick_frame_sequencer : Nil
|
||||
@first_half_of_length_period = @frame_sequencer_stage & 1 == 0
|
||||
case @frame_sequencer_stage
|
||||
when 0
|
||||
@channel1.length_step
|
||||
@channel2.length_step
|
||||
@channel3.length_step
|
||||
@channel4.length_step
|
||||
when 1 then nil
|
||||
when 2
|
||||
@channel1.length_step
|
||||
@channel2.length_step
|
||||
@channel3.length_step
|
||||
@channel4.length_step
|
||||
@channel1.sweep_step
|
||||
when 3 then nil
|
||||
when 4
|
||||
@channel1.length_step
|
||||
@channel2.length_step
|
||||
@channel3.length_step
|
||||
@channel4.length_step
|
||||
when 5 then nil
|
||||
when 6
|
||||
@channel1.length_step
|
||||
@channel2.length_step
|
||||
@channel3.length_step
|
||||
@channel4.length_step
|
||||
@channel1.sweep_step
|
||||
when 7
|
||||
@channel1.volume_step
|
||||
@channel2.volume_step
|
||||
@channel4.volume_step
|
||||
else nil
|
||||
end
|
||||
@frame_sequencer_stage = 0 if (@frame_sequencer_stage += 1) > 7
|
||||
@gba.scheduler.schedule FRAME_SEQUENCER_PERIOD, ->tick_frame_sequencer
|
||||
end
|
||||
|
||||
def get_sample : Nil
|
||||
channel1_amp = @channel1.get_amplitude
|
||||
channel2_amp = @channel2.get_amplitude
|
||||
channel3_amp = @channel3.get_amplitude
|
||||
channel4_amp = @channel4.get_amplitude
|
||||
@buffer[@buffer_pos] = (@left_volume / 7).to_f32 *
|
||||
((@nr51 & 0x80 > 0 ? channel4_amp : 0) +
|
||||
(@nr51 & 0x40 > 0 ? channel3_amp : 0) +
|
||||
(@nr51 & 0x20 > 0 ? channel2_amp : 0) +
|
||||
(@nr51 & 0x10 > 0 ? channel1_amp : 0)) / 4
|
||||
@buffer[@buffer_pos + 1] = (@right_volume / 7).to_f32 *
|
||||
((@nr51 & 0x08 > 0 ? channel4_amp : 0) +
|
||||
(@nr51 & 0x04 > 0 ? channel3_amp : 0) +
|
||||
(@nr51 & 0x02 > 0 ? channel2_amp : 0) +
|
||||
(@nr51 & 0x01 > 0 ? channel1_amp : 0)) / 4
|
||||
@buffer_pos += 2
|
||||
|
||||
# push to SDL if buffer is full
|
||||
if @buffer_pos >= BUFFER_SIZE
|
||||
while LibSDL.get_queued_audio_size(1) > BUFFER_SIZE * sizeof(Float32) * 2
|
||||
LibSDL.delay(1)
|
||||
end
|
||||
LibSDL.queue_audio 1, @buffer, BUFFER_SIZE * sizeof(Float32)
|
||||
@buffer_pos = 0
|
||||
end
|
||||
|
||||
@gba.scheduler.schedule SAMPLE_PERIOD, ->get_sample
|
||||
end
|
||||
|
||||
def read_io(index : Int) : UInt8
|
||||
case index
|
||||
when @channel1 then @channel1.read_io index
|
||||
when @channel2 then @channel2.read_io index
|
||||
when @channel3 then @channel3.read_io index
|
||||
when @channel4 then @channel4.read_io index
|
||||
when 0xFF24
|
||||
((@left_enable ? 0b10000000 : 0) | (@left_volume << 4) |
|
||||
(@right_enable ? 0b00001000 : 0) | @right_volume).to_u8
|
||||
when 0xFF25 then @nr51
|
||||
when 0xFF26
|
||||
0x70 |
|
||||
(@sound_enabled ? 0x80 : 0) |
|
||||
(@channel4.enabled ? 0b1000 : 0) |
|
||||
(@channel3.enabled ? 0b0100 : 0) |
|
||||
(@channel2.enabled ? 0b0010 : 0) |
|
||||
(@channel1.enabled ? 0b0001 : 0)
|
||||
else 0xFF
|
||||
end.to_u8
|
||||
end
|
||||
|
||||
# write to apu memory
|
||||
def write_io(index : Int, value : UInt8) : Nil
|
||||
return unless @sound_enabled || index == 0x84 || Channel3::WAVE_RAM_RANGE.includes?(index)
|
||||
case index
|
||||
when @channel1 then @channel1.write_io index, value
|
||||
when @channel2 then @channel2.write_io index, value
|
||||
when @channel3 then @channel3.write_io index, value
|
||||
when @channel4 then @channel4.write_io index, value
|
||||
when 0xFF24
|
||||
@left_enable = value & 0b10000000 > 0
|
||||
@left_volume = (value & 0b01110000) >> 4
|
||||
@right_enable = value & 0b00001000 > 0
|
||||
@right_volume = value & 0b00000111
|
||||
when 0xFF25 then @nr51 = value
|
||||
when 0x84
|
||||
if value & 0x80 == 0 && @sound_enabled
|
||||
(0xFF10..0xFF25).each { |addr| self.write_io addr, 0x00 }
|
||||
@sound_enabled = false
|
||||
elsif value & 0x80 > 0 && !@sound_enabled
|
||||
@sound_enabled = true
|
||||
@frame_sequencer_stage = 0
|
||||
@channel1.length_counter = 0
|
||||
@channel2.length_counter = 0
|
||||
@channel3.length_counter = 0
|
||||
@channel4.length_counter = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
98
src/crab/apu/abstract_channels.cr
Normal file
98
src/crab/apu/abstract_channels.cr
Normal file
|
@ -0,0 +1,98 @@
|
|||
# All of the channels were developed using the following guide on gbdev
|
||||
# https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware
|
||||
|
||||
abstract class SoundChannel
|
||||
property enabled : Bool = false
|
||||
@dac_enabled : Bool = false
|
||||
|
||||
# NRx1
|
||||
property length_counter = 0
|
||||
|
||||
# NRx4
|
||||
@length_enable : Bool = false
|
||||
|
||||
def initialize(@gba : GBA)
|
||||
end
|
||||
|
||||
# Step the channel, calling helpers to reload the period and step the wave generation
|
||||
def step : Nil
|
||||
step_wave_generation
|
||||
schedule_reload frequency_timer
|
||||
end
|
||||
|
||||
# Step the length, disabling the channel if the length counter expires
|
||||
def length_step : Nil
|
||||
if @length_enable && @length_counter > 0
|
||||
@length_counter -= 1
|
||||
@enabled = false if @length_counter == 0
|
||||
end
|
||||
end
|
||||
|
||||
# Used so that channels can be matched with case..when statements
|
||||
abstract def ===(value) : Bool
|
||||
|
||||
# Calculate the frequency timer
|
||||
abstract def frequency_timer : UInt32
|
||||
|
||||
abstract def schedule_reload(frequency_timer : UInt32) : Nil
|
||||
|
||||
# Called when @period reaches 0
|
||||
abstract def step_wave_generation : Nil
|
||||
|
||||
abstract def get_amplitude : Float32
|
||||
|
||||
abstract def read_io(index : Int) : UInt8
|
||||
abstract def write_io(index : Int, value : UInt8) : Nil
|
||||
end
|
||||
|
||||
abstract class VolumeEnvelopeChannel < SoundChannel
|
||||
# NRx2
|
||||
@starting_volume : UInt8 = 0x00
|
||||
@envelope_add_mode : Bool = false
|
||||
@period : UInt8 = 0x00
|
||||
|
||||
@volume_envelope_timer : UInt8 = 0x00
|
||||
@current_volume : UInt8 = 0x00
|
||||
|
||||
@volume_envelope_is_updating = false
|
||||
|
||||
def volume_step : Nil
|
||||
if @period != 0
|
||||
@volume_envelope_timer -= 1 if @volume_envelope_timer > 0
|
||||
if @volume_envelope_timer == 0
|
||||
@volume_envelope_timer = @period
|
||||
if (@current_volume < 0xF && @envelope_add_mode) || (@current_volume > 0 && !@envelope_add_mode)
|
||||
@current_volume += (@envelope_add_mode ? 1 : -1)
|
||||
else
|
||||
@volume_envelope_is_updating = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def init_volume_envelope : Nil
|
||||
@volume_envelope_timer = @period
|
||||
@current_volume = @starting_volume
|
||||
@volume_envelope_is_updating = true
|
||||
end
|
||||
|
||||
def read_NRx2 : UInt8
|
||||
@starting_volume << 4 | (@envelope_add_mode ? 0x08 : 0) | @period
|
||||
end
|
||||
|
||||
def write_NRx2(value : UInt8) : Nil
|
||||
envelope_add_mode = value & 0x08 > 0
|
||||
if @enabled # Zombie mode glitch
|
||||
@current_volume += 1 if (@period == 0 && @volume_envelope_is_updating) || !@envelope_add_mode
|
||||
@current_volume = 0x10_u8 - @current_volume if (envelope_add_mode != @envelope_add_mode)
|
||||
@current_volume &= 0x0F
|
||||
end
|
||||
|
||||
@starting_volume = value >> 4
|
||||
@envelope_add_mode = envelope_add_mode
|
||||
@period = value & 0x07
|
||||
# Internal values
|
||||
@dac_enabled = value & 0xF8 > 0
|
||||
@enabled = false if !@dac_enabled
|
||||
end
|
||||
end
|
144
src/crab/apu/channel1.cr
Normal file
144
src/crab/apu/channel1.cr
Normal file
|
@ -0,0 +1,144 @@
|
|||
class Channel1 < VolumeEnvelopeChannel
|
||||
WAVE_DUTY = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 1], # 12.5%
|
||||
[1, 0, 0, 0, 0, 0, 0, 1], # 25%
|
||||
[1, 0, 0, 0, 0, 1, 1, 1], # 50%
|
||||
[0, 1, 1, 1, 1, 1, 1, 0], # 75%
|
||||
]
|
||||
|
||||
RANGE = 0x60..0x67
|
||||
|
||||
def ===(value) : Bool
|
||||
value.is_a?(Int) && RANGE.includes?(value)
|
||||
end
|
||||
|
||||
@wave_duty_position = 0
|
||||
|
||||
# NR10
|
||||
@sweep_period : UInt8 = 0x00
|
||||
@negate : Bool = false
|
||||
@shift : UInt8 = 0x00
|
||||
|
||||
@sweep_timer : UInt8 = 0x00
|
||||
@frequency_shadow : UInt16 = 0x0000
|
||||
@sweep_enabled : Bool = false
|
||||
@negate_has_been_used : Bool = false
|
||||
|
||||
# NR11
|
||||
@duty : UInt8 = 0x00
|
||||
@length_load : UInt8 = 0x00
|
||||
|
||||
# NR13 / NR14
|
||||
@frequency : UInt16 = 0x00
|
||||
|
||||
def step_wave_generation : Nil
|
||||
@wave_duty_position = (@wave_duty_position + 1) & 7
|
||||
end
|
||||
|
||||
def frequency_timer : UInt32
|
||||
(0x800_u32 - @frequency) * 4 * 4
|
||||
end
|
||||
|
||||
def schedule_reload(frequency_timer : UInt32) : Nil
|
||||
@gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel1
|
||||
end
|
||||
|
||||
def sweep_step : Nil
|
||||
@sweep_timer -= 1 if @sweep_timer > 0
|
||||
if @sweep_timer == 0
|
||||
@sweep_timer = @sweep_period > 0 ? @sweep_period : 8_u8
|
||||
if @sweep_enabled && @sweep_period > 0
|
||||
calculated = frequency_calculation
|
||||
if calculated <= 0x07FF && @shift > 0
|
||||
@frequency_shadow = @frequency = calculated
|
||||
frequency_calculation
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_amplitude : Float32
|
||||
if @enabled && @dac_enabled
|
||||
dac_input = WAVE_DUTY[@duty][@wave_duty_position] * @current_volume
|
||||
dac_output = (dac_input / 7.5) - 1
|
||||
dac_output
|
||||
else
|
||||
0
|
||||
end.to_f32
|
||||
end
|
||||
|
||||
# Calculate the new shadow frequency, disable channel if overflow 11 bits
|
||||
# https://gist.github.com/drhelius/3652407#file-game-boy-sound-operation-L243-L250
|
||||
def frequency_calculation : UInt16
|
||||
calculated = @frequency_shadow >> @shift
|
||||
calculated = @frequency_shadow + (@negate ? -1 : 1) * calculated
|
||||
@negate_has_been_used = true if @negate
|
||||
@enabled = false if calculated > 0x07FF
|
||||
calculated
|
||||
end
|
||||
|
||||
def read_io(index : Int) : UInt8
|
||||
case index
|
||||
when 0x60 then 0x80_u8 | @sweep_period << 4 | (@negate ? 0x08 : 0) | @shift
|
||||
when 0x62 then 0x3F_u8 | @duty << 6
|
||||
when 0x63 then read_NRx2
|
||||
when 0x64 then 0xFF_u8 # write-only
|
||||
when 0x65 then 0xBF_u8 | (@length_enable ? 0x40 : 0)
|
||||
else raise "Reading from invalid Channel1 register: #{hex_str index.to_u16}"
|
||||
end
|
||||
end
|
||||
|
||||
def write_io(index : Int, value : UInt8) : Nil
|
||||
case index
|
||||
when 0x60
|
||||
@sweep_period = (value & 0x70) >> 4
|
||||
@negate = value & 0x08 > 0
|
||||
@shift = value & 0x07
|
||||
# Internal values
|
||||
@enabled = false if !@negate && @negate_has_been_used
|
||||
when 0x61 # not used
|
||||
when 0x62
|
||||
@duty = (value & 0xC0) >> 6
|
||||
@length_load = value & 0x3F
|
||||
# Internal values
|
||||
@length_counter = 0x40 - @length_load
|
||||
when 0x63
|
||||
write_NRx2 value
|
||||
when 0x64
|
||||
@frequency = (@frequency & 0x0700) | value
|
||||
when 0x65
|
||||
@frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8
|
||||
length_enable = value & 0x40 > 0
|
||||
# Obscure length counter behavior #1
|
||||
if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0
|
||||
@length_counter -= 1
|
||||
@enabled = false if @length_counter == 0
|
||||
end
|
||||
@length_enable = length_enable
|
||||
trigger = value & 0x80 > 0
|
||||
if trigger
|
||||
@enabled = true if @dac_enabled
|
||||
# Init length
|
||||
if @length_counter == 0
|
||||
@length_counter = 0x40
|
||||
# Obscure length counter behavior #2
|
||||
@length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period
|
||||
end
|
||||
# Init frequency
|
||||
@gba.scheduler.clear Scheduler::EventType::APUChannel1
|
||||
schedule_reload frequency_timer
|
||||
# Init volume envelope
|
||||
init_volume_envelope
|
||||
# Init sweep
|
||||
@frequency_shadow = @frequency
|
||||
@sweep_timer = @sweep_period > 0 ? @sweep_period : 8_u8
|
||||
@sweep_enabled = @sweep_period > 0 || @shift > 0
|
||||
@negate_has_been_used = false
|
||||
if @shift > 0 # If sweep shift is non-zero, frequency calculation and overflow check are performed immediately
|
||||
frequency_calculation
|
||||
end
|
||||
end
|
||||
else raise "Writing to invalid Channel1 register: #{hex_str index.to_u16}"
|
||||
end
|
||||
end
|
||||
end
|
94
src/crab/apu/channel2.cr
Normal file
94
src/crab/apu/channel2.cr
Normal file
|
@ -0,0 +1,94 @@
|
|||
class Channel2 < VolumeEnvelopeChannel
|
||||
WAVE_DUTY = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 1], # 12.5%
|
||||
[1, 0, 0, 0, 0, 0, 0, 1], # 25%
|
||||
[1, 0, 0, 0, 0, 1, 1, 1], # 50%
|
||||
[0, 1, 1, 1, 1, 1, 1, 0], # 75%
|
||||
]
|
||||
|
||||
RANGE = 0x68..0x6F
|
||||
|
||||
def ===(value) : Bool
|
||||
value.is_a?(Int) && RANGE.includes?(value)
|
||||
end
|
||||
|
||||
@wave_duty_position = 0
|
||||
|
||||
# NR21
|
||||
@duty : UInt8 = 0x00
|
||||
@length_load : UInt8 = 0x00
|
||||
|
||||
# NR23 / NR24
|
||||
@frequency : UInt16 = 0x00
|
||||
|
||||
def step_wave_generation : Nil
|
||||
@wave_duty_position = (@wave_duty_position + 1) & 7
|
||||
end
|
||||
|
||||
def frequency_timer : UInt32
|
||||
(0x800_u32 - @frequency) * 4 * 4
|
||||
end
|
||||
|
||||
def schedule_reload(frequency_timer : UInt32) : Nil
|
||||
@gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel2
|
||||
end
|
||||
|
||||
def get_amplitude : Float32
|
||||
if @enabled && @dac_enabled
|
||||
dac_input = WAVE_DUTY[@duty][@wave_duty_position] * @current_volume
|
||||
dac_output = (dac_input / 7.5) - 1
|
||||
dac_output
|
||||
else
|
||||
0
|
||||
end.to_f32
|
||||
end
|
||||
|
||||
def read_io(index : Int) : UInt8
|
||||
case index
|
||||
when 0x68 then 0x3F_u8 | @duty << 6
|
||||
when 0x69 then read_NRx2
|
||||
when 0x6C then 0xFF_u8 # write-only
|
||||
when 0x6D then 0xBF_u8 | (@length_enable ? 0x40 : 0)
|
||||
else raise "Reading from invalid Channel2 register: #{hex_str index.to_u16}"
|
||||
end
|
||||
end
|
||||
|
||||
def write_io(index : Int, value : UInt8) : Nil
|
||||
case index
|
||||
when 0x68
|
||||
@duty = (value & 0xC0) >> 6
|
||||
@length_load = value & 0x3F
|
||||
# Internal values
|
||||
@length_counter = 0x40 - @length_load
|
||||
when 0x69
|
||||
write_NRx2 value
|
||||
when 0x6C
|
||||
@frequency = (@frequency & 0x0700) | value
|
||||
when 0x6D
|
||||
@frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8
|
||||
length_enable = value & 0x40 > 0
|
||||
# Obscure length counter behavior #1
|
||||
if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0
|
||||
@length_counter -= 1
|
||||
@enabled = false if @length_counter == 0
|
||||
end
|
||||
@length_enable = length_enable
|
||||
trigger = value & 0x80 > 0
|
||||
if trigger
|
||||
@enabled = true if @dac_enabled
|
||||
# Init length
|
||||
if @length_counter == 0
|
||||
@length_counter = 0x40
|
||||
# Obscure length counter behavior #2
|
||||
@length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period
|
||||
end
|
||||
# Init frequency
|
||||
@gba.scheduler.clear Scheduler::EventType::APUChannel2
|
||||
schedule_reload frequency_timer
|
||||
# Init volume envelope
|
||||
init_volume_envelope
|
||||
end
|
||||
else raise "Writing to invalid Channel2 register: #{hex_str index.to_u16}"
|
||||
end
|
||||
end
|
||||
end
|
123
src/crab/apu/channel3.cr
Normal file
123
src/crab/apu/channel3.cr
Normal file
|
@ -0,0 +1,123 @@
|
|||
class Channel3 < SoundChannel
|
||||
RANGE = 0x70..0x77
|
||||
WAVE_RAM_RANGE = 0x90..0x9F
|
||||
|
||||
def ===(value) : Bool
|
||||
value.is_a?(Int) && RANGE.includes?(value) || WAVE_RAM_RANGE.includes?(value)
|
||||
end
|
||||
|
||||
@wave_ram = Bytes.new(WAVE_RAM_RANGE.size) { |idx| idx & 1 == 0 ? 0x00_u8 : 0xFF_u8 }
|
||||
@wave_ram_position : UInt8 = 0
|
||||
@wave_ram_sample_buffer : UInt8 = 0x00
|
||||
|
||||
# NR31
|
||||
@length_load : UInt8 = 0x00
|
||||
|
||||
# NR32
|
||||
@volume_code : UInt8 = 0x00
|
||||
|
||||
@volume_code_shift : UInt8 = 0
|
||||
|
||||
# NR33 / NR34
|
||||
@frequency : UInt16 = 0x00
|
||||
|
||||
def step_wave_generation : Nil
|
||||
@wave_ram_position = (@wave_ram_position + 1) % (WAVE_RAM_RANGE.size * 2)
|
||||
@wave_ram_sample_buffer = @wave_ram[@wave_ram_position // 2]
|
||||
end
|
||||
|
||||
def frequency_timer : UInt32
|
||||
(0x800_u32 - @frequency) * 2 * 4
|
||||
end
|
||||
|
||||
def schedule_reload(frequency_timer : UInt32) : Nil
|
||||
@gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel3
|
||||
end
|
||||
|
||||
def get_amplitude : Float32
|
||||
if @enabled && @dac_enabled
|
||||
dac_input = ((@wave_ram_sample_buffer >> (@wave_ram_position & 1 == 0 ? 4 : 0)) & 0x0F) >> @volume_code_shift
|
||||
dac_output = (dac_input / 7.5) - 1
|
||||
dac_output
|
||||
else
|
||||
0
|
||||
end.to_f32
|
||||
end
|
||||
|
||||
def read_io(index : Int) : UInt8
|
||||
case index
|
||||
when 0x70 then 0x7F | (@dac_enabled ? 0x80 : 0)
|
||||
when 0x72 then 0xFF
|
||||
when 0x73 then 0x9F | @volume_code << 5
|
||||
when 0x74 then 0xFF
|
||||
when 0x75 then 0xBF | (@length_enable ? 0x40 : 0)
|
||||
when WAVE_RAM_RANGE
|
||||
if @enabled
|
||||
@wave_ram[@wave_ram_position // 2]
|
||||
else
|
||||
@wave_ram[index - WAVE_RAM_RANGE.begin]
|
||||
end
|
||||
else raise "Reading from invalid Channel3 register: #{hex_str index.to_u16}"
|
||||
end.to_u8
|
||||
end
|
||||
|
||||
def write_io(index : Int, value : UInt8) : Nil
|
||||
case index
|
||||
when 0x70
|
||||
@dac_enabled = value & 0x80 > 0
|
||||
@enabled = false if !@dac_enabled
|
||||
when 0x72
|
||||
@length_load = value
|
||||
# Internal values
|
||||
@length_counter = 0x100 - @length_load
|
||||
when 0x73
|
||||
@volume_code = (value & 0x60) >> 5
|
||||
# Internal values
|
||||
@volume_code_shift = case @volume_code
|
||||
when 0b00 then 4
|
||||
when 0b01 then 0
|
||||
when 0b10 then 1
|
||||
when 0b11 then 2
|
||||
else raise "Impossible volume code #{@volume_code}"
|
||||
end.to_u8
|
||||
when 0x74
|
||||
@frequency = (@frequency & 0x0700) | value
|
||||
when 0x75
|
||||
@frequency = (@frequency & 0x00FF) | (value.to_u16 & 0x07) << 8
|
||||
length_enable = value & 0x40 > 0
|
||||
# Obscure length counter behavior #1
|
||||
if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0
|
||||
@length_counter -= 1
|
||||
@enabled = false if @length_counter == 0
|
||||
end
|
||||
@length_enable = length_enable
|
||||
trigger = value & 0x80 > 0
|
||||
if trigger
|
||||
@enabled = true if @dac_enabled
|
||||
# Init length
|
||||
if @length_counter == 0
|
||||
@length_counter = 0x100
|
||||
# Obscure length counter behavior #2
|
||||
@length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period
|
||||
end
|
||||
# Init frequency
|
||||
# todo: I'm patching in an extra 6 T-cycles here with the `+ 6`. This is specifically
|
||||
# to get blargg's "09-wave read while on.s" to pass. I'm _not_ refilling the
|
||||
# frequency timer with this extra cycles when it reaches 0. For now, I'm letting
|
||||
# this be in order to work on other audio behavior. Note that this is pretty
|
||||
# brittle in it's current state though...
|
||||
@gba.scheduler.clear Scheduler::EventType::APUChannel3
|
||||
schedule_reload frequency_timer + 6
|
||||
# Init wave ram position
|
||||
@wave_ram_position = 0
|
||||
end
|
||||
when WAVE_RAM_RANGE
|
||||
if @enabled
|
||||
@wave_ram[@wave_ram_position // 2] = value
|
||||
else
|
||||
@wave_ram[index - WAVE_RAM_RANGE.begin] = value
|
||||
end
|
||||
else raise "Writing to invalid Channel3 register: #{hex_str index.to_u16}"
|
||||
end
|
||||
end
|
||||
end
|
96
src/crab/apu/channel4.cr
Normal file
96
src/crab/apu/channel4.cr
Normal file
|
@ -0,0 +1,96 @@
|
|||
class Channel4 < VolumeEnvelopeChannel
|
||||
RANGE = 0x78..0x7F
|
||||
|
||||
def ===(value) : Bool
|
||||
value.is_a?(Int) && RANGE.includes?(value)
|
||||
end
|
||||
|
||||
@lfsr : UInt16 = 0x0000
|
||||
|
||||
# NR41
|
||||
@length_load : UInt8 = 0x00
|
||||
|
||||
# NR43
|
||||
@clock_shift : UInt8 = 0x00
|
||||
@width_mode : UInt8 = 0x00
|
||||
@divisor_code : UInt8 = 0x00
|
||||
|
||||
def step_wave_generation : Nil
|
||||
new_bit = (@lfsr & 0b01) ^ ((@lfsr & 0b10) >> 1)
|
||||
@lfsr >>= 1
|
||||
@lfsr |= new_bit << 14
|
||||
if @width_mode != 0
|
||||
@lfsr &= ~(1 << 6)
|
||||
@lfsr |= new_bit << 6
|
||||
end
|
||||
end
|
||||
|
||||
def frequency_timer : UInt32
|
||||
((@divisor_code == 0 ? 8_u32 : @divisor_code.to_u32 << 4) << @clock_shift) * 4
|
||||
end
|
||||
|
||||
def schedule_reload(frequency_timer : UInt32) : Nil
|
||||
@gba.scheduler.schedule frequency_timer, ->step, Scheduler::EventType::APUChannel4
|
||||
end
|
||||
|
||||
def get_amplitude : Float32
|
||||
if @enabled && @dac_enabled
|
||||
dac_input = (~@lfsr & 1) * @current_volume
|
||||
dac_output = (dac_input / 7.5) - 1
|
||||
dac_output
|
||||
else
|
||||
0
|
||||
end.to_f32
|
||||
end
|
||||
|
||||
def read_io(index : Int) : UInt8
|
||||
case index
|
||||
when 0x78 then 0xFF
|
||||
when 0x79 then read_NRx2
|
||||
when 0x7C then @clock_shift << 4 | @width_mode << 3 | @divisor_code
|
||||
when 0x7D then 0xBF | (@length_enable ? 0x40 : 0)
|
||||
else raise "Reading from invalid Channel4 register: #{hex_str index.to_u16}"
|
||||
end.to_u8
|
||||
end
|
||||
|
||||
def write_io(index : Int, value : UInt8) : Nil
|
||||
case index
|
||||
when 0x78
|
||||
@length_load = value & 0x3F
|
||||
# Internal values
|
||||
@length_counter = 0x40 - @length_load
|
||||
when 0x79
|
||||
write_NRx2 value
|
||||
when 0x7C
|
||||
@clock_shift = value >> 4
|
||||
@width_mode = (value & 0x08) >> 3
|
||||
@divisor_code = value & 0x07
|
||||
when 0x7D
|
||||
length_enable = value & 0x40 > 0
|
||||
# Obscure length counter behavior #1
|
||||
if @gba.apu.first_half_of_length_period && !@length_enable && length_enable && @length_counter > 0
|
||||
@length_counter -= 1
|
||||
@enabled = false if @length_counter == 0
|
||||
end
|
||||
@length_enable = length_enable
|
||||
trigger = value & 0x80 > 0
|
||||
if trigger
|
||||
@enabled = true if @dac_enabled
|
||||
# Init length
|
||||
if @length_counter == 0
|
||||
@length_counter = 0x40
|
||||
# Obscure length counter behavior #2
|
||||
@length_counter -= 1 if @length_enable && @gba.apu.first_half_of_length_period
|
||||
end
|
||||
# Init frequency
|
||||
@gba.scheduler.clear Scheduler::EventType::APUChannel4
|
||||
schedule_reload frequency_timer
|
||||
# Init volume envelope
|
||||
init_volume_envelope
|
||||
# Init lfsr
|
||||
@lfsr = 0x7FFF
|
||||
end
|
||||
else raise "Writing to invalid Channel4 register: #{hex_str index.to_u16}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,6 +6,8 @@ class CPU
|
|||
include ARM
|
||||
include THUMB
|
||||
|
||||
CLOCK_SPEED = 2**24
|
||||
|
||||
enum Mode : UInt32
|
||||
USR = 0b10000
|
||||
FIQ = 0b10001
|
||||
|
|
|
@ -9,6 +9,7 @@ require "./interrupts"
|
|||
require "./cpu"
|
||||
require "./display"
|
||||
require "./ppu"
|
||||
require "./apu"
|
||||
require "./debugger"
|
||||
|
||||
class GBA
|
||||
|
@ -21,6 +22,7 @@ class GBA
|
|||
getter! cpu : CPU
|
||||
getter! display : Display
|
||||
getter! ppu : PPU
|
||||
getter! apu : APU
|
||||
getter! debugger : Debugger
|
||||
|
||||
def initialize(@bios_path : String, rom_path : String)
|
||||
|
@ -41,6 +43,7 @@ class GBA
|
|||
@cpu = CPU.new self
|
||||
@display = Display.new
|
||||
@ppu = PPU.new self
|
||||
@apu = APU.new self
|
||||
@debugger = Debugger.new self
|
||||
end
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ class MMIO
|
|||
io_addr = 0x0FFF_u16 & index
|
||||
if io_addr <= 0x05F
|
||||
@gba.ppu.read_io io_addr
|
||||
elsif io_addr <= 0xAF
|
||||
@gba.apu.read_io io_addr
|
||||
elsif (io_addr >= 0x200 && io_addr <= 0x203) || (io_addr >= 0x208 && io_addr <= 0x209)
|
||||
@gba.interrupts.read_io io_addr
|
||||
elsif io_addr >= 0x130 && io_addr <= 0x133
|
||||
|
@ -40,6 +42,8 @@ class MMIO
|
|||
io_addr = 0x0FFF_u16 & index
|
||||
if io_addr <= 0x05F
|
||||
@gba.ppu.write_io io_addr, value
|
||||
elsif io_addr <= 0xAF
|
||||
@gba.apu.write_io io_addr, value
|
||||
elsif (io_addr >= 0x200 && io_addr <= 0x203) || (io_addr >= 0x208 && io_addr <= 0x209)
|
||||
@gba.interrupts.write_io io_addr, value
|
||||
elsif io_addr >= 0x130 && io_addr <= 0x133
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
class Scheduler
|
||||
private record Event, cycles : UInt64, proc : Proc(Void)
|
||||
enum EventType
|
||||
DEFAULT
|
||||
APUChannel1
|
||||
APUChannel2
|
||||
APUChannel3
|
||||
APUChannel4
|
||||
end
|
||||
|
||||
private record Event, cycles : UInt64, proc : Proc(Void), type : EventType
|
||||
|
||||
@events : Deque(Event) = Deque(Event).new 10
|
||||
@cycles : UInt64 = 0
|
||||
@next_event : UInt64 = UInt64::MAX
|
||||
|
||||
def schedule(cycles : Int, proc : Proc(Void)) : Nil
|
||||
self << Event.new @cycles + cycles, proc
|
||||
end
|
||||
|
||||
def schedule(cycles : Int, &block : ->)
|
||||
self << Event.new @cycles + cycles, block
|
||||
def schedule(cycles : Int, proc : Proc(Void), type = EventType::DEFAULT) : Nil
|
||||
self << Event.new @cycles + cycles, proc, type
|
||||
end
|
||||
|
||||
@[AlwaysInline]
|
||||
|
@ -20,12 +25,21 @@ class Scheduler
|
|||
else
|
||||
@events << event
|
||||
end
|
||||
@next_event = @events[0].cycles
|
||||
end
|
||||
|
||||
def clear(type : EventType) : Nil
|
||||
@events.delete_if { |event| event.type == type }
|
||||
end
|
||||
|
||||
def tick(cycles : Int) : Nil
|
||||
cycles.times do
|
||||
@cycles += 1
|
||||
call_current
|
||||
if @cycles + cycles < @next_event
|
||||
@cycles += cycles
|
||||
else
|
||||
cycles.times do
|
||||
@cycles += 1
|
||||
call_current
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -36,7 +50,12 @@ class Scheduler
|
|||
event.proc.call
|
||||
@events.shift
|
||||
else
|
||||
break
|
||||
if event
|
||||
@next_event = event.cycles
|
||||
else
|
||||
@next_event = UInt64::MAX
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue