pulsar/engine.lua
2024-03-16 14:23:02 -07:00

191 lines
4.4 KiB
Lua

function klass()
local k={}
k.__index=k
function k:new(...)
local n={}
setmetatable(n,k)
n:init(...)
return n
end
return k
end
song=klass()
function song:init()
self.frames={}
self.ix_to_frame={}
self.next_frame_start=0
end
function song:add(len)
for i=0,len-1 do
self.ix_to_frame[self.next_frame_start+i]={#self.frames+1,i}
end
add(self.frames,{
pattern:new({len=len}),
pattern:new({len=len}),
pattern:new({len=len}),
pattern:new({len=len}),
})
self.next_frame_start+=len
end
function song:pattern(channel,offset)
offset = offset or -1
channel &= 0xffff
offset &= 0xffff
assert(channel>=0 and channel<4, "channel must be [0,4)")
local n_frames_long=#self.frames
if offset<0 then
assert(offset>=-n_frames_long, "offset must not exceed -"..n_frames_long)
offset+=n_frames_long+1
else
assert(offset>=0 and offset<n_frames_long)
offset+=1
end
return self.frames[offset][channel+1]
end
function song:plot(channel,offset,instant)
assert(channel>=0 and channel<4, "channel must be [0,4)")
local tup=self.ix_to_frame[offset]
assert(tup, "invalid offset for current length: "..offset)
local f,offset=unpack(tup)
self.frames[f][channel+1]:plot(offset, instant)
end
function song:build(
free_patterns,
frame_a,
frame_z
)
local n_frames_long = #self.frames
if (not frame_z) frame_z = frame_a + n_frames_long
assert(frame_z-frame_a == n_frames_long, "wrong number of frames (must be "..frame_a.." to "..frame_a+n_frames_long..")")
-- dump patterns and frames
mapped_patterns={}
function map_to_real_pattern(pat)
if (pat:silent()) return 0 | (1<<6)
local key = pat:key()
mapped_patterns[key] = mapped_patterns[key] or {}
for other in all(mapped_patterns[key]) do
if (pat:eq(other)) return other.map_ix
end
assert(#free_patterns>0, "out of free patterns")
pat:map_to(deli(free_patterns,1))
add(mapped_patterns[key],pat)
return pat.map_ix
end
local fmaddr=0x3100+(frame_a)*4
for frame=1,n_frames_long do
for i=1,4 do
poke(fmaddr+i-1,map_to_real_pattern(self.frames[frame][i]))
end
fmaddr+=1
end
end
pattern=klass()
function pattern:init(o)
speed = o.speed or 15
len = o.len or 32
noiz = o.noiz or 0
buzz = o.buzz or 0
detune = o.detune or 0
reverb = o.reverb or 0
dampen = o.dampen or 0
editormode = true
assert(speed >= 1 and speed <255, "speed must be [1,255)")
assert(len >= 1 and len < 33, "len must be [1,33)")
assert(noiz >= 0 and noiz < 2, "noiz must be [0,2)")
assert(buzz >= 0 and buzz < 2, "buzz must be [0,2)")
assert(detune >= 0 and detune < 3, "detune must be [0,3)")
assert(reverb >= 0 and reverb < 3, "reverb must be [0,3)")
assert(dampen >= 0 and dampen < 3, "dampen must be [0,3)")
self.instants={}
self.len=len
-- https://pico-8.fandom.com/wiki/Memory#Music
self.speed=speed
self.pattern_flags=(
(
tonum(editormode) |
noiz<<1 |
buzz<<2
) +
detune*8 +
reverb*24 +
dampen*72
)
for i=0,self.len-1 do
self.instants[i]=0
end
end
function pattern:plot(ix, iat)
assert(ix>=0 and ix<self.len, "index invalid")
self.instants[ix]=encode_instant(iat)
end
function pattern:silent()
for i=0,self.len-1 do
if (self.instants[i]&0x0e00!=0) return
end
return true
end
function pattern:key()
local key=0
for i=0,self.len-1 do
key ^= self.instants[i]<<(i%16)
end
return key
end
function pattern:eq(other)
if (self.len!=other.len) return
for i=0,self.len-1 do
if (self.instants[i]!=other.instants[i]) return
end
return true
end
function pattern:map_to(ix)
self.map_ix=ix
local at=0x3200+ix*68
for i=0,31 do
poke2(at+i*2,self.instants[i] or 0)
end
poke(at+64,self.pattern_flags)
poke(at+65,self.speed)
-- start, end
poke(at+66,self.len)
poke(at+67,0)
end
function encode_instant(o)
custom = o.c or false
effect = o.e or 0
volume = o.v or 0
waveform = o.w or 0
pitch = o.p or 0
assert(custom == false or custom == true, "custom must be true or false")
assert(effect >= 0 and effect < 8, "effect must be [0,8)")
assert(volume >= 0 and volume < 8, "volume must be [0,8)")
assert(waveform >= 0 and waveform < 8, "waveform must be [0,8)")
assert(pitch >= 0 and pitch < 64, "pitch must be [0,64)")
custom = custom
effect = effect & 0xffff
volume = volume & 0xffff
waveform = waveform & 0xffff
pitch = pitch & 0xffff
-- not a method: handle the nil instant
-- https://pico-8.fandom.com/wiki/Memory#Music
return (
(tonum(custom) << 15) |
(effect << 12) |
(volume << 9) |
(waveform << 6) |
(pitch)
)
end