Skip to content

Implement a native ADP decoder #407

@rdw-software

Description

@rdw-software

Details:

Proof of concept below (don't try this at home):

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"))

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions