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[] = [];
|
||||
|
||||
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<string, GroupPattern>,
|
||||
patternNumbers: Record<Group, number>,
|
||||
) {
|
||||
const patternEvents: Partial<Record<Group, SceneGroupPatternData>> = {};
|
||||
|
||||
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<string, Scene> = {};
|
||||
const scenePatternUsage: Record<string, SceneMeta> = {};
|
||||
|
||||
// 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 buildScene(
|
||||
sceneRecord: SceneRecord,
|
||||
groupPatterns: Map<string, GroupPattern>,
|
||||
): 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),
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
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) {
|
||||
const chunks = chunkArray(scenesFile.data, 6, 7);
|
||||
chunks.forEach((chunk, i) => {
|
||||
if (i > 98) {
|
||||
return;
|
||||
}
|
||||
|
||||
scenePatternUsage[i + 1] = {
|
||||
a: chunk[0],
|
||||
b: chunk[1],
|
||||
c: chunk[2],
|
||||
d: chunk[3],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
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,
|
||||
}));
|
||||
}
|
||||
|
||||
const scenes = sceneRecords.map((sceneRecord) => buildScene(sceneRecord, groupPatternLookup));
|
||||
const scenesSettings = {
|
||||
timeSignature: scenes[0]?.timeSignature ?? defaultScenesSettings.timeSignature,
|
||||
};
|
||||
}
|
||||
|
||||
Object.entries(scenesIntermediateData).forEach(([sceneIndex, sceneData]) => {
|
||||
if (!scenePatternUsage[sceneIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const patterns = getPatternsForScene(scenesIntermediateData, scenePatternUsage[sceneIndex]);
|
||||
if (patterns.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
scenes[sceneIndex] = {
|
||||
name: sceneData.name,
|
||||
patterns,
|
||||
patternEvents: getPatternEventsForScene(
|
||||
scenesIntermediateData,
|
||||
scenePatternUsage[sceneIndex],
|
||||
),
|
||||
return {
|
||||
groupPatterns,
|
||||
scenes,
|
||||
songPositions: scenesFile?.data ? collectSongPositions(scenesFile.data) : [],
|
||||
scenesSettings,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user