3620 lines
162 KiB
Lua
3620 lines
162 KiB
Lua
--- AUKit: aukit.Audio decoding and processing framework for ComputerCraft
|
|
--- @file
|
|
---
|
|
--- 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, G.711, and 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 aukit.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. aukit.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, cubic,
|
|
--- and sinc 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.
|
|
---
|
|
--- For simple audio playback tasks, the aukit.stream table provides a number of
|
|
--- functions that can quickly decode audio for real-time playback. Each function
|
|
--- returns an iterator function that can be called multiple times to obtain fully
|
|
--- decoded chunks of audio in 8-bit PCM, ready for playback to one or more
|
|
--- speakers. The functions decode the data, resample it to 48 kHz (using the
|
|
--- default resampling method), apply a low-pass filter to decrease interpolation
|
|
--- error, mix to mono if desired, and then return a list of tables with samples
|
|
--- in the range [-128, 127], plus the current position of the audio. The
|
|
--- iterators can be passed directly to the aukit.play function, which complements
|
|
--- the aukit.stream suite by playing the decoded audio on speakers while decoding
|
|
--- it in real-time, handling synchronization of speakers as best as possible.
|
|
---
|
|
--- If you're really lazy, you can also call `aukit` as a function, which takes
|
|
--- the path to a file, and plays this on all available speakers.
|
|
---
|
|
--- 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
|
|
---
|
|
--- @copyright
|
|
---
|
|
--- MIT License
|
|
---
|
|
--- Copyright (c) 2021-2024 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 bit32_band, bit32_bxor, bit32_lshift, bit32_rshift, bit32_arshift, bit32_btest, bit32_extract = bit32.band, bit32.bxor, bit32.lshift, bit32.rshift, bit32.arshift, bit32.btest, bit32.extract
|
|
local math_floor, math_ceil, math_sin, math_abs, math_fmod, math_min, math_max, math_pi = math.floor, math.ceil, math.sin, math.abs, math.fmod, math.min, math.max, math.pi
|
|
local os_epoch, os_queueEvent, os_pullEvent = os.epoch, os.queueEvent, os.pullEvent
|
|
local str_pack, str_unpack, str_sub, str_byte, str_rep = string.pack, string.unpack, string.sub, string.byte, string.rep
|
|
local table_pack, table_unpack, table_insert, table_remove = table.pack, table.unpack, table.insert, table.remove
|
|
|
|
--- The main AUKit module.
|
|
---@class aukit
|
|
---@field _VERSION string The version of AUKit that is loaded. This follows [SemVer](https://semver.org) format.
|
|
---@field defaultInterpolation "none"|"linear"|"cubic"|"sinc" Default interpolation mode for `Audio.resample` and other functions that need to resample.
|
|
local aukit = setmetatable({
|
|
_VERSION = "1.10.0",
|
|
defaultInterpolation = "linear"
|
|
}, {__call = function(aukit, path)
|
|
expect(1, path, "string")
|
|
local file = assert(fs.open(path, "rb"))
|
|
local type = aukit.detect(file.read(64)) or "dfpwm"
|
|
file.seek("set", 0)
|
|
aukit.play(aukit.stream[type](function() return file.read(48000) end), peripheral.find("speaker"))
|
|
file.close()
|
|
end})
|
|
--- Effects that can modify audio chunks.
|
|
---@class aukit.effects
|
|
aukit.effects = {}
|
|
--- Loader functions for streaming audio from a remote resource. These are usually used with `aukit.play`.
|
|
---@class aukit.stream
|
|
aukit.stream = {}
|
|
|
|
|
|
--- The aukit.Audio class represents a chunk of audio with variable channels and sample rate.
|
|
---@class aukit.Audio
|
|
---@field data number[][] The samples in each channel.
|
|
---@field sampleRate number The sample rate of the audio.
|
|
---@field metadata table Stores any metadata read from the file if present.
|
|
---@field info Metadata Stores any decoder-specific information, including `bitDepth` and `dataType`.
|
|
local Audio = {}
|
|
local Audio_mt
|
|
|
|
local dfpwmUUID = "3ac1fa38-811d-4361-a40d-ce53ca607cd1" -- UUID for DFPWM in WAV files
|
|
|
|
local function uuidBytes(uuid) return uuid:gsub("-", ""):gsub("%x%x", function(c) return string.char(tonumber(c, 16)) end) end
|
|
|
|
local sincWindowSize = jit and 30 or 10
|
|
|
|
local wavExtensible = {
|
|
dfpwm = uuidBytes(dfpwmUUID),
|
|
pcm = uuidBytes "01000000-0000-1000-8000-00aa00389b71",
|
|
msadpcm = uuidBytes "02000000-0000-1000-8000-00aa00389b71",
|
|
alaw = uuidBytes "06000000-0000-1000-8000-00aa00389b71",
|
|
ulaw = uuidBytes "07000000-0000-1000-8000-00aa00389b71",
|
|
adpcm = uuidBytes "11000000-0000-1000-8000-00aa00389b71",
|
|
pcm_float = uuidBytes "03000000-0000-1000-8000-00aa00389b71"
|
|
}
|
|
|
|
local wavExtensibleChannels = {
|
|
0x04,
|
|
0x03,
|
|
0x07,
|
|
0x33,
|
|
0x37,
|
|
0x3F,
|
|
0x637,
|
|
0x63F,
|
|
0x50F7,
|
|
0x50FF,
|
|
0x56F7,
|
|
0x56FF
|
|
}
|
|
|
|
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 msadpcm_adaption_table = {
|
|
[0] = 230, 230, 230, 230, 307, 409, 512, 614,
|
|
[-8] = 768, [-7] = 614, [-6] = 512, [-5] = 409, [-4] = 307, [-3] = 230, [-2] = 230, [-1] = 230
|
|
}
|
|
|
|
local flacMetadata = {
|
|
tracknumber = "trackNumber",
|
|
["encoded-by"] = "encodedBy",
|
|
sourcemedia = "sourceMedia",
|
|
labelno = "labelNumber",
|
|
discnumber = "discNumber",
|
|
partnumber = "partNumber",
|
|
productnumber = "productNumber",
|
|
catalognumber = "catalogNumber",
|
|
["release date"] = "releaseDate",
|
|
["source medium"] = "sourceMedium",
|
|
["source artist"] = "sourceArtist",
|
|
["guest artist"] = "guestArtist",
|
|
["source work"] = "sourceWork",
|
|
disctotal = "discCount",
|
|
tracktotal = "trackCount",
|
|
parttotal = "partCount",
|
|
tcm = "composer"
|
|
}
|
|
|
|
local wavMetadata = {
|
|
IPRD = "album",
|
|
INAM = "title",
|
|
IART = "artist",
|
|
IWRI = "author",
|
|
IMUS = "composer",
|
|
IPRO = "producer",
|
|
IPRT = "trackNumber",
|
|
ITRK = "trackNumber",
|
|
IFRM = "trackCount",
|
|
PRT1 = "partNumber",
|
|
PRT2 = "partCount",
|
|
TLEN = "length",
|
|
IRTD = "rating",
|
|
ICRD = "date",
|
|
ITCH = "encodedBy",
|
|
ISFT = "encoder",
|
|
ISRF = "media",
|
|
IGNR = "genre",
|
|
ICMT = "comment",
|
|
ICOP = "copyright",
|
|
ILNG = "language"
|
|
}
|
|
|
|
local function utf8decode(str, pos)
|
|
local codes = {utf8.codepoint(str, 1, -1)}
|
|
for i, v in ipairs(codes) do if v > 0xFF then codes[i] = 0x3F end end
|
|
return string.char(table_unpack(codes)), pos
|
|
end
|
|
|
|
local function clamp(n, min, max)
|
|
if n < min then return min
|
|
elseif n > max then return max
|
|
else return n end
|
|
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 copy(tab)
|
|
local t = {}
|
|
for k, v in pairs(tab) do t[k] = v end
|
|
return t
|
|
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(str, pos+i) end
|
|
else for i = 0, sz - 1 do n = n + str_byte(str, 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)
|
|
local ffx = math_floor(x)
|
|
return data[ffx] + ((data[ffx+1] or data[ffx]) - data[ffx]) * (x - ffx)
|
|
end,
|
|
cubic = function(data, x)
|
|
local ffx = math_floor(x)
|
|
local p0, p1, p2, p3, fx = data[ffx-1], data[ffx], data[ffx+1], data[ffx+2], x - ffx
|
|
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,
|
|
sinc = function(data, x)
|
|
local ffx = math_floor(x)
|
|
local fx = x - ffx
|
|
local sum = 0
|
|
for n = -sincWindowSize, sincWindowSize do
|
|
local idx = ffx+n
|
|
local d = data[idx]
|
|
if d then
|
|
local px = math_pi * (fx - n)
|
|
if px == 0 then sum = sum + d
|
|
else sum = sum + d * math_sin(px) / px end
|
|
end
|
|
end
|
|
return sum
|
|
end
|
|
}
|
|
local interpolation_start = {none = 1, linear = 1, cubic = 0, sinc = 0}
|
|
local interpolation_end = {none = 1, linear = 2, cubic = 3, sinc = 0}
|
|
|
|
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 = str_byte(data, 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(2);
|
|
local blockSizeCode = inp.readUint(4);
|
|
local sampleRateCode = inp.readUint(4);
|
|
local chanAsgn = inp.readUint(4);
|
|
inp.readUint(4);
|
|
|
|
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, head)
|
|
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
|
|
local meta = {}
|
|
while not last do
|
|
temp, pos = str_byte(inp, 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(str_byte(inp, pos), 4)
|
|
numChannels = bit32_band(bit32_rshift(str_byte(inp, pos), 1), 7) + 1;
|
|
sampleDepth = bit32_band(str_byte(inp, pos), 1) * 16 + bit32_rshift(str_byte(inp, pos+1), 4) + 1;
|
|
numSamples, pos = intunpack(inp, pos + 2, 4, false, true)
|
|
numSamples = numSamples + bit32_band(str_byte(inp, pos-5), 15) * 2^32
|
|
pos = pos + 16
|
|
elseif type == 4 then
|
|
local ncomments
|
|
meta.vendor, ncomments, pos = str_unpack("<s4I4", inp, pos)
|
|
for i = 1, ncomments do
|
|
local str
|
|
str, pos = utf8decode(str_unpack("<s4", inp, pos))
|
|
local k, v = str:match "^([^=]+)=(.*)$"
|
|
if k then meta[flacMetadata[k:lower()] or k:lower()] = v end
|
|
end
|
|
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
|
|
if head then return {sampleRate = sampleRate, data = out, metadata = meta, info = {bitDepth = sampleDepth, dataType = "signed"}} end
|
|
inp = BitInputStream(inp, pos)
|
|
repeat until not decodeFrame(inp, numChannels, sampleDepth, out, callback)
|
|
if not callback then return {sampleRate = sampleRate, data = out, metadata = meta, info = {bitDepth = sampleDepth, dataType = "signed"}} end
|
|
end
|
|
|
|
end
|
|
|
|
--[[
|
|
....###....##.....##.########..####..#######.
|
|
...##.##...##.....##.##.....##..##..##.....##
|
|
..##...##..##.....##.##.....##..##..##.....##
|
|
.##.....##.##.....##.##.....##..##..##.....##
|
|
.#########.##.....##.##.....##..##..##.....##
|
|
.##.....##.##.....##.##.....##..##..##.....##
|
|
.##.....##..#######..########..####..#######.
|
|
]]
|
|
|
|
--- Audio
|
|
---@section Audio
|
|
|
|
---@alias Metadata {bitDepth: number|nil, dataType: string|nil}
|
|
|
|
--- Returns the length of the audio object in seconds.
|
|
---@return number _ The audio length
|
|
function Audio:len()
|
|
return #self.data[1] / self.sampleRate
|
|
end
|
|
|
|
--- Returns the number of channels in the audio object.
|
|
---@return number _ The number of channels
|
|
function Audio:channels()
|
|
return #self.data
|
|
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.
|
|
---@param sampleRate number The new sample rate in Hertz
|
|
---@param interpolation? "none"|"linear"|"cubic" The interpolation mode to use
|
|
---@return aukit.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 not interpolate[interpolation] then error("bad argument #2 (invalid interpolation type)", 2) end
|
|
local new = setmetatable({sampleRate = sampleRate, data = {}, metadata = copy(self.metadata), info = copy(self.info)}, 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.
|
|
---@return aukit.Audio _ A new audio object with the audio mixed to mono
|
|
function Audio:mono()
|
|
local new = setmetatable({sampleRate = self.sampleRate, data = {{}}, metadata = copy(self.metadata), info = copy(self.info)}, Audio_mt)
|
|
local ndata = new.data[1]
|
|
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
|
|
ndata[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.
|
|
---@param ... aukit.Audio The audio objects to concatenate
|
|
---@return aukit.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 = {}, metadata = copy(self.metadata), info = copy(self.info)}, 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.
|
|
---@param start? number The start position of the audio in seconds
|
|
---@param last? number The end position of the audio in seconds (0 means end of file)
|
|
---@return aukit.Audio _ The new split audio object
|
|
function Audio:sub(start, last)
|
|
start = math_floor(expect(1, start, "number", "nil") or 0)
|
|
last = math_floor(expect(2, last, "number", "nil") or 0)
|
|
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, 0, len)
|
|
expect.range(last, 0, len)
|
|
start, last = start * self.sampleRate + 1, last * self.sampleRate + 1
|
|
local obj = setmetatable({sampleRate = self.sampleRate, data = {}, metadata = copy(self.metadata), info = copy(self.info)}, 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.
|
|
---@param ... aukit.Audio The audio objects to combine with
|
|
---@return aukit.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 = {}, metadata = copy(self.metadata), info = copy(self.info)}, 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 an error.
|
|
---@param ... number[] The lists of channels in each new object
|
|
---@return aukit.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")
|
|
if #cl == 0 then error("bad argument #" .. n .. " (cannot use empty table)") end
|
|
local obj = setmetatable({sampleRate = self.sampleRate, data = {}, metadata = copy(self.metadata), info = copy(self.info)}, 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.data[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.
|
|
---@param amplifier number|Audio The multiplier to apply, or the first audio object
|
|
---@param ... aukit.Audio The objects to mix with this one
|
|
---@return aukit.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 = {}, metadata = copy(self.metadata), info = copy(self.info)}, 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.data[c] = ch
|
|
end
|
|
return obj
|
|
end
|
|
|
|
--- Returns a new audio object that repeats this audio a number of times.
|
|
---@param count number The number of times to play the audio
|
|
---@return aukit.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 = {}, metadata = copy(self.metadata), info = copy(self.info)}, 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.
|
|
---@return aukit.Audio _ The reversed audio
|
|
function Audio:reverse()
|
|
local obj = setmetatable({sampleRate = self.sampleRate, data = {}, metadata = copy(self.metadata), info = copy(self.info)}, 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 encode
|
|
if info.dataType == "float" then encode = function(d) return d end
|
|
else encode = function(d) 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
|
|
local start = os_epoch "utc"
|
|
if info.interleaved then for n = pos, pos + info.len - 1 do if os_epoch "utc" - start > 3000 then start = os_epoch "utc" sleep(0) end 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
|
|
if os_epoch "utc" - start > 3000 then start = os_epoch "utc" sleep(0) end
|
|
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 if os_epoch "utc" - start > 3000 then start = os_epoch "utc" sleep(0) end data[(c-1)*len+n] = encode(source[c][n]) end end end
|
|
return data
|
|
end
|
|
|
|
--- Converts the audio data to raw PCM samples.
|
|
---@param bitDepth? number The bit depth of the audio (8, 16, 24, 32)
|
|
---@param dataType? "signed"|"unsigned"|"float" The type of each sample
|
|
---@param interleaved? boolean Whether to interleave each channel
|
|
---@return number[]|nil ... 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`.
|
|
---@param chunkSize? number The size of each chunk
|
|
---@param bitDepth? number The bit depth of the audio (8, 16, 24, 32)
|
|
---@param dataType? "signed"|"unsigned"|"float" The type of each sample
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return 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.
|
|
---@param bitDepth? number The bit depth of the audio (1 = DFPWM, 8, 16, 24, 32)
|
|
---@return string _ The resulting WAV file data
|
|
function Audio:wav(bitDepth)
|
|
-- TODO: Support float data
|
|
bitDepth = expect(1, bitDepth, "number", "nil") or 16
|
|
if bitDepth == 1 then
|
|
local str = self:dfpwm(true)
|
|
if self.metadata and next(self.metadata) then
|
|
local info = {}
|
|
for k, v in pairs(self.metadata) do
|
|
for l, w in pairs(wavMetadata) do
|
|
if w == k then
|
|
info[#info+1] = l
|
|
info[#info+1] = tostring(v)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
local list = str_pack("!2<c4" .. ("c4s4Xh"):rep(#info / 2), "INFO", table.unpack(info))
|
|
return str_pack("<c4Ic4c4IHHIIHHHHIc16c4IIc4s4c4I",
|
|
"RIFF", #str + 72, "WAVE",
|
|
"fmt ", 40, 0xFFFE, #self.data, self.sampleRate, self.sampleRate * #self.data / 8, math_ceil(#self.data / 8), 1,
|
|
22, 1, wavExtensibleChannels[#self.data] or 0, wavExtensible.dfpwm,
|
|
"fact", 4, #self.data[1],
|
|
"LIST", list,
|
|
"data", #str) .. str
|
|
else
|
|
return str_pack("<c4Ic4c4IHHIIHHHHIc16c4IIc4I",
|
|
"RIFF", #str + 72, "WAVE",
|
|
"fmt ", 40, 0xFFFE, #self.data, self.sampleRate, self.sampleRate * #self.data / 8, math_ceil(#self.data / 8), 1,
|
|
22, 1, wavExtensibleChannels[#self.data] or 0, wavExtensible.dfpwm,
|
|
"fact", 4, #self.data[1],
|
|
"data", #str) .. str
|
|
end
|
|
elseif 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))
|
|
if self.metadata and next(self.metadata) then
|
|
local info = {}
|
|
for k, v in pairs(self.metadata) do
|
|
for l, w in pairs(wavMetadata) do
|
|
if w == k then
|
|
info[#info+1] = l
|
|
info[#info+1] = tostring(v)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
local list = str_pack("!2<c4" .. ("c4s4Xh"):rep(#info / 2), "INFO", table.unpack(info))
|
|
return str_pack("<c4Ic4c4IHHIIHHc4s4c4I", "RIFF", #str + 36, "WAVE", "fmt ", 16, 1, #self.data, self.sampleRate, self.sampleRate * #self.data * bitDepth / 8, #self.data * bitDepth / 8, bitDepth, "LIST", list, "data", #str) .. str
|
|
else
|
|
return str_pack("<c4Ic4c4IHHIIHHc4I", "RIFF", #str + 36, "WAVE", "fmt ", 16, 1, #self.data, self.sampleRate, self.sampleRate * #self.data * bitDepth / 8, #self.data * bitDepth / 8, bitDepth, "data", #str) .. str
|
|
end
|
|
end
|
|
|
|
--- Converts the audio data to DFPWM. All channels share the same encoder, and
|
|
--- channels are stored sequentially uninterleaved if `interleaved` is false, or
|
|
--- in one interleaved string if `interleaved` is true.
|
|
---@param interleaved? boolean Whether to interleave the channels
|
|
---@return string ... The resulting DFPWM data for each channel (only one string
|
|
--- if `interleaved` is true)
|
|
function Audio:dfpwm(interleaved)
|
|
expect(1, interleaved, "boolean", "nil")
|
|
if interleaved == nil then interleaved = true end
|
|
if interleaved then
|
|
return dfpwm.encode(self:pcm(8, "signed", true))
|
|
else
|
|
local channels = {self:pcm(8, "signed", false)}
|
|
---@type fun(samples:number[]):string
|
|
local encode = dfpwm.make_encoder()
|
|
for i = 1, #channels do channels[i] = encode(channels[i]) end
|
|
---@diagnostic disable-next-line return-type-mismatch
|
|
return table_unpack(channels)
|
|
end
|
|
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.
|
|
---@param data string|table The audio data, either as a raw string, or a table
|
|
--- of values (in the format specified by `bitDepth` and `dataType`)
|
|
---@param bitDepth? number The bit depth of the audio (8, 16, 24, 32); if `dataType` is "float" then this must be 32
|
|
---@param dataType? "signed"|"unsigned"|"float" The type of each sample
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@param interleaved? boolean Whether each channel is interleaved or separate
|
|
---@param bigEndian? boolean Whether the audio is big-endian or little-endian; ignored if data is a table
|
|
---@return aukit.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 csizeb = csize * byteDepth
|
|
local bitDir = bigEndian and ">" or "<"
|
|
local sformat = dataType == "float" and "f" or ((dataType == "signed" and "i" or "I") .. byteDepth)
|
|
local format = bitDir .. str_rep(sformat, csize)
|
|
local maxValue = 2^(bitDepth-1)
|
|
local obj = setmetatable({sampleRate = sampleRate, data = {}, metadata = {}, info = {bitDepth = bitDepth, dataType = dataType}}, 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 + csizeb > #data then
|
|
local f = bitDir .. str_rep(sformat, (#data - spos + 1) / byteDepth)
|
|
tmp = {str_unpack(f, data, spos)}
|
|
spos = tmp[#tmp]
|
|
tmp[#tmp] = nil
|
|
else
|
|
tmp = {str_unpack(format, 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 + csizeb > #data then
|
|
local f = bitDir .. str_rep(sformat, (#data - spos + 1) / byteDepth)
|
|
tmp = {str_unpack(f, data, spos)}
|
|
spos = tmp[#tmp]
|
|
tmp[#tmp] = nil
|
|
else
|
|
tmp = {str_unpack(format, 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 + csizeb > #data then
|
|
local f = bitDir .. str_rep(sformat, (#data - spos + 1) / byteDepth)
|
|
tmp = {str_unpack(f, data, spos)}
|
|
spos = tmp[#tmp]
|
|
tmp[#tmp] = nil
|
|
else
|
|
tmp = {str_unpack(format, 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.
|
|
---@param data string|table The audio data, either as a raw string, or a table of nibbles
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@param topFirst? boolean Whether the top nibble is the first nibble
|
|
--- (true) or last (false); ignored if `data` is a table
|
|
---@param interleaved? boolean Whether each channel is interleaved or separate
|
|
---@param predictor? number|table The initial predictor value(s)
|
|
---@param step_index? number|table The initial step index(es)
|
|
---@return aukit.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, 88)}
|
|
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, 88) 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 = str_byte(data, 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 = {}, metadata = {}, info = {bitDepth = 16, dataType = "signed"}}, Audio_mt)
|
|
local step = {}
|
|
local start = os_epoch "utc"
|
|
if interleaved then
|
|
local d = obj.data
|
|
for j = 1, channels do d[j] = {} end
|
|
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[j] = ima_step_table[step_index[j]]
|
|
step_index[j] = clamp(step_index[j] + ima_index_table[nibble], 0, 88)
|
|
local diff = bit32_rshift((nibble % 8) * step[j], 2) + bit32_rshift(step[j], 3)
|
|
if nibble >= 8 then predictor[j] = clamp(predictor[j] - diff, -32768, 32767)
|
|
else predictor[j] = clamp(predictor[j] + diff, -32768, 32767) end
|
|
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 = ima_step_table[step_index]
|
|
step_index = clamp(step_index + ima_index_table[nibble], 0, 88)
|
|
local diff = bit32_rshift((nibble % 8) * step, 2) + bit32_rshift(step, 3)
|
|
if nibble >= 8 then predictor = clamp(predictor - diff, -32768, 32767)
|
|
else predictor = clamp(predictor + diff, -32768, 32767) end
|
|
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 Microsoft ADPCM data.
|
|
---@param data string The audio data as a raw string
|
|
---@param blockAlign number The number of bytes in each block
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@param coefficients? table Two lists of coefficients to use
|
|
---@return aukit.Audio _ A new audio object containing the decoded data
|
|
function aukit.msadpcm(data, blockAlign, channels, sampleRate, coefficients)
|
|
expect(1, data, "string")
|
|
expect(2, blockAlign, "number")
|
|
channels = expect(3, channels, "number", "nil") or 1
|
|
sampleRate = expect(4, sampleRate, "number", "nil") or 48000
|
|
expect(5, coefficients, "table", "nil")
|
|
expect.range(sampleRate, 1)
|
|
local coeff1, coeff2
|
|
if coefficients then
|
|
if type(coefficients[1]) ~= "table" then error("bad argument #5 (first entry is not a table)", 2) end
|
|
if type(coefficients[2]) ~= "table" then error("bad argument #5 (second entry is not a table)", 2) end
|
|
--if #coefficients[1] ~= #coefficients[2] then error("bad argument #5 (lists are not the same length)", 2) end
|
|
--coeff1, coeff2 = {}, {}
|
|
for i, v in ipairs(coefficients[1]) do
|
|
if type(v) ~= "number" then error("bad entry #" .. i .. " in coefficient list 1 (expected number, got " .. type(v) .. ")", 2) end
|
|
coeff1[i-1] = v
|
|
end
|
|
for i, v in ipairs(coefficients[2]) do
|
|
if type(v) ~= "number" then error("bad entry #" .. i .. " in coefficient list 2 (expected number, got " .. type(v) .. ")", 2) end
|
|
coeff2[i-1] = v
|
|
end
|
|
else coeff1, coeff2 = {[0] = 256, 512, 0, 192, 240, 460, 392}, {[0] = 0, -256, 0, 64, 0, -208, -232} end
|
|
local obj = setmetatable({sampleRate = sampleRate, data = {{}, channels == 2 and {} or nil}, metadata = {}, info = {bitDepth = 16, dataType = "signed"}}, Audio_mt)
|
|
local left, right = obj.data[1], obj.data[2]
|
|
local start = os_epoch "utc"
|
|
for n = 1, #data, blockAlign do
|
|
if channels == 2 then
|
|
local predictorIndexL, predictorIndexR, deltaL, deltaR, sample1L, sample1R, sample2L, sample2R = str_unpack("<BBhhhhhh", data, n)
|
|
local c1L, c2L, c1R, c2R = coeff1[predictorIndexL], coeff2[predictorIndexL], coeff1[predictorIndexR], coeff2[predictorIndexR]
|
|
left[#left+1] = sample2L / (sample2L < 0 and 32768 or 32767)
|
|
left[#left+1] = sample1L / (sample1L < 0 and 32768 or 32767)
|
|
right[#right+1] = sample2R / (sample2R < 0 and 32768 or 32767)
|
|
right[#right+1] = sample1R / (sample1R < 0 and 32768 or 32767)
|
|
for i = 14, blockAlign - 1 do
|
|
local b = str_byte(data, n+i)
|
|
local hi, lo = bit32_rshift(b, 4), bit32_band(b, 0x0F)
|
|
if hi >= 8 then hi = hi - 16 end
|
|
if lo >= 8 then lo = lo - 16 end
|
|
local predictor = clamp(math_floor((sample1L * c1L + sample2L * c2L) / 256) + hi * deltaL, -32768, 32767)
|
|
left[#left+1] = predictor / (predictor < 0 and 32768 or 32767)
|
|
sample2L, sample1L = sample1L, predictor
|
|
deltaL = math_max(math_floor(msadpcm_adaption_table[hi] * deltaL / 256), 16)
|
|
predictor = clamp(math_floor((sample1R * c1R + sample2R * c2R) / 256) + lo * deltaR, -32768, 32767)
|
|
right[#right+1] = predictor / (predictor < 0 and 32768 or 32767)
|
|
sample2R, sample1R = sample1R, predictor
|
|
deltaR = math_max(math_floor(msadpcm_adaption_table[lo] * deltaR / 256), 16)
|
|
end
|
|
elseif channels == 1 then
|
|
local predictorIndex, delta, sample1, sample2 = str_unpack("<!1Bhhh", data)
|
|
local c1, c2 = coeff1[predictorIndex], coeff2[predictorIndex]
|
|
left[#left+1] = sample2 / (sample2 < 0 and 32768 or 32767)
|
|
left[#left+1] = sample1 / (sample1 < 0 and 32768 or 32767)
|
|
for i = 7, blockAlign - 1 do
|
|
local b = str_byte(data, n+i)
|
|
local hi, lo = bit32_rshift(b, 4), bit32_band(b, 0x0F)
|
|
if hi >= 8 then hi = hi - 16 end
|
|
if lo >= 8 then lo = lo - 16 end
|
|
local predictor = clamp(math_floor((sample1 * c1 + sample2 * c2) / 256) + hi * delta, -32768, 32767)
|
|
left[#left+1] = predictor / (predictor < 0 and 32768 or 32767)
|
|
sample2, sample1 = sample1, predictor
|
|
delta = math_max(math_floor(msadpcm_adaption_table[hi] * delta / 256), 16)
|
|
predictor = clamp(math_floor((sample1 * c1 + sample2 * c2) / 256) + lo * delta, -32768, 32767)
|
|
left[#left+1] = predictor / (predictor < 0 and 32768 or 32767)
|
|
sample2, sample1 = sample1, predictor
|
|
delta = math_max(math_floor(msadpcm_adaption_table[lo] * delta / 256), 16)
|
|
end
|
|
else error("Unsupported number of channels: " .. channels) end
|
|
if os_epoch "utc" - start > 3000 then start = os_epoch "utc" sleep(0) end
|
|
end
|
|
return obj
|
|
end
|
|
|
|
--- Creates a new audio object from G.711 u-law/A-law data.
|
|
---@param data string The audio data as a raw string
|
|
---@param ulaw boolean Whether the audio uses u-law (true) or A-law (false).
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@return aukit.Audio _ A new audio object containing the decoded data
|
|
function aukit.g711(data, ulaw, channels, sampleRate)
|
|
expect(1, data, "string")
|
|
expect(2, ulaw, "boolean")
|
|
channels = expect(3, channels, "number", "nil") or 1
|
|
sampleRate = expect(4, sampleRate, "number", "nil") or 8000
|
|
local retval = {}
|
|
local csize = jit and 7680 or 32768
|
|
local xor = ulaw and 0xFF or 0x55
|
|
for i = 1, channels do retval[i] = {} end
|
|
local start = os_epoch "utc"
|
|
for i = 1, #data, csize do
|
|
local bytes = {str_byte(data, i, i + csize - 1)}
|
|
for j = 1, #bytes do
|
|
local b = bit32_bxor(bytes[j], xor)
|
|
local m, e = bit32_band(b, 0x0F), bit32_extract(b, 4, 3)
|
|
if not ulaw and e == 0 then m = m * 4 + 2
|
|
else m = bit32_lshift(m * 2 + 33, e) end
|
|
if ulaw then m = m - 33 end
|
|
retval[(i+j-2) % channels + 1][math_floor((i+j-2) / channels + 1)] = m / (bit32_btest(b, 0x80) == ulaw and -0x2000 or 0x2000)
|
|
end
|
|
if os_epoch "utc" - start > 3000 then start = os_epoch "utc" sleep(0) end
|
|
end
|
|
return setmetatable({sampleRate = sampleRate, data = retval, metadata = {bitDepth = ulaw and 14 or 13, dataType = "signed"}, info = {}}, Audio_mt)
|
|
end
|
|
|
|
--- Creates a new audio object from DFPWM1a data. All channels are expected to
|
|
--- share the same decoder, and are stored interleaved in a single stream.
|
|
---@param data string The audio data as a raw string
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@return aukit.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)
|
|
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(str_sub(data, 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, true, false)
|
|
end
|
|
|
|
--- Creates a new audio object from MDFPWMv3 data.
|
|
---@param data string The audio data as a raw string
|
|
---@param head boolean Whether to only load metadata (header data) - this will not decode the audio
|
|
---@return aukit.Audio _ A new audio object containing the decoded data
|
|
function aukit.mdfpwm(data, head)
|
|
expect(1, data, "string")
|
|
if data:sub(1, 7) ~= "MDFPWM\3" then error("bad argument #1 (not a MDFPWM file)", 2) end
|
|
local length, artist, title, album, pos = ("<Is1s1s1"):unpack(data, 8)
|
|
if head then
|
|
local obj = aukit.new(0, 2, 48000)
|
|
obj.metadata = {artist = artist, title = title, album = album}
|
|
return obj
|
|
end
|
|
local audio = {}
|
|
local decoderL, decoderR = dfpwm.make_decoder(), dfpwm.make_decoder()
|
|
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 tempL = decoderL(str_sub(data, pos, pos + 5999))
|
|
if tempL == nil or #tempL == 0 then break end
|
|
for i = 1, #tempL do audio[last+i*2-1] = tempL[i] end
|
|
local tempR = decoderR(str_sub(data, pos + 6000, pos + 11999 ))
|
|
if tempR == nil or #tempR == 0 then break end
|
|
for i = 1, #tempR do audio[last+i*2] = tempR[i] end
|
|
last = last + #tempL + #tempR
|
|
pos = pos + 12000
|
|
end
|
|
for i = length * 8 + 1, #audio do audio[i] = nil end
|
|
local obj = aukit.pcm(audio, 8, "signed", 2, 48000, true, false)
|
|
obj.metadata = {artist = artist, title = title, album = album}
|
|
return obj
|
|
end
|
|
|
|
--- Creates a new audio object from a WAV file. This accepts PCM files up to 32
|
|
--- bits, including float data, as well as DFPWM files [as specified here](https://gist.github.com/MCJack123/90c24b64c8e626c7f130b57e9800962c),
|
|
--- plus IMA and Microsoft ADPCM formats and G.711 u-law/A-law.
|
|
---@param data string The WAV data to load
|
|
---@param head boolean Whether to only load metadata (header data) - this will not decode the audio
|
|
---@return aukit.Audio _ A new audio object with the contents of the WAV file
|
|
function aukit.wav(data, head)
|
|
expect(1, data, "string")
|
|
local channels, sampleRate, bitDepth, length, dataType, blockAlign, coefficients
|
|
local temp, pos = str_unpack("c4", data)
|
|
if temp ~= "RIFF" then error("bad argument #1 (not a WAV file)", 2) end
|
|
pos = pos + 4
|
|
temp, pos = str_unpack("c4", data, pos)
|
|
if temp ~= "WAVE" then error("bad argument #1 (not a WAV file)", 2) end
|
|
local meta = {}
|
|
local obj
|
|
while pos <= #data do
|
|
local size
|
|
temp, size, pos = str_unpack("<c4I", data, pos)
|
|
if temp == "fmt " then
|
|
local chunk = str_sub(data, pos, pos + size - 1)
|
|
pos = pos + size
|
|
local format
|
|
format, channels, sampleRate, blockAlign, bitDepth = str_unpack("<HHIxxxxHH", chunk)
|
|
if format == 1 then
|
|
dataType = bitDepth == 8 and "unsigned" or "signed"
|
|
elseif format == 2 then
|
|
dataType = "msadpcm"
|
|
local numcoeff = str_unpack("<H", chunk, 21)
|
|
if numcoeff > 0 then
|
|
coefficients = {{}, {}}
|
|
for i = 1, numcoeff do
|
|
coefficients[1][i], coefficients[2][i] = str_unpack("<hh", chunk, i * 4 + 19)
|
|
end
|
|
end
|
|
elseif format == 3 then
|
|
dataType = "float"
|
|
elseif format == 6 then
|
|
dataType = "alaw"
|
|
elseif format == 7 then
|
|
dataType = "ulaw"
|
|
elseif format == 0x11 then
|
|
dataType = "adpcm"
|
|
elseif format == 0xFFFE then
|
|
bitDepth = str_unpack("<H", chunk, 19)
|
|
local uuid = str_sub(chunk, 25, 40)
|
|
if uuid == wavExtensible.pcm then dataType = bitDepth == 8 and "unsigned" or "signed"
|
|
elseif uuid == wavExtensible.dfpwm then dataType = "dfpwm"
|
|
elseif uuid == wavExtensible.msadpcm then dataType = "msadpcm"
|
|
elseif uuid == wavExtensible.pcm_float then dataType = "float"
|
|
elseif uuid == wavExtensible.alaw then dataType = "alaw"
|
|
elseif uuid == wavExtensible.ulaw then dataType = "ulaw"
|
|
elseif uuid == wavExtensible.adpcm then dataType = "adpcm"
|
|
else error("unsupported WAV file", 2) end
|
|
else error("unsupported WAV file", 2) end
|
|
elseif temp == "data" then
|
|
local data = str_sub(data, pos, pos + size - 1)
|
|
if #data < size then error("invalid WAV file", 2) end
|
|
if head then obj = aukit.new(0, channels, sampleRate)
|
|
elseif dataType == "adpcm" then
|
|
local blocks = {}
|
|
for n = 1, #data, blockAlign do
|
|
if channels == 2 then
|
|
local predictorL, indexL, predictorR, indexR = str_unpack("<hBxhB", data, n)
|
|
local nibbles = {}
|
|
for i = 8, blockAlign - 1, 8 do
|
|
local b = str_byte(data, n+i)
|
|
nibbles[(i-7)*2-1] = bit32_band(b, 0x0F)
|
|
nibbles[(i-6)*2-1] = bit32_rshift(b, 4)
|
|
b = str_byte(data, n+i+1)
|
|
nibbles[(i-5)*2-1] = bit32_band(b, 0x0F)
|
|
nibbles[(i-4)*2-1] = bit32_rshift(b, 4)
|
|
b = str_byte(data, n+i+2)
|
|
nibbles[(i-3)*2-1] = bit32_band(b, 0x0F)
|
|
nibbles[(i-2)*2-1] = bit32_rshift(b, 4)
|
|
b = str_byte(data, n+i+3)
|
|
nibbles[(i-1)*2-1] = bit32_band(b, 0x0F)
|
|
nibbles[i*2-1] = bit32_rshift(b, 4)
|
|
b = str_byte(data, n+i+4)
|
|
nibbles[(i-7)*2] = bit32_band(b, 0x0F)
|
|
nibbles[(i-6)*2] = bit32_rshift(b, 4)
|
|
b = str_byte(data, n+i+5)
|
|
nibbles[(i-5)*2] = bit32_band(b, 0x0F)
|
|
nibbles[(i-4)*2] = bit32_rshift(b, 4)
|
|
b = str_byte(data, n+i+6)
|
|
nibbles[(i-3)*2] = bit32_band(b, 0x0F)
|
|
nibbles[(i-2)*2] = bit32_rshift(b, 4)
|
|
b = str_byte(data, n+i+7)
|
|
nibbles[(i-1)*2] = bit32_band(b, 0x0F)
|
|
nibbles[i*2] = bit32_rshift(b, 4)
|
|
end
|
|
blocks[#blocks+1] = aukit.adpcm(nibbles, channels, sampleRate, false, true, {predictorL, predictorR}, {indexL, indexR})
|
|
else
|
|
local predictor, index = str_unpack("<hB", data, n)
|
|
index = bit32_band(index, 0x0F)
|
|
blocks[#blocks+1] = aukit.adpcm(str_sub(data, n + 4, n + blockAlign - 1), channels, sampleRate, false, false, predictor, index)
|
|
end
|
|
end
|
|
obj = blocks[1]:concat(table_unpack(blocks, 2))
|
|
elseif dataType == "msadpcm" then obj = aukit.msadpcm(data, blockAlign, channels, sampleRate, coefficients)
|
|
elseif dataType == "alaw" or dataType == "ulaw" then obj = aukit.g711(data, dataType == "ulaw", channels, sampleRate)
|
|
elseif dataType == "dfpwm" then obj = aukit.dfpwm(data, channels, sampleRate)
|
|
else obj = aukit.pcm(data, bitDepth, dataType, channels, sampleRate, true, false) end
|
|
obj.metadata = meta
|
|
obj.info = {dataType = dataType, bitDepth = bitDepth}
|
|
pos = pos + size
|
|
elseif temp == "fact" then
|
|
-- TODO
|
|
pos = pos + size
|
|
elseif temp == "LIST" then
|
|
local type = str_unpack("c4", data, pos)
|
|
if type == "INFO" then
|
|
local e = pos + size
|
|
pos = pos + 4
|
|
while pos < e do
|
|
local str
|
|
type, str, pos = str_unpack("!2<c4s4Xh", data, pos)
|
|
if wavMetadata[type] then meta[wavMetadata[type]] = tonumber(str) or str end
|
|
end
|
|
else pos = pos + size end
|
|
else pos = pos + size end
|
|
end
|
|
if obj then return obj end
|
|
error("invalid WAV file", 2)
|
|
end
|
|
|
|
--- Creates a new audio object from an AIFF or AIFC file.
|
|
---@param data string The AIFF data to load
|
|
---@param head boolean Whether to only load metadata (header data) - this will not decode the audio
|
|
---@return aukit.Audio _ A new audio object with the contents of the AIFF file
|
|
function aukit.aiff(data, head)
|
|
expect(1, data, "string")
|
|
local channels, sampleRate, bitDepth, length, offset, compression, blockAlign
|
|
local isAIFC = false
|
|
local temp, pos = str_unpack("c4", data)
|
|
if temp ~= "FORM" then error("bad argument #1 (not an AIFF file)", 2) end
|
|
pos = pos + 4
|
|
temp, pos = str_unpack("c4", data, pos)
|
|
if temp == "AIFC" then isAIFC = true
|
|
elseif temp ~= "AIFF" then error("bad argument #1 (not an AIFF file)", 2) end
|
|
local meta = {}
|
|
while pos <= #data do
|
|
local size
|
|
temp, size, pos = str_unpack(">c4I", data, pos)
|
|
if temp == "COMM" then
|
|
local e, m
|
|
channels, length, bitDepth, e, m, pos = str_unpack(">hIhHI7x", data, pos)
|
|
if isAIFC then
|
|
local s
|
|
compression, s, pos = str_unpack(">c4s1", data, pos)
|
|
if #s % 2 == 0 then pos = pos + 1 end
|
|
end
|
|
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, blockAlign, pos = str_unpack(">II", data, pos)
|
|
local data = str_sub(data, pos + offset, pos + offset + length - 1)
|
|
--if #data < length then error("invalid AIFF file", 2) end
|
|
local obj
|
|
if head then obj = aukit.new(0, channels, sampleRate)
|
|
elseif compression == nil or compression == "NONE" then obj = aukit.pcm(data, bitDepth, "signed", channels, sampleRate, true, true)
|
|
elseif compression == "sowt" then obj = aukit.pcm(data, bitDepth, "signed", channels, sampleRate, true, false)
|
|
elseif compression == "fl32" or compression == "FL32" then obj = aukit.pcm(data, 32, "float", channels, sampleRate, true, true)
|
|
elseif compression == "alaw" or compression == "ulaw" or compression == "ALAW" or compression == "ULAW" then obj = aukit.g711(data, compression == "ulaw" or compression == "ULAW", channels, sampleRate)
|
|
else error("Unsupported compression scheme " .. compression, 2) end
|
|
obj.metadata = meta
|
|
return obj
|
|
elseif temp == "NAME" then
|
|
meta.title = str_sub(data, pos, pos + size - 1)
|
|
pos = pos + size
|
|
elseif temp == "AUTH" then
|
|
meta.artist = str_sub(data, pos, pos + size - 1)
|
|
pos = pos + size
|
|
elseif temp == "(c) " then
|
|
meta.copyright = str_sub(data, pos, pos + size - 1)
|
|
pos = pos + size
|
|
elseif temp == "ANNO" then
|
|
meta.comment = str_sub(data, pos, pos + size - 1)
|
|
pos = pos + size
|
|
else pos = pos + size end
|
|
end
|
|
error("invalid AIFF file", 2)
|
|
end
|
|
|
|
--- Creates a new audio object from an AU file.
|
|
---@param data string The AU data to load
|
|
---@return aukit.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 = str_unpack(">c4IIIII", data)
|
|
if magic ~= ".snd" then error("invalid AU file", 2) end
|
|
if encoding == 1 then return aukit.g711(str_sub(data, offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), true, channels, sampleRate)
|
|
elseif encoding == 2 then return aukit.pcm(str_sub(data, offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 8, "signed", channels, sampleRate, true, true)
|
|
elseif encoding == 3 then return aukit.pcm(str_sub(data, offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 16, "signed", channels, sampleRate, true, true)
|
|
elseif encoding == 4 then return aukit.pcm(str_sub(data, offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 24, "signed", channels, sampleRate, true, true)
|
|
elseif encoding == 5 then return aukit.pcm(str_sub(data, offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 32, "signed", channels, sampleRate, true, true)
|
|
elseif encoding == 6 then return aukit.pcm(str_sub(data, offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), 32, "float", channels, sampleRate, true, true)
|
|
elseif encoding == 27 then return aukit.g711(str_sub(data, offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), false, channels, sampleRate)
|
|
else error("unsupported encoding type " .. encoding, 2) end
|
|
end
|
|
|
|
--- Creates a new audio object from a FLAC file.
|
|
---@param data string The FLAC data to load
|
|
---@param head boolean Whether to only load metadata (header data) - this will not decode the audio
|
|
---@return aukit.Audio _ A new audio object with the contents of the FLAC file
|
|
function aukit.flac(data, head)
|
|
expect(1, data, "string")
|
|
return setmetatable(decodeFLAC(data, nil, head), Audio_mt)
|
|
end
|
|
|
|
local qoa_dequant_tab = {
|
|
[0] = {[0] = 1, -1, 3, -3, 5, -5, 7, -7},
|
|
{[0] = 5, -5, 18, -18, 32, -32, 49, -49},
|
|
{[0] = 16, -16, 53, -53, 95, -95, 147, -147},
|
|
{[0] = 34, -34, 113, -113, 203, -203, 315, -315},
|
|
{[0] = 63, -63, 210, -210, 378, -378, 588, -588},
|
|
{[0] = 104, -104, 345, -345, 621, -621, 966, -966},
|
|
{[0] = 158, -158, 528, -528, 950, -950, 1477, -1477},
|
|
{[0] = 228, -228, 760, -760, 1368, -1368, 2128, -2128},
|
|
{[0] = 316, -316, 1053, -1053, 1895, -1895, 2947, -2947},
|
|
{[0] = 422, -422, 1405, -1405, 2529, -2529, 3934, -3934},
|
|
{[0] = 548, -548, 1828, -1828, 3290, -3290, 5117, -5117},
|
|
{[0] = 696, -696, 2320, -2320, 4176, -4176, 6496, -6496},
|
|
{[0] = 868, -868, 2893, -2893, 5207, -5207, 8099, -8099},
|
|
{[0] = 1064, -1064, 3548, -3548, 6386, -6386, 9933, -9933},
|
|
{[0] = 1286, -1286, 4288, -4288, 7718, -7718, 12005, -12005},
|
|
{[0] = 1536, -1536, 5120, -5120, 9216, -9216, 14336, -14336},
|
|
}
|
|
|
|
local function signed_rshift(a, b)
|
|
local n = bit32_arshift(a, b)
|
|
if n >= 0x80000000 then return n - 0x100000000 else return n end
|
|
end
|
|
|
|
local function qoa_lms_predict(lms)
|
|
local w, h = lms.weights, lms.history
|
|
return signed_rshift(w[1] * h[1] + w[2] * h[2] + w[3] * h[3] + w[4] * h[4], 13)
|
|
end
|
|
|
|
local function qoa_lms_update(lms, sample, residual)
|
|
local delta = signed_rshift(residual, 4)
|
|
local w, h = lms.weights, lms.history
|
|
lms.weights = {
|
|
w[1] + (h[1] < 0 and -delta or delta),
|
|
w[2] + (h[2] < 0 and -delta or delta),
|
|
w[3] + (h[3] < 0 and -delta or delta),
|
|
w[4] + (h[4] < 0 and -delta or delta)
|
|
}
|
|
lms.history = {h[2], h[3], h[4], sample}
|
|
end
|
|
|
|
--- Creates a new audio object from a QOA file.
|
|
---@param data string The QOA data to load
|
|
---@return aukit.Audio _ A new audio object with the contents of the QOA file
|
|
function aukit.qoa(data)
|
|
expect(1, data, "string")
|
|
local magic, file_samples, pos = (">c4I4"):unpack(data)
|
|
if magic ~= "qoaf" then error("Not a QOA file", 2) end
|
|
local file_channels, file_sampleRate = (">BI3"):unpack(data, pos)
|
|
local retval = setmetatable({sampleRate = file_sampleRate, data = {}, metadata = {}, info = {bitDepth = 16, dataType = "signed"}}, Audio_mt) ---@type aukit.Audio
|
|
local lms = {}
|
|
for i = 1, file_channels do
|
|
retval.data[i] = {}
|
|
lms[i] = {history = {}, weights = {}}
|
|
end
|
|
|
|
local start = os_epoch "utc"
|
|
local sample_pos = 0
|
|
while pos + 16 * file_channels + 8 <= #data and sample_pos < file_samples do
|
|
-- from https://github.com/phoboslab/qoa/blob/master/qoa.h
|
|
-- MIT license - Copyright (c) 2023 Dominic Szablewski
|
|
|
|
-- Read and verify the frame header
|
|
local channels, samplerate, samples, frame_size
|
|
channels, samplerate, samples, frame_size, pos = (">BI3I2I2"):unpack(data, pos)
|
|
|
|
local data_size = frame_size - 8 - 4 * 4 * channels
|
|
local num_slices = math_floor(data_size / 8)
|
|
local max_total_samples = num_slices * 20
|
|
|
|
if
|
|
channels ~= file_channels or
|
|
samplerate ~= file_sampleRate or
|
|
frame_size > #data - pos + 1 or
|
|
samples * channels > max_total_samples
|
|
then
|
|
--error("Bad frame data", 2)
|
|
break
|
|
end
|
|
|
|
-- Read the LMS state: 4 x 2 bytes history, 4 x 2 bytes weights per channel
|
|
for c = 1, channels do
|
|
lms[c].history = {(">i2i2i2i2"):unpack(data, pos)}
|
|
pos = table.remove(lms[c].history)
|
|
lms[c].weights = {(">i2i2i2i2"):unpack(data, pos)}
|
|
pos = table.remove(lms[c].weights)
|
|
end
|
|
|
|
-- Decode all slices for all channels in this frame
|
|
for sample_index = 1, samples, 20 do
|
|
for c = 1, channels do
|
|
local sliceH, sliceL
|
|
sliceH, sliceL, pos = (">I4I4"):unpack(data, pos)
|
|
|
|
local scalefactor = bit32_extract(sliceH, 28, 4)
|
|
|
|
for si = sample_index, sample_index + 19 do
|
|
local predicted = qoa_lms_predict(lms[c])
|
|
local quantized = bit32_extract(sliceH, 25, 3)
|
|
local dequantized = qoa_dequant_tab[scalefactor][quantized]
|
|
--print(predicted, dequantized)
|
|
local reconstructed = math_min(math_max(predicted + dequantized, -32768), 32767)
|
|
|
|
retval.data[c][sample_pos + si] = reconstructed / (reconstructed < 0 and 32768 or 32767)
|
|
sliceH = bit32_lshift(sliceH, 3) + bit32_extract(sliceL, 29, 3)
|
|
sliceL = bit32_lshift(sliceL, 3)
|
|
|
|
qoa_lms_update(lms[c], reconstructed, dequantized)
|
|
end
|
|
end
|
|
end
|
|
sample_pos = sample_pos + samples
|
|
if os_epoch "utc" - start > 3000 then start = os_epoch "utc" sleep(0) end
|
|
end
|
|
return retval
|
|
end
|
|
|
|
--- Creates a new empty audio object with the specified duration.
|
|
---@param duration number The length of the audio in seconds
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@return aukit.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 = {}, metadata = {}, info = {}}, 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.
|
|
---@param frequency number The frequency of the tone in Hertz
|
|
---@param duration number The length of the audio in seconds
|
|
---@param amplitude? number The amplitude of the audio from 0.0 to 1.0
|
|
---@param waveType? "sine"|"triangle"|"sawtooth"|"square" The type of wave to generate
|
|
---@param duty? number The duty cycle of the square wave if selected; ignored otherwise
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@return aukit.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 = {}, metadata = {}, info = {}}, 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.
|
|
---@param duration number The length of the audio in seconds
|
|
---@param amplitude? number The amplitude of the audio from 0.0 to 1.0
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@return aukit.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 = {}, metadata = {}, info = {}}, Audio_mt)
|
|
local random = math.random
|
|
for c = 1, channels do
|
|
local l = {}
|
|
for i = 1, duration * sampleRate do l[i] = (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.
|
|
---@param data number[] The PCM data to pack
|
|
---@param bitDepth? number The bit depth of the audio (8, 16, 24, 32); if `dataType` is "float" then this must be 32
|
|
---@param dataType? "signed"|"unsigned"|"float" The type of each sample
|
|
---@param bigEndian? boolean Whether the data should be big-endian or little-endian
|
|
---@return 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 = str_sub(format, 1, 1) .. str_sub(format, 2):rep(512)
|
|
local retval = ""
|
|
for i = 1, #data, 512 do
|
|
if #data < i + 512 then retval = retval .. str_pack(str_rep(format, #data % 512), table_unpack(data, i, #data))
|
|
else retval = retval .. str_pack(formatChunk, table_unpack(data, i, i+511)) end
|
|
end
|
|
return retval
|
|
end
|
|
|
|
---@alias speaker {playAudio: fun(samples: number[], volume?: number)}
|
|
|
|
--- Plays back stream functions created by one of the `aukit.stream` functions
|
|
--- or `Audio.stream`.
|
|
---@param callback fun():number[][]|nil The iterator function that returns each chunk
|
|
---@param progress? fun(pos:number) A callback to report progress to
|
|
--- the caller; if omitted then this argument is the first speaker
|
|
---@param volume? number The volume to play the audio at; if omitted then
|
|
--- this argument is the second speaker (if provided)
|
|
---@param ... speaker The speakers to play on
|
|
function aukit.play(callback, progress, volume, ...)
|
|
expect(1, callback, "function")
|
|
expect(2, progress, "function", "table")
|
|
expect(3, volume, "number", "table", "nil")
|
|
local speakers = {...}
|
|
if type(volume) == "table" then
|
|
table_insert(speakers, 1, volume)
|
|
volume = nil
|
|
end
|
|
if type(progress) == "table" then
|
|
table_insert(speakers, 1, progress)
|
|
progress = nil
|
|
end
|
|
if #speakers == 0 then error("bad argument #2 (expected speakers, got nil)", 2) end
|
|
local chunks = {}
|
|
local complete = false
|
|
local a, b = coroutine.create(function()
|
|
for chunk, pos in callback do chunks[#chunks+1] = {chunk, pos} coroutine.yield(speakers) end
|
|
complete = true
|
|
end), coroutine.create(function()
|
|
while not complete or #chunks > 0 do
|
|
while not chunks[1] do if complete then return end coroutine.yield(speakers) end
|
|
local pchunk = table_remove(chunks, 1)
|
|
local fn = {}
|
|
if progress then progress(pchunk[2]) end
|
|
pchunk = pchunk[1]
|
|
local chunklist = {}
|
|
if #pchunk[1] < 96000 then chunklist = {pchunk}
|
|
else
|
|
for i = 0, #pchunk[1] - 1, 48000 do
|
|
local chunk = {}
|
|
chunklist[#chunklist+1] = chunk
|
|
for j = 1, #pchunk do
|
|
local s, c = pchunk[j], {}
|
|
chunk[j] = c
|
|
for k = 1, 48000 do c[k] = s[k+i] end
|
|
end
|
|
end
|
|
end
|
|
for _, chunk in ipairs(chunklist) do
|
|
for i, v in ipairs(speakers) do fn[i] = function()
|
|
local name = peripheral.getName(v)
|
|
if _HOST:find("CraftOS-PC v2.6.4") and config and not config.get("standardsMode") then
|
|
v.playAudio(chunk[i] or chunk[1], volume)
|
|
repeat until select(2, os_pullEvent("speaker_audio_empty")) == name
|
|
else while not v.playAudio(chunk[i] or chunk[1], volume) do
|
|
repeat until select(2, os_pullEvent("speaker_audio_empty")) == name
|
|
end end
|
|
end end
|
|
parallel.waitForAll(table_unpack(fn))
|
|
end
|
|
end
|
|
end)
|
|
local ok, af, bf
|
|
local aq, bq = {{}}, {{}}
|
|
repeat
|
|
if #aq > 0 then
|
|
local event = table_remove(aq, 1)
|
|
if af == speakers then
|
|
af = nil
|
|
table_insert(aq, 1, event)
|
|
end
|
|
if af == nil or event[1] == af then
|
|
ok, af = coroutine.resume(a, table_unpack(event, 1, event.n))
|
|
if not ok then error(af, 2) end
|
|
end
|
|
end
|
|
if #bq > 0 then
|
|
local event = table_remove(bq, 1)
|
|
if bf == speakers then
|
|
bf = nil
|
|
table_insert(bq, 1, event)
|
|
end
|
|
if bf == nil or event[1] == bf then
|
|
ok, bf = coroutine.resume(b, table_unpack(event, 1, event.n))
|
|
if not ok then error(bf, 2) end
|
|
end
|
|
end
|
|
if coroutine.status(b) == "suspended" and (#aq == 0 or #bq == 0) then
|
|
if af ~= nil and bf ~= nil then
|
|
local event = table_pack(os_pullEvent())
|
|
aq[#aq+1] = event
|
|
bq[#bq+1] = event
|
|
else
|
|
os_queueEvent("__queue_end")
|
|
while true do
|
|
local event = table_pack(os_pullEvent())
|
|
if event[1] == "__queue_end" then break end
|
|
aq[#aq+1] = event
|
|
bq[#bq+1] = event
|
|
end
|
|
end
|
|
end
|
|
until coroutine.status(b) == "dead" or complete
|
|
while coroutine.status(b) == "suspended" and #bq > 0 do
|
|
local event = table_remove(bq, 1)
|
|
if bf == nil or event[1] == bf then
|
|
ok, bf = coroutine.resume(b, table_unpack(event, 1, event.n))
|
|
if not ok then error(bf, 2) end
|
|
end
|
|
end
|
|
while coroutine.status(b) == "suspended" do
|
|
ok, bf = coroutine.resume(b, os_pullEvent())
|
|
if not ok then error(bf, 2) end
|
|
end
|
|
end
|
|
|
|
--- An asynchronous player created by `aukit.player`.
|
|
---@class aukit.Player
|
|
---@field isPaused boolean Whether the player is paused.
|
|
---@field position number The current position of the audio, in seconds
|
|
---@field volume number The volume of the audio
|
|
---@field loaderTask Task The task that is loading the audio
|
|
---@field playerTask Task The task that is playing the audio
|
|
---@field speakers speaker[] The speakers the audio is playing on
|
|
---@field lastPlayback {pos:number,time:number} Internal playback positions for pausing
|
|
---@field invalidate boolean Internal flag to invalidate current chunk
|
|
local Player = {}
|
|
local Player_mt = {__index = Player, __name = "aukit.player"}
|
|
|
|
--- Pauses the player if it's playing.
|
|
function Player:pause()
|
|
if self.isPaused then return end
|
|
if not self.playerTask then error("Player is stopped", 2) end
|
|
local time = os_epoch "utc"
|
|
self.isPaused = true
|
|
self.invalidate = true
|
|
for _, v in ipairs(self.speakers) do v.stop() end
|
|
self.position = (self.lastPlayback.pos + ((time - self.lastPlayback.time) * 48) - 1) / 48000
|
|
end
|
|
|
|
--- Plays the audio if it's paused.
|
|
function Player:play()
|
|
if not self.isPaused then return end
|
|
if not self.playerTask then error("Player is stopped", 2) end
|
|
self.isPaused = false
|
|
end
|
|
|
|
--- Returns the estimated current position of audio playback.
|
|
---@return number pos The position of the audio, in seconds
|
|
function Player:livePosition()
|
|
if not self.playerTask then error("Player is stopped", 2) end
|
|
if not self.lastPlayback then return 0 end
|
|
if self.isPaused then return self.position end
|
|
return (self.lastPlayback.pos + ((os_epoch "utc" - self.lastPlayback.time) * 48) - 1) / 48000
|
|
end
|
|
|
|
--- Seeks the current audio playback position. If the requested position isn't
|
|
--- loaded yet, the player will hang until the audio is loaded. (If the position
|
|
--- is out of bounds, the player will not continue.)
|
|
---@param pos number The position to seek to, in seconds
|
|
function Player:seek(pos)
|
|
expect.range(pos, 0)
|
|
if not self.playerTask then error("Player is stopped", 2) end
|
|
self.position = pos
|
|
self.invalidate = true
|
|
for _, v in ipairs(self.speakers) do v.stop() end
|
|
end
|
|
|
|
--- Stops the audio playback. This kills all tasks and invalidates the player.
|
|
function Player:stop()
|
|
if not self.playerTask then error("Player is stopped", 2) end
|
|
self.playerTask:remove()
|
|
if self.loaderTask then self.loaderTask:remove() end
|
|
self.playerTask = nil
|
|
self.loaderTask = nil
|
|
end
|
|
|
|
--- Creates a player object that runs asynchronously. This requires the
|
|
--- [Taskmaster](https://gist.github.com/MCJack123/1678fb2c240052f1480b07e9053d4537)
|
|
--- library to function.
|
|
---@param loop Taskmaster The Taskmaster loop to start the player on
|
|
---@param callback fun():number[][]|nil The iterator function that returns each chunk
|
|
---@param volume? number The volume to play the audio at; if omitted then
|
|
--- this argument is the second speaker (if provided)
|
|
---@param ... speaker The speakers to play on
|
|
---@return aukit.Player player The player object to control playback with
|
|
function aukit.player(loop, callback, volume, ...)
|
|
expect(1, loop, "table")
|
|
expect(2, callback, "function")
|
|
expect(3, volume, "number", "table")
|
|
local speakers = {...}
|
|
if type(volume) == "table" then
|
|
table_insert(speakers, 1, volume)
|
|
volume = nil
|
|
end
|
|
if #speakers == 0 then error("bad argument #3 (expected speakers, got nil)", 2) end
|
|
local player = setmetatable({
|
|
isPaused = false,
|
|
position = 0,
|
|
volume = volume,
|
|
speakers = speakers
|
|
}, Player_mt)
|
|
local decoded = {}
|
|
local id = tostring(player)
|
|
player.loaderTask = loop:addTask(function()
|
|
local n = 0
|
|
for chunk, pos in callback do
|
|
for j = 1, #chunk do
|
|
local c, d = chunk[j], decoded[j]
|
|
if not d then d = {} decoded[j] = d end
|
|
for i = 1, #c do
|
|
d[n+i] = c[i]
|
|
end
|
|
end
|
|
n = n + #chunk[1]
|
|
os_queueEvent("aukit.loader_next", id)
|
|
repeat local _, param = os_pullEvent("aukit.loader_next")
|
|
until param == id
|
|
end
|
|
player.loaderTask = nil
|
|
end)
|
|
player.playerTask = loop:addTask(function()
|
|
while true do
|
|
local spos = math_floor(player.position * 48000) + 1
|
|
if not player.isPaused and decoded[1] and decoded[1][spos] then
|
|
local chunk = {}
|
|
for j = 1, #decoded do
|
|
chunk[j] = {table_unpack(decoded[j], spos, math_min(spos + 47999, #decoded[j]))}
|
|
end
|
|
player.position = player.position + #chunk[1] / 48000
|
|
local fn = {}
|
|
for i, v in ipairs(speakers) do fn[i] = function()
|
|
local name = peripheral.getName(v)
|
|
if _HOST:find("CraftOS-PC v2.6.4") and config and not config.get("standardsMode") then
|
|
v.playAudio(chunk[i] or chunk[1], volume)
|
|
repeat until select(2, os_pullEvent("speaker_audio_empty")) == name
|
|
else while not v.playAudio(chunk[i] or chunk[1], volume) do
|
|
repeat until select(2, os_pullEvent("speaker_audio_empty")) == name
|
|
if player.invalidate then chunk = nil return end
|
|
end end
|
|
player.lastPlayback = {time = os.epoch "utc", pos = spos}
|
|
end end
|
|
parallel.waitForAll(table_unpack(fn))
|
|
player.invalidate = false
|
|
elseif not player.isPaused and player.loaderTask == nil and spos == #decoded[1] + 1 then
|
|
player.playerTask = nil
|
|
return
|
|
else
|
|
os_pullEvent()
|
|
end
|
|
end
|
|
end)
|
|
return player
|
|
end
|
|
|
|
local datafmts = {
|
|
{"bbbbbbbb", 8, "signed"},
|
|
{"BBBBBBBB", 8, "unsigned"},
|
|
{"hhhhhhhh", 16, "signed"},
|
|
{"iiiiiiii", 32, "signed"},
|
|
{"ffffffff", 32, "float"},
|
|
{"i3i3i3i3i3i3i3i3", 24, "signed"},
|
|
{"IIIIIIII", 32, "unsigned"},
|
|
{"I3I3I3I3I3I3I3I3", 24, "unsigned"},
|
|
{"HHHHHHHH", 16, "unsigned"},
|
|
}
|
|
|
|
--- Detect the type of audio file from the specified data. This uses heuristic
|
|
--- detection methods to attempt to find the correct data type for files without
|
|
--- headers. It is not recommended to rely on the data type/bit depth reported
|
|
--- for PCM files - they are merely a suggestion.
|
|
---@param data string The audio file to check
|
|
---@return string|nil type The type of audio file detected, or `nil` if none could be found ("pcm"|"dfpwm"|"mdfpwm"|"wav"|"aiff"|"au"|"flac")
|
|
---@return number|nil bitDepth The bit depth for PCM data, if the type is "pcm" and the bit depth can be detected
|
|
---@return string|nil dataType The data type for PCM data, if the type is "pcm" and the type can be detected ("signed"|"unsigned"|"float")
|
|
function aukit.detect(data)
|
|
expect(1, data, "string")
|
|
if data:match "^RIFF....WAVE" then return "wav"
|
|
elseif data:match "^FORM....AIF[FC]" then return "aiff"
|
|
elseif data:match "^%.snd" then return "au"
|
|
elseif data:match "^fLaC" then return "flac"
|
|
elseif data:match "^MDFPWM\3" then return "mdfpwm"
|
|
elseif data:match "^qoaf" then return "qoa"
|
|
else
|
|
-- Detect data type otherwise
|
|
-- This expects the start or end of the audio to be (near) silence
|
|
for _, bits in pairs(datafmts) do
|
|
local mid, gap = bits[3] == "unsigned" and 2^(bits[2]-1) or 0, bits[3] == "float" and 0.001 or 8 * 2^(bits[2]-8)
|
|
local nums = {pcall(str_unpack, bits[1], data)}
|
|
nums[#nums] = nil
|
|
if table_remove(nums, 1) then
|
|
local allzero, ok = true, true
|
|
for _, v in ipairs(nums) do
|
|
if v ~= mid then allzero = false end
|
|
if v < mid - gap or v > mid + gap then ok = false break end
|
|
end
|
|
---@diagnostic disable-next-line return-type-mismatch
|
|
if ok and not allzero then return "pcm", table_unpack(bits, 2) end
|
|
end
|
|
nums = {pcall(str_unpack, bits[1], data, #data - bits[2])}
|
|
nums[#nums] = nil
|
|
if table_remove(nums, 1) then
|
|
local allzero, ok = true, true
|
|
for _, v in ipairs(nums) do
|
|
if v ~= mid then allzero = false end
|
|
if v < mid - gap or v > mid + gap then ok = false break end
|
|
end
|
|
---@diagnostic disable-next-line return-type-mismatch
|
|
if ok and not allzero then return "pcm", table_unpack(bits, 2) end
|
|
end
|
|
end
|
|
if data:match(("\x55"):rep(12)) or data:match(("\xAA"):rep(12)) then return "dfpwm" end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--[[
|
|
..######..########.########..########....###....##.....##
|
|
.##....##....##....##.....##.##.........##.##...###...###
|
|
.##..........##....##.....##.##........##...##..####.####
|
|
..######.....##....########..######...##.....##.##.###.##
|
|
.......##....##....##...##...##.......#########.##.....##
|
|
.##....##....##....##....##..##.......##.....##.##.....##
|
|
..######.....##....##.....##.########.##.....##.##.....##
|
|
]]
|
|
|
|
--- aukit.stream
|
|
---@section aukit.stream
|
|
|
|
--- Returns an iterator to stream raw PCM audio in CC format. aukit.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.
|
|
---@param data string|table|function The audio data, either as a raw string, a
|
|
--- table of values (in the format specified by `bitDepth` and `dataType`), or a
|
|
--- function that returns either of those types. Functions will be called at
|
|
--- least once before returning to get the type of data to use.
|
|
---@param bitDepth? number The bit depth of the audio (8, 16, 24, 32); if `dataType` is "float" then this must be 32
|
|
---@param dataType? "signed"|"unsigned"|"float" The type of each sample
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@param bigEndian? boolean Whether the audio is big-endian or little-endian; ignored if data is a table
|
|
---@param mono? boolean Whether to mix the audio down to mono
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number _ The total length of the audio in seconds, or the length of
|
|
--- the first chunk if using a function
|
|
function aukit.stream.pcm(data, bitDepth, dataType, channels, sampleRate, bigEndian, mono)
|
|
local fn, complete
|
|
if type(data) == "function" then fn, data = data, data() end
|
|
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)
|
|
if channels == 1 then mono = false end
|
|
local byteDepth = bitDepth / 8
|
|
local len = (#data / (type(data) == "table" and 1 or byteDepth)) / channels
|
|
local csize = jit and 7680 or 32768
|
|
local csizeb = csize * byteDepth
|
|
local bitDir = bigEndian and ">" or "<"
|
|
local sformat = dataType == "float" and "f" or ((dataType == "signed" and "i" or "I") .. byteDepth)
|
|
local format = bitDir .. str_rep(sformat, 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()
|
|
if complete then return nil end
|
|
if fn and pos > #data then
|
|
data, pos = fn(), 1
|
|
if not data then complete = true return nil end
|
|
end
|
|
local s = data[pos]
|
|
pos = pos + 1
|
|
return s / (s < 0 and maxValue or maxValue-1)
|
|
end
|
|
elseif dataType == "unsigned" then
|
|
function read()
|
|
if complete then return nil end
|
|
if fn and pos > #data then
|
|
data, pos = fn(), 1
|
|
if not data then complete = true return nil end
|
|
end
|
|
local s = data[pos]
|
|
pos = pos + 1
|
|
return (s - 128) / (s < 128 and maxValue or maxValue-1)
|
|
end
|
|
else
|
|
function read()
|
|
if complete then return nil end
|
|
if fn and pos > #data then
|
|
data, pos = fn(), 1
|
|
if not data then complete = true return nil end
|
|
end
|
|
local s = data[pos]
|
|
pos = pos + 1
|
|
return s
|
|
end
|
|
end
|
|
elseif dataType == "float" then
|
|
function read()
|
|
if complete then return nil end
|
|
if pos > #tmp then
|
|
if fn and spos > #data then
|
|
data, spos = fn(), 1
|
|
if not data then complete = true return nil end
|
|
end
|
|
if spos + csizeb > #data then
|
|
local f = bitDir .. str_rep(sformat, (#data - spos + 1) / byteDepth)
|
|
tmp = {str_unpack(f, data, spos)}
|
|
spos = tmp[#tmp]
|
|
tmp[#tmp] = nil
|
|
else
|
|
tmp = {str_unpack(format, 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 complete then return nil end
|
|
if pos > #tmp then
|
|
if fn and spos > #data then
|
|
data, spos = fn(), 1
|
|
if not data then complete = true return nil end
|
|
end
|
|
if spos + csizeb > #data then
|
|
local f = bitDir .. str_rep(sformat, (#data - spos + 1) / byteDepth)
|
|
tmp = {str_unpack(f, data, spos)}
|
|
spos = tmp[#tmp]
|
|
tmp[#tmp] = nil
|
|
else
|
|
tmp = {str_unpack(format, 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 complete then return nil end
|
|
if pos > #tmp then
|
|
if fn and spos > #data then
|
|
data, spos = fn(), 1
|
|
if not data then complete = true return nil end
|
|
end
|
|
if spos + csizeb > #data then
|
|
local f = bitDir .. str_rep(sformat, (#data - spos + 1) / byteDepth)
|
|
tmp = {str_unpack(f, data, spos)}
|
|
spos = tmp[#tmp]
|
|
tmp[#tmp] = nil
|
|
else
|
|
tmp = {str_unpack(format, 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 lp_alpha = 1 - math.exp(-(sampleRate / 96000) * 2 * math_pi)
|
|
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 or complete 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()
|
|
local ls = {}
|
|
for y = 1, #d do
|
|
local s = chunk[y][0] or 0
|
|
ls[y] = s / (s < 0 and 128 or 127)
|
|
end
|
|
for i = 1, 48000 do
|
|
for y = 1, #d do
|
|
local x = ((i - 1) / ratio) + 1
|
|
local s
|
|
if x % 1 == 0 then s = d[y][x]
|
|
else s = interp(d[y], x) end
|
|
local ns = ls[y] + lp_alpha * (s - ls[y])
|
|
chunk[y][i] = clamp(ns * (ns < 0 and 128 or 127), -128, 127)
|
|
ls[y] = s
|
|
end
|
|
end
|
|
end)
|
|
if #chunk[1] == 0 then return nil end
|
|
n = n + #chunk[1]
|
|
for y = 1, #d do
|
|
if aukit.defaultInterpolation == "sinc" then
|
|
local t, l = {}, #d[y]
|
|
for i = -sincWindowSize, 0 do
|
|
t[i] = d[y][l + i]
|
|
end
|
|
d[y] = setmetatable(t, getmetatable(d[y]))
|
|
else
|
|
local l2, l1 = d[y][#d[y]-1], d[y][#d[y]]
|
|
d[y] = setmetatable({}, getmetatable(d[y]))
|
|
d[y][-1], d[y][0] = l2, l1
|
|
end
|
|
end
|
|
return chunk, (n - #chunk[1]) / 48000
|
|
end, len / sampleRate
|
|
end
|
|
|
|
--- Returns an iterator to stream audio from DFPWM data. aukit.Audio will automatically
|
|
--- be resampled to 48 kHz. Multiple channels are expected to be interleaved in
|
|
--- the encoded DFPWM data.
|
|
---@param data string|fun():string The DFPWM data to decode, or a function
|
|
--- returning chunks to decode
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param mono? boolean Whether to mix the audio down to mono
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number|nil _ The total length of the audio in seconds, or nil if data
|
|
--- is a function
|
|
function aukit.stream.dfpwm(data, sampleRate, channels, mono)
|
|
expect(1, data, "string", "function")
|
|
sampleRate = expect(2, sampleRate, "number", "nil") or 48000
|
|
channels = expect(3, channels, "number", "nil") or 1
|
|
expect.range(sampleRate, 1)
|
|
expect.range(channels, 1)
|
|
if channels == 1 then mono = false end
|
|
local decoder = dfpwm.make_decoder()
|
|
local pos = 1
|
|
local last = 0
|
|
local isstr = type(data) == "string"
|
|
local buf = ""
|
|
return function()
|
|
local d
|
|
if isstr then
|
|
if pos > #data then return nil end
|
|
d = str_sub(data, pos, pos + 6000 * channels)
|
|
else
|
|
while #buf < sampleRate / 8 * channels do
|
|
local chunk = data()
|
|
if not chunk then
|
|
if #buf == 0 then return nil
|
|
else break end
|
|
end
|
|
buf = buf .. chunk
|
|
end
|
|
d = str_sub(buf, 1, 6000 * channels)
|
|
buf = str_sub(buf, 6000 * channels + 1)
|
|
end
|
|
local audio = decoder(d)
|
|
if audio == nil or #audio == 0 then return nil end
|
|
audio[0], last = last, audio[#audio]
|
|
os_queueEvent("nosleep")
|
|
repeat until "nosleep" == os_pullEvent()
|
|
local ratio = 48000 / sampleRate
|
|
local newlen = #audio * ratio
|
|
local interp = interpolate[aukit.defaultInterpolation]
|
|
local lines = {{}}
|
|
if not mono then for j = 1, channels do lines[j] = {} end end
|
|
for i = 1, newlen, channels do
|
|
local n = 0
|
|
for j = 1, channels do
|
|
local x = (i - 1) / ratio + 1
|
|
local s
|
|
if x % 1 == 0 then s = audio[x]
|
|
else s = clamp(interp(audio, x), -128, 127) end
|
|
if mono then n = n + s
|
|
else lines[j][math_ceil(i / channels)] = s end
|
|
end
|
|
if mono then lines[1][math_ceil(i / channels)] = n / channels end
|
|
end
|
|
os_queueEvent("nosleep")
|
|
repeat until "nosleep" == os_pullEvent()
|
|
local p = pos
|
|
pos = pos + 6000 * channels
|
|
return lines, p * 8 / sampleRate / channels
|
|
end, isstr and #data * 8 / sampleRate / channels or nil
|
|
end
|
|
|
|
--- Returns an iterator to stream audio from MDFPWMv3 data.
|
|
---@param data string|fun():string The MDFPWM data to decode, or a function
|
|
--- returning chunks to decode
|
|
---@param mono? boolean Whether to mix the audio down to mono
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number|nil _ The total length of the audio in seconds, or nil if data
|
|
--- is a function
|
|
function aukit.stream.mdfpwm(data, mono)
|
|
expect(1, data, "string", "function")
|
|
local decoderL, decoderR = dfpwm.make_decoder(), dfpwm.make_decoder()
|
|
local isstr = type(data) == "string"
|
|
local pos = 1
|
|
local headerSize = 0
|
|
local length
|
|
local buf = ""
|
|
local _
|
|
if isstr then
|
|
if data:sub(1, 7) ~= "MDFPWM\3" then error("bad argument #1 (invalid MDFPWM data)", 2) end
|
|
length, _, _, _, pos = str_unpack("<Is1s1s1", data, 8)
|
|
headerSize = pos - 1
|
|
else
|
|
repeat buf = buf .. data()
|
|
until pcall(string.unpack, "<c7Is1s1s1", buf)
|
|
if str_sub(buf, 1, 7) ~= "MDFPWM\3" then error("bad argument #1 (invalid MDFPWM data)", 2) end
|
|
length, _, _, _, pos = str_unpack("<Is1s1s1", buf, 8)
|
|
buf = str_sub(buf, pos)
|
|
pos = 1
|
|
end
|
|
return function()
|
|
local dL, dR
|
|
if isstr then
|
|
if pos > #data then return nil end
|
|
dL = str_sub(data, pos, pos + 5999)
|
|
dR = str_sub(data, pos + 6000, pos + 11999)
|
|
else
|
|
while #buf < 12000 do
|
|
local chunk = data()
|
|
if not chunk then
|
|
if #buf == 0 then return nil
|
|
else break end
|
|
end
|
|
buf = buf .. chunk
|
|
end
|
|
dL = str_sub(buf, 1, 6000)
|
|
dR = str_sub(buf, 6001, 12000)
|
|
buf = str_sub(buf, 12001)
|
|
end
|
|
local audioL = decoderL(dL)
|
|
if audioL == nil or #audioL == 0 then return nil end
|
|
local audioR = decoderR(dR)
|
|
if audioR == nil or #audioR == 0 then return nil end
|
|
os_queueEvent("nosleep")
|
|
repeat until "nosleep" == os_pullEvent()
|
|
if pos - headerSize + 12000 > length then
|
|
for i = (length / 2) % 6000 + 1, 6000 do
|
|
audioL[i], audioR[i] = nil
|
|
end
|
|
end
|
|
local lines
|
|
if mono then
|
|
local l = {}
|
|
lines = {l}
|
|
for i = 1, 48000 do
|
|
l[i] = clamp(math_floor(audioL[i] + audioR[i] / 2), -128, 127)
|
|
end
|
|
else lines = {audioL, audioR} end
|
|
os_queueEvent("nosleep")
|
|
repeat until "nosleep" == os_pullEvent()
|
|
local p = pos - headerSize
|
|
pos = pos + #dL + #dR
|
|
return lines, p / 12000
|
|
end, length / 12000
|
|
end
|
|
|
|
--- Returns an iterator to stream audio from Microsoft ADPCM data. aukit.Audio will
|
|
--- automatically be resampled to 48 kHz.
|
|
---@param input string|fun():string The audio data as a raw string or
|
|
--- reader function
|
|
---@param blockAlign number The number of bytes in each block
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@param mono? boolean Whether to mix the audio down to mono
|
|
---@param coefficients? table Two lists of coefficients to use
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number|nil _ The total length of the audio in seconds, or nil if data
|
|
--- is a function
|
|
function aukit.stream.msadpcm(input, blockAlign, channels, sampleRate, mono, coefficients)
|
|
expect(1, input, "string", "function")
|
|
expect(2, blockAlign, "number")
|
|
channels = expect(3, channels, "number", "nil") or 1
|
|
sampleRate = expect(4, sampleRate, "number", "nil") or 48000
|
|
expect(5, mono, "boolean", "nil")
|
|
expect(6, coefficients, "table", "nil")
|
|
expect.range(sampleRate, 1)
|
|
local isfunc = type(input) == "function"
|
|
local coeff1, coeff2
|
|
if coefficients then
|
|
if type(coefficients[1]) ~= "table" then error("bad argument #5 (first entry is not a table)", 2) end
|
|
if type(coefficients[2]) ~= "table" then error("bad argument #5 (second entry is not a table)", 2) end
|
|
if #coefficients[1] ~= #coefficients[2] then error("bad argument #5 (lists are not the same length)", 2) end
|
|
coeff1, coeff2 = {}, {}
|
|
for i, v in ipairs(coefficients[1]) do
|
|
if type(v) ~= "number" then error("bad entry #" .. i .. " in coefficient list 1 (expected number, got " .. type(v) .. ")", 2) end
|
|
coeff1[i-1] = v
|
|
end
|
|
for i, v in ipairs(coefficients[2]) do
|
|
if type(v) ~= "number" then error("bad entry #" .. i .. " in coefficient list 2 (expected number, got " .. type(v) .. ")", 2) end
|
|
coeff2[i-1] = v
|
|
end
|
|
else coeff1, coeff2 = {[0] = 256, 512, 0, 192, 240, 460, 392}, {[0] = 0, -256, 0, 64, 0, -208, -232} end
|
|
local ratio = 48000 / sampleRate
|
|
local interp = interpolate[aukit.defaultInterpolation]
|
|
local n, pos = 1, 0
|
|
local data = isfunc and input() or input
|
|
if channels == 2 then
|
|
local samplesPerBlock = blockAlign - 14
|
|
local iterPerSecond = math_ceil(sampleRate / samplesPerBlock)
|
|
local bytesPerSecond = blockAlign * iterPerSecond
|
|
local newlen = math_floor(samplesPerBlock * ratio)
|
|
local lastL, lastR
|
|
return function()
|
|
if data == nil then return nil end
|
|
local target = n + bytesPerSecond
|
|
local retval = {{}, not mono and {} or nil}
|
|
local rp = 0
|
|
local start = os_epoch "utc"
|
|
while n < target do
|
|
if os_epoch "utc" - start > 3000 then
|
|
os_queueEvent("nosleep")
|
|
repeat until "nosleep" == os_pullEvent()
|
|
start = os_epoch "utc"
|
|
end
|
|
if isfunc and n > #data then
|
|
pos = pos + #data
|
|
n = n - #data
|
|
data = input()
|
|
if data == nil then return nil end
|
|
end
|
|
if n > #data then break end
|
|
local left, right = {}, {}
|
|
if lastL then for i = 1, #lastL do
|
|
left[i-#lastL-1] = lastL[i]
|
|
right[i-#lastR-1] = lastR[i]
|
|
end end
|
|
local predictorIndexL, predictorIndexR, deltaL, deltaR, sample1L, sample1R, sample2L, sample2R = str_unpack("<BBhhhhhh", data, n)
|
|
local c1L, c2L, c1R, c2R = coeff1[predictorIndexL], coeff2[predictorIndexL], coeff1[predictorIndexR], coeff2[predictorIndexR]
|
|
left[1] = math_floor(sample2L / (sample2L < 0 and 128 or 127))
|
|
left[2] = math_floor(sample1L / (sample1L < 0 and 128 or 127))
|
|
right[1] = math_floor(sample2R / (sample2R < 0 and 128 or 127))
|
|
right[2] = math_floor(sample1R / (sample1R < 0 and 128 or 127))
|
|
for i = 14, blockAlign - 1 do
|
|
local b = str_byte(data, n+i)
|
|
local hi, lo = bit32_rshift(b, 4), bit32_band(b, 0x0F)
|
|
if hi >= 8 then hi = hi - 16 end
|
|
if lo >= 8 then lo = lo - 16 end
|
|
local predictor = clamp(math_floor((sample1L * c1L + sample2L * c2L) / 256) + hi * deltaL, -32768, 32767)
|
|
left[#left+1] = math_floor(predictor / (predictor < 0 and 128 or 127))
|
|
sample2L, sample1L = sample1L, predictor
|
|
deltaL = math_max(math_floor(msadpcm_adaption_table[hi] * deltaL / 256), 16)
|
|
predictor = clamp(math_floor((sample1R * c1R + sample2R * c2R) / 256) + lo * deltaR, -32768, 32767)
|
|
right[#right+1] = math_floor(predictor / (predictor < 0 and 128 or 127))
|
|
sample2R, sample1R = sample1R, predictor
|
|
deltaR = math_max(math_floor(msadpcm_adaption_table[lo] * deltaR / 256), 16)
|
|
end
|
|
lastL, lastR = left, right
|
|
for i = 1, newlen do
|
|
local x = (i - 1) / ratio + 1
|
|
local l, r
|
|
if x % 1 == 0 then l, r = left[x], right[x]
|
|
else l, r = interp(left, x), interp(right, x) end
|
|
if mono then retval[1][rp+i] = clamp(math_floor(l + r / 2), -128, 127)
|
|
else retval[1][rp+i], retval[2][rp+i] = clamp(math_floor(l), -128, 127), clamp(math_floor(r), -128, 127) end
|
|
end
|
|
rp = rp + newlen
|
|
n = n + blockAlign
|
|
end
|
|
if #retval[1] == 0 then return nil end
|
|
return retval, (n + pos) / bytesPerSecond
|
|
end, not isfunc and #data / blockAlign * samplesPerBlock / sampleRate or nil
|
|
elseif channels == 1 then
|
|
local samplesPerBlock = (blockAlign - 7) * 2
|
|
local iterPerSecond = math_ceil(sampleRate / samplesPerBlock)
|
|
local bytesPerSecond = blockAlign * iterPerSecond
|
|
local newlen = math_floor(samplesPerBlock * ratio)
|
|
return function()
|
|
if data == nil then return nil end
|
|
local target = n + bytesPerSecond
|
|
local retval = {{}}
|
|
local rp = 0
|
|
local start = os_epoch "utc"
|
|
while n < target do
|
|
if os_epoch "utc" - start > 3000 then
|
|
os_queueEvent("nosleep")
|
|
repeat until "nosleep" == os_pullEvent()
|
|
start = os_epoch "utc"
|
|
end
|
|
if isfunc and n > #data then
|
|
pos = pos + #data
|
|
n = n - #data
|
|
data = input()
|
|
if data == nil then return nil end
|
|
end
|
|
if n > #data then break end
|
|
local left = {}
|
|
local predictorIndex, delta, sample1, sample2 = str_unpack("<!1Bhhh", data)
|
|
local c1, c2 = coeff1[predictorIndex], coeff2[predictorIndex]
|
|
left[1] = sample2 / (sample2 < 0 and 128 or 127)
|
|
left[2] = sample1 / (sample1 < 0 and 128 or 127)
|
|
for i = 7, blockAlign - 1 do
|
|
local b = str_byte(data, n+i)
|
|
local hi, lo = bit32_rshift(b, 4), bit32_band(b, 0x0F)
|
|
if hi >= 8 then hi = hi - 16 end
|
|
if lo >= 8 then lo = lo - 16 end
|
|
local predictor = clamp(math_floor((sample1 * c1 + sample2 * c2) / 256) + hi * delta, -32768, 32767)
|
|
left[#left+1] = predictor / (predictor < 0 and 128 or 127)
|
|
sample2, sample1 = sample1, predictor
|
|
delta = math_max(math_floor(msadpcm_adaption_table[hi] * delta / 256), 16)
|
|
predictor = clamp(math_floor((sample1 * c1 + sample2 * c2) / 256) + lo * delta, -32768, 32767)
|
|
left[#left+1] = predictor / (predictor < 0 and 128 or 127)
|
|
sample2, sample1 = sample1, predictor
|
|
delta = math_max(math_floor(msadpcm_adaption_table[lo] * delta / 256), 16)
|
|
end
|
|
for i = 1, newlen do
|
|
local x = (i - 1) / ratio + 1
|
|
if x % 1 == 0 then retval[1][rp+i] = clamp(math_floor(left[x]), -128, 127)
|
|
else retval[1][rp+i] = clamp(math_floor(interp(left, x)), -128, 127) end
|
|
end
|
|
rp = rp + newlen
|
|
n = n + blockAlign
|
|
end
|
|
if #retval[1] == 0 then return nil end
|
|
return retval, (n + pos) / bytesPerSecond
|
|
end, not isfunc and #data / blockAlign * samplesPerBlock / sampleRate or nil
|
|
else error("Unsupported number of channels: " .. channels) end
|
|
end
|
|
|
|
--- Returns an iterator to stream data from IMA ADPCM data. aukit.Audio will
|
|
--- automatically be resampled to 48 kHz, and mixed to mono if desired. Data
|
|
--- *must* be in the interleaving format used in WAV files (i.e. periodic blocks
|
|
--- with 4/8-byte headers, channels alternating every 4 bytes, lower nibble first).
|
|
---@param input string|fun():string The audio data as a raw string or
|
|
--- reader function
|
|
---@param blockAlign number The number of bytes in each block
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@param mono? boolean Whether to mix the audio down to mono
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number|nil _ The total length of the audio in seconds, or nil if data
|
|
--- is a function
|
|
function aukit.stream.adpcm(input, blockAlign, channels, sampleRate, mono)
|
|
expect(1, input, "string", "function")
|
|
expect(2, blockAlign, "number")
|
|
channels = expect(3, channels, "number", "nil") or 1
|
|
sampleRate = expect(4, sampleRate, "number", "nil") or 48000
|
|
expect(5, mono, "boolean", "nil")
|
|
expect.range(sampleRate, 1)
|
|
local isfunc = type(input) == "function"
|
|
local ratio = 48000 / sampleRate
|
|
local interp = interpolate[aukit.defaultInterpolation]
|
|
local n, pos = 1, 0
|
|
local data = isfunc and input() or input
|
|
local samplesPerBlock = (blockAlign - 4 * channels) * 2 / channels
|
|
local iterPerSecond = math_ceil(sampleRate / samplesPerBlock)
|
|
local bytesPerSecond = blockAlign * iterPerSecond
|
|
local newlen = math_floor(samplesPerBlock * ratio)
|
|
local last
|
|
return function()
|
|
if data == nil then return nil end
|
|
local target = n + bytesPerSecond
|
|
local retval = {{}}
|
|
if not mono then for i = 2, channels do retval[i] = {} end end
|
|
local rp = 0
|
|
if isfunc and target > #data then
|
|
pos = pos + n - 1
|
|
target = target - n + 1
|
|
data = str_sub(data, n)
|
|
n = 1
|
|
while #data < target do
|
|
local d = input()
|
|
if not d then break end
|
|
data = data .. d
|
|
end
|
|
end
|
|
local start = os_epoch "utc"
|
|
while n < target do
|
|
if os_epoch "utc" - start > 3000 then
|
|
os_queueEvent("nosleep")
|
|
repeat until "nosleep" == os_pullEvent()
|
|
start = os_epoch "utc"
|
|
end
|
|
if n + channels * 4 > #data then break end
|
|
local d = {}
|
|
for i = 1, channels do d[i] = {} end
|
|
if last then for i = 1, channels do for j = 1, #last[i] do d[j-#last[i]-1] = last[i][j] end end end
|
|
local predictor, step_index, step = {}, {}, {}
|
|
for i = 1, channels do predictor[i], step_index[i] = str_unpack("<hB", data, n + (i - 1) * 4) end
|
|
for i = channels * 4, blockAlign, channels * 4 do
|
|
local p = (i - channels * 4) / channels * 2 + 1
|
|
if #data < n + i + channels*4 then break end
|
|
for j = 1, channels do
|
|
local num = str_unpack("<I", data, n + i + (j-1)*4)
|
|
for k = 0, 7 do
|
|
local nibble = bit32_extract(num, k*4, 4)
|
|
step[j] = ima_step_table[step_index[j]]
|
|
step_index[j] = clamp(step_index[j] + ima_index_table[nibble], 0, 88)
|
|
local diff = bit32_rshift((nibble % 8) * step[j], 2) + bit32_rshift(step[j], 3)
|
|
if nibble >= 8 then predictor[j] = clamp(predictor[j] - diff, -32768, 32767)
|
|
else predictor[j] = clamp(predictor[j] + diff, -32768, 32767) end
|
|
d[j][p+k] = predictor[j] / (predictor[j] < 0 and 128 or 127)
|
|
end
|
|
end
|
|
end
|
|
last = d
|
|
if #d[1] < samplesPerBlock then newlen = math_floor(#d[1] * ratio) end
|
|
for i = 1, newlen do
|
|
local x = (i - 1) / ratio + 1
|
|
local c = {}
|
|
if x % 1 == 0 then for j = 1, channels do c[j] = d[j][x] end
|
|
else for j = 1, channels do c[j] = interp(d[j], x) end end
|
|
if mono then
|
|
local n = 0
|
|
for j = 1, channels do n = n + c[j] end
|
|
retval[1][rp+i] = clamp(math_floor(n / channels), -128, 127)
|
|
else for j = 1, channels do retval[j][rp+i] = clamp(math_floor(c[j]), -128, 127) end end
|
|
end
|
|
rp = rp + newlen
|
|
n = n + blockAlign
|
|
end
|
|
if #retval[1] == 0 then return nil end
|
|
return retval, (n + pos) / bytesPerSecond
|
|
end, not isfunc and #data / blockAlign * samplesPerBlock / sampleRate or nil
|
|
end
|
|
|
|
--- Returns an iterator to stream data from u-law/A-law G.711 data. aukit.Audio will
|
|
--- automatically be resampled to 48 kHz, and mixed to mono if desired.
|
|
---@param input string|fun():string The audio data as a raw string or
|
|
--- reader function
|
|
---@param ulaw boolean Whether the audio uses u-law (true) or A-law (false).
|
|
---@param channels? number The number of channels present in the audio
|
|
---@param sampleRate? number The sample rate of the audio in Hertz
|
|
---@param mono? boolean Whether to mix the audio down to mono
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number|nil _ The total length of the audio in seconds, or nil if data
|
|
--- is a function
|
|
function aukit.stream.g711(input, ulaw, channels, sampleRate, mono)
|
|
expect(1, input, "string", "function")
|
|
expect(2, ulaw, "boolean")
|
|
channels = expect(3, channels, "number", "nil") or 1
|
|
sampleRate = expect(4, sampleRate, "number", "nil") or 8000
|
|
expect(5, mono, "boolean", "nil")
|
|
local csize = jit and 7680 or 32768
|
|
local xor = ulaw and 0xFF or 0x55
|
|
local isfunc = type(input) == "function"
|
|
local buf, pos = "", 1
|
|
local ratio = 48000 / sampleRate
|
|
local interp = interpolate[aukit.defaultInterpolation]
|
|
local last
|
|
return function()
|
|
local lp = pos
|
|
local retval = {}
|
|
for i = 1, channels do retval[i] = {} end
|
|
if last then for i = 1, channels do for j = 1, #last[i] do retval[j-#last[i]-1] = last[i][j] end end end
|
|
local data
|
|
if isfunc then
|
|
while #buf < sampleRate * channels do
|
|
local d = input()
|
|
if not input then
|
|
if #buf == 0 then return nil
|
|
else break end
|
|
end
|
|
buf = buf .. d
|
|
end
|
|
data = str_sub(buf, 1, sampleRate * channels)
|
|
buf = str_sub(buf, sampleRate * channels + 1)
|
|
else data = str_sub(input, pos, pos + sampleRate * channels - 1) end
|
|
pos = pos + sampleRate * channels
|
|
local start = os_epoch "utc"
|
|
for i = 1, #data, csize do
|
|
local bytes = {str_byte(data, i, i + csize - 1)}
|
|
for j = 1, #bytes do
|
|
local b = bit32_bxor(bytes[j], xor)
|
|
local m, e = bit32_band(b, 0x0F), bit32_extract(b, 4, 3)
|
|
if not ulaw and e == 0 then m = m * 4 + 2
|
|
else m = bit32_lshift(m * 2 + 33, e) end
|
|
if ulaw then m = m - 33 end
|
|
retval[(i+j-2) % channels + 1][math_floor((i+j-2) / channels + 1)] = m / (bit32_btest(b, 0x80) == ulaw and -0x40 or 0x40)
|
|
end
|
|
if os_epoch "utc" - start > 3000 then start = os_epoch "utc" sleep(0) end
|
|
end
|
|
last = {}
|
|
for j = 1, channels do last[j] = {} for i = 1, sincWindowSize do last[j][i] = retval[j][#retval[j]-30+i] end end
|
|
local newlen = math_floor(#retval[1] * ratio)
|
|
local resamp = {}
|
|
for j = 1, channels do resamp[j] = {} end
|
|
for i = 1, newlen do
|
|
local x = (i - 1) / ratio + 1
|
|
local c = {}
|
|
if x % 1 == 0 then for j = 1, channels do c[j] = retval[j][x] end
|
|
else for j = 1, channels do c[j] = interp(retval[j], x) end end
|
|
if mono then
|
|
local n = 0
|
|
for j = 1, channels do n = n + c[j] end
|
|
resamp[1][i] = clamp(math_floor(n / channels), -128, 127)
|
|
else for j = 1, channels do resamp[j][i] = clamp(math_floor(c[j]), -128, 127) end end
|
|
end
|
|
return resamp, (lp - 1) / sampleRate / channels
|
|
end, not isfunc and #input / sampleRate / channels or nil
|
|
end
|
|
|
|
--- Returns an iterator to stream audio from a WAV file. aukit.Audio will automatically
|
|
--- be resampled to 48 kHz, and optionally mixed down to mono. This accepts PCM
|
|
--- files up to 32 bits, including float data, as well as DFPWM files [as specified here](https://gist.github.com/MCJack123/90c24b64c8e626c7f130b57e9800962c).
|
|
---@param data string|fun():string The WAV file to decode, or a function
|
|
--- returning chunks to decode (the first chunk MUST contain the ENTIRE header)
|
|
---@param mono? boolean Whether to mix the audio to mono
|
|
---@param ignoreHeader? boolean Whether to ignore additional headers
|
|
--- if they appear later in the audio stream
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number|nil _ The total length of the audio in seconds
|
|
function aukit.stream.wav(data, mono, ignoreHeader)
|
|
local fn
|
|
if type(data) == "function" then fn, data = data, data() end
|
|
expect(1, data, "string")
|
|
local channels, sampleRate, bitDepth, length, dataType, blockAlign, coefficients
|
|
local temp, pos = str_unpack("c4", data)
|
|
if temp ~= "RIFF" then error("bad argument #1 (not a WAV file)", 2) end
|
|
pos = pos + 4
|
|
temp, pos = str_unpack("c4", data, pos)
|
|
if temp ~= "WAVE" then error("bad argument #1 (not a WAV file)", 2) end
|
|
while pos <= #data do
|
|
local size
|
|
temp, pos = str_unpack("c4", data, pos)
|
|
size, pos = str_unpack("<I", data, pos)
|
|
if temp == "fmt " then
|
|
local chunk = str_sub(data, pos, pos + size - 1)
|
|
pos = pos + size
|
|
local format
|
|
format, channels, sampleRate, blockAlign, bitDepth = str_unpack("<HHIxxxxHH", chunk)
|
|
if format == 1 then
|
|
dataType = bitDepth == 8 and "unsigned" or "signed"
|
|
elseif format == 2 then
|
|
dataType = "msadpcm"
|
|
local numcoeff = str_unpack("<H", chunk, 21)
|
|
if numcoeff > 0 then
|
|
coefficients = {{}, {}}
|
|
for i = 1, numcoeff do
|
|
coefficients[1][i], coefficients[2][i] = str_unpack("<hh", chunk, i * 4 + 19)
|
|
end
|
|
end
|
|
elseif format == 3 then
|
|
dataType = "float"
|
|
elseif format == 6 then
|
|
dataType = "alaw"
|
|
elseif format == 7 then
|
|
dataType = "ulaw"
|
|
elseif format == 0x11 then
|
|
dataType = "adpcm"
|
|
elseif format == 0xFFFE then
|
|
bitDepth = str_unpack("<H", chunk, 19)
|
|
local uuid = str_sub(chunk, 25, 40)
|
|
if uuid == wavExtensible.pcm then dataType = bitDepth == 8 and "unsigned" or "signed"
|
|
elseif uuid == wavExtensible.dfpwm then dataType = "dfpwm"
|
|
elseif uuid == wavExtensible.msadpcm then dataType = "msadpcm"
|
|
elseif uuid == wavExtensible.pcm_float then dataType = "float"
|
|
elseif uuid == wavExtensible.alaw then dataType = "alaw"
|
|
elseif uuid == wavExtensible.ulaw then dataType = "ulaw"
|
|
elseif uuid == wavExtensible.adpcm then dataType = "adpcm"
|
|
else error("unsupported WAV file", 2) end
|
|
else error("unsupported WAV file", 2) end
|
|
elseif temp == "data" then
|
|
local data = str_sub(data, pos, pos + size - 1)
|
|
if not fn and #data < size then error("invalid WAV file", 2) end
|
|
if fn then
|
|
local first, f = data
|
|
data = function()
|
|
if first then f, first = first return f
|
|
elseif ignoreHeader then
|
|
local d = fn()
|
|
if not d then return nil end
|
|
if d:match "^RIFF....WAVE" then return str_sub(d, d:match("^RIFF....WAVE.?data....()"))
|
|
else return d end
|
|
else return fn() end
|
|
end
|
|
end
|
|
if dataType == "adpcm" then return aukit.stream.adpcm(data, blockAlign, channels, sampleRate, mono)
|
|
elseif dataType == "msadpcm" then return aukit.stream.msadpcm(data, blockAlign, channels, sampleRate, mono, coefficients)
|
|
elseif dataType == "dfpwm" then return aukit.stream.dfpwm(data, sampleRate, channels, mono), size / channels / (bitDepth / 8) / sampleRate
|
|
elseif dataType == "alaw" or dataType == "ulaw" then return aukit.stream.g711(data, dataType == "ulaw", channels, sampleRate, mono)
|
|
else return aukit.stream.pcm(data, bitDepth, dataType, channels, sampleRate, false, mono), size / channels / (bitDepth / 8) / sampleRate end
|
|
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 audio from an AIFF or AIFC file. aukit.Audio will
|
|
--- automatically be resampled to 48 kHz, and optionally mixed down to mono.
|
|
---@param data string|fun():string The AIFF file to decode, or a function
|
|
--- returning chunks to decode (the first chunk MUST contain the ENTIRE header)
|
|
---@param mono? boolean Whether to mix the audio to mono
|
|
---@param ignoreHeader? boolean Whether to ignore additional headers
|
|
--- if they appear later in the audio stream
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number _ The total length of the audio in seconds
|
|
function aukit.stream.aiff(data, mono, ignoreHeader)
|
|
local fn
|
|
if type(data) == "function" then fn, data = data, data() end
|
|
expect(1, data, "string")
|
|
expect(2, mono, "boolean", "nil")
|
|
local channels, sampleRate, bitDepth, length, offset, compression, blockAlign
|
|
local isAIFC = false
|
|
local temp, pos = str_unpack("c4", data)
|
|
if temp ~= "FORM" then error("bad argument #1 (not an AIFF file)", 2) end
|
|
pos = pos + 4
|
|
temp, pos = str_unpack("c4", data, pos)
|
|
if temp == "AIFC" then isAIFC = true
|
|
elseif temp ~= "AIFF" then error("bad argument #1 (not an AIFF file)", 2) end
|
|
while pos <= #data do
|
|
local size
|
|
temp, pos = str_unpack("c4", data, pos)
|
|
size, pos = str_unpack(">I", data, pos)
|
|
if temp == "COMM" then
|
|
local e, m
|
|
channels, length, bitDepth, e, m, pos = str_unpack(">hIhHI7x", data, pos)
|
|
if isAIFC then
|
|
local s
|
|
compression, s, pos = str_unpack(">c4s1", data, pos)
|
|
if #s % 2 == 0 then pos = pos + 1 end
|
|
end
|
|
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, blockAlign, pos = str_unpack(">II", data, pos)
|
|
local data = str_sub(data, pos + offset, pos + offset + length - 1)
|
|
if not fn and #data < length then error("invalid AIFF file", 2) end
|
|
if fn then
|
|
local first, f = data
|
|
data = function()
|
|
if first then f, first = first return f
|
|
elseif ignoreHeader then
|
|
local d = fn()
|
|
if not d then return nil end
|
|
if d:match "^FORM....AIF[FC]" then
|
|
local n, p = d:match("^FORM....AIF[FC].-SSND(....)....()")
|
|
offset = str_unpack(">I", n)
|
|
return str_sub(d, p + offset)
|
|
else return d end
|
|
else return fn() end
|
|
end
|
|
end
|
|
if compression == nil or compression == "NONE" then return aukit.stream.pcm(data, bitDepth, "signed", channels, sampleRate, true, mono), length / channels / (bitDepth / 8) / sampleRate
|
|
elseif compression == "sowt" then return aukit.stream.pcm(data, bitDepth, "signed", channels, sampleRate, true, mono), length / channels / (bitDepth / 8) / sampleRate
|
|
elseif compression == "fl32" or compression == "FL32" then return aukit.stream.pcm(data, 32, "float", channels, sampleRate, true, mono), length / channels / 4 / sampleRate
|
|
elseif compression == "alaw" or compression == "ulaw" or compression == "ALAW" or compression == "ULAW" then return aukit.stream.g711(data, compression == "ulaw" or compression == "ULAW", channels, sampleRate, mono), length / channels / sampleRate
|
|
else error("Unsupported compression scheme " .. compression, 2) end
|
|
return aukit.stream.pcm(data, bitDepth, "signed", channels, sampleRate, true, mono), length / channels / (bitDepth / 8) / sampleRate
|
|
else pos = pos + size end
|
|
end
|
|
error("invalid AIFF file", 2)
|
|
end
|
|
|
|
--- Returns an iterator to stream data from an AU file. aukit.Audio will automatically
|
|
--- be resampled to 48 kHz, and optionally mixed down to mono.
|
|
---@param data string|fun():string The AU file to decode, or a function
|
|
--- returning chunks to decode (the first chunk MUST contain the ENTIRE header)
|
|
---@param mono? boolean Whether to mix the audio to mono
|
|
---@param ignoreHeader? boolean Whether to ignore additional headers
|
|
--- if they appear later in the audio stream
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number _ The total length of the audio in seconds
|
|
function aukit.stream.au(data, mono, ignoreHeader)
|
|
local fn
|
|
if type(data) == "function" then fn, data = data, data() end
|
|
expect(1, data, "string")
|
|
expect(2, mono, "boolean", "nil")
|
|
local magic, offset, size, encoding, sampleRate, channels = str_unpack(">c4IIIII", data)
|
|
if magic ~= ".snd" then error("invalid AU file", 2) end
|
|
if fn then
|
|
local first, f = str_sub(data, offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil), nil
|
|
data = function()
|
|
if first then f, first = first return f
|
|
elseif ignoreHeader then
|
|
local d = fn()
|
|
if not d then return nil end
|
|
if d:match "^.snd" then return str_sub(d, str_unpack(">I", str_sub(d, 5, 8)), nil)
|
|
else return d end
|
|
else return fn() end
|
|
end
|
|
else data = str_sub(data, offset, size ~= 0xFFFFFFFF and offset + size - 1 or nil) end
|
|
if encoding == 1 then return aukit.stream.g711(data, true, channels, sampleRate, mono), size / channels / sampleRate
|
|
elseif encoding == 2 then return aukit.stream.pcm(data, 8, "signed", channels, sampleRate, true, mono), size / channels / sampleRate
|
|
elseif encoding == 3 then return aukit.stream.pcm(data, 16, "signed", channels, sampleRate, true, mono), size / channels / 2 / sampleRate
|
|
elseif encoding == 4 then return aukit.stream.pcm(data, 24, "signed", channels, sampleRate, true, mono), size / channels / 3 / sampleRate
|
|
elseif encoding == 5 then return aukit.stream.pcm(data, 32, "signed", channels, sampleRate, true, mono), size / channels / 4 / sampleRate
|
|
elseif encoding == 6 then return aukit.stream.pcm(data, 32, "float", channels, sampleRate, true, mono), size / channels / 4 / sampleRate
|
|
elseif encoding == 27 then return aukit.stream.g711(data, false, channels, sampleRate, mono), size / channels / sampleRate
|
|
else error("unsupported encoding type " .. encoding, 2) end
|
|
end
|
|
|
|
--- Returns an iterator to stream data from a FLAC file. aukit.Audio will automatically
|
|
--- be resampled to 48 kHz, and optionally mixed down to mono.
|
|
---@param data string|fun():string The FLAC file to decode, or a function
|
|
--- returning chunks to decode
|
|
---@param mono? boolean Whether to mix the audio to mono
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number _ The total length of the audio in seconds
|
|
function aukit.stream.flac(data, mono)
|
|
expect(1, data, "string", "function")
|
|
expect(2, mono, "boolean", "nil")
|
|
local infn = false
|
|
if type(data) == "function" then data = setmetatable({str = "", fn = data, final = false, byte = function(self, start, e)
|
|
while start > #self.str do
|
|
infn = true
|
|
local d = self.fn()
|
|
infn = false
|
|
if not d then self.final = true return nil end
|
|
self.str = self.str .. d
|
|
end
|
|
while e and e > #self.str do
|
|
infn = true
|
|
local d = self.fn()
|
|
infn = false
|
|
if not d then self.final = true return nil end
|
|
self.str = self.str .. d
|
|
end
|
|
return str_byte(self.str, start, e)
|
|
end}, {__len = function(self) return self.final and #self.str or math.huge end}) end
|
|
local function saferesume(coro, ...)
|
|
local res = table_pack(coroutine.resume(coro, ...))
|
|
while res[1] and infn do res = table_pack(coroutine.resume(coro, coroutine.yield(table_unpack(res, 2, res.n)))) end
|
|
return table_unpack(res, 1, res.n)
|
|
end
|
|
local coro = coroutine.create(decodeFLAC)
|
|
local ok, sampleRate, len = saferesume(coro, data, coroutine.yield)
|
|
if not ok then error(sampleRate, 2) end
|
|
local pos = 0
|
|
local ratio = 48000 / sampleRate
|
|
local lp_alpha = 1 - math.exp(-(sampleRate / 96000) * 2 * math_pi)
|
|
local interp = interpolate[aukit.defaultInterpolation]
|
|
local last = {0, 0}
|
|
return function()
|
|
if coroutine.status(coro) == "dead" then return nil end
|
|
local chunk = {{}}
|
|
while #chunk[1] < sampleRate do
|
|
local ok, res = saferesume(coro)
|
|
if not ok or res == nil or res.sampleRate then break end
|
|
os_queueEvent("nosleep")
|
|
repeat until "nosleep" == os_pullEvent()
|
|
for c = 1, #res do
|
|
chunk[c] = chunk[c] or {}
|
|
local src, dest = res[c], chunk[c]
|
|
local start = #dest
|
|
src[0] = last[2]
|
|
src[-1] = last[1]
|
|
local ls = last[2] / (last[2] < 0 and 128 or 127)
|
|
for i = 1, math_floor(#src * ratio) do
|
|
local d = start+i
|
|
local x = ((i - 1) / ratio) + 1
|
|
local s
|
|
if x % 1 == 0 then s = src[x]
|
|
else s = interp(src, x) end
|
|
s = ls + lp_alpha * (s - ls)
|
|
ls = s
|
|
dest[d] = clamp(s * (s < 0 and 128 or 127), -128, 127)
|
|
end
|
|
last = {src[#src-1], src[#src]}
|
|
end
|
|
os_queueEvent("nosleep")
|
|
repeat until "nosleep" == os_pullEvent()
|
|
end
|
|
pos = pos + #chunk[1] / 48000
|
|
return chunk, pos
|
|
end, len / sampleRate
|
|
end
|
|
|
|
--- Returns an iterator to stream data from a QOA file. aukit.Audio will automatically
|
|
--- be resampled to 48 kHz, and optionally mixed down to mono.
|
|
---@param data string|fun():string The QOA file to decode, or a function
|
|
--- returning chunks to decode
|
|
---@param mono? boolean Whether to mix the audio to mono
|
|
---@return fun():number[][]|nil,number|nil _ 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
|
|
---@return number _ The total length of the audio in seconds
|
|
function aukit.stream.qoa(data, mono)
|
|
expect(1, data, "string", "function")
|
|
expect(2, mono, "boolean", "nil")
|
|
local read, peek
|
|
if type(data) == "string" then
|
|
local pos = 1
|
|
function read(n)
|
|
if pos > #data then return nil end
|
|
local d = data:sub(pos, pos + n - 1)
|
|
pos = pos + n
|
|
return d
|
|
end
|
|
function peek(n)
|
|
if pos > #data then return nil end
|
|
return data:sub(pos, pos + n - 1)
|
|
end
|
|
else
|
|
local buf = ""
|
|
function read(n)
|
|
while #buf < n do
|
|
local d = data()
|
|
if not d then return nil end
|
|
buf = buf .. d
|
|
end
|
|
local d = buf:sub(1, n)
|
|
buf = buf:sub(n + 1)
|
|
return d
|
|
end
|
|
function peek(n)
|
|
while #buf < n do
|
|
local d = data()
|
|
if not d then return nil end
|
|
buf = buf .. d
|
|
end
|
|
return buf:sub(1, n)
|
|
end
|
|
end
|
|
local magic, file_samples = (">c4I4"):unpack(assert(read(8), "Not a QOA file"), nil)
|
|
if magic ~= "qoaf" then error("Not a QOA file", 2) end
|
|
local file_channels, file_sampleRate = (">BI3"):unpack(assert(peek(4), "Not a QOA file"), nil)
|
|
local lms = {}
|
|
local last = {}
|
|
for i = 1, file_channels do
|
|
lms[i] = {history = {}, weights = {}}
|
|
last[i] = {0, 0}
|
|
end
|
|
|
|
local file_pos = 0
|
|
local ratio = 48000 / file_sampleRate
|
|
local lp_alpha = 1 - math.exp(-(file_sampleRate / 96000) * 2 * math_pi)
|
|
local interp = interpolate[aukit.defaultInterpolation]
|
|
return function()
|
|
local chunk = {}
|
|
for i = 1, file_channels do chunk[i] = {[-1] = last[i][1], [0] = last[i][2]} end
|
|
local sample_pos = 0
|
|
while sample_pos < file_sampleRate do
|
|
-- from https://github.com/phoboslab/qoa/blob/master/qoa.h
|
|
-- MIT license - Copyright (c) 2023 Dominic Szablewski
|
|
|
|
-- Read and verify the frame header
|
|
local d = read(8)
|
|
if not d then break end
|
|
local channels, samplerate, samples, frame_size = (">BI3I2I2"):unpack(d)
|
|
|
|
local data_size = frame_size - 8 - 4 * 4 * channels
|
|
local num_slices = math_floor(data_size / 8)
|
|
local max_total_samples = num_slices * 20
|
|
|
|
if
|
|
channels ~= file_channels or
|
|
samplerate ~= file_sampleRate or
|
|
samples * channels > max_total_samples
|
|
then
|
|
--error("Bad frame data", 2)
|
|
break
|
|
end
|
|
|
|
-- Read the LMS state: 4 x 2 bytes history, 4 x 2 bytes weights per channel
|
|
for c = 1, channels do
|
|
lms[c].history = {(">i2i2i2i2"):unpack(assert(read(8), "Invalid QOA data"), nil)}
|
|
lms[c].weights = {(">i2i2i2i2"):unpack(assert(read(8), "Invalid QOA data"), nil)}
|
|
end
|
|
|
|
-- Decode all slices for all channels in this frame
|
|
for sample_index = 1, samples, 20 do
|
|
for c = 1, channels do
|
|
local sliceH, sliceL = (">I4I4"):unpack(assert(read(8), "Invalid QOA data"), nil)
|
|
|
|
local scalefactor = bit32_extract(sliceH, 28, 4)
|
|
|
|
for si = sample_index, sample_index + 19 do
|
|
local predicted = qoa_lms_predict(lms[c])
|
|
local quantized = bit32_extract(sliceH, 25, 3)
|
|
local dequantized = qoa_dequant_tab[scalefactor][quantized]
|
|
--print(predicted, dequantized)
|
|
local reconstructed = math_min(math_max(predicted + dequantized, -32768), 32767)
|
|
|
|
chunk[c][sample_pos + si] = math_floor(reconstructed / 256)
|
|
sliceH = bit32_lshift(sliceH, 3) + bit32_extract(sliceL, 29, 3)
|
|
sliceL = bit32_lshift(sliceL, 3)
|
|
|
|
qoa_lms_update(lms[c], reconstructed, dequantized)
|
|
end
|
|
end
|
|
end
|
|
sample_pos = sample_pos + samples
|
|
end
|
|
|
|
if #chunk[1] == 0 then return nil end
|
|
|
|
local newlen = #chunk[1] * ratio
|
|
local lines = {{}}
|
|
if not mono then for j = 2, file_channels do lines[j] = {} end end
|
|
local ls = {}
|
|
for j = 1, file_channels do ls[j] = last[j][2] end
|
|
for i = 1, newlen do
|
|
local n = 0
|
|
for j = 1, file_channels do
|
|
local x = (i - 1) / ratio + 1
|
|
local s
|
|
if x % 1 == 0 then s = chunk[j][x]
|
|
else s = clamp(interp(chunk[j], x), -128, 127) end
|
|
s = ls[j] + lp_alpha * (s - ls[j])
|
|
ls[j] = s
|
|
if mono then n = n + s
|
|
else lines[j][i] = s end
|
|
end
|
|
if mono then lines[1][i] = n / file_channels end
|
|
end
|
|
|
|
local pos = file_pos / file_sampleRate
|
|
file_pos = file_pos + sample_pos
|
|
for i = 1, file_channels do last[i] = {chunk[i][#chunk[i]-1], chunk[i][#chunk[i]]} end
|
|
return lines, pos
|
|
end, file_samples / file_sampleRate
|
|
end
|
|
|
|
--[[
|
|
.########.########.########.########..######..########..######.
|
|
.##.......##.......##.......##.......##....##....##....##....##
|
|
.##.......##.......##.......##.......##..........##....##......
|
|
.######...######...######...######...##..........##.....######.
|
|
.##.......##.......##.......##.......##..........##..........##
|
|
.##.......##.......##.......##.......##....##....##....##....##
|
|
.########.##.......##.......########..######.....##.....######.
|
|
]]
|
|
|
|
--- aukit.effects
|
|
---@section aukit.effects
|
|
|
|
--- Amplifies the audio by the multiplier specified.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@param multiplier number The multiplier to apply
|
|
---@return aukit.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.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@param multiplier number The multiplier to apply
|
|
---@return aukit.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.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@param startTime number The start time of the fade, in seconds
|
|
---@param startAmplitude number The amplitude of the beginning of the fade
|
|
---@param endTime number The end time of the fade, in seconds
|
|
---@param endAmplitude number The amplitude of the end of the fade
|
|
---@return aukit.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.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@return aukit.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.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@param peakAmplitude? number The maximum amplitude
|
|
---@param independent? boolean Whether to normalize each channel independently
|
|
---@return aukit.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.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@return aukit.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.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@param threshold? number The maximum value to register as silence
|
|
---@return aukit.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 = str_sub(audio, s / audio.sampleRate, e / audio.sampleRate)
|
|
audio.data = new.data
|
|
return audio
|
|
end
|
|
|
|
--- Adds a delay to the specified audio.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@param delay number The amount of time to delay for, in seconds
|
|
---@param multiplier? number The multiplier to apply to the delayed audio
|
|
---@return aukit.Audio _ The audio modified
|
|
function aukit.effects.delay(audio, delay, multiplier)
|
|
expectAudio(1, audio)
|
|
expect(2, delay, "number")
|
|
multiplier = expect(3, multiplier, "number", "nil") or 0.5
|
|
local samples = math_floor(delay * audio.sampleRate)
|
|
for c = 1, #audio.data do
|
|
local original = {}
|
|
local o = audio.data[c]
|
|
for i = 1, #o do original[i] = o[i] end
|
|
for i = samples + 1, #o do o[i] = clamp(o[i] + original[i - samples] * multiplier, -1, 1) end
|
|
end
|
|
return audio
|
|
end
|
|
|
|
--- Adds an echo to the specified audio.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@param delay? number The amount of time to echo after, in seconds
|
|
---@param multiplier? number The decay multiplier to apply to the echoed audio
|
|
---@return aukit.Audio _ The audio modified
|
|
function aukit.effects.echo(audio, delay, multiplier)
|
|
expectAudio(1, audio)
|
|
delay = expect(2, delay, "number", "nil") or 1
|
|
multiplier = expect(3, multiplier, "number", "nil") or 0.5
|
|
local samples = math_floor(delay * audio.sampleRate)
|
|
for c = 1, #audio.data do
|
|
local o = audio.data[c]
|
|
for i = samples + 1, #o do o[i] = clamp(o[i] + o[i - samples] * multiplier, -1, 1) end
|
|
end
|
|
return audio
|
|
end
|
|
|
|
local combDelayShift = {0, -11.73, 19.31, -7.97}
|
|
local combDecayShift = {0, 0.1313, 0.2743, 0.31}
|
|
|
|
--- Adds reverb to the specified audio.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@param delay? number The amount of time to reverb after, in **milliseconds**
|
|
---@param decay? number The decay factor to use
|
|
---@param wetMultiplier? number The wet (reverbed) mix amount
|
|
---@param dryMultiplier? number The dry (original) mix amount
|
|
---@return aukit.Audio _ The audio modified
|
|
function aukit.effects.reverb(audio, delay, decay, wetMultiplier, dryMultiplier)
|
|
expectAudio(1, audio)
|
|
delay = expect(2, delay, "number", "nil") or 100
|
|
decay = expect(3, decay, "number", "nil") or 0.3
|
|
wetMultiplier = expect(4, wetMultiplier, "number", "nil") or 1
|
|
dryMultiplier = expect(5, dryMultiplier, "number", "nil") or 0
|
|
for c = 1, #audio.data do
|
|
-- four comb filters
|
|
local sum = {}
|
|
local o = audio.data[c]
|
|
for n = 1, 4 do
|
|
local comb = {}
|
|
local samples = math_floor((delay + combDelayShift[n]) / 1000 * audio.sampleRate)
|
|
local multiplier = decay - combDecayShift[n]
|
|
for i = 1, math_min(samples, #o) do
|
|
comb[i] = o[i]
|
|
sum[i] = (sum[i] or 0) + o[i]
|
|
end
|
|
for i = samples + 1, #o do
|
|
local s = o[i] + comb[i - samples] * multiplier
|
|
comb[i] = s
|
|
sum[i] = (sum[i] or 0) + s
|
|
end
|
|
end
|
|
-- mix wet/dry
|
|
for i = 1, #sum do sum[i] = sum[i] * wetMultiplier + o[i] * dryMultiplier end
|
|
-- two all pass filters
|
|
local samples = math_floor(0.08927 * audio.sampleRate)
|
|
sum[samples+1] = sum[samples+1] - 0.131 * sum[1]
|
|
for i = samples + 2, #sum do sum[i] = sum[i] - 0.131 * sum[i - samples] + 0.131 * sum[i + 20 - samples] end
|
|
o[samples+1] = clamp(sum[samples+1] - 0.131 * sum[1], -1, 1)
|
|
for i = samples + 2, #sum do o[i] = clamp(sum[i] - 0.131 * sum[i - samples] + 0.131 * sum[i + 20 - samples], -1, 1) end
|
|
end
|
|
return audio
|
|
end
|
|
|
|
--- Applies a low-pass filter to the specified audio.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@param frequency number The cutoff frequency for the filter
|
|
---@return aukit.Audio _ The audio modified
|
|
function aukit.effects.lowpass(audio, frequency)
|
|
expectAudio(1, audio)
|
|
expect(2, frequency, "number")
|
|
local a = 1 - math.exp(-(frequency / audio.sampleRate) * 2 * math_pi)
|
|
for c = 1, #audio.data do
|
|
local d = audio.data[c]
|
|
for i = 2, #d do
|
|
local l = d[i-1]
|
|
d[i] = l + a * (d[i] - l)
|
|
end
|
|
end
|
|
return audio
|
|
end
|
|
|
|
--- Applies a high-pass filter to the specified audio.
|
|
---@param audio aukit.Audio The audio to modify
|
|
---@param frequency number The cutoff frequency for the filter
|
|
---@return aukit.Audio _ The audio modified
|
|
function aukit.effects.highpass(audio, frequency)
|
|
expectAudio(1, audio)
|
|
expect(2, frequency, "number")
|
|
local a = 1 / (2 * math_pi * (frequency / audio.sampleRate) + 1)
|
|
for c = 1, #audio.data do
|
|
local d = audio.data[c]
|
|
local lx = d[1]
|
|
for i = 2, #d do
|
|
local llx = d[i]
|
|
d[i] = a * (d[i-1] + llx - lx)
|
|
lx = llx
|
|
end
|
|
end
|
|
return audio
|
|
end
|
|
|
|
return aukit |