Refactor scene and pattern collection logic to use group patterns instead of intermediate scenes data
This commit is contained in:
@@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// build group/pattern lookup table
|
function collectSongPositions(data: Uint8Array): SongPosition[] {
|
||||||
const scenesFile = files.find((f) => f.name === 'scenes' && f.type === 'file');
|
const songCountOffset = 612;
|
||||||
|
const firstSongPositionOffset = 613;
|
||||||
|
if (data.length <= songCountOffset) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const availablePositions = Math.max(0, data.length - firstSongPositionOffset);
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupPatterns,
|
||||||
|
scenes,
|
||||||
|
songPositions: scenesFile?.data ? collectSongPositions(scenesFile.data) : [],
|
||||||
|
scenesSettings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(scenesIntermediateData).forEach(([sceneIndex, sceneData]) => {
|
export function collectScenesAndPatterns(files: TarFile[], sku: string) {
|
||||||
if (!scenePatternUsage[sceneIndex]) {
|
return collectProjectStructure(files, sku).scenes;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 collectSettings(files: TarFile[]): ProjectSettings {
|
export function collectSettings(files: TarFile[]): ProjectSettings {
|
||||||
|
|||||||
Reference in New Issue
Block a user