Implement structured parsing and typing for pattern events (Note, Fader, MIDI, Unknown)

This commit is contained in:
2026-07-13 04:41:18 +02:00
parent fac7e80bb7
commit 3e43356733
2 changed files with 183 additions and 15 deletions

View File

@@ -5,8 +5,11 @@ import {
Pad,
PadCode,
Pattern,
PatternEvent,
PatternEventKind,
ProjectSettings,
Scene,
SceneGroupPatternData,
ScenesSettings,
Sound,
} from '../types/types';
@@ -28,10 +31,17 @@ type IntermediateScenes = Record<
number,
{
name: string;
groups: Record<string, { bars: number; notes: Record<number, Note[]> }>;
groups: Record<string, ParsedPatternData>;
}
>;
export type ParsedPatternData = {
bars: number;
recordCount: number;
events: PatternEvent[];
notes: Record<number, Note[]>;
};
const defaultProjectSettings = {
bpm: 120,
scale: 0,
@@ -195,32 +205,104 @@ export function collectPads(files: TarFile[], sounds: Sound[]) {
return result;
}
function parsePatterns(data: Uint8Array, startOffset: number = 4) {
const chunks = chunkArray(data, 8, startOffset);
export function parsePatternEvents(
data: Uint8Array,
startOffset: number = 4,
): ParsedPatternData {
const recordCount = data.length >= 4 ? data[2] | (data[3] << 8) : 0;
const availableRecordCount = Math.max(0, Math.floor((data.length - startOffset) / 8));
const chunks = chunkArray(data, 8, startOffset).slice(
0,
Math.min(recordCount, availableRecordCount),
);
const events: PatternEvent[] = [];
const notes: Record<number, Note[]> = {};
chunks.forEach((chunk) => {
// I discovered there could be some weird chunks with pad numbers not multiple of 8
// since I don't know what they are, just skip them
if (chunk[2] % 8 !== 0) {
const kind = chunk[2] & 0x07;
const source = chunk[2] >> 3;
const position = chunk[0] | (chunk[1] << 8);
const flags = chunk[7];
const rawData = chunk.slice();
if (kind === PatternEventKind.Note) {
const duration = chunk[5] | (chunk[6] << 8);
const noteEvent: PatternEvent = {
type: 'note',
kind: PatternEventKind.Note,
position,
source,
flags,
rawData,
pad: source,
note: chunk[3],
velocity: chunk[4],
duration,
};
events.push(noteEvent);
if (!notes[source]) {
notes[source] = [];
}
notes[source].push({
note: noteEvent.note,
position: noteEvent.position,
duration: noteEvent.duration,
velocity: noteEvent.velocity,
});
return;
}
const pad = chunk[2] / 8;
if (!notes[pad]) {
notes[pad] = [];
if (kind === PatternEventKind.Fader) {
const rawValue = chunk[5] | (chunk[6] << 8);
events.push({
type: 'fader',
kind: PatternEventKind.Fader,
position,
source,
flags,
rawData,
parameter: chunk[3],
reserved: chunk[4],
rawValue,
value: rawValue / 32767,
});
return;
}
notes[pad].push({
note: chunk[3],
position: (chunk[1] << 8) + chunk[0],
duration: (chunk[6] << 8) + chunk[5],
velocity: chunk[4],
const eventData = chunk.slice(3, 7);
if (kind === PatternEventKind.Midi) {
events.push({
type: 'midi',
kind: PatternEventKind.Midi,
position,
source,
flags,
rawData,
data: eventData,
});
return;
}
events.push({
type: 'unknown',
kind,
position,
source,
flags,
rawData,
data: eventData,
});
});
return {
bars: data[1],
recordCount,
events,
notes,
};
}
@@ -248,6 +330,30 @@ function getPatternsForScene(scenesIntermediate: IntermediateScenes, scenePatter
return patterns;
}
function getPatternEventsForScene(
scenesIntermediate: IntermediateScenes,
scenePatternUsage: SceneMeta,
) {
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];
if (groupData) {
patternEvents[group] = {
group,
patternNumber,
bars: groupData.bars,
recordCount: groupData.recordCount,
events: groupData.events,
};
}
}
return patternEvents;
}
export function collectScenesAndPatterns(files: TarFile[], sku: string) {
const scenesIntermediateData: IntermediateScenes = {};
const scenes: Record<string, Scene> = {};
@@ -306,7 +412,7 @@ export function collectScenesAndPatterns(files: TarFile[], sku: string) {
...scenesIntermediateData[sceneIndex],
groups: {
...scenesIntermediateData[sceneIndex].groups,
[group]: parsePatterns(file.data, sku === SKU_EP40 ? 6 : 4), // EP-40 has 6-byte header
[group]: parsePatternEvents(file.data, sku === SKU_EP40 ? 6 : 4), // EP-40 has 6-byte header
},
};
}
@@ -324,6 +430,10 @@ export function collectScenesAndPatterns(files: TarFile[], sku: string) {
scenes[sceneIndex] = {
name: sceneData.name,
patterns,
patternEvents: getPatternEventsForScene(
scenesIntermediateData,
scenePatternUsage[sceneIndex],
),
};
});

View File

@@ -74,9 +74,67 @@ export type Pattern = {
bars: number;
};
export enum PatternEventKind {
Note = 0,
Fader = 1,
Midi = 2,
}
export type PatternEventBase = {
position: number;
source: number;
flags: number;
rawData: Uint8Array;
};
export type NotePatternEvent = PatternEventBase & {
type: 'note';
kind: PatternEventKind.Note;
pad: number;
note: number;
velocity: number;
duration: number;
};
export type FaderAutomationEvent = PatternEventBase & {
type: 'fader';
kind: PatternEventKind.Fader;
parameter: number;
reserved: number;
rawValue: number;
value: number;
};
export type MidiPatternEvent = PatternEventBase & {
type: 'midi';
kind: PatternEventKind.Midi;
data: Uint8Array;
};
export type UnknownPatternEvent = PatternEventBase & {
type: 'unknown';
kind: number;
data: Uint8Array;
};
export type PatternEvent =
| NotePatternEvent
| FaderAutomationEvent
| MidiPatternEvent
| UnknownPatternEvent;
export type SceneGroupPatternData = {
group: Group;
patternNumber: number;
bars: number;
recordCount: number;
events: PatternEvent[];
};
export type Scene = {
name: string;
patterns: Pattern[];
patternEvents: Partial<Record<Group, SceneGroupPatternData>>;
};
export type Note = {