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 {
// build default scene metadata (in case "scenes" file is missing) number: sceneRecord.number,
for (let i = 1; i <= 99; i++) { name: String(sceneRecord.number).padStart(2, '0'),
scenePatternUsage[i] = { patternNumbers: sceneRecord.patternNumbers,
a: i, timeSignature: sceneRecord.timeSignature,
b: i, patterns: getPatternsForScene(groupPatterns, sceneRecord.patternNumbers),
c: i, patternEvents: getPatternEventsForScene(groupPatterns, sceneRecord.patternNumbers),
d: i,
}; };
}
function collectSongPositions(data: Uint8Array): SongPosition[] {
const songCountOffset = 612;
const firstSongPositionOffset = 613;
if (data.length <= songCountOffset) {
return [];
} }
// 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 (songCount === 1 && data[firstSongPositionOffset] === 1) {
return [];
}
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,
});
}
return songPositions;
}
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[];
if (scenesFile?.data) { if (scenesFile?.data) {
const chunks = chunkArray(scenesFile.data, 6, 7); sceneRecords = collectSceneRecords(scenesFile.data);
chunks.forEach((chunk, i) => { } else {
if (i > 98) { const patternNumbers = [...new Set(groupPatterns.map((pattern) => pattern.patternNumber))]
return; .toSorted((a, b) => a - b);
} sceneRecords = patternNumbers.map((patternNumber) => ({
number: patternNumber,
scenePatternUsage[i + 1] = { patternNumbers: {
a: chunk[0], a: groupPatternLookup.has(getPatternKey('a', patternNumber)) ? patternNumber : 0,
b: chunk[1], b: groupPatternLookup.has(getPatternKey('b', patternNumber)) ? patternNumber : 0,
c: chunk[2], c: groupPatternLookup.has(getPatternKey('c', patternNumber)) ? patternNumber : 0,
d: chunk[3], d: groupPatternLookup.has(getPatternKey('d', patternNumber)) ? patternNumber : 0,
};
});
}
for (const file of files) {
if (!file.name.startsWith('patterns') || file.type !== 'file' || !file.data) {
continue;
}
const matches = file.name.match(/patterns\/([abcd])(\d+)/);
if (!matches) {
continue;
}
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
}, },
timeSignature: defaultScenesSettings.timeSignature,
}));
}
const scenes = sceneRecords.map((sceneRecord) => buildScene(sceneRecord, groupPatternLookup));
const scenesSettings = {
timeSignature: scenes[0]?.timeSignature ?? defaultScenesSettings.timeSignature,
}; };
}
Object.entries(scenesIntermediateData).forEach(([sceneIndex, sceneData]) => { return {
if (!scenePatternUsage[sceneIndex]) { groupPatterns,
return; scenes,
} songPositions: scenesFile?.data ? collectSongPositions(scenesFile.data) : [],
scenesSettings,
const patterns = getPatternsForScene(scenesIntermediateData, scenePatternUsage[sceneIndex]);
if (patterns.length === 0) {
return;
}
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 { export function collectSettings(files: TarFile[]): ProjectSettings {