diff --git a/src/lib/parsers.ts b/src/lib/parsers.ts index 7502c10..12feb84 100644 --- a/src/lib/parsers.ts +++ b/src/lib/parsers.ts @@ -13,6 +13,7 @@ import { Scene, SceneGroupPatternData, ScenesSettings, + SidechainGroupRouting, SongPosition, Sound, TimeSignature, @@ -608,12 +609,43 @@ export function collectEffects(files: TarFile[]) { const param2 = bytesToFloat32( fxFile.data.slice(76 + (effectType - 1) * 4, 80 + (effectType - 1) * 4), ); + const outputCompressor = + fxFile.data.length >= 144 + ? { + drive: bytesToFloat32(fxFile.data.slice(136, 140)), + speed: bytesToFloat32(fxFile.data.slice(140, 144)), + } + : undefined; + const sidechain = + fxFile.data.length >= 152 + ? { + length: bytesToFloat32(fxFile.data.slice(144, 148)), + shape: bytesToFloat32(fxFile.data.slice(148, 152)), + routing: + fxFile.data.length >= 160 + ? (Object.fromEntries( + projectGroups.map((group, index) => { + const offset = 152 + index * 2; + const rawValue = fxFile.data[offset] | (fxFile.data[offset + 1] << 8); + const routing: SidechainGroupRouting = { + rawValue, + sourcePadMask: rawValue & 0x0fff, + duckedDestination: (rawValue & 0x8000) !== 0, + }; + return [group, routing]; + }), + ) as Record) + : undefined, + } + : undefined; return { rawData: fxFile.data, effectType, param1, param2, + ...(outputCompressor ? { outputCompressor } : {}), + ...(sidechain ? { sidechain } : {}), }; } diff --git a/src/types/types.ts b/src/types/types.ts index dec8af3..243f74b 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -60,11 +60,30 @@ export type ScenesSettings = { timeSignature: TimeSignature; }; +export type OutputCompressorSettings = { + drive: number; + speed: number; +}; + +export type SidechainGroupRouting = { + rawValue: number; + sourcePadMask: number; + duckedDestination: boolean; +}; + +export type SidechainSettings = { + length: number; + shape: number; + routing?: Record; +}; + export type Effects = { rawData: Uint8Array; effectType: EffectType; param1: number; param2: number; + outputCompressor?: OutputCompressorSettings; + sidechain?: SidechainSettings; }; export type Pattern = { diff --git a/tests/project-effects.test.ts b/tests/project-effects.test.ts new file mode 100644 index 0000000..71f087e --- /dev/null +++ b/tests/project-effects.test.ts @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { collectEffects } from '../src/lib/parsers'; +import { TarFile } from '../src/lib/untar'; + +function effectsFile(length: number) { + const data = new Uint8Array(length); + data[4] = 6; + return { + data, + file: new TarFile('fx_settings', data.length, data), + view: new DataView(data.buffer), + }; +} + +test('preserves selected FX parsing for short files without extended state', () => { + const fixture = effectsFile(100); + fixture.view.setFloat32(32, 0.25, true); + fixture.view.setFloat32(96, 0.75, true); + + const effects = collectEffects([fixture.file]); + + assert.equal(effects.effectType, 6); + assert.equal(effects.param1, 0.25); + assert.equal(effects.param2, 0.75); + assert.equal(effects.rawData, fixture.data); + assert.equal(effects.outputCompressor, undefined); + assert.equal(effects.sidechain, undefined); +}); + +test('parses output compressor state from a 144-byte file without sidechain state', () => { + const fixture = effectsFile(144); + fixture.view.setFloat32(136, 0.25, true); + fixture.view.setFloat32(140, 0.75, true); + + const effects = collectEffects([fixture.file]); + + assert.deepEqual(effects.outputCompressor, { drive: 0.25, speed: 0.75 }); + assert.equal(effects.sidechain, undefined); +}); + +test('parses P07-like output dynamics and sidechain state from a 160-byte file', () => { + const fixture = effectsFile(160); + fixture.view.setFloat32(32, 0.5, true); + fixture.view.setFloat32(96, 0.5, true); + fixture.view.setFloat32(136, 0.5, true); + fixture.view.setFloat32(140, 0.5, true); + fixture.view.setFloat32(144, 0, true); + fixture.view.setFloat32(148, 0.5, true); + fixture.view.setUint16(152, 0x0600, true); + fixture.view.setUint16(154, 0x8000, true); + fixture.view.setUint16(156, 0x8000, true); + fixture.view.setUint16(158, 0x8000, true); + + const effects = collectEffects([fixture.file]); + + assert.deepEqual(effects.outputCompressor, { drive: 0.5, speed: 0.5 }); + assert.deepEqual(effects.sidechain, { + length: 0, + shape: 0.5, + routing: { + a: { rawValue: 0x0600, sourcePadMask: 0x0600, duckedDestination: false }, + b: { rawValue: 0x8000, sourcePadMask: 0, duckedDestination: true }, + c: { rawValue: 0x8000, sourcePadMask: 0, duckedDestination: true }, + d: { rawValue: 0x8000, sourcePadMask: 0, duckedDestination: true }, + }, + }); +});