// Import necessary libraries and modules import { icon } from "@fortawesome/fontawesome-svg-core"; import { FaustMonoDspGenerator, FaustPolyDspGenerator, IFaustMonoWebAudioNode } from "@grame/faustwasm"; import { FaustUI } from "@shren/faust-ui"; import faustCSS from "@shren/faust-ui/dist/esm/index.css?inline"; import Split from "split.js"; import { faustPromise, audioCtx, compiler, svgDiagrams, default_generator, get_poly_generator, getInputDevices, deviceUpdateCallbacks, accessMIDIDevice, midiInputCallback, extractMidiAndNvoices } from "./common"; import { createEditor, setError, clearError } from "./editor"; import { Scope } from "./scope"; import faustSvg from "./faustText.svg"; // Create a template for the component const template = document.createElement("template") template.innerHTML = `
` // FaustEditor Web Component export default class FaustEditor extends HTMLElement { constructor() { super(); } connectedCallback() { // Initial setup when the component is attached to the DOM let code = this.innerHTML.replace("", "").trim(); this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true)); // Set up links, buttons, and editor const ideLink = this.shadowRoot!.querySelector("#ide") as HTMLAnchorElement; const editorEl = this.shadowRoot!.querySelector("#editor") as HTMLDivElement; const editor = createEditor(editorEl, code); ideLink.onfocus = () => { // Open current contents of editor in IDE const urlParams = new URLSearchParams(); urlParams.set("inline", btoa(editor.state.doc.toString()).replace("+", "-").replace("/", "_")); ideLink.href = `https://faustide.grame.fr/?${urlParams.toString()}`; } const runButton = this.shadowRoot!.querySelector("#run") as HTMLButtonElement; const stopButton = this.shadowRoot!.querySelector("#stop") as HTMLButtonElement; const faustUIRoot = this.shadowRoot!.querySelector("#faust-ui") as HTMLDivElement; const faustDiagram = this.shadowRoot!.querySelector("#faust-diagram") as HTMLDivElement; const sidebar = this.shadowRoot!.querySelector("#sidebar") as HTMLDivElement; const sidebarContent = this.shadowRoot!.querySelector("#sidebar-content") as HTMLDivElement; const tabButtons = [...this.shadowRoot!.querySelectorAll(".tab")] as HTMLButtonElement[]; const tabContents = [...sidebarContent.querySelectorAll("div")] as HTMLDivElement[]; // Initialize split.js for resizable editor and sidebar const split = Split([editorEl, sidebar], { sizes: [100, 0], minSize: [0, 20], gutterSize: 7, snapOffset: 150, onDragEnd: () => { scope?.onResize(); spectrum?.onResize() }, }) faustPromise.then(() => runButton.disabled = false); // Default sizes for sidebar const defaultSizes = [70, 30]; let sidebarOpen = false; // Function to open the sidebar with predefined sizes const openSidebar = () => { if (!sidebarOpen) { split.setSizes(defaultSizes); } sidebarOpen = true; } // Variables for audio and visualization nodes let node: IFaustMonoWebAudioNode | undefined; let input: MediaStreamAudioSourceNode | undefined; let analyser: AnalyserNode | undefined; let scope: Scope | undefined; let spectrum: Scope | undefined; let gmidi = false; let gnvoices = -1; let sourceNode: AudioBufferSourceNode | undefined; // Counter for compiled DSP let compiledDSPCounter = 0; // Counter for compiled SVG let compiledSVGCounter = 0; // Event handler for the run button runButton.onclick = async () => { if (audioCtx.state === "suspended") { await audioCtx.resume(); } await faustPromise; // Compile Faust code code = editor.state.doc.toString(); let generator = null; try { // Compile Faust code to access JSON metadata await default_generator.compile(compiler, "main", code, "-ftz 2"); const json = default_generator.getMeta(); let { midi, nvoices } = extractMidiAndNvoices(json); gmidi = midi; gnvoices = nvoices; // Build the generator (possibly reusing default_generator which is a FaustMonoDspGenerator) generator = nvoices > 0 ? get_poly_generator() : default_generator; await generator.compile(compiler, "main", code, "-ftz 2"); compiledDSPCounter++; } catch (e: any) { setError(editor, e); return } // Clear any old errors clearError(editor); // Create an audio node from compiled Faust if (node !== undefined) node.disconnect(); if (gnvoices > 0) { node = (await (generator as FaustPolyDspGenerator).createNode(audioCtx, gnvoices))!; } else { node = (await (generator as FaustMonoDspGenerator).createNode(audioCtx))!; } // Set up audio input if necessary if (node.numberOfInputs > 0) { audioInputSelector.disabled = false; updateInputDevices(await getInputDevices()); await connectInput(); } else { audioInputSelector.disabled = true; audioInputSelector.innerHTML = ""; } node.connect(audioCtx.destination); stopButton.disabled = false; for (const tabButton of tabButtons) { tabButton.disabled = false; } // Start sensors if available await node.startSensors(); // Access MIDI device if available if (gmidi) { accessMIDIDevice(midiInputCallback(node)) .then(() => { console.log('Successfully connected to the MIDI device.'); }) .catch((error) => { console.error('Error accessing MIDI device:', error.message); }); } openSidebar(); // Clear old tab contents for (const tab of tabContents) { while (tab.lastChild) tab.lastChild.remove(); } // Create scope & spectrum plots analyser = new AnalyserNode(audioCtx, { fftSize: Math.pow(2, 11), minDecibels: -96, maxDecibels: 0, smoothingTimeConstant: 0.85 }); node.connect(analyser); scope = new Scope(tabContents[2]); spectrum = new Scope(tabContents[3]); // If there are UI elements, open Faust UI (controls tab); otherwise open spectrum analyzer. const ui = node.getUI(); openTab(ui.length > 1 || ui[0].items.length > 0 ? 0 : 3); // Create controls via Faust UI const faustUI = new FaustUI({ ui, root: faustUIRoot }); faustUI.paramChangeByUI = (path, value) => node?.setParamValue(path, value); node.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value)); // Set editor size to fit UI size editorEl.style.height = `${Math.max(125, faustUI.minHeight)}px`; faustUIRoot.style.width = faustUI.minWidth * 1.25 + "px"; faustUIRoot.style.height = faustUI.minHeight * 1.25 + "px"; } // Function to set SVG in the block diagram tab const setSVG = (svgString: string) => { faustDiagram.innerHTML = svgString; for (const a of faustDiagram.querySelectorAll("a")) { a.onclick = e => { e.preventDefault(); const filename = (a.href as any as SVGAnimatedString).baseVal; const svgString = compiler.fs().readFile("main-svg/" + filename, { encoding: "utf8" }) as string; setSVG(svgString); } } } let animPlot: number | undefined; // Function to render the scope const drawScope = () => { scope!.renderScope([{ analyser: analyser!, style: "rgb(212, 100, 100)", edgeThreshold: 0.09, }]) animPlot = requestAnimationFrame(drawScope); } // Function to render the spectrum const drawSpectrum = () => { spectrum!.renderSpectrum(analyser!); animPlot = requestAnimationFrame(drawSpectrum); } // Function to switch between tabs const openTab = (i: number) => { for (const [j, tab] of tabButtons.entries()) { if (i === j) { tab.classList.add("active"); tabContents[j].classList.add("active"); } else { tab.classList.remove("active"); tabContents[j].classList.remove("active"); } } // Check if the clicked tab is the "Block Diagram" tab (index 1) if (i === 1) { // Check if the SVG has already been compiled for a given DSP if (compiledSVGCounter !== compiledDSPCounter) { // Display a "Computing SVG..." message while the SVG is being generated faustDiagram.innerHTML = "