Skip to content

Instantly share code, notes, and snippets.

@MikuAuahDark
Created June 13, 2018 15:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MikuAuahDark/8face42bcbf41506c0a9160be8453cdc to your computer and use it in GitHub Desktop.
Save MikuAuahDark/8face42bcbf41506c0a9160be8453cdc to your computer and use it in GitHub Desktop.
deHCA: LuaJIT FFI Binding to vgmstream's clHCA to play HCA audio in LOVE
; If you're using MSVC, use this DEF file or deHCA will complain
; about a nil value. Example command to build clHCA in Windows:
; > cl.exe /c /Ox /Ot /GL /arch:SSE2 /MD clHCA.c
; > link.exe /DLL /LTCG /DEF:clHCA.def /OUT:clHCA.dll clHCA.obj
; Then copy clHCA.dll to LOVE directory.
; The library name must be "clHCA" (clHCA.dll/libclHCA.so/libclHCA.dylib)
EXPORTS
clHCA_isOurFile0
clHCA_isOurFile1
clHCA_sizeof
clHCA_clear
clHCA_done
clHCA_new
clHCA_delete
clHCA_Decode
clHCA_DecodeSamples16
clHCA_getInfo
-- deHCA: Play HCA
-- LuaJIT FFI Binding of vgmstream's clHCA decoder.
-- clHCA decoder: https://gist.github.com/kode54/ce2bf799b445002e125f06ed833903c0
local ffi = require("ffi")
local love = require("love")
assert(love.sound, "love.sound is required")
assert(love.audio, "love.audio is required")
local deHCA = {
playing = {},
-- Key is default to BanG Dream: Girls Band Party!
-- Change with deHCA.setKey(key1, key2) function.
key1 = 8910,
key2 = 0
}
ffi.cdef [[
int clHCA_isOurFile0(const void *data);
int clHCA_isOurFile1(const void *data, unsigned int size);
typedef struct clHCA clHCA;
int clHCA_sizeof();
void clHCA_clear(clHCA *, unsigned int ciphKey1, unsigned int ciphKey2);
void clHCA_done(clHCA *);
clHCA * clHCA_new(unsigned int ciphKey1, unsigned int ciphKey2);
void clHCA_delete(clHCA *);
int clHCA_Decode(clHCA *, void *data, unsigned int size, unsigned int address);
void clHCA_DecodeSamples16(clHCA *, signed short * outSamples);
typedef struct clHCA_stInfo {
unsigned int version;
unsigned int dataOffset;
unsigned int samplingRate;
unsigned int channelCount;
unsigned int blockSize;
unsigned int blockCount;
unsigned int loopEnabled;
unsigned int loopStart;
unsigned int loopEnd;
const char *comment;
} clHCA_stInfo;
int clHCA_getInfo(clHCA *, clHCA_stInfo *out);
]]
local clHCA
do
local clHCALib = ffi.load("clHCA")
local function indexingSafe(lib, var)
return lib["clHCA_"..var]
end
clHCA = setmetatable({}, {
__index = function(_, var)
local s, x = pcall(indexingSafe, clHCALib, var)
if s then return x end
return nil
end
})
end
deHCA.__index = deHCA
function deHCA.setKey(key1, key2)
deHCA.key1, deHCA.key2 = key1, key2
end
function deHCA.loadHCA(path)
if not(type(path) == "userdata" and path:typeOf("Data")) then
path = love.filesystem.newFileData(path)
end
local this = setmetatable({}, deHCA)
local data = ffi.cast("uint8_t*", path:getPointer())
local dataSize = path:getSize()
-- Check file
local headerSize = clHCA.isOurFile0(data)
if headerSize < 0 then
error("Invalid HCA file (isOurFile0)", 2)
end
if clHCA.isOurFile1(data, headerSize) < 0 then
error("Invalid HCA file (isOurFile1)", 2)
end
-- Allocate HCA structure
local hcaData = ffi.new("uint8_t[?]", clHCA.sizeof())
local hcaPtr = ffi.cast("clHCA*", hcaData)
ffi.gc(hcaData, clHCA.done)
clHCA.clear(hcaPtr, deHCA.key1, deHCA.key2)
-- Decode check
if clHCA.Decode(hcaPtr, data, dataSize, 0) < 0 then
error("Failed to decode first block", 2)
end
-- Get HCA information
local hcaInfo = ffi.new("clHCA_stInfo")
clHCA.getInfo(hcaPtr, hcaInfo)
-- Allocate temporary buffer for decoding
local tempbuf = ffi.new("uint8_t[?]", hcaInfo.blockSize)
-- Create queueable source
this.source = love.audio.newQueueableSource(hcaInfo.samplingRate, 16, hcaInfo.channelCount, 16)
-- Fill table
this.hcaData = hcaData -- We need to keep this reference around so FFI doesn't free it
this.hcaPtr = hcaPtr -- For faster access (ffi.cast can be slow)
this.hcaInfo = hcaInfo -- Also for faster access (ffi.new can be slow)
this.data = path -- Keep reference around so Lua GC doesn't free it
this.dataPtr = data -- You don't want to always use ffi.cast and Data:getPointer() do you? (it's very slow)
this.dataSize = dataSize -- For faster access (calling Data:getSize() is not compiled)
this.tempBuf = tempbuf
this.currentBlock = 0
this.loop = false
this.playing = false
return this
end
-- Internal function to create and fill SoundData
local function getSoundData(self)
local soundData = love.sound.newSoundData(1024, self.hcaInfo.samplingRate, 16, self.hcaInfo.channelCount)
clHCA.DecodeSamples16(self.hcaPtr, ffi.cast("signed short*", soundData:getPointer()))
return soundData
end
function deHCA:fillBuffers()
-- If current buffer exceeded then break
if self.currentBlock >= self.hcaInfo.blockCount then return end
-- Fill buffer as many as possible
local bufEmpty = self.source:getFreeBufferCount()
for i = 1, bufEmpty do
-- Calculate address and decode
local address = self.hcaInfo.dataOffset + self.currentBlock * self.hcaInfo.blockSize
-- Copy to temporary buffer so it doesn't modified (clHCA_Decode modifies the data buffer)
ffi.copy(self.tempBuf, self.dataPtr + address, self.hcaInfo.blockSize)
if clHCA.Decode(self.hcaPtr, self.tempBuf, self.hcaInfo.blockSize, address) < 0 then
error("Failed to decode samples", 2)
end
self.source:queue(getSoundData(self))
self.currentBlock = self.currentBlock + 1
if self.looping then
if self.hcaInfo.loopEnabled > 0 and self.currentBlock == self.hcaInfo.loopEnd then
-- Set current block to loop start block
self.currentBlock = self.hcaInfo.loopStart
elseif self.currentBlock >= self.hcaInfo.blockCount then
-- Set current block to zero if no loop points
self.currentBlock = 0
end
end
end
self.source:play()
end
function deHCA:play()
for i = #deHCA.playing, 1, -1 do
if deHCA.playing[i] == self then return end
end
deHCA.playing[#deHCA.playing + 1] = self
self:fillBuffers()
self.source:play()
self.playing = true
end
function deHCA:stop()
for i = #deHCA.playing, 1, -1 do
if deHCA.playing[i] == self then
table.remove(deHCA.playing, i)
self.source:stop()
self.currentBlock = 0
self.playing = false
return
end
end
end
function deHCA:pause()
for i = #deHCA.playing, 1, -1 do
if deHCA.playing[i] == self then
table.remove(deHCA.playing, i)
self.source:pause()
self.playing = false
return
end
end
end
function deHCA:update()
if self == nil then
for i = #deHCA.playing, 1, -1 do
deHCA.playing[i]:update()
end
else
if self.playing and not(self.source:isPlaying()) then
-- Queue depleted
self:stop()
else
self:fillBuffers()
end
end
end
function deHCA:isPlaying()
return self.playing
end
function deHCA:setLooping(loop)
self.looping = not(not(loop))
end
function deHCA:isLooping()
return self.looping
end
return deHCA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment