Created
June 13, 2018 15:18
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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