feat(website): add the fl.narl.io site
Check and Windows preview / check (anvil) (push) Successful in 49s
Check and Windows preview / check (crucible) (push) Successful in 48s
Check and Windows preview / check (knurl) (push) Successful in 40s
Check and Windows preview / check (lathe) (push) Successful in 41s
Check and Windows preview / check (patina) (push) Successful in 56s
Check and Windows preview / check (rivet) (push) Successful in 45s
Check and Windows preview / check (temper) (push) Successful in 43s
Check and Windows preview / check (weld) (push) Successful in 1m20s
Check and Windows preview / previews (push) Successful in 1m15s
Check and Windows preview / windows (push) Successful in 2m31s

Astro site for the suite: a home page with the plugin cards, install
steps, principles, the metalwork names and the ideas on the bench, one
page per plugin, and the Impressum and privacy policy. Plugin names,
versions, download links and editor previews come from the catalog, the
wrapper manifests and docs/*/assets at build time.

Served by unprivileged nginx in Docker behind Traefik on fl.narl.io,
the same setup as proton.narl.io: no access logs, CSP 'self', no
JavaScript, legal pages filled from the environment at start.
This commit is contained in:
nvrl committed 2026-11-06 11:30:12 +01:00
1 parent 5cc3770737
commit 7e1776bb04
40 files changed
+8515

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
---
import { IDEAS, REPO } from '../data/plugins';
---
<section id="bench" class="section">
<div class="container">
<div class="section-head">
<p class="eyebrow">On the bench</p>
<h2>What could come next</h2>
<p>
Ideas from the <a href={`${REPO}/src/branch/main/docs/ideas.md`}>ideas list</a>, each written against a problem
seen in bass-music production. Most of the new ones are about texture. These are ideas, not promises.
</p>
</div>
<ul class="grid" role="list">
{
IDEAS.map((idea) => (
<li class={idea.texture ? 'texture' : undefined}>
<div class="head">
<h3>{idea.name}</h3>
{idea.texture && <span class="badge badge-accent">Texture</span>}
</div>
<p class="label">{idea.role}</p>
<p class="text">{idea.text}</p>
</li>
))
}
</ul>
</div>
</section>
<style>
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
margin: 0;
padding: 0;
list-style: none;
}
li {
display: grid;
align-content: start;
gap: 0.45rem;
padding: 1.4rem 1.5rem 1.5rem;
border: 1px dashed var(--line-strong);
border-radius: var(--radius);
background: rgb(255 255 255 / 0.012);
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
h3 {
font-size: 1.4rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.text {
font-size: 0.975rem;
}
@media (max-width: 960px) {
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 600px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>
+138
View File
@@ -0,0 +1,138 @@
---
import logo from '../assets/logo.svg';
import { PLUGINS, REPO } from '../data/plugins';
const year = new Date().getFullYear();
---
<footer class="site-footer">
<div class="knurl" aria-hidden="true"></div>
<div class="container grid">
<div class="about">
<a class="brand" href="/">
<img src={logo.src} alt="" width="30" height="30" />
<span>Bass <b>Foundry</b></span>
</a>
<p>Open-source CLAP plugins for writing, designing and mixing bass music in FL Studio.</p>
</div>
<nav aria-label="Plugins">
<p class="label">Plugins</p>
<ul role="list">
{
PLUGINS.map((p) => (
<li>
<a href={`/plugins/${p.slug}/`}>{p.name}</a>
</li>
))
}
</ul>
</nav>
<nav aria-label="Project">
<p class="label">Project</p>
<ul role="list">
<li><a href={REPO}>Source code</a></li>
<li><a href={`${REPO}/releases`}>All releases</a></li>
<li><a href={`${REPO}/src/branch/main/docs/ideas.md`}>Ideas</a></li>
<li><a href={`${REPO}/src/branch/main/LICENSE`}>License (ISC)</a></li>
</ul>
</nav>
<nav aria-label="Legal">
<p class="label">Legal</p>
<ul role="list">
<li><a href="/impressum/">Impressum</a></li>
<li><a href="/datenschutz/">Datenschutz</a></li>
</ul>
</nav>
</div>
<div class="container fine">
<p>
FL Studio is a trademark of Image-Line. Bass Foundry is an independent, non-commercial project and is not
affiliated with Image-Line.
</p>
<p>© {year} Nils Pukropp · No cookies, no tracking.</p>
</div>
</footer>
<style>
.site-footer {
margin-top: 2rem;
background: var(--bg-2);
font-size: 0.95rem;
}
.grid {
display: grid;
grid-template-columns: 2fr repeat(3, 1fr);
gap: 2.5rem;
padding-block: 3.5rem 2.5rem;
}
.about {
display: grid;
align-content: start;
gap: 1rem;
max-width: 340px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.65rem;
color: var(--text);
font-family: var(--display);
font-size: 1.1rem;
letter-spacing: 0.14em;
text-decoration: none;
text-transform: uppercase;
}
.brand b {
color: var(--accent);
}
ul {
display: grid;
gap: 0.4rem;
margin: 0.8rem 0 0;
padding: 0;
list-style: none;
}
nav a {
color: var(--muted);
text-decoration: none;
}
nav a:hover {
color: var(--text);
}
.fine {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 0.75rem 2rem;
padding-block: 1.5rem 2.25rem;
border-top: 1px solid var(--line);
color: var(--faint);
font-size: 0.85rem;
}
.fine p:first-child {
max-width: 640px;
}
@media (max-width: 860px) {
.grid {
grid-template-columns: 1fr 1fr;
}
.about {
grid-column: 1 / -1;
}
}
</style>
+91
View File
@@ -0,0 +1,91 @@
---
import { PLUGINS } from '../data/plugins';
---
<section id="foundry" class="section foundry">
<div class="container cols">
<div class="section-head">
<p class="eyebrow">The foundry</p>
<h2>Named after metalwork</h2>
<p>
The suite is the foundry: the workshop where the bass gets cast, shaped and finished. Each plugin is named after
the craft that does the same job to metal.
</p>
</div>
<dl class="names">
{
PLUGINS.map((p) => (
<div>
<dt>
<a href={`/plugins/${p.slug}/`}>{p.name}</a>
</dt>
<dd>{p.metal}</dd>
</div>
))
}
</dl>
</div>
</section>
<style>
.foundry {
border-block: 1px solid var(--line);
background:
radial-gradient(60% 80% at 100% 0%, rgb(255 1 96 / 0.07), transparent 60%),
var(--bg-2);
}
.cols {
display: grid;
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
gap: clamp(2rem, 5vw, 4rem);
align-items: start;
}
.section-head {
margin-bottom: 0;
}
.names {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
margin: 0;
}
.names div {
padding: 1.1rem 1.25rem 1.1rem 0;
border-top: 1px solid var(--line);
}
dt a {
color: var(--text);
font-family: var(--display);
font-size: 1.25rem;
font-weight: 600;
letter-spacing: 0.08em;
text-decoration: none;
text-transform: uppercase;
}
dt a:hover {
color: var(--accent);
}
dd {
margin: 0.2rem 0 0;
font-size: 0.975rem;
}
@media (max-width: 860px) {
.cols {
grid-template-columns: 1fr;
}
}
@media (max-width: 540px) {
.names {
grid-template-columns: 1fr;
}
}
</style>
+110
View File
@@ -0,0 +1,110 @@
---
import logo from '../assets/logo.svg';
import { REPO } from '../data/plugins';
import Icon from './Icon.astro';
interface Props {
/** Without section links, for the legal pages and the 404 page. */
minimal?: boolean;
}
const { minimal = false } = Astro.props;
---
<header class="site-header">
<div class="container bar">
<a class="brand" href="/" aria-label="Bass Foundry, home">
<img src={logo.src} alt="" width="34" height="34" />
<span>Bass <b>Foundry</b></span>
</a>
{
!minimal && (
<nav aria-label="Sections">
<a href="/#plugins">Plugins</a>
<a href="/#install">Install</a>
<a href="/#principles">Principles</a>
<a href="/#bench">On the bench</a>
</nav>
)
}
<a class="btn btn-ghost btn-sm" href={REPO}><Icon name="code" />Source</a>
</div>
</header>
<style>
.site-header {
position: sticky;
top: 0;
z-index: 50;
border-bottom: 1px solid var(--line);
background: rgb(11 12 14 / 0.78);
backdrop-filter: blur(14px) saturate(1.4);
}
.bar {
display: flex;
align-items: center;
gap: 2rem;
min-height: 4.25rem;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.7rem;
color: var(--text);
font-family: var(--display);
font-size: 1.2rem;
font-weight: 500;
letter-spacing: 0.14em;
text-decoration: none;
text-transform: uppercase;
}
.brand b {
color: var(--accent);
font-weight: 700;
}
.brand:hover {
color: var(--text);
}
nav {
display: flex;
gap: 1.75rem;
margin-left: auto;
}
nav a {
color: var(--muted);
font-family: var(--display);
font-size: 0.95rem;
font-weight: 500;
letter-spacing: 0.12em;
text-decoration: none;
text-transform: uppercase;
}
nav a:hover {
color: var(--text);
}
.btn {
margin-left: auto;
}
nav + .btn {
margin-left: 0;
}
@media (max-width: 860px) {
nav {
display: none;
}
nav + .btn {
margin-left: auto;
}
}
</style>
+244
View File
@@ -0,0 +1,244 @@
---
import { PLUGINS, REPO } from '../data/plugins';
import Icon from './Icon.astro';
// The scope shows what Knurl's "Fry" pattern does to a saw bass: every cycle gets its own
// level (0 / −14 / −5 / −14 dB), with a raised-cosine crossfade over 15 % of a cycle.
const W = 1200;
const H = 300;
const CYCLES = 8;
const STEPS = 900;
const pattern = [0, -14, -5, -14].map((db) => 10 ** (db / 20));
const edge = 0.15;
function gainAt(phase: number): number {
const cycle = Math.floor(phase);
const frac = phase - cycle;
const g = pattern[cycle % pattern.length];
if (frac < 1 - edge) return g;
const next = pattern[(cycle + 1) % pattern.length];
const t = (frac - (1 - edge)) / edge;
return g + (next - g) * (0.5 - 0.5 * Math.cos(Math.PI * t));
}
function wave(gain: (phase: number) => number): string {
const points: string[] = [];
for (let i = 0; i <= STEPS; i++) {
const phase = (i / STEPS) * CYCLES;
// A tanh-shaped saw, as in the Knurl prototype.
const saw = 2 * (phase - Math.floor(phase)) - 1;
const y = Math.tanh(2.2 * saw) / Math.tanh(2.2);
const x = (i / STEPS) * W;
points.push(`${x.toFixed(1)},${(H / 2 - y * gain(phase) * (H * 0.4)).toFixed(1)}`);
}
return `M${points.join('L')}`;
}
const dry = wave(() => 1);
const knurled = wave(gainAt);
const count = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'][PLUGINS.length] ?? PLUGINS.length;
---
<section class="hero">
<div class="glow" aria-hidden="true"></div>
<div class="container inner">
<p class="eyebrow">CLAP plugins for FL Studio</p>
<h1>Bass <span>Foundry</span></h1>
<p class="lead">
{count[0].toUpperCase() + count.slice(1)} open-source plugins for writing, designing and mixing riddim, dubstep and
bass music. Most of them follow the played note, so a setting means the same thing on every note.
</p>
<div class="ctas">
<a class="btn btn-primary" href="#plugins">Browse the plugins<Icon name="arrow" /></a>
<a class="btn btn-ghost" href={`${REPO}/releases`}><Icon name="download" />All releases</a>
</div>
<ul class="facts" role="list">
<li>Free &amp; open source</li>
<li>CLAP</li>
<li>Windows x64</li>
<li>FL Studio 2024.1+</li>
<li>No accounts, no telemetry</li>
</ul>
<figure class="scope plate">
<figcaption>
<span class="label"><b>Knurl</b> / cycle texture · Fry pattern</span>
<span class="label readout">F1 · 43.7 Hz · 8 cycles</span>
</figcaption>
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label="A saw bass in grey, and the same bass in pink after Knurl gives each cycle its own level">
<defs>
<filter id="hero-glow" x="-5%" y="-20%" width="110%" height="140%">
<feGaussianBlur stdDeviation="6" />
</filter>
</defs>
<g class="grid">
{Array.from({ length: CYCLES + 1 }, (_, i) => <line x1={(i * W) / CYCLES} x2={(i * W) / CYCLES} y1="0" y2={H} />)}
<line class="axis" x1="0" x2={W} y1={H / 2} y2={H / 2} />
</g>
<path class="dry" d={dry} />
<path class="wave halo" d={knurled} pathLength="1" filter="url(#hero-glow)" />
<path class="wave" d={knurled} pathLength="1" />
</svg>
</figure>
</div>
</section>
<style>
.hero {
position: relative;
overflow: hidden;
padding-block: clamp(4rem, 9vw, 7rem) clamp(1rem, 3vw, 2rem);
}
.hero::before {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(rgb(184 192 204 / 0.045) 1px, transparent 1px) 0 0 / 48px 48px,
linear-gradient(90deg, rgb(184 192 204 / 0.045) 1px, transparent 1px) 0 0 / 48px 48px;
mask-image: linear-gradient(180deg, #000 40%, transparent);
pointer-events: none;
}
.glow {
position: absolute;
inset: auto -20% -10% -20%;
height: 70%;
background: radial-gradient(50% 60% at 50% 70%, rgb(255 1 96 / 0.16), transparent 70%);
pointer-events: none;
}
.inner {
position: relative;
display: grid;
gap: 1.5rem;
}
h1 {
font-size: clamp(3.6rem, 1.5rem + 9vw, 8.5rem);
font-weight: 700;
letter-spacing: -0.01em;
line-height: 0.92;
text-transform: uppercase;
}
h1 span {
display: block;
color: transparent;
background: linear-gradient(90deg, var(--accent), #ff5c95 55%, var(--steel));
background-clip: text;
}
.lead {
max-width: 640px;
color: #c3c7ce;
font-size: clamp(1.1rem, 1rem + 0.45vw, 1.3rem);
}
.ctas {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
margin-top: 0.5rem;
}
.facts {
display: flex;
flex-wrap: wrap;
gap: 0.4rem 1.4rem;
margin: 0.25rem 0 0;
padding: 0;
color: var(--faint);
font-family: var(--display);
font-size: 0.9rem;
letter-spacing: 0.1em;
list-style: none;
text-transform: uppercase;
}
.facts li {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.facts li::before {
content: '';
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--accent);
}
.scope {
margin: clamp(2rem, 5vw, 3.5rem) 0 0;
padding: 1rem 1.25rem 1.25rem;
}
figcaption {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0 0.6rem 0.75rem;
}
figcaption b {
color: var(--text);
font-weight: 600;
}
.readout {
color: var(--steel);
}
svg {
width: 100%;
height: auto;
border-radius: var(--radius-sm);
background: var(--bg);
}
.grid line {
stroke: rgb(184 192 204 / 0.08);
stroke-width: 1;
}
.grid .axis {
stroke: rgb(184 192 204 / 0.16);
}
.dry {
fill: none;
stroke: var(--dim);
stroke-width: 1.5;
stroke-dasharray: 5 5;
}
.wave {
fill: none;
stroke: var(--accent);
stroke-width: 2.25;
stroke-linejoin: round;
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: draw 2.6s cubic-bezier(0.3, 0.6, 0.2, 1) 0.2s forwards;
}
.halo {
stroke-width: 8;
opacity: 0.45;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
@media (max-width: 640px) {
.readout {
display: none;
}
}
</style>
+42
View File
@@ -0,0 +1,42 @@
---
// A small stroke icon set, 24 × 24, drawn in currentColor.
const PATHS = {
download: ['M12 4v11', 'M7 10l5 5 5-5', 'M5 20h14'],
code: ['M8 7l-5 5 5 5', 'M16 7l5 5-5 5', 'M14 4l-4 16'],
book: ['M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2z', 'M4 21V5', 'M8 7h7'],
arrow: ['M5 12h14', 'M13 6l6 6-6 6'],
back: ['M19 12H5', 'M11 6l-6 6 6 6'],
tag: ['M3 12V4a1 1 0 0 1 1-1h8l9 9-9 9z', 'M8 8h.01'],
list: ['M8 6h12', 'M8 12h12', 'M8 18h12', 'M4 6h.01', 'M4 12h.01', 'M4 18h.01'],
note: ['M9 18V5l11-2v13', 'M9 18a3 3 0 1 1-3-3 3 3 0 0 1 3 3z', 'M20 16a3 3 0 1 1-3-3 3 3 0 0 1 3 3z'],
shield: ['M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z', 'M9 12l2 2 4-4'],
clock: ['M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z', 'M12 7v5l3 2'],
plug: ['M9 3v5', 'M15 3v5', 'M6 8h12v3a6 6 0 0 1-12 0z', 'M12 17v4'],
open: ['M12 3l2.5 5.5L20 9l-4 4 1 6-5-3-5 3 1-6-4-4 5.5-.5z'],
cpu: ['M7 7h10v10H7z', 'M10 3v4', 'M14 3v4', 'M10 17v4', 'M14 17v4', 'M3 10h4', 'M3 14h4', 'M17 10h4', 'M17 14h4'],
eye: ['M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z', 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'],
} as const;
interface Props {
name: keyof typeof PATHS;
class?: string;
}
const { name, class: className } = Astro.props;
---
<svg
class={className}
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
focusable="false"
>
{PATHS[name].map((d) => <path d={d} />)}
</svg>
+182
View File
@@ -0,0 +1,182 @@
---
// Two harmonic ladders: a fixed 130 Hz cut lands on a different harmonic of every note,
// while Lathe's "harmonic 3" stays on harmonic 3.
const NOTES = [
{ name: 'F1', hz: 43.65 },
{ name: 'C2', hz: 65.41 },
{ name: 'C3', hz: 130.81 },
];
const LO = 30;
const HI = 1000;
const W = 560;
const PAD = 54;
const ROW = 46;
const H = NOTES.length * ROW + 40;
const x = (hz: number) => PAD + ((Math.log(hz) - Math.log(LO)) / (Math.log(HI) - Math.log(LO))) * (W - PAD - 12);
const FIXED = 130;
function rows(pick: (f0: number) => number) {
return NOTES.map((n, i) => {
const target = pick(n.hz);
const harmonics = [];
for (let k = 1; k * n.hz <= HI; k++) harmonics.push({ k, x: x(k * n.hz), hit: k === target });
return { ...n, y: 24 + i * ROW, harmonics, target };
});
}
const fixed = rows((f0) => Math.round(FIXED / f0));
const tracked = rows(() => 3);
const ticks = [50, 100, 200, 500, 1000];
---
<section id="idea" class="section idea">
<div class="container">
<div class="section-head">
<p class="eyebrow">The idea</p>
<h2>One setting, every note</h2>
<p>
Riddim and dubstep basses change patch on every switch-up and move across an octave or two. Fixed-frequency tools
then act differently on every note: the sub jumps, an EQ cut lands on another harmonic, the distortion fizzes on
one note and goes dull on the next. Anvil, Lathe, Temper and Knurl track the pitch, from the audio or from MIDI,
and put their work on the note itself.
</p>
</div>
<div class="ladders">
{
[
{ title: 'A normal EQ: −12 dB at 130 Hz', note: 'hits harmonic', data: fixed, cls: 'fixed' },
{ title: 'Lathe: −12 dB on harmonic 3', note: 'hits harmonic', data: tracked, cls: 'tracked' },
].map((panel) => (
<figure class={`plate ladder ${panel.cls}`}>
<figcaption class="label">{panel.title}</figcaption>
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${panel.title}: ${panel.data.map((r) => `${r.name} harmonic ${r.target}`).join(', ')}`}>
{ticks.map((t) => (
<g class="tick">
<line x1={x(t)} x2={x(t)} y1="6" y2={H - 22} />
<text x={x(t)} y={H - 6}>{t >= 1000 ? '1k' : t}</text>
</g>
))}
{panel.cls === 'fixed' && <line class="cut" x1={x(FIXED)} x2={x(FIXED)} y1="4" y2={H - 22} />}
{panel.data.map((r) => (
<g>
<text class="note" x="8" y={r.y + 5}>{r.name}</text>
<line class="rail" x1={PAD} x2={W - 12} y1={r.y} y2={r.y} />
{r.harmonics.map((h) => (
<circle class={h.hit ? 'hit' : 'harm'} cx={h.x} cy={r.y} r={h.hit ? 7 : 3.2} />
))}
{r.harmonics
.filter((h) => h.hit)
.map((h) => (
<text class="hit-label" x={h.x + 12} y={r.y - 8}>
h{h.k}
</text>
))}
</g>
))}
</svg>
</figure>
))
}
</div>
<p class="caption">
Dots are the harmonics of each note on a log frequency axis (Hz). The fixed cut takes the 3rd harmonic of F1, the
2nd of C2 and the fundamental of C3. The tracked cut stays on the 3rd.
</p>
</div>
</section>
<style>
.idea {
padding-bottom: 1rem;
}
.ladders {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.25rem;
}
.ladder {
margin: 0;
padding: 1.25rem 1.5rem 1rem;
}
figcaption {
margin-bottom: 0.75rem;
color: var(--text);
}
.tick line {
stroke: rgb(184 192 204 / 0.08);
}
.tick text {
fill: var(--faint);
font-family: var(--display);
font-size: 12px;
text-anchor: middle;
}
.note {
fill: var(--steel);
font-family: var(--display);
font-size: 15px;
font-weight: 600;
letter-spacing: 0.05em;
}
.rail {
stroke: rgb(184 192 204 / 0.16);
}
.harm {
fill: var(--dim);
}
.cut {
stroke: var(--accent);
stroke-width: 2;
stroke-dasharray: 5 4;
}
.fixed .hit {
fill: var(--accent);
}
.tracked .hit {
fill: var(--focus);
}
.hit {
stroke: var(--bg);
stroke-width: 2;
}
.hit-label {
font-family: var(--display);
font-size: 13px;
font-weight: 600;
}
.fixed .hit-label {
fill: #ff6b9f;
}
.tracked .hit-label {
fill: var(--focus);
}
.caption {
max-width: 720px;
margin-top: 1rem;
color: var(--faint);
font-size: 0.925rem;
}
@media (max-width: 860px) {
.ladders {
grid-template-columns: 1fr;
}
}
</style>
+132
View File
@@ -0,0 +1,132 @@
---
import { REPO } from '../data/plugins';
---
<section id="install" class="section install">
<div class="container cols">
<div class="section-head">
<p class="eyebrow">Install</p>
<h2>Into the rack in a minute</h2>
<p>
The plugins are built for FL Studio 2024.1 or newer on Windows x64. Nothing to register, no installer, no
account.
</p>
</div>
<ol class="steps" role="list">
<li class="plate">
<span class="n">01</span>
<div>
<h3>Download</h3>
<p>
Pick a plugin above, or open its release on <a href={`${REPO}/releases`}>Gitea</a>. Each ZIP has a
<code>.sha256</code> file next to it to check the download.
</p>
</div>
</li>
<li class="plate">
<span class="n">02</span>
<div>
<h3>Copy the .clap file</h3>
<p>Extract the ZIP and copy the <code>.clap</code> file into</p>
<pre><code>C:\Program Files\Common Files\CLAP\</code></pre>
</div>
</li>
<li class="plate">
<span class="n">03</span>
<div>
<h3>Rescan in FL Studio</h3>
<p>
Open <b>Options → Manage plugins</b>, enable <b>Verify plugins</b> and click <b>Find installed plugins</b>.
The plugins appear under the vendor <b>Bass Foundry</b>.
</p>
</div>
</li>
<li class="plate">
<span class="n">04</span>
<div>
<h3>Load it</h3>
<p>
Rivet goes in the Channel Rack. All the others go in mixer effect slots; each plugin’s page says where it
sits best.
</p>
</div>
</li>
</ol>
</div>
</section>
<style>
.install {
border-block: 1px solid var(--line);
background: linear-gradient(180deg, var(--bg-2), var(--bg));
}
.cols {
display: grid;
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
gap: clamp(2rem, 5vw, 4rem);
align-items: start;
}
.section-head {
position: sticky;
top: 6.5rem;
margin-bottom: 0;
}
.steps {
display: grid;
gap: 1rem;
margin: 0;
padding: 0;
list-style: none;
}
.steps li {
display: grid;
grid-template-columns: auto 1fr;
gap: 1.25rem;
padding: 1.5rem 1.75rem;
}
.n {
color: var(--accent);
font-family: var(--display);
font-size: 1.6rem;
font-weight: 700;
line-height: 1.1;
}
h3 {
margin-bottom: 0.4rem;
font-size: 1.35rem;
text-transform: uppercase;
}
b {
color: var(--text);
font-weight: 600;
}
pre {
margin: 0.7rem 0 0;
overflow-x: auto;
}
pre code {
display: block;
padding: 0.75rem 1rem;
background: var(--bg);
}
@media (max-width: 860px) {
.cols {
grid-template-columns: 1fr;
}
.section-head {
position: static;
}
}
</style>
+145
View File
@@ -0,0 +1,145 @@
---
import { Picture } from 'astro:assets';
import type { Plugin } from '../data/plugins';
import Icon from './Icon.astro';
interface Props {
plugin: Plugin;
}
const { plugin: p } = Astro.props;
---
<li class="card plate">
<a class="shot" href={`/plugins/${p.slug}/`} tabindex="-1" aria-hidden="true">
<Picture
src={p.shot}
alt=""
formats={['avif', 'webp']}
fallbackFormat="webp"
widths={[480, 720, 1040]}
sizes="(max-width: 860px) 100vw, 560px"
loading="lazy"
/>
</a>
<div class="body">
<div class="title">
<h3><a href={`/plugins/${p.slug}/`}>{p.name}</a></h3>
<span class="label">{p.role}</span>
<span class="badge">v{p.version}</span>
</div>
<p class="text">{p.description}.</p>
<dl class="meta">
<div><dt class="label">Load on</dt><dd>{p.loadOn}</dd></div>
<div><dt class="label">Latency</dt><dd>{p.latency}</dd></div>
</dl>
<div class="links">
<a class="btn btn-primary btn-sm" href={p.download}>
<Icon name="download" />Download<span class="visually-hidden"> {p.name} {p.version} for Windows</span>
</a>
<a class="btn btn-ghost btn-sm" href={`/plugins/${p.slug}/`}>Details<span class="visually-hidden"> on {p.name}</span></a>
{p.tracksNotes && <span class="badge badge-accent"><Icon name="note" />Follows the note</span>}
</div>
</div>
</li>
<style>
.card {
display: grid;
grid-template-rows: auto 1fr;
overflow: hidden;
transition:
border-color 0.2s,
transform 0.2s;
}
.card:hover {
border-color: var(--line-strong);
transform: translateY(-2px);
}
.shot {
display: block;
margin: 0.85rem 0.85rem 0;
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--bg);
aspect-ratio: 16 / 10;
}
.shot :global(img) {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top center;
transition: transform 0.4s;
}
.card:hover .shot :global(img) {
transform: scale(1.025);
}
.body {
display: grid;
align-content: start;
gap: 0.9rem;
padding: 1.25rem 1.5rem 1.5rem;
}
.title {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.4rem 0.9rem;
}
h3 {
font-size: 1.85rem;
text-transform: uppercase;
}
h3 a {
text-decoration: none;
}
.title .badge {
margin-left: auto;
}
.text {
color: #c3c7ce;
}
.meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin: 0;
padding-block: 0.75rem;
border-block: 1px solid var(--line);
}
dd {
margin: 0.15rem 0 0;
color: var(--text);
font-size: 0.95rem;
}
.links {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
}
.links .badge-accent {
margin-left: auto;
}
.badge :global(svg) {
width: 0.95em;
height: 0.95em;
}
</style>
+37
View File
@@ -0,0 +1,37 @@
---
import { PLUGINS } from '../data/plugins';
import PluginCard from './PluginCard.astro';
---
<section id="plugins" class="section">
<div class="container">
<div class="section-head">
<p class="eyebrow">The plugins</p>
<h2>Cast, shaped and finished</h2>
<p>
Each plugin is a separate product with its own version, guide and release. Every download is a ZIP with one
<code>.clap</code> file and a checksum. The editor previews below are rendered from the plugins’ own code.
</p>
</div>
<ul class="grid" role="list">
{PLUGINS.map((plugin) => <PluginCard plugin={plugin} />)}
</ul>
</div>
</section>
<style>
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.5rem;
margin: 0;
padding: 0;
list-style: none;
}
@media (max-width: 860px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>
+116
View File
@@ -0,0 +1,116 @@
---
import Icon from './Icon.astro';
const ITEMS = [
{
icon: 'note',
title: 'Follows the note',
text: 'Pitch comes from the audio with a short lookahead, or from MIDI. Settings are counted in harmonics and cycles, not in Hz.',
},
{
icon: 'cpu',
title: 'Realtime-safe',
text: 'Every audio effect has a test that fails if the audio thread allocates memory. The audio thread never waits on a lock.',
},
{
icon: 'clock',
title: 'Honest latency',
text: 'Plugins that look ahead report it to the host, so FL Studio compensates the delay and the mix stays in time.',
},
{
icon: 'shield',
title: 'Tested without a DAW',
text: 'CI loads each plugin through the CLAP ABI and checks ports, latency, state and audio at 44.1, 48 and 96 kHz.',
},
{
icon: 'eye',
title: 'Shows its work',
text: 'The editors draw what the plugin hears and what it changes: cuts, harmonics, recovered level, cycle shapes.',
},
{
icon: 'open',
title: 'Open and private',
text: 'ISC-licensed Rust. No accounts, no licence keys, no network access and no telemetry.',
},
] as const;
---
<section id="principles" class="section">
<div class="container">
<div class="section-head">
<p class="eyebrow">Principles</p>
<h2>Built like tools</h2>
<p>The same rules hold for every plugin in the suite. CI checks the testable ones on every push.</p>
</div>
<ul class="grid" role="list">
{
ITEMS.map((item) => (
<li>
<span class="icon">
<Icon name={item.icon} />
</span>
<h3>{item.title}</h3>
<p>{item.text}</p>
</li>
))
}
</ul>
</div>
</section>
<style>
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
margin: 0;
padding: 0;
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--line);
list-style: none;
}
li {
display: grid;
align-content: start;
gap: 0.6rem;
padding: 1.75rem;
background: var(--panel);
}
.icon {
display: inline-grid;
place-items: center;
width: 2.6rem;
height: 2.6rem;
margin-bottom: 0.4rem;
border: 1px solid rgb(255 1 96 / 0.35);
border-radius: 10px;
background: var(--accent-soft);
color: var(--accent);
}
.icon :global(svg) {
width: 1.3rem;
height: 1.3rem;
}
h3 {
font-size: 1.3rem;
text-transform: uppercase;
}
@media (max-width: 960px) {
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 600px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>