Last active
January 27, 2022 01:08
-
-
Save MCJack123/c8a4a3205881c9b870e93321bd905d09 to your computer and use it in GitHub Desktop.
AUKit: Audio decoding and processing framework for ComputerCraft
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- AUKit: Audio decoding and processing framework for ComputerCraft | |
| -- | |
| -- AUKit is a framework designed to simplify the process of loading, modifying, | |
| -- and playing audio files in various formats. It includes support for loading | |
| -- audio from many sources, including PCM, DFPWM, and IMA ADPCM codecs, as well | |
| -- as WAV, AIFF, AU, and FLAC files. It can also generate audio on-the-fly as | |
| -- tones, noise, or silence. | |
| -- | |
| -- AUKit uses a structure called Audio to store information about each audio | |
| -- chunk. An audio object holds the sample rate of the audio, as well as the | |
| -- data for each channel stored as floating-point numbers. Audio objects can | |
| -- hold any number of channels at any sample rate with any duration. | |
| -- | |
| -- To obtain an audio object, you can use any of the main functions in the aukit | |
| -- module. These allow loading from various raw codecs or file formats, with | |
| -- data sources as strings, or tables if using a raw codec loader. | |
| -- | |
| -- Once the audio is loaded, various basic operations are available. A subset of | |
| -- the string library is available to simplify operations on the audio, and a | |
| -- number of operators (+, *, .., #) are overridden as well. There's also built- | |
| -- in functions for resampling the audio, with nearest-neighbor, linear, and | |
| -- cubic interpolation available; as well as mixing channels (including down to | |
| -- mono) and combining/splitting channels. Finally, audio objects can be exported | |
| -- back to PCM, DFPWM, or WAV data, allowing changes to be easily stored on disk. | |
| -- The stream function also automatically chunks data for use with a speaker. | |
| -- All of these functions return a new audio object, leaving the original intact. | |
| -- | |
| -- There are also a number of effects available for audio. These are contained | |
| -- in the aukit.effects table, and modify the audio passed to them (as well as | |
| -- returning the audio for streamlining). The effects are intended to speed up | |
| -- common operations on audio. More effects may be added in future versions. | |
| -- | |
| -- Be aware that processing large amounts of audio (especially loading FLAC or | |
| -- resampling with higher quality) is *very* slow. It's recommended to use audio | |
| -- files with lower data size (8-bit mono PCM/WAV/AIFF is ideal), and potentially | |
| -- a lower sample rate, to reduce the load on the system - especially as all | |
| -- data gets converted to 8-bit DFPWM data on playback anyway. The code yields | |
| -- internally when things take a long time to avoid abort timeouts. | |
| -- | |
| -- For an example of how to use AUKit, see the accompanying auplay.lua file. | |
| -- | |
| -- @author JackMacWindows | |
| -- @license MIT | |
| -- | |
| -- <style>#content {width: unset !important;}</style> | |
| -- | |
| -- @module aukit | |
| -- @set project=AUKit | |
| -- MIT License | |
| -- | |
| -- Copyright (c) 2021-2022 JackMacWindows | |
| -- | |
| -- Permission is hereby granted, free of charge, to any person obtaining a copy | |
| -- of this software and associated documentation files (the "Software"), to deal | |
| -- in the Software without restriction, including without limitation the rights | |
| -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| -- copies of the Software, and to permit persons to whom the Software is | |
| -- furnished to do so, subject to the following conditions: | |
| -- | |
| -- The above copyright notice and this permission notice shall be included in all | |
| -- copies or substantial portions of the Software. | |
| -- | |
| -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| -- SOFTWARE. | |
| local expect = require "cc.expect" | |
| local dfpwm = require "cc.audio.dfpwm" | |
| local aukit = {} | |
| aukit.effects, aukit.stream = {}, {} | |
| --- @tfield "none"|"linear"|"cubic" defaultInterpolation Default interpolation mode for @{Audio:resample} and other functions that need to resample. | |
| aukit.defaultInterpolation = "linear" | |
| --- @type Audio | |
| local Audio = {} | |
| local Audio_mt | |
| local ima_index_table = { | |
| [0] = -1, -1, -1, -1, 2, 4, 6, 8, | |
| -1, -1, -1, -1, 2, 4, 6, 8 | |
| } | |
| local ima_step_table = { | |
| [0] = 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, | |
| 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, | |
| 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, | |
| 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, | |
| 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, | |
| 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, | |
| 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, | |
| 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, | |
| 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767 | |
| } | |
| local function clamp(n, min, max) return math.max(math.min(n, max), min) end | |
| local function expectAudio(n, var) | |
| if type(var) == "table" and getmetatable(var) == Audio_mt then return var end | |
| expect(n, var, "Audio") -- always fails | |
| end | |
| local function intunpack(str, pos, sz, signed, be) | |
| local n = 0 | |
| if be then for i = 0, sz - 1 do n = n * 256 + str:byte(pos+i) end | |
| else for i = 0, sz - 1 do n = n + str:byte(pos+i) * 2^(8*i) end end | |
| if signed and n >= 2^(sz*8-1) then n = n - 2^(sz*8) end | |
| return n, pos + sz | |
| end | |
| local interpolate = { | |
| none = function(data, x) | |
| return data[math.floor(x)] | |
| end, | |
| linear = function(data, x) | |
| return data[math.floor(x)] + ((data[math.ceil(x)] or data[math.floor(x)]) - data[math.floor(x)]) * (x - math.floor(x)) | |
| end, | |
| cubic = function(data, x) | |
| local p0, p1, p2, p3, fx = data[math.floor(x)-1], data[math.floor(x)], data[math.ceil(x)], data[math.ceil(x)+1], x - math.floor(x) | |
| p0, p2, p3 = p0 or p1, p2 or p1, p3 or p2 or p1 | |
| return (-0.5*p0 + 1.5*p1 - 1.5*p2 + 0.5*p3)*fx^3 + (p0 - 2.5*p1 + 2*p2 - 0.5*p3)*fx^2 + (-0.5*p0 + 0.5*p2)*fx + p1 | |
| end | |
| } | |
| local interpolation_start = {none = 1, linear = 1, cubic = 0} | |
| local interpolation_end = {none = 1, linear = 2, cubic = 3} | |
| local wavegen = { | |
| sine = function(x, freq, amplitude) | |
| return math.sin(2 * x * math.pi * freq) * amplitude | |
| end, | |
| triangle = function(x, freq, amplitude) | |
| return 2.0 * math.abs(amplitude * math.fmod(2.0 * x * freq + 1.5, 2.0) - amplitude) - amplitude | |
| end, | |
| square = function(x, freq, amplitude, duty) | |
| if (x * freq) % 1 >= duty then return -amplitude else return amplitude end | |
| end, | |
| sawtooth = function(x, freq, amplitude) | |
| return amplitude * math.fmod(2.0 * x * freq + 1.0, 2.0) - amplitude | |
| end | |
| } | |
| local decodeFLAC do | |
| -- Simple FLAC decoder (Java) | |
| -- | |
| -- Copyright (c) 2017 Project Nayuki. (MIT License) | |
| -- https://www.nayuki.io/page/simple-flac-implementation | |
| -- | |
| -- Permission is hereby granted, free of charge, to any person obtaining a copy of | |
| -- this software and associated documentation files (the "Software"), to deal in | |
| -- the Software without restriction, including without limitation the rights to | |
| -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | |
| -- the Software, and to permit persons to whom the Software is furnished to do so, | |
| -- subject to the following conditions: | |
| -- - The above copyright notice and this permission notice shall be included in | |
| -- all copies or substantial portions of the Software. | |
| -- - The Software is provided "as is", without warranty of any kind, express or | |
| -- implied, including but not limited to the warranties of merchantability, | |
| -- fitness for a particular purpose and noninfringement. In no event shall the | |
| -- authors or copyright holders be liable for any claim, damages or other | |
| -- liability, whether in an action of contract, tort or otherwise, arising from, | |
| -- out of or in connection with the Software or the use or other dealings in the | |
| -- Software. | |
| local FIXED_PREDICTION_COEFFICIENTS = { | |
| {}, | |
| {1}, | |
| {2, -1}, | |
| {3, -3, 1}, | |
| {4, -6, 4, -1}, | |
| }; | |
| local function BitInputStream(data, pos) | |
| local obj = {} | |
| local bitBuffer, bitBufferLen = 0, 0 | |
| function obj.alignToByte() | |
| bitBufferLen = bitBufferLen - bitBufferLen % 8 | |
| end | |
| function obj.readByte() | |
| return obj.readUint(8) | |
| end | |
| function obj.readUint(n) | |
| if n == 0 then return 0 end | |
| while bitBufferLen < n do | |
| local temp = data:byte(pos) | |
| pos = pos + 1 | |
| if temp == nil then return nil end | |
| bitBuffer = (bitBuffer * 256 + temp) % 0x100000000000 | |
| bitBufferLen = bitBufferLen + 8 | |
| end | |
| bitBufferLen = bitBufferLen - n | |
| local result = math.floor(bitBuffer / 2^bitBufferLen) | |
| if n < 32 then result = result % 2^n end | |
| return result | |
| end | |
| function obj.readSignedInt(n) | |
| local v = obj.readUint(n) | |
| if v >= 2^(n-1) then v = v - 2^n end | |
| return v | |
| end | |
| function obj.readRiceSignedInt(param) | |
| local val = 0 | |
| while (obj.readUint(1) == 0) do val = val + 1 end | |
| val = val * 2^param + obj.readUint(param) | |
| if bit32.btest(val, 1) then return -math.floor(val / 2) - 1 | |
| else return math.floor(val / 2) end | |
| end | |
| return obj | |
| end | |
| local function decodeResiduals(inp, warmup, blockSize, result) | |
| local method = inp.readUint(2); | |
| if (method >= 2) then error("Reserved residual coding method " .. method) end | |
| local paramBits = method == 0 and 4 or 5; | |
| local escapeParam = method == 0 and 0xF or 0x1F; | |
| local partitionOrder = inp.readUint(4); | |
| local numPartitions = 2^partitionOrder; | |
| if (blockSize % numPartitions ~= 0) then | |
| error("Block size not divisible by number of Rice partitions") | |
| end | |
| local partitionSize = math.floor(blockSize / numPartitions); | |
| for i = 0, numPartitions-1 do | |
| local start = i * partitionSize + (i == 0 and warmup or 0); | |
| local endd = (i + 1) * partitionSize; | |
| local param = inp.readUint(paramBits); | |
| if (param < escapeParam) then | |
| for j = start, endd - 1 do | |
| result[j+1] = inp.readRiceSignedInt(param) | |
| end | |
| else | |
| local numBits = inp.readUint(5); | |
| for j = start, endd - 1 do | |
| result[j+1] = inp.readSignedInt(numBits) | |
| end | |
| end | |
| end | |
| end | |
| local function restoreLinearPrediction(result, coefs, shift, blockSize) | |
| for i = #coefs, blockSize - 1 do | |
| local sum = 0 | |
| for j = 0, #coefs - 1 do | |
| sum = sum + result[i - j] * coefs[j + 1] | |
| end | |
| result[i + 1] = result[i + 1] + math.floor(sum / 2^shift) | |
| end | |
| end | |
| local function decodeFixedPredictionSubframe(inp, predOrder, sampleDepth, blockSize, result) | |
| for i = 1, predOrder do | |
| result[i] = inp.readSignedInt(sampleDepth); | |
| end | |
| decodeResiduals(inp, predOrder, blockSize, result); | |
| restoreLinearPrediction(result, FIXED_PREDICTION_COEFFICIENTS[predOrder+1], 0, blockSize); | |
| end | |
| local function decodeLinearPredictiveCodingSubframe(inp, lpcOrder, sampleDepth, blockSize, result) | |
| for i = 1, lpcOrder do | |
| result[i] = inp.readSignedInt(sampleDepth); | |
| end | |
| local precision = inp.readUint(4) + 1; | |
| local shift = inp.readSignedInt(5); | |
| local coefs = {}; | |
| for i = 1, lpcOrder do | |
| coefs[i] = inp.readSignedInt(precision); | |
| end | |
| decodeResiduals(inp, lpcOrder, blockSize, result); | |
| restoreLinearPrediction(result, coefs, shift, blockSize); | |
| end | |
| local function decodeSubframe(inp, sampleDepth, blockSize, result) | |
| inp.readUint(1); | |
| local type = inp.readUint(6); | |
| local shift = inp.readUint(1); | |
| if (shift == 1) then | |
| while (inp.readUint(1) == 0) do shift = shift + 1 end | |
| end | |
| sampleDepth = sampleDepth - shift | |
| if (type == 0) then -- Constant coding | |
| local c = inp.readSignedInt(sampleDepth) | |
| for i = 1, blockSize do result[i] = c end | |
| elseif (type == 1) then -- Verbatim coding | |
| for i = 1, blockSize do | |
| result[i] = inp.readSignedInt(sampleDepth); | |
| end | |
| elseif (8 <= type and type <= 12) then | |
| decodeFixedPredictionSubframe(inp, type - 8, sampleDepth, blockSize, result) | |
| elseif (32 <= type and type <= 63) then | |
| decodeLinearPredictiveCodingSubframe(inp, type - 31, sampleDepth, blockSize, result) | |
| else | |
| error("Reserved subframe type") | |
| end | |
| for i = 1, blockSize do | |
| result[i] = result[i] * 2^shift | |
| end | |
| end | |
| local function decodeSubframes(inp, sampleDepth, chanAsgn, blockSize, result) | |
| local subframes = {} | |
| for i = 1, #result do subframes[i] = {} end | |
| if (0 <= chanAsgn and chanAsgn <= 7) then | |
| for ch = 1, #result do | |
| decodeSubframe(inp, sampleDepth, blockSize, subframes[ch]) | |
| end | |
| elseif (8 <= chanAsgn and chanAsgn <= 10) then | |
| decodeSubframe(inp, sampleDepth + (chanAsgn == 9 and 1 or 0), blockSize, subframes[1]) | |
| decodeSubframe(inp, sampleDepth + (chanAsgn == 9 and 0 or 1), blockSize, subframes[2]) | |
| if (chanAsgn == 8) then | |
| for i = 1, blockSize do | |
| subframes[2][i] = subframes[1][i] - subframes[2][i] | |
| end | |
| elseif (chanAsgn == 9) then | |
| for i = 1, blockSize do | |
| subframes[1][i] = subframes[1][i] + subframes[2][i] | |
| end | |
| elseif (chanAsgn == 10) then | |
| for i = 1, blockSize do | |
| local side = subframes[2][i] | |
| local right = subframes[1][i] - math.floor(side / 2) | |
| subframes[2][i] = right | |
| subframes[1][i] = right + side | |
| end | |
| end | |
| else | |
| error("Reserved channel assignment"); | |
| end | |
| for ch = 1, #result do | |
| for i = 1, blockSize do | |
| local s = subframes[ch][i] | |
| if s >= 2^(sampleDepth-1) then s = s - 2^sampleDepth end | |
| result[ch][i] = s / 2^sampleDepth | |
| end | |
| end | |
| end | |
| local function decodeFrame(inp, numChannels, sampleDepth, out2, callback) | |
| local out = {} | |
| for i = 1, numChannels do out[i] = {} end | |
| -- Read a ton of header fields, and ignore most of them | |
| local temp = inp.readByte() | |
| if temp == nil then | |
| return false | |
| end | |
| local sync = temp * 64 + inp.readUint(6); | |
| if sync ~= 0x3FFE then error("Sync code expected") end | |
| inp.readUint(1); | |
| inp.readUint(1); | |
| local blockSizeCode = inp.readUint(4); | |
| local sampleRateCode = inp.readUint(4); | |
| local chanAsgn = inp.readUint(4); | |
| inp.readUint(3); | |
| inp.readUint(1); | |
| temp = inp.readUint(8); | |
| local t2 = -1 | |
| for i = 7, 0, -1 do if not bit32.btest(temp, 2^i) then break end t2 = t2 + 1 end | |
| for i = 1, t2 do inp.readUint(8) end | |
| local blockSize | |
| if (blockSizeCode == 1) then | |
| blockSize = 192 | |
| elseif (2 <= blockSizeCode and blockSizeCode <= 5) then | |
| blockSize = 576 * 2^(blockSizeCode - 2) | |
| elseif (blockSizeCode == 6) then | |
| blockSize = inp.readUint(8) + 1 | |
| elseif (blockSizeCode == 7) then | |
| blockSize = inp.readUint(16) + 1 | |
| elseif (8 <= blockSizeCode and blockSizeCode <= 15) then | |
| blockSize = 256 * 2^(blockSizeCode - 8) | |
| else | |
| error("Reserved block size") | |
| end | |
| if (sampleRateCode == 12) then | |
| inp.readUint(8) | |
| elseif (sampleRateCode == 13 or sampleRateCode == 14) then | |
| inp.readUint(16) | |
| end | |
| inp.readUint(8) | |
| decodeSubframes(inp, sampleDepth, chanAsgn, blockSize, out) | |
| inp.alignToByte() | |
| inp.readUint(16) | |
| if callback then callback(out) else | |
| for c = 1, numChannels do | |
| local n = #out2[c] | |
| for i = 1, blockSize do out2[c][n+i] = out[c][i] end | |
| end | |
| end | |
| return true | |
| end | |
| function decodeFLAC(inp, callback) | |
| local out = {} | |
| local pos = 1 | |
| -- Handle FLAC header and metadata blocks | |
| local temp temp, pos = intunpack(inp, pos, 4, false, true) | |
| if temp ~= 0x664C6143 then error("Invalid magic string") end | |
| local sampleRate, numChannels, sampleDepth, numSamples | |
| local last = false | |
| while not last do | |
| temp, pos = inp:byte(pos), pos + 1 | |
| last = bit32.btest(temp, 0x80) | |
| local type = bit32.band(temp, 0x7F); | |
| local length length, pos = intunpack(inp, pos, 3, false, true) | |
| if type == 0 then -- Stream info block | |
| pos = pos + 10 | |
| sampleRate, pos = intunpack(inp, pos, 2, false, true) | |
| sampleRate = sampleRate * 16 + bit32.rshift(inp:byte(pos), 4) | |
| numChannels = bit32.band(bit32.rshift(inp:byte(pos), 1), 7) + 1; | |
| sampleDepth = bit32.band(inp:byte(pos), 1) * 16 + bit32.rshift(inp:byte(pos+1), 4) + 1; | |
| numSamples, pos = intunpack(inp, pos + 2, 4, false, true) | |
| numSamples = numSamples + bit32.band(inp:byte(pos-5), 15) * 2^32 | |
| pos = pos + 16 | |
| else | |
| pos = pos + length | |
| end | |
| end | |
| if not sampleRate then error("Stream info metadata block absent") end | |
| if sampleDepth % 8 ~= 0 then error("Sample depth not supported") end | |
| for i = 1, numChannels do out[i] = {} end | |
| if callback then callback(sampleRate, numSamples) end | |
| -- Decode FLAC audio frames and write raw samples | |
| inp = BitInputStream(inp, pos) | |
| repeat until not decodeFrame(inp, numChannels, sampleDepth, out, callback) | |
| if not callback then return {sampleRate = sampleRate, data = out} end | |
| end | |
| end | |
| --- Returns the length of the audio object in seconds. | |
| -- @treturn number The audio length | |
| function Audio:len() | |
| return #self.data[1] / self.sampleRate | |
| end | |
| --- Creates a new audio object with the data resampled to a different sample rate. | |
| -- If the target rate is the same, the object is copied without modification. | |
| -- @tparam number sampleRate The new sample rate in Hertz | |
| -- @tparam[opt=aukit.defaultInterpolation] "none"|"linear"|"cubic" interpolation The interpolation mode to use | |
| -- @treturn Audio A new audio object with the resampled data | |
| function Audio:resample(sampleRate, interpolation) | |
| expect(1, sampleRate, "number") | |
| interpolation = expect(2, interpolation, "string", "nil") or aukit.defaultInterpolation | |
| if interpolation ~= "none" and interpolation ~= "linear" and interpolation ~= "cubic" then error("bad argument #2 (invalid interpolation type)", 2) end | |
| local new = setmetatable({sampleRate = sampleRate, data = {}}, Audio_mt) | |
| local ratio = sampleRate / self.sampleRate | |
| local newlen = #self.data[1] * ratio | |
| local interp = interpolate[interpolation] | |
| local start = os.epoch "utc" | |
| for y, c in ipairs(self.data) do | |
| local line = {} | |
| for i = 1, newlen do | |
| if os.epoch "utc" - start > 3000 then start = os.epoch "utc" sleep(0) end | |
| local x = (i - 1) / ratio + 1 | |
| if x % 1 == 0 then line[i] = c[x] | |
| else line[i] = clamp(interp(c, x), -1, 1) end | |
| end | |
| new.data[y] = line | |
| end | |
| return new | |
| end | |
| --- Mixes down all channels to a new mono-channel audio object. | |
| -- @treturn Audio A new audio object with the audio mixed to mono | |
| function Audio:mono() | |
| local new = setmetatable({sampleRate = self.sampleRate, data = {{}}}, Audio_mt) | |
| local cn = #self.data | |
| local start = os.epoch "utc" | |
| for i = 1, #self.data[1] do | |
| if os.epoch "utc" - start > 3000 then start = os.epoch "utc" sleep(0) end | |
| local s = 0 | |
| for c = 1, cn do s = s + self.data[c][i] end | |
| new.data[1][i] = s / cn | |
| end | |
| return new | |
| end | |
| --- Concatenates this audio object with another, adding the contents of each | |
| -- new channel to the end of each old channel, resampling the new channels to match | |
| -- this one (if necessary), and inserting silence in any missing channels. | |
| -- @tparam Audio ... The audio objects to concatenate | |
| -- @treturn Audio The new concatenated audio object | |
| function Audio:concat(...) | |
| local audios = {self, ...} | |
| local l = {#self.data[1]} | |
| local cn = #self.data | |
| for i = 2, #audios do | |
| expectAudio(i-1, audios[i]) | |
| if audios[i].sampleRate ~= self.sampleRate then audios[i] = audios[i]:resample(self.sampleRate) end | |
| l[i] = #audios[i].data[1] | |
| cn = math.max(cn, #audios[i].data) | |
| end | |
| local obj = setmetatable({sampleRate = self.sampleRate, data = {}}, Audio_mt) | |
| for c = 1, cn do | |
| local ch = {} | |
| local pos = 0 | |
| for a = 1, #audios do | |
| local sch = audios[a].data[c] | |
| if sch then for i = 1, l[a] do ch[pos+i] = sch[i] end | |
| else for i = 1, l[a] do ch[pos+i] = 0 end end | |
| pos = pos + l[a] | |
| end | |
| obj.data[c] = ch | |
| end | |
| return obj | |
| end | |
| --- Takes a subregion of the audio and returns a new audio object with its contents. | |
| -- This takes the same arguments as @{string.sub}, but positions start at 0. | |
| -- @tparam[opt=1] number start The start position of the audio in seconds | |
| -- @tparam[opt=-1] number last The end position of the audio in seconds | |
| -- @treturn Audio The new split audio object | |
| function Audio:sub(start, last) | |
| start = math.floor(expect(1, start, "number", "nil") or 1) | |
| last = math.floor(expect(2, last, "number", "nil") or -1) | |
| local len = #self.data[1] / self.sampleRate | |
| if start < 0 then start = len + start end | |
| if last < 0 then last = len + last end | |
| expect.range(start, 1, len) | |
| expect.range(last, 1, len) | |
| start, last = start * self.sampleRate + 1, last * self.sampleRate + 1 | |
| local obj = setmetatable({sampleRate = self.sampleRate, data = {}}, Audio_mt) | |
| for c = 1, #self.data do | |
| local ch = {} | |
| local sch = self.data[c] | |
| for i = start, last do ch[i-start+1] = sch[i] end | |
| obj.data[c] = ch | |
| end | |
| return obj | |
| end | |
| --- Combines the channels of this audio object with another, adding the new | |
| -- channels on the end of the new object, resampling the new channels to match | |
| -- this one (if necessary), and extending any channels that are shorter than the | |
| -- longest channel with zeroes. | |
| -- @tparam Audio ... The audio objects to combine with | |
| -- @treturn Audio The new combined audio object | |
| function Audio:combine(...) | |
| local audios = {self, ...} | |
| local len = #self.data[1] | |
| for i = 2, #audios do | |
| expectAudio(i-1, audios[i]) | |
| if audios[i].sampleRate ~= self.sampleRate then audios[i] = audios[i]:resample(self.sampleRate) end | |
| len = math.max(len, #audios[i].data[1]) | |
| end | |
| local obj = setmetatable({sampleRate = self.sampleRate, data = {}}, Audio_mt) | |
| local pos = 0 | |
| for a = 1, #audios do | |
| for c = 1, #audios[a].data do | |
| local sch, ch = audios[a].data[c], {} | |
| for i = 1, len do ch[i] = sch[i] or 0 end | |
| obj.data[pos+c] = ch | |
| end | |
| pos = pos + #audios[a].data | |
| end | |
| return obj | |
| end | |
| --- Splits this audio object into one or more objects with the specified channels. | |
| -- Passing a channel that doesn't exist will throw and error. | |
| -- @tparam {[number]...} ... The lists of channels in each new object | |
| -- @treturn Audio... The new audio objects created from the channels in each list | |
| -- @usage Split a stereo track into independent mono objects | |
| -- | |
| -- local left, right = stereo:split({1}, {2}) | |
| function Audio:split(...) | |
| local retval = {} | |
| for n, cl in ipairs{...} do | |
| expect(n, cl, "table") | |
| local obj = setmetatable({sampleRate = self.sampleRate, data = {}}, Audio_mt) | |
| for cd, cs in ipairs(cl) do | |
| local sch, ch = self.data[expect(cd, cs, "number")], {} | |
| if not sch then error("channel " .. cs .. " (in argument " .. n .. ") out of range", 2) end | |
| for i = 1, #sch do ch[i] = sch[i] end | |
| obj[cd] = ch | |
| end | |
| retval[#retval+1] = obj | |
| end | |
| return table.unpack(retval) | |
| end | |
| --- Mixes two or more audio objects into a single object, amplifying each sample | |
| -- with a multiplier (before clipping) if desired, and clipping any values | |
| -- outside the audio range ([-1, 1]). Channels that are shorter are padded with | |
| -- zeroes at the end, and non-existent channels are replaced with all zeroes. | |
| -- Any audio objects with a different sample rate are resampled to match this one. | |
| -- @tparam number|Audio amplifier The multiplier to apply, or the first audio object | |
| -- @tparam[opt] Audio ... The objects to mix with this one | |
| -- @treturn Audio The new mixed audio object | |
| function Audio:mix(amplifier, ...) | |
| local audios = {self, ...} | |
| local len = #self.data[1] | |
| local cn = #self.data | |
| for i = 2, #audios do | |
| expectAudio(i, audios[i]) | |
| if audios[i].sampleRate ~= self.sampleRate then audios[i] = audios[i]:resample(self.sampleRate) end | |
| len = math.max(len, #audios[i].data[1]) | |
| cn = math.max(cn, #audios[i].data) | |
| end | |
| if type(amplifier) ~= "number" then | |
| expectAudio(1, amplifier) | |
| if amplifier.sampleRate ~= self.sampleRate then amplifier = amplifier:resample(self.sampleRate) end | |
| len = math.max(len, #amplifier.data[1]) | |
| cn = math.max(cn, #amplifier.data) | |
| table.insert(audios, 2, amplifier) | |
| amplifier = 1 | |
| end | |
| local obj = setmetatable({sampleRate = self.sampleRate, data = {}}, Audio_mt) | |
| for c = 1, cn do | |
| local ch = {} | |
| local sch = {} | |
| for a = 1, #audios do sch[a] = audios[a].data[c] end | |
| for i = 1, len do | |
| local s = 0 | |
| for a = 1, #audios do if sch[a] then s = s + (sch[a][i] or 0) end end | |
| ch[i] = clamp(s * amplifier, -1, 1) | |
| end | |
| obj[c] = ch | |
| end | |
| return obj | |
| end | |
| --- Returns a new audio object that repeats this audio a number of times. | |
| -- @tparam number count The number of times to play the audio | |
| -- @treturn Audio The repeated audio | |
| function Audio:rep(count) | |
| if type(self) ~= "table" and type(count) == "table" then self, count = count, self end | |
| expect(1, count, "number") | |
| local obj = setmetatable({sampleRate = self.sampleRate, data = {}}, Audio_mt) | |
| for c = 1, #self.data do | |
| local sch, ch = self.data[c], {} | |
| for n = 0, count - 1 do | |
| local pos = n * #sch | |
| for i = 1, #sch do ch[pos+i] = sch[i] end | |
| end | |
| obj.data[c] = ch | |
| end | |
| return obj | |
| end | |
| --- Returns a reversed version of this audio. | |
| -- @treturn Audio The reversed audio | |
| function Audio:reverse() | |
| local obj = setmetatable({sampleRate = self.sampleRate, data = {}}, Audio_mt) | |
| for c = 1, #self.data do | |
| local sch, ch = self.data[c], {} | |
| local len = #sch | |
| for i = 1, len do ch[len-i+1] = sch[i] end | |
| obj.data[c] = ch | |
| end | |
| return obj | |
| end | |
| local function encodePCM(info, pos) | |
| local maxValue = 2^(info.bitDepth-1) | |
| local add = info.dataType == "unsigned" and maxValue or 0 | |
| local source = info.audio.data | |
| local function encode(d) | |
| if info.dataType == "float" then return d | |
| else return d * (d < 0 and maxValue or maxValue-1) + add end | |
| end | |
| local data = {} | |
| local nc = #source | |
| local len = #source[1] | |
| if pos > len then return nil end | |
| if info.interleaved then for n = pos, pos + info.len - 1 do for c = 1, nc do data[(n-1)*nc+c] = encode(source[c][n]) end end | |
| elseif info.multiple then | |
| for c = 1, nc do | |
| data[c] = {} | |
| for n = pos, pos + info.len - 1 do | |
| local s = source[c][n] | |
| if not s then break end | |
| data[c][n-pos+1] = encode(s) | |
| end | |
| end | |
| return pos + info.len, table.unpack(data) | |
| else for c = 1, nc do for n = pos, pos + info.len - 1 do data[(c-1)*len+n] = encode(source[c][n]) end end end | |
| return data | |
| end | |
| --- Converts the audio data to raw PCM samples. | |
| -- @tparam[opt=8] number bitDepth The bit depth of the audio (8, 16, 24, 32) | |
| -- @tparam[opt="signed"] "signed"|"unsigned"|"float" dataType The type of each sample | |
| -- @tparam[opt=true] boolean interleaved Whether to interleave each channel | |
| -- @treturn {[number]...} The resulting audio data | |
| function Audio:pcm(bitDepth, dataType, interleaved) | |
| bitDepth = expect(1, bitDepth, "number", "nil") or 8 | |
| dataType = expect(2, dataType, "string", "nil") or "signed" | |
| expect(3, interleaved, "boolean", "nil") | |
| if interleaved == nil then interleaved = true end | |
| if bitDepth ~= 8 and bitDepth ~= 16 and bitDepth ~= 24 and bitDepth ~= 32 then error("bad argument #2 (invalid bit depth)", 2) end | |
| if dataType ~= "signed" and dataType ~= "unsigned" and dataType ~= "float" then error("bad argument #3 (invalid data type)", 2) end | |
| if dataType == "float" and bitDepth ~= 32 then error("bad argument #2 (float audio must have 32-bit depth)", 2) end | |
| return encodePCM({audio = self, bitDepth = bitDepth, dataType = dataType, interleaved = interleaved, len = #self.data[1]}, 1) | |
| end | |
| --- Returns a function that can be called to encode PCM samples in chunks. | |
| -- This is useful as a for iterator, and can be used with @{aukit.play}. | |
| -- @tparam[opt=131072] number chunkSize The size of each chunk | |
| -- @tparam[opt=8] number bitDepth The bit depth of the audio (8, 16, 24, 32) | |
| -- @tparam[opt="signed"] "signed"|"unsigned"|"float" dataType The type of each sample | |
| -- @treturn function():{{[number]...}...},number An iterator function that returns | |
| -- chunks of each channel's data as arrays of signed 8-bit 48kHz PCM, as well as | |
| -- the current position of the audio in seconds | |
| -- @treturn number The total length of the audio in seconds | |
| function Audio:stream(chunkSize, bitDepth, dataType) | |
| chunkSize = expect(1, chunkSize, "number", "nil") or 131072 | |
| bitDepth = expect(2, bitDepth, "number", "nil") or 8 | |
| dataType = expect(3, dataType, "string", "nil") or "signed" | |
| if bitDepth ~= 8 and bitDepth ~= 16 and bitDepth ~= 24 and bitDepth ~= 32 then error("bad argument #2 (invalid bit depth)", 2) end | |
| if dataType ~= "signed" and dataType ~= "unsigned" and dataType ~= "float" then error("bad argument #3 (invalid data type)", 2) end | |
| if dataType == "float" and bitDepth ~= 32 then error("bad argument #2 (float audio must have 32-bit depth)", 2) end | |
| local info, pos = {audio = self, bitDepth = bitDepth, dataType = dataType, interleaved = false, multiple = true, len = chunkSize}, 1 | |
| return function() | |
| if info == nil then return nil end | |
| local p = pos / self.sampleRate | |
| local v = {encodePCM(info, pos)} | |
| if v[1] == nil then info = nil return nil end | |
| pos = table.remove(v, 1) | |
| return v, p | |
| end, #self.data[1] / self.sampleRate | |
| end | |
| --- Coverts the audio data to a WAV file. | |
| -- @tparam[opt=16] number bitDepth The bit depth of the audio (8, 16, 24, 32) | |
| -- @treturn string The resulting WAV file data | |
| function Audio:wav(bitDepth) | |
| -- TODO: Support float data | |
| bitDepth = expect(1, bitDepth, "number", "nil") or 16 | |
| if bitDepth ~= 8 and bitDepth ~= 16 and bitDepth ~= 24 and bitDepth ~= 32 then error("bad argument #2 (invalid bit depth)", 2) end | |
| local data = self:pcm(bitDepth, bitDepth == 8 and "unsigned" or "signed", true) | |
| local str = "" | |
| local csize = jit and 7680 or 32768 | |
| local format = ((bitDepth == 8 and "I" or "i") .. (bitDepth / 8)):rep(csize) | |
| for i = 1, #data - csize, csize do str = str .. format:pack(table.unpack(data, i, i + csize - 1)) end | |
| str = str .. ((bitDepth == 8 and "I" or "i") .. (bitDepth / 8)):rep(#data % csize):pack(table.unpack(data, math.floor(#data / csize) * csize)) | |
| return ("<c4Ic4c4IHHIIHHc4I"):pack("RIFF", #str + 36, "WAVE", "fmt ", 16, 1, #self.data, self.sampleRate, self.sampleRate * #self.data, #self.data * bitDepth / 8, bitDepth, "data", #str) .. str | |
| end | |
| --- Converts the audio data to DFPWM. All channels share the same encoder, and | |
| -- channels are stored sequentially uninterleaved. | |
| -- @treturn string... The resulting DFPWM data for each channel | |
| function Audio:dfpwm() | |
| local channels = {self:pcm(8, "signed", false)} | |
| local encode = dfpwm.make_encoder() | |
| for i = 1, #channels do channels[i] = encode(channels[i]) end | |
| return table.unpack(channels) | |
| end | |
| Audio_mt = {__index = Audio, __add = Audio.combine, __mul = Audio.rep, __concat = Audio.concat, __len = Audio.len, __name = "Audio"} | |
| function Audio_mt:__tostring() | |
| return "Audio: " .. self.sampleRate .. " Hz, " .. #self.data .. " channels, " .. (#self.data[1] / self.sampleRate) .. " seconds" | |
| end | |
| --- aukit | |
| -- @section aukit | |
| --- Creates a new audio object from the specified raw PCM data. | |
| -- @tparam string|table data The audio data, either as a raw string, or a table | |
| -- of values (in the format specified by `bitDepth` and `dataType`) | |
| -- @tparam[opt=8] number bitDepth The bit depth of the audio (8, 16, 24, 32); if `dataType` is "float" then this must be 32 | |
| -- @tparam[opt="signed"] "signed"|"unsigned"|"float" dataType The type of each sample | |
| -- @tparam[opt=1] number channels The number of channels present in the audio | |
| -- @tparam[opt=48000] number sampleRate The sample rate of the audio in Hertz | |
| -- @tparam[opt=true] boolean interleaved Whether each channel is interleaved or separate | |
| -- @tparam[opt=false] boolean bigEndian Whether the audio is big-endian or little-endian; ignored if data is a table | |
| -- @treturn Audio A new audio object containing the specified data | |
| function aukit.pcm(data, bitDepth, dataType, channels, sampleRate, interleaved, bigEndian) | |
| expect(1, data, "string", "table") | |
| bitDepth = expect(2, bitDepth, "number", "nil") or 8 | |
| dataType = expect(3, dataType, "string", "nil") or "signed" | |
| channels = expect(4, channels, "number", "nil") or 1 | |
| sampleRate = expect(5, sampleRate, "number", "nil") or 48000 | |
| expect(6, interleaved, "boolean", "nil") | |
| if interleaved == nil then interleaved = true end | |
| expect(7, bigEndian, "boolean", "nil") | |
| if bitDepth ~= 8 and bitDepth ~= 16 and bitDepth ~= 24 and bitDepth ~= 32 then error("bad argument #2 (invalid bit depth)", 2) end | |
| if dataType ~= "signed" and dataType ~= "unsigned" and dataType ~= "float" then error("bad argument #3 (invalid data type)", 2) end | |
| if dataType == "float" and bitDepth ~= 32 then error("bad argument #2 (float audio must have 32-bit depth)", 2) end | |
| expect.range(channels, 1) | |
| expect.range(sampleRate, 1) | |
| local byteDepth = bitDepth / 8 | |
| if (#data / (type(data) == "table" and 1 or byteDepth)) % channels ~= 0 then error("bad argument #1 (uneven amount of data per channel)", 2) end | |
| local len = (#data / (type(data) == "table" and 1 or byteDepth)) / channels | |
| local csize = jit and 7680 or 32768 | |
| local format = (bigEndian and ">" or "<") .. (dataType == "float" and "f" or ((dataType == "signed" and "i" or "I") .. byteDepth)):rep(csize) | |
| local maxValue = 2^(bitDepth-1) | |
| local obj = setmetatable({sampleRate = sampleRate, data = {}}, Audio_mt) | |
| for i = 1, channels do obj.data[i] = {} end | |
| local pos, spos = 1, 1 | |
| local tmp = {} | |
| local read | |
| if type(data) == "table" then | |
| if dataType == "signed" then | |
| function read() | |
| local s = data[pos] | |
| pos = pos + 1 | |
| return s / (s < 0 and maxValue or maxValue-1) | |
| end | |
| elseif dataType == "unsigned" then | |
| function read() | |
| local s = data[pos] | |
| pos = pos + 1 | |
| return (s - 128) / (s < 128 and maxValue or maxValue-1) | |
| end | |
| else | |
| function read() | |
| local s = data[pos] | |
| pos = pos + 1 | |
| return s | |
| end | |
| end | |
| elseif dataType == "float" then | |
| function read() | |
| if pos > #tmp then | |
| if spos + (csize * byteDepth) > #data then | |
| local f = (bigEndian and ">" or "<") .. ("f"):rep((#data - spos + 1) / byteDepth) | |
| tmp = {f:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| else | |
| tmp = {format:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| end | |
| pos = 1 | |
| end | |
| local s = tmp[pos] | |
| pos = pos + 1 | |
| return s | |
| end | |
| elseif dataType == "signed" then | |
| function read() | |
| if pos > #tmp then | |
| if spos + (csize * byteDepth) > #data then | |
| local f = (bigEndian and ">" or "<") .. ("i" .. byteDepth):rep((#data - spos + 1) / byteDepth) | |
| tmp = {f:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| else | |
| tmp = {format:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| end | |
| pos = 1 | |
| end | |
| local s = tmp[pos] | |
| pos = pos + 1 | |
| return s / (s < 0 and maxValue or maxValue-1) | |
| end | |
| else -- unsigned | |
| function read() | |
| if pos > #tmp then | |
| if spos + (csize * byteDepth) > #data then | |
| local f = (bigEndian and ">" or "<") .. ("I" .. byteDepth):rep((#data - spos + 1) / byteDepth) | |
| tmp = {f:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| else | |
| tmp = {format:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| end | |
| pos = 1 | |
| end | |
| local s = tmp[pos] | |
| pos = pos + 1 | |
| return (s - 128) / (s < 128 and maxValue or maxValue-1) | |
| end | |
| end | |
| local start = os.epoch "utc" | |
| if interleaved and channels > 1 then | |
| local d = obj.data | |
| for i = 1, len do | |
| if os.epoch "utc" - start > 3000 then start = os.epoch "utc" sleep(0) end | |
| for j = 1, channels do d[j][i] = read() end | |
| end | |
| else for j = 1, channels do | |
| local line = {} | |
| obj.data[j] = line | |
| for i = 1, len do | |
| if os.epoch "utc" - start > 3000 then start = os.epoch "utc" sleep(0) end | |
| line[i] = read() | |
| end | |
| end end | |
| return obj | |
| end | |
| --- Creates a new audio object from IMA ADPCM data. | |
| -- @tparam string|table data The audio data, either as a raw string, or a table of nibbles | |
| -- @tparam[opt=1] number channels The number of channels present in the audio | |
| -- @tparam[opt=48000] number sampleRate The sample rate of the audio in Hertz | |
| -- @tparam[opt=true] boolean topFirst Whether the top nibble is the first nibble | |
| -- (true) or last (false); ignored if `data` is a table | |
| -- @tparam[opt=true] boolean interleaved Whether each channel is interleaved or separate | |
| -- @tparam[opt=0] number|table predictor The initial predictor value(s) | |
| -- @tparam[opt=0] number|table step_index The initial step index(es) | |
| -- @treturn Audio A new audio object containing the decoded data | |
| function aukit.adpcm(data, channels, sampleRate, topFirst, interleaved, predictor, step_index) | |
| expect(1, data, "string", "table") | |
| channels = expect(2, channels, "number", "nil") or 1 | |
| sampleRate = expect(3, sampleRate, "number", "nil") or 48000 | |
| expect(4, topFirst, "boolean", "nil") | |
| if topFirst == nil then topFirst = true end | |
| expect(5, interleaved, "boolean", "nil") | |
| if interleaved == nil then interleaved = true end | |
| predictor = expect(6, predictor, "number", "table", "nil") | |
| step_index = expect(7, step_index, "number", "table", "nil") | |
| expect.range(channels, 1) | |
| expect.range(sampleRate, 1) | |
| if predictor == nil then | |
| predictor = {} | |
| for i = 1, channels do predictor[i] = 0 end | |
| elseif type(predictor) == "number" then | |
| if channels ~= 1 then error("bad argument #6 (table too short)", 2) end | |
| predictor = {expect.range(predictor, -32768, 32767)} | |
| else | |
| if channels > #predictor then error("bad argument #6 (table too short)", 2) end | |
| for i = 1, channels do expect.range(predictor[i], -32768, 32767) end | |
| end | |
| if step_index == nil then | |
| step_index = {} | |
| for i = 1, channels do step_index[i] = 0 end | |
| elseif type(step_index) == "number" then | |
| if channels ~= 1 then error("bad argument #7 (table too short)", 2) end | |
| step_index = {expect.range(step_index, 0, 15)} | |
| else | |
| if channels > #step_index then error("bad argument #7 (table too short)", 2) end | |
| for i = 1, channels do expect.range(step_index[i], 0, 15) end | |
| end | |
| local pos = 1 | |
| local read, tmp, len | |
| if type(data) == "string" then | |
| function read() | |
| if tmp then | |
| local v = tmp | |
| tmp = nil | |
| return v | |
| else | |
| local b = data:byte(pos) | |
| pos = pos + 1 | |
| if topFirst then tmp, b = bit32.band(b, 0x0F), bit32.rshift(b, 4) | |
| else tmp, b = bit32.rshift(b, 4), bit32.band(b, 0x0F) end | |
| return b | |
| end | |
| end | |
| len = math.floor(#data * 2 / channels) | |
| else | |
| function read() | |
| local v = data[pos] | |
| pos = pos + 1 | |
| return v | |
| end | |
| len = #data / channels | |
| end | |
| local obj = setmetatable({sampleRate = sampleRate, data = {}}, Audio_mt) | |
| local step = {} | |
| local start = os.epoch "utc" | |
| if interleaved then | |
| local d = obj.data | |
| for i = 1, len do | |
| if os.epoch "utc" - start > 3000 then start = os.epoch "utc" sleep(0) end | |
| for j = 1, channels do | |
| local nibble = read() | |
| step_index[j] = clamp(step_index[j] + ima_index_table[nibble], 0, 88) | |
| local diff = ((nibble >= 8 and nibble - 16 or nibble) + 0.5) * step[j] / 4 | |
| predictor[j] = clamp(predictor[j] + diff, -32768, 32767) | |
| step[j] = ima_step_table[step_index] | |
| d[j][i] = predictor[j] / (predictor[j] < 0 and 32768 or 32767) | |
| end | |
| end | |
| else for j = 1, channels do | |
| local line = {} | |
| local predictor, step_index, step = predictor[j], step_index[j], nil | |
| for i = 1, len do | |
| if os.epoch "utc" - start > 3000 then start = os.epoch "utc" sleep(0) end | |
| local nibble = read() | |
| step_index = clamp(step_index + ima_index_table[nibble], 0, 88) | |
| local diff = ((nibble >= 8 and nibble - 16 or nibble) + 0.5) * step / 4 | |
| predictor = clamp(predictor + diff, -32768, 32767) | |
| step = ima_step_table[step_index] | |
| line[i] = predictor / (predictor < 0 and 32768 or 32767) | |
| end | |
| obj.data[j] = line | |
| end end | |
| return obj | |
| end | |
| --- Creates a new audio object from DFPWM1a data. All channels are expected to | |
| -- share the same decoder, and are stored uninterleaved sequentially. | |
| -- @tparam string data The audio data as a raw string | |
| -- @tparam[opt=1] number channels The number of channels present in the audio | |
| -- @tparam[opt=48000] number sampleRate The sample rate of the audio in Hertz | |
| -- @treturn Audio A new audio object containing the decoded data | |
| function aukit.dfpwm(data, channels, sampleRate) | |
| expect(1, data, "string") | |
| channels = expect(2, channels, "number", "nil") or 1 | |
| sampleRate = expect(3, sampleRate, "number", "nil") or 48000 | |
| expect.range(channels, 1) | |
| expect.range(sampleRate, 1) | |
| if #data % channels ~= 0 then error("bad argument #1 (uneven amount of data per channel)", 2) end | |
| local audio = {} | |
| local decoder = dfpwm.make_decoder() | |
| local pos = 1 | |
| local last = 0 | |
| local start = os.epoch "utc" | |
| while pos <= #data do | |
| if os.epoch "utc" - start > 3000 then start = os.epoch "utc" sleep(0) end | |
| local temp = decoder(data:sub(pos, pos + 6000)) | |
| if temp == nil or #temp == 0 then break end | |
| for i=1,#temp do | |
| audio[last+i] = temp[i] | |
| end | |
| last = last + #temp | |
| pos = pos + 6000 | |
| end | |
| return aukit.pcm(audio, 8, "signed", channels, sampleRate, false, false) | |
| end | |
| --- Creates a new audio object from a WAV file. | |
| -- @tparam string data The WAV data to load | |
| -- @treturn Audio A new audio object with the contents of the WAV file | |
| function aukit.wav(data) | |
| -- TODO: add float support | |
| expect(1, data, "string") | |
| local channels, sampleRate, bitDepth, length | |
| local temp, pos = ("c4"):unpack(data) | |
| if temp ~= "RIFF" then error("bad argument #1 (not a WAV file)", 2) end | |
| pos = pos + 4 | |
| temp, pos = ("c4"):unpack(data, pos) | |
| if temp ~= "WAVE" then error("bad argument #1 (not a WAV file)", 2) end | |
| while pos <= #data do | |
| local size | |
| temp, pos = ("c4"):unpack(data, pos) | |
| size, pos = ("<I"):unpack(data, pos) | |
| if temp == "fmt " then | |
| if size ~= 16 then error("unsupported WAV file", 2) end | |
| temp, pos = ("<H"):unpack(data, pos) | |
| if temp ~= 1 then error("unsupported WAV file", 2) end | |
| channels, sampleRate, pos = ("<HI"):unpack(data, pos) | |
| pos = pos + 6 | |
| bitDepth, pos = ("<H"):unpack(data, pos) | |
| elseif temp == "data" then | |
| local data = data:sub(pos, pos + size - 1) | |
| if #data < size then error("invalid WAV file", 2) end | |
| return aukit.pcm(data, bitDepth, bitDepth == 8 and "unsigned" or "signed", channels, sampleRate, true, false) | |
| elseif temp == "fact" then | |
| -- TODO | |
| pos = pos + size | |
| else pos = pos + size end | |
| end | |
| error("invalid WAV file", 2) | |
| end | |
| --- Creates a new audio object from an AIFF file. | |
| -- @tparam string data The AIFF data to load | |
| -- @treturn Audio A new audio object with the contents of the AIFF file | |
| function aukit.aiff(data) | |
| expect(1, data, "string") | |
| local channels, sampleRate, bitDepth, length, offset | |
| local temp, pos = ("c4"):unpack(data) | |
| if temp ~= "FORM" then error("bad argument #1 (not an AIFF file)", 2) end | |
| pos = pos + 4 | |
| temp, pos = ("c4"):unpack(data, pos) | |
| if temp ~= "AIFF" then error("bad argument #1 (not an AIFF file)", 2) end | |
| while pos <= #data do | |
| local size | |
| temp, pos = ("c4"):unpack(data, pos) | |
| size, pos = (">I"):unpack(data, pos) | |
| if temp == "COMM" then | |
| local e, m | |
| channels, length, bitDepth, e, m, pos = (">hIhHI7x"):unpack(data, pos) | |
| length = length * channels * math.floor(bitDepth / 8) | |
| local s = bit32.btest(e, 0x8000) | |
| e = ((bit32.band(e, 0x7FFF) - 0x3FFE) % 0x800) | |
| sampleRate = math.ldexp(m * (s and -1 or 1) / 0x100000000000000, e) | |
| elseif temp == "SSND" then | |
| offset, _, pos = (">II"):unpack(data, pos) | |
| local data = data:sub(pos + offset, pos + offset + length - 1) | |
| if #data < length then error("invalid AIFF file", 2) end | |
| return aukit.pcm(data, bitDepth, "signed", channels, sampleRate, true, true) | |
| else pos = pos + size end | |
| end | |
| error("invalid AIFF file", 2) | |
| end | |
| --- Creates a new audio object from an AU file. | |
| -- @tparam string data The AU data to load | |
| -- @treturn Audio A new audio object with the contents of the AU file | |
| function aukit.au(data) | |
| expect(1, data, "string") | |
| local magic, offset, size, encoding, sampleRate, channels = (">c4IIIII"):unpack(data) | |
| if magic ~= ".snd" then error("invalid AU file", 2) end | |
| if encoding == 2 then return aukit.pcm(data:sub(offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 8, "signed", channels, sampleRate, true, true) | |
| elseif encoding == 3 then return aukit.pcm(data:sub(offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 16, "signed", channels, sampleRate, true, true) | |
| elseif encoding == 4 then return aukit.pcm(data:sub(offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 24, "signed", channels, sampleRate, true, true) | |
| elseif encoding == 5 then return aukit.pcm(data:sub(offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 32, "signed", channels, sampleRate, true, true) | |
| elseif encoding == 6 then return aukit.pcm(data:sub(offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 32, "float", channels, sampleRate, true, true) | |
| else error("unsupported encoding type " .. encoding, 2) end | |
| end | |
| --- Creates a new audio object from a FLAC file. | |
| -- @tparam string data The FLAC data to load | |
| -- @treturn Audio A new audio object with the contents of the FLAC file | |
| function aukit.flac(data) | |
| expect(1, data, "string") | |
| return setmetatable(decodeFLAC(data), Audio_mt) | |
| end | |
| --- Creates a new empty audio object with the specified duration. | |
| -- @tparam number duration The length of the audio in seconds | |
| -- @tparam[opt=1] number channels The number of channels present in the audio | |
| -- @tparam[opt=48000] number sampleRate The sample rate of the audio in Hertz | |
| -- @treturn Audio The new empty audio object | |
| function aukit.new(duration, channels, sampleRate) | |
| expect(1, duration, "number") | |
| channels = expect(2, channels, "number", "nil") or 1 | |
| sampleRate = expect(3, sampleRate, "number", "nil") or 48000 | |
| expect.range(channels, 1) | |
| expect.range(sampleRate, 1) | |
| local obj = setmetatable({sampleRate = sampleRate, data = {}}, Audio_mt) | |
| for c = 1, channels do | |
| local l = {} | |
| for i = 1, duration * sampleRate do l[i] = 0 end | |
| obj.data[c] = l | |
| end | |
| return obj | |
| end | |
| --- Creates a new audio object with a tone of the specified frequency and duration. | |
| -- @tparam number frequency The frequency of the tone in Hertz | |
| -- @tparam number duration The length of the audio in seconds | |
| -- @tparam[opt=1] number amplitude The amplitude of the audio from 0.0 to 1.0 | |
| -- @tparam[opt="sine"] "sine"|"triangle"|"sawtooth"|"square" waveType The type of wave to generate | |
| -- @tparam[opt=0.5] number duty The duty cycle of the square wave if selected; ignored otherwise | |
| -- @tparam[opt=1] number channels The number of channels present in the audio | |
| -- @tparam[opt=48000] number sampleRate The sample rate of the audio in Hertz | |
| -- @treturn Audio A new audio object with the tone | |
| function aukit.tone(frequency, duration, amplitude, waveType, duty, channels, sampleRate) | |
| expect(1, frequency, "number") | |
| expect(2, duration, "number") | |
| amplitude = expect(3, amplitude, "number", "nil") or 1 | |
| waveType = expect(4, waveType, "string", "nil") or "sine" | |
| duty = expect(5, duty, "number", "nil") or 0.5 | |
| channels = expect(6, channels, "number", "nil") or 1 | |
| sampleRate = expect(7, sampleRate, "number", "nil") or 48000 | |
| expect.range(amplitude, 0, 1) | |
| local f = wavegen[waveType] | |
| if not f then error("bad argument #4 (invalid wave type)", 2) end | |
| expect.range(duty, 0, 1) | |
| expect.range(channels, 1) | |
| expect.range(sampleRate, 1) | |
| local obj = setmetatable({sampleRate = sampleRate, data = {}}, Audio_mt) | |
| for c = 1, channels do | |
| local l = {} | |
| for i = 1, duration * sampleRate do l[i] = f(i / sampleRate, frequency, amplitude, duty) end | |
| obj.data[c] = l | |
| end | |
| return obj | |
| end | |
| --- Creates a new audio object with white noise for the specified duration. | |
| -- @tparam number duration The length of the audio in seconds | |
| -- @tparam[opt=1] number amplitude The amplitude of the audio from 0.0 to 1.0 | |
| -- @tparam[opt=1] number channels The number of channels present in the audio | |
| -- @tparam[opt=48000] number sampleRate The sample rate of the audio in Hertz | |
| -- @treturn Audio A new audio object with noise | |
| function aukit.noise(duration, amplitude, channels, sampleRate) | |
| expect(1, duration, "number") | |
| amplitude = expect(2, amplitude, "number", "nil") or 1 | |
| channels = expect(3, channels, "number", "nil") or 1 | |
| sampleRate = expect(4, sampleRate, "number", "nil") or 48000 | |
| expect.range(amplitude, 0, 1) | |
| expect.range(channels, 1) | |
| expect.range(sampleRate, 1) | |
| local obj = setmetatable({sampleRate = sampleRate, data = {}}, Audio_mt) | |
| for c = 1, channels do | |
| local l = {} | |
| for i = 1, duration * sampleRate do l[i] = (math.random() * 2 - 1) * amplitude end | |
| obj.data[c] = l | |
| end | |
| return obj | |
| end | |
| --- Packs a table with PCM data into a string using the specified data type. | |
| -- @tparam {[number]...} data The PCM data to pack | |
| -- @tparam[opt=8] number bitDepth The bit depth of the audio (8, 16, 24, 32); if `dataType` is "float" then this must be 32 | |
| -- @tparam[opt="signed"] "signed"|"unsigned"|"float" dataType The type of each sample | |
| -- @tparam[opt=false] boolean bigEndian Whether the data should be big-endian or little-endian | |
| -- @treturn string The packed PCM data | |
| function aukit.pack(data, bitDepth, dataType, bigEndian) | |
| expect(1, data, "string", "table") | |
| bitDepth = expect(2, bitDepth, "number", "nil") or 8 | |
| dataType = expect(3, dataType, "string", "nil") or "signed" | |
| expect(4, bigEndian, "boolean", "nil") | |
| if bitDepth ~= 8 and bitDepth ~= 16 and bitDepth ~= 24 and bitDepth ~= 32 then error("bad argument #2 (invalid bit depth)", 2) end | |
| if dataType ~= "signed" and dataType ~= "unsigned" and dataType ~= "float" then error("bad argument #3 (invalid data type)", 2) end | |
| if dataType == "float" and bitDepth ~= 32 then error("bad argument #2 (float audio must have 32-bit depth)", 2) end | |
| local byteDepth = bitDepth / 8 | |
| local format = (bigEndian and ">" or "<") .. (dataType == "float" and "f" or ((dataType == "signed" and "i" or "I") .. byteDepth)) | |
| local formatChunk = format:sub(1, 1) .. format:sub(2):rep(512) | |
| local retval = "" | |
| for i = 1, #data, 512 do | |
| if #data < i + 512 then retval = retval .. format:rep(#data % 512):pack(table.unpack(data, i, #data)) | |
| else retval = retval .. formatChunk:pack(table.unpack(data, i, i+511)) end | |
| end | |
| return retval | |
| end | |
| --- Plays back stream functions created by one of the @{aukit.stream} functions | |
| -- or @{Audio:stream}. | |
| -- @tparam function():{{[number]...}...} callback The iterator function that returns each chunk | |
| -- @tparam speaker ... The speakers to play on | |
| function aukit.play(callback, ...) | |
| expect(1, callback, "function") | |
| local speakers = {...} | |
| local chunks = {} | |
| local complete = false | |
| parallel.waitForAll(function() | |
| for chunk in callback do chunks[#chunks+1] = chunk sleep(0) end | |
| complete = true | |
| end, function() | |
| while not complete or #chunks > 0 do | |
| while not chunks[1] do sleep(0) end | |
| local chunk = table.remove(chunks, 1) | |
| local fn = {} | |
| for i, v in ipairs(speakers) do fn[i] = function() | |
| local name = peripheral.getName(v) | |
| if config and not config.get("standardsMode") then | |
| v.playAudio(chunk[i] or chunk[1], 3) | |
| repeat until select(2, os.pullEvent("speaker_audio_empty")) == name | |
| else while not v.playAudio(chunk[i] or chunk[1]) do | |
| repeat until select(2, os.pullEvent("speaker_audio_empty")) == name | |
| end end | |
| end end | |
| parallel.waitForAll(table.unpack(fn)) | |
| end | |
| end) | |
| end | |
| --- aukit.stream | |
| -- @section aukit.stream | |
| --- Returns an iterator to stream raw PCM data in CC format. Audio will automatically | |
| -- be resampled to 48 kHz, and optionally mixed down to mono. Data *must* be | |
| -- interleaved - this will not work with planar audio. | |
| -- @tparam string|table data The audio data, either as a raw string, or a table | |
| -- of values (in the format specified by `bitDepth` and `dataType`) | |
| -- @tparam[opt=8] number bitDepth The bit depth of the audio (8, 16, 24, 32); if `dataType` is "float" then this must be 32 | |
| -- @tparam[opt="signed"] "signed"|"unsigned"|"float" dataType The type of each sample | |
| -- @tparam[opt=1] number channels The number of channels present in the audio | |
| -- @tparam[opt=48000] number sampleRate The sample rate of the audio in Hertz | |
| -- @tparam[opt=false] boolean bigEndian Whether the audio is big-endian or little-endian; ignored if data is a table | |
| -- @tparam[opt=false] boolean mono Whether to mix the audio down to mono | |
| -- @treturn function():{{[number]...}...},number An iterator function that returns | |
| -- chunks of each channel's data as arrays of signed 8-bit 48kHz PCM, as well as | |
| -- the current position of the audio in seconds | |
| -- @treturn number The total length of the audio in seconds | |
| function aukit.stream.pcm(data, bitDepth, dataType, channels, sampleRate, bigEndian, mono) | |
| expect(1, data, "string", "table") | |
| bitDepth = expect(2, bitDepth, "number", "nil") or 8 | |
| dataType = expect(3, dataType, "string", "nil") or "signed" | |
| channels = expect(4, channels, "number", "nil") or 1 | |
| sampleRate = expect(5, sampleRate, "number", "nil") or 48000 | |
| expect(6, bigEndian, "boolean", "nil") | |
| expect(7, mono, "boolean", "nil") | |
| if bitDepth ~= 8 and bitDepth ~= 16 and bitDepth ~= 24 and bitDepth ~= 32 then error("bad argument #2 (invalid bit depth)", 2) end | |
| if dataType ~= "signed" and dataType ~= "unsigned" and dataType ~= "float" then error("bad argument #3 (invalid data type)", 2) end | |
| if dataType == "float" and bitDepth ~= 32 then error("bad argument #2 (float audio must have 32-bit depth)", 2) end | |
| expect.range(channels, 1) | |
| expect.range(sampleRate, 1) | |
| local byteDepth = bitDepth / 8 | |
| local len = (#data / (type(data) == "table" and 1 or byteDepth)) / channels | |
| local csize = jit and 7680 or 32768 | |
| local format = (bigEndian and ">" or "<") .. (dataType == "float" and "f" or ((dataType == "signed" and "i" or "I") .. byteDepth)):rep(csize) | |
| local maxValue = 2^(bitDepth-1) | |
| local pos, spos = 1, 1 | |
| local tmp = {} | |
| local read | |
| if type(data) == "table" then | |
| if dataType == "signed" then | |
| function read() | |
| local s = data[pos] | |
| pos = pos + 1 | |
| return s / (s < 0 and maxValue or maxValue-1) | |
| end | |
| elseif dataType == "unsigned" then | |
| function read() | |
| local s = data[pos] | |
| pos = pos + 1 | |
| return (s - 128) / (s < 128 and maxValue or maxValue-1) | |
| end | |
| else | |
| function read() | |
| local s = data[pos] | |
| pos = pos + 1 | |
| return s | |
| end | |
| end | |
| elseif dataType == "float" then | |
| function read() | |
| if pos > #tmp then | |
| if spos + (csize * byteDepth) > #data then | |
| local f = (bigEndian and ">" or "<") .. ("f"):rep((#data - spos + 1) / byteDepth) | |
| tmp = {f:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| else | |
| tmp = {format:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| end | |
| pos = 1 | |
| end | |
| local s = tmp[pos] | |
| pos = pos + 1 | |
| return s | |
| end | |
| elseif dataType == "signed" then | |
| function read() | |
| if pos > #tmp then | |
| if spos + (csize * byteDepth) > #data then | |
| local f = (bigEndian and ">" or "<") .. ("i" .. byteDepth):rep((#data - spos + 1) / byteDepth) | |
| tmp = {f:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| else | |
| tmp = {format:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| end | |
| pos = 1 | |
| end | |
| local s = tmp[pos] | |
| pos = pos + 1 | |
| return s / (s < 0 and maxValue or maxValue-1) | |
| end | |
| else -- unsigned | |
| function read() | |
| if pos > #tmp then | |
| if spos + (csize * byteDepth) > #data then | |
| local f = (bigEndian and ">" or "<") .. ("I" .. byteDepth):rep((#data - spos + 1) / byteDepth) | |
| tmp = {f:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| else | |
| tmp = {format:unpack(data, spos)} | |
| spos = tmp[#tmp] | |
| tmp[#tmp] = nil | |
| end | |
| pos = 1 | |
| end | |
| local s = tmp[pos] | |
| pos = pos + 1 | |
| return (s - 128) / (s < 128 and maxValue or maxValue-1) | |
| end | |
| end | |
| local d = {} | |
| local ratio = 48000 / sampleRate | |
| local interp = interpolate[aukit.defaultInterpolation] | |
| for j = 1, (mono and 1 or channels) do d[j] = setmetatable({}, {__index = function(self, i) | |
| if mono then for _ = 1, channels do self[i] = (rawget(self, i) or 0) + read() end self[i] = self[i] / channels | |
| else self[i] = read() end | |
| return rawget(self, i) | |
| end}) end | |
| local n = 0 | |
| local ok = true | |
| return function() | |
| if not ok then return nil end | |
| for i = (n == 0 and interpolation_start[aukit.defaultInterpolation] or 1), interpolation_end[aukit.defaultInterpolation] do | |
| if mono then | |
| local s = 0 | |
| for j = 1, channels do | |
| local c = read() | |
| if not c then return nil end | |
| s = s + c | |
| end | |
| d[1][i] = s / channels | |
| else for j = 1, channels do d[j][i] = read() if not d[j][i] then return nil end end end | |
| end | |
| local chunk = {} | |
| for j = 1, #d do chunk[j] = {} end | |
| ok = pcall(function() | |
| for i = 1, 48000 do | |
| for y = 1, #d do | |
| local x = ((n * 48000 + i - 1) / ratio) + 1 | |
| if x % 1 == 0 then chunk[y][i] = d[y][x] | |
| else chunk[y][i] = interp(d[y], x) end | |
| chunk[y][i] = clamp(chunk[y][i] * (chunk[y][i] < 0 and 128 or 127), -128, 127) | |
| end | |
| end | |
| end) | |
| if #chunk[1] == 0 then return nil end | |
| n = n + 1 | |
| return chunk, n - 1 | |
| end, len / sampleRate | |
| end | |
| --- Returns an iterator to stream data from DFPWM data. Audio will automatically | |
| -- be resampled to 48 kHz. This only supports mono audio. | |
| -- @tparam string data The DFPWM data to decode | |
| -- @tparam[opt=48000] number sampleRate The sample rate of the audio in Hertz | |
| -- @treturn function():{{[number]...}...},number An iterator function that | |
| -- returns chunks of the only channel's data as arrays of signed 8-bit 48kHz PCM, | |
| -- as well as the current position of the audio in seconds | |
| -- @treturn number The total length of the audio in seconds | |
| function aukit.stream.dfpwm(data, sampleRate) | |
| expect(1, data, "string") | |
| sampleRate = expect(2, sampleRate, "number", "nil") or 48000 | |
| expect.range(sampleRate, 1) | |
| local decoder = dfpwm.make_decoder() | |
| local pos = 1 | |
| local last = 0 | |
| return function() | |
| if pos > #data then return nil end | |
| local audio = decoder(data:sub(pos, pos + 6000)) | |
| if audio == nil or #audio == 0 then return nil end | |
| audio[0], last = last, audio[#audio] | |
| sleep(0) | |
| local ratio = 48000 / sampleRate | |
| local newlen = #audio * ratio | |
| local interp = interpolate[aukit.defaultInterpolation] | |
| local line = {} | |
| for i = 1, newlen do | |
| local x = (i - 1) / ratio + 1 | |
| if x % 1 == 0 then line[i] = audio[x] | |
| else line[i] = clamp(interp(audio, x), -128, 127) end | |
| end | |
| sleep(0) | |
| local p = pos | |
| pos = pos + 6000 | |
| return {line}, p * 8 / sampleRate | |
| end, #data * 8 / sampleRate | |
| end | |
| --- Returns an iterator to stream data from a WAV file. Audio will automatically | |
| -- be resampled to 48 kHz, and optionally mixed down to mono. | |
| -- @tparam string data The WAV file to decode | |
| -- @tparam[opt=false] boolean mono Whether to mix the audio to mono | |
| -- @treturn function():{{[number]...}...},number An iterator function that returns | |
| -- chunks of each channel's data as arrays of signed 8-bit 48kHz PCM, as well as | |
| -- the current position of the audio in seconds | |
| -- @treturn number The total length of the audio in seconds | |
| function aukit.stream.wav(data, mono) | |
| expect(1, data, "string") | |
| local channels, sampleRate, bitDepth, length | |
| local temp, pos = ("c4"):unpack(data) | |
| if temp ~= "RIFF" then error("bad argument #1 (not a WAV file)", 2) end | |
| pos = pos + 4 | |
| temp, pos = ("c4"):unpack(data, pos) | |
| if temp ~= "WAVE" then error("bad argument #1 (not a WAV file)", 2) end | |
| while pos <= #data do | |
| local size | |
| temp, pos = ("c4"):unpack(data, pos) | |
| size, pos = ("<I"):unpack(data, pos) | |
| if temp == "fmt " then | |
| if size ~= 16 then error("unsupported WAV file", 2) end | |
| temp, pos = ("<H"):unpack(data, pos) | |
| if temp ~= 1 then error("unsupported WAV file", 2) end | |
| channels, sampleRate, pos = ("<HI"):unpack(data, pos) | |
| pos = pos + 6 | |
| bitDepth, pos = ("<H"):unpack(data, pos) | |
| elseif temp == "data" then | |
| local data = data:sub(pos, pos + size - 1) | |
| if #data < size then error("invalid WAV file", 2) end | |
| return aukit.stream.pcm(data, bitDepth, bitDepth == 8 and "unsigned" or "signed", channels, sampleRate, false, mono) | |
| elseif temp == "fact" then | |
| -- TODO | |
| pos = pos + size | |
| else pos = pos + size end | |
| end | |
| error("invalid WAV file", 2) | |
| end | |
| --- Returns an iterator to stream data from an AIFF file. Audio will automatically | |
| -- be resampled to 48 kHz, and optionally mixed down to mono. | |
| -- @tparam string data The AIFF file to decode | |
| -- @tparam[opt=false] boolean mono Whether to mix the audio to mono | |
| -- @treturn function():{{[number]...}...},number An iterator function that returns | |
| -- chunks of each channel's data as arrays of signed 8-bit 48kHz PCM, as well as | |
| -- the current position of the audio in seconds | |
| -- @treturn number The total length of the audio in seconds | |
| function aukit.stream.aiff(data, mono) | |
| expect(1, data, "string") | |
| expect(2, mono, "boolean", "nil") | |
| local channels, sampleRate, bitDepth, length, offset | |
| local temp, pos = ("c4"):unpack(data) | |
| if temp ~= "FORM" then error("bad argument #1 (not an AIFF file)", 2) end | |
| pos = pos + 4 | |
| temp, pos = ("c4"):unpack(data, pos) | |
| if temp ~= "AIFF" then error("bad argument #1 (not an AIFF file)", 2) end | |
| while pos <= #data do | |
| local size | |
| temp, pos = ("c4"):unpack(data, pos) | |
| size, pos = (">I"):unpack(data, pos) | |
| if temp == "COMM" then | |
| local e, m | |
| channels, length, bitDepth, e, m, pos = (">hIhHI7x"):unpack(data, pos) | |
| length = length * channels * math.floor(bitDepth / 8) | |
| local s = bit32.btest(e, 0x8000) | |
| e = ((bit32.band(e, 0x7FFF) - 0x3FFE) % 0x800) | |
| sampleRate = math.ldexp(m * (s and -1 or 1) / 0x100000000000000, e) | |
| elseif temp == "SSND" then | |
| offset, _, pos = (">II"):unpack(data, pos) | |
| local data = data:sub(pos + offset, pos + offset + length - 1) | |
| if #data < length then error("invalid AIFF file", 2) end | |
| return aukit.stream.pcm(data, bitDepth, "signed", channels, sampleRate, true, mono) | |
| else pos = pos + size end | |
| end | |
| error("invalid AIFF file", 2) | |
| end | |
| --- Returns an iterator to stream data from an AU file. Audio will automatically | |
| -- be resampled to 48 kHz, and optionally mixed down to mono. | |
| -- @tparam string data The AU file to decode | |
| -- @tparam[opt=false] boolean mono Whether to mix the audio to mono | |
| -- @treturn function():{{[number]...}...},number An iterator function that returns | |
| -- chunks of each channel's data as arrays of signed 8-bit 48kHz PCM, as well as | |
| -- the current position of the audio in seconds | |
| -- @treturn number The total length of the audio in seconds | |
| function aukit.stream.au(data, mono) | |
| expect(1, data, "string") | |
| expect(2, mono, "boolean", "nil") | |
| local magic, offset, size, encoding, sampleRate, channels = (">c4IIIII"):unpack(data) | |
| if magic ~= ".snd" then error("invalid AU file", 2) end | |
| if encoding == 2 then return aukit.stream.pcm(data:sub(offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 8, "signed", channels, sampleRate, true, mono) | |
| elseif encoding == 3 then return aukit.stream.pcm(data:sub(offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 16, "signed", channels, sampleRate, true, mono) | |
| elseif encoding == 4 then return aukit.stream.pcm(data:sub(offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 24, "signed", channels, sampleRate, true, mono) | |
| elseif encoding == 5 then return aukit.stream.pcm(data:sub(offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 32, "signed", channels, sampleRate, true, mono) | |
| elseif encoding == 6 then return aukit.stream.pcm(data:sub(offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 32, "float", channels, sampleRate, true, mono) | |
| else error("unsupported encoding type " .. encoding, 2) end | |
| end | |
| --- Returns an iterator to stream data from a FLAC file. Audio will automatically | |
| -- be resampled to 48 kHz, and optionally mixed down to mono. | |
| -- @tparam string data The FLAC file to decode | |
| -- @tparam[opt=false] boolean mono Whether to mix the audio to mono | |
| -- @treturn function():{{[number]...}...},number An iterator function that returns | |
| -- chunks of each channel's data as arrays of signed 8-bit 48kHz PCM, as well as | |
| -- the current position of the audio in seconds | |
| -- @treturn number The total length of the audio in seconds | |
| function aukit.stream.flac(data, mono) | |
| expect(1, data, "string") | |
| expect(2, mono, "boolean", "nil") | |
| local coro = coroutine.create(decodeFLAC) | |
| local _, sampleRate, len = coroutine.resume(coro, data, coroutine.yield) | |
| local pos = 0 | |
| return function() | |
| if coroutine.status(coro) == "dead" then return nil end | |
| local chunk = {{}} | |
| while #chunk[1] < sampleRate do | |
| local ok, res = coroutine.resume(coro) | |
| if not ok or res == nil or res.sampleRate then break end | |
| sleep(0) | |
| for c = 1, #res do | |
| chunk[c] = chunk[c] or {} | |
| local src, dest = res[c], chunk[c] | |
| local start = #dest | |
| for i = 1, #res[c] do dest[start+i] = src[i] end | |
| end | |
| sleep(0) | |
| end | |
| local audio = setmetatable({sampleRate = sampleRate, data = chunk}, Audio_mt) | |
| if mono and #chunk > 1 then audio = audio:mono() end | |
| sleep(0) | |
| audio = audio:resample(48000) | |
| sleep(0) | |
| chunk = {} | |
| for c = 1, #audio.data do | |
| local src, dest = audio.data[c], {} | |
| for i = 1, #src do dest[i] = src[i] * (src[i] < 0 and 128 or 127) end | |
| chunk[c] = dest | |
| end | |
| local p = pos | |
| pos = pos + #audio.data[1] / 48000 | |
| return chunk, p | |
| end, len / sampleRate | |
| end | |
| --- aukit.effects | |
| -- @section aukit.effects | |
| --- Amplifies the audio by the multiplier specified. | |
| -- @tparam Audio audio The audio to modify | |
| -- @tparam number multiplier The multiplier to apply | |
| -- @treturn Audio The audio modified | |
| function aukit.effects.amplify(audio, multiplier) | |
| expectAudio(1, audio) | |
| expect(2, multiplier, "number") | |
| if multiplier == 1 then return audio end | |
| local start = os.epoch "utc" | |
| for c = 1, #audio.data do | |
| local ch = audio.data[c] | |
| for i = 1, #ch do | |
| if os.epoch "utc" - start > 3000 then start = os.epoch "utc" sleep(0) end | |
| ch[i] = clamp(ch[i] * multiplier, -1, 1) | |
| end | |
| end | |
| return audio | |
| end | |
| --- Changes the speed and pitch of audio by a multiplier, resampling to keep the | |
| -- same sample rate. | |
| -- @tparam Audio audio The audio to modify | |
| -- @tparam number multiplier The multiplier to apply | |
| -- @treturn Audio The audio modified | |
| function aukit.effects.speed(audio, multiplier) | |
| expectAudio(1, audio) | |
| expect(2, multiplier, "number") | |
| if multiplier == 1 then return audio end | |
| local rate = audio.sampleRate | |
| audio.sampleRate = audio.sampleRate * multiplier | |
| local new = audio:resample(rate) | |
| audio.sampleRate, audio.data = rate, new.data | |
| return audio | |
| end | |
| --- Fades a period of music from one amplitude to another. | |
| -- @tparam Audio audio The audio to modify | |
| -- @tparam number startTime The start time of the fade, in seconds | |
| -- @tparam number startAmplitude The amplitude of the beginning of the fade | |
| -- @tparam number endTime The end time of the fade, in seconds | |
| -- @tparam number endAmplitude The amplitude of the end of the fade | |
| -- @treturn Audio The audio modified | |
| function aukit.effects.fade(audio, startTime, startAmplitude, endTime, endAmplitude) | |
| expectAudio(1, audio) | |
| expect(2, startTime, "number") | |
| expect(3, startAmplitude, "number") | |
| expect(4, endTime, "number") | |
| expect(5, endAmplitude, "number") | |
| if startAmplitude == 1 and endAmplitude == 1 then return audio end | |
| local startt = os.epoch "utc" | |
| for c = 1, #audio.data do | |
| local ch = audio.data[c] | |
| local start = startTime * audio.sampleRate | |
| local m = (endAmplitude - startAmplitude) / ((endTime - startTime) * audio.sampleRate) | |
| for i = start, endTime * audio.sampleRate do | |
| if os.epoch "utc" - startt > 5000 then startt = os.epoch "utc" sleep(0) end | |
| ch[i] = clamp(ch[i] * (m * (i - start) + startAmplitude), -1, 1) | |
| end | |
| end | |
| return audio | |
| end | |
| --- Inverts all channels in the specified audio. | |
| -- @tparam Audio audio The audio to modify | |
| -- @treturn Audio The audio modified | |
| function aukit.effects.invert(audio) | |
| expectAudio(1, audio) | |
| for c = 1, #audio.data do | |
| local ch = audio.data[c] | |
| for i = 1, #ch do ch[i] = -ch[i] end | |
| end | |
| return audio | |
| end | |
| --- Normalizes audio to the specified peak amplitude. | |
| -- @tparam Audio audio The audio to modify | |
| -- @tparam[opt=1] number peakAmplitude The maximum amplitude | |
| -- @tparam[opt=false] boolean independent Whether to normalize each channel independently | |
| -- @treturn Audio The audio modified | |
| function aukit.effects.normalize(audio, peakAmplitude, independent) | |
| expectAudio(1, audio) | |
| peakAmplitude = expect(2, peakAmplitude, "number", "nil") or 1 | |
| expect(3, independent, "boolean", "nil") | |
| local mult | |
| local start, sampleRate = os.epoch "utc", audio.sampleRate | |
| if not independent then | |
| local max = 0 | |
| for c = 1, #audio.data do | |
| local ch = audio.data[c] | |
| if os.epoch "utc" - start > 3000 then start = os.epoch "utc" sleep(0) end | |
| for i = 1, #ch do max = math.max(max, math.abs(ch[i])) end | |
| end | |
| mult = peakAmplitude / max | |
| end | |
| for c = 1, #audio.data do | |
| local ch = audio.data[c] | |
| if independent then | |
| local max = 0 | |
| for i = 1, #ch do max = math.max(max, math.abs(ch[i])) end | |
| mult = peakAmplitude / max | |
| end | |
| if os.epoch "utc" - start > 3000 then start = os.epoch "utc" sleep(0) end | |
| for i = 1, #ch do | |
| ch[i] = clamp(ch[i] * mult, -1, 1) | |
| end | |
| end | |
| return audio | |
| end | |
| --- Centers the DC offset of each channel. | |
| -- @tparam Audio audio The audio to modify | |
| -- @treturn Audio The audio modified | |
| function aukit.effects.center(audio) | |
| expectAudio(1, audio) | |
| for c = 1, #audio.data do | |
| local ch = audio.data[c] | |
| for i = 0, #ch - 1, audio.sampleRate do | |
| local avg = 0 | |
| local l = math.min(#ch - i, audio.sampleRate) | |
| for j = 1, l do avg = avg + ch[i+j] end | |
| avg = avg / l | |
| for j = 1, l do ch[i+j] = clamp(ch[i+j] - avg, -1, 1) end | |
| end | |
| end | |
| return audio | |
| end | |
| --- Trims any extra silence on either end of the specified audio. | |
| -- @tparam Audio audio The audio to modify | |
| -- @tparam[opt=1/65536] number threshold The maximum value to register as silence | |
| -- @treturn Audio The audio modified | |
| function aukit.effects.trim(audio, threshold) | |
| expectAudio(1, audio) | |
| threshold = expect(2, threshold, "number", "nil") or (1/65536) | |
| local s, e | |
| for i = 1, #audio.data[1] do | |
| for c = 1, #audio.data do if math.abs(audio.data[c][i]) > threshold then s = i break end end | |
| if s then break end | |
| end | |
| for i = #audio.data[1], 1, -1 do | |
| for c = 1, #audio.data do if math.abs(audio.data[c][i]) > threshold then e = i break end end | |
| if e then break end | |
| end | |
| local new = audio:sub(s / audio.sampleRate, e / audio.sampleRate) | |
| audio.data = new.data | |
| return audio | |
| end | |
| return aukit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| local aukit = require "aukit" | |
| local speaker = peripheral.find "speaker" | |
| local path = shell.resolve(...) | |
| local file = fs.open(path, "rb") | |
| local data = file.readAll() | |
| file.close() | |
| print("Loading file...") | |
| local audio | |
| if path:match("%.dfpwm$") then audio = aukit.dfpwm(data, 1, 48000) | |
| elseif path:match("%.wav$") then audio = aukit.wav(data) | |
| elseif path:match("%.aiff?$") then audio = aukit.aiff(data) | |
| elseif path:match("%.au$") then audio = aukit.au(data) | |
| elseif path:match("%.flac$") then audio = aukit.flac(data) | |
| else error("Unknown file type!") end | |
| sleep(0) | |
| print("Resampling...") | |
| local resamp = audio:resample(48000) | |
| sleep(0) | |
| print("Converting to mono...") | |
| local mono = resamp:mono() | |
| sleep(0) | |
| print("Normalizing...") | |
| local normal = aukit.effects.normalize(mono, 0.8) | |
| sleep(0) | |
| print("Playing.") | |
| aukit.play(normal:stream(48000), speaker) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| local aukit = require "aukit" | |
| local path = ... | |
| local data | |
| if path:match("^https?://") then | |
| local handle, err = http.get(path) | |
| if not handle then error("Could not connect to " .. path .. ": " .. err) end | |
| local code = handle.getResponseCode() | |
| if code ~= 200 then handle.close() error("Could not connect to " .. path .. ": HTTP " .. code) end | |
| data = handle.readAll() | |
| handle.close() | |
| elseif path:match("^wss?://") then | |
| local handle, err = http.websocket(path) | |
| if not handle then error("Could not connect to " .. path .. ": " .. err) end | |
| data = "" | |
| repeat | |
| local ev, url, msg, bin = os.pullEvent() | |
| if ev == "websocket_message" and url == path then | |
| data = data .. msg | |
| if not bin then print("Warning: A text message was sent. This data may have been corrupted.") end | |
| end | |
| until ev == "websocket_closed" and url == path | |
| else | |
| path = shell.resolve(...) | |
| local file, err = fs.open(path, "rb") | |
| if not file then error("Could not open " .. path .. ": " .. err) end | |
| data = file.readAll() | |
| file.close() | |
| end | |
| print("Streaming...") | |
| if path:match("%.dfpwm$") then aukit.play(aukit.stream.dfpwm(data, 48000), peripheral.find "speaker") | |
| elseif path:match("%.wav$") then aukit.play(aukit.stream.wav(data), peripheral.find "speaker") | |
| elseif path:match("%.aiff?$") then aukit.play(aukit.stream.aiff(data), peripheral.find "speaker") | |
| elseif path:match("%.au$") then aukit.play(aukit.stream.au(data), peripheral.find "speaker") | |
| elseif path:match("%.flac$") then aukit.play(aukit.stream.flac(data), peripheral.find "speaker") | |
| else error("Unknown file type.") end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment