Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local speaker = peripheral.find("speaker")
- if not speaker then error("No speaker attached!") end
- local AMIGA_CLOCK = 3579545
- local INTERNAL_MIX_RATE = 44100
- local SPEAKER_RATE = 48000
- local CHANNELS = 4
- local TARGET_CHUNK = 16384
- local proPeriods = {
- 856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453,
- 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226,
- 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113
- }
- local samples, songOrder, patterns = {}, {}, {}
- local songLength, restartPos = 0, 0
- local curOrder, curRow = 1, 1
- local speed, bpm = 6, 125
- local jumpToRow, jumpToOrder = -1, -1
- local rawBuffer = {}
- local outputBuffer = {}
- local tracks = {}
- local math_floor = math.floor
- local math_min = math.min
- local math_max = math.max
- local math_abs = math.abs
- local math_sin = math.sin
- local math_sqrt = math.sqrt
- local bit32_rshift = bit32.rshift
- local bit32_band = bit32.band
- local bit32_lshift = bit32.lshift
- local bit32_bor = bit32.bor
- local function updateStep(t)
- local s = samples[t.sampleIdx]
- if not s or t.period <= 0 then
- t.step = 0
- return
- end
- local prd = math_max(113, math_min(856, t.period))
- local frequency = AMIGA_CLOCK / prd
- if s.fine ~= 0 then
- frequency = frequency * (2 ^ (s.fine / 96))
- end
- t.step = frequency / INTERNAL_MIX_RATE
- t.volumeScale = t.volume / 64
- end
- local function getArpeggioPeriod(basePeriod, noteOffset)
- local bestIdx, minDiff = 1, 1e9
- for i = 1, 36 do
- local diff = math_abs(proPeriods[i] - basePeriod)
- if diff < minDiff then
- minDiff = diff
- bestIdx = i
- end
- end
- local targetIdx = math_max(1, math_min(36, bestIdx + noteOffset))
- return proPeriods[targetIdx]
- end
- local function processEffects(tick)
- for i = 1, CHANNELS do
- local t = tracks[i]
- local cmd, data = t.effect, t.eData
- if tick == 0 then
- if cmd == 0xC then
- t.volume = math_min(data, 64)
- t.volumeScale = t.volume / 64
- elseif cmd == 0xF then
- if data > 0 and data < 32 then
- speed = data
- elseif data >= 32 then
- bpm = math_max(32, math_min(255, data))
- end
- elseif cmd == 0xD then
- local newRow = math_floor(data / 16) * 10 + (data % 16)
- if newRow < 64 then
- jumpToRow = newRow + 1
- jumpToOrder = curOrder + 1
- end
- elseif cmd == 0xB then
- if data < 128 then
- jumpToOrder = data + 1
- jumpToRow = 1
- end
- elseif cmd == 0x3 or cmd == 0x5 then
- if data > 0 then t.portamentoSpeed = data end
- elseif cmd == 0x4 or cmd == 0x6 then
- if data > 0 then
- local s, d = bit32_rshift(data, 4), bit32_band(data, 0x0F)
- if s > 0 then t.vibratoSpeed = s end
- if d > 0 then t.vibratoDepth = d end
- end
- elseif cmd == 0xE then
- local subCmd, subData = bit32_rshift(data, 4), bit32_band(data, 0x0F)
- if subCmd == 0x1 then
- t.period = math_max(113, t.period - subData)
- updateStep(t)
- elseif subCmd == 0x2 then
- t.period = math_min(856, t.period + subData)
- updateStep(t)
- elseif subCmd == 0xA then
- t.volume = math_min(64, t.volume + subData)
- t.volumeScale = t.volume / 64
- elseif subCmd == 0xB then
- t.volume = math_max(0, t.volume - subData)
- t.volumeScale = t.volume / 64
- elseif subCmd == 0xC then
- t.active = false
- end
- end
- else
- if cmd == 0x0 and data > 0 then
- local phase = tick % 3
- if phase == 1 then
- t.period = getArpeggioPeriod(t.targetPeriod, bit32_rshift(data, 4))
- elseif phase == 2 then
- t.period = getArpeggioPeriod(t.targetPeriod, bit32_band(data, 0x0F))
- else
- t.period = t.targetPeriod
- end
- updateStep(t)
- elseif cmd == 0x1 then
- t.period = math_max(113, t.period - data)
- updateStep(t)
- elseif cmd == 0x2 then
- t.period = math_min(856, t.period + data)
- updateStep(t)
- elseif cmd == 0x3 or cmd == 0x5 then
- if t.period < t.targetPeriod then
- t.period = math_min(t.targetPeriod, t.period + t.portamentoSpeed)
- elseif t.period > t.targetPeriod then
- t.period = math_max(t.targetPeriod, t.period - t.portamentoSpeed)
- end
- updateStep(t)
- if cmd == 0x5 and data > 0 then
- local u, d = bit32_rshift(data, 4), bit32_band(data, 0x0F)
- t.volume = math_max(0, math_min(64, t.volume + u - d))
- t.volumeScale = t.volume / 64
- end
- elseif cmd == 0x4 or cmd == 0x6 then
- local vib = math_sin(t.vibratoPos * 0.09817477) * t.vibratoDepth * 2
- t.period = math_max(113, math_min(856, t.targetPeriod + vib))
- t.vibratoPos = (t.vibratoPos + t.vibratoSpeed) % 64
- updateStep(t)
- if cmd == 0x6 and data > 0 then
- local u, d = bit32_rshift(data, 4), bit32_band(data, 0x0F)
- t.volume = math_max(0, math_min(64, t.volume + u - d))
- t.volumeScale = t.volume / 64
- end
- elseif cmd == 0xA then
- local u, d = bit32_rshift(data, 4), bit32_band(data, 0x0F)
- t.volume = math_max(0, math_min(64, t.volume + u - d))
- t.volumeScale = t.volume / 64
- elseif cmd == 0xE then
- local subCmd, subData = bit32_rshift(data, 4), bit32_band(data, 0x0F)
- if subCmd == 0x9 and subData > 0 and tick % subData == 0 then
- t.cursor = 0
- end
- end
- end
- end
- end
- local mixCache = {}
- for i = 1, 32 do
- mixCache[i] = { track = {}, data = {}, len = 0, loopStart = 0, loopLen = 0, hasLoop = false, loopEnd = 0 }
- end
- local function mixSamples(count)
- local normalizationFactor = math_sqrt(CHANNELS) * 1.1
- local activeCount = 0
- for c = 1, CHANNELS do
- local t = tracks[c]
- if t and t.active then
- local s = samples[t.sampleIdx]
- if s and s.len > 0 then
- activeCount = activeCount + 1
- local mc = mixCache[activeCount]
- mc.track = t
- mc.data = s.data
- mc.len = s.len
- mc.loopStart = s.loopStart
- mc.loopLen = s.loopLen
- mc.hasLoop = s.loopLen > 2
- mc.loopEnd = s.loopStart + s.loopLen
- end
- end
- end
- for i = 1, count do
- local out = 0
- for c = 1, activeCount do
- local at = mixCache[c]
- local t = at.track
- if t.active then
- if at.hasLoop then
- if t.cursor >= at.loopEnd then
- t.cursor = at.loopStart + ((t.cursor - at.loopEnd) % at.loopLen)
- end
- elseif t.cursor >= at.len then
- t.active = false
- end
- if t.active then
- local idx = math_floor(t.cursor) + 1
- local val = at.data[idx]
- if val then
- out = out + (val * t.volumeScale)
- t.cursor = t.cursor + t.step
- else
- t.active = false
- end
- end
- end
- end
- rawBuffer[i] = out / normalizationFactor
- end
- local ratio = SPEAKER_RATE / INTERNAL_MIX_RATE
- local outSize = math_floor(count * ratio + 0.5)
- local invRatio = 1 / ratio
- local src_acc = 1
- for i = 1, outSize do
- local idx = math_floor(src_acc)
- local s1 = rawBuffer[idx] or 0
- local s2 = rawBuffer[idx + 1] or s1
- outputBuffer[i] = math_max(-128, math_min(127, math_floor(s1 + (s2 - s1) * (src_acc - idx))))
- src_acc = src_acc + invRatio
- end
- return outputBuffer, outSize
- end
- local chunkBuffer = {}
- local function flushAudio(buf)
- local bufSize = #buf
- if bufSize == 0 then return end
- local chunkSize = 4096
- for i = 1, bufSize, chunkSize do
- local chunkIdx = 1
- local endRange = math_min(i + chunkSize - 1, bufSize)
- for j = i, endRange do
- chunkBuffer[chunkIdx] = buf[j]
- chunkIdx = chunkIdx + 1
- end
- local oldLen = #chunkBuffer
- if oldLen >= chunkIdx then
- for j = chunkIdx, oldLen do chunkBuffer[j] = nil end
- end
- local success = false
- local attempts = 0
- while not success and attempts < 10 do
- success = speaker.playAudio(chunkBuffer)
- if not success then
- os.pullEvent("speaker_audio_empty")
- attempts = attempts + 1
- end
- end
- end
- end
- local function loadMOD(filename)
- local f = fs.open(filename, "rb")
- if not f then error("File not found: " .. filename) end
- local success, err = pcall(function()
- f.read(20)
- for i = 1, 31 do
- f.read(22)
- local len = (f.read() * 256 + f.read()) * 2
- local fine = f.read()
- if fine > 7 then fine = fine - 16 end
- local vol = f.read()
- local lStart = (f.read() * 256 + f.read()) * 2
- local lLen = (f.read() * 256 + f.read()) * 2
- if len > 0 and vol > 0 then
- samples[i] = {
- len = len,
- vol = math_min(64, vol),
- loopStart = lStart,
- loopLen = lLen,
- fine = fine,
- data = {}
- }
- else
- samples[i] = { len = 0, data = {} }
- end
- end
- songLength = f.read()
- restartPos = f.read()
- local maxPattern = 0
- for i = 1, 128 do
- songOrder[i] = f.read() or 0
- if songOrder[i] > maxPattern then maxPattern = songOrder[i] end
- end
- local m1, m2, m3, m4 = f.read(), f.read(), f.read(), f.read()
- local tag = string.char(m1 or 0, m2 or 0, m3 or 0, m4 or 0)
- CHANNELS = 4
- if tag == "M.K." or tag == "M!K!" or tag == "FLT4" or tag == "4CHN" then
- CHANNELS = 4
- elseif tag == "6CHN" or tag == "6CNM" then
- CHANNELS = 6
- elseif tag == "8CHN" or tag == "8CNM" or tag == "CD81" or tag == "OKTA" then
- CHANNELS = 8
- elseif tag:match("^%d+CH") or tag:match("^%d+CN") then
- local digits = tag:match("^(%d+)CH") or tag:match("^(%d+)CN")
- if digits then
- CHANNELS = tonumber(digits)
- end
- elseif tag == "FLT8" or tag == "FLT6" then
- CHANNELS = 8
- end
- for i = 1, CHANNELS do
- tracks[i] = {
- sampleIdx = 0, cursor = 0, period = 0, step = 0,
- targetPeriod = 0, volume = 64, active = false,
- effect = 0, eData = 0, portamentoSpeed = 0,
- vibratoSpeed = 0, vibratoDepth = 0, vibratoPos = 0,
- volumeScale = 1
- }
- end
- for p = 0, maxPattern do
- patterns[p] = {}
- for row = 1, 64 do
- patterns[p][row] = {}
- for chan = 1, CHANNELS do
- local b1, b2, b3, b4 = f.read(), f.read(), f.read(), f.read()
- if b1 and b2 and b3 and b4 then
- local sIdx = bit32_bor(bit32_band(b1, 0xF0), bit32_rshift(b3, 4))
- local prd = bit32_bor(bit32_lshift(bit32_band(b1, 0x0F), 8), b2)
- patterns[p][row][chan] = {
- samp = sIdx,
- period = prd,
- effect = bit32_band(b3, 0x0F),
- eData = b4
- }
- else
- patterns[p][row][chan] = { samp = 0, period = 0, effect = 0, eData = 0 }
- end
- end
- end
- end
- for i = 1, 31 do
- if samples[i] and samples[i].len > 0 then
- local data = samples[i].data
- for j = 1, samples[i].len do
- local b = f.read()
- if b then
- data[j] = (b > 127) and (b - 256) or b
- else
- data[j] = 0
- end
- end
- end
- end
- end)
- f.close()
- if not success then error("Failed to load MOD: " .. tostring(err)) end
- end
- print("NekoMOD: ProTracker for CC by nonogamer9")
- write("Filename: ")
- local filename = read()
- if not fs.exists(filename) then error("File not found: " .. filename) end
- loadMOD(filename)
- print("Detected Channels: " .. CHANNELS)
- print("Playing: " .. filename)
- local pending = {}
- local pendingCount = 0
- while curOrder <= songLength do
- local pIdx = songOrder[curOrder]
- if not pIdx or not patterns[pIdx] then
- curOrder = curOrder + 1
- curRow = 1
- goto continue
- end
- local rowData = patterns[pIdx][curRow]
- if not rowData then
- curRow = curRow + 1
- goto continue
- end
- for c = 1, CHANNELS do
- local cell = rowData[c]
- if cell then
- if cell.samp > 0 then
- local s = samples[cell.samp]
- if s and s.len > 0 then
- tracks[c].sampleIdx = cell.samp
- if s.vol > 0 then
- tracks[c].volume = s.vol
- tracks[c].volumeScale = tracks[c].volume / 64
- end
- end
- end
- if cell.period > 0 then
- if cell.effect == 0x3 or cell.effect == 0x5 then
- tracks[c].targetPeriod = cell.period
- else
- tracks[c].period = cell.period
- tracks[c].targetPeriod = cell.period
- tracks[c].cursor = 0
- tracks[c].vibratoPos = 0
- updateStep(tracks[c])
- tracks[c].active = true
- end
- elseif cell.period == 0 and cell.samp > 0 then
- if tracks[c].targetPeriod > 0 then
- tracks[c].cursor = 0
- tracks[c].active = true
- updateStep(tracks[c])
- end
- end
- tracks[c].effect = cell.effect
- tracks[c].eData = cell.eData
- end
- end
- speed = math_max(1, speed)
- bpm = math_max(32, math_min(255, bpm))
- local rowTime = (speed * 2.5) / bpm
- local samplesNeeded = math_floor(INTERNAL_MIX_RATE * rowTime + 0.5)
- if samplesNeeded > 0 then
- local samplesPerTick = samplesNeeded / speed
- for tick = 0, speed - 1 do
- processEffects(tick)
- local tickSamples = math_floor((tick + 1) * samplesPerTick + 0.5) - math_floor(tick * samplesPerTick + 0.5)
- if tickSamples > 0 then
- local audio, n = mixSamples(tickSamples)
- if pendingCount + n <= TARGET_CHUNK * 2 then
- for i = 1, n do
- pending[pendingCount + i] = audio[i]
- end
- pendingCount = pendingCount + n
- end
- if pendingCount >= TARGET_CHUNK then
- flushAudio(pending)
- for i = 1, pendingCount do pending[i] = nil end
- pendingCount = 0
- end
- end
- end
- end
- if jumpToOrder ~= -1 then
- curOrder = jumpToOrder
- curRow = jumpToRow
- jumpToOrder = -1
- jumpToRow = -1
- else
- curRow = curRow + 1
- if curRow > 64 then
- curRow = 1
- curOrder = curOrder + 1
- if curOrder > songLength then
- curOrder = restartPos + 1
- if curOrder < 1 then curOrder = 1 end
- curRow = 1
- end
- end
- end
- ::continue::
- end
- if pendingCount > 0 then
- flushAudio(pending)
- end
- print("Playback finished!")
Advertisement
Add Comment
Please, Sign In to add comment