#### Software implementation of Yamaha FM sound generator
####
#### Copyright Jarek Burczynski (bujar at mame dot net)
#### Copyright Tatsuyuki Satoh , MultiArcadeMachineEmulator development
#### Copyright (C) 2023 Remilia Scarlet
require "math"
require "./opn-mame-constants"
####
#### Common Yamaha OPN Mame Routines
####
module Yuno::Chips
# :nodoc:
module OPNMame
###
### Aliases
###
alias FmTimerHandler = Proc(AbstractChip?, Int32, Int32, Int32, Nil)
alias FmIrqHandler = Proc(AbstractChip?, Int32, Nil)
###
### Internal classes
###
class SsgCallbacks
property! chip : Yuno::Chips::Ay8910?
property! setClock : Proc(UInt32, Nil)?
property! write : Proc(Int32, Int32, Nil)?
property! read : Proc(UInt8)?
property! reset : Proc(Nil)?
def initialize
end
def initialize(@chip, @setClock, @write, @read, @reset)
end
end
class FmSlot
property dt : Array(Int32) = Array(Int32).new(0, 0) # Detune
property ksr : UInt8 = 0 # Key scale rate
property ar : UInt32 = 0 # Attack rate
property d1r : UInt32 = 0 # Decay rate
property d2r : UInt32 = 0 # Sustain rate
property rr : UInt32 = 0 # Release rate
property ksr3 : UInt8 = 0 # Key scale rate
property mul : UInt32 = 0 # Multiple
#
# Phase generator
#
property phase : UInt32 = 0 # Phase counter
property incr : Int32 = 0 # Phase step
#
# Envelope generator
#
property state : UInt8 = 0 # Phase type
property tl : UInt32 = 0 # Total level
property volume : Int32 = 0 # Envelope counter
property sl : UInt32 = 0 # Sustain level
property volOut : UInt32 = 0 # Current output from EG circuit (without AM from LFO)
property egShAr : UInt8 = 0 # Attack state
property egSelAr : UInt8 = 0 # Attack state
property egShD1r : UInt8 = 0 # Decay state
property egSelD1r : UInt8 = 0 # Decay state
property egShD2r : UInt8 = 0 # Sustain state
property egSelD2r : UInt8 = 0 # Sustain state
property egShRr : UInt8 = 0 # Release state
property egSelRr : UInt8 = 0 # Release state
property ssg : UInt8 = 0 # SSG-EG waveform
property ssgn : UInt8 = 0 #SSG_EG negated output
property key : UInt32 = 0 # 0 - last key was off, 1 - key on
#
# LFO
#
property amMask : UInt32 = 0 # AM enable flag
def initialize
end
end
class FmCh
property slot : Array(FmSlot) # Four SLOTs (operators)
property algo : UInt8 = 0 # Algorithm
property fb : UInt8 = 0 # Feedback shift
property op1Out : Array(Int32) = [0, 0] # OP1 output for feedback
property connect1 : Pointer(Int32) = Pointer(Int32).malloc(1, 0) # SLOT1 output pointer
property connect2 : Pointer(Int32) = Pointer(Int32).malloc(1, 0) # SLOT1 output pointer
property connect3 : Pointer(Int32) = Pointer(Int32).malloc(1, 0) # SLOT1 output pointer
property connect4 : Pointer(Int32) = Pointer(Int32).malloc(1, 0) # SLOT1 output pointer
property memConnect : Pointer(Int32) = Pointer(Int32).malloc(1, 0) # Where to put the delayed sample (MEM)
property memValue : Int32 = 0 # Delayed sample (MEM) value
property pms : Int32 = 0 # Channel PMS
property ams : UInt8 = 0 # Channel AMS
property fc : UInt32 = 0 # fnum, blk: adjusted to sample rate
property kcode : UInt8 = 0 # Key code
property blockFnum : UInt32 = 0 # Current block/fnum value for this slot
property muted : UInt8 = 0
def initialize
@slot = Array(FmSlot).new(4) { |_| FmSlot.new }
end
@[AlwaysInline]
def memConnectAddr
pointerof(@memConnect)
end
{% begin %}
{% for i in 1..4 %}
@[AlwaysInline]
protected def connect{{i}}Addr
pointerof(@connect{{i}})
end
@[AlwaysInline]
def slot{{i}} : FmSlot
@slot.unsafe_fetch(SLOT{{i}})
end
{% end %}
{% end %}
end
class FmSt
property! param : AbstractChip?
property clock : UInt32 = 0 # Main clock rate in hertz
property rate : UInt32 = 0 # Sampling rate in hertz
property freqBase : Float64 = 0.0 # Frequency base
property timerPrescaler : Int32 = 0 # Timer pre-scaler
property address : UInt8 = 0 # Address register
property irq : UInt8 = 0 # Interrupt level
property irqMask : UInt8 = 0 # IRQ mask
property status : UInt8 = 0 # Status flag
property mode : UInt32 = 0 # Mode CSM/3SLOT
property prescalerSel : UInt8 = 0 # Pre-scaler selector
property fnh : UInt8 = 0 # Frequency latch
property ta : Int32 = 0 # Timer A
property tac : Int32 = 0 # Timer A counter
property tb : UInt8 = 0 # Timer B
property tbc : Int32 = 0 # Timer B counter
property dtTab : Array(Array(Int32)) # Detune table
property timerHandler : FmTimerHandler?
property irqHandler : FmIrqHandler?
property ssg : SsgCallbacks = SsgCallbacks.new
def initialize
@dtTab = Array(Array(Int32)).new(8) do |_|
Array(Int32).new(32, 0i32)
end
end
end
class Fm3Slot
property fc : Array(UInt32) = [0u32, 0u32, 0u32] # fnum3, block3: Calculated
property fnh : UInt8 = 0 # Freq3 latch
property kcode : Array(UInt8) = Array(UInt8).new(3, 0u8) # Key code
property blockFnum : Array(UInt32) = [0u32, 0u32, 0u32] # Current fnum value for this slot
def initialize
end
end
# OPN/A/B common state.
class FmOpn
property type : UInt8 = 0 # Chip type
property st : FmSt = FmSt.new # General state
property sl3 : Fm3Slot = Fm3Slot.new # 3-slot mode state
getter pch : Slice(FmCh)
property pan : Array(UInt32) = Array(UInt32).new(6 * 2, 0u32) # FM channel output masks (0xFFFFFFFF = enable)
property egCount : UInt32 = 0 # Global envelope generator counter
property egTimer : UInt32 = 0 # Global envelope generator works at frequency = chipclock / 64 / 3
property egTimerAdd : UInt32 = 0 # Step of egTimer
property egTimerOverflow : UInt32 = 0 # Envelope generator timer overflows every 3 samples (on real chip)
# there are 2048 FNUMs that can be generated using FNUM/BLK registers,
# but LFO works with one more bit of a precision so we really need 4096
# elements.
property fnTable : Array(UInt32) = Array(UInt32).new(4096, 0u32) # fnumber -> increment counter
property fnMax : UInt32 = 0 # Maximal phase increment (used for phase overflow)
#
# LFO
#
property lfoAm : UInt32 = 0 # Runtime LFO calculations helper
property lfoPm : Int32 = 0 # Runtime LFO calculations helper
property lfoCount : UInt32 = 0
property lfoInc : UInt32 = 0
property lfoFreq : Array(UInt32) = Array(UInt32).new(8, 0u32) # LFO freq table
property m2 : Int32 = 0 # Phase modulation inpuyt for operators 2, 3, and 4
property c1 : Int32 = 0 # Phase modulation inpuyt for operators 2, 3, and 4
property c2 : Int32 = 0 # Phase modulation inpuyt for operators 2, 3, and 4
property mem : Int32 = 0 # One sample delay memory
property outFm : Array(Int32) = [0, 0, 0, 0, 0, 0, 0, 0] # Outputs of working channels
# Channel output NONE, LEFT, RIGHT, or CENTER for YM2608/YM2610 ADPCM
property outAdpcm : Array(Int32) = [0, 0, 0, 0]
# Channel output NONE, LEFT, RIGHT, or CENTER for YM2608/YM2610 DELTA
property outDelta : Array(Int32) = [0, 0, 0, 0]
def initialize(@pch : Slice(FmCh))
end
#@[AlwaysInline]
def m2Addr : Pointer(Int32)
pointerof(@m2)
end
#@[AlwaysInline]
def c1Addr : Pointer(Int32)
pointerof(@c1)
end
#@[AlwaysInline]
def c2Addr : Pointer(Int32)
pointerof(@c2)
end
#@[AlwaysInline]
def memAddr : Pointer(Int32)
pointerof(@mem)
end
end
###
### Methods and Macros
###
private macro opnChan(n)
({{n}} & 3)
end
private macro opnSlot(n)
(({{n}} >> 2) & 3)
end
@[AlwaysInline]
protected def statusSet(st : FmSt, flag : Int) : Nil
st.status |= flag
if st.irq == 0 && Yuno.bitflag?(st.status, st.irqMask)
st.irq = 1
st.irqHandler.try &.call(st.param, 1)
end
end
@[AlwaysInline]
protected def statusReset(st : FmSt, flag : Int) : Nil
st.status &= ~flag
if st.irq != 0 && !Yuno.bitflag?(st.status, st.irqMask)
st.irq = 0
st.irqHandler.try &.call(st.param, 0)
end
end
@[AlwaysInline]
protected def irqMaskSet(st : FmSt, flag : Int) : Nil
st.irqMask = flag.to_u8!
statusSet(st, 0)
statusReset(st, 0)
end
@[AlwaysInline]
protected def setTimers(st : FmSt, n : AbstractChip?, v : Int) : Nil
# b7 = CSM MODE
# b6 = 3 slot mode
# b5 = reset b
# b4 = reset a
# b3 = timer enable b
# b2 = timer enable a
# b1 = load b
# b0 = load a
st.mode = v.to_u32!
# Reset timer B flag
statusReset(st, 0x02) if Yuno.bitflag?(v, 0x20)
# Reset timer A flag
statusReset(st, 0x01) if Yuno.bitflag?(v, 0x10)
# Load B
if Yuno.bitflag?(v, 0x02)
if st.tbc == 0
st.tbc = (256 - st.tb.to_i32!) << 4
st.timerHandler.try &.call(n, 1, st.tbc * st.timerPrescaler, st.clock.to_i32!)
end
else
# Stop timer B
unless st.tbc == 0
st.tbc = 0
st.timerHandler.try &.call(n, 1, 0, st.clock.to_i32!)
end
end
# Load A
if Yuno.bitflag?(v, 0x01)
if st.tac == 0
st.tac = 1024 - st.ta
st.timerHandler.try &.call(n, 0, st.tac * st.timerPrescaler, st.clock.to_i32!)
end
else
# Stop timer A
unless st.tac == 0
st.tac = 0
st.timerHandler.try &.call(n, 0, 0, st.clock.to_i32!)
end
end
end
@[AlwaysInline]
protected def timerAOver(st : FmSt) : Nil
# Set status (if enabled)
statusSet(st, 0x01) if Yuno.bitflag?(st.mode, 0x04)
# Clear and reload the counter
st.tac = 1024 - st.ta
st.timerHandler.try &.call(st.param, 0, st.tac * st.timerPrescaler, st.clock.to_i32!)
end
@[AlwaysInline]
protected def timerBOver(st : FmSt) : Nil
# Set status (if enabled)
statusSet(st, 0x02) if Yuno.bitflag?(st.mode, 0x08)
# Clear and reload the counter
st.tbc = (256 - st.tb.to_i32!) << 4
st.timerHandler.try &.call(st.param, 1, st.tbc * st.timerPrescaler, st.clock.to_i32!)
end
@[AlwaysInline]
protected def internalTimerA(opn : FmOpn, st : FmSt, csmCh : FmCh) : Nil
if st.tac != 0 && st.timerHandler.nil?
st.tac -= (st.freqBase * 4096).to_i32!
if st.tac <= 0
timerAOver(st)
# CSM mode total level latch and auto key on
csmKeyControl(csmCh) if Yuno.bitflag?(st.mode, 0x80)
end
end
end
@[AlwaysInline]
protected def internalTimerB(opn : FmOpn, st : FmSt, step : Int) : Nil
if st.tbc != 0 && st.timerHandler.nil?
st.tbc -= (st.freqBase * 4096 * step).to_i32!
if st.tbc <= 0
timerBOver(st)
end
end
end
@[AlwaysInline]
protected def keyOn(ch : FmCh, s : Int) : Nil
slot = ch.slot[s]
if slot.key == 0
slot.key = 1
slot.phase = 0 # Restart phase generator
slot.ssgn = (slot.ssg & 4) >> 1
slot.state = EG_ATT
end
end
@[AlwaysInline]
protected def keyOff(ch : FmCh, s : Int) : Nil
slot = ch.slot[s]
if slot.key != 0
slot.key = 0
slot.state = EG_REL if slot.state > EG_REL # Phase -> Release
end
end
protected def setupConnection(opn : FmOpn, ch : FmCh, chNum : Int) : Nil
# Remi: The algorithms are basically represented as a topology of pointer
# connections, where an operator (FmCh) has pointers to some sample
# placeholders in the FmOpn instance, or to another operator's pointer.
# Pointer to the carrier value.
carrier : Pointer(Int32) = opn.outFm.to_unsafe + chNum
# These are just here to keep the code below a bit easier to read.
om1 : Pointer(Pointer(Int32)) = ch.connect1Addr
om2 : Pointer(Pointer(Int32)) = ch.connect3Addr
oc1 : Pointer(Pointer(Int32)) = ch.connect2Addr
memc : Pointer(Pointer(Int32)) = ch.memConnectAddr
# Set up the topology according to the selected algorithm.
case ch.algo
when 0
# M1---C1---MEM---M2---C2---OUT
om1.value = opn.c1Addr
oc1.value = opn.memAddr
om2.value = opn.c2Addr
memc.value = opn.m2Addr
when 1
# M1------+-MEM---M2---C2---OUT/
# C1-+
om1.value = opn.memAddr
oc1.value = opn.memAddr
om2.value = opn.c2Addr
memc.value = opn.m2Addr
when 2
# M1-----------------+-C2---OUT
# C1---MEM---M2-+
om1.value = opn.c2Addr
oc1.value = opn.memAddr
om2.value = opn.c2Addr
memc.value = opn.m2Addr
when 3
# M1---C1---MEM------+-C2---OUT
# M2-+
om1.value = opn.c1Addr
oc1.value = opn.memAddr
om2.value = opn.c2Addr
memc.value = opn.c2Addr
when 4
# M1---C1-+-OUT
# M2---C2-+
# MEM: not used
om1.value = opn.c1Addr
oc1.value = carrier
om2.value = opn.c2Addr
memc.value = opn.memAddr # Store it somewhere where it will not be used
when 5
# +----C1----+
# M1-+-MEM---M2-+-OUT
# +----C2----+
om1.value = Pointer(Int32).null # Special mark
oc1.value = carrier
om2.value = carrier
memc.value = opn.m2Addr
when 6
# M1---C1-+
# M2-+-OUT
# C2-+
# MEM: not used
om1.value = opn.c1Addr
oc1.value = carrier
om2.value = carrier
memc.value = opn.memAddr # Store it somewhere where it will not be used
when 7
# M1-+
# C1-+-OUT
# M2-+
# C2-+
# MEM: not used
om1.value = carrier
oc1.value = carrier
om2.value = carrier
memc.value = opn.memAddr # Store it somewhere where it will not be used
end
ch.connect4 = carrier
end
# Sets the detune amount and multiple.
@[AlwaysInline]
protected def setDetMul(st : FmSt, ch : FmCh, slot : FmSlot, v : Int) : Nil
slot.mul = if Yuno.bitflag?(v, 0x0F)
(v & 0x0F).to_u32! * 2u32
else
1u32
end
slot.dt = st.dtTab[(v >> 4) & 7]
ch.slot1.incr = -1
end
# Sets the total level.
@[AlwaysInline]
protected def setTl(ch : FmCh, slot : FmSlot, v : Int) : Nil
slot.tl = (v.to_u32! & 0x7F) << (ENV_BITS - 7) # 7-bit TL
end
# Sets the attack rate and key scaling.
@[AlwaysInline]
protected def setArKsr(ch : FmCh, slot : FmSlot, v : Int) : Nil
oldKsr : UInt8 = slot.ksr
slot.ar = if Yuno.bitflag?(v, 0x1F)
32u32 + ((v.to_u32! & 0x1F) << 1)
else
0u32
end
slot.ksr = 3u8 - (v >> 6).to_u8!
if slot.ksr != oldKsr
ch.slot1.incr = -1
end
# Refresh attack rate
if (slot.ar + slot.ksr3.to_u32!) < (32 + 62)
slot.egShAr = EG_RATE_SHIFT[slot.ar + slot.ksr3.to_u32!].to_u8!
slot.egSelAr = EG_RATE_SELECT[slot.ar + slot.ksr3.to_u32!]
else
slot.egShAr = 0
slot.egSelAr = (17 * RATE_STEPS).to_u8!
end
end
# Sets the decay rate
@[AlwaysInline]
protected def setDr(slot : FmSlot, v : Int) : Nil
slot.d1r = (Yuno.bitflag?(v, 0x1F) ? 32u32 + ((v.to_u32! & 0x1F) << 1) : 0u32)
slot.egShD1r = EG_RATE_SHIFT[slot.d1r + slot.ksr3.to_u32!].to_u8!
slot.egSelD1r = EG_RATE_SELECT[slot.d1r + slot.ksr3.to_u32!]
end
# Sets the sustain "rate".
@[AlwaysInline]
protected def setSr(slot : FmSlot, v : Int) : Nil
slot.d2r = Yuno.bitflag?(v, 0x1F) ? 32u32 + ((v.to_u32! & 0x1F) << 1) : 0u32
slot.egShD2r = EG_RATE_SHIFT[slot.d2r + slot.ksr3.to_u32!].to_u8!
slot.egSelD2r = EG_RATE_SELECT[slot.d2r + slot.ksr3.to_u32!]
end
# Sets the release rate.
@[AlwaysInline]
protected def setSlRr(slot : FmSlot, v : Int) : Nil
slot.sl = SL_TABLE[v >> 4]
slot.rr = 34u32 + ((v.to_u32! & 0x0F) << 2)
slot.egShRr = EG_RATE_SHIFT[slot.rr + slot.ksr3.to_u32!].to_u8!
slot.egSelRr = EG_RATE_SELECT[slot.rr + slot.ksr3.to_u32!]
end
@[AlwaysInline]
protected def opCalc(phase : UInt32, env : UInt32, pm : Int32) : Int32
x : UInt32 = (env.to_u64!.unsafe_shl(3) &+
@sinTab.unsafe_fetch((((phase & INV_FREQ_MASK) &+
pm.unsafe_shl(15)).unsafe_shr(FREQ_SH)) & SIN_MASK)).to_u32!
if x >= TL_TAB_LEN
0
else
@tlTab.unsafe_fetch(x)
end
end
# 2715293376
# 284
# 0
@[AlwaysInline]
protected def opCalc1(phase : UInt32, env : UInt32, pm : Int32) : Int32
x : UInt32 = (env.to_u64!.unsafe_shl(3) &+
@sinTab.unsafe_fetch((((phase & INV_FREQ_MASK) &+ pm).unsafe_shr(FREQ_SH)) & SIN_MASK)).to_u32!
if x >= TL_TAB_LEN
0
else
@tlTab.unsafe_fetch(x)
end
end
# Advances the LFO to the next sample.
#@[AlwaysInline]
protected def advanceLfo(opn : FmOpn) : Nil
pos : UInt8 = 0
# Is the LFO enabled?
if opn.lfoInc != 0
# LFO is enabled.
opn.lfoCount += opn.lfoInc
pos = ((opn.lfoCount >> LFO_SH) & 127).to_u8!
# Update AM when LFO output changes.
opn.lfoAm = if pos < 64
(pos & 63).to_u32! * 2u32
else
(126 - ((pos & 63).to_i32! * 2)).to_u32!
end
# PM works with 4 times slower clock.
pos >>= 2
opn.lfoPm = pos
else
# LFO is disabled.
opn.lfoAm = 0u32
opn.lfoPm = 0u32
end
end
protected def advanceEgChannel(opn : FmOpn, slots : Array(FmSlot)) : Nil
swapFlag : UInt32 = 0
output : UInt32 = 0
slotPos : Int32 = 0
# Four operators per channel
i : UInt8 = 4u8
loop do
slot : FmSlot = slots[slotPos]
# Reset SSG-EG swap flag
swapFlag = 0u32
case slot.state
when EG_ATT # Attack phase
unless Yuno.bitflag?(opn.egCount, (1 << slot.egShAr) - 1)
slot.volume += (~(slot.volume) * (EG_INC[slot.egSelAr + ((opn.egCount >> slot.egShAr) & 7)])) >> 4
if slot.volume <= MIN_ATT_INDEX
slot.volume = MIN_ATT_INDEX
slot.state = EG_DEC
end
end
when EG_DEC # Decay phase
if Yuno.bitflag?(slot.ssg, 0x08) # SSG EG type envelope selected
unless Yuno.bitflag?(opn.egCount, (1 << slot.egShD1r) - 1)
slot.volume += 4 * EG_INC[slot.egSelD1r + ((opn.egCount >> slot.egShD1r) & 7)]
if slot.volume >= slot.sl.to_i32!
slot.state = EG_SUS
end
end
else
unless Yuno.bitflag?(opn.egCount, (1 << slot.egShD1r) - 1)
slot.volume += EG_INC[slot.egSelD1r + ((opn.egCount >> slot.egShD1r) & 7)]
if slot.volume >= slot.sl.to_i32!
slot.state = EG_SUS
end
end
end
when EG_SUS # Sustain phase
if Yuno.bitflag?(slot.ssg, 0x08) # SSG EG type envelope selected
unless Yuno.bitflag?(opn.egCount, (1 << slot.egShD2r) - 1)
slot.volume += 4 * EG_INC[slot.egSelD2r + ((opn.egCount >> slot.egShD2r) & 7)]
if slot.volume >= ENV_QUIET
slot.volume = MAX_ATT_INDEX
if Yuno.bitflag?(slot.ssg, 0x01) # bit 0 = hold
if Yuno.bitflag?(slot.ssgn, 1) # Have we swapped once?
# Do nothing, just hold current level.
else
# Swap.
swapFlag = ((slot.ssg & 0x02) | 1).to_u32! # bit 1 = alternate
end
else
# Same as key on operation.
# Restart of the phase generator should be here.
slot.phase = 0
# Phase -> Attack
slot.volume = 511
slot.state = EG_ATT
swapFlag = (slot.ssg & 0x02).to_u32! # bit 1 = alternate
end
end
end
else
unless Yuno.bitflag?(opn.egCount, (1 << slot.egShD2r) - 1)
slot.volume += EG_INC[slot.egSelD2r + ((opn.egCount >> slot.egShD2r) & 7)]
if slot.volume >= MAX_ATT_INDEX
slot.volume = MAX_ATT_INDEX
# do not change SLOT->state (verified on real chip)
end
end
end
when EG_REL # Release phase
unless Yuno.bitflag?(opn.egCount, (1 << slot.egShRr) - 1)
# SSG-EG affects release phase also (Nemesis)
slot.volume += EG_INC[slot.egSelRr + ((opn.egCount >> slot.egShRr) & 7)]
if slot.volume >= MAX_ATT_INDEX
slot.volume = MAX_ATT_INDEX
slot.state = EG_OFF
end
end
end # case slot.state
output = slot.volume.to_u32!
# negate output (changes come from alternate bit, init comes from attack bit)
if Yuno.bitflag?(slot.ssg, 0x08) && Yuno.bitflag?(slot.ssgn, 2) && (slot.state > EG_REL)
output ^= MAX_ATT_INDEX
end
# We need to store the result here because we are going to change ssgn
# in next instruction.
slot.volOut = output + slot.tl
# Reverse slot inversion flag
slot.ssgn ^= swapFlag.to_u8!
slotPos += 1
i -= 1
break if i == 0
end # Loop do
end
#@[AlwaysInline]
protected def updatePhaseLfoSlot(opn : FmOpn, slot : FmSlot, pms : Int32, blockFnum : UInt32) : Nil
fnumLfo : UInt32 = ((blockFnum & 0x7F0) >> 4) * 32u32 * 8u32
lfoFnTableIndexOffset : Int32 = @lfoPmTable[fnumLfo + pms + opn.lfoPm]
if lfoFnTableIndexOffset != 0
# LFO phase modulation is active.
blockFnum = (blockFnum * 2u32) + lfoFnTableIndexOffset.to_u32!
blk : UInt8 = ((blockFnum & 0x7000) >> 12).to_u8!
fn : UInt32 = blockFnum & 0xFFF
# Keyscale code
kc : Int32 = (blk.to_i32! << 2) | OPN_FK_TABLE[fn >> 8]
# Phase increment counter
fc : Int32 = (opn.fnTable[fn] >> (7 - blk.to_i32!)).to_i32! + slot.dt[kc]
# Detects frequency overflow (credits to Nemesis)
fc += opn.fnMax if fc < 0
# Update phase
slot.phase += (fc.to_u32! * slot.mul) >> 1
else
# LFO phase modulation is not active.
slot.phase = slot.phase &+ slot.incr
end
end
@[AlwaysInline]
protected def updatePhaseLfoChannel(opn : FmOpn, ch : FmCh) : Nil
blockFnum : UInt32 = ch.blockFnum
fnumLfo : UInt32 = ((blockFnum & 0x7F0) >> 4) * 32u32 * 8u32
lfoFnTableIndexOffset : Int32 = @lfoPmTable[fnumLfo + ch.pms + opn.lfoPm]
if lfoFnTableIndexOffset != 0
# LFO phase modulation is active.
blockFnum = (blockFnum * 2u32) + lfoFnTableIndexOffset.to_u32!
blk : UInt8 = ((blockFnum & 0x7000) >> 12).to_u8!
fn : UInt32 = blockFnum & 0xFFF
# Keyscale code
kc : Int32 = (blk.to_i32! << 2) | OPN_FK_TABLE[fn >> 8]
# Phase increment counter
fc : Int32 = (opn.fnTable[fn] >> (7 - blk.to_i32!)).to_i32!
# Detects frequency overflow (credits to Nemesis)
finc : Int32 = fc + ch.slot1.dt[kc]
finc += opn.fnMax if finc < 0
ch.slot1.phase += ((finc * ch.slot1.mul) >> 1).to_u32!
finc = fc + ch.slot2.dt[kc]
finc += opn.fnMax if finc < 0
ch.slot2.phase += ((finc * ch.slot2.mul) >> 1).to_u32!
finc = fc + ch.slot3.dt[kc]
finc += opn.fnMax if finc < 0
ch.slot3.phase += ((finc * ch.slot3.mul) >> 1).to_u32!
finc = fc + ch.slot4.dt[kc]
finc += opn.fnMax if finc < 0
ch.slot4.phase += ((finc * ch.slot4.mul) >> 1).to_u32!
else
# LFO phase modulation is not active.
{% begin %}
{% for i in 1..4 %}
ch.slot{{i}}.phase = ch.slot{{i}}.phase &+ ch.slot{{i}}.incr
{% end %}
{% end %}
end
end
@[AlwaysInline]
protected def volumeCalc(op : FmSlot, am : UInt32) : UInt32
op.volOut + (am & op.amMask)
end
@[AlwaysInline]
private def chanCalc(opn : FmOpn, ch : FmCh, chNum : UInt32) : Nil
return if ch.muted != 0
am : UInt32 = opn.lfoAm >> ch.ams
opn.m2 = 0
opn.c1 = 0
opn.c2 = 0
opn.mem = 0
ch.memConnect.value = ch.memValue # Restored delayed sample (MEM) value to m2 or c2
egOut : UInt32 = volumeCalc(ch.slot1, am)
output : Int32 = ch.op1Out.unsafe_fetch(0) + ch.op1Out.unsafe_fetch(1)
ch.op1Out.unsafe_put(0, ch.op1Out.unsafe_fetch(1))
if ch.connect1.null?
# Algorithm 5
opn.mem = ch.op1Out.unsafe_fetch(0)
opn.c1 = ch.op1Out.unsafe_fetch(0)
opn.c2 = ch.op1Out.unsafe_fetch(0)
else
# Other algorithms
ch.connect1.value += ch.op1Out.unsafe_fetch(0)
end
ch.op1Out.unsafe_put(1, 0)
# Slot 1
if egOut < ENV_QUIET
output = 0 if ch.fb == 0
ch.op1Out.unsafe_put(1, opCalc1(ch.slot1.phase, egOut, (output << ch.fb)))
end
# Slot 3
egOut = volumeCalc(ch.slot3, am)
if egOut < ENV_QUIET
ch.connect3.value += opCalc(ch.slot3.phase, egOut, opn.m2)
end
# Slot 2
egOut = volumeCalc(ch.slot2, am)
if egOut < ENV_QUIET
ch.connect2.value += opCalc(ch.slot2.phase, egOut, opn.c1)
end
# Slot 4
egOut = volumeCalc(ch.slot4, am)
if egOut < ENV_QUIET
ch.connect4.value += opCalc(ch.slot4.phase, egOut, opn.c2)
end
# Store current MEM
ch.memValue = opn.mem
# Update phase counters *after* output calculations.
if ch.pms != 0
# Add support for 3-slot mode.
if Yuno.bitflag?(opn.st.mode, 0xC0) && chNum == 2
updatePhaseLfoSlot(opn, ch.slot1, ch.pms, opn.sl3.blockFnum.unsafe_fetch(1))
updatePhaseLfoSlot(opn, ch.slot2, ch.pms, opn.sl3.blockFnum.unsafe_fetch(2))
updatePhaseLfoSlot(opn, ch.slot3, ch.pms, opn.sl3.blockFnum.unsafe_fetch(0))
updatePhaseLfoSlot(opn, ch.slot4, ch.pms, ch.blockFnum)
else
updatePhaseLfoChannel(opn, ch)
end
else
# No LFO phase modulation.
{% begin %}
{% for i in 1..4 %}
ch.slot{{i}}.phase = ch.slot{{i}}.phase &+ ch.slot{{i}}.incr
{% end %}
{% end %}
end
end
# Updates the phase increment and envelope generator.
@[AlwaysInline]
protected def refreshFcEgSlot(opn : FmOpn, slot : FmSlot, fc : Int32, kc : Int)
ksr : Int32 = kc.to_i32! >> slot.ksr
fc += slot.dt[kc]
# Detects frequency overflow (credits to Nemesis)
fc += opn.fnMax if fc < 0
# (frequency) phase increment counter
slot.incr = ((fc &* slot.mul) >> 1).to_i32!
if slot.ksr3 != ksr
slot.ksr3 = ksr.to_u8!
# Calculate envelope generator rates
if (slot.ar + slot.ksr3.to_u32!) < (32 + 62)
slot.egShAr = EG_RATE_SHIFT[slot.ar + slot.ksr3.to_u32!].to_u8!
slot.egSelAr = EG_RATE_SELECT[slot.ar + slot.ksr3.to_u32!]
else
slot.egShAr = 0
slot.egSelAr = (17 * RATE_STEPS).to_u8!
end
slot.egShD1r = EG_RATE_SHIFT[slot.d1r + slot.ksr3.to_u32!].to_u8!
slot.egShD2r = EG_RATE_SHIFT[slot.d2r + slot.ksr3.to_u32!].to_u8!
slot.egShRr = EG_RATE_SHIFT[slot.rr + slot.ksr3.to_u32!].to_u8!
slot.egSelD1r = EG_RATE_SELECT[slot.d1r + slot.ksr3.to_u32!]
slot.egSelD2r = EG_RATE_SELECT[slot.d2r + slot.ksr3.to_u32!]
slot.egSelRr = EG_RATE_SELECT[slot.rr + slot.ksr3.to_u32!]
end
end
# Updates phase increment counters.
@[AlwaysInline]
protected def refreshFcEgChan(opn : FmOpn, chan : FmCh) : Nil
if chan.slot1.incr == -1
fc : Int32 = chan.fc.to_i32!
kc : Int32 = chan.kcode.to_i32!
refreshFcEgSlot(opn, chan.slot1, fc, kc)
refreshFcEgSlot(opn, chan.slot2, fc, kc)
refreshFcEgSlot(opn, chan.slot3, fc, kc)
refreshFcEgSlot(opn, chan.slot4, fc, kc)
end
end
# Initialize time tables
protected def initTimeTables(st : FmSt) : Nil
rate : Float64 = 0.0
# Detune table
4.times do |d|
32.times do |i|
rate = DT_TAB[d * 32 + i].to_f64! * SIN_LEN * st.freqBase * (1 << FREQ_SH) / (1u32 << 20).to_f64!
st.dtTab[d][i] = rate.to_i32!
st.dtTab[d + 4][i] = -(st.dtTab[d][i])
end
end
end
protected def resetChannels(st : FmSt, ch : Slice(FmCh), num : Int) : Nil
st.mode = 0 # Normal mode
st.ta = 0
st.tac = 0
st.tb = 0
st.tbc = 0
num.times do |c|
chan = ch[c]
chan.memValue = 0
chan.op1Out[0] = 0
chan.op1Out[1] = 0
chan.fc = 0
4.times do |s|
slot = chan.slot[s]
slot.incr = -1
slot.key = 0
slot.phase = 0
slot.ssg = 0
slot.ssgn = 0
slot.state = EG_OFF
slot.volume = MAX_ATT_INDEX
slot.volOut = MAX_ATT_INDEX.to_u32!
end
end
end
# Initializes generic tables.
protected def initTables : Nil
n : Int32 = 0
o : Float64 = 0.0
m : Float64 = 0.0
TL_RES_LEN.times do |x|
m = (1 << 16) / (2 ** ((x + 1) * (ENV_STEP / 4.0) / 8.0))
m = m.floor
# We never reach (1<<16) here due to the (x+1). Result fits within 16
# bits at maximum.
n = m.to_i32! # 16 bits here
n >>= 4 # 12 bits here
n = if Yuno.bitflag?(n, 1) # Round to nearest
(n >> 1) + 1
else
n >> 1
end
# 11 bits here
n <<= 2 # 13 bits here
@tlTab[x * 2 + 0] = n
@tlTab[x * 2 + 1] = -(@tlTab[x * 2 + 0])
(1...13).each do |i|
@tlTab[x * 2 + 0 + i * 2 * TL_RES_LEN] = @tlTab[x * 2 + 0] >> i
@tlTab[x * 2 + 1 + i * 2 * TL_RES_LEN] = -(@tlTab[x * 2 + 0 + i * 2 * TL_RES_LEN])
end
end
SIN_LEN.times do |i|
# Non-standard sinus
m = Math.sin(((i * 2) + 1) * Math::PI / SIN_LEN) # Checked against real chip)
# We never reach zero here due to ((i * 2) + 1)
# Conver to decibels.
o = if m > 0.0
8 * Math.log(1.0 / m) / Math.log(2.0)
else
8 * Math.log(-1.0 / m) / Math.log(2.0)
end
o = o / (ENV_STEP / 4)
n = (2.0 * o).to_i32!
# Round to nearest
n = if Yuno.bitflag?(n, 1)
(n >> 1) + 1
else
n >> 1
end
@sinTab[i] = (n * 2 + (m >= 0.0 ? 0 : 1)).to_u32!
end
# Build LFO PM modulation table.
8u32.times do |i|
128u32.times do |fnum| # 7-bits meaningful fnum
value : UInt8 = 0
offsetDepth : UInt32 = i
offsetFnumBit : UInt32 = 0
8.times do |step|
value = 0
7u32.times do |bitTmp| # 7 bits
if Yuno.bitflag?(fnum, (1u32 << bitTmp)) # Only if bit "bitTmp" is set.
offsetFnumBit = bitTmp * 8
value += LFO_PM_OUTPUT[offsetFnumBit + offsetDepth][step]
end
end
@lfoPmTable[(fnum * 32 * 8) + (i * 32) + step + 0] = value.to_i32!
@lfoPmTable[(fnum * 32 * 8) + (i * 32) + (step ^ 7) + 8] = value.to_i32!
@lfoPmTable[(fnum * 32 * 8) + (i * 32) + step + 16] = -(value.to_i32!)
@lfoPmTable[(fnum * 32 * 8) + (i * 32) + (step ^ 7) + 24] = -(value.to_i32!)
end
end
end
end
# CSM key control
#@[AlwaysInline]
protected def csmKeyControl(ch : FmCh) : Nil
# All key on, then off (only for operators which were off in the first place).
{% begin %}
{% for snum in [:SLOT1, :SLOT2, :SLOT3, :SLOT4] %}
if ch.slot[{{snum.id}}].key == 0
keyOn(ch, {{snum.id}})
keyOff(ch, {{snum.id}})
end
{% end %}
{% end %}
end
# Prescaler set (and make time tables)
protected def opnSetPres(opn : FmOpn, pres : Int, timerPrescaler : Int32, ssgPres : Int) : Nil
# Frequency base
opn.st.freqBase = if opn.st.rate != 0
(opn.st.clock.to_f64! / opn.st.rate) / pres
else
0.0
end
opn.egTimerAdd = ((1u32 << EG_SH) * opn.st.freqBase).to_u32!
opn.egTimerOverflow = 3u32 * (1u32 << EG_SH)
# Timer base time
opn.st.timerPrescaler = timerPrescaler
# SSG part prescaler set
if ssgPres != 0
opn.st.ssg.setClock.call((opn.st.clock * 2 / ssgPres).to_u32!)
end
# Make time tables
initTimeTables(opn.st)
# there are 2048 FNUMs that can be generated using FNUM/BLK registers, but
# LFO works with one more bit of a precision so we really need 4096
# elements. Calculate fnumber -> increment counter table.
4096.times do |i|
# Freq table for octave 7.
# OPN phase increment counter = 20 bit
#
# -10 because chip works with 10.10 fixed point, while we use 16.16.
opn.fnTable[i] = (i.to_f64! * 32 * opn.st.freqBase * (1 << (FREQ_SH - 10))).to_u32!
end
# Maximal frequency is required for phase overflow calculation, register
# size is 17 bits (Nemesis).
opn.fnMax = (131072.0 * opn.st.freqBase * (1 << (FREQ_SH - 10))).to_u32!
# LFO freq. table
8.times do |i|
# Amplitude modulation: 64 output levels (triangle waveform); 1 level
# lasts for one of "lfo_samples_per_step" samples.
#
# Phase modulation: one entry from lfo_pm_output lasts for one of 4 *
# "lfo_samples_per_step" samples.
opn.lfoFreq[i] = ((1.0 / LFO_SAMPLES_PER_STEP[i]) * (1 << LFO_SH) * opn.st.freqBase).to_u32!
end
end
# Write an OPN mode register (0x20 - 0x2F).
#@[AlwaysInline]
protected def opnWriteMode(opn : FmOpn, r : Int32, v : Int32) : Nil
case r
#when 0x21 # Test
# nil
when 0x22 # LFO Freq (YM2608 / YM2610 / YM2610B / YM2612)
if Yuno.bitflag?(opn.type, TYPE_LFOPAN)
if Yuno.bitflag?(v, 8) # LFO enabled?
opn.lfoInc = opn.lfoFreq[v & 7]
else
opn.lfoInc = 0
end
end
when 0x24 # Timer A high 8
opn.st.ta = (opn.st.ta & 0x03) | (v << 2)
when 0x25 # Timer A low 2
opn.st.ta = (opn.st.ta & 0x3FC) | (v & 3)
when 0x26 # Timer B
opn.st.tb = v.to_u8
when 0x27 # Mode, timer control
setTimers(opn.st, opn.st.param, v)
when 0x28 # Key on/off
c : UInt8 = v.to_u8! & 0x03
return if c == 3
c += 3 if Yuno.bitflag?(v, 0x04) && Yuno.bitflag?(opn.type, TYPE_6CH)
ch = opn.pch[c]
Yuno.bitflag?(v, 0x10) ? keyOn(ch, SLOT1) : keyOff(ch, SLOT1)
Yuno.bitflag?(v, 0x20) ? keyOn(ch, SLOT2) : keyOff(ch, SLOT2)
Yuno.bitflag?(v, 0x40) ? keyOn(ch, SLOT3) : keyOff(ch, SLOT3)
Yuno.bitflag?(v, 0x80) ? keyOn(ch, SLOT4) : keyOff(ch, SLOT4)
end
end
# Writes to an OPN register (0x30 - 0xFF)
#@[AlwaysInline]
protected def opnWriteReg(opn : FmOpn, r : Int32, v : Int32) : Nil
c : UInt8 = opnChan(r).to_u8!
return if c == 3 # 0xX3, 0xX7, 0xXB, 0xXF
c += 3 if r >= 0x100
ch = opn.pch[c]
slot = ch.slot[opnSlot(r)]
case r & 0xF0
when 0x30 # DET, MUL
setDetMul(opn.st, ch, slot, v)
when 0x40 # TL
setTl(ch, slot, v)
when 0x50 # KS, AR
setArKsr(ch, slot, v)
when 0x60 # bit 7 = AM ENABLE, DR
setDr(slot, v)
if Yuno.bitflag?(opn.type, TYPE_LFOPAN) # YM2608/YM2610/YM2610B/YM2612
slot.amMask = if Yuno.bitflag?(v, 0x80)
UInt32::MAX
else
0u32
end
end
when 0x70 # SR
setSr(slot, v.to_i32!)
when 0x80 # SL, RR
setSlRr(slot, v.to_i32!)
when 0x90 # SSG-EG
slot.ssg = (v & 0x0F).to_u8!
slot.ssgn = (v & 0x04).to_u8! >> 1 # bit 1 in ssgn = attack
# SSG-EG envelope wshapes:
#
# E AtAlH
# 1 0 0 0 \\\\
#
# 1 0 0 1 \___
#
# 1 0 1 0 \/\/
# ___
# 1 0 1 1 \
#
# 1 1 0 0 ////
# ___
# 1 1 0 1 /
#
# 1 1 1 0 /\/\
#
# 1 1 1 1 /___
#
#
# E = SSG-EG enable
#
#
# The shapes are generated using Attack, Decay and Sustain phases.
#
# Each single character in the diagrams above represents this whole
# sequence:
#
# - when KEY-ON = 1, normal Attack phase is generated (*without* any
# difference when compared to normal mode),
#
# - later, when envelope level reaches minimum level (max volume),
# the EG switches to Decay phase (which works with bigger steps
# when compared to normal mode - see below),
#
# - later when envelope level passes the SL level,
# the EG swithes to Sustain phase (which works with bigger steps
# when compared to normal mode - see below),
#
# - finally when envelope level reaches maximum level (min volume),
# the EG switches to Attack phase again (depends on actual waveform).
#
# Important is that when switch to Attack phase occurs, the phase counter
# of that operator will be zeroed-out (as in normal KEY-ON) but not always.
# (I havent found the rule for that - perhaps only when the output level is low)
#
# The difference (when compared to normal Envelope Generator mode) is
# that the resolution in Decay and Sustain phases is 4 times lower;
# this results in only 256 steps instead of normal 1024.
# In other words:
# when SSG-EG is disabled, the step inside of the EG is one,
# when SSG-EG is enabled, the step is four (in Decay and Sustain phases).
#
# Times between the level changes are the same in both modes.
#
#
# Important:
# Decay 1 Level (so called SL) is compared to actual SSG-EG output, so
# it is the same in both SSG and no-SSG modes, with this exception:
#
# when the SSG-EG is enabled and is generating raising levels
# (when the EG output is inverted) the SL will be found at wrong level !!!
# For example, when SL=02:
# 0 -6 = -6dB in non-inverted EG output
# 96-6 = -90dB in inverted EG output
# Which means that EG compares its level to SL as usual, and that the
# output is simply inverted afterall.
#
#
# The Yamaha's manuals say that AR should be set to 0x1f (max speed).
# That is not necessary, but then EG will be generating Attack phase.
when 0xA0
case opnSlot(r)
when 0 # 0xA0 - 0xA2 : FNUM1
fn : UInt32 = ((opn.st.fnh & 7).to_u32! << 8) + v
blk : UInt8 = opn.st.fnh >> 3
# Keyscale code
ch.kcode = ((blk.to_u32! << 2) | OPN_FK_TABLE[fn >> 7]).to_u8!
# Phase increment counter
ch.fc = opn.fnTable[fn * 2] >> (7 - blk.to_i32!)
# Store fnum in clear form for LFO PM calculations
ch.blockFnum = (blk.to_u32! << 11) | fn
ch.slot1.incr = -1
when 1 # 0xA4 - 0xA6 : FNUM2, BLK
opn.st.fnh = (v & 0x3F).to_u8!
when 2 # 0xA8 - 0xAA : 3CH FNUM1
if r < 0x100
fn = ((opn.sl3.fnh.to_u32! & 7) << 8) + v
blk = opn.sl3.fnh >> 3
# Keyscale code
opn.sl3.kcode[c] = ((blk.to_u32! << 2) | OPN_FK_TABLE[fn >> 7]).to_u8!
# Phase increment counter
opn.sl3.fc[c] = opn.fnTable[fn * 2] >> (7 - blk.to_i32!)
# Store fnum in clear form for LFO PM calculations
opn.sl3.blockFnum[c] = (blk.to_u32! << 11) | fn
opn.pch[2].slot1.incr = -1
end
when 3 # 0xAC - 0xAE : #3CH FNUM2, BLK
if r < 0x100
opn.sl3.fnh = (v & 0x3F).to_u8!
end
end
when 0xB0
case opnSlot(r)
when 0 # 0xB0 - 0xB2 : FB, ALGO
feedback : UInt8 = ((v >> 3) & 7).to_u8!
ch.algo = (v & 7).to_u8!
ch.fb = if feedback != 0
feedback &+ 6
else
0u8
end
setupConnection(opn, ch, c)
when 1 # 0xB4 - 0xB6 : L, R, AMS, PMS (YM2612/YM2610B/YM2610/YM2608)
if Yuno.bitflag?(opn.type, TYPE_LFOPAN)
# b0-2 PMS
ch.pms = (v.to_i32! & 7) * 32
# b4-5 Ams
ch.ams = LFO_AMS_DEPTH_SHIFT[(v >> 4) & 3]
# PAN : b7 = L, B6 = R
opn.pan[c * 2 ] = (Yuno.bitflag?(v, 0x80) ? UInt32::MAX : 0u32)
opn.pan[c * 2 + 1] = (Yuno.bitflag?(v, 0x40) ? UInt32::MAX : 0u32)
end
end
end
end
# prescaler circuit (best guess to verified chip behaviour)
#
# +--------------+ +-sel2-+
# | +--|in20 |
# +---+ | +-sel1-+ | |
# M-CLK -+-|1/2|-+--|in10 | +---+ | out|--INT_CLOCK
# | +---+ | out|-|1/3|-|in21 |
# +----------|in11 | +---+ +------+
# +------+
#
# reg.2d : sel2 = in21 (select sel2)
# reg.2e : sel1 = in11 (select sel1)
# reg.2f : sel1 = in10 , sel2 = in20 (clear selector)
# reset : sel1 = in11 , sel2 = in21 (clear both)
protected def opnPrescalerWrite(opn : FmOpn, addr : Int32, preDivider : Int32) : Nil
case addr
when 0 # Reset
opn.st.prescalerSel = 2u8
when 0x2D # Divider sel : sel 1/1 for 1/3line
opn.st.prescalerSel |= 0x02_u8
when 0x2E # Divider sel : select 1/3line for output
opn.st.prescalerSel |= 0x01_u8
when 0x2F # Divider sel : Clear both selector to 1/2, 1/2
opn.st.prescalerSel = 0u8
end
sel = opn.st.prescalerSel & 3
opnSetPres(opn,
OPN_PRES[sel] * preDivider,
OPN_PRES[sel] * preDivider,
SSG_PRES[sel] * preDivider)
end
end
end