Skip to content
Draft
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
82 changes: 82 additions & 0 deletions configurator/src/components/ManualTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1595,6 +1595,88 @@ On load, both registers are restored at the next phrase boundary so the recalled
},
],
},
{
appId: 30,
title: "Golden Gate",
description: "Fibonacci-spaced gates — successive ratios approach φ",
color: "Violet",
icon: "sequence-square",
params: [
"MIDI Channel",
"MIDI Note",
"MIDI CC",
"GATE %",
"Speed",
"Color",
"MIDI Out",
"Jack",
"Range",
"CV Dest",
"CV Att",
"Mode",
],
storage: [
"Fibonacci depth",
"Cycle length",
"Muted",
"Reversed",
"Output mode",
"Speed override",
],
text: `Golden Gate is a one-channel gate / pitch generator whose hit spacing comes from the Fibonacci sequence (1, 1, 2, 3, 5, 8, 13, and so on). Successive Fibonacci ratios converge on **φ** (the golden ratio ≈ 1.618), so as the pattern uses deeper gaps the rhythm feels increasingly “golden” — still built from whole-number steps, not continuous φ timing by default.

#### What you hear

At shallow depth the hits are denser and more regular; deeper Fibonacci values open larger holes, so the same cycle length feels sparser and more asymmetric. Reverse plays the gaps of *this* filled cycle backwards (not the whole table from its largest value). In pitch modes the jack carries 1V/oct while MIDI sends related notes — either chromatic Fibonacci intervals (12-TET) or near-φ intervals (~833 cents × gap), with MIDI using nearest note + pitch bend (±2 semitone bend range assumed).

#### Gestures

| Control | Action |
| --- | --- |
| **Fader** | Fibonacci depth — how many sequence values rotate |
| **Shift + Fader** | Cycle length N in steps (bar-aligned; Fibonacci gaps fill, then reset) |
| **Button + Fader** | Speed — Heat Pump grid (1/1 … 1/32, incl. dotted & triplets); overrides Configurator **Speed** |
| **Short press** | Reset to downbeat |
| **Long press** | Mute (no fader move) |
| **Shift + short** | Reverse this cycle’s gap order (Button LED: white↔off fade) |
| **Shift + long** | Cycle output mode: Note → CC → 12-TET pitch → φ pitch (also a **Mode** parameter) |

#### Faders & LEDs

| Control | Edits | Visual feedback |
| --- | --- | --- |
| **Fader** | Depth | **Top LED** = cycle progress in app color. **Bottom LED** flashes on each hit |
| **Shift + Fader** | Cycle length | **Top LED** = cycle length (red) while Shift is held |
| **Button + Fader** | Speed | **Top LED** = division type (cyan straight / yellow dotted / orange triplet) |
| **Button LED** | — | Output mode color (app color / orange / red / pink); white↔off on reverse; off when muted |`,
channels: [
{
jackTitle: "Gate / Pitch Out",
jackDescription:
"Note and CC modes: gate pulse. Pitch modes: 1V/oct CV (MIDI note in parallel).",
faderTitle: "Fibonacci depth",
faderDescription:
"How far into 1, 1, 2, 3, 5 … 89 the gaps go — shallow = denser, deep = sparser.",
faderPlusShiftTitle: "Cycle length",
faderPlusShiftDescription:
"How many steps before the pattern wraps (about 8–128).",
faderPlusFnTitle: "Speed",
faderPlusFnDescription:
"Clock division (same as Heat Pump): 1/1, 1/2, 1/4., 1/4, 1/8., 1/4T, 1/8, 1/8T, 1/16, 1/32. Up = faster. Overrides the Speed parameter.",
fnTitle: "Reset / Mute",
fnDescription:
"Short press: restart at the downbeat. Long press: mute (do not move the fader).",
fnPlusShiftTitle: "Reverse / Output mode",
fnPlusShiftDescription:
"Short: reverse this cycle’s gaps. Long: cycle Note → CC → 12-TET pitch → φ pitch.",
ledTop: "Progress through the cycle (app color)",
ledTopPlusShift: "Cycle length (red) while Shift is held",
ledTopPlusFn:
"Speed division: cyan = straight, yellow = dotted, orange = triplet",
ledBottom: "Flash on each hit",
},
],
},
];

export const ManualTab = () => {
Expand Down
3 changes: 2 additions & 1 deletion configurator/src/components/manual/Apps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export const Apps = ({ apps }: Props) => (
button 2 mutes Output B)
</li>
<li>
<strong>Long press (no shift)</strong> — AD Envelope, LFO, LFO+
<strong>Long press (no shift)</strong> — AD Envelope, LFO, LFO+, Golden
Gate
</li>
<li>
<strong>Shift + long press on button 0 / 2 / 4 / 6</strong> — Sequencer
Expand Down
4 changes: 2 additions & 2 deletions configurator/src/components/manual/ManualApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,9 @@ export const ManualApp = ({ app }: Props) => {
</List>
</div>
) : null}
<p className="mb-4">
<div className="mb-4">
<Md>{app.text}</Md>
</p>
</div>
<div
className="inline-grid"
style={{
Expand Down
174 changes: 140 additions & 34 deletions configurator/src/components/manual/Md.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,150 @@ interface MdProps {
children: ReactNode;
}

type Segment =
| { kind: "md"; text: string }
| { kind: "table"; headers: string[]; rows: string[][] };

/** Split GFM pipe-tables out so we can render them without remark-gfm. */
function splitMarkdownTables(src: string): Segment[] {
const lines = src.split("\n");
const out: Segment[] = [];
let buf: string[] = [];

const flushMd = () => {
if (!buf.length) return;
out.push({ kind: "md", text: buf.join("\n") });
buf = [];
};

const isRow = (line: string) => /^\s*\|.*\|\s*$/.test(line);
const isSep = (line: string) =>
/^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);

const cells = (line: string) =>
line
.trim()
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|")
.map((c) => c.trim());

let i = 0;
while (i < lines.length) {
if (isRow(lines[i]) && i + 1 < lines.length && isSep(lines[i + 1])) {
flushMd();
const headers = cells(lines[i]);
i += 2;
const rows: string[][] = [];
while (i < lines.length && isRow(lines[i]) && !isSep(lines[i])) {
rows.push(cells(lines[i]));
i++;
}
out.push({ kind: "table", headers, rows });
continue;
}
buf.push(lines[i]);
i++;
}
flushMd();
return out;
}

const mdComponents = {
p: ({ children }: { children?: ReactNode }) => (
<p className="mb-2 last:mb-0">{children}</p>
),
h4: ({ children }: { children?: ReactNode }) => (
<h4 className="mt-6 mb-2 font-bold">{children}</h4>
),
strong: ({ children }: { children?: ReactNode }) => (
<strong className="font-semibold">{children}</strong>
),
em: ({ children }: { children?: ReactNode }) => <em>{children}</em>,
code: ({ children }: { children?: ReactNode }) => (
<code className="rounded bg-white/10 px-1 py-0.5 text-sm">{children}</code>
),
ul: ({ children }: { children?: ReactNode }) => (
<ul className="my-1 ml-3 list-inside list-disc">{children}</ul>
),
ol: ({ children }: { children?: ReactNode }) => (
<ol className="my-1 ml-3 list-inside list-decimal">{children}</ol>
),
a: ({ href, children }: { href?: string; children?: ReactNode }) => (
<a
className="font-semibold underline"
href={href}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
};

const InlineMd = ({ text }: { text: string }) => (
<ReactMarkdown
components={{
...mdComponents,
// Table cells: keep inline — no wrapping <p>
p: ({ children }) => <>{children}</>,
}}
>
{text}
</ReactMarkdown>
);

const ManualTable = ({
headers,
rows,
}: {
headers: string[];
rows: string[][];
}) => (
<div className="my-3 overflow-x-auto">
<table className="w-full min-w-[20rem] border-collapse text-left text-sm">
<thead>
<tr className="border-b border-white/20">
{headers.map((h) => (
<th
key={h}
className="font-vox px-2 py-1.5 font-semibold whitespace-nowrap"
>
<InlineMd text={h} />
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, ri) => (
<tr key={ri} className="border-b border-white/10 align-top">
{headers.map((_, ci) => (
<td key={ci} className="px-2 py-1.5">
<InlineMd text={row[ci] ?? ""} />
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);

export const Md = ({ children }: MdProps) => {
if (typeof children !== "string") return <>{children}</>;

const segments = splitMarkdownTables(children);
return (
<ReactMarkdown
components={{
p: ({ children }) => <>{children}</>,
h4: ({ children }) => (
<h4 className="mt-6 mb-2 font-bold">{children}</h4>
),
strong: ({ children }) => (
<strong className="font-semibold">{children}</strong>
),
em: ({ children }) => <em>{children}</em>,
code: ({ children }) => (
<code className="rounded bg-white/10 px-1 py-0.5 text-sm">
{children}
</code>
<div className="manual-md">
{segments.map((seg, i) =>
seg.kind === "table" ? (
<ManualTable key={i} headers={seg.headers} rows={seg.rows} />
) : (
<ReactMarkdown key={i} components={mdComponents}>
{seg.text}
</ReactMarkdown>
),
ul: ({ children }) => (
<ul className="my-1 ml-3 list-inside list-disc">{children}</ul>
),
ol: ({ children }) => (
<ol className="my-1 ml-3 list-inside list-decimal">{children}</ol>
),
a: ({ href, children }) => (
<a
className="font-semibold underline"
href={href}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
}}
>
{children}
</ReactMarkdown>
)}
</div>
);
};
Loading