@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 +66 -0
- package/index.html +68 -0
- package/index1.html +31 -0
- package/package.json +39 -0
- package/public/vite.svg +1 -0
- package/src/common.ts +92 -0
- package/src/editor.ts +88 -0
- package/src/faust-editor.ts +418 -0
- package/src/faust-widget copie.ts +212 -0
- package/src/faust-widget.ts +212 -0
- package/src/faustText.svg +2 -0
- package/src/main.ts +5 -0
- package/src/scope.ts +205 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +23 -0
- package/vite.config.js +13 -0
@@ -0,0 +1,212 @@
|
|
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 } 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 node: IFaustMonoWebAudioNode | undefined
|
115
|
+
let node: IFaustPolyWebAudioNode | undefined
|
116
|
+
let input: MediaStreamAudioSourceNode | undefined
|
117
|
+
let faustUI: FaustUI
|
118
|
+
|
119
|
+
const setup = async () => {
|
120
|
+
await faustPromise
|
121
|
+
// Compile Faust code
|
122
|
+
//await mono_generator.compile(compiler, "main", code, "")
|
123
|
+
await poly_generator.compile(compiler, "main", code, "")
|
124
|
+
// Create controls via Faust UI
|
125
|
+
//const ui = mono_generator.getUI()
|
126
|
+
const ui = poly_generator.getUI()
|
127
|
+
faustUI = new FaustUI({ ui, root: faustUIRoot })
|
128
|
+
faustUIRoot.style.width = faustUI.minWidth * 1.25 + "px"
|
129
|
+
faustUIRoot.style.height = faustUI.minHeight * 1.25 + "px"
|
130
|
+
faustUI.resize()
|
131
|
+
}
|
132
|
+
|
133
|
+
const start = async () => {
|
134
|
+
if (audioCtx.state === "suspended") {
|
135
|
+
await audioCtx.resume()
|
136
|
+
}
|
137
|
+
// Create an audio node from compiled Faust
|
138
|
+
if (node === undefined) {
|
139
|
+
//node = (await mono_generator.createNode(audioCtx))!
|
140
|
+
node = (await poly_generator.createNode(audioCtx, 16))!
|
141
|
+
}
|
142
|
+
|
143
|
+
faustUI.paramChangeByUI = (path, value) => node?.setParamValue(path, value)
|
144
|
+
node.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value))
|
145
|
+
|
146
|
+
if (node.numberOfInputs > 0) {
|
147
|
+
audioInputSelector.disabled = false
|
148
|
+
updateInputDevices(await getInputDevices())
|
149
|
+
await connectInput()
|
150
|
+
} else {
|
151
|
+
audioInputSelector.disabled = true
|
152
|
+
audioInputSelector.innerHTML = "<option>Audio input</option>"
|
153
|
+
}
|
154
|
+
|
155
|
+
accessMIDIDevice(midiInputCallback(node))
|
156
|
+
.then(() => {
|
157
|
+
console.log('Successfully connected to the MIDI device.');
|
158
|
+
})
|
159
|
+
.catch((error) => {
|
160
|
+
console.error('Error accessing MIDI device:', error.message);
|
161
|
+
});
|
162
|
+
|
163
|
+
node.connect(audioCtx.destination)
|
164
|
+
powerButton.style.color = "#ffa500"
|
165
|
+
}
|
166
|
+
|
167
|
+
const stop = () => {
|
168
|
+
node?.disconnect()
|
169
|
+
powerButton.style.color = "#fff"
|
170
|
+
}
|
171
|
+
|
172
|
+
powerButton.onclick = () => {
|
173
|
+
if (on) {
|
174
|
+
stop()
|
175
|
+
} else {
|
176
|
+
start()
|
177
|
+
}
|
178
|
+
on = !on
|
179
|
+
}
|
180
|
+
|
181
|
+
const audioInputSelector = this.shadowRoot!.querySelector("#audio-input") as HTMLSelectElement
|
182
|
+
|
183
|
+
const updateInputDevices = (devices: MediaDeviceInfo[]) => {
|
184
|
+
if (audioInputSelector.disabled) return
|
185
|
+
while (audioInputSelector.lastChild) audioInputSelector.lastChild.remove()
|
186
|
+
for (const device of devices) {
|
187
|
+
if (device.kind === "audioinput") {
|
188
|
+
audioInputSelector.appendChild(new Option(device.label || device.deviceId, device.deviceId))
|
189
|
+
}
|
190
|
+
}
|
191
|
+
}
|
192
|
+
deviceUpdateCallbacks.push(updateInputDevices)
|
193
|
+
|
194
|
+
const connectInput = async () => {
|
195
|
+
const deviceId = audioInputSelector.value
|
196
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId, echoCancellation: false, noiseSuppression: false, autoGainControl: false } })
|
197
|
+
if (input) {
|
198
|
+
input.disconnect()
|
199
|
+
input = undefined
|
200
|
+
}
|
201
|
+
if (node && node.numberOfInputs > 0) {
|
202
|
+
input = audioCtx.createMediaStreamSource(stream)
|
203
|
+
input.connect(node!)
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
audioInputSelector.onchange = connectInput
|
208
|
+
|
209
|
+
setup()
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
@@ -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
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
|
+
})
|