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