Skip to content

Instantly share code, notes, and snippets.

@MCJack123
Last active January 27, 2022 01:08
Show Gist options
  • Save MCJack123/c8a4a3205881c9b870e93321bd905d09 to your computer and use it in GitHub Desktop.
Save MCJack123/c8a4a3205881c9b870e93321bd905d09 to your computer and use it in GitHub Desktop.
AUKit: Audio decoding and processing framework for ComputerCraft
--- 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
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)
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