a rough start to fifo audio

This commit is contained in:
Matthew Berry 2020-12-26 10:17:47 -08:00
parent b6e5c54da4
commit ddb0d1f423
9 changed files with 157 additions and 88 deletions

View file

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

View file

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

View file

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

View file

@ -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)

View file

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

View file

@ -1,4 +1,5 @@
require "./types"
require "./reg"
require "./util"
require "./scheduler"
require "./cartridge"

View file

@ -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
View 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

View file

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