@grame/faust-web-component 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # faust-web-component
2
+
3
+ This package provides two [web components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) for embedding interactive [Faust](https://faust.grame.fr) snippets in web pages.
4
+
5
+ - `<faust-editor>` displays an editor (using [CodeMirror 6](https://codemirror.net/)) with executable, editable Faust code, along with some bells & whistles (controls, block diagram, plots) in a side pane.
6
+ This component is ideal for demonstrating some code in Faust and allowing the reader to try it out and tweak it themselves without having to leave the page. (For more extensive work, it also includes a button to open the code in the Faust IDE.)
7
+
8
+ - `<faust-widget>` just shows the controls and does not allow editing, so it serves simply as a way to embed interactive DSP.
9
+
10
+ These components are built on top of [faustwasm](https://github.com/grame-cncm/faustwasm) and [faust-ui](https://github.com/Fr0stbyteR/faust-ui) packages.
11
+
12
+ ## Example Usage
13
+
14
+ ```html
15
+ <p><em>Here's an embedded editor!</em></p>
16
+
17
+ <faust-editor>
18
+ <!--
19
+ import("stdfaust.lib");
20
+ ctFreq = hslider("cutoffFrequency",500,50,10000,0.01);
21
+ q = hslider("q",5,1,30,0.1);
22
+ gain = hslider("gain",1,0,1,0.01);
23
+ process = no.noise : fi.resonlp(ctFreq,q,gain);
24
+ -->
25
+ </faust-editor>
26
+
27
+ <p><em>And here's a simple DSP widget!</em></p>
28
+
29
+ <faust-widget>
30
+ <!--
31
+ import("stdfaust.lib");
32
+ ctFreq = hslider("[0]cutoffFrequency",500,50,10000,0.01) : si.smoo;
33
+ q = hslider("[1]q",5,1,30,0.1) : si.smoo;
34
+ gain = hslider("[2]gain",1,0,1,0.01) : si.smoo;
35
+ t = button("[3]gate") : si.smoo;
36
+ process = no.noise : fi.resonlp(ctFreq,q,gain)*t <: dm.zita_light;
37
+ -->
38
+ </faust-widget>
39
+
40
+ <script src="faust-web-component.js"></script>
41
+ ```
42
+
43
+ We plan to soon publish a package on npm soon so that you can use a CDN for hosting.
44
+
45
+ ## Build Instructions
46
+
47
+ Clone this repository, then run:
48
+
49
+ ```shell
50
+ npm install
51
+ npm run build
52
+ ```
53
+
54
+ This will generate `dist/faust-web-component.js`, which you can use with a `<script>` tag as in the above example.
55
+
56
+ ## Demo
57
+
58
+ A concrete use-case can be seen in the this [updated version](https://ijc8.me/faustdoc/) of the Faust documentation site.
59
+
60
+ ## TODO
61
+
62
+ Several steps needs to be done before official release:
63
+
64
+ - audio input via file (including some stock signals)
65
+ - greater configurability via HTML attributes
66
+ - package publication on npm
package/index.html ADDED
@@ -0,0 +1,68 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vite + TS</title>
8
+ <script type="module" src="/src/main.ts"></script>
9
+ <style>
10
+ #content {
11
+ width: 50%;
12
+ margin: auto;
13
+ }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <div id="content">
18
+ <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
19
+ <faust-editor>
20
+ import("stdfaust.lib");
21
+ process = no.noise;
22
+ </faust-editor>
23
+ <p>Nostrum, id deserunt inventore ut veritatis iusto autem soluta officia debitis omnis, dolor sint cum, illo quam dignissimos fuga quod optio nesciunt.</p>
24
+ <!-- Need to comment below example due to occurrence of `<`. -->
25
+ <faust-editor>
26
+ <!--
27
+ import("stdfaust.lib");
28
+ ctFreq = 500;
29
+ q = 5;
30
+ gain = 1;
31
+ process = no.noise : _ <: fi.resonlp(ctFreq,q,gain),fi.resonlp(ctFreq,q,gain);
32
+ -->
33
+ </faust-editor>
34
+ <p>Ex velit voluptates, laudantium laboriosam est quis veniam temporibus tempore minima aspernatur, modi labore molestiae provident adipisci voluptatem dolorem voluptatibus asperiores possimus.</p>
35
+ <faust-editor>
36
+ <!--
37
+ import("stdfaust.lib");
38
+ ctFreq = hslider("cutoffFrequency",500,50,10000,0.01);
39
+ q = hslider("q",5,1,30,0.1);
40
+ gain = hslider("gain",1,0,1,0.01);
41
+ process = no.noise : fi.resonlp(ctFreq,q,gain);
42
+ -->
43
+ </faust-editor>
44
+ <p>Nam tempora officiis sunt nisi cupiditate expedita magnam atque. Unde, itaque.</p>
45
+ <faust-editor>
46
+ <!--
47
+ import("stdfaust.lib");
48
+ process =
49
+ dm.cubicnl_demo : // distortion
50
+ dm.wah4_demo <: // wah pedal
51
+ dm.phaser2_demo : // stereo phaser
52
+ dm.compressor_demo : // stereo compressor
53
+ dm.zita_light; // stereo reverb
54
+ -->
55
+ </faust-editor>
56
+ <p>Minus, ducimus consequuntur? Corrupti animi aut magni nihil, dolor eos tenetur, autem deserunt et iure culpa, suscipit minus quia velit laudantium asperiores.</p>
57
+ <faust-widget>
58
+ <!--
59
+ import("stdfaust.lib");
60
+ ctFreq = hslider("cutoffFrequency",500,50,10000,0.01);
61
+ q = hslider("q",5,1,30,0.1);
62
+ gain = hslider("gain",1,0,1,0.01);
63
+ process = no.noise : fi.resonlp(ctFreq,q,gain);
64
+ -->
65
+ </faust-widget>
66
+ </div>
67
+ </body>
68
+ </html>
package/index1.html ADDED
@@ -0,0 +1,31 @@
1
+ <p><em>Here's an embedded editor!</em></p>
2
+
3
+ <faust-editor>
4
+ <!--
5
+ import("stdfaust.lib");
6
+ ctFreq = hslider("cutoffFrequency",500,50,10000,0.01);
7
+ q = hslider("q",5,1,30,0.1);
8
+ gain = hslider("gain",1,0,1,0.01);
9
+ process = no.noise : fi.resonlp(ctFreq,q,gain);
10
+ -->
11
+ </faust-editor>
12
+
13
+ <p><em>And here's a simple DSP widget!</em></p>
14
+
15
+ <faust-widget>
16
+ <!--
17
+ import("stdfaust.lib");
18
+ ctFreq = hslider("[0]cutoffFrequency",500,50,10000,0.01) : si.smoo;
19
+ q = hslider("[1]q",5,1,30,0.1) : si.smoo;
20
+ gain = hslider("[2]gain",1,0,1,0.01) : si.smoo;
21
+ t = button("[3]gate") : si.smoo;
22
+ //process = no.noise : fi.resonlp(ctFreq,q,gain)*t <: dm.zita_light;
23
+ process = pm.clarinet_ui_MIDI <: _,_;
24
+ //effect = dm.zita_light;
25
+
26
+ //process = _*vslider("volume[midi:ctrl 7 ]", 0.5, 0, 1, 0.01);
27
+
28
+ -->
29
+ </faust-widget>
30
+
31
+ <script src="faust-web-component.js"></script>
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@grame/faust-web-component",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/grame-cncm/faust-web-component.git"
13
+ },
14
+ "keywords": [
15
+ "faust",
16
+ "webassembly",
17
+ "audio",
18
+ "signal processing"
19
+ ],
20
+ "author": "Grame-CNCM",
21
+ "license": "LGPL-3.0",
22
+ "bugs": {
23
+ "url": "https://github.com/grame-cncm/faust-web-component/issues"
24
+ },
25
+ "homepage": "https://github.com/grame-cncm/faust-web-component#readme",
26
+ "devDependencies": {
27
+ "typescript": "^5.0.2",
28
+ "vite": "^4.4.5"
29
+ },
30
+ "dependencies": {
31
+ "@codemirror/legacy-modes": "^6.3.3",
32
+ "@fortawesome/fontawesome-svg-core": "^6.4.2",
33
+ "@fortawesome/free-solid-svg-icons": "^6.4.2",
34
+ "@grame/faustwasm": "^0.0.34",
35
+ "@shren/faust-ui": "^1.1.2",
36
+ "codemirror": "^6.0.1",
37
+ "split.js": "^1.6.5"
38
+ }
39
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/src/common.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { FaustCompiler, FaustMonoDspGenerator, FaustPolyDspGenerator, FaustSvgDiagrams, LibFaust, instantiateFaustModuleFromFile } from "@grame/faustwasm"
2
+ import jsURL from "@grame/faustwasm/libfaust-wasm/libfaust-wasm.js?url"
3
+ import dataURL from "@grame/faustwasm/libfaust-wasm/libfaust-wasm.data?url"
4
+ import wasmURL from "@grame/faustwasm/libfaust-wasm/libfaust-wasm.wasm?url"
5
+ import { library } from "@fortawesome/fontawesome-svg-core"
6
+ import { faPlay, faStop, faUpRightFromSquare, faSquareCaretLeft, faAnglesLeft, faAnglesRight, faSliders, faDiagramProject, faWaveSquare, faChartLine, faPowerOff } from "@fortawesome/free-solid-svg-icons"
7
+
8
+ for (const icon of [faPlay, faStop, faUpRightFromSquare, faSquareCaretLeft, faAnglesLeft, faAnglesRight, faSliders, faDiagramProject, faWaveSquare, faChartLine, faPowerOff]) {
9
+ library.add(icon)
10
+ }
11
+
12
+ export let compiler: FaustCompiler
13
+ export let svgDiagrams: FaustSvgDiagrams
14
+ export const mono_generator = new FaustMonoDspGenerator() // TODO: Support polyphony
15
+ export const poly_generator = new FaustPolyDspGenerator() // TODO: Support polyphony
16
+
17
+ async function loadFaust() {
18
+ // Setup Faust
19
+ const module = await instantiateFaustModuleFromFile(jsURL, dataURL, wasmURL)
20
+ const libFaust = new LibFaust(module)
21
+ compiler = new FaustCompiler(libFaust)
22
+ svgDiagrams = new FaustSvgDiagrams(compiler)
23
+ }
24
+
25
+ export const faustPromise = loadFaust()
26
+ export const audioCtx = new AudioContext()
27
+
28
+ export const deviceUpdateCallbacks: ((d: MediaDeviceInfo[]) => void)[] = []
29
+ let devices: MediaDeviceInfo[] = []
30
+ async function _getInputDevices() {
31
+ if (navigator.mediaDevices) {
32
+ navigator.mediaDevices.ondevicechange = _getInputDevices
33
+ try {
34
+ await navigator.mediaDevices.getUserMedia({ audio: true })
35
+ } catch (e) { }
36
+ devices = await navigator.mediaDevices.enumerateDevices()
37
+ for (const callback of deviceUpdateCallbacks) {
38
+ callback(devices)
39
+ }
40
+ }
41
+ }
42
+
43
+ let getInputDevicesPromise: Promise<void> | undefined
44
+ export async function getInputDevices() {
45
+ if (getInputDevicesPromise === undefined) {
46
+ getInputDevicesPromise = _getInputDevices()
47
+ }
48
+ await getInputDevicesPromise
49
+ return devices
50
+ }
51
+
52
+ export async function accessMIDIDevice(
53
+ onMIDIMessage: (data) => void
54
+ ): Promise<void> {
55
+ return new Promise<void>((resolve, reject) => {
56
+ if (navigator.requestMIDIAccess) {
57
+ navigator
58
+ .requestMIDIAccess()
59
+ .then((midiAccess) => {
60
+ const inputDevices = midiAccess.inputs.values();
61
+ let midiInput: WebMidi.MIDIInput | null = null;
62
+ for (const midiInput of inputDevices) {
63
+ midiInput.onmidimessage = (event) => {
64
+ onMIDIMessage(event.data);
65
+ };
66
+ resolve();
67
+ }
68
+ })
69
+ .catch((error) => {
70
+ reject(error);
71
+ });
72
+ } else {
73
+ reject(new Error('Web MIDI API is not supported by this browser.'));
74
+ }
75
+ });
76
+ }
77
+
78
+ export const midiInputCallback = (node: IFaustPolyWebAudioNode | undefined) => {
79
+ return (data) => {
80
+
81
+ const cmd = data[0] >> 4;
82
+ const channel = data[0] & 0xf;
83
+ const data1 = data[1];
84
+ const data2 = data[2];
85
+
86
+ if (channel === 9) return;
87
+ else if (cmd === 8 || (cmd === 9 && data2 === 0)) node.keyOff(channel, data1, data2);
88
+ else if (cmd === 9) node.keyOn(channel, data1, data2);
89
+ else if (cmd === 11) node.ctrlChange(channel, data1, data2);
90
+ else if (cmd === 14) node.pitchWheel(channel, (data2 * 128.0 + data1));
91
+ }
92
+ }
package/src/editor.ts ADDED
@@ -0,0 +1,88 @@
1
+ import {EditorView} from "codemirror"
2
+ // Most of the basic CodeMirror setup, sans folds.
3
+ import {lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap} from "@codemirror/view"
4
+ import {history, defaultKeymap, historyKeymap} from "@codemirror/commands"
5
+ import {indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching} from "@codemirror/language"
6
+ import {highlightSelectionMatches, searchKeymap} from "@codemirror/search"
7
+ import {closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap} from "@codemirror/autocomplete"
8
+ import {EditorState} from "@codemirror/state"
9
+ // Custom CodeMirror setup
10
+ import {StreamLanguage} from "@codemirror/language"
11
+ import {clike} from "@codemirror/legacy-modes/mode/clike"
12
+ import {lintKeymap, setDiagnostics, openLintPanel, closeLintPanel} from "@codemirror/lint"
13
+
14
+ const keywords = "process component import library declare with environment route waveform soundfile"
15
+ const atoms = "mem prefix int float rdtable rwtable select2 select3 ffunction fconstant fvariable button checkbox vslider hslider nentry vgroup hgroup tgroup vbargraph hbargraph attach acos asin atan atan2 cos sin tan exp log log10 pow sqrt abs min max fmod remainder floor ceil rint"
16
+
17
+ function words(str: string) {
18
+ const obj: {[key: string]: true} = {}
19
+ const words = str.split(" ")
20
+ for (let i = 0; i < words.length; i++) obj[words[i]] = true
21
+ return obj
22
+ }
23
+
24
+ const faustLanguage = StreamLanguage.define(clike({
25
+ name: "clike",
26
+ multiLineStrings: true,
27
+ keywords: words(keywords),
28
+ atoms: words(atoms),
29
+ hooks: {
30
+ "@": () => "meta",
31
+ "'": () => "meta",
32
+ }
33
+ }))
34
+
35
+ export function createEditor(parent: HTMLElement, doc: string) {
36
+ return new EditorView({
37
+ parent,
38
+ doc,
39
+ extensions: [
40
+ lineNumbers(),
41
+ highlightActiveLineGutter(),
42
+ highlightSpecialChars(),
43
+ history(),
44
+ // foldGutter(),
45
+ drawSelection(),
46
+ dropCursor(),
47
+ EditorState.allowMultipleSelections.of(true),
48
+ indentOnInput(),
49
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
50
+ bracketMatching(),
51
+ closeBrackets(),
52
+ autocompletion(),
53
+ rectangularSelection(),
54
+ crosshairCursor(),
55
+ highlightActiveLine(),
56
+ highlightSelectionMatches(),
57
+ keymap.of([
58
+ ...closeBracketsKeymap,
59
+ ...defaultKeymap,
60
+ ...searchKeymap,
61
+ ...historyKeymap,
62
+ ...completionKeymap,
63
+ ...lintKeymap
64
+ ]),
65
+ faustLanguage,
66
+ ],
67
+ })
68
+ }
69
+
70
+ export function setError(editor: EditorView, error: Error) {
71
+ // Extract line number if available
72
+ const rawMessage = error.message.trim()
73
+ const match = rawMessage.match(/^main : (\d+) : (.*)$/)
74
+ const message = match ? match[2] : rawMessage
75
+ const { from, to } = match ? editor.state.doc.line(+match[1]) : { from: 0, to: 0 }
76
+ // Show error in editor
77
+ editor.dispatch(setDiagnostics(editor.state, [{
78
+ from, to,
79
+ severity: "error",
80
+ message,
81
+ }]))
82
+ openLintPanel(editor)
83
+ }
84
+
85
+ export function clearError(editor: EditorView) {
86
+ editor.dispatch(setDiagnostics(editor.state, []))
87
+ closeLintPanel(editor)
88
+ }