Skip to content

Instantly share code, notes, and snippets.

@MCJack123
Created March 17, 2023 07:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MCJack123/4e746df3c81232410e1eacbbca78b1d6 to your computer and use it in GitHub Desktop.
Save MCJack123/4e746df3c81232410e1eacbbca78b1d6 to your computer and use it in GitHub Desktop.
Atari ST SAP player for CraftOS-PC sound plugin (requires https://gist.github.com/MCJack123/cf500b62fdbcb135d2db6cf97a771c6e)
local M6502 = require "M6502"
local nsongs = 1
local defaultsong = 0
local stereo = false
local ntsc = false
local type = 0 -- 0 = B, 1 = C, 2 = D, 3 = S, 4 = R
local fastplay = 312
local initaddr = 0x0000
local musicaddr = 0x0000
local playeraddr = 0x0000
local duration = 0
local stwave = {}
do
--local m = 0
local sz = 256
for i = 1, sz/4 do stwave[i] = -16*((i-1)/sz - 0.25)^2 end
for i = sz/4+1, sz do stwave[i] = 0.05/((i-1)/sz - 0.2028) - 0.0625 end
--for i = 1, #stwave do stwave[i] = math.min(math.max(stwave[i], -1), 1) end
end
term.clear()
term.setCursorPos(1, 1)
local win = window.create(term.current(), 41, 1, 10, 10)
win.clear()
local status = window.create(term.current(), 1, 1, 40, 19)
local oldterm = term.redirect(status)
pcall(function(...)
local mem = setmetatable({}, {__index = function() return 0 end})
local file = assert(fs.open(shell.resolve(...), "rb"))
if file.read(5) ~= "SAP\r\n" then file.close() error("Not a SAP file") end
while true do
local s = file.read(2)
if s == "\xFF\xFF" then break end
repeat s = s .. file.read(1)
until s:sub(-2, -1) == "\r\n"
local cmd, par = s:match "^(%u+) ?(.*)\r\n"
print(cmd, par)
if cmd == "SONG" then nsongs = tonumber(par)
elseif cmd == "DEFSONG" then defaultsong = tonumber(par)
elseif cmd == "STEREO" then stereo = true
elseif cmd == "NTSC" then ntsc, fastplay = true, 262
elseif cmd == "TYPE" then
if par == "B" then type = 0
elseif par == "C" then type = 1
elseif par == "D" then type = 2
elseif par == "S" then type = 3
elseif par == "R" then type = 4
else file.close() error("Unknown TYPE") end
elseif cmd == "FASTPLAY" then fastplay = tonumber(par)
elseif cmd == "INIT" then initaddr = tonumber(par, 16)
elseif cmd == "MUSIC" then musicaddr = tonumber(par, 16)
elseif cmd == "PLAYER" then playeraddr = tonumber(par, 16)
elseif cmd == "NAME" then --print("Title:", par:sub(2, -2))
elseif cmd == "AUTHOR" then --print("Author:", par:sub(2, -2))
elseif cmd == "DATE" then --print("Date:", par:sub(2, -2))
end
end
while true do
local s = file.read(2)
if not s then break end
local addr = ("<H"):unpack(s)
if addr == 0xFFFF then addr = ("<H"):unpack(file.read(2)) end
local e = ("<H"):unpack(file.read(2))
for a = addr, e do mem[a] = file.read() end
end
file.close()
local cpu = M6502.new()
cpu:power(true)
local cpufreq = ntsc and 1789772.5 or 1773447
local pokey = {
{lowfreq = false, hp24 = false, hp13 = false, div43 = false, div21 = false, c3hifreq = false, c1hifreq = false},
{lowfreq = false, hp24 = false, hp13 = false, div43 = false, div21 = false, c3hifreq = false, c1hifreq = false}
}
for i = 1, stereo and 8 or 4 do
sound.setVolume(i, 0)
sound.setFrequency(i, 0)
sound.setWaveType(i, "square", 0.5)
-- sound.setPan(i, stereo and (i > 4 and -1 or 1) or 0)
if sound.version then sound.setInterpolation(i, "linear") end
end
local start = os.epoch "utc"
function cpu:read(addr)
if bit32.band(addr, 0xFF0F) == 0xD40B then print("v") return math.floor((((os.epoch "utc" - start) / (ntsc and 16.666666 or 20)) % 1) * (ntsc and 130 or 155)) end
if bit32.band(addr, 0xFF00) == 0xD200 then return mem[bit32.band(addr, stereo and 0xFF1F or 0xFF0F)] end
if bit32.band(addr, 0xF800) == 0xD000 then return mem[bit32.band(addr, 0xFF0F)] end
return mem[addr]
end
function cpu:write(addr, val)
if bit32.band(addr, 0xFF00) == 0xD200 then
local reg = bit32.band(addr, 0x000F)
local chipsel = (stereo and bit32.btest(addr, 0x0010)) and 2 or 1
mem[0xD200 + reg + ((chipsel-1) * 0x10)] = val -- technically not supposed to happen, but for safety in the emulator
--print(reg, val)
if reg == 8 then
win.setCursorPos(1, 1)
win.clearLine()
win.write(("AUDCTL %02X"):format(val))
pokey[chipsel].lowfreq = bit32.btest(val, 0x01)
pokey[chipsel].hp24 = bit32.btest(val, 0x02) if pokey[chipsel].hp24 then print("Unimplemented high-pass") end
pokey[chipsel].hp13 = bit32.btest(val, 0x04) if pokey[chipsel].hp13 then print("Unimplemented high-pass") end
pokey[chipsel].div43 = bit32.btest(val, 0x08)
pokey[chipsel].div21 = bit32.btest(val, 0x10)
pokey[chipsel].c3hifreq = bit32.btest(val, 0x20)
pokey[chipsel].c1hifreq = bit32.btest(val, 0x40)
elseif reg == 0xE then
print("IRQ!")
elseif reg < 8 then
local channel = bit32.rshift(reg, 1) + ((stereo and bit32.btest(addr, 0x0010)) and 4 or 0) + 1
if bit32.btest(addr, 1) then
-- control
win.setCursorPos(1, channel + 1)
--win.clearLine()
win.write(("%d %02X"):format(channel, val))
local vol = bit32.band(val, 0x0F) / 15 / 2
--print(channel, val)
--sound.setVolume(channel + 8, bit32.band(val, 0x0F) / 150)
local mode = bit32.band(val, 0xE0)
local wt, arg = "none"
if bit32.btest(val, 0x10) then wt, arg = "square", 1
elseif mode == 0xA0 or mode == 0xE0 then wt, arg = "square", 0.5
elseif mode == 0x20 then wt, arg = "triangle"
elseif mode == 0xC0 then wt, arg, vol = sound.version and "custom" or "rsawtooth", stwave, vol * 2
elseif mode == 0x80 then wt = "pnoise" end
-- TODO: maybe implement other types (weird square-ish waves)
if sound.getWaveType(channel) ~= wt then sound.setWaveType(channel, wt, arg) end
--if channel == 1 then
sound.setVolume(channel, vol)
--end
else
-- frequency
local basefreq = (((channel % 4 == 1 and pokey[chipsel].c1hifreq) or (channel % 4 == 3 and pokey[chipsel].c3hifreq)) and cpufreq) or (pokey[chipsel].lowfreq and 15700 or 63921)
if (pokey[chipsel].div43 and bit32.rshift(reg, 1) == 2) or (pokey[chipsel].div21 and bit32.rshift(reg, 1) == 0) then basefreq = sound.getFrequency(channel + 1) sound.setVolume(channel + 1, 0) end
local freq
if basefreq == cpufreq then freq = basefreq / (2 * (val + 4)) -- TODO: hifreq
else freq = basefreq / (2 * (val + 1)) end
--print(channel, freq)
if freq >= 24000 then freq = 0 end
win.setCursorPos(6, channel + 1)
win.write(("% 5d"):format(freq))
if sound.getWaveType(channel) == "custom" then freq = freq / 2^(35/12) end
if sound.getFrequency(channel) ~= freq then sound.setFrequency(channel, freq) end
--sound.setFrequency(channel + 8, freq)
end
end
elseif addr == 0xD40A then print("punt!")
else
mem[addr] = val
end
end
local song = tonumber(select(2, ...) or nil) or defaultsong
if type == 0 then
print(("Initializing at %04X"):format(initaddr))
cpu.state.a = song
cpu:call(initaddr)
print(("Playing at %04X"):format(playeraddr))
local n = 0
while true do
local start = os.epoch "utc"
cpu.state.pc = 0
cpu:call(playeraddr, 114 * 20 * fastplay)
if n % 100 == 0 then sleep(fastplay / (ntsc and 60*262 or 50*312))
else while (os.epoch "utc" - start) / 1000 < fastplay / (ntsc and 60*262 or 50*312) do end end
n = n + 1
end
elseif type == 1 then
print(("Initializing at %04X"):format(playeraddr + 3))
cpu.state.a = 0x70
cpu.state.x = bit32.rshift(musicaddr, 8)
cpu.state.y = bit32.band(musicaddr, 0xFF)
cpu:call(playeraddr + 3)
cpu.state.a = 0
cpu.state.x = song
cpu:call(playeraddr + 3)
print(("Playing at %04X"):format(playeraddr + 6))
local n = 0
while true do
local start = os.epoch "utc"
cpu.state.pc = 0
cpu:call(playeraddr + 6, 114 * 20 * fastplay)
if n % 100 == 0 then sleep(fastplay / (ntsc and 60*262 or 50*312))
else while (os.epoch "utc" - start) / 1000 < fastplay / (ntsc and 60*262 or 50*312) do end end
n = n + 1
end
else error("Unimplemented TYPE " .. type) end
end, ...)
term.redirect(oldterm)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment