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: