TypeScript port of VGAudio's and vgmstream's HCA codec
Some parts are ported from HCADecoder
Decrypt & decode hca(2.0, 3.0) file in browser.
- HCA 3.0
- HCA 2.0
- HCA 1.3
- unpack awb
- a/b Keys
- subkey
- decrypt
- test and find decryption key
- decode
- wave mode (8/16/24/32/float)
- loop
- volume
- encode (must be PCM16 wav)
- encrypt
- recode (ogg/aac/mp3/flac)
- FFT/DCT/DCTM/IDCTM (?)
Standalone version (can be saved for offline use): hca-standalone.html
Includes dst/ folder for Node or other use.
Generally not recommended: when called in the foreground main thread, raw APIs block the main thread for significant time (1000-1200ms for an 1.3MB HCA file being decrypted and decoded)
Generally HCAWorker APIs below are recommended.
Static methods can be directly called without creating an instance, like:
let decryptedHca = HCA.decrypt(hca, "defaultkey");
let wav = HCA.decode(decryptedHca);
let hca = HCA.encode(wav, "defaultkey");
Decrypt/encrypt & return the whole HCA file in-place with specified keys - in other words, if you don't want the input HCA to be overwritten, you must pass in something like hca.slice(0)
, which makes a new copy in a newly allocated buffer.
-
key1
is lower 32 bits of the keycode, which is not optional.-
If
key2
is not given, it defaults to zero. -
If
key1
is"nokey"
or"defaultkey"
,key2
is then ignored. Settingkey1
to"nokey"
means the encryption/decryption should be done in "no key" mode. Settingkey1
to"defaultkey"
means the hard-coded default keys (allegedly for Magia Record) should be used for encryption/decryption. -
subkey
is ignored if set to zero. -
subkey
is only applied with cipher type0x38
for now.
-
-
Already-encrypted HCA cannot be directly re-encrypted. You may check whether an HCA is already encrypted with something like:
let info = new HCAInfo(hca); let isAlreadyEncrypted = info.hasHeader["ciph"] && info.cipher != 0;
...or just decrypt it before re-encrypting, because decrypting an already-unencrypted HCA is okay.
-
Unencrypted HCA which lacks
ciph
header section cannot be directly encrypted. See HCAInfo.addCipherHeader below. -
Checksums will be verified in the process, and
Error
will be thrown on any mismatch.
HCA.findKey(hca: Uint8Array, givenKeyList?: [any, any][], subkey?: any, threshold = 0.5, depth = 1024): [number, number] | undefined
Test and find valid decryption key.
givenKeyList
is optional. If given, it should be an array of[key1, key2]
.
An example givenKeyList
:
[
[0x01395C51, 0x00000000], // Magia Record
[0x8ECED447, 0x6615518E], // Heaven Burns Red (Android)
]
-
A built-in known key list will always be included regardless whether
givenKeyList
is given or not. -
For explanation of
key1
,key2
,subkey
, please refer to HCA.decrypt/HCA.encrypt above. -
This method will search for
depth
HCA blocks, 1024 by default. -
If
threshold
percentage (50% by default) of blocks can be decrypted and unpacked, the found key will be returned as[key1, key2]
. Otherwise,undefined
will be returned.
Return decoded (Windows PCM) WAV of the input whole HCA file. The input HCA must be unencrypted, otherwise Error
will be thrown.
-
mode
argumentmode
is optional, by default it's set to 32. Validmode
values includes:-
0
32-bit float PCM mode
-
8/16/24/32
8/16/24/32-bit integer PCM mode
Note: according to the standard, only 8-bit mode uses unsigned integer, while modes with more bits (like 16-bit) use signed integer.
-
-
loop
argumentHCA make use of
loop
header section to record which part of the audio should be looped, for how many times.loop
argument is optional, and it is meaningful only if the input HCA hasloop
header.loop
simply indicates how many times the looped part of audio should be inserted. For example, withloop
set to2
, the resulting WAV audio will be like:Beginning part Looped part 1st inserted Looped part 2nd inserted Looped part Ending part Setting
loop
argument to 0 indicates the output WAV will just contain the decoded audio from the beginning to the end, without any looped part inserted, like:Beginning part Originally supposed looped part Ending part -
Checksums will be verified in the process, and
Error
will be thrown on any mismatch.
Return encoded HCA of the input whole wav file. The input wav must be PCM16 at the moment, otherwise Error
will be thrown.
-
key1
,key2
,subkey
argument if at least key1 is supplied, HCA will be encrypted after creation. -
More options are possible down the line inculding different PCM formats, looping control and quality control.
-
Set checksums of HCA header and all blocks to recalculated actual value, in-place. Pass in something like
hca.slice(0)
if you don't want the input HCA to be overwritten. -
Return the modifed HCA.
-
Return a new HCA in a newly allocated buffer which is the input HCA with a newly added
ciph
header section. -
There might be some HCA files which lacks
ciph
header section. Since HCA.encrypt is supposed to be in-place, combining with the fact that the size ofArrayBuffer
in JavaScript cannot be adjusted, you must manually add theciph
header section back before encrypting it. -
Throw
Error
if an existingciph
header section is already present. Please check it with something likenew HCAInfo(hca).hasHeader["ciph"]
first.
-
Return a new HCA in a newly allocated buffer, which is the input HCA with newly added header section. The newly added header section has specified
sig
andnewData
. -
Just like above, Throw
Error
if an existing header section is already present. Please check it withnew HCAInfo(hca).hasHeader[SIG]
first.
-
Set the checksum of HCA header to recalculated actual value, in-place. Pass in something like
hca.slice(0)
if you don't want the input HCA to be overwritten. -
Return the modifed HCA.
Non-static methods can only be called after creating an instance, like:
let hcaInfoInstance = new HCAInfo(hca);
let hasCiphHeader = hcaInfoInstance.hasHeader["ciph"];
-
Return an
HCAInfo
instance (referred ashcaInfoInstance
below) which contains various information parsed from HCA headers. -
It's observed that in encrypted HCAs, header section sigs like
HCA
,fmt
,ciph
etc areOR
'ed with0x80
(in other words, masked/unmasked), which should be a kind of disguise/obfusication. WhenchangeMask
is set totrue
, the input HCA will be overwritten, with each byte of its header sigs:-
OR
'ed with0x80
(ifencrypt
is set totrue
); -
AND
'ed with0x7F
(ifencrypt
is set tofalse
).
-
-
Otherwise (when
changeMask
is set tofalse
or omitted), the input HCA won't be changed. -
Throw
Error
if the inputhca
buffer has inconsistent checksum of its header, or just doesn't actually contains valid HCA data - however, this is determined by very rough method.;
- Indicates whether specified header
SIG
(like"fmt"
,"loop"
etc) exists,true
if exists, othewrise (typicallyundefined
) if not.
-
Modify header section of specified
hca
in-place according to specifiedsig
andnewData
. -
Nothing will be returned.
- Returns a clone/copy of existing
hcaInfoInstance
.
Web Worker APIs are generally recommended because they do the computational job in a background Worker thread, which won't block the foreground main thread.
For example, you may decrypt & decode a HCA (as Uint8Array
) like:
async function decryptAndDecode(hca) {
const hcaUrl = new URL("hca.js", document.baseURI);
let worker = await HCAWorker.create(hcaUrl);
let decrypted = await worker.decrypt(hca.slice(0), "defaultkey");
let wav = await worker.decode(decrypted, 16);
await worker.shutdown();
return wav;
}
-
Return a new
HCAWorker
instance (referred ashcaWorkerInstance
below), which is generally used in main thread to control aWorker
runninghca.js
, so that computational jobs can be done in background without blocking the foreground main thread. -
selfUrl
should be the URL ofhca.js
itself.
- Similar to the HCAInfo.fixHeaderChecksum/HCA.fixChecksum raw APIs described above.
async hcaWorkerInstance.addHeader(hca: Uint8Array, sig: string, newData: Uint8Array): Promise<Uint8Array>
- Similar to the HCAInfo.addCipherHeader/HCAInfo.addHeader raw APIs described above.
async hcaWorkerInstance.decrypt(hca: Uint8Array, key1?: any, key2?: any, subkey?: any): Promise<Uint8Array>
async hcaWorkerInstance.encrypt(hca: Uint8Array, key1?: any, key2?: any, subkey?: any): Promise<Uint8Array>
async hcaWorkerInstance.findKey(hca: Uint8Array, givenKeyList?: [any, any][], subkey?: any, threshold = 0.5, depth = 1024): Promise<[number, number] | undefined>
async hcaWorkerInstance.decode(hca: Uint8Array, mode = 32, loop = 0, volume = 1.0): Promise<Uint8Array>
- Similar to the HCA.decrypt/HCA.encrypt/HCA.decode/HCA.findKey raw APIs described above.
-
Measure how long a command being executed by the
Worker
controlled byhcaWorkerInstance
takes. -
Generally,
tick()
should be called right before the command(s) to be measured, andtock()
should be called after it(them). -
tick()
marks the time when something starts, returning nothing;tock()
logs (in the console) and returns how many milliseconds (ms) has elapsed since lasttick()
. -
text
is optional, which will be included in console output. -
Watch out for the characteristics of async calls.
tick()
/tock()
should be used like:hcaWorkerInstance.tick(); let wavPromise = hcaWorkerInstance.decode(hca, "defaultkey"); hcaWorkerInstance.tock(); let wav = await wavPromise;
The following incorrect usage may result in incorrect
tock()
measuring results, becausetock()
command won't be sent to theWorker
untildecode()
returns, in the meantime anothertick()
call may change the last tick time:await hcaWorkerInstance.tick(); let wav = await hcaWorkerInstance.decode(hca, "defaultkey"); await hcaWorkerInstance.tock();
-
Gracefully shut down the
Worker
controlled byhcaWorkerInstance
. -
Return nothing.
-
Once shut down, the
hcaWorkerInstance
will throwError
when its methods are still called. You may sethcaWorkerInstance = null
after shutting it down.
-
Enable or disable using transferable objects when communicating between foreground main thread and background workers. Transfering is generally much more fast if data size is large because of zero-copy.
-
Once
transferArgs
is set totrue
, arguments (like a HCA file in the form ofUint8Array
TypedArray) passed (from the foreground main thread) to hcaWorkerInstance will no longer be accessible (in the foreground main thread)! -
replyArgs
controls whether the callee/receiver (usually, but not always, the background worker) should send back the arguments originally passed in - turning this off is supposed to save a little time/overhead. Note that replying arguments always uses transfering. -
Return nothing.
- Return the
transferArgs
,replyArgs
config parameters described above.
- Return
true
if givenfile
begins with "AFS2" magic value, which indicates it's an AWB archive. Returnfalse
otherwise.
- Return an
AWBArchive
instance (referred asAWBArchiveInstance
below) which contains various information parsed from AWB headers, andHCA
files packed inside it.
subkey
used to decrypt the packed HCA files. For explanation ofsubkey
, please refer to HCA.decrypt/HCA.encrypt above.
- The given AWB archive file is splitted, so that the packed HCA files can be extracted.
new HCA(key1, key2)
Init HCA decoder with keyHCA.load(hca: Uint8Array)
Load and decrypt hca fileHCA.decode(hca: Uint8Array): Uint8Array
Decrode a decrypted hca file and return wave file