From b74c13621d17c4536ce49ecf366f8ae8789483ed Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Jul 2026 11:48:26 +0200 Subject: [PATCH] Update pad parser to handle fractional pitch and trim length correctly --- src/lib/parsers.ts | 13 ++++++++---- tests/pad-parser.test.ts | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 tests/pad-parser.test.ts diff --git a/src/lib/parsers.ts b/src/lib/parsers.ts index 0dff24c..7502c10 100644 --- a/src/lib/parsers.ts +++ b/src/lib/parsers.ts @@ -167,10 +167,15 @@ export function collectPads(files: TarFile[], sounds: Sound[]) { const soundId = (file.data[2] << 8) + file.data[1]; const sound = sounds.find((s) => s.id === soundId); const pitch = file.data[17] <= 12 ? file.data[17] : -(256 - file.data[17]); // pitch from -12 to +12 - const pitchDecimal = file.data[26]; + const pitchFraction = (file.data[26] ?? 0) / 100; const pan = (file.data[18] >= 240 ? -(256 - file.data[18]) : file.data[18]) / 16; // normalized pan const trimLeft = (file.data[6] << 16) + (file.data[5] << 8) + file.data[4]; - const trimRight = trimLeft + (file.data[10] << 16) + (file.data[9] << 8) + file.data[8]; + const trimLength = new DataView( + file.data.buffer, + file.data.byteOffset, + file.data.byteLength, + ).getUint32(8, true); + const trimRight = trimLeft + trimLength; result[group.id].push({ pad: i, @@ -187,12 +192,12 @@ export function collectPads(files: TarFile[], sounds: Sound[]) { trimRight, playMode: file.data[23] === 0 ? 'oneshot' : file.data[23] === 1 ? 'key' : 'legato', soundLength: sound ? calculateSoundLength(sound) : 0, - pitch: Math.max(-12, Math.min(12, parseFloat(`${pitch}.${pitchDecimal}`))), + pitch: Math.max(-12, Math.min(12, pitch + pitchFraction)), rootNote: file.data[24], timeStretch: timeStretch(file.data[21]), timeStretchBpm: Number(bytesToFloat32(file.data.slice(12, 16)).toFixed(2)), timeStretchBars: timeStretchBars(file.data[25]), - inChokeGroup: file.data[22] === 1, + inChokeGroup: file.data[22] !== 0, midiChannel: file.data[3], }); } diff --git a/tests/pad-parser.test.ts b/tests/pad-parser.test.ts new file mode 100644 index 0000000..034c9ff --- /dev/null +++ b/tests/pad-parser.test.ts @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { collectPads } from '../src/lib/parsers'; +import { TarFile } from '../src/lib/untar'; + +function padFile(name: string, length: 26 | 27) { + const data = new Uint8Array(length); + return { data, file: new TarFile(name, data.length, data) }; +} + +test('decodes optional fractional pitch arithmetically for 26- and 27-byte pad records', () => { + const native = padFile('pads/a/p01', 26); + native.data[17] = 0xff; + const extended = padFile('pads/a/p02', 27); + extended.data[17] = 0xff; + extended.data[26] = 91; + + const pads = collectPads([native.file, extended.file], []); + + assert.equal(pads.a[0].pitch, -1); + assert.ok(Math.abs(pads.a[1].pitch - -0.09) < 1e-12); +}); + +test('reads trim length as unsigned 32-bit little-endian data', () => { + const pad = padFile('pads/a/p01', 26); + pad.data.set([0x03, 0x02, 0x01], 4); + pad.data.set([0x04, 0x03, 0x02, 0x01], 8); + + const parsed = collectPads([pad.file], []).a[0]; + + assert.equal(parsed.trimLeft, 0x010203); + assert.equal(parsed.trimRight, 0x010203 + 0x01020304); +}); + +test('treats every nonzero mute byte as shared mute-group membership', () => { + const member = padFile('pads/a/p01', 26); + const nonmember = padFile('pads/a/p02', 26); + member.data[22] = 200; + + const pads = collectPads([member.file, nonmember.file], []).a; + + assert.equal(pads[0].inChokeGroup, true); + assert.equal(pads[1].inChokeGroup, false); +});