nonogamer9

NekoMOD: ProTracker for ComputerCraft

Apr 20th, 2026 (edited)
186
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 17.13 KB | Source Code | 0 0
  1. local speaker = peripheral.find("speaker")
  2. if not speaker then error("No speaker attached!") end
  3.  
  4. local AMIGA_CLOCK = 3579545
  5. local INTERNAL_MIX_RATE = 44100
  6. local SPEAKER_RATE = 48000
  7. local CHANNELS = 4
  8. local TARGET_CHUNK = 16384
  9.  
  10. local proPeriods = {
  11.     856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453,
  12.     428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226,
  13.     214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113
  14. }
  15.  
  16. local samples, songOrder, patterns = {}, {}, {}
  17. local songLength, restartPos = 0, 0
  18. local curOrder, curRow = 1, 1
  19. local speed, bpm = 6, 125
  20. local jumpToRow, jumpToOrder = -1, -1
  21.  
  22. local rawBuffer = {}
  23. local outputBuffer = {}
  24. local tracks = {}
  25.  
  26. local math_floor = math.floor
  27. local math_min = math.min
  28. local math_max = math.max
  29. local math_abs = math.abs
  30. local math_sin = math.sin
  31. local math_sqrt = math.sqrt
  32. local bit32_rshift = bit32.rshift
  33. local bit32_band = bit32.band
  34. local bit32_lshift = bit32.lshift
  35. local bit32_bor = bit32.bor
  36.  
  37. local function updateStep(t)
  38.     local s = samples[t.sampleIdx]
  39.     if not s or t.period <= 0 then
  40.         t.step = 0
  41.         return
  42.     end
  43.  
  44.     local prd = math_max(113, math_min(856, t.period))
  45.     local frequency = AMIGA_CLOCK / prd
  46.     if s.fine ~= 0 then
  47.         frequency = frequency * (2 ^ (s.fine / 96))
  48.     end
  49.     t.step = frequency / INTERNAL_MIX_RATE
  50.     t.volumeScale = t.volume / 64
  51. end
  52.  
  53. local function getArpeggioPeriod(basePeriod, noteOffset)
  54.     local bestIdx, minDiff = 1, 1e9
  55.     for i = 1, 36 do
  56.         local diff = math_abs(proPeriods[i] - basePeriod)
  57.         if diff < minDiff then
  58.             minDiff = diff
  59.             bestIdx = i
  60.         end
  61.     end
  62.     local targetIdx = math_max(1, math_min(36, bestIdx + noteOffset))
  63.     return proPeriods[targetIdx]
  64. end
  65.  
  66. local function processEffects(tick)
  67.     for i = 1, CHANNELS do
  68.         local t = tracks[i]
  69.         local cmd, data = t.effect, t.eData
  70.  
  71.         if tick == 0 then
  72.             if cmd == 0xC then
  73.                 t.volume = math_min(data, 64)
  74.                 t.volumeScale = t.volume / 64
  75.             elseif cmd == 0xF then
  76.                 if data > 0 and data < 32 then
  77.                     speed = data
  78.                 elseif data >= 32 then
  79.                     bpm = math_max(32, math_min(255, data))
  80.                 end
  81.             elseif cmd == 0xD then
  82.                 local newRow = math_floor(data / 16) * 10 + (data % 16)
  83.                 if newRow < 64 then
  84.                     jumpToRow = newRow + 1
  85.                     jumpToOrder = curOrder + 1
  86.                 end
  87.             elseif cmd == 0xB then
  88.                 if data < 128 then
  89.                     jumpToOrder = data + 1
  90.                     jumpToRow = 1
  91.                 end
  92.             elseif cmd == 0x3 or cmd == 0x5 then
  93.                 if data > 0 then t.portamentoSpeed = data end
  94.             elseif cmd == 0x4 or cmd == 0x6 then
  95.                 if data > 0 then
  96.                     local s, d = bit32_rshift(data, 4), bit32_band(data, 0x0F)
  97.                     if s > 0 then t.vibratoSpeed = s end
  98.                     if d > 0 then t.vibratoDepth = d end
  99.                 end
  100.             elseif cmd == 0xE then
  101.                 local subCmd, subData = bit32_rshift(data, 4), bit32_band(data, 0x0F)
  102.                 if subCmd == 0x1 then
  103.                     t.period = math_max(113, t.period - subData)
  104.                     updateStep(t)
  105.                 elseif subCmd == 0x2 then
  106.                     t.period = math_min(856, t.period + subData)
  107.                     updateStep(t)
  108.                 elseif subCmd == 0xA then
  109.                     t.volume = math_min(64, t.volume + subData)
  110.                     t.volumeScale = t.volume / 64
  111.                 elseif subCmd == 0xB then
  112.                     t.volume = math_max(0, t.volume - subData)
  113.                     t.volumeScale = t.volume / 64
  114.                 elseif subCmd == 0xC then
  115.                     t.active = false
  116.                 end
  117.             end
  118.         else
  119.             if cmd == 0x0 and data > 0 then
  120.                 local phase = tick % 3
  121.                 if phase == 1 then
  122.                     t.period = getArpeggioPeriod(t.targetPeriod, bit32_rshift(data, 4))
  123.                 elseif phase == 2 then
  124.                     t.period = getArpeggioPeriod(t.targetPeriod, bit32_band(data, 0x0F))
  125.                 else
  126.                     t.period = t.targetPeriod
  127.                 end
  128.                 updateStep(t)
  129.             elseif cmd == 0x1 then
  130.                 t.period = math_max(113, t.period - data)
  131.                 updateStep(t)
  132.             elseif cmd == 0x2 then
  133.                 t.period = math_min(856, t.period + data)
  134.                 updateStep(t)
  135.             elseif cmd == 0x3 or cmd == 0x5 then
  136.                 if t.period < t.targetPeriod then
  137.                     t.period = math_min(t.targetPeriod, t.period + t.portamentoSpeed)
  138.                 elseif t.period > t.targetPeriod then
  139.                     t.period = math_max(t.targetPeriod, t.period - t.portamentoSpeed)
  140.                 end
  141.                 updateStep(t)
  142.                 if cmd == 0x5 and data > 0 then
  143.                     local u, d = bit32_rshift(data, 4), bit32_band(data, 0x0F)
  144.                     t.volume = math_max(0, math_min(64, t.volume + u - d))
  145.                     t.volumeScale = t.volume / 64
  146.                 end
  147.             elseif cmd == 0x4 or cmd == 0x6 then
  148.                 local vib = math_sin(t.vibratoPos * 0.09817477) * t.vibratoDepth * 2
  149.                 t.period = math_max(113, math_min(856, t.targetPeriod + vib))
  150.                 t.vibratoPos = (t.vibratoPos + t.vibratoSpeed) % 64
  151.                 updateStep(t)
  152.                 if cmd == 0x6 and data > 0 then
  153.                     local u, d = bit32_rshift(data, 4), bit32_band(data, 0x0F)
  154.                     t.volume = math_max(0, math_min(64, t.volume + u - d))
  155.                     t.volumeScale = t.volume / 64
  156.                 end
  157.             elseif cmd == 0xA then
  158.                 local u, d = bit32_rshift(data, 4), bit32_band(data, 0x0F)
  159.                 t.volume = math_max(0, math_min(64, t.volume + u - d))
  160.                 t.volumeScale = t.volume / 64
  161.             elseif cmd == 0xE then
  162.                 local subCmd, subData = bit32_rshift(data, 4), bit32_band(data, 0x0F)
  163.                 if subCmd == 0x9 and subData > 0 and tick % subData == 0 then
  164.                     t.cursor = 0
  165.                 end
  166.             end
  167.         end
  168.     end
  169. end
  170.  
  171. local mixCache = {}
  172. for i = 1, 32 do
  173.     mixCache[i] = { track = {}, data = {}, len = 0, loopStart = 0, loopLen = 0, hasLoop = false, loopEnd = 0 }
  174. end
  175.  
  176. local function mixSamples(count)
  177.     local normalizationFactor = math_sqrt(CHANNELS) * 1.1
  178.     local activeCount = 0
  179.  
  180.     for c = 1, CHANNELS do
  181.         local t = tracks[c]
  182.         if t and t.active then
  183.             local s = samples[t.sampleIdx]
  184.             if s and s.len > 0 then
  185.                 activeCount = activeCount + 1
  186.                 local mc = mixCache[activeCount]
  187.                 mc.track = t
  188.                 mc.data = s.data
  189.                 mc.len = s.len
  190.                 mc.loopStart = s.loopStart
  191.                 mc.loopLen = s.loopLen
  192.                 mc.hasLoop = s.loopLen > 2
  193.                 mc.loopEnd = s.loopStart + s.loopLen
  194.             end
  195.         end
  196.     end
  197.  
  198.     for i = 1, count do
  199.         local out = 0
  200.         for c = 1, activeCount do
  201.             local at = mixCache[c]
  202.             local t = at.track
  203.             if t.active then
  204.                 if at.hasLoop then
  205.                     if t.cursor >= at.loopEnd then
  206.                         t.cursor = at.loopStart + ((t.cursor - at.loopEnd) % at.loopLen)
  207.                     end
  208.                 elseif t.cursor >= at.len then
  209.                     t.active = false
  210.                 end
  211.  
  212.                 if t.active then
  213.                     local idx = math_floor(t.cursor) + 1
  214.                     local val = at.data[idx]
  215.                     if val then
  216.                         out = out + (val * t.volumeScale)
  217.                         t.cursor = t.cursor + t.step
  218.                     else
  219.                         t.active = false
  220.                     end
  221.                 end
  222.             end
  223.         end
  224.         rawBuffer[i] = out / normalizationFactor
  225.     end
  226.  
  227.     local ratio = SPEAKER_RATE / INTERNAL_MIX_RATE
  228.     local outSize = math_floor(count * ratio + 0.5)
  229.     local invRatio = 1 / ratio
  230.    
  231.     local src_acc = 1
  232.     for i = 1, outSize do
  233.         local idx = math_floor(src_acc)
  234.         local s1 = rawBuffer[idx] or 0
  235.         local s2 = rawBuffer[idx + 1] or s1
  236.        
  237.         outputBuffer[i] = math_max(-128, math_min(127, math_floor(s1 + (s2 - s1) * (src_acc - idx))))
  238.         src_acc = src_acc + invRatio
  239.     end
  240.    
  241.     return outputBuffer, outSize
  242. end
  243.  
  244. local chunkBuffer = {}
  245.  
  246. local function flushAudio(buf)
  247.     local bufSize = #buf
  248.     if bufSize == 0 then return end
  249.    
  250.     local chunkSize = 4096
  251.     for i = 1, bufSize, chunkSize do
  252.         local chunkIdx = 1
  253.         local endRange = math_min(i + chunkSize - 1, bufSize)
  254.        
  255.         for j = i, endRange do
  256.             chunkBuffer[chunkIdx] = buf[j]
  257.             chunkIdx = chunkIdx + 1
  258.         end
  259.        
  260.         local oldLen = #chunkBuffer
  261.         if oldLen >= chunkIdx then
  262.             for j = chunkIdx, oldLen do chunkBuffer[j] = nil end
  263.         end
  264.        
  265.         local success = false
  266.         local attempts = 0
  267.         while not success and attempts < 10 do
  268.             success = speaker.playAudio(chunkBuffer)
  269.             if not success then
  270.                 os.pullEvent("speaker_audio_empty")
  271.                 attempts = attempts + 1
  272.             end
  273.         end
  274.     end
  275. end
  276.  
  277. local function loadMOD(filename)
  278.     local f = fs.open(filename, "rb")
  279.     if not f then error("File not found: " .. filename) end
  280.  
  281.     local success, err = pcall(function()
  282.         f.read(20)
  283.        
  284.         for i = 1, 31 do
  285.             f.read(22)
  286.             local len = (f.read() * 256 + f.read()) * 2
  287.             local fine = f.read()
  288.             if fine > 7 then fine = fine - 16 end
  289.             local vol = f.read()
  290.             local lStart = (f.read() * 256 + f.read()) * 2
  291.             local lLen = (f.read() * 256 + f.read()) * 2
  292.            
  293.             if len > 0 and vol > 0 then
  294.                 samples[i] = {
  295.                     len = len,
  296.                     vol = math_min(64, vol),
  297.                     loopStart = lStart,
  298.                     loopLen = lLen,
  299.                     fine = fine,
  300.                     data = {}
  301.                 }
  302.             else
  303.                 samples[i] = { len = 0, data = {} }
  304.             end
  305.         end
  306.  
  307.         songLength = f.read()
  308.         restartPos = f.read()
  309.  
  310.         local maxPattern = 0
  311.         for i = 1, 128 do
  312.             songOrder[i] = f.read() or 0
  313.             if songOrder[i] > maxPattern then maxPattern = songOrder[i] end
  314.         end
  315.  
  316.         local m1, m2, m3, m4 = f.read(), f.read(), f.read(), f.read()
  317.         local tag = string.char(m1 or 0, m2 or 0, m3 or 0, m4 or 0)
  318.        
  319.         CHANNELS = 4
  320.         if tag == "M.K." or tag == "M!K!" or tag == "FLT4" or tag == "4CHN" then
  321.             CHANNELS = 4
  322.         elseif tag == "6CHN" or tag == "6CNM" then
  323.             CHANNELS = 6
  324.         elseif tag == "8CHN" or tag == "8CNM" or tag == "CD81" or tag == "OKTA" then
  325.             CHANNELS = 8
  326.         elseif tag:match("^%d+CH") or tag:match("^%d+CN") then
  327.             local digits = tag:match("^(%d+)CH") or tag:match("^(%d+)CN")
  328.             if digits then
  329.                 CHANNELS = tonumber(digits)
  330.             end
  331.         elseif tag == "FLT8" or tag == "FLT6" then
  332.             CHANNELS = 8
  333.         end
  334.  
  335.         for i = 1, CHANNELS do
  336.             tracks[i] = {
  337.                 sampleIdx = 0, cursor = 0, period = 0, step = 0,
  338.                 targetPeriod = 0, volume = 64, active = false,
  339.                 effect = 0, eData = 0, portamentoSpeed = 0,
  340.                 vibratoSpeed = 0, vibratoDepth = 0, vibratoPos = 0,
  341.                 volumeScale = 1
  342.             }
  343.         end
  344.  
  345.         for p = 0, maxPattern do
  346.             patterns[p] = {}
  347.             for row = 1, 64 do
  348.                 patterns[p][row] = {}
  349.                 for chan = 1, CHANNELS do
  350.                     local b1, b2, b3, b4 = f.read(), f.read(), f.read(), f.read()
  351.                     if b1 and b2 and b3 and b4 then
  352.                         local sIdx = bit32_bor(bit32_band(b1, 0xF0), bit32_rshift(b3, 4))
  353.                         local prd = bit32_bor(bit32_lshift(bit32_band(b1, 0x0F), 8), b2)
  354.                         patterns[p][row][chan] = {
  355.                             samp = sIdx,
  356.                             period = prd,
  357.                             effect = bit32_band(b3, 0x0F),
  358.                             eData = b4
  359.                         }
  360.                     else
  361.                         patterns[p][row][chan] = { samp = 0, period = 0, effect = 0, eData = 0 }
  362.                     end
  363.                 end
  364.             end
  365.         end
  366.  
  367.         for i = 1, 31 do
  368.             if samples[i] and samples[i].len > 0 then
  369.                 local data = samples[i].data
  370.                 for j = 1, samples[i].len do
  371.                     local b = f.read()
  372.                     if b then
  373.                         data[j] = (b > 127) and (b - 256) or b
  374.                     else
  375.                         data[j] = 0
  376.                     end
  377.                 end
  378.             end
  379.         end
  380.     end)
  381.  
  382.     f.close()
  383.     if not success then error("Failed to load MOD: " .. tostring(err)) end
  384. end
  385.  
  386. print("NekoMOD: ProTracker for CC by nonogamer9")
  387. write("Filename: ")
  388. local filename = read()
  389. if not fs.exists(filename) then error("File not found: " .. filename) end
  390.  
  391. loadMOD(filename)
  392. print("Detected Channels: " .. CHANNELS)
  393. print("Playing: " .. filename)
  394.  
  395. local pending = {}
  396. local pendingCount = 0
  397.  
  398. while curOrder <= songLength do
  399.     local pIdx = songOrder[curOrder]
  400.     if not pIdx or not patterns[pIdx] then
  401.         curOrder = curOrder + 1
  402.         curRow = 1
  403.         goto continue
  404.     end
  405.  
  406.     local rowData = patterns[pIdx][curRow]
  407.     if not rowData then
  408.         curRow = curRow + 1
  409.         goto continue
  410.     end
  411.  
  412.     for c = 1, CHANNELS do
  413.         local cell = rowData[c]
  414.         if cell then
  415.             if cell.samp > 0 then
  416.                 local s = samples[cell.samp]
  417.                 if s and s.len > 0 then
  418.                     tracks[c].sampleIdx = cell.samp
  419.                     if s.vol > 0 then
  420.                         tracks[c].volume = s.vol
  421.                         tracks[c].volumeScale = tracks[c].volume / 64
  422.                     end
  423.                 end
  424.             end
  425.  
  426.             if cell.period > 0 then
  427.                 if cell.effect == 0x3 or cell.effect == 0x5 then
  428.                     tracks[c].targetPeriod = cell.period
  429.                 else
  430.                     tracks[c].period = cell.period
  431.                     tracks[c].targetPeriod = cell.period
  432.                     tracks[c].cursor = 0
  433.                     tracks[c].vibratoPos = 0
  434.                     updateStep(tracks[c])
  435.                     tracks[c].active = true
  436.                 end
  437.             elseif cell.period == 0 and cell.samp > 0 then
  438.                 if tracks[c].targetPeriod > 0 then
  439.                     tracks[c].cursor = 0
  440.                     tracks[c].active = true
  441.                     updateStep(tracks[c])
  442.                 end
  443.             end
  444.  
  445.             tracks[c].effect = cell.effect
  446.             tracks[c].eData = cell.eData
  447.         end
  448.     end
  449.  
  450.     speed = math_max(1, speed)
  451.     bpm = math_max(32, math_min(255, bpm))
  452.    
  453.     local rowTime = (speed * 2.5) / bpm
  454.     local samplesNeeded = math_floor(INTERNAL_MIX_RATE * rowTime + 0.5)
  455.    
  456.     if samplesNeeded > 0 then
  457.         local samplesPerTick = samplesNeeded / speed
  458.        
  459.         for tick = 0, speed - 1 do
  460.             processEffects(tick)
  461.            
  462.             local tickSamples = math_floor((tick + 1) * samplesPerTick + 0.5) - math_floor(tick * samplesPerTick + 0.5)
  463.             if tickSamples > 0 then
  464.                 local audio, n = mixSamples(tickSamples)
  465.                
  466.                 if pendingCount + n <= TARGET_CHUNK * 2 then
  467.                     for i = 1, n do
  468.                         pending[pendingCount + i] = audio[i]
  469.                     end
  470.                     pendingCount = pendingCount + n
  471.                 end
  472.  
  473.                 if pendingCount >= TARGET_CHUNK then
  474.                     flushAudio(pending)
  475.                     for i = 1, pendingCount do pending[i] = nil end
  476.                     pendingCount = 0
  477.                 end
  478.             end
  479.         end
  480.     end
  481.  
  482.     if jumpToOrder ~= -1 then
  483.         curOrder = jumpToOrder
  484.         curRow = jumpToRow
  485.         jumpToOrder = -1
  486.         jumpToRow = -1
  487.     else
  488.         curRow = curRow + 1
  489.         if curRow > 64 then
  490.             curRow = 1
  491.             curOrder = curOrder + 1
  492.             if curOrder > songLength then
  493.                 curOrder = restartPos + 1
  494.                 if curOrder < 1 then curOrder = 1 end
  495.                 curRow = 1
  496.             end
  497.         end
  498.     end
  499.  
  500.     ::continue::
  501. end
  502.  
  503. if pendingCount > 0 then
  504.     flushAudio(pending)
  505. end
  506.  
  507. print("Playback finished!")
Advertisement
Add Comment
Please, Sign In to add comment