diff --git a/packages/core/src/repl.js b/packages/core/src/repl.js index 4b0a7ac..b055eb3 100644 --- a/packages/core/src/repl.js +++ b/packages/core/src/repl.js @@ -2,8 +2,9 @@ import { AudioView } from "./audioview.js"; import * as api from "./node.js"; export class SalatRepl { - constructor() { + constructor({ onToggle } = {}) { this.audio = new AudioView(); + this.onToggle = onToggle; Object.assign(globalThis, api); } evaluate(code) { @@ -14,8 +15,10 @@ export class SalatRepl { await this.audio.init(); } this.audio.updateGraph(node); + this.onToggle?.(true); } stop() { this.audio.stop(); + this.onToggle?.(false); } } diff --git a/packages/graphviz/src/graphviz.js b/packages/graphviz/src/graphviz.js index f0677d7..5c48b0f 100644 --- a/packages/graphviz/src/graphviz.js +++ b/packages/graphviz/src/graphviz.js @@ -5,7 +5,12 @@ import { Graphviz } from "@hpcc-js/wasm"; const graphvizLoaded = Graphviz.load(); Node.prototype.render = async function (container, options = {}) { - const { dagify = false, resolveModules = false } = options; + const { + dagify = false, + resolveModules = false, + rankdir = "TB", + size = 0, + } = options; let node = this; resolveModules && node.resolveModules(); if (dagify) { @@ -18,12 +23,16 @@ Node.prototype.render = async function (container, options = {}) { let edges = []; const color = "teal"; const fontcolor = "teal"; + const fontsize = 10; + const fontname = "monospace"; for (let id in nodes) { nodes[id].ins.forEach((parent, i) => { edges.push({ source: parent, target: id, + fontsize, + fontname, color, fontcolor, directed: true, @@ -37,8 +46,12 @@ Node.prototype.render = async function (container, options = {}) { id, color, fontcolor, + fontsize, + fontname, label: node.value ?? node.type, ordering: "in", + width: 0.5, + height: 0.4, })); const graphviz = await graphvizLoaded; @@ -50,6 +63,8 @@ Node.prototype.render = async function (container, options = {}) { }, }); dot = dot.split("\n"); + dot.splice(1, 0, `rankdir="${rankdir}"`); + size && dot.splice(1, 0, `size="${size}"`); dot.splice(1, 0, 'bgcolor="transparent"'); dot.splice(1, 0, 'color="white"'); dot = dot.join("\n"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f2363b..f564296 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,10 @@ importers: tailwindcss: specifier: ^3.4.4 version: 3.4.4 + devDependencies: + '@tailwindcss/typography': + specifier: ^0.5.13 + version: 0.5.13(tailwindcss@3.4.4) packages: @@ -606,6 +610,11 @@ packages: '@shikijs/core@1.9.1': resolution: {integrity: sha512-EmUful2MQtY8KgCF1OkBtOuMcvaZEvmdubhW0UHCGXi21O9dRLeADVCj+k6ZS+de7Mz9d2qixOXJ+GLhcK3pXg==} + '@tailwindcss/typography@0.5.13': + resolution: {integrity: sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1261,6 +1270,15 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@3.10.1: resolution: {integrity: sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==} @@ -1614,6 +1632,10 @@ packages: peerDependencies: postcss: ^8.2.14 + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss-selector-parser@6.1.0: resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==} engines: {node: '>=4'} @@ -2529,6 +2551,14 @@ snapshots: '@shikijs/core@1.9.1': {} + '@tailwindcss/typography@0.5.13(tailwindcss@3.4.4)': + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.4 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.24.7 @@ -3270,6 +3300,12 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.castarray@4.4.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.merge@4.6.2: {} + lodash@3.10.1: {} lodash@4.17.21: {} @@ -3775,6 +3811,11 @@ snapshots: postcss: 8.4.38 postcss-selector-parser: 6.1.0 + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@6.1.0: dependencies: cssesc: 3.0.0 diff --git a/website/package.json b/website/package.json index bc8095a..72d1ee9 100644 --- a/website/package.json +++ b/website/package.json @@ -14,10 +14,13 @@ "dependencies": { "@astrojs/solid-js": "^4.4.0", "@astrojs/tailwind": "^5.1.0", + "@kabelsalat/core": "workspace:*", + "@kabelsalat/graphviz": "workspace:*", "astro": "^4.11.3", "solid-js": "^1.8.17", - "tailwindcss": "^3.4.4", - "@kabelsalat/core": "workspace:*", - "@kabelsalat/graphviz": "workspace:*" + "tailwindcss": "^3.4.4" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.13" } } diff --git a/website/src/components/Box.astro b/website/src/components/Box.astro new file mode 100644 index 0000000..4055e3c --- /dev/null +++ b/website/src/components/Box.astro @@ -0,0 +1,3 @@ +
+
+
diff --git a/website/src/components/Icon.jsx b/website/src/components/Icon.jsx new file mode 100644 index 0000000..c0aa254 --- /dev/null +++ b/website/src/components/Icon.jsx @@ -0,0 +1,50 @@ +export function Icon({ type }) { + return ( + + { + { + refresh: ( + + ), + play: ( + + ), + pause: ( + + ), + stop: ( + + ), + skip: ( + + ), + }[type] + } + + ); +} diff --git a/website/src/components/MiniRepl.jsx b/website/src/components/MiniRepl.jsx new file mode 100644 index 0000000..1e8af51 --- /dev/null +++ b/website/src/components/MiniRepl.jsx @@ -0,0 +1,69 @@ +import { createSignal } from "solid-js"; +import "@kabelsalat/graphviz"; +import { SalatRepl } from "@kabelsalat/core"; +import { Icon } from "./Icon"; + +let vizSettings = { + resolveModules: false, + dagify: false, + rankdir: "LR", + size: 12, +}; + +export function MiniRepl(props) { + const initialCode = props.code; + let [code, setCode] = createSignal(initialCode); + let [started, setStarted] = createSignal(false); + const repl = new SalatRepl({ onToggle: (_started) => setStarted(_started) }); + let container; + async function run() { + const node = repl.evaluate(code()); + node.render(container, vizSettings); // update viz + repl.play(node); + } + let handleKeydown = (e) => { + // console.log("key", e.code); + if (e.key === "Enter" && (e.ctrlKey || e.altKey)) { + run(); + } else if (e.code === "Period" && (e.ctrlKey || e.altKey)) { + repl.stop(); + e.preventDefault(); + } + }; + + return ( +
+
+ + +
+
+ +
+
{ + container = el; + repl.evaluate(code()).render(container, vizSettings); + }} + >
+
+ ); +} diff --git a/website/src/components/Repl.jsx b/website/src/components/Repl.jsx index dab987b..318921c 100644 --- a/website/src/components/Repl.jsx +++ b/website/src/components/Repl.jsx @@ -52,6 +52,7 @@ export function Repl() { let [hideCode, setHideCode] = createSignal(false); let container; async function run() { + setInited(true); const node = repl.evaluate(code()); node.render(container, vizSettings); // update viz window.location.hash = "#" + btoa(code()); @@ -84,18 +85,18 @@ export function Repl() { return (
{ - !inited() && run(); - setInited(true); + onClick={(e) => { + e.target.tagName !== "A" && !inited() && run(); }} > -
+
KABƎL.SALAT.KABƎL.SALAT.KABƎL.SALAT.KABƎL.SALAT.KABƎL.SALAT
{!inited() && "click somewhere to play"}
+ learn more
{!hideCode() && ( @@ -126,20 +127,7 @@ export function Repl() { welcome to kabelsalat. this is a very experimental audio graph live coding prototype.

-
keyboard: ctrl+enter: start, ctrl+dot: stop
- - utility functions:{" "} - {`n(number) .mul(n) .add(n) .range(min,max) .apply(fn)`} - -
- - audio functions:{" "} - {`adsr(gate, att, dec, sus, rel) clock(bpm) clockdiv(clock, divisor) distort(in, amt) noise() pulse(freq, pw) saw(freq) sine(freq, sync) tri(freq) slide(in, rate) filter(in, cutoff, reso) fold(in, rate) seq(clock, ...steps) delay(in, time) hold(in, trig)`} - - {/* clockout(clock) */} - {/* midiin() */} - {/* monoseq(clock, gateT) */} - {/* gateseq(clock, gateT) */} +
keyboard: ctrl+enter: run, ctrl+dot: stop

       
diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 3862fdd..230d20f 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -2,10 +2,10 @@ import { Repl } from "../components/Repl"; --- - + - + kabelsalat diff --git a/website/src/pages/learn.astro b/website/src/pages/learn.astro new file mode 100644 index 0000000..9acf4fe --- /dev/null +++ b/website/src/pages/learn.astro @@ -0,0 +1,255 @@ +--- +import { MiniRepl } from "../components/MiniRepl"; +import { Icon } from "../components/Icon"; +import Box from "../components/Box.astro"; +--- + + + + + + + + learn kabelsalat + + + go to REPL +
+

What is kabelsalat?

+

+ kabelsalat (= cable salad) is a live codable modular synthesizer for the browser. It let's you write synthesizer patches with an easy to + use language based on JavaScript. + You don't need coding skills to learn this! +

+

Hello World

+

Here is a very simple example that generates a sine wave:

+ + +
    +
  1. Press to hear a beautiful sine tone.
  2. +
  3. Change the number 200 to 400
  4. +
  5. Hit ctrl+enter to update (or press )
  6. +
  7. Hit ctrl+. to stop (or press )
  8. +
+
+ +

Amplitude Modulation

+

Let's modulate the amplitude using `mul`:

+ + +

Frequency Modulation

+

Let's modulate the frequency instead:

+ +

Note: We could also have written `sine(sine(4).range(210,230))`

+
+ +

Subtractive Synthesis

+

+ A lonely sine wave is pretty thin, let's add some oomph with a sawtooth + wave and a filter: +

+ + +

Impulses & Envelopes

+

We can apply a simple decay envelope with `impulse` and `perc`:

+ +

For more control over the shape, we can also use ADSR:

+ + +

+ Note: `impulse(1).perc(.5)` effectively creates a gate that lasts .5 + seconds +

+
+ +

Sequences

+

+ The `seq` function allows us to cycle through different values using an + impulse: +

+ +

+ Note: `.saw()` will take everything on the left as input! More + generally, `x.y()` is the same as `y(x)`! +

+
+ +

Reusing nodes

+

+ In the above example, we might want to use the impulse to control the + sequence and also an envelope: +

+ +

+ Here we are creating the variable `imp` to use the impulse in 2 places. + Another way to write the same is this: +

+ imp + .seq(110,220,330,440) + .sine() + .mul( imp.perc(.2).slide(.2) ) +).out()` + client:only="solid" + /> +

+ The `apply` method allows us to get a variable without breaking up our + patch into multiple blocks +

+

slide

+ `slide` acts as a so called "slew limiter", making the incoming signal sluggish, + preventing harsh clicks. It can also be used for glissando effects: + +

Feedback Delay

+ Feedback is a core feature of kabelsalat. You can plug a node back to itself + using a so called lambda function: + x.delay(.1).mul(.8)) +.out()` + client:only="solid" + /> +

Multichannel Expansion

+ We can create multiple channels by using brackets: + + +

+ In this case, we get 2 sine waves that will be used for the left and + right channel of your sound system. +

+
+ +

If we want more channels, we have to mix them down:

+ +

Look what happens when brackets are used in more than one place:

+ +

Fold

+

fold limits the signal in between [-1,1] by folding:

+ +

Distort

+ +

Receiving MIDI Notes

+

+ You can also send midi to kabelsalat. The `midifreq` function will + receive any notes sent by any midi device. If you don't have a midi + device at hand, you can send MIDI with strudel +

+ +

Receiving MIDI Gates

+ +

+ You can also limit it to a specific channel with `midifreq(1)` (to + listen only for notes on midi channel 1) Similarly, `midigate` can be + used: +

+ +

Here is a monosynth that is a little bit more exciting:

+ +

Receiving MIDI CC Messages

+ You can receive cc messages with the `midicc` function: + +

Polyphonic MIDI

+ Using the `fork` function, you can clone a node multiple times. The midifreq + and midigate functions will automatically do voice allocation of incoming notes: + x.delay(0.2).mul(0.4)) // feedback delay + .out();` + client:only="solid" + /> +
+ + diff --git a/website/tailwind.config.mjs b/website/tailwind.config.mjs index 19f0614..2e9b1f7 100644 --- a/website/tailwind.config.mjs +++ b/website/tailwind.config.mjs @@ -1,8 +1,8 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], - theme: { - extend: {}, - }, - plugins: [], -} + content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], + theme: { + extend: {}, + }, + plugins: [require("@tailwindcss/typography")], +};