local ffi = require("ffi")
local bit = require("bit")
local band = bit.band
local rshift = bit.rshift
local ADPCM = {
-- Table of index changes
indexTable = { -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 },
-- Quantizer lookup table
stepsizeTable = {
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,
},
}
-- Simple 1-pole high-pass: y[n] = x[n] - x[n-1] + α y[n-1]
local alpha = 0.995
local prev_x, prev_y = 0, 0
function ADPCM:ApplyHighPassFilter(x)
local y = x - prev_x + alpha * prev_y
prev_x, prev_y = x, y
return math.floor(y)
end
ADPCM.indexTable = ffi.new("int8_t[?]", #ADPCM.indexTable, ADPCM.indexTable)
ADPCM.stepsizeTable = ffi.new("int16_t[?]", #ADPCM.stepsizeTable, ADPCM.stepsizeTable)
function ADPCM:Construct()
local instance = {
index = 0,
stepsize = 7,
samples = {},
newSample = 0,
}
setmetatable(instance, self)
return instance
end
ADPCM.__index = ADPCM
ADPCM.__call = ADPCM.Construct
setmetatable(ADPCM, ADPCM)
local SAMPLE_PRECISION_POT = 16
local SAMPLE_PRECISION_BITS = math.pow(2, SAMPLE_PRECISION_POT - 1)
function ADPCM:DecompressLinear4to16(originalSample)
local difference = 0
if band(originalSample, 4) ~= 0 then
difference = difference + self.stepsize
end
if band(originalSample, 2) ~= 0 then
difference = difference + rshift(self.stepsize, 1)
end
if band(originalSample, 1) ~= 0 then
difference = difference + rshift(self.stepsize, 2)
end
difference = difference + rshift(self.stepsize, 3)
if band(originalSample, 8) ~= 0 then
difference = -difference
end
self.newSample = self.newSample + difference
if self.newSample > SAMPLE_PRECISION_BITS - 1 then
self.newSample = SAMPLE_PRECISION_BITS - 1
elseif self.newSample < - SAMPLE_PRECISION_BITS then
self.newSample = - SAMPLE_PRECISION_BITS
end
self.index = self.index + self.indexTable[originalSample]
if self.index < 0 then
self.index = 0
elseif self.index > 88 then
self.index = 88
end
self.stepsize = self.stepsizeTable[self.index]
return self:ApplyHighPassFilter(self.newSample), difference
end
ffi.cdef([[
typedef struct ArcturusADP {
uint32_t adpcmHeaderSize; // ADPCM stream begins after the header
uint32_t numSamplesPerSecond; // Playback speed
uint32_t numEncodedAudioChannels;
uint32_t audioStreamSizeInBytes;
} ArcturusADP;
]])
local ArcturusADP = {}
function ArcturusADP:Construct()
local instance = {}
setmetatable(instance, self)
return instance
end
ArcturusADP.__index = ArcturusADP
ArcturusADP.__call = ArcturusADP.Construct
setmetatable(ArcturusADP, ArcturusADP)
function ArcturusADP:DumpSamples(where, how)
how = how or "lua"
local what = {}
for index, sample in ipairs(self.samples) do
table.insert(what, self.channels[1].estimatedWaveformBytes[index])
table.insert(what, self.channels[2].estimatedWaveformBytes[index])
end
printf("[ArcturusADP] Dumping samples: %s.%s", where, how)
local samples = require("json").stringify(what)
samples = samples:gsub("%[", "return {")
samples = samples:gsub("%]", "}")
local outputFilePath = where .. "." .. how
C_FileSystem.MakeDirectoryTree(path.dirname(outputFilePath))
C_FileSystem.WriteFile(outputFilePath, samples)
end
function ArcturusADP:DecodeFileContents(path)
local f = assert(io.open(path, "rb"))
local fileContents = f:read("*a")
local adpBuffer = buffer.new(#fileContents):set(fileContents)
local header = ffi.cast("ArcturusADP*", adpBuffer)
f:close()
printf(
"[ArcturusADP] Decoding file contents: %s (adpcmHeaderSize: %d numSamplesPerSecond: %d numEncodedAudioChannels: %d audioStreamSizeInBytes: %s)",
path,
header.adpcmHeaderSize,
header.numSamplesPerSecond,
header.numEncodedAudioChannels,
string.filesize(header.audioStreamSizeInBytes)
)
ArcturusADP:DecodeCompressedStream(header, adpBuffer)
end
function ArcturusADP:DecodeCompressedStream(header, adpBuffer)
self.header = {
adpcmHeaderSize = tonumber(header.adpcmHeaderSize),
numSamplesPerSecond = tonumber(header.numSamplesPerSecond),
numEncodedAudioChannels = tonumber(header.numEncodedAudioChannels),
audioStreamSizeInBytes = tonumber(header.audioStreamSizeInBytes),
totalFileSize = #adpBuffer,
}
assert(self.header.adpcmHeaderSize == ffi.sizeof("ArcturusADP"), "Header size doesn't match the struct definition")
assert(self.header.numSamplesPerSecond == 22050, "Expected 22.05 kHz bit rate") -- Isn't that 44 kHZ? (2 samples/byte)
assert(self.header.numEncodedAudioChannels == 2, "Expected stereo audio (two interleaved channels)")
local expectedStreamLength = (#adpBuffer - ffi.sizeof("ArcturusADP")) * 2
assert(self.header.audioStreamSizeInBytes == expectedStreamLength, "Expected stream to end at EOF pointer")
adpBuffer:skip(ffi.sizeof("ArcturusADP"))
assert(self.header.audioStreamSizeInBytes == #adpBuffer * 2, "Expected stream to end at EOF pointer")
local channels = {}
local numBytesPerEncodedChannel = math.floor(
(self.header.audioStreamSizeInBytes - ffi.sizeof("ArcturusADP") / self.header.numEncodedAudioChannels)
)
-- It's conceivable that ADPCM streams need to be padded; not implemented here and likely not necessary
assert(numBytesPerEncodedChannel % 2 == 0, "NYI: Incomplete ADPCM streams may need to be padded")
for channelID = 1, tonumber(self.header.numEncodedAudioChannels), 1 do
local channel = {
name = "AudioChannel" .. channelID,
decoder = ADPCM(),
estimatedWaveformBytes = table.new(numBytesPerEncodedChannel, 0),
}
table.insert(channels, channel)
end
local left = channels[1]
local right = channels[2]
do
left.decoder.index = 16
right.decoder.index = 16
left.decoder.newSample = ADPCM.stepsizeTable[left.decoder.index]
right.decoder.newSample = ADPCM.stepsizeTable[left.decoder.index]
left.decoder.stepsize = ADPCM.stepsizeTable[left.decoder.index]
right.decoder.stepsize = ADPCM.stepsizeTable[right.decoder.index]
end
local numDecompressedBytes = 0
repeat
local ptr, len = adpBuffer:ref()
if len == 0 then
print("it's empty... Decompressed bytes: " .. numDecompressedBytes)
break
end
local adpcmCode = ptr[0]
local encodedByte = tonumber(adpcmCode)
local low = bit.band(encodedByte, 0x0F)
local high = bit.band(encodedByte, 0xF0)
high = bit.rshift(high, 4)
local newSample = left.decoder:DecompressLinear4to16(low)
table.insert(left.estimatedWaveformBytes, newSample)
newSample = right.decoder:DecompressLinear4to16(high)
table.insert(right.estimatedWaveformBytes, newSample)
adpBuffer:skip(1)
numDecompressedBytes = numDecompressedBytes + 2 -- ffi.sizeof("uint16_t")
until numDecompressedBytes >= numBytesPerEncodedChannel
self.channels = channels
local samples = table.new(self.header.audioStreamSizeInBytes, 0)
local sampleMin = math.huge
local sampleMax = 0
for index = 1, #channels[1].estimatedWaveformBytes, 1 do
local left = channels[1].estimatedWaveformBytes[index]
local right = channels[2].estimatedWaveformBytes[index]
local avg = math.floor((left + right) / 2)
table.insert(samples, avg)
sampleMin = math.min(sampleMin, avg)
sampleMax = math.max(sampleMax, avg)
end
printf("Samples: %d (LEFT: %s / RIGHT: %s)", #samples, #left.estimatedWaveformBytes, #right.estimatedWaveformBytes)
printf("[ArcturusADP] Finished decoding %d samples in range [%s, %s]", #samples, sampleMin, sampleMax)
self.samples = samples
self.sampleMin = sampleMin
self.sampleMax = sampleMax
end
function ArcturusADP:DrawSamples(what, where, how)
how = how or "png"
printf("[ArcturusADP] Drawing %d samples: %s.%s", #what, where, how)
local WAVEFORM_VISUALIZATION_SCALE = 1024
local pw, ph = 4 * WAVEFORM_VISUALIZATION_SCALE, 2 * WAVEFORM_VISUALIZATION_SCALE
local bytes = buffer.new(pw * ph * 4)
local pb, size = bytes:reserve(pw * ph * 4)
-- 1) Clear background
for i = 0, pw * ph * 4 - 1 do
pb[i] = 0xFF
end
-- 2) Compute scales
local samples = what
local maxVal = math.abs(self.sampleMax - self.sampleMin) / 2
local yScale = (ph - 1) / (2 * maxVal)
local xScale = (pw - 1) / (#samples - 1)
local function clamp(v, lo, hi)
return v < lo and lo or (v > hi and hi or v)
end
-- Bresenham line between two points
local function drawLine(x0, y0, x1, y1)
local dx, dy = math.abs(x1 - x0), -math.abs(y1 - y0)
local sx = x0 < x1 and 1 or -1
local sy = y0 < y1 and 1 or -1
local err = dx + dy
while true do
local i = (y0 * pw + x0) * 4
pb[i + 0] = math.floor(1 / 2 * math.min(255, pb[i + 0] + 100)) -- R
pb[i + 1] = math.floor(1 / 2 * math.min(255, pb[i + 1] + 100)) -- G
pb[i + 2] = math.floor(1 / 2 * math.min(255, pb[i + 2] + 220)) -- B
pb[i + 3] = 255 -- A
if x0 == x1 and y0 == y1 then
break
end
local e2 = 2 * err
if e2 >= dy then
err = err + dy
x0 = x0 + sx
end
if e2 <= dx then
err = err + dx
y0 = y0 + sy
end
end
end
-- 3) Plot waveform, badly
local prevX, prevY
for i = 1, #samples, 32 do
local sample = samples[i]
-- Map sample to pixel Y (invert so + goes up)
local y = clamp(math.floor((maxVal - sample) * yScale), 0, ph - 1)
local x = clamp(math.floor((i - 1) * xScale), 0, pw - 1)
if prevX then
drawLine(prevX, prevY, x, y)
end
prevX, prevY = x, y
end
-- 4) Draw center line at y = ph/2
local cY = math.floor(ph / 2)
for x = 0, pw - 1 do
local idx = (cY * pw + x) * 4
pb[idx + 0], pb[idx + 1], pb[idx + 2] = 0xAA, 0xAA, 0xAA
end
bytes:commit(pw * ph * 4)
assert(how == "png", "how=png is the only supported option right now")
local png = C_ImageProcessing.EncodePNG(bytes, pw, ph)
local outputFilePath = where .. "." .. how
C_FileSystem.MakeDirectoryTree(path.dirname(outputFilePath))
C_FileSystem.WriteFile(outputFilePath, png)
end
function ArcturusADP:DecodeSamples(fileContents)
local samples = table.new(#fileContents / ffi.sizeof("int16_t"), 0)
local rawBuffer = buffer.new(#fileContents):put(fileContents)
while true do
local ptr, len = rawBuffer:ref()
if len == 0 then
break
end
local sample = ffi.cast("int16_t*", ptr)[0]
sample = tonumber(sample)
table.insert(samples, sample)
rawBuffer:skip(ffi.sizeof("int16_t"))
end
local sampleMin = math.huge
local sampleMax = 0
for index = 1, #samples, 1 do
local sample = samples[index]
sampleMin = math.min(sampleMin, sample)
sampleMin = math.min(sampleMin, sample)
sampleMax = math.max(sampleMax, sample)
sampleMax = math.max(sampleMax, sample)
end
printf("[ArcturusADP] Finished decoding %d samples in range [%s, %s]", #samples, sampleMin, sampleMax)
self.samples = samples
self.sampleMin = sampleMin
self.sampleMax = sampleMax
self.samples = samples
end
local dir = arg[1] or "ADP"
local files = C_FileSystem.ReadDirectoryTree(dir)
files = table.keys(files)
table.sort(files)
for _, file in ipairs(files) do
if file:match("%.adp$") or file:match("%.apc$") then
local adp = ArcturusADP()
adp:DecodeFileContents(file)
local fname = path.basename(file)
adp:DumpSamples(path.join("Samples", fname))
adp:DrawSamples(adp.samples, path.join("Samples", fname .. ".avg"))
adp:DrawSamples(adp.channels[1].estimatedWaveformBytes, path.join("Samples", fname .. ".left"))
adp:DrawSamples(adp.channels[2].estimatedWaveformBytes, path.join("Samples", fname .. ".right"))
end
end
local manifest = "return " .. dump(files)
C_FileSystem.WriteFile("samples.lua", manifest) -- For the drwav script (fix later/use FFI to dump WAV directly)
local adpMP3 = ArcturusADP()
adpMP3:DecodeSamples(C_FileSystem.ReadFile("002.samples.raw"))
adpMP3:DrawSamples(adpMP3.samples, path.join("Samples", "002.mp3"))
-- local file = "ADP/002.adp"
-- local adpADP = ArcturusADP()
-- adpADP:DecodeFileContents(file)
-- local fname = path.basename(file)
-- adpADP:DumpSamples(path.join("Samples", fname))
-- adpADP:DrawSamples(path.join("Samples", fname))
-- local deltas = {}
-- for sampleID, sample in ipairs(adpMP3.samples) do
-- local adpSample = adpADP.samples[sampleID]
-- -- local delta = math.abs(sample, adpSample)
-- local delta = sample - adpSample
-- if math.abs(delta) > 2 then
-- -- printf("%d: %s VS %s (delta: %s)", sampleID, sample, adpSample, delta)
-- end
-- table.insert(deltas, delta)
-- end
-- local adpTemp = ArcturusADP()
-- adpTemp.samples = deltas
-- adpTemp:DrawSamples(path.join("Samples", "delta"))
Details:
Proof of concept below (don't try this at home):