diff --git a/.gitignore b/.gitignore index 6c38d55..b244040 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ *.log *.brstm +*.bfstm node_modules .DS_Store dist diff --git a/app/package.json b/app/package.json index 3f911f7..ac38e72 100644 --- a/app/package.json +++ b/app/package.json @@ -29,6 +29,7 @@ "build-gh-pages": "vite build --base \"/nikku/\"" }, "dependencies": { + "bfstm": "^1.0.0", "brstm": "^1.6.1", "comlink": "^4.3.1", "lit": "^2.4.0" diff --git a/app/src/audio-decoder/worker.ts b/app/src/audio-decoder/worker.ts index 470b1da..2188b96 100644 --- a/app/src/audio-decoder/worker.ts +++ b/app/src/audio-decoder/worker.ts @@ -1,41 +1,41 @@ -import { Brstm, Metadata } from 'brstm'; -import { transfer } from 'comlink'; +import { Bfstm, Metadata } from 'bfstm'; +// import { transfer } from 'comlink'; -let brstm: Brstm | null = null; +let instance: Bfstm | null = null; export function init(receivedBuffer: ArrayBuffer) { - brstm = new Brstm(receivedBuffer); + instance = new Bfstm(receivedBuffer); } export function destroy() { - brstm = null; + instance = null; } export function getMetadata(): Metadata | undefined { - if (!brstm) { + if (!instance) { return; } - return brstm.metadata; + return instance.metadata; } -export function getAllSamples() { - if (!brstm) { - return; - } - const allSamples = brstm.getAllSamples(); - return transfer( - allSamples, - allSamples.map((allSamplesPerChannel) => allSamplesPerChannel.buffer) - ); -} +// export function getAllSamples() { +// if (!brstm) { +// return; +// } +// const allSamples = brstm.getAllSamples(); +// return transfer( +// allSamples, +// allSamples.map((allSamplesPerChannel) => allSamplesPerChannel.buffer) +// ); +// } -export function getSamples(offset: number, size: number) { - if (!brstm) { - return; - } - const allSamples = brstm.getSamples(offset, size).map(convertToFloat32); - return transfer( - allSamples, - allSamples.map((allSamplesPerChannel) => allSamplesPerChannel.buffer) - ); -} +// export function getSamples(offset: number, size: number) { +// if (!brstm) { +// return; +// } +// const allSamples = brstm.getSamples(offset, size).map(convertToFloat32); +// return transfer( +// allSamples, +// allSamples.map((allSamplesPerChannel) => allSamplesPerChannel.buffer) +// ); +// } function convertToFloat32(pcmSamples: Int16Array): Float32Array { diff --git a/app/src/elements/nikku-main.ts b/app/src/elements/nikku-main.ts index 0b511bd..89cbd48 100644 --- a/app/src/elements/nikku-main.ts +++ b/app/src/elements/nikku-main.ts @@ -95,7 +95,7 @@ export class NikkuMain extends LitElement { @@ -210,6 +210,8 @@ export class NikkuMain extends LitElement { try { await this.workerInstance.init(transfer(buffer, [buffer])); const metadata = await this.workerInstance.getMetadata(); + console.log('metadata', metadata); + return; if (this.audioPlayer) { await this.audioPlayer.destroy(); diff --git a/packages/bfstm/README.md b/packages/bfstm/README.md new file mode 100644 index 0000000..e3d0eb1 --- /dev/null +++ b/packages/bfstm/README.md @@ -0,0 +1,3 @@ +# BFSTM + +https://mk8.tockdom.com/wiki/BFSTM_(File_Format) diff --git a/packages/bfstm/package.json b/packages/bfstm/package.json new file mode 100644 index 0000000..f578a58 --- /dev/null +++ b/packages/bfstm/package.json @@ -0,0 +1,55 @@ +{ + "name": "bfstm", + "version": "1.0.0", + "description": "BFSTM Decoder", + "keywords": [ + "bfstm" + ], + "sideEffects": false, + "source": "src/index.ts", + "main": "./dist/bfstm.js", + "module": "./dist/bfstm.mjs", + "umd:main": "./dist/bfstm.umd.js", + "unpkg": "./dist/bfstm.umd.js", + "exports": { + ".": { + "import": { + "nikku:source": "./src/index.ts", + "default": "./dist/bfstm.mjs" + }, + "require": "./dist/bfstm.js" + } + }, + "types": "./types/index.d.ts", + "files": [ + "dist/", + "types/", + "src/" + ], + "author": { + "name": "Kenrick", + "email": "kenrick95@gmail.com" + }, + "repository": { + "type": "git", + "url": "https://github.com/kenrick95/nikku.git", + "directory": "packages/bfstm" + }, + "homepage": "https://github.com/kenrick95/nikku", + "bugs": { + "url": "https://github.com/kenrick95/nikku/issues" + }, + "license": "MIT", + "scripts": { + "prepublishOnly": "pnpm run build", + "typecheck": "tsc --noEmit --emitDeclarationOnly false", + "build": "pnpm run build-declaration && pnpm build-lib", + "build-declaration": "tsc", + "build-lib": "vite build" + }, + "devDependencies": { + "@nikku/utils": "workspace:*", + "typescript": "^4.8.4", + "vite": "^3.1.8" + } +} diff --git a/packages/bfstm/src/index.ts b/packages/bfstm/src/index.ts new file mode 100644 index 0000000..26d457c --- /dev/null +++ b/packages/bfstm/src/index.ts @@ -0,0 +1,193 @@ +import { + getSliceAsString, + getSliceAsNumber, + getInt16, + clamp, + getEndianness, +} from '@nikku/utils'; +import type { Endianness } from '@nikku/utils'; +import type { + ChannelInfo, + CodecType, + Metadata, + TrackDescription, +} from './types'; + +export * from './types'; + +declare var console: any; + +/** + * @class + */ +export class Bfstm { + rawData: Uint8Array; + endianness: Endianness; + versionNumber: number; + metadata: Metadata; + + #offsetToInfo: number; + #offsetToSeek: number; + #offsetToData: number; + + constructor(arrayBuffer: ArrayBuffer) { + /** + * @type {Uint8Array} rawData + */ + this.rawData = new Uint8Array(arrayBuffer); + + if (getSliceAsString(this.rawData, 0, 4) !== 'FSTM') { + throw new Error('Not a valid BFSTM file'); + } + + this.endianness = getEndianness(this.rawData); + this.versionNumber = getSliceAsNumber( + this.rawData, + 0x08, + 4, + this.endianness + ); + this.#offsetToInfo = getSliceAsNumber( + this.rawData, + 0x18, + 4, + this.endianness + ); + this.#offsetToSeek = getSliceAsNumber( + this.rawData, + 0x24, + 4, + this.endianness + ); + this.#offsetToData = getSliceAsNumber( + this.rawData, + 0x30, + 4, + this.endianness + ); + + this.metadata = this.#getMetadata(); + } + + #getMetadata() { + const offsetToStreamInfo = + this.#offsetToInfo + + getSliceAsNumber( + this.rawData, + this.#offsetToInfo + 0x0c, + 4, + this.endianness + ) + + 0x08; + const offsetToTrackInfo = + this.#offsetToInfo + + getSliceAsNumber( + this.rawData, + this.#offsetToInfo + 0x14, + 4, + this.endianness + ) + + 0x08; + const offsetToChannelInfo = + this.#offsetToInfo + + getSliceAsNumber( + this.rawData, + this.#offsetToInfo + 0x1c, + 4, + this.endianness + ) + + 0x08; + + /** + * @type {Metadata} + */ + const metadata: Metadata = { + + offsetToTrackInfo, + offsetToChannelInfo, + + fileSize: getSliceAsNumber(this.rawData, 0x0c, 4, this.endianness), + endianness: this.endianness, + codec: getSliceAsNumber( + this.rawData, + offsetToStreamInfo, + 1, + this.endianness + ) as CodecType, + loopFlag: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x01, + 1, + this.endianness + ), + numberChannels: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x02, + 1, + this.endianness + ), + numberRegions: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x03, + 1, + this.endianness + ), + sampleRate: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x04, + 4, + this.endianness + ), + loopStartSample: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x08, + 4, + this.endianness + ), + totalSamples: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x0c, + 4, + this.endianness + ), + totalBlocks: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x10, + 4, + this.endianness + ), + blockSize: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x14, + 4, + this.endianness + ), + samplesPerBlock: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x18, + 4, + this.endianness + ), + finalBlockSize: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x1c, + 4, + this.endianness + ), + totalSamplesInFinalBlock: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x20, + 4, + this.endianness + ), + finalBlockSizeWithPadding: getSliceAsNumber( + this.rawData, + offsetToStreamInfo + 0x24, + 4, + this.endianness + ), + }; + + return metadata; + } +} diff --git a/packages/bfstm/src/types.ts b/packages/bfstm/src/types.ts new file mode 100644 index 0000000..52fb081 --- /dev/null +++ b/packages/bfstm/src/types.ts @@ -0,0 +1,53 @@ +import type { Endianness } from '@nikku/utils'; +export type ChannelInfo = { + adpcmCoefficients: number[]; + gain: number; + initialPredictorScale: number; + historySample1: number; + historySample2: number; + loopPredictorScale: number; + loopHistorySample1: number; + loopHistorySample2: number; +}; +export type TrackDescription = { + numberChannels: number; + type: number; +}; +/** + * - 0 = PCM8 + * - 1 = PCM16 + * - 2 = DSP ADPCM + * - 3 = IMA ADPCM. + */ +export type CodecType = 0 | 1 | 2 | 3; +export type Metadata = { + fileSize: number; + endianness: Endianness; + codec: CodecType; + loopFlag: number; + numberChannels: number; + numberRegions: number; + sampleRate: number; + /** loop start, in terms of sample # */ + loopStartSample: number; + totalSamples: number; + /** total number of blocks, per channel, including final block */ + totalBlocks: number; + blockSize: number; + samplesPerBlock: number; + /** Final block size, without padding, in bytes */ + finalBlockSize: number; + /** Final block size, **with** padding, in bytes */ + finalBlockSizeWithPadding: number; + /** Total samples in final block */ + totalSamplesInFinalBlock: number; + + /** Samples per entry in ADPC table */ + adpcTableSamplesPerEntry: number; + /** Bytes per entry in ADPC table */ + adpcTableBytesPerEntry: number; + /** Number of tracks */ + numberTracks: number; + trackDescriptionType: number; + trackDescriptions: TrackDescription[]; +}; diff --git a/packages/bfstm/tsconfig.json b/packages/bfstm/tsconfig.json new file mode 100644 index 0000000..9cd71ad --- /dev/null +++ b/packages/bfstm/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "esnext", + "lib": ["es2017"], + "outDir": "./types", + "rootDir": "./src", + "strict": true, + "declaration": true, + "emitDeclarationOnly": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": false, + "target": "ESNext", + "skipLibCheck": true, + "typeRoots": [] + }, + "include": ["src/"], + "exclude": ["node_modules/", "tests/"] +} diff --git a/packages/brstm/package.json b/packages/brstm/package.json index 96efbe7..407a496 100644 --- a/packages/brstm/package.json +++ b/packages/brstm/package.json @@ -49,6 +49,7 @@ "test": "pnpm run build-lib && node tests/index.test.js" }, "devDependencies": { + "@nikku/utils": "workspace:*", "typescript": "^4.8.4", "vite": "^3.1.8" } diff --git a/packages/brstm/src/index.ts b/packages/brstm/src/index.ts index 9e5d7a8..d28512e 100644 --- a/packages/brstm/src/index.ts +++ b/packages/brstm/src/index.ts @@ -4,8 +4,8 @@ import { getInt16, clamp, getEndianness, -} from './utils'; -import type { Endianness } from './utils'; +} from '@nikku/utils'; +import type { Endianness } from '@nikku/utils'; import type { ChannelInfo, CodecType, diff --git a/packages/brstm/src/types.ts b/packages/brstm/src/types.ts index c5a8bfa..09903d6 100644 --- a/packages/brstm/src/types.ts +++ b/packages/brstm/src/types.ts @@ -1,4 +1,4 @@ -import type { Endianness } from './utils'; +import type { Endianness } from '@nikku/utils'; export type ChannelInfo = { adpcmCoefficients: number[]; gain: number; diff --git a/packages/dsp-adpcm/package.json b/packages/dsp-adpcm/package.json new file mode 100644 index 0000000..26358d3 --- /dev/null +++ b/packages/dsp-adpcm/package.json @@ -0,0 +1,9 @@ +{ + "name": "@nikku/dsp-adpcm", + "private": true, + "main": "src/index.ts", + "sideEffects": false, + "dependencies": { + "@nikku/utils": "workspace:^" + } +} diff --git a/packages/dsp-adpcm/src/index.ts b/packages/dsp-adpcm/src/index.ts new file mode 100644 index 0000000..b2456b3 --- /dev/null +++ b/packages/dsp-adpcm/src/index.ts @@ -0,0 +1,89 @@ +import { clamp } from '@nikku/utils'; + +export class DspAdpcm { + samplesInFinalBlock: number; + samplesPerBlock: number; + totalBlocks: number; + totalChannels: number; + + constructor({ + samplesInFinalBlock, + samplesPerBlock, + totalBlocks, + totalChannels, + }: { + samplesInFinalBlock: number; + samplesPerBlock: number; + totalBlocks: number; + totalChannels: number; + }) { + this.samplesInFinalBlock = samplesInFinalBlock; + this.samplesPerBlock = samplesPerBlock; + this.totalBlocks = totalBlocks; + this.totalChannels = totalChannels; + } + #getSamplesAtBlock(b: number): Array { + const result: Array = []; + const totalSamplesInBlock = + b === totalBlocks - 1 ? samplesInFinalBlock : samplesPerBlock; + + for (let c = 0; c < numberChannels; c++) { + result.push(new Int16Array(totalSamplesInBlock)); + } + + for (let c = 0; c < numberChannels; c++) { + const { adpcmCoefficients } = channelInfo[c]; + const blockData = allChannelsBlockData[c]; + + const sampleResult: Array = []; + const ps = blockData[0]; + const { yn1, yn2 } = adpcChunkData[c][b]; + + // #region Magic adapted from brawllib's ADPCMState.cs + let cps = ps, + cyn1 = yn1, + cyn2 = yn2, + dataIndex = 0; + + for (let sampleIndex = 0; sampleIndex < totalSamplesInBlock; ) { + let outSample = 0; + if (sampleIndex % 14 === 0) { + cps = blockData[dataIndex++]; + } + if ((sampleIndex++ & 1) === 0) { + outSample = blockData[dataIndex] >> 4; + } else { + outSample = blockData[dataIndex++] & 0x0f; + } + if (outSample >= 8) { + outSample -= 16; + } + const scale = 1 << (cps & 0x0f); + const cIndex = (cps >> 4) << 1; + + outSample = + (0x400 + + ((scale * outSample) << 11) + + adpcmCoefficients[clamp(cIndex, 0, 15)] * cyn1 + + adpcmCoefficients[clamp(cIndex + 1, 0, 15)] * cyn2) >> + 11; + + cyn2 = cyn1; + cyn1 = clamp(outSample, -32768, 32767); + + sampleResult.push(cyn1); + } + + // Overwrite history samples for the next block with decoded samples + if (b < totalBlocks - 1) { + adpcChunkData[c][b + 1].yn1 = sampleResult[totalSamplesInBlock - 1]; + adpcChunkData[c][b + 1].yn2 = sampleResult[totalSamplesInBlock - 2]; + } + + // #endregion + + result[c].set(sampleResult); + } + return result; + } +} diff --git a/packages/dsp-adpcm/tsconfig.json b/packages/dsp-adpcm/tsconfig.json new file mode 100644 index 0000000..9cd71ad --- /dev/null +++ b/packages/dsp-adpcm/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "esnext", + "lib": ["es2017"], + "outDir": "./types", + "rootDir": "./src", + "strict": true, + "declaration": true, + "emitDeclarationOnly": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": false, + "target": "ESNext", + "skipLibCheck": true, + "typeRoots": [] + }, + "include": ["src/"], + "exclude": ["node_modules/", "tests/"] +} diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 0000000..9d58811 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@nikku/utils", + "private": true, + "main": "src/index.ts", + "sideEffects": false +} diff --git a/packages/brstm/src/utils.ts b/packages/utils/src/index.ts similarity index 91% rename from packages/brstm/src/utils.ts rename to packages/utils/src/index.ts index a0cb432..ecfac86 100644 --- a/packages/brstm/src/utils.ts +++ b/packages/utils/src/index.ts @@ -5,7 +5,6 @@ export function getSlice( ): number[] { const result = []; for (let i = start; i < start + length; i++) { - // Apparently unsigned result.push(uint8Array[i]); } return result; @@ -77,7 +76,8 @@ export function getSliceAsNumber( if (endianness === ENDIAN.LITTLE) { resArr.reverse(); } - return resArr.reduce((acc, curr) => acc * 256 + curr, 0); + const res = resArr.reduce((acc, curr) => acc * 256 + curr, 0); + return res; } /** diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 0000000..9cd71ad --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "esnext", + "lib": ["es2017"], + "outDir": "./types", + "rootDir": "./src", + "strict": true, + "declaration": true, + "emitDeclarationOnly": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": false, + "target": "ESNext", + "skipLibCheck": true, + "typeRoots": [] + }, + "include": ["src/"], + "exclude": ["node_modules/", "tests/"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bed2dee..87c83f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,7 @@ importers: app: specifiers: '@types/audioworklet': ^0.0.33 + bfstm: ^1.0.0 brstm: ^1.6.1 comlink: ^4.3.1 lit: ^2.4.0 @@ -17,6 +18,7 @@ importers: vite-plugin-comlink: ^3.0.4 vite-plugin-pwa: ^0.13.1 dependencies: + bfstm: link:../packages/bfstm brstm: link:../packages/brstm comlink: 4.3.1 lit: 2.4.0 @@ -28,14 +30,35 @@ importers: vite-plugin-comlink: 3.0.4_comlink@4.3.1+vite@3.1.8 vite-plugin-pwa: 0.13.1_vite@3.1.8 + packages/bfstm: + specifiers: + '@nikku/utils': workspace:* + typescript: ^4.8.4 + vite: ^3.1.8 + devDependencies: + '@nikku/utils': link:../utils + typescript: 4.8.4 + vite: 3.1.8 + packages/brstm: specifiers: + '@nikku/utils': workspace:* typescript: ^4.8.4 vite: ^3.1.8 devDependencies: + '@nikku/utils': link:../utils typescript: 4.8.4 vite: 3.1.8 + packages/dsp-adpcm: + specifiers: + '@nikku/utils': workspace:^ + dependencies: + '@nikku/utils': link:../utils + + packages/utils: + specifiers: {} + packages: /@ampproject/remapping/2.2.0: