mirror of
https://github.com/mattrberry/crab.git
synced 2024-11-16 19:49:30 +01:00
a rough start to fifo audio
This commit is contained in:
parent
b6e5c54da4
commit
ddb0d1f423
9 changed files with 157 additions and 88 deletions
|
@ -17,47 +17,10 @@ class APU
|
|||
FRAME_SEQUENCER_RATE = 512 # Hz
|
||||
FRAME_SEQUENCER_PERIOD = CPU::CLOCK_SPEED // FRAME_SEQUENCER_RATE
|
||||
|
||||
class SOUNDCNT_L < BitField(UInt16)
|
||||
num channel_4_left, 1
|
||||
num channel_3_left, 1
|
||||
num channel_2_left, 1
|
||||
num channel_1_left, 1
|
||||
num channel_4_right, 1
|
||||
num channel_3_right, 1
|
||||
num channel_2_right, 1
|
||||
num channel_1_right, 1
|
||||
bool not_used_1, lock: true
|
||||
num left_volume, 3
|
||||
bool not_used_2, lock: true
|
||||
num right_volume, 3
|
||||
end
|
||||
|
||||
class SOUNDCNT_H < BitField(UInt16)
|
||||
bool dma_sound_b_reset, lock: true
|
||||
num dma_sound_b_timer, 1
|
||||
bool dma_sound_b_left
|
||||
bool dma_sound_b_right
|
||||
bool dma_sound_a_reset, lock: true
|
||||
num dma_sound_a_timer, 1
|
||||
bool dma_sound_a_left
|
||||
bool dma_sound_a_right
|
||||
num not_used, 4, lock: true
|
||||
bool dma_sound_b_volume
|
||||
bool dma_sound_a_volume
|
||||
num sound_volume, 2
|
||||
end
|
||||
|
||||
class SOUNDBIAS < BitField(UInt16)
|
||||
num amplitude_resolution, 2
|
||||
num not_used_1, 4
|
||||
num bias_level, 9
|
||||
bool not_used_2
|
||||
end
|
||||
|
||||
@soundcnt_l = SOUNDCNT_L.new 0
|
||||
@soundcnt_h = SOUNDCNT_H.new 0
|
||||
@soundcnt_l = Reg::SOUNDCNT_L.new 0
|
||||
getter soundcnt_h = Reg::SOUNDCNT_H.new 0
|
||||
@sound_enabled : Bool = false
|
||||
@soundbias = SOUNDBIAS.new 0
|
||||
@soundbias = Reg::SOUNDBIAS.new 0
|
||||
|
||||
@buffer = Slice(Float32).new BUFFER_SIZE
|
||||
@buffer_pos = 0
|
||||
|
@ -82,7 +45,7 @@ class APU
|
|||
@channel2 = Channel2.new @gba
|
||||
@channel3 = Channel3.new @gba
|
||||
@channel4 = Channel4.new @gba
|
||||
@dma_channels = DMAChannels.new @gba
|
||||
@dma_channels = DMAChannels.new @gba, @soundcnt_h
|
||||
|
||||
tick_frame_sequencer
|
||||
get_sample
|
||||
|
@ -135,16 +98,20 @@ class APU
|
|||
channel2_amp = @channel2.get_amplitude
|
||||
channel3_amp = @channel3.get_amplitude
|
||||
channel4_amp = @channel4.get_amplitude
|
||||
dma_amp = @dma_channels.get_amplitude
|
||||
@buffer[@buffer_pos] = (@soundcnt_l.left_volume / 7).to_f32 *
|
||||
((channel4_amp * @soundcnt_l.channel_4_left) +
|
||||
(channel3_amp * @soundcnt_l.channel_3_left) +
|
||||
(channel2_amp * @soundcnt_l.channel_2_left) +
|
||||
(channel1_amp * @soundcnt_l.channel_1_left)) / 4
|
||||
(channel1_amp * @soundcnt_l.channel_1_left) +
|
||||
(dma_amp)) / 5
|
||||
@buffer[@buffer_pos + 1] = (@soundcnt_l.right_volume).to_f32 *
|
||||
((channel4_amp * @soundcnt_l.channel_4_right) +
|
||||
(channel3_amp * @soundcnt_l.channel_3_right) +
|
||||
(channel2_amp * @soundcnt_l.channel_2_right) +
|
||||
(channel1_amp * @soundcnt_l.channel_1_right)) / 4
|
||||
(channel1_amp * @soundcnt_l.channel_1_right) +
|
||||
(dma_amp)) / 5
|
||||
abort @buffer[@buffer_pos] if @buffer[@buffer_pos].abs > 1
|
||||
@buffer_pos += 2
|
||||
|
||||
# push to SDL if buffer is full
|
||||
|
@ -159,6 +126,10 @@ class APU
|
|||
@gba.scheduler.schedule SAMPLE_PERIOD, ->get_sample
|
||||
end
|
||||
|
||||
def timer_overflow(timer : Int) : Nil
|
||||
@dma_channels.timer_overflow timer
|
||||
end
|
||||
|
||||
def read_io(io_addr : Int) : UInt8
|
||||
case io_addr
|
||||
when @channel1 then @channel1.read_io io_addr
|
||||
|
@ -214,5 +185,7 @@ class APU
|
|||
when 0x89 then @soundbias.value = (@soundbias.value & 0x00FF) | value.to_u16 << 8
|
||||
else puts "Unmapped APU write ~ addr:#{hex_str io_addr.to_u8}, val:#{value}".colorize(:yellow)
|
||||
end
|
||||
|
||||
puts "SOUNDCNT_H(vol:#{@soundcnt_h.sound_volume}, a_vol:#{@soundcnt_h.dma_sound_a_volume}, b_vol:#{@soundcnt_h.dma_sound_b_volume}, a_right:#{@soundcnt_h.dma_sound_a_right}, a_left:#{@soundcnt_h.dma_sound_a_left}, a_timer:#{@soundcnt_h.dma_sound_a_timer}, b_right:#{@soundcnt_h.dma_sound_b_right}, b_left:#{@soundcnt_h.dma_sound_b_left}, b_timer:#{@soundcnt_h.dma_sound_b_timer}".colorize.fore(:blue) if 0x82 <= io_addr <= 0x83
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,19 +1,57 @@
|
|||
class DMAChannels
|
||||
RANGE = 0xA0..0xA7
|
||||
|
||||
@fifos = Array(Array(Int8)).new 2 { Array(Int8).new 32, 0 }
|
||||
@positions = Array(Int32).new 2, 0
|
||||
@sizes = Array(Int32).new 2, 0
|
||||
@timers : Array(Proc(UInt16))
|
||||
@latches = Array(Float32).new 2, 0
|
||||
|
||||
def ===(value) : Bool
|
||||
value.is_a?(Int) && RANGE.includes?(value)
|
||||
end
|
||||
|
||||
def initialize(@gba : GBA)
|
||||
def initialize(@gba : GBA, @control : Reg::SOUNDCNT_H)
|
||||
@timers = [
|
||||
->{ @control.dma_sound_a_timer },
|
||||
->{ @control.dma_sound_b_timer },
|
||||
]
|
||||
end
|
||||
|
||||
def read_io(index : Int) : UInt8
|
||||
abort "Reading DMA sound: #{hex_str index.to_u8}"
|
||||
0_u8
|
||||
end
|
||||
|
||||
def write_io(index : Int, value : UInt8) : Nil
|
||||
abort "Writing DMA sound: #{hex_str index.to_u8} -> #{hex_str value}"
|
||||
def write_io(index : Int, value : Byte) : Nil
|
||||
puts "DMA FIFO write: #{hex_str index.to_u16} -> #{hex_str value}"
|
||||
channel = bit?(index, 2).to_unsafe
|
||||
if @sizes[channel] < 32
|
||||
@fifos[channel][(@positions[channel] + @sizes[channel]) % 32] = value.to_i8!
|
||||
@sizes[channel] += 1
|
||||
else
|
||||
# abort "Writing #{hex_str value} to fifo #{(channel + 65).chr}, but it's already full".colorize.fore(:red)
|
||||
end
|
||||
end
|
||||
|
||||
def timer_overflow(timer : Int) : Nil
|
||||
(0..1).each do |channel|
|
||||
if timer == @timers[channel].call
|
||||
if @sizes[channel] > 0
|
||||
@latches[channel] = (@fifos[channel][@positions[channel]] / 128).to_f32
|
||||
@positions[channel] = (@positions[channel] + 1) % 32
|
||||
@sizes[channel] -= 1
|
||||
else
|
||||
puts "Timer overflow but empty"
|
||||
@latches[channel] = 0
|
||||
end
|
||||
end
|
||||
puts "triggering dma for channel #{channel} / #{channel + 1}" if @sizes[channel] < 16
|
||||
@gba.dma.trigger channel + 1 if @sizes[channel] < 16
|
||||
end
|
||||
end
|
||||
|
||||
def get_amplitude : Float32
|
||||
# STDERR.puts @latches.sum / 2 #unless -0.001 < @latches.sum / 2 < 0.001
|
||||
@latches.sum / 2
|
||||
end
|
||||
end
|
||||
|
|
|
@ -79,11 +79,13 @@ module ARM
|
|||
end
|
||||
@r[15] &-= 4 if pc_reads_12_ahead
|
||||
if rd == 15 && set_conditions
|
||||
# puts "returning"
|
||||
old_spsr = @spsr.value
|
||||
new_mode = CPU::Mode.from_value(@spsr.mode)
|
||||
switch_mode new_mode
|
||||
@cpsr.value = old_spsr
|
||||
@spsr.value = new_mode.bank == 0 ? @cpsr.value : @spsr_banks[new_mode.bank]
|
||||
# puts " cpsr:#{hex_str @cpsr.value}, spsr:#{hex_str @spsr.value}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -58,12 +58,14 @@ class CPU
|
|||
@r[15] = 0x08000000
|
||||
end
|
||||
|
||||
def switch_mode(new_mode : Mode) : Nil
|
||||
def switch_mode(new_mode : Mode, caller = __FILE__) : Nil
|
||||
old_mode = Mode.from_value @cpsr.mode
|
||||
return if new_mode == old_mode
|
||||
new_bank = new_mode.bank
|
||||
old_bank = old_mode.bank
|
||||
# puts "switching mode from #{old_mode} to #{new_mode}, cpsr:#{hex_str @cpsr.value}, spsr:#{hex_str @spsr.value}, new bank:#{new_bank}, caller:#{caller}"
|
||||
if new_mode == Mode::FIQ || old_mode == Mode::FIQ
|
||||
# puts " One of the modes is FIQ, switching out FIQ regs"
|
||||
5.times do |idx|
|
||||
@reg_banks[old_bank][idx] = @r[8 + idx]
|
||||
@r[8 + idx] = @reg_banks[new_bank][idx]
|
||||
|
@ -78,9 +80,11 @@ class CPU
|
|||
@r[14] = @reg_banks[new_bank][6]
|
||||
@spsr.value = @cpsr.value
|
||||
@cpsr.mode = new_mode.value
|
||||
# puts " cpsr:#{hex_str @cpsr.value}, spsr:#{hex_str @spsr.value}"
|
||||
end
|
||||
|
||||
def irq : Nil
|
||||
# puts "ime is enabled, cpsr:#{hex_str @cpsr.value}, cpsr.irq_disable:#{@cpsr.irq_disable}"
|
||||
unless @cpsr.irq_disable
|
||||
fill_pipeline
|
||||
lr = @r[15] - (@cpsr.thumb ? 0 : 4)
|
||||
|
|
|
@ -11,7 +11,7 @@ class DMA
|
|||
num not_used, 5
|
||||
|
||||
def to_s(io)
|
||||
io << "enable:#{enable},irq:#{irq_enable},timing:#{start_timing},game_pak:#{game_pak},type:#{type},repeat:#{repeat},srcctl:#{source_control},dstctl:#{dest_control}"
|
||||
io << "enable:#{enable}, irq:#{irq_enable}, timing:#{start_timing}, game_pak:#{game_pak}, type:#{type}, repeat:#{repeat}, srcctl:#{source_control}, dstctl:#{dest_control}"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -69,37 +69,45 @@ class DMA
|
|||
dmacnt_h = @dmacnt_h[dma_number]
|
||||
enabled = dmacnt_h.enable
|
||||
dmacnt_h.value = (dmacnt_h.value & ~mask) | value
|
||||
if dmacnt_h.enable && !enabled
|
||||
puts "DMA channel ##{dma_number} enabled, #{hex_str @dmasad[dma_number]} -> #{hex_str @dmadad[dma_number]}"
|
||||
puts dmacnt_h.to_s
|
||||
puts "Unsupported DMA start timing: #{dmacnt_h.start_timing}".colorize.fore(:yellow) unless dmacnt_h.start_timing == 0
|
||||
puts "Unsupported DMA src addr control: #{dmacnt_h.source_control}".colorize.fore(:yellow) unless 0 <= dmacnt_h.source_control <= 1
|
||||
puts "Unsupported DMA dst addr control: #{dmacnt_h.dest_control}".colorize.fore(:yellow) unless 0 <= dmacnt_h.dest_control <= 1
|
||||
delta = 2 << dmacnt_h.type
|
||||
ds = delta * case dmacnt_h.source_control
|
||||
when 0 then 1
|
||||
when 1 then -1
|
||||
when 2 then 0
|
||||
when 3 then puts "Prohibited source control".colorize.fore(:red); 1
|
||||
else abort "Impossible source control: #{dmacnt_h.source_control}"
|
||||
end
|
||||
dd = delta * case dmacnt_h.dest_control
|
||||
when 0 then 1
|
||||
when 1 then -1
|
||||
when 2 then 0
|
||||
when 3 then 1 # todo: reload
|
||||
else abort "Impossible source control: #{dmacnt_h.dest_control}"
|
||||
end
|
||||
src, dst = @dmasad[dma_number], @dmadad[dma_number]
|
||||
@dmacnt_l[dma_number].times do |idx|
|
||||
# puts "transferring #{dmacnt_h.type == 0 ? "16" : "32"} bits from #{hex_str src} to #{hex_str dst}"
|
||||
@gba.bus[dst] = dmacnt_h.type == 0 ? @gba.bus.read_half(src).to_u16! : @gba.bus.read_word(src)
|
||||
src += ds
|
||||
dst += dd
|
||||
end
|
||||
dmacnt_h.enable = false
|
||||
end
|
||||
trigger dma_number, on_write: true if dmacnt_h.enable && !enabled
|
||||
else abort "Unmapped DMA write ~ addr:#{hex_str io_addr.to_u8}, val:#{value}".colorize(:yellow)
|
||||
end
|
||||
end
|
||||
|
||||
def trigger(channel : Int, on_write = false) : Nil
|
||||
dmacnt_h = @dmacnt_h[channel]
|
||||
puts "DMA channel ##{channel} enabled, #{hex_str @dmasad[channel]} -> #{hex_str @dmadad[channel]}, len: #{hex_str @dmacnt_l[channel]}"
|
||||
puts " DMACNT: #{dmacnt_h.to_s}"
|
||||
puts " TM#{@gba.apu.soundcnt_h.dma_sound_a_timer}CNT: #{@gba.timer.tmcnt[@gba.apu.soundcnt_h.dma_sound_a_timer]}"
|
||||
if dmacnt_h.start_timing == 0 || !on_write
|
||||
special = dmacnt_h.start_timing == 3
|
||||
delta = 2 << dmacnt_h.type # transfer either halfwords or words
|
||||
delta = 0 if special && 1 <= channel <= 2 # fifo audio always transfers in words
|
||||
ds = delta * case dmacnt_h.source_control
|
||||
when 0 then 1
|
||||
when 1 then -1
|
||||
when 2 then 0
|
||||
when 3 then puts "Prohibited source control".colorize.fore(:red); 1
|
||||
else abort "Impossible source control: #{dmacnt_h.source_control}"
|
||||
end
|
||||
dd = delta * case dmacnt_h.dest_control
|
||||
when 0 then 1
|
||||
when 1 then -1
|
||||
when 2 then 0
|
||||
when 3 then puts "todo: DMA dst addr reload not yet supported".colorize.fore(:yellow); 1
|
||||
else abort "Impossible source control: #{dmacnt_h.dest_control}"
|
||||
end
|
||||
src, dst = @dmasad[channel], @dmadad[channel]
|
||||
len = @dmacnt_l[channel]
|
||||
len = 4 if special && 1 <= channel <= 2
|
||||
puts " Starting transfer of #{len} #{"half" if dmacnt_h.type == 0}words from #{hex_str src} to #{hex_str dst}"
|
||||
len.times do |idx|
|
||||
# puts "transferring #{dmacnt_h.type == 0 ? "16" : "32"} bits from #{hex_str src} to #{hex_str dst}"
|
||||
@gba.bus[dst] = dmacnt_h.type == 0 ? @gba.bus.read_half(src).to_u16! : @gba.bus.read_word(src)
|
||||
src += ds
|
||||
dst += dd
|
||||
end
|
||||
dmacnt_h.enable = false unless dmacnt_h.dest_control == 3
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require "./types"
|
||||
require "./reg"
|
||||
require "./util"
|
||||
require "./scheduler"
|
||||
require "./cartridge"
|
||||
|
|
|
@ -56,6 +56,7 @@ class Interrupts
|
|||
private def check_interrupts : Nil
|
||||
if @reg_ie.value & @reg_if.value != 0
|
||||
@gba.cpu.halted = false
|
||||
# puts "IE:#{hex_str @reg_ie.value} & IF:#{hex_str @reg_if.value} != 0"
|
||||
@gba.cpu.irq if @ime
|
||||
end
|
||||
end
|
||||
|
|
41
src/crab/reg.cr
Normal file
41
src/crab/reg.cr
Normal file
|
@ -0,0 +1,41 @@
|
|||
module Reg
|
||||
####################
|
||||
# APU
|
||||
|
||||
class SOUNDCNT_L < BitField(UInt16)
|
||||
num channel_4_left, 1
|
||||
num channel_3_left, 1
|
||||
num channel_2_left, 1
|
||||
num channel_1_left, 1
|
||||
num channel_4_right, 1
|
||||
num channel_3_right, 1
|
||||
num channel_2_right, 1
|
||||
num channel_1_right, 1
|
||||
bool not_used_1, lock: true
|
||||
num left_volume, 3
|
||||
bool not_used_2, lock: true
|
||||
num right_volume, 3
|
||||
end
|
||||
|
||||
class SOUNDCNT_H < BitField(UInt16)
|
||||
bool dma_sound_b_reset, lock: true
|
||||
num dma_sound_b_timer, 1
|
||||
bool dma_sound_b_left
|
||||
bool dma_sound_b_right
|
||||
bool dma_sound_a_reset, lock: true
|
||||
num dma_sound_a_timer, 1
|
||||
bool dma_sound_a_left
|
||||
bool dma_sound_a_right
|
||||
num not_used, 4, lock: true
|
||||
bool dma_sound_b_volume
|
||||
bool dma_sound_a_volume
|
||||
num sound_volume, 2
|
||||
end
|
||||
|
||||
class SOUNDBIAS < BitField(UInt16)
|
||||
num amplitude_resolution, 2
|
||||
num not_used_1, 4
|
||||
num bias_level, 9
|
||||
bool not_used_2
|
||||
end
|
||||
end
|
|
@ -8,10 +8,12 @@ class Timer
|
|||
num frequency, 2
|
||||
|
||||
def to_s(io)
|
||||
io << "TMCNT(enable:#{enable},irq:#{irq_enable},cascade:#{cascade},freq:#{frequency}"
|
||||
io << "enable:#{enable}, irq:#{irq_enable}, cascade:#{cascade}, freq:#{frequency}"
|
||||
end
|
||||
end
|
||||
|
||||
getter tmcnt
|
||||
|
||||
@interrupt_events : Array(Proc(Nil))
|
||||
|
||||
def initialize(@gba : GBA)
|
||||
|
@ -37,6 +39,7 @@ class Timer
|
|||
@events[next_timer_number].call if @tm[next_timer_number] == 0 # tell the next timer that it has overflowed
|
||||
end
|
||||
end
|
||||
@gba.apu.timer_overflow timer_number if timer_number <= 1
|
||||
@interrupt_events[timer_number].call
|
||||
@gba.interrupts.schedule_interrupt_check if tmcnt.irq_enable
|
||||
cycles_until_overflow = freq_to_cycles(tmcnt.frequency) * (0xFFFF - @tm[timer_number])
|
||||
|
@ -64,7 +67,6 @@ class Timer
|
|||
def write_io(io_addr : Int, value : UInt8) : Nil
|
||||
timer_number = (io_addr & 0xFF) // 4
|
||||
timer_control = bit?(io_addr, 1)
|
||||
puts "io_addr: #{hex_str io_addr.to_u16}, value: #{hex_str value}, timer number: #{timer_number}, timer_control: #{timer_control}"
|
||||
high = bit?(io_addr, 0)
|
||||
mask = 0xFF_u16
|
||||
mask <<= 8 unless high
|
||||
|
@ -72,27 +74,26 @@ class Timer
|
|||
if timer_control
|
||||
# todo: properly handle disabling / enabling timers via `cascade` field
|
||||
tmcnt = @tmcnt[timer_number]
|
||||
puts " updating TM#{timer_number}CNT from #{hex_str tmcnt.value} to #{hex_str (tmcnt.value & mask) | value}"
|
||||
puts " #{tmcnt.to_s}"
|
||||
enabled = tmcnt.enable
|
||||
tmcnt.value = (tmcnt.value & mask) | value
|
||||
if tmcnt.enable && !enabled # enabled
|
||||
puts " enabling".colorize.mode(:bold)
|
||||
puts "Timer #{timer_number} enabled, freq: #{hex_str freq_to_cycles(tmcnt.frequency)}, tm:#{hex_str @tm[timer_number]}"
|
||||
puts " TMCNT: #{tmcnt.to_s}"
|
||||
@cycle_enabled[timer_number] = @gba.scheduler.cycles
|
||||
@tm[timer_number] = @tmd[timer_number]
|
||||
puts " freq_to_cycles(#{tmcnt.frequency}) -> #{hex_str freq_to_cycles(tmcnt.frequency)}, @tm[#{timer_number}] -> #{hex_str @tm[timer_number]}, 0xFFFF - @tm[#{timer_number}] -> #{0xFFFF - @tm[timer_number]}"
|
||||
# puts " freq_to_cycles(#{tmcnt.frequency}) -> #{hex_str freq_to_cycles(tmcnt.frequency)}, @tm[#{timer_number}] -> #{hex_str @tm[timer_number]}, 0xFFFF - @tm[#{timer_number}] -> #{0xFFFF - @tm[timer_number]}"
|
||||
cycles_until_overflow = freq_to_cycles(tmcnt.frequency) * (0xFFFF - @tm[timer_number])
|
||||
puts " scheduling overflow for timer #{timer_number} in #{cycles_until_overflow} cycles"
|
||||
puts " Scheduling overflow for timer #{timer_number} in #{cycles_until_overflow} cycles"
|
||||
@gba.scheduler.schedule cycles_until_overflow, @events[timer_number], @event_types[timer_number] unless tmcnt.cascade
|
||||
elsif !tmcnt.enable && enabled # disabled
|
||||
puts " disabling".colorize.mode(:bold)
|
||||
puts "Timer #{timer_number} disabled".colorize.mode(:bold)
|
||||
elapsed = @gba.scheduler.cycles - @cycle_enabled[timer_number]
|
||||
@tm[timer_number] &+= elapsed // freq_to_cycles(tmcnt.frequency)
|
||||
@gba.scheduler.clear @event_types[timer_number]
|
||||
end
|
||||
else
|
||||
tmd = @tmd[timer_number]
|
||||
puts " updating TM#{timer_number}D from #{hex_str tmd} to #{hex_str (tmd & mask) | value}"
|
||||
# puts " updating TM#{timer_number}D from #{hex_str tmd} to #{hex_str (tmd & mask) | value}"
|
||||
@tmd[timer_number] = (tmd & mask) | value
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue