Initial commit
This commit is contained in:
298
website/src/app.css
Normal file
298
website/src/app.css
Normal file
@@ -0,0 +1,298 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||
"Liberation Mono", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #111;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: inherit;
|
||||
background: #f5f5f5;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
pre,
|
||||
input,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
background: #f5f5f5;
|
||||
color: #111;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
background: #f5f5f5;
|
||||
color: #111;
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 13px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 12px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
nav {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
nav h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mic-btn {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.mic-btn.mic-enabled {
|
||||
background: #e8f5e8;
|
||||
border-color: #8c8;
|
||||
color: #2a5a2a;
|
||||
}
|
||||
|
||||
.mic-btn:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 57px);
|
||||
margin-top: 57px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 160px;
|
||||
border-right: 1px solid #ccc;
|
||||
padding: 12px 0;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 57px;
|
||||
height: calc(100vh - 57px);
|
||||
overflow-y: auto;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
padding: 8px 16px 8px;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
display: block;
|
||||
padding: 4px 16px;
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.sidebar a:hover {
|
||||
color: #000;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 0 20px 40px;
|
||||
}
|
||||
|
||||
.nav-scope {
|
||||
flex: 1;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.nav-scope .scope {
|
||||
height: 32px;
|
||||
border: 1px solid #ccc;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.scope canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.content section {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content h2 {
|
||||
padding: 12px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.intro {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.repl {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.repl-editor {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.repl-editor textarea {
|
||||
display: block;
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
caret-color: #111;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hl-pre {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.hl-slash {
|
||||
color: #999;
|
||||
}
|
||||
.hl-command {
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.hl-number {
|
||||
color: #c00;
|
||||
}
|
||||
.hl-comment {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.repl-controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.repl-controls button {
|
||||
width: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@keyframes eval-flash {
|
||||
from {
|
||||
outline: 1px solid #999;
|
||||
}
|
||||
to {
|
||||
outline: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.evaluated {
|
||||
animation: eval-flash 0.3s;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-scope {
|
||||
position: fixed;
|
||||
bottom: 48px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-scope .scope {
|
||||
height: 48px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.layout {
|
||||
padding-bottom: 96px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
13
website/src/app.d.ts
vendored
Normal file
13
website/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
16
website/src/app.html
Normal file
16
website/src/app.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>doux</title>
|
||||
<meta name="description" content="Audio engine for live coding" />
|
||||
<meta name="author" content="Raphaël Forment" />
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>☁️</text></svg>" />
|
||||
<script src="/coi-serviceworker.min.js"></script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
37
website/src/content/am.md
Normal file
37
website/src/content/am.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "Amplitude Modulation"
|
||||
slug: "am"
|
||||
group: "synthesis"
|
||||
order: 109
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Amplitude modulation multiplies the signal by a modulating oscillator. The formula preserves the original signal at depth 0: <code>signal *= 1.0 + modulator * depth</code>. This creates sidebands at <code>carrier ± modulator</code> frequencies while keeping the carrier present.
|
||||
|
||||
<CommandEntry name="am" type="number" min={0} default={0} unit="Hz">
|
||||
|
||||
AM oscillator frequency in Hz. When set above 0, an LFO modulates the signal amplitude.
|
||||
|
||||
<CodeEditor code={`/freq/300/am/4/amdepth/0.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="amdepth" type="number" min={0} max={1} default={0.5}>
|
||||
|
||||
Modulation depth (0-1). At 0, the signal is unchanged. At 1, the signal varies between 0 and 2x its amplitude.
|
||||
|
||||
<CodeEditor code={`/freq/300/am/2/amdepth/1.0`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="amshape" type="string" default="sine">
|
||||
|
||||
AM LFO waveform shape. Options: `sine`, `tri`, `saw`, `square`, `sh` (sample-and-hold).
|
||||
|
||||
<CodeEditor code={`/freq/300/am/4/amdepth/0.8/amshape/square`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
69
website/src/content/bandpass.md
Normal file
69
website/src/content/bandpass.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: "Bandpass Filter"
|
||||
slug: "bandpass"
|
||||
group: "effects"
|
||||
order: 112
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
A bandpass filter attenuates frequencies outside a band around the center frequency. Each filter has its own ADSR envelope that modulates the center frequency.
|
||||
|
||||
<CommandEntry name="bpf" type="number" min={20} max={20000} unit="Hz">
|
||||
|
||||
Center frequency in Hz. Frequencies outside the band are attenuated.
|
||||
|
||||
<CodeEditor code={`/sound/saw/bpf/800`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="bpq" type="number" min={0} max={1} default={0.2}>
|
||||
|
||||
Resonance (0-1). Higher values narrow the passband.
|
||||
|
||||
<CodeEditor code={`/sound/saw/bpf/800/bpq/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="bpe" type="number" default={0}>
|
||||
|
||||
Envelope amount. Positive values sweep the center up, negative values sweep down.
|
||||
|
||||
<CodeEditor code={`/sound/saw/bpf/800/bpe/5/bpd/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="bpa" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Envelope attack time in seconds.
|
||||
|
||||
<CodeEditor code={`/sound/saw/bpf/800/bpa/.2`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="bpd" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Envelope decay time in seconds.
|
||||
|
||||
<CodeEditor code={`/sound/saw/bpf/800/bpd/.2`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="bps" type="number" min={0} max={1} default={1}>
|
||||
|
||||
Envelope sustain level (0-1).
|
||||
|
||||
<CodeEditor code={`/sound/saw/bpf/800/bpd/.2/bps/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="bpr" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Envelope release time in seconds.
|
||||
|
||||
<CodeEditor code={`/sound/saw/bpf/800/bpr/.25/duration/.1/release/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
103
website/src/content/basic.md
Normal file
103
website/src/content/basic.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: "Basic"
|
||||
slug: "basic"
|
||||
group: "sources"
|
||||
order: 0
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
These sources provide fundamental waveforms that can be combined and manipulated to create complex sounds. They are inspired by classic substractive synthesizers.
|
||||
|
||||
<CommandEntry name="sine" type="source">
|
||||
|
||||
Pure sine wave. The simplest waveform with no harmonics.
|
||||
|
||||
<CodeEditor code={`/sound/sine`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/sine/note/60`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="tri" type="source">
|
||||
|
||||
Triangle wave. The default source. Contains only odd harmonics with gentle rolloff.
|
||||
|
||||
<CodeEditor code={`/sound/tri`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/tri/note/60`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="saw" type="source">
|
||||
|
||||
Band-limited sawtooth wave. Rich in harmonics, bright and buzzy.
|
||||
|
||||
<CodeEditor code={`/sound/saw`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/note/60`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="zaw" type="source">
|
||||
|
||||
Naive sawtooth with no anti-aliasing. Cheaper but more aliasing artifacts than saw.
|
||||
|
||||
<CodeEditor code={`/sound/zaw`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/zaw/note/60`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="pulse" type="source">
|
||||
|
||||
Band-limited pulse wave. Hollow sound with only odd harmonics. Use /pw to control pulse width.
|
||||
|
||||
<CodeEditor code={`/sound/pulse`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/pulse/pw/0.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="pulze" type="source">
|
||||
|
||||
Naive pulse with no anti-aliasing. Cheaper but more aliasing artifacts than pulse.
|
||||
|
||||
<CodeEditor code={`/sound/pulze`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/pulze/pw/0.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="white" type="source">
|
||||
|
||||
White noise. Equal energy at all frequencies.
|
||||
|
||||
<CodeEditor code={`/sound/white`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/white/lpf/2000`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="pink" type="source">
|
||||
|
||||
Pink noise (1/f). Equal energy per octave, more natural sounding.
|
||||
|
||||
<CodeEditor code={`/sound/pink`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/pink/lpf/4000`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="brown" type="source">
|
||||
|
||||
Brown/red noise (1/f^2). Deep rumbling, heavily weighted toward low frequencies.
|
||||
|
||||
<CodeEditor code={`/sound/brown`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/brown/hpf/100`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
43
website/src/content/chorus.md
Normal file
43
website/src/content/chorus.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: "Chorus"
|
||||
slug: "chorus"
|
||||
group: "effects"
|
||||
order: 202
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
A rich chorus effect that adds depth and movement to any sound.
|
||||
|
||||
<CommandEntry name="chorus" type="number" min={0} default={0} unit="Hz">
|
||||
|
||||
Chorus LFO rate in Hz.
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/100/chorus/0.1`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/100/chorus/0.05/chorusdepth/0.7`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="chorusdepth" type="number" min={0} max={1} default={0.5}>
|
||||
|
||||
Chorus modulation depth (0-1).
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/200/chorus/0.5/chorusdepth/0.3`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/pulse/freq/100/chorus/0.2/chorusdepth/0.9`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="chorusdelay" type="number" min={0} default={20} unit="ms">
|
||||
|
||||
Chorus base delay time in milliseconds.
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/200/chorus/0.3/chorusdelay/20`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/200/chorus/0.3/chorusdelay/30`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
47
website/src/content/comb.md
Normal file
47
website/src/content/comb.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: "Comb Filter"
|
||||
slug: "comb"
|
||||
group: "effects"
|
||||
order: 113
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Send effect with feedback comb filter. Creates pitched resonance, metallic timbres, and Karplus-Strong plucked sounds. Tail persists after voice ends.
|
||||
|
||||
<CommandEntry name="comb" type="number" min={0} max={1} default={0}>
|
||||
|
||||
Send amount to comb filter.
|
||||
|
||||
<CodeEditor code={`/sound/white/comb/1/combfreq/110/decay/.5/sustain/0`} rows={2} />
|
||||
|
||||
Noise into a tuned comb creates plucked string sounds (Karplus-Strong).
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="combfreq" type="number" min={20} max={20000} default={220} unit="Hz">
|
||||
|
||||
Resonant frequency. All voices share the same orbit comb.
|
||||
|
||||
<CodeEditor code={`/sound/saw/comb/0.5/combfreq/880/decay/.5/sustain/0`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="combfeedback" type="number" min={0} max={0.99} default={0.9}>
|
||||
|
||||
Feedback amount. Higher values create longer resonance.
|
||||
|
||||
<CodeEditor code={`/sound/white/comb/1/combfeedback/0.99/combfreq/220/decay/.5/sustain/0`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="combdamp" type="number" min={0} max={1} default={0.1}>
|
||||
|
||||
High-frequency damping. Higher values darken the sound over time.
|
||||
|
||||
<CodeEditor code={`/sound/white/comb/1/combfeedback/0.95/combdamp/0.4/combfreq/220/decay/.5/sustain/0`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
58
website/src/content/delay.md
Normal file
58
website/src/content/delay.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: "Delay"
|
||||
slug: "delay"
|
||||
group: "effects"
|
||||
order: 203
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Stereo delay line with feedback (max 1 second at 48kHz, clamped to 0.95 feedback).
|
||||
|
||||
<CommandEntry name="delay" type="number" min={0} max={1} default={0}>
|
||||
|
||||
Send level to the delay bus.
|
||||
|
||||
<CodeEditor code={`/delay/.5/duration/.1`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="delayfeedback" type="number" min={0} max={1} default={0.5}>
|
||||
|
||||
Feedback amount (clamped to 0.95 max). Output is fed back into input.
|
||||
|
||||
<CodeEditor code={`/delay/.5/delayfeedback/.8/duration/.1`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="delaytime" type="number" min={0} default={0.25} unit="s">
|
||||
|
||||
Delay time in seconds (max ~1s at 48kHz).
|
||||
|
||||
<CodeEditor code={`/delay/.5/delaytime/.08/duration/.1`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="delaytype" type="enum" default="standard" values={["standard", "pingpong", "tape", "multitap"]}>
|
||||
|
||||
<ul>
|
||||
<li><strong>standard</strong> — Clean digital. Precise repeats.</li>
|
||||
<li><strong>pingpong</strong> — Mono in, bounces L→R→L→R.</li>
|
||||
<li><strong>tape</strong> — Each repeat darker. Analog warmth.</li>
|
||||
<li><strong>multitap</strong> — 4 taps. Feedback 0=straight, 1=triplet, between=swing.</li>
|
||||
</ul>
|
||||
|
||||
<CodeEditor code={`/sound/saw/delay/.6/dtype/std/delaytime/.15/delayfeedback/.7/d/.05`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/delay/.7/dtype/pp/delaytime/.12/delayfeedback/.8/d/.05`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/delay/.6/dtype/tape/delaytime/.2/delayfeedback/.9/d/.05`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/delay/.7/dtype/multi/delaytime/.3/delayfeedback/0/d/.05`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/delay/.7/dtype/multi/delaytime/.3/delayfeedback/1/d/.05`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
56
website/src/content/envelope.md
Normal file
56
website/src/content/envelope.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "Envelope"
|
||||
slug: "envelope"
|
||||
group: "synthesis"
|
||||
order: 102
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
The envelope parameters control the shape of the gain envelope over time. It uses a typical ADSR envelope with exponential curves:
|
||||
|
||||
- **Attack**: Ramps from 0 to full amplitude. Uses <code>x²</code> (slow start, fast finish).
|
||||
- **Decay**: Falls from full amplitude to the sustain level. Uses <code>1-(1-x)²</code> (fast drop, slow finish).
|
||||
- **Sustain**: Holds at a constant level while the note is held.
|
||||
- **Release**: Falls from the sustain level to 0 when the note ends. Uses <code>1-(1-x)²</code> (fast drop, slow finish).
|
||||
|
||||
<CommandEntry name="attack" type="number" min={0} default={0.001} unit="s">
|
||||
|
||||
The duration (seconds) of the attack phase of the gain envelope.
|
||||
|
||||
<CodeEditor code={`/attack/.1`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/attack/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="decay" type="number" min={0} default={0} unit="s">
|
||||
|
||||
The duration (seconds) of the decay phase of the gain envelope.
|
||||
|
||||
<CodeEditor code={`/decay/.1`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/decay/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="sustain" type="number" min={0} max={1} default={1}>
|
||||
|
||||
The sustain level (0-1) of the gain envelope.
|
||||
|
||||
<CodeEditor code={`/decay/.1/sustain/.2`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/decay/.1/sustain/.6`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="release" type="number" min={0} default={0.005} unit="s">
|
||||
|
||||
The duration (seconds) of the release phase of the gain envelope.
|
||||
|
||||
<CodeEditor code={`/duration/.25/release/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
43
website/src/content/flanger.md
Normal file
43
website/src/content/flanger.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: "Flanger"
|
||||
slug: "flanger"
|
||||
group: "effects"
|
||||
order: 201
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
LFO-modulated delay (0.5-10ms) with feedback and linear interpolation. Output is 50% dry, 50% wet.
|
||||
|
||||
<CommandEntry name="flanger" type="number" min={0} default={0} unit="Hz">
|
||||
|
||||
Flanger LFO rate in Hz. Creates sweeping comb filter effect with short delay modulation.
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/100/flanger/0.5`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/tri/freq/200/flanger/2/flangerdepth/0.8`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="flangerdepth" type="number" min={0} max={1} default={0.5}>
|
||||
|
||||
Flanger modulation depth (0-1). Controls delay time sweep range.
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/100/flanger/1/flangerdepth/0.3`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/pulse/freq/80/flanger/0.5/flangerdepth/0.9`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="flangerfeedback" type="number" min={0} max={0.95} default={0}>
|
||||
|
||||
Flanger feedback amount (0-0.95).
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/100/flanger/1/flangerfeedback/0.7`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/tri/freq/150/flanger/0.3/flangerdepth/0.5/flangerfeedback/0.9`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
83
website/src/content/frequency-modulation.md
Normal file
83
website/src/content/frequency-modulation.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: "Frequency Modulation"
|
||||
slug: "frequency-modulation"
|
||||
group: "synthesis"
|
||||
order: 108
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Any source can be frequency modulated. Frequency modulation (FM) is a technique where the frequency of a carrier wave is varied by an audio signal. This creates complex timbres and can produce rich harmonics, from mellow timbres to harsh digital noise.
|
||||
|
||||
<CommandEntry name="fm" type="number" min={0} default={0}>
|
||||
|
||||
The frequency modulation index. FM multiplies the gain of the modulator, thus controls the amount of FM applied.
|
||||
|
||||
<CodeEditor code={`/fm/2/note/60\n\n/fm/4/note/63\n\n/fm/2/note/67`} rows={6} />
|
||||
|
||||
<CodeEditor code={`/voice/0/fm/2/time/0\n\n/voice/0/fm/4/time/1`} rows={4} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="fmh" type="number" default={1}>
|
||||
|
||||
The harmonic ratio of the frequency modulation. fmh*freq defines the modulation frequency. As a rule of thumb, numbers close to simple ratios sound more harmonic.
|
||||
|
||||
<CodeEditor code={`/fm/2/fmh/2/`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/fm/0.5/fmh/1.5/`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/fm/0.25/fmh/3/`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="fmshape" type="string" default="sine">
|
||||
|
||||
FM modulator waveform shape. Options: `sine`, `tri`, `saw`, `square`, `sh` (sample-and-hold). Different shapes create different harmonic spectra.
|
||||
|
||||
<CodeEditor code={`/fm/2/fmshape/saw`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="fmenv" type="number" default={0}>
|
||||
|
||||
Envelope amount of frequency envelope.
|
||||
|
||||
<CodeEditor code={`/fm/4/fmenv/4/fmd/0.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="fma" type="number" min={0} default={0} unit="s">
|
||||
|
||||
The duration (seconds) of the fm envelope's attack phase.
|
||||
|
||||
<CodeEditor code={`/fm/4/fma/0.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="fmd" type="number" min={0} default={0} unit="s">
|
||||
|
||||
The duration (seconds) of the fm envelope's decay phase.
|
||||
|
||||
<CodeEditor code={`/fm/4/fmd/0.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="fms" type="number" min={0} max={1} default={1}>
|
||||
|
||||
The sustain level of the fm envelope.
|
||||
|
||||
<CodeEditor code={`/fm/4/fmd/0.25/fms/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="fmr" type="number" min={0} default={0} unit="s">
|
||||
|
||||
The duration (seconds) of the fm envelope's release phase.
|
||||
|
||||
<CodeEditor code={`/fm/4/fmr/1/release/1/duration/.1`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
21
website/src/content/ftype.md
Normal file
21
website/src/content/ftype.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: "Filter Type"
|
||||
slug: "ftype"
|
||||
group: "effects"
|
||||
order: 114
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Controls the steepness of all filters. Higher dB/octave values create sharper transitions between passed and attenuated frequencies.
|
||||
|
||||
<CommandEntry name="ftype" type="enum" default="12db" values={["12db", "24db", "48db"]}>
|
||||
|
||||
Filter slope steepness. Higher dB/octave values create sharper cutoffs. Applies to all filter types (lowpass, highpass, bandpass).
|
||||
|
||||
<CodeEditor code={`/sound/pulse/freq/50/lpf/500/lpq/0.8/lpe/4/lpd/0.2/ftype/12db/d/.5\n\n/sound/pulse/freq/50/lpf/500/lpq/0.8/lpe/4/lpd/0.2/ftype/24db/time/1/d/.5\n\n/sound/pulse/freq/50/lpf/500/lpq/0.8/lpe/4/lpd/0.2/ftype/48db/time/2/d/.5`} rows={6} />
|
||||
|
||||
</CommandEntry>
|
||||
45
website/src/content/gain.md
Normal file
45
website/src/content/gain.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: "Gain"
|
||||
slug: "gain"
|
||||
group: "synthesis"
|
||||
order: 105
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
The signal path is: oscillator → <code>gain * velocity</code> → filters → distortion → modulation → phaser/flanger → <code>envelope * postgain</code> → chorus → <code>pan</code>.
|
||||
|
||||
<CommandEntry name="gain" type="number" min={0} default={1}>
|
||||
|
||||
Pre-filter gain multiplier. Applied before filters and distortion, combined with <code>velocity</code> as <code>gain * velocity</code>.
|
||||
|
||||
<CodeEditor code={`/sound/saw/gain/0.2`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="postgain" type="number" min={0} default={1}>
|
||||
|
||||
Post-effects gain multiplier. Applied after phaser/flanger, combined with the envelope as <code>envelope * postgain</code>.
|
||||
|
||||
<CodeEditor code={`/sound/saw/postgain/0.2\n\n/sound/saw/postgain/1/time/0.25`} rows={4} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="velocity" type="number" min={0} max={1} default={1}>
|
||||
|
||||
Multiplied with <code>gain</code> before filters. Also passed as <code>accent</code> to Plaits engines.
|
||||
|
||||
<CodeEditor code={`/sound/saw/velocity/0.2\n\n/sound/saw/velocity/1/time/0.25`} rows={4} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="pan" type="number" min={0} max={1} default={0.5}>
|
||||
|
||||
Stereo position using constant-power panning: <code>left = cos(pan * π/2)</code>, <code>right = sin(pan * π/2)</code>. 0 = left, 0.5 = center, 1 = right.
|
||||
|
||||
<CodeEditor code={`/pan/0/freq/329\n\n/pan/1/freq/331`} rows={4} />
|
||||
|
||||
</CommandEntry>
|
||||
69
website/src/content/highpass.md
Normal file
69
website/src/content/highpass.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: "Highpass Filter"
|
||||
slug: "highpass"
|
||||
group: "effects"
|
||||
order: 111
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
A highpass filter attenuates frequencies below the cutoff. Each filter has its own ADSR envelope that modulates the cutoff frequency.
|
||||
|
||||
<CommandEntry name="hpf" type="number" min={20} max={20000} unit="Hz">
|
||||
|
||||
Cutoff frequency in Hz. Frequencies below this are attenuated.
|
||||
|
||||
<CodeEditor code={`/sound/saw/hpf/500`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="hpq" type="number" min={0} max={1} default={0.2}>
|
||||
|
||||
Resonance (0-1). Boosts frequencies near the cutoff.
|
||||
|
||||
<CodeEditor code={`/sound/saw/hpf/500/hpq/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="hpe" type="number" default={0}>
|
||||
|
||||
Envelope amount. Positive values sweep the cutoff up, negative values sweep down.
|
||||
|
||||
<CodeEditor code={`/sound/saw/hpf/500/hpe/5/hpd/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="hpa" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Envelope attack time in seconds.
|
||||
|
||||
<CodeEditor code={`/sound/saw/hpf/500/hpa/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="hpd" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Envelope decay time in seconds.
|
||||
|
||||
<CodeEditor code={`/sound/saw/hpf/500/hpd/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="hps" type="number" min={0} max={1} default={1}>
|
||||
|
||||
Envelope sustain level (0-1).
|
||||
|
||||
<CodeEditor code={`/sound/saw/hpf/500/hpd/.25/hps/.4`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="hpr" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Envelope release time in seconds.
|
||||
|
||||
<CodeEditor code={`/sound/saw/hpf/500/hpr/.25/duration/.1/release/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
25
website/src/content/io.md
Normal file
25
website/src/content/io.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: "Io"
|
||||
slug: "io"
|
||||
group: "sources"
|
||||
order: 2
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
This special source allows you to create a live audio input (microphone) source. Click the 'Enable Mic' button in the nav bar first. Effects chain applies normally, envelopes are applied to the input signal too.
|
||||
|
||||
<CommandEntry name="live" type="source">
|
||||
|
||||
Live audio input (microphone). Click the 'Enable Mic' button in the nav bar first. Effects chain applies normally.
|
||||
|
||||
<CodeEditor code={`/sound/live`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/live/lpf/800`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/live/verb/0.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
61
website/src/content/lofi.md
Normal file
61
website/src/content/lofi.md
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: "Lo-Fi"
|
||||
slug: "lofi"
|
||||
group: "effects"
|
||||
order: 205
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Sample rate reduction, bit crushing, and waveshaping distortion.
|
||||
|
||||
<CommandEntry name="coarse" type="number" min={1} default={1}>
|
||||
|
||||
Sample rate reduction. Holds each sample for <code>n</code> samples, creating stair-stepping and aliasing artifacts.
|
||||
|
||||
<CodeEditor code={`/penv/36/pdec/.5/coarse/8`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="crush" type="number" min={1} max={16} default={16} unit="bits">
|
||||
|
||||
Bit depth reduction. Quantizes amplitude to <code>2^(bits-1)</code> levels, creating stepping distortion.
|
||||
|
||||
<CodeEditor code={`/penv/36/pdec/.5/crush/4`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="fold" type="number" min={1} default={1}>
|
||||
|
||||
Sine-based wavefold (Serge-style). At 1, near-passthrough. At 2, one fold per peak. At 4, two folds.
|
||||
|
||||
<CodeEditor code={`/sound/sine/fold/3`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="wrap" type="number" min={1} default={1}>
|
||||
|
||||
Wrap distortion. Signal wraps around creating harsh digital artifacts.
|
||||
|
||||
<CodeEditor code={`/sound/tri/wrap/2`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="distort" type="number" min={0} default={0}>
|
||||
|
||||
Soft-clipping waveshaper using <code>(1+k)*x / (1+k*|x|)</code> where <code>k = e^amount - 1</code>. Higher values add harmonic saturation.
|
||||
|
||||
<CodeEditor code={`/sound/sine/distort/4`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="distortvol" type="number" min={0} default={1}>
|
||||
|
||||
Output gain applied after distortion to compensate for increased level.
|
||||
|
||||
<CodeEditor code={`/sound/sine/distort/4/distortvol/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
69
website/src/content/lowpass.md
Normal file
69
website/src/content/lowpass.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: "Lowpass Filter"
|
||||
slug: "lowpass"
|
||||
group: "effects"
|
||||
order: 110
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
A lowpass filter attenuates frequencies above the cutoff. Each filter has its own ADSR envelope that modulates the cutoff frequency.
|
||||
|
||||
<CommandEntry name="lpf" type="number" min={20} max={20000} unit="Hz">
|
||||
|
||||
Cutoff frequency in Hz. Frequencies above this are attenuated.
|
||||
|
||||
<CodeEditor code={`/sound/saw/lpf/200`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="lpq" type="number" min={0} max={1} default={0.2}>
|
||||
|
||||
Resonance (0-1). Boosts frequencies near the cutoff.
|
||||
|
||||
<CodeEditor code={`/sound/saw/lpf/200/lpq/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="lpe" type="number" default={0}>
|
||||
|
||||
Envelope amount. Positive values sweep the cutoff up, negative values sweep down.
|
||||
|
||||
<CodeEditor code={`/sound/saw/lpf/100/lpe/5/lpd/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="lpa" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Envelope attack time in seconds.
|
||||
|
||||
<CodeEditor code={`/sound/saw/lpf/100/lpa/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="lpd" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Envelope decay time in seconds.
|
||||
|
||||
<CodeEditor code={`/sound/saw/lpf/100/lpd/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="lps" type="number" min={0} max={1} default={1}>
|
||||
|
||||
Envelope sustain level (0-1).
|
||||
|
||||
<CodeEditor code={`/sound/saw/lpf/100/lpd/.25/lps/.4`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="lpr" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Envelope release time in seconds.
|
||||
|
||||
<CodeEditor code={`/sound/saw/lpf/100/lpr/.25/duration/.1/release/.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
63
website/src/content/oscillator.md
Normal file
63
website/src/content/oscillator.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "Oscillator"
|
||||
slug: "oscillator"
|
||||
group: "synthesis"
|
||||
order: 104
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
These parameters are dedicated to alter the nominal behavior of each oscillator. Some parameters are specific to certain oscillators, most others can be used with all oscillators.
|
||||
|
||||
<CommandEntry name="pw" type="number" min={0} max={1} default={0.5}>
|
||||
|
||||
The pulse width (between 0 and 1) of the pulse oscillator. The default is 0.5 (square wave). Only has an effect when used with <code>/sound/pulse</code> or <code>/sound/pulze</code>.
|
||||
|
||||
<CodeEditor code={`/sound/pulse/pw/.1`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="spread" type="number" min={0} max={100} default={0}>
|
||||
|
||||
Stereo unison. Adds 6 detuned voices (7 total) with stereo panning. Works with sine, tri, saw, zaw, pulse, pulze.
|
||||
|
||||
<CodeEditor code={`/sound/saw/spread/30`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
Inspired by the M8 Tracker's WavSynth, these parameters transform the oscillator phase to create new timbres from basic waveforms. They work with all basic oscillators (sine, tri, saw, zaw, pulse, pulze).
|
||||
|
||||
<CommandEntry name="size" type="number" min={0} max={256} default={0}>
|
||||
|
||||
Phase quantization steps. Creates stair-step waveforms similar to 8-bit sound chips. Set to 0 to disable, or 2-256 for increasing resolution. Lower values produce more lo-fi, chiptune-like sounds.
|
||||
|
||||
<CodeEditor code={`/sound/sine/size/8`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="mult" type="number" min={0.25} max={16} default={1}>
|
||||
|
||||
Phase multiplier that wraps the waveform multiple times per cycle. Creates hard-sync-like harmonic effects. A value of 2 doubles the frequency content, 4 quadruples it, etc.
|
||||
|
||||
<CodeEditor code={`/sound/saw/mult/4`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="warp" type="number" min={-1} max={1} default={0}>
|
||||
|
||||
Phase asymmetry using a power curve. Positive values compress the early phase and expand the late phase. Negative values do the opposite. Creates timbral variations without changing pitch.
|
||||
|
||||
<CodeEditor code={`/sound/tri/warp/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="mirror" type="number" min={0} max={1} default={0}>
|
||||
|
||||
Reflects the phase at the specified position. At 0.5, creates symmetric waveforms (a saw becomes triangle-like). Values closer to 0 or 1 create increasingly asymmetric reflections.
|
||||
|
||||
<CodeEditor code={`/sound/saw/mirror/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
53
website/src/content/phaser.md
Normal file
53
website/src/content/phaser.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: "Phaser"
|
||||
slug: "phaser"
|
||||
group: "effects"
|
||||
order: 200
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Two cascaded notch filters (offset by 282Hz) with LFO-modulated center frequency.
|
||||
|
||||
<CommandEntry name="phaser" type="number" min={0} default={0} unit="Hz">
|
||||
|
||||
Phaser LFO rate in Hz. Creates sweeping notch filter effect.
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/50/phaser/0.5`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/50/phaser/2/phaserdepth/0.9`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="phaserdepth" type="number" min={0} max={1} default={0.5}>
|
||||
|
||||
Phaser effect intensity (0-1). Controls resonance and wet/dry mix.
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/50/phaser/1/phaserdepth/0.5`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/50/phaser/0.25/phaserdepth/1.0`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="phasersweep" type="number" min={0} default={2000} unit="Hz">
|
||||
|
||||
Phaser frequency sweep range in Hz. Default is 2000 (±2000Hz sweep).
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/50/phaser/1/phasersweep/4000`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/50/phaser/0.5/phasersweep/500`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="phasercenter" type="number" min={20} max={20000} default={1000} unit="Hz">
|
||||
|
||||
Phaser center frequency in Hz. Default is 1000Hz.
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/50/phaser/1/phasercenter/500`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/50/phaser/2/phasercenter/2000`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
49
website/src/content/pitch-env.md
Normal file
49
website/src/content/pitch-env.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: "Pitch Env"
|
||||
slug: "pitch-env"
|
||||
group: "synthesis"
|
||||
order: 106
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
An ADSR envelope applied to pitch. The envelope runs with gate always on (no release phase during note). The frequency is multiplied by <code>2^(env * penv / 12)</code>. When <code>psus = 1</code>, the envelope value is offset by -1 so sustained notes return to base pitch.
|
||||
|
||||
<CommandEntry name="penv" type="number" default={0} unit="semitones">
|
||||
|
||||
Pitch envelope depth in semitones. Positive values sweep up, negative values sweep down.
|
||||
|
||||
<CodeEditor code={`/penv/24/pdec/.2`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="patt" type="number" min={0} default={0.001} unit="s">
|
||||
|
||||
Attack time. Duration to reach peak pitch offset.
|
||||
|
||||
<CodeEditor code={`/patt/.2`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="pdec" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Decay time. Duration to fall from peak to sustain level.
|
||||
|
||||
<CodeEditor code={`/pdec/.2`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="psus" type="number" min={0} max={1} default={1}>
|
||||
|
||||
Sustain level. At 1.0, the envelope returns to base pitch after decay.
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="prel" type="number" min={0} default={0.005} unit="s">
|
||||
|
||||
Release time. Not typically audible since pitch envelope gate stays on.
|
||||
|
||||
</CommandEntry>
|
||||
65
website/src/content/pitch.md
Normal file
65
website/src/content/pitch.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: "Pitch"
|
||||
slug: "pitch"
|
||||
group: "synthesis"
|
||||
order: 100
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Pitch control for all sources, including audio samples.
|
||||
|
||||
<CommandEntry name="freq" type="number" min={20} max={20000} default={330} unit="Hz">
|
||||
|
||||
The frequency of the sound. Has no effect on noise.
|
||||
|
||||
<CodeEditor code={`/freq/400`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/freq/800`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/freq/1200`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="note" type="number" min={0} max={127} unit="midi">
|
||||
|
||||
The note (midi number) that should be played.
|
||||
If both note and freq is set, freq wins.
|
||||
|
||||
<CodeEditor code={`/note/60\n\n/note/67`} rows={4} />
|
||||
|
||||
<CodeEditor code={`/note/48\n\n/note/60\n\n/note/63\n\n/note/67`} rows={8} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="speed" type="number" default={1}>
|
||||
|
||||
Multiplies with the source frequency or buffer playback speed.
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/220/speed/0.5`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/saw/freq/220/speed/1.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="detune" type="number" default={0} unit="cents">
|
||||
|
||||
Shifts the pitch by the given amount in cents. 100 cents = 1 semitone.
|
||||
|
||||
<CodeEditor code={`/freq/440/detune/50`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/freq/440/detune/-50`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="glide" type="number" min={0} default={0} unit="s">
|
||||
|
||||
Creates a pitch slide when changing the frequency of an active voice.
|
||||
Only has an effect when used with <code>voice</code>.
|
||||
|
||||
<CodeEditor code={`/voice/0/freq/220\n\n/voice/0/freq/330/glide/0.5/time/0.25`} rows={4} />
|
||||
|
||||
</CommandEntry>
|
||||
175
website/src/content/plaits.md
Normal file
175
website/src/content/plaits.md
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
title: "Complex"
|
||||
slug: "plaits"
|
||||
group: "sources"
|
||||
order: 1
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Complex oscillator engines based on Mutable Instruments Plaits. All engines share three parameters (0 to 1):
|
||||
|
||||
- **harmonics** — harmonic content, structure, detuning, etc.
|
||||
- **timbre** — brightness, tonal color, etc.
|
||||
- **morph** — smooth transitions between variations, etc.
|
||||
|
||||
Each engine interprets these differently.
|
||||
|
||||
<CommandEntry name="modal" type="source">
|
||||
|
||||
Modal resonator (physical modeling). Simulates struck/plucked resonant bodies. harmonics: structure, timbre: brightness, morph: damping/decay.
|
||||
|
||||
<CodeEditor code={`/sound/modal/note/48`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/modal/note/36/harmonics/0.8/morph/0.2`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/modal/note/60/timbre/0.7/morph/0.5/verb/0.3`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="va" type="source">
|
||||
|
||||
Virtual analog. Classic waveforms with sync and crossfading. harmonics: detuning, timbre: variable square, morph: variable saw.
|
||||
|
||||
<CodeEditor code={`/sound/va/note/36`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/va/note/48/harmonics/0.3/morph/0.8`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/va/note/36/timbre/0.2/lpf/1000`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="ws" type="source">
|
||||
|
||||
Waveshaping oscillator. Asymmetric triangle through waveshaper and wavefolder. harmonics: waveshaper shape, timbre: fold amount, morph: waveform asymmetry.
|
||||
|
||||
<CodeEditor code={`/sound/ws/note/36`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/ws/note/48/timbre/0.7/harmonics/0.5`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/ws/note/36/morph/0.8/timbre/0.9`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="fm2" type="source">
|
||||
|
||||
Two-operator FM synthesis. harmonics: frequency ratio, timbre: modulation index, morph: feedback.
|
||||
|
||||
<CodeEditor code={`/sound/fm2/note/48`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/fm2/note/60/timbre/0.5/harmonics/0.3`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/fm2/note/36/morph/0.7/timbre/0.8`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="grain" type="source">
|
||||
|
||||
Granular formant oscillator. Simulates formants through windowed sines. harmonics: formant ratio, timbre: formant frequency, morph: formant width.
|
||||
|
||||
<CodeEditor code={`/sound/grain/note/48`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/grain/note/36/timbre/0.6/harmonics/0.4`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/grain/note/60/morph/0.3/timbre/0.8`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="additive" type="source">
|
||||
|
||||
Harmonic oscillator. Additive mixture of sine harmonics. harmonics: number of bumps, timbre: prominent harmonic index, morph: bump shape.
|
||||
|
||||
<CodeEditor code={`/sound/additive/note/48`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/additive/note/36/timbre/0.5/harmonics/0.3`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/additive/note/60/morph/0.8/timbre/0.7`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="wavetable" type="source">
|
||||
|
||||
Wavetable oscillator. Four banks of 8x8 waveforms. harmonics: bank selection, timbre: row index, morph: column index.
|
||||
|
||||
<CodeEditor code={`/sound/wavetable/note/48`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/wavetable/note/36/timbre/0.5/morph/0.5`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/wavetable/note/60/harmonics/0.3/timbre/0.7/morph/0.2`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="chord" type="source">
|
||||
|
||||
Four-note chord engine. Virtual analog or wavetable chords. harmonics: chord type, timbre: inversion/transposition, morph: waveform.
|
||||
|
||||
<CodeEditor code={`/sound/chord/note/48`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/chord/note/36/harmonics/0.5`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/chord/note/48/harmonics/0.3/morph/0.7/verb/0.2`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="swarm" type="source">
|
||||
|
||||
Granular cloud of 8 enveloped sawtooth oscillators. harmonics: pitch randomization, timbre: grain density, morph: grain duration/overlap.
|
||||
|
||||
<CodeEditor code={`/sound/swarm/note/36`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/swarm/note/48/harmonics/0.5/morph/0.3`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/swarm/note/36/timbre/0.4/harmonics/0.7`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="pnoise" type="source">
|
||||
|
||||
Filtered noise. Clocked noise through multimode filter. harmonics: filter type (LP/BP/HP), timbre: clock frequency, morph: filter resonance.
|
||||
|
||||
<CodeEditor code={`/sound/pnoise/note/48`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/pnoise/note/36/harmonics/0.5/morph/0.7`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/pnoise/note/60/timbre/0.8/morph/0.9`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="kick" type="source">
|
||||
|
||||
Analog bass drum. 808-style kick. harmonics: punch, timbre: tone, morph: decay.
|
||||
|
||||
<CodeEditor code={`/sound/kick/note/36`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/kick/note/36/morph/0.3`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/kick/note/36/timbre/0.5/harmonics/0.7`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="snare" type="source">
|
||||
|
||||
Analog snare drum. harmonics: tone/noise balance, timbre: drum mode balance, morph: decay.
|
||||
|
||||
<CodeEditor code={`/sound/snare/note/48`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/snare/note/48/morph/0.5`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/snare/note/48/harmonics/0.8/timbre/0.3`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="hihat" type="source">
|
||||
|
||||
Analog hihat. 808-style metallic hihat. harmonics: metallic tone, timbre: high-pass filter, morph: decay.
|
||||
|
||||
<CodeEditor code={`/sound/hihat/note/60`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/hihat/note/60/morph/0.2`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/sound/hihat/note/60/harmonics/0.5/timbre/0.6`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
53
website/src/content/reverb.md
Normal file
53
website/src/content/reverb.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: "Reverb"
|
||||
slug: "reverb"
|
||||
group: "effects"
|
||||
order: 204
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Dattorro plate reverb with 4 input diffusers and a cross-fed stereo tank.
|
||||
|
||||
<CommandEntry name="verb" type="number" min={0} max={1} default={0}>
|
||||
|
||||
Send level to the reverb bus.
|
||||
|
||||
<CodeEditor code={`/verb/0.5/duration/.1`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="verbdecay" type="number" min={0} max={1} default={0.5}>
|
||||
|
||||
Tank feedback amount (clamped to 0.99 max). Controls tail length.
|
||||
|
||||
<CodeEditor code={`/verb/0.8/verbdecay/0.9/duration/.1`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="verbdamp" type="number" min={0} max={1} default={0.5}>
|
||||
|
||||
One-pole lowpass in the tank feedback path. Higher values darken the tail.
|
||||
|
||||
<CodeEditor code={`/verb/0.7/verbdamp/0.5/duration/.1`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="verbpredelay" type="number" min={0} max={1} default={0}>
|
||||
|
||||
Delay before the diffusers (0-1 of max ~100ms). Creates space before reverb onset.
|
||||
|
||||
<CodeEditor code={`/verb/0.6/verbpredelay/0.3/duration/.1`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="verbdiff" type="number" min={0} max={1} default={0.7}>
|
||||
|
||||
Allpass coefficients in both input and tank diffusers. Higher values smear transients.
|
||||
|
||||
<CodeEditor code={`/verb/0.7/verbdiff/0.9/duration/.1`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
37
website/src/content/rm.md
Normal file
37
website/src/content/rm.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "Ring Modulation"
|
||||
slug: "rm"
|
||||
group: "synthesis"
|
||||
order: 110
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Ring modulation is a crossfade between dry signal and full multiplication: <code>signal *= (1.0 - depth) + modulator * depth</code>. Unlike AM, ring modulation at full depth removes the carrier entirely, leaving only sum and difference frequencies at <code>carrier ± modulator</code>.
|
||||
|
||||
<CommandEntry name="rm" type="number" min={0} default={0} unit="Hz">
|
||||
|
||||
Ring modulation oscillator frequency in Hz. When set above 0, an LFO multiplies the signal.
|
||||
|
||||
<CodeEditor code={`/freq/300/rm/440/rmdepth/1.0`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="rmdepth" type="number" min={0} max={1} default={1}>
|
||||
|
||||
Modulation depth (0-1). At 0, the signal is unchanged. At 1, full ring modulation with no dry signal.
|
||||
|
||||
<CodeEditor code={`/freq/300/rm/440/rmdepth/0.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="rmshape" type="string" default="sine">
|
||||
|
||||
Ring modulation LFO waveform shape. Options: `sine`, `tri`, `saw`, `square`, `sh` (sample-and-hold).
|
||||
|
||||
<CodeEditor code={`/freq/300/rm/8/rmdepth/1/rmshape/sh`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
51
website/src/content/sample.md
Normal file
51
website/src/content/sample.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: "Sample"
|
||||
slug: "sample"
|
||||
group: "synthesis"
|
||||
order: 111
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Doux can play back audio samples organized in folders. Point to a samples directory using the <code>--samples</code> flag. Each subfolder becomes a sample bank accessible via <code>/s/folder_name</code>. Use <code>/n/</code> to index into a folder.
|
||||
|
||||
<CommandEntry name="n" type="number" min={0} default={0}>
|
||||
|
||||
Sample index within the folder. If the index exceeds the number of samples, it wraps around using modulo. Samples in a folder are indexed starting from 0.
|
||||
|
||||
<CodeEditor code={`/s/crate_rd/n/0`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/s/crate_rd/n/2`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="begin" type="number" min={0} max={1} default={0}>
|
||||
|
||||
Sample start position (0-1). 0 = beginning, 0.5 = middle, 1 = end. Only works with samples.
|
||||
|
||||
<CodeEditor code={`/s/crate_rd/n/2/begin/0.0`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/s/crate_rd/n/2/begin/0.25`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="end" type="number" min={0} max={1} default={1}>
|
||||
|
||||
Sample end position (0-1). 0 = beginning, 0.5 = middle, 1 = end. Only works with samples.
|
||||
|
||||
<CodeEditor code={`/s/crate_rd/n/2/end/0.05`} rows={2} />
|
||||
|
||||
<CodeEditor code={`/s/crate_rd/n/3/end/0.1/speed/0.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="cut" type="number" min={0}>
|
||||
|
||||
Choke group. Voices with the same cut value silence each other. Use for hi-hats where open should be cut by closed.
|
||||
|
||||
<CodeEditor code={`/s/crate_hh/n/0/cut/1\n\n/s/crate_hh/n/1/cut/1/time/.25`} rows={4} />
|
||||
|
||||
</CommandEntry>
|
||||
37
website/src/content/timing.md
Normal file
37
website/src/content/timing.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "Timing"
|
||||
slug: "timing"
|
||||
group: "synthesis"
|
||||
order: 101
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
The engine clock starts at 0 and advances with each sample. Events with <code>time</code> are scheduled and fired when the clock reaches that value. The <code>duration</code> sets how long the gate stays open before triggering release. The <code>repeat</code> reschedules the event at regular intervals.
|
||||
|
||||
<CommandEntry name="time" type="number" min={0} default={0} unit="s">
|
||||
|
||||
The time at which the voice should start. Defaults to 0.
|
||||
|
||||
<CodeEditor code={`/freq/330/time/0\n\n/freq/440/time/0.5`} rows={4} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="duration" type="number" min={0} unit="s">
|
||||
|
||||
The duration (seconds) of the gate phase. If not set, the voice will play indefinitely, until released explicitly.
|
||||
|
||||
<CodeEditor code={`/duration/.5`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="repeat" type="number" min={0} unit="s">
|
||||
|
||||
If set, the command is repeated within the given number of seconds.
|
||||
|
||||
<CodeEditor code={`/freq/330/time/0/duration/0.5/repeat/1\n\n/freq/440/time/0.5/duration/0.5/repeat/1`} rows={4} />
|
||||
|
||||
</CommandEntry>
|
||||
37
website/src/content/vibrato.md
Normal file
37
website/src/content/vibrato.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "Vibrato"
|
||||
slug: "vibrato"
|
||||
group: "synthesis"
|
||||
order: 107
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
The pitch of every oscillator can be modulated by a vibrato effect. Vibrato is a technique where the pitch of a note is modulated slightly around a central pitch, creating a shimmering effect.
|
||||
|
||||
<CommandEntry name="vib" type="number" min={0} default={0} unit="Hz">
|
||||
|
||||
Vibrato frequency (in hertz).
|
||||
|
||||
<CodeEditor code={`/vib/8`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="vibmod" type="number" min={0} default={0} unit="semitones">
|
||||
|
||||
Vibrato modulation depth (semitones).
|
||||
|
||||
<CodeEditor code={`/vib/8/vibmod/24`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="vibshape" type="string" default="sine">
|
||||
|
||||
Vibrato LFO waveform shape. Options: `sine`, `tri`, `saw`, `square`, `sh` (sample-and-hold).
|
||||
|
||||
<CodeEditor code={`/vib/4/vibmod/1/vibshape/tri`} rows={2} />
|
||||
|
||||
</CommandEntry>
|
||||
29
website/src/content/voice.md
Normal file
29
website/src/content/voice.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: "Voice"
|
||||
slug: "voice"
|
||||
group: "synthesis"
|
||||
order: 103
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
||||
import CommandEntry from '$lib/components/CommandEntry.svelte';
|
||||
</script>
|
||||
|
||||
Doux is a polyphonic synthesizer with up to 32 simultaneous voices. By default, each event allocates a new voice automatically. When a voice finishes (envelope reaches zero), it is freed and recycled. The <code>voice</code> parameter lets you take manual control over voice allocation, enabling parameter updates on active voices (e.g., pitch slides with <code>glide</code>) or retriggering with <code>reset</code>.
|
||||
|
||||
<CommandEntry name="voice" type="number" min={0}>
|
||||
|
||||
The voice index to use. If set, voice allocation will be skipped and the selected voice will be used. If the voice is still active, the sent params will update the active voice.
|
||||
|
||||
<CodeEditor code={`/voice/0/freq/220\n\n/voice/0/freq/330/time/.5`} rows={4} />
|
||||
|
||||
</CommandEntry>
|
||||
|
||||
<CommandEntry name="reset" type="boolean" default={false}>
|
||||
|
||||
Only has an effect when used together with voice. If set to 1, the selected voice will be reset, even when it's still active. This will cause envelopes to retrigger for example.
|
||||
|
||||
<CodeEditor code={`/voice/0/freq/220/attack/.1\n\n/voice/0/freq/330/time/.25/reset/1`} rows={4} />
|
||||
|
||||
</CommandEntry>
|
||||
111
website/src/lib/components/CodeEditor.svelte
Normal file
111
website/src/lib/components/CodeEditor.svelte
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { doux } from '$lib/doux';
|
||||
import { startScope, stopScope, registerActiveEditor, unregisterActiveEditor } from '$lib/scope';
|
||||
import { Play, Square } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
code?: string;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
let { code = '', rows = 3 }: Props = $props();
|
||||
|
||||
let textarea: HTMLTextAreaElement;
|
||||
let active = $state(false);
|
||||
let currentCode = $state(untrack(() => code));
|
||||
let evaluated = $state(false);
|
||||
|
||||
const resetCallback = () => {
|
||||
active = false;
|
||||
};
|
||||
|
||||
const highlight = (code: string) =>
|
||||
code
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/(\/\/.*)/g, '<span class="hl-comment">$1</span>')
|
||||
.replace(
|
||||
/(\/)([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
||||
'<span class="hl-slash">$1</span><span class="hl-command">$2</span>'
|
||||
)
|
||||
.replace(
|
||||
/(\/)(-?[0-9]*\.?[0-9]+)/g,
|
||||
'<span class="hl-slash">$1</span><span class="hl-number">$2</span>'
|
||||
) + '\n';
|
||||
|
||||
let highlighted = $derived(highlight(currentCode));
|
||||
|
||||
function flash() {
|
||||
evaluated = false;
|
||||
setTimeout(() => {
|
||||
evaluated = true;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
flash();
|
||||
await doux.ready;
|
||||
doux.evaluate({ doux: 'reset_schedule' });
|
||||
doux.evaluate({ doux: !active ? 'reset' : 'hush_endless' });
|
||||
|
||||
const blocks = currentCode.split('\n\n').filter(Boolean);
|
||||
|
||||
const msgs = await Promise.all(
|
||||
blocks.map((block) => {
|
||||
const event = doux.parsePath(block);
|
||||
return doux.prepare({ doux: 'play', ...event });
|
||||
})
|
||||
);
|
||||
|
||||
if (!active) {
|
||||
doux.evaluate({ doux: 'reset_time' });
|
||||
registerActiveEditor(resetCallback);
|
||||
active = true;
|
||||
startScope();
|
||||
}
|
||||
|
||||
msgs.forEach((msg) => doux.send(msg));
|
||||
}
|
||||
|
||||
function stop() {
|
||||
active = false;
|
||||
unregisterActiveEditor(resetCallback);
|
||||
stopScope();
|
||||
doux.hush();
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.altKey) && e.key === 'Enter') {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
const pre = textarea.previousElementSibling as HTMLPreElement;
|
||||
if (pre) pre.scrollTop = textarea.scrollTop;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="repl">
|
||||
<div class="repl-editor">
|
||||
<pre class="hl-pre" aria-hidden="true">{@html highlighted}</pre>
|
||||
<textarea
|
||||
bind:this={textarea}
|
||||
bind:value={currentCode}
|
||||
spellcheck="false"
|
||||
{rows}
|
||||
class:evaluated
|
||||
onkeydown={handleKeydown}
|
||||
onscroll={handleScroll}
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="repl-controls">
|
||||
{#if active}
|
||||
<button class="stop" onclick={stop}><Square size={16} /></button>
|
||||
{:else}
|
||||
<button class="play" onclick={run}><Play size={16} /></button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
131
website/src/lib/components/CommandEntry.svelte
Normal file
131
website/src/lib/components/CommandEntry.svelte
Normal file
@@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
type?: "number" | "boolean" | "enum" | "source";
|
||||
min?: number;
|
||||
max?: number;
|
||||
default?: number | string | boolean;
|
||||
unit?: string;
|
||||
values?: string[];
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
name,
|
||||
type,
|
||||
min,
|
||||
max,
|
||||
default: defaultValue,
|
||||
unit,
|
||||
values,
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
function formatRange(): string | null {
|
||||
if (min !== undefined && max !== undefined) {
|
||||
return `${min}–${max}`;
|
||||
}
|
||||
if (min !== undefined) {
|
||||
return `≥${min}`;
|
||||
}
|
||||
if (max !== undefined) {
|
||||
return `≤${max}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<details id={name}>
|
||||
<summary>
|
||||
<span class="name">{name}</span>
|
||||
{#if type && type !== "source"}
|
||||
<span class="meta">
|
||||
<span class="type">{type}</span>
|
||||
{#if formatRange()}
|
||||
<span class="range"
|
||||
>{formatRange()}{#if unit}
|
||||
{unit}{/if}</span
|
||||
>
|
||||
{:else if unit}
|
||||
<span class="unit">{unit}</span>
|
||||
{/if}
|
||||
{#if defaultValue !== undefined}
|
||||
<span class="default">={defaultValue}</span>
|
||||
{/if}
|
||||
{#if values}
|
||||
<span class="values">{values.join(" | ")}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</summary>
|
||||
<div class="content">
|
||||
{@render children()}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<style>
|
||||
details {
|
||||
margin: 16px 0;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
summary {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
summary::before {
|
||||
content: "▶";
|
||||
font-size: 0.7em;
|
||||
color: #999;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
details[open] summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.meta span {
|
||||
padding: 2px 6px;
|
||||
background: #eee;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.type {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.range {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.default {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 14px 14px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
114
website/src/lib/components/Nav.svelte
Normal file
114
website/src/lib/components/Nav.svelte
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { doux } from '$lib/doux';
|
||||
import { Home, FileText, LifeBuoy, Terminal } from 'lucide-svelte';
|
||||
import Scope from './Scope.svelte';
|
||||
|
||||
let micEnabled = $state(false);
|
||||
let micLoading = $state(false);
|
||||
|
||||
async function toggleMic() {
|
||||
if (micEnabled) {
|
||||
doux.disableMic();
|
||||
micEnabled = false;
|
||||
} else {
|
||||
micLoading = true;
|
||||
await doux.enableMic();
|
||||
micEnabled = true;
|
||||
micLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<a href="/" class="nav-title"><h1>Doux</h1></a>
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link"><Home size={16} /> Home</a>
|
||||
<a href="/reference" class="nav-link"><FileText size={16} /> Reference</a>
|
||||
<a href="/native" class="nav-link"><Terminal size={16} /> Native</a>
|
||||
<a href="/support" class="nav-link"><LifeBuoy size={16} /> Support</a>
|
||||
</div>
|
||||
<div class="nav-scope">
|
||||
<Scope />
|
||||
</div>
|
||||
<button
|
||||
class="mic-btn"
|
||||
class:mic-enabled={micEnabled}
|
||||
disabled={micLoading}
|
||||
onclick={toggleMic}
|
||||
>
|
||||
{micLoading ? '...' : '🎤 Microphone'}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="nav-tabs">
|
||||
<a href="/" class="nav-tab"><Home size={20} /></a>
|
||||
<a href="/reference" class="nav-tab"><FileText size={20} /></a>
|
||||
<a href="/native" class="nav-tab"><Terminal size={20} /></a>
|
||||
<a href="/support" class="nav-tab"><LifeBuoy size={20} /></a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
text-decoration: none;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.nav-title h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-tab:hover {
|
||||
color: #000;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-links {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 48px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #ccc;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
website/src/lib/components/Scope.svelte
Normal file
16
website/src/lib/components/Scope.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { initScope } from '$lib/scope';
|
||||
|
||||
let { class: className = '' }: { class?: string } = $props();
|
||||
let canvas: HTMLCanvasElement;
|
||||
|
||||
onMount(() => {
|
||||
const cleanup = initScope(canvas);
|
||||
return cleanup;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="scope {className}">
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
</div>
|
||||
122
website/src/lib/components/Sidebar.svelte
Normal file
122
website/src/lib/components/Sidebar.svelte
Normal file
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
interface NavItem {
|
||||
name: string;
|
||||
category: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
let { items }: Props = $props();
|
||||
|
||||
let expanded = $state<Record<string, boolean>>({});
|
||||
|
||||
function toggleCategory(category: string) {
|
||||
expanded[category] = !expanded[category];
|
||||
}
|
||||
|
||||
function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
const categoryNames: Record<string, string> = {
|
||||
plaits: "Complex",
|
||||
io: "Audio Input",
|
||||
am: "Amplitude Modulation",
|
||||
rm: "Ring Modulation",
|
||||
lowpass: "Lowpass Filter",
|
||||
highpass: "Highpass Filter",
|
||||
bandpass: "Bandpass Filter",
|
||||
ftype: "Filter Type",
|
||||
};
|
||||
|
||||
function formatCategory(str: string): string {
|
||||
return categoryNames[str] ?? capitalize(str);
|
||||
}
|
||||
|
||||
const grouped = $derived.by(() => {
|
||||
const groups: Record<string, Record<string, NavItem[]>> = {
|
||||
sources: {},
|
||||
synthesis: {},
|
||||
effects: {},
|
||||
};
|
||||
|
||||
for (const item of items) {
|
||||
const group = item.group;
|
||||
const category = item.category;
|
||||
if (!groups[group]) continue;
|
||||
if (!groups[group][category]) {
|
||||
groups[group][category] = [];
|
||||
}
|
||||
groups[group][category].push(item);
|
||||
}
|
||||
|
||||
return groups;
|
||||
});
|
||||
</script>
|
||||
|
||||
<aside class="sidebar">
|
||||
{#each ["sources", "synthesis", "effects"] as group (group)}
|
||||
<div class="sidebar-section">{capitalize(group)}</div>
|
||||
{#each Object.entries(grouped[group]) as [category, navItems] (category)}
|
||||
<button
|
||||
class="category-toggle"
|
||||
onclick={() => toggleCategory(category)}
|
||||
>
|
||||
{formatCategory(category)}
|
||||
</button>
|
||||
{#if expanded[category]}
|
||||
<div class="commands">
|
||||
{#each navItems as item (item.name)}
|
||||
<a href="#{item.name}" class="command-link"
|
||||
>{item.name}</a
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.sidebar-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.category-toggle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.category-toggle:hover {
|
||||
color: #000;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.commands {
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.command-link {
|
||||
display: block;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.command-link:hover {
|
||||
color: #000;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
443
website/src/lib/doux.ts
Normal file
443
website/src/lib/doux.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
import type { DouxEvent, SoundInfo, ClockMessage, DouxOptions, PreparedMessage } from './types';
|
||||
|
||||
const soundMap = new Map<string, string[]>();
|
||||
const loadedSounds = new Map<string, SoundInfo>();
|
||||
const loadingSounds = new Map<string, Promise<SoundInfo>>();
|
||||
let pcm_offset = 0;
|
||||
|
||||
const sources = [
|
||||
'triangle', 'tri', 'sine', 'sawtooth', 'saw', 'zawtooth', 'zaw',
|
||||
'pulse', 'square', 'pulze', 'zquare', 'white', 'pink', 'brown',
|
||||
'live', 'livein', 'mic'
|
||||
];
|
||||
|
||||
function githubPath(base: string, subpath = ''): string {
|
||||
if (!base.startsWith('github:')) {
|
||||
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||
}
|
||||
let [, path] = base.split('github:');
|
||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
if (path.split('/').length === 2) {
|
||||
path += '/main';
|
||||
}
|
||||
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||
}
|
||||
|
||||
async function fetchSampleMap(url: string): Promise<[Record<string, string[]>, string] | undefined> {
|
||||
if (url.startsWith('github:')) {
|
||||
url = githubPath(url, 'strudel.json');
|
||||
}
|
||||
if (url.startsWith('local:')) {
|
||||
url = `http://localhost:5432`;
|
||||
}
|
||||
if (url.startsWith('shabda:')) {
|
||||
const [, path] = url.split('shabda:');
|
||||
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||
}
|
||||
if (url.startsWith('shabda/speech')) {
|
||||
let [, path] = url.split('shabda/speech');
|
||||
path = path.startsWith('/') ? path.substring(1) : path;
|
||||
const [params, words] = path.split(':');
|
||||
let gender = 'f';
|
||||
let language = 'en-GB';
|
||||
if (params) {
|
||||
[language, gender] = params.split('/');
|
||||
}
|
||||
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
return;
|
||||
}
|
||||
const base = url.split('/').slice(0, -1).join('/');
|
||||
const json = await fetch(url)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(`error loading "${url}": ${error.message}`);
|
||||
});
|
||||
return [json, json._base || base];
|
||||
}
|
||||
|
||||
export async function douxsamples(
|
||||
sampleMap: string | Record<string, string[]>,
|
||||
baseUrl?: string
|
||||
): Promise<void> {
|
||||
if (typeof sampleMap === 'string') {
|
||||
const result = await fetchSampleMap(sampleMap);
|
||||
if (!result) return;
|
||||
const [json, base] = result;
|
||||
return douxsamples(json, base);
|
||||
}
|
||||
Object.entries(sampleMap).map(async ([key, urls]) => {
|
||||
if (key !== '_base') {
|
||||
urls = urls.map((url) => baseUrl + url);
|
||||
soundMap.set(key, urls);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const BLOCK_SIZE = 128;
|
||||
const CHANNELS = 2;
|
||||
const CLOCK_SIZE = 16;
|
||||
|
||||
// AudioWorklet processor code - runs in worklet context
|
||||
const workletCode = `
|
||||
const BLOCK_SIZE = 128;
|
||||
const CHANNELS = 2;
|
||||
const CLOCK_SIZE = 16;
|
||||
|
||||
let wasmExports = null;
|
||||
let wasmMemory = null;
|
||||
let output = null;
|
||||
let input_buffer = null;
|
||||
let event_input_ptr = 0;
|
||||
let framebuffer = null;
|
||||
let framebuffer_ptr = 0;
|
||||
let frame_ptr = 0;
|
||||
let frameIdx = 0;
|
||||
let block = 0;
|
||||
|
||||
class DouxProcessor extends AudioWorkletProcessor {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.active = true;
|
||||
this.clock_active = options.processorOptions?.clock_active || false;
|
||||
this.clockmsg = {
|
||||
clock: true,
|
||||
t0: 0,
|
||||
t1: 0,
|
||||
latency: (CLOCK_SIZE * BLOCK_SIZE) / sampleRate,
|
||||
};
|
||||
this.port.onmessage = async (e) => {
|
||||
const { wasm, evaluate, event_input, panic, writePcm } = e.data;
|
||||
if (wasm) {
|
||||
const { instance } = await WebAssembly.instantiate(wasm, {});
|
||||
wasmExports = instance.exports;
|
||||
wasmMemory = wasmExports.memory;
|
||||
wasmExports.doux_init(sampleRate);
|
||||
event_input_ptr = wasmExports.get_event_input_pointer();
|
||||
output = new Float32Array(
|
||||
wasmMemory.buffer,
|
||||
wasmExports.get_output_pointer(),
|
||||
BLOCK_SIZE * CHANNELS,
|
||||
);
|
||||
input_buffer = new Float32Array(
|
||||
wasmMemory.buffer,
|
||||
wasmExports.get_input_buffer_pointer(),
|
||||
BLOCK_SIZE * CHANNELS,
|
||||
);
|
||||
framebuffer_ptr = wasmExports.get_framebuffer_pointer();
|
||||
frame_ptr = wasmExports.get_frame_pointer();
|
||||
const framebufferLen = Math.floor((sampleRate / 60) * CHANNELS) * 4;
|
||||
framebuffer = new Float32Array(framebufferLen);
|
||||
this.port.postMessage({ ready: true, sampleRate });
|
||||
} else if (writePcm) {
|
||||
const { data, offset } = writePcm;
|
||||
const pcm_ptr = wasmExports.get_sample_buffer_pointer();
|
||||
const pcm_len = wasmExports.get_sample_buffer_len();
|
||||
const pcm = new Float32Array(wasmMemory.buffer, pcm_ptr, pcm_len);
|
||||
pcm.set(data, offset);
|
||||
this.port.postMessage({ pcmWritten: offset });
|
||||
} else if (evaluate && event_input) {
|
||||
new Uint8Array(
|
||||
wasmMemory.buffer,
|
||||
event_input_ptr,
|
||||
event_input.length,
|
||||
).set(event_input);
|
||||
wasmExports.evaluate();
|
||||
} else if (panic) {
|
||||
wasmExports.panic();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
if (wasmExports && outputs[0][0]) {
|
||||
if (input_buffer && inputs[0] && inputs[0][0]) {
|
||||
for (let i = 0; i < inputs[0][0].length; i++) {
|
||||
const offset = i * CHANNELS;
|
||||
for (let c = 0; c < CHANNELS; c++) {
|
||||
input_buffer[offset + c] = inputs[0][c]?.[i] ?? inputs[0][0][i];
|
||||
}
|
||||
}
|
||||
}
|
||||
wasmExports.dsp();
|
||||
const out = outputs[0];
|
||||
for (let i = 0; i < out[0].length; i++) {
|
||||
const offset = i * CHANNELS;
|
||||
for (let c = 0; c < CHANNELS; c++) {
|
||||
out[c][i] = output[offset + c];
|
||||
if (framebuffer) {
|
||||
framebuffer[frameIdx * CHANNELS + c] = output[offset + c];
|
||||
}
|
||||
}
|
||||
frameIdx = (frameIdx + 1) % (framebuffer.length / CHANNELS);
|
||||
}
|
||||
|
||||
block++;
|
||||
if (block % 8 === 0 && framebuffer) {
|
||||
this.port.postMessage({
|
||||
framebuffer: framebuffer.slice(),
|
||||
frame: frameIdx,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.clock_active && block % CLOCK_SIZE === 0) {
|
||||
this.clockmsg.t0 = this.clockmsg.t1;
|
||||
this.clockmsg.t1 = wasmExports.get_time();
|
||||
this.port.postMessage(this.clockmsg);
|
||||
}
|
||||
}
|
||||
return this.active;
|
||||
}
|
||||
}
|
||||
registerProcessor("doux-processor", DouxProcessor);
|
||||
`;
|
||||
|
||||
export class Doux {
|
||||
base: string;
|
||||
BLOCK_SIZE = BLOCK_SIZE;
|
||||
CHANNELS = CHANNELS;
|
||||
ready: Promise<void>;
|
||||
sampleRate = 0;
|
||||
frame: Int32Array = new Int32Array(1);
|
||||
framebuffer: Float32Array = new Float32Array(0);
|
||||
samplesReady: Promise<void> | null = null;
|
||||
|
||||
private initAudio: Promise<AudioContext>;
|
||||
private worklet: AudioWorkletNode | null = null;
|
||||
private encoder: TextEncoder | null = null;
|
||||
private micSource: MediaStreamAudioSourceNode | null = null;
|
||||
private micStream: MediaStream | null = null;
|
||||
private onTick?: (msg: ClockMessage) => void;
|
||||
|
||||
constructor(options: DouxOptions = {}) {
|
||||
this.base = options.base ?? '/';
|
||||
this.onTick = options.onTick;
|
||||
this.initAudio = new Promise((resolve) => {
|
||||
if (typeof document === 'undefined') return;
|
||||
document.addEventListener('click', async function init() {
|
||||
const ac = new AudioContext();
|
||||
await ac.resume();
|
||||
resolve(ac);
|
||||
document.removeEventListener('click', init);
|
||||
});
|
||||
});
|
||||
this.ready = this.runWorklet();
|
||||
}
|
||||
|
||||
private async initWorklet(): Promise<AudioWorkletNode> {
|
||||
const ac = await this.initAudio;
|
||||
const blob = new Blob([workletCode], { type: 'application/javascript' });
|
||||
const dataURL = URL.createObjectURL(blob);
|
||||
await ac.audioWorklet.addModule(dataURL);
|
||||
const worklet = new AudioWorkletNode(ac, 'doux-processor', {
|
||||
outputChannelCount: [CHANNELS],
|
||||
processorOptions: { clock_active: !!this.onTick }
|
||||
});
|
||||
worklet.connect(ac.destination);
|
||||
const res = await fetch(`${this.base}doux.wasm?t=${Date.now()}`);
|
||||
const wasm = await res.arrayBuffer();
|
||||
return new Promise((resolve) => {
|
||||
worklet.port.onmessage = async (e) => {
|
||||
if (e.data.ready) {
|
||||
this.sampleRate = e.data.sampleRate;
|
||||
this.frame = new Int32Array(1);
|
||||
this.frame[0] = 0;
|
||||
const framebufferLen = Math.floor((this.sampleRate / 60) * CHANNELS) * 4;
|
||||
this.framebuffer = new Float32Array(framebufferLen);
|
||||
this.samplesReady = douxsamples('https://samples.raphaelforment.fr');
|
||||
resolve(worklet);
|
||||
} else if (e.data.clock) {
|
||||
this.onTick?.(e.data);
|
||||
} else if (e.data.framebuffer) {
|
||||
this.framebuffer.set(e.data.framebuffer);
|
||||
this.frame[0] = e.data.frame;
|
||||
}
|
||||
};
|
||||
worklet.port.postMessage({ wasm });
|
||||
});
|
||||
}
|
||||
|
||||
private async runWorklet(): Promise<void> {
|
||||
const ac = await this.initAudio;
|
||||
if (ac.state !== 'running') await ac.resume();
|
||||
if (this.worklet) return;
|
||||
this.worklet = await this.initWorklet();
|
||||
}
|
||||
|
||||
parsePath(path: string): DouxEvent {
|
||||
const chunks = path
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => line.split('//')[0])
|
||||
.join('')
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
const pairs: [string, string | undefined][] = [];
|
||||
for (let i = 0; i < chunks.length; i += 2) {
|
||||
pairs.push([chunks[i].trim(), chunks[i + 1]?.trim()]);
|
||||
}
|
||||
return Object.fromEntries(pairs);
|
||||
}
|
||||
|
||||
private encodeEvent(input: string | DouxEvent): Uint8Array {
|
||||
if (!this.encoder) this.encoder = new TextEncoder();
|
||||
const str =
|
||||
typeof input === 'string'
|
||||
? input
|
||||
: Object.entries(input)
|
||||
.map(([k, v]) => `${k}/${v}`)
|
||||
.join('/');
|
||||
return this.encoder.encode(str + '\0');
|
||||
}
|
||||
|
||||
async evaluate(input: DouxEvent): Promise<void> {
|
||||
const msg = await this.prepare(input);
|
||||
return this.send(msg);
|
||||
}
|
||||
|
||||
async hush(): Promise<void> {
|
||||
await this.panic();
|
||||
const ac = await this.initAudio;
|
||||
ac.suspend();
|
||||
}
|
||||
|
||||
async resume(): Promise<void> {
|
||||
const ac = await this.initAudio;
|
||||
if (ac.state !== 'running') await ac.resume();
|
||||
}
|
||||
|
||||
async panic(): Promise<void> {
|
||||
await this.ready;
|
||||
this.worklet?.port.postMessage({ panic: true });
|
||||
}
|
||||
|
||||
async enableMic(): Promise<void> {
|
||||
await this.ready;
|
||||
const ac = await this.initAudio;
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const source = ac.createMediaStreamSource(stream);
|
||||
if (this.worklet) source.connect(this.worklet);
|
||||
this.micSource = source;
|
||||
this.micStream = stream;
|
||||
}
|
||||
|
||||
disableMic(): void {
|
||||
if (this.micSource) {
|
||||
this.micSource.disconnect();
|
||||
this.micSource = null;
|
||||
}
|
||||
if (this.micStream) {
|
||||
this.micStream.getTracks().forEach((t) => t.stop());
|
||||
this.micStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
async prepare(event: DouxEvent): Promise<PreparedMessage> {
|
||||
await this.ready;
|
||||
if (this.samplesReady) await this.samplesReady;
|
||||
await this.maybeLoadFile(event);
|
||||
const encoded = this.encodeEvent(event);
|
||||
return {
|
||||
evaluate: true,
|
||||
event_input: encoded
|
||||
};
|
||||
}
|
||||
|
||||
async send(msg: PreparedMessage): Promise<void> {
|
||||
await this.resume();
|
||||
this.worklet?.port.postMessage(msg);
|
||||
}
|
||||
|
||||
private async fetchSample(url: string): Promise<Float32Array> {
|
||||
const ac = await this.initAudio;
|
||||
const encoded = encodeURI(url);
|
||||
const buffer = await fetch(encoded)
|
||||
.then((res) => res.arrayBuffer())
|
||||
.then((buf) => ac.decodeAudioData(buf));
|
||||
return buffer.getChannelData(0);
|
||||
}
|
||||
|
||||
private async loadSound(s: string, n = 0): Promise<SoundInfo> {
|
||||
const soundKey = `${s}:${n}`;
|
||||
|
||||
if (loadedSounds.has(soundKey)) {
|
||||
return loadedSounds.get(soundKey)!;
|
||||
}
|
||||
|
||||
if (!loadingSounds.has(soundKey)) {
|
||||
const urls = soundMap.get(s);
|
||||
if (!urls) throw new Error(`sound ${s} not found in soundMap`);
|
||||
const url = urls[n % urls.length];
|
||||
|
||||
const promise = this.fetchSample(url).then(async (data) => {
|
||||
const offset = pcm_offset;
|
||||
pcm_offset += data.length;
|
||||
|
||||
await this.sendPcmData(data, offset);
|
||||
|
||||
const info: SoundInfo = {
|
||||
pcm_offset: offset,
|
||||
frames: data.length,
|
||||
channels: 1,
|
||||
freq: 65.406
|
||||
};
|
||||
loadedSounds.set(soundKey, info);
|
||||
return info;
|
||||
});
|
||||
|
||||
loadingSounds.set(soundKey, promise);
|
||||
}
|
||||
|
||||
return loadingSounds.get(soundKey)!;
|
||||
}
|
||||
|
||||
private sendPcmData(data: Float32Array, offset: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const handler = (e: MessageEvent) => {
|
||||
if (e.data.pcmWritten === offset) {
|
||||
this.worklet?.port.removeEventListener('message', handler);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this.worklet?.port.addEventListener('message', handler);
|
||||
this.worklet?.port.postMessage({ writePcm: { data, offset } });
|
||||
});
|
||||
}
|
||||
|
||||
private async maybeLoadFile(event: DouxEvent): Promise<void> {
|
||||
const s = event.s || event.sound;
|
||||
if (!s || typeof s !== 'string') return;
|
||||
if (sources.includes(s)) return;
|
||||
if (!soundMap.has(s)) return;
|
||||
|
||||
const n = typeof event.n === 'string' ? parseInt(event.n) : event.n ?? 0;
|
||||
const info = await this.loadSound(s, n);
|
||||
event.file_pcm = info.pcm_offset;
|
||||
event.file_frames = info.frames;
|
||||
event.file_channels = info.channels;
|
||||
event.file_freq = info.freq;
|
||||
}
|
||||
|
||||
async play(path: string): Promise<void> {
|
||||
await this.resume();
|
||||
if (this.samplesReady) await this.samplesReady;
|
||||
const event = this.parsePath(path);
|
||||
await this.maybeLoadFile(event);
|
||||
const encoded = this.encodeEvent(event);
|
||||
const msg = {
|
||||
evaluate: true,
|
||||
event_input: encoded
|
||||
};
|
||||
this.worklet?.port.postMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const doux = new Doux();
|
||||
|
||||
// Load default samples
|
||||
douxsamples('github:eddyflux/crate');
|
||||
132
website/src/lib/navigation.json
Normal file
132
website/src/lib/navigation.json
Normal file
@@ -0,0 +1,132 @@
|
||||
[
|
||||
{ "name": "sine", "category": "basic", "group": "sources" },
|
||||
{ "name": "tri", "category": "basic", "group": "sources" },
|
||||
{ "name": "saw", "category": "basic", "group": "sources" },
|
||||
{ "name": "zaw", "category": "basic", "group": "sources" },
|
||||
{ "name": "pulse", "category": "basic", "group": "sources" },
|
||||
{ "name": "pulze", "category": "basic", "group": "sources" },
|
||||
{ "name": "white", "category": "basic", "group": "sources" },
|
||||
{ "name": "pink", "category": "basic", "group": "sources" },
|
||||
{ "name": "brown", "category": "basic", "group": "sources" },
|
||||
{ "name": "modal", "category": "plaits", "group": "sources" },
|
||||
{ "name": "va", "category": "plaits", "group": "sources" },
|
||||
{ "name": "ws", "category": "plaits", "group": "sources" },
|
||||
{ "name": "fm2", "category": "plaits", "group": "sources" },
|
||||
{ "name": "grain", "category": "plaits", "group": "sources" },
|
||||
{ "name": "additive", "category": "plaits", "group": "sources" },
|
||||
{ "name": "wavetable", "category": "plaits", "group": "sources" },
|
||||
{ "name": "chord", "category": "plaits", "group": "sources" },
|
||||
{ "name": "swarm", "category": "plaits", "group": "sources" },
|
||||
{ "name": "pnoise", "category": "plaits", "group": "sources" },
|
||||
{ "name": "particle", "category": "plaits", "group": "sources" },
|
||||
{ "name": "string", "category": "plaits", "group": "sources" },
|
||||
{ "name": "speech", "category": "plaits", "group": "sources" },
|
||||
{ "name": "kick", "category": "plaits", "group": "sources" },
|
||||
{ "name": "snare", "category": "plaits", "group": "sources" },
|
||||
{ "name": "hihat", "category": "plaits", "group": "sources" },
|
||||
{ "name": "live", "category": "io", "group": "sources" },
|
||||
{ "name": "freq", "category": "pitch", "group": "synthesis" },
|
||||
{ "name": "note", "category": "pitch", "group": "synthesis" },
|
||||
{ "name": "speed", "category": "pitch", "group": "synthesis" },
|
||||
{ "name": "detune", "category": "pitch", "group": "synthesis" },
|
||||
{ "name": "glide", "category": "pitch", "group": "synthesis" },
|
||||
{ "name": "time", "category": "timing", "group": "synthesis" },
|
||||
{ "name": "duration", "category": "timing", "group": "synthesis" },
|
||||
{ "name": "repeat", "category": "timing", "group": "synthesis" },
|
||||
{ "name": "attack", "category": "envelope", "group": "synthesis" },
|
||||
{ "name": "decay", "category": "envelope", "group": "synthesis" },
|
||||
{ "name": "sustain", "category": "envelope", "group": "synthesis" },
|
||||
{ "name": "release", "category": "envelope", "group": "synthesis" },
|
||||
{ "name": "voice", "category": "voice", "group": "synthesis" },
|
||||
{ "name": "reset", "category": "voice", "group": "synthesis" },
|
||||
{ "name": "pw", "category": "oscillator", "group": "synthesis" },
|
||||
{ "name": "spread", "category": "oscillator", "group": "synthesis" },
|
||||
{ "name": "size", "category": "oscillator", "group": "synthesis" },
|
||||
{ "name": "mult", "category": "oscillator", "group": "synthesis" },
|
||||
{ "name": "warp", "category": "oscillator", "group": "synthesis" },
|
||||
{ "name": "mirror", "category": "oscillator", "group": "synthesis" },
|
||||
{ "name": "timbre", "category": "plaits", "group": "synthesis" },
|
||||
{ "name": "harmonics", "category": "plaits", "group": "synthesis" },
|
||||
{ "name": "morph", "category": "plaits", "group": "synthesis" },
|
||||
{ "name": "gain", "category": "gain", "group": "synthesis" },
|
||||
{ "name": "postgain", "category": "gain", "group": "synthesis" },
|
||||
{ "name": "velocity", "category": "gain", "group": "synthesis" },
|
||||
{ "name": "pan", "category": "gain", "group": "synthesis" },
|
||||
{ "name": "penv", "category": "pitch-env", "group": "synthesis" },
|
||||
{ "name": "patt", "category": "pitch-env", "group": "synthesis" },
|
||||
{ "name": "pdec", "category": "pitch-env", "group": "synthesis" },
|
||||
{ "name": "psus", "category": "pitch-env", "group": "synthesis" },
|
||||
{ "name": "prel", "category": "pitch-env", "group": "synthesis" },
|
||||
{ "name": "vib", "category": "vibrato", "group": "synthesis" },
|
||||
{ "name": "vibmod", "category": "vibrato", "group": "synthesis" },
|
||||
{ "name": "vibshape", "category": "vibrato", "group": "synthesis" },
|
||||
{ "name": "fmh", "category": "fm", "group": "synthesis" },
|
||||
{ "name": "fm", "category": "fm", "group": "synthesis" },
|
||||
{ "name": "fmshape", "category": "fm", "group": "synthesis" },
|
||||
{ "name": "fmenv", "category": "fm", "group": "synthesis" },
|
||||
{ "name": "fma", "category": "fm", "group": "synthesis" },
|
||||
{ "name": "fmd", "category": "fm", "group": "synthesis" },
|
||||
{ "name": "fms", "category": "fm", "group": "synthesis" },
|
||||
{ "name": "fmr", "category": "fm", "group": "synthesis" },
|
||||
{ "name": "am", "category": "am", "group": "synthesis" },
|
||||
{ "name": "amdepth", "category": "am", "group": "synthesis" },
|
||||
{ "name": "amshape", "category": "am", "group": "synthesis" },
|
||||
{ "name": "rm", "category": "rm", "group": "synthesis" },
|
||||
{ "name": "rmdepth", "category": "rm", "group": "synthesis" },
|
||||
{ "name": "rmshape", "category": "rm", "group": "synthesis" },
|
||||
{ "name": "n", "category": "sample", "group": "synthesis" },
|
||||
{ "name": "begin", "category": "sample", "group": "synthesis" },
|
||||
{ "name": "end", "category": "sample", "group": "synthesis" },
|
||||
{ "name": "cut", "category": "sample", "group": "synthesis" },
|
||||
{ "name": "lpf", "category": "lowpass", "group": "effects" },
|
||||
{ "name": "lpq", "category": "lowpass", "group": "effects" },
|
||||
{ "name": "lpe", "category": "lowpass", "group": "effects" },
|
||||
{ "name": "lpa", "category": "lowpass", "group": "effects" },
|
||||
{ "name": "lpd", "category": "lowpass", "group": "effects" },
|
||||
{ "name": "lps", "category": "lowpass", "group": "effects" },
|
||||
{ "name": "lpr", "category": "lowpass", "group": "effects" },
|
||||
{ "name": "hpf", "category": "highpass", "group": "effects" },
|
||||
{ "name": "hpq", "category": "highpass", "group": "effects" },
|
||||
{ "name": "hpe", "category": "highpass", "group": "effects" },
|
||||
{ "name": "hpa", "category": "highpass", "group": "effects" },
|
||||
{ "name": "hpd", "category": "highpass", "group": "effects" },
|
||||
{ "name": "hps", "category": "highpass", "group": "effects" },
|
||||
{ "name": "hpr", "category": "highpass", "group": "effects" },
|
||||
{ "name": "bpf", "category": "bandpass", "group": "effects" },
|
||||
{ "name": "bpq", "category": "bandpass", "group": "effects" },
|
||||
{ "name": "bpe", "category": "bandpass", "group": "effects" },
|
||||
{ "name": "bpa", "category": "bandpass", "group": "effects" },
|
||||
{ "name": "bpd", "category": "bandpass", "group": "effects" },
|
||||
{ "name": "bps", "category": "bandpass", "group": "effects" },
|
||||
{ "name": "bpr", "category": "bandpass", "group": "effects" },
|
||||
{ "name": "comb", "category": "comb-filter", "group": "effects" },
|
||||
{ "name": "combfreq", "category": "comb-filter", "group": "effects" },
|
||||
{ "name": "combfeedback", "category": "comb-filter", "group": "effects" },
|
||||
{ "name": "combdamp", "category": "comb-filter", "group": "effects" },
|
||||
{ "name": "ftype", "category": "ftype", "group": "effects" },
|
||||
{ "name": "phaser", "category": "phaser", "group": "effects" },
|
||||
{ "name": "phaserdepth", "category": "phaser", "group": "effects" },
|
||||
{ "name": "phasersweep", "category": "phaser", "group": "effects" },
|
||||
{ "name": "phasercenter", "category": "phaser", "group": "effects" },
|
||||
{ "name": "flanger", "category": "flanger", "group": "effects" },
|
||||
{ "name": "flangerdepth", "category": "flanger", "group": "effects" },
|
||||
{ "name": "flangerfeedback", "category": "flanger", "group": "effects" },
|
||||
{ "name": "chorus", "category": "chorus", "group": "effects" },
|
||||
{ "name": "chorusdepth", "category": "chorus", "group": "effects" },
|
||||
{ "name": "chorusdelay", "category": "chorus", "group": "effects" },
|
||||
{ "name": "delay", "category": "delay", "group": "effects" },
|
||||
{ "name": "delayfeedback", "category": "delay", "group": "effects" },
|
||||
{ "name": "delaytime", "category": "delay", "group": "effects" },
|
||||
{ "name": "delaytype", "category": "delay", "group": "effects" },
|
||||
{ "name": "verb", "category": "reverb", "group": "effects" },
|
||||
{ "name": "verbdecay", "category": "reverb", "group": "effects" },
|
||||
{ "name": "verbdamp", "category": "reverb", "group": "effects" },
|
||||
{ "name": "verbpredelay", "category": "reverb", "group": "effects" },
|
||||
{ "name": "verbdiff", "category": "reverb", "group": "effects" },
|
||||
{ "name": "coarse", "category": "lofi", "group": "effects" },
|
||||
{ "name": "crush", "category": "lofi", "group": "effects" },
|
||||
{ "name": "fold", "category": "lofi", "group": "effects" },
|
||||
{ "name": "wrap", "category": "lofi", "group": "effects" },
|
||||
{ "name": "distort", "category": "lofi", "group": "effects" },
|
||||
{ "name": "distortvol", "category": "lofi", "group": "effects" }
|
||||
]
|
||||
99
website/src/lib/scope.ts
Normal file
99
website/src/lib/scope.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { doux } from './doux';
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let raf: number | null = null;
|
||||
|
||||
const lerp = (v: number, min: number, max: number) => v * (max - min) + min;
|
||||
const invLerp = (v: number, min: number, max: number) => (v - min) / (max - min);
|
||||
const remap = (v: number, vmin: number, vmax: number, omin: number, omax: number) =>
|
||||
lerp(invLerp(v, vmin, vmax), omin, omax);
|
||||
|
||||
function drawBuffer(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
samples: Float32Array,
|
||||
channels: number,
|
||||
channel: number,
|
||||
ampMin: number,
|
||||
ampMax: number
|
||||
) {
|
||||
const lineWidth = 2;
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = 'black';
|
||||
const perChannel = samples.length / channels / 2;
|
||||
const pingbuffer = doux.frame[0] > samples.length / 2;
|
||||
const s0 = pingbuffer ? 0 : perChannel;
|
||||
const s1 = pingbuffer ? perChannel : perChannel * 2;
|
||||
const px0 = ctx.lineWidth;
|
||||
const px1 = ctx.canvas.width - ctx.lineWidth;
|
||||
const py0 = ctx.lineWidth;
|
||||
const py1 = ctx.canvas.height - ctx.lineWidth;
|
||||
ctx.beginPath();
|
||||
for (let px = 1; px <= ctx.canvas.width; px++) {
|
||||
const si = remap(px, px0, px1, s0, s1);
|
||||
const idx = Math.floor(si) * channels + channel;
|
||||
const amp = samples[idx];
|
||||
if (amp >= 1) ctx.strokeStyle = 'red';
|
||||
const py = remap(amp, ampMin, ampMax, py1, py0);
|
||||
px === 1 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawScope() {
|
||||
if (!ctx) return;
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
for (let c = 0; c < doux.CHANNELS; c++) {
|
||||
drawBuffer(ctx, doux.framebuffer, doux.CHANNELS, c, -1, 1);
|
||||
}
|
||||
raf = requestAnimationFrame(drawScope);
|
||||
}
|
||||
|
||||
export function initScope(canvas: HTMLCanvasElement) {
|
||||
function resize() {
|
||||
canvas.width = canvas.clientWidth * devicePixelRatio;
|
||||
canvas.height = canvas.clientHeight * devicePixelRatio;
|
||||
}
|
||||
resize();
|
||||
ctx = canvas.getContext('2d');
|
||||
const observer = new ResizeObserver(resize);
|
||||
observer.observe(canvas);
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
export function startScope() {
|
||||
if (!raf && ctx) {
|
||||
drawScope();
|
||||
}
|
||||
}
|
||||
|
||||
export function stopScope() {
|
||||
if (raf) {
|
||||
cancelAnimationFrame(raf);
|
||||
raf = null;
|
||||
}
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
let activeResetCallback: (() => void) | null = null;
|
||||
|
||||
export function registerActiveEditor(resetCallback: () => void) {
|
||||
if (activeResetCallback && activeResetCallback !== resetCallback) {
|
||||
activeResetCallback();
|
||||
}
|
||||
activeResetCallback = resetCallback;
|
||||
}
|
||||
|
||||
export function unregisterActiveEditor(resetCallback: () => void) {
|
||||
if (activeResetCallback === resetCallback) {
|
||||
activeResetCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resetActiveEditor() {
|
||||
if (activeResetCallback) {
|
||||
activeResetCallback();
|
||||
activeResetCallback = null;
|
||||
}
|
||||
}
|
||||
37
website/src/lib/types.ts
Normal file
37
website/src/lib/types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface DouxEvent {
|
||||
doux?: string;
|
||||
s?: string;
|
||||
sound?: string;
|
||||
n?: string | number;
|
||||
freq?: number;
|
||||
wave?: string;
|
||||
file_pcm?: number;
|
||||
file_frames?: number;
|
||||
file_channels?: number;
|
||||
file_freq?: number;
|
||||
[key: string]: string | number | undefined;
|
||||
}
|
||||
|
||||
export interface SoundInfo {
|
||||
pcm_offset: number;
|
||||
frames: number;
|
||||
channels: number;
|
||||
freq: number;
|
||||
}
|
||||
|
||||
export interface ClockMessage {
|
||||
clock: boolean;
|
||||
t0: number;
|
||||
t1: number;
|
||||
latency: number;
|
||||
}
|
||||
|
||||
export interface DouxOptions {
|
||||
onTick?: (msg: ClockMessage) => void;
|
||||
base?: string;
|
||||
}
|
||||
|
||||
export interface PreparedMessage {
|
||||
evaluate: boolean;
|
||||
event_input: Uint8Array;
|
||||
}
|
||||
2
website/src/routes/+layout.js
Normal file
2
website/src/routes/+layout.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export const prerender = true;
|
||||
export const trailingSlash = 'always';
|
||||
4
website/src/routes/+layout.server.ts
Normal file
4
website/src/routes/+layout.server.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Layout doesn't need to load content - it's loaded by +page.ts
|
||||
export function load() {
|
||||
return {};
|
||||
}
|
||||
23
website/src/routes/+layout.svelte
Normal file
23
website/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import "../app.css";
|
||||
import Nav from "$lib/components/Nav.svelte";
|
||||
import { doux } from "$lib/doux";
|
||||
import { stopScope, resetActiveEditor } from "$lib/scope";
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
resetActiveEditor();
|
||||
stopScope();
|
||||
doux.hush();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<Nav />
|
||||
<div class="layout">
|
||||
{@render children()}
|
||||
</div>
|
||||
185
website/src/routes/+page.svelte
Normal file
185
website/src/routes/+page.svelte
Normal file
@@ -0,0 +1,185 @@
|
||||
<script lang="ts">
|
||||
import CodeEditor from "$lib/components/CodeEditor.svelte";
|
||||
</script>
|
||||
|
||||
<main class="tutorial">
|
||||
<section class="intro">
|
||||
<p>
|
||||
A monolithic audio engine for live coding ported from C to Rust by <a
|
||||
href="https://raphaelforment.fr">BuboBubo</a
|
||||
>. The initial project is called
|
||||
<a href="https://dough.strudel.cc/">Dough</a>, designed by
|
||||
<a href="https://eddyflux.cc/">Felix Roos</a>
|
||||
and al. Doux can run in a web browser via
|
||||
<a href="https://webassembly.org/">WebAssembly</a>, or natively
|
||||
using an an OSC server and/or a REPL. Doux is an opinionated, fixed
|
||||
path, semi-modular synth that is remote-controlled via messages. It
|
||||
was designed to be used with
|
||||
<a href="https://strudel.cc">Strudel</a> and
|
||||
<a href="https://tidalcycles.org">TidalCycles</a>.
|
||||
<b
|
||||
>This fork is a bit special: it adapts and specialize the engine
|
||||
for integration with
|
||||
<a href="https://sova.livecoding.fr">Sova</a>, a live coding
|
||||
environment built in Rust</b
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
<b>Important note</b>: this project is AGPL 3.0 licensed. We
|
||||
encourage you to support the development of the original version
|
||||
through the
|
||||
<a href="https://opencollective.com/tidalcycles"
|
||||
>TidalCycles Open Collective</a
|
||||
>. See the license page for more information.
|
||||
</p>
|
||||
<a href="/support" class="support-link">License & Support</a>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Getting Started</h2>
|
||||
<p>
|
||||
Click anywhere on the page to start the audio context. Then click
|
||||
inside a code block and press <code>Ctrl+Enter</code> to run it, or
|
||||
<code>Escape</code> to stop. The easiest way to start is just to
|
||||
specify a <code>sound</code> to use:
|
||||
</p>
|
||||
<CodeEditor code={`/sound/sine`} rows={2} />
|
||||
<p>
|
||||
You can set the pitch with the <code>/note</code> parameter (MIDI note
|
||||
numbers):
|
||||
</p>
|
||||
<CodeEditor code={`/sound/sine/note/64`} rows={2} />
|
||||
<p>
|
||||
Or use frequency directly with <code>/freq</code>:
|
||||
</p>
|
||||
<CodeEditor code={`/sound/sine/freq/330`} rows={2} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Omitting parameters</h2>
|
||||
<p>
|
||||
It is possible to omit a large number of parameters or to
|
||||
under-specify a voice. Doux has preconfigured defaults for most of
|
||||
the core parameters.
|
||||
</p>
|
||||
<CodeEditor code={`/freq/330`} rows={2} />
|
||||
<CodeEditor code={`/spread/5/decay/0.5`} rows={2} />
|
||||
<p>
|
||||
The default voice is always a <code>tri</code> with sensible envelope
|
||||
defaults.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Sound Sources</h2>
|
||||
<p>
|
||||
There are multiple sound sources you can use, detailed in the
|
||||
reference. You can also import your own audio samples or use a live
|
||||
input as a source.
|
||||
</p>
|
||||
<CodeEditor code={`/sound/tri`} rows={2} />
|
||||
<CodeEditor code={`/sound/saw`} rows={2} />
|
||||
<CodeEditor code={`/sound/pulse/pw/0.25`} rows={2} />
|
||||
<CodeEditor code={`/sound/white`} rows={2} />
|
||||
<CodeEditor code={`/sound/pink`} rows={2} />
|
||||
<CodeEditor code={`/sound/analog`} rows={2} />
|
||||
<p>Turn on your microphone (and beware of feedback!):</p>
|
||||
<CodeEditor code={`/sound/live/gain/2`} rows={2} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Envelopes</h2>
|
||||
<p>
|
||||
The amplitude envelope controls how the sound fades in and out. It
|
||||
uses the classic ADSR model: attack, decay, sustain, release.
|
||||
</p>
|
||||
<CodeEditor
|
||||
code={`/sound/saw/attack/0.5/decay/0.2/sustain/0.0/release/1`}
|
||||
rows={2}
|
||||
/>
|
||||
<p>
|
||||
<code>/attack</code> is the time (in seconds) to reach full volume.<br
|
||||
/>
|
||||
<code>/decay</code> is the time to fall to the sustain level.<br />
|
||||
<code>/sustain</code> is the level held while the note is on (0-1).<br
|
||||
/>
|
||||
<code>/release</code> is the time to fade to silence after note off.
|
||||
</p>
|
||||
<p>Try changing each parameter to hear how it affects the sound.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Filters</h2>
|
||||
<p>
|
||||
Most of the default sources are producing very rich timbres, full of
|
||||
harmonics. You are likely to play a lot with filters to remove some
|
||||
components of the spectrum. Doux has all the basic filters needed:
|
||||
</p>
|
||||
<CodeEditor code={`/sound/saw/lpf/800`} rows={2} />
|
||||
<p>
|
||||
All the basic filters come with a control over resonance <code
|
||||
>/..q</code
|
||||
>:
|
||||
</p>
|
||||
<CodeEditor code={`/sound/saw/lpf/800/lpq/10`} rows={2} />
|
||||
<CodeEditor code={`/sound/white/hpf/2000`} rows={2} />
|
||||
<CodeEditor code={`/sound/white/bpf/1000/bpq/20`} rows={2} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Effects</h2>
|
||||
<p>Doux includes several effects. Here's a sound with reverb:</p>
|
||||
<CodeEditor code={`/sound/saw/note/48/reverb/0.8/decay/0.2`} rows={2} />
|
||||
<p>And now another sound with a delay:</p>
|
||||
<CodeEditor
|
||||
code={`/sound/saw/note/60/delay/0.5/delaytime/0.3/delayfeedback/0.6/decay/0.5`}
|
||||
rows={2}
|
||||
/>
|
||||
<p>If you stack up effects, it can become quite crazy:</p>
|
||||
<CodeEditor
|
||||
code={`/sound/saw/note/48/delay/0.5/delaytime/0.1/delayfeedback/0.8/decay/1.5/fanger/0.5/coarse/12/phaser/0.9/gain/1/phaserfeedback/0.9`}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<p>
|
||||
Note that the order in which the effects is applied is fixed by
|
||||
default!
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.tutorial {
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 20px 60px;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tutorial h2 {
|
||||
margin-top: 2.5em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.intro {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.support-link {
|
||||
display: block;
|
||||
margin: 1.5em 0;
|
||||
padding: 12px 24px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ccc;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.support-link:hover {
|
||||
border-color: #999;
|
||||
background: #eee;
|
||||
}
|
||||
</style>
|
||||
3
website/src/routes/+page.ts
Normal file
3
website/src/routes/+page.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function load() {
|
||||
return {};
|
||||
}
|
||||
167
website/src/routes/native/+page.svelte
Normal file
167
website/src/routes/native/+page.svelte
Normal file
@@ -0,0 +1,167 @@
|
||||
<main class="native-page">
|
||||
<h1>Native</h1>
|
||||
|
||||
<p>
|
||||
The web version is really fun if you want to build a website that uses a
|
||||
robust open source audio engine. However, Doux was ported from C mostly
|
||||
to be used natively. You will need to compile it yourself or use it
|
||||
directly as it is integrated in <a href="https://sova.livecoding.fr"
|
||||
>Sova</a
|
||||
>. Read the instructions in the repo to learn how to compile it. You
|
||||
will need the Rust toolchain that you can get using
|
||||
<a href="https://rustup.rs/">Rustup</a>.
|
||||
</p>
|
||||
<br />
|
||||
<section>
|
||||
<h2>Binaries</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<code>doux</code> runs as an OSC server, listening on a configurable
|
||||
port (default 57120). Use it with TidalCycles, Strudel, or Sova.
|
||||
It will listen to any incoming message until it is killed.
|
||||
</li>
|
||||
<br />
|
||||
<li>
|
||||
REPL with readline support and command history saved to
|
||||
<code>~/.doux_history</code>. Built-in commands:
|
||||
<code>.quit</code>, <code>.reset</code>, <code>.hush</code>,
|
||||
<code>.panic</code>, <code>.voices</code>, <code>.time</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Flags</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Flag</th>
|
||||
<th>Short</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>--samples</code></td>
|
||||
<td><code>-s</code></td>
|
||||
<td>Directory containing audio samples</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--list-devices</code></td>
|
||||
<td></td>
|
||||
<td>List audio devices and exit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--input</code></td>
|
||||
<td><code>-i</code></td>
|
||||
<td>Input device (name or index)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--output</code></td>
|
||||
<td><code>-o</code></td>
|
||||
<td>Output device (name or index)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--channels</code></td>
|
||||
<td></td>
|
||||
<td>Number of output channels (default: 2)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--buffer-size</code></td>
|
||||
<td><code>-b</code></td>
|
||||
<td>Audio buffer size in samples (default: system)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--port</code></td>
|
||||
<td><code>-p</code></td>
|
||||
<td>OSC port, <code>doux</code> only (default: 57120)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Multichannel</h2>
|
||||
<p>
|
||||
The <code>--channels</code> flag enables multichannel output beyond
|
||||
stereo. The number of channels is clamped to your device's maximum
|
||||
supported count. The <code>orbit</code> parameter controls output routing.
|
||||
With N output channels, there are N/2 stereo pairs. Voices on orbit 0
|
||||
output to channels 0–1, orbit 1 to channels 2–3, and so on. When the
|
||||
orbit exceeds the number of pairs, it wraps around via modulo. Effects
|
||||
(delay, reverb) follow the same routing: each orbit's effect bus outputs
|
||||
to its corresponding stereo pair.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Buffer size</h2>
|
||||
<p>
|
||||
The <code>--buffer-size</code> flag controls audio latency. Lower values
|
||||
mean less latency but require more CPU. Common values: 64, 128, 256, 512,
|
||||
1024. At 48kHz, 256 samples gives ~5.3ms latency. If unset, the system
|
||||
chooses a default. Use lower values for live performance, higher values
|
||||
for stability.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Sample loading</h2>
|
||||
<p>
|
||||
The <code>--samples</code> flag allows you to (lazy-)load audio
|
||||
samples to play with. The engine expects a folder containing folders
|
||||
of audio samples. Samples will be available using the folder name.
|
||||
You can index into a folder by using the <code>/n/</code> command. Check
|
||||
the reference to learn more about this.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.native-page {
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 20px 60px;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.native-page h1 {
|
||||
font-size: 18px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.native-page h2 {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.native-page section:first-of-type h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f5f5f5;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
</style>
|
||||
58
website/src/routes/reference/+page.svelte
Normal file
58
website/src/routes/reference/+page.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from "svelte";
|
||||
import Sidebar from "$lib/components/Sidebar.svelte";
|
||||
|
||||
interface Category {
|
||||
path: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
group: string;
|
||||
order: number;
|
||||
component: Component;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
name: string;
|
||||
category: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
categories: Category[];
|
||||
navigation: NavItem[];
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Sidebar items={data.navigation} />
|
||||
|
||||
<main class="content">
|
||||
{#each data.categories as category}
|
||||
{@const Component = category.component}
|
||||
<section id={category.slug} class="category">
|
||||
<h2 class="category-title">{category.title}</h2>
|
||||
<Component />
|
||||
</section>
|
||||
{/each}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.category-title {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.category :global(h2:not(.category-title)) {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
margin: 24px 0 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
</style>
|
||||
41
website/src/routes/reference/+page.ts
Normal file
41
website/src/routes/reference/+page.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Component } from "svelte";
|
||||
import navigation from "$lib/navigation.json";
|
||||
|
||||
const contentModules = import.meta.glob("/src/content/*.md", { eager: true });
|
||||
|
||||
interface ContentMetadata {
|
||||
title: string;
|
||||
slug: string;
|
||||
group: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface ContentModule {
|
||||
metadata: ContentMetadata;
|
||||
default: Component;
|
||||
}
|
||||
|
||||
export function load() {
|
||||
const categories = Object.entries(contentModules).map(([path, module]) => {
|
||||
const mod = module as ContentModule;
|
||||
return {
|
||||
path,
|
||||
...mod.metadata,
|
||||
component: mod.default,
|
||||
};
|
||||
});
|
||||
|
||||
const groupOrder = { sources: 0, synthesis: 1, effects: 2 };
|
||||
categories.sort((a, b) => {
|
||||
const groupDiff =
|
||||
(groupOrder[a.group as keyof typeof groupOrder] ?? 99) -
|
||||
(groupOrder[b.group as keyof typeof groupOrder] ?? 99);
|
||||
if (groupDiff !== 0) return groupDiff;
|
||||
return a.order - b.order;
|
||||
});
|
||||
|
||||
return {
|
||||
categories,
|
||||
navigation,
|
||||
};
|
||||
}
|
||||
105
website/src/routes/support/+page.svelte
Normal file
105
website/src/routes/support/+page.svelte
Normal file
@@ -0,0 +1,105 @@
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<main class="support-page">
|
||||
<h1>License & Support</h1>
|
||||
|
||||
<section>
|
||||
<p>
|
||||
Doux is free & open source software under the
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
>GNU Affero General Public License</a
|
||||
> (AGPL-3.0). This means you are free to use, modify, and distribute Doux,
|
||||
as long as any modifications are also released under the same license.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p>
|
||||
The original Dough project is part of the <a
|
||||
href="https://tidalcycles.org">TidalCycles</a
|
||||
> ecosystem. If you find Doux useful, consider supporting Dough instead
|
||||
through Open Collective. Your contributions help maintain and improve
|
||||
Dough and related live coding projects.
|
||||
</p>
|
||||
<a href="https://opencollective.com/tidalcycles" class="donate-link">
|
||||
Support on Open Collective
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Credits</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Doux is a Rust port of <a href="https://dough.strudel.cc/"
|
||||
>Dough</a
|
||||
>, originally written in C by
|
||||
<a href="https://github.com/felixroos">Felix Roos</a>. This
|
||||
project is AGPL 3.0 licensed, just like Doux!
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/sourcebox/mi-plaits-dsp-rs"
|
||||
>mi-plaits-dsp-rs</a
|
||||
>
|
||||
is a Rust port of the code used by the
|
||||
<a
|
||||
href="https://pichenettes.github.io/mutable-instruments-documentation/modules/plaits/"
|
||||
>Mutable Instruments Plaits</a
|
||||
>.
|
||||
<ul>
|
||||
<li>
|
||||
<b>Author:</b> Oliver Rockstedt
|
||||
<a href="mailto:info@sourcebox.de">info@sourcebox.de</a>
|
||||
</li>
|
||||
<li>
|
||||
<b>Original author:</b> Emilie Gillet
|
||||
<a href="mailto:emilie.o.gillet@gmail.com"
|
||||
>emilie.o.gillet@gmail.com</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.support-page {
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 20px 60px;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.support-page h1 {
|
||||
font-size: 18px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.support-page h2 {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.support-page section:first-of-type h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.donate-link {
|
||||
display: inline-block;
|
||||
margin-top: 1em;
|
||||
padding: 12px 24px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ccc;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.donate-link:hover {
|
||||
background: #eee;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user