WAVLib.lua
1 --[[ 2 CC-WAV-loader - Load any WAV file for use in ComputerCraft 3 Repository: https://github.com/Kestin4Real/CC-WAV-loader 4 Description: Load any WAV file for use in ComputerCraft 5 Language: Lua (100%) 6 7 Copyright (c) 2025 Kestin4Real. 8 All rights reserved. 9 10 Note: This code is provided without an explicit license. Use at your own discretion. 11 If you plan to redistribute or modify this work, please give proper attribution to the original repository. 12 For any formal permissions or licensing clarification, consider contacting the original author. 13 ]] 14 -- 15 16 ---@class Audio 17 ---@field channels integer Number of audio channels (1 = mono, 2 = stereo) 18 ---@field frequency integer Sample rate in Hz (e.g., 44100) 19 ---@field bytesPerSample integer Bytes per sample (1 = 8-bit, 2 = 16-bit) 20 ---@field samples integer Total number of samples per channel 21 ---@field lenght number Total duration in seconds 22 ---@field channelData table<integer, integer[]> Array of channel data arrays (1-based index) 23 24 local api = {} 25 26 ---@class AudioLoader 27 ---@field file any The file handle being read 28 local AudioLoader = {} 29 AudioLoader.__index = AudioLoader 30 31 ---Create a new AudioLoader instance 32 ---@return AudioLoader 33 function AudioLoader.new() 34 local self = setmetatable({}, AudioLoader) 35 self.file = nil 36 return self 37 end 38 39 ---Reads bytes in big-endian format 40 ---@param amount integer Number of bytes to read 41 ---@return integer 42 function AudioLoader:ReadBytesBig(amount) 43 local integer = 0 44 for i = 1, amount do 45 integer = bit.blshift(integer, 8) + self.file.read() 46 end 47 return integer 48 end 49 50 ---Reads bytes in little-endian format 51 ---@param amount integer Number of bytes to read 52 ---@param signed? boolean Whether to interpret as signed integer (default false) 53 ---@return integer 54 function AudioLoader:ReadBytesLittle(amount, signed) 55 if signed == nil then 56 signed = false 57 end 58 local integer = 0 59 for i = 1, amount do 60 local byte = self.file.read() 61 integer = integer + bit.blshift(byte, 8 * (i - 1)) 62 if signed then 63 if i == amount then 64 local sign = bit.brshift(byte, 7) == 1 65 if not sign then 66 return integer 67 end 68 integer = integer - bit.blshift(1, 7 + (8 * (i - 1))) 69 integer = integer - (math.pow(256, amount) / 2) 70 end 71 end 72 end 73 return integer 74 end 75 76 ---Reads bytes as hexadecimal string 77 ---@param amount integer Number of bytes to read 78 ---@return string 79 function AudioLoader:ReadBytesAsHex(amount) 80 local hex = "" 81 for i = 1, amount do 82 hex = hex .. string.format("%02X", self.file.read()) 83 end 84 return hex 85 end 86 87 ---Parses the format chunk (fmt ) 88 ---@param audio Audio Audio object to populate 89 function AudioLoader:ParseFMTchunk(audio) --FMT subchunk parser 90 if not (self:ReadBytesLittle(4) == 16) then 91 print("File is in a incorrect format or corrupted 0x04") 92 return 93 end --Check for chunk size 16 94 if not (self:ReadBytesAsHex(2) == "0100") then 95 print("File is in a incorrect format or corrupted 0x05") 96 return 97 end --Check WAVE type 0x01 98 audio.channels = self:ReadBytesLittle(2) --number of channels 99 audio.frequency = self:ReadBytesLittle(4) --sample frequency 100 self:ReadBytesBig(4) --bytes/sec (Garbage Data) 101 self:ReadBytesBig(2) --block alignment (Garbage Data) 102 audio.bytesPerSample = self:ReadBytesLittle(2) / 8 --bits per sample 103 end 104 105 ---Parses the data chunk (data) 106 ---@param audio Audio Audio object to populate 107 function AudioLoader:ParseDATAchunk(audio) --DATA subchunk parser 108 --size of the data chunk 109 audio.samples = self:ReadBytesLittle(4) / audio.channels / audio.bytesPerSample 110 audio.lenght = audio.samples / audio.frequency 111 for s = 0, audio.samples - 1 do 112 local sample = 0 113 for c = 0, audio.channels - 1 do 114 sample = sample + self:ReadBytesLittle(audio.bytesPerSample, audio.bytesPerSample > 1) 115 if audio.bytesPerSample == 1 then 116 sample = sample - 128 117 end 118 end 119 audio[s] = math.floor(sample / audio.channels / math.max(256 * (audio.bytesPerSample - 1), 1)) 120 end 121 end 122 123 ---Skips non-essential chunks 124 ---@param audio Audio Audio object being parsed 125 function AudioLoader:ParseDUMMYchunk(audio) --Parser for non essential data 126 self:ReadBytesLittle(self:ReadBytesLittle(4)) 127 end 128 129 ---Main loading function 130 ---@param path string Path to WAV file 131 ---@return Audio|nil Loaded audio object or nil on failure 132 function AudioLoader:Load(path) 133 path = shell.resolve(path) 134 if not fs.exists(path) then 135 print("File does not exists") 136 return 137 end 138 if fs.isDir(path) then 139 print("File does not exists") 140 return 141 end 142 self.file = fs.open(path, "rb") 143 local audio = {} 144 145 if not (self:ReadBytesAsHex(4) == "52494646") then 146 print("File is in a incorrect format or corrupted 0x01") 147 self.file.close() 148 return 149 end --Check header for RIFF 150 self:ReadBytesLittle(4) -- size of file 151 if not (self:ReadBytesAsHex(4) == "57415645") then 152 print("File is in a incorrect format or corrupted 0x02") 153 self.file.close() 154 return 155 end --Check WAVE header for WAVE 156 157 local parsers = {} 158 parsers["666D7420"] = function(audio) 159 self:ParseFMTchunk(audio) 160 end --FMT subchunk parser 161 parsers["64617461"] = function(audio) 162 self:ParseDATAchunk(audio) 163 end --DATA subchunk parser 164 165 while audio.lenght == nil do 166 local subchunk = self:ReadBytesAsHex(4) 167 if subchunk == nil then 168 print("File is in a incorrect format or corrupted 0x03") 169 self.file.close() 170 return 171 end 172 local parser = parsers[subchunk] 173 if parser == nil then 174 self:ParseDUMMYchunk(audio) 175 else 176 parser(audio) 177 end 178 end 179 self.file.close() 180 181 return audio 182 end 183 184 -- Create the public API 185 function api.Load(path) 186 local loader = AudioLoader.new() 187 return loader:Load(path) 188 end 189 190 return api