TypeScript port of VGAudio's and vgmstream's HCA codec
Some parts are ported from HCADecoder
Decrypt & decode hca(2.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
- encrypt
- recode (ogg/aac/mp3/flac)
- FFT/DCT/DCTM/IDCTM (?)
Standalone version (can be saved for offline use): hca-standalone.html
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);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.
-
key1is lower 32 bits of the keycode, which is not optional.-
If
key2is not given, it defaults to zero. -
If
key1is"nokey"or"defaultkey",key2is then ignored. Settingkey1to"nokey"means the encryption/decryption should be done in "no key" mode. Settingkey1to"defaultkey"means the hard-coded default keys (allegedly for Magia Record) should be used for encryption/decryption. -
subkeyis ignored if set to zero. -
subkeyis only applied with cipher type0x38for 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
ciphheader section cannot be directly encrypted. See HCAInfo.addCipherHeader below. -
Checksums will be verified in the process, and
Errorwill 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.
givenKeyListis 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
givenKeyListis given or not. -
For explanation of
key1,key2,subkey, please refer to HCA.decrypt/HCA.encrypt above. -
This method will search for
depthHCA blocks, 1024 by default. -
If
thresholdpercentage (50% by default) of blocks can be decrypted and unpacked, the found key will be returned as[key1, key2]. Otherwise,undefinedwill be returned.
Return decoded (Windows PCM) WAV of the input whole HCA file. The input HCA must be unencrypted, otherwise Error will be thrown.
-
modeargumentmodeis optional, by default it's set to 32. Validmodevalues 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.
-
-
loopargumentHCA make use of
loopheader section to record which part of the audio should be looped, for how many times.loopargument is optional, and it is meaningful only if the input HCA hasloopheader.loopsimply indicates how many times the looped part of audio should be inserted. For example, withloopset to2, the resulting WAV audio will be like:Beginning part Looped part 1st inserted Looped part 2nd inserted Looped part Ending part Setting
loopargument 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
Errorwill be thrown on any mismatch.
-
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
ciphheader section. -
There might be some HCA files which lacks
ciphheader section. Since HCA.encrypt is supposed to be in-place, combining with the fact that the size ofArrayBufferin JavaScript cannot be adjusted, you must manually add theciphheader section back before encrypting it. -
Throw
Errorif an existingciphheader 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
sigandnewData. -
Just like above, Throw
Errorif 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
HCAInfoinstance (referred ashcaInfoInstancebelow) which contains various information parsed from HCA headers. -
It's observed that in encrypted HCAs, header section sigs like
HCA,fmt,ciphetc areOR'ed with0x80(in other words, masked/unmasked), which should be a kind of disguise/obfusication. WhenchangeMaskis set totrue, the input HCA will be overwritten, with each byte of its header sigs:-
OR'ed with0x80(ifencryptis set totrue); -
AND'ed with0x7F(ifencryptis set tofalse).
-
-
Otherwise (when
changeMaskis set tofalseor omitted), the input HCA won't be changed. -
Throw
Errorif the inputhcabuffer 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,trueif exists, othewrise (typicallyundefined) if not.
-
Modify header section of specified
hcain-place according to specifiedsigandnewData. -
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
HCAWorkerinstance (referred ashcaWorkerInstancebelow), which is generally used in main thread to control aWorkerrunninghca.js, so that computational jobs can be done in background without blocking the foreground main thread. -
selfUrlshould be the URL ofhca.jsitself.
- 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
Workercontrolled byhcaWorkerInstancetakes. -
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(). -
textis 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 theWorkeruntildecode()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
Workercontrolled byhcaWorkerInstance. -
Return nothing.
-
Once shut down, the
hcaWorkerInstancewill throwErrorwhen its methods are still called. You may sethcaWorkerInstance = nullafter 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
transferArgsis set totrue, arguments (like a HCA file in the form ofUint8ArrayTypedArray) passed (from the foreground main thread) to hcaWorkerInstance will no longer be accessible (in the foreground main thread)! -
replyArgscontrols 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,replyArgsconfig parameters described above.
- Return
trueif givenfilebegins with "AFS2" magic value, which indicates it's an AWB archive. Returnfalseotherwise.
- Return an
AWBArchiveinstance (referred asAWBArchiveInstancebelow) which contains various information parsed from AWB headers, andHCAfiles packed inside it.
subkeyused 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): Uint8ArrayDecrode a decrypted hca file and return wave file