From 3e4335673367dd0b650d376094cba96e25cfcac8 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 13 Jul 2026 04:41:18 +0200 Subject: [PATCH] Implement structured parsing and typing for pattern events (Note, Fader, MIDI, Unknown) --- src/lib/parsers.ts | 140 ++++++++++++++++++++++++++++++++++++++++----- src/types/types.ts | 58 +++++++++++++++++++ 2 files changed, 183 insertions(+), 15 deletions(-) diff --git a/src/lib/parsers.ts b/src/lib/parsers.ts index acbc752..4441d2a 100644 --- a/src/lib/parsers.ts +++ b/src/lib/parsers.ts @@ -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 }>; + groups: Record; } >; +export type ParsedPatternData = { + bars: number; + recordCount: number; + events: PatternEvent[]; + notes: Record; +}; + 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 = {}; 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> = {}; + + 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 = {}; @@ -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], + ), }; }); diff --git a/src/types/types.ts b/src/types/types.ts index a3e2db4..bd80ce4 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -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>; }; export type Note = {