Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions client/src/components/DrumMachine/DrumMachine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ import {
} from "@/utils/e2sExport";
import { verifyE2AllpatBank } from "@/utils/korg/e2AllpatVerify";
import { convertSynthstudioPatternToE2 } from "@/utils/electribePatternConvert";
import { buildE2ExportGuide, downloadGuideMarkdown } from "@/utils/e2ExportGuide";
import { wrapPatternBodyAsFile } from "@/utils/korg/e2NativeSysex";
import { requestPatternFromDevice, sendPatternToDevice } from "@/audio/E2NativeSysexTransfer";
import { useElectron } from "../../../../electron/useElectron";
Expand Down Expand Up @@ -4851,12 +4852,19 @@ function DrumMachineInner({
.slice(0, 60) || "pattern";
const filename = `${safeName}.e2spat`;

// v3.314: Zuweisungsdatei — was das .e2spat NICHT trägt
// (Sample-Zuordnung, Channel-FX) zum Nachbauen am Gerät.
const guide = buildE2ExportGuide([currentPattern], {
title: filename,
});

if (electron.isElectron) {
const result = await electron.saveE2Pattern(
filename,
buffer
);
if (result.success) {
downloadGuideMarkdown(`${safeName}-zuweisung.md`, guide);
toast(`E2 Pattern gespeichert: ${result.filePath}`, {
kind: "success",
});
Expand All @@ -4878,6 +4886,7 @@ function DrumMachineInner({
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
downloadGuideMarkdown(`${safeName}-zuweisung.md`, guide);
toast(`E2 Pattern heruntergeladen: ${filename}`, {
kind: "success",
});
Expand Down Expand Up @@ -4929,13 +4938,19 @@ function DrumMachineInner({
allPatterns.length > 250 ? allPatterns.length - 250 : 0;

const filename = "synthstudio-bank.e2sallpat";
// v3.314: Zuweisungsdatei für die ganze Bank (Sample-
// Zuordnung + Channel-FX zum Nachbauen am Gerät).
const guide = buildE2ExportGuide(allPatterns.slice(0, 250), {
title: filename,
});

if (electron.isElectron) {
const result = await electron.saveE2AllPat(
filename,
buffer
);
if (result.success) {
downloadGuideMarkdown("synthstudio-bank-zuweisung.md", guide);
toast(
`E2 Pattern-Bank gespeichert (${e2Inputs.length} Patterns${dropped ? `, ${dropped} übersprungen` : ""}): ${result.filePath}`,
{ kind: "success" }
Expand All @@ -4957,6 +4972,7 @@ function DrumMachineInner({
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
downloadGuideMarkdown("synthstudio-bank-zuweisung.md", guide);
toast(
`E2 Pattern-Bank heruntergeladen (${e2Inputs.length} Patterns${dropped ? `, ${dropped} übersprungen` : ""}): ${filename}`,
{ kind: "success" }
Expand Down
152 changes: 152 additions & 0 deletions client/src/utils/e2ExportGuide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* client/src/utils/e2ExportGuide.ts
*
* v3.314 — Zuweisungsdatei für den Sequenzer→E2-Export.
*
* Der `.e2spat`/`.e2sallpat`-Export trägt Trigger, Velocity, Note, Volume,
* Pan und Mute — aber NICHT: welches Sample auf welchem Part liegt (die E2S
* nutzt ihre eigene Sample-Bank), und keine der Synthstudio-Channel-FX
* (Filter/Distortion/Compressor/Delay/Reverb/EQ laufen in der WebAudio-Engine
* und haben kein Byte-Feld im Pattern-Format). Diese Markdown-Datei listet
* pro Part alles auf, was am Gerät von Hand nachgebaut werden muss —
* analog zur FX-Zuweisung des ESX-Imports (v3.313).
*
* Reine TS-Logik (kein DOM) — testbar in Node.
*/

import type { PatternData, PartData, ChannelFx } from "../audio/AudioEngine";
import { synthVolumeToE2, synthPanToE2 } from "./electribePatternConvert";

/** Ein aktivierter Channel-FX als Zeile "Name (Params) — E2-Vorschlag". */
function fxLines(fx: ChannelFx | undefined): string[] {
if (!fx) return [];
const out: string[] = [];
if (fx.filterEnabled) {
const type =
fx.filterType === "highpass"
? "HPF"
: fx.filterType === "bandpass"
? "BPF"
: fx.filterType === "notch"
? "Notch"
: "LPF";
out.push(
`Filter ${type} @ ${Math.round(fx.filterFreq)} Hz, Q ${fx.filterQ.toFixed(1)}` +
` — E2: Part-Filter (${type === "HPF" ? "HPF" : "LPF"}-Typ, Cutoff/Reso)`
);
}
if (fx.distortionEnabled) {
out.push(
`Distortion (Drive ${Math.round(fx.distortionAmount)}) — E2: IFX Distortion`
);
}
if (fx.compressorEnabled) {
out.push(
`Compressor (Thr ${fx.compressorThreshold} dB, Ratio ${fx.compressorRatio}:1)` +
` — E2: IFX Compressor`
);
}
if (fx.delayEnabled) {
out.push(
`Delay (${(fx.delayTime * 1000).toFixed(0)} ms, FB ${(fx.delayFeedback * 100).toFixed(0)} %,` +
` Mix ${(fx.delayMix * 100).toFixed(0)} %) — E2: IFX BPM Sync Delay / Tape Echo`
);
}
if (fx.reverbEnabled) {
out.push(
`Reverb (Decay ${fx.reverbDecay.toFixed(1)} s, Mix ${(fx.reverbMix * 100).toFixed(0)} %)` +
` — E2: MFX Hall/Plate + MFX-Send AN`
);
}
if (fx.eqEnabled) {
out.push(
`EQ (Low ${fx.eqLow > 0 ? "+" : ""}${fx.eqLow} / Mid ${fx.eqMid > 0 ? "+" : ""}${fx.eqMid}` +
` / High ${fx.eqHigh > 0 ? "+" : ""}${fx.eqHigh} dB) — E2: IFX 2-Band EQ`
);
}
return out;
}

function partHasActivity(part: PartData | undefined): boolean {
return !!part && (part.steps ?? []).some((s) => s?.active);
}

function sourceLabel(part: PartData): string {
if (part.sampleName) return part.sampleName;
if (part.sourceType && part.sourceType !== "sample")
return `[${part.sourceType}-Synth]`;
return "—";
}

/**
* Baut die Zuweisungs-Markdown für einen Sequenzer→E2-Export.
* `patterns` = die exportierten Patterns in Bank-Reihenfolge (Slot 1..n).
*/
export function buildE2ExportGuide(
patterns: PatternData[],
opts?: { title?: string }
): string {
const lines: string[] = [];
lines.push(`# ${opts?.title ?? "Synthstudio → E2 Sampler"} — Zuweisung`);
lines.push("");
lines.push(
"Das Pattern-Format trägt Trigger/Velocity/Note/Volume/Pan/Mute. NICHT"
);
lines.push(
"übertragen werden: Sample-Zuordnung (die E2S spielt ihre eigene Bank)"
);
lines.push(
"und die Synthstudio-Channel-FX. Diese Liste zum Nachbauen am Gerät:"
);
lines.push("");

let any = false;
patterns.forEach((pattern, idx) => {
const parts = (pattern.parts ?? []).filter(partHasActivity);
if (parts.length === 0) return;
any = true;
const bpm = typeof pattern.bpm === "number" ? ` · ${pattern.bpm} BPM` : "";
lines.push(`## Pattern ${idx + 1}${pattern.name ? ` „${pattern.name}"` : ""}${bpm}`);
lines.push("");
lines.push("| Part | Sound | Level (E2) | Pan (E2) | Nachbauen |");
lines.push("|---:|---|---:|---:|---|");
(pattern.parts ?? []).forEach((part, pi) => {
if (!partHasActivity(part)) return;
const fx = fxLines(part.fx);
const extra: string[] = [...fx];
if (part.stretchRatio && part.stretchRatio !== 1)
extra.push(`Time-Stretch ×${part.stretchRatio}`);
if (part.microTiming)
extra.push(`Micro-Timing ${part.microTiming > 0 ? "+" : ""}${part.microTiming} ms`);
lines.push(
`| ${pi + 1} | ${sourceLabel(part)} | ${synthVolumeToE2(part.volume)} | ` +
`${synthPanToE2(part.pan)} | ${extra.length ? extra.join("; ") : "—"} |`
);
});
lines.push("");
});

if (!any) {
lines.push("_Keine Parts mit aktiven Steps im Export._");
lines.push("");
}
return lines.join("\n");
}

/**
* Bietet die Zuweisung als `.md`-Download an (Browser UND Electron — beide
* rendern in Chromium, der Anchor-Download funktioniert in beiden). Ohne DOM
* (Node-Testkontext) ein No-op, damit die reine Guide-Logik testbar bleibt.
*/
export function downloadGuideMarkdown(filename: string, text: string): void {
if (typeof document === "undefined" || typeof URL === "undefined") return;
const blob = new Blob([text], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
10 changes: 10 additions & 0 deletions client/src/utils/e2sExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ const PART_VOLUME_OFF = 0x18;
/** Amp Pan (i8, 0 = Center) @ +0x19 — v3.297-Korrektur: vorher u8@0x22
* (= IFX Edit). UI-0..127 wird via e2PanUiToDevice konvertiert. */
const PART_PAN_OFF = 0x19;
/** EG Decay/Release (u8) @ +0x15 (Korg TABLE 6 "EG"). Init/Default 127 =
* klingt voll aus. v3.312: nimmt beim ESX-Import das Part-egtime auf —
* vorher blieben alle Parts auf 127 und kurze perkussive ESX-Hüllkurven
* gingen verloren (Gerätebefund: Mix klingt anders als auf der ESX). */
const PART_EGTIME_OFF = 0x15;
const PART_STEPS_OFF = 0x30;
const STEP_RECORD_SIZE = 12;
const STEPS_PER_PART = 64;
Expand Down Expand Up @@ -258,6 +263,11 @@ export function buildE2PatternBody(input: E2PatternInput): Uint8Array {
clampInt(part.pan, 0, 127, 64)
);
}
// v3.312: Amp-EG-Zeit @ +0x15 — nur überlagern, wenn der Aufrufer einen
// Wert liefert (ESX-Import); sonst bleibt der Template-Wert (127) stehen.
if (typeof part.egTime === "number") {
body[partStart + PART_EGTIME_OFF] = clampInt(part.egTime, 0, 127, 127);
}
// Per-part sample reference @ +0x08 (u16 LE). Only written when the caller
// provides one (e.g. repointing parts to imported user samples at 501+);
// otherwise the template's factory sample assignment is preserved.
Expand Down
7 changes: 7 additions & 0 deletions client/src/utils/electribePatternBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ export interface E2PartInput {
pitch?: number;
/** 0..127 — Effect-Send. Default 0. */
fxSend?: number;
/**
* v3.312 — Amp-EG-Zeit (EG Decay/Release) @ part+0x15. 0..127, Default 127
* (Init-Wert; klingt voll aus). ESX-Import: das Part-egtime-Byte trug die
* perkussiven Hüllkurven — ohne Übernahme klangen kurze ESX-Parts auf dem
* E2S dauerhaft lang und verschoben den gehörten Mix.
*/
egTime?: number;
/**
* v3.288: Part-Mute-Zustand. Wird beim Export in das E2-Pattern geschrieben
* (Offset aus verifizierter Reverse-Eng), damit die exportierte Bank die
Expand Down
6 changes: 5 additions & 1 deletion client/src/utils/korg/e2sBankBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,11 @@ function buildKorgSubchunk(

// Fixed-value fields the device expects (Oe2sSLE "_UFix"; verified constant
// across real factory/user banks). Oe2sSLE warns if these differ → set them.
chunk[bodyOffset + 0x1d] = 0x02;
// v3.316-Fix: +0x1D ist KEIN Fix-Byte, sondern das MSB von importNum
// (u16 @0x1C = sampleNumber+50)! In den Referenz-Banken lagen alle
// importNums zufaellig in 0x200..0x2FF, daher sah das Byte konstant 0x02
// aus. Der Hardcode hat ab sampleNumber 718 (importNum >= 0x300) das MSB
// zerstoert — Befund beim Bau der 233-Slot-Bank "hunnel" (Slots bis 733).
chunk[bodyOffset + 0x21] = 0x7f;
chunk[bodyOffset + 0x23] = 0x01;
chunk[bodyOffset + 0x4b] = 0x01;
Expand Down
Loading
Loading