From da0398e738a3f0208a16bb702d7a53f13a8582f8 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Jul 2026 09:28:59 +0200 Subject: [PATCH] Refactor scene and pattern collection logic to use group patterns instead of intermediate scenes data --- src/lib/parsers.ts | 277 ++++++++++++++++++++++++++++++--------------- 1 file changed, 188 insertions(+), 89 deletions(-) diff --git a/src/lib/parsers.ts b/src/lib/parsers.ts index b6a3fc6..847a5c8 100644 --- a/src/lib/parsers.ts +++ b/src/lib/parsers.ts @@ -301,17 +301,116 @@ export function parsePatternEvents(data: Uint8Array, startOffset: number = 4): P }; } -function getPatternsForScene(scenesIntermediate: IntermediateScenes, scenePatternUsage: SceneMeta) { +function getPatternKey(group: Group, patternNumber: number) { + return `${group}${patternNumber}`; +} + +function normalizeTimeSignature( + numerator: number, + denominator: number, + fallback: TimeSignature, +): TimeSignature { + const validNumerator = Number.isInteger(numerator) && numerator > 0 && numerator <= 99; + const validDenominator = + Number.isInteger(denominator) && + denominator > 0 && + denominator <= 64 && + (denominator & (denominator - 1)) === 0; + + return { + numerator: validNumerator ? numerator : fallback.numerator, + denominator: validDenominator ? denominator : fallback.denominator, + }; +} + +function collectSceneRecords(data: Uint8Array): SceneRecord[] { + const headerTimeSignature = normalizeTimeSignature( + data[5], + data[6], + defaultScenesSettings.timeSignature, + ); + const sceneRecords: SceneRecord[] = []; + + for (let sceneIndex = 0; sceneIndex < 99; sceneIndex++) { + const offset = 7 + sceneIndex * 6; + if (offset + 6 > data.length) { + break; + } + + const patternNumbers: Record = { + a: data[offset], + b: data[offset + 1], + c: data[offset + 2], + d: data[offset + 3], + }; + + if (!projectGroups.some((group) => patternNumbers[group] !== 0)) { + continue; + } + + sceneRecords.push({ + number: sceneIndex + 1, + patternNumbers, + timeSignature: normalizeTimeSignature( + data[offset + 4], + data[offset + 5], + headerTimeSignature, + ), + }); + } + + return sceneRecords; +} + +function collectGroupPatterns(files: TarFile[], sku: string): GroupPattern[] { + const groupPatterns = new Map(); + + for (const file of files) { + if (file.type !== 'file' || !file.data) { + continue; + } + + const matches = file.name.match(/^patterns\/([abcd])(\d{1,2})$/); + if (!matches) { + continue; + } + + const group = matches[1] as Group; + const patternNumber = Number(matches[2]); + if (patternNumber < 1 || patternNumber > 99) { + continue; + } + + const parsedPattern = parsePatternEvents(file.data, sku === SKU_EP40 ? 6 : 4); + groupPatterns.set(getPatternKey(group, patternNumber), { + group, + patternNumber, + ...parsedPattern, + }); + } + + return [...groupPatterns.values()].toSorted((a, b) => { + const numberDifference = a.patternNumber - b.patternNumber; + if (numberDifference !== 0) { + return numberDifference; + } + + return projectGroups.indexOf(a.group) - projectGroups.indexOf(b.group); + }); +} + +function getPatternsForScene( + groupPatterns: Map, + patternNumbers: Record, +) { const patterns: Pattern[] = []; - for (const group of ['a', 'b', 'c', 'd'] as const) { - const scene = scenePatternUsage[group]; - const lookupSceneData = scenesIntermediate[scene]; + for (const group of projectGroups) { + const groupPattern = groupPatterns.get(getPatternKey(group, patternNumbers[group])); - if (lookupSceneData?.groups[group]) { - const groupData = lookupSceneData.groups[group]; - const sources = new Set(Object.keys(groupData.notes).map(Number)); - groupData.events.forEach((event) => { + if (groupPattern) { + const sources = new Set(Object.keys(groupPattern.notes).map(Number)); + groupPattern.events.forEach((event) => { if (event.type === 'midi' && event.source >= 0 && event.source < 12) { sources.add(event.source); } @@ -321,8 +420,8 @@ function getPatternsForScene(scenesIntermediate: IntermediateScenes, scenePatter patterns.push({ pad: `${group}${padNum}` as PadCode, group, - notes: groupData.notes[padNum] || [], - bars: groupData.bars, + notes: groupPattern.notes[padNum] || [], + bars: groupPattern.bars, }); }); } @@ -332,22 +431,22 @@ function getPatternsForScene(scenesIntermediate: IntermediateScenes, scenePatter } function getPatternEventsForScene( - scenesIntermediate: IntermediateScenes, - scenePatternUsage: SceneMeta, + groupPatterns: Map, + patternNumbers: Record, ) { const patternEvents: Partial> = {}; - for (const group of ['a', 'b', 'c', 'd'] as const) { - const patternNumber = scenePatternUsage[group]; - const groupData = scenesIntermediate[patternNumber]?.groups[group]; + for (const group of projectGroups) { + const patternNumber = patternNumbers[group]; + const groupPattern = groupPatterns.get(getPatternKey(group, patternNumber)); - if (groupData) { + if (groupPattern) { patternEvents[group] = { group, patternNumber, - bars: groupData.bars, - recordCount: groupData.recordCount, - events: groupData.events, + bars: groupPattern.bars, + recordCount: groupPattern.recordCount, + events: groupPattern.events, }; } } @@ -355,90 +454,90 @@ function getPatternEventsForScene( return patternEvents; } -export function collectScenesAndPatterns(files: TarFile[], sku: string) { - const scenesIntermediateData: IntermediateScenes = {}; - const scenes: Record = {}; - const scenePatternUsage: Record = {}; +function buildScene( + sceneRecord: SceneRecord, + groupPatterns: Map, +): Scene { + return { + number: sceneRecord.number, + name: String(sceneRecord.number).padStart(2, '0'), + patternNumbers: sceneRecord.patternNumbers, + timeSignature: sceneRecord.timeSignature, + patterns: getPatternsForScene(groupPatterns, sceneRecord.patternNumbers), + patternEvents: getPatternEventsForScene(groupPatterns, sceneRecord.patternNumbers), + }; +} - // build default scene metadata (in case "scenes" file is missing) - for (let i = 1; i <= 99; i++) { - scenePatternUsage[i] = { - a: i, - b: i, - c: i, - d: i, - }; +function collectSongPositions(data: Uint8Array): SongPosition[] { + const songCountOffset = 612; + const firstSongPositionOffset = 613; + if (data.length <= songCountOffset) { + return []; } - // build group/pattern lookup table - const scenesFile = files.find((f) => f.name === 'scenes' && f.type === 'file'); - if (scenesFile?.data) { - const chunks = chunkArray(scenesFile.data, 6, 7); - chunks.forEach((chunk, i) => { - if (i > 98) { - return; - } + const availablePositions = Math.max(0, data.length - firstSongPositionOffset); + const songCount = Math.min(data[songCountOffset], 99, availablePositions); + if (songCount === 1 && data[firstSongPositionOffset] === 1) { + return []; + } - scenePatternUsage[i + 1] = { - a: chunk[0], - b: chunk[1], - c: chunk[2], - d: chunk[3], - }; + const songPositions: SongPosition[] = []; + + for (let index = 0; index < songCount; index++) { + const sceneNumber = data[firstSongPositionOffset + index]; + if (sceneNumber < 1 || sceneNumber > 99) { + continue; + } + + songPositions.push({ + position: index + 1, + sceneNumber, }); } - for (const file of files) { - if (!file.name.startsWith('patterns') || file.type !== 'file' || !file.data) { - continue; - } + return songPositions; +} - const matches = file.name.match(/patterns\/([abcd])(\d+)/); - if (!matches) { - continue; - } +export function collectProjectStructure(files: TarFile[], sku: string) { + const groupPatterns = collectGroupPatterns(files, sku); + const groupPatternLookup = new Map( + groupPatterns.map((pattern) => [getPatternKey(pattern.group, pattern.patternNumber), pattern]), + ); + const scenesFile = files.find((file) => file.name === 'scenes' && file.type === 'file'); + let sceneRecords: SceneRecord[]; - const [, group, scene] = matches; - const sceneIndex = Number(scene); - if (!scenesIntermediateData[sceneIndex]) { - scenesIntermediateData[sceneIndex] = { - name: String(scene).padStart(2, '0'), - groups: {}, - }; - } - - // build intermediate scenes data - // will be used to resolve patterns later using sceneMetadata - scenesIntermediateData[sceneIndex] = { - ...scenesIntermediateData[sceneIndex], - groups: { - ...scenesIntermediateData[sceneIndex].groups, - [group]: parsePatternEvents(file.data, sku === SKU_EP40 ? 6 : 4), // EP-40 has 6-byte header + if (scenesFile?.data) { + sceneRecords = collectSceneRecords(scenesFile.data); + } else { + const patternNumbers = [...new Set(groupPatterns.map((pattern) => pattern.patternNumber))] + .toSorted((a, b) => a - b); + sceneRecords = patternNumbers.map((patternNumber) => ({ + number: patternNumber, + patternNumbers: { + a: groupPatternLookup.has(getPatternKey('a', patternNumber)) ? patternNumber : 0, + b: groupPatternLookup.has(getPatternKey('b', patternNumber)) ? patternNumber : 0, + c: groupPatternLookup.has(getPatternKey('c', patternNumber)) ? patternNumber : 0, + d: groupPatternLookup.has(getPatternKey('d', patternNumber)) ? patternNumber : 0, }, - }; + timeSignature: defaultScenesSettings.timeSignature, + })); } - Object.entries(scenesIntermediateData).forEach(([sceneIndex, sceneData]) => { - if (!scenePatternUsage[sceneIndex]) { - return; - } + const scenes = sceneRecords.map((sceneRecord) => buildScene(sceneRecord, groupPatternLookup)); + const scenesSettings = { + timeSignature: scenes[0]?.timeSignature ?? defaultScenesSettings.timeSignature, + }; - const patterns = getPatternsForScene(scenesIntermediateData, scenePatternUsage[sceneIndex]); - if (patterns.length === 0) { - return; - } + return { + groupPatterns, + scenes, + songPositions: scenesFile?.data ? collectSongPositions(scenesFile.data) : [], + scenesSettings, + }; +} - scenes[sceneIndex] = { - name: sceneData.name, - patterns, - patternEvents: getPatternEventsForScene( - scenesIntermediateData, - scenePatternUsage[sceneIndex], - ), - }; - }); - - return Object.values(scenes).sort((a, b) => a.name.localeCompare(b.name)); +export function collectScenesAndPatterns(files: TarFile[], sku: string) { + return collectProjectStructure(files, sku).scenes; } export function collectSettings(files: TarFile[]): ProjectSettings {