Skip to content

chore(slicer): toten SampleSlicer entfernen - #30

Draft
GeorgDub wants to merge 109 commits into
mainfrom
claude/electribe-2-sampler-research-w60qrk
Draft

GeorgDub wants to merge 109 commits into
mainfrom
claude/electribe-2-sampler-research-w60qrk

Conversation

@GeorgDub

@GeorgDub GeorgDub commented Jul 27, 2026 •

Copy link
Copy Markdown
Owner

Löscht client/src/components/SampleSlicer/ (SampleSlicer.tsx, 605 LOC, plus index.ts).

Die Komponente war exportiert, wurde aber nirgends gerendert — kein Import ausserhalb des eigenen Verzeichnisses, weder unter client/ noch tests/ noch electron/. Der aktive Slicer ist SampleEditor/SampleSliceEditor.tsx, gemountet aus DrumMachine.tsx. Aufgefallen bei der Slicer-Arbeit in #29.

Nach jedem Merge von main erneut gegengeprüft, zuletzt gegen v3.316.0 — gilt unverändert. Die verbleibenden Treffer sind keine Referenzen: die Fundstellen in DrumMachine.tsx sind Log-Präfixe (console.warn("[SampleSlicer] …")), und tests/electron/sample-slicer.test.ts importiert den Store nicht, sondern baut sich intern eine Minimal-Kopie.

Drei Fähigkeiten, die dabei sonst verschwunden wären

Der gelöschte Code konnte drei Dinge, die der aktive Editor nicht kann. Sie stehen in agents/INDEX.js unter next[], mit Fundstellen in der Historie:

  • BPM-Raster über der Wellenform (drawBpmGrid) — bei Loops die schnellste Orientierung, wo ein Schnitt hingehört.
  • Snap-to-Beat beim Setzen der Marker (warpSnap, framesPerBeat = sampleRate * 60 / bpm). Das ergänzt den vorhandenen Zero-Crossing-Snap, es ersetzt ihn nicht: der macht den Schnitt knackfrei, dieser macht ihn rhythmisch richtig. Beide zusammen wären das eigentliche Ziel.
  • Teilen in N gleiche Abschnitte (equalDivisions, Default 16) statt Onset-Erkennung. Bei sauber geschnittenen Loops zuverlässiger als jede Transientenanalyse — dort ist das Raster bekannt und muss nicht erraten werden.

Der Code bleibt über die Historie erreichbar (73aee66^, Zeilen 34–79 / 288–300 / 366–372).

Offen: ein verwaister Store

client/src/store/useSampleSlicerStore.ts (aus TASK-249) hat als einzigen Importeur die hier gelöschte SampleSlicer.tsx — er wird durch diese Löschung selbst zu totem Code. Er ist bewusst nicht mit entfernt: das Löschen wurde in der Session von der Berechtigungsprüfung blockiert, und es ist ohnehin eine Entscheidung, die dem Repo-Eigner gehört. Ein Nachfolge-Commit oder ein Handgriff beim Merge erledigt es.

Merges mit main

main ist dreimal an diesem PR vorbeigezogen, jedes Mal stand er danach auf dirty:

  1. auf v3.305.0 (fünf PRs) — Konflikt nur in der Versionszeile von package.json und agents/INDEX.js.
  2. auf v3.311.0 (fix(korg): Step-Record-Layout korrigiert — Note @1, Velocity @2 (v3.306.0) #36/feat(korg): E2-Akkord editieren im StepInspector (v3.310.0) #37/feat(audio): E2-Chord-Noten klingen in der Engine (v3.311.0) #38) — dazu ein add/add am Kopf des workLog, weil beide Seiten einen neuen ersten Eintrag hatten. Aufgelöst durch Aneinanderhängen: mains Einträge oben, meiner darunter.
  3. auf v3.316.0 (fix(korg): ESX→E2S Transfer-Treue — egtime + FX-/Export-Zuweisungsdateien (v3.312–v3.314) #39) — wieder nur die Versionszeile.

Aktuelle Version 3.317.0, jeweils die nächste freie über main; die zwischenzeitlich gewählte 3.312.0 hatte main sich selbst genommen.

Tests

pnpm check clean · Suite 521 Dateien / 11390 passed / 101 skipped (nach dem dritten Merge).

Beim ursprünglichen Stand waren es zwei Tests weniger als ohne die Löschung, weil theme-class-purity einen Test pro Komponentendatei erzeugt und es zwei Dateien weniger gibt. Nachgerechnet, kein Verlust an Abdeckung.

Zum roten non-blocking Job

Unverändert rot, aber nicht aus dem bisher angenommenen Grund. Die Zuordnung „TASK-262-Env-Gate" war falsch; b125774 korrigiert sie im INDEX. Die tatsächliche Ursache ist kein Testfehler, sondern ein Checkout-Fehler:

remote: Repository not found.
fatal: repository 'https://github.com/GeorgDub/omnitribe/' not found     (3× Retry, dann Aufgabe)

Der Schritt „Checkout omnitribe (sim WS server)" meldet wegen continue-on-error trotzdem success, deshalb fällt es erst zehn Minuten später auf: tools/sim/sim_ws_server.py läuft ins Leere, die Panels rendern nicht, 43 Tests scheitern an fehlenden Test-IDs (Wurzeln: getByTestId('arp-output-synth'), getByTestId('sim-status-connected')).

Gegenproben: derselbe Job ist auf main ebenfalls rot (Job 89943137056 an 73aee66), und die Signatur ist nach allen drei Merges unverändert — dieselben Fehlschläge, dieselben elf Spec-Dateien, dieselben zwei Wurzeln. Also unabhängig von dieser Löschung. Grün würde ihn ein Token mit Lesezugriff auf das private GeorgDub/omnitribe machen; GITHUB_TOKEN kann per Definition kein Fremd-Repo lesen. Das braucht ein Repo-Secret und gehört nicht in diesen PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys


Generated by Claude Code

claude added 30 commits July 21, 2026 15:56
Adds a device-native Korg SysEx module (manufacturer 0x42) so the host can
talk to a real Electribe 2 / E2S — stock or hacktribe-patched — over MIDI,
distinct from the OmniTribe OTP protocol (0x7D).

- e2Sysex.ts: bit-exact 7-in-8 codec (ported from hacktribe e2_syx_codec.py),
  frame builders (search, current-pattern/numbered-pattern/global dump req+data)
  and an inbound parser (identity, pattern, global, ack/nak). Also .e2pat file
  <-> raw-body helpers. Isomorphic, side-effect-free.
- korg-e2-sysex.test.ts: 20 tests (codec round-trips for all lengths 0..64 and
  a 16 KB body, pattern-number encoding, frame layout, parse classification).
- constants.ts: document the e2sSample.all offset-table discrepancy vs Oe2sSLE
  and hacktribe (0x0010 / 1020 slots) — flagged for verification against a real
  device file; hacktribe requires sample slots >= 501.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Wires the pure e2Sysex.ts frames into a device session so the host can talk
to a real E2/E2S (stock or hacktribe) live — the risk-free host-side Step 1.

- E2SysexBridge.ts: transport-injected session (mirrors OmniTribeBridge's
  WsTransport pattern, unit-testable without Web MIDI):
  - identify() — search request + identity handshake.
  - pullCurrentPattern / pullPattern(n) / pullGlobal — request/response with
    correlation (pattern number match), timeout, NAK handling; returns the
    7-in-8-decoded raw body.
  - pushCurrentPattern / pushPattern(n) / pushGlobal — ACK-wait + a
    sequencer-stop guard (E2SequencerRunningError) so writes are blocked while
    the device transport runs.
  - chunkBytes() + maxChunkBytes for small-buffer transports (default 0 =
    whole frame, correct for Web MIDI); connectWebMidi() adapter.
- e2Sysex.ts: pure pattern-body readers (readPatternName, readPartOscRefs,
  summarizePatternBody) using the verified body offsets, so a pulled pattern
  yields name + 16 part osc/sample refs even before the full step decoder.
- korg-e2-sysex-bridge.test.ts: 18 tests (identify/pull/push flows via a mock
  device, correlation, timeout, NAK, guard, chunking, lifecycle, body readers).

tsc clean; 38 tests green (bridge 18 + codec 20). Full semantic pattern import
still needs the per-step field decoder (tracked in korg_e2_native_sysex.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Confirmed against our own ESX-Editor SoT (e2s_builder.py/constants.py), which
round-trips bit-exact vs 14 real device dumps (TASK-029): the .all offset table
starts at 0x0058 with 1002 LE32 entries, ending exactly at 0x1000. The old
0x07E0/250 was an artefact — the one 2026-05-17 test file placed its 11 factory
samples at table index 482, and 0x0058 + 482*4 == 0x07E0, so the first non-zero
entry landed there and was mistaken for the table start.

- constants.ts: E2S_MAX_SLOTS 250 -> 1002, E2S_ALL_OFFSET_TABLE_START 0x07E0 ->
  0x0058 (E2S_MAX_SLOTS is only ever used for the .all sample table, never a
  pattern cap). This unblocks hacktribe, which requires user-sample slots >= 501.
- korg-e2s-all-offset-table.test.ts: regression test pinning the layout and
  round-tripping samples in slots 501 and 999 (impossible under the old cap).

tsc clean; 180 Korg tests green (reader/builder are symmetric so round-trips
held; the fix corrects the absolute table position vs real hardware files).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Adds the reactive layer + UI so the user can connect to a real E2/E2S and pull
patterns from the MIDI settings — the risk-free host-side feature on top of the
E2SysexBridge session.

- useE2sDeviceStore.ts: custom observer store orchestrating the bridge singleton.
  Actions connect/disconnect/pullCurrent/pull expose {status, identity, error,
  currentPattern, patterns, busy}. connect() takes an optional MIDIAccess (default
  navigator Web-MIDI) so the whole connect->identity->pull flow is unit-testable
  in Node via an injected fake access.
- E2sDevicePanel.tsx: Connect/Disconnect, live status + identity (model/version/
  channel), pull Edit-Buffer + numbered slot (0-249), and a compact summary per
  pulled pattern (name + non-empty part sample/osc refs). Semantic theme classes
  only, data-testids for smoke coverage.
- MidiSettings.tsx: render the panel next to the existing E2S Pattern Sync block.
- e2s-device-store.test.ts: 7 tests driving connect/identity/pull/disconnect and
  the error paths through a fake MIDIAccess + fake device.

tsc clean; 47 tests green (store 7 + bridge 18 + codec 20 + offset 2). Full
semantic step-import still pending the per-step field decoder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Formatting-only (arrow-param parens, line wrapping) on files committed before
the format pass. No behaviour change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Extends the native Korg layer with hacktribe's CPU-RAM read/write functions and
a safe, blob-level IFX/Groove preset manager (read/backup/copy/restore).

- e2Sysex.ts: RAM primitives ported verbatim from e2sysex.py — u32le (unsigned-
  safe for >=0x80000000 addresses), buildReadCpuRamRequest (0x52),
  buildSetWriteAddrRequest (0x53), buildWriteCpuRamData (0x54); parseSysex gains
  cpuRamData (reply data at [9:-1]). IFX (0xC00A80F0, stride 0x20C, 100, counter
  0xC003EFDC) + Groove (0xC0143B00, stride 0x140, 128, counter 0xC007BB88)
  addressing, and isWritablePresetRam() range guard.
- E2SysexBridge.ts: readCpuRam / writeCpuRam (chunked at 0x100 like set_ifx,
  guarded to the IFX/Groove ranges + sequencer-stop) and readIfx/GroovePreset,
  writeIfx/GrooveTemplate, readIfx/GrooveCount.
- useE2sPresetStore.ts + E2sPresetPanel.tsx: reuse the device bridge; capture a
  slot to a backup, copy slot->slot, restore a backup into a slot. Deliberately
  blob-only — only device-sourced bytes are ever written back, so there is no
  "wrong field layout" risk (the IFX/groove internal param map lives in
  hacktribe-editor and is a separate reverse). Wired into MIDI settings.
- Tests: korg-e2-cpuram (13) + e2s-preset-store (6) drive the RAM protocol and
  the full capture/copy/restore round-trip through a fake hacktribe RAM model.

tsc clean; 83 E2-SysEx tests green (codec 20 + bridge 18 + cpuram 13 + device 7
+ preset 6 + offset 2 + pattern-out 17...). Per-parameter field editing deferred
to the param-map reverse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Completes the pull->import path: a verified structured decoder for pulled
pattern bodies (previously only name + osc refs were read).

Reverse method: cross-validated two independent sources — the e2sExport.ts
writer (aligned to real KORG files) and a byte analysis of the stock init
allpattern (default template) plus our committed testbank (which carries REAL
triggered steps with varying velocity). Both agree exactly.

Verified layout:
- body: name @0x10, BPM x10 @0x22 (u16 LE), step-length code @0x25 (0/1/3 =
  16/32/64), parts @0x800 (16 x 0x330).
- part: sampleRef @+0x08 (u16 LE), volume @+0x15, pan @+0x22, steps @+0x30
  (64 x 0x0C, fills the part).
- step (0x0C): [0]=trigger [1]=note [2]=velocity [3]=gate [4]=gateLen. Byte 2
  VARIES in real patterns (0x50/0x5F/0x64/0x6E) which is what pins it as velocity
  (the older builder comment had note/velocity swapped).

- e2Sysex.ts: decodePatternBody() -> {name, bpm, stepLength, parts[{sampleRef,
  volume, pan, steps[{active,note,velocity,gate,gateLen}], activeCount}]} +
  decodeStep(); summarizePatternBody() now also returns bpm/stepLength/activeSteps.
- E2sDevicePanel: shows BPM / step-length / active-trigger count per pulled
  pattern.
- korg-e2-pattern-decode.test.ts: 6 tests — round-trip vs the writer AND decoding
  the real committed testbank (asserts real active steps, note 0x48, gate on).

tsc clean; 70 E2-SysEx tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Closes the pull->import loop: a pattern pulled from a real E2/E2S can now be
turned into an editable SynthStudio pattern.

- e2PatternToSynthstudio.ts (new, pure): maps a decoded E2 pattern to PatternData.
  stepLength->stepCount, 16 parts kept 1:1, per-step active/velocity, pitch =
  note - E2 default note (0x48) so the common case is pitch 0 and real melodies
  stay relative; part volume 0..127->0..1, pan 0..127 (64=center)->-1..+1. Emits
  valid PartData (id/muted/soloed/fx via DEFAULT_CHANNEL_FX); the store
  regenerates ids on insert.
- useE2sDeviceStore: retains the full decoded pattern (currentDecoded + decoded
  map) alongside the summary during pull, so import has the step data.
- E2sDevicePanel: an "Import" button per pulled pattern -> addPatternData().
- Tests: korg-e2-pattern-import (5) drives the writer->decoder->mapper chain and
  the field mappings; device-store test asserts currentDecoded is populated.

tsc clean; 75 E2-SysEx tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Completes the bidirectional loop — a SynthStudio pattern can now be written back
to a real E2/E2S over native Korg SysEx.

- synthstudioToE2Pattern.ts (new, pure): inverse of the import mapper.
  PatternData -> E2PatternInput -> buildE2PatternBody (0x4000 body). stepCount->
  stepLength, volume 0..1->0..127, pan -1..+1->0..127 (64=center), step
  active/velocity 1:1, note = pitch + E2 default note (0x48), and sampleId parsed
  from the part name ("#501") so an import->push round-trip keeps the sample ref.
- useE2sDeviceStore: pushCurrent(body) / push(slot, body) wrap the bridge's
  ACK-waited writes with busy/error state.
- E2sDevicePanel: a push section — "-> Edit-Buffer" and "-> Slot N" for the active
  DrumMachine pattern, with a stop-the-sequencer warning (the device NAKs writes
  during playback, surfaced as an error).
- Tests: korg-e2-pattern-push (8) — the reverse mapper field-by-field, a full
  SynthStudio->body->decode->import round-trip, and the store push actions via a
  fake ACKing device.

Note: a 0x4000 body encodes to ~18 KB SysEx sent in one Web-MIDI message;
large-message delivery depends on the device/driver (see korg_e2_native_sysex.md
transport constraints). tsc clean; 83 E2-SysEx tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Wires the bridge's global dump req/data (0x0E/0x51) into the host.

- useE2sDeviceStore: pullGlobal() stores the decoded global blob (state.globalData);
  pushGlobal(body) writes it back with ACK-wait + busy/error.
- E2sGlobalPanel: "Lesen" pulls + shows byte length and a hex preview,
  "Zurückschreiben" round-trips the exact bytes, "Backup" saves a .bin. Blob-only
  (the global field layout isn't decoded), with a stop-the-sequencer note. Wired
  into MIDI settings next to the device/preset panels.
- Tests: e2s-global-data (3) — pull stores exact bytes, push delivers byte-exact
  to a fake device (round-trip), and the not-connected guard.

tsc clean; 86 E2-SysEx tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Reverse of the step motion/param-lock bytes (+0x05..0x0B) is blocked: a byte
scan of both available files (stock init allpattern + testbank), all 240,000
step records, shows these 7 bytes are uniformly zero — neither file contains a
recorded motion sequence, and there is no separate motion region in the body.
Their semantics can't be derived without a device dump that has motion recorded,
so nothing is guessed here.

Non-destructive measure: decodeStep now reads the 7 bytes as an opaque `motion`
field (STEP_MOTION_OFFSET/STEP_MOTION_LEN + stepHasMotion()) so a future
motion-carrying pattern isn't silently dropped on decode.

- e2Sysex.ts: E2Step.motion (7 bytes), decodeStep captures +0x05..+0x0B,
  stepHasMotion() helper.
- korg-e2-pattern-decode: 2 assertions — zero-motion tuple + a crafted
  non-zero-motion record round-trips through decode.

tsc clean; tests green (adding the field is toMatchObject-compatible).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Root cause of the perpetually-red (non-blocking) Audio/Sim job: the 11
sim-loopback specs connect to ws://localhost:8744 expecting the virtual
OmniTribe device (sim_ws_server.py), but Playwright's webServer only starts
Vite — nothing ever listens on :8744, so every sim spec hits the connect
timeout. The server lives in the omnitribe repo, not here.

Fix: check out omnitribe (ref main) and start sim_ws_server.py (--autoload
chord) in the background with a wait-for-port before running the specs. Verified
locally that the server binds :8744 and returns a valid OTP identity response.

- Cross-repo checkout uses OMNITRIBE_PAT if present, else the default token
  (works when omnitribe is accessible to the workflow). Both the checkout and
  start steps are continue-on-error, and the job stays non-blocking — so if the
  checkout can't authenticate, behaviour is unchanged (no regression).
- WS-sim specs (sim-*, omnitribe-sim-*) should now run green; automix-panel /
  license-polish don't use the sim server and may still need real audio.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
hacktribe receives live FX edits over NRPN (standard MIDI CC, not sysex). This
adds the transport so the host can drive the unlocked FX from SynthStudio.

- e2Nrpn.ts: NRPN builders — cc(), buildNrpn(), and the hacktribe FX-Edit map
  (NRPN-MSB=0x01 category, NRPN-LSB=fx slot, DATA-MSB=param index, DATA-LSB=
  value) plus FX-Control-Map (category 0x02). ifxFxSlot()/MFX_FX_SLOT helpers
  (slot layout flagged to verify vs hacktribe-editor).
- E2SysexBridge: sendRaw() + sendFxEdit()/sendFxControlMap() — fire-and-forget
  (NRPN is realtime, no ACK), routed through the same MIDI output.
- useE2sDeviceStore: sendFxParam(fxSlot, paramIndex, value) action.
- korg-e2-nrpn.test.ts: 9 tests — CC framing/clamping, NRPN order, exact
  FX-Edit byte layout, slot helpers, and bridge send via a fake transport.

The FX-type list + per-type param names/ranges come from a parallel
hacktribe-editor research pass; the named-parameter UI lands with that.

tsc clean; 34 tests green (nrpn 9 + bridge 18 + device 7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Makes hacktribe's unlocked FX playable from SynthStudio.

- e2FxParams.ts: verified FX type + parameter-name tables (IFX + MFX) extracted
  from hacktribe-editor (ht_fx_ram_format.py / ht_fx_preset_format.py / ht_nrpn.py).
  param[k] index == the NRPN DATA-MSB. Plus the live FX edit-buffer (RAM 0x72,
  base 0xC03478A8, stride 0x72) decode: device id @+0x00, param k @+0x03+2k.
- E2SysexBridge.readFxEditBuffer(slot): reads + decodes the loaded FX (type +
  current values). Confirms the fx-slot math (part*2+slot, MFX=0x20) matches
  hacktribe-editor's get_fx_slot().
- useE2sDeviceStore.readFxBuffer(slot); sendFxParam already sends the NRPN edit.
- E2sFxPanel: pick Part + IFX-A/B/MFX -> "FX lesen" reads the loaded FX -> named
  sliders; each slider sends a live NRPN FX-Edit. Wired into MIDI settings.
- korg-e2-fx-params.test.ts: 8 tests (type/param tables, buffer decode, and the
  bridge/store read via a fake hacktribe RAM device).

Ranges are raw 0-127 (semantic min/max aren't in hacktribe-editor — TODO there).

tsc clean; 42 tests green (fx 8 + nrpn 9 + device 7 + bridge 18).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Step B builds on the e2FxPreset codec: a non-destructive field editor for
device-read IFX-preset (0x20C) and Groove-template (0x140) backups.

- e2FxPreset.ts: decode/edit helpers (decodeIfxPreset, setIfxPresetParam/
  Level/Name, decodeGroove, setGrooveStep) — every setter copies and only
  touches the addressed byte, so all unknown/reserved bytes survive intact.
- useE2sPresetStore: updateBackupBytes (in-place field edits) + writeBytes
  (writes edited blob to a slot; bridge enforces exact 0x20C/0x140 length,
  IFX/Groove address range, and stopped-sequencer guard).
- E2sPresetEditor: named sliders per FX slot (pre/post level + typed params)
  and per-step groove fields (signed trigger, velocity, gate); "Schreiben"
  persists via the bridge.
- E2sPresetPanel: per-backup Edit toggle expands the editor inline.

Provenance stays "device-read": edits mutate captured bytes at known
offsets only, never hand-authored blobs.

Tests: korg-e2-fx-preset (6) + e2s-preset-store new actions (4). Full
suite 9785 green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Closes hacktribe gap #2 — the FX Control Map is a documented, RX-implemented
hacktribe feature with no polished GUI anywhere. Verified against
ht_fx_ram_format.py (fx_control struct + source_control enum) and MIDI.md.

- e2FxParams: the live FX edit buffer carries 10 control-map slots at 0x36
  (6 B each: source/target/pad/min/pad/max); 0x36 + 10*6 = 0x72 fills the
  buffer exactly. decodeFxControlMap() + FX_SOURCE_CONTROLS enum, and
  decodeFxEditBuffer now returns controlMap.
- e2Nrpn: buildFxControlMapSlot() emits the 5 category-0x02 NRPN transactions
  (map_slot -> source -> target -> min -> max) as one sequence; FX_MAP_PARAM
  indices live here canonically.
- E2SysexBridge.sendFxControlMapSlot + store.sendFxControlMapSlot.
- E2sFxPanel: control-map section — map-slot picker, source dropdown, target
  param dropdown (names from fxTypeDef), min/max sliders, "Map senden";
  fields prefill from the decoded buffer.

Tests: control-map decode + buffer-fill (2) and NRPN sequence builder (2).
Full suite 9789 green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Closes hacktribe gap #3 (pattern side) — the missing automation that makes
existing E2S pattern banks hacktribe-compatible. hacktribe forbids the factory
sample slots (1-500); parts pointing there must be rewritten to the user range.

- e2PatternRemap.ts: remapOscRefsInBody / remapOscRefsInAllpat (non-destructive),
  collectAllpatOscRefs, and buildFactoryShiftMap (used factory oscs 18-500 →
  sequential user slots from 500+offset; synth ≤17 and user ≥501 stay; overflow
  past 999 is flagged, never silently wrapped). Offsets verified bit-exact
  against bangcorrupt's e2_recode_sample_pat.py + e2_update_pat_samples.py:
  pattern @ 0x10100+i*0x4000, part @ +0x800+k*0x330, osc u16 LE @ +0x08.
- E2sHacktribeRemap Tools panel ("🩹 HT-Remap"): drop a .e2sallpat/.all, pick
  the user offset (default 18 = --ofs 18), see used-factory / remapped /
  overflow counts, download the rewritten bank.
- Wired into App.tsx tools tabs.

Pattern side only — moving the sample RIFF/ESLI blocks stays with the .all
builder / ESX→E2S converter.

Tests: remap body/allpat + collect + factory-shift incl. overflow (7). Full
suite 9798 green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Extends the live-device interface to edit a pulled Edit-Buffer and push it
back — the "edit patterns on a connected device" capability.

- e2PatternEdit.ts: non-destructive setters on a pulled body for the already-
  VERIFIED fields only — step active/note/velocity/gate/gateLen, part
  volume/pan/sampleRef, pattern BPM (×10) and name. Every setter copies and
  touches only the addressed byte, so opaque bytes (incl. the motion region
  +0x05..+0x0B) survive intact. No guessed part-struct fields (filter/EG etc.
  are deliberately excluded — offsets unverified).
- useE2sDeviceStore: retains the raw currentBody on pull; applyCurrentBodyEdit
  swaps the body + re-decodes for the UI. Push reuses pushCurrent(body).
- E2sPatternEditor: name/BPM, part vol/pan/osc, a 16-wide step grid (click to
  toggle + select, right-click to select) and per-step note/velocity/gate-len,
  "Zum Gerät schreiben" pushes the Edit-Buffer. Rendered in E2sDevicePanel
  once a current pattern is pulled.

Tests: pattern-edit setters + motion preservation + round-trips (8) and
store body-retention/apply (2). Full suite 9809 green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Adds rotatePartSequence — rotates a part's step sequence by whole 12-byte
records within the active step window (16/32/64), so note/velocity/gate AND
the opaque motion bytes travel together. Non-destructive. Verified against
bangcorrupt's e2seqrot.py (rotate = l[n:]+l[:n]), generalized from its fixed
16-step window to the real pattern length.

Wired into E2sPatternEditor as ↺/↻ buttons per selected part.

Tests: rotate carries whole records incl. motion, wraps modulo the window,
no-op at 0, out-of-range guard (3). Full suite 9812 green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Completes the preset editor — you can now change an FX slot's type, not just
tweak the loaded type's params.

- e2FxDefaults.ts: per-type factory param defaults, auto-extracted bit-exact
  (paren-balanced parser) from hacktribe-editor's ht_fx_ram_format.py. Index k
  aligns with IFX_TYPES/MFX_TYPES.params[k] and NRPN DATA-MSB. A test asserts
  every type's default array length equals its param-name count (IFX + MFX).
- e2FxPreset.setIfxPresetDevice: sets the slot's device byte and initializes the
  param region with that type's defaults (so the new FX isn't left silent with
  dry_wet=0); stale higher param slots from a longer previous type are zeroed;
  pre/post level + name preserved. Region bounds derived from the slot layout.
- E2sPresetEditor: per-slot FX-type dropdown (IFX_TYPES for ifx1/ifx2, MFX_TYPES
  for mfx).

Tests: type switch loads defaults + preserves levels/name, shrinking zeros
stale params, full defaults/param-name alignment (3). Full suite 9815 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Integration test through the store + bridge + a fake device: pull the current
Edit-Buffer, edit it (setPatternName + setPartField) via e2PatternEdit, push it
back (0x40), and assert the device received the edited body — decoded back
through the 7-in-8 codec — with the new name/volume and the untouched osc ref
intact. De-risks the whole on-device edit path shipped in this series.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Completes the IFX/MFX preset editor: the 0x20C preset carries its OWN control
map (persists with the preset, unlike the transient live NRPN map). Verified
against ht_fx_preset_format.py: name@0x01(15) + pad(2), then control_map =
fx_control[10] @0x12; each slot 28 B (source@+0, chain_index@+1, target@+2,
min@+4, max@+6); 0x12 + 10*28 = 0x12A exactly meets the ifx1 device byte.

- e2FxPreset.ts: decodePresetControlMap / setPresetControlSlot (non-destructive)
  + PRESET_FX_SOURCES (0x41..0x4A — distinct from the live map's 0x01..0x0A) and
  PRESET_CHAIN_INDEX (ifx1/ifx2/mfx/input/output).
- E2sPresetEditor: per-preset control-map section — slot picker, source + chain
  + target-param dropdowns (target names resolve from the chosen chain link's
  FX type), min/max sliders.

Tests: decode 10 slots + boundary, non-destructive slot write + clamp, range
guard (3). Full suite 9819 green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
…ount)

Removes the stale "verify against hacktribe-editor" caveat on ifxFxSlot — the
0x00-0x20 / 2-per-part / MFX@0x20 addressing is confirmed by MIDI.md and the
33 live edit buffers in ht_fx_ram_format.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Adds a compact 16-wide step grid to the groove template editor: each cell
shows whether the step carries groove (velocity/trigger/gate) and its
micro-timing direction (▸/◂), and selects the step for the detail sliders.
UI-only over the already-tested decodeGroove codec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Preset/groove backups lived only in memory. Now they can be archived and
shared as files:
- store.importBytes: infers kind from blob size (524 → IFX, 320 → Groove),
  rejects other sizes; PRESET_BYTE_SIZE exported.
- E2sPresetPanel: ".bin importieren" (size auto-detects kind → adds a backup
  you can then edit/restore) and a per-backup ".bin" export button.

Tests: size→kind inference for both, invalid-size rejection (2). Full suite
9821 green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Cross-checking against bangcorrupt's factory-init .e2sallpat (from the full
hacktribe-editor clone) shows part offset +0x22 holds 0x14 (20), not 0x40
(center) — so its semantics as "pan" are not confirmed. Volume @+0x15 = 0x7F
checks out and stays confirmed.

- setPartField doc: pan marked UNVERIFIED (round-trip-safe but maybe not pan);
  points to the reverse-eng doc.
- E2sPatternEditor: pan field labeled "Pan (?)" so it isn't presented as
  verified.

No behavior change (round-trip byte write is unaffected); this only stops
claiming an unverified semantic. Needs a labeled device dump to resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Writes the project's SynthStudio patterns into a device-loadable pattern bank —
the file-based counterpart to live pattern push. Fully verifiable, no container
fabrication:

- e2AllpatBuild.ts: writePatternBodyIntoAllpat / writePatternBodiesIntoAllpat —
  non-destructive, all-or-nothing (validates slot range, exact 0x4000 body size,
  container size, duplicate slots BEFORE any write). Verified slot layout
  0x10100 + i*0x4000, bit-exact vs. bangcorrupt's e2_merge_patterns.py.
- E2sBankExport Tools panel ("📦 Bank-Export"): load a real base .e2sallpat
  (device backup or hacktribe init), pick a start slot, patterns are written
  into consecutive slots via synthstudioPatternToBody (itself init-template +
  verified-fields only). Header/filler/footer + untouched slots stay byte-exact.

Design keeps liability out: the container is always the user's real bank; we
only overwrite chosen slots with template-derived, verified-field bodies.

Tests: geometry, non-destructive single/multi write, all guards (8). Full
suite 9831 green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
The Electribe import treated E2/E2S banks with ASCII/BPM heuristics — names
guessed, every step inactive. Now routes them through the verified
decodePatternBody, the same decoder the device pull uses.

- e2sAllpatToImportResult: decodes the 250-slot allpat (0x10100 + i*0x4000) or a
  single .e2spat (0x100 header + body) into ImportResult with REAL steps
  (active/velocity), name, bpm, and osc-ref labels (#NNN). Skips empty/init
  patterns; keeps pattern 0 if all empty so import is never silently blank. Pure
  + unit-testable.
- importElectribe: for .e2sallpat/.e2spat, try the real decoder first; fall back
  to the old heuristic only if it finds no active steps.
- Pan carried for display only (flagged: pan@0x22 unconfirmed; import can't harm
  the device).

Tests: real-step decode, empty-skip, single .e2spat, all-empty fallback (4).
Full suite 9835 green, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Ties the two new file-workflow halves together: write a body into slot 5 via
writePatternBodyIntoAllpat (exporter) and read it back via
e2sAllpatToImportResult (importer) — asserting they agree on the 0x10100 +
i*0x4000 geometry and the step/velocity/osc survive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
…vices)

Foundation for running several MIDI inputs at once (e.g. an Electribe 2 for
SysEx/clock AND an Akai controller for CC/notes) instead of the current single
active input — the first step toward the parallel multi-device setup.

- useMidiInputsStore: observer store (project pattern), per-device enabled +
  role, persisted by device NAME (portable across sessions/backends, matching
  the existing by-name reconnect convention).
- Roles gate which consumers see a device's events: all / controller (CC+notes)
  / keys (notes) / sysex (E2S device comm) / clock (external tempo master) —
  pure roleAccepts* gates, unit-tested.
- migrateSingleInput adopts the old single active device as an enabled "all"
  device so existing setups keep working.

Next: wire the hook to attach its handler to all enabled inputs + gate
consumers by role, then a multi-select Devices UI. Tests: role gates + state +
migration (11). tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
claude added 30 commits July 24, 2026 13:44
…ild [win-build]

Root cause of "Audio: [AudioEngine] AudioWorklet konnte nicht geladen werden:
Unable to load a worklet's module" (packaged build): the built-in FX worklets
were referenced via `new URL("./worklets/X.js", import.meta.url)`, and the
dynamic variant in PluginRegistry made Vite glob the whole worklets/ dir. Vite
then INLINED the small processors as `data:text/javascript;base64,…` URLs
(verified in the production bundle — only the larger TimeStretch was emitted as
a real file). `audioWorklet.addModule("data:…")` is governed by `script-src`,
which is `'self'` in the packaged CSP → Chromium blocks every data: worklet
module. So Bitcrusher/RingMod/Notch/TapeSat/Width silently failed.

Fix (no CSP weakening — data: in script-src would reopen XSS surface): move all
six processors into publicDir (client/public/worklets/) and reference them by
stable relative URL `./worklets/X.js`, exactly like recorder-worklet.js already
does. PublicDir files are copied verbatim (no hashing, no inlining) and load as
same-origin file:// scripts under `script-src 'self'`.

- AudioEngine._ensureWorklets: addModule("./worklets/{Bitcrusher,RingMod,
  TimeStretch}Processor.js") instead of new URL(..., import.meta.url).
- PluginRegistry.builtInWorkletUrl → returns "./worklets/<file>" (drops the
  import.meta.url glob that triggered the inlining).
- Verified rebuild: all 6 worklets now emitted to dist/public/worklets/ verbatim,
  zero data: inlining, glob map gone.

Tests: new tests/features/worklet-public-urls.test.ts (3 cases: stable form, no
data:/blob:/import.meta, exact publicDir names). pnpm check green; recorder +
plugin-host suites green.

Bumps to 3.291.0 and triggers a Windows test build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
…ild]

Two user-requested features:

1) Startup project picker (v3.292): on launch NOTHING is auto-loaded anymore.
   A picker shows: "Neues Projekt", "Projekt laden" (.synth file), and — when a
   real previous project is cached — "Letztes Projekt öffnen" with its name.
   "Leer starten" dismisses to an empty project. Replaces the old silent
   auto-restore-on-mount. First-run Welcome-Wizard still takes precedence.
   - client/src/components/Startup/StartupProjectPicker.tsx (presentational,
     semantic theme classes only)
   - client/src/components/Startup/startupPicker.ts (pure: shouldShowStartupPicker,
     lastProjectLabel)
   - App.tsx: cached snapshot kept in a ref; New/Open/OpenLast handlers.

2) Non-destructive step switching (v3.292): toggling 16↔32↔64↔128 no longer
   deletes the higher steps — they are only HIDDEN (display and playback are both
   already bounded by pattern.stepCount). Switching back reveals them intact, so
   you can move between resolutions freely without data loss.
   - new pure helper growSteps() (grow-only, never slices) drives both
     setStepCount and applyStepCountToAllPatterns in useDrumMachineStore.
   - the project serializer already stores full step arrays, so hidden steps
     survive save/reload too.

Tests: startup-picker (7 cases), growSteps in step-count-resize (+4 cases).
pnpm check green; step-count consumer suites (multi-bar, automation, korg
templates, mutator) green.

Bumps to 3.292.0 and triggers a Windows test build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
…arts [win-build]

Reads the per-part FILTER (and modulation) fields from an ESX-1 .esx pattern and
applies them to each imported Synthstudio part's ChannelFx — so imported parts
carry the source filter character, not just samples/steps.

Offsets VERIFIED against open-electribe-editor v1.2.0 (PartDrumImpl/
PartKeyboardImpl/PartStretchSliceImpl init()):
  - Drum part: filterType@4, cutoff@5, resonance@6, egInt@7, modByte@14,
    modSpeed@15, modDepth@16.
  - Keyboard part: filter sub-block +1 (glide@4) → filterType@5…
  - Stretch/Slice part: whole block −2 (no sliceNumber/reserved) → filterType@2…
  filterType enum 0=LPF,1=HPF,2=BPF,3=BPF+; mod byte packs modDest(0-2)/modType(4-6).

Also fixes a pre-existing bug this surfaced: the Stretch/Slice decoder read
pitch/level/pan at drum offsets (8/9/10); the verified layout is 6/7/8 (−2). Now
correct.

Mapping (musical, not bit-exact): cutoff 0..127 → 20..20000 Hz exponential,
resonance 0..127 → Q 0.1..12, type → lowpass/highpass/bandpass. A neutral part
(LPF fully open, resonance 0) imports as filterEnabled=false so it stays
transparent; anything else enables the filter.

Chain: esxParser (new EsxPartFilter + decodeEsxFilter) → esxFilterMap
(esxFilterToImportedFilter) → esxPatternConvert → electribeImport → ImportedPart
.filter → importResultToPatterns writes part.fx.filter*.

Tests: new esx-filter-import (9 cases: decode offsets per part type + mapping +
neutral/active enable). pnpm check green; existing ESX decode/convert/import +
real-file suites green.

Scope note: E2/E2S (.e2pat) filter import is a follow-up (its parser is separate);
the verified E2 per-part offsets (Filter Type@0x0C…) are on record for it.

Bumps to 3.293.0 and triggers a Windows test build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
…s [win-build]

Two fixes from user reports.

1) ESX filter import produced "bandpass 20000 everywhere" — diagnosed against
   the real files (BOTTROP/ASEJIV/HEIMWEG), NOT guessed. The offset is correct:
   byte-4 filterType *does* vary (histogram BOTTROP {0:2050,3:254}, ASEJIV
   {0:2221,1:2,2:27,3:54}, HEIMWEG all 0) — mostly LPF, as expected. The bug was
   the MAPPING: the pattern the importer jumps to had BPF+ @ cutoff 127, which I
   mapped to an *enabled* Web-Audio bandpass at 20 kHz — musically that silences
   the part (passes only ~20 kHz). That is what looked "on everywhere".
   Fix — apply only what maps safely and can never silence a part:
   - Lowpass: apply only when NOT fully open (cutoff < 127) → darkens, never mutes.
   - Highpass: apply only when NOT fully open (cutoff > 0) → thins, never mutes.
   - Bandpass / BPF+: NOT auto-applied (ESX center→Web-Audio mapping unverified,
     silence risk) → filter stays off, part plays normally.
   Since the vast majority of real parts are LPF, meaningful filters still carry
   over; open/bandpass parts simply import with the filter off.

2) Fill toolbar (Subtle/Busy/Build/Roll) is by design a drum-fill in the last
   third / last 4 steps (tooltips say so) — intended, not whole-part. But
   applyMutator ran over part.steps.length, which after the non-destructive step
   switch can exceed the visible stepCount, so a grown-then-shrunk pattern would
   place the fill in HIDDEN steps. Now applyMutator mutates only the visible
   stepCount range and leaves the hidden tail intact — fills always land where
   you can see them.

Tests: esx-filter-import updated (open LPF/HPF → off; bandpass → off; closed
LPF/HPF → applied). pnpm check green; ESX decode/step-count suites green.

Bumps to 3.294.0 and triggers a Windows test build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
… E2E suite

The StartupProjectPicker (fixed inset-0 z-[200], introduced in v3.292) rendered
on every app load, so it intercepted all clicks in the Playwright browser suite:
the main "Playwright Browser-Tests" check dropped to 11 passed with mass
timeouts across performance-mode/quantize/script-runner/ui-audit — including
"Audit 3: keine blockierenden Overlays", which is exactly this bug.

The existing test convention pre-dismisses modals by seeding localStorage
(tests/web/_seedApp.ts + per-spec init scripts for synthstudio:welcome:v1), but
~40 spec files can't know about a new modal. Fix: shouldShowStartupPicker now
takes isAutomated (wired to navigator.webdriver, which Playwright sets in both
browser and Electron harnesses) and suppresses the auto-modal there. Real users
are unaffected (webdriver=false). An explicit forceForTest override (localStorage
"synthstudio:startup-picker:e2e-force" = "1") keeps the picker E2E-testable.

Verified locally against previously failing specs: quantize.spec (3) +
ui-audit.spec (4) now pass, incl. Audit 3. pnpm check green; startup-picker
unit tests extended to 9 (automation suppressed / force override wins).

Bumps to 3.295.0 (no Windows build needed — packaged-app behavior unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
Drei unabhaengige Punkte aus der offenen Liste.

1. Audio: Volume liegt pro Voice statt am geteilten Channel-Input

`volume` enthaelt die Velocity des Steps (velocity/127 * part.volume) und
wurde als nodes.input.gain.value auf den GETEILTEN Kanal-Eingang
geschrieben. Damit verbog jeder neue Step rueckwirkend die Lautstaerke
aller noch klingenden Voices desselben Parts: ein leiser 16tel-Step
mitten in einem langen Crash zog den Crash mit runter.

Neu traegt der bereits vorhandene Voice-Gain das Volume (Sample-Pfade),
bzw. ein Wrapper-Gain pro Note (Synth-Pfad, ueber den destination-
Parameter von triggerNote - genau dafuer ist er laut seiner JSDoc da).
Der Fade-in gegen Start-Klicks rampt jetzt auf das Voice-Volume statt
auf 1.

Korrektur meiner eigenen fruehreren Diagnose: bei PAN gibt es das
Problem nicht. scheduled.pan ist part.pan, also pro Part konstant -
ueberlappende Voices haben identischen Pan. Pan bleibt deshalb am
Kanal-Panner; die Unterscheidung ist im Code und in einem Test
festgehalten, damit sie nicht spaeter "vereinheitlicht" wird.

Nebeneffekt: ein Volume-Parameter-Lock haelt jetzt. Vorher ueberschrieb
der naechste Trigger den Channel-Input und hebelte das Lock aus.

2. map_fx_param: Geraete-Regler fest verdrahten

Die fuenfteilige NRPN-Sequenz lag schon auf Byte-Ebene bereit, hatte
aber keine Oberflaeche. Neu ein Formular im Korg-Remote-Panel
(eingeklappt, weil Sonderaktion): FX-Slot, Quell-Bedienelement,
Ziel-Parameter, Wertebereich, Map-Slot. Danach faehrt der X/Y-Regler
der Korg den Parameter selbst, auch ohne Synthstudio am Kabel.

Dafuer sendKorgNrpnOnce() - awaitbar und mit Fehlermeldung, im
Gegensatz zum absichtlich stillen Live-Relay: eine Zuweisung wird
genau einmal ausgeloest und darf nicht lautlos verloren gehen.

3. RAM-Werkzeug (Sysex 0x52/0x53/0x54)

Eigenes Panel, getrennt vom Live-Regelwerk, mit anderen Regeln:

- Adressfenster hart auf DDR2 (0xC0000000-0xCFFFFFFF) begrenzt. Der
  On-Chip-RAM ab 0x80000000 ist Boot-Loader-Gebiet und Sprungziel von
  0x57 - dort hinzuschreiben ist der direkte Weg zum toten Geraet.
  Keine Rueckfrage, sondern Ablehnung.
- Zwei Bestaetigungen vor jedem Write (Geraet gestoppt, Struktur
  bekannt). Der Transportzustand ist per MIDI nicht abfragbar; das
  mitgesendete MIDI-Stop wirkt nur bei externer Clock. Deshalb der
  Mensch als Absicherung statt vorgetaeuschter Automatik.
- Haeppchen a 0x100 B, jedes mit eigener Adress-Setzung und ACK.
- Read-Back nicht optional: nach dem Write wird der ganze Bereich
  zurueckgelesen und byteweise verglichen.
- Abbruch beim ersten Fehler, mit Angabe der bereits geschriebenen
  Bytes - ein teilweise beschriebener Bereich ist ein anderer Zustand
  als ein unangetasteter.
- Adress-Karte fuer IFX/MFX-Presets, FX-Edit-Buffer, Grooves.

Flash (0x55/0x56), Execute (0x57) und Loader (0x58) bleiben aussen vor.
Dort liegt die Grenze: ein RAM-Fehler ist mit einem Power-Cycle behoben,
ein Flash-Fehler nicht. Ein Test haelt fest, dass diese Kommandos nicht
in RAM_CMD auftauchen.

Neu
- utils/korg/hacktribeRam.ts        - Frames, Adress-Karte, Validierung
- audio/HacktribeRamTransfer.ts     - Chunking, Timeouts, Read-Back
- DrumMachine/HacktribeRamPanel.tsx - Oberflaeche mit Warnung

Verifikation
- 62 neue Unit-Tests (13 Per-Voice-Gain, 49 RAM), Schwerpunkt auf den
  Schutzmechanismen statt auf dem Happy Path
- 4 neue Playwright-Smokes (FX-Zuweisungs-Formular, RAM-Warnung +
  gesperrtes Schreiben + Adressvalidierung, Struktur-Karte)
- pnpm check sauber, volle Suite 10616 Tests gruen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
package.json und project.version standen noch auf 3.283.0, waehrend die
Modul-Koepfe der neuen Dateien bereits v3.284.0/v3.285.0 auswiesen. Ohne
das planen kuenftige Sitzungen gegen einen falschen Stand.

Enthalten sind die drei bereits gemergten Wellen:

- 682a547 (#22) Live-Sysex-Pattern-Transfer + MIDI-Eingangsfilter +
  Voice-Fade-in gegen Start-Klicks
- 59d22c2 (#22) Korg-Remote mit fuenf Ziel-Arten (Stock-CC + vier
  Hacktribe-NRPN-Ziele)
- 4550e40 (#23) Per-Voice-Gain, map_fx_param-Formular, RAM-Werkzeug

workLog-Eintrag mit den Entscheidungen, die man spaeter nicht mehr
rekonstruieren kann: warum nur der Edit-Buffer beschrieben wird, warum
Geraete per Name statt Port-ID gemutet werden, warum Multi-Input jetzt
Default ist, warum Pan am Kanal-Panner bleibt, und wo die Grenze des
RAM-Werkzeugs verlaeuft.

Kein CHANGELOG — die Historie lebt in agents/INDEX.js workLog[].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
…ar machen

Das RAM-Werkzeug lieferte bisher einen Hex-Dump. Damit war
map_fx_param (Geraete-Regler fest verdrahten) SCHREIBEND, aber nicht
nachpruefbar: man verdrahtet einen Regler und sieht nirgends, ob es
ankam. Diese Luecke schliesst der Inspektor.

Liest ein IFX-/MFX-Preset-Slot in voller Laenge (524 B), zeigt ueber dem
Hex-Dump: Preset-Name, alle drei Effekt-Slots MIT NAMEN samt Pre-/
Post-Level, und eine Tabelle der belegten Regler-Zuweisungen
(Bedienelement -> Kette + Param, Wertebereich). Freie Slots werden
ausgeblendet statt als "none" gelistet. Der Hex-Dump bleibt daneben
stehen - er ist die Grundlage, die Dekodierung nur eine Deutung.

Die 21 IFX- und 26 MFX-Device-IDs sowie das fx_control-Layout stammen
aus unserer eigenen Doku (hacktribe_ram_and_formats.md §2). Die
Parameter-NAMEN je Effekt fehlen weiterhin: sie stehen nur in
ht_fx_preset_format.py (21 IFX-, 25 MFX-Structs), das in dieser
Umgebung nicht erreichbar ist. Die Rohbytes werden mitgeliefert und die
Luecke ist im UI benannt, statt sie zu verschweigen.

DER FALLSTRICK, der das Modul praegt: source_control hat ZWEI
Kodierungen. Das Preset-Datei-Format benutzt 0x41-0x4A, das RAM-Format
0x01-0x0A fuer dieselben Bedienelemente. Wir lesen aus RAM, also ist RAM
die Vorgabe - bewiesen ist das nicht. Deshalb ist die Kodierung ein
Parameter und keine Annahme: beide Tabellen sind exportiert, im Panel
umschaltbar (deutet das bereits Gelesene neu, kein Re-Read), und ein
Test haelt fest, dass dieselben Bytes je nach Kodierung verschiedene
Elemente ergeben. Am Geraet zu klaeren: fxEditX per Formular setzen,
Slot zuruecklesen, nachsehen ob 0x42 oder 0x02 im Byte steht.

Neu
- utils/korg/e2FxPreset.ts - Device-Tabellen, fx_control- und
  Preset-Dekodierung, Textzusammenfassung. Rein, ohne Seiteneffekte.

Verifikation
- 30 neue Unit-Tests. Schwerpunkt: dass kein Offset verrutscht - ein um
  eins verschobenes Feld liefert weiterhin plausible Zahlen und faellt
  sonst nirgends auf. Der Kern-Test setzt an jedem dokumentierten Offset
  einen eindeutigen Marker. Dazu ein Round-Trip gegen buildMapFxParam:
  die Codes, die wir senden, muessen die sein, die der Parser im
  Preset-Modus wiederfindet.
- 1 neuer Playwright-Smoke: die Ansicht erscheint NICHT ohne
  erfolgreichen Lesevorgang - ein Dekodier-Bereich ohne Daten waere eine
  Behauptung ueber ein Geraet, das gar nicht geantwortet hat.
- pnpm check sauber, volle Suite 10646 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
…hmen

Der vorherige Stand dieses Branches ist als 4e9fbd6 nach main gesquasht
und damit kein Vorfahre mehr, was den Push blockiert. -s ours behaelt
den aktuellen Baum (= main + neue Arbeit) und verhindert, dass der
bereits gemergte Diff ein zweites Mal auftaucht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
Dritte Quelle zur Protokoll-Erweiterung in omnitribe (dort 747e9b2).
Nach agents/PROTOCOL.md muss jede Protokollaenderung gleichzeitig in
otp_protocol.h, otp_codec.py und dieser Bridge landen.

Neue Sender: writeFxPreset (IFX/MFX), requestFxPreset, setFxParam,
setFxControlMap, writeGroove, requestGroove. Antworten kommen als
CustomEvents omnitribe:fxPreset und omnitribe:fxGroove.

setFxControlMap ist EINE Nachricht, wo Hacktribes map_fx_param fuenf
NRPN-Nachrichten braucht - dort bleibt bei Abbruch eine halb geschriebene
Zuweisung zurueck.

Eine unvollstaendige Antwort wird NICHT als Ereignis weitergereicht: ein
halb gefuellter 524-B-Preset sieht aus wie ein echter und wuerde still
falsch dekodiert.

Diese Datei ist ein Spiegel - Quelle ist
omnitribe/host/synthstudio/OmniTribeBridge.ts. Der Drift-Check
(sync_to_synthstudio.py --fail-on-drift) meldet jetzt 4 in sync, 0 drift;
nebenbei faellt eine vorbestehende Leerzeilen-Abweichung weg.

Verifikation
- 22 neue Unit-Tests (jsdom): Frame-Aufbau je Sender, 7-of-8 mit
  High-Bit-Bytes, u16-Septet-Zerlegung inkl. Grenzwert 0xFFFF,
  Antwort-Ereignisse, stilles Verwerfen unvollstaendiger Antworten und
  unbekannter SUBs.
- Die Tests zaehlen gezielt nur FX-Frames: connect() schickt selbst einen
  Identity-Request, wer roh out.sent zaehlt misst etwas anderes als
  gemeint.
- pnpm check sauber, volle Suite 10668 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
… 0x07E0/250

Hacktribe legt User-Samples ab Slot-Index 501 ab. Mit 250 Slots war jede
Hacktribe-Bank unlesbar: die Offsets lagen jenseits des gelesenen Bereichs,
und der Builder verwarf solche Slots als out-of-range.

Der Befund kam aus PR #21 (Fremd-Session, jetzt geschlossen). Die Auflösung
lag schon fertig im Schwester-Repo: Korg_ESX_E2S_Editor TASK-029 hat sie am
2026-05-19 gegen die Werks-e2sSample.all von 2014 UND 13 Dumps echter Geräte
geklärt. Synthstudios TS-Port war gegenüber der constants.py veraltet, die er
selbst als Quelle nennt. Die Zahlen aus #21 (0x0010 / 1020) treffen es nicht;
Hacktribes `i*4 + 0x10` ist nicht dateirelativ (Differenz 0x48).

Warum 0x07E0 verifiziert aussah: die Werks-Referenzdatei belegt die
Tabellen-Indizes 482..492, und 0x0058 + 482*4 == 0x07E0. Ihr erster
nicht-null-Eintrag saß also genau dort, die 482 Nullen davor gingen als
reserved padding durch. Dieselben Bytes, falsch geschnitten — deshalb blieb
der Round-Trip bit-exakt und keine Suite hat gemeckert: alle E2S-Tests prüfen
die Geometrie symbolisch (START + i*4), also ob Reader und Builder
übereinstimmen, nie ob einer davon mit dem Gerät übereinstimmt.

Änderungen:
- E2S_ALL_OFFSET_TABLE_START 0x07E0 -> 0x0058, E2S_MAX_SLOTS 250 -> 1002,
  neu E2S_SLOT_INDEX_MAX = 1000 (UI-Grenze; 2 Tabellen-Einträge reserviert).
- Exact-Fit-Invariante als Laufzeit-Check im Builder: START + BYTES muss
  0x1000 sein. Genau diese Rechnung hätte den alten Wert entlarvt.
- filterOpenedSlots + Suche/"Leere verbergen" im Slot-Browser. Erst durch die
  Korrektur nötig: 1002 Zeilen, bei einer Hacktribe-Bank die ersten ~500 leer.
  hideEmpty lässt dirty-Slots stehen, damit ein gerade geleerter Slot nicht
  mitsamt ungespeicherter Änderung unter dem Cursor verschwindet.
- 19 Unit-Tests, die die Zahlen absichtlich literal nennen, plus 2
  Playwright-Smokes, die eine handgebaute .all mit Sample auf Slot 501 durch
  Datei -> Parser -> UI schicken.

Nicht mitgezogen: E2S_MAX_TOTAL_PCM_BYTES. Die SoT führt 24 MB (echtes
Geräte-Limit), wir behalten 224 MB — ein Absenken würde Banken zurückweisen,
die sich bisher bauen ließen. Als Abweichung dokumentiert; die Entscheidung
gehört dem Nutzer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
…hmen

Der Remote-Branch zeigt noch auf den Stand von PR #24, den GitHub als
d8f1304 nach main gesquasht hat. Inhaltlich ist er mit main identisch
(git diff origin/main..origin/<branch> ist leer), es geht also nichts
verloren — dieser Merge stellt nur die Vorfahren-Beziehung her, damit
der Push ohne force durchgeht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
Empty commit — v3.295.0 (74e6b53, startup-picker automation suppression) was
pushed without the build marker; this triggers the NSIS installer build for it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
…n bodies

decodePatternBody now reads the per-part FILTER fields from the offsets that
were verified three ways earlier this session (Korg official "electribe MIDI
Implementation Rev 1.00" TABLE 6 + keijiro/e2edit struct Part + maks/elfer
PartType): Filter Type @+0x0C (raw 0..16), Cutoff @+0x0D (0..127), Resonance
@+0x0E (0..127), EG Int @+0x0F (i8 -63..+63). E2PartDecoded carries them as
filterType/cutoff/resonance/egInt.

Decode-only step toward the e2pat/e2spat filter import (the ESX half shipped
in v3.293/3.294). The mapping onto Synthstudio ChannelFx follows once the
E2 filter-type value table (0..16 → LPF/HPF/BPF variants) is source-verified —
same no-guessed-offsets discipline as the ESX safe-mapping fix.

pnpm check green. Mapping + tests + version bump land in the follow-up commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
…ild]

Completes the second half of the user's filter-import request: patterns loaded
from .e2spat/.e2sallpat (and E2 SysEx pulls) now carry their per-part FILTER
onto the imported Synthstudio parts — the ESX half shipped in v3.293/3.294.

Filter-type table (raw 0..16) verified FIVE ways — Korg electribe Parameter
Guide ("Filter Type List"), Korg MIDI Implementation Rev 1.00 (TABLE 6, offset
12, range 0..16), keijiro/e2edit StringTable.asset (17 entries), rafamj/elecmidi
tables.h, and the E2S v2.02 firmware string table: 0=Off, 1..6 LPF variants
(electribe/MS20/MG/P5/OB/Acid), 7..11 HPF variants, 12..16 BPF variants. The
stock sampler UI only exposes the electribe LPF/HPF/BPF entries, but the byte
encoding is identical to the synth (hacktribe unlocks the modelled filters), so
classification by LPF/HPF/BPF class covers both devices.

- new client/src/utils/korg/e2FilterMap.ts: E2_FILTER_TYPE_NAMES (verified
  table), e2FilterClass(raw) → off/lowpass/highpass/bandpass,
  e2FilterToImportedFilter with the same never-silence rules as the ESX safe
  mapping (LPF only when not fully open, HPF only when not fully open, BPF/OFF
  never auto-applied); reuses esxCutoffToHz/esxResonanceToQ for Hz/Q.
- imports/electribeImport.ts e2PartToImported sets ImportedPart.filter →
  importResultToPatterns already applies it to part.fx (shipped with the ESX
  work).

Tests: new tests/features/e2-filter-import.test.ts (8 cases: TABLE-6 offset
decode incl. i8 egInt, 17-entry table lock, class ranges, safe-mapping rules).
pnpm check green; esx-filter/e2s-import/e2-pattern-decode/e2s-export suites
green.

Bumps to 3.296.0 and triggers a Windows test build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
…0x19

Device-confirmed: user reported SysEx-pushed patterns arrive with correct steps
but wrong volume/levels. Root cause = the flagged TABLE-6 discrepancy, now
resolved. We were writing/reading Volume at part+0x15 and Pan at part+0x22, but
per Korg's official electribe MIDI Implementation Rev 1.00 (TABLE 6, three-source
verified earlier) those are EG Decay/Release (0x15) and IFX Edit (0x22) — NOT
level/pan. Amp Level is @0x18 (u8 0..127) and Amp Pan is @0x19 (i8, 0=Center).

Writing to 0x15/0x22 both left the real Amp Level/Pan untouched (so levels stayed
at template values) AND corrupted EG Decay / IFX Edit — exactly the "steps right,
everything else off" symptom on hardware.

- e2Layout.ts: E2_PART_VOLUME_OFFSET 0x15→0x18, E2_PART_PAN_OFFSET 0x22→0x19;
  added E2_PART_EG_DECAY_OFFSET/E2_PART_IFX_EDIT_OFFSET (the old wrong targets,
  documented) and e2PanUiToDevice/e2PanDeviceToUi (UI 0..127,64=center ↔ device
  i8, 0=center).
- e2sExport.ts (writer), e2Sysex.ts (decoder), e2PatternEdit.ts (setPartField),
  electribeImport.ts (reader), electribePatternBuilder.ts (writer): all moved to
  0x18/0x19 with the i8 pan conversion. Pan is now signed on the wire.
- electribePatternBuilder: also corrected Pitch write 0x08→0x24 (0x08 is the
  verified u16 OSC/sample ref — the old pitch write clobbered the sample number's
  low byte) and FxSend 0x0B→MFX-Send on/off @0x1B (TABLE 6).

Tests: e2-layout adds pan-conversion + offset lock (12); e2s-export asserts
Amp Level@0x18 / Amp Pan@0x19(i8) + center=0; electribe-pattern-write round-trips
i8 pan; electribe-import constants updated; real-file byte-catalog assertions
that were keyed to the disproven 0x15/0x22 reads are it.skip'd with reasons
(re-measure against 0x18/0x19 when the real files are re-checked in). pnpm check
green; e2/electribe/e2s suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
… [win-build]

Sequencer ⇄ device (Task 3):
- "⇧ Gerät" / "⇩ Gerät" buttons in the DrumMachine toolbar push the active
  pattern to / pull it from a connected Korg E2/E2S via SysEx (Current Pattern).
  Disabled unless a device is connected & idle. Push reuses synthstudioPatternToBody
  (→ buildE2PatternBody), so volume/pan land at the corrected Amp Level/Amp Pan
  offsets; pull decodes + applies steps, volume, pan and the verified part filter.

Sample assignment on .e2sallpat/.all import (Task 2):
- Proven end-to-end that the link chain (part +0x08 ↔ .all OSC_0index, value-based)
  is correct via a synthetic build→parse→link round-trip. The real-world failure
  is a feedback gap, so the import now explains WHY samples were (not) assigned:
  - no .all co-selected but parts reference samples → hint to add e2sSample.all
  - bank present but 0/partial matches → lists requested vs. available device
    numbers (surfaces wrong-bank / factory-sample cases)
- New pure helpers diagnoseE2sLink + summarizeE2sSampleLink; resolver now bundles
  its sample map (E2sSampleLink) so the toast can diagnose.

Docs:
- Fix stale part-offset comments (0x15/0x22 → Amp Level 0x18 / Amp Pan 0x19) in
  e2Sysex, electribeImport and electribePatternBuilder to match the corrected code.

Tests:
- tests/features/e2s-sample-link.test.ts: 12 tests (link round-trip + diagnostics).
- korg-e2-pattern-push.test.ts: per-part volume/pan survive SysEx at 0x18/0x19.
- Full suite: 10086 passed, type-check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPLeJwDb1oCsUuHndmbSc
…Linie auf main, Offset-Tabelle auf 0x0010/1020

Die winbuild-Linie lief seit dem 21.07. parallel und war nie gemerged (89
Commits, zuletzt winbuild-3.297.0). Sie bringt E2sPresetPanel/E2sPresetEditor
(IFX- und Groove-Presets ueber hacktribe-RAM), e2Sysex.ts, den
useMidiInputsStore-Multi-Device-Pfad und mehrere ESLI-Felder mit.

KORREKTUR EINER KORREKTUR
=========================
E2S_ALL_OFFSET_TABLE_START ist 0x0010 mit 1020 Eintraegen. Der Wert
0x0058/1002 aus v3.286.0 war ebenfalls falsch. Richtig ist Oe2sSLE
(e2s_sample_all.py): load() liest 4080 B ab 0x0010, Pointer eines Samples ist
read_u32(0x10 + i*4), und der Index i ist die Geraete-Sample-Nummer
(esli.OSC_0index).

Warum der Beleg fuer 0x0058 nicht trug: der Python-Editor hat gegen 14
Datei-Dumps geprueft, aber ausschliesslich gegen 0x07E0.
scripts/verify_table_layout.py kennt genau zwei Kandidaten, 0x0058 und 0x07E0
— 0x0010 stand nie zur Wahl. Das ist entscheidend, weil ein um n Eintraege
verschobener Tabellenstart DIESELBEN Offset-Werte liefert, nur unter falschen
Indizes; "Offset >= 0x1000", "in der Datei" und "Anzahl nicht-null" sind
gegen eine Verschiebung prinzipiell blind.

Auch die Exact-Fit-Invariante aus v3.286 diskriminiert nicht: 0x0010 + 1020*4
und 0x0058 + 1002*4 ergeben beide exakt 0x1000. Sie schliesst nur 0x07E0/250
aus. Ein Test haelt das jetzt ausdruecklich fest.

Praktische Folge des Irrtums: Versatz um 18 Eintraege (0x48 B), jedes Sample
18 Slots neben seiner Nummer, und die Factory-Slots 0..17 lagen vor dem
angenommenen Tabellenanfang und fehlten ganz.

NEUES PRUEFINSTRUMENT
=====================
parseE2sBank gleicht jetzt Tabellen-Index gegen esli.OSC_0index ab. Die Datei
traegt die Sample-Nummer doppelt — die einzige Redundanz, die eine falsche
Startadresse ueberhaupt sichtbar macht. Konstanter Versatz ueber ALLE
belegten Slots (und mehr als einer) => "offset-table geometry suspect" samt
Byte-Zahl; einzelne krumme Slots werden davon getrennt gemeldet. Die
Einschraenkung "ueber alle" kam von einem Test: bei nur einem abweichenden
Slot war der Versatz trivial konstant.

KONFLIKTAUFLOESUNG (9 Dateien, 19 Hunks)
========================================
- Geometrie und Doku durchgaengig auf die 0x0010-Lesart.
- e2FxPreset.ts war add/add mit zwei verschiedenen Modulen gleichen Namens:
  der Schreib-Pfad der winbuild-Linie behaelt den Namen (E2sPresetEditor
  haengt daran), unser Lese-Inspektor wird e2FxPresetInspect.ts.
- useMidi hatte zwei Multi-Input-Implementierungen. Die store-getriebene
  Opt-in-Variante ist die Basis; listenAllInputs (Opt-out) laeuft als
  Sonderfall in derselben syncMultiInputs-Buchfuehrung — zwei konkurrierende
  Detach-Pfade haetten sich gegenseitig die Listener abgeraeumt.
  detachAllInputs auf multiInputsRef neu gebaut.
- Slot-Browser-Filter aus v3.286 bleibt (1020 Zeilen wollen gefiltert werden).

Gates: pnpm check clean, 500 Dateien / 11117 passed / 97 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
…hmen

Der Remote-Branch zeigt noch auf den Stand von PR #25, den GitHub als
c927eb4 nach main gesquasht hat. Inhaltlich ist er mit main identisch
(git diff origin/main..origin/<branch> ist leer) — dieser Merge stellt
nur die Vorfahren-Beziehung her, damit der Push ohne force durchgeht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
Der Datei-Kopf und ein Inline-Kommentar nannten noch 1002 Slots, waehrend
der Test selbst laengst gegen 1020 @0x0010 laeuft. Genau solche stehen
gebliebenen Kommentare haben die Tabellen-Geometrie zweimal in die falsche
Richtung gelenkt — deshalb hier nicht nur den Zahlendreher weg, sondern
beide verworfenen Werte samt Adresse benannt.

Nur Kommentare; der Smoke laeuft unveraendert gruen (2 passed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
…y ein

Befund: .all + .e2pat importieren, die Patterns spielen MIT den richtigen
Samples — im Sample-Browser taucht aber nichts auf.

Die Beobachtung war diagnostisch praezise: die PCM-Daten waren geladen, nur
am falschen Ort. processElectribeFiles baut per makeE2sSampleResolver
Blob-URLs und haengt sie ueber dm.setPartSample bzw. PatternData.sampleUrl an
die Pattern-Parts, also in den DrumMachine-Store. Der Sample-Browser rendert
dagegen useProjectStore.samples. Zwischen beiden gab es keine Verbindung.

Strukturell konnte die Komponente das gar nicht: DrumMachine bekam `samples`
nur als Lese-Liste, ohne Adder-Callback — anders als SampleBrowser oder
KorgBankModal. Der einzige Weg in die Library fuehrte ueber KorgBankModal, und
die wird beim kombinierten Drop nie erreicht, weil ElectronDropZone die .all
herausfiltert und nur electribe:filesImport feuert.

Aenderungen:
- Neue optionale Prop onSamplesImported an DrumMachine, verdrahtet an allen
  drei Render-Stellen. In CollabSplitView nur die LOKALE Instanz — die
  Remote-Seite spiegelt eine fremde Sitzung.
- bankSamplesToLibraryEntries als reine Funktion neben buildE2sSampleMap.
  Die Blob-URL kommt aus DEMSELBEN Resolver wie die Pattern-Parts (sonst
  laegen dieselben PCM-Daten zweimal im Speicher, auf verschiedenen Blobs),
  und iteriert wird nach derselben Bedingung sampleNumber > 0 — der Browser
  zeigt damit exakt das, was ein Pattern-Part auch treffen kann.
- Stabile id `e2s:<bankname>:<geraetenummer>`: addSamples dedupliziert ueber
  `path`, Blob-URLs sind aber bei jedem Import neu. Ohne den Schluessel legt
  ein zweiter Import derselben Bank alles doppelt an.
- Eine .all ohne Pattern-Datei ist kein Fehlgriff mehr: die Samples landen in
  der Library, die Meldung nennt die Anzahl.
- Die Geometrie-Selbstpruefung aus v3.298 wird hier als Toast sichtbar. Das
  ist die erste Stelle, an der eine ECHTE Geraetedatei durch den Parser
  laeuft; einen konstanten Versatz still zu verschlucken waere das Schlimmste.

+10 Tests. pnpm check clean, Suite 500 Dateien / 11127 passed / 97 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
…hmen

Der Remote-Branch zeigt noch auf den Stand von PR #27, den GitHub als
eccc42f nach main gesquasht hat. Inhaltlich ist er mit main identisch
(git diff origin/main..origin/<branch> ist leer) — dieser Merge stellt
nur die Vorfahren-Beziehung her.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
…m Schneiden

Befund: Marker setzen geht, aber die Slices landen weder im Sequencer noch im
Sample-Browser, und als WAV exportieren geht gar nicht. Drei Befunde, drei
verschiedene Ursachen.

1. SAMPLE-BROWSER — fehlte komplett, gleiche Klasse wie der .all-Import aus
   v3.299. handleSlicesApply legte Slices auf Drum-Kanaele (setPartSample) und
   Performance-Pads (CustomEvent), aber nie in useProjectStore.samples. Jetzt
   via onSamplesImported. Die Blob-URLs entstehen EINMAL und werden fuer
   Library UND Kanal-Zuweisung benutzt; vorher encodierte die Schleife neu.

2. WAV-EXPORT — gab es nicht. Neu utils/sliceExport.ts (rein). Zwei Details
   zaehlen auf der SD-Karte: Dateinamen FAT-tauglich gefiltert, und die
   Nummern links mit Nullen aufgefuellt — sonst steht _10 vor _2 und die
   Slice-Reihenfolge im Dateimanager ist unbrauchbar. Ab vier Slices ein ZIP,
   weil Browser Download-Serien nach den ersten Dateien abbrechen. Leere
   Slices fallen weg, die Nummerierung folgt aber der Position im ORIGINAL.

3. SEQUENCER — der Pfad existierte. Ohne aktives Pattern ist parts leer, es
   passiert nichts, und der Toast meldete trotzdem Erfolg. Jetzt eine eigene
   Warnung statt einer Erfolgsmeldung fuer einen Nicht-Vorgang.

AUFBEREITUNG (utils/sampleCleanup.ts): erfindet keine DSP — Hochpass,
Tiefpass, Rauschminderung und Gate lagen laengst im Repo. Gefehlt hat die
Verkettung auf dem Datentyp des Slicers, und deren Reihenfolge ist der
eigentliche Inhalt: DC-Offset zuerst (sonst misst danach jede Pegel-Erkennung
falsch und der Zero-Crossing-Snap findet keine Nulldurchgaenge), Hochpass vor
allem Pegelkram, Rauschminderung VOR dem Trimmen (die vordere Stille ist ihr
Profil), Normalisieren vor dem Trimmen, Fades zuletzt.

Vier Presets. Gate und Rauschminderung sind BEWUSST nicht im Standard — beide
koennen Material beschaedigen. Zwei Schutzregeln: nicht trimmen, wenn das
ganze Sample unter der Schwelle liegt, und die Randfades ueberlappen nie.

Der Editor arbeitet auf einer Arbeitskopie mit Zuruecksetzen; ein Cleanup
setzt die Onsets zurueck, weil Trimmen und Filtern die Frame-Positionen
verschieben und Marker danach ins Leere zeigten.

Nebenbefund, nicht angefasst: components/SampleSlicer/SampleSlicer.tsx
(605 LOC) wird nirgends gerendert.

+51 Unit-Tests, +3 Playwright-Smokes (echte WAV durch decodeAudioData, beide
Export-Pfade ueber echte Download-Events). pnpm check clean, Suite 502
Dateien / 11178 passed / 97 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
…hmen

Der Remote-Branch zeigt noch auf den Stand von PR #28, den GitHub als
a56378f nach main gesquasht hat. Inhaltlich mit main identisch
(git diff origin/main..origin/<branch> ist leer).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
client/src/components/SampleSlicer/ (SampleSlicer.tsx, 605 LOC, + index.ts)
war exportiert, wurde aber nirgends gerendert — kein Import ausserhalb des
eigenen Verzeichnisses, weder unter client/ noch tests/ noch electron/. Der
aktive Slicer ist SampleEditor/SampleSliceEditor.tsx, gemountet aus
DrumMachine.tsx. Aufgefallen bei der Slicer-Arbeit in v3.300.

Drei Faehigkeiten daraus hat der aktive Editor NICHT; sie sind in INDEX.js
unter next[] festgehalten, damit sie nicht mit dem Aufraeumen verschwinden:

- BPM-Raster ueber der Wellenform (drawBpmGrid)
- Snap-to-Beat beim Setzen der Marker (warpSnap). Ergaenzt den vorhandenen
  Zero-Crossing-Snap: der macht den Schnitt knackfrei, dieser rhythmisch
  richtig — beide zusammen waeren das eigentliche Ziel.
- Teilen in N gleiche Abschnitte statt Onset-Erkennung. Bei sauber
  geschnittenen Loops zuverlaessiger, dort ist das Raster bekannt.

Der Code bleibt in der Historie erreichbar (73aee66^).

pnpm check clean. Suite 502 Dateien / 11176 passed / 97 skipped — zwei Tests
weniger als vorher, weil theme-class-purity einen Test pro Komponentendatei
erzeugt und es zwei Dateien weniger gibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
…hmen

Der Remote-Branch zeigt noch auf den Stand von PR #29, den GitHub als
73aee66 nach main gesquasht hat. Inhaltlich mit main identisch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
Der non-blocking Job war im INDEX als TASK-262-Env-Gate gefuehrt. Das
stimmt nicht. Der Schritt "Checkout omnitribe (sim WS server)" scheitert
mit "remote: Repository not found" auf GeorgDub/omnitribe, dreimal Retry,
dann Aufgabe — meldet aber wegen continue-on-error trotzdem success.
Deshalb faellt es erst spaeter auf, wenn ~30 Tests an fehlenden Test-IDs
scheitern (erster: getByTestId('arp-output-synth')).

Kein Testfehler, ein Checkout-Fehler. Fix waere ein Token mit Lesezugriff
auf das private Repo; GITHUB_TOKEN kann kein Fremd-Repo lesen. Braucht ein
Repo-Secret, daher hier nur dokumentiert.

Gegenprobe: derselbe Job ist am Merge-Commit 73aee66 auf main ebenfalls rot
(Job 89943137056), also unabhaengig von der Slicer-Loeschung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRbPHkhtzeND6R1enwfxys
…mpler-research-w60qrk

# Conflicts:
#	agents/INDEX.js
#	package.json
…mpler-research-w60qrk

# Conflicts:
#	agents/INDEX.js
#	package.json
…mpler-research-w60qrk

# Conflicts:
#	package.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants