Refactor scene and pattern collection logic to use group patterns instead of intermediate scenes data

This commit is contained in:
2026-07-14 09:28:59 +02:00
parent f46703ce4f
commit da0398e738

View File

@@ -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<Group, number> = {
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<string, GroupPattern>();
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<string, GroupPattern>,
patternNumbers: Record<Group, number>,
) {
const patterns: Pattern[] = []; const patterns: Pattern[] = [];
for (const group of ['a', 'b', 'c', 'd'] as const) { for (const group of projectGroups) {
const scene = scenePatternUsage[group]; const groupPattern = groupPatterns.get(getPatternKey(group, patternNumbers[group]));
const lookupSceneData = scenesIntermediate[scene];
if (lookupSceneData?.groups[group]) { if (groupPattern) {
const groupData = lookupSceneData.groups[group]; const sources = new Set(Object.keys(groupPattern.notes).map(Number));
const sources = new Set(Object.keys(groupData.notes).map(Number)); groupPattern.events.forEach((event) => {
groupData.events.forEach((event) => {
if (event.type === 'midi' && event.source >= 0 && event.source < 12) { if (event.type === 'midi' && event.source >= 0 && event.source < 12) {
sources.add(event.source); sources.add(event.source);
} }
@@ -321,8 +420,8 @@ function getPatternsForScene(scenesIntermediate: IntermediateScenes, scenePatter
patterns.push({ patterns.push({
pad: `${group}${padNum}` as PadCode, pad: `${group}${padNum}` as PadCode,
group, group,
notes: groupData.notes[padNum] || [], notes: groupPattern.notes[padNum] || [],
bars: groupData.bars, bars: groupPattern.bars,
}); });
}); });
} }
@@ -332,22 +431,22 @@ function getPatternsForScene(scenesIntermediate: IntermediateScenes, scenePatter
} }
function getPatternEventsForScene( function getPatternEventsForScene(
scenesIntermediate: IntermediateScenes, groupPatterns: Map<string, GroupPattern>,
scenePatternUsage: SceneMeta, patternNumbers: Record<Group, number>,
) { ) {
const patternEvents: Partial<Record<Group, SceneGroupPatternData>> = {}; const patternEvents: Partial<Record<Group, SceneGroupPatternData>> = {};
for (const group of ['a', 'b', 'c', 'd'] as const) { for (const group of projectGroups) {
const patternNumber = scenePatternUsage[group]; const patternNumber = patternNumbers[group];
const groupData = scenesIntermediate[patternNumber]?.groups[group]; const groupPattern = groupPatterns.get(getPatternKey(group, patternNumber));
if (groupData) { if (groupPattern) {
patternEvents[group] = { patternEvents[group] = {
group, group,
patternNumber, patternNumber,
bars: groupData.bars, bars: groupPattern.bars,
recordCount: groupData.recordCount, recordCount: groupPattern.recordCount,
events: groupData.events, events: groupPattern.events,
}; };
} }
} }
@@ -355,90 +454,90 @@ function getPatternEventsForScene(
return patternEvents; return patternEvents;
} }
export function collectScenesAndPatterns(files: TarFile[], sku: string) { function buildScene(
const scenesIntermediateData: IntermediateScenes = {}; sceneRecord: SceneRecord,
const scenes: Record<string, Scene> = {}; groupPatterns: Map<string, GroupPattern>,
const scenePatternUsage: Record<string, SceneMeta> = {}; ): 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) function collectSongPositions(data: Uint8Array): SongPosition[] {
for (let i = 1; i <= 99; i++) { const songCountOffset = 612;
scenePatternUsage[i] = { const firstSongPositionOffset = 613;
a: i, if (data.length <= songCountOffset) {
b: i, return [];
c: i,
d: i,
};
} }
// build group/pattern lookup table const availablePositions = Math.max(0, data.length - firstSongPositionOffset);
const scenesFile = files.find((f) => f.name === 'scenes' && f.type === 'file'); const songCount = Math.min(data[songCountOffset], 99, availablePositions);
if (scenesFile?.data) { if (songCount === 1 && data[firstSongPositionOffset] === 1) {
const chunks = chunkArray(scenesFile.data, 6, 7); return [];
chunks.forEach((chunk, i) => { }
if (i > 98) {
return;
}
scenePatternUsage[i + 1] = { const songPositions: SongPosition[] = [];
a: chunk[0],
b: chunk[1], for (let index = 0; index < songCount; index++) {
c: chunk[2], const sceneNumber = data[firstSongPositionOffset + index];
d: chunk[3], if (sceneNumber < 1 || sceneNumber > 99) {
}; continue;
}
songPositions.push({
position: index + 1,
sceneNumber,
}); });
} }
for (const file of files) { return songPositions;
if (!file.name.startsWith('patterns') || file.type !== 'file' || !file.data) { }
continue;
}
const matches = file.name.match(/patterns\/([abcd])(\d+)/); export function collectProjectStructure(files: TarFile[], sku: string) {
if (!matches) { const groupPatterns = collectGroupPatterns(files, sku);
continue; 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; if (scenesFile?.data) {
const sceneIndex = Number(scene); sceneRecords = collectSceneRecords(scenesFile.data);
if (!scenesIntermediateData[sceneIndex]) { } else {
scenesIntermediateData[sceneIndex] = { const patternNumbers = [...new Set(groupPatterns.map((pattern) => pattern.patternNumber))]
name: String(scene).padStart(2, '0'), .toSorted((a, b) => a - b);
groups: {}, sceneRecords = patternNumbers.map((patternNumber) => ({
}; number: patternNumber,
} patternNumbers: {
a: groupPatternLookup.has(getPatternKey('a', patternNumber)) ? patternNumber : 0,
// build intermediate scenes data b: groupPatternLookup.has(getPatternKey('b', patternNumber)) ? patternNumber : 0,
// will be used to resolve patterns later using sceneMetadata c: groupPatternLookup.has(getPatternKey('c', patternNumber)) ? patternNumber : 0,
scenesIntermediateData[sceneIndex] = { d: groupPatternLookup.has(getPatternKey('d', patternNumber)) ? patternNumber : 0,
...scenesIntermediateData[sceneIndex],
groups: {
...scenesIntermediateData[sceneIndex].groups,
[group]: parsePatternEvents(file.data, sku === SKU_EP40 ? 6 : 4), // EP-40 has 6-byte header
}, },
}; timeSignature: defaultScenesSettings.timeSignature,
}));
} }
Object.entries(scenesIntermediateData).forEach(([sceneIndex, sceneData]) => { const scenes = sceneRecords.map((sceneRecord) => buildScene(sceneRecord, groupPatternLookup));
if (!scenePatternUsage[sceneIndex]) { const scenesSettings = {
return; timeSignature: scenes[0]?.timeSignature ?? defaultScenesSettings.timeSignature,
} };
const patterns = getPatternsForScene(scenesIntermediateData, scenePatternUsage[sceneIndex]); return {
if (patterns.length === 0) { groupPatterns,
return; scenes,
} songPositions: scenesFile?.data ? collectSongPositions(scenesFile.data) : [],
scenesSettings,
};
}
scenes[sceneIndex] = { export function collectScenesAndPatterns(files: TarFile[], sku: string) {
name: sceneData.name, return collectProjectStructure(files, sku).scenes;
patterns,
patternEvents: getPatternEventsForScene(
scenesIntermediateData,
scenePatternUsage[sceneIndex],
),
};
});
return Object.values(scenes).sort((a, b) => a.name.localeCompare(b.name));
} }
export function collectSettings(files: TarFile[]): ProjectSettings { export function collectSettings(files: TarFile[]): ProjectSettings {