@grame/faust-web-component 0.2.2 → 0.2.4

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 CHANGED
@@ -9,6 +9,17 @@ This component is ideal for demonstrating some code in Faust and allowing the re
9
9
 
10
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
11
 
12
+ ## Build Instructions
13
+
14
+ Clone this repository, then run:
15
+
16
+ ```shell
17
+ npm install
18
+ npm run build
19
+ ```
20
+
21
+ This will generate `dist/faust-web-component.js`, which you can use with a `<script>` tag.
22
+
12
23
  ## Example Usage
13
24
 
14
25
  ```html
@@ -40,18 +51,29 @@ process = no.noise : fi.resonlp(ctFreq,q,gain)*t <: dm.zita_light;
40
51
  <script src="faust-web-component.js"></script>
41
52
  ```
42
53
 
43
- We plan to soon publish a package on npm soon so that you can use a CDN for hosting.
54
+ ## NPM package
44
55
 
45
- ## Build Instructions
56
+ A [npm package](https://www.npmjs.com/package/@grame/faust-web-component) can be used with the CDN link: https://cdn.jsdelivr.net/npm/@grame/faust-web-component@0.2.3/dist/faust-web-component.js (possibly update the version number).
57
+
58
+ Here is an HTML example using this model:
59
+
60
+ ```html
61
+ <p><em>Here's an embedded editor!</em></p>
46
62
 
47
- Clone this repository, then run:
63
+ <faust-editor>
64
+ <!--
65
+ import("stdfaust.lib");
48
66
 
49
- ```shell
50
- npm install
51
- npm run build
52
- ```
67
+ vol = hslider("volume [unit:dB]", -10, -96, 0, 0.1) : ba.db2linear : si.smoo;
68
+ freq1 = hslider("freq1 [unit:Hz]", 1000, 20, 3000, 1);
69
+ freq2 = hslider("freq2 [unit:Hz]", 200, 20, 3000, 1);
70
+
71
+ process = vgroup("Oscillator", os.osc(freq1) * vol, os.osc(freq2) * vol);
72
+ -->
73
+ </faust-editor>
53
74
 
54
- This will generate `dist/faust-web-component.js`, which you can use with a `<script>` tag as in the above example.
75
+ <script src="https://cdn.jsdelivr.net/npm/@grame/faust-web-component@0.2.3/dist/faust-web-component.js"></script>
76
+ ```
55
77
 
56
78
  ## Demo
57
79
 
@@ -63,4 +85,3 @@ Several steps needs to be done before official release:
63
85
 
64
86
  - audio input via file (including some stock signals)
65
87
  - greater configurability via HTML attributes
66
- - package publication on npm
@@ -0,0 +1,26 @@
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
+
19
+ declare options "[midi:on][nvoices:12]";
20
+
21
+ process = pm.clarinet_ui_MIDI <: _,_;
22
+ -->
23
+ </faust-widget>
24
+
25
+ <script src="https://cdn.jsdelivr.net/npm/@grame/faust-web-component@0.2.3/dist/faust-web-component.js"></script>
26
+
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/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@grame/faust-web-component",
3
3
  "description": "Web component embedding the Faust Compiler",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "module": "dist/faust-web-component.js",
6
- "files": ["dist/faust-web-component.js"],
6
+ "files": ["src/", "public/", "index.html", "tsconfig.json", "vite.config.js", "dist/faust-web-component.js"],
7
7
  "type": "module",
8
8
  "scripts": {
9
9
  "dev": "vite",
@@ -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,114 @@
1
+ import { IFaustMonoWebAudioNode, IFaustPolyWebAudioNode, 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
+ // Set up MIDI input callback
79
+ export const midiInputCallback = (node: IFaustMonoWebAudioNode | IFaustPolyWebAudioNode) => {
80
+ return (data) => {
81
+
82
+ const cmd = data[0] >> 4;
83
+ const channel = data[0] & 0xf;
84
+ const data1 = data[1];
85
+ const data2 = data[2];
86
+
87
+ if (channel === 9) return;
88
+ else if (cmd === 8 || (cmd === 9 && data2 === 0)) node.keyOff(channel, data1, data2);
89
+ else if (cmd === 9) node.keyOn(channel, data1, data2);
90
+ else if (cmd === 11) node.ctrlChange(channel, data1, data2);
91
+ else if (cmd === 14) node.pitchWheel(channel, (data2 * 128.0 + data1));
92
+ }
93
+ }
94
+
95
+ // Analyze the metadata of a Faust JSON file extract the [midi:on] and [nvoices:n] options
96
+ export function extractMidiAndNvoices(jsonData: JSONData): { midi: boolean, nvoices: number } {
97
+ const optionsMetadata = jsonData.meta.find(meta => meta.options);
98
+ if (optionsMetadata) {
99
+ const options = optionsMetadata.options;
100
+
101
+ const midiRegex = /\[midi:(on|off)\]/;
102
+ const nvoicesRegex = /\[nvoices:(\d+)\]/;
103
+
104
+ const midiMatch = options.match(midiRegex);
105
+ const nvoicesMatch = options.match(nvoicesRegex);
106
+
107
+ const midi = midiMatch ? midiMatch[1] === "on" : false;
108
+ const nvoices = nvoicesMatch ? parseInt(nvoicesMatch[1]) : -1;
109
+
110
+ return { midi, nvoices };
111
+ } else {
112
+ return { midi: false, nvoices: -1 };
113
+ }
114
+ }
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
+ }
@@ -0,0 +1,451 @@
1
+ import { icon } from "@fortawesome/fontawesome-svg-core"
2
+ import { IFaustMonoWebAudioNode } from "@grame/faustwasm"
3
+ import { FaustUI } from "@shren/faust-ui"
4
+ import faustCSS from "@shren/faust-ui/dist/esm/index.css?inline"
5
+ import Split from "split.js"
6
+ import { faustPromise, audioCtx, compiler, svgDiagrams, mono_generator, poly_generator, getInputDevices, deviceUpdateCallbacks, accessMIDIDevice, midiInputCallback, extractMidiAndNvoices } from "./common"
7
+ import { createEditor, setError, clearError } from "./editor"
8
+ import { Scope } from "./scope"
9
+ import faustSvg from "./faustText.svg"
10
+
11
+ const template = document.createElement("template")
12
+ template.innerHTML = `
13
+ <div id="root">
14
+ <div id="controls">
15
+ <button title="Run" class="button" id="run" disabled>${icon({ prefix: "fas", iconName: "play" }).html[0]}</button>
16
+ <button title="Stop" class="button" id="stop" disabled>${icon({ prefix: "fas", iconName: "stop" }).html[0]}</button>
17
+ <a title="Open in Faust IDE" id="ide" href="https://faustide.grame.fr/" class="button" target="_blank">${icon({ prefix: "fas", iconName: "up-right-from-square" }).html[0]}</a>
18
+ <select id="audio-input" class="dropdown" disabled>
19
+ <option>Audio input</option>
20
+ </select>
21
+ <!-- TODO: MIDI input
22
+ <select id="midi-input" class="dropdown" disabled>
23
+ <option>MIDI input</option>
24
+ </select>
25
+ -->
26
+ <!-- TODO: volume control? <input id="volume" type="range" min="0" max="100"> -->
27
+ <a title="Faust website" id="faust" href="https://faust.grame.fr/" target="_blank"><img src="${faustSvg}" height="15px" /></a>
28
+ </div>
29
+ <div id="content">
30
+ <div id="editor"></div>
31
+ <div id="sidebar">
32
+ <div id="sidebar-buttons">
33
+ <button title="Controls" id="tab-ui" class="button tab" disabled>${icon({ prefix: "fas", iconName: "sliders" }).html[0]}</button>
34
+ <button title="Block Diagram" id="tab-diagram" class="button tab" disabled>${icon({ prefix: "fas", iconName: "diagram-project" }).html[0]}</button>
35
+ <button title="Scope" id="tab-scope" class="button tab" disabled>${icon({ prefix: "fas", iconName: "wave-square" }).html[0]}</button>
36
+ <button title="Spectrum" id="tab-spectrum" class="button tab" disabled>${icon({ prefix: "fas", iconName: "chart-line" }).html[0]}</button>
37
+ </div>
38
+ <div id="sidebar-content">
39
+ <div id="faust-ui"></div>
40
+ <div id="faust-diagram"></div>
41
+ <div id="faust-scope"></div>
42
+ <div id="faust-spectrum"></div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ <style>
48
+ #root {
49
+ border: 1px solid black;
50
+ border-radius: 5px;
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ *, *:before, *:after {
55
+ box-sizing: inherit;
56
+ }
57
+
58
+ #controls {
59
+ background-color: #384d64;
60
+ border-bottom: 1px solid black;
61
+ display: flex;
62
+ }
63
+
64
+ #faust {
65
+ margin-left: auto;
66
+ margin-right: 10px;
67
+ display: flex;
68
+ align-items: center;
69
+ }
70
+
71
+ #faust-ui {
72
+ width: 232px;
73
+ max-height: 150px;
74
+ }
75
+
76
+ #faust-scope, #faust-spectrum {
77
+ min-width: 232px;
78
+ min-height: 150px;
79
+ }
80
+
81
+ #faust-diagram {
82
+ max-width: 232px;
83
+ height: 150px;
84
+ }
85
+
86
+ #content {
87
+ display: flex;
88
+ }
89
+
90
+ #editor {
91
+ flex-grow: 1;
92
+ overflow-y: auto;
93
+ }
94
+
95
+ #editor .cm-editor {
96
+ height: 100%;
97
+ }
98
+
99
+ .cm-diagnostic {
100
+ font-family: monospace;
101
+ }
102
+
103
+ .cm-diagnostic-error {
104
+ background-color: #fdf2f5 !important;
105
+ color: #a4000f !important;
106
+ border-color: #a4000f !important;
107
+ }
108
+
109
+ #sidebar {
110
+ display: flex;
111
+ max-width: 100%;
112
+ }
113
+
114
+ .tab {
115
+ flex-grow: 1;
116
+ }
117
+
118
+ #sidebar-buttons .tab.active {
119
+ background-color: #bbb;
120
+ }
121
+
122
+ #sidebar-buttons {
123
+ background-color: #f5f5f5;
124
+ display: flex;
125
+ flex-direction: column;
126
+ }
127
+
128
+ #sidebar-buttons .button {
129
+ background-color: #f5f5f5;
130
+ color: #000;
131
+ width: 20px;
132
+ height: 20px;
133
+ padding: 4px;
134
+ }
135
+
136
+ #sidebar-buttons .button:hover {
137
+ background-color: #ddd;
138
+ }
139
+
140
+ #sidebar-buttons .button:active {
141
+ background-color: #aaa;
142
+ }
143
+
144
+ #sidebar-content {
145
+ background-color: #fff;
146
+ border-left: 1px solid #ccc;
147
+ overflow: auto;
148
+ flex-grow: 1;
149
+ max-height: 100%;
150
+ }
151
+
152
+ #sidebar-content > div {
153
+ display: none;
154
+ }
155
+
156
+ #sidebar-content > div.active {
157
+ display: block;
158
+ }
159
+
160
+ a.button {
161
+ appearance: button;
162
+ }
163
+
164
+ .button {
165
+ background-color: #384d64;
166
+ border: 0;
167
+ padding: 5px;
168
+ width: 25px;
169
+ height: 25px;
170
+ color: #fff;
171
+ }
172
+
173
+ .button:hover {
174
+ background-color: #4b71a1;
175
+ }
176
+
177
+ .button:active {
178
+ background-color: #373736;
179
+ }
180
+
181
+ .button:disabled {
182
+ opacity: 0.65;
183
+ cursor: not-allowed;
184
+ pointer-events: none;
185
+ }
186
+
187
+ #controls > .button > svg {
188
+ width: 15px;
189
+ height: 15px;
190
+ vertical-align: top;
191
+ }
192
+
193
+ .dropdown {
194
+ height: 19px;
195
+ margin: 3px 0 3px 10px;
196
+ border: 0;
197
+ background: #fff;
198
+ }
199
+
200
+ .gutter {
201
+ background-color: #f5f5f5;
202
+ background-repeat: no-repeat;
203
+ background-position: 50%;
204
+ }
205
+
206
+ .gutter.gutter-horizontal {
207
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
208
+ cursor: col-resize;
209
+ }
210
+
211
+ ${faustCSS}
212
+ </style>
213
+ `
214
+
215
+ export default class FaustEditor extends HTMLElement {
216
+ constructor() {
217
+ super()
218
+ }
219
+
220
+ connectedCallback() {
221
+ const code = this.innerHTML.replace("<!--", "").replace("-->", "").trim()
222
+ this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true))
223
+
224
+ const ideLink = this.shadowRoot!.querySelector("#ide") as HTMLAnchorElement
225
+ ideLink.onfocus = () => {
226
+ // Open current contents of editor in IDE
227
+ const urlParams = new URLSearchParams()
228
+ urlParams.set("inline", btoa(editor.state.doc.toString()).replace("+", "-").replace("/", "_"))
229
+ ideLink.href = `https://faustide.grame.fr/?${urlParams.toString()}`
230
+ }
231
+
232
+ const editorEl = this.shadowRoot!.querySelector("#editor") as HTMLDivElement
233
+ const editor = createEditor(editorEl, code)
234
+
235
+ const runButton = this.shadowRoot!.querySelector("#run") as HTMLButtonElement
236
+ const stopButton = this.shadowRoot!.querySelector("#stop") as HTMLButtonElement
237
+ const faustUIRoot = this.shadowRoot!.querySelector("#faust-ui") as HTMLDivElement
238
+ const faustDiagram = this.shadowRoot!.querySelector("#faust-diagram") as HTMLDivElement
239
+ const sidebar = this.shadowRoot!.querySelector("#sidebar") as HTMLDivElement
240
+ const sidebarContent = this.shadowRoot!.querySelector("#sidebar-content") as HTMLDivElement
241
+ const tabButtons = [...this.shadowRoot!.querySelectorAll(".tab")] as HTMLButtonElement[]
242
+ const tabContents = [...sidebarContent.querySelectorAll("div")] as HTMLDivElement[]
243
+
244
+ const split = Split([editorEl, sidebar], {
245
+ sizes: [100, 0],
246
+ minSize: [0, 20],
247
+ gutterSize: 7,
248
+ snapOffset: 150,
249
+ onDragEnd: () => { scope?.onResize(); spectrum?.onResize() },
250
+ })
251
+
252
+ faustPromise.then(() => runButton.disabled = false)
253
+
254
+ const defaultSizes = [70, 30]
255
+ let sidebarOpen = false
256
+ const openSidebar = () => {
257
+ if (!sidebarOpen) {
258
+ split.setSizes(defaultSizes)
259
+ }
260
+ sidebarOpen = true
261
+ }
262
+
263
+ let node: IFaustMonoWebAudioNode | undefined
264
+ let input: MediaStreamAudioSourceNode | undefined
265
+ let analyser: AnalyserNode | undefined
266
+ let scope: Scope | undefined
267
+ let spectrum: Scope | undefined
268
+ let gmidi = false
269
+ let gnvoices = -1
270
+
271
+
272
+ runButton.onclick = async () => {
273
+ if (audioCtx.state === "suspended") {
274
+ await audioCtx.resume()
275
+ }
276
+ await faustPromise
277
+ // Compile Faust code
278
+ const code = editor.state.doc.toString()
279
+ let generator = null
280
+ try {
281
+ // Compile Faust code to access JSON metadata
282
+ await mono_generator.compile(compiler, "main", code, "")
283
+ const json = mono_generator.getMeta()
284
+ let { midi, nvoices } = extractMidiAndNvoices(json);
285
+ gmidi = midi;
286
+ gnvoices = nvoices;
287
+
288
+ // Build the generator
289
+ generator = nvoices > 0 ? poly_generator : mono_generator;
290
+ await generator.compile(compiler, "main", code, "");
291
+
292
+ } catch (e: any) {
293
+ setError(editor, e)
294
+ return
295
+ }
296
+ // Clear any old errors
297
+ clearError(editor)
298
+
299
+ // Create an audio node from compiled Faust
300
+ if (node !== undefined) node.disconnect()
301
+ if (gnvoices > 0) {
302
+ node = (await poly_generator.createNode(audioCtx, gnvoices))!
303
+ } else {
304
+ node = (await mono_generator.createNode(audioCtx))!
305
+ }
306
+ if (node.numberOfInputs > 0) {
307
+ audioInputSelector.disabled = false
308
+ updateInputDevices(await getInputDevices())
309
+ await connectInput()
310
+ } else {
311
+ audioInputSelector.disabled = true
312
+ audioInputSelector.innerHTML = "<option>Audio input</option>"
313
+ }
314
+ node.connect(audioCtx.destination)
315
+ stopButton.disabled = false
316
+ for (const tabButton of tabButtons) {
317
+ tabButton.disabled = false
318
+ }
319
+
320
+ // Access MIDI device
321
+ if (gmidi) {
322
+ accessMIDIDevice(midiInputCallback(node))
323
+ .then(() => {
324
+ console.log('Successfully connected to the MIDI device.');
325
+ })
326
+ .catch((error) => {
327
+ console.error('Error accessing MIDI device:', error.message);
328
+ });
329
+ }
330
+
331
+ openSidebar()
332
+ // Clear old tab contents
333
+ for (const tab of tabContents) {
334
+ while (tab.lastChild) tab.lastChild.remove()
335
+ }
336
+ // Create scope & spectrum plots
337
+ analyser = new AnalyserNode(audioCtx, {
338
+ fftSize: Math.pow(2, 11), minDecibels: -96, maxDecibels: 0, smoothingTimeConstant: 0.85
339
+ })
340
+ node.connect(analyser)
341
+ scope = new Scope(tabContents[2])
342
+ spectrum = new Scope(tabContents[3])
343
+
344
+ // If there are UI elements, open Faust UI (controls tab); otherwise open spectrum analyzer.
345
+ const ui = node.getUI()
346
+ openTab(ui.length > 1 || ui[0].items.length > 0 ? 0 : 3)
347
+
348
+ // Create controls via Faust UI
349
+ const faustUI = new FaustUI({ ui, root: faustUIRoot })
350
+ faustUI.paramChangeByUI = (path, value) => node?.setParamValue(path, value)
351
+ node.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value))
352
+
353
+ // Create SVG block diagram
354
+ setSVG(svgDiagrams.from("main", code, "")["process.svg"])
355
+ }
356
+
357
+ const setSVG = (svgString: string) => {
358
+ faustDiagram.innerHTML = svgString
359
+
360
+ for (const a of faustDiagram.querySelectorAll("a")) {
361
+ a.onclick = e => {
362
+ e.preventDefault()
363
+ const filename = (a.href as any as SVGAnimatedString).baseVal
364
+ const svgString = compiler.fs().readFile("main-svg/" + filename, { encoding: "utf8" }) as string
365
+ setSVG(svgString)
366
+ }
367
+ }
368
+ }
369
+
370
+ let animPlot: number | undefined
371
+ const drawScope = () => {
372
+ scope!.renderScope([{
373
+ analyser: analyser!,
374
+ style: "rgb(212, 100, 100)",
375
+ edgeThreshold: 0.09,
376
+ }])
377
+ animPlot = requestAnimationFrame(drawScope)
378
+ }
379
+
380
+ const drawSpectrum = () => {
381
+ spectrum!.renderSpectrum(analyser!)
382
+ animPlot = requestAnimationFrame(drawSpectrum)
383
+ }
384
+
385
+ const openTab = (i: number) => {
386
+ for (const [j, tab] of tabButtons.entries()) {
387
+ if (i === j) {
388
+ tab.classList.add("active")
389
+ tabContents[j].classList.add("active")
390
+ } else {
391
+ tab.classList.remove("active")
392
+ tabContents[j].classList.remove("active")
393
+ }
394
+ }
395
+ if (i === 2) {
396
+ scope!.onResize()
397
+ if (animPlot !== undefined) cancelAnimationFrame(animPlot)
398
+ animPlot = requestAnimationFrame(drawScope)
399
+ } else if (i === 3) {
400
+ spectrum!.onResize()
401
+ if (animPlot !== undefined) cancelAnimationFrame(animPlot)
402
+ animPlot = requestAnimationFrame(drawSpectrum)
403
+ } else if (animPlot !== undefined) {
404
+ cancelAnimationFrame(animPlot)
405
+ animPlot = undefined
406
+ }
407
+ }
408
+
409
+ for (const [i, tabButton] of tabButtons.entries()) {
410
+ tabButton.onclick = () => openTab(i)
411
+ }
412
+
413
+ stopButton.onclick = () => {
414
+ if (node !== undefined) {
415
+ node.disconnect()
416
+ node.destroy()
417
+ node = undefined
418
+ stopButton.disabled = true
419
+ // TODO: Maybe disable controls in faust-ui tab.
420
+ }
421
+ }
422
+
423
+ const audioInputSelector = this.shadowRoot!.querySelector("#audio-input") as HTMLSelectElement
424
+
425
+ const updateInputDevices = (devices: MediaDeviceInfo[]) => {
426
+ if (audioInputSelector.disabled) return
427
+ while (audioInputSelector.lastChild) audioInputSelector.lastChild.remove()
428
+ for (const device of devices) {
429
+ if (device.kind === "audioinput") {
430
+ audioInputSelector.appendChild(new Option(device.label || device.deviceId, device.deviceId))
431
+ }
432
+ }
433
+ }
434
+ deviceUpdateCallbacks.push(updateInputDevices)
435
+
436
+ const connectInput = async () => {
437
+ const deviceId = audioInputSelector.value
438
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId, echoCancellation: false, noiseSuppression: false, autoGainControl: false } })
439
+ if (input) {
440
+ input.disconnect()
441
+ input = undefined
442
+ }
443
+ if (node && node.numberOfInputs > 0) {
444
+ input = audioCtx.createMediaStreamSource(stream)
445
+ input.connect(node!)
446
+ }
447
+ }
448
+
449
+ audioInputSelector.onchange = connectInput
450
+ }
451
+ }
@@ -0,0 +1,226 @@
1
+ import { icon } from "@fortawesome/fontawesome-svg-core"
2
+ import faustCSS from "@shren/faust-ui/dist/esm/index.css?inline"
3
+ import faustSvg from "./faustText.svg"
4
+ import { IFaustMonoWebAudioNode } from "@grame/faustwasm"
5
+ import { IFaustPolyWebAudioNode } from "@grame/faustwasm"
6
+ import { FaustUI } from "@shren/faust-ui"
7
+ import { faustPromise, audioCtx, mono_generator, poly_generator, compiler, getInputDevices, deviceUpdateCallbacks, accessMIDIDevice, midiInputCallback, extractMidiAndNvoices } from "./common"
8
+
9
+ const template = document.createElement("template")
10
+ template.innerHTML = `
11
+ <div id="root">
12
+ <div id="controls">
13
+ <button title="On/off" class="button" id="power" disabled>${icon({ prefix: "fas", iconName: "power-off" }).html[0]}</button>
14
+ <select id="audio-input" class="dropdown" disabled>
15
+ <option>Audio input</option>
16
+ </select>
17
+ <!-- TODO: MIDI input
18
+ <select id="midi-input" class="dropdown" disabled>
19
+ <option>MIDI input</option>
20
+ </select>
21
+ -->
22
+ <!-- TODO: volume control? <input id="volume" type="range" min="0" max="100"> -->
23
+ <a title="Faust website" id="faust" href="https://faust.grame.fr/" target="_blank"><img src="${faustSvg}" height="15px" /></a>
24
+ </div>
25
+ <div id="faust-ui"></div>
26
+ </div>
27
+ <style>
28
+ #root {
29
+ border: 1px solid black;
30
+ border-radius: 5px;
31
+ box-sizing: border-box;
32
+ display: inline-block;
33
+ background-color: #384d64;
34
+ }
35
+
36
+ *, *:before, *:after {
37
+ box-sizing: inherit;
38
+ }
39
+
40
+ #controls {
41
+ display: flex;
42
+ margin-bottom: -20px;
43
+ position: relative;
44
+ z-index: 1;
45
+ }
46
+
47
+ #faust {
48
+ margin-left: auto;
49
+ padding-left: 10px;
50
+ margin-right: 10px;
51
+ display: flex;
52
+ align-items: center;
53
+ }
54
+
55
+ a.button {
56
+ appearance: button;
57
+ }
58
+
59
+ .button {
60
+ background-color: #384d64;
61
+ border: 0;
62
+ padding: 5px;
63
+ width: 25px;
64
+ height: 25px;
65
+ color: #fff;
66
+ }
67
+
68
+ .button:hover {
69
+ background-color: #4b71a1;
70
+ }
71
+
72
+ .button:active {
73
+ background-color: #373736;
74
+ }
75
+
76
+ .button:disabled {
77
+ opacity: 0.65;
78
+ cursor: not-allowed;
79
+ pointer-events: none;
80
+ }
81
+
82
+ #controls > .button > svg {
83
+ width: 15px;
84
+ height: 15px;
85
+ vertical-align: top;
86
+ }
87
+
88
+ .dropdown {
89
+ height: 19px;
90
+ margin: 3px 0 3px 10px;
91
+ border: 0;
92
+ background: #fff;
93
+ }
94
+
95
+ ${faustCSS}
96
+ </style>
97
+ `
98
+
99
+ export default class FaustWidget extends HTMLElement {
100
+ constructor() {
101
+ super()
102
+ }
103
+
104
+ connectedCallback() {
105
+ const code = this.innerHTML.replace("<!--", "").replace("-->", "").trim()
106
+ this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true))
107
+
108
+ const powerButton = this.shadowRoot!.querySelector("#power") as HTMLButtonElement
109
+ const faustUIRoot = this.shadowRoot!.querySelector("#faust-ui") as HTMLDivElement
110
+
111
+ faustPromise.then(() => powerButton.disabled = false)
112
+
113
+ let on = false
114
+ let gmidi = false
115
+ let gnvoices = -1
116
+ let node: IFaustMonoWebAudioNode | IFaustPolyWebAudioNode
117
+ let input: MediaStreamAudioSourceNode | undefined
118
+ let faustUI: FaustUI
119
+
120
+ const setup = async () => {
121
+ await faustPromise
122
+ // Compile Faust code to access JSON metadata
123
+ await mono_generator.compile(compiler, "main", code, "")
124
+ const json = mono_generator.getMeta()
125
+ let { midi, nvoices } = extractMidiAndNvoices(json);
126
+ gmidi = midi;
127
+ gnvoices = nvoices;
128
+
129
+ // Build the generator and generate UI
130
+ const generator = nvoices > 0 ? poly_generator : mono_generator;
131
+ await generator.compile(compiler, "main", code, "");
132
+ const ui = generator.getUI();
133
+
134
+ faustUI = new FaustUI({ ui, root: faustUIRoot });
135
+ faustUIRoot.style.width = faustUI.minWidth * 1.25 + "px";
136
+ faustUIRoot.style.height = faustUI.minHeight * 1.25 + "px";
137
+ faustUI.resize();
138
+ }
139
+
140
+ const start = async () => {
141
+ if (audioCtx.state === "suspended") {
142
+ await audioCtx.resume()
143
+ }
144
+
145
+ // Create an audio node from compiled Faust
146
+ if (node === undefined) {
147
+ if (gnvoices > 0) {
148
+ node = (await poly_generator.createNode(audioCtx, gnvoices))!
149
+ } else {
150
+ node = (await mono_generator.createNode(audioCtx))!
151
+ }
152
+ }
153
+
154
+ // Access MIDI device
155
+ if (gmidi) {
156
+ accessMIDIDevice(midiInputCallback(node))
157
+ .then(() => {
158
+ console.log('Successfully connected to the MIDI device.');
159
+ })
160
+ .catch((error) => {
161
+ console.error('Error accessing MIDI device:', error.message);
162
+ });
163
+ }
164
+
165
+ faustUI.paramChangeByUI = (path, value) => node?.setParamValue(path, value)
166
+ node.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value))
167
+
168
+ if (node.numberOfInputs > 0) {
169
+ audioInputSelector.disabled = false
170
+ updateInputDevices(await getInputDevices())
171
+ await connectInput()
172
+ } else {
173
+ audioInputSelector.disabled = true
174
+ audioInputSelector.innerHTML = "<option>Audio input</option>"
175
+ }
176
+
177
+ node.connect(audioCtx.destination)
178
+ powerButton.style.color = "#ffa500"
179
+ }
180
+
181
+ const stop = () => {
182
+ node?.disconnect()
183
+ powerButton.style.color = "#fff"
184
+ }
185
+
186
+ powerButton.onclick = () => {
187
+ if (on) {
188
+ stop()
189
+ } else {
190
+ start()
191
+ }
192
+ on = !on
193
+ }
194
+
195
+ const audioInputSelector = this.shadowRoot!.querySelector("#audio-input") as HTMLSelectElement
196
+
197
+ const updateInputDevices = (devices: MediaDeviceInfo[]) => {
198
+ if (audioInputSelector.disabled) return
199
+ while (audioInputSelector.lastChild) audioInputSelector.lastChild.remove()
200
+ for (const device of devices) {
201
+ if (device.kind === "audioinput") {
202
+ audioInputSelector.appendChild(new Option(device.label || device.deviceId, device.deviceId))
203
+ }
204
+ }
205
+ }
206
+ deviceUpdateCallbacks.push(updateInputDevices)
207
+
208
+ const connectInput = async () => {
209
+ const deviceId = audioInputSelector.value
210
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId, echoCancellation: false, noiseSuppression: false, autoGainControl: false } })
211
+ if (input) {
212
+ input.disconnect()
213
+ input = undefined
214
+ }
215
+ if (node && node.numberOfInputs > 0) {
216
+ input = audioCtx.createMediaStreamSource(stream)
217
+ input.connect(node!)
218
+ }
219
+ }
220
+
221
+ audioInputSelector.onchange = connectInput
222
+
223
+ setup()
224
+ }
225
+ }
226
+
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" xml:space="preserve" width="64.333328" height="12.414666" viewBox="0 0 64.333327 12.414666"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath clipPathUnits="userSpaceOnUse" id="clipPath18"><path d="M 0,93.507 H 93.507 V 0 H 0 Z" id="path16"/></clipPath></defs><g style="fill:#ffffff" id="g10" transform="matrix(1.3333333,0,0,-1.3333333,-31.440266,68.495732)"><g id="g12" style="fill:#ffffff"><g id="g14" clip-path="url(#clipPath18)" style="fill:#ffffff"><g id="g20" transform="translate(69.0172,49.3868)" style="fill:#ffffff"><path d="M 0,0 V -7.026 H -2.017 V 0 H -4.843 V 1.747 H 2.813 V 0 Z m -8.276,-5.615 c -0.176,-0.333 -0.415,-0.62 -0.718,-0.863 -0.302,-0.243 -0.658,-0.433 -1.067,-0.569 -0.409,-0.136 -0.849,-0.204 -1.321,-0.204 -0.435,0 -0.833,0.041 -1.193,0.124 -0.361,0.082 -0.685,0.191 -0.973,0.327 -0.289,0.135 -0.548,0.29 -0.779,0.463 -0.232,0.174 -0.431,0.352 -0.598,0.534 l 1.278,1.384 c 0.115,-0.125 0.248,-0.252 0.4,-0.381 0.152,-0.129 0.321,-0.247 0.509,-0.352 0.187,-0.106 0.392,-0.191 0.614,-0.255 0.221,-0.065 0.46,-0.097 0.716,-0.097 0.15,0 0.3,0.021 0.447,0.064 0.149,0.043 0.285,0.107 0.41,0.191 0.124,0.084 0.223,0.185 0.296,0.303 0.073,0.119 0.11,0.253 0.11,0.404 0,0.327 -0.158,0.581 -0.474,0.762 -0.316,0.181 -0.817,0.36 -1.504,0.536 -0.333,0.078 -0.64,0.192 -0.922,0.342 -0.283,0.151 -0.526,0.332 -0.729,0.543 -0.204,0.211 -0.361,0.453 -0.474,0.726 -0.112,0.274 -0.169,0.578 -0.169,0.914 0,0.368 0.065,0.714 0.195,1.038 0.13,0.325 0.328,0.611 0.595,0.859 0.266,0.247 0.597,0.443 0.993,0.589 0.397,0.145 0.865,0.218 1.406,0.218 0.446,0 0.842,-0.046 1.187,-0.137 0.344,-0.092 0.637,-0.201 0.877,-0.329 0.24,-0.127 0.437,-0.258 0.591,-0.391 0.155,-0.134 0.271,-0.239 0.348,-0.316 l -1.143,-1.266 c -0.099,0.078 -0.212,0.162 -0.338,0.255 -0.127,0.093 -0.268,0.179 -0.425,0.258 -0.156,0.08 -0.33,0.147 -0.521,0.201 -0.19,0.053 -0.391,0.08 -0.601,0.08 -0.142,0 -0.28,-0.022 -0.416,-0.067 -0.135,-0.046 -0.258,-0.105 -0.367,-0.178 -0.11,-0.073 -0.198,-0.161 -0.264,-0.265 -0.066,-0.103 -0.1,-0.211 -0.1,-0.323 0,-0.301 0.159,-0.544 0.477,-0.73 0.318,-0.185 0.753,-0.34 1.307,-0.465 0.327,-0.077 0.645,-0.184 0.955,-0.319 0.311,-0.136 0.588,-0.314 0.832,-0.536 0.244,-0.222 0.441,-0.493 0.591,-0.814 0.15,-0.321 0.225,-0.711 0.225,-1.172 0,-0.392 -0.088,-0.754 -0.263,-1.086 m -10.307,1.854 c 0,-0.527 -0.086,-1.01 -0.257,-1.449 -0.172,-0.439 -0.418,-0.813 -0.74,-1.124 -0.321,-0.31 -0.71,-0.553 -1.167,-0.729 -0.456,-0.175 -0.965,-0.263 -1.526,-0.263 -0.56,0 -1.067,0.086 -1.522,0.257 -0.455,0.171 -0.841,0.414 -1.159,0.729 -0.318,0.315 -0.563,0.691 -0.734,1.13 -0.171,0.439 -0.256,0.922 -0.256,1.449 v 5.446 h 1.985 v -5.271 c 0,-0.295 0.029,-0.56 0.086,-0.795 0.058,-0.235 0.153,-0.437 0.284,-0.606 0.131,-0.169 0.304,-0.301 0.521,-0.394 0.217,-0.094 0.481,-0.14 0.795,-0.14 0.309,0 0.57,0.046 0.785,0.14 0.214,0.093 0.388,0.225 0.521,0.394 0.133,0.169 0.227,0.371 0.283,0.606 0.056,0.235 0.084,0.5 0.084,0.795 v 5.271 h 2.017 z M -30.68,-7.026 c -0.596,0 -1.078,0.482 -1.078,1.077 0,0.595 0.482,1.078 1.078,1.078 0.595,0 1.077,-0.483 1.077,-1.078 0,-0.595 -0.482,-1.077 -1.077,-1.077 m -1.702,3.551 -1.067,3.314 -1.092,-3.314 -0.054,-0.181 h -1.899 l 2.018,5.403 h 2.196 l 1.977,-5.4 -2.023,-0.005 z m -3.708,-3.551 c -0.595,0 -1.078,0.482 -1.078,1.077 0,0.595 0.483,1.078 1.078,1.078 0.595,0 1.078,-0.483 1.078,-1.078 0,-0.595 -0.483,-1.077 -1.078,-1.077 M -39.541,0 h -3.912 v -1.991 h 2.974 v -1.696 h -2.974 v -3.339 h -1.984 v 8.773 h 5.896 z" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path22"/></g></g></g></g></svg>
package/src/main.ts ADDED
@@ -0,0 +1,5 @@
1
+ import FaustEditor from "./faust-editor"
2
+ import FaustWidget from "./faust-widget"
3
+
4
+ customElements.define("faust-editor", FaustEditor)
5
+ customElements.define("faust-widget", FaustWidget)
package/src/scope.ts ADDED
@@ -0,0 +1,205 @@
1
+ // Based on https://codepen.io/ContemporaryInsanity/pen/Mwvqpb
2
+
3
+ export class Scope {
4
+ container: HTMLElement
5
+ canvas: HTMLCanvasElement
6
+ ctx: CanvasRenderingContext2D
7
+
8
+ constructor(container: HTMLElement) {
9
+ this.container = container
10
+ this.container.classList.add("scope")
11
+
12
+ this.canvas = document.createElement("canvas")
13
+ this.canvas.style.transformOrigin = "top left"
14
+ this.ctx = this.canvas.getContext("2d")!
15
+ this.onResize = this.onResize.bind(this)
16
+
17
+ this.container.appendChild(this.canvas)
18
+ this.onResize()
19
+ window.addEventListener("resize", this.onResize)
20
+ }
21
+
22
+ get canvasWidth() { return this.canvas.width / devicePixelRatio }
23
+ set canvasWidth(canvasWidth) {
24
+ this.canvas.width = Math.floor(canvasWidth * devicePixelRatio)
25
+ this.canvas.style.width = `${canvasWidth}px`
26
+ }
27
+
28
+ get canvasHeight() { return this.canvas.height / devicePixelRatio }
29
+ set canvasHeight(canvasHeight) {
30
+ this.canvas.height = Math.floor(canvasHeight * devicePixelRatio)
31
+ this.canvas.style.height = `${canvasHeight}px`
32
+ }
33
+
34
+ renderScope(toRender: { analyser: AnalyserNode, style: string, edgeThreshold: number }[] = []) {
35
+ // grid
36
+ this.ctx.fillStyle = "white"
37
+ this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
38
+ this.ctx.lineWidth = 1
39
+ this.ctx.strokeStyle = "rgba(200, 200, 200, 0.5)"
40
+ this.ctx.fillStyle = "rgba(200, 200, 200, 0.5)"
41
+ this.ctx.beginPath()
42
+
43
+ const numHorzSteps = 8
44
+ const horzStep = this.canvasWidth / numHorzSteps
45
+ for (let i = horzStep; i < this.canvasWidth; i += horzStep) {
46
+ this.ctx.moveTo(i, 0)
47
+ this.ctx.lineTo(i, this.canvasHeight)
48
+ }
49
+
50
+ const numVertSteps = 4
51
+ const vertStep = this.canvasHeight / numVertSteps
52
+ for (let i = 0; i < this.canvasHeight; i += vertStep) {
53
+ this.ctx.moveTo(0, i)
54
+ this.ctx.lineTo(this.canvasWidth, i)
55
+ }
56
+ this.ctx.stroke()
57
+
58
+ // 0 line
59
+ this.ctx.strokeStyle = "rgba(100, 100, 100, 0.5)"
60
+ this.ctx.beginPath()
61
+ this.ctx.lineWidth = 2
62
+ this.ctx.moveTo(0, this.canvasHeight / 2)
63
+ this.ctx.lineTo(this.canvasWidth, this.canvasHeight / 2)
64
+ this.ctx.stroke()
65
+
66
+ // waveforms
67
+ toRender.forEach(({ analyser, style = "rgb(43, 156, 212)", edgeThreshold = 0 }) => {
68
+ if (analyser === undefined) { return }
69
+
70
+ const timeData = new Float32Array(analyser.frequencyBinCount)
71
+ let risingEdge = 0
72
+
73
+ analyser.getFloatTimeDomainData(timeData)
74
+
75
+ this.ctx.lineWidth = 2
76
+ this.ctx.strokeStyle = style
77
+
78
+ this.ctx.beginPath()
79
+
80
+ while (timeData[risingEdge] > 0 &&
81
+ risingEdge <= this.canvasWidth &&
82
+ risingEdge < timeData.length) {
83
+ risingEdge++
84
+ }
85
+
86
+ if (risingEdge >= this.canvasWidth) { risingEdge = 0 }
87
+
88
+
89
+ while (timeData[risingEdge] < edgeThreshold &&
90
+ risingEdge <= this.canvasWidth &&
91
+ risingEdge< timeData.length) {
92
+ risingEdge++
93
+ }
94
+
95
+ if (risingEdge >= this.canvasWidth) { risingEdge = 0 }
96
+
97
+ for (let x = risingEdge; x < timeData.length && x - risingEdge < this.canvasWidth; x++) {
98
+ const y = this.canvasHeight - (((timeData[x] + 1) / 2) * this.canvasHeight)
99
+ this.ctx.lineTo(x - risingEdge, y)
100
+ }
101
+
102
+ this.ctx.stroke()
103
+ })
104
+
105
+ // markers
106
+ this.ctx.fillStyle = "black"
107
+ this.ctx.font = "11px Courier"
108
+ this.ctx.textAlign = "left"
109
+ const numMarkers = 4
110
+ const markerStep = this.canvasHeight / numMarkers
111
+ for (let i = 0; i <= numMarkers; i++) {
112
+ this.ctx.textBaseline =
113
+ i === 0 ? "top"
114
+ : i === numMarkers ? "bottom"
115
+ : "middle"
116
+
117
+ const value = ((numMarkers - i) - (numMarkers / 2)) / numMarkers * 2
118
+ this.ctx.textAlign = "left"
119
+ this.ctx.fillText(value + "", 5, i * markerStep)
120
+ this.ctx.textAlign = "right"
121
+ this.ctx.fillText(value + "", this.canvasWidth - 5, i * markerStep)
122
+ }
123
+ }
124
+
125
+ renderSpectrum(analyser: AnalyserNode) {
126
+ const freqData = new Uint8Array(analyser.frequencyBinCount)
127
+
128
+ analyser.getByteFrequencyData(freqData)
129
+
130
+ this.ctx.fillStyle = "white"
131
+ this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
132
+
133
+ this.ctx.lineWidth = 2
134
+ this.ctx.strokeStyle = "rgb(43, 156, 212)"
135
+ this.ctx.beginPath()
136
+
137
+ for (let i = 0; i < freqData.length; i++) {
138
+ const x = (Math.log(i / 1)) / (Math.log(freqData.length / 1)) * this.canvasWidth
139
+ const height = (freqData[i] * this.canvasHeight) / 256
140
+ this.ctx.lineTo(x, this.canvasHeight - height)
141
+ }
142
+ this.ctx.stroke()
143
+
144
+ const fontSize = 12
145
+
146
+ // frequencies
147
+ function explin(value: number, inMin: number, inMax: number, outMin: number, outMax: number) {
148
+ inMin = Math.max(inMin, 1)
149
+ outMin = Math.max(outMin, 1)
150
+ return Math.log10(value / inMin) / Math.log10(inMax / inMin) * (outMax - outMin) + outMin
151
+ }
152
+
153
+ const nyquist = analyser.context.sampleRate / 2;
154
+ [0, 100, 300, 1000, 3000, 10000, 20000].forEach(freq => {
155
+ const minFreq = 20
156
+ const x = freq <= 0
157
+ ? fontSize - 5
158
+ : explin(freq, minFreq, nyquist, 0, this.canvasWidth)
159
+
160
+ this.ctx.fillStyle = "black"
161
+ this.ctx.textBaseline = "middle"
162
+ this.ctx.textAlign = "right"
163
+ this.ctx.font = `${fontSize}px Courier`
164
+ this.ctx.save()
165
+ this.ctx.translate(x, this.canvasHeight - 5)
166
+ this.ctx.rotate(Math.PI * 0.5)
167
+ this.ctx.fillText(`${freq.toFixed(0)}hz`, 0, 0)
168
+ this.ctx.restore()
169
+ });
170
+
171
+ [0, -3, -6, -12].forEach(db => {
172
+ const x = 5
173
+ const amp = Math.pow(10, db * 0.05)
174
+ const y = (1 - amp) * this.canvasHeight
175
+
176
+ this.ctx.fillStyle = "black"
177
+ this.ctx.textBaseline = "top"
178
+ this.ctx.textAlign = "left"
179
+ this.ctx.font = `${fontSize}px Courier`
180
+ this.ctx.fillText(`${db.toFixed(0)}db`, x, y)
181
+ })
182
+ }
183
+
184
+ onResize() {
185
+ this.canvasWidth = 0
186
+ this.canvasHeight = 0
187
+
188
+ const rect = this.container.getBoundingClientRect()
189
+ const style = getComputedStyle(this.container)
190
+
191
+ let borderLeft = style.getPropertyValue("border-left-width")
192
+ let left = borderLeft === "" ? 0 : parseFloat(borderLeft)
193
+ let borderRight = style.getPropertyValue("border-right-width")
194
+ let right = borderRight === "" ? 0 : parseFloat(borderRight)
195
+ this.canvasWidth = rect.width - left - right
196
+
197
+ let borderTop = style.getPropertyValue("border-top-width")
198
+ let top = borderTop === "" ? 0 : parseFloat(borderTop)
199
+ let borderBottom = style.getPropertyValue("border-bottom-width")
200
+ let bottom = borderBottom === "" ? 0 : parseFloat(borderBottom)
201
+ this.canvasHeight = rect.height - top - bottom
202
+
203
+ this.ctx.scale(devicePixelRatio, devicePixelRatio)
204
+ }
205
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true
21
+ },
22
+ "include": ["src"]
23
+ }
package/vite.config.js ADDED
@@ -0,0 +1,13 @@
1
+ import { resolve } from 'path'
2
+ import { defineConfig } from 'vite'
3
+
4
+ export default defineConfig({
5
+ build: {
6
+ lib: {
7
+ entry: resolve(__dirname, "src/main.ts"),
8
+ name: "faust_web_component",
9
+ formats: ["iife"],
10
+ fileName: () => "faust-web-component.js",
11
+ },
12
+ },
13
+ })