Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local component = require("component")
- local event = require("event")
- local computer = require("computer")
- local bit = require("bit32")
- if not component.isAvailable("sound") then
- error("A Computronics Sound Card is required.")
- end
- local sound = component.sound
- local RAM_SIZE = 65536
- local SID_BASE = 0xD400
- local SID_END = 0xD41F
- local MAX_SIDS = 3
- local VOICES_PER_SID = 3
- local TOTAL_VOICES = MAX_SIDS * VOICES_PER_SID
- local SOUND_CHANNELS = 8
- local AUDIO_BATCH_SINGLE = 2
- local AUDIO_BATCH_MULTI = 4
- local PWM_MODE = "blend"
- local SAW_BLEND_GAIN = 1.6
- local SAW_BLEND_MAX = 1.5
- local PWM_DETUNE = 0.006
- local PWM_BALANCE = 0.65
- local PWM_NARROW = 0.15
- local FILTER_ENABLED = true
- local FILTER_MIN_HZ = 30
- local FILTER_MAX_HZ = 12000
- local FILTER_HARMONIC_RATIO = 3
- local FILTER_MAX_GAIN = 1.6
- local ENVELOPE_SHAPING = true
- local DECAY_SCALE = 0.6
- local RELEASE_SCALE = 0.6
- local PULSE_LEVEL_STRENGTH = 1.0
- local PULSE_LEVEL_FLOOR = 0.15
- local HF_SOFTEN_START = 4000
- local HF_SOFTEN_END = 10000
- local HF_SOFTEN_STRENGTH = 0.6
- local HF_SOFTEN_MIX = 0.8
- local COMBINED_LEVEL = 0.7
- local SYNC_APPROX = true
- local RING_APPROX = true
- local SAW_BLEND_EXP = 0.35
- local PULSE_LEVEL_CURVE = 0.6
- local ANALOG_DRIFT_ENABLED = true
- local ANALOG_DRIFT_STRENGTH = 0.0015
- local ANALOG_DRIFT_SMOOTH = 0.05
- local FILTER_MODEL_AWARE = true
- local FILTER_6581_EXPONENT = 1.7
- local FILTER_SELF_OSC_RESONANCE = 14
- local FILTER_SELF_OSC_GAIN = 3.0
- local ENV_ACCURATE = true
- local ENV_SUBSTEPS = 4
- local ENV_ADSR_DELAY_BUG = true
- local ENV_VOLUME_STEP = 0.006
- local NOISE_RATE_SCALE = 16
- local NOISE_LOCKUP = true
- local DEFAULT_SID_MODEL = "6581"
- local BUS_READBACK = true
- local BUS_TTL_CYCLES = 8192
- local CIA_TRACKING = true
- local PAL_HZ = 50
- local NTSC_HZ = 60
- local PAL_CLOCK = 985248
- local NTSC_CLOCK = 1022727
- local SID_FREQ_DIV = 16777216
- local DEFAULT_VOLUME = 0.55
- local MIN_VOLUME = 0.0
- local MAX_VOLUME = 1.0
- local MAX_INSTRUCTIONS_PER_CALL = 200000
- local MAX_INSTRUCTIONS_PER_IRQ = 200000
- local MAX_CYCLES_PER_CALL = 1000000
- local MAX_CYCLES_PER_IRQ = 1000000
- local KERNAL_IRQ_EXIT = 0xEA31
- local WAVE_NOISE = sound.modes.noise
- local WAVE_SQUARE = sound.modes.square
- local WAVE_SAW = sound.modes.sawtooth
- local WAVE_TRIANGLE = sound.modes.triangle
- local function u8(x)
- x = x % 256
- if x < 0 then
- x = x + 256
- end
- return x
- end
- local function u16(x)
- x = x % 65536
- if x < 0 then
- x = x + 65536
- end
- return x
- end
- local function lo(x)
- return bit.band(x, 0xFF)
- end
- local function hi(x)
- return bit.band(bit.rshift(x, 8), 0xFF)
- end
- local function signed8(x)
- if x >= 128 then
- return x - 256
- end
- return x
- end
- local function clamp(x, a, b)
- if x < a then
- return a
- end
- if x > b then
- return b
- end
- return x
- end
- local SID = {}
- function SID.readBE16(data, p)
- local a, b = string.byte(data, p, p + 1)
- if not a or not b then
- error("Unexpected end of SID header.")
- end
- return a * 256 + b
- end
- function SID.readBE32(data, p)
- local a, b, c, d = string.byte(data, p, p + 3)
- if not a or not b or not c or not d then
- error("Unexpected end of SID header.")
- end
- return ((a * 256 + b) * 256 + c) * 256 + d
- end
- function SID.text(data, p, length)
- local s = data:sub(p, p + length - 1)
- s = s:gsub("%z.*$", "")
- s = s:gsub("[%c]", "")
- return s
- end
- function SID.decodeClock(flags)
- if flags == 0 then
- return "unknown"
- end
- local clockBits = bit.band(bit.rshift(flags, 2), 0x03)
- if clockBits == 1 then
- return "PAL"
- end
- if clockBits == 2 then
- return "NTSC"
- end
- if clockBits == 3 then
- return "PAL+NTSC"
- end
- return "unknown"
- end
- function SID.decodeModel(flags, shift)
- local modelBits = bit.band(bit.rshift(flags, shift), 0x03)
- if modelBits == 1 then
- return "6581"
- end
- if modelBits == 2 then
- return "8580"
- end
- if modelBits == 3 then
- return "6581/8580"
- end
- return "unknown"
- end
- function SID.decodeSIDAddress(selector)
- if not selector or selector == 0 then
- return 0
- end
- selector = bit.band(selector, 0xFF)
- if bit.band(selector, 1) ~= 0 then
- return 0
- end
- if selector < 0x42 then
- return 0
- end
- if selector > 0x7F and selector < 0xE0 then
- return 0
- end
- if selector > 0xFE then
- return 0
- end
- return 0xD000 + bit.lshift(selector, 4)
- end
- function SID.loadFile(path)
- local f, err = io.open(path, "rb")
- if not f then
- error("Cannot open SID file: " .. tostring(err))
- end
- local data = f:read("*a")
- f:close()
- if not data or #data < 0x76 then
- error("File is too small to be a valid SID file.")
- end
- local magic = data:sub(1, 4)
- if magic ~= "PSID" and magic ~= "RSID" then
- error("Not a PSID/RSID file.")
- end
- local version = SID.readBE16(data, 5)
- local dataOffset = SID.readBE16(data, 7)
- local loadAddress = SID.readBE16(data, 9)
- local initAddress = SID.readBE16(data, 11)
- local playAddress = SID.readBE16(data, 13)
- local songs = SID.readBE16(data, 15)
- local startSong = SID.readBE16(data, 17)
- local speed = SID.readBE32(data, 19)
- if songs < 1 then
- songs = 1
- end
- if startSong < 1 or startSong > songs then
- startSong = 1
- end
- if dataOffset < 0x76 or dataOffset > #data then
- error("Invalid SID data offset.")
- end
- local flags = 0
- if version >= 2 and #data >= 0x7C then
- flags = SID.readBE16(data, 0x77)
- end
- local secondSID = 0
- local thirdSID = 0
- if version >= 3 and #data >= 0x7C then
- secondSID = SID.decodeSIDAddress(string.byte(data, 0x7B))
- end
- if version >= 4 and #data >= 0x7C then
- thirdSID = SID.decodeSIDAddress(string.byte(data, 0x7C))
- end
- if secondSID == SID_BASE then
- secondSID = 0
- end
- if thirdSID == SID_BASE or thirdSID == secondSID then
- thirdSID = 0
- end
- local name = SID.text(data, 0x17, 32)
- local author = SID.text(data, 0x37, 32)
- local released = SID.text(data, 0x57, 32)
- local binary = data:sub(dataOffset + 1)
- if loadAddress == 0 then
- if #binary < 2 then
- error("SID data has no embedded load address.")
- end
- local a = string.byte(binary, 1)
- local b = string.byte(binary, 2)
- loadAddress = a + b * 256
- binary = binary:sub(3)
- end
- if loadAddress < 0 or loadAddress + #binary > RAM_SIZE then
- error("SID program does not fit into C64 memory.")
- end
- local effectiveInitAddress = initAddress
- if effectiveInitAddress == 0 then
- effectiveInitAddress = loadAddress
- end
- local sidBases = { SID_BASE }
- if secondSID ~= 0 then
- sidBases[2] = secondSID
- end
- if thirdSID ~= 0 then
- sidBases[3] = thirdSID
- end
- return {
- magic = magic,
- version = version,
- dataOffset = dataOffset,
- loadAddress = loadAddress,
- initAddress = initAddress,
- effectiveInitAddress = effectiveInitAddress,
- playAddress = playAddress,
- songs = songs,
- startSong = startSong,
- speed = speed,
- flags = flags,
- clock = SID.decodeClock(flags),
- sidModel1 = SID.decodeModel(flags, 4),
- sidModel2 = SID.decodeModel(flags, 6),
- sidModel3 = SID.decodeModel(flags, 8),
- secondSID = secondSID,
- thirdSID = thirdSID,
- sidBases = sidBases,
- name = name,
- author = author,
- released = released,
- data = binary
- }
- end
- local function lfsrStep(l)
- local b = bit.bxor(bit.rshift(l, 22), bit.rshift(l, 17))
- return bit.band(bit.bor(bit.lshift(l, 1), bit.band(b, 1)), 0x7FFFFF)
- end
- local function lfsrByte(l)
- local function b(n)
- return bit.band(bit.rshift(l, n), 1)
- end
- return b(22) * 128 + b(20) * 64 + b(16) * 32 + b(13) * 16 +
- b(11) * 8 + b(7) * 4 + b(4) * 2 + b(2)
- end
- local ENV_PERIODS = {
- 9, 32, 63, 95, 149, 220, 267, 313,
- 392, 977, 1954, 3126, 3907, 11720, 19532, 31251
- }
- local EnvGen = {}
- EnvGen.__index = EnvGen
- function EnvGen.new()
- local self = setmetatable({}, EnvGen)
- self.level = 0
- self.state = "release"
- self.gate = false
- self.holdZero = true
- self.rateCounter = 0
- self.ratePeriod = ENV_PERIODS[1]
- self.expCounter = 0
- self.expPeriod = 1
- self.attack = 0
- self.decay = 0
- self.release = 0
- self.sustainLevel = 0
- self.t = 0
- return self
- end
- function EnvGen:refreshPeriod()
- if self.state == "attack" then
- self.ratePeriod = ENV_PERIODS[self.attack + 1]
- elseif self.state == "decay" then
- self.ratePeriod = ENV_PERIODS[self.decay + 1]
- else
- self.ratePeriod = ENV_PERIODS[self.release + 1]
- end
- if not ENV_ADSR_DELAY_BUG and self.rateCounter >= self.ratePeriod then
- self.rateCounter = 0
- end
- end
- function EnvGen:setGate(gate)
- if gate == self.gate then
- return
- end
- self.gate = gate
- if gate then
- self.state = "attack"
- self.holdZero = false
- else
- self.state = "release"
- end
- self:refreshPeriod()
- end
- function EnvGen:setAD(value)
- self.attack = math.floor(value / 16) % 16
- self.decay = value % 16
- if self.state ~= "release" then
- self:refreshPeriod()
- end
- end
- function EnvGen:setSR(value)
- self.sustainLevel = (math.floor(value / 16) % 16) * 17
- self.release = value % 16
- if self.state == "release" then
- self:refreshPeriod()
- end
- end
- function EnvGen:tick()
- if self.state ~= "attack" then
- self.expCounter = self.expCounter + 1
- if self.expCounter ~= self.expPeriod then
- return
- end
- end
- self.expCounter = 0
- if self.holdZero then
- return
- end
- local level = self.level
- if self.state == "attack" then
- level = (level + 1) % 256
- if level == 255 then
- self.state = "decay"
- self.ratePeriod = ENV_PERIODS[self.decay + 1]
- end
- elseif self.state == "decay" then
- if level ~= self.sustainLevel then
- level = (level - 1) % 256
- end
- else
- level = (level - 1) % 256
- end
- self.level = level
- if level == 255 then
- self.expPeriod = 1
- elseif level == 93 then
- self.expPeriod = 2
- elseif level == 54 then
- self.expPeriod = 4
- elseif level == 26 then
- self.expPeriod = 8
- elseif level == 14 then
- self.expPeriod = 16
- elseif level == 6 then
- self.expPeriod = 30
- elseif level == 0 then
- self.expPeriod = 1
- self.holdZero = true
- end
- end
- function EnvGen:isIdle()
- if self.state == "decay" then
- return self.holdZero or self.level == self.sustainLevel
- end
- if self.state == "release" then
- return self.holdZero
- end
- return false
- end
- function EnvGen:advance(cycles)
- while cycles > 0 do
- local period = self.ratePeriod
- local rc = self.rateCounter
- local need
- if rc < period then
- need = period - rc
- else
- need = 0x7FFF - rc + period
- end
- if cycles < need then
- rc = rc + cycles
- if rc >= 0x8000 then
- rc = rc - 0x7FFF
- end
- self.rateCounter = rc
- return
- end
- cycles = cycles - need
- self.rateCounter = 0
- self:tick()
- if self:isIdle() then
- period = self.ratePeriod
- local events = math.floor(cycles / period)
- self.rateCounter = cycles - events * period
- self.expCounter = (self.expCounter + events) % self.expPeriod
- return
- end
- end
- end
- function EnvGen:advanceTo(now)
- local n = now - self.t
- if n > 0 then
- self.t = now
- self:advance(n)
- end
- end
- local ENV_CONTROL_VOICE = { [0x04] = 1, [0x0B] = 2, [0x12] = 3 }
- local ENV_AD_VOICE = { [0x05] = 1, [0x0C] = 2, [0x13] = 3 }
- local ENV_SR_VOICE = { [0x06] = 1, [0x0D] = 2, [0x14] = 3 }
- local SIDChip = {}
- SIDChip.__index = SIDChip
- function SIDChip.new()
- local self = setmetatable({}, SIDChip)
- self.reg = {}
- for i = 0, 0x1F do
- self.reg[i] = 0
- end
- self.voices = { {}, {}, {} }
- self.gateOn = { false, false, false }
- self.filter = {}
- self.cpu = nil
- self.osc = { phase = 0, last = 0, lfsr = 0x7FFFF8 }
- self.envs = { EnvGen.new(), EnvGen.new(), EnvGen.new() }
- self.noiseLocked = { false, false, false }
- self.bus = 0
- self.busTime = 0
- return self
- end
- function SIDChip:now()
- if self.cpu then
- return self.cpu.cycles
- end
- return 0
- end
- function SIDChip:advanceOsc(now)
- local o = self.osc
- local elapsed = now - o.last
- o.last = now
- if elapsed <= 0 then
- return
- end
- if bit.band(self.reg[0x12] or 0, 0x08) ~= 0 then
- o.phase = 0
- return
- end
- local freq = (self.reg[0x0E] or 0) + (self.reg[0x0F] or 0) * 256
- local prev = o.phase
- local total = prev + freq * elapsed
- o.phase = total % 16777216
- local rises =
- math.floor((total - 524288) / 1048576) -
- math.floor((prev - 524288) / 1048576)
- if rises > 32 then
- rises = 32
- end
- for _ = 1, rises do
- o.lfsr = lfsrStep(o.lfsr)
- end
- end
- function SIDChip:osc3Output()
- self:advanceOsc(self:now())
- local control = self.reg[0x12] or 0
- local phase = self.osc.phase
- if bit.band(control, 0xF0) == 0 then
- return 0
- end
- local value = 0xFF
- if bit.band(control, 0x10) ~= 0 then
- local t = bit.band(bit.rshift(phase, 15), 0xFF)
- if bit.band(bit.rshift(phase, 23), 1) == 1 then
- t = bit.bxor(t, 0xFF)
- end
- value = bit.band(value, t)
- end
- if bit.band(control, 0x20) ~= 0 then
- value = bit.band(value, bit.band(bit.rshift(phase, 16), 0xFF))
- end
- if bit.band(control, 0x40) ~= 0 then
- local pw = (self.reg[0x10] or 0) +
- bit.band(self.reg[0x11] or 0, 0x0F) * 256
- local acc12 = math.floor(phase / 4096)
- value = bit.band(value, acc12 >= pw and 0xFF or 0)
- end
- if bit.band(control, 0x80) ~= 0 then
- value = bit.band(value, lfsrByte(self.osc.lfsr))
- end
- return value
- end
- function SIDChip:env3Output()
- local e = self.envs[3]
- e:advanceTo(self:now())
- return e.level
- end
- function SIDChip:read(reg)
- reg = bit.band(reg, 0x1F)
- if reg == 0x1B then
- return self:osc3Output()
- end
- if reg == 0x1C then
- return self:env3Output()
- end
- if reg == 0x19 or reg == 0x1A then
- return 0
- end
- if BUS_READBACK then
- if self:now() - self.busTime > BUS_TTL_CYCLES then
- return 0
- end
- return self.bus
- end
- return self.reg[reg] or 0
- end
- function SIDChip:write(reg, value)
- reg = bit.band(reg, 0x1F)
- value = u8(value)
- if reg == 0x03 or reg == 0x0A or reg == 0x11 then
- value = bit.band(value, 0x0F)
- end
- if reg == 0x15 then
- value = bit.band(value, 0x07)
- end
- local now = self:now()
- self.bus = value
- self.busTime = now
- if reg == 0x0E or reg == 0x0F or reg == 0x12 then
- self:advanceOsc(now)
- end
- local envVoice = ENV_CONTROL_VOICE[reg]
- if envVoice then
- local env = self.envs[envVoice]
- env:advanceTo(now)
- env:setGate(bit.band(value, 0x01) ~= 0)
- if bit.band(value, 0x08) ~= 0 then
- self.noiseLocked[envVoice] = false
- end
- end
- envVoice = ENV_AD_VOICE[reg]
- if envVoice then
- local env = self.envs[envVoice]
- env:advanceTo(now)
- env:setAD(value)
- end
- envVoice = ENV_SR_VOICE[reg]
- if envVoice then
- local env = self.envs[envVoice]
- env:advanceTo(now)
- env:setSR(value)
- end
- if reg == 0x04 or reg == 0x0B or reg == 0x12 then
- local voiceIndex = (reg == 0x04) and 1 or ((reg == 0x0B) and 2 or 3)
- local oldGate = bit.band(self.reg[reg] or 0, 1)
- local newGate = bit.band(value, 1)
- if newGate == 1 and oldGate == 0 then
- self.gateOn[voiceIndex] = true
- end
- end
- if reg >= 0x19 and reg <= 0x1C then
- return
- end
- self.reg[reg] = value
- end
- function SIDChip:masterVolume()
- return bit.band(self.reg[0x18] or 0, 0x0F)
- end
- function SIDChip:voice3Off()
- return bit.band(self.reg[0x18] or 0, 0x80) ~= 0
- end
- function SIDChip:voice(v)
- local base = (v - 1) * 7
- local out = self.voices[v]
- local freqLo = self.reg[base] or 0
- local freqHi = self.reg[base + 1] or 0
- local pwLo = self.reg[base + 2] or 0
- local pwHi = self.reg[base + 3] or 0
- local control = self.reg[base + 4] or 0
- local attackDecay = self.reg[base + 5] or 0
- local sustainRelease = self.reg[base + 6] or 0
- out.frequency = freqLo + bit.lshift(freqHi, 8)
- out.pulse = pwLo + bit.lshift(bit.band(pwHi, 0x0F), 8)
- out.control = control
- out.attack = bit.rshift(attackDecay, 4)
- out.decay = bit.band(attackDecay, 0x0F)
- out.sustain = bit.rshift(sustainRelease, 4)
- out.release = bit.band(sustainRelease, 0x0F)
- out.gate = bit.band(control, 0x01) ~= 0
- out.sync = bit.band(control, 0x02) ~= 0
- out.ring = bit.band(control, 0x04) ~= 0
- out.test = bit.band(control, 0x08) ~= 0
- out.triangle = bit.band(control, 0x10) ~= 0
- out.saw = bit.band(control, 0x20) ~= 0
- out.square = bit.band(control, 0x40) ~= 0
- out.noise = bit.band(control, 0x80) ~= 0
- if NOISE_LOCKUP and out.noise and
- (out.triangle or out.saw or out.square) then
- self.noiseLocked[v] = true
- end
- out.noiseLocked = self.noiseLocked[v]
- out.retrigger = self.gateOn[v]
- self.gateOn[v] = false
- return out
- end
- function SIDChip:filterState()
- local r15 = self.reg[0x15] or 0
- local r16 = self.reg[0x16] or 0
- local r17 = self.reg[0x17] or 0
- local r18 = self.reg[0x18] or 0
- local st = self.filter
- st.cutoff = bit.band(r15, 0x07) + bit.lshift(r16, 3)
- st.resonance = bit.rshift(r17, 4)
- st.route = bit.band(r17, 0x07)
- st.lp = bit.band(r18, 0x10) ~= 0
- st.bp = bit.band(r18, 0x20) ~= 0
- st.hp = bit.band(r18, 0x40) ~= 0
- return st
- end
- local OPCODE
- local CPU = {}
- CPU.__index = CPU
- function CPU.new(memory, sids, sidBases)
- local self = setmetatable({}, CPU)
- self.mem = memory
- self.sids = sids
- self.sidBases = sidBases
- self.sidCount = #sids
- self.A = 0
- self.X = 0
- self.Y = 0
- self.P = 0x24
- self.SP = 0xFD
- self.PC = 0
- self.running = true
- self.cycles = 0
- self.lastCallCycles = 0
- self.lastIRQCycles = 0
- for i = 1, #sids do
- sids[i].cpu = self
- end
- return self
- end
- local RMW_OPS = {
- ASL = true, LSR = true, ROL = true, ROR = true,
- INC = true, DEC = true, SLO = true, RLA = true,
- SRE = true, RRA = true, DCP = true, ISC = true
- }
- function CPU:estimateCycles(opcode)
- local entry = OPCODE[opcode]
- if not entry then
- return 2
- end
- local op = entry.op
- local mode = entry.mode
- if op == "BRK" then return 7 end
- if op == "JSR" then return 6 end
- if op == "RTI" or op == "RTS" then return 6 end
- if op == "JMP" then
- return mode == "ind" and 5 or 3
- end
- if mode == "rel" then return 2 end
- if mode == "imp" or mode == "acc" then
- if op == "PHA" or op == "PHP" or op == "PLA" or op == "PLP" then
- return 3
- end
- return 2
- end
- if mode == "imm" then return 2 end
- if mode == "zp" then
- return RMW_OPS[op] and 5 or 3
- end
- if mode == "zpx" or mode == "zpy" then
- return RMW_OPS[op] and 6 or 4
- end
- if mode == "izx" then
- return 6
- end
- if mode == "izy" then
- return (op == "STA" or op == "SAX" or op == "SHA" or op == "AHX") and 6 or 5
- end
- if mode == "abs" then
- return RMW_OPS[op] and 6 or 4
- end
- if mode == "abx" or mode == "aby" then
- if op == "STA" or op == "SHX" or op == "SHY" or op == "TAS" or op == "AHX" then
- return 5
- end
- if RMW_OPS[op] then
- return 7
- end
- return 4
- end
- return 2
- end
- function CPU:sidAt(addr)
- if addr >= SID_BASE and addr <= SID_END then
- return self.sids[1], addr - SID_BASE
- end
- for i = 2, self.sidCount do
- local base = self.sidBases[i]
- if base and base ~= 0 and
- addr >= base and addr <= base + 0x1F then
- return self.sids[i], addr - base
- end
- end
- return nil
- end
- function CPU:read(addr)
- addr = bit.band(addr, 0xFFFF)
- local sid, reg = self:sidAt(addr)
- if sid then
- return sid:read(reg)
- end
- return self.mem[addr] or 0
- end
- function CPU:write(addr, value)
- addr = bit.band(addr, 0xFFFF)
- value = u8(value)
- local sid, reg = self:sidAt(addr)
- if sid then
- sid:write(reg, value)
- return
- end
- self.mem[addr] = value
- end
- function CPU:fetch()
- local pc = self.PC
- local value = self:read(pc)
- self.PC = bit.band(pc + 1, 0xFFFF)
- return value
- end
- function CPU:fetch16()
- local l = self:fetch()
- local h = self:fetch()
- return l + bit.lshift(h, 8)
- end
- function CPU:push(v)
- self:write(0x100 + self.SP, v)
- self.SP = u8(self.SP - 1)
- end
- function CPU:pull()
- self.SP = u8(self.SP + 1)
- return self:read(0x100 + self.SP)
- end
- function CPU:push16(v)
- self:push(hi(v))
- self:push(lo(v))
- end
- function CPU:pull16()
- local l = self:pull()
- local h = self:pull()
- return l + bit.lshift(h, 8)
- end
- function CPU:getFlag(mask)
- return bit.band(self.P, mask) ~= 0
- end
- function CPU:setFlag(mask, value)
- if value then
- self.P = bit.bor(self.P, mask)
- else
- self.P = bit.band(self.P, bit.bnot(mask))
- end
- self.P = bit.bor(self.P, 0x20)
- end
- function CPU:setZN(v)
- v = u8(v)
- self:setFlag(0x02, v == 0)
- self:setFlag(0x80, bit.band(v, 0x80) ~= 0)
- end
- function CPU:adc(v)
- v = u8(v)
- local carry = self:getFlag(0x01) and 1 or 0
- local a = self.A
- local result = a + v + carry
- self:setFlag(0x01, result > 255)
- local r = u8(result)
- local overflow =
- bit.band(
- bit.bnot(bit.bxor(a, v)),
- bit.bxor(a, r),
- 0x80
- ) ~= 0
- self:setFlag(0x40, overflow)
- self.A = r
- self:setZN(self.A)
- end
- function CPU:sbc(v)
- self:adc(bit.bxor(v, 0xFF))
- end
- function CPU:cmp(reg, v)
- v = u8(v)
- local result = u8(reg - v)
- self:setFlag(0x01, reg >= v)
- self:setZN(result)
- end
- function CPU:asl(v)
- v = u8(v)
- local carry = bit.band(v, 0x80) ~= 0
- v = u8(bit.lshift(v, 1))
- self:setFlag(0x01, carry)
- self:setZN(v)
- return v
- end
- function CPU:lsr(v)
- v = u8(v)
- local carry = bit.band(v, 1) ~= 0
- v = bit.rshift(v, 1)
- self:setFlag(0x01, carry)
- self:setZN(v)
- return v
- end
- function CPU:rol(v)
- v = u8(v)
- local carryIn = self:getFlag(0x01) and 1 or 0
- local carryOut = bit.band(v, 0x80) ~= 0
- v = u8(bit.lshift(v, 1) + carryIn)
- self:setFlag(0x01, carryOut)
- self:setZN(v)
- return v
- end
- function CPU:ror(v)
- v = u8(v)
- local carryIn = self:getFlag(0x01) and 0x80 or 0
- local carryOut = bit.band(v, 1) ~= 0
- v = bit.bor(bit.rshift(v, 1), carryIn)
- self:setFlag(0x01, carryOut)
- self:setZN(v)
- return v
- end
- function CPU:addr(mode)
- if mode == "imp" or mode == "acc" then
- return nil
- end
- if mode == "imm" then
- local a = self.PC
- self.PC = u16(self.PC + 1)
- return a
- end
- if mode == "zp" then
- return self:fetch()
- end
- if mode == "zpx" then
- return bit.band(self:fetch() + self.X, 0xFF)
- end
- if mode == "zpy" then
- return bit.band(self:fetch() + self.Y, 0xFF)
- end
- if mode == "abs" then
- return self:fetch16()
- end
- if mode == "abx" then
- return u16(self:fetch16() + self.X)
- end
- if mode == "aby" then
- return u16(self:fetch16() + self.Y)
- end
- if mode == "izx" then
- local p = bit.band(self:fetch() + self.X, 0xFF)
- local l = self:read(p)
- local h = self:read(bit.band(p + 1, 0xFF))
- return l + bit.lshift(h, 8)
- end
- if mode == "izy" then
- local p = self:fetch()
- local l = self:read(p)
- local h = self:read(bit.band(p + 1, 0xFF))
- return u16(l + bit.lshift(h, 8) + self.Y)
- end
- if mode == "ind" then
- local p = self:fetch16()
- local l = self:read(p)
- local hp = bit.bor(bit.band(p, 0xFF00), bit.band(p + 1, 0xFF))
- local h = self:read(hp)
- return l + bit.lshift(h, 8)
- end
- if mode == "rel" then
- local r = signed8(self:fetch())
- return u16(self.PC + r)
- end
- error("Unknown addressing mode: " .. tostring(mode))
- end
- local opcodeRows = {
- "BRK/imp ORA/izx */imp */imp NOP/zp ORA/zp ASL/zp */imp PHP/imp ORA/imm ASL/acc */imp NOP/abs ORA/abs ASL/abs */imp",
- "BPL/rel ORA/izy */imp */imp NOP/zpx ORA/zpx ASL/zpx */imp CLC/imp ORA/aby NOP/imp */imp NOP/abx ORA/abx ASL/abx */imp",
- "JSR/abs AND/izx */imp */imp BIT/zp AND/zp ROL/zp */imp PLP/imp AND/imm ROL/acc */imp BIT/abs AND/abs ROL/abs */imp",
- "BMI/rel AND/izy */imp */imp NOP/zpx AND/zpx ROL/zpx */imp SEC/imp AND/aby NOP/imp */imp NOP/abx AND/abx ROL/abx */imp",
- "RTI/imp EOR/izx */imp */imp NOP/zp EOR/zp LSR/zp */imp PHA/imp EOR/imm LSR/acc */imp JMP/abs EOR/abs LSR/abs */imp",
- "BVC/rel EOR/izy */imp */imp NOP/zpx EOR/zpx LSR/zpx */imp CLI/imp EOR/aby NOP/imp */imp NOP/abx EOR/abx LSR/abx */imp",
- "RTS/imp ADC/izx */imp */imp NOP/zp ADC/zp ROR/zp */imp PLA/imp ADC/imm ROR/acc */imp JMP/ind ADC/abs ROR/abs */imp",
- "BVS/rel ADC/izy */imp */imp NOP/zpx ADC/zpx ROR/zpx */imp SEI/imp ADC/aby NOP/imp */imp NOP/abx ADC/abx ROR/abx */imp",
- "NOP/imm STA/izx NOP/imm */imp STY/zp STA/zp STX/zp */imp DEY/imp NOP/imm TXA/imp */imp STY/abs STA/abs STX/abs */imp",
- "BCC/rel STA/izy */imp */imp STY/zpx STA/zpx STX/zpy */imp TYA/imp STA/aby TXS/imp */imp */imp STA/abx */imp */imp",
- "LDY/imm LDA/izx LDX/imm */imp LDY/zp LDA/zp LDX/zp */imp TAY/imp LDA/imm TAX/imp */imp LDY/abs LDA/abs LDX/abs */imp",
- "BCS/rel LDA/izy */imp */imp LDY/zpx LDA/zpx LDX/zpy */imp CLV/imp LDA/aby TSX/imp */imp LDY/abx LDA/abx LDX/aby */imp",
- "CPY/imm CMP/izx NOP/imm */imp CPY/zp CMP/zp DEC/zp */imp INY/imp CMP/imm DEX/imp */imp CPY/abs CMP/abs DEC/abs */imp",
- "BNE/rel CMP/izy */imp */imp NOP/zpx CMP/zpx DEC/zpx */imp CLD/imp CMP/aby NOP/imp */imp NOP/abx CMP/abx DEC/abx */imp",
- "CPX/imm SBC/izx NOP/imm */imp CPX/zp SBC/zp INC/zp */imp INX/imp SBC/imm NOP/imp */imp CPX/abs SBC/abs INC/abs */imp",
- "BEQ/rel SBC/izy */imp */imp NOP/zpx SBC/zpx INC/zpx */imp SED/imp SBC/aby NOP/imp */imp NOP/abx SBC/abx INC/abx */imp"
- }
- OPCODE = {}
- for rowIndex = 1, #opcodeRows do
- local col = 0
- for token in string.gmatch(opcodeRows[rowIndex], "%S+") do
- col = col + 1
- local slash = string.find(token, "/", 1, true)
- local mnemonic = token:sub(1, slash - 1)
- local mode = token:sub(slash + 1)
- local opcode = (rowIndex - 1) * 16 + (col - 1)
- OPCODE[opcode] = {
- op = mnemonic,
- mode = mode
- }
- end
- end
- local ILLEGAL = {
- [0x03] = {"SLO", "izx"}, [0x07] = {"SLO", "zp"}, [0x0F] = {"SLO", "abs"},
- [0x13] = {"SLO", "izy"}, [0x17] = {"SLO", "zpx"}, [0x1B] = {"SLO", "aby"},
- [0x1F] = {"SLO", "abx"},
- [0x23] = {"RLA", "izx"}, [0x27] = {"RLA", "zp"}, [0x2F] = {"RLA", "abs"},
- [0x33] = {"RLA", "izy"}, [0x37] = {"RLA", "zpx"}, [0x3B] = {"RLA", "aby"},
- [0x3F] = {"RLA", "abx"},
- [0x43] = {"SRE", "izx"}, [0x47] = {"SRE", "zp"}, [0x4F] = {"SRE", "abs"},
- [0x53] = {"SRE", "izy"}, [0x57] = {"SRE", "zpx"}, [0x5B] = {"SRE", "aby"},
- [0x5F] = {"SRE", "abx"},
- [0x63] = {"RRA", "izx"}, [0x67] = {"RRA", "zp"}, [0x6F] = {"RRA", "abs"},
- [0x73] = {"RRA", "izy"}, [0x77] = {"RRA", "zpx"}, [0x7B] = {"RRA", "aby"},
- [0x7F] = {"RRA", "abx"},
- [0x83] = {"SAX", "izx"}, [0x87] = {"SAX", "zp"}, [0x8F] = {"SAX", "abs"},
- [0x97] = {"SAX", "zpy"},
- [0xA3] = {"LAX", "izx"}, [0xA7] = {"LAX", "zp"}, [0xAB] = {"LAX", "imm"},
- [0xAF] = {"LAX", "abs"}, [0xB3] = {"LAX", "izy"}, [0xB7] = {"LAX", "zpy"},
- [0xBF] = {"LAX", "aby"},
- [0xC3] = {"DCP", "izx"}, [0xC7] = {"DCP", "zp"}, [0xCF] = {"DCP", "abs"},
- [0xD3] = {"DCP", "izy"}, [0xD7] = {"DCP", "zpx"}, [0xDB] = {"DCP", "aby"},
- [0xDF] = {"DCP", "abx"},
- [0xE3] = {"ISC", "izx"}, [0xE7] = {"ISC", "zp"}, [0xEF] = {"ISC", "abs"},
- [0xF3] = {"ISC", "izy"}, [0xF7] = {"ISC", "zpx"}, [0xFB] = {"ISC", "aby"},
- [0xFF] = {"ISC", "abx"},
- [0x1A] = {"NOP", "imp"}, [0x3A] = {"NOP", "imp"}, [0x5A] = {"NOP", "imp"},
- [0x7A] = {"NOP", "imp"}, [0xDA] = {"NOP", "imp"}, [0xFA] = {"NOP", "imp"},
- [0x80] = {"NOP", "imm"}, [0x82] = {"NOP", "imm"}, [0x89] = {"NOP", "imm"},
- [0x04] = {"NOP", "zp"}, [0x44] = {"NOP", "zp"}, [0x64] = {"NOP", "zp"},
- [0x14] = {"NOP", "zpx"}, [0x34] = {"NOP", "zpx"}, [0x54] = {"NOP", "zpx"},
- [0x74] = {"NOP", "zpx"}, [0xD4] = {"NOP", "zpx"}, [0xF4] = {"NOP", "zpx"},
- [0x0C] = {"NOP", "abs"},
- [0x1C] = {"NOP", "abx"}, [0x3C] = {"NOP", "abx"}, [0x5C] = {"NOP", "abx"},
- [0x7C] = {"NOP", "abx"}, [0xDC] = {"NOP", "abx"}, [0xFC] = {"NOP", "abx"},
- [0x0B] = {"ANC", "imm"}, [0x2B] = {"ANC", "imm"},
- [0x4B] = {"ALR", "imm"}, [0x6B] = {"ARR", "imm"},
- [0xCB] = {"AXS", "imm"},
- [0xBB] = {"LAS", "aby"},
- [0x9C] = {"SHY", "abx"}, [0x9E] = {"SHX", "aby"}, [0x9B] = {"TAS", "aby"},
- [0x93] = {"AHX", "izy"}, [0x9F] = {"AHX", "aby"}
- }
- for opcode, info in pairs(ILLEGAL) do
- OPCODE[opcode] = {
- op = info[1],
- mode = info[2]
- }
- end
- local KIL = {
- [0x02] = true, [0x12] = true, [0x22] = true, [0x32] = true,
- [0x42] = true, [0x52] = true, [0x62] = true, [0x72] = true,
- [0x92] = true, [0xB2] = true, [0xD2] = true, [0xF2] = true
- }
- for opcode in pairs(KIL) do
- OPCODE[opcode] = {
- op = "KIL",
- mode = "imp"
- }
- end
- local CYCLES = {}
- for opcode = 0, 255 do
- CYCLES[opcode] = CPU.estimateCycles(nil, opcode)
- end
- local BRANCHES = {
- BPL = {0x80, false}, BMI = {0x80, true},
- BVC = {0x40, false}, BVS = {0x40, true},
- BCC = {0x01, false}, BCS = {0x01, true},
- BNE = {0x02, false}, BEQ = {0x02, true}
- }
- local FLAG_OPS = {
- CLC = {0x01, false}, SEC = {0x01, true},
- CLI = {0x04, false}, SEI = {0x04, true},
- CLV = {0x40, false},
- CLD = {0x08, false}, SED = {0x08, true}
- }
- function CPU:execute(opcode)
- local entry = OPCODE[opcode]
- if not entry then
- return
- end
- local op = entry.op
- local mode = entry.mode
- if op == "KIL" then
- self.running = false
- return
- end
- if op == "*" then
- if mode == "imm" or mode == "zp" or mode == "zpx" or
- mode == "zpy" or mode == "rel" then
- self.PC = u16(self.PC + 1)
- elseif mode == "abs" or mode == "abx" or mode == "aby" then
- self.PC = u16(self.PC + 2)
- elseif mode == "izx" or mode == "izy" or mode == "ind" then
- self.PC = u16(self.PC + 1)
- end
- return
- end
- local branch = BRANCHES[op]
- if branch then
- local a = self:addr("rel")
- if self:getFlag(branch[1]) == branch[2] then
- self.PC = a
- end
- return
- end
- local flagOp = FLAG_OPS[op]
- if flagOp then
- self:setFlag(flagOp[1], flagOp[2])
- return
- end
- if op == "JMP" then
- self.PC = self:addr(mode)
- return
- end
- if op == "JSR" then
- local target = self:addr("abs")
- self:push16(u16(self.PC - 1))
- self.PC = target
- return
- end
- if op == "RTS" then
- self.PC = u16(self:pull16() + 1)
- return
- end
- if op == "RTI" then
- self.P = self:pull()
- self.PC = self:pull16()
- self.P = bit.bor(self.P, 0x20)
- return
- end
- if op == "BRK" then
- self.PC = u16(self.PC + 1)
- self:push16(self.PC)
- self:push(bit.bor(self.P, 0x30))
- self:setFlag(0x04, true)
- local l = self:read(0xFFFE)
- local h = self:read(0xFFFF)
- self.PC = l + bit.lshift(h, 8)
- return
- end
- if op == "PHA" then
- self:push(self.A)
- return
- end
- if op == "PLA" then
- self.A = self:pull()
- self:setZN(self.A)
- return
- end
- if op == "PHP" then
- self:push(bit.bor(self.P, 0x30))
- return
- end
- if op == "PLP" then
- self.P = self:pull()
- self.P = bit.bor(self.P, 0x20)
- return
- end
- if op == "TAX" then
- self.X = self.A
- self:setZN(self.X)
- return
- end
- if op == "TAY" then
- self.Y = self.A
- self:setZN(self.Y)
- return
- end
- if op == "TXA" then
- self.A = self.X
- self:setZN(self.A)
- return
- end
- if op == "TYA" then
- self.A = self.Y
- self:setZN(self.A)
- return
- end
- if op == "TSX" then
- self.X = self.SP
- self:setZN(self.X)
- return
- end
- if op == "TXS" then
- self.SP = self.X
- return
- end
- if op == "INX" then
- self.X = u8(self.X + 1)
- self:setZN(self.X)
- return
- end
- if op == "DEX" then
- self.X = u8(self.X - 1)
- self:setZN(self.X)
- return
- end
- if op == "INY" then
- self.Y = u8(self.Y + 1)
- self:setZN(self.Y)
- return
- end
- if op == "DEY" then
- self.Y = u8(self.Y - 1)
- self:setZN(self.Y)
- return
- end
- if op == "ASL" or op == "LSR" or op == "ROL" or op == "ROR" then
- local fn
- if op == "ASL" then
- fn = self.asl
- elseif op == "LSR" then
- fn = self.lsr
- elseif op == "ROL" then
- fn = self.rol
- else
- fn = self.ror
- end
- if mode == "acc" then
- self.A = fn(self, self.A)
- else
- local a = self:addr(mode)
- self:write(a, fn(self, self:read(a)))
- end
- return
- end
- if op == "LDA" then
- local a = self:addr(mode)
- self.A = self:read(a)
- self:setZN(self.A)
- return
- end
- if op == "LDX" then
- local a = self:addr(mode)
- self.X = self:read(a)
- self:setZN(self.X)
- return
- end
- if op == "LDY" then
- local a = self:addr(mode)
- self.Y = self:read(a)
- self:setZN(self.Y)
- return
- end
- if op == "STA" then
- local a = self:addr(mode)
- self:write(a, self.A)
- return
- end
- if op == "STX" then
- local a = self:addr(mode)
- self:write(a, self.X)
- return
- end
- if op == "STY" then
- local a = self:addr(mode)
- self:write(a, self.Y)
- return
- end
- if op == "AND" then
- local a = self:addr(mode)
- self.A = bit.band(self.A, self:read(a))
- self:setZN(self.A)
- return
- end
- if op == "ORA" then
- local a = self:addr(mode)
- self.A = bit.bor(self.A, self:read(a))
- self:setZN(self.A)
- return
- end
- if op == "EOR" then
- local a = self:addr(mode)
- self.A = bit.bxor(self.A, self:read(a))
- self:setZN(self.A)
- return
- end
- if op == "BIT" then
- local a = self:addr(mode)
- local v = self:read(a)
- self:setFlag(0x02, bit.band(self.A, v) == 0)
- self:setFlag(0x80, bit.band(v, 0x80) ~= 0)
- self:setFlag(0x40, bit.band(v, 0x40) ~= 0)
- return
- end
- if op == "ADC" then
- local a = self:addr(mode)
- self:adc(self:read(a))
- return
- end
- if op == "SBC" then
- local a = self:addr(mode)
- self:sbc(self:read(a))
- return
- end
- if op == "CMP" then
- local a = self:addr(mode)
- self:cmp(self.A, self:read(a))
- return
- end
- if op == "CPX" then
- local a = self:addr(mode)
- self:cmp(self.X, self:read(a))
- return
- end
- if op == "CPY" then
- local a = self:addr(mode)
- self:cmp(self.Y, self:read(a))
- return
- end
- if op == "INC" then
- local a = self:addr(mode)
- local v = u8(self:read(a) + 1)
- self:write(a, v)
- self:setZN(v)
- return
- end
- if op == "DEC" then
- local a = self:addr(mode)
- local v = u8(self:read(a) - 1)
- self:write(a, v)
- self:setZN(v)
- return
- end
- if op == "NOP" then
- if mode ~= "imp" and mode ~= "acc" then
- self:addr(mode)
- end
- return
- end
- if op == "SLO" then
- local a = self:addr(mode)
- local v = self:asl(self:read(a))
- self:write(a, v)
- self.A = bit.bor(self.A, v)
- self:setZN(self.A)
- return
- end
- if op == "RLA" then
- local a = self:addr(mode)
- local v = self:rol(self:read(a))
- self:write(a, v)
- self.A = bit.band(self.A, v)
- self:setZN(self.A)
- return
- end
- if op == "SRE" then
- local a = self:addr(mode)
- local v = self:lsr(self:read(a))
- self:write(a, v)
- self.A = bit.bxor(self.A, v)
- self:setZN(self.A)
- return
- end
- if op == "RRA" then
- local a = self:addr(mode)
- local v = self:ror(self:read(a))
- self:write(a, v)
- self:adc(v)
- return
- end
- if op == "SAX" then
- local a = self:addr(mode)
- self:write(a, bit.band(self.A, self.X))
- return
- end
- if op == "LAX" then
- local a = self:addr(mode)
- local v = self:read(a)
- self.A = v
- self.X = v
- self:setZN(v)
- return
- end
- if op == "DCP" then
- local a = self:addr(mode)
- local v = u8(self:read(a) - 1)
- self:write(a, v)
- self:cmp(self.A, v)
- return
- end
- if op == "ISC" then
- local a = self:addr(mode)
- local v = u8(self:read(a) + 1)
- self:write(a, v)
- self:sbc(v)
- return
- end
- if op == "ANC" then
- local a = self:addr(mode)
- self.A = bit.band(self.A, self:read(a))
- self:setZN(self.A)
- self:setFlag(0x01, bit.band(self.A, 0x80) ~= 0)
- return
- end
- if op == "ALR" then
- local a = self:addr(mode)
- self.A = bit.band(self.A, self:read(a))
- self.A = self:lsr(self.A)
- return
- end
- if op == "ARR" then
- local a = self:addr(mode)
- local value = bit.band(self.A, self:read(a))
- local carryIn = self:getFlag(0x01) and 0x80 or 0
- value = bit.bor(bit.rshift(value, 1), carryIn)
- self.A = value
- self:setZN(value)
- self:setFlag(0x01, bit.band(value, 0x40) ~= 0)
- self:setFlag(
- 0x40,
- bit.bxor(bit.band(value, 0x40), bit.band(value, 0x20)) ~= 0
- )
- return
- end
- if op == "AXS" then
- local a = self:addr(mode)
- local value = self:read(a)
- local result = bit.band(self.A, self.X) - value
- self:setFlag(0x01, result >= 0)
- self.X = u8(result)
- self:setZN(self.X)
- return
- end
- if op == "LAS" then
- local a = self:addr(mode)
- local v = bit.band(self:read(a), self.SP)
- self.A = v
- self.X = v
- self.SP = v
- self:setZN(v)
- return
- end
- if op == "SHY" then
- local base = self:fetch16()
- local address = u16(base + self.X)
- self:write(address, bit.band(self.Y, hi(address) + 1))
- return
- end
- if op == "SHX" then
- local base = self:fetch16()
- local address = u16(base + self.Y)
- self:write(address, bit.band(self.X, hi(address) + 1))
- return
- end
- if op == "TAS" then
- local base = self:fetch16()
- local address = u16(base + self.Y)
- self.SP = bit.band(self.A, self.X)
- self:write(address, bit.band(self.SP, hi(address) + 1))
- return
- end
- if op == "AHX" then
- local a = self:addr(mode)
- self:write(a, bit.band(self.A, self.X, hi(a) + 1))
- return
- end
- end
- function CPU:runUntilReturn(maxInstructions)
- maxInstructions = maxInstructions or MAX_INSTRUCTIONS_PER_CALL
- local count = 0
- local cycles = 0
- self.lastCallCycles = 0
- while self.running and
- count < maxInstructions and
- cycles < MAX_CYCLES_PER_CALL do
- if self.PC == 0xFFFF then
- self.running = false
- return true
- end
- local opcode = self:fetch()
- if not OPCODE[opcode] then
- self.running = false
- self.lastCallCycles = cycles
- return false,
- string.format("Unknown 6502 opcode $%02X", opcode)
- end
- local instructionCycles = CYCLES[opcode] or 2
- self:execute(opcode)
- cycles = cycles + instructionCycles
- self.cycles = self.cycles + instructionCycles
- count = count + 1
- end
- self.lastCallCycles = cycles
- if not self.running then
- return false, "CPU stopped on KIL/JAM opcode"
- end
- self.running = false
- if cycles >= MAX_CYCLES_PER_CALL then
- return false, "CPU cycle watchdog triggered"
- end
- return false, "CPU instruction watchdog triggered"
- end
- function CPU:finishSyntheticIRQ()
- self.cycles = self.cycles + 6
- self.lastIRQCycles = self.lastIRQCycles + 6
- self.P = self:pull()
- self.PC = self:pull16()
- self.P = bit.bor(self.P, 0x20)
- self.PC = u16(self.PC)
- return true
- end
- function CPU:runUntilIRQReturn(maxInstructions)
- maxInstructions = maxInstructions or MAX_INSTRUCTIONS_PER_IRQ
- local count = 0
- local cycles = 0
- self.lastIRQCycles = 0
- while self.running and
- count < maxInstructions and
- cycles < MAX_CYCLES_PER_IRQ do
- if self.PC == KERNAL_IRQ_EXIT then
- return self:finishSyntheticIRQ()
- end
- local opcode = self:fetch()
- if not OPCODE[opcode] then
- self.running = false
- self.lastIRQCycles = cycles
- return false,
- string.format("Unknown 6502 IRQ opcode $%02X", opcode)
- end
- local instructionCycles = CYCLES[opcode] or 2
- if opcode == 0x40 then
- self:execute(opcode)
- cycles = cycles + instructionCycles
- self.cycles = self.cycles + instructionCycles
- self.lastIRQCycles = cycles
- return true
- end
- self:execute(opcode)
- cycles = cycles + instructionCycles
- self.cycles = self.cycles + instructionCycles
- count = count + 1
- end
- self.lastIRQCycles = cycles
- if not self.running then
- return false, "IRQ CPU stopped on KIL/JAM opcode"
- end
- if cycles >= MAX_CYCLES_PER_IRQ then
- return false, "IRQ cycle watchdog triggered"
- end
- return false, "IRQ instruction watchdog triggered"
- end
- function CPU:interrupt(vector)
- local oldP = self.P
- self:push16(self.PC)
- self:push(bit.band(oldP, 0xEF))
- self.P = bit.bor(bit.band(oldP, 0xFB), 0x20)
- local low = self:read(vector)
- local high = self:read(bit.band(vector + 1, 0xFFFF))
- self.PC = low + bit.lshift(high, 8)
- self.running = true
- return self:runUntilIRQReturn()
- end
- function CPU:call(address, a, x, y)
- self.A = u8(a or 0)
- self.X = u8(x or 0)
- self.Y = u8(y or 0)
- self.P = 0x24
- self.SP = 0xFB
- self.running = true
- self:push16(0xFFFE)
- self.PC = u16(address)
- return self:runUntilReturn()
- end
- local AUDIO = {}
- AUDIO.__index = AUDIO
- local ATTACK_TIMES = {
- 2, 8, 16, 24,
- 38, 56, 68, 80,
- 100, 250, 500, 800,
- 1000, 3000, 5000, 8000
- }
- local DECAY_TIMES = {
- 6, 24, 48, 72,
- 114, 168, 204, 240,
- 300, 750, 1500, 2400,
- 3000, 9000, 15000, 24000
- }
- function AUDIO.new(sids, sidCount)
- local self = setmetatable({}, AUDIO)
- self.sids = sids
- self.sidCount = sidCount
- self.volume = DEFAULT_VOLUME
- self.channels = {}
- self.drift = {}
- self.sidModels = nil
- self.audioChannelCount = math.min(SOUND_CHANNELS, TOTAL_VOICES)
- self.pendingFrames = 0
- self.batchFrames =
- sidCount >= 2 and AUDIO_BATCH_MULTI or AUDIO_BATCH_SINGLE
- self.channelsSupported = self.audioChannelCount
- self.pwmMode = PWM_MODE
- self.pwmPair = ((PWM_MODE == "blend" or PWM_MODE == "pair") and sidCount == 1)
- self.filterEnabled = FILTER_ENABLED
- self.muteMask = {}
- self.curSamples = nil
- self.delayCarry = 0
- for i = 1, self.audioChannelCount do
- self.channels[i] = {
- opened = false,
- gate = false,
- waveform = nil,
- frequency = nil,
- pulse = nil,
- volume = nil,
- attack = nil,
- decay = nil,
- sustain = nil,
- release = nil,
- adsrSet = false,
- base = 0,
- envSamples = nil,
- touched = false,
- audible = false
- }
- end
- return self
- end
- function AUDIO:driftFactor(key)
- if not ANALOG_DRIFT_ENABLED then
- return 1
- end
- local d = self.drift[key]
- if not d then
- d = { value = 0, target = 0 }
- self.drift[key] = d
- end
- d.target = clamp(d.target + (math.random() * 2 - 1) * 0.15, -1, 1)
- d.value = d.value + (d.target - d.value) * ANALOG_DRIFT_SMOOTH
- return 1 + d.value * ANALOG_DRIFT_STRENGTH
- end
- function AUDIO:setDrift(enabled)
- ANALOG_DRIFT_ENABLED = enabled
- end
- function AUDIO:filterGain(f, voiceNumber, freq, model)
- if not self.filterEnabled then
- return 1, false
- end
- local routeBit = bit.lshift(1, voiceNumber - 1)
- if bit.band(f.route, routeBit) == 0 then
- return 1, false
- end
- if not (f.lp or f.bp or f.hp) then
- return 0, false
- end
- if freq <= 0 then
- return 1, false
- end
- if model ~= "6581" and model ~= "8580" then
- model = DEFAULT_SID_MODEL
- end
- local fc
- if FILTER_MODEL_AWARE and model == "6581" then
- local frac = f.cutoff / 2047
- fc = FILTER_MIN_HZ +
- (FILTER_MAX_HZ - FILTER_MIN_HZ) * (frac ^ FILTER_6581_EXPONENT)
- else
- fc = FILTER_MIN_HZ +
- f.cutoff * (FILTER_MAX_HZ - FILTER_MIN_HZ) / 2047
- end
- local r = freq / fc
- local r2 = r * r
- local q = 0.7 + f.resonance * 0.4
- local maxGain = FILTER_MAX_GAIN
- if f.resonance >= FILTER_SELF_OSC_RESONANCE then
- maxGain = FILTER_SELF_OSC_GAIN
- end
- local numRe = (f.lp and 1 or 0) - (f.hp and r2 or 0)
- local numIm = f.bp and (r / q) or 0
- local denRe = 1 - r2
- local denIm = r / q
- local den = math.sqrt(denRe * denRe + denIm * denIm)
- if den < 1e-6 then
- den = 1e-6
- end
- local gain = math.sqrt(numRe * numRe + numIm * numIm) / den
- gain = clamp(gain, 0, maxGain)
- local rich =
- (f.lp or f.bp) and
- (not f.hp) and
- fc < freq * FILTER_HARMONIC_RATIO
- return gain, rich
- end
- function AUDIO:setFilter(enabled)
- self.filterEnabled = enabled
- end
- function AUDIO:setPWMMode(mode)
- self.pwmMode = mode
- self.pwmPair = ((mode == "blend" or mode == "pair") and self.sidCount == 1)
- self:resetChannels()
- end
- function AUDIO:pwmDescription()
- if self.pwmPair and self.pwmMode == "blend" then
- return "square+saw blend (channels 4-6 used as partners)"
- end
- if self.pwmPair then
- return "detuned pair (channels 4-6 used as partners)"
- end
- if self.pwmMode == "morph" then
- return "timbre morph (square -> saw at narrow duty)"
- end
- if self.pwmMode ~= "off" then
- return "morph fallback (" .. self.pwmMode .. " mode needs a single-SID tune)"
- end
- return "off (plain 50% square)"
- end
- function AUDIO:frequencyFromSID(value, pal)
- value = bit.band(value or 0, 0xFFFF)
- if value == 0 then
- return 0
- end
- local clock = pal and PAL_CLOCK or NTSC_CLOCK
- local f = value * clock / SID_FREQ_DIV
- if f <= 0 then
- return 0
- end
- if f > 20000 then
- return 20000
- end
- return f
- end
- function AUDIO:waveform(v)
- if v.noise then
- return WAVE_NOISE
- end
- if v.square then
- if self.pwmMode ~= "off" and not self.pwmPair then
- local duty = v.pulse / 4096
- if duty < PWM_NARROW or duty > 1 - PWM_NARROW then
- return WAVE_SAW
- end
- end
- return WAVE_SQUARE
- end
- if v.saw then
- return WAVE_SAW
- end
- if v.triangle then
- return WAVE_TRIANGLE
- end
- return WAVE_SQUARE
- end
- function AUDIO:adsr(v)
- local attack = ATTACK_TIMES[v.attack + 1] or 8
- local decay = DECAY_TIMES[v.decay + 1] or 24
- local sustain = v.sustain / 15
- local release = DECAY_TIMES[v.release + 1] or 24
- if ENVELOPE_SHAPING then
- decay = decay * (1 - sustain) * DECAY_SCALE
- release = release * math.max(sustain, 0.25) * RELEASE_SCALE
- end
- attack = math.max(1, math.floor(attack + 0.5))
- decay = math.max(1, math.floor(decay + 0.5))
- release = math.max(1, math.floor(release + 0.5))
- return attack, decay, sustain, release
- end
- function AUDIO:setVolume(v)
- self.volume = clamp(v, MIN_VOLUME, MAX_VOLUME)
- end
- function AUDIO:toggleMute(logical)
- self.muteMask[logical] = not self.muteMask[logical]
- return self.muteMask[logical]
- end
- function AUDIO:queueDelay(ms)
- local total = ms + self.delayCarry
- local whole = math.floor(total + 0.5)
- self.delayCarry = total - whole
- if whole > 0 then
- sound.delay(whole)
- end
- end
- function AUDIO:sampleEnvelope(env, startCycles, frameCycles, steps)
- local samples = {}
- for s = 1, steps do
- env:advanceTo(
- math.floor(startCycles + frameCycles * (s - 0.5) / steps)
- )
- samples[s] = env.level
- end
- env:advanceTo(startCycles + frameCycles)
- return samples
- end
- function AUDIO:setChannelVolume(ch, channel, volume)
- if ch.volume == nil or
- math.abs(ch.volume - volume) > ENV_VOLUME_STEP or
- (volume == 0 and ch.volume ~= 0) then
- sound.setVolume(channel, volume)
- ch.volume = volume
- end
- end
- function AUDIO:applyChannelEnvelope(ch, channel, volume)
- local samples = self.curSamples
- if not ch.adsrSet then
- sound.setADSR(channel, 1, 1, 1, 1)
- ch.adsrSet = true
- ch.attack = nil
- ch.decay = nil
- ch.sustain = nil
- ch.release = nil
- end
- local peak = 0
- for i = 1, #samples do
- if samples[i] > peak then
- peak = samples[i]
- end
- end
- ch.base = volume
- ch.envSamples = samples
- ch.touched = true
- if volume > 0 and peak > 0 then
- self:setChannelVolume(ch, channel, volume * samples[1] / 255)
- if not ch.opened then
- sound.open(channel)
- ch.opened = true
- end
- elseif ch.opened then
- sound.close(channel)
- ch.opened = false
- end
- end
- function AUDIO:channelFor(sidIndex, voiceNumber)
- local logical = (sidIndex - 1) * VOICES_PER_SID + voiceNumber
- if logical > self.audioChannelCount then
- return nil
- end
- return logical
- end
- function AUDIO:applyChannel(channel, wave, frequency, volume,
- attack, decay, sustain, release, gate, retrigger)
- local ch = self.channels[channel]
- if ch.waveform ~= wave then
- sound.setWave(channel, wave)
- ch.waveform = wave
- end
- if ch.frequency == nil or math.abs(ch.frequency - frequency) > 0.25 then
- sound.setFrequency(channel, frequency)
- ch.frequency = frequency
- end
- if self.curSamples then
- self:applyChannelEnvelope(ch, channel, volume)
- ch.gate = gate
- return
- end
- if ch.attack ~= attack or ch.decay ~= decay or
- ch.sustain ~= sustain or ch.release ~= release then
- sound.setADSR(channel, attack, decay, sustain, release)
- ch.attack = attack
- ch.decay = decay
- ch.sustain = sustain
- ch.release = release
- end
- if ch.volume == nil or math.abs(ch.volume - volume) > 0.001 then
- sound.setVolume(channel, volume)
- ch.volume = volume
- end
- if gate then
- if ch.opened and retrigger then
- sound.close(channel)
- ch.opened = false
- end
- if not ch.opened then
- sound.open(channel)
- ch.opened = true
- end
- elseif ch.opened then
- sound.close(channel)
- ch.opened = false
- end
- ch.gate = gate
- end
- function AUDIO:updateVoice(channel, voiceNumber, v, pal, masterVolume, muted, fstate, modV, sidIndex)
- if not channel then
- return
- end
- local frequency = self:frequencyFromSID(v.frequency, pal)
- local modFrequency = 0
- if modV then
- modFrequency = self:frequencyFromSID(modV.frequency, pal)
- end
- if SYNC_APPROX and v.sync and modFrequency > 0 then
- frequency = modFrequency
- end
- if frequency > 0 then
- frequency = frequency * self:driftFactor(sidIndex * 10 + voiceNumber)
- end
- if v.noise and frequency > 0 then
- frequency = math.min(frequency * NOISE_RATE_SCALE, 20000)
- end
- local ringActive =
- RING_APPROX and
- v.ring and
- v.triangle and
- (not v.noise) and
- modFrequency > 0 and
- frequency > 0
- local ringSum = 0
- local ringDiff = 0
- if ringActive then
- ringSum = math.min(frequency + modFrequency, 20000)
- ringDiff = math.abs(frequency - modFrequency)
- end
- local wave = self:waveform(v)
- local attack, decay, sustain, release = self:adsr(v)
- local hasWave = v.noise or v.square or v.saw or v.triangle
- local noiseDead = NOISE_LOCKUP and v.noise and v.noiseLocked
- local audible =
- (not v.test) and hasWave and (not muted) and (not noiseDead)
- local levelScale = 1
- local waveBits =
- (v.triangle and 1 or 0) +
- (v.saw and 1 or 0) +
- (v.square and 1 or 0)
- if not v.noise then
- if waveBits >= 2 then
- levelScale = COMBINED_LEVEL
- elseif v.square then
- local d = v.pulse / 4096
- local pulseLevel =
- math.max(PULSE_LEVEL_FLOOR, 2 * math.sqrt(d * (1 - d)))
- local perceivedLevel = pulseLevel ^ PULSE_LEVEL_CURVE
- levelScale = 1 + PULSE_LEVEL_STRENGTH * (perceivedLevel - 1)
- end
- end
- local targetVolume = 0
- if audible then
- local gain, rich = 1, false
- if fstate then
- gain, rich = self:filterGain(fstate, voiceNumber, frequency, self.sidModels and self.sidModels[sidIndex])
- end
- if rich and (wave == WAVE_SAW or wave == WAVE_SQUARE) then
- wave = WAVE_TRIANGLE
- end
- targetVolume = clamp(
- self.volume * masterVolume / 15 * gain * levelScale,
- MIN_VOLUME,
- MAX_VOLUME
- )
- end
- self.channels[channel].pulse = v.pulse
- self.channels[channel].audible = audible
- local pairChannel = nil
- if self.pwmPair then
- pairChannel = channel + VOICES_PER_SID
- end
- local partnerMode = nil
- if ringActive then
- wave = WAVE_TRIANGLE
- if pairChannel and ringDiff >= 20 then
- partnerMode = "ring"
- else
- frequency = ringSum
- end
- elseif pairChannel and not v.noise then
- if wave == WAVE_SQUARE and v.square then
- if v.triangle and not v.saw then
- partnerMode = "square-tri"
- elseif self.pwmMode == "blend" then
- partnerMode = "blend"
- elseif self.pwmMode == "pair" and
- not v.saw and not v.triangle then
- partnerMode = "pair"
- end
- elseif wave == WAVE_SAW and v.saw and v.triangle and
- not v.square then
- partnerMode = "saw-tri"
- end
- end
- if partnerMode == "blend" then
- local duty = v.pulse / 4096
- local e = math.abs(1 - 2 * duty)
- local w = math.min(SAW_BLEND_MAX, SAW_BLEND_GAIN * e ^ SAW_BLEND_EXP)
- if v.saw then
- w = SAW_BLEND_MAX
- end
- local hf = clamp(
- (frequency - HF_SOFTEN_START) /
- (HF_SOFTEN_END - HF_SOFTEN_START),
- 0,
- 1
- )
- local partnerWave = WAVE_SAW
- if hf > 0 then
- w = math.max(w, HF_SOFTEN_MIX * hf)
- if hf > 0.5 then
- partnerWave = WAVE_TRIANGLE
- end
- end
- local squareShare = 1 - HF_SOFTEN_STRENGTH * hf
- local norm = math.sqrt(squareShare * squareShare + w * w / 3)
- local squareVolume = clamp(targetVolume * squareShare / norm, 0, 1)
- local partnerVolume = clamp(targetVolume * w / norm, 0, 1)
- self:applyChannel(
- channel, WAVE_SQUARE, frequency, squareVolume,
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- self:applyChannel(
- pairChannel, partnerWave, frequency, partnerVolume,
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- elseif partnerMode == "pair" then
- self:applyChannel(
- channel, WAVE_SQUARE, frequency,
- targetVolume * PWM_BALANCE,
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- self:applyChannel(
- pairChannel, WAVE_SQUARE,
- math.min(frequency * (1 + PWM_DETUNE), 20000),
- targetVolume * (1 - PWM_BALANCE),
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- elseif partnerMode == "ring" then
- self:applyChannel(
- channel, WAVE_TRIANGLE, ringSum, targetVolume * 0.6,
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- self:applyChannel(
- pairChannel, WAVE_TRIANGLE, ringDiff, targetVolume * 0.6,
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- elseif partnerMode == "square-tri" then
- self:applyChannel(
- channel, WAVE_SQUARE, frequency, targetVolume * 0.7,
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- self:applyChannel(
- pairChannel, WAVE_TRIANGLE, frequency, targetVolume * 0.7,
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- elseif partnerMode == "saw-tri" then
- self:applyChannel(
- channel, WAVE_SAW, frequency, targetVolume * 0.65,
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- self:applyChannel(
- pairChannel, WAVE_TRIANGLE, frequency, targetVolume * 0.65,
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- else
- self:applyChannel(
- channel, wave, frequency, targetVolume,
- attack, decay, sustain, release, v.gate, v.retrigger
- )
- if pairChannel then
- self:applyChannel(
- pairChannel, WAVE_SQUARE, 0, 0,
- attack, decay, sustain, release, false
- )
- end
- end
- end
- function AUDIO:render(pal, frameMilliseconds, startCycles, frameCycles)
- local envMode =
- ENV_ACCURATE and startCycles ~= nil and frameCycles ~= nil
- local steps = 1
- local samples = nil
- if envMode then
- steps = math.max(
- 1,
- math.min(ENV_SUBSTEPS, math.floor(frameMilliseconds / 2))
- )
- samples = {}
- local changing = false
- for sidIndex = 1, self.sidCount do
- local chip = self.sids[sidIndex]
- samples[sidIndex] = {}
- for voiceNumber = 1, 3 do
- local s = self:sampleEnvelope(
- chip.envs[voiceNumber], startCycles, frameCycles, steps
- )
- samples[sidIndex][voiceNumber] = s
- local low, high = s[1], s[1]
- for i = 2, steps do
- if s[i] < low then low = s[i] end
- if s[i] > high then high = s[i] end
- end
- if high - low >= 2 then
- changing = true
- end
- end
- end
- if not changing then
- steps = 1
- end
- for i = 1, self.audioChannelCount do
- self.channels[i].touched = false
- end
- end
- for sidIndex = 1, self.sidCount do
- local chip = self.sids[sidIndex]
- local masterVolume = chip:masterVolume()
- local voice3Off = chip:voice3Off()
- local fstate = chip:filterState()
- local vs = { chip:voice(1), chip:voice(2), chip:voice(3) }
- for voiceNumber = 1, 3 do
- local channel = self:channelFor(sidIndex, voiceNumber)
- local v = vs[voiceNumber]
- local modV = vs[((voiceNumber - 2) % 3) + 1]
- local logical = (sidIndex - 1) * VOICES_PER_SID + voiceNumber
- local muted =
- (voiceNumber == 3 and voice3Off and
- bit.band(fstate.route, 0x04) == 0) or
- self.muteMask[logical] == true
- self.curSamples = samples and samples[sidIndex][voiceNumber]
- self:updateVoice(
- channel,
- voiceNumber,
- v,
- pal,
- masterVolume,
- muted,
- fstate,
- modV,
- sidIndex
- )
- end
- end
- self.curSamples = nil
- if envMode then
- local slice = frameMilliseconds / steps
- self:queueDelay(slice)
- for s = 2, steps do
- for i = 1, self.audioChannelCount do
- local ch = self.channels[i]
- if ch.touched and ch.opened and ch.envSamples then
- self:setChannelVolume(
- ch, i, ch.base * ch.envSamples[s] / 255
- )
- end
- end
- self:queueDelay(slice)
- end
- else
- sound.delay(frameMilliseconds)
- end
- self.pendingFrames = self.pendingFrames + 1
- if self.pendingFrames >= self.batchFrames then
- sound.process()
- self.pendingFrames = 0
- end
- end
- function AUDIO:resetChannels()
- local anyOpen = false
- for i = 1, self.audioChannelCount do
- local ch = self.channels[i]
- if ch.opened then
- sound.close(i)
- ch.opened = false
- anyOpen = true
- end
- end
- if anyOpen or self.pendingFrames > 0 then
- sound.process()
- end
- self.pendingFrames = 0
- self.delayCarry = 0
- for i = 1, self.audioChannelCount do
- local ch = self.channels[i]
- ch.gate = false
- ch.waveform = nil
- ch.frequency = nil
- ch.pulse = nil
- ch.volume = nil
- ch.attack = nil
- ch.decay = nil
- ch.sustain = nil
- ch.release = nil
- ch.adsrSet = false
- ch.base = 0
- ch.envSamples = nil
- ch.touched = false
- ch.audible = false
- end
- end
- function AUDIO:stop()
- local anyOpen = false
- for i = 1, self.audioChannelCount do
- local ch = self.channels[i]
- if ch.opened then
- sound.close(i)
- ch.opened = false
- anyOpen = true
- end
- end
- if anyOpen or self.pendingFrames > 0 then
- sound.process()
- end
- self.pendingFrames = 0
- end
- local Machine = {}
- Machine.__index = Machine
- function Machine.new(tune)
- local self = setmetatable({}, Machine)
- self.memory = {}
- self.tune = tune
- self.sids = { SIDChip.new() }
- if tune.secondSID and tune.secondSID ~= 0 then
- self.sids[2] = SIDChip.new()
- end
- if tune.thirdSID and tune.thirdSID ~= 0 then
- self.sids[3] = SIDChip.new()
- end
- self.sidBases = {
- SID_BASE,
- tune.secondSID or 0,
- tune.thirdSID or 0
- }
- self.sidCount = #self.sids
- self.cpu = CPU.new(self.memory, self.sids, self.sidBases)
- self.audio = AUDIO.new(self.sids, self.sidCount)
- self.audio.sidModels = {
- tune.sidModel1,
- tune.sidModel2,
- tune.sidModel3
- }
- self.song = tune.startSong
- if tune.clock == "NTSC" then
- self.pal = false
- else
- self.pal = true
- end
- self.frameRate = self.pal and PAL_HZ or NTSC_HZ
- self.irqMode = tune.playAddress == 0
- return self
- end
- function Machine:setFrameRate(rate)
- self.frameRate = rate
- end
- function Machine:ciaRate()
- local latch =
- (self.memory[0xDC04] or 0) +
- (self.memory[0xDC05] or 0) * 256
- if latch < 1000 then
- return nil
- end
- local clock = self.pal and PAL_CLOCK or NTSC_CLOCK
- local rate = clock / (latch + 1)
- if rate < 20 or rate > 200 then
- return nil
- end
- return rate
- end
- function Machine:setClock(pal)
- self.pal = pal
- self.frameRate = pal and PAL_HZ or NTSC_HZ
- self.memory[0x02A6] = pal and 0x01 or 0x00
- end
- function Machine:load()
- local address = self.tune.loadAddress
- local data = self.tune.data
- local memory = self.memory
- for i = 1, #data do
- memory[address + i - 1] = string.byte(data, i)
- end
- memory[0x02A6] = self.pal and 0x01 or 0x00
- memory[0xFFFA] = 0
- memory[0xFFFB] = 0
- memory[0xFFFC] = lo(self.tune.effectiveInitAddress)
- memory[0xFFFD] = hi(self.tune.effectiveInitAddress)
- end
- function Machine:init(song)
- song = clamp(song or self.song, 1, self.tune.songs)
- self.song = song
- local ok, reason =
- self.cpu:call(self.tune.effectiveInitAddress, song - 1, 0, 0)
- if not ok then
- print("Warning: init CPU: " .. tostring(reason))
- end
- self.audio:resetChannels()
- if self.irqMode then
- local irqLo = self.memory[0x0314]
- local irqHi = self.memory[0x0315]
- local irqVector = (irqLo or 0) + bit.lshift(irqHi or 0, 8)
- if irqVector == 0 then
- print(
- "Warning: play address is $0000, " ..
- "but the init routine did not install a non-zero IRQ vector at $0314/$0315."
- )
- else
- print(string.format("IRQ playback vector: $%04X", irqVector))
- end
- end
- end
- function Machine:playFrame(frameMilliseconds)
- local ok
- local reason
- local startCycles = self.cpu.cycles
- if self.tune.playAddress == 0 then
- ok, reason = self.cpu:interrupt(0x0314)
- else
- ok, reason = self.cpu:call(self.tune.playAddress, 0, 0, 0)
- end
- if not ok then
- return false, reason
- end
- local frameCycles = math.floor(
- (self.pal and PAL_CLOCK or NTSC_CLOCK) / self.frameRate
- )
- if self.cpu.cycles < startCycles + frameCycles then
- self.cpu.cycles = startCycles + frameCycles
- end
- self.audio:render(self.pal, frameMilliseconds, startCycles, frameCycles)
- return true
- end
- local function printInfo(tune)
- print("")
- print("========================================")
- print("OC-SID! A Sid Player for OpenComputers!")
- print("========================================")
- print("")
- if tune.name ~= "" then
- print("Title : " .. tune.name)
- end
- if tune.author ~= "" then
- print("Author : " .. tune.author)
- end
- if tune.released ~= "" then
- print("Release: " .. tune.released)
- end
- print("----------------------------------------")
- print("Format : " .. tune.magic)
- print("Version: " .. tostring(tune.version))
- print("Songs : " .. tostring(tune.songs))
- print("Start : " .. tostring(tune.startSong))
- print(string.format("Load : $%04X", tune.loadAddress))
- print(string.format("Init : $%04X", tune.effectiveInitAddress))
- if tune.initAddress == 0 then
- print(" (effective load address)")
- end
- print(string.format("Play : $%04X", tune.playAddress))
- print("----------------------------------------")
- print("Clock : " .. tostring(tune.clock))
- print("SID #1 : " .. tostring(tune.sidModel1) .. " @ $D400")
- if tune.secondSID ~= 0 then
- print(
- string.format(
- "SID #2 : %s @ $%04X",
- tostring(tune.sidModel2),
- tune.secondSID
- )
- )
- else
- print("SID #2 : none")
- end
- if tune.thirdSID ~= 0 then
- print(
- string.format(
- "SID #3 : %s @ $%04X",
- tostring(tune.sidModel3),
- tune.thirdSID
- )
- )
- print(" emulated; audio voice 3 is unavailable on 8-channel cards")
- end
- print("----------------------------------------")
- if tune.playAddress == 0 then
- print("Playback: installed IRQ handler")
- else
- print("Playback: direct play routine")
- end
- print("")
- print("Space = pause/resume")
- print("N / P = next / previous song")
- print("+ / - = volume")
- print("D = analog drift on/off")
- print("F = filter approximation on/off")
- print("M = pulse mode (blend/pair/morph/off)")
- print("1-9 = mute / unmute voice")
- print("V = voice / envelope status")
- print("Q = quit")
- print("========================================")
- print("")
- end
- math.randomseed(math.floor(computer.uptime() * 1000) % 2147483647)
- local args = {...}
- if not args[1] then
- print("Usage:")
- print(" sid.lua /path/to/file.sid [song]")
- print("")
- print("Keys: Space pause | N/P next/previous song | +/- volume")
- print(" F filter on/off | M pulse mode | Q quit")
- print(" 1-9 mute voice | V voice status")
- return
- end
- local filename = args[1]
- local requestedSong = tonumber(args[2])
- local tune = SID.loadFile(filename)
- if tune.magic == "RSID" then
- print("")
- print("WARNING: RSID detected.")
- print("RSID tunes require a much more complete C64 environment.")
- print("VIC/CIA I/O, ROM behavior and cycle timing are not fully emulated.")
- print("")
- end
- if tune.thirdSID ~= 0 then
- print("")
- print("WARNING: This tune declares a third SID.")
- print("SID #3 will be CPU-emulated; the standard sound card exposes only 8 audio channels.")
- print(string.format("Third SID address: $%04X", tune.thirdSID))
- print("")
- end
- local machine = nil
- local usingCIA = false
- local frameMilliseconds = 20
- local frameDelay = 0.02
- local paused = false
- local running = true
- local nextFrame = computer.uptime()
- local song = requestedSong or tune.startSong
- if song < 1 or song > tune.songs then
- error("Invalid song number. Valid range: 1-" .. tostring(tune.songs))
- end
- local function startMachine(songNumber)
- if machine then
- machine.audio:stop()
- end
- machine = Machine.new(tune)
- machine:load()
- machine:init(songNumber)
- local songBit = songNumber - 1
- local speedBit = 0
- if songBit < 32 then
- speedBit = bit.band(bit.rshift(tune.speed, songBit), 1)
- end
- usingCIA = speedBit ~= 0
- local ciaRate = nil
- if usingCIA then
- ciaRate = machine:ciaRate()
- machine:setFrameRate(ciaRate or NTSC_HZ)
- else
- machine:setFrameRate(machine.pal and PAL_HZ or NTSC_HZ)
- end
- frameMilliseconds = 1000 / machine.frameRate
- frameDelay = 1 / machine.frameRate
- if usingCIA then
- if ciaRate then
- print(
- string.format(
- "Playing song %d/%d, CIA timer latch -> %.2f Hz.",
- songNumber,
- tune.songs,
- ciaRate
- )
- )
- else
- print(
- string.format(
- "Playing song %d/%d, CIA default timing at %d Hz.",
- songNumber,
- tune.songs,
- machine.frameRate
- )
- )
- end
- print("Note: CIA interrupts are not cycle-emulated.")
- else
- print(
- string.format(
- "Playing song %d/%d at %d Hz.",
- songNumber,
- tune.songs,
- machine.frameRate
- )
- )
- end
- print(string.format("Sound frame: %.3f ms", frameMilliseconds))
- print("Pulse : " .. machine.audio:pwmDescription())
- print(
- "Filter : " ..
- (machine.audio.filterEnabled and "approximated" or "off")
- )
- print("Drift : " .. (ANALOG_DRIFT_ENABLED and "on" or "off"))
- print(
- "Envelope: " ..
- (ENV_ACCURATE and "SID ADSR emulated per voice" or "sound card ADSR")
- )
- if machine.sidCount >= 2 then
- print("Multi-SID audio: SID #1 = channels 1-3, SID #2 = channels 4-6.")
- if machine.sidCount >= 3 then
- print("SID #3 = channels 7-8; voice 3 remains CPU-emulated only on an 8-channel sound card.")
- end
- print(
- string.format(
- "Audio queue batching: %d frames/process.",
- machine.audio.batchFrames
- )
- )
- end
- print("")
- end
- local function changeSong(n)
- if n < 1 then
- n = tune.songs
- end
- if n > tune.songs then
- n = 1
- end
- song = n
- print("")
- startMachine(song)
- paused = false
- nextFrame = computer.uptime()
- end
- local function printVolume()
- print(
- string.format(
- "Volume: %d%%",
- math.floor(machine.audio.volume * 100 + 0.5)
- )
- )
- end
- local NOTE_NAMES = {
- "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
- }
- local function noteName(hz)
- if not hz or hz < 16 then
- return "---"
- end
- local n = math.floor(12 * math.log(hz / 440) / math.log(2) + 69.5)
- if n < 0 or n > 127 then
- return "???"
- end
- return NOTE_NAMES[n % 12 + 1] .. tostring(math.floor(n / 12) - 1)
- end
- local function printVoiceStatus()
- print("")
- for sidIndex = 1, machine.sidCount do
- local chip = machine.sids[sidIndex]
- local reg = chip.reg
- local f = chip:filterState()
- local mode = ""
- if f.lp then mode = mode .. "LP " end
- if f.bp then mode = mode .. "BP " end
- if f.hp then mode = mode .. "HP " end
- if mode == "" then
- mode = "none"
- end
- print(
- string.format(
- "SID %d: vol=%d filter=%s cutoff=%d res=%d route=%d",
- sidIndex,
- chip:masterVolume(),
- mode,
- f.cutoff,
- f.resonance,
- f.route
- )
- )
- for v = 1, 3 do
- local base = (v - 1) * 7
- local value = reg[base] + reg[base + 1] * 256
- local control = reg[base + 4]
- local pw = reg[base + 2] + (reg[base + 3] % 16) * 256
- local wave = ""
- if bit.band(control, 0x10) ~= 0 then wave = wave .. "T" end
- if bit.band(control, 0x20) ~= 0 then wave = wave .. "S" end
- if bit.band(control, 0x40) ~= 0 then wave = wave .. "P" end
- if bit.band(control, 0x80) ~= 0 then wave = wave .. "N" end
- if wave == "" then
- wave = "-"
- end
- local extra = ""
- if bit.band(control, 0x02) ~= 0 then extra = extra .. " sync" end
- if bit.band(control, 0x04) ~= 0 then extra = extra .. " ring" end
- if bit.band(control, 0x08) ~= 0 then extra = extra .. " test" end
- if chip.noiseLocked[v] then
- extra = extra .. " noise-locked"
- end
- local env = chip.envs[v]
- env:advanceTo(chip:now())
- local logical = (sidIndex - 1) * VOICES_PER_SID + v
- print(
- string.format(
- " V%d %-4s %-4s pw=%4d env=%3d%% %s%s%s",
- logical,
- noteName(machine.audio:frequencyFromSID(value, machine.pal)),
- wave,
- pw,
- math.floor(env.level * 100 / 255 + 0.5),
- env.gate and "gate " or " ",
- machine.audio.muteMask[logical] and "[muted]" or "",
- extra
- )
- )
- end
- end
- print("")
- end
- local NEXT_PWM_MODE = {
- blend = "pair",
- pair = "morph",
- morph = "off",
- off = "blend"
- }
- local function handleKey(character)
- if character == 113 or character == 81 then
- running = false
- elseif character == 32 then
- if paused then
- paused = false
- nextFrame = computer.uptime()
- print("Resumed.")
- else
- paused = true
- machine.audio:stop()
- print("Paused.")
- end
- elseif character == 43 or character == 61 then
- machine.audio:setVolume(machine.audio.volume + 0.05)
- printVolume()
- elseif character == 45 then
- machine.audio:setVolume(machine.audio.volume - 0.05)
- printVolume()
- elseif character == 110 or character == 78 then
- changeSong(song + 1)
- elseif character == 112 or character == 80 then
- changeSong(song - 1)
- elseif character == 100 or character == 68 then
- machine.audio:setDrift(not ANALOG_DRIFT_ENABLED)
- print("Drift : " .. (ANALOG_DRIFT_ENABLED and "on" or "off"))
- elseif character == 102 or character == 70 then
- FILTER_ENABLED = not machine.audio.filterEnabled
- machine.audio:setFilter(FILTER_ENABLED)
- print("Filter : " .. (FILTER_ENABLED and "approximated" or "off"))
- elseif character == 109 or character == 77 then
- PWM_MODE = NEXT_PWM_MODE[PWM_MODE] or "blend"
- machine.audio:setPWMMode(PWM_MODE)
- print("Pulse : " .. machine.audio:pwmDescription())
- elseif character >= 49 and character <= 57 then
- local n = character - 48
- if n <= machine.sidCount * VOICES_PER_SID then
- local muted = machine.audio:toggleMute(n)
- print("Voice " .. n .. ": " .. (muted and "muted" or "on"))
- end
- elseif character == 118 or character == 86 then
- printVoiceStatus()
- end
- end
- printInfo(tune)
- startMachine(song)
- nextFrame = computer.uptime()
- while running do
- if paused then
- local e1, e2, character = event.pull(0.05)
- if e1 == "key_down" then
- handleKey(character)
- end
- else
- local wait = nextFrame - computer.uptime()
- if wait > 0 then
- local e1, e2, character = event.pull(wait)
- if e1 == "key_down" then
- handleKey(character)
- end
- else
- local ok, reason = machine:playFrame(frameMilliseconds)
- if not ok then
- print("")
- print("Playback stopped: " .. tostring(reason))
- running = false
- else
- if CIA_TRACKING and usingCIA then
- local rate = machine:ciaRate()
- if rate and math.abs(rate - machine.frameRate) > 0.5 then
- machine:setFrameRate(rate)
- frameMilliseconds = 1000 / rate
- frameDelay = 1 / rate
- end
- end
- nextFrame = nextFrame + frameDelay
- local current = computer.uptime()
- if current > nextFrame + frameDelay * 8 then
- nextFrame = current + frameDelay
- end
- end
- end
- end
- end
- machine.audio:stop()
- print("")
- print("SID playback stopped.")
Advertisement